refactor: adjust tauri codes (#4332)

* fix: some bugs

* refactor: delete code that is no longer needed
This commit is contained in:
Kilu.He 2024-01-10 19:24:40 +08:00 committed by GitHub
parent a6baabbafc
commit 239bf2fa70
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
349 changed files with 1116 additions and 12185 deletions

View File

@ -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"
}
},
}
]
}

View File

@ -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",

View File

@ -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

View File

@ -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,
};
}

View File

@ -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>
);
}

View File

@ -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 }) {

View File

@ -5,7 +5,7 @@ import {
ChecklistCellDataChangesetPB,
DateChangesetPB,
FieldType,
} from '@/services/backend';
} from '../../../../services/backend';
import {
DatabaseEventGetCell,
DatabaseEventUpdateCell,

View File

@ -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;

View File

@ -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;

View File

@ -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,

View File

@ -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) {

View File

@ -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) => {

View File

@ -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) => {

View File

@ -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);
};

View File

@ -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);
};

View File

@ -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);
}

View File

@ -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;

View File

@ -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();
},
};

View File

@ -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;
},
};

View File

@ -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

View File

@ -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>
);
};

View File

@ -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>);
};

View File

@ -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,
};
};

View File

@ -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>
</>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
}

View File

@ -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>
);
};

View File

@ -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,
};
};

View File

@ -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>
);
};

View File

@ -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}&nbsp;
</div>
);
};

View File

@ -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,
};
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
)}
</>
);
};

View File

@ -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>}
</>
);
};

View File

@ -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')}
</>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
))}
&nbsp;
</div>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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;
}

View File

@ -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