diff --git a/frontend/appflowy_tauri/index.html b/frontend/appflowy_tauri/index.html index 194012b116..64bc70f149 100644 --- a/frontend/appflowy_tauri/index.html +++ b/frontend/appflowy_tauri/index.html @@ -4,7 +4,7 @@ - Tauri + React + TS + AppFlowy: The Open Source Alternative To Notion diff --git a/frontend/appflowy_tauri/package.json b/frontend/appflowy_tauri/package.json index 88ef8ce5b8..cf13929dcc 100644 --- a/frontend/appflowy_tauri/package.json +++ b/frontend/appflowy_tauri/package.json @@ -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", diff --git a/frontend/appflowy_tauri/pnpm-lock.yaml b/frontend/appflowy_tauri/pnpm-lock.yaml index e1d2bfe99c..70b224092e 100644 --- a/frontend/appflowy_tauri/pnpm-lock.yaml +++ b/frontend/appflowy_tauri/pnpm-lock.yaml @@ -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'} diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index a564b5ffe8..8530acc68f 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -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", diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/Button.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/Button.tsx index 615bc1fdf5..e455831e74 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/Button.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/Button.tsx @@ -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': diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/ButtonPopoverList/index.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/ButtonPopoverList/index.tsx index f5e957d14e..840fc8924a 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/ButtonPopoverList/index.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/ButtonPopoverList/index.tsx @@ -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; } -function ButtonPopoverList({ popoverOrigin, isVisible, children, popoverOptions }: ButtonPopoverListProps) { + +function ButtonPopoverList({ popoverOrigin, isVisible, children, popoverOptions, onClose, sx }: ButtonPopoverListProps) { const [anchorEl, setAnchorEl] = useState(); const open = Boolean(anchorEl); const visible = isVisible || open; @@ -32,8 +36,16 @@ function ButtonPopoverList({ popoverOrigin, isVisible, children, popoverOptions <> {visible &&
{children}
} - - + { + handleClose(); + onClose?.(); + }} + > + {popoverOptions.map((option) => ( 0 && filledCheckListBars({ amount: completed })} {max - completed > 0 && emptyCheckListBars({ amount: max - completed })} -
{((100 * completed) / max).toFixed(0)}%
+
{((100 * completed) / max).toFixed(0)}%
)} @@ -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) =>
); + .map((item, index) =>
); }; const emptyCheckListBars = ({ amount }: { amount: number }) => { return Array(amount) .fill(0) - .map((item, index) =>
); + .map((item, index) =>
); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/DatabaseFilter/DatabaseFilterItem.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/DatabaseFilter/DatabaseFilterItem.tsx new file mode 100644 index 0000000000..87c766c814 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/DatabaseFilter/DatabaseFilterItem.tsx @@ -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(data?.fieldId ?? null); + const [currentOperator, setCurrentOperator] = useState(data?.operator ?? null); + const [currentValue, setCurrentValue] = useState(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 ( + <> +
+
+ {index === 0 ? ( + Where + ) : ( + + )} +
+ + + + + + + + +
+ + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/DatabaseFilter/DatabaseFilterPopup.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/DatabaseFilter/DatabaseFilterPopup.tsx new file mode 100644 index 0000000000..5dbc1e50ea --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/DatabaseFilter/DatabaseFilterPopup.tsx @@ -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 ( +
+
e.stopPropagation()} className='flex flex-col rounded-lg bg-bg-body shadow-md'> +
{t('grid.settings.filter')}
+ +
+ {rows.map((filter, index: number) => ( + onDeleteFilterItem(filter)} + key={index} + index={index} + > + ))} +
+ +
+ + +
+
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/DatabaseFilter/FieldSelect.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/DatabaseFilter/FieldSelect.tsx new file mode 100644 index 0000000000..c93e7d370b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/DatabaseFilter/FieldSelect.tsx @@ -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 ( + ({ + key: column.fieldId, + icon: ( + + + + ), + 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` }} + > +
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 ? ( +
+ + + + {fields[currentFieldId].title} +
+ ) : ( + Select a field + )} + + + +
+
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/DatabaseFilter/FilterValue.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/DatabaseFilter/FilterValue.tsx new file mode 100644 index 0000000000..28d562e40b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/DatabaseFilter/FilterValue.tsx @@ -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(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) && ( + <> +
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 ? ( +
+ {(currentValue as string[]).length === 0 && ( + none selected + )} + {(currentValue as string[]).map((option, i) => ( + + {getSelectOption(option)?.title} + + ))} +
+ ) : ( + Select an option + )} + + + + +
+ + setShowValueOptions(false)} + > +
+
Value option
+
+ {(fields[currentFieldId].fieldOptions as ISelectOptionType).selectOptions.map((option, index) => ( + o === option.selectOptionId) !== -1} + noSelect={true} + noDetail={true} + onOptionClick={() => onValueOptionClick(option)} + > + ))} +
+
+
+ + )} + {currentFieldType === FieldType.RichText && ( +
+ setTextInputActive(true)} + onBlur={() => setTextInputActive(false)} + value={currentValue as string} + onChange={(e) => setCurrentValue(e.target.value)} + /> +
+ )} + {currentFieldType === FieldType.Checkbox && ( +
setCurrentValue(!currentValue)} + className={`flex cursor-pointer items-center gap-2 rounded-lg border border-line-border px-2 py-1`} + style={{ width: `${WIDTH}px` }} + > + + {currentValue ? 'Checked' : 'Unchecked'} +
+ )} + + ) : ( +
+ Select field +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/DatabaseFilter/LogicalOperatorSelect.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/DatabaseFilter/LogicalOperatorSelect.tsx new file mode 100644 index 0000000000..e9cbc6d9aa --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/DatabaseFilter/LogicalOperatorSelect.tsx @@ -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 ( + ({ + 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` }} + > +
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 + + + +
+
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/DatabaseFilter/OperatorSelect.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/DatabaseFilter/OperatorSelect.tsx new file mode 100644 index 0000000000..2a9ce61d3d --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/DatabaseFilter/OperatorSelect.tsx @@ -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 ( + ({ + 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` }} + > +
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 ? ( + {currentOperator} + ) : ( + Select an option + )} + + + +
+
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/DatabaseSort/DatabaseSortItem.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/DatabaseSort/DatabaseSortItem.tsx new file mode 100644 index 0000000000..bc308e574c --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/DatabaseSort/DatabaseSortItem.tsx @@ -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(data?.fieldId ?? null); + const [currentOrder, setCurrentOrder] = useState(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 ( +
+ + + + + + + +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/DatabaseSort/DatabaseSortPopup.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/DatabaseSort/DatabaseSortPopup.tsx new file mode 100644 index 0000000000..fe2c89772a --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/DatabaseSort/DatabaseSortPopup.tsx @@ -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 ( +
+
e.stopPropagation()} className='flex flex-col rounded-lg bg-white shadow-md'> +
{t('grid.settings.sort')}
+ +
+ {rows.map((sortItem, index) => ( + onDeleteClick(sortItem)} + /> + ))} +
+ +
+ + +
+
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/DatabaseSort/OrderSelect.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/DatabaseSort/OrderSelect.tsx new file mode 100644 index 0000000000..c0057371d6 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/DatabaseSort/OrderSelect.tsx @@ -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 ( + + + + ), + label: 'Ascending', + key: SortConditionPB.Ascending, + onClick: () => { + onSelectOrderClick(SortConditionPB.Ascending); + setShowSelect(false); + }, + }, + { + icon: ( + + + + ), + 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` }} + > +
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 ? ( + + ) : ( + Select order + )} + + + +
+
+ ); +}; + +const SortLabel = ({ value }: { value: SortConditionPB }) => { + return value === SortConditionPB.Ascending ? ( +
+ + + + Ascending +
+ ) : ( +
+ + + + Descending +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/ChangeFieldTypePopup.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/ChangeFieldTypePopup.tsx index ffac4ec2b1..9f94ddd61d 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/ChangeFieldTypePopup.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/ChangeFieldTypePopup.tsx @@ -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 ( - -
+ +
{typesOrder.map((t, i) => ( ))}
- +
); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CheckList/CheckListPopup.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CheckList/CheckListPopup.tsx index 9b3424858d..0bf50c824d 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CheckList/CheckListPopup.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CheckList/CheckListPopup.tsx @@ -58,7 +58,7 @@ export const CheckListPopup = ({ return (
-
+
@@ -87,7 +87,7 @@ export const CheckListPopup = ({ > ))}
-
+
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CheckList/EditCheckListPopup.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CheckList/EditCheckListPopup.tsx index a345d5d7b9..4dbd45712e 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CheckList/EditCheckListPopup.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/CheckList/EditCheckListPopup.tsx @@ -78,7 +78,7 @@ export const EditCheckListPopup = ({ onKeyDown={onKeyDown} onBlur={() => onBlur()} /> -
{value.length}/30
+
{value.length}/30
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/DateTypeOptions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/DateTypeOptions.tsx index 502bea491b..0b3f9415d2 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/DateTypeOptions.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/Date/DateTypeOptions.tsx @@ -87,7 +87,7 @@ export const DateTypeOptions = ({ return (
-
+
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditFieldPopup.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditFieldPopup.tsx index 2856d82899..b29220bd99 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditFieldPopup.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditFieldPopup.tsx @@ -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(null); const inputRef = useRef(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 ( - { + { await save(); onOutsideClick(); }} - left={left} - top={top} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'left', + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'left', + }} > -
+
- {cellIdentifier.fieldType === FieldType.Number && ( - <> -
- - - )} +
- {cellIdentifier.fieldType === FieldType.DateTime && fieldController && ( - - )} +
+
+
+ {fieldsStore[cellIdentifier.fieldId]?.visible ? ( + <> + + + + {t('grid.field.hide')} + + ) : ( + <> + + + + Show + + )} +
+
onDuplicatePropertyClick()} + className={'flex cursor-pointer items-center gap-2 rounded-lg p-2 pr-8 hover:bg-fill-list-hover'} + > + + + + {t('grid.field.duplicate')} +
+
{ + onOutsideClick(); + onDeletePropertyClick(cellIdentifier.fieldId); + }} + className={'flex cursor-pointer items-center gap-2 rounded-lg p-2 pr-8 hover:bg-fill-list-hover'} + > + + + + {t('grid.field.delete')} +
+
+ +
+
+ + + + {t('grid.field.insertLeft')} +
+
+ + + + {t('grid.field.insertRight')} +
+
+
-
+
); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditRow.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditRow.tsx index 3127b49ead..490d1cddb5 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditRow.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/EditRow.tsx @@ -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(null); + const [editFieldAnchorEl, setEditFieldAnchorEl] = useState(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(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 = ({
{cells .filter((cell) => { - return databaseStore.fields[cell.cellIdentifier.fieldId]?.visible; + return fieldsStore[cell.cellIdentifier.fieldId]?.visible; }) .map((cell, cellIndex) => (
- {showFieldEditor && editingCell && ( + {editingCell && ( )} {showChangeFieldTypePopup && ( changeFieldType(newType)} onOutsideClick={() => setShowChangeFieldTypePopup(false)} > @@ -323,6 +312,7 @@ export const EditRow = ({ left={editCellOptionLeft} cellIdentifier={editingCell} editingSelectOption={editingSelectOption} + setEditingSelectOption={setEditingSelectOption} onOutsideClick={() => { setShowEditCellOption(false); }} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/InlineEditFields/EditCellNumber.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/InlineEditFields/EditCellNumber.tsx index efe1afd5f0..381f25ae3e 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/InlineEditFields/EditCellNumber.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/InlineEditFields/EditCellNumber.tsx @@ -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; -}) => { +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 ( setValue(e.target.value)} - onBlur={() => save()} + onBlur={() => onSave(value)} className={'w-full px-4 py-1'} > ); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/InlineEditFields/EditCellText.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/InlineEditFields/EditCellText.tsx index 80247a6f71..8a92b71ce4 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/InlineEditFields/EditCellText.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/EditRow/InlineEditFields/EditCellText.tsx @@ -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; -}) => { +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 (