mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-09-11 11:15:53 +03:00
refactor: adjust tauri codes (#4332)
* fix: some bugs * refactor: delete code that is no longer needed
This commit is contained in:
parent
a6baabbafc
commit
239bf2fa70
4
frontend/.vscode/tasks.json
vendored
4
frontend/.vscode/tasks.json
vendored
@ -257,7 +257,7 @@
|
||||
"label": "AF: Tauri UI Dev",
|
||||
"type": "shell",
|
||||
"isBackground": true,
|
||||
"command": "pnpm sync:i18n && pnpm run dev",
|
||||
"command": "pnpm run tauri:dev",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/appflowy_tauri"
|
||||
}
|
||||
@ -297,6 +297,6 @@
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/appflowy_flutter"
|
||||
}
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
@ -51,7 +51,7 @@
|
||||
"quill-delta": "^5.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-calendar": "^4.1.0",
|
||||
"react-big-calendar": "^1.8.5",
|
||||
"react-color": "^2.19.3",
|
||||
"react-datepicker": "^4.23.0",
|
||||
"react-dom": "^18.2.0",
|
||||
|
@ -1,9 +1,5 @@
|
||||
lockfileVersion: '6.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
dependencies:
|
||||
'@emoji-mart/data':
|
||||
specifier: ^1.1.2
|
||||
@ -104,9 +100,9 @@ dependencies:
|
||||
react-beautiful-dnd:
|
||||
specifier: ^13.1.1
|
||||
version: 13.1.1(react-dom@18.2.0)(react@18.2.0)
|
||||
react-calendar:
|
||||
specifier: ^4.1.0
|
||||
version: 4.2.1(react-dom@18.2.0)(react@18.2.0)
|
||||
react-big-calendar:
|
||||
specifier: ^1.8.5
|
||||
version: 1.8.5(react-dom@18.2.0)(react@18.2.0)
|
||||
react-color:
|
||||
specifier: ^2.19.3
|
||||
version: 2.19.3(react@18.2.0)
|
||||
@ -1882,6 +1878,15 @@ packages:
|
||||
engines: {node: '>=14'}
|
||||
dev: false
|
||||
|
||||
/@restart/hooks@0.4.15(react@18.2.0):
|
||||
resolution: {integrity: sha512-cZFXYTxbpzYcieq/mBwSyXgqnGMHoBVh3J7MU0CCoIB4NRZxV9/TuwTBAaLMqpNhC3zTPMCgkQ5Ey07L02Xmcw==}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
dependencies:
|
||||
dequal: 2.0.3
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@rollup/pluginutils@5.0.2:
|
||||
resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@ -2302,14 +2307,9 @@ packages:
|
||||
'@types/lodash': 4.14.194
|
||||
dev: true
|
||||
|
||||
/@types/lodash.memoize@4.1.7:
|
||||
resolution: {integrity: sha512-lGN7WeO4vO6sICVpf041Q7BX/9k1Y24Zo3FY0aUezr1QlKznpjzsDk3T3wvH8ofYzoK0QupN9TWcFAFZlyPwQQ==}
|
||||
dependencies:
|
||||
'@types/lodash': 4.14.194
|
||||
dev: false
|
||||
|
||||
/@types/lodash@4.14.194:
|
||||
resolution: {integrity: sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g==}
|
||||
dev: true
|
||||
|
||||
/@types/lodash@4.14.202:
|
||||
resolution: {integrity: sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==}
|
||||
@ -2477,6 +2477,10 @@ packages:
|
||||
resolution: {integrity: sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==}
|
||||
dev: true
|
||||
|
||||
/@types/warning@3.0.3:
|
||||
resolution: {integrity: sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==}
|
||||
dev: false
|
||||
|
||||
/@types/yargs-parser@21.0.0:
|
||||
resolution: {integrity: sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==}
|
||||
|
||||
@ -2631,10 +2635,6 @@ packages:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@wojtekmaj/date-utils@1.1.3:
|
||||
resolution: {integrity: sha512-rHrDuTl1cx5LYo8F4K4HVauVjwzx4LwrKfEk4br4fj4nK8JjJZ8IG6a6pBHkYmPLBQHCOEDwstb0WNXMGsmdOw==}
|
||||
dev: false
|
||||
|
||||
/abab@2.0.6:
|
||||
resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==}
|
||||
dev: true
|
||||
@ -3232,6 +3232,10 @@ packages:
|
||||
whatwg-url: 11.0.0
|
||||
dev: true
|
||||
|
||||
/date-arithmetic@4.1.0:
|
||||
resolution: {integrity: sha512-QWxYLR5P/6GStZcdem+V1xoto6DMadYWpMXU82ES3/RfR3Wdwr3D0+be7mgOJ+Ov0G9D5Dmb9T17sNLQYj9XOg==}
|
||||
dev: false
|
||||
|
||||
/date-fns@2.30.0:
|
||||
resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==}
|
||||
engines: {node: '>=0.11'}
|
||||
@ -3291,6 +3295,11 @@ packages:
|
||||
engines: {node: '>=0.4.0'}
|
||||
dev: true
|
||||
|
||||
/dequal@2.0.3:
|
||||
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
||||
engines: {node: '>=6'}
|
||||
dev: false
|
||||
|
||||
/derive-valtio@0.1.0(valtio@1.12.1):
|
||||
resolution: {integrity: sha512-OCg2UsLbXK7GmmpzMXhYkdO64vhJ1ROUUGaTFyHjVwEdMEcTTRj7W1TxLbSBxdY8QLBPCcp66MTyaSy0RpO17A==}
|
||||
peerDependencies:
|
||||
@ -3916,13 +3925,6 @@ packages:
|
||||
get-intrinsic: 1.2.1
|
||||
dev: true
|
||||
|
||||
/get-user-locale@2.2.1:
|
||||
resolution: {integrity: sha512-3814zipTZ2MvczOcppEXB3jXu+0HWwj5WmPI6//SeCnUIUaRXu7W4S54eQZTEPadlMZefE+jAlPOn+zY3tD4Qw==}
|
||||
dependencies:
|
||||
'@types/lodash.memoize': 4.1.7
|
||||
lodash.memoize: 4.1.2
|
||||
dev: false
|
||||
|
||||
/glob-parent@5.1.2:
|
||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||
engines: {node: '>= 6'}
|
||||
@ -3958,6 +3960,10 @@ packages:
|
||||
once: 1.4.0
|
||||
path-is-absolute: 1.0.1
|
||||
|
||||
/globalize@0.1.1:
|
||||
resolution: {integrity: sha512-5e01v8eLGfuQSOvx2MsDMOWS0GFtCx1wPzQSmcHw4hkxFzrQDBO3Xwg/m8Hr/7qXMrHeOIE29qWVzyv06u1TZA==}
|
||||
dev: false
|
||||
|
||||
/globals@11.12.0:
|
||||
resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
|
||||
engines: {node: '>=4'}
|
||||
@ -4175,6 +4181,12 @@ packages:
|
||||
side-channel: 1.0.4
|
||||
dev: true
|
||||
|
||||
/invariant@2.2.4:
|
||||
resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
dev: false
|
||||
|
||||
/is-arguments@1.1.1:
|
||||
resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -5039,6 +5051,7 @@ packages:
|
||||
|
||||
/lodash.memoize@4.1.2:
|
||||
resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==}
|
||||
dev: true
|
||||
|
||||
/lodash.merge@4.6.2:
|
||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||
@ -5070,6 +5083,11 @@ packages:
|
||||
dependencies:
|
||||
yallist: 4.0.0
|
||||
|
||||
/luxon@3.4.4:
|
||||
resolution: {integrity: sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==}
|
||||
engines: {node: '>=12'}
|
||||
dev: false
|
||||
|
||||
/magic-string@0.27.0:
|
||||
resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==}
|
||||
engines: {node: '>=12'}
|
||||
@ -5108,6 +5126,10 @@ packages:
|
||||
resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==}
|
||||
dev: false
|
||||
|
||||
/memoize-one@6.0.0:
|
||||
resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==}
|
||||
dev: false
|
||||
|
||||
/merge-stream@2.0.0:
|
||||
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
|
||||
|
||||
@ -5154,6 +5176,16 @@ packages:
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/moment-timezone@0.5.44:
|
||||
resolution: {integrity: sha512-nv3YpzI/8lkQn0U6RkLd+f0W/zy/JnoR5/EyPz/dNkPTBjA2jNLCVxaiQ8QpeLymhSZvX0wCL5s27NQWdOPwAw==}
|
||||
dependencies:
|
||||
moment: 2.30.1
|
||||
dev: false
|
||||
|
||||
/moment@2.30.1:
|
||||
resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==}
|
||||
dev: false
|
||||
|
||||
/ms@2.1.2:
|
||||
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
|
||||
|
||||
@ -5692,19 +5724,30 @@ packages:
|
||||
- react-native
|
||||
dev: false
|
||||
|
||||
/react-calendar@4.2.1(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-T5oKXD+KLy/g6bmJJkZ7E9wj0iRMesWMZcrC7q2kI6ybOsu9NlPQx8uXJzG4A4C3Sh5Xi0deznyzWIVsUpF8tA==}
|
||||
/react-big-calendar@1.8.5(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-cra8WPfoTSQthFfqxi0k9xm/Shv5jWSw19LkNzpSJcnQhP6XGes/eJjd8P8g/iwaJjXIWPpg3+HB5wO5wabRyA==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
react: ^16.14.0 || ^17 || ^18
|
||||
react-dom: ^16.14.0 || ^17 || ^18
|
||||
dependencies:
|
||||
'@types/react': 18.2.6
|
||||
'@wojtekmaj/date-utils': 1.1.3
|
||||
'@babel/runtime': 7.23.4
|
||||
clsx: 1.2.1
|
||||
get-user-locale: 2.2.1
|
||||
date-arithmetic: 4.1.0
|
||||
dayjs: 1.11.9
|
||||
dom-helpers: 5.2.1
|
||||
globalize: 0.1.1
|
||||
invariant: 2.2.4
|
||||
lodash: 4.17.21
|
||||
lodash-es: 4.17.21
|
||||
luxon: 3.4.4
|
||||
memoize-one: 6.0.0
|
||||
moment: 2.30.1
|
||||
moment-timezone: 0.5.44
|
||||
prop-types: 15.8.1
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
react-overlays: 5.2.1(react-dom@18.2.0)(react@18.2.0)
|
||||
uncontrollable: 7.2.1(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/react-color@2.19.3(react@18.2.0):
|
||||
@ -5826,6 +5869,10 @@ packages:
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/react-lifecycles-compat@3.0.4:
|
||||
resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==}
|
||||
dev: false
|
||||
|
||||
/react-onclickoutside@6.13.0(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-ty8So6tcUpIb+ZE+1HAhbLROvAIJYyJe/1vRrrcmW+jLsaM+/powDRqxzo6hSh9CuRZGSL1Q8mvcF5WRD93a0A==}
|
||||
peerDependencies:
|
||||
@ -5836,6 +5883,24 @@ packages:
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/react-overlays@5.2.1(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-GLLSOLWr21CqtJn8geSwQfoJufdt3mfdsnIiQswouuQ2MMPns+ihZklxvsTDKD3cR2tF8ELbi5xUsvqVhR6WvA==}
|
||||
peerDependencies:
|
||||
react: '>=16.3.0'
|
||||
react-dom: '>=16.3.0'
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.4
|
||||
'@popperjs/core': 2.11.8
|
||||
'@restart/hooks': 0.4.15(react@18.2.0)
|
||||
'@types/warning': 3.0.3
|
||||
dom-helpers: 5.2.1
|
||||
prop-types: 15.8.1
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
uncontrollable: 7.2.1(react@18.2.0)
|
||||
warning: 4.0.3
|
||||
dev: false
|
||||
|
||||
/react-popper@2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==}
|
||||
peerDependencies:
|
||||
@ -6780,6 +6845,18 @@ packages:
|
||||
which-boxed-primitive: 1.0.2
|
||||
dev: true
|
||||
|
||||
/uncontrollable@7.2.1(react@18.2.0):
|
||||
resolution: {integrity: sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==}
|
||||
peerDependencies:
|
||||
react: '>=15.0.0'
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.4
|
||||
'@types/react': 18.2.6
|
||||
invariant: 2.2.4
|
||||
react: 18.2.0
|
||||
react-lifecycles-compat: 3.0.4
|
||||
dev: false
|
||||
|
||||
/universalify@0.2.0:
|
||||
resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
|
||||
engines: {node: '>= 4.0.0'}
|
||||
@ -7119,3 +7196,7 @@ packages:
|
||||
/yocto-queue@0.1.0:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
@ -1,31 +1,39 @@
|
||||
import { useAppDispatch, useAppSelector } from '$app/stores/store';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { UserSettingController } from '$app/stores/effects/user/user_setting_controller';
|
||||
import { currentUserActions } from '$app_reducers/current-user/slice';
|
||||
import { Theme as ThemeType, ThemeMode } from '$app/stores/reducers/current-user/slice';
|
||||
import { createTheme } from '@mui/material/styles';
|
||||
import { getDesignTokens } from '$app/utils/mui';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ThemeModePB } from '@/services/backend';
|
||||
import { UserService } from '$app/application/user/user.service';
|
||||
|
||||
export function useUserSetting() {
|
||||
const dispatch = useAppDispatch();
|
||||
const { i18n } = useTranslation();
|
||||
const currentUser = useAppSelector((state) => state.currentUser);
|
||||
const userSettingController = useMemo(() => {
|
||||
if (!currentUser?.id) return;
|
||||
const controller = new UserSettingController(currentUser.id);
|
||||
|
||||
return controller;
|
||||
}, [currentUser?.id]);
|
||||
const handleSystemThemeChange = useCallback(() => {
|
||||
const mode = window.matchMedia('(prefers-color-scheme: dark)').matches ? ThemeMode.Dark : ThemeMode.Light;
|
||||
|
||||
dispatch(currentUserActions.setUserSetting({ themeMode: mode }));
|
||||
}, [dispatch]);
|
||||
|
||||
const loadUserSetting = useCallback(async () => {
|
||||
if (!userSettingController) return;
|
||||
const settings = await userSettingController.getAppearanceSetting();
|
||||
const settings = await UserService.getAppearanceSetting();
|
||||
|
||||
if (!settings) return;
|
||||
dispatch(currentUserActions.setUserSetting(settings));
|
||||
|
||||
if (settings.themeMode === ThemeModePB.System) {
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
handleSystemThemeChange();
|
||||
|
||||
mediaQuery.addEventListener('change', handleSystemThemeChange);
|
||||
}
|
||||
|
||||
await i18n.changeLanguage(settings.language);
|
||||
}, [dispatch, i18n, userSettingController]);
|
||||
}, [dispatch, handleSystemThemeChange, i18n]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadUserSetting();
|
||||
@ -35,12 +43,26 @@ export function useUserSetting() {
|
||||
return state.currentUser.userSetting || {};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const html = document.documentElement;
|
||||
|
||||
html?.setAttribute('data-dark-mode', String(themeMode === ThemeMode.Dark));
|
||||
html?.setAttribute('data-theme', themeType);
|
||||
}, [themeType, themeMode]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
mediaQuery.removeEventListener('change', handleSystemThemeChange);
|
||||
};
|
||||
}, [dispatch, handleSystemThemeChange]);
|
||||
|
||||
const muiTheme = useMemo(() => createTheme(getDesignTokens(themeMode)), [themeMode]);
|
||||
|
||||
return {
|
||||
muiTheme,
|
||||
themeMode,
|
||||
themeType,
|
||||
userSettingController,
|
||||
};
|
||||
}
|
||||
|
@ -1,44 +1,26 @@
|
||||
import React from 'react';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import { ProtectedRoutes } from '$app/components/auth/ProtectedRoutes';
|
||||
import { AllIcons } from '$app/components/tests/AllIcons';
|
||||
import { ColorPalette } from '$app/components/tests/ColorPalette';
|
||||
import { TestAPI } from '$app/components/tests/TestAPI';
|
||||
import { BoardPage } from '$app/views/BoardPage';
|
||||
import { DatabasePage } from '$app/views/DatabasePage';
|
||||
import { LoginPage } from '$app/views/LoginPage';
|
||||
import { GetStarted } from '$app/components/auth/GetStarted/GetStarted';
|
||||
import { SignUpPage } from '$app/views/SignUpPage';
|
||||
import { ConfirmAccountPage } from '$app/views/ConfirmAccountPage';
|
||||
|
||||
import { ThemeProvider } from '@mui/material';
|
||||
import { useUserSetting } from '$app/AppMain.hooks';
|
||||
import { UserSettingControllerContext } from '$app/components/_shared/app-hooks/useUserSettingControllerContext';
|
||||
import TrashPage from '$app/views/TrashPage';
|
||||
import DocumentPage from '$app/views/DocumentPage';
|
||||
|
||||
function AppMain() {
|
||||
const { muiTheme, userSettingController } = useUserSetting();
|
||||
const { muiTheme } = useUserSetting();
|
||||
|
||||
return (
|
||||
<UserSettingControllerContext.Provider value={userSettingController}>
|
||||
<ThemeProvider theme={muiTheme}>
|
||||
<Routes>
|
||||
<Route path={'/'} element={<ProtectedRoutes />}>
|
||||
<Route path={'/page/all-icons'} element={<AllIcons />} />
|
||||
<Route path={'/page/colors'} element={<ColorPalette />} />
|
||||
<Route path={'/page/api-test'} element={<TestAPI />} />
|
||||
<Route path={'/page/document/:id'} element={<DocumentPage />} />
|
||||
<Route path={'/page/board/:id'} element={<BoardPage />} />
|
||||
<Route path={'/page/grid/:id'} element={<DatabasePage />} />
|
||||
<Route path={'/trash'} id={'trash'} element={<TrashPage />} />
|
||||
</Route>
|
||||
<Route path={'/auth/login'} element={<LoginPage />}></Route>
|
||||
<Route path={'/auth/getStarted'} element={<GetStarted />}></Route>
|
||||
<Route path={'/auth/signUp'} element={<SignUpPage />}></Route>
|
||||
<Route path={'/auth/confirm-account'} element={<ConfirmAccountPage />}></Route>
|
||||
</Routes>
|
||||
</ThemeProvider>
|
||||
</UserSettingControllerContext.Provider>
|
||||
<ThemeProvider theme={muiTheme}>
|
||||
<Routes>
|
||||
<Route path={'/'} element={<ProtectedRoutes />}>
|
||||
<Route path={'/page/document/:id'} element={<DocumentPage />} />
|
||||
<Route path={'/page/grid/:id'} element={<DatabasePage />} />
|
||||
<Route path={'/trash'} id={'trash'} element={<TrashPage />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Database } from '$app/components/database/application';
|
||||
import { Database } from '$app/application/database';
|
||||
import { getCell } from './cell_service';
|
||||
|
||||
export function didDeleteCells({ database, rowId, fieldId }: { database: Database; rowId?: string; fieldId?: string }) {
|
@ -5,7 +5,7 @@ import {
|
||||
ChecklistCellDataChangesetPB,
|
||||
DateChangesetPB,
|
||||
FieldType,
|
||||
} from '@/services/backend';
|
||||
} from '../../../../services/backend';
|
||||
import {
|
||||
DatabaseEventGetCell,
|
||||
DatabaseEventUpdateCell,
|
@ -6,11 +6,8 @@ import {
|
||||
SelectOptionCellDataPB,
|
||||
TimestampCellDataPB,
|
||||
URLCellDataPB,
|
||||
} from '@/services/backend';
|
||||
import {
|
||||
SelectOption,
|
||||
pbToSelectOption,
|
||||
} from '$app/components/database/application/field/select_option/select_option_types';
|
||||
} from '../../../../services/backend';
|
||||
import { SelectOption, pbToSelectOption } from '../field/select_option/select_option_types';
|
||||
|
||||
export interface Cell {
|
||||
rowId: string;
|
@ -1,6 +1,6 @@
|
||||
import { DatabaseFieldChangesetPB, FieldSettingsPB, FieldVisibility } from '@/services/backend';
|
||||
import { Database, fieldService } from '$app/components/database/application';
|
||||
import { didDeleteCells, didUpdateCells } from '$app/components/database/application/cell/cell_listeners';
|
||||
import { Database, fieldService } from '$app/application/database';
|
||||
import { didDeleteCells, didUpdateCells } from '$app/application/database/cell/cell_listeners';
|
||||
|
||||
export function didUpdateFieldSettings(database: Database, settings: FieldSettingsPB) {
|
||||
const { field_id: fieldId, visibility, width } = settings;
|
@ -26,7 +26,7 @@ import {
|
||||
} from '@/services/backend/events/flowy-database2';
|
||||
import { Field, pbToField } from './field_types';
|
||||
import { bytesToTypeOption } from './type_option';
|
||||
import { Database } from '$app/components/database/application';
|
||||
import { Database } from '$app/application/database';
|
||||
|
||||
export async function getFields(
|
||||
viewId: string,
|
@ -5,7 +5,7 @@ import {
|
||||
NumberFilterConditionPB,
|
||||
TextFilterConditionPB,
|
||||
} from '@/services/backend';
|
||||
import { UndeterminedFilter } from '$app/components/database/application';
|
||||
import { UndeterminedFilter } from '$app/application/database';
|
||||
|
||||
export function getDefaultFilter(fieldType: FieldType): UndeterminedFilter['data'] | undefined {
|
||||
switch (fieldType) {
|
@ -1,4 +1,4 @@
|
||||
import { Database, pbToFilter } from '$app/components/database/application';
|
||||
import { Database, pbToFilter } from '$app/application/database';
|
||||
import { FilterChangesetNotificationPB } from '@/services/backend';
|
||||
|
||||
const deleteFiltersFromChange = (database: Database, changeset: FilterChangesetNotificationPB) => {
|
@ -1,7 +1,7 @@
|
||||
import { ReorderAllRowsPB, ReorderSingleRowPB, RowsChangePB, RowsVisibilityChangePB } from '@/services/backend';
|
||||
import { Database } from '../database';
|
||||
import { pbToRowMeta, RowMeta } from './row_types';
|
||||
import { didDeleteCells } from '$app/components/database/application/cell/cell_listeners';
|
||||
import { didDeleteCells } from '$app/application/database/cell/cell_listeners';
|
||||
|
||||
const deleteRowsFromChangeset = (database: Database, changeset: RowsChangePB) => {
|
||||
changeset.deleted_rows.forEach((rowId) => {
|
@ -0,0 +1,152 @@
|
||||
import { Page, PageIcon, parserViewPBToPage } from '$app_reducers/pages/slice';
|
||||
import {
|
||||
CreateOrphanViewPayloadPB,
|
||||
CreateViewPayloadPB,
|
||||
MoveNestedViewPayloadPB,
|
||||
RepeatedViewIdPB,
|
||||
UpdateViewIconPayloadPB,
|
||||
UpdateViewPayloadPB,
|
||||
ViewIconPB,
|
||||
ViewIdPB,
|
||||
ViewPB,
|
||||
} from '@/services/backend';
|
||||
import {
|
||||
FolderEventCreateOrphanView,
|
||||
FolderEventCreateView,
|
||||
FolderEventDeleteView,
|
||||
FolderEventDuplicateView,
|
||||
FolderEventGetView,
|
||||
FolderEventMoveNestedView,
|
||||
FolderEventUpdateView,
|
||||
FolderEventUpdateViewIcon,
|
||||
} from '@/services/backend/events/flowy-folder';
|
||||
|
||||
export async function getPage(id: string) {
|
||||
const payload = new ViewIdPB({
|
||||
value: id,
|
||||
});
|
||||
|
||||
const result = await FolderEventGetView(payload);
|
||||
|
||||
if (result.ok) {
|
||||
return parserViewPBToPage(result.val);
|
||||
}
|
||||
|
||||
return Promise.reject(result.val);
|
||||
}
|
||||
|
||||
export const createOrphanPage = async (
|
||||
params: ReturnType<typeof CreateOrphanViewPayloadPB.prototype.toObject>
|
||||
): Promise<Page> => {
|
||||
const payload = CreateOrphanViewPayloadPB.fromObject(params);
|
||||
|
||||
const result = await FolderEventCreateOrphanView(payload);
|
||||
|
||||
if (result.ok) {
|
||||
return parserViewPBToPage(result.val);
|
||||
}
|
||||
|
||||
return Promise.reject(result.val);
|
||||
};
|
||||
|
||||
export const duplicatePage = async (id: string) => {
|
||||
const page = await getPage(id);
|
||||
const payload = ViewPB.fromObject(page);
|
||||
|
||||
const result = await FolderEventDuplicateView(payload);
|
||||
|
||||
if (result.ok) {
|
||||
return result.val;
|
||||
}
|
||||
|
||||
return Promise.reject(result.err);
|
||||
};
|
||||
|
||||
export const deletePage = async (id: string) => {
|
||||
const payload = new RepeatedViewIdPB({
|
||||
items: [id],
|
||||
});
|
||||
|
||||
const result = await FolderEventDeleteView(payload);
|
||||
|
||||
if (result.ok) {
|
||||
return result.val;
|
||||
}
|
||||
|
||||
return Promise.reject(result.err);
|
||||
};
|
||||
|
||||
export const createPage = async (params: ReturnType<typeof CreateViewPayloadPB.prototype.toObject>): Promise<string> => {
|
||||
const payload = CreateViewPayloadPB.fromObject(params);
|
||||
|
||||
const result = await FolderEventCreateView(payload);
|
||||
|
||||
if (result.ok) {
|
||||
return result.val.id;
|
||||
}
|
||||
|
||||
return Promise.reject(result.err);
|
||||
};
|
||||
|
||||
export const movePage = async (params: ReturnType<typeof MoveNestedViewPayloadPB.prototype.toObject>) => {
|
||||
const payload = new MoveNestedViewPayloadPB(params);
|
||||
|
||||
const result = await FolderEventMoveNestedView(payload);
|
||||
|
||||
if (result.ok) {
|
||||
return result.val;
|
||||
}
|
||||
|
||||
return Promise.reject(result.err);
|
||||
};
|
||||
|
||||
export const getChildPages = async (id: string): Promise<Page[]> => {
|
||||
const payload = new ViewIdPB({
|
||||
value: id,
|
||||
});
|
||||
|
||||
const result = await FolderEventGetView(payload);
|
||||
|
||||
if (result.ok) {
|
||||
return result.val.child_views.map(parserViewPBToPage);
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
export const updatePage = async (page: { id: string } & Partial<Page>) => {
|
||||
const payload = new UpdateViewPayloadPB();
|
||||
|
||||
payload.view_id = page.id;
|
||||
if (page.name !== undefined) {
|
||||
payload.name = page.name;
|
||||
}
|
||||
|
||||
const result = await FolderEventUpdateView(payload);
|
||||
|
||||
if (result.ok) {
|
||||
return result.val.toObject();
|
||||
}
|
||||
|
||||
return Promise.reject(result.err);
|
||||
};
|
||||
|
||||
export const updatePageIcon = async (viewId: string, icon?: PageIcon) => {
|
||||
const payload = new UpdateViewIconPayloadPB({
|
||||
view_id: viewId,
|
||||
icon: icon
|
||||
? new ViewIconPB({
|
||||
ty: icon.ty,
|
||||
value: icon.value,
|
||||
})
|
||||
: undefined,
|
||||
});
|
||||
|
||||
const result = await FolderEventUpdateViewIcon(payload);
|
||||
|
||||
if (result.ok) {
|
||||
return result.val;
|
||||
}
|
||||
|
||||
return Promise.reject(result.err);
|
||||
};
|
@ -0,0 +1,68 @@
|
||||
import {
|
||||
FolderEventListTrashItems,
|
||||
FolderEventPermanentlyDeleteAllTrashItem,
|
||||
FolderEventPermanentlyDeleteTrashItem,
|
||||
FolderEventRecoverAllTrashItems,
|
||||
FolderEventRestoreTrashItem,
|
||||
RepeatedTrashIdPB,
|
||||
TrashIdPB,
|
||||
} from '@/services/backend/events/flowy-folder';
|
||||
|
||||
export const getTrash = async () => {
|
||||
const res = await FolderEventListTrashItems();
|
||||
|
||||
if (res.ok) {
|
||||
return res.val.items;
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
export const putback = async (id: string) => {
|
||||
const payload = new TrashIdPB({
|
||||
id,
|
||||
});
|
||||
|
||||
const res = await FolderEventRestoreTrashItem(payload);
|
||||
|
||||
if (res.ok) {
|
||||
return res.val;
|
||||
}
|
||||
|
||||
return Promise.reject(res.err);
|
||||
};
|
||||
|
||||
export const deleteTrashItem = async (ids: string[]) => {
|
||||
const items = ids.map((id) => new TrashIdPB({ id }));
|
||||
const payload = new RepeatedTrashIdPB({
|
||||
items,
|
||||
});
|
||||
|
||||
const res = await FolderEventPermanentlyDeleteTrashItem(payload);
|
||||
|
||||
if (res.ok) {
|
||||
return res.val;
|
||||
}
|
||||
|
||||
return Promise.reject(res.err);
|
||||
};
|
||||
|
||||
export const deleteAll = async () => {
|
||||
const res = await FolderEventPermanentlyDeleteAllTrashItem();
|
||||
|
||||
if (res.ok) {
|
||||
return res.val;
|
||||
}
|
||||
|
||||
return Promise.reject(res.err);
|
||||
};
|
||||
|
||||
export const restoreAll = async () => {
|
||||
const res = await FolderEventRecoverAllTrashItems();
|
||||
|
||||
if (res.ok) {
|
||||
return res.val;
|
||||
}
|
||||
|
||||
return Promise.reject(res.err);
|
||||
};
|
@ -0,0 +1,110 @@
|
||||
import { CreateViewPayloadPB, UserWorkspaceIdPB, WorkspaceIdPB } from '@/services/backend';
|
||||
import { UserEventOpenWorkspace } from '@/services/backend/events/flowy-user';
|
||||
import {
|
||||
FolderEventCreateView,
|
||||
FolderEventDeleteWorkspace,
|
||||
FolderEventGetCurrentWorkspaceSetting,
|
||||
FolderEventReadCurrentWorkspace,
|
||||
FolderEventReadWorkspaceViews,
|
||||
} from '@/services/backend/events/flowy-folder';
|
||||
import { parserViewPBToPage } from '$app_reducers/pages/slice';
|
||||
|
||||
export async function openWorkspace(id: string) {
|
||||
const payload = new UserWorkspaceIdPB({
|
||||
workspace_id: id,
|
||||
});
|
||||
|
||||
const result = await UserEventOpenWorkspace(payload);
|
||||
|
||||
if (result.ok) {
|
||||
return result.val;
|
||||
}
|
||||
|
||||
return Promise.reject(result.err);
|
||||
}
|
||||
|
||||
export async function deleteWorkspace(id: string) {
|
||||
const payload = new WorkspaceIdPB({
|
||||
value: id,
|
||||
});
|
||||
|
||||
const result = await FolderEventDeleteWorkspace(payload);
|
||||
|
||||
if (result.ok) {
|
||||
return result.val;
|
||||
}
|
||||
|
||||
return Promise.reject(result.err);
|
||||
}
|
||||
|
||||
export async function getWorkspaceChildViews(id: string) {
|
||||
const payload = new WorkspaceIdPB({
|
||||
value: id,
|
||||
});
|
||||
|
||||
const result = await FolderEventReadWorkspaceViews(payload);
|
||||
|
||||
if (result.ok) {
|
||||
return result.val.items.map(parserViewPBToPage);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
export async function getWorkspaces() {
|
||||
const result = await FolderEventReadCurrentWorkspace();
|
||||
|
||||
if (result.ok) {
|
||||
const item = result.val;
|
||||
|
||||
return [
|
||||
{
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
export async function getCurrentWorkspaceSetting() {
|
||||
const res = await FolderEventGetCurrentWorkspaceSetting();
|
||||
|
||||
if (res.ok) {
|
||||
return res.val;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
export async function getCurrentWorkspace() {
|
||||
const result = await FolderEventReadCurrentWorkspace();
|
||||
|
||||
if (result.ok) {
|
||||
const workspace = result.val;
|
||||
|
||||
return {
|
||||
id: workspace.id,
|
||||
name: workspace.name,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function createCurrentWorkspaceChildView(
|
||||
params: ReturnType<typeof CreateViewPayloadPB.prototype.toObject>
|
||||
) {
|
||||
const payload = CreateViewPayloadPB.fromObject(params);
|
||||
|
||||
const result = await FolderEventCreateView(payload);
|
||||
|
||||
if (result.ok) {
|
||||
const view = result.val;
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
return Promise.reject(result.err);
|
||||
}
|
@ -15,6 +15,13 @@ import {
|
||||
RowsChangePB,
|
||||
RowsVisibilityChangePB,
|
||||
SortChangesetNotificationPB,
|
||||
UserNotification,
|
||||
UserProfilePB,
|
||||
FolderNotification,
|
||||
RepeatedViewPB,
|
||||
ViewPB,
|
||||
RepeatedTrashPB,
|
||||
ChildViewUpdatePB,
|
||||
} from '@/services/backend';
|
||||
|
||||
const Notification = {
|
||||
@ -32,6 +39,11 @@ const Notification = {
|
||||
[DatabaseNotification.DidUpdateFieldSettings]: FieldSettingsPB,
|
||||
[DatabaseNotification.DidUpdateFilter]: FilterChangesetNotificationPB,
|
||||
[DocumentNotification.DidReceiveUpdate]: DocEventPB,
|
||||
[UserNotification.DidUpdateUserProfile]: UserProfilePB,
|
||||
[FolderNotification.DidUpdateWorkspaceViews]: RepeatedViewPB,
|
||||
[FolderNotification.DidUpdateView]: ViewPB,
|
||||
[FolderNotification.DidUpdateChildViews]: ChildViewUpdatePB,
|
||||
[FolderNotification.DidUpdateTrash]: RepeatedTrashPB,
|
||||
};
|
||||
|
||||
type NotificationMap = typeof Notification;
|
||||
|
@ -0,0 +1,46 @@
|
||||
import { SignInPayloadPB, SignUpPayloadPB } from '@/services/backend';
|
||||
import {
|
||||
UserEventSignInWithEmailPassword,
|
||||
UserEventSignOut,
|
||||
UserEventSignUp,
|
||||
} from '@/services/backend/events/flowy-user';
|
||||
import { nanoid } from '@reduxjs/toolkit';
|
||||
import { Log } from '$app/utils/log';
|
||||
|
||||
export const AuthService = {
|
||||
signIn: async (params: { email: string; password: string }) => {
|
||||
const payload = SignInPayloadPB.fromObject({ email: params.email, password: params.password });
|
||||
|
||||
const res = await UserEventSignInWithEmailPassword(payload);
|
||||
|
||||
if (res.ok) {
|
||||
return res.val;
|
||||
}
|
||||
|
||||
Log.error(res.val.msg);
|
||||
throw new Error(res.val.msg);
|
||||
},
|
||||
|
||||
signUp: async (params: { name: string; email: string; password: string }) => {
|
||||
const deviceId = nanoid(8);
|
||||
const payload = SignUpPayloadPB.fromObject({
|
||||
name: params.name,
|
||||
email: params.email,
|
||||
password: params.password,
|
||||
device_id: deviceId,
|
||||
});
|
||||
|
||||
const res = await UserEventSignUp(payload);
|
||||
|
||||
if (!res.ok) {
|
||||
Log.error(res.val.msg);
|
||||
throw new Error(res.val.msg);
|
||||
}
|
||||
|
||||
return res.val;
|
||||
},
|
||||
|
||||
signOut: () => {
|
||||
return UserEventSignOut();
|
||||
},
|
||||
};
|
@ -0,0 +1,55 @@
|
||||
import { Theme, ThemeMode, UserSetting } from '$app_reducers/current-user/slice';
|
||||
import { AppearanceSettingsPB } from '@/services/backend';
|
||||
import {
|
||||
UserEventGetAppearanceSetting,
|
||||
UserEventGetUserProfile,
|
||||
UserEventSetAppearanceSetting,
|
||||
} from '@/services/backend/events/flowy-user';
|
||||
|
||||
export const UserService = {
|
||||
getAppearanceSetting: async (): Promise<Partial<UserSetting> | undefined> => {
|
||||
const appearanceSetting = await UserEventGetAppearanceSetting();
|
||||
|
||||
if (appearanceSetting.ok) {
|
||||
const res = appearanceSetting.val;
|
||||
const { locale, theme = Theme.Default, theme_mode = ThemeMode.Light } = res;
|
||||
let language = 'en';
|
||||
|
||||
if (locale.language_code && locale.country_code) {
|
||||
language = `${locale.language_code}-${locale.country_code}`;
|
||||
} else if (locale.language_code) {
|
||||
language = locale.language_code;
|
||||
}
|
||||
|
||||
return {
|
||||
themeMode: theme_mode,
|
||||
theme: theme as Theme,
|
||||
language: language,
|
||||
};
|
||||
}
|
||||
|
||||
return;
|
||||
},
|
||||
|
||||
setAppearanceSetting: async (params: ReturnType<typeof AppearanceSettingsPB.prototype.toObject>) => {
|
||||
const payload = AppearanceSettingsPB.fromObject(params);
|
||||
|
||||
const res = await UserEventSetAppearanceSetting(payload);
|
||||
|
||||
if (res.ok) {
|
||||
return res.val;
|
||||
}
|
||||
|
||||
return Promise.reject(res.err);
|
||||
},
|
||||
|
||||
getUserProfile: async () => {
|
||||
const res = await UserEventGetUserProfile();
|
||||
|
||||
if (res.ok) {
|
||||
return res.val;
|
||||
}
|
||||
|
||||
return;
|
||||
},
|
||||
};
|
@ -1,4 +0,0 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9 2.5V6C9 6.55228 9.44772 7 10 7H12" stroke="#333333"/>
|
||||
<path d="M3.5 3.5C3.5 2.94771 3.94772 2.5 4.5 2.5H8H8.5C9.12951 2.5 9.72229 2.79639 10.1 3.3L12.1 5.96667C12.3596 6.31286 12.5 6.73393 12.5 7.16667V8V12.5C12.5 13.0523 12.0523 13.5 11.5 13.5H4.5C3.94772 13.5 3.5 13.0523 3.5 12.5V3.5Z" stroke="#333333"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 423 B |
@ -1,48 +0,0 @@
|
||||
import { MouseEventHandler, MouseEvent, ReactNode, useEffect, useState } from 'react';
|
||||
|
||||
export const Button = ({
|
||||
size = 'primary',
|
||||
children,
|
||||
onClick,
|
||||
}: {
|
||||
size?: 'primary' | 'medium' | 'small' | 'box-small-transparent' | 'medium-transparent';
|
||||
children: ReactNode;
|
||||
onClick?: MouseEventHandler<HTMLButtonElement>;
|
||||
}) => {
|
||||
const [cls, setCls] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
switch (size) {
|
||||
case 'primary':
|
||||
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-fill-default text-content-on-fill');
|
||||
break;
|
||||
case 'small':
|
||||
setCls(
|
||||
'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-fill-default text-fill-default transition-colors duration-300 hover:bg-content-blue-50 '
|
||||
);
|
||||
break;
|
||||
case 'box-small-transparent':
|
||||
setCls('text-icon-default w-[24px] h-[24px] rounded hover:bg-fill-list-hover');
|
||||
break;
|
||||
}
|
||||
}, [size]);
|
||||
|
||||
const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation();
|
||||
onClick && onClick(e);
|
||||
};
|
||||
|
||||
return (
|
||||
<button className={cls} onClick={(e) => handleClick(e)}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
@ -1,27 +0,0 @@
|
||||
export const CheckListProgress = ({ completed, max }: { completed: number; max: number }) => {
|
||||
return (
|
||||
<div className={'flex w-full items-center gap-4 py-1'}>
|
||||
{max > 0 && (
|
||||
<>
|
||||
<div className={'flex flex-1 gap-1'}>
|
||||
{completed > 0 && filledCheckListBars({ amount: completed })}
|
||||
{max - completed > 0 && emptyCheckListBars({ amount: max - completed })}
|
||||
</div>
|
||||
<div className={'text-xs text-text-caption'}>{((100 * completed) / max).toFixed(0)}%</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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-fill-hover'}></div>);
|
||||
};
|
||||
|
||||
const emptyCheckListBars = ({ amount }: { amount: number }) => {
|
||||
return Array(amount)
|
||||
.fill(0)
|
||||
.map((item, index) => <div key={index} className={'bg-tint-9 h-[4px] flex-1 flex-shrink-0 rounded'}></div>);
|
||||
};
|
@ -1,43 +0,0 @@
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
|
||||
export const useDatabase = () => {
|
||||
const database = useAppSelector((state) => state.database);
|
||||
|
||||
const newField = () => {
|
||||
/* dispatch(
|
||||
databaseActions.addField({
|
||||
field: {
|
||||
fieldId: nanoid(8),
|
||||
fieldType: FieldType.RichText,
|
||||
fieldOptions: {},
|
||||
title: 'new field',
|
||||
},
|
||||
})
|
||||
);*/
|
||||
console.log('depreciated');
|
||||
};
|
||||
|
||||
const renameField = (_fieldId: string, _newTitle: string) => {
|
||||
/* const field = database.fields[fieldId];
|
||||
field.title = newTitle;
|
||||
|
||||
dispatch(
|
||||
databaseActions.updateField({
|
||||
field,
|
||||
})
|
||||
);*/
|
||||
console.log('depreciated');
|
||||
};
|
||||
|
||||
const newRow = () => {
|
||||
// dispatch(databaseActions.addRow());
|
||||
console.log('depreciated');
|
||||
};
|
||||
|
||||
return {
|
||||
database,
|
||||
newField,
|
||||
renameField,
|
||||
newRow,
|
||||
};
|
||||
};
|
@ -1,175 +0,0 @@
|
||||
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,
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [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>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,183 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
@ -1,79 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
@ -1,146 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
@ -1,50 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
@ -1,63 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
@ -1,98 +0,0 @@
|
||||
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,
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [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>
|
||||
);
|
||||
};
|
@ -1,99 +0,0 @@
|
||||
import { t } from 'i18next';
|
||||
import { MouseEventHandler, useMemo, 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>
|
||||
);
|
||||
};
|
@ -1,97 +0,0 @@
|
||||
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,60 +0,0 @@
|
||||
import { FieldType } from '@/services/backend';
|
||||
import { FieldTypeIcon } from '$app/components/_shared/EditRow/FieldTypeIcon';
|
||||
import { FieldTypeName } from '$app/components/_shared/EditRow/FieldTypeName';
|
||||
import { Popover } from '@mui/material';
|
||||
|
||||
const typesOrder: FieldType[] = [
|
||||
FieldType.RichText,
|
||||
FieldType.Number,
|
||||
FieldType.DateTime,
|
||||
FieldType.SingleSelect,
|
||||
FieldType.MultiSelect,
|
||||
FieldType.Checkbox,
|
||||
FieldType.URL,
|
||||
FieldType.Checklist,
|
||||
];
|
||||
|
||||
export const ChangeFieldTypePopup = ({
|
||||
open,
|
||||
anchorEl,
|
||||
onClick,
|
||||
onOutsideClick,
|
||||
}: {
|
||||
open: boolean;
|
||||
anchorEl: HTMLDivElement | null;
|
||||
onClick: (newType: FieldType) => void;
|
||||
onOutsideClick: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<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)}
|
||||
key={i}
|
||||
className={'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 pr-8 hover:bg-fill-list-hover'}
|
||||
>
|
||||
<i className={'h-5 w-5'}>
|
||||
<FieldTypeIcon fieldType={t}></FieldTypeIcon>
|
||||
</i>
|
||||
<span>
|
||||
<FieldTypeName fieldType={t}></FieldTypeName>
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
};
|
@ -1,45 +0,0 @@
|
||||
import { SelectOptionCellDataPB } from '@/services/backend';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { ISelectOptionType } from '$app_reducers/database/slice';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
import { CheckListProgress } from '$app/components/_shared/CheckListProgress';
|
||||
|
||||
export const CheckList = ({
|
||||
data,
|
||||
fieldId,
|
||||
onEditClick,
|
||||
}: {
|
||||
data: SelectOptionCellDataPB | undefined;
|
||||
fieldId: string;
|
||||
onEditClick: (left: number, top: number) => void;
|
||||
}) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [allOptionsCount, setAllOptionsCount] = useState(0);
|
||||
const [selectedOptionsCount, setSelectedOptionsCount] = useState(0);
|
||||
const databaseStore = useAppSelector((state) => state.database);
|
||||
|
||||
useEffect(() => {
|
||||
setAllOptionsCount((databaseStore.fields[fieldId]?.fieldOptions as ISelectOptionType)?.selectOptions?.length ?? 0);
|
||||
}, [databaseStore, fieldId]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedOptionsCount((data as SelectOptionCellDataPB)?.select_options?.length ?? 0);
|
||||
}, [data]);
|
||||
|
||||
const onClick = () => {
|
||||
if (!ref.current) return;
|
||||
const { left, top } = ref.current.getBoundingClientRect();
|
||||
|
||||
onEditClick(left, top);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
onClick={onClick}
|
||||
className={'flex w-full flex-wrap items-center gap-2 px-4 py-1 text-xs text-text-title'}
|
||||
>
|
||||
<CheckListProgress completed={selectedOptionsCount} max={allOptionsCount} />
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,61 +0,0 @@
|
||||
import { SelectOptionPB } from '@/services/backend';
|
||||
import { EditorCheckSvg } from '$app/components/_shared/svg/EditorCheckSvg';
|
||||
import { EditorUncheckSvg } from '$app/components/_shared/svg/EditorUncheckSvg';
|
||||
import { Details2Svg } from '$app/components/_shared/svg/Details2Svg';
|
||||
import { ISelectOption } from '$app_reducers/database/slice';
|
||||
import { MouseEventHandler } from 'react';
|
||||
|
||||
export const CheckListOption = ({
|
||||
option,
|
||||
checked,
|
||||
onToggleOptionClick,
|
||||
openCheckListDetail,
|
||||
}: {
|
||||
option: ISelectOption;
|
||||
checked: boolean;
|
||||
onToggleOptionClick: (v: SelectOptionPB) => void;
|
||||
openCheckListDetail: (left: number, top: number, option: SelectOptionPB) => void;
|
||||
}) => {
|
||||
const onCheckListDetailClick: MouseEventHandler = (e) => {
|
||||
e.stopPropagation();
|
||||
let target = e.target as HTMLElement;
|
||||
|
||||
while (!(target instanceof HTMLButtonElement)) {
|
||||
if (target.parentElement === null) return;
|
||||
target = target.parentElement;
|
||||
}
|
||||
|
||||
const selectOption = new SelectOptionPB({
|
||||
id: option.selectOptionId,
|
||||
name: option.title,
|
||||
});
|
||||
|
||||
const { right: _left, top: _top } = target.getBoundingClientRect();
|
||||
|
||||
openCheckListDetail(_left, _top, selectOption);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={'flex cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-fill-list-hover'}
|
||||
onClick={() =>
|
||||
onToggleOptionClick(
|
||||
new SelectOptionPB({
|
||||
id: option.selectOptionId,
|
||||
name: option.title,
|
||||
})
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className={'h-5 w-5'}>
|
||||
{checked ? <EditorCheckSvg></EditorCheckSvg> : <EditorUncheckSvg></EditorUncheckSvg>}
|
||||
</div>
|
||||
<div className={`flex-1 px-2 py-0.5`}>{option.title}</div>
|
||||
<div className={'flex items-center'}>
|
||||
<button onClick={onCheckListDetailClick} className={'h-6 w-6 p-1'}>
|
||||
<Details2Svg></Details2Svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,97 +0,0 @@
|
||||
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
|
||||
import { SelectOptionCellDataPB, SelectOptionPB } from '@/services/backend';
|
||||
import { PopupWindow } from '$app/components/_shared/PopupWindow';
|
||||
import { ISelectOptionType } from '$app_reducers/database/slice';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
import { useCell } from '$app/components/_shared/database-hooks/useCell';
|
||||
import { CellCache } from '$app/stores/effects/database/cell/cell_cache';
|
||||
import { FieldController } from '$app/stores/effects/database/field/field_controller';
|
||||
import { SelectOptionCellBackendService } from '$app/stores/effects/database/cell/select_option_bd_svc';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { CheckListProgress } from '$app/components/_shared/CheckListProgress';
|
||||
import { NewCheckListOption } from '$app/components/_shared/EditRow/CheckList/NewCheckListOption';
|
||||
import { CheckListOption } from '$app/components/_shared/EditRow/CheckList/CheckListOption';
|
||||
import { NewCheckListButton } from '$app/components/_shared/EditRow/CheckList/NewCheckListButton';
|
||||
|
||||
export const CheckListPopup = ({
|
||||
left,
|
||||
top,
|
||||
cellIdentifier,
|
||||
cellCache,
|
||||
fieldController,
|
||||
openCheckListDetail,
|
||||
onOutsideClick,
|
||||
}: {
|
||||
left: number;
|
||||
top: number;
|
||||
cellIdentifier: CellIdentifier;
|
||||
cellCache: CellCache;
|
||||
fieldController: FieldController;
|
||||
openCheckListDetail: (left: number, top: number, option: SelectOptionPB) => void;
|
||||
onOutsideClick: () => void;
|
||||
}) => {
|
||||
const databaseStore = useAppSelector((state) => state.database);
|
||||
const { data } = useCell(cellIdentifier, cellCache, fieldController);
|
||||
|
||||
const [allOptionsCount, setAllOptionsCount] = useState(0);
|
||||
const [selectedOptionsCount, setSelectedOptionsCount] = useState(0);
|
||||
const [newOptions, setNewOptions] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setAllOptionsCount(
|
||||
(databaseStore.fields[cellIdentifier.fieldId]?.fieldOptions as ISelectOptionType)?.selectOptions?.length ?? 0
|
||||
);
|
||||
}, [databaseStore, cellIdentifier]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedOptionsCount((data as SelectOptionCellDataPB)?.select_options?.length ?? 0);
|
||||
}, [data]);
|
||||
|
||||
const onToggleOptionClick = async (option: SelectOptionPB) => {
|
||||
if ((data as SelectOptionCellDataPB)?.select_options?.find((selectedOption) => selectedOption.id === option.id)) {
|
||||
await new SelectOptionCellBackendService(cellIdentifier).unselectOption([option.id]);
|
||||
} else {
|
||||
await new SelectOptionCellBackendService(cellIdentifier).selectOption([option.id]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PopupWindow className={'text-xs'} onOutsideClick={onOutsideClick} left={left} top={top}>
|
||||
<div className={'min-w-[320px]'}>
|
||||
<div className={'px-4 pb-4 pt-8'}>
|
||||
<CheckListProgress completed={selectedOptionsCount} max={allOptionsCount} />
|
||||
</div>
|
||||
|
||||
<div className={'flex flex-col p-2'}>
|
||||
{(databaseStore.fields[cellIdentifier.fieldId]?.fieldOptions as ISelectOptionType).selectOptions.map(
|
||||
(option, index) => (
|
||||
<CheckListOption
|
||||
key={index}
|
||||
option={option}
|
||||
checked={
|
||||
!!(data as SelectOptionCellDataPB)?.select_options?.find((so) => so.id === option.selectOptionId)
|
||||
}
|
||||
onToggleOptionClick={onToggleOptionClick}
|
||||
openCheckListDetail={openCheckListDetail}
|
||||
></CheckListOption>
|
||||
)
|
||||
)}
|
||||
{newOptions.map((option, index) => (
|
||||
<NewCheckListOption
|
||||
key={index}
|
||||
index={index}
|
||||
option={option}
|
||||
newOptions={newOptions}
|
||||
setNewOptions={setNewOptions}
|
||||
cellIdentifier={cellIdentifier}
|
||||
></NewCheckListOption>
|
||||
))}
|
||||
</div>
|
||||
<div className={'h-[1px] bg-line-divider'}></div>
|
||||
<div className={'p-2'}>
|
||||
<NewCheckListButton newOptions={newOptions} setNewOptions={setNewOptions}></NewCheckListButton>
|
||||
</div>
|
||||
</div>
|
||||
</PopupWindow>
|
||||
);
|
||||
};
|
@ -1,97 +0,0 @@
|
||||
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
|
||||
import { KeyboardEventHandler, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SelectOptionPB } from '@/services/backend';
|
||||
import { SelectOptionCellBackendService } from '$app/stores/effects/database/cell/select_option_bd_svc';
|
||||
import { TrashSvg } from '$app/components/_shared/svg/TrashSvg';
|
||||
import { PopupWindow } from '$app/components/_shared/PopupWindow';
|
||||
|
||||
export const EditCheckListPopup = ({
|
||||
left,
|
||||
top,
|
||||
cellIdentifier,
|
||||
editingSelectOption,
|
||||
onOutsideClick,
|
||||
}: {
|
||||
left: number;
|
||||
top: number;
|
||||
cellIdentifier: CellIdentifier;
|
||||
editingSelectOption: SelectOptionPB;
|
||||
onOutsideClick: () => void;
|
||||
}) => {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const { t } = useTranslation();
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setValue(editingSelectOption.name);
|
||||
}, [editingSelectOption]);
|
||||
|
||||
const onKeyDown: KeyboardEventHandler = async (e) => {
|
||||
if (e.key === 'Enter' && value.length > 0) {
|
||||
await new SelectOptionCellBackendService(cellIdentifier).createOption({ name: value });
|
||||
setValue('');
|
||||
}
|
||||
};
|
||||
|
||||
const onKeyDownWrapper: KeyboardEventHandler = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
onOutsideClick();
|
||||
}
|
||||
};
|
||||
|
||||
const onBlur = async () => {
|
||||
const svc = new SelectOptionCellBackendService(cellIdentifier);
|
||||
|
||||
await svc.updateOption(
|
||||
new SelectOptionPB({
|
||||
id: editingSelectOption.id,
|
||||
name: value,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onDeleteOptionClick = async () => {
|
||||
const svc = new SelectOptionCellBackendService(cellIdentifier);
|
||||
|
||||
await svc.deleteOption([editingSelectOption]);
|
||||
onOutsideClick();
|
||||
};
|
||||
|
||||
return (
|
||||
<PopupWindow
|
||||
className={'p-2 text-xs'}
|
||||
onOutsideClick={async () => {
|
||||
await onBlur();
|
||||
onOutsideClick();
|
||||
}}
|
||||
left={left}
|
||||
top={top}
|
||||
>
|
||||
<div onKeyDown={onKeyDownWrapper} className={'flex flex-col gap-2 p-2'}>
|
||||
<div className={'flex flex-1 items-center gap-2 rounded border border-line-divider bg-fill-list-hover px-2 '}>
|
||||
<input
|
||||
ref={inputRef}
|
||||
className={'py-2'}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
onBlur={() => onBlur()}
|
||||
/>
|
||||
<div className={'font-mono text-text-caption'}>{value.length}/30</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onDeleteOptionClick()}
|
||||
className={
|
||||
'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 text-fill-default hover:bg-fill-list-hover'
|
||||
}
|
||||
>
|
||||
<i className={'h-5 w-5'}>
|
||||
<TrashSvg></TrashSvg>
|
||||
</i>
|
||||
<span>{t('grid.selectOption.deleteTag')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</PopupWindow>
|
||||
);
|
||||
};
|
@ -1,28 +0,0 @@
|
||||
import AddSvg from '$app/components/_shared/svg/AddSvg';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const NewCheckListButton = ({
|
||||
newOptions,
|
||||
setNewOptions,
|
||||
}: {
|
||||
newOptions: string[];
|
||||
setNewOptions: (v: string[]) => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const newOptionClick = () => {
|
||||
setNewOptions([...newOptions, '']);
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => newOptionClick()}
|
||||
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>
|
||||
</i>
|
||||
<span>{t('grid.field.addOption')}</span>
|
||||
</button>
|
||||
);
|
||||
};
|
@ -1,54 +0,0 @@
|
||||
import { SelectOptionCellBackendService } from '$app/stores/effects/database/cell/select_option_bd_svc';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
|
||||
|
||||
export const NewCheckListOption = ({
|
||||
index,
|
||||
option,
|
||||
newOptions,
|
||||
setNewOptions,
|
||||
cellIdentifier,
|
||||
}: {
|
||||
index: number;
|
||||
option: string;
|
||||
newOptions: string[];
|
||||
setNewOptions: (v: string[]) => void;
|
||||
cellIdentifier: CellIdentifier;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const updateNewOption = (value: string) => {
|
||||
const newOptionsCopy = [...newOptions];
|
||||
|
||||
newOptionsCopy[index] = value;
|
||||
setNewOptions(newOptionsCopy);
|
||||
};
|
||||
|
||||
const onNewOptionKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
void onSaveNewOptionClick();
|
||||
}
|
||||
};
|
||||
|
||||
const onSaveNewOptionClick = async () => {
|
||||
await new SelectOptionCellBackendService(cellIdentifier).createOption({ name: newOptions[index] });
|
||||
setNewOptions(newOptions.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
return (
|
||||
<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'}
|
||||
value={option}
|
||||
onChange={(e) => updateNewOption(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
onClick={() => onSaveNewOptionClick()}
|
||||
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>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,97 +0,0 @@
|
||||
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
|
||||
import { FieldController } from '$app/stores/effects/database/field/field_controller';
|
||||
import { PopupWindow } from '$app/components/_shared/PopupWindow';
|
||||
import { CheckmarkSvg } from '$app/components/_shared/svg/CheckmarkSvg';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DateFormatPB } from '@/services/backend';
|
||||
import { useDateTimeFormat } from '$app/components/_shared/EditRow/Date/DateTimeFormat.hooks';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { IDateType } from '$app_reducers/database/slice';
|
||||
|
||||
export const DateFormatPopup = ({
|
||||
left,
|
||||
top,
|
||||
cellIdentifier,
|
||||
fieldController,
|
||||
onOutsideClick,
|
||||
}: {
|
||||
left: number;
|
||||
top: number;
|
||||
cellIdentifier: CellIdentifier;
|
||||
fieldController: FieldController;
|
||||
onOutsideClick: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { changeDateFormat } = useDateTimeFormat(cellIdentifier, fieldController);
|
||||
const databaseStore = useAppSelector((state) => state.database);
|
||||
const [dateType, setDateType] = useState<IDateType | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
setDateType(databaseStore.fields[cellIdentifier.fieldId]?.fieldOptions as IDateType);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [databaseStore]);
|
||||
|
||||
const changeFormat = async (format: DateFormatPB) => {
|
||||
await changeDateFormat(format);
|
||||
onOutsideClick();
|
||||
};
|
||||
|
||||
return (
|
||||
<PopupWindow className={'p-2 text-xs'} onOutsideClick={onOutsideClick} left={left} top={top}>
|
||||
<PopupItem
|
||||
changeFormat={changeFormat}
|
||||
format={DateFormatPB.Friendly}
|
||||
checked={dateType?.dateFormat === DateFormatPB.Friendly}
|
||||
text={t('grid.field.dateFormatFriendly')}
|
||||
/>
|
||||
<PopupItem
|
||||
changeFormat={changeFormat}
|
||||
format={DateFormatPB.ISO}
|
||||
checked={dateType?.dateFormat === DateFormatPB.ISO}
|
||||
text={t('grid.field.dateFormatISO')}
|
||||
/>
|
||||
<PopupItem
|
||||
changeFormat={changeFormat}
|
||||
format={DateFormatPB.Local}
|
||||
checked={dateType?.dateFormat === DateFormatPB.Local}
|
||||
text={t('grid.field.dateFormatLocal')}
|
||||
/>
|
||||
<PopupItem
|
||||
changeFormat={changeFormat}
|
||||
format={DateFormatPB.US}
|
||||
checked={dateType?.dateFormat === DateFormatPB.US}
|
||||
text={t('grid.field.dateFormatUS')}
|
||||
/>
|
||||
</PopupWindow>
|
||||
);
|
||||
};
|
||||
|
||||
function PopupItem({
|
||||
format,
|
||||
text,
|
||||
changeFormat,
|
||||
checked,
|
||||
}: {
|
||||
format: DateFormatPB;
|
||||
text: string;
|
||||
changeFormat: (_: DateFormatPB) => Promise<void>;
|
||||
checked: boolean;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
onClick={() => changeFormat(format)}
|
||||
className={
|
||||
'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-fill-list-hover'
|
||||
}
|
||||
>
|
||||
{text}
|
||||
|
||||
{checked && (
|
||||
<div className={'ml-8 h-5 w-5 p-1'}>
|
||||
<CheckmarkSvg></CheckmarkSvg>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
|
||||
import { CellCache } from '$app/stores/effects/database/cell/cell_cache';
|
||||
import { FieldController } from '$app/stores/effects/database/field/field_controller';
|
||||
import Calendar from 'react-calendar';
|
||||
import dayjs from 'dayjs';
|
||||
import { useCell } from '$app/components/_shared/database-hooks/useCell';
|
||||
import { CalendarData } from '$app/stores/effects/database/cell/controller_builder';
|
||||
import { DateCellDataPB } from '@/services/backend';
|
||||
import { PopupWindow } from '$app/components/_shared/PopupWindow';
|
||||
import { DateTypeOptions } from '$app/components/_shared/EditRow/Date/DateTypeOptions';
|
||||
|
||||
export const DatePickerPopup = ({
|
||||
left,
|
||||
top,
|
||||
cellIdentifier,
|
||||
cellCache,
|
||||
fieldController,
|
||||
onOutsideClick,
|
||||
}: {
|
||||
left: number;
|
||||
top: number;
|
||||
cellIdentifier: CellIdentifier;
|
||||
cellCache: CellCache;
|
||||
fieldController: FieldController;
|
||||
onOutsideClick: () => void;
|
||||
}) => {
|
||||
const { data, cellController } = useCell(cellIdentifier, cellCache, fieldController);
|
||||
const [selectedDate, setSelectedDate] = useState<Date>(new Date());
|
||||
|
||||
useEffect(() => {
|
||||
const date_pb = data as DateCellDataPB | undefined;
|
||||
|
||||
if (!date_pb || !date_pb?.date.length) return;
|
||||
|
||||
setSelectedDate(dayjs(date_pb.date).toDate());
|
||||
}, [data]);
|
||||
|
||||
const onChange = async (v: Date | null | (Date | null)[]) => {
|
||||
if (v instanceof Date) {
|
||||
setSelectedDate(v);
|
||||
const date = new CalendarData(dayjs(v).add(dayjs().utcOffset(), 'minutes').toDate(), false);
|
||||
|
||||
await cellController?.saveCellData(date);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PopupWindow className={'p-2 text-xs'} onOutsideClick={onOutsideClick} left={left} top={top}>
|
||||
<div className={'px-2 pb-2'}>
|
||||
<Calendar onChange={(d) => onChange(d)} value={selectedDate} />
|
||||
</div>
|
||||
<DateTypeOptions cellIdentifier={cellIdentifier} fieldController={fieldController}></DateTypeOptions>
|
||||
</PopupWindow>
|
||||
);
|
||||
};
|
@ -1,42 +0,0 @@
|
||||
import { TypeOptionController } from '$app/stores/effects/database/field/type_option/type_option_controller';
|
||||
import { Some } from 'ts-results';
|
||||
import { DateFormatPB, DateTypeOptionPB, FieldType, TimeFormatPB } from '@/services/backend';
|
||||
import { makeDateTypeOptionContext } from '$app/stores/effects/database/field/type_option/type_option_context';
|
||||
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
|
||||
import { FieldController } from '$app/stores/effects/database/field/field_controller';
|
||||
|
||||
export const useDateTimeFormat = (cellIdentifier: CellIdentifier, fieldController: FieldController) => {
|
||||
const changeFormat = async (change: (option: DateTypeOptionPB) => void) => {
|
||||
const fieldInfo = fieldController.getField(cellIdentifier.fieldId);
|
||||
|
||||
if (!fieldInfo) return;
|
||||
const typeOptionController = new TypeOptionController(cellIdentifier.viewId, Some(fieldInfo), FieldType.DateTime);
|
||||
|
||||
await typeOptionController.initialize();
|
||||
const dateTypeOptionContext = makeDateTypeOptionContext(typeOptionController);
|
||||
const typeOption = dateTypeOptionContext.getTypeOption();
|
||||
|
||||
change(typeOption);
|
||||
await dateTypeOptionContext.setTypeOption(typeOption);
|
||||
};
|
||||
|
||||
const changeDateFormat = async (format: DateFormatPB) => {
|
||||
await changeFormat((option) => (option.date_format = format));
|
||||
};
|
||||
|
||||
const changeTimeFormat = async (format: TimeFormatPB) => {
|
||||
await changeFormat((option) => (option.time_format = format));
|
||||
};
|
||||
|
||||
const includeTime = async (_include: boolean) => {
|
||||
await changeFormat((_option) => {
|
||||
// option.include_time = include;
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
changeDateFormat,
|
||||
changeTimeFormat,
|
||||
includeTime,
|
||||
};
|
||||
};
|
@ -1,148 +0,0 @@
|
||||
import { DateFormatPopup } from '$app/components/_shared/EditRow/Date/DateFormatPopup';
|
||||
import { TimeFormatPopup } from '$app/components/_shared/EditRow/Date/TimeFormatPopup';
|
||||
import { MoreSvg } from '$app/components/_shared/svg/MoreSvg';
|
||||
import { MouseEventHandler, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IDateType } from '$app_reducers/database/slice';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
import { useDateTimeFormat } from '$app/components/_shared/EditRow/Date/DateTimeFormat.hooks';
|
||||
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
|
||||
import { FieldController } from '$app/stores/effects/database/field/field_controller';
|
||||
|
||||
export const DateTypeOptions = ({
|
||||
cellIdentifier,
|
||||
fieldController,
|
||||
}: {
|
||||
cellIdentifier: CellIdentifier;
|
||||
fieldController: FieldController;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [showDateFormatPopup, setShowDateFormatPopup] = useState(false);
|
||||
const [dateFormatTop, setDateFormatTop] = useState(0);
|
||||
const [dateFormatLeft, setDateFormatLeft] = useState(0);
|
||||
|
||||
const [showTimeFormatPopup, setShowTimeFormatPopup] = useState(false);
|
||||
const [timeFormatTop, setTimeFormatTop] = useState(0);
|
||||
const [timeFormatLeft, setTimeFormatLeft] = useState(0);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [dateType, setDateType] = useState<IDateType | undefined>();
|
||||
|
||||
const databaseStore = useAppSelector((state) => state.database);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { includeTime } = useDateTimeFormat(cellIdentifier, fieldController);
|
||||
|
||||
useEffect(() => {
|
||||
setDateType(databaseStore.fields[cellIdentifier.fieldId]?.fieldOptions as IDateType);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [databaseStore]);
|
||||
|
||||
const onDateFormatClick = (_left: number, _top: number) => {
|
||||
setShowDateFormatPopup(true);
|
||||
setDateFormatLeft(_left + 10);
|
||||
setDateFormatTop(_top);
|
||||
};
|
||||
|
||||
const onTimeFormatClick = (_left: number, _top: number) => {
|
||||
setShowTimeFormatPopup(true);
|
||||
setTimeFormatLeft(_left + 10);
|
||||
setTimeFormatTop(_top);
|
||||
};
|
||||
|
||||
const _onDateFormatClick: MouseEventHandler = (e) => {
|
||||
e.stopPropagation();
|
||||
let target = e.target as HTMLElement;
|
||||
|
||||
while (!(target instanceof HTMLButtonElement)) {
|
||||
if (target.parentElement === null) return;
|
||||
target = target.parentElement;
|
||||
}
|
||||
|
||||
const { right: _left, top: _top } = target.getBoundingClientRect();
|
||||
|
||||
onDateFormatClick(_left, _top);
|
||||
};
|
||||
|
||||
const _onTimeFormatClick: MouseEventHandler = (e) => {
|
||||
e.stopPropagation();
|
||||
let target = e.target as HTMLElement;
|
||||
|
||||
while (!(target instanceof HTMLButtonElement)) {
|
||||
if (target.parentElement === null) return;
|
||||
target = target.parentElement;
|
||||
}
|
||||
|
||||
const { right: _left, top: _top } = target.getBoundingClientRect();
|
||||
|
||||
onTimeFormatClick(_left, _top);
|
||||
};
|
||||
|
||||
const toggleIncludeTime = async () => {
|
||||
// if (dateType?.includeTime) {
|
||||
// await includeTime(false);
|
||||
// } else {
|
||||
// await includeTime(true);
|
||||
// }
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={'flex flex-col'}>
|
||||
<hr className={'-mx-2 my-2 border-line-divider'} />
|
||||
<button
|
||||
onClick={_onDateFormatClick}
|
||||
className={
|
||||
'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-2 hover:bg-fill-list-hover'
|
||||
}
|
||||
>
|
||||
<span>{t('grid.field.dateFormat')}</span>
|
||||
<i className={'h-5 w-5'}>
|
||||
<MoreSvg></MoreSvg>
|
||||
</i>
|
||||
</button>
|
||||
<hr className={'-mx-2 my-2 border-line-divider'} />
|
||||
<button
|
||||
onClick={() => toggleIncludeTime()}
|
||||
className={
|
||||
'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-2 hover:bg-fill-list-hover'
|
||||
}
|
||||
>
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<span>{t('grid.field.includeTime')}</span>
|
||||
</div>
|
||||
{/*<i className={'h-5 w-5'}>*/}
|
||||
{/* {dateType?.includeTime ? <EditorCheckSvg></EditorCheckSvg> : <EditorUncheckSvg></EditorUncheckSvg>}*/}
|
||||
{/*</i>*/}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={_onTimeFormatClick}
|
||||
className={
|
||||
'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-2 hover:bg-fill-list-hover'
|
||||
}
|
||||
>
|
||||
<span>{t('grid.field.timeFormat')}</span>
|
||||
<i className={'h-5 w-5'}>
|
||||
<MoreSvg></MoreSvg>
|
||||
</i>
|
||||
</button>
|
||||
{showDateFormatPopup && (
|
||||
<DateFormatPopup
|
||||
top={dateFormatTop}
|
||||
left={dateFormatLeft}
|
||||
cellIdentifier={cellIdentifier}
|
||||
fieldController={fieldController}
|
||||
onOutsideClick={() => setShowDateFormatPopup(false)}
|
||||
></DateFormatPopup>
|
||||
)}
|
||||
{showTimeFormatPopup && (
|
||||
<TimeFormatPopup
|
||||
top={timeFormatTop}
|
||||
left={timeFormatLeft}
|
||||
cellIdentifier={cellIdentifier}
|
||||
fieldController={fieldController}
|
||||
onOutsideClick={() => setShowTimeFormatPopup(false)}
|
||||
></TimeFormatPopup>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,25 +0,0 @@
|
||||
import { MouseEventHandler, useRef } from 'react';
|
||||
import { DateCellDataPB } from '@/services/backend';
|
||||
|
||||
export const EditCellDate = ({
|
||||
data,
|
||||
onEditClick,
|
||||
}: {
|
||||
data?: DateCellDataPB;
|
||||
onEditClick: (left: number, top: number) => void;
|
||||
}) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const onClick: MouseEventHandler = () => {
|
||||
if (!ref.current) return;
|
||||
const { left, top } = ref.current.getBoundingClientRect();
|
||||
|
||||
onEditClick(left, top);
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={ref} onClick={onClick} className={'w-full px-4 py-1'}>
|
||||
{data?.date}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,26 +0,0 @@
|
||||
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
|
||||
import { FieldController } from '$app/stores/effects/database/field/field_controller';
|
||||
import { FieldType, NumberFormatPB } from '@/services/backend';
|
||||
import { TypeOptionController } from '$app/stores/effects/database/field/type_option/type_option_controller';
|
||||
import { Some } from 'ts-results';
|
||||
import { makeNumberTypeOptionContext } from '$app/stores/effects/database/field/type_option/type_option_context';
|
||||
|
||||
export const useNumberFormat = (cellIdentifier: CellIdentifier, fieldController: FieldController) => {
|
||||
const changeNumberFormat = async (format: NumberFormatPB) => {
|
||||
const fieldInfo = fieldController.getField(cellIdentifier.fieldId);
|
||||
|
||||
if (!fieldInfo) return;
|
||||
const typeOptionController = new TypeOptionController(cellIdentifier.viewId, Some(fieldInfo), FieldType.Number);
|
||||
|
||||
await typeOptionController.initialize();
|
||||
const numberTypeOptionContext = makeNumberTypeOptionContext(typeOptionController);
|
||||
const typeOption = numberTypeOptionContext.getTypeOption();
|
||||
|
||||
typeOption.format = format;
|
||||
await numberTypeOptionContext.setTypeOption(typeOption);
|
||||
};
|
||||
|
||||
return {
|
||||
changeNumberFormat,
|
||||
};
|
||||
};
|
@ -1,109 +0,0 @@
|
||||
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
|
||||
import { FieldController } from '$app/stores/effects/database/field/field_controller';
|
||||
import { PopupWindow } from '$app/components/_shared/PopupWindow';
|
||||
import { useNumberFormat } from '$app/components/_shared/EditRow/Date/NumberFormat.hooks';
|
||||
import { NumberFormatPB } from '@/services/backend';
|
||||
import { CheckmarkSvg } from '$app/components/_shared/svg/CheckmarkSvg';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { INumberType } from '$app_reducers/database/slice';
|
||||
|
||||
const list = [
|
||||
{ format: NumberFormatPB.Num, title: 'Num' },
|
||||
{ format: NumberFormatPB.USD, title: 'USD' },
|
||||
{ format: NumberFormatPB.CanadianDollar, title: 'CanadianDollar' },
|
||||
{ format: NumberFormatPB.EUR, title: 'EUR' },
|
||||
{ format: NumberFormatPB.Pound, title: 'Pound' },
|
||||
{ format: NumberFormatPB.Yen, title: 'Yen' },
|
||||
{ format: NumberFormatPB.Ruble, title: 'Ruble' },
|
||||
{ format: NumberFormatPB.Rupee, title: 'Rupee' },
|
||||
{ format: NumberFormatPB.Won, title: 'Won' },
|
||||
{ format: NumberFormatPB.Yuan, title: 'Yuan' },
|
||||
{ format: NumberFormatPB.Real, title: 'Real' },
|
||||
{ format: NumberFormatPB.Lira, title: 'Lira' },
|
||||
{ format: NumberFormatPB.Rupiah, title: 'Rupiah' },
|
||||
{ format: NumberFormatPB.Franc, title: 'Franc' },
|
||||
{ format: NumberFormatPB.HongKongDollar, title: 'HongKongDollar' },
|
||||
{ format: NumberFormatPB.NewZealandDollar, title: 'NewZealandDollar' },
|
||||
{ format: NumberFormatPB.Krona, title: 'Krona' },
|
||||
{ format: NumberFormatPB.NorwegianKrone, title: 'NorwegianKrone' },
|
||||
{ format: NumberFormatPB.MexicanPeso, title: 'MexicanPeso' },
|
||||
{ format: NumberFormatPB.Rand, title: 'Rand' },
|
||||
{ format: NumberFormatPB.NewTaiwanDollar, title: 'NewTaiwanDollar' },
|
||||
{ format: NumberFormatPB.DanishKrone, title: 'DanishKrone' },
|
||||
{ format: NumberFormatPB.Baht, title: 'Baht' },
|
||||
{ format: NumberFormatPB.Forint, title: 'Forint' },
|
||||
{ format: NumberFormatPB.Koruna, title: 'Koruna' },
|
||||
{ format: NumberFormatPB.Shekel, title: 'Shekel' },
|
||||
{ format: NumberFormatPB.ChileanPeso, title: 'ChileanPeso' },
|
||||
{ format: NumberFormatPB.PhilippinePeso, title: 'PhilippinePeso' },
|
||||
{ format: NumberFormatPB.Dirham, title: 'Dirham' },
|
||||
{ format: NumberFormatPB.ColombianPeso, title: 'ColombianPeso' },
|
||||
{ format: NumberFormatPB.Riyal, title: 'Riyal' },
|
||||
{ format: NumberFormatPB.Ringgit, title: 'Ringgit' },
|
||||
{ format: NumberFormatPB.Leu, title: 'Leu' },
|
||||
{ format: NumberFormatPB.ArgentinePeso, title: 'ArgentinePeso' },
|
||||
{ format: NumberFormatPB.UruguayanPeso, title: 'UruguayanPeso' },
|
||||
{ format: NumberFormatPB.Percent, title: 'Percent' },
|
||||
];
|
||||
|
||||
export const NumberFormatPopup = ({
|
||||
left,
|
||||
top,
|
||||
cellIdentifier,
|
||||
fieldController,
|
||||
onOutsideClick,
|
||||
}: {
|
||||
left: number;
|
||||
top: number;
|
||||
cellIdentifier: CellIdentifier;
|
||||
fieldController: FieldController;
|
||||
onOutsideClick: () => void;
|
||||
}) => {
|
||||
const { changeNumberFormat } = useNumberFormat(cellIdentifier, fieldController);
|
||||
const databaseStore = useAppSelector((state) => state.database);
|
||||
const [numberType, setNumberType] = useState<INumberType | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
setNumberType(databaseStore.fields[cellIdentifier.fieldId]?.fieldOptions as INumberType);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [databaseStore]);
|
||||
|
||||
const changeNumberFormatClick = async (format: NumberFormatPB) => {
|
||||
await changeNumberFormat(format);
|
||||
onOutsideClick();
|
||||
};
|
||||
|
||||
return (
|
||||
<PopupWindow className={'p-2 text-xs'} onOutsideClick={onOutsideClick} left={left} top={top}>
|
||||
<div className={'h-[400px] overflow-auto'}>
|
||||
{list.map((item, index) => (
|
||||
<FormatButton
|
||||
key={index}
|
||||
title={item.title}
|
||||
checked={numberType?.numberFormat === item.format}
|
||||
onClick={() => changeNumberFormatClick(item.format)}
|
||||
></FormatButton>
|
||||
))}
|
||||
</div>
|
||||
</PopupWindow>
|
||||
);
|
||||
};
|
||||
|
||||
const FormatButton = ({ title, checked, onClick }: { title: string; checked: boolean; onClick: () => void }) => {
|
||||
return (
|
||||
<button
|
||||
onClick={() => onClick()}
|
||||
className={
|
||||
'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-fill-list-hover'
|
||||
}
|
||||
>
|
||||
<span className={'block pr-8'}>{title}</span>
|
||||
{checked && (
|
||||
<div className={'h-5 w-5 p-1'}>
|
||||
<CheckmarkSvg></CheckmarkSvg>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
@ -1,73 +0,0 @@
|
||||
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
|
||||
import { FieldController } from '$app/stores/effects/database/field/field_controller';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PopupWindow } from '$app/components/_shared/PopupWindow';
|
||||
import { TimeFormatPB } from '@/services/backend';
|
||||
import { CheckmarkSvg } from '$app/components/_shared/svg/CheckmarkSvg';
|
||||
import { useDateTimeFormat } from '$app/components/_shared/EditRow/Date/DateTimeFormat.hooks';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { IDateType } from '$app_reducers/database/slice';
|
||||
|
||||
export const TimeFormatPopup = ({
|
||||
left,
|
||||
top,
|
||||
cellIdentifier,
|
||||
fieldController,
|
||||
onOutsideClick,
|
||||
}: {
|
||||
left: number;
|
||||
top: number;
|
||||
cellIdentifier: CellIdentifier;
|
||||
fieldController: FieldController;
|
||||
onOutsideClick: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const databaseStore = useAppSelector((state) => state.database);
|
||||
const [dateType, setDateType] = useState<IDateType | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
setDateType(databaseStore.fields[cellIdentifier.fieldId]?.fieldOptions as IDateType);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [databaseStore]);
|
||||
|
||||
const { changeTimeFormat } = useDateTimeFormat(cellIdentifier, fieldController);
|
||||
|
||||
const changeFormat = async (format: TimeFormatPB) => {
|
||||
await changeTimeFormat(format);
|
||||
onOutsideClick();
|
||||
};
|
||||
|
||||
return (
|
||||
<PopupWindow className={'p-2 text-xs'} onOutsideClick={onOutsideClick} left={left} top={top}>
|
||||
<button
|
||||
onClick={() => changeFormat(TimeFormatPB.TwelveHour)}
|
||||
className={
|
||||
'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-fill-list-hover'
|
||||
}
|
||||
>
|
||||
{t('grid.field.timeFormatTwelveHour')}
|
||||
|
||||
{dateType?.timeFormat === TimeFormatPB.TwelveHour && (
|
||||
<div className={'ml-8 h-5 w-5 p-1'}>
|
||||
<CheckmarkSvg></CheckmarkSvg>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => changeFormat(TimeFormatPB.TwentyFourHour)}
|
||||
className={
|
||||
'flex w-full cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-fill-list-hover'
|
||||
}
|
||||
>
|
||||
{t('grid.field.timeFormatTwentyFourHour')}
|
||||
|
||||
{dateType?.timeFormat === TimeFormatPB.TwentyFourHour && (
|
||||
<div className={'ml-8 h-5 w-5 p-1'}>
|
||||
<CheckmarkSvg></CheckmarkSvg>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</PopupWindow>
|
||||
);
|
||||
};
|
@ -1,143 +0,0 @@
|
||||
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
|
||||
import { useCell } from '$app/components/_shared/database-hooks/useCell';
|
||||
import { CellCache } from '$app/stores/effects/database/cell/cell_cache';
|
||||
import { FieldController } from '$app/stores/effects/database/field/field_controller';
|
||||
import { DateCellDataPB, FieldType, SelectOptionCellDataPB, URLCellDataPB } from '@/services/backend';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
import { EditCellText } from '$app/components/_shared/EditRow/InlineEditFields/EditCellText';
|
||||
import { FieldTypeIcon } from '$app/components/_shared/EditRow/FieldTypeIcon';
|
||||
import { EditCellDate } from '$app/components/_shared/EditRow/Date/EditCellDate';
|
||||
import { useRef } from 'react';
|
||||
import { CellOptions } from '$app/components/_shared/EditRow/Options/CellOptions';
|
||||
import { EditCellNumber } from '$app/components/_shared/EditRow/InlineEditFields/EditCellNumber';
|
||||
import { EditCheckboxCell } from '$app/components/_shared/EditRow/InlineEditFields/EditCheckboxCell';
|
||||
import { EditCellUrl } from '$app/components/_shared/EditRow/InlineEditFields/EditCellUrl';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
import { DragElementSvg } from '$app/components/_shared/svg/DragElementSvg';
|
||||
import { CheckList } from '$app/components/_shared/EditRow/CheckList/CheckList';
|
||||
|
||||
export const EditCellWrapper = ({
|
||||
index,
|
||||
cellIdentifier,
|
||||
cellCache,
|
||||
fieldController,
|
||||
onEditFieldClick,
|
||||
onEditOptionsClick,
|
||||
onEditDateClick,
|
||||
onEditCheckListClick,
|
||||
}: {
|
||||
index: number;
|
||||
cellIdentifier: CellIdentifier;
|
||||
cellCache: CellCache;
|
||||
fieldController: FieldController;
|
||||
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;
|
||||
}) => {
|
||||
const { data, cellController } = useCell(cellIdentifier, cellCache, fieldController);
|
||||
const databaseStore = useAppSelector((state) => state.database);
|
||||
const el = useRef<HTMLDivElement>(null);
|
||||
|
||||
const onClick = () => {
|
||||
if (!el.current) return;
|
||||
|
||||
onEditFieldClick(cellIdentifier, el.current);
|
||||
};
|
||||
|
||||
return (
|
||||
<Draggable draggableId={cellIdentifier.fieldId} index={index} key={cellIdentifier.fieldId}>
|
||||
{(provided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
className={'flex w-full flex-col items-start gap-2 text-xs'}
|
||||
>
|
||||
<div className={'relative flex cursor-pointer items-center gap-2 rounded-lg transition-colors duration-200'}>
|
||||
<div
|
||||
ref={el}
|
||||
onClick={() => onClick()}
|
||||
className={'text-icon-default flex h-5 w-5 rounded hover:bg-fill-list-hover'}
|
||||
>
|
||||
<DragElementSvg></DragElementSvg>
|
||||
</div>
|
||||
|
||||
<div className={'flex h-5 w-5 flex-shrink-0 items-center justify-center text-text-caption'}>
|
||||
<FieldTypeIcon fieldType={cellIdentifier.fieldType}></FieldTypeIcon>
|
||||
</div>
|
||||
<span className={'overflow-hidden text-ellipsis whitespace-nowrap text-text-caption'}>
|
||||
{databaseStore.fields[cellIdentifier.fieldId]?.title ?? ''}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className={'w-full cursor-pointer rounded-lg pl-3 text-sm hover:bg-content-blue-50'}>
|
||||
{(cellIdentifier.fieldType === FieldType.SingleSelect ||
|
||||
cellIdentifier.fieldType === FieldType.MultiSelect) &&
|
||||
cellController && (
|
||||
<CellOptions
|
||||
data={data as SelectOptionCellDataPB}
|
||||
onEditClick={(left, top) => onEditOptionsClick(cellIdentifier, left, top)}
|
||||
></CellOptions>
|
||||
)}
|
||||
|
||||
{cellIdentifier.fieldType === FieldType.Checklist && cellController && (
|
||||
<CheckList
|
||||
data={data as SelectOptionCellDataPB}
|
||||
fieldId={cellIdentifier.fieldId}
|
||||
onEditClick={(left, top) => onEditCheckListClick(cellIdentifier, left, top)}
|
||||
></CheckList>
|
||||
)}
|
||||
|
||||
{cellIdentifier.fieldType === FieldType.Checkbox && cellController && (
|
||||
<EditCheckboxCell
|
||||
data={data as 'Yes' | 'No' | undefined}
|
||||
onToggle={async () => {
|
||||
if (data === 'Yes') {
|
||||
await cellController?.saveCellData('No');
|
||||
} else {
|
||||
await cellController?.saveCellData('Yes');
|
||||
}
|
||||
}}
|
||||
></EditCheckboxCell>
|
||||
)}
|
||||
|
||||
{cellIdentifier.fieldType === FieldType.DateTime && (
|
||||
<EditCellDate
|
||||
data={data as DateCellDataPB}
|
||||
onEditClick={(left, top) => onEditDateClick(cellIdentifier, left, top)}
|
||||
></EditCellDate>
|
||||
)}
|
||||
|
||||
{cellIdentifier.fieldType === FieldType.Number && cellController && (
|
||||
<EditCellNumber
|
||||
data={data as string | undefined}
|
||||
onSave={async (value) => {
|
||||
await cellController?.saveCellData(value);
|
||||
}}
|
||||
></EditCellNumber>
|
||||
)}
|
||||
|
||||
{cellIdentifier.fieldType === FieldType.URL && cellController && (
|
||||
<EditCellUrl
|
||||
data={data as URLCellDataPB}
|
||||
onSave={async (value) => {
|
||||
await cellController?.saveCellData(value);
|
||||
}}
|
||||
></EditCellUrl>
|
||||
)}
|
||||
|
||||
{cellIdentifier.fieldType === FieldType.RichText && cellController && (
|
||||
<EditCellText
|
||||
data={data as string | undefined}
|
||||
onSave={async (value) => {
|
||||
await cellController?.saveCellData(value);
|
||||
}}
|
||||
></EditCellText>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
);
|
||||
};
|
@ -1,231 +0,0 @@
|
||||
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 { 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 { 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 = ({
|
||||
open,
|
||||
anchorEl,
|
||||
cellIdentifier,
|
||||
viewId,
|
||||
onOutsideClick,
|
||||
controller,
|
||||
changeFieldTypeClick,
|
||||
onDeletePropertyClick,
|
||||
}: {
|
||||
open: boolean;
|
||||
anchorEl: HTMLDivElement | null;
|
||||
cellIdentifier: CellIdentifier;
|
||||
viewId: string;
|
||||
onOutsideClick: () => void;
|
||||
controller: DatabaseController;
|
||||
changeFieldTypeClick: (el: HTMLDivElement) => void;
|
||||
onDeletePropertyClick: (fieldId: string) => void;
|
||||
}) => {
|
||||
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(fieldsStore[cellIdentifier.fieldId].title);
|
||||
}, [fieldsStore, cellIdentifier]);
|
||||
|
||||
// focus input on mount
|
||||
useEffect(() => {
|
||||
if (!inputRef.current || !name) return;
|
||||
inputRef.current.focus();
|
||||
}, [inputRef, name]);
|
||||
|
||||
const selectAll: FocusEventHandler<HTMLInputElement> = (e) => {
|
||||
e.target.selectionStart = 0;
|
||||
e.target.selectionEnd = e.target.value.length;
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
if (!controller) return;
|
||||
const fieldInfo = controller.fieldController.getField(cellIdentifier.fieldId);
|
||||
|
||||
if (!fieldInfo) return;
|
||||
const typeOptionController = new TypeOptionController(viewId, Some(fieldInfo));
|
||||
|
||||
await typeOptionController.initialize();
|
||||
await typeOptionController.setFieldName(name);
|
||||
};
|
||||
|
||||
const onChangeFieldTypeClick = () => {
|
||||
if (!changeTypeButtonRef.current) return;
|
||||
|
||||
changeFieldTypeClick(changeTypeButtonRef.current);
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
if (fieldInfo) {
|
||||
const typeController = new TypeOptionController(viewId, Some(fieldInfo));
|
||||
|
||||
await typeController.initialize();
|
||||
if (fieldInfo.field.visibility) {
|
||||
await typeController.hideField();
|
||||
} else {
|
||||
await typeController.showField();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onDuplicatePropertyClick = async () => {
|
||||
onOutsideClick();
|
||||
await controller.duplicateField(cellIdentifier.fieldId);
|
||||
};
|
||||
|
||||
const onAddToLeftClick = async () => {
|
||||
onOutsideClick();
|
||||
await controller.addFieldToLeft(cellIdentifier.fieldId);
|
||||
};
|
||||
|
||||
const onAddToRightClick = async () => {
|
||||
onOutsideClick();
|
||||
await controller.addFieldToRight(cellIdentifier.fieldId);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={async () => {
|
||||
await save();
|
||||
onOutsideClick();
|
||||
}}
|
||||
disableRestoreFocus={true}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
>
|
||||
<div className={'flex flex-col gap-2 p-2 text-xs'}>
|
||||
<input
|
||||
ref={inputRef}
|
||||
onFocus={selectAll}
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
onBlur={() => save()}
|
||||
className={
|
||||
'flex-1 rounded border border-line-divider px-2 py-2 hover:border-fill-default focus:border-fill-default'
|
||||
}
|
||||
/>
|
||||
|
||||
<div
|
||||
ref={changeTypeButtonRef}
|
||||
onClick={() => onChangeFieldTypeClick()}
|
||||
className={
|
||||
'relative flex cursor-pointer items-center justify-between rounded-lg py-2 text-text-title hover:bg-fill-list-hover'
|
||||
}
|
||||
>
|
||||
<button className={'flex cursor-pointer items-center gap-2 rounded-lg pl-2'}>
|
||||
<i className={'h-5 w-5'}>
|
||||
<FieldTypeIcon fieldType={cellIdentifier.fieldType}></FieldTypeIcon>
|
||||
</i>
|
||||
<span>
|
||||
<FieldTypeName fieldType={cellIdentifier.fieldType}></FieldTypeName>
|
||||
</span>
|
||||
</button>
|
||||
<span className={'pr-2'}>
|
||||
<i className={' block h-5 w-5'}>
|
||||
<MoreSvg></MoreSvg>
|
||||
</i>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className={'-mx-2 h-[1px] bg-line-divider'}></div>
|
||||
|
||||
<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>
|
||||
</Popover>
|
||||
);
|
||||
};
|
@ -1,367 +0,0 @@
|
||||
import { CloseSvg } from '$app/components/_shared/svg/CloseSvg';
|
||||
import { useRow } from '$app/components/_shared/database-hooks/useRow';
|
||||
import { DatabaseController } from '$app/stores/effects/database/database_controller';
|
||||
import { RowInfo } from '$app/stores/effects/database/row/row_cache';
|
||||
import { EditCellWrapper } from '$app/components/_shared/EditRow/EditCellWrapper';
|
||||
import AddSvg from '$app/components/_shared/svg/AddSvg';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { EditFieldPopup } from '$app/components/_shared/EditRow/EditFieldPopup';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
|
||||
import { ChangeFieldTypePopup } from '$app/components/_shared/EditRow/ChangeFieldTypePopup';
|
||||
import { TypeOptionController } from '$app/stores/effects/database/field/type_option/type_option_controller';
|
||||
import { Some } from 'ts-results';
|
||||
import { FieldType, SelectOptionPB } from '@/services/backend';
|
||||
import { CellOptionsPopup } from '$app/components/_shared/EditRow/Options/CellOptionsPopup';
|
||||
import { DatePickerPopup } from '$app/components/_shared/EditRow/Date/DatePickerPopup';
|
||||
import { DragDropContext, Droppable, OnDragEndResponder } from 'react-beautiful-dnd';
|
||||
import { EditCellOptionPopup } from '$app/components/_shared/EditRow/Options/EditCellOptionPopup';
|
||||
import { NumberFormatPopup } from '$app/components/_shared/EditRow/Date/NumberFormatPopup';
|
||||
import { CheckListPopup } from '$app/components/_shared/EditRow/CheckList/CheckListPopup';
|
||||
import { EditCheckListPopup } from '$app/components/_shared/EditRow/CheckList/EditCheckListPopup';
|
||||
import { PropertiesPanel } from '$app/components/_shared/EditRow/PropertiesPanel';
|
||||
import { ImageSvg } from '$app/components/_shared/svg/ImageSvg';
|
||||
import { PromptWindow } from '$app/components/_shared/PromptWindow';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
|
||||
export const EditRow = ({
|
||||
onClose,
|
||||
viewId,
|
||||
controller,
|
||||
rowInfo,
|
||||
}: {
|
||||
onClose: () => void;
|
||||
viewId: string;
|
||||
controller: DatabaseController;
|
||||
rowInfo: RowInfo;
|
||||
}) => {
|
||||
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 [showChangeFieldTypePopup, setShowChangeFieldTypePopup] = useState(false);
|
||||
const [changeFieldTypeAnchorEl, setChangeFieldTypeAnchorEl] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
const [showChangeOptionsPopup, setShowChangeOptionsPopup] = useState(false);
|
||||
const [changeOptionsTop, setChangeOptionsTop] = useState(0);
|
||||
const [changeOptionsLeft, setChangeOptionsLeft] = useState(0);
|
||||
|
||||
const [showDatePicker, setShowDatePicker] = useState(false);
|
||||
const [datePickerTop, setDatePickerTop] = useState(0);
|
||||
const [datePickerLeft, setDatePickerLeft] = useState(0);
|
||||
|
||||
const [showEditCellOption, setShowEditCellOption] = useState(false);
|
||||
const [editCellOptionTop, setEditCellOptionTop] = useState(0);
|
||||
const [editCellOptionLeft, setEditCellOptionLeft] = useState(0);
|
||||
|
||||
const [editingSelectOption, setEditingSelectOption] = useState<SelectOptionPB | undefined>();
|
||||
|
||||
const [showEditCheckList, setShowEditCheckList] = useState(false);
|
||||
const [editCheckListTop, setEditCheckListTop] = useState(0);
|
||||
const [editCheckListLeft, setEditCheckListLeft] = useState(0);
|
||||
|
||||
const [showNumberFormatPopup, setShowNumberFormatPopup] = useState(false);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [numberFormatTop, setNumberFormatTop] = useState(0);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [numberFormatLeft, setNumberFormatLeft] = useState(0);
|
||||
|
||||
const [showCheckListPopup, setShowCheckListPopup] = useState(false);
|
||||
const [checkListPopupTop, setCheckListPopupTop] = useState(0);
|
||||
const [checkListPopupLeft, setCheckListPopupLeft] = useState(0);
|
||||
|
||||
const [deletingPropertyId, setDeletingPropertyId] = useState<string | null>(null);
|
||||
const [showDeletePropertyPrompt, setShowDeletePropertyPrompt] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setUnveil(true);
|
||||
}, []);
|
||||
|
||||
const onCloseClick = () => {
|
||||
setUnveil(false);
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const onEditFieldClick = (cellIdentifier: CellIdentifier, anchorEl: HTMLDivElement) => {
|
||||
setEditFieldAnchorEl(anchorEl);
|
||||
setEditingCell(cellIdentifier);
|
||||
setShowFieldEditor(true);
|
||||
};
|
||||
|
||||
const onOutsideEditFieldClick = () => {
|
||||
setShowFieldEditor(false);
|
||||
};
|
||||
|
||||
const onChangeFieldTypeClick = (el: HTMLDivElement) => {
|
||||
setChangeFieldTypeAnchorEl(el);
|
||||
setShowChangeFieldTypePopup(true);
|
||||
};
|
||||
|
||||
const changeFieldType = async (newType: FieldType) => {
|
||||
if (!editingCell) return;
|
||||
|
||||
const currentField = controller.fieldController.getField(editingCell.fieldId);
|
||||
|
||||
if (!currentField) return;
|
||||
|
||||
const typeOptionController = new TypeOptionController(viewId, Some(currentField));
|
||||
|
||||
await typeOptionController.switchToField(newType);
|
||||
|
||||
setEditingCell(new CellIdentifier(viewId, rowInfo.row.id, editingCell.fieldId, newType));
|
||||
|
||||
setShowChangeFieldTypePopup(false);
|
||||
};
|
||||
|
||||
const onEditOptionsClick = async (cellIdentifier: CellIdentifier, left: number, top: number) => {
|
||||
setEditingCell(cellIdentifier);
|
||||
setChangeOptionsLeft(left);
|
||||
setChangeOptionsTop(top + 40);
|
||||
setShowChangeOptionsPopup(true);
|
||||
};
|
||||
|
||||
const onEditDateClick = async (cellIdentifier: CellIdentifier, left: number, top: number) => {
|
||||
setEditingCell(cellIdentifier);
|
||||
setDatePickerLeft(left);
|
||||
setDatePickerTop(top + 40);
|
||||
setShowDatePicker(true);
|
||||
};
|
||||
|
||||
const onOpenOptionDetailClick = (_left: number, _top: number, _select_option: SelectOptionPB) => {
|
||||
setEditingSelectOption(_select_option);
|
||||
setShowEditCellOption(true);
|
||||
setEditCellOptionLeft(_left);
|
||||
setEditCellOptionTop(_top);
|
||||
};
|
||||
|
||||
const onOpenCheckListDetailClick = (_left: number, _top: number, _select_option: SelectOptionPB) => {
|
||||
setEditingSelectOption(_select_option);
|
||||
setShowEditCheckList(true);
|
||||
setEditCheckListLeft(_left + 10);
|
||||
setEditCheckListTop(_top);
|
||||
};
|
||||
|
||||
const onEditCheckListClick = (cellIdentifier: CellIdentifier, left: number, top: number) => {
|
||||
setEditingCell(cellIdentifier);
|
||||
setShowCheckListPopup(true);
|
||||
setCheckListPopupLeft(left);
|
||||
setCheckListPopupTop(top + 40);
|
||||
};
|
||||
|
||||
const onDragEnd: OnDragEndResponder = (result) => {
|
||||
if (!result.destination?.index) return;
|
||||
const fields = cells
|
||||
.filter((cell) => {
|
||||
return fieldsStore[cell.cellIdentifier.fieldId]?.visible;
|
||||
});
|
||||
|
||||
void controller.moveField({
|
||||
fromFieldId: result.draggableId,
|
||||
toFieldId: fields[result.source.index].fieldId,
|
||||
});
|
||||
};
|
||||
|
||||
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(viewId, Some(fieldInfo));
|
||||
|
||||
setEditingCell(null);
|
||||
|
||||
await typeController.initialize();
|
||||
await typeController.deleteField();
|
||||
setShowDeletePropertyPrompt(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`fixed inset-0 z-10 flex items-center justify-center bg-black/30 backdrop-blur-sm transition-opacity duration-300 ${unveil ? 'opacity-100' : 'opacity-0'
|
||||
}`}
|
||||
onClick={() => onCloseClick()}
|
||||
>
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className={`relative flex h-[90%] w-[70%] flex-col gap-8 rounded-xl bg-bg-body `}
|
||||
>
|
||||
<div onClick={() => onCloseClick()} className={'absolute right-1 top-1'}>
|
||||
<button className={'block h-8 w-8 rounded-lg text-text-title hover:bg-fill-list-hover'}>
|
||||
<CloseSvg></CloseSvg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={'flex h-full'}>
|
||||
<div className={'flex h-full flex-1 flex-col border-r border-line-divider pb-4 pt-6'}>
|
||||
<div className={'pb-4 pl-12'}>
|
||||
<button className={'flex items-center gap-2 p-4'}>
|
||||
<i className={'h-5 w-5'}>
|
||||
<ImageSvg></ImageSvg>
|
||||
</i>
|
||||
<span className={'text-xs'}>Add Cover</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId={'field-list'}>
|
||||
{(provided) => (
|
||||
<div
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
className={`flex flex-1 flex-col gap-8 px-8 pb-8 ${showFieldEditor || showChangeOptionsPopup || showDatePicker ? 'overflow-hidden' : 'overflow-auto'
|
||||
}`}
|
||||
>
|
||||
{cells
|
||||
.filter((cell) => {
|
||||
return fieldsStore[cell.cellIdentifier.fieldId]?.visible;
|
||||
})
|
||||
.map((cell, cellIndex) => (
|
||||
<EditCellWrapper
|
||||
index={cellIndex}
|
||||
key={cellIndex}
|
||||
cellIdentifier={cell.cellIdentifier}
|
||||
cellCache={controller.databaseViewCache.getRowCache().getCellCache()}
|
||||
fieldController={controller.fieldController}
|
||||
onEditFieldClick={onEditFieldClick}
|
||||
onEditOptionsClick={onEditOptionsClick}
|
||||
onEditDateClick={onEditDateClick}
|
||||
onEditCheckListClick={onEditCheckListClick}
|
||||
></EditCellWrapper>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
|
||||
<div className={'border-t border-line-divider px-8 pt-2'}>
|
||||
<button
|
||||
onClick={() => onNewColumnClick()}
|
||||
className={'flex w-full items-center gap-2 rounded-lg px-4 py-2 hover:bg-fill-list-hover'}
|
||||
>
|
||||
<i className={'h-5 w-5'}>
|
||||
<AddSvg></AddSvg>
|
||||
</i>
|
||||
<span>{t('grid.field.newProperty')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<PropertiesPanel
|
||||
viewId={viewId}
|
||||
controller={controller}
|
||||
rowInfo={rowInfo}
|
||||
onDeletePropertyClick={onDeletePropertyClick}
|
||||
onNewColumnClick={onNewColumnClick}
|
||||
></PropertiesPanel>
|
||||
</div>
|
||||
|
||||
{editingCell && (
|
||||
<EditFieldPopup
|
||||
open={showFieldEditor}
|
||||
anchorEl={editFieldAnchorEl}
|
||||
cellIdentifier={editingCell}
|
||||
viewId={viewId}
|
||||
onOutsideClick={onOutsideEditFieldClick}
|
||||
controller={controller}
|
||||
changeFieldTypeClick={onChangeFieldTypeClick}
|
||||
onDeletePropertyClick={onDeletePropertyClick}
|
||||
></EditFieldPopup>
|
||||
)}
|
||||
{showChangeFieldTypePopup && (
|
||||
<ChangeFieldTypePopup
|
||||
open={showChangeFieldTypePopup}
|
||||
anchorEl={changeFieldTypeAnchorEl}
|
||||
onClick={(newType) => changeFieldType(newType)}
|
||||
onOutsideClick={() => setShowChangeFieldTypePopup(false)}
|
||||
></ChangeFieldTypePopup>
|
||||
)}
|
||||
{showChangeOptionsPopup && editingCell && (
|
||||
<CellOptionsPopup
|
||||
top={changeOptionsTop}
|
||||
left={changeOptionsLeft}
|
||||
cellIdentifier={editingCell}
|
||||
cellCache={controller.databaseViewCache.getRowCache().getCellCache()}
|
||||
fieldController={controller.fieldController}
|
||||
onOutsideClick={() => !showEditCellOption && setShowChangeOptionsPopup(false)}
|
||||
openOptionDetail={onOpenOptionDetailClick}
|
||||
></CellOptionsPopup>
|
||||
)}
|
||||
{showDatePicker && editingCell && (
|
||||
<DatePickerPopup
|
||||
top={datePickerTop}
|
||||
left={datePickerLeft}
|
||||
cellIdentifier={editingCell}
|
||||
cellCache={controller.databaseViewCache.getRowCache().getCellCache()}
|
||||
fieldController={controller.fieldController}
|
||||
onOutsideClick={() => setShowDatePicker(false)}
|
||||
></DatePickerPopup>
|
||||
)}
|
||||
{showEditCellOption && editingCell && editingSelectOption && (
|
||||
<EditCellOptionPopup
|
||||
top={editCellOptionTop}
|
||||
left={editCellOptionLeft}
|
||||
cellIdentifier={editingCell}
|
||||
editingSelectOption={editingSelectOption}
|
||||
setEditingSelectOption={setEditingSelectOption}
|
||||
onOutsideClick={() => {
|
||||
setShowEditCellOption(false);
|
||||
}}
|
||||
></EditCellOptionPopup>
|
||||
)}
|
||||
{showNumberFormatPopup && editingCell && (
|
||||
<NumberFormatPopup
|
||||
top={numberFormatTop}
|
||||
left={numberFormatLeft}
|
||||
cellIdentifier={editingCell}
|
||||
fieldController={controller.fieldController}
|
||||
onOutsideClick={() => {
|
||||
setShowNumberFormatPopup(false);
|
||||
}}
|
||||
></NumberFormatPopup>
|
||||
)}
|
||||
{showCheckListPopup && editingCell && (
|
||||
<CheckListPopup
|
||||
top={checkListPopupTop}
|
||||
left={checkListPopupLeft}
|
||||
cellIdentifier={editingCell}
|
||||
cellCache={controller.databaseViewCache.getRowCache().getCellCache()}
|
||||
fieldController={controller.fieldController}
|
||||
onOutsideClick={() => !showEditCheckList && setShowCheckListPopup(false)}
|
||||
openCheckListDetail={onOpenCheckListDetailClick}
|
||||
></CheckListPopup>
|
||||
)}
|
||||
{showEditCheckList && editingCell && editingSelectOption && (
|
||||
<EditCheckListPopup
|
||||
top={editCheckListTop}
|
||||
left={editCheckListLeft}
|
||||
cellIdentifier={editingCell}
|
||||
editingSelectOption={editingSelectOption}
|
||||
onOutsideClick={() => setShowEditCheckList(false)}
|
||||
></EditCheckListPopup>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{showDeletePropertyPrompt && (
|
||||
<PromptWindow
|
||||
msg={'Are you sure you want to delete this property?'}
|
||||
onYes={() => onDelete()}
|
||||
onCancel={() => setShowDeletePropertyPrompt(false)}
|
||||
></PromptWindow>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,24 +0,0 @@
|
||||
import { FieldType } from '@/services/backend';
|
||||
import { TextTypeSvg } from '$app/components/_shared/svg/TextTypeSvg';
|
||||
import { NumberTypeSvg } from '$app/components/_shared/svg/NumberTypeSvg';
|
||||
import { DateTypeSvg } from '$app/components/_shared/svg/DateTypeSvg';
|
||||
import { SingleSelectTypeSvg } from '$app/components/_shared/svg/SingleSelectTypeSvg';
|
||||
import { MultiSelectTypeSvg } from '$app/components/_shared/svg/MultiSelectTypeSvg';
|
||||
import { ChecklistTypeSvg } from '$app/components/_shared/svg/ChecklistTypeSvg';
|
||||
import { UrlTypeSvg } from '$app/components/_shared/svg/UrlTypeSvg';
|
||||
import { CheckboxSvg } from '$app/components/_shared/svg/CheckboxSvg';
|
||||
|
||||
export const FieldTypeIcon = ({ fieldType }: { fieldType: FieldType }) => {
|
||||
return (
|
||||
<>
|
||||
{fieldType === FieldType.RichText && <TextTypeSvg></TextTypeSvg>}
|
||||
{fieldType === FieldType.Number && <NumberTypeSvg></NumberTypeSvg>}
|
||||
{fieldType === FieldType.DateTime && <DateTypeSvg></DateTypeSvg>}
|
||||
{fieldType === FieldType.SingleSelect && <SingleSelectTypeSvg></SingleSelectTypeSvg>}
|
||||
{fieldType === FieldType.MultiSelect && <MultiSelectTypeSvg></MultiSelectTypeSvg>}
|
||||
{fieldType === FieldType.Checklist && <ChecklistTypeSvg></ChecklistTypeSvg>}
|
||||
{fieldType === FieldType.URL && <UrlTypeSvg></UrlTypeSvg>}
|
||||
{fieldType === FieldType.Checkbox && <CheckboxSvg></CheckboxSvg>}
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,19 +0,0 @@
|
||||
import { FieldType } from '@/services/backend';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const FieldTypeName = ({ fieldType }: { fieldType: FieldType }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
{fieldType === FieldType.RichText && t('grid.field.textFieldName')}
|
||||
{fieldType === FieldType.Number && t('grid.field.numberFieldName')}
|
||||
{fieldType === FieldType.DateTime && t('grid.field.dateFieldName')}
|
||||
{fieldType === FieldType.SingleSelect && t('grid.field.singleSelectFieldName')}
|
||||
{fieldType === FieldType.MultiSelect && t('grid.field.multiSelectFieldName')}
|
||||
{fieldType === FieldType.Checklist && t('grid.field.checklistFieldName')}
|
||||
{fieldType === FieldType.URL && t('grid.field.urlFieldName')}
|
||||
{fieldType === FieldType.Checkbox && t('grid.field.checkboxFieldName')}
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,22 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
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);
|
||||
// };
|
||||
|
||||
return (
|
||||
<input
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onBlur={() => onSave(value)}
|
||||
className={'w-full px-4 py-1'}
|
||||
></input>
|
||||
);
|
||||
};
|
@ -1,31 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export const EditCellText = ({ data, onSave }: { data: string | undefined; onSave: (value: string) => void }) => {
|
||||
const [value, setValue] = useState('');
|
||||
const [contentRows, setContentRows] = useState(1);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(data ?? '');
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!value?.length) return;
|
||||
setContentRows(Math.max(1, (value ?? '').split('\n').length));
|
||||
}, [value]);
|
||||
|
||||
const onTextFieldChange = async (v: string) => {
|
||||
setValue(v);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<textarea
|
||||
className={'mt-0.5 h-full w-full resize-none px-4 py-1'}
|
||||
rows={contentRows}
|
||||
value={value}
|
||||
onChange={(e) => onTextFieldChange(e.target.value)}
|
||||
onBlur={() => onSave(value)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,19 +0,0 @@
|
||||
import { URLCellDataPB } from '@/services/backend';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export const EditCellUrl = ({ data, onSave }: { data: URLCellDataPB | undefined; onSave: (value: string) => void }) => {
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setValue((data as URLCellDataPB)?.url ?? '');
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<input
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onBlur={() => onSave(value)}
|
||||
className={'w-full px-4 py-1'}
|
||||
></input>
|
||||
);
|
||||
};
|
@ -1,20 +0,0 @@
|
||||
import { EditorCheckSvg } from '$app/components/_shared/svg/EditorCheckSvg';
|
||||
import { EditorUncheckSvg } from '$app/components/_shared/svg/EditorUncheckSvg';
|
||||
|
||||
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={() => onToggle()} className={'block px-4 py-1'}>
|
||||
<button className={'h-5 w-5'}>
|
||||
{data === 'Yes' ? <EditorCheckSvg></EditorCheckSvg> : <EditorUncheckSvg></EditorUncheckSvg>}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,81 +0,0 @@
|
||||
import { SelectOptionColorPB, SelectOptionPB } from '@/services/backend';
|
||||
import { getBgColor } from '$app/components/_shared/getColor';
|
||||
import { CheckmarkSvg } from '$app/components/_shared/svg/CheckmarkSvg';
|
||||
import { Details2Svg } from '$app/components/_shared/svg/Details2Svg';
|
||||
import { ISelectOption } from '$app_reducers/database/slice';
|
||||
import { SelectOptionCellBackendService } from '$app/stores/effects/database/cell/select_option_bd_svc';
|
||||
import { MouseEventHandler } from 'react';
|
||||
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
|
||||
|
||||
export const CellOption = ({
|
||||
option,
|
||||
checked,
|
||||
cellIdentifier,
|
||||
openOptionDetail,
|
||||
clearValue,
|
||||
noSelect,
|
||||
noDetail,
|
||||
onOptionClick,
|
||||
}: {
|
||||
option: ISelectOption;
|
||||
checked: boolean;
|
||||
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();
|
||||
let target = e.target as HTMLElement;
|
||||
|
||||
while (!(target instanceof HTMLButtonElement)) {
|
||||
if (target.parentElement === null) return;
|
||||
target = target.parentElement;
|
||||
}
|
||||
|
||||
const selectOption = new SelectOptionPB({
|
||||
id: option.selectOptionId,
|
||||
name: option.title,
|
||||
color: option.color ?? SelectOptionColorPB.Purple,
|
||||
});
|
||||
|
||||
const { right: _left, top: _top } = target.getBoundingClientRect();
|
||||
|
||||
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?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={onToggleOptionClick}
|
||||
className={'flex cursor-pointer items-center justify-between rounded-lg px-2 py-1.5 hover:bg-fill-list-hover'}
|
||||
>
|
||||
<div className={`${getBgColor(option.color)} rounded px-2 py-0.5 text-text-title`}>{option.title}</div>
|
||||
<div className={'flex items-center'}>
|
||||
{checked && (
|
||||
<button className={'h-5 w-5 p-1'}>
|
||||
<CheckmarkSvg></CheckmarkSvg>
|
||||
</button>
|
||||
)}
|
||||
{!noDetail && (
|
||||
<button onClick={onOptionDetailClick} className={'h-6 w-6 p-1'}>
|
||||
<Details2Svg></Details2Svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,31 +0,0 @@
|
||||
import { SelectOptionCellDataPB } from '@/services/backend';
|
||||
import { getBgColor } from '$app/components/_shared/getColor';
|
||||
import { MouseEventHandler, useRef } from 'react';
|
||||
|
||||
export const CellOptions = ({
|
||||
data,
|
||||
onEditClick,
|
||||
}: {
|
||||
data: SelectOptionCellDataPB | undefined;
|
||||
onEditClick: (left: number, top: number) => void;
|
||||
}) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const onClick: MouseEventHandler = () => {
|
||||
if (!ref.current) return;
|
||||
const { left, top } = ref.current.getBoundingClientRect();
|
||||
|
||||
onEditClick(left, top);
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={ref} onClick={onClick} className={'flex w-full flex-wrap items-center gap-2 px-4 py-1 text-xs'}>
|
||||
{data?.select_options?.map((option, index) => (
|
||||
<div className={`${getBgColor(option.color)} rounded px-2 py-0.5 text-text-title`} key={index}>
|
||||
{option?.name ?? ''}
|
||||
</div>
|
||||
))}
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,108 +0,0 @@
|
||||
import { KeyboardEventHandler, useEffect, useRef, useState } from 'react';
|
||||
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
|
||||
import { useCell } from '$app/components/_shared/database-hooks/useCell';
|
||||
import { CellCache } from '$app/stores/effects/database/cell/cell_cache';
|
||||
import { FieldController } from '$app/stores/effects/database/field/field_controller';
|
||||
import { SelectOptionCellDataPB, SelectOptionPB } from '@/services/backend';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SelectOptionCellBackendService } from '$app/stores/effects/database/cell/select_option_bd_svc';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
import { ISelectOptionType } from '$app_reducers/database/slice';
|
||||
import { PopupWindow } from '$app/components/_shared/PopupWindow';
|
||||
import { CellOption } from '$app/components/_shared/EditRow/Options/CellOption';
|
||||
import { SelectedOption } from '$app/components/_shared/EditRow/Options/SelectedOption';
|
||||
|
||||
export const CellOptionsPopup = ({
|
||||
top,
|
||||
left,
|
||||
cellIdentifier,
|
||||
cellCache,
|
||||
fieldController,
|
||||
onOutsideClick,
|
||||
openOptionDetail,
|
||||
}: {
|
||||
top: number;
|
||||
left: number;
|
||||
cellIdentifier: CellIdentifier;
|
||||
cellCache: CellCache;
|
||||
fieldController: FieldController;
|
||||
onOutsideClick: () => void;
|
||||
openOptionDetail: (_left: number, _top: number, _select_option: SelectOptionPB) => void;
|
||||
}) => {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const { t } = useTranslation();
|
||||
const [value, setValue] = useState('');
|
||||
const { data } = useCell(cellIdentifier, cellCache, fieldController);
|
||||
const databaseStore = useAppSelector((state) => state.database);
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef?.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [inputRef]);
|
||||
|
||||
const onKeyDown: KeyboardEventHandler = async (e) => {
|
||||
if (e.key === 'Enter' && value.length > 0) {
|
||||
await new SelectOptionCellBackendService(cellIdentifier).createOption({ name: value, isSelect: true });
|
||||
setValue('');
|
||||
}
|
||||
};
|
||||
|
||||
const onKeyDownWrapper: KeyboardEventHandler = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
onOutsideClick();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PopupWindow className={'p-2 text-xs'} onOutsideClick={onOutsideClick} left={left} top={top}>
|
||||
<div onKeyDown={onKeyDownWrapper} className={'flex flex-col gap-2 p-2'}>
|
||||
<div
|
||||
className={
|
||||
'flex flex-1 items-center gap-2 rounded border border-line-divider px-2 hover:border-fill-default focus:border-fill-default'
|
||||
}
|
||||
>
|
||||
<div className={'flex flex-wrap items-center gap-2 text-text-title'}>
|
||||
{(data as SelectOptionCellDataPB)?.select_options?.map((option, index) => (
|
||||
<SelectedOption
|
||||
option={option}
|
||||
key={index}
|
||||
cellIdentifier={cellIdentifier}
|
||||
clearValue={() => setValue('')}
|
||||
></SelectedOption>
|
||||
))}
|
||||
</div>
|
||||
<input
|
||||
ref={inputRef}
|
||||
className={'py-2'}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
placeholder={t('grid.selectOption.searchOption') ?? ''}
|
||||
onKeyDown={onKeyDown}
|
||||
/>
|
||||
<div className={'font-mono text-text-caption'}>{value.length}/30</div>
|
||||
</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(
|
||||
(option, index) => (
|
||||
<CellOption
|
||||
key={index}
|
||||
option={option}
|
||||
checked={
|
||||
!!(data as SelectOptionCellDataPB)?.select_options?.find(
|
||||
(selectedOption) => selectedOption.id === option.selectOptionId
|
||||
)
|
||||
}
|
||||
cellIdentifier={cellIdentifier}
|
||||
openOptionDetail={openOptionDetail}
|
||||
clearValue={() => setValue('')}
|
||||
></CellOption>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</PopupWindow>
|
||||
);
|
||||
};
|
@ -1,236 +0,0 @@
|
||||
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
|
||||
import { KeyboardEventHandler, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SelectOptionColorPB, SelectOptionPB } from '@/services/backend';
|
||||
import { getBgColor } from '$app/components/_shared/getColor';
|
||||
import { SelectOptionCellBackendService } from '$app/stores/effects/database/cell/select_option_bd_svc';
|
||||
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);
|
||||
}, [editingSelectOption]);
|
||||
|
||||
const onKeyDown: KeyboardEventHandler = async (e) => {
|
||||
if (e.key === 'Enter' && value.length > 0) {
|
||||
await new SelectOptionCellBackendService(cellIdentifier).createOption({ name: value });
|
||||
setValue('');
|
||||
}
|
||||
};
|
||||
|
||||
const onKeyDownWrapper: KeyboardEventHandler = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
onOutsideClick();
|
||||
}
|
||||
};
|
||||
|
||||
const onBlur = async () => {
|
||||
const svc = new SelectOptionCellBackendService(cellIdentifier);
|
||||
|
||||
await svc.updateOption(
|
||||
new SelectOptionPB({
|
||||
id: editingSelectOption.id,
|
||||
color: editingSelectOption.color,
|
||||
name: value,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
const updatedOption = new SelectOptionPB({
|
||||
id: editingSelectOption.id,
|
||||
color,
|
||||
name: editingSelectOption.name,
|
||||
});
|
||||
|
||||
await svc.updateOption(updatedOption);
|
||||
onUpdateSelectOption(updatedOption);
|
||||
};
|
||||
|
||||
const onDeleteOptionClick = async () => {
|
||||
const svc = new SelectOptionCellBackendService(cellIdentifier);
|
||||
|
||||
await svc.deleteOption([editingSelectOption]);
|
||||
onOutsideClick();
|
||||
};
|
||||
|
||||
return (
|
||||
<PopupWindow
|
||||
className={'p-2 text-xs'}
|
||||
onOutsideClick={async () => {
|
||||
await onBlur();
|
||||
onOutsideClick();
|
||||
}}
|
||||
left={left}
|
||||
top={top}
|
||||
>
|
||||
<div onKeyDown={onKeyDownWrapper} className={'flex flex-col gap-2 p-2'}>
|
||||
<div
|
||||
className={
|
||||
'flex flex-1 items-center gap-2 rounded border border-line-divider px-2 hover:border-fill-hover focus:border-fill-hover'
|
||||
}
|
||||
>
|
||||
<input
|
||||
ref={inputRef}
|
||||
className={'py-2'}
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
onBlur={() => onBlur()}
|
||||
/>
|
||||
<div className={'font-mono text-text-caption'}>{value.length}/30</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onDeleteOptionClick()}
|
||||
className={
|
||||
'text-main-alert flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 hover:bg-fill-list-hover'
|
||||
}
|
||||
>
|
||||
<i className={'h-5 w-5'}>
|
||||
<TrashSvg></TrashSvg>
|
||||
</i>
|
||||
<span>{t('grid.selectOption.deleteTag')}</span>
|
||||
</button>
|
||||
<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')}
|
||||
onClick={() => onColorClick(SelectOptionColorPB.Purple)}
|
||||
bgColor={getBgColor(SelectOptionColorPB.Purple)}
|
||||
checked={editingSelectOption.color === SelectOptionColorPB.Purple}
|
||||
></ColorItem>
|
||||
<ColorItem
|
||||
title={t('grid.selectOption.pinkColor')}
|
||||
onClick={() => onColorClick(SelectOptionColorPB.Pink)}
|
||||
bgColor={getBgColor(SelectOptionColorPB.Pink)}
|
||||
checked={editingSelectOption.color === SelectOptionColorPB.Pink}
|
||||
></ColorItem>
|
||||
<ColorItem
|
||||
title={t('grid.selectOption.lightPinkColor')}
|
||||
onClick={() => onColorClick(SelectOptionColorPB.LightPink)}
|
||||
bgColor={getBgColor(SelectOptionColorPB.LightPink)}
|
||||
checked={editingSelectOption.color === SelectOptionColorPB.LightPink}
|
||||
></ColorItem>
|
||||
<ColorItem
|
||||
title={t('grid.selectOption.orangeColor')}
|
||||
onClick={() => onColorClick(SelectOptionColorPB.Orange)}
|
||||
bgColor={getBgColor(SelectOptionColorPB.Orange)}
|
||||
checked={editingSelectOption.color === SelectOptionColorPB.Orange}
|
||||
></ColorItem>
|
||||
<ColorItem
|
||||
title={t('grid.selectOption.yellowColor')}
|
||||
onClick={() => onColorClick(SelectOptionColorPB.Yellow)}
|
||||
bgColor={getBgColor(SelectOptionColorPB.Yellow)}
|
||||
checked={editingSelectOption.color === SelectOptionColorPB.Yellow}
|
||||
></ColorItem>
|
||||
<ColorItem
|
||||
title={t('grid.selectOption.limeColor')}
|
||||
onClick={() => onColorClick(SelectOptionColorPB.Lime)}
|
||||
bgColor={getBgColor(SelectOptionColorPB.Lime)}
|
||||
checked={editingSelectOption.color === SelectOptionColorPB.Lime}
|
||||
></ColorItem>
|
||||
<ColorItem
|
||||
title={t('grid.selectOption.greenColor')}
|
||||
onClick={() => onColorClick(SelectOptionColorPB.Green)}
|
||||
bgColor={getBgColor(SelectOptionColorPB.Green)}
|
||||
checked={editingSelectOption.color === SelectOptionColorPB.Green}
|
||||
></ColorItem>
|
||||
<ColorItem
|
||||
title={t('grid.selectOption.aquaColor')}
|
||||
onClick={() => onColorClick(SelectOptionColorPB.Aqua)}
|
||||
bgColor={getBgColor(SelectOptionColorPB.Aqua)}
|
||||
checked={editingSelectOption.color === SelectOptionColorPB.Aqua}
|
||||
></ColorItem>
|
||||
<ColorItem
|
||||
title={t('grid.selectOption.blueColor')}
|
||||
onClick={() => onColorClick(SelectOptionColorPB.Blue)}
|
||||
bgColor={getBgColor(SelectOptionColorPB.Blue)}
|
||||
checked={editingSelectOption.color === SelectOptionColorPB.Blue}
|
||||
></ColorItem>
|
||||
</div>
|
||||
</div>
|
||||
</PopupWindow>
|
||||
);
|
||||
};
|
||||
|
||||
const ColorItem = ({
|
||||
title,
|
||||
bgColor,
|
||||
onClick,
|
||||
checked,
|
||||
}: {
|
||||
title: string;
|
||||
bgColor: string;
|
||||
onClick: () => void;
|
||||
checked: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={'flex cursor-pointer items-center justify-between rounded-lg p-2 hover:bg-fill-list-hover'}
|
||||
onClick={() => onClick()}
|
||||
>
|
||||
<div className={'flex items-center gap-2'}>
|
||||
<div className={`h-4 w-4 rounded-full ${bgColor}`}></div>
|
||||
<span>{title}</span>
|
||||
</div>
|
||||
{checked && (
|
||||
<i className={'block h-3 w-3'}>
|
||||
<CheckmarkSvg></CheckmarkSvg>
|
||||
</i>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,90 +0,0 @@
|
||||
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 { 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 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>
|
||||
);
|
||||
};
|
@ -1,30 +0,0 @@
|
||||
import { getBgColor } from '$app/components/_shared/getColor';
|
||||
import { CloseSvg } from '$app/components/_shared/svg/CloseSvg';
|
||||
import { SelectOptionPB } from '@/services/backend';
|
||||
import { SelectOptionCellBackendService } from '$app/stores/effects/database/cell/select_option_bd_svc';
|
||||
import { CellIdentifier } from '$app/stores/effects/database/cell/cell_bd_svc';
|
||||
import { MouseEventHandler } from 'react';
|
||||
|
||||
export const SelectedOption = ({
|
||||
option,
|
||||
cellIdentifier,
|
||||
clearValue,
|
||||
}: {
|
||||
option: SelectOptionPB;
|
||||
cellIdentifier: CellIdentifier;
|
||||
clearValue: () => void;
|
||||
}) => {
|
||||
const onUnselectOptionClick: MouseEventHandler = async () => {
|
||||
await new SelectOptionCellBackendService(cellIdentifier).unselectOption([option.id]);
|
||||
clearValue();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`${getBgColor(option.color)} flex items-center gap-0.5 rounded px-1 py-0.5 text-content-on-fill`}>
|
||||
<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,223 +0,0 @@
|
||||
import { DropDownShowSvg } from '$app/components/_shared/svg/DropDownShowSvg';
|
||||
import { useState } from 'react';
|
||||
import { useRow } from '$app/components/_shared/database-hooks/useRow';
|
||||
import { DatabaseController } from '$app/stores/effects/database/database_controller';
|
||||
import { RowInfo } from '$app/stores/effects/database/row/row_cache';
|
||||
import { FieldTypeIcon } from '$app/components/_shared/EditRow/FieldTypeIcon';
|
||||
import { useAppSelector } from '$app/stores/store';
|
||||
import { Switch } from '$app/components/_shared/Switch';
|
||||
import { FieldType } from '@/services/backend';
|
||||
import { FieldTypeName } from '$app/components/_shared/EditRow/FieldTypeName';
|
||||
import { TrashSvg } from '$app/components/_shared/svg/TrashSvg';
|
||||
import { MultiSelectTypeSvg } from '$app/components/_shared/svg/MultiSelectTypeSvg';
|
||||
import { DocumentSvg } from '$app/components/_shared/svg/DocumentSvg';
|
||||
import { SingleSelectTypeSvg } from '$app/components/_shared/svg/SingleSelectTypeSvg';
|
||||
import { TypeOptionController } from '$app/stores/effects/database/field/type_option/type_option_controller';
|
||||
import { Some } from 'ts-results';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const typesOrder: FieldType[] = [
|
||||
FieldType.RichText,
|
||||
FieldType.Number,
|
||||
FieldType.DateTime,
|
||||
FieldType.SingleSelect,
|
||||
FieldType.MultiSelect,
|
||||
FieldType.Checkbox,
|
||||
FieldType.URL,
|
||||
FieldType.Checklist,
|
||||
];
|
||||
|
||||
export const PropertiesPanel = ({
|
||||
viewId,
|
||||
controller,
|
||||
rowInfo,
|
||||
onDeletePropertyClick,
|
||||
onNewColumnClick,
|
||||
}: {
|
||||
viewId: string;
|
||||
controller: DatabaseController;
|
||||
rowInfo: RowInfo;
|
||||
onDeletePropertyClick: (fieldId: string) => void;
|
||||
onNewColumnClick: (initialFieldType: FieldType, name?: string) => Promise<void>;
|
||||
}) => {
|
||||
const { cells } = useRow(viewId, controller, rowInfo);
|
||||
const databaseStore = useAppSelector((state) => state.database);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [showAddedProperties, setShowAddedProperties] = useState(true);
|
||||
const [showBasicProperties, setShowBasicProperties] = useState(false);
|
||||
const [showAdvancedProperties, setShowAdvancedProperties] = useState(false);
|
||||
|
||||
const [hoveredPropertyIndex, setHoveredPropertyIndex] = useState(-1);
|
||||
|
||||
const toggleHideProperty = async (v: boolean, index: number) => {
|
||||
const fieldInfo = controller.fieldController.getField(cells[index].fieldId);
|
||||
|
||||
if (fieldInfo) {
|
||||
const typeController = new TypeOptionController(viewId, Some(fieldInfo));
|
||||
|
||||
await typeController.initialize();
|
||||
if (fieldInfo.field.visibility) {
|
||||
await typeController.hideField();
|
||||
} else {
|
||||
await typeController.showField();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const addSelectedFieldType = async (fieldType: FieldType) => {
|
||||
let name = 'New Field';
|
||||
|
||||
switch (fieldType) {
|
||||
case FieldType.RichText:
|
||||
name = t('grid.field.textFieldName');
|
||||
break;
|
||||
case FieldType.Number:
|
||||
name = t('grid.field.numberFieldName');
|
||||
break;
|
||||
case FieldType.DateTime:
|
||||
name = t('grid.field.dateFieldName');
|
||||
break;
|
||||
case FieldType.SingleSelect:
|
||||
name = t('grid.field.singleSelectFieldName');
|
||||
break;
|
||||
case FieldType.MultiSelect:
|
||||
name = t('grid.field.multiSelectFieldName');
|
||||
break;
|
||||
case FieldType.Checklist:
|
||||
name = t('grid.field.checklistFieldName');
|
||||
break;
|
||||
case FieldType.URL:
|
||||
name = t('grid.field.urlFieldName');
|
||||
break;
|
||||
case FieldType.Checkbox:
|
||||
name = t('grid.field.checkboxFieldName');
|
||||
break;
|
||||
}
|
||||
|
||||
await onNewColumnClick(fieldType, name);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={'flex flex-col gap-2 overflow-auto px-4 py-12'}>
|
||||
<div
|
||||
onClick={() => setShowAddedProperties(!showAddedProperties)}
|
||||
className={
|
||||
'flex cursor-pointer items-center justify-between gap-8 rounded-lg px-2 py-2 text-text-title hover:bg-fill-list-active'
|
||||
}
|
||||
>
|
||||
<div className={'text-sm'}>Added Properties</div>
|
||||
<i className={`h-5 w-5 transition-transform duration-500 ${showAddedProperties && 'rotate-180'}`}>
|
||||
<DropDownShowSvg></DropDownShowSvg>
|
||||
</i>
|
||||
</div>
|
||||
<div className={'flex flex-col text-xs'} onMouseLeave={() => setHoveredPropertyIndex(-1)}>
|
||||
{showAddedProperties &&
|
||||
cells.map((cell, cellIndex) => (
|
||||
<div
|
||||
key={cellIndex}
|
||||
onMouseEnter={() => setHoveredPropertyIndex(cellIndex)}
|
||||
className={
|
||||
'flex cursor-pointer items-center justify-between gap-4 rounded-lg px-2 py-1 hover:bg-fill-list-hover'
|
||||
}
|
||||
>
|
||||
<div className={'flex items-center gap-2 text-text-title '}>
|
||||
<div className={'flex h-5 w-5 flex-shrink-0 items-center justify-center'}>
|
||||
<FieldTypeIcon fieldType={cell.cellIdentifier.fieldType}></FieldTypeIcon>
|
||||
</div>
|
||||
<span className={'overflow-hidden text-ellipsis whitespace-nowrap'}>
|
||||
{databaseStore.fields[cell.cellIdentifier.fieldId]?.title ?? ''}
|
||||
</span>
|
||||
</div>
|
||||
<div className={'flex items-center'}>
|
||||
<i
|
||||
onClick={() => onDeletePropertyClick(cell.cellIdentifier.fieldId)}
|
||||
className={`h-[16px] w-[16px] text-text-title transition-opacity duration-300 ${
|
||||
hoveredPropertyIndex === cellIndex ? 'opacity-100' : 'opacity-0'
|
||||
}`}
|
||||
>
|
||||
<TrashSvg></TrashSvg>
|
||||
</i>
|
||||
<Switch
|
||||
value={!!databaseStore.fields[cell.cellIdentifier.fieldId]?.visible}
|
||||
setValue={(v) => toggleHideProperty(v, cellIndex)}
|
||||
></Switch>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
onClick={() => setShowBasicProperties(!showBasicProperties)}
|
||||
className={
|
||||
'flex cursor-pointer items-center justify-between gap-8 rounded-lg px-2 py-2 hover:bg-fill-list-active'
|
||||
}
|
||||
>
|
||||
<div className={'text-sm'}>Basic Properties</div>
|
||||
<i className={`h-5 w-5 transition-transform duration-500 ${showBasicProperties && 'rotate-180'}`}>
|
||||
<DropDownShowSvg></DropDownShowSvg>
|
||||
</i>
|
||||
</div>
|
||||
<div className={'flex flex-col gap-2 text-xs'}>
|
||||
{showBasicProperties && (
|
||||
<div className={'flex flex-col'}>
|
||||
{typesOrder.map((type, i) => (
|
||||
<button
|
||||
onClick={() => addSelectedFieldType(type)}
|
||||
key={i}
|
||||
className={'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 pr-8 hover:bg-fill-list-hover'}
|
||||
>
|
||||
<i className={'h-5 w-5'}>
|
||||
<FieldTypeIcon fieldType={type}></FieldTypeIcon>
|
||||
</i>
|
||||
<span>
|
||||
<FieldTypeName fieldType={type}></FieldTypeName>
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
onClick={() => setShowAdvancedProperties(!showAdvancedProperties)}
|
||||
className={
|
||||
'flex cursor-pointer items-center justify-between gap-8 rounded-lg px-2 py-2 hover:bg-fill-list-active'
|
||||
}
|
||||
>
|
||||
<div className={'text-sm'}>Advanced Properties</div>
|
||||
<i className={`h-5 w-5 transition-transform duration-500 ${showAdvancedProperties && 'rotate-180'}`}>
|
||||
<DropDownShowSvg></DropDownShowSvg>
|
||||
</i>
|
||||
</div>
|
||||
<div className={'flex flex-col gap-2 text-xs'}>
|
||||
{showAdvancedProperties && (
|
||||
<div className={'flex flex-col'}>
|
||||
<button
|
||||
className={'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 pr-8 hover:bg-fill-list-hover'}
|
||||
>
|
||||
<i className={'h-5 w-5'}>
|
||||
<MultiSelectTypeSvg></MultiSelectTypeSvg>
|
||||
</i>
|
||||
<span>Last edited time</span>
|
||||
</button>
|
||||
<button
|
||||
className={'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 pr-8 hover:bg-fill-list-hover'}
|
||||
>
|
||||
<i className={'h-5 w-5'}>
|
||||
<DocumentSvg></DocumentSvg>
|
||||
</i>
|
||||
<span>Document</span>
|
||||
</button>
|
||||
<button
|
||||
className={'flex cursor-pointer items-center gap-2 rounded-lg px-2 py-2 pr-8 hover:bg-fill-list-hover'}
|
||||
>
|
||||
<i className={'h-5 w-5'}>
|
||||
<SingleSelectTypeSvg></SingleSelectTypeSvg>
|
||||
</i>
|
||||
<span>Relation to</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,50 +0,0 @@
|
||||
import { IPopupItem, PopupSelect } from './PopupSelect';
|
||||
import i18n from 'i18next';
|
||||
|
||||
const supportedLanguages: { key: string; title: string }[] = [
|
||||
{
|
||||
key: 'en',
|
||||
title: 'English',
|
||||
},
|
||||
{ key: 'ar-SA', title: 'ar-SA' },
|
||||
{ key: 'ca-ES', title: 'ca-ES' },
|
||||
{ key: 'de-DE', title: 'de-DE' },
|
||||
{ key: 'es-VE', title: 'es-VE' },
|
||||
{ key: 'eu-ES', title: 'eu-ES' },
|
||||
{ key: 'fr-CA', title: 'fr-CA' },
|
||||
{ key: 'fr-FR', title: 'fr-FR' },
|
||||
{ key: 'hu-HU', title: 'hu-HU' },
|
||||
{ key: 'id-ID', title: 'id-ID' },
|
||||
{ key: 'it-IT', title: 'it-IT' },
|
||||
{ key: 'ja-JP', title: 'ja-JP' },
|
||||
{ key: 'ko-KR', title: 'ko-KR' },
|
||||
{ key: 'pl-PL', title: 'pl-PL' },
|
||||
{ key: 'pt-BR', title: 'pt-BR' },
|
||||
{ key: 'pt-PT', title: 'pt-PT' },
|
||||
{ key: 'ru-RU', title: 'ru-RU' },
|
||||
{ key: 'sv', title: 'sv' },
|
||||
{ key: 'th-TH', title: 'th-TH' },
|
||||
{ key: 'tr-TR', title: 'tr-TR' },
|
||||
{ key: 'zh-CN', title: 'zh-CN' },
|
||||
{ key: 'zh-TW', title: 'zh-TW' },
|
||||
];
|
||||
|
||||
export const LanguageSelectPopup = ({ onClose }: { onClose: () => void }) => {
|
||||
const items: IPopupItem[] = supportedLanguages.map<IPopupItem>((item) => ({
|
||||
onClick: () => {
|
||||
void i18n.changeLanguage(item.key);
|
||||
onClose();
|
||||
},
|
||||
title: item.title,
|
||||
icon: <></>,
|
||||
}));
|
||||
|
||||
return (
|
||||
<PopupSelect
|
||||
items={items}
|
||||
className={'absolute top-full right-0 z-10 w-[200px]'}
|
||||
onOutsideClick={onClose}
|
||||
columns={2}
|
||||
></PopupSelect>
|
||||
);
|
||||
};
|
@ -1,54 +0,0 @@
|
||||
import { CSSProperties, MouseEvent, ReactNode, useRef } from 'react';
|
||||
import useOutsideClick from './useOutsideClick';
|
||||
|
||||
export interface IPopupItem {
|
||||
icon: ReactNode | (() => JSX.Element);
|
||||
title: string;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export const PopupSelect = ({
|
||||
items,
|
||||
className = '',
|
||||
onOutsideClick,
|
||||
columns = 1,
|
||||
style,
|
||||
}: {
|
||||
items: IPopupItem[];
|
||||
className: string;
|
||||
onOutsideClick?: () => void;
|
||||
columns?: 1 | 2 | 3;
|
||||
style?: CSSProperties;
|
||||
}) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useOutsideClick(ref, () => onOutsideClick && onOutsideClick());
|
||||
|
||||
const handleClick = (e: MouseEvent, item: IPopupItem) => {
|
||||
e.stopPropagation();
|
||||
item.onClick();
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={ref} className={`${className} rounded-lg bg-bg-body px-2 py-2 text-text-title shadow-md`} style={style}>
|
||||
<div
|
||||
className={
|
||||
(columns === 2 ? 'grid grid-cols-2' : '') + (columns === 3 ? 'grid grid-cols-3' : '') + ' w-full gap-x-4'
|
||||
}
|
||||
>
|
||||
{items.map((item, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className={'flex w-full cursor-pointer items-center gap-2 rounded-lg px-2 py-2 hover:bg-fill-list-hover'}
|
||||
onClick={(e) => handleClick(e, item)}
|
||||
>
|
||||
<>
|
||||
{typeof item.icon === 'function' ? item.icon() : item.icon}
|
||||
<span className={'flex-shrink-0'}>{item.title}</span>
|
||||
</>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,68 +0,0 @@
|
||||
import { CSSProperties, ReactNode, useEffect, useRef, useState } from 'react';
|
||||
import useOutsideClick from '$app/components/_shared/useOutsideClick';
|
||||
|
||||
export const PopupWindow = ({
|
||||
children,
|
||||
className,
|
||||
onOutsideClick,
|
||||
left,
|
||||
top,
|
||||
style,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
onOutsideClick: () => void;
|
||||
left: number;
|
||||
top: number;
|
||||
style?: CSSProperties;
|
||||
}) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useOutsideClick(ref, onOutsideClick);
|
||||
|
||||
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;
|
||||
|
||||
new ResizeObserver(() => {
|
||||
if (!ref.current) return;
|
||||
const { height, width } = ref.current.getBoundingClientRect();
|
||||
|
||||
setAdjustedTop(top);
|
||||
if (top + height > window.innerHeight) {
|
||||
setStickToBottom(true);
|
||||
} else {
|
||||
setStickToBottom(false);
|
||||
}
|
||||
|
||||
setAdjustedLeft(left);
|
||||
if (left + width > window.innerWidth) {
|
||||
setStickToRight(true);
|
||||
} else {
|
||||
setStickToRight(false);
|
||||
}
|
||||
}).observe(ref.current);
|
||||
}, [ref, left, top]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={
|
||||
'fixed z-10 rounded-lg bg-bg-body shadow-md transition-opacity duration-300 ' +
|
||||
(adjustedTop === -100 && adjustedLeft === -100 ? 'opacity-0 ' : 'opacity-100 ') +
|
||||
(className ?? '')
|
||||
}
|
||||
style={{
|
||||
[stickToBottom ? 'bottom' : 'top']: `${stickToBottom ? '0' : adjustedTop}px`,
|
||||
[stickToRight ? 'right' : 'left']: `${stickToRight ? '0' : adjustedLeft}px`,
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,24 +0,0 @@
|
||||
import { Button } from '$app/components/_shared/Button';
|
||||
|
||||
export const PromptWindow = ({ msg, onYes, onCancel }: { msg: string; onYes: () => void; onCancel: () => void }) => {
|
||||
return (
|
||||
<div
|
||||
className='fixed inset-0 z-20 flex items-center justify-center bg-black/30 backdrop-blur-sm'
|
||||
onClick={() => onCancel()}
|
||||
>
|
||||
<div className={'rounded-xl bg-white p-16'} onClick={(e) => e.stopPropagation()}>
|
||||
<div className={'flex flex-col items-center justify-center gap-8'}>
|
||||
<div className={'text-text-title'}>{msg}</div>
|
||||
<div className={'flex items-center justify-around gap-4'}>
|
||||
<Button onClick={() => onCancel()} size={'medium-transparent'}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={() => onYes()} size={'medium'}>
|
||||
Yes
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,21 +0,0 @@
|
||||
import { SearchSvg } from './svg/SearchSvg';
|
||||
import { useState } from 'react';
|
||||
|
||||
export const SearchInput = () => {
|
||||
const [active, setActive] = useState(false);
|
||||
|
||||
return (
|
||||
<div className={`flex items-center rounded-lg border p-2 ${active ? 'border-fill-default' : 'border-line-divider'}`}>
|
||||
<i className='mr-2 h-5 w-5'>
|
||||
<SearchSvg />
|
||||
</i>
|
||||
<input
|
||||
onFocus={() => setActive(true)}
|
||||
onBlur={() => setActive(false)}
|
||||
className='w-52 text-sm text-text-placeholder focus:text-text-title'
|
||||
placeholder='Search'
|
||||
type='search'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,8 +0,0 @@
|
||||
export const Switch = ({ value, setValue }: { value: boolean; setValue: (v: boolean) => void }) => {
|
||||
return (
|
||||
<label className='form-switch' style={{ transform: 'scale(0.5)', marginRight: '-16px' }}>
|
||||
<input type='checkbox' checked={value} onChange={() => setValue(!value)} />
|
||||
<i></i>
|
||||
</label>
|
||||
);
|
||||
};
|
@ -1,9 +0,0 @@
|
||||
import { UserSettingController } from '$app/stores/effects/user/user_setting_controller';
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
export const UserSettingControllerContext = createContext<UserSettingController | undefined>(undefined);
|
||||
export function useUserSettingControllerContext() {
|
||||
const context = useContext(UserSettingControllerContext);
|
||||
|
||||
return context;
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
export const INITIAL_FOLDER_HEIGHT = 40;
|
||||
export const FOLDER_MARGIN = 16;
|
||||
export const PAGE_ITEM_HEIGHT = 40;
|
||||
export const ANIMATION_DURATION = 300;
|
||||
export const NAV_PANEL_MINIMUM_WIDTH = 200;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user