mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: tauri grid changes and fixes (#2995)
* (feat) implement row drag and drop functionality * (feat) implement grid row actions options ui * (feat) add delete row function * (feat) implemet grid filter and sort popup layout * chore: move row methods to database controller * (feat) integrate delete and duplicate row functions * chore: add option on field popup * chore: padding on edit row * fix: change option color * chore: stick to corner on resize * fix: stick to corner * chore: grid row popup changes * chore: grid title * chore: add field width * chore: replace table layout with fixed column size * chore: resize column WIP * chore: save column width and draggable WIP * chore: nav panel resize fix * chore: database filter store * chore: filter popups * chore: filter value options * chore: remove console * chore: database filter refactor * chore: prevent jitter and dont include used fields * chore: checked field type * chore: reset operator * chore: filter icon * chore: database sort popup * chore: add icons into ref page * chore: sort icon in column header * chore: grid title fix * chore: change text and border colors * chore: grid rows dnd and optimise components * chore: select option color change fix * chore: filter service and controller * chore: wire filter UI to service WIP * chore: show only fields/cells with visiblity set to true * fix: grid visible column exception * chore: add update text filter backend * chore: select option filter save modify get * fix: filter reload and new filter * fix: new filter order * chore: sort backend service * chore: database sort UI * chore: field select popover component * chore: operator select popover * chore: select options popover * chore: change text color * chore: post merge * chore: sort popover * chore: bg body * chore: grid row actions popover * chore: dragging row change * chore: new field column fix * chore: field actions popover and field type popover * chore: hide and delete field actions * chore: duplicate field * fix: pnpm lock file has error and button color update * fix: tsc error * chore: add field to left and right --------- Co-authored-by: Mikias Tilahun Abebe <mikiastilahun@gmail.com> Co-authored-by: qinluhe <qinluhe.twodog@gmail.com>
This commit is contained in:
parent
27b1f00e17
commit
6fc8072459
@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Tauri + React + TS</title>
|
||||
<title>AppFlowy: The Open Source Alternative To Notion</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -23,6 +23,7 @@
|
||||
"@emotion/styled": "^11.10.6",
|
||||
"@mui/icons-material": "^5.11.11",
|
||||
"@mui/material": "^5.11.12",
|
||||
"@mui/system": "^5.14.4",
|
||||
"@reduxjs/toolkit": "^1.9.2",
|
||||
"@slate-yjs/core": "^1.0.0",
|
||||
"@tanstack/react-virtual": "3.0.0-beta.54",
|
||||
|
@ -19,6 +19,9 @@ dependencies:
|
||||
'@mui/material':
|
||||
specifier: ^5.11.12
|
||||
version: 5.13.0(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@mui/system':
|
||||
specifier: ^5.14.4
|
||||
version: 5.14.4(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react@18.2.0)
|
||||
'@reduxjs/toolkit':
|
||||
specifier: ^1.9.2
|
||||
version: 1.9.5(react-redux@8.0.5)(react@18.2.0)
|
||||
@ -544,6 +547,13 @@ packages:
|
||||
regenerator-runtime: 0.13.11
|
||||
dev: false
|
||||
|
||||
/@babel/runtime@7.22.10:
|
||||
resolution: {integrity: sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
regenerator-runtime: 0.14.0
|
||||
dev: false
|
||||
|
||||
/@babel/template@7.20.7:
|
||||
resolution: {integrity: sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@ -1302,7 +1312,7 @@ packages:
|
||||
'@emotion/styled': 11.11.0(@emotion/react@11.11.0)(@types/react@18.2.6)(react@18.2.0)
|
||||
'@mui/base': 5.0.0-beta.0(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@mui/core-downloads-tracker': 5.13.0
|
||||
'@mui/system': 5.12.3(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react@18.2.0)
|
||||
'@mui/system': 5.14.4(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react@18.2.0)
|
||||
'@mui/types': 7.2.4(@types/react@18.2.6)
|
||||
'@mui/utils': 5.12.3(react@18.2.0)
|
||||
'@types/react': 18.2.6
|
||||
@ -1316,8 +1326,8 @@ packages:
|
||||
react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@mui/private-theming@5.12.3(@types/react@18.2.6)(react@18.2.0):
|
||||
resolution: {integrity: sha512-o1e7Z1Bp27n4x2iUHhegV4/Jp6H3T6iBKHJdLivS5GbwsuAE/5l4SnZ+7+K+e5u9TuhwcAKZLkjvqzkDe8zqfA==}
|
||||
/@mui/private-theming@5.14.4(@types/react@18.2.6)(react@18.2.0):
|
||||
resolution: {integrity: sha512-ISXsHDiQ3z1XA4IuKn+iXDWvDjcz/UcQBiFZqtdoIsEBt8CB7wgdQf3LwcwqO81dl5ofg/vNQBEnXuKfZHrnYA==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
peerDependencies:
|
||||
'@types/react': ^17.0.0 || ^18.0.0
|
||||
@ -1326,15 +1336,15 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.21.5
|
||||
'@mui/utils': 5.12.3(react@18.2.0)
|
||||
'@babel/runtime': 7.22.10
|
||||
'@mui/utils': 5.14.4(react@18.2.0)
|
||||
'@types/react': 18.2.6
|
||||
prop-types: 15.8.1
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@mui/styled-engine@5.12.3(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-AhZtiRyT8Bjr7fufxE/mLS+QJ3LxwX1kghIcM2B2dvJzSSg9rnIuXDXM959QfUVIM3C8U4x3mgVoPFMQJvc4/g==}
|
||||
/@mui/styled-engine@5.13.2(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-VCYCU6xVtXOrIN8lcbuPmoG+u7FYuOERG++fpY74hPpEWkyFQG97F+/XfTQVYzlR2m7nPjnwVUgATcTCMEaMvw==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
peerDependencies:
|
||||
'@emotion/react': ^11.4.1
|
||||
@ -1346,7 +1356,7 @@ packages:
|
||||
'@emotion/styled':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.21.5
|
||||
'@babel/runtime': 7.22.10
|
||||
'@emotion/cache': 11.11.0
|
||||
'@emotion/react': 11.11.0(@types/react@18.2.6)(react@18.2.0)
|
||||
'@emotion/styled': 11.11.0(@emotion/react@11.11.0)(@types/react@18.2.6)(react@18.2.0)
|
||||
@ -1355,8 +1365,8 @@ packages:
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@mui/system@5.12.3(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react@18.2.0):
|
||||
resolution: {integrity: sha512-JB/6sypHqeJCqwldWeQ1MKkijH829EcZAKKizxbU2MJdxGG5KSwZvTBa5D9qiJUA1hJFYYupjiuy9ZdJt6rV6w==}
|
||||
/@mui/system@5.14.4(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react@18.2.0):
|
||||
resolution: {integrity: sha512-oPgfWS97QNfHcDBapdkZIs4G5i85BJt69Hp6wbXF6s7vi3Evcmhdk8AbCRW6n0sX4vTj8oe0mh0RIm1G2A1KDA==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
peerDependencies:
|
||||
'@emotion/react': ^11.5.0
|
||||
@ -1371,15 +1381,15 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.21.5
|
||||
'@babel/runtime': 7.22.10
|
||||
'@emotion/react': 11.11.0(@types/react@18.2.6)(react@18.2.0)
|
||||
'@emotion/styled': 11.11.0(@emotion/react@11.11.0)(@types/react@18.2.6)(react@18.2.0)
|
||||
'@mui/private-theming': 5.12.3(@types/react@18.2.6)(react@18.2.0)
|
||||
'@mui/styled-engine': 5.12.3(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(react@18.2.0)
|
||||
'@mui/private-theming': 5.14.4(@types/react@18.2.6)(react@18.2.0)
|
||||
'@mui/styled-engine': 5.13.2(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(react@18.2.0)
|
||||
'@mui/types': 7.2.4(@types/react@18.2.6)
|
||||
'@mui/utils': 5.12.3(react@18.2.0)
|
||||
'@mui/utils': 5.14.4(react@18.2.0)
|
||||
'@types/react': 18.2.6
|
||||
clsx: 1.2.1
|
||||
clsx: 2.0.0
|
||||
csstype: 3.1.2
|
||||
prop-types: 15.8.1
|
||||
react: 18.2.0
|
||||
@ -1410,6 +1420,20 @@ packages:
|
||||
react-is: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@mui/utils@5.14.4(react@18.2.0):
|
||||
resolution: {integrity: sha512-4ANV0txPD3x0IcTCSEHKDWnsutg1K3m6Vz5IckkbLXVYu17oOZCVUdOKsb/txUmaCd0v0PmSRe5PW+Mlvns5dQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
peerDependencies:
|
||||
react: ^17.0.0 || ^18.0.0
|
||||
dependencies:
|
||||
'@babel/runtime': 7.22.10
|
||||
'@types/prop-types': 15.7.5
|
||||
'@types/react-is': 18.2.1
|
||||
prop-types: 15.8.1
|
||||
react: 18.2.0
|
||||
react-is: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@nodelib/fs.scandir@2.1.5:
|
||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||
engines: {node: '>= 8'}
|
||||
@ -1733,6 +1757,12 @@ packages:
|
||||
'@types/react': 17.0.59
|
||||
dev: false
|
||||
|
||||
/@types/react-is@18.2.1:
|
||||
resolution: {integrity: sha512-wyUkmaaSZEzFZivD8F2ftSyAfk6L+DfFliVj/mYdOXbVjRcS87fQJLTnhk6dRZPuJjI+9g6RZJO4PNCngUrmyw==}
|
||||
dependencies:
|
||||
'@types/react': 18.2.6
|
||||
dev: false
|
||||
|
||||
/@types/react-katex@3.0.0:
|
||||
resolution: {integrity: sha512-AiHHXh71a2M7Z6z1wj6iA23SkiRF9r0neHUdu8zjU/cT3MyLxDefYHbcceKhV/gjDEZgF3YaiNHyPNtoGUjPvg==}
|
||||
dependencies:
|
||||
@ -2341,6 +2371,11 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
dev: false
|
||||
|
||||
/clsx@2.0.0:
|
||||
resolution: {integrity: sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==}
|
||||
engines: {node: '>=6'}
|
||||
dev: false
|
||||
|
||||
/co@4.6.0:
|
||||
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
|
||||
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
|
||||
@ -4860,6 +4895,10 @@ packages:
|
||||
resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
|
||||
dev: false
|
||||
|
||||
/regenerator-runtime@0.14.0:
|
||||
resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==}
|
||||
dev: false
|
||||
|
||||
/regexp.prototype.flags@1.5.0:
|
||||
resolution: {integrity: sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
20
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
20
frontend/appflowy_tauri/src-tauri/Cargo.lock
generated
@ -105,7 +105,7 @@ checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
|
||||
[[package]]
|
||||
name = "appflowy-integrate"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=aac4e56#aac4e56bdb6a61598697283582d369a7af85d9c8"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ba963f#ba963fa299d294e5b2cafd940b9eaa8520280b7b"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collab",
|
||||
@ -1021,7 +1021,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=aac4e56#aac4e56bdb6a61598697283582d369a7af85d9c8"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ba963f#ba963fa299d294e5b2cafd940b9eaa8520280b7b"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@ -1039,7 +1039,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-client-ws"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=aac4e56#aac4e56bdb6a61598697283582d369a7af85d9c8"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ba963f#ba963fa299d294e5b2cafd940b9eaa8520280b7b"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"collab-sync",
|
||||
@ -1057,7 +1057,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-database"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=aac4e56#aac4e56bdb6a61598697283582d369a7af85d9c8"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ba963f#ba963fa299d294e5b2cafd940b9eaa8520280b7b"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -1084,7 +1084,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-derive"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=aac4e56#aac4e56bdb6a61598697283582d369a7af85d9c8"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ba963f#ba963fa299d294e5b2cafd940b9eaa8520280b7b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -1096,7 +1096,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-document"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=aac4e56#aac4e56bdb6a61598697283582d369a7af85d9c8"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ba963f#ba963fa299d294e5b2cafd940b9eaa8520280b7b"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collab",
|
||||
@ -1115,7 +1115,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-folder"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=aac4e56#aac4e56bdb6a61598697283582d369a7af85d9c8"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ba963f#ba963fa299d294e5b2cafd940b9eaa8520280b7b"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@ -1135,7 +1135,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-persistence"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=aac4e56#aac4e56bdb6a61598697283582d369a7af85d9c8"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ba963f#ba963fa299d294e5b2cafd940b9eaa8520280b7b"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"chrono",
|
||||
@ -1155,7 +1155,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-plugins"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=aac4e56#aac4e56bdb6a61598697283582d369a7af85d9c8"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ba963f#ba963fa299d294e5b2cafd940b9eaa8520280b7b"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@ -1185,7 +1185,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collab-sync"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=aac4e56#aac4e56bdb6a61598697283582d369a7af85d9c8"
|
||||
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=ba963f#ba963fa299d294e5b2cafd940b9eaa8520280b7b"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"collab",
|
||||
|
@ -14,19 +14,19 @@ export const Button = ({
|
||||
useEffect(() => {
|
||||
switch (size) {
|
||||
case 'primary':
|
||||
setCls('w-[340px] h-[48px] flex items-center justify-center rounded-lg bg-content-default text-content-on-fill');
|
||||
setCls('w-[340px] h-[48px] flex items-center justify-center rounded-lg bg-fill-default text-content-on-fill');
|
||||
break;
|
||||
case 'medium':
|
||||
setCls('w-[170px] h-[48px] flex items-center justify-center rounded-lg bg-content-default text-content-on-fill');
|
||||
setCls('w-[170px] h-[48px] flex items-center justify-center rounded-lg bg-fill-default text-content-on-fill');
|
||||
break;
|
||||
case 'small':
|
||||
setCls(
|
||||
'w-[68px] h-[32px] flex items-center justify-center rounded-lg bg-content-default text-content-on-fill text-xs hover:bg-content-hover'
|
||||
'w-[68px] h-[32px] flex items-center justify-center rounded-lg bg-fill-default text-content-on-fill text-xs hover:bg-fill-list-hover'
|
||||
);
|
||||
break;
|
||||
case 'medium-transparent':
|
||||
setCls(
|
||||
'w-[170px] h-[48px] flex items-center justify-center rounded-lg border border-content-default text-content-default transition-colors duration-300 hover:bg-content-blue-50 hover:text-content-on-fill'
|
||||
'w-[170px] h-[48px] flex items-center justify-center rounded-lg border border-fill-default text-fill-default transition-colors duration-300 hover:bg-content-blue-50 '
|
||||
);
|
||||
break;
|
||||
case 'box-small-transparent':
|
||||
|
@ -1,12 +1,13 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { List, MenuItem, Popover, Portal } from '@mui/material';
|
||||
import { List, MenuItem, Popover, Portal, Theme } from '@mui/material';
|
||||
import { PopoverOrigin } from '@mui/material/Popover/Popover';
|
||||
import { SxProps } from '@mui/system';
|
||||
|
||||
interface ButtonPopoverListProps {
|
||||
isVisible: boolean;
|
||||
children: React.ReactNode;
|
||||
popoverOptions: {
|
||||
key: string;
|
||||
key: React.Key;
|
||||
icon: React.ReactNode;
|
||||
label: React.ReactNode | string;
|
||||
onClick: () => void;
|
||||
@ -15,8 +16,11 @@ interface ButtonPopoverListProps {
|
||||
anchorOrigin: PopoverOrigin;
|
||||
transformOrigin: PopoverOrigin;
|
||||
};
|
||||
onClose?: () => void;
|
||||
sx?: SxProps<Theme>;
|
||||
}
|
||||
function ButtonPopoverList({ popoverOrigin, isVisible, children, popoverOptions }: ButtonPopoverListProps) {
|
||||
|
||||
function ButtonPopoverList({ popoverOrigin, isVisible, children, popoverOptions, onClose, sx }: ButtonPopoverListProps) {
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLDivElement>();
|
||||
const open = Boolean(anchorEl);
|
||||
const visible = isVisible || open;
|
||||
@ -32,8 +36,16 @@ function ButtonPopoverList({ popoverOrigin, isVisible, children, popoverOptions
|
||||
<>
|
||||
{visible && <div onClick={handleClick}>{children}</div>}
|
||||
<Portal>
|
||||
<Popover open={open} {...popoverOrigin} anchorEl={anchorEl} onClose={handleClose}>
|
||||
<List>
|
||||
<Popover
|
||||
open={open}
|
||||
{...popoverOrigin}
|
||||
anchorEl={anchorEl}
|
||||
onClose={() => {
|
||||
handleClose();
|
||||
onClose?.();
|
||||
}}
|
||||
>
|
||||
<List sx={{ ...sx }}>
|
||||
{popoverOptions.map((option) => (
|
||||
<MenuItem
|
||||
key={option.key}
|
||||
|
@ -7,7 +7,7 @@ export const CheckListProgress = ({ completed, max }: { completed: number; max:
|
||||
{completed > 0 && filledCheckListBars({ amount: completed })}
|
||||
{max - completed > 0 && emptyCheckListBars({ amount: max - completed })}
|
||||
</div>
|
||||
<div className={'text-xs text-shade-4'}>{((100 * completed) / max).toFixed(0)}%</div>
|
||||
<div className={'text-xs text-text-caption'}>{((100 * completed) / max).toFixed(0)}%</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@ -17,11 +17,11 @@ export const CheckListProgress = ({ completed, max }: { completed: number; max:
|
||||
const filledCheckListBars = ({ amount }: { amount: number }) => {
|
||||
return Array(amount)
|
||||
.fill(0)
|
||||
.map((item, index) => <div key={index} className={'h-[4px] flex-1 flex-shrink-0 rounded bg-main-accent'}></div>);
|
||||
.map((item, index) => <div key={index} className={'h-[4px] flex-1 flex-shrink-0 rounded bg-fill-hover'}></div>);
|
||||
};
|
||||
|
||||
const emptyCheckListBars = ({ amount }: { amount: number }) => {
|
||||
return Array(amount)
|
||||
.fill(0)
|
||||
.map((item, index) => <div key={index} className={'h-[4px] flex-1 flex-shrink-0 rounded bg-tint-9'}></div>);
|
||||
.map((item, index) => <div key={index} className={'bg-tint-9 h-[4px] flex-1 flex-shrink-0 rounded'}></div>);
|
||||
};
|
||||
|
@ -0,0 +1,174 @@
|
||||
import { FieldType } from '@/services/backend';
|
||||
import { TrashSvg } from '$app/components/_shared/svg/TrashSvg';
|
||||
import {
|
||||
IDatabaseFilter,
|
||||
ISelectOption,
|
||||
SupportedOperatorsByType,
|
||||
TDatabaseOperators,
|
||||
} from '$app_reducers/database/slice';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { FieldSelect } from '$app/components/_shared/DatabaseFilter/FieldSelect';
|
||||
import { LogicalOperatorSelect } from '$app/components/_shared/DatabaseFilter/LogicalOperatorSelect';
|
||||
import { OperatorSelect } from '$app/components/_shared/DatabaseFilter/OperatorSelect';
|
||||
import { FilterValue } from '$app/components/_shared/DatabaseFilter/FilterValue';
|
||||
|
||||
export const DatabaseFilterItem = ({
|
||||
data,
|
||||
onSave,
|
||||
onDelete,
|
||||
index,
|
||||
}: {
|
||||
data: IDatabaseFilter | null;
|
||||
onSave: (filter: IDatabaseFilter) => void;
|
||||
onDelete?: () => void;
|
||||
index: number;
|
||||
}) => {
|
||||
// stores
|
||||
const columns = useAppSelector((state) => state.database.columns);
|
||||
const fields = useAppSelector((state) => state.database.fields);
|
||||
const filtersStore = useAppSelector((state) => state.database.filters);
|
||||
|
||||
// values
|
||||
const [currentLogicalOperator, setCurrentLogicalOperator] = useState<'and' | 'or'>('and');
|
||||
const [currentFieldId, setCurrentFieldId] = useState<string | null>(data?.fieldId ?? null);
|
||||
const [currentOperator, setCurrentOperator] = useState<TDatabaseOperators | null>(data?.operator ?? null);
|
||||
const [currentValue, setCurrentValue] = useState<string[] | string | boolean | null>(data?.value ?? null);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
setCurrentLogicalOperator(data.logicalOperator);
|
||||
setCurrentFieldId(data.fieldId);
|
||||
setCurrentOperator(data.operator);
|
||||
setCurrentValue(data.value);
|
||||
} else {
|
||||
setCurrentLogicalOperator('and');
|
||||
setCurrentFieldId(null);
|
||||
setCurrentOperator(null);
|
||||
setCurrentValue(null);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const [textInputActive, setTextInputActive] = useState(false);
|
||||
|
||||
// shortcut
|
||||
const currentFieldType = useMemo(
|
||||
() => (currentFieldId ? fields[currentFieldId].fieldType : undefined),
|
||||
[currentFieldId, fields]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// if the user is typing in a text input, don't update the filter
|
||||
if (textInputActive) return;
|
||||
|
||||
if (currentFieldId && currentFieldType !== undefined && currentOperator && currentValue !== null) {
|
||||
if (currentFieldType === FieldType.RichText && (currentValue as string).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
onSave({
|
||||
id: data?.id,
|
||||
logicalOperator: currentLogicalOperator,
|
||||
fieldId: currentFieldId,
|
||||
fieldType: currentFieldType,
|
||||
operator: currentOperator,
|
||||
value: currentValue,
|
||||
});
|
||||
}
|
||||
}, [currentFieldId, currentFieldType, currentOperator, currentValue, textInputActive]);
|
||||
|
||||
// 1. not all field types support filtering
|
||||
// 2. we don't want to show fields that are already in use
|
||||
const supportedColumns = useMemo(
|
||||
() =>
|
||||
columns
|
||||
.filter((column) => SupportedOperatorsByType[fields[column.fieldId].fieldType] !== undefined)
|
||||
.filter((column) => filtersStore.findIndex((filter) => filter?.fieldId === column.fieldId) === -1),
|
||||
[columns, fields, filtersStore]
|
||||
);
|
||||
|
||||
const onSelectFieldClick = (id: string) => {
|
||||
setCurrentFieldId(id);
|
||||
|
||||
switch (fields[id].fieldType) {
|
||||
case FieldType.RichText:
|
||||
setCurrentValue('');
|
||||
setCurrentOperator(null);
|
||||
break;
|
||||
case FieldType.MultiSelect:
|
||||
case FieldType.SingleSelect:
|
||||
setCurrentValue([]);
|
||||
setCurrentOperator(null);
|
||||
break;
|
||||
case FieldType.Checkbox:
|
||||
setCurrentOperator('is');
|
||||
setCurrentValue(false);
|
||||
break;
|
||||
default:
|
||||
setCurrentOperator(null);
|
||||
setCurrentValue(null);
|
||||
}
|
||||
};
|
||||
|
||||
const onSelectOperatorClick = (operator: TDatabaseOperators) => {
|
||||
setCurrentOperator(operator);
|
||||
};
|
||||
|
||||
const onValueOptionClick = (option: ISelectOption) => {
|
||||
const value = currentValue as string[];
|
||||
|
||||
if (value.findIndex((v) => v === option.selectOptionId) === -1) {
|
||||
setCurrentValue([...value, option.selectOptionId]);
|
||||
} else {
|
||||
setCurrentValue(value.filter((v) => v !== option.selectOptionId));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='flex items-center gap-4'>
|
||||
<div className={'w-[88px]'}>
|
||||
{index === 0 ? (
|
||||
<span className={'text-sm text-text-caption'}>Where</span>
|
||||
) : (
|
||||
<LogicalOperatorSelect></LogicalOperatorSelect>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<FieldSelect
|
||||
columns={supportedColumns}
|
||||
fields={fields}
|
||||
onSelectFieldClick={onSelectFieldClick}
|
||||
currentFieldId={currentFieldId}
|
||||
currentFieldType={currentFieldType}
|
||||
></FieldSelect>
|
||||
|
||||
<OperatorSelect
|
||||
currentOperator={currentOperator}
|
||||
currentFieldType={currentFieldType}
|
||||
onSelectOperatorClick={onSelectOperatorClick}
|
||||
></OperatorSelect>
|
||||
|
||||
<FilterValue
|
||||
currentFieldId={currentFieldId}
|
||||
currentFieldType={currentFieldType}
|
||||
currentValue={currentValue}
|
||||
setCurrentValue={setCurrentValue}
|
||||
fields={fields}
|
||||
textInputActive={textInputActive}
|
||||
setTextInputActive={setTextInputActive}
|
||||
onValueOptionClick={onValueOptionClick}
|
||||
></FilterValue>
|
||||
|
||||
<button
|
||||
onClick={() => onDelete?.()}
|
||||
className={`rounded p-1 hover:bg-fill-list-hover ${data ? 'opacity-100' : 'opacity-0'}`}
|
||||
>
|
||||
<i className={'block h-[16px] w-[16px]'}>
|
||||
<TrashSvg />
|
||||
</i>
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,183 @@
|
||||
import { t } from 'i18next';
|
||||
import AddSvg from '../../_shared/svg/AddSvg';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
import { MouseEventHandler, useMemo, useState } from 'react';
|
||||
import { DatabaseFilterItem } from '$app/components/_shared/DatabaseFilter/DatabaseFilterItem';
|
||||
import { IDatabaseFilter, TDatabaseOperators } from '$app_reducers/database/slice';
|
||||
import { FilterController } from '$app/stores/effects/database/filter/filter_controller';
|
||||
import {
|
||||
CheckboxFilterPB,
|
||||
FieldType,
|
||||
SelectOptionConditionPB,
|
||||
SelectOptionFilterPB,
|
||||
TextFilterConditionPB,
|
||||
TextFilterPB,
|
||||
} from '@/services/backend';
|
||||
|
||||
export const DatabaseFilterPopup = ({
|
||||
filterController,
|
||||
onOutsideClick,
|
||||
}: {
|
||||
filterController: FilterController;
|
||||
onOutsideClick: () => void;
|
||||
}) => {
|
||||
// stores
|
||||
const filtersStore = useAppSelector((state) => state.database.filters);
|
||||
|
||||
// local copy to prevent jitter when adding new filter
|
||||
const [filters, setFilters] = useState<(IDatabaseFilter | null)[]>(filtersStore);
|
||||
const [showBlankFilter, setShowBlankFilter] = useState(filtersStore.length === 0);
|
||||
|
||||
const onAddClick: MouseEventHandler = () => {
|
||||
setShowBlankFilter(true);
|
||||
};
|
||||
|
||||
const transformOperator: (
|
||||
operator: TDatabaseOperators,
|
||||
type: FieldType
|
||||
) => TextFilterConditionPB | SelectOptionConditionPB = (operator, type) => {
|
||||
switch (type) {
|
||||
case FieldType.RichText:
|
||||
switch (operator) {
|
||||
case 'contains':
|
||||
return TextFilterConditionPB.Contains;
|
||||
case 'doesNotContain':
|
||||
return TextFilterConditionPB.DoesNotContain;
|
||||
case 'endsWith':
|
||||
return TextFilterConditionPB.EndsWith;
|
||||
case 'startWith':
|
||||
return TextFilterConditionPB.StartsWith;
|
||||
case 'is':
|
||||
return TextFilterConditionPB.Is;
|
||||
case 'isNot':
|
||||
return TextFilterConditionPB.IsNot;
|
||||
case 'isEmpty':
|
||||
return TextFilterConditionPB.TextIsEmpty;
|
||||
case 'isNotEmpty':
|
||||
return TextFilterConditionPB.TextIsNotEmpty;
|
||||
default:
|
||||
return TextFilterConditionPB.Is;
|
||||
}
|
||||
|
||||
case FieldType.SingleSelect:
|
||||
case FieldType.MultiSelect:
|
||||
switch (operator) {
|
||||
case 'is':
|
||||
case 'contains':
|
||||
return SelectOptionConditionPB.OptionIs;
|
||||
case 'isNot':
|
||||
case 'doesNotContain':
|
||||
return SelectOptionConditionPB.OptionIsNot;
|
||||
case 'isEmpty':
|
||||
return SelectOptionConditionPB.OptionIsEmpty;
|
||||
case 'isNotEmpty':
|
||||
return SelectOptionConditionPB.OptionIsNotEmpty;
|
||||
default:
|
||||
return SelectOptionConditionPB.OptionIs;
|
||||
}
|
||||
|
||||
default:
|
||||
return TextFilterConditionPB.Is;
|
||||
}
|
||||
};
|
||||
|
||||
const onSaveFilterItem = async (filter: IDatabaseFilter) => {
|
||||
let val: TextFilterPB | SelectOptionFilterPB | CheckboxFilterPB;
|
||||
|
||||
switch (filter.fieldType) {
|
||||
case FieldType.RichText:
|
||||
val = new TextFilterPB({
|
||||
condition: transformOperator(filter.operator, filter.fieldType) as TextFilterConditionPB,
|
||||
content: filter.value as string,
|
||||
});
|
||||
break;
|
||||
case FieldType.SingleSelect:
|
||||
case FieldType.MultiSelect:
|
||||
val = new SelectOptionFilterPB({
|
||||
condition: transformOperator(filter.operator, filter.fieldType) as SelectOptionConditionPB,
|
||||
option_ids: filter.value as string[],
|
||||
});
|
||||
break;
|
||||
default:
|
||||
val = new TextFilterPB({
|
||||
condition: transformOperator('is', FieldType.RichText) as TextFilterConditionPB,
|
||||
content: '',
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
let updatedFilter = filter;
|
||||
|
||||
if (filter.id) {
|
||||
await filterController.updateFilter(filter.id, filter.fieldId, filter.fieldType, val);
|
||||
} else {
|
||||
const newId = await filterController.addFilter(filter.fieldId, filter.fieldType, val);
|
||||
|
||||
updatedFilter = { ...filter, id: newId };
|
||||
}
|
||||
|
||||
const index = filters.findIndex((f) => f?.fieldId === filter.fieldId);
|
||||
|
||||
if (index === -1) {
|
||||
setFilters([...filters, updatedFilter]);
|
||||
} else {
|
||||
setFilters([...filters.slice(0, index), updatedFilter, ...filters.slice(index + 1)]);
|
||||
}
|
||||
|
||||
setShowBlankFilter(false);
|
||||
};
|
||||
|
||||
const onDeleteFilterItem = async (filter: IDatabaseFilter | null) => {
|
||||
if (!filter || !filter.id || !filter.fieldId) return;
|
||||
|
||||
// add blank filter if no filters left
|
||||
if (filters.length === 1) {
|
||||
setShowBlankFilter(true);
|
||||
}
|
||||
|
||||
await filterController.removeFilter(filter.fieldId, filter.fieldType, filter.id);
|
||||
|
||||
// update local copy
|
||||
const index = filters.findIndex((f) => f?.fieldId === filter.fieldId);
|
||||
|
||||
setFilters([...filters.slice(0, index), ...filters.slice(index + 1)]);
|
||||
};
|
||||
|
||||
// null row represents new filter
|
||||
const rows = useMemo(() => (showBlankFilter ? filters.concat([null]) : filters), [filters, showBlankFilter]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={'fixed inset-0 z-10 flex items-center justify-center overflow-y-auto backdrop-blur-sm'}
|
||||
onClick={onOutsideClick}
|
||||
>
|
||||
<div onClick={(e) => e.stopPropagation()} className='flex flex-col rounded-lg bg-bg-body shadow-md'>
|
||||
<div className='px-6 pt-6 text-sm text-text-caption'>{t('grid.settings.filter')}</div>
|
||||
|
||||
<div className='flex flex-col gap-3 overflow-y-scroll px-6 py-6 text-sm'>
|
||||
{rows.map((filter, index: number) => (
|
||||
<DatabaseFilterItem
|
||||
data={filter}
|
||||
onSave={onSaveFilterItem}
|
||||
onDelete={() => onDeleteFilterItem(filter)}
|
||||
key={index}
|
||||
index={index}
|
||||
></DatabaseFilterItem>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<button
|
||||
onClick={onAddClick}
|
||||
className='flex cursor-pointer items-center gap-2 px-6 py-6 text-sm text-text-caption'
|
||||
>
|
||||
<div className='h-5 w-5'>
|
||||
<AddSvg />
|
||||
</div>
|
||||
{t('grid.settings.addFilter')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,79 @@
|
||||
import { FieldTypeIcon } from '$app/components/_shared/EditRow/FieldTypeIcon';
|
||||
import { DropDownShowSvg } from '$app/components/_shared/svg/DropDownShowSvg';
|
||||
import ButtonPopoverList from '$app/components/_shared/ButtonPopoverList';
|
||||
import React, { useState } from 'react';
|
||||
import { DatabaseFieldMap, IDatabaseColumn } from '$app_reducers/database/slice';
|
||||
import { FieldType } from '@/services/backend';
|
||||
|
||||
interface IFieldSelectProps {
|
||||
columns: IDatabaseColumn[];
|
||||
fields: DatabaseFieldMap;
|
||||
onSelectFieldClick: (fieldId: string) => void;
|
||||
currentFieldId: string | null;
|
||||
currentFieldType: FieldType | undefined;
|
||||
}
|
||||
|
||||
const WIDTH = 180;
|
||||
|
||||
export const FieldSelect = ({
|
||||
columns,
|
||||
fields,
|
||||
onSelectFieldClick,
|
||||
currentFieldId,
|
||||
currentFieldType,
|
||||
}: IFieldSelectProps) => {
|
||||
const [showSelect, setShowSelect] = useState(false);
|
||||
|
||||
return (
|
||||
<ButtonPopoverList
|
||||
isVisible={true}
|
||||
popoverOptions={columns.map((column) => ({
|
||||
key: column.fieldId,
|
||||
icon: (
|
||||
<i className={'block h-5 w-5'}>
|
||||
<FieldTypeIcon fieldType={fields[column.fieldId].fieldType}></FieldTypeIcon>
|
||||
</i>
|
||||
),
|
||||
label: fields[column.fieldId].title,
|
||||
onClick: () => {
|
||||
onSelectFieldClick(column.fieldId);
|
||||
setShowSelect(false);
|
||||
},
|
||||
}))}
|
||||
popoverOrigin={{
|
||||
anchorOrigin: {
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
},
|
||||
transformOrigin: {
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
},
|
||||
}}
|
||||
onClose={() => setShowSelect(false)}
|
||||
sx={{ width: `${WIDTH}px` }}
|
||||
>
|
||||
<div
|
||||
onClick={() => setShowSelect(true)}
|
||||
className={`flex items-center justify-between rounded-lg border px-2 py-1 ${
|
||||
showSelect ? 'border-fill-hover' : 'border-line-border'
|
||||
}`}
|
||||
style={{ width: `${WIDTH}px` }}
|
||||
>
|
||||
{currentFieldType !== undefined && currentFieldId ? (
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<i className={'block h-5 w-5'}>
|
||||
<FieldTypeIcon fieldType={currentFieldType}></FieldTypeIcon>
|
||||
</i>
|
||||
<span>{fields[currentFieldId].title}</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className={'text-text-placeholder'}>Select a field</span>
|
||||
)}
|
||||
<i className={`h-5 w-5 transition-transform duration-500 ${showSelect ? 'rotate-180' : 'rotate-0'}`}>
|
||||
<DropDownShowSvg></DropDownShowSvg>
|
||||
</i>
|
||||
</div>
|
||||
</ButtonPopoverList>
|
||||
);
|
||||
};
|
@ -0,0 +1,146 @@
|
||||
import { FieldType } from '@/services/backend';
|
||||
import { getBgColor } from '$app/components/_shared/getColor';
|
||||
import { DropDownShowSvg } from '$app/components/_shared/svg/DropDownShowSvg';
|
||||
import { EditorCheckSvg } from '$app/components/_shared/svg/EditorCheckSvg';
|
||||
import { EditorUncheckSvg } from '$app/components/_shared/svg/EditorUncheckSvg';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { DatabaseFieldMap, ISelectOption, ISelectOptionType } from '$app_reducers/database/slice';
|
||||
import { CellOption } from '$app/components/_shared/EditRow/Options/CellOption';
|
||||
import { Popover } from '@mui/material';
|
||||
|
||||
interface IFilterValueProps {
|
||||
currentFieldId: string | null;
|
||||
currentFieldType: FieldType | undefined;
|
||||
currentValue: string[] | string | boolean | null;
|
||||
fields: DatabaseFieldMap;
|
||||
textInputActive: boolean;
|
||||
setTextInputActive: (v: boolean) => void;
|
||||
setCurrentValue: (v: string[] | string | boolean | null) => void;
|
||||
onValueOptionClick: (option: ISelectOption) => void;
|
||||
}
|
||||
|
||||
const WIDTH = 180;
|
||||
|
||||
export const FilterValue = ({
|
||||
currentFieldId,
|
||||
currentFieldType,
|
||||
currentValue,
|
||||
fields,
|
||||
textInputActive,
|
||||
setTextInputActive,
|
||||
setCurrentValue,
|
||||
onValueOptionClick,
|
||||
}: IFilterValueProps) => {
|
||||
const [showValueOptions, setShowValueOptions] = useState(false);
|
||||
|
||||
const refValueOptions = useRef<HTMLDivElement>(null);
|
||||
|
||||
const getSelectOption = (optionId: string) => {
|
||||
if (!currentFieldId) return undefined;
|
||||
return (fields[currentFieldId].fieldOptions as ISelectOptionType).selectOptions.find(
|
||||
(option) => option.selectOptionId === optionId
|
||||
);
|
||||
};
|
||||
|
||||
return currentFieldId ? (
|
||||
<>
|
||||
{(currentFieldType === FieldType.MultiSelect || currentFieldType === FieldType.SingleSelect) && (
|
||||
<>
|
||||
<div
|
||||
ref={refValueOptions}
|
||||
onClick={() => setShowValueOptions(true)}
|
||||
className={`flex items-center justify-between rounded-lg border px-2 py-1 ${
|
||||
showValueOptions ? 'border-fill-hover' : 'border-line-border'
|
||||
}`}
|
||||
style={{ width: `${WIDTH}px` }}
|
||||
>
|
||||
{currentValue ? (
|
||||
<div className={'flex flex-1 items-center gap-1 overflow-hidden'}>
|
||||
{(currentValue as string[]).length === 0 && (
|
||||
<span className={'text-text-placeholder'}>none selected</span>
|
||||
)}
|
||||
{(currentValue as string[]).map((option, i) => (
|
||||
<span className={`${getBgColor(getSelectOption(option)?.color)} rounded px-2 py-0.5 text-xs`} key={i}>
|
||||
{getSelectOption(option)?.title}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span className={'text-text-placeholder'}>Select an option</span>
|
||||
)}
|
||||
|
||||
<i className={`h-5 w-5 transition-transform duration-500 ${showValueOptions ? 'rotate-180' : 'rotate-0'}`}>
|
||||
<DropDownShowSvg></DropDownShowSvg>
|
||||
</i>
|
||||
</div>
|
||||
|
||||
<Popover
|
||||
open={showValueOptions}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
anchorEl={refValueOptions.current}
|
||||
onClose={() => setShowValueOptions(false)}
|
||||
>
|
||||
<div style={{ width: `${WIDTH}px` }} className={'flex flex-col gap-2 p-2 text-xs'}>
|
||||
<div className={'font-medium text-text-caption'}>Value option</div>
|
||||
<div className={'flex flex-col gap-1'}>
|
||||
{(fields[currentFieldId].fieldOptions as ISelectOptionType).selectOptions.map((option, index) => (
|
||||
<CellOption
|
||||
key={index}
|
||||
option={option}
|
||||
checked={(currentValue as string[]).findIndex((o) => o === option.selectOptionId) !== -1}
|
||||
noSelect={true}
|
||||
noDetail={true}
|
||||
onOptionClick={() => onValueOptionClick(option)}
|
||||
></CellOption>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
</>
|
||||
)}
|
||||
{currentFieldType === FieldType.RichText && (
|
||||
<div
|
||||
className={`flex items-center justify-between rounded-lg border px-2 py-1 ${
|
||||
textInputActive ? 'border-fill-hover' : 'border-line-border'
|
||||
}`}
|
||||
style={{ width: `${WIDTH}px` }}
|
||||
>
|
||||
<input
|
||||
placeholder={'Enter value'}
|
||||
className={'flex-1'}
|
||||
onFocus={() => setTextInputActive(true)}
|
||||
onBlur={() => setTextInputActive(false)}
|
||||
value={currentValue as string}
|
||||
onChange={(e) => setCurrentValue(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{currentFieldType === FieldType.Checkbox && (
|
||||
<div
|
||||
onClick={() => setCurrentValue(!currentValue)}
|
||||
className={`flex cursor-pointer items-center gap-2 rounded-lg border border-line-border px-2 py-1`}
|
||||
style={{ width: `${WIDTH}px` }}
|
||||
>
|
||||
<button className={'h-5 w-5'}>
|
||||
{currentValue ? <EditorCheckSvg></EditorCheckSvg> : <EditorUncheckSvg></EditorUncheckSvg>}
|
||||
</button>
|
||||
<span>{currentValue ? 'Checked' : 'Unchecked'}</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div
|
||||
className={`flex items-center justify-between rounded-lg border border-line-border px-2 py-1`}
|
||||
style={{ width: `${WIDTH}px` }}
|
||||
>
|
||||
<span className={'text-text-placeholder'}>Select field</span>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,50 @@
|
||||
import ButtonPopoverList from '$app/components/_shared/ButtonPopoverList';
|
||||
import { DropDownShowSvg } from '$app/components/_shared/svg/DropDownShowSvg';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const LogicalOperators: ('and' | 'or')[] = ['and', 'or'];
|
||||
const WIDTH = 88;
|
||||
|
||||
export const LogicalOperatorSelect = () => {
|
||||
const [showSelect, setShowSelect] = useState(false);
|
||||
|
||||
return (
|
||||
<ButtonPopoverList
|
||||
isVisible={true}
|
||||
popoverOptions={LogicalOperators.map((operator) => ({
|
||||
key: operator,
|
||||
label: operator,
|
||||
icon: null,
|
||||
onClick: () => {
|
||||
console.log('logical operator: ', operator);
|
||||
setShowSelect(false);
|
||||
},
|
||||
}))}
|
||||
popoverOrigin={{
|
||||
anchorOrigin: {
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
},
|
||||
transformOrigin: {
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
},
|
||||
}}
|
||||
onClose={() => setShowSelect(false)}
|
||||
sx={{ width: `${WIDTH}px` }}
|
||||
>
|
||||
<div
|
||||
onClick={() => setShowSelect(true)}
|
||||
className={`flex items-center justify-between rounded-lg border px-2 py-1 ${
|
||||
showSelect ? 'border-fill-hover' : 'border-line-border'
|
||||
}`}
|
||||
style={{ width: `${WIDTH}px` }}
|
||||
>
|
||||
and
|
||||
<i className={`h-5 w-5 transition-transform duration-500 ${showSelect ? 'rotate-180' : 'rotate-0'}`}>
|
||||
<DropDownShowSvg></DropDownShowSvg>
|
||||
</i>
|
||||
</div>
|
||||
</ButtonPopoverList>
|
||||
);
|
||||
};
|
@ -0,0 +1,63 @@
|
||||
import ButtonPopoverList from '$app/components/_shared/ButtonPopoverList';
|
||||
import React, { useState } from 'react';
|
||||
import { DropDownShowSvg } from '$app/components/_shared/svg/DropDownShowSvg';
|
||||
import { SupportedOperatorsByType, TDatabaseOperators } from '$app_reducers/database/slice';
|
||||
import { FieldType } from '@/services/backend';
|
||||
|
||||
interface IOperatorSelectProps {
|
||||
currentOperator: TDatabaseOperators | null;
|
||||
currentFieldType: FieldType | undefined;
|
||||
onSelectOperatorClick: (operator: TDatabaseOperators) => void;
|
||||
}
|
||||
|
||||
const WIDTH = 180;
|
||||
|
||||
export const OperatorSelect = ({ currentOperator, currentFieldType, onSelectOperatorClick }: IOperatorSelectProps) => {
|
||||
const [showSelect, setShowSelect] = useState(false);
|
||||
|
||||
return (
|
||||
<ButtonPopoverList
|
||||
isVisible={true}
|
||||
popoverOptions={SupportedOperatorsByType[currentFieldType ? currentFieldType : FieldType.RichText].map(
|
||||
(operatorName, index) => ({
|
||||
icon: null,
|
||||
key: index,
|
||||
label: operatorName,
|
||||
onClick: () => {
|
||||
onSelectOperatorClick(operatorName);
|
||||
setShowSelect(false);
|
||||
},
|
||||
})
|
||||
)}
|
||||
popoverOrigin={{
|
||||
anchorOrigin: {
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
},
|
||||
transformOrigin: {
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
},
|
||||
}}
|
||||
onClose={() => setShowSelect(false)}
|
||||
sx={{ width: `${WIDTH}px` }}
|
||||
>
|
||||
<div
|
||||
onClick={() => setShowSelect(true)}
|
||||
className={`flex items-center justify-between rounded-lg border px-2 py-1 ${
|
||||
showSelect ? 'border-fill-hover' : 'border-line-border'
|
||||
}`}
|
||||
style={{ width: `${WIDTH}px` }}
|
||||
>
|
||||
{currentOperator ? (
|
||||
<span>{currentOperator}</span>
|
||||
) : (
|
||||
<span className={'text-text-placeholder'}>Select an option</span>
|
||||
)}
|
||||
<i className={`h-5 w-5 transition-transform duration-500 ${showSelect ? 'rotate-180' : 'rotate-0'}`}>
|
||||
<DropDownShowSvg></DropDownShowSvg>
|
||||
</i>
|
||||
</div>
|
||||
</ButtonPopoverList>
|
||||
);
|
||||
};
|
@ -0,0 +1,97 @@
|
||||
import { IDatabaseSort } from '$app_reducers/database/slice';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
import { DragElementSvg } from '$app/components/_shared/svg/DragElementSvg';
|
||||
import { SortConditionPB } from '@/services/backend';
|
||||
import { TrashSvg } from '$app/components/_shared/svg/TrashSvg';
|
||||
import { FieldSelect } from '$app/components/_shared/DatabaseFilter/FieldSelect';
|
||||
import { OrderSelect } from '$app/components/_shared/DatabaseSort/OrderSelect';
|
||||
|
||||
export const DatabaseSortItem = ({
|
||||
data,
|
||||
onSave,
|
||||
onDelete,
|
||||
}: {
|
||||
data: IDatabaseSort | null;
|
||||
onSave: (sortItem: IDatabaseSort) => void;
|
||||
onDelete?: () => void;
|
||||
}) => {
|
||||
// stores
|
||||
const columns = useAppSelector((state) => state.database.columns);
|
||||
const fields = useAppSelector((state) => state.database.fields);
|
||||
const sortStore = useAppSelector((state) => state.database.sort);
|
||||
|
||||
// values
|
||||
const [currentFieldId, setCurrentFieldId] = useState<string | null>(data?.fieldId ?? null);
|
||||
const [currentOrder, setCurrentOrder] = useState<SortConditionPB | null>(data?.order ?? null);
|
||||
|
||||
const supportedColumns = useMemo(
|
||||
() => columns.filter((c) => sortStore.findIndex((s) => s.fieldId === c.fieldId) === -1),
|
||||
[columns, sortStore]
|
||||
);
|
||||
|
||||
const currentFieldType = useMemo(
|
||||
() => (currentFieldId ? fields[currentFieldId].fieldType : undefined),
|
||||
[currentFieldId, fields]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
setCurrentFieldId(data.fieldId);
|
||||
setCurrentOrder(data.order);
|
||||
} else {
|
||||
setCurrentFieldId(null);
|
||||
setCurrentOrder(null);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentFieldId && currentOrder !== null) {
|
||||
onSave({
|
||||
id: data?.id,
|
||||
fieldId: currentFieldId,
|
||||
order: currentOrder,
|
||||
fieldType: fields[currentFieldId].fieldType,
|
||||
});
|
||||
}
|
||||
}, [currentFieldId, currentOrder]);
|
||||
|
||||
const onSelectFieldClick = (id: string) => {
|
||||
setCurrentFieldId(id);
|
||||
// set ascending order by default
|
||||
setCurrentOrder(SortConditionPB.Ascending);
|
||||
};
|
||||
|
||||
const onSelectOrderClick = (order: SortConditionPB) => {
|
||||
setCurrentOrder(order);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={'flex items-center gap-4'}>
|
||||
<button className={'flex-shrink-0 rounded p-1 hover:bg-fill-list-hover'}>
|
||||
<i className={'block h-[16px] w-[16px]'}>
|
||||
<DragElementSvg></DragElementSvg>
|
||||
</i>
|
||||
</button>
|
||||
|
||||
<FieldSelect
|
||||
columns={supportedColumns}
|
||||
fields={fields}
|
||||
onSelectFieldClick={onSelectFieldClick}
|
||||
currentFieldId={currentFieldId}
|
||||
currentFieldType={currentFieldType}
|
||||
></FieldSelect>
|
||||
|
||||
<OrderSelect currentOrder={currentOrder} onSelectOrderClick={onSelectOrderClick}></OrderSelect>
|
||||
|
||||
<button
|
||||
onClick={() => onDelete?.()}
|
||||
className={`rounded p-1 hover:bg-fill-list-hover ${data ? 'opacity-100' : 'opacity-0'}`}
|
||||
>
|
||||
<i className={'block h-[16px] w-[16px]'}>
|
||||
<TrashSvg />
|
||||
</i>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,99 @@
|
||||
import { t } from 'i18next';
|
||||
import { MouseEventHandler, useMemo, useRef, useState } from 'react';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
import { IDatabaseSort } from '$app_reducers/database/slice';
|
||||
import { DatabaseSortItem } from '$app/components/_shared/DatabaseSort/DatabaseSortItem';
|
||||
import AddSvg from '$app/components/_shared/svg/AddSvg';
|
||||
import { SortController } from '$app/stores/effects/database/sort/sort_controller';
|
||||
|
||||
export const DatabaseSortPopup = ({
|
||||
sortController,
|
||||
onOutsideClick,
|
||||
}: {
|
||||
sortController: SortController;
|
||||
onOutsideClick: () => void;
|
||||
}) => {
|
||||
// stores
|
||||
const sortStore = useAppSelector((state) => state.database.sort);
|
||||
const [sort, setSort] = useState<(IDatabaseSort | null)[]>(sortStore);
|
||||
|
||||
const [showBlankSort, setShowBlankSort] = useState(sortStore.length === 0);
|
||||
|
||||
const onSaveSortItem = async (sortItem: IDatabaseSort) => {
|
||||
let updatedSort = sortItem;
|
||||
|
||||
if (sortItem.id) {
|
||||
await sortController.updateSort(sortItem.id, sortItem.fieldId, sortItem.fieldType, sortItem.order);
|
||||
} else {
|
||||
const newId = await sortController.addSort(sortItem.fieldId, sortItem.fieldType, sortItem.order);
|
||||
|
||||
updatedSort = { ...updatedSort, id: newId };
|
||||
}
|
||||
|
||||
// update local copy
|
||||
const index = sort.findIndex((s) => s?.fieldId === sortItem.fieldId);
|
||||
|
||||
if (index === -1) {
|
||||
setSort([...sort, updatedSort]);
|
||||
} else {
|
||||
setSort([...sort.slice(0, index), updatedSort, ...sort.slice(index + 1)]);
|
||||
}
|
||||
|
||||
setShowBlankSort(false);
|
||||
};
|
||||
|
||||
const onDeleteClick = async (sortItem: IDatabaseSort | null) => {
|
||||
if (!sortItem || !sortItem.id) return;
|
||||
|
||||
// add blank sort if no sorts left
|
||||
if (sort.length === 1) {
|
||||
setShowBlankSort(true);
|
||||
}
|
||||
|
||||
await sortController.removeSort(sortItem.fieldId, sortItem.fieldType, sortItem.id);
|
||||
|
||||
const index = sort.findIndex((s) => s?.fieldId === sortItem.fieldId);
|
||||
|
||||
setSort([...sort.slice(0, index), ...sort.slice(index + 1)]);
|
||||
};
|
||||
|
||||
const onAddClick: MouseEventHandler = () => {
|
||||
setShowBlankSort(true);
|
||||
};
|
||||
|
||||
const rows = useMemo(() => (showBlankSort ? [...sort, null] : sort), [sort, showBlankSort]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={'fixed inset-0 z-10 flex items-center justify-center overflow-y-auto backdrop-blur-sm'}
|
||||
onClick={onOutsideClick}
|
||||
>
|
||||
<div onClick={(e) => e.stopPropagation()} className='flex flex-col rounded-lg bg-white shadow-md'>
|
||||
<div className='px-6 pt-6 text-sm text-text-caption'>{t('grid.settings.sort')}</div>
|
||||
|
||||
<div className='flex flex-col gap-3 overflow-y-scroll px-6 py-6 text-sm'>
|
||||
{rows.map((sortItem, index) => (
|
||||
<DatabaseSortItem
|
||||
key={index}
|
||||
data={sortItem}
|
||||
onSave={onSaveSortItem}
|
||||
onDelete={() => onDeleteClick(sortItem)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<button
|
||||
onClick={onAddClick}
|
||||
className='flex cursor-pointer items-center gap-2 px-6 py-6 text-sm text-text-caption'
|
||||
>
|
||||
<div className='h-5 w-5'>
|
||||
<AddSvg />
|
||||
</div>
|
||||
{t('grid.sort.addSort')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,97 @@
|
||||
import ButtonPopoverList from '$app/components/_shared/ButtonPopoverList';
|
||||
import React, { useState } from 'react';
|
||||
import { SortConditionPB } from '@/services/backend';
|
||||
import { SortAscSvg } from '$app/components/_shared/svg/SortAscSvg';
|
||||
import { SortDescSvg } from '$app/components/_shared/svg/SortDescSvg';
|
||||
import { DropDownShowSvg } from '$app/components/_shared/svg/DropDownShowSvg';
|
||||
|
||||
interface IOrderSelectProps {
|
||||
currentOrder: SortConditionPB | null;
|
||||
onSelectOrderClick: (order: SortConditionPB) => void;
|
||||
}
|
||||
|
||||
const WIDTH = 180;
|
||||
|
||||
export const OrderSelect = ({ currentOrder, onSelectOrderClick }: IOrderSelectProps) => {
|
||||
const [showSelect, setShowSelect] = useState(false);
|
||||
|
||||
return (
|
||||
<ButtonPopoverList
|
||||
isVisible={true}
|
||||
popoverOptions={[
|
||||
{
|
||||
icon: (
|
||||
<i className={'block h-5 w-5'}>
|
||||
<SortAscSvg></SortAscSvg>
|
||||
</i>
|
||||
),
|
||||
label: 'Ascending',
|
||||
key: SortConditionPB.Ascending,
|
||||
onClick: () => {
|
||||
onSelectOrderClick(SortConditionPB.Ascending);
|
||||
setShowSelect(false);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<i className={'block h-5 w-5'}>
|
||||
<SortDescSvg></SortDescSvg>
|
||||
</i>
|
||||
),
|
||||
label: 'Descending',
|
||||
key: SortConditionPB.Descending,
|
||||
onClick: () => {
|
||||
onSelectOrderClick(SortConditionPB.Descending);
|
||||
setShowSelect(false);
|
||||
},
|
||||
},
|
||||
]}
|
||||
popoverOrigin={{
|
||||
anchorOrigin: {
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
},
|
||||
transformOrigin: {
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
},
|
||||
}}
|
||||
onClose={() => setShowSelect(false)}
|
||||
sx={{ width: `${WIDTH}px` }}
|
||||
>
|
||||
<div
|
||||
onClick={() => setShowSelect(true)}
|
||||
className={`flex w-[180px] items-center justify-between rounded-lg border px-2 py-1 ${
|
||||
showSelect ? 'border-fill-hover' : 'border-line-border'
|
||||
}`}
|
||||
>
|
||||
{currentOrder !== null ? (
|
||||
<SortLabel value={currentOrder}></SortLabel>
|
||||
) : (
|
||||
<span className={'text-text-caption'}>Select order</span>
|
||||
)}
|
||||
<i className={`h-5 w-5 transition-transform duration-500 ${showSelect ? 'rotate-180' : 'rotate-0'}`}>
|
||||
<DropDownShowSvg></DropDownShowSvg>
|
||||
</i>
|
||||
</div>
|
||||
</ButtonPopoverList>
|
||||
);
|
||||
};
|
||||
|
||||
const SortLabel = ({ value }: { value: SortConditionPB }) => {
|
||||
return value === SortConditionPB.Ascending ? (
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<i className={'block h-5 w-5'}>
|
||||
<SortAscSvg></SortAscSvg>
|
||||
</i>
|
||||
<span>Ascending</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<i className={'block h-5 w-5'}>
|
||||
<SortDescSvg></SortDescSvg>
|
||||
</i>
|
||||
<span>Descending</span>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,7 +1,7 @@
|
||||
import { FieldType } from '@/services/backend';
|
||||
import { FieldTypeIcon } from '$app/components/_shared/EditRow/FieldTypeIcon';
|
||||
import { FieldTypeName } from '$app/components/_shared/EditRow/FieldTypeName';
|
||||
import { PopupWindow } from '$app/components/_shared/PopupWindow';
|
||||
import { Popover } from '@mui/material';
|
||||
|
||||
const typesOrder: FieldType[] = [
|
||||
FieldType.RichText,
|
||||
@ -15,19 +15,31 @@ const typesOrder: FieldType[] = [
|
||||
];
|
||||
|
||||
export const ChangeFieldTypePopup = ({
|
||||
top,
|
||||
left,
|
||||
open,
|
||||
anchorEl,
|
||||
onClick,
|
||||
onOutsideClick,
|
||||
}: {
|
||||
top: number;
|
||||
left: number;
|
||||
open: boolean;
|
||||
anchorEl: HTMLDivElement | null;
|
||||
onClick: (newType: FieldType) => void;
|
||||
onOutsideClick: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<PopupWindow className={'p-2 text-xs'} onOutsideClick={onOutsideClick} left={left} top={top}>
|
||||
<div className={'flex flex-col'}>
|
||||
<Popover
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
onClose={onOutsideClick}
|
||||
>
|
||||
<div className={'flex flex-col p-2 text-xs'}>
|
||||
{typesOrder.map((t, i) => (
|
||||
<button
|
||||
onClick={() => onClick(t)}
|
||||
@ -43,6 +55,6 @@ export const ChangeFieldTypePopup = ({
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</PopupWindow>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
@ -58,7 +58,7 @@ export const CheckListPopup = ({
|
||||
return (
|
||||
<PopupWindow className={'text-xs'} onOutsideClick={onOutsideClick} left={left} top={top}>
|
||||
<div className={'min-w-[320px]'}>
|
||||
<div className={'px-4 pt-8 pb-4'}>
|
||||
<div className={'px-4 pb-4 pt-8'}>
|
||||
<CheckListProgress completed={selectedOptionsCount} max={allOptionsCount} />
|
||||
</div>
|
||||
|
||||
@ -87,7 +87,7 @@ export const CheckListPopup = ({
|
||||
></NewCheckListOption>
|
||||
))}
|
||||
</div>
|
||||
<div className={'h-[1px] bg-shade-6'}></div>
|
||||
<div className={'h-[1px] bg-line-divider'}></div>
|
||||
<div className={'p-2'}>
|
||||
<NewCheckListButton newOptions={newOptions} setNewOptions={setNewOptions}></NewCheckListButton>
|
||||
</div>
|
||||
|
@ -78,7 +78,7 @@ export const EditCheckListPopup = ({
|
||||
onKeyDown={onKeyDown}
|
||||
onBlur={() => onBlur()}
|
||||
/>
|
||||
<div className={'text-shade-3 font-mono'}>{value.length}/30</div>
|
||||
<div className={'font-mono text-text-caption'}>{value.length}/30</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onDeleteOptionClick()}
|
||||
|
@ -17,7 +17,7 @@ export const NewCheckListButton = ({
|
||||
return (
|
||||
<button
|
||||
onClick={() => newOptionClick()}
|
||||
className={'flex w-full items-center gap-2 rounded-lg px-2 py-2 hover:bg-shade-6'}
|
||||
className={'flex w-full items-center gap-2 rounded-lg px-2 py-2 hover:bg-line-divider'}
|
||||
>
|
||||
<i className={'h-5 w-5'}>
|
||||
<AddSvg></AddSvg>
|
||||
|
@ -35,7 +35,7 @@ export const NewCheckListOption = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={'flex cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-shade-6'}>
|
||||
<div className={'flex cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-line-divider'}>
|
||||
<input
|
||||
onKeyDown={(e) => onNewOptionKeyDown(e as unknown as KeyboardEvent)}
|
||||
className={'min-w-0 flex-1 pl-7'}
|
||||
@ -44,7 +44,7 @@ export const NewCheckListOption = ({
|
||||
/>
|
||||
<button
|
||||
onClick={() => onSaveNewOptionClick()}
|
||||
className={'flex items-center gap-2 rounded-lg bg-main-accent px-4 py-2 text-white hover:bg-main-hovered'}
|
||||
className={'flex items-center gap-2 rounded-lg bg-fill-hover px-4 py-2 text-white hover:bg-main-hovered'}
|
||||
>
|
||||
{t('grid.selectOption.create')}
|
||||
</button>
|
||||
|
@ -87,7 +87,7 @@ export const DateTypeOptions = ({
|
||||
|
||||
return (
|
||||
<div className={'flex flex-col'}>
|
||||
<hr className={'border-shade-6 -mx-2 my-2'} />
|
||||
<hr className={'border-line-divider -mx-2 my-2'} />
|
||||
<button
|
||||
onClick={_onDateFormatClick}
|
||||
className={
|
||||
|
@ -30,7 +30,7 @@ export const EditCellWrapper = ({
|
||||
cellIdentifier: CellIdentifier;
|
||||
cellCache: CellCache;
|
||||
fieldController: FieldController;
|
||||
onEditFieldClick: (cell: CellIdentifier, left: number, top: number) => void;
|
||||
onEditFieldClick: (cell: CellIdentifier, anchorEl: HTMLDivElement) => void;
|
||||
onEditOptionsClick: (cell: CellIdentifier, left: number, top: number) => void;
|
||||
onEditDateClick: (cell: CellIdentifier, left: number, top: number) => void;
|
||||
onEditCheckListClick: (cell: CellIdentifier, left: number, top: number) => void;
|
||||
@ -41,9 +41,8 @@ export const EditCellWrapper = ({
|
||||
|
||||
const onClick = () => {
|
||||
if (!el.current) return;
|
||||
const { top, right } = el.current.getBoundingClientRect();
|
||||
|
||||
onEditFieldClick(cellIdentifier, right, top);
|
||||
onEditFieldClick(cellIdentifier, el.current);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -93,7 +92,13 @@ export const EditCellWrapper = ({
|
||||
{cellIdentifier.fieldType === FieldType.Checkbox && cellController && (
|
||||
<EditCheckboxCell
|
||||
data={data as 'Yes' | 'No' | undefined}
|
||||
cellController={cellController}
|
||||
onToggle={async () => {
|
||||
if (data === 'Yes') {
|
||||
await cellController?.saveCellData('No');
|
||||
} else {
|
||||
await cellController?.saveCellData('Yes');
|
||||
}
|
||||
}}
|
||||
></EditCheckboxCell>
|
||||
)}
|
||||
|
||||
@ -105,15 +110,30 @@ export const EditCellWrapper = ({
|
||||
)}
|
||||
|
||||
{cellIdentifier.fieldType === FieldType.Number && cellController && (
|
||||
<EditCellNumber data={data as string | undefined} cellController={cellController}></EditCellNumber>
|
||||
<EditCellNumber
|
||||
data={data as string | undefined}
|
||||
onSave={async (value) => {
|
||||
await cellController?.saveCellData(value);
|
||||
}}
|
||||
></EditCellNumber>
|
||||
)}
|
||||
|
||||
{cellIdentifier.fieldType === FieldType.URL && cellController && (
|
||||
<EditCellUrl data={data as URLCellDataPB} cellController={cellController}></EditCellUrl>
|
||||
<EditCellUrl
|
||||
data={data as URLCellDataPB}
|
||||
onSave={async (value) => {
|
||||
await cellController?.saveCellData(value);
|
||||
}}
|
||||
></EditCellUrl>
|
||||
)}
|
||||
|
||||
{cellIdentifier.fieldType === FieldType.RichText && cellController && (
|
||||
<EditCellText data={data as string | undefined} cellController={cellController}></EditCellText>
|
||||
<EditCellText
|
||||
data={data as string | undefined}
|
||||
onSave={async (value) => {
|
||||
await cellController?.saveCellData(value);
|
||||
}}
|
||||
></EditCellText>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,47 +1,49 @@
|
||||
import { FocusEventHandler, MouseEventHandler, useEffect, useRef, useState } from 'react';
|
||||
import React, { FocusEventHandler, useEffect, useRef, useState } from 'react';
|
||||
import { FieldTypeIcon } from '$app/components/_shared/EditRow/FieldTypeIcon';
|
||||
import { FieldTypeName } from '$app/components/_shared/EditRow/FieldTypeName';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TypeOptionController } from '$app/stores/effects/database/field/type_option/type_option_controller';
|
||||
import { Some } from 'ts-results';
|
||||
import { FieldController, FieldInfo } from '$app/stores/effects/database/field/field_controller';
|
||||
import { MoreSvg } from '$app/components/_shared/svg/MoreSvg';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
|
||||
import { PopupWindow } from '$app/components/_shared/PopupWindow';
|
||||
import { FieldType } from '@/services/backend';
|
||||
import { DateTypeOptions } from '$app/components/_shared/EditRow/Date/DateTypeOptions';
|
||||
import { DatabaseController } from '$app/stores/effects/database/database_controller';
|
||||
import { EyeClosedSvg } from '$app/components/_shared/svg/EyeClosedSvg';
|
||||
import { Popover } from '@mui/material';
|
||||
import { CopySvg } from '$app/components/_shared/svg/CopySvg';
|
||||
import { TrashSvg } from '$app/components/_shared/svg/TrashSvg';
|
||||
import { SkipLeftSvg } from '$app/components/_shared/svg/SkipLeftSvg';
|
||||
import { SkipRightSvg } from '$app/components/_shared/svg/SkipRightSvg';
|
||||
import { EyeOpenSvg } from '$app/components/_shared/svg/EyeOpenSvg';
|
||||
|
||||
export const EditFieldPopup = ({
|
||||
top,
|
||||
left,
|
||||
open,
|
||||
anchorEl,
|
||||
cellIdentifier,
|
||||
viewId,
|
||||
onOutsideClick,
|
||||
fieldInfo,
|
||||
fieldController,
|
||||
controller,
|
||||
changeFieldTypeClick,
|
||||
onNumberFormat,
|
||||
onDeletePropertyClick,
|
||||
}: {
|
||||
top: number;
|
||||
left: number;
|
||||
open: boolean;
|
||||
anchorEl: HTMLDivElement | null;
|
||||
cellIdentifier: CellIdentifier;
|
||||
viewId: string;
|
||||
onOutsideClick: () => void;
|
||||
fieldInfo: FieldInfo | undefined;
|
||||
fieldController?: FieldController;
|
||||
changeFieldTypeClick: (buttonTop: number, buttonRight: number) => void;
|
||||
onNumberFormat?: (buttonLeft: number, buttonTop: number) => void;
|
||||
controller: DatabaseController;
|
||||
changeFieldTypeClick: (el: HTMLDivElement) => void;
|
||||
onDeletePropertyClick: (fieldId: string) => void;
|
||||
}) => {
|
||||
const databaseStore = useAppSelector((state) => state.database);
|
||||
const fieldsStore = useAppSelector((state) => state.database.fields);
|
||||
const { t } = useTranslation();
|
||||
const changeTypeButtonRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [name, setName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setName(databaseStore.fields[cellIdentifier.fieldId].title);
|
||||
}, [databaseStore, cellIdentifier]);
|
||||
setName(fieldsStore[cellIdentifier.fieldId].title);
|
||||
}, [fieldsStore, cellIdentifier]);
|
||||
|
||||
// focus input on mount
|
||||
useEffect(() => {
|
||||
@ -55,45 +57,71 @@ export const EditFieldPopup = ({
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
if (!controller) return;
|
||||
const fieldInfo = controller.fieldController.getField(cellIdentifier.fieldId);
|
||||
if (!fieldInfo) return;
|
||||
const controller = new TypeOptionController(viewId, Some(fieldInfo));
|
||||
const typeOptionController = new TypeOptionController(viewId, Some(fieldInfo));
|
||||
|
||||
await controller.initialize();
|
||||
await controller.setFieldName(name);
|
||||
await typeOptionController.initialize();
|
||||
await typeOptionController.setFieldName(name);
|
||||
};
|
||||
|
||||
const onChangeFieldTypeClick = () => {
|
||||
if (!changeTypeButtonRef.current) return;
|
||||
const { top: buttonTop, right: buttonRight } = changeTypeButtonRef.current.getBoundingClientRect();
|
||||
|
||||
changeFieldTypeClick(buttonTop, buttonRight);
|
||||
changeFieldTypeClick(changeTypeButtonRef.current);
|
||||
};
|
||||
|
||||
const onNumberFormatClick: MouseEventHandler = (e) => {
|
||||
e.stopPropagation();
|
||||
let target = e.target as HTMLElement;
|
||||
const toggleHideProperty = async () => {
|
||||
// we need to close the popup because after hiding the field, parent element will be removed
|
||||
onOutsideClick();
|
||||
const fieldInfo = controller.fieldController.getField(cellIdentifier.fieldId);
|
||||
|
||||
while (!(target instanceof HTMLButtonElement)) {
|
||||
if (target.parentElement === null) return;
|
||||
target = target.parentElement;
|
||||
if (fieldInfo) {
|
||||
const typeController = new TypeOptionController(viewId, Some(fieldInfo));
|
||||
|
||||
await typeController.initialize();
|
||||
if (fieldInfo.field.visibility) {
|
||||
await typeController.hideField();
|
||||
} else {
|
||||
await typeController.showField();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const { right: _left, top: _top } = target.getBoundingClientRect();
|
||||
const onDuplicatePropertyClick = async () => {
|
||||
onOutsideClick();
|
||||
await controller.duplicateField(cellIdentifier.fieldId);
|
||||
};
|
||||
|
||||
onNumberFormat?.(_left, _top);
|
||||
const onAddToLeftClick = async () => {
|
||||
onOutsideClick();
|
||||
await controller.addFieldToLeft(cellIdentifier.fieldId);
|
||||
};
|
||||
|
||||
const onAddToRightClick = async () => {
|
||||
onOutsideClick();
|
||||
await controller.addFieldToRight(cellIdentifier.fieldId);
|
||||
};
|
||||
|
||||
return (
|
||||
<PopupWindow
|
||||
className={'px-2 py-2 text-xs'}
|
||||
onOutsideClick={async () => {
|
||||
<Popover
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={async () => {
|
||||
await save();
|
||||
onOutsideClick();
|
||||
}}
|
||||
left={left}
|
||||
top={top}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
>
|
||||
<div className={'flex flex-col gap-2'}>
|
||||
<div className={'flex flex-col gap-2 p-2 text-xs'}>
|
||||
<input
|
||||
ref={inputRef}
|
||||
onFocus={selectAll}
|
||||
@ -127,29 +155,75 @@ export const EditFieldPopup = ({
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{cellIdentifier.fieldType === FieldType.Number && (
|
||||
<>
|
||||
<hr className={'-mx-2 border-line-divider'} />
|
||||
<button
|
||||
onClick={onNumberFormatClick}
|
||||
className={
|
||||
'flex w-full cursor-pointer items-center justify-between rounded-lg py-2 hover:bg-fill-list-hover'
|
||||
}
|
||||
>
|
||||
<span className={'pl-2'}>{t('grid.field.numberFormat')}</span>
|
||||
<span className={'pr-2'}>
|
||||
<i className={'block h-5 w-5'}>
|
||||
<MoreSvg></MoreSvg>
|
||||
</i>
|
||||
</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<div className={'-mx-2 h-[1px] bg-line-divider'}></div>
|
||||
|
||||
{cellIdentifier.fieldType === FieldType.DateTime && fieldController && (
|
||||
<DateTypeOptions cellIdentifier={cellIdentifier} fieldController={fieldController}></DateTypeOptions>
|
||||
)}
|
||||
<div className={'grid grid-cols-2'}>
|
||||
<div className={'flex flex-col gap-2'}>
|
||||
<div
|
||||
onClick={toggleHideProperty}
|
||||
className={'flex cursor-pointer items-center gap-2 rounded-lg p-2 pr-8 hover:bg-fill-list-hover'}
|
||||
>
|
||||
{fieldsStore[cellIdentifier.fieldId]?.visible ? (
|
||||
<>
|
||||
<i className={'block h-5 w-5'}>
|
||||
<EyeClosedSvg />
|
||||
</i>
|
||||
<span>{t('grid.field.hide')}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<i className={'block h-5 w-5'}>
|
||||
<EyeOpenSvg />
|
||||
</i>
|
||||
<span>Show</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
onClick={() => onDuplicatePropertyClick()}
|
||||
className={'flex cursor-pointer items-center gap-2 rounded-lg p-2 pr-8 hover:bg-fill-list-hover'}
|
||||
>
|
||||
<i className={'block h-5 w-5'}>
|
||||
<CopySvg />
|
||||
</i>
|
||||
<span>{t('grid.field.duplicate')}</span>
|
||||
</div>
|
||||
<div
|
||||
onClick={() => {
|
||||
onOutsideClick();
|
||||
onDeletePropertyClick(cellIdentifier.fieldId);
|
||||
}}
|
||||
className={'flex cursor-pointer items-center gap-2 rounded-lg p-2 pr-8 hover:bg-fill-list-hover'}
|
||||
>
|
||||
<i className={'block h-5 w-5'}>
|
||||
<TrashSvg />
|
||||
</i>
|
||||
<span>{t('grid.field.delete')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={'flex flex-col gap-2'}>
|
||||
<div
|
||||
onClick={onAddToLeftClick}
|
||||
className={'flex cursor-pointer items-center gap-2 rounded-lg p-2 pr-8 hover:bg-fill-list-hover'}
|
||||
>
|
||||
<i className={'block h-5 w-5'}>
|
||||
<SkipLeftSvg />
|
||||
</i>
|
||||
<span>{t('grid.field.insertLeft')}</span>
|
||||
</div>
|
||||
<div
|
||||
onClick={onAddToRightClick}
|
||||
className={'flex cursor-pointer items-center gap-2 rounded-lg p-2 pr-8 hover:bg-fill-list-hover'}
|
||||
>
|
||||
<i className={'block h-5 w-5'}>
|
||||
<SkipRightSvg />
|
||||
</i>
|
||||
<span>{t('grid.field.insertRight')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PopupWindow>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
@ -35,19 +35,17 @@ export const EditRow = ({
|
||||
controller: DatabaseController;
|
||||
rowInfo: RowInfo;
|
||||
}) => {
|
||||
const databaseStore = useAppSelector((state) => state.database);
|
||||
const fieldsStore = useAppSelector((state) => state.database.fields);
|
||||
const { cells, onNewColumnClick } = useRow(viewId, controller, rowInfo);
|
||||
const { t } = useTranslation();
|
||||
const [unveil, setUnveil] = useState(false);
|
||||
|
||||
const [editingCell, setEditingCell] = useState<CellIdentifier | null>(null);
|
||||
const [editFieldAnchorEl, setEditFieldAnchorEl] = useState<HTMLDivElement | null>(null);
|
||||
const [showFieldEditor, setShowFieldEditor] = useState(false);
|
||||
const [editFieldTop, setEditFieldTop] = useState(0);
|
||||
const [editFieldLeft, setEditFieldLeft] = useState(0);
|
||||
|
||||
const [showChangeFieldTypePopup, setShowChangeFieldTypePopup] = useState(false);
|
||||
const [changeFieldTypeTop, setChangeFieldTypeTop] = useState(0);
|
||||
const [changeFieldTypeLeft, setChangeFieldTypeLeft] = useState(0);
|
||||
const [changeFieldTypeAnchorEl, setChangeFieldTypeAnchorEl] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
const [showChangeOptionsPopup, setShowChangeOptionsPopup] = useState(false);
|
||||
const [changeOptionsTop, setChangeOptionsTop] = useState(0);
|
||||
@ -89,22 +87,18 @@ export const EditRow = ({
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const onEditFieldClick = (cellIdentifier: CellIdentifier, left: number, top: number) => {
|
||||
const onEditFieldClick = (cellIdentifier: CellIdentifier, anchorEl: HTMLDivElement) => {
|
||||
setEditFieldAnchorEl(anchorEl);
|
||||
setEditingCell(cellIdentifier);
|
||||
setEditFieldTop(top);
|
||||
setEditFieldLeft(left + 10);
|
||||
setShowFieldEditor(true);
|
||||
};
|
||||
|
||||
const onOutsideEditFieldClick = () => {
|
||||
if (!showChangeFieldTypePopup) {
|
||||
setShowFieldEditor(false);
|
||||
}
|
||||
setShowFieldEditor(false);
|
||||
};
|
||||
|
||||
const onChangeFieldTypeClick = (buttonTop: number, buttonRight: number) => {
|
||||
setChangeFieldTypeTop(buttonTop);
|
||||
setChangeFieldTypeLeft(buttonRight + 30);
|
||||
const onChangeFieldTypeClick = (el: HTMLDivElement) => {
|
||||
setChangeFieldTypeAnchorEl(el);
|
||||
setShowChangeFieldTypePopup(true);
|
||||
};
|
||||
|
||||
@ -152,12 +146,6 @@ export const EditRow = ({
|
||||
setEditCheckListTop(_top);
|
||||
};
|
||||
|
||||
const onNumberFormat = (_left: number, _top: number) => {
|
||||
setShowNumberFormatPopup(true);
|
||||
setNumberFormatLeft(_left + 10);
|
||||
setNumberFormatTop(_top);
|
||||
};
|
||||
|
||||
const onEditCheckListClick = (cellIdentifier: CellIdentifier, left: number, top: number) => {
|
||||
setEditingCell(cellIdentifier);
|
||||
setShowCheckListPopup(true);
|
||||
@ -186,6 +174,8 @@ export const EditRow = ({
|
||||
if (!fieldInfo) return;
|
||||
const typeController = new TypeOptionController(viewId, Some(fieldInfo));
|
||||
|
||||
setEditingCell(null);
|
||||
|
||||
await typeController.initialize();
|
||||
await typeController.deleteField();
|
||||
setShowDeletePropertyPrompt(false);
|
||||
@ -228,13 +218,13 @@ export const EditRow = ({
|
||||
<div
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
className={`flex flex-1 flex-col gap-8 px-8 ${
|
||||
className={`flex flex-1 flex-col gap-8 px-8 pb-8 ${
|
||||
showFieldEditor || showChangeOptionsPopup || showDatePicker ? 'overflow-hidden' : 'overflow-auto'
|
||||
}`}
|
||||
>
|
||||
{cells
|
||||
.filter((cell) => {
|
||||
return databaseStore.fields[cell.cellIdentifier.fieldId]?.visible;
|
||||
return fieldsStore[cell.cellIdentifier.fieldId]?.visible;
|
||||
})
|
||||
.map((cell, cellIndex) => (
|
||||
<EditCellWrapper
|
||||
@ -275,23 +265,22 @@ export const EditRow = ({
|
||||
></PropertiesPanel>
|
||||
</div>
|
||||
|
||||
{showFieldEditor && editingCell && (
|
||||
{editingCell && (
|
||||
<EditFieldPopup
|
||||
top={editFieldTop}
|
||||
left={editFieldLeft}
|
||||
open={showFieldEditor}
|
||||
anchorEl={editFieldAnchorEl}
|
||||
cellIdentifier={editingCell}
|
||||
viewId={viewId}
|
||||
onOutsideClick={onOutsideEditFieldClick}
|
||||
fieldInfo={controller.fieldController.getField(editingCell.fieldId)}
|
||||
fieldController={controller.fieldController}
|
||||
controller={controller}
|
||||
changeFieldTypeClick={onChangeFieldTypeClick}
|
||||
onNumberFormat={onNumberFormat}
|
||||
onDeletePropertyClick={onDeletePropertyClick}
|
||||
></EditFieldPopup>
|
||||
)}
|
||||
{showChangeFieldTypePopup && (
|
||||
<ChangeFieldTypePopup
|
||||
top={changeFieldTypeTop}
|
||||
left={changeFieldTypeLeft}
|
||||
open={showChangeFieldTypePopup}
|
||||
anchorEl={changeFieldTypeAnchorEl}
|
||||
onClick={(newType) => changeFieldType(newType)}
|
||||
onOutsideClick={() => setShowChangeFieldTypePopup(false)}
|
||||
></ChangeFieldTypePopup>
|
||||
@ -323,6 +312,7 @@ export const EditRow = ({
|
||||
left={editCellOptionLeft}
|
||||
cellIdentifier={editingCell}
|
||||
editingSelectOption={editingSelectOption}
|
||||
setEditingSelectOption={setEditingSelectOption}
|
||||
onOutsideClick={() => {
|
||||
setShowEditCellOption(false);
|
||||
}}
|
||||
|
@ -1,28 +1,21 @@
|
||||
import { CellController } from '$app/stores/effects/database/cell/cell_controller';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export const EditCellNumber = ({
|
||||
data,
|
||||
cellController,
|
||||
}: {
|
||||
data: string | undefined;
|
||||
cellController: CellController<any, any>;
|
||||
}) => {
|
||||
export const EditCellNumber = ({ data, onSave }: { data: string | undefined; onSave: (value: string) => void }) => {
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setValue(data ?? '');
|
||||
}, [data]);
|
||||
|
||||
const save = async () => {
|
||||
await cellController?.saveCellData(value);
|
||||
};
|
||||
// const save = async () => {
|
||||
// await cellController?.saveCellData(value);
|
||||
// };
|
||||
|
||||
return (
|
||||
<input
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onBlur={() => save()}
|
||||
onBlur={() => onSave(value)}
|
||||
className={'w-full px-4 py-1'}
|
||||
></input>
|
||||
);
|
||||
|
@ -1,13 +1,6 @@
|
||||
import { CellController } from '$app/stores/effects/database/cell/cell_controller';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export const EditCellText = ({
|
||||
data,
|
||||
cellController,
|
||||
}: {
|
||||
data: string | undefined;
|
||||
cellController: CellController<any, any>;
|
||||
}) => {
|
||||
export const EditCellText = ({ data, onSave }: { data: string | undefined; onSave: (value: string) => void }) => {
|
||||
const [value, setValue] = useState('');
|
||||
const [contentRows, setContentRows] = useState(1);
|
||||
|
||||
@ -24,10 +17,6 @@ export const EditCellText = ({
|
||||
setValue(v);
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
await cellController?.saveCellData(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<textarea
|
||||
@ -35,7 +24,7 @@ export const EditCellText = ({
|
||||
rows={contentRows}
|
||||
value={value}
|
||||
onChange={(e) => onTextFieldChange(e.target.value)}
|
||||
onBlur={() => save()}
|
||||
onBlur={() => onSave(value)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,30 +1,18 @@
|
||||
import { URLCellDataPB } from '@/services/backend';
|
||||
import { CellController } from '$app/stores/effects/database/cell/cell_controller';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { URLCellController } from '$app/stores/effects/database/cell/controller_builder';
|
||||
|
||||
export const EditCellUrl = ({
|
||||
data,
|
||||
cellController,
|
||||
}: {
|
||||
data: URLCellDataPB | undefined;
|
||||
cellController: CellController<any, any>;
|
||||
}) => {
|
||||
export const EditCellUrl = ({ data, onSave }: { data: URLCellDataPB | undefined; onSave: (value: string) => void }) => {
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setValue((data as URLCellDataPB)?.url ?? '');
|
||||
}, [data]);
|
||||
|
||||
const save = async () => {
|
||||
await (cellController as URLCellController)?.saveCellData(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<input
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onBlur={() => save()}
|
||||
onBlur={() => onSave(value)}
|
||||
className={'w-full px-4 py-1'}
|
||||
></input>
|
||||
);
|
||||
|
@ -2,23 +2,17 @@ import { EditorCheckSvg } from '$app/components/_shared/svg/EditorCheckSvg';
|
||||
import { EditorUncheckSvg } from '$app/components/_shared/svg/EditorUncheckSvg';
|
||||
import { CheckboxCellController } from '$app/stores/effects/database/cell/controller_builder';
|
||||
|
||||
export const EditCheckboxCell = ({
|
||||
data,
|
||||
cellController,
|
||||
}: {
|
||||
data: 'Yes' | 'No' | undefined;
|
||||
cellController: CheckboxCellController;
|
||||
}) => {
|
||||
const toggleValue = async () => {
|
||||
if (data === 'Yes') {
|
||||
await cellController?.saveCellData('No');
|
||||
} else {
|
||||
await cellController?.saveCellData('Yes');
|
||||
}
|
||||
};
|
||||
export const EditCheckboxCell = ({ data, onToggle }: { data: 'Yes' | 'No' | undefined; onToggle: () => void }) => {
|
||||
// const toggleValue = async () => {
|
||||
// if (data === 'Yes') {
|
||||
// await cellController?.saveCellData('No');
|
||||
// } else {
|
||||
// await cellController?.saveCellData('Yes');
|
||||
// }
|
||||
// };
|
||||
|
||||
return (
|
||||
<div onClick={() => toggleValue()} className={'block px-4 py-1'}>
|
||||
<div onClick={() => onToggle()} className={'block px-4 py-1'}>
|
||||
<button className={'h-5 w-5'}>
|
||||
{data === 'Yes' ? <EditorCheckSvg></EditorCheckSvg> : <EditorUncheckSvg></EditorUncheckSvg>}
|
||||
</button>
|
||||
|
@ -13,12 +13,18 @@ export const CellOption = ({
|
||||
cellIdentifier,
|
||||
openOptionDetail,
|
||||
clearValue,
|
||||
noSelect,
|
||||
noDetail,
|
||||
onOptionClick,
|
||||
}: {
|
||||
option: ISelectOption;
|
||||
checked: boolean;
|
||||
cellIdentifier: CellIdentifier;
|
||||
openOptionDetail: (_left: number, _top: number, _select_option: SelectOptionPB) => void;
|
||||
clearValue: () => void;
|
||||
cellIdentifier?: CellIdentifier;
|
||||
openOptionDetail?: (_left: number, _top: number, _select_option: SelectOptionPB) => void;
|
||||
clearValue?: () => void;
|
||||
noSelect?: boolean;
|
||||
noDetail?: boolean;
|
||||
onOptionClick?: () => void;
|
||||
}) => {
|
||||
const onOptionDetailClick: MouseEventHandler = (e) => {
|
||||
e.stopPropagation();
|
||||
@ -37,17 +43,19 @@ export const CellOption = ({
|
||||
|
||||
const { right: _left, top: _top } = target.getBoundingClientRect();
|
||||
|
||||
openOptionDetail(_left, _top, selectOption);
|
||||
openOptionDetail?.(_left, _top, selectOption);
|
||||
};
|
||||
|
||||
const onToggleOptionClick: MouseEventHandler = async () => {
|
||||
onOptionClick?.();
|
||||
if (noSelect || !cellIdentifier) return;
|
||||
if (checked) {
|
||||
await new SelectOptionCellBackendService(cellIdentifier).unselectOption([option.selectOptionId]);
|
||||
} else {
|
||||
await new SelectOptionCellBackendService(cellIdentifier).selectOption([option.selectOptionId]);
|
||||
}
|
||||
|
||||
clearValue();
|
||||
clearValue?.();
|
||||
};
|
||||
|
||||
return (
|
||||
@ -62,9 +70,11 @@ export const CellOption = ({
|
||||
<CheckmarkSvg></CheckmarkSvg>
|
||||
</button>
|
||||
)}
|
||||
<button onClick={onOptionDetailClick} className={'h-6 w-6 p-1'}>
|
||||
<Details2Svg></Details2Svg>
|
||||
</button>
|
||||
{!noDetail && (
|
||||
<button onClick={onOptionDetailClick} className={'h-6 w-6 p-1'}>
|
||||
<Details2Svg></Details2Svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -43,7 +43,7 @@ export const CellOptionsPopup = ({
|
||||
|
||||
const onKeyDown: KeyboardEventHandler = async (e) => {
|
||||
if (e.key === 'Enter' && value.length > 0) {
|
||||
await new SelectOptionCellBackendService(cellIdentifier).createOption({ name: value });
|
||||
await new SelectOptionCellBackendService(cellIdentifier).createOption({ name: value, isSelect: true });
|
||||
setValue('');
|
||||
}
|
||||
};
|
||||
@ -82,7 +82,7 @@ export const CellOptionsPopup = ({
|
||||
/>
|
||||
<div className={'font-mono text-text-caption'}>{value.length}/30</div>
|
||||
</div>
|
||||
<div className={'-mx-4 h-[1px] bg-line-border'}></div>
|
||||
<div className={'-mx-4 h-[1px] bg-line-divider'}></div>
|
||||
<div className={'font-medium text-text-caption'}>{t('grid.selectOption.panelTitle') ?? ''}</div>
|
||||
<div className={'flex flex-col gap-1'}>
|
||||
{(databaseStore.fields[cellIdentifier.fieldId]?.fieldOptions as ISelectOptionType).selectOptions.map(
|
||||
|
@ -7,23 +7,29 @@ import { SelectOptionCellBackendService } from '$app/stores/effects/database/cel
|
||||
import { TrashSvg } from '$app/components/_shared/svg/TrashSvg';
|
||||
import { CheckmarkSvg } from '$app/components/_shared/svg/CheckmarkSvg';
|
||||
import { PopupWindow } from '$app/components/_shared/PopupWindow';
|
||||
import { databaseActions, ISelectOptionType } from '$app_reducers/database/slice';
|
||||
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||
|
||||
export const EditCellOptionPopup = ({
|
||||
left,
|
||||
top,
|
||||
cellIdentifier,
|
||||
editingSelectOption,
|
||||
setEditingSelectOption,
|
||||
onOutsideClick,
|
||||
}: {
|
||||
left: number;
|
||||
top: number;
|
||||
cellIdentifier: CellIdentifier;
|
||||
editingSelectOption: SelectOptionPB;
|
||||
setEditingSelectOption: (option: SelectOptionPB) => void;
|
||||
onOutsideClick: () => void;
|
||||
}) => {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const { t } = useTranslation();
|
||||
const [value, setValue] = useState('');
|
||||
const fieldsStore = useAppSelector((state) => state.database.fields);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
setValue(editingSelectOption.name);
|
||||
@ -54,16 +60,43 @@ export const EditCellOptionPopup = ({
|
||||
);
|
||||
};
|
||||
|
||||
const onUpdateSelectOption = (option: SelectOptionPB) => {
|
||||
const updatingField = fieldsStore[cellIdentifier.fieldId];
|
||||
const allOptions = (updatingField.fieldOptions as ISelectOptionType).selectOptions;
|
||||
|
||||
dispatch(
|
||||
databaseActions.updateField({
|
||||
field: {
|
||||
...updatingField,
|
||||
fieldOptions: {
|
||||
selectOptions: allOptions.map((o) =>
|
||||
o.selectOptionId === option.id
|
||||
? {
|
||||
selectOptionId: option.id,
|
||||
color: option.color,
|
||||
title: option.name,
|
||||
}
|
||||
: o
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
setEditingSelectOption(option);
|
||||
};
|
||||
|
||||
const onColorClick = async (color: SelectOptionColorPB) => {
|
||||
const svc = new SelectOptionCellBackendService(cellIdentifier);
|
||||
|
||||
await svc.updateOption(
|
||||
new SelectOptionPB({
|
||||
id: editingSelectOption.id,
|
||||
color,
|
||||
name: editingSelectOption.name,
|
||||
})
|
||||
);
|
||||
const updatedOption = new SelectOptionPB({
|
||||
id: editingSelectOption.id,
|
||||
color,
|
||||
name: editingSelectOption.name,
|
||||
});
|
||||
|
||||
await svc.updateOption(updatedOption);
|
||||
onUpdateSelectOption(updatedOption);
|
||||
};
|
||||
|
||||
const onDeleteOptionClick = async () => {
|
||||
@ -97,7 +130,7 @@ export const EditCellOptionPopup = ({
|
||||
onKeyDown={onKeyDown}
|
||||
onBlur={() => onBlur()}
|
||||
/>
|
||||
<div className={'text-shade-3 font-mono'}>{value.length}/30</div>
|
||||
<div className={'font-mono text-text-caption'}>{value.length}/30</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onDeleteOptionClick()}
|
||||
@ -110,8 +143,8 @@ export const EditCellOptionPopup = ({
|
||||
</i>
|
||||
<span>{t('grid.selectOption.deleteTag')}</span>
|
||||
</button>
|
||||
<div className={'bg-shade-6 -mx-4 h-[1px]'}></div>
|
||||
<div className={'text-shade-3 my-2 font-medium'}>{t('grid.selectOption.colorPanelTitle')}</div>
|
||||
<div className={'-mx-4 h-[1px] bg-line-divider'}></div>
|
||||
<div className={'my-2 font-medium text-text-caption'}>{t('grid.selectOption.colorPanelTitle')}</div>
|
||||
<div className={'flex flex-col'}>
|
||||
<ColorItem
|
||||
title={t('grid.selectOption.purpleColor')}
|
||||
|
@ -0,0 +1,92 @@
|
||||
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
|
||||
import { ISelectOptionType } from '$app_reducers/database/slice';
|
||||
import { CellOption } from '$app/components/_shared/EditRow/Options/CellOption';
|
||||
import { SelectOptionPB } from '@/services/backend';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
import { KeyboardEventHandler, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SelectOptionCellBackendService } from '$app/stores/effects/database/cell/select_option_bd_svc';
|
||||
|
||||
export const MultiSelectTypeOptions = ({
|
||||
cellIdentifier,
|
||||
openOptionDetail,
|
||||
}: {
|
||||
cellIdentifier: CellIdentifier;
|
||||
openOptionDetail?: (_left: number, _top: number, _select_option: SelectOptionPB) => void;
|
||||
}) => {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const inputContainerRef = useRef<HTMLDivElement>(null);
|
||||
const { t } = useTranslation();
|
||||
const fieldsStore = useAppSelector((state) => state.database.fields);
|
||||
const [value, setValue] = useState('');
|
||||
const [showInput, setShowInput] = useState(false);
|
||||
const [newInputWidth, setNewInputWidth] = useState(0);
|
||||
|
||||
const onKeyDown: KeyboardEventHandler = async (e) => {
|
||||
if (e.key === 'Enter' && value.length > 0) {
|
||||
await new SelectOptionCellBackendService(cellIdentifier).createOption({ name: value, isSelect: false });
|
||||
setValue('');
|
||||
}
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
setShowInput(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef?.current && showInput) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [inputRef, showInput, newInputWidth]);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputContainerRef?.current && showInput) {
|
||||
setNewInputWidth(inputContainerRef.current.getBoundingClientRect().width - 56);
|
||||
} else {
|
||||
setNewInputWidth(0);
|
||||
}
|
||||
}, [inputContainerRef, showInput]);
|
||||
|
||||
return (
|
||||
<div className={'flex flex-col'}>
|
||||
<hr className={'-mx-2 my-2 border-line-divider'} />
|
||||
<div className={'flex flex-col gap-1'}>
|
||||
<div className={'flex items-center justify-between px-3 py-1.5'}>
|
||||
<div>Options</div>
|
||||
{!showInput && <button onClick={() => setShowInput(true)}>Add option</button>}
|
||||
</div>
|
||||
{showInput && (
|
||||
<div
|
||||
ref={inputContainerRef}
|
||||
className={`border-shades-3 bg-main-selector flex items-center gap-2 rounded border px-2`}
|
||||
>
|
||||
{newInputWidth > 0 && (
|
||||
<input
|
||||
ref={inputRef}
|
||||
style={{ width: newInputWidth }}
|
||||
className={'py-2'}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onBlur={() => setShowInput(false)}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
)}
|
||||
<div className={'font-mono text-text-caption'}>{value.length}/30</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(fieldsStore[cellIdentifier.fieldId]?.fieldOptions as ISelectOptionType).selectOptions.map((option, index) => (
|
||||
<CellOption
|
||||
key={index}
|
||||
option={option}
|
||||
noSelect={true}
|
||||
checked={false}
|
||||
cellIdentifier={cellIdentifier}
|
||||
openOptionDetail={openOptionDetail}
|
||||
clearValue={() => setValue('')}
|
||||
></CellOption>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -21,8 +21,8 @@ export const SelectedOption = ({
|
||||
|
||||
return (
|
||||
<div className={`${getBgColor(option.color)} flex items-center gap-0.5 rounded px-1 py-0.5 text-content-on-fill`}>
|
||||
<span>{option?.name ?? ''}</span>
|
||||
<button onClick={onUnselectOptionClick} className={'h-5 w-5 cursor-pointer'}>
|
||||
<span className={'text-text-title'}>{option?.name ?? ''}</span>
|
||||
<button onClick={onUnselectOptionClick} className={'h-5 w-5 cursor-pointer text-text-title'}>
|
||||
<CloseSvg></CloseSvg>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { MouseEvent, ReactNode, useRef } from 'react';
|
||||
import { CSSProperties, MouseEvent, ReactNode, useRef } from 'react';
|
||||
import useOutsideClick from './useOutsideClick';
|
||||
|
||||
export interface IPopupItem {
|
||||
@ -18,7 +18,7 @@ export const PopupSelect = ({
|
||||
className: string;
|
||||
onOutsideClick?: () => void;
|
||||
columns?: 1 | 2 | 3;
|
||||
style?: any;
|
||||
style?: CSSProperties;
|
||||
}) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ReactNode, useEffect, useRef, useState } from 'react';
|
||||
import { CSSProperties, ReactNode, useEffect, useRef, useState } from 'react';
|
||||
import useOutsideClick from '$app/components/_shared/useOutsideClick';
|
||||
|
||||
export const PopupWindow = ({
|
||||
@ -7,12 +7,14 @@ export const PopupWindow = ({
|
||||
onOutsideClick,
|
||||
left,
|
||||
top,
|
||||
style,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
onOutsideClick: () => void;
|
||||
left: number;
|
||||
top: number;
|
||||
style?: CSSProperties;
|
||||
}) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
@ -20,22 +22,30 @@ export const PopupWindow = ({
|
||||
|
||||
const [adjustedTop, setAdjustedTop] = useState(-100);
|
||||
const [adjustedLeft, setAdjustedLeft] = useState(-100);
|
||||
const [stickToBottom, setStickToBottom] = useState(false);
|
||||
const [stickToRight, setStickToRight] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current) return;
|
||||
const { height, width } = ref.current.getBoundingClientRect();
|
||||
|
||||
if (top + height > window.innerHeight) {
|
||||
setAdjustedTop(window.innerHeight - height);
|
||||
} else {
|
||||
new ResizeObserver(() => {
|
||||
if (!ref.current) return;
|
||||
const { height, width } = ref.current.getBoundingClientRect();
|
||||
|
||||
setAdjustedTop(top);
|
||||
}
|
||||
if (top + height > window.innerHeight) {
|
||||
setStickToBottom(true);
|
||||
} else {
|
||||
setStickToBottom(false);
|
||||
}
|
||||
|
||||
if (left + width > window.innerWidth) {
|
||||
setAdjustedLeft(window.innerWidth - width);
|
||||
} else {
|
||||
setAdjustedLeft(left);
|
||||
}
|
||||
if (left + width > window.innerWidth) {
|
||||
setStickToRight(true);
|
||||
} else {
|
||||
setStickToRight(false);
|
||||
}
|
||||
}).observe(ref.current);
|
||||
}, [ref, left, top]);
|
||||
|
||||
return (
|
||||
@ -46,7 +56,11 @@ export const PopupWindow = ({
|
||||
(adjustedTop === -100 && adjustedLeft === -100 ? 'opacity-0 ' : 'opacity-100 ') +
|
||||
(className ?? '')
|
||||
}
|
||||
style={{ top: `${adjustedTop}px`, left: `${adjustedLeft}px` }}
|
||||
style={{
|
||||
[stickToBottom ? 'bottom' : 'top']: `${stickToBottom ? '0' : adjustedTop}px`,
|
||||
[stickToRight ? 'right' : 'left']: `${stickToRight ? '0' : adjustedLeft}px`,
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
@ -54,6 +54,7 @@ export default async function (viewId: string, fieldInfo: FieldInfo, dispatch?:
|
||||
title: field.name,
|
||||
fieldType: field.field_type,
|
||||
visible: field.visibility,
|
||||
width: field.width,
|
||||
fieldOptions: {
|
||||
selectOptions,
|
||||
},
|
||||
@ -66,6 +67,7 @@ export default async function (viewId: string, fieldInfo: FieldInfo, dispatch?:
|
||||
fieldId: field.id,
|
||||
title: field.name,
|
||||
visible: field.visibility,
|
||||
width: field.width,
|
||||
fieldType: field.field_type,
|
||||
fieldOptions: {
|
||||
numberFormat: typeOption.format,
|
||||
@ -79,6 +81,7 @@ export default async function (viewId: string, fieldInfo: FieldInfo, dispatch?:
|
||||
fieldId: field.id,
|
||||
title: field.name,
|
||||
visible: field.visibility,
|
||||
width: field.width,
|
||||
fieldType: field.field_type,
|
||||
fieldOptions: {
|
||||
dateFormat: typeOption.date_format,
|
||||
@ -92,6 +95,7 @@ export default async function (viewId: string, fieldInfo: FieldInfo, dispatch?:
|
||||
fieldId: field.id,
|
||||
title: field.name,
|
||||
visible: field.visibility,
|
||||
width: field.width,
|
||||
fieldType: field.field_type,
|
||||
};
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ export const useCell = (cellIdentifier: CellIdentifier, cellCache: CellCache, fi
|
||||
// update redux store for database field if there are new select options
|
||||
if (
|
||||
value instanceof SelectOptionCellDataPB &&
|
||||
databaseStore.fields[cellIdentifier.fieldId] &&
|
||||
(databaseStore.fields[cellIdentifier.fieldId].fieldOptions as ISelectOptionType).selectOptions.length !==
|
||||
value.options.length
|
||||
) {
|
||||
|
@ -1,11 +1,24 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { DatabaseController } from '$app/stores/effects/database/database_controller';
|
||||
import { databaseActions, DatabaseFieldMap, IDatabaseColumn } from '$app/stores/reducers/database/slice';
|
||||
import {
|
||||
databaseActions,
|
||||
DatabaseFieldMap,
|
||||
IDatabaseColumn,
|
||||
IDatabaseFilter,
|
||||
TDatabaseOperators,
|
||||
} from '$app/stores/reducers/database/slice';
|
||||
import { useAppDispatch } from '$app/stores/store';
|
||||
import loadField from './loadField';
|
||||
import { FieldInfo } from '$app/stores/effects/database/field/field_controller';
|
||||
import { RowInfo } from '$app/stores/effects/database/row/row_cache';
|
||||
import { ViewLayoutPB } from '@/services/backend';
|
||||
import {
|
||||
FieldType,
|
||||
SelectOptionConditionPB,
|
||||
SelectOptionFilterPB,
|
||||
TextFilterConditionPB,
|
||||
TextFilterPB,
|
||||
ViewLayoutPB,
|
||||
} from '@/services/backend';
|
||||
import { DatabaseGroupController } from '$app/stores/effects/database/group/group_controller';
|
||||
import { OnDragEndResponder } from 'react-beautiful-dnd';
|
||||
import { AsyncQueue } from '$app/utils/async_queue';
|
||||
@ -20,6 +33,7 @@ export const useDatabase = (viewId: string, type?: ViewLayoutPB) => {
|
||||
useEffect(() => {
|
||||
if (!viewId.length) return;
|
||||
const c = new DatabaseController(viewId);
|
||||
|
||||
setController(c);
|
||||
|
||||
return () => void c.dispose();
|
||||
@ -29,8 +43,10 @@ export const useDatabase = (viewId: string, type?: ViewLayoutPB) => {
|
||||
async (fieldInfos: readonly FieldInfo[]) => {
|
||||
const fields: DatabaseFieldMap = {};
|
||||
const columns: IDatabaseColumn[] = [];
|
||||
|
||||
for (const fieldInfo of fieldInfos) {
|
||||
const fieldPB = fieldInfo.field;
|
||||
|
||||
columns.push({
|
||||
fieldId: fieldPB.id,
|
||||
sort: 'none',
|
||||
@ -38,8 +54,10 @@ export const useDatabase = (viewId: string, type?: ViewLayoutPB) => {
|
||||
});
|
||||
|
||||
const field = await loadField(viewId, fieldInfo, dispatch);
|
||||
|
||||
fields[field.fieldId] = field;
|
||||
}
|
||||
|
||||
dispatch(databaseActions.updateFields({ fields }));
|
||||
dispatch(databaseActions.updateColumns({ columns }));
|
||||
},
|
||||
@ -50,6 +68,50 @@ export const useDatabase = (viewId: string, type?: ViewLayoutPB) => {
|
||||
return new AsyncQueue<readonly FieldInfo[]>(loadFields);
|
||||
}, [loadFields]);
|
||||
|
||||
const transformCondition: (condition: number, fieldType: FieldType) => TDatabaseOperators = (condition, fieldType) => {
|
||||
switch (fieldType) {
|
||||
case FieldType.RichText:
|
||||
switch (condition) {
|
||||
case TextFilterConditionPB.Contains:
|
||||
return 'contains';
|
||||
case TextFilterConditionPB.DoesNotContain:
|
||||
return 'doesNotContain';
|
||||
case TextFilterConditionPB.EndsWith:
|
||||
return 'endsWith';
|
||||
case TextFilterConditionPB.StartsWith:
|
||||
return 'startWith';
|
||||
case TextFilterConditionPB.Is:
|
||||
return 'is';
|
||||
case TextFilterConditionPB.IsNot:
|
||||
return 'isNot';
|
||||
case TextFilterConditionPB.TextIsEmpty:
|
||||
return 'isEmpty';
|
||||
case TextFilterConditionPB.TextIsNotEmpty:
|
||||
return 'isNotEmpty';
|
||||
default:
|
||||
return 'is';
|
||||
}
|
||||
|
||||
case FieldType.SingleSelect:
|
||||
case FieldType.MultiSelect:
|
||||
switch (condition) {
|
||||
case SelectOptionConditionPB.OptionIs:
|
||||
return 'is';
|
||||
case SelectOptionConditionPB.OptionIsNot:
|
||||
return 'isNot';
|
||||
case SelectOptionConditionPB.OptionIsEmpty:
|
||||
return 'isEmpty';
|
||||
case SelectOptionConditionPB.OptionIsNotEmpty:
|
||||
return 'isNotEmpty';
|
||||
default:
|
||||
return 'is';
|
||||
}
|
||||
|
||||
default:
|
||||
return 'is';
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
void (async () => {
|
||||
if (!controller) return;
|
||||
@ -61,9 +123,50 @@ export const useDatabase = (viewId: string, type?: ViewLayoutPB) => {
|
||||
onFieldsChanged: (fieldInfos) => {
|
||||
queue.enqueue(fieldInfos);
|
||||
},
|
||||
onFiltersChanged: (filters) => {
|
||||
const reduxFilters = filters.map<IDatabaseFilter>((filter) => {
|
||||
switch (filter.field_type) {
|
||||
case FieldType.SingleSelect:
|
||||
case FieldType.MultiSelect:
|
||||
return {
|
||||
logicalOperator: 'and',
|
||||
fieldType: filter.field_type,
|
||||
fieldId: filter.field_id,
|
||||
id: filter.id,
|
||||
operator: transformCondition((filter.data as SelectOptionFilterPB).condition, filter.field_type),
|
||||
value: (filter.data as SelectOptionFilterPB).option_ids,
|
||||
};
|
||||
case FieldType.RichText:
|
||||
return {
|
||||
logicalOperator: 'and',
|
||||
fieldType: filter.field_type,
|
||||
fieldId: filter.field_id,
|
||||
id: filter.id,
|
||||
operator: transformCondition((filter.data as TextFilterPB).condition, filter.field_type),
|
||||
value: (filter.data as TextFilterPB).content,
|
||||
};
|
||||
|
||||
default:
|
||||
return {
|
||||
logicalOperator: 'and',
|
||||
fieldType: filter.field_type,
|
||||
fieldId: filter.field_id,
|
||||
id: filter.id,
|
||||
operator: 'is',
|
||||
value: '',
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
dispatch(databaseActions.updateFilters({ filters: reduxFilters }));
|
||||
},
|
||||
onSortChanged: (sorts) => {
|
||||
dispatch(databaseActions.updateSorts({ sorts: [...sorts] }));
|
||||
},
|
||||
});
|
||||
|
||||
const openResult = await controller.open();
|
||||
|
||||
if (openResult.ok) {
|
||||
setRows(
|
||||
openResult.val.map((pb) => {
|
||||
@ -74,6 +177,7 @@ export const useDatabase = (viewId: string, type?: ViewLayoutPB) => {
|
||||
|
||||
if (type === ViewLayoutPB.Board) {
|
||||
const fieldId = await controller.getGroupByFieldId();
|
||||
|
||||
setGroupByFieldId(fieldId.unwrap());
|
||||
setGroups(controller.groups.value);
|
||||
}
|
||||
@ -88,6 +192,7 @@ export const useDatabase = (viewId: string, type?: ViewLayoutPB) => {
|
||||
if (!groups) return;
|
||||
if (!controller?.groups) return;
|
||||
const group = groups[index];
|
||||
|
||||
await group.createRow();
|
||||
|
||||
setGroups([...controller.groups.value]);
|
||||
@ -97,6 +202,7 @@ export const useDatabase = (viewId: string, type?: ViewLayoutPB) => {
|
||||
if (!controller) return;
|
||||
const { source, destination } = result;
|
||||
const group = groups.find((g) => g.groupId === source.droppableId);
|
||||
|
||||
if (!group) return;
|
||||
|
||||
if (source.droppableId === destination?.droppableId) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { SelectOptionColorPB } from '../../../services/backend';
|
||||
import { SelectOptionColorPB } from '@/services/backend';
|
||||
|
||||
export const getBgColor = (color: SelectOptionColorPB | undefined): string => {
|
||||
switch (color) {
|
||||
|
@ -0,0 +1,12 @@
|
||||
export const DragSvg = () => {
|
||||
return (
|
||||
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<rect x='9' y='3' width='2' height='2' rx='0.5' fill='#333333' />
|
||||
<rect x='5' y='3' width='2' height='2' rx='0.5' fill='#333333' />
|
||||
<rect x='9' y='7' width='2' height='2' rx='0.5' fill='#333333' />
|
||||
<rect x='5' y='7' width='2' height='2' rx='0.5' fill='#333333' />
|
||||
<rect x='9' y='11' width='2' height='2' rx='0.5' fill='#333333' />
|
||||
<rect x='5' y='11' width='2' height='2' rx='0.5' fill='#333333' />
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -0,0 +1,18 @@
|
||||
export const ShareSvg = () => {
|
||||
return (
|
||||
<svg width='100%' height='100%' viewBox='0 0 16 16' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
d='M7 8.91521C7.21574 9.22582 7.49099 9.48283 7.80707 9.66881C8.12315 9.85479 8.47268 9.96538 8.83194 9.99309C9.1912 10.0208 9.5518 9.96497 9.88926 9.8294C10.2267 9.69383 10.5332 9.48169 10.7878 9.20736L12.2949 7.58431C12.7525 7.07413 13.0056 6.39083 12.9999 5.68156C12.9942 4.9723 12.73 4.29384 12.2643 3.7923C11.7986 3.29075 11.1686 3.00627 10.51 3.0001C9.85142 2.99394 9.21693 3.26659 8.7432 3.75935L7.87913 4.68448'
|
||||
stroke='#333333'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
<path
|
||||
d='M9 7.08479C8.78426 6.77418 8.50901 6.51717 8.19293 6.33119C7.87685 6.14521 7.52732 6.03462 7.16806 6.00691C6.8088 5.9792 6.4482 6.03503 6.11074 6.1706C5.77327 6.30617 5.46683 6.51831 5.21218 6.79264L3.7051 8.41569C3.24755 8.92587 2.99437 9.60918 3.00009 10.3184C3.00582 11.0277 3.26998 11.7062 3.73569 12.2077C4.2014 12.7092 4.8314 12.9937 5.48999 12.9999C6.14858 13.0061 6.78307 12.7334 7.2568 12.2407L8.11584 11.3155'
|
||||
stroke='#333333'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -0,0 +1,22 @@
|
||||
export const SortAscSvg = () => {
|
||||
return (
|
||||
<svg width='100%' height='100%' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path d='M11.25 7L17.25 7' stroke='currentColor' strokeWidth='1.5' strokeLinecap='round' strokeLinejoin='round' />
|
||||
<path d='M11.25 12H18.75' stroke='currentColor' strokeWidth='1.5' strokeLinecap='round' strokeLinejoin='round' />
|
||||
<path
|
||||
d='M11.25 17L20.25 17'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
<path
|
||||
d='M6.75 6V18L3.75 15'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -0,0 +1,22 @@
|
||||
export const SortDescSvg = () => {
|
||||
return (
|
||||
<svg width='100%' height='100%' viewBox='0 0 24 24' fill='none' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path d='M11.25 7L20.25 7' stroke='currentColor' strokeWidth='1.5' strokeLinecap='round' strokeLinejoin='round' />
|
||||
<path d='M11.25 12H18.75' stroke='currentColor' strokeWidth='1.5' strokeLinecap='round' strokeLinejoin='round' />
|
||||
<path
|
||||
d='M11.25 17L17.25 17'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
<path
|
||||
d='M6.75 6V18L3.75 15'
|
||||
stroke='currentColor'
|
||||
strokeWidth='1.5'
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -1,16 +1,27 @@
|
||||
import { useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
export const useResizer = () => {
|
||||
export const useResizer = (onEnd?: (final: number) => void) => {
|
||||
const [movementX, setMovementX] = useState(0);
|
||||
const [movementY, setMovementY] = useState(0);
|
||||
const [newSizeX, setNewSizeX] = useState(0);
|
||||
const [newSizeY, setNewSizeY] = useState(0);
|
||||
|
||||
const onMouseDown = () => {
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
setMovementX(e.movementX);
|
||||
setMovementY(e.movementY);
|
||||
const onMouseDown = (e1: React.MouseEvent<HTMLElement>, initial = 0) => {
|
||||
const startX = e1.screenX;
|
||||
const startY = e1.screenY;
|
||||
|
||||
setNewSizeX(initial);
|
||||
setNewSizeY(initial);
|
||||
|
||||
const onMouseMove = (e2: MouseEvent) => {
|
||||
setNewSizeX(initial + e2.screenX - startX);
|
||||
setNewSizeY(initial + e2.screenY - startY);
|
||||
setMovementX(e2.movementX);
|
||||
setMovementY(e2.movementY);
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
const onMouseUp = (e2: MouseEvent) => {
|
||||
onEnd?.(initial + e2.screenX - startX);
|
||||
window.removeEventListener('mousemove', onMouseMove);
|
||||
window.removeEventListener('mouseup', onMouseUp);
|
||||
};
|
||||
@ -22,6 +33,8 @@ export const useResizer = () => {
|
||||
return {
|
||||
movementX,
|
||||
movementY,
|
||||
newSizeX,
|
||||
newSizeY,
|
||||
onMouseDown,
|
||||
};
|
||||
};
|
||||
|
@ -30,12 +30,12 @@ export const ConfirmAccount = () => {
|
||||
numInputs={5}
|
||||
isInputNum={true}
|
||||
separator={<span> </span>}
|
||||
inputStyle='border border-gray-300 rounded-lg h-full !w-14 font-semibold focus:ring-2 focus:ring-main-accent focus:ring-opacity-50'
|
||||
inputStyle='border border-gray-300 rounded-lg h-full !w-14 font-semibold focus:ring-2 focus:ring-fill-hover focus:ring-opacity-50'
|
||||
containerStyle='h-full w-full flex justify-around gap-2 '
|
||||
/>
|
||||
</div>
|
||||
|
||||
<a href='#' className='text-xs text-main-accent hover:text-main-hovered'>
|
||||
<a href='#' className='hover:text-content-hover text-xs text-fill-hover'>
|
||||
<span>Send code again</span>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -104,7 +104,7 @@ export const SignUp = () => {
|
||||
<span className='text-xs text-gray-500'>
|
||||
{t('signUp.alreadyHaveAnAccount')}
|
||||
<Link to={'/auth/login'}>
|
||||
<span className='ml-2 text-main-accent hover:text-main-hovered'>{t('signIn.buttonText')}</span>
|
||||
<span className='hover:text-content-hover ml-2 text-fill-hover'>{t('signIn.buttonText')}</span>
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
|
@ -49,7 +49,7 @@ export const BoardGroup = ({
|
||||
<div className={'flex items-center justify-between p-4'}>
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<span>{group.name}</span>
|
||||
<span className={'text-shade-4'}>({group.rows.length})</span>
|
||||
<span className={'text-text-caption'}>({group.rows.length})</span>
|
||||
</div>
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<button className={'h-5 w-5 rounded hover:bg-fill-list-hover'}>
|
||||
|
@ -17,7 +17,7 @@ export const BoardUrlCell = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<a className={'text-main-accent hover:underline'} href={(data as URLCellDataPB)?.url ?? ''} target={'_blank'}>
|
||||
<a className={'text-fill-hover hover:underline'} href={(data as URLCellDataPB)?.url ?? ''} target={'_blank'}>
|
||||
{(data as URLCellDataPB)?.content ?? ''}
|
||||
</a>
|
||||
</>
|
||||
|
@ -22,7 +22,7 @@ function Editor({
|
||||
return (
|
||||
<div className={'min-h-[30px]'}>
|
||||
<div ref={ref} {...props} />
|
||||
{!editor && <div className={'px-0.5 py-1 text-shade-4'}>{placeholder}</div>}
|
||||
{!editor && <div className={'px-0.5 py-1 text-text-caption'}>{placeholder}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { useDatabase } from '$app/components/_shared/database-hooks/useDatabase';
|
||||
import { GridTableCount } from '../GridTableCount/GridTableCount';
|
||||
import { GridTableHeader } from '../GridTableHeader/GridTableHeader';
|
||||
import { GridAddRow } from '../GridTableRows/GridAddRow';
|
||||
import { GridTableRows } from '../GridTableRows/GridTableRows';
|
||||
import { GridTitle } from '../GridTitle/GridTitle';
|
||||
import { GridToolbar } from '../GridToolbar/GridToolbar';
|
||||
@ -9,35 +8,51 @@ import { EditRow } from '$app/components/_shared/EditRow/EditRow';
|
||||
import { useState } from 'react';
|
||||
import { RowInfo } from '$app/stores/effects/database/row/row_cache';
|
||||
import { ViewLayoutPB } from '@/services/backend';
|
||||
import { DatabaseFilterPopup } from '$app/components/_shared/DatabaseFilter/DatabaseFilterPopup';
|
||||
import { DatabaseSortPopup } from '$app/components/_shared/DatabaseSort/DatabaseSortPopup';
|
||||
|
||||
export const Grid = ({ viewId }: { viewId: string }) => {
|
||||
const { controller, rows, groups } = useDatabase(viewId, ViewLayoutPB.Grid);
|
||||
const [showGridRow, setShowGridRow] = useState(false);
|
||||
const [boardRowInfo, setBoardRowInfo] = useState<RowInfo>();
|
||||
const [showFilterPopup, setShowFilterPopup] = useState(false);
|
||||
const [showSortPopup, setShowSortPopup] = useState(false);
|
||||
|
||||
const onOpenRow = (rowInfo: RowInfo) => {
|
||||
setBoardRowInfo(rowInfo);
|
||||
setShowGridRow(true);
|
||||
};
|
||||
|
||||
const onShowFilterClick = () => {
|
||||
setShowFilterPopup(true);
|
||||
};
|
||||
|
||||
const onShowSortClick = () => {
|
||||
setShowSortPopup(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{controller && groups && (
|
||||
<>
|
||||
<div className='mx-auto mt-8 flex flex-col gap-8 px-8'>
|
||||
<div className='flex flex-1 flex-col gap-4'>
|
||||
<div className='flex w-full items-center justify-between'>
|
||||
<GridTitle />
|
||||
<GridTitle onShowFilterClick={onShowFilterClick} onShowSortClick={onShowSortClick} viewId={viewId} />
|
||||
<GridToolbar />
|
||||
</div>
|
||||
|
||||
{/* table component page with text area for td */}
|
||||
<div className='flex flex-col gap-4'>
|
||||
<table className='w-full table-fixed text-sm'>
|
||||
<GridTableHeader controller={controller} />
|
||||
<GridTableRows onOpenRow={onOpenRow} allRows={rows} viewId={viewId} controller={controller} />
|
||||
</table>
|
||||
|
||||
<GridAddRow controller={controller} />
|
||||
<div className='flex flex-1 flex-col gap-4'>
|
||||
<div className='flex flex-1 flex-col overflow-x-auto'>
|
||||
<GridTableHeader
|
||||
controller={controller}
|
||||
onShowFilterClick={onShowFilterClick}
|
||||
onShowSortClick={onShowSortClick}
|
||||
/>
|
||||
<div className={'relative flex-1'}>
|
||||
<GridTableRows onOpenRow={onOpenRow} allRows={rows} viewId={viewId} controller={controller} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<GridTableCount rows={rows} />
|
||||
@ -52,6 +67,15 @@ export const Grid = ({ viewId }: { viewId: string }) => {
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{showFilterPopup && controller && controller.filterController && (
|
||||
<DatabaseFilterPopup
|
||||
filterController={controller.filterController}
|
||||
onOutsideClick={() => setShowFilterPopup(false)}
|
||||
/>
|
||||
)}
|
||||
{showSortPopup && controller && controller.sortController && (
|
||||
<DatabaseSortPopup sortController={controller.sortController} onOutsideClick={() => setShowSortPopup(false)} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -13,13 +13,15 @@ export const GridCell = ({
|
||||
cellIdentifier,
|
||||
cellCache,
|
||||
fieldController,
|
||||
width,
|
||||
}: {
|
||||
cellIdentifier: CellIdentifier;
|
||||
cellCache: CellCache;
|
||||
fieldController: FieldController;
|
||||
width?: number;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<div style={{ width }}>
|
||||
{cellIdentifier.fieldType === FieldType.MultiSelect ||
|
||||
cellIdentifier.fieldType === FieldType.Checklist ||
|
||||
cellIdentifier.fieldType === FieldType.SingleSelect ? (
|
||||
@ -39,6 +41,6 @@ export const GridCell = ({
|
||||
) : (
|
||||
<GridTextCell cellIdentifier={cellIdentifier} cellCache={cellCache} fieldController={fieldController} />
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -16,8 +16,15 @@ export const GridCheckBox = ({
|
||||
const { data, cellController } = useCell(cellIdentifier, cellCache, fieldController);
|
||||
|
||||
return (
|
||||
<div className='flex w-full justify-start'>
|
||||
{cellController && <EditCheckboxCell cellController={cellController} data={data as 'Yes' | 'No' | undefined} />}
|
||||
</div>
|
||||
<EditCheckboxCell
|
||||
onToggle={async () => {
|
||||
if (data === 'Yes') {
|
||||
await cellController?.saveCellData('No');
|
||||
} else {
|
||||
await cellController?.saveCellData('Yes');
|
||||
}
|
||||
}}
|
||||
data={data as 'Yes' | 'No' | undefined}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -29,7 +29,7 @@ export const GridDate = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='flex w-full cursor-pointer justify-start'>
|
||||
<>
|
||||
{cellController && <EditCellDate data={data as DateCellDataPB} onEditClick={onEditDateClick}></EditCellDate>}
|
||||
|
||||
{showDatePopup && (
|
||||
@ -42,6 +42,6 @@ export const GridDate = ({
|
||||
onOutsideClick={() => setShowDatePopup(false)}
|
||||
></DatePickerPopup>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -16,10 +16,11 @@ export const GridNumberCell = ({
|
||||
const { data, cellController } = useCell(cellIdentifier, cellCache, fieldController);
|
||||
|
||||
return (
|
||||
<div className='w-full'>
|
||||
{cellController && (
|
||||
<EditCellNumber data={data as string | undefined} cellController={cellController}></EditCellNumber>
|
||||
)}
|
||||
</div>
|
||||
<EditCellNumber
|
||||
data={data as string | undefined}
|
||||
onSave={async (value) => {
|
||||
await cellController?.saveCellData(value);
|
||||
}}
|
||||
></EditCellNumber>
|
||||
);
|
||||
};
|
||||
|
@ -44,9 +44,7 @@ export default function GridSingleSelectOptions({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='flex w-full cursor-pointer justify-start'>
|
||||
<CellOptions data={data as SelectOptionCellDataPB} onEditClick={onEditOptionsClick} />
|
||||
</div>
|
||||
<CellOptions data={data as SelectOptionCellDataPB} onEditClick={onEditOptionsClick} />
|
||||
|
||||
{showOptionsPopup && (
|
||||
<CellOptionsPopup
|
||||
@ -55,7 +53,7 @@ export default function GridSingleSelectOptions({
|
||||
cellIdentifier={cellIdentifier}
|
||||
cellCache={cellCache}
|
||||
fieldController={fieldController}
|
||||
onOutsideClick={() => setShowOptionsPopup(false)}
|
||||
onOutsideClick={() => !showEditCellOption && setShowOptionsPopup(false)}
|
||||
openOptionDetail={onOpenOptionDetailClick}
|
||||
/>
|
||||
)}
|
||||
@ -65,6 +63,7 @@ export default function GridSingleSelectOptions({
|
||||
left={editCellOptionLeft}
|
||||
cellIdentifier={cellIdentifier}
|
||||
editingSelectOption={editingSelectOption}
|
||||
setEditingSelectOption={setEditingSelectOption}
|
||||
onOutsideClick={() => {
|
||||
setShowEditCellOption(false);
|
||||
}}
|
||||
|
@ -16,8 +16,11 @@ export default function GridTextCell({
|
||||
const { data, cellController } = useCell(cellIdentifier, cellCache, fieldController);
|
||||
|
||||
return (
|
||||
<div className='w-full'>
|
||||
{cellController && <EditCellText data={data as string | undefined} cellController={cellController}></EditCellText>}
|
||||
</div>
|
||||
<EditCellText
|
||||
data={data as string | undefined}
|
||||
onSave={async (value) => {
|
||||
await cellController?.saveCellData(value);
|
||||
}}
|
||||
></EditCellText>
|
||||
);
|
||||
}
|
||||
|
@ -17,6 +17,11 @@ export const GridUrl = ({
|
||||
const { data, cellController } = useCell(cellIdentifier, cellCache, fieldController);
|
||||
|
||||
return (
|
||||
<>{cellController && <EditCellUrl data={data as URLCellDataPB} cellController={cellController}></EditCellUrl>}</>
|
||||
<EditCellUrl
|
||||
data={data as URLCellDataPB}
|
||||
onSave={async (value) => {
|
||||
await cellController?.saveCellData(value);
|
||||
}}
|
||||
></EditCellUrl>
|
||||
);
|
||||
};
|
||||
|
@ -9,6 +9,7 @@ export const useGridTableHeaderHooks = function (controller: DatabaseController)
|
||||
const onAddField = async () => {
|
||||
// TODO: move this to database controller hook
|
||||
const fieldController = new TypeOptionController(controller.viewId, None);
|
||||
|
||||
await fieldController.initialize();
|
||||
};
|
||||
|
||||
|
@ -1,36 +1,50 @@
|
||||
import { DatabaseController } from '@/appflowy_app/stores/effects/database/database_controller';
|
||||
|
||||
import AddSvg from '../../_shared/svg/AddSvg';
|
||||
import { useGridTableHeaderHooks } from './GridTableHeader.hooks';
|
||||
|
||||
import { GridTableHeaderItem } from './GridTableHeaderItem';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
|
||||
export const GridTableHeader = ({ controller }: { controller: DatabaseController }) => {
|
||||
const { fields, onAddField } = useGridTableHeaderHooks(controller);
|
||||
export const GridTableHeader = ({
|
||||
controller,
|
||||
onShowFilterClick,
|
||||
onShowSortClick,
|
||||
}: {
|
||||
controller: DatabaseController;
|
||||
onShowFilterClick: () => void;
|
||||
onShowSortClick: () => void;
|
||||
}) => {
|
||||
const columns = useAppSelector((state) => state.database.columns);
|
||||
const fields = useAppSelector((state) => state.database.fields);
|
||||
const { onAddField } = useGridTableHeaderHooks(controller);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<thead>
|
||||
<tr>
|
||||
{fields.map((field, i) => {
|
||||
return <GridTableHeaderItem field={field} controller={controller} key={i} />;
|
||||
})}
|
||||
|
||||
<th className='m-0 w-40 border border-r-0 border-line-divider p-0'>
|
||||
<div
|
||||
className='flex cursor-pointer items-center px-4 py-2 text-text-caption hover:bg-fill-list-hover hover:text-text-title'
|
||||
onClick={onAddField}
|
||||
>
|
||||
<i className='mr-2 h-5 w-5'>
|
||||
<AddSvg />
|
||||
</i>
|
||||
<span>{t('grid.field.newProperty')}</span>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</>
|
||||
<div className={'flex select-none text-xs'} style={{ userSelect: 'none' }}>
|
||||
<div className={'w-7 flex-shrink-0'}></div>
|
||||
{columns
|
||||
.filter((column) => column.visible)
|
||||
.map((column, i) => {
|
||||
return (
|
||||
<GridTableHeaderItem
|
||||
onShowFilterClick={onShowFilterClick}
|
||||
onShowSortClick={onShowSortClick}
|
||||
field={fields[column.fieldId]}
|
||||
controller={controller}
|
||||
key={i}
|
||||
index={i}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<div
|
||||
onClick={onAddField}
|
||||
className='-ml-1.5 flex w-40 flex-shrink-0 cursor-pointer items-center border-b border-t border-line-divider px-4 py-2 text-text-caption hover:bg-fill-list-hover hover:text-text-title'
|
||||
>
|
||||
<i className='mr-2 h-5 w-5'>
|
||||
<AddSvg />
|
||||
</i>
|
||||
<span className={'whitespace-nowrap'}>{t('grid.field.newProperty')}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -2,45 +2,59 @@ import { CellIdentifier } from '@/appflowy_app/stores/effects/database/cell/cell
|
||||
import { DatabaseController } from '@/appflowy_app/stores/effects/database/database_controller';
|
||||
import { TypeOptionController } from '@/appflowy_app/stores/effects/database/field/type_option/type_option_controller';
|
||||
import { FieldType } from '@/services/backend';
|
||||
import { useState, useRef } from 'react';
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { Some } from 'ts-results';
|
||||
import { ChangeFieldTypePopup } from '../../_shared/EditRow/ChangeFieldTypePopup';
|
||||
import { EditFieldPopup } from '../../_shared/EditRow/EditFieldPopup';
|
||||
import { ChecklistTypeSvg } from '../../_shared/svg/ChecklistTypeSvg';
|
||||
import { DateTypeSvg } from '../../_shared/svg/DateTypeSvg';
|
||||
import { MultiSelectTypeSvg } from '../../_shared/svg/MultiSelectTypeSvg';
|
||||
import { NumberTypeSvg } from '../../_shared/svg/NumberTypeSvg';
|
||||
import { SingleSelectTypeSvg } from '../../_shared/svg/SingleSelectTypeSvg';
|
||||
import { TextTypeSvg } from '../../_shared/svg/TextTypeSvg';
|
||||
import { UrlTypeSvg } from '../../_shared/svg/UrlTypeSvg';
|
||||
import { databaseActions, IDatabaseField } from '$app_reducers/database/slice';
|
||||
import { FieldTypeIcon } from '$app/components/_shared/EditRow/FieldTypeIcon';
|
||||
import { useResizer } from '$app/components/_shared/useResizer';
|
||||
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||
import { Details2Svg } from '$app/components/_shared/svg/Details2Svg';
|
||||
import { FilterSvg } from '$app/components/_shared/svg/FilterSvg';
|
||||
import { SortAscSvg } from '$app/components/_shared/svg/SortAscSvg';
|
||||
import { PromptWindow } from '$app/components/_shared/PromptWindow';
|
||||
|
||||
const MIN_COLUMN_WIDTH = 100;
|
||||
|
||||
export const GridTableHeaderItem = ({
|
||||
controller,
|
||||
field,
|
||||
index,
|
||||
onShowFilterClick,
|
||||
onShowSortClick,
|
||||
}: {
|
||||
controller: DatabaseController;
|
||||
field: {
|
||||
fieldId: string;
|
||||
name: string;
|
||||
fieldType: FieldType;
|
||||
};
|
||||
field: IDatabaseField;
|
||||
index: number;
|
||||
onShowFilterClick: () => void;
|
||||
onShowSortClick: () => void;
|
||||
}) => {
|
||||
const { onMouseDown, newSizeX } = useResizer((final) => {
|
||||
if (final < MIN_COLUMN_WIDTH) return;
|
||||
void controller.changeWidth({ fieldId: field.fieldId, width: final });
|
||||
});
|
||||
|
||||
const filtersStore = useAppSelector((state) => state.database.filters);
|
||||
const sortStore = useAppSelector((state) => state.database.sort);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const [showFieldEditor, setShowFieldEditor] = useState(false);
|
||||
const [editFieldTop, setEditFieldTop] = useState(0);
|
||||
const [editFieldRight, setEditFieldRight] = useState(0);
|
||||
|
||||
const [showChangeFieldTypePopup, setShowChangeFieldTypePopup] = useState(false);
|
||||
const [changeFieldTypeTop, setChangeFieldTypeTop] = useState(0);
|
||||
const [changeFieldTypeRight, setChangeFieldTypeRight] = useState(0);
|
||||
|
||||
const [editingField, setEditingField] = useState<{
|
||||
fieldId: string;
|
||||
name: string;
|
||||
fieldType: FieldType;
|
||||
} | null>(null);
|
||||
const [changeFieldTypeAnchorEl, setChangeFieldTypeAnchorEl] = useState<HTMLDivElement | null>(null);
|
||||
const [editingField, setEditingField] = useState<IDatabaseField | null>(null);
|
||||
const [deletingPropertyId, setDeletingPropertyId] = useState<string | null>(null);
|
||||
const [showDeletePropertyPrompt, setShowDeletePropertyPrompt] = useState(false);
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!newSizeX) return;
|
||||
if (newSizeX >= MIN_COLUMN_WIDTH) {
|
||||
dispatch(databaseActions.changeWidth({ fieldId: field.fieldId, width: newSizeX }));
|
||||
}
|
||||
}, [newSizeX]);
|
||||
|
||||
const changeFieldType = async (newType: FieldType) => {
|
||||
if (!editingField) return;
|
||||
|
||||
@ -60,66 +74,115 @@ export const GridTableHeaderItem = ({
|
||||
setShowChangeFieldTypePopup(false);
|
||||
};
|
||||
|
||||
const onFieldOptionsClick = () => {
|
||||
setEditingField(field);
|
||||
setShowFieldEditor(true);
|
||||
};
|
||||
|
||||
const onDeletePropertyClick = (fieldId: string) => {
|
||||
setDeletingPropertyId(fieldId);
|
||||
setShowDeletePropertyPrompt(true);
|
||||
};
|
||||
|
||||
const onDelete = async () => {
|
||||
if (!deletingPropertyId) return;
|
||||
const fieldInfo = controller.fieldController.getField(deletingPropertyId);
|
||||
|
||||
if (!fieldInfo) return;
|
||||
const typeController = new TypeOptionController(controller.viewId, Some(fieldInfo));
|
||||
|
||||
setEditingField(null);
|
||||
|
||||
await typeController.initialize();
|
||||
await typeController.deleteField();
|
||||
setShowDeletePropertyPrompt(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<th key={field.fieldId} className='m-0 border border-l-0 border-line-divider p-0'>
|
||||
<>
|
||||
<div
|
||||
className={'flex w-full cursor-pointer items-center px-4 py-2 hover:bg-fill-list-hover'}
|
||||
ref={ref}
|
||||
onClick={() => {
|
||||
if (!ref.current) return;
|
||||
const { top, left } = ref.current.getBoundingClientRect();
|
||||
|
||||
setEditFieldRight(left - 10);
|
||||
setEditFieldTop(top + 35);
|
||||
setEditingField(field);
|
||||
setShowFieldEditor(true);
|
||||
}}
|
||||
// field width minus divider width with padding
|
||||
style={{ width: `${field.width - (index === 0 ? 7 : 14)}px` }}
|
||||
className='flex-shrink-0 border-b border-t border-line-divider'
|
||||
>
|
||||
<i className={'mr-2 h-5 w-5 text-text-caption'}>
|
||||
{field.fieldType === FieldType.RichText && <TextTypeSvg></TextTypeSvg>}
|
||||
{field.fieldType === FieldType.Number && <NumberTypeSvg></NumberTypeSvg>}
|
||||
{field.fieldType === FieldType.DateTime && <DateTypeSvg></DateTypeSvg>}
|
||||
{field.fieldType === FieldType.SingleSelect && <SingleSelectTypeSvg></SingleSelectTypeSvg>}
|
||||
{field.fieldType === FieldType.MultiSelect && <MultiSelectTypeSvg></MultiSelectTypeSvg>}
|
||||
{field.fieldType === FieldType.Checklist && <ChecklistTypeSvg></ChecklistTypeSvg>}
|
||||
{field.fieldType === FieldType.Checkbox && <ChecklistTypeSvg></ChecklistTypeSvg>}
|
||||
{field.fieldType === FieldType.URL && <UrlTypeSvg></UrlTypeSvg>}
|
||||
</i>
|
||||
<span>{field.name}</span>
|
||||
<div className={'flex w-full items-center justify-between py-2 pl-2'} ref={ref}>
|
||||
<div className={'flex min-w-0 items-center gap-2'}>
|
||||
<div className={'flex h-5 w-5 flex-shrink-0 items-center justify-center text-text-caption'}>
|
||||
<FieldTypeIcon fieldType={field.fieldType}></FieldTypeIcon>
|
||||
</div>
|
||||
<span className={'overflow-hidden text-ellipsis whitespace-nowrap text-text-caption'}>{field.title}</span>
|
||||
</div>
|
||||
<div className={'flex items-center gap-1'}>
|
||||
{sortStore.findIndex((sort) => sort.fieldId === field.fieldId) !== -1 && (
|
||||
<button onClick={onShowSortClick} className={'rounded p-1 hover:bg-fill-list-hover'}>
|
||||
<i className={'block h-[16px] w-[16px]'}>
|
||||
<SortAscSvg></SortAscSvg>
|
||||
</i>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{showFieldEditor && editingField && (
|
||||
<EditFieldPopup
|
||||
top={editFieldTop}
|
||||
left={editFieldRight}
|
||||
cellIdentifier={
|
||||
{
|
||||
fieldId: editingField.fieldId,
|
||||
fieldType: editingField.fieldType,
|
||||
viewId: controller.viewId,
|
||||
} as CellIdentifier
|
||||
}
|
||||
viewId={controller.viewId}
|
||||
onOutsideClick={() => {
|
||||
setShowFieldEditor(false);
|
||||
}}
|
||||
fieldInfo={controller.fieldController.getField(editingField.fieldId)}
|
||||
changeFieldTypeClick={(buttonTop, buttonRight) => {
|
||||
setChangeFieldTypeTop(buttonTop);
|
||||
setChangeFieldTypeRight(buttonRight);
|
||||
setShowChangeFieldTypePopup(true);
|
||||
}}
|
||||
></EditFieldPopup>
|
||||
)}
|
||||
{filtersStore.findIndex((filter) => filter.fieldId === field.fieldId) !== -1 && (
|
||||
<button onClick={onShowFilterClick} className={'rounded p-1 hover:bg-fill-list-hover'}>
|
||||
<i className={'block h-[16px] w-[16px]'}>
|
||||
<FilterSvg></FilterSvg>
|
||||
</i>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{showChangeFieldTypePopup && (
|
||||
<ChangeFieldTypePopup
|
||||
top={changeFieldTypeTop}
|
||||
left={changeFieldTypeRight}
|
||||
onClick={(newType) => changeFieldType(newType)}
|
||||
onOutsideClick={() => setShowChangeFieldTypePopup(false)}
|
||||
></ChangeFieldTypePopup>
|
||||
)}
|
||||
<button className={'rounded p-1 hover:bg-fill-list-hover'} onClick={() => onFieldOptionsClick()}>
|
||||
<i className={'block h-[16px] w-[16px]'}>
|
||||
<Details2Svg></Details2Svg>
|
||||
</i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<div
|
||||
className={'group h-full cursor-col-resize border-b border-t border-line-divider px-[6px]'}
|
||||
onMouseDown={(e) => onMouseDown(e, field.width)}
|
||||
>
|
||||
<div className={'flex h-full w-[3px] justify-center group-hover:bg-fill-hover'}>
|
||||
<div className={'h-full w-[1px] bg-line-divider group-hover:bg-fill-hover'}></div>
|
||||
</div>
|
||||
</div>
|
||||
{editingField && (
|
||||
<EditFieldPopup
|
||||
open={showFieldEditor}
|
||||
anchorEl={ref.current}
|
||||
cellIdentifier={
|
||||
{
|
||||
fieldId: editingField.fieldId,
|
||||
fieldType: editingField.fieldType,
|
||||
viewId: controller.viewId,
|
||||
} as CellIdentifier
|
||||
}
|
||||
viewId={controller.viewId}
|
||||
onOutsideClick={() => {
|
||||
setShowFieldEditor(false);
|
||||
}}
|
||||
controller={controller}
|
||||
changeFieldTypeClick={(el) => {
|
||||
setChangeFieldTypeAnchorEl(el);
|
||||
setShowChangeFieldTypePopup(true);
|
||||
}}
|
||||
onDeletePropertyClick={onDeletePropertyClick}
|
||||
></EditFieldPopup>
|
||||
)}
|
||||
|
||||
<ChangeFieldTypePopup
|
||||
open={showChangeFieldTypePopup}
|
||||
anchorEl={changeFieldTypeAnchorEl}
|
||||
onClick={(newType) => changeFieldType(newType)}
|
||||
onOutsideClick={() => setShowChangeFieldTypePopup(false)}
|
||||
></ChangeFieldTypePopup>
|
||||
|
||||
{showDeletePropertyPrompt && (
|
||||
<PromptWindow
|
||||
msg={'Are you sure you want to delete this property?'}
|
||||
onYes={() => onDelete()}
|
||||
onCancel={() => setShowDeletePropertyPrompt(false)}
|
||||
></PromptWindow>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,11 +0,0 @@
|
||||
import { DatabaseController } from '@/appflowy_app/stores/effects/database/database_controller';
|
||||
|
||||
export const useGridAddRow = (controller: DatabaseController) => {
|
||||
async function addRow() {
|
||||
await controller.createRow();
|
||||
}
|
||||
|
||||
return {
|
||||
addRow,
|
||||
};
|
||||
};
|
@ -1,20 +0,0 @@
|
||||
import { DatabaseController } from '@/appflowy_app/stores/effects/database/database_controller';
|
||||
import AddSvg from '../../_shared/svg/AddSvg';
|
||||
import { useGridAddRow } from './GridAddRow.hooks';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const GridAddRow = ({ controller }: { controller: DatabaseController }) => {
|
||||
const { addRow } = useGridAddRow(controller);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button className='flex cursor-pointer items-center text-text-caption hover:text-text-title' onClick={addRow}>
|
||||
<i className='mr-2 h-5 w-5'>
|
||||
<AddSvg />
|
||||
</i>
|
||||
<span>{t('grid.row.newRow')}</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,21 @@
|
||||
import { DatabaseController } from '@/appflowy_app/stores/effects/database/database_controller';
|
||||
|
||||
export const useGridRowActions = (controller: DatabaseController) => {
|
||||
const deleteRow = async (rowId: string) => {
|
||||
await controller.deleteRow(rowId);
|
||||
};
|
||||
|
||||
const insertRowAfter = async (rowId: string) => {
|
||||
await controller.createRowAfter(rowId);
|
||||
};
|
||||
|
||||
const duplicateRow = async (rowId: string) => {
|
||||
await controller.duplicateRow(rowId);
|
||||
};
|
||||
|
||||
return {
|
||||
deleteRow,
|
||||
insertRowAfter,
|
||||
duplicateRow,
|
||||
};
|
||||
};
|
@ -0,0 +1,106 @@
|
||||
import AddSvg from '../../_shared/svg/AddSvg';
|
||||
import { CopySvg } from '../../_shared/svg/CopySvg';
|
||||
import { TrashSvg } from '../../_shared/svg/TrashSvg';
|
||||
import { ShareSvg } from '../../_shared/svg/ShareSvg';
|
||||
import { DatabaseController } from '@/appflowy_app/stores/effects/database/database_controller';
|
||||
import { useGridRowActions } from './GridRowActions.hooks';
|
||||
import { List, Popover } from '@mui/material';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const GridRowActions = ({
|
||||
controller,
|
||||
rowId,
|
||||
isDragging,
|
||||
children,
|
||||
}: {
|
||||
controller: DatabaseController;
|
||||
rowId: string;
|
||||
isDragging: boolean;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const { deleteRow, duplicateRow, insertRowAfter } = useGridRowActions(controller);
|
||||
const optionsButtonEl = useRef<HTMLButtonElement>(null);
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'flex flex-shrink-0 items-center justify-center'}>
|
||||
<button
|
||||
ref={optionsButtonEl}
|
||||
onClick={() => setShowMenu(true)}
|
||||
className={`cursor-pointer items-center justify-center rounded p-1 opacity-0 hover:bg-fill-list-hover group-hover/row:opacity-100 ${
|
||||
isDragging || showMenu ? '!opacity-100' : ''
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
</div>
|
||||
<Popover
|
||||
open={showMenu}
|
||||
anchorEl={optionsButtonEl.current}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
onClose={() => setShowMenu(false)}
|
||||
>
|
||||
<List>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
void insertRowAfter(rowId);
|
||||
setShowMenu(false);
|
||||
}}
|
||||
>
|
||||
<span className={'mr-2'}>
|
||||
<i className={'block h-[16px] w-[16px]'}>
|
||||
<AddSvg />
|
||||
</i>
|
||||
</span>
|
||||
<span>{t('button.insertBelow')}</span>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => console.log('copy link')}>
|
||||
<span className={'mr-2'}>
|
||||
<i className={'block h-[16px] w-[16px]'}>
|
||||
<ShareSvg />
|
||||
</i>
|
||||
</span>
|
||||
<span>{t('shareAction.copyLink')}</span>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
void duplicateRow(rowId);
|
||||
setShowMenu(false);
|
||||
}}
|
||||
>
|
||||
<span className={'mr-2'}>
|
||||
<i className={'block h-[16px] w-[16px]'}>
|
||||
<CopySvg />
|
||||
</i>
|
||||
</span>
|
||||
<span>{t('grid.row.duplicate')}</span>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
void deleteRow(rowId);
|
||||
setShowMenu(false);
|
||||
}}
|
||||
>
|
||||
<span className={'mr-2'}>
|
||||
<i className={'block h-[16px] w-[16px]'}>
|
||||
<TrashSvg />
|
||||
</i>
|
||||
</span>
|
||||
<span>{t('grid.row.delete')}</span>
|
||||
</MenuItem>
|
||||
</List>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
};
|
@ -3,44 +3,87 @@ import { RowInfo } from '@/appflowy_app/stores/effects/database/row/row_cache';
|
||||
import { useRow } from '../../_shared/database-hooks/useRow';
|
||||
import { FullView } from '../../_shared/svg/FullView';
|
||||
import { GridCell } from '../GridCell/GridCell';
|
||||
import { DragSvg } from '../../_shared/svg/DragSvg';
|
||||
import { Draggable, DraggableProvided, DraggableStateSnapshot } from 'react-beautiful-dnd';
|
||||
import { GridRowActions } from './GridRowActions';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
|
||||
export const GridTableRow = ({
|
||||
viewId,
|
||||
controller,
|
||||
row,
|
||||
onOpenRow,
|
||||
index,
|
||||
}: {
|
||||
viewId: string;
|
||||
controller: DatabaseController;
|
||||
row: RowInfo;
|
||||
onOpenRow: (rowId: RowInfo) => void;
|
||||
index: number;
|
||||
}) => {
|
||||
const { cells } = useRow(viewId, controller, row);
|
||||
const fields = useAppSelector((state) => state.database.fields);
|
||||
|
||||
return (
|
||||
<tr className='group'>
|
||||
{cells.map((cell, cellIndex) => {
|
||||
return (
|
||||
<td className='m-0 border border-l-0 border-line-divider p-0 ' key={cellIndex}>
|
||||
<div className='flex w-full items-center justify-end'>
|
||||
<GridCell
|
||||
cellIdentifier={cell.cellIdentifier}
|
||||
cellCache={controller.databaseViewCache.getRowCache().getCellCache()}
|
||||
fieldController={controller.fieldController}
|
||||
/>
|
||||
// this is needed to prevent DnD from causing exceptions
|
||||
cells.length ? (
|
||||
<Draggable draggableId={row.row.id} key={row.row.id} index={index}>
|
||||
{(provided: DraggableProvided, snapshot: DraggableStateSnapshot) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
className={`group/row flex cursor-pointer items-stretch ${snapshot.isDragging ? 'shadow-md' : ''}`}
|
||||
>
|
||||
<GridRowActions controller={controller} rowId={row.row.id} isDragging={snapshot.isDragging}>
|
||||
<i className={`block h-5 w-5`} {...provided.dragHandleProps}>
|
||||
<DragSvg />
|
||||
</i>
|
||||
</GridRowActions>
|
||||
{cells
|
||||
// filter out hidden fields
|
||||
// ?? true is to prevent DnD from causing exceptions
|
||||
.filter((cell) => fields[cell.fieldId]?.visible ?? true)
|
||||
.map((cell, cellIndex) => {
|
||||
return (
|
||||
<div
|
||||
className={`group/cell relative flex flex-shrink-0 border-b border-line-divider bg-bg-body ${
|
||||
snapshot.isDragging ? 'border-t' : ''
|
||||
}`}
|
||||
key={cellIndex}
|
||||
draggable={false}
|
||||
>
|
||||
<GridCell
|
||||
width={fields[cell.fieldId]?.width}
|
||||
cellIdentifier={cell.cellIdentifier}
|
||||
cellCache={controller.databaseViewCache.getRowCache().getCellCache()}
|
||||
fieldController={controller.fieldController}
|
||||
/>
|
||||
|
||||
{cellIndex === 0 && (
|
||||
<div
|
||||
onClick={() => onOpenRow(row)}
|
||||
className='mr-1 hidden h-8 w-8 cursor-pointer rounded p-1.5 text-text-caption hover:bg-fill-list-hover group-hover:block '
|
||||
>
|
||||
<FullView />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
{cellIndex === 0 && (
|
||||
<div
|
||||
onClick={() => onOpenRow(row)}
|
||||
className='absolute inset-y-0 right-0 my-auto mr-1 hidden flex-shrink-0 cursor-pointer items-center justify-center rounded p-1 hover:bg-fill-list-hover group-hover/cell:flex'
|
||||
>
|
||||
<i className={' block h-5 w-5'}>
|
||||
<FullView />
|
||||
</i>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={'flex h-full justify-center'}>
|
||||
<div className={'h-full w-[1px] bg-line-divider'}></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div
|
||||
className={`-ml-1.5 w-40 border-b border-line-divider bg-bg-body ${snapshot.isDragging ? 'border-t' : ''}`}
|
||||
></div>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { DatabaseController } from '@/appflowy_app/stores/effects/database/database_controller';
|
||||
import { RowInfo } from '@/appflowy_app/stores/effects/database/row/row_cache';
|
||||
import { GridTableRow } from './GridTableRow';
|
||||
import { DragDropContext, Droppable, DroppableProvided, OnDragEndResponder } from 'react-beautiful-dnd';
|
||||
|
||||
export const GridTableRows = ({
|
||||
viewId,
|
||||
controller,
|
||||
@ -12,11 +14,37 @@ export const GridTableRows = ({
|
||||
allRows: readonly RowInfo[];
|
||||
onOpenRow: (rowId: RowInfo) => void;
|
||||
}) => {
|
||||
const onRowsDragEnd: OnDragEndResponder = async (result) => {
|
||||
if (!result.destination) return;
|
||||
if (result.destination.index === result.source.index) return;
|
||||
await controller.moveRow(result.draggableId, allRows[result.destination.index].row.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<tbody>
|
||||
{allRows.map((row, i) => {
|
||||
return <GridTableRow onOpenRow={onOpenRow} row={row} key={i} viewId={viewId} controller={controller} />;
|
||||
})}
|
||||
</tbody>
|
||||
<DragDropContext onDragEnd={onRowsDragEnd}>
|
||||
<Droppable droppableId='table'>
|
||||
{(droppableProvided: DroppableProvided) => (
|
||||
<div
|
||||
className={'absolute h-full overflow-y-auto overflow-x-hidden'}
|
||||
ref={droppableProvided.innerRef}
|
||||
{...droppableProvided.droppableProps}
|
||||
>
|
||||
{allRows.map((row, i) => {
|
||||
return (
|
||||
<GridTableRow
|
||||
onOpenRow={onOpenRow}
|
||||
row={row}
|
||||
key={i}
|
||||
index={i}
|
||||
viewId={viewId}
|
||||
controller={controller}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{droppableProvided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
);
|
||||
};
|
||||
|
@ -1,28 +0,0 @@
|
||||
import { useAppDispatch, useAppSelector } from '@/appflowy_app/stores/store';
|
||||
import { useState } from 'react';
|
||||
|
||||
export const useGridTitleHooks = function () {
|
||||
const dispatch = useAppDispatch();
|
||||
const grid = useAppSelector((state) => state.grid);
|
||||
|
||||
const [title, setTitle] = useState(grid.title);
|
||||
const [changingTitle, setChangingTitle] = useState(false);
|
||||
const [showOptions, setShowOptions] = useState(false);
|
||||
|
||||
const onTitleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setTitle(event.target.value);
|
||||
};
|
||||
|
||||
const onTitleClick = () => {
|
||||
setChangingTitle(true);
|
||||
};
|
||||
|
||||
return {
|
||||
title,
|
||||
onTitleChange,
|
||||
onTitleClick,
|
||||
changingTitle,
|
||||
showOptions,
|
||||
setShowOptions,
|
||||
};
|
||||
};
|
@ -1,20 +1,35 @@
|
||||
import { useGridTitleHooks } from './GridTitle.hooks';
|
||||
import { SettingsSvg } from '../../_shared/svg/SettingsSvg';
|
||||
import { GridTitleOptionsPopup } from './GridTitleOptionsPopup';
|
||||
import { useState } from 'react';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
|
||||
export const GridTitle = () => {
|
||||
const { title, showOptions, setShowOptions } = useGridTitleHooks();
|
||||
export const GridTitle = ({
|
||||
onShowFilterClick,
|
||||
onShowSortClick,
|
||||
viewId,
|
||||
}: {
|
||||
onShowFilterClick: () => void;
|
||||
onShowSortClick: () => void;
|
||||
viewId: string;
|
||||
}) => {
|
||||
const [showOptions, setShowOptions] = useState(false);
|
||||
const pagesStore = useAppSelector((state) => state.pages.pageMap[viewId]);
|
||||
|
||||
return (
|
||||
<div className={'relative flex items-center '}>
|
||||
<div>{title}</div>
|
||||
|
||||
<div className='flex '>
|
||||
<div>{pagesStore?.name}</div>
|
||||
<button className={'ml-2 h-5 w-5 '} onClick={() => setShowOptions(!showOptions)}>
|
||||
<SettingsSvg></SettingsSvg>
|
||||
</button>
|
||||
|
||||
{showOptions && <GridTitleOptionsPopup onClose={() => setShowOptions(!showOptions)} />}
|
||||
{showOptions && (
|
||||
<GridTitleOptionsPopup
|
||||
onClose={() => setShowOptions(!showOptions)}
|
||||
onFilterClick={() => onShowFilterClick()}
|
||||
onSortClick={() => onShowSortClick()}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -4,33 +4,43 @@ import { GroupBySvg } from '../../_shared/svg/GroupBySvg';
|
||||
import { PropertiesSvg } from '../../_shared/svg/PropertiesSvg';
|
||||
import { SortSvg } from '../../_shared/svg/SortSvg';
|
||||
|
||||
export const GridTitleOptionsPopup = ({ onClose }: { onClose?: () => void }) => {
|
||||
export const GridTitleOptionsPopup = ({
|
||||
onClose,
|
||||
onFilterClick,
|
||||
onSortClick,
|
||||
}: {
|
||||
onClose?: () => void;
|
||||
onFilterClick: () => void;
|
||||
onSortClick: () => void;
|
||||
}) => {
|
||||
const items: IPopupItem[] = [
|
||||
{
|
||||
icon: (
|
||||
<i className={'h-[16px] w-[16px] text-text-title'}>
|
||||
<i className={'h-[16px] w-[16px] flex-shrink-0 text-text-title'}>
|
||||
<FilterSvg />
|
||||
</i>
|
||||
),
|
||||
onClick: () => {
|
||||
console.log('filter');
|
||||
onFilterClick && onFilterClick();
|
||||
onClose && onClose();
|
||||
},
|
||||
title: 'Filter',
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<i className={'h-[16px] w-[16px] text-text-title'}>
|
||||
<i className={'h-[16px] w-[16px] flex-shrink-0 text-text-title'}>
|
||||
<SortSvg />
|
||||
</i>
|
||||
),
|
||||
onClick: () => {
|
||||
console.log('sort');
|
||||
onSortClick && onSortClick();
|
||||
onClose && onClose();
|
||||
},
|
||||
title: 'Sort',
|
||||
title: 'Sort By',
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<i className={'h-[16px] w-[16px] text-text-title'}>
|
||||
<i className={'h-[16px] w-[16px] flex-shrink-0 text-text-title'}>
|
||||
<PropertiesSvg />
|
||||
</i>
|
||||
),
|
||||
@ -41,7 +51,7 @@ export const GridTitleOptionsPopup = ({ onClose }: { onClose?: () => void }) =>
|
||||
},
|
||||
{
|
||||
icon: (
|
||||
<i className={'h-[16px] w-[16px] text-text-title'}>
|
||||
<i className={'h-[16px] w-[16px] flex-shrink-0 text-text-title'}>
|
||||
<GroupBySvg />
|
||||
</i>
|
||||
),
|
||||
@ -52,5 +62,5 @@ export const GridTitleOptionsPopup = ({ onClose }: { onClose?: () => void }) =>
|
||||
},
|
||||
];
|
||||
|
||||
return <PopupSelect items={items} className={'absolute top-full z-10 w-fit'} onOutsideClick={onClose} />;
|
||||
return <PopupSelect items={items} className={'absolute top-full z-10 w-[140px]'} onOutsideClick={onClose} />;
|
||||
};
|
||||
|
@ -39,6 +39,13 @@ import { TrashSvg } from '$app/components/_shared/svg/TrashSvg';
|
||||
import { UrlTypeSvg } from '$app/components/_shared/svg/UrlTypeSvg';
|
||||
import { DragElementSvg } from '$app/components/_shared/svg/DragElementSvg';
|
||||
import { ImageSvg } from '$app/components/_shared/svg/ImageSvg';
|
||||
import { DragSvg } from '$app/components/_shared/svg/DragSvg';
|
||||
import { FullView } from '$app/components/_shared/svg/FullView';
|
||||
import { GroupBySvg } from '$app/components/_shared/svg/GroupBySvg';
|
||||
import { SettingsSvg } from '$app/components/_shared/svg/SettingsSvg';
|
||||
import { ShareSvg } from '$app/components/_shared/svg/ShareSvg';
|
||||
import { SortAscSvg } from '$app/components/_shared/svg/SortAscSvg';
|
||||
import { SortDescSvg } from '$app/components/_shared/svg/SortDescSvg';
|
||||
|
||||
export const AllIcons = () => {
|
||||
return (
|
||||
@ -88,6 +95,9 @@ export const AllIcons = () => {
|
||||
<i className={'h-5 w-5'} title={'DragElementSvg'}>
|
||||
<DragElementSvg></DragElementSvg>
|
||||
</i>
|
||||
<i className={'h-5 w-5'} title={'DragSvg'}>
|
||||
<DragSvg></DragSvg>
|
||||
</i>
|
||||
<i className={'h-5 w-5'} title={'DropDownShowSvg'}>
|
||||
<DropDownShowSvg></DropDownShowSvg>
|
||||
</i>
|
||||
@ -112,12 +122,18 @@ export const AllIcons = () => {
|
||||
<i className={'h-5 w-5'} title={'FilterSvg'}>
|
||||
<FilterSvg></FilterSvg>
|
||||
</i>
|
||||
<i className={'h-5 w-5'} title={'FullView'}>
|
||||
<FullView></FullView>
|
||||
</i>
|
||||
<i className={'h-5 w-5'} title={'GridSvg'}>
|
||||
<GridSvg></GridSvg>
|
||||
</i>
|
||||
<i className={'h-5 w-5'} title={'GroupByFieldSvg'}>
|
||||
<GroupByFieldSvg></GroupByFieldSvg>
|
||||
</i>
|
||||
<i className={'h-5 w-5'} title={'GroupBySvg'}>
|
||||
<GroupBySvg></GroupBySvg>
|
||||
</i>
|
||||
<i className={'h-5 w-5'} title={'HideMenuSvg'}>
|
||||
<HideMenuSvg></HideMenuSvg>
|
||||
</i>
|
||||
@ -145,6 +161,12 @@ export const AllIcons = () => {
|
||||
<i className={'h-5 w-5'} title={'SearchSvg'}>
|
||||
<SearchSvg></SearchSvg>
|
||||
</i>
|
||||
<i className={'h-5 w-5'} title={'SettingsSvg'}>
|
||||
<SettingsSvg></SettingsSvg>
|
||||
</i>
|
||||
<i className={'h-5 w-5'} title={'ShareSvg'}>
|
||||
<ShareSvg></ShareSvg>
|
||||
</i>
|
||||
<i className={'h-5 w-5'} title={'ShowMenuSvg'}>
|
||||
<ShowMenuSvg></ShowMenuSvg>
|
||||
</i>
|
||||
@ -157,6 +179,12 @@ export const AllIcons = () => {
|
||||
<i className={'h-5 w-5'} title={'SkipRightSvg'}>
|
||||
<SkipRightSvg></SkipRightSvg>
|
||||
</i>
|
||||
<i className={'h-5 w-5'} title={'SortAscSvg'}>
|
||||
<SortAscSvg></SortAscSvg>
|
||||
</i>
|
||||
<i className={'h-5 w-5'} title={'SortDescSvg'}>
|
||||
<SortDescSvg></SortDescSvg>
|
||||
</i>
|
||||
<i className={'h-5 w-5'} title={'SortSvg'}>
|
||||
<SortSvg></SortSvg>
|
||||
</i>
|
||||
|
@ -4,9 +4,9 @@ export const ColorPalette = () => {
|
||||
<h1 className={'mb-4 text-2xl'}>Colors</h1>
|
||||
<h2 className={'mb-4'}>Main</h2>
|
||||
<div className={'mb-8 flex flex-wrap items-center'}>
|
||||
<div title={'main-accent'} className={'m-2 h-[100px] w-[100px] bg-fill-default'}></div>
|
||||
<div title={'fill-hover'} className={'m-2 h-[100px] w-[100px] bg-fill-default'}></div>
|
||||
<div title={'main-hovered'} className={'m-2 h-[100px] w-[100px] bg-fill-list-hover'}></div>
|
||||
<div title={'main-secondary'} className={'m-2 h-[100px] w-[100px] bg-fill-list-hover'}></div>
|
||||
<div title={'fill-list-hover'} className={'m-2 h-[100px] w-[100px] bg-fill-list-hover'}></div>
|
||||
<div title={'main-selector'} className={'m-2 h-[100px] w-[100px] bg-fill-selector'}></div>
|
||||
<div title={'main-alert'} className={'m-2 h-[100px] w-[100px] bg-function-info'}></div>
|
||||
<div title={'main-warning'} className={'m-2 h-[100px] w-[100px] bg-function-warning'}></div>
|
||||
@ -31,7 +31,7 @@ export const ColorPalette = () => {
|
||||
<div title={'shade-3'} className={'bg-shade-3 m-2 h-[100px] w-[100px]'}></div>
|
||||
<div title={'shade-4'} className={'bg-shade-4 m-2 h-[100px] w-[100px]'}></div>
|
||||
<div title={'shade-5'} className={'bg-shade-5 m-2 h-[100px] w-[100px]'}></div>
|
||||
<div title={'shade-6'} className={'bg-shade-6 m-2 h-[100px] w-[100px]'}></div>
|
||||
<div title={'line-divider'} className={'m-2 h-[100px] w-[100px] bg-line-divider'}></div>
|
||||
</div>
|
||||
<h2 className={'mb-4'}>Surface</h2>
|
||||
<div className={'mb-8 flex flex-wrap items-center'}>
|
||||
|
@ -51,19 +51,19 @@ export class SelectOptionCellBackendService {
|
||||
const payload = RepeatedSelectOptionPayload.fromObject({
|
||||
view_id: this.cellIdentifier.viewId,
|
||||
field_id: this.cellIdentifier.fieldId,
|
||||
row_id: this.cellIdentifier.rowId,
|
||||
items: [option],
|
||||
});
|
||||
payload.items.push(option);
|
||||
|
||||
return DatabaseEventInsertOrUpdateSelectOption(payload);
|
||||
};
|
||||
|
||||
updateOption = (option: SelectOptionPB) => {
|
||||
updateOption = async (option: SelectOptionPB) => {
|
||||
const payload = RepeatedSelectOptionPayload.fromObject({
|
||||
view_id: this.cellIdentifier.viewId,
|
||||
field_id: this.cellIdentifier.fieldId,
|
||||
row_id: this.cellIdentifier.rowId,
|
||||
items: [option],
|
||||
});
|
||||
payload.items.push(option);
|
||||
|
||||
return DatabaseEventInsertOrUpdateSelectOption(payload);
|
||||
};
|
||||
|
||||
|
@ -11,13 +11,17 @@ import {
|
||||
DatabaseEventMoveGroup,
|
||||
DatabaseEventMoveGroupRow,
|
||||
DatabaseEventMoveRow,
|
||||
DatabaseEventUpdateField,
|
||||
DatabaseGroupIdPB,
|
||||
FieldChangesetPB,
|
||||
MoveFieldPayloadPB,
|
||||
MoveGroupPayloadPB,
|
||||
MoveGroupRowPayloadPB,
|
||||
MoveRowPayloadPB,
|
||||
RowIdPB,
|
||||
DatabaseEventUpdateDatabaseSetting,
|
||||
DuplicateFieldPayloadPB,
|
||||
DatabaseEventDuplicateField,
|
||||
} from '@/services/backend/events/flowy-database2';
|
||||
import {
|
||||
GetFieldPayloadPB,
|
||||
@ -28,6 +32,8 @@ import {
|
||||
ViewIdPB,
|
||||
} from '@/services/backend';
|
||||
import { FolderEventCloseView } from '@/services/backend/events/flowy-folder2';
|
||||
import { TypeOptionController } from '$app/stores/effects/database/field/type_option/type_option_controller';
|
||||
import { None } from 'ts-results';
|
||||
|
||||
/// A service that wraps the backend service
|
||||
export class DatabaseBackendService {
|
||||
@ -84,6 +90,11 @@ export class DatabaseBackendService {
|
||||
return DatabaseEventDeleteRow(payload);
|
||||
};
|
||||
|
||||
moveRow = async (fromRowId: string, toRowId: string) => {
|
||||
const payload = MoveRowPayloadPB.fromObject({ view_id: this.viewId, from_row_id: fromRowId, to_row_id: toRowId });
|
||||
return DatabaseEventMoveRow(payload);
|
||||
};
|
||||
|
||||
/// Move the row from one group to another group
|
||||
/// [toRowId] is used to locate the moving row location.
|
||||
moveGroupRow = (fromRowId: string, toGroupId: string, toRowId?: string) => {
|
||||
@ -139,6 +150,24 @@ export class DatabaseBackendService {
|
||||
return DatabaseEventMoveField(payload);
|
||||
};
|
||||
|
||||
changeWidth = (params: { fieldId: string; width: number }) => {
|
||||
const payload = FieldChangesetPB.fromObject({ view_id: this.viewId, field_id: params.fieldId, width: params.width });
|
||||
|
||||
return DatabaseEventUpdateField(payload);
|
||||
};
|
||||
|
||||
duplicateField = (fieldId: string) => {
|
||||
const payload = DuplicateFieldPayloadPB.fromObject({ view_id: this.viewId, field_id: fieldId });
|
||||
|
||||
return DatabaseEventDuplicateField(payload);
|
||||
};
|
||||
|
||||
createField = async () => {
|
||||
const fieldController = new TypeOptionController(this.viewId, None);
|
||||
|
||||
await fieldController.initialize();
|
||||
};
|
||||
|
||||
/// Get all groups in database
|
||||
/// It should only call once after the board open
|
||||
loadGroups = () => {
|
||||
|
@ -8,11 +8,17 @@ import { DatabaseGroupController } from './group/group_controller';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { DatabaseGroupObserver } from './group/group_observer';
|
||||
import { Log } from '$app/utils/log';
|
||||
import { FilterController } from '$app/stores/effects/database/filter/filter_controller';
|
||||
import { FilterParsed } from '$app/stores/effects/database/filter/filter_bd_svc';
|
||||
import { SortController } from '$app/stores/effects/database/sort/sort_controller';
|
||||
import { IDatabaseSort } from '$app_reducers/database/slice';
|
||||
|
||||
export type DatabaseSubscriberCallbacks = {
|
||||
onViewChanged?: (data: DatabasePB) => void;
|
||||
onRowsChanged?: (rowInfos: readonly RowInfo[], reason: RowChangedReason) => void;
|
||||
onFieldsChanged?: (fieldInfos: readonly FieldInfo[]) => void;
|
||||
onFiltersChanged?: (filters: readonly FilterParsed[]) => void;
|
||||
onSortChanged?: (sorts: readonly IDatabaseSort[]) => void;
|
||||
onGroupByField?: (groups: GroupPB[]) => void;
|
||||
|
||||
onNumOfGroupChanged?: {
|
||||
@ -25,6 +31,8 @@ export type DatabaseSubscriberCallbacks = {
|
||||
export class DatabaseController {
|
||||
private readonly backendService: DatabaseBackendService;
|
||||
fieldController: FieldController;
|
||||
sortController: SortController;
|
||||
filterController: FilterController;
|
||||
databaseViewCache: DatabaseViewCache;
|
||||
private _callback?: DatabaseSubscriberCallbacks;
|
||||
public groups: BehaviorSubject<DatabaseGroupController[]>;
|
||||
@ -33,6 +41,8 @@ export class DatabaseController {
|
||||
constructor(public readonly viewId: string) {
|
||||
this.backendService = new DatabaseBackendService(viewId);
|
||||
this.fieldController = new FieldController(viewId);
|
||||
this.filterController = new FilterController(viewId);
|
||||
this.sortController = new SortController(viewId);
|
||||
this.databaseViewCache = new DatabaseViewCache(viewId, this.fieldController);
|
||||
this.groups = new BehaviorSubject<DatabaseGroupController[]>([]);
|
||||
this.groupsObserver = new DatabaseGroupObserver(viewId);
|
||||
@ -41,6 +51,8 @@ export class DatabaseController {
|
||||
subscribe = (callbacks: DatabaseSubscriberCallbacks) => {
|
||||
this._callback = callbacks;
|
||||
this.fieldController.subscribe({ onNumOfFieldsChanged: callbacks.onFieldsChanged });
|
||||
this.filterController.subscribe({ onFiltersChanged: callbacks.onFiltersChanged });
|
||||
this.sortController.subscribe({ onSortChanged: callbacks.onSortChanged });
|
||||
this.databaseViewCache.getRowCache().subscribe({
|
||||
onRowsChanged: (reason) => {
|
||||
this._callback?.onRowsChanged?.(this.databaseViewCache.rowInfos, reason);
|
||||
@ -50,10 +62,14 @@ export class DatabaseController {
|
||||
|
||||
open = async () => {
|
||||
const openDatabaseResult = await this.backendService.openDatabase();
|
||||
|
||||
if (openDatabaseResult.ok) {
|
||||
const database: DatabasePB = openDatabaseResult.val;
|
||||
|
||||
await this.databaseViewCache.initialize();
|
||||
await this.fieldController.initialize();
|
||||
await this.filterController.initialize();
|
||||
await this.sortController.initialize();
|
||||
|
||||
// subscriptions
|
||||
await this.subscribeOnGroupsChanged();
|
||||
@ -73,12 +89,15 @@ export class DatabaseController {
|
||||
|
||||
getGroupByFieldId = async () => {
|
||||
const settingsResult = await this.backendService.getSettings();
|
||||
|
||||
if (settingsResult.ok) {
|
||||
const settings = settingsResult.val;
|
||||
const groupConfig = settings.group_settings.items;
|
||||
|
||||
if (groupConfig.length === 0) {
|
||||
return Err(new FlowyError({ msg: 'this database has no groups' }));
|
||||
}
|
||||
|
||||
return Ok(settings.group_settings.items[0].field_id);
|
||||
} else {
|
||||
return Err(settingsResult.val);
|
||||
@ -89,6 +108,10 @@ export class DatabaseController {
|
||||
return this.backendService.createRow();
|
||||
};
|
||||
|
||||
createRowAfter = (rowId: string) => {
|
||||
return this.backendService.createRow({ rowId });
|
||||
};
|
||||
|
||||
duplicateRow = async (rowId: string) => {
|
||||
return this.backendService.duplicateRow(rowId);
|
||||
};
|
||||
@ -97,6 +120,10 @@ export class DatabaseController {
|
||||
return this.backendService.deleteRow(rowId);
|
||||
};
|
||||
|
||||
moveRow = (fromRowId: string, toRowId: string) => {
|
||||
return this.backendService.moveRow(fromRowId, toRowId);
|
||||
};
|
||||
|
||||
moveGroupRow = (rowId: string, groupId: string) => {
|
||||
return this.backendService.moveGroupRow(rowId, groupId);
|
||||
};
|
||||
@ -114,12 +141,51 @@ export class DatabaseController {
|
||||
return this.backendService.moveField(params);
|
||||
};
|
||||
|
||||
changeWidth = (params: { fieldId: string; width: number }) => {
|
||||
return this.backendService.changeWidth(params);
|
||||
};
|
||||
|
||||
duplicateField = (fieldId: string) => {
|
||||
return this.backendService.duplicateField(fieldId);
|
||||
};
|
||||
|
||||
addFieldToLeft = async (fieldId: string) => {
|
||||
const index = this.fieldController.fieldInfos.findIndex((fieldInfo) => fieldInfo.field.id === fieldId);
|
||||
|
||||
await this.backendService.createField();
|
||||
|
||||
const newFieldId = this.fieldController.fieldInfos[this.fieldController.fieldInfos.length - 1].field.id;
|
||||
|
||||
await this.moveField({
|
||||
fieldId: newFieldId,
|
||||
fromIndex: this.fieldController.fieldInfos.length - 1,
|
||||
toIndex: index,
|
||||
});
|
||||
};
|
||||
|
||||
addFieldToRight = async (fieldId: string) => {
|
||||
const index = this.fieldController.fieldInfos.findIndex((fieldInfo) => fieldInfo.field.id === fieldId);
|
||||
|
||||
await this.backendService.createField();
|
||||
|
||||
const newFieldId = this.fieldController.fieldInfos[this.fieldController.fieldInfos.length - 1].field.id;
|
||||
|
||||
await this.moveField({
|
||||
fieldId: newFieldId,
|
||||
fromIndex: this.fieldController.fieldInfos.length - 1,
|
||||
toIndex: index + 1,
|
||||
});
|
||||
};
|
||||
|
||||
private loadGroup = async () => {
|
||||
const result = await this.backendService.loadGroups();
|
||||
|
||||
if (result.ok) {
|
||||
const groups = result.val.items;
|
||||
|
||||
await this.initialGroups(groups);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
@ -129,11 +195,14 @@ export class DatabaseController {
|
||||
});
|
||||
|
||||
const controllers: DatabaseGroupController[] = [];
|
||||
|
||||
for (const groupPB of groups) {
|
||||
const controller = new DatabaseGroupController(groupPB, this.backendService);
|
||||
|
||||
await controller.initialize();
|
||||
controllers.push(controller);
|
||||
}
|
||||
|
||||
this.groups.next(controllers);
|
||||
this.groups.value;
|
||||
};
|
||||
@ -150,14 +219,17 @@ export class DatabaseController {
|
||||
Log.error(result.val);
|
||||
return;
|
||||
}
|
||||
|
||||
const changeset = result.val;
|
||||
let existControllers = [...this.groups.getValue()];
|
||||
|
||||
for (const deleteId of changeset.deleted_groups) {
|
||||
existControllers = existControllers.filter((c) => c.groupId !== deleteId);
|
||||
}
|
||||
|
||||
for (const update of changeset.update_groups) {
|
||||
const index = existControllers.findIndex((c) => c.groupId === update.group_id);
|
||||
|
||||
if (index !== -1) {
|
||||
existControllers[index].updateGroup(update);
|
||||
}
|
||||
@ -165,12 +237,14 @@ export class DatabaseController {
|
||||
|
||||
for (const insert of changeset.inserted_groups) {
|
||||
const controller = new DatabaseGroupController(insert.group, this.backendService);
|
||||
|
||||
if (insert.index > existControllers.length) {
|
||||
existControllers.push(controller);
|
||||
} else {
|
||||
existControllers.splice(insert.index, 0, controller);
|
||||
}
|
||||
}
|
||||
|
||||
this.groups.next(existControllers);
|
||||
},
|
||||
});
|
||||
@ -183,6 +257,7 @@ export class DatabaseController {
|
||||
await this.groupsObserver.unsubscribe();
|
||||
await this.backendService.closeDatabase();
|
||||
await this.fieldController.dispose();
|
||||
this.filterController.dispose();
|
||||
await this.databaseViewCache.dispose();
|
||||
};
|
||||
}
|
||||
|
@ -89,6 +89,14 @@ export class TypeOptionController {
|
||||
}
|
||||
};
|
||||
|
||||
changeWidth = async (width: number) => {
|
||||
if (this.fieldBackendSvc) {
|
||||
void this.fieldBackendSvc.updateField({ width: width });
|
||||
} else {
|
||||
throw Error('Unexpected empty field backend service');
|
||||
}
|
||||
};
|
||||
|
||||
saveTypeOption = async (data: Uint8Array) => {
|
||||
if (this.typeOptionData.some) {
|
||||
this.typeOptionData.val.type_option_data = data;
|
||||
|
@ -0,0 +1,127 @@
|
||||
import {
|
||||
CheckboxFilterPB,
|
||||
DatabaseSettingChangesetPB,
|
||||
DatabaseViewIdPB,
|
||||
DeleteFilterPayloadPB,
|
||||
FieldType,
|
||||
FilterPB,
|
||||
FlowyError,
|
||||
SelectOptionFilterPB,
|
||||
TextFilterPB,
|
||||
UpdateFilterPayloadPB,
|
||||
} from '@/services/backend';
|
||||
import {
|
||||
DatabaseEventGetAllFilters,
|
||||
DatabaseEventUpdateDatabaseSetting,
|
||||
} from '@/services/backend/events/flowy-database2';
|
||||
import { Err, Ok, Result } from 'ts-results';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
export class FilterBackendService {
|
||||
constructor(public readonly viewId: string) {}
|
||||
|
||||
getFilters = async (): Promise<Result<FilterParsed[], FlowyError>> => {
|
||||
const payload = DatabaseViewIdPB.fromObject({
|
||||
value: this.viewId,
|
||||
});
|
||||
|
||||
const res = await DatabaseEventGetAllFilters(payload);
|
||||
|
||||
if (res.ok) {
|
||||
return Ok(res.val.items.map<FilterParsed>((f) => new FilterParsed(this.viewId, f)));
|
||||
} else {
|
||||
return Err(res.val);
|
||||
}
|
||||
};
|
||||
|
||||
addFilter = async (
|
||||
fieldId: string,
|
||||
fieldType: FieldType,
|
||||
filter: TextFilterPB | SelectOptionFilterPB | CheckboxFilterPB
|
||||
) => {
|
||||
const data = filter.serializeBinary();
|
||||
const id = nanoid(4);
|
||||
|
||||
await DatabaseEventUpdateDatabaseSetting(
|
||||
DatabaseSettingChangesetPB.fromObject({
|
||||
view_id: this.viewId,
|
||||
update_filter: UpdateFilterPayloadPB.fromObject({
|
||||
filter_id: id,
|
||||
view_id: this.viewId,
|
||||
field_id: fieldId,
|
||||
field_type: fieldType,
|
||||
data,
|
||||
}),
|
||||
})
|
||||
);
|
||||
return id;
|
||||
};
|
||||
|
||||
updateFilter = (
|
||||
filterId: string,
|
||||
fieldId: string,
|
||||
fieldType: FieldType,
|
||||
filter: TextFilterPB | SelectOptionFilterPB | CheckboxFilterPB
|
||||
) => {
|
||||
const data = filter.serializeBinary();
|
||||
|
||||
return DatabaseEventUpdateDatabaseSetting(
|
||||
DatabaseSettingChangesetPB.fromObject({
|
||||
view_id: this.viewId,
|
||||
update_filter: UpdateFilterPayloadPB.fromObject({
|
||||
view_id: this.viewId,
|
||||
field_id: fieldId,
|
||||
field_type: fieldType,
|
||||
filter_id: filterId,
|
||||
data,
|
||||
}),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
removeFilter = (fieldId: string, fieldType: FieldType, filterId: string) => {
|
||||
return DatabaseEventUpdateDatabaseSetting(
|
||||
DatabaseSettingChangesetPB.fromObject({
|
||||
view_id: this.viewId,
|
||||
delete_filter: DeleteFilterPayloadPB.fromObject({
|
||||
view_id: this.viewId,
|
||||
field_id: fieldId,
|
||||
field_type: fieldType,
|
||||
filter_id: filterId,
|
||||
}),
|
||||
})
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export class FilterParsed {
|
||||
view_id: string;
|
||||
id: string;
|
||||
field_id: string;
|
||||
field_type: FieldType;
|
||||
data: TextFilterPB | SelectOptionFilterPB | CheckboxFilterPB | Uint8Array;
|
||||
|
||||
constructor(view_id: string, filter: FilterPB) {
|
||||
this.view_id = view_id;
|
||||
|
||||
this.id = filter.id;
|
||||
this.field_id = filter.field_id;
|
||||
this.field_type = filter.field_type;
|
||||
|
||||
switch (filter.field_type) {
|
||||
case FieldType.RichText:
|
||||
this.data = TextFilterPB.deserializeBinary(filter.data);
|
||||
break;
|
||||
case FieldType.SingleSelect:
|
||||
case FieldType.MultiSelect:
|
||||
this.data = SelectOptionFilterPB.deserializeBinary(filter.data);
|
||||
break;
|
||||
case FieldType.Checkbox:
|
||||
this.data = CheckboxFilterPB.deserializeBinary(filter.data);
|
||||
break;
|
||||
default:
|
||||
this.data = filter.data;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
import { FilterBackendService, FilterParsed } from '$app/stores/effects/database/filter/filter_bd_svc';
|
||||
import { CheckboxFilterPB, FieldType, SelectOptionFilterPB, TextFilterPB } from '@/services/backend';
|
||||
import { ChangeNotifier } from '$app/utils/change_notifier';
|
||||
|
||||
export class FilterController {
|
||||
filterService: FilterBackendService;
|
||||
notifier: FilterNotifier;
|
||||
|
||||
constructor(public readonly viewId: string) {
|
||||
this.filterService = new FilterBackendService(viewId);
|
||||
this.notifier = new FilterNotifier();
|
||||
}
|
||||
|
||||
initialize = async () => {
|
||||
await this.readFilters();
|
||||
};
|
||||
|
||||
readFilters = async () => {
|
||||
const result = await this.filterService.getFilters();
|
||||
|
||||
if (result.ok) {
|
||||
this.notifier.filters = result.val;
|
||||
}
|
||||
};
|
||||
|
||||
addFilter = async (
|
||||
fieldId: string,
|
||||
fieldType: FieldType,
|
||||
filter: TextFilterPB | SelectOptionFilterPB | CheckboxFilterPB
|
||||
) => {
|
||||
const id = await this.filterService.addFilter(fieldId, fieldType, filter);
|
||||
|
||||
await this.readFilters();
|
||||
return id;
|
||||
};
|
||||
|
||||
updateFilter = async (
|
||||
filterId: string,
|
||||
fieldId: string,
|
||||
fieldType: FieldType,
|
||||
filter: TextFilterPB | SelectOptionFilterPB | CheckboxFilterPB
|
||||
) => {
|
||||
const result = await this.filterService.updateFilter(filterId, fieldId, fieldType, filter);
|
||||
|
||||
if (result.ok) {
|
||||
await this.readFilters();
|
||||
}
|
||||
};
|
||||
|
||||
removeFilter = async (fieldId: string, fieldType: FieldType, filterId: string) => {
|
||||
const result = await this.filterService.removeFilter(fieldId, fieldType, filterId);
|
||||
|
||||
if (result.ok) {
|
||||
await this.readFilters();
|
||||
}
|
||||
};
|
||||
|
||||
subscribe = (callbacks: { onFiltersChanged?: (filters: FilterParsed[]) => void }) => {
|
||||
if (callbacks.onFiltersChanged) {
|
||||
this.notifier.observer?.subscribe(callbacks.onFiltersChanged);
|
||||
}
|
||||
};
|
||||
|
||||
dispose = () => {
|
||||
this.notifier.unsubscribe();
|
||||
};
|
||||
}
|
||||
|
||||
class FilterNotifier extends ChangeNotifier<FilterParsed[]> {
|
||||
private _filters: FilterParsed[] = [];
|
||||
|
||||
get filters(): FilterParsed[] {
|
||||
return this._filters;
|
||||
}
|
||||
|
||||
set filters(value: FilterParsed[]) {
|
||||
this._filters = value;
|
||||
this.notify(value);
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
import {
|
||||
DatabaseSettingChangesetPB,
|
||||
DatabaseViewIdPB,
|
||||
DeleteSortPayloadPB,
|
||||
FieldType,
|
||||
FlowyError,
|
||||
SortConditionPB,
|
||||
UpdateSortPayloadPB,
|
||||
} from '@/services/backend';
|
||||
import { DatabaseEventGetAllSorts, DatabaseEventUpdateDatabaseSetting } from '@/services/backend/events/flowy-database2';
|
||||
import { Err, Ok, Result } from 'ts-results';
|
||||
import { nanoid } from 'nanoid';
|
||||
import type { IDatabaseSort } from '$app_reducers/database/slice';
|
||||
|
||||
export class SortBackendService {
|
||||
constructor(public readonly viewId: string) {}
|
||||
|
||||
getSorts = async (): Promise<Result<IDatabaseSort[], FlowyError>> => {
|
||||
const payload = DatabaseViewIdPB.fromObject({
|
||||
value: this.viewId,
|
||||
});
|
||||
|
||||
const res = await DatabaseEventGetAllSorts(payload);
|
||||
|
||||
if (res.ok) {
|
||||
return Ok(
|
||||
res.val.items.map<IDatabaseSort>((o) => ({
|
||||
id: o.id,
|
||||
fieldId: o.field_id,
|
||||
fieldType: o.field_type,
|
||||
order: o.condition,
|
||||
}))
|
||||
);
|
||||
} else {
|
||||
return Err(res.val);
|
||||
}
|
||||
};
|
||||
|
||||
addSort = async (fieldId: string, fieldType: FieldType, order: SortConditionPB) => {
|
||||
const id = nanoid(4);
|
||||
|
||||
await DatabaseEventUpdateDatabaseSetting(
|
||||
DatabaseSettingChangesetPB.fromObject({
|
||||
view_id: this.viewId,
|
||||
update_sort: UpdateSortPayloadPB.fromObject({
|
||||
view_id: this.viewId,
|
||||
field_id: fieldId,
|
||||
field_type: fieldType,
|
||||
sort_id: id,
|
||||
condition: order,
|
||||
}),
|
||||
})
|
||||
);
|
||||
return id;
|
||||
};
|
||||
|
||||
updateSort = (sortId: string, fieldId: string, fieldType: FieldType, order: SortConditionPB) => {
|
||||
return DatabaseEventUpdateDatabaseSetting(
|
||||
DatabaseSettingChangesetPB.fromObject({
|
||||
view_id: this.viewId,
|
||||
update_sort: UpdateSortPayloadPB.fromObject({
|
||||
view_id: this.viewId,
|
||||
field_id: fieldId,
|
||||
field_type: fieldType,
|
||||
sort_id: sortId,
|
||||
condition: order,
|
||||
}),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
removeSort = (fieldId: string, fieldType: FieldType, sortId: string) => {
|
||||
return DatabaseEventUpdateDatabaseSetting(
|
||||
DatabaseSettingChangesetPB.fromObject({
|
||||
view_id: this.viewId,
|
||||
delete_sort: DeleteSortPayloadPB.fromObject({
|
||||
view_id: this.viewId,
|
||||
field_id: fieldId,
|
||||
field_type: fieldType,
|
||||
sort_id: sortId,
|
||||
}),
|
||||
})
|
||||
);
|
||||
};
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
import { FieldType, SortConditionPB } from '@/services/backend';
|
||||
import { ChangeNotifier } from '$app/utils/change_notifier';
|
||||
import { IDatabaseSort } from '$app_reducers/database/slice';
|
||||
import { SortBackendService } from '$app/stores/effects/database/sort/sort_bd_svc';
|
||||
|
||||
export class SortController {
|
||||
sortService: SortBackendService;
|
||||
notifier: SortNotifier;
|
||||
|
||||
constructor(public readonly viewId: string) {
|
||||
this.sortService = new SortBackendService(viewId);
|
||||
this.notifier = new SortNotifier();
|
||||
}
|
||||
|
||||
initialize = async () => {
|
||||
await this.readSorts();
|
||||
};
|
||||
|
||||
readSorts = async () => {
|
||||
const result = await this.sortService.getSorts();
|
||||
|
||||
if (result.ok) {
|
||||
this.notifier.sorts = result.val;
|
||||
}
|
||||
};
|
||||
|
||||
addSort = async (fieldId: string, fieldType: FieldType, sort: SortConditionPB) => {
|
||||
const id = await this.sortService.addSort(fieldId, fieldType, sort);
|
||||
|
||||
await this.readSorts();
|
||||
return id;
|
||||
};
|
||||
|
||||
updateSort = async (sortId: string, fieldId: string, fieldType: FieldType, sort: SortConditionPB) => {
|
||||
const result = await this.sortService.updateSort(sortId, fieldId, fieldType, sort);
|
||||
|
||||
if (result.ok) {
|
||||
await this.readSorts();
|
||||
}
|
||||
};
|
||||
|
||||
removeSort = async (fieldId: string, fieldType: FieldType, sortId: string) => {
|
||||
const result = await this.sortService.removeSort(fieldId, fieldType, sortId);
|
||||
|
||||
if (result.ok) {
|
||||
await this.readSorts();
|
||||
}
|
||||
};
|
||||
|
||||
subscribe = (callbacks: { onSortChanged?: (sorts: IDatabaseSort[]) => void }) => {
|
||||
if (callbacks.onSortChanged) {
|
||||
this.notifier.observer?.subscribe(callbacks.onSortChanged);
|
||||
}
|
||||
};
|
||||
|
||||
dispose = () => {
|
||||
this.notifier.unsubscribe();
|
||||
};
|
||||
}
|
||||
|
||||
class SortNotifier extends ChangeNotifier<IDatabaseSort[]> {
|
||||
private _sorts: IDatabaseSort[] = [];
|
||||
|
||||
get sorts(): IDatabaseSort[] {
|
||||
return this._sorts;
|
||||
}
|
||||
|
||||
set sorts(value: IDatabaseSort[]) {
|
||||
this._sorts = value;
|
||||
this.notify(value);
|
||||
}
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { FieldType } from '@/services/backend/models/flowy-database2/field_entities';
|
||||
import { DateFormatPB, NumberFormatPB, SelectOptionColorPB, TimeFormatPB } from '@/services/backend';
|
||||
import { DateFormatPB, NumberFormatPB, SelectOptionColorPB, SortConditionPB, TimeFormatPB } from '@/services/backend';
|
||||
|
||||
export interface ISelectOption {
|
||||
selectOptionId: string;
|
||||
title: string;
|
||||
color?: SelectOptionColorPB;
|
||||
color: SelectOptionColorPB;
|
||||
}
|
||||
|
||||
export interface ISelectOptionType {
|
||||
@ -26,6 +26,7 @@ export interface IDatabaseField {
|
||||
fieldId: string;
|
||||
title: string;
|
||||
visible: boolean;
|
||||
width: number;
|
||||
fieldType: FieldType;
|
||||
fieldOptions?: ISelectOptionType | IDateType | INumberType;
|
||||
}
|
||||
@ -42,11 +43,51 @@ export interface IDatabaseRow {
|
||||
|
||||
export type DatabaseFieldMap = { [keys: string]: IDatabaseField };
|
||||
|
||||
export type TDatabaseOperators =
|
||||
| 'contains'
|
||||
| 'doesNotContain'
|
||||
| 'endsWith'
|
||||
| 'startWith'
|
||||
| 'is'
|
||||
| 'isNot'
|
||||
| 'isEmpty'
|
||||
| 'isNotEmpty'
|
||||
| 'isComplete'
|
||||
| 'isIncomplted';
|
||||
|
||||
export type TSupportedOperatorsByType = { [keys: number]: TDatabaseOperators[] };
|
||||
|
||||
export const SupportedOperatorsByType: TSupportedOperatorsByType = {
|
||||
[FieldType.RichText]: ['contains', 'doesNotContain', 'endsWith', 'startWith', 'is', 'isNot', 'isEmpty', 'isNotEmpty'],
|
||||
[FieldType.SingleSelect]: ['is', 'isNot', 'isEmpty', 'isNotEmpty'],
|
||||
[FieldType.MultiSelect]: ['contains', 'doesNotContain', 'isEmpty', 'isNotEmpty'],
|
||||
[FieldType.Checkbox]: ['is'],
|
||||
[FieldType.Checklist]: ['isComplete', 'isIncomplted'],
|
||||
};
|
||||
|
||||
export interface IDatabaseFilter {
|
||||
id?: string;
|
||||
fieldId: string;
|
||||
fieldType: FieldType;
|
||||
logicalOperator: 'and' | 'or';
|
||||
operator: TDatabaseOperators;
|
||||
value: string[] | string | boolean;
|
||||
}
|
||||
|
||||
export interface IDatabaseSort {
|
||||
id?: string;
|
||||
fieldId: string;
|
||||
fieldType: FieldType;
|
||||
order: SortConditionPB;
|
||||
}
|
||||
|
||||
export interface IDatabase {
|
||||
title: string;
|
||||
fields: DatabaseFieldMap;
|
||||
rows: IDatabaseRow[];
|
||||
columns: IDatabaseColumn[];
|
||||
filters: IDatabaseFilter[];
|
||||
sort: IDatabaseSort[];
|
||||
}
|
||||
|
||||
const initialState: IDatabase = {
|
||||
@ -54,6 +95,8 @@ const initialState: IDatabase = {
|
||||
columns: [],
|
||||
fields: {},
|
||||
rows: [],
|
||||
filters: [],
|
||||
sort: [],
|
||||
};
|
||||
|
||||
export const databaseSlice = createSlice({
|
||||
@ -89,87 +132,25 @@ export const databaseSlice = createSlice({
|
||||
state.title = action.payload.title;
|
||||
},
|
||||
|
||||
/*addField: (state, action: PayloadAction<{ field: IDatabaseField }>) => {
|
||||
const { field } = action.payload;
|
||||
|
||||
state.fields[field.fieldId] = field;
|
||||
state.columns.push({
|
||||
fieldId: field.fieldId,
|
||||
sort: 'none',
|
||||
visible: true,
|
||||
});
|
||||
state.rows = state.rows.map<IDatabaseRow>((r: IDatabaseRow) => {
|
||||
const cells = r.cells;
|
||||
cells[field.fieldId] = {
|
||||
rowId: r.rowId,
|
||||
fieldId: field.fieldId,
|
||||
data: [''],
|
||||
cellId: nanoid(6),
|
||||
};
|
||||
return {
|
||||
rowId: r.rowId,
|
||||
cells: cells,
|
||||
};
|
||||
});
|
||||
},*/
|
||||
|
||||
updateField: (state, action: PayloadAction<{ field: IDatabaseField }>) => {
|
||||
const { field } = action.payload;
|
||||
|
||||
state.fields[field.fieldId] = field;
|
||||
},
|
||||
|
||||
/*addFieldSelectOption: (state, action: PayloadAction<{ fieldId: string; option: ISelectOption }>) => {
|
||||
const { fieldId, option } = action.payload;
|
||||
changeWidth: (state, action: PayloadAction<{ fieldId: string; width: number }>) => {
|
||||
const { fieldId, width } = action.payload;
|
||||
|
||||
const field = state.fields[fieldId];
|
||||
const selectOptions = field.fieldOptions?.selectOptions;
|
||||
state.fields[fieldId].width = width;
|
||||
},
|
||||
|
||||
if (selectOptions) {
|
||||
selectOptions.push(option);
|
||||
} else {
|
||||
state.fields[field.fieldId].fieldOptions = {
|
||||
...state.fields[field.fieldId].fieldOptions,
|
||||
selectOptions: [option],
|
||||
};
|
||||
}
|
||||
},*/
|
||||
updateFilters: (state, action: PayloadAction<{ filters: IDatabaseFilter[] }>) => {
|
||||
state.filters = action.payload.filters;
|
||||
},
|
||||
|
||||
/*updateFieldSelectOption: (state, action: PayloadAction<{ fieldId: string; option: ISelectOption }>) => {
|
||||
const { fieldId, option } = action.payload;
|
||||
|
||||
const field = state.fields[fieldId];
|
||||
const selectOptions = field.fieldOptions?.selectOptions;
|
||||
if (selectOptions) {
|
||||
selectOptions[selectOptions.findIndex((o) => o.selectOptionId === option.selectOptionId)] = option;
|
||||
}
|
||||
},*/
|
||||
|
||||
/*addRow: (state) => {
|
||||
const rowId = nanoid(6);
|
||||
const cells: { [keys: string]: ICellData } = {};
|
||||
Object.keys(state.fields).forEach((id) => {
|
||||
cells[id] = {
|
||||
rowId: rowId,
|
||||
fieldId: id,
|
||||
data: [''],
|
||||
cellId: nanoid(6),
|
||||
};
|
||||
});
|
||||
const newRow: IDatabaseRow = {
|
||||
rowId: rowId,
|
||||
cells: cells,
|
||||
};
|
||||
|
||||
state.rows.push(newRow);
|
||||
},*/
|
||||
|
||||
/*updateCellValue: (source, action: PayloadAction<{ cell: ICellData }>) => {
|
||||
const { cell } = action.payload;
|
||||
const row = source.rows.find((r) => r.rowId === cell.rowId);
|
||||
if (row) {
|
||||
row.cells[cell.fieldId] = cell;
|
||||
}
|
||||
},*/
|
||||
updateSorts: (state, action: PayloadAction<{ sorts: IDatabaseSort[] }>) => {
|
||||
state.sort = action.payload.sorts;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -1,141 +0,0 @@
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { FieldType } from '@/services/backend/models/flowy-database2/field_entities';
|
||||
|
||||
const initialState = {
|
||||
title: 'My plans on the week',
|
||||
fields: [
|
||||
{
|
||||
fieldId: '1',
|
||||
fieldType: FieldType.RichText,
|
||||
fieldOptions: {},
|
||||
name: 'Todo',
|
||||
},
|
||||
{
|
||||
fieldId: '2',
|
||||
fieldType: FieldType.SingleSelect,
|
||||
fieldOptions: [],
|
||||
name: 'Status',
|
||||
},
|
||||
{
|
||||
fieldId: '3',
|
||||
fieldType: FieldType.Number,
|
||||
fieldOptions: [],
|
||||
name: 'Progress',
|
||||
},
|
||||
{
|
||||
fieldId: '4',
|
||||
fieldType: FieldType.DateTime,
|
||||
fieldOptions: [],
|
||||
name: 'Due Date',
|
||||
},
|
||||
],
|
||||
rows: [
|
||||
{
|
||||
rowId: '1',
|
||||
values: [
|
||||
{
|
||||
fieldId: '1',
|
||||
value: 'Name 1',
|
||||
cellId: '1',
|
||||
},
|
||||
{
|
||||
fieldId: '2',
|
||||
value: 'Status 1',
|
||||
cellId: '2',
|
||||
},
|
||||
{
|
||||
fieldId: '3',
|
||||
value: 30,
|
||||
cellId: '3',
|
||||
},
|
||||
{
|
||||
fieldId: '4',
|
||||
value: 'tomorrow',
|
||||
cellId: '4',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
rowId: '2',
|
||||
values: [
|
||||
{
|
||||
fieldId: '1',
|
||||
value: 'Name 2',
|
||||
cellId: '5',
|
||||
},
|
||||
{
|
||||
fieldId: '2',
|
||||
value: 'Status 2',
|
||||
cellId: '6',
|
||||
},
|
||||
{
|
||||
fieldId: '3',
|
||||
value: 40,
|
||||
cellId: '7',
|
||||
},
|
||||
{
|
||||
fieldId: '4',
|
||||
value: 'tomorrow',
|
||||
cellId: '8',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export type field = {
|
||||
fieldId: string;
|
||||
fieldType: FieldType;
|
||||
fieldOptions: any;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export const gridSlice = createSlice({
|
||||
name: 'grid',
|
||||
initialState: initialState,
|
||||
reducers: {
|
||||
updateGridTitle: (state, action: PayloadAction<{ title: string }>) => {
|
||||
state.title = action.payload.title;
|
||||
},
|
||||
|
||||
addField: (state, action: PayloadAction<{ field: field }>) => {
|
||||
state.fields.push(action.payload.field);
|
||||
|
||||
state.rows.map((row) => {
|
||||
row.values.push({
|
||||
fieldId: action.payload.field.fieldId,
|
||||
value: '',
|
||||
cellId: nanoid(),
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
addRow: (state) => {
|
||||
const newRow = {
|
||||
rowId: nanoid(),
|
||||
values: state.fields.map((f) => ({
|
||||
fieldId: f.fieldId,
|
||||
value: '',
|
||||
cellId: nanoid(),
|
||||
})),
|
||||
};
|
||||
|
||||
state.rows.push(newRow);
|
||||
},
|
||||
|
||||
updateRowValue: (state, action: PayloadAction<{ rowId: string; cellId: string; value: string | number }>) => {
|
||||
console.log('updateRowValue', action.payload);
|
||||
const row = state.rows.find((r) => r.rowId === action.payload.rowId);
|
||||
|
||||
if (row) {
|
||||
const cell = row.values.find((c) => c.cellId === action.payload.cellId);
|
||||
if (cell) {
|
||||
cell.value = action.payload.value;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const gridActions = gridSlice.actions;
|
@ -9,7 +9,6 @@ import {
|
||||
} from '@reduxjs/toolkit';
|
||||
import { pagesSlice } from './reducers/pages/slice';
|
||||
import { currentUserSlice } from './reducers/current-user/slice';
|
||||
import { gridSlice } from './reducers/grid/slice';
|
||||
import { workspaceSlice } from './reducers/workspace/slice';
|
||||
import { databaseSlice } from './reducers/database/slice';
|
||||
import { documentReducers } from './reducers/document/slice';
|
||||
@ -27,7 +26,6 @@ const store = configureStore({
|
||||
reducer: {
|
||||
[pagesSlice.name]: pagesSlice.reducer,
|
||||
[currentUserSlice.name]: currentUserSlice.reducer,
|
||||
[gridSlice.name]: gridSlice.reducer,
|
||||
[databaseSlice.name]: databaseSlice.reducer,
|
||||
[boardSlice.name]: boardSlice.reducer,
|
||||
[workspaceSlice.name]: workspaceSlice.reducer,
|
||||
|
@ -6,16 +6,15 @@ import { Grid } from '../components/grid/Grid/Grid';
|
||||
export const GridPage = () => {
|
||||
const params = useParams();
|
||||
const [viewId, setViewId] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (params?.id?.length) {
|
||||
setViewId(params.id);
|
||||
// setDatabaseId('testDb');
|
||||
}
|
||||
}, [params]);
|
||||
|
||||
return (
|
||||
<div className='flex h-full flex-col gap-8 px-8 pt-8'>
|
||||
<h1 className='text-4xl font-bold'>Grid: {viewId}</h1>
|
||||
{viewId?.length && <Grid viewId={viewId} />}
|
||||
</div>
|
||||
);
|
||||
|
@ -403,7 +403,8 @@
|
||||
"addOption": "Add option",
|
||||
"editProperty": "Edit property",
|
||||
"newProperty": "New property",
|
||||
"deleteFieldPromptMessage": "Are you sure? This property will be deleted"
|
||||
"deleteFieldPromptMessage": "Are you sure? This property will be deleted",
|
||||
"newColumn": "New Column"
|
||||
},
|
||||
"sort": {
|
||||
"ascending": "Ascending",
|
||||
|
Loading…
Reference in New Issue
Block a user