mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: support all fields type and filters in grid of tauri
* feat: support the single select field actions in grid of tauri * feat: support multiselect * feat: support number field and number filter * feat: support url field * fix: eslint error * feat: support checkbox filter * feat: support checklist field * fix: adjusting keydown event * fix: edit record ui * feat: support date field * fix: url field bugs * fix: the bug of the type option wasn't update * chore: make plural tokens compatible with tauri * fix: plural key * fix: optimize get cell performance * fix: update ts error * fix: update select option bugs * fix: grid calculate css * fix: add DidUpdateFieldSettings --------- Co-authored-by: Richard Shiue <71320345+richardshiue@users.noreply.github.com>
This commit is contained in:
parent
5fa441cbf5
commit
a070ed2441
@ -422,8 +422,10 @@ class _UnscheduledEventsButtonState extends State<UnscheduledEventsButton> {
|
||||
}
|
||||
},
|
||||
child: FlowyTooltip(
|
||||
message: LocaleKeys.calendar_settings_noDateHint
|
||||
.plural(state.unscheduleEvents.length),
|
||||
message: LocaleKeys.calendar_settings_noDateHint.plural(
|
||||
state.unscheduleEvents.length,
|
||||
namedArgs: {'count': '${state.unscheduleEvents.length}'},
|
||||
),
|
||||
child: FlowyText.regular(
|
||||
"${LocaleKeys.calendar_settings_noDateTitle.tr()} (${state.unscheduleEvents.length})",
|
||||
fontSize: 10,
|
||||
|
@ -307,10 +307,14 @@ class ToggleHiddenFieldsVisibilityButton extends StatelessWidget {
|
||||
return BlocBuilder<RowDetailBloc, RowDetailState>(
|
||||
builder: (context, state) {
|
||||
final text = switch (state.showHiddenFields) {
|
||||
false => LocaleKeys.grid_rowPage_showHiddenFields
|
||||
.plural(state.numHiddenFields),
|
||||
true => LocaleKeys.grid_rowPage_hideHiddenFields
|
||||
.plural(state.numHiddenFields),
|
||||
false => LocaleKeys.grid_rowPage_showHiddenFields.plural(
|
||||
state.numHiddenFields,
|
||||
namedArgs: {'count': '${state.numHiddenFields}'},
|
||||
),
|
||||
true => LocaleKeys.grid_rowPage_hideHiddenFields.plural(
|
||||
state.numHiddenFields,
|
||||
namedArgs: {'count': '${state.numHiddenFields}'},
|
||||
),
|
||||
};
|
||||
|
||||
return SizedBox(
|
||||
|
@ -25,6 +25,7 @@
|
||||
"@mui/icons-material": "^5.11.11",
|
||||
"@mui/material": "^5.11.12",
|
||||
"@mui/system": "^5.14.4",
|
||||
"@mui/x-date-pickers-pro": "^6.18.2",
|
||||
"@reduxjs/toolkit": "^1.9.2",
|
||||
"@slate-yjs/core": "^1.0.0",
|
||||
"@tanstack/react-virtual": "3.0.0-beta.54",
|
||||
@ -51,6 +52,7 @@
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-calendar": "^4.1.0",
|
||||
"react-color": "^2.19.3",
|
||||
"react-datepicker": "^4.23.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-error-boundary": "^3.1.4",
|
||||
"react-i18next": "^12.2.0",
|
||||
@ -66,7 +68,7 @@
|
||||
"slate-react": "^0.94.2",
|
||||
"ts-results": "^3.3.0",
|
||||
"utf8": "^3.0.0",
|
||||
"valtio": "^1.11.1",
|
||||
"valtio": "^1.12.1",
|
||||
"yjs": "^13.5.51"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -83,6 +85,7 @@
|
||||
"@types/react": "^18.0.15",
|
||||
"@types/react-beautiful-dnd": "^13.1.3",
|
||||
"@types/react-color": "^3.0.6",
|
||||
"@types/react-datepicker": "^4.19.3",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/react-katex": "^3.0.0",
|
||||
"@types/react-transition-group": "^4.4.6",
|
||||
|
@ -22,6 +22,9 @@ dependencies:
|
||||
'@mui/system':
|
||||
specifier: ^5.14.4
|
||||
version: 5.14.4(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react@18.2.0)
|
||||
'@mui/x-date-pickers-pro':
|
||||
specifier: ^6.18.2
|
||||
version: 6.18.2(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@mui/material@5.13.0)(@mui/system@5.14.4)(@types/react@18.2.6)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@reduxjs/toolkit':
|
||||
specifier: ^1.9.2
|
||||
version: 1.9.5(react-redux@8.0.5)(react@18.2.0)
|
||||
@ -100,6 +103,9 @@ dependencies:
|
||||
react-color:
|
||||
specifier: ^2.19.3
|
||||
version: 2.19.3(react@18.2.0)
|
||||
react-datepicker:
|
||||
specifier: ^4.23.0
|
||||
version: 4.23.0(react-dom@18.2.0)(react@18.2.0)
|
||||
react-dom:
|
||||
specifier: ^18.2.0
|
||||
version: 18.2.0(react@18.2.0)
|
||||
@ -146,8 +152,8 @@ dependencies:
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0
|
||||
valtio:
|
||||
specifier: ^1.11.1
|
||||
version: 1.11.1(react@18.2.0)
|
||||
specifier: ^1.12.1
|
||||
version: 1.12.1(@types/react@18.2.6)(react@18.2.0)
|
||||
yjs:
|
||||
specifier: ^13.5.51
|
||||
version: 13.6.1
|
||||
@ -192,6 +198,9 @@ devDependencies:
|
||||
'@types/react-color':
|
||||
specifier: ^3.0.6
|
||||
version: 3.0.6
|
||||
'@types/react-datepicker':
|
||||
specifier: ^4.19.3
|
||||
version: 4.19.3(react-dom@18.2.0)(react@18.2.0)
|
||||
'@types/react-dom':
|
||||
specifier: ^18.0.6
|
||||
version: 18.2.4
|
||||
@ -585,6 +594,12 @@ packages:
|
||||
regenerator-runtime: 0.14.0
|
||||
dev: false
|
||||
|
||||
/@babel/runtime@7.23.4:
|
||||
resolution: {integrity: sha512-2Yv65nlWnWlSpe3fXEyX5i7fx5kIKo4Qbcj+hMO0odwaneFjfXw5fdum+4yL20O0QiaHpia0cYQ9xpNMqrBwHg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
dependencies:
|
||||
regenerator-runtime: 0.14.0
|
||||
|
||||
/@babel/template@7.20.7:
|
||||
resolution: {integrity: sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@ -993,6 +1008,34 @@ packages:
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
dev: true
|
||||
|
||||
/@floating-ui/core@1.5.0:
|
||||
resolution: {integrity: sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==}
|
||||
dependencies:
|
||||
'@floating-ui/utils': 0.1.6
|
||||
dev: false
|
||||
|
||||
/@floating-ui/dom@1.5.3:
|
||||
resolution: {integrity: sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==}
|
||||
dependencies:
|
||||
'@floating-ui/core': 1.5.0
|
||||
'@floating-ui/utils': 0.1.6
|
||||
dev: false
|
||||
|
||||
/@floating-ui/react-dom@2.0.4(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-CF8k2rgKeh/49UrnIBs4BdxPUV6vize/Db1d/YbCLyp9GiVZ0BEwf5AiDSxJRCr6yOkGqTFHtmrULxkEfYZ7dQ==}
|
||||
peerDependencies:
|
||||
react: '>=16.8.0'
|
||||
react-dom: '>=16.8.0'
|
||||
dependencies:
|
||||
'@floating-ui/dom': 1.5.3
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@floating-ui/utils@0.1.6:
|
||||
resolution: {integrity: sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==}
|
||||
dev: false
|
||||
|
||||
/@humanwhocodes/config-array@0.11.8:
|
||||
resolution: {integrity: sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==}
|
||||
engines: {node: '>=10.10.0'}
|
||||
@ -1356,6 +1399,29 @@ packages:
|
||||
react-is: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@mui/base@5.0.0-beta.24(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-bKt2pUADHGQtqWDZ8nvL2Lvg2GNJyd/ZUgZAJoYzRgmnxBL9j36MSlS3+exEdYkikcnvVafcBtD904RypFKb0w==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
peerDependencies:
|
||||
'@types/react': ^17.0.0 || ^18.0.0
|
||||
react: ^17.0.0 || ^18.0.0
|
||||
react-dom: ^17.0.0 || ^18.0.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.4
|
||||
'@floating-ui/react-dom': 2.0.4(react-dom@18.2.0)(react@18.2.0)
|
||||
'@mui/types': 7.2.9(@types/react@18.2.6)
|
||||
'@mui/utils': 5.14.18(@types/react@18.2.6)(react@18.2.0)
|
||||
'@popperjs/core': 2.11.8
|
||||
'@types/react': 18.2.6
|
||||
clsx: 2.0.0
|
||||
prop-types: 15.8.1
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@mui/core-downloads-tracker@5.13.0:
|
||||
resolution: {integrity: sha512-5nXz2k8Rv2ZjtQY6kXirJVyn2+ODaQuAJmXSJtLDUQDKWp3PFUj6j3bILqR0JGOs9R5ejgwz3crLKsl6GwjwkQ==}
|
||||
dev: false
|
||||
@ -1493,6 +1559,17 @@ packages:
|
||||
'@types/react': 18.2.6
|
||||
dev: false
|
||||
|
||||
/@mui/types@7.2.9(@types/react@18.2.6):
|
||||
resolution: {integrity: sha512-k1lN/PolaRZfNsRdAqXtcR71sTnv3z/VCCGPxU8HfdftDkzi335MdJ6scZxvofMAd/K/9EbzCZTFBmlNpQVdCg==}
|
||||
peerDependencies:
|
||||
'@types/react': ^17.0.0 || ^18.0.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@types/react': 18.2.6
|
||||
dev: false
|
||||
|
||||
/@mui/utils@5.12.3(react@18.2.0):
|
||||
resolution: {integrity: sha512-D/Z4Ub3MRl7HiUccid7sQYclTr24TqUAQFFlxHQF8FR177BrCTQ0JJZom7EqYjZCdXhwnSkOj2ph685MSKNtIA==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
@ -1507,6 +1584,24 @@ packages:
|
||||
react-is: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@mui/utils@5.14.18(@types/react@18.2.6)(react@18.2.0):
|
||||
resolution: {integrity: sha512-HZDRsJtEZ7WMSnrHV9uwScGze4wM/Y+u6pDVo+grUjt5yXzn+wI8QX/JwTHh9YSw/WpnUL80mJJjgCnWj2VrzQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
peerDependencies:
|
||||
'@types/react': ^17.0.0 || ^18.0.0
|
||||
react: ^17.0.0 || ^18.0.0
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.4
|
||||
'@types/prop-types': 15.7.11
|
||||
'@types/react': 18.2.6
|
||||
prop-types: 15.8.1
|
||||
react: 18.2.0
|
||||
react-is: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@mui/utils@5.14.4(react@18.2.0):
|
||||
resolution: {integrity: sha512-4ANV0txPD3x0IcTCSEHKDWnsutg1K3m6Vz5IckkbLXVYu17oOZCVUdOKsb/txUmaCd0v0PmSRe5PW+Mlvns5dQ==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
@ -1521,6 +1616,130 @@ packages:
|
||||
react-is: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@mui/x-date-pickers-pro@6.18.2(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@mui/material@5.13.0)(@mui/system@5.14.4)(@types/react@18.2.6)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-8lEVEOtCQssKWel4Ey1pRulGPXUQ73TnkHKzHWsjdv03FjiUs3eYB+Ej0Uk5yWPmsqlShWhOzOlOGDpzsYJsUg==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
'@emotion/react': ^11.9.0
|
||||
'@emotion/styled': ^11.8.1
|
||||
'@mui/material': ^5.8.6
|
||||
'@mui/system': ^5.8.0
|
||||
date-fns: ^2.25.0
|
||||
date-fns-jalali: ^2.13.0-0
|
||||
dayjs: ^1.10.7
|
||||
luxon: ^3.0.2
|
||||
moment: ^2.29.4
|
||||
moment-hijri: ^2.1.2
|
||||
moment-jalaali: ^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0
|
||||
react: ^17.0.0 || ^18.0.0
|
||||
react-dom: ^17.0.0 || ^18.0.0
|
||||
peerDependenciesMeta:
|
||||
'@emotion/react':
|
||||
optional: true
|
||||
'@emotion/styled':
|
||||
optional: true
|
||||
date-fns:
|
||||
optional: true
|
||||
date-fns-jalali:
|
||||
optional: true
|
||||
dayjs:
|
||||
optional: true
|
||||
luxon:
|
||||
optional: true
|
||||
moment:
|
||||
optional: true
|
||||
moment-hijri:
|
||||
optional: true
|
||||
moment-jalaali:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.4
|
||||
'@emotion/react': 11.11.0(@types/react@18.2.6)(react@18.2.0)
|
||||
'@emotion/styled': 11.11.0(@emotion/react@11.11.0)(@types/react@18.2.6)(react@18.2.0)
|
||||
'@mui/base': 5.0.0-beta.24(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@mui/material': 5.13.0(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@mui/system': 5.14.4(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react@18.2.0)
|
||||
'@mui/utils': 5.14.18(@types/react@18.2.6)(react@18.2.0)
|
||||
'@mui/x-date-pickers': 6.18.2(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@mui/material@5.13.0)(@mui/system@5.14.4)(@types/react@18.2.6)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@mui/x-license-pro': 6.10.2(@types/react@18.2.6)(react@18.2.0)
|
||||
clsx: 2.0.0
|
||||
dayjs: 1.11.9
|
||||
prop-types: 15.8.1
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0)
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
dev: false
|
||||
|
||||
/@mui/x-date-pickers@6.18.2(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@mui/material@5.13.0)(@mui/system@5.14.4)(@types/react@18.2.6)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-HJq4uoFQSu5isa/mesWw2BKh8KBRYUQb+KaSlVlWfJNgP3YhPvWZ6yqCNYyxOAiPMxb0n3nBjS9ErO27OHjFMA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
'@emotion/react': ^11.9.0
|
||||
'@emotion/styled': ^11.8.1
|
||||
'@mui/material': ^5.8.6
|
||||
'@mui/system': ^5.8.0
|
||||
date-fns: ^2.25.0
|
||||
date-fns-jalali: ^2.13.0-0
|
||||
dayjs: ^1.10.7
|
||||
luxon: ^3.0.2
|
||||
moment: ^2.29.4
|
||||
moment-hijri: ^2.1.2
|
||||
moment-jalaali: ^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0
|
||||
react: ^17.0.0 || ^18.0.0
|
||||
react-dom: ^17.0.0 || ^18.0.0
|
||||
peerDependenciesMeta:
|
||||
'@emotion/react':
|
||||
optional: true
|
||||
'@emotion/styled':
|
||||
optional: true
|
||||
date-fns:
|
||||
optional: true
|
||||
date-fns-jalali:
|
||||
optional: true
|
||||
dayjs:
|
||||
optional: true
|
||||
luxon:
|
||||
optional: true
|
||||
moment:
|
||||
optional: true
|
||||
moment-hijri:
|
||||
optional: true
|
||||
moment-jalaali:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.4
|
||||
'@emotion/react': 11.11.0(@types/react@18.2.6)(react@18.2.0)
|
||||
'@emotion/styled': 11.11.0(@emotion/react@11.11.0)(@types/react@18.2.6)(react@18.2.0)
|
||||
'@mui/base': 5.0.0-beta.24(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@mui/material': 5.13.0(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
|
||||
'@mui/system': 5.14.4(@emotion/react@11.11.0)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react@18.2.0)
|
||||
'@mui/utils': 5.14.18(@types/react@18.2.6)(react@18.2.0)
|
||||
'@types/react-transition-group': 4.4.9
|
||||
clsx: 2.0.0
|
||||
dayjs: 1.11.9
|
||||
prop-types: 15.8.1
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0)
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
dev: false
|
||||
|
||||
/@mui/x-license-pro@6.10.2(@types/react@18.2.6)(react@18.2.0):
|
||||
resolution: {integrity: sha512-Baw3shilU+eHgU+QYKNPFUKvfS5rSyNJ98pQx02E0gKA22hWp/XAt88K1qUfUMPlkPpvg/uci6gviQSSLZkuKw==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
peerDependencies:
|
||||
react: ^17.0.0 || ^18.0.0
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.4
|
||||
'@mui/utils': 5.14.18(@types/react@18.2.6)(react@18.2.0)
|
||||
react: 18.2.0
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
dev: false
|
||||
|
||||
/@nodelib/fs.scandir@2.1.5:
|
||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||
engines: {node: '>= 8'}
|
||||
@ -1546,6 +1765,9 @@ packages:
|
||||
resolution: {integrity: sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==}
|
||||
dev: false
|
||||
|
||||
/@popperjs/core@2.11.8:
|
||||
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
|
||||
|
||||
/@reduxjs/toolkit@1.9.5(react-redux@8.0.5)(react@18.2.0):
|
||||
resolution: {integrity: sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ==}
|
||||
peerDependencies:
|
||||
@ -1998,6 +2220,10 @@ packages:
|
||||
resolution: {integrity: sha512-ZTaqn/qSqUuAq1YwvOFQfVW1AR/oQJlLSZVustdjwI+GZ8kr0MSHBj0tsXPW1EqHubx50gtBEjbPGsdZwQwCjQ==}
|
||||
dev: true
|
||||
|
||||
/@types/prop-types@15.7.11:
|
||||
resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==}
|
||||
dev: false
|
||||
|
||||
/@types/prop-types@15.7.5:
|
||||
resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==}
|
||||
|
||||
@ -2021,6 +2247,18 @@ packages:
|
||||
'@types/reactcss': 1.2.6
|
||||
dev: true
|
||||
|
||||
/@types/react-datepicker@4.19.3(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-85F9eKWu9fGiD9r4KVVMPYAdkJJswR3Wci9PvqplmB6T+D+VbUqPeKtifg96NZ4nEhufjehW+SX4JLrEWVplWw==}
|
||||
dependencies:
|
||||
'@popperjs/core': 2.11.8
|
||||
'@types/react': 18.2.6
|
||||
date-fns: 2.30.0
|
||||
react-popper: 2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0)(react@18.2.0)
|
||||
transitivePeerDependencies:
|
||||
- react
|
||||
- react-dom
|
||||
dev: true
|
||||
|
||||
/@types/react-dom@18.2.4:
|
||||
resolution: {integrity: sha512-G2mHoTMTL4yoydITgOGwWdWMVd8sNgyEP85xVmMKAPUBwQWm9wBPQUmvbeF4V3WBY1P7mmL4BkjQ0SqUpf1snw==}
|
||||
dependencies:
|
||||
@ -2064,6 +2302,12 @@ packages:
|
||||
dependencies:
|
||||
'@types/react': 18.2.6
|
||||
|
||||
/@types/react-transition-group@4.4.9:
|
||||
resolution: {integrity: sha512-ZVNmWumUIh5NhH8aMD9CR2hdW0fNuYInlocZHaZ+dgk/1K49j1w/HoAuK1ki+pgscQrOFRTlXeoURtuzEkV3dg==}
|
||||
dependencies:
|
||||
'@types/react': 18.2.6
|
||||
dev: false
|
||||
|
||||
/@types/react@17.0.59:
|
||||
resolution: {integrity: sha512-gSON5zWYIGyoBcycCE75E9+r6dCC2dHdsrVkOEiIYNU5+Q28HcBAuqvDuxHcCbMfHBHdeT5Tva/AFn3rnMKE4g==}
|
||||
dependencies:
|
||||
@ -2671,6 +2915,10 @@ packages:
|
||||
/cjs-module-lexer@1.2.2:
|
||||
resolution: {integrity: sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==}
|
||||
|
||||
/classnames@2.3.2:
|
||||
resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==}
|
||||
dev: false
|
||||
|
||||
/cliui@8.0.1:
|
||||
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
||||
engines: {node: '>=12'}
|
||||
@ -2870,6 +3118,12 @@ packages:
|
||||
whatwg-url: 11.0.0
|
||||
dev: true
|
||||
|
||||
/date-fns@2.30.0:
|
||||
resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==}
|
||||
engines: {node: '>=0.11'}
|
||||
dependencies:
|
||||
'@babel/runtime': 7.23.4
|
||||
|
||||
/dayjs@1.11.9:
|
||||
resolution: {integrity: sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==}
|
||||
dev: false
|
||||
@ -2923,6 +3177,14 @@ packages:
|
||||
engines: {node: '>=0.4.0'}
|
||||
dev: true
|
||||
|
||||
/derive-valtio@0.1.0(valtio@1.12.1):
|
||||
resolution: {integrity: sha512-OCg2UsLbXK7GmmpzMXhYkdO64vhJ1ROUUGaTFyHjVwEdMEcTTRj7W1TxLbSBxdY8QLBPCcp66MTyaSy0RpO17A==}
|
||||
peerDependencies:
|
||||
valtio: '*'
|
||||
dependencies:
|
||||
valtio: 1.12.1(@types/react@18.2.6)(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/detect-newline@3.1.0:
|
||||
resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==}
|
||||
engines: {node: '>=8'}
|
||||
@ -5330,6 +5592,22 @@ packages:
|
||||
tinycolor2: 1.6.0
|
||||
dev: false
|
||||
|
||||
/react-datepicker@4.23.0(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-w+msqlOZ14v6H1UknTKtZw/dw9naFMgAOspf59eY130gWpvy5dvKj/bgsFICDdvxB7PtKWxDcbGlAqCloY1d2A==}
|
||||
peerDependencies:
|
||||
react: ^16.9.0 || ^17 || ^18
|
||||
react-dom: ^16.9.0 || ^17 || ^18
|
||||
dependencies:
|
||||
'@popperjs/core': 2.11.8
|
||||
classnames: 2.3.2
|
||||
date-fns: 2.30.0
|
||||
prop-types: 15.8.1
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
react-onclickoutside: 6.13.0(react-dom@18.2.0)(react@18.2.0)
|
||||
react-popper: 2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0)(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/react-dom@18.2.0(react@18.2.0):
|
||||
resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==}
|
||||
peerDependencies:
|
||||
@ -5338,7 +5616,6 @@ packages:
|
||||
loose-envify: 1.4.0
|
||||
react: 18.2.0
|
||||
scheduler: 0.23.0
|
||||
dev: false
|
||||
|
||||
/react-error-boundary@3.1.4(react@18.2.0):
|
||||
resolution: {integrity: sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==}
|
||||
@ -5361,6 +5638,9 @@ packages:
|
||||
warning: 4.0.3
|
||||
dev: false
|
||||
|
||||
/react-fast-compare@3.2.2:
|
||||
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
|
||||
|
||||
/react-i18next@12.2.2(i18next@22.4.15)(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-KBB6buBmVKXUWNxXHdnthp+38gPyBT46hJCAIQ8rX19NFL/m2ahte2KARfIDf2tMnSAL7wwck6eDOd/9zn6aFg==}
|
||||
peerDependencies:
|
||||
@ -5402,6 +5682,29 @@ packages:
|
||||
react: 18.2.0
|
||||
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:
|
||||
react: ^15.5.x || ^16.x || ^17.x || ^18.x
|
||||
react-dom: ^15.5.x || ^16.x || ^17.x || ^18.x
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
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:
|
||||
'@popperjs/core': ^2.0.0
|
||||
react: ^16.8.0 || ^17 || ^18
|
||||
react-dom: ^16.8.0 || ^17 || ^18
|
||||
dependencies:
|
||||
'@popperjs/core': 2.11.8
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
react-fast-compare: 3.2.2
|
||||
warning: 4.0.3
|
||||
|
||||
/react-redux@7.2.9(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==}
|
||||
peerDependencies:
|
||||
@ -5551,7 +5854,6 @@ packages:
|
||||
engines: {node: '>=0.10.0'}
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
dev: false
|
||||
|
||||
/reactcss@1.2.3(react@18.2.0):
|
||||
resolution: {integrity: sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==}
|
||||
@ -5599,7 +5901,6 @@ packages:
|
||||
|
||||
/regenerator-runtime@0.14.0:
|
||||
resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==}
|
||||
dev: false
|
||||
|
||||
/regexp.prototype.flags@1.5.0:
|
||||
resolution: {integrity: sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==}
|
||||
@ -5718,7 +6019,6 @@ packages:
|
||||
resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==}
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
dev: false
|
||||
|
||||
/scroll-into-view-if-needed@2.2.31:
|
||||
resolution: {integrity: sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==}
|
||||
@ -6375,15 +6675,20 @@ packages:
|
||||
'@types/istanbul-lib-coverage': 2.0.4
|
||||
convert-source-map: 1.9.0
|
||||
|
||||
/valtio@1.11.1(react@18.2.0):
|
||||
resolution: {integrity: sha512-sTKWY1e1AVUu4sY9CimoSZpufAsAXO+fzZrw0X5xtijEmDDQaPPLHZxlpONUpTLtvxPjpQURCSdUuUyBszoEOg==}
|
||||
/valtio@1.12.1(@types/react@18.2.6)(react@18.2.0):
|
||||
resolution: {integrity: sha512-R0V4H86Xi2Pp7pmxN/EtV4Q6jr6PMN3t1IwxEvKUp6160r8FimvPh941oWyeK1iec/DTsh9Jb3Q+GputMS8SYg==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
peerDependencies:
|
||||
'@types/react': '>=16.8'
|
||||
react: '>=16.8'
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@types/react': 18.2.6
|
||||
derive-valtio: 0.1.0(valtio@1.12.1)
|
||||
proxy-compare: 2.5.1
|
||||
react: 18.2.0
|
||||
use-sync-external-store: 1.2.0(react@18.2.0)
|
||||
@ -6457,7 +6762,6 @@ packages:
|
||||
resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==}
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
dev: false
|
||||
|
||||
/webidl-conversions@7.0.0:
|
||||
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
|
||||
|
@ -36,17 +36,25 @@ languages.forEach(language => {
|
||||
console.error(res);
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
function flattenJSON(obj, prefix = '') {
|
||||
let result = {};
|
||||
const pluralsKey = ["one", "other", "few", "many", "two", "zero"];
|
||||
|
||||
for (let key in obj) {
|
||||
if (typeof obj[key] === 'object' && obj[key] !== null) {
|
||||
|
||||
const nestedKeys = flattenJSON(obj[key], `${prefix}${key}.`);
|
||||
result = { ...result, ...nestedKeys };
|
||||
} else {
|
||||
|
||||
result[`${prefix}${key}`] = obj[key].replaceAll('{', '{{').replaceAll('}', '}}');
|
||||
let newKey = `${prefix}${key}`;
|
||||
let replaceChar = '{'
|
||||
if (pluralsKey.includes(key)) {
|
||||
newKey = `${prefix.slice(0, -1)}_${key}`;
|
||||
}
|
||||
result[newKey] = obj[key].replaceAll('{', '{{').replaceAll('}', '}}');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 4L6 8L10 12" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 195 B |
@ -0,0 +1,3 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 4L10 8L6 12" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 194 B |
@ -1,4 +1,4 @@
|
||||
import React, { FormEventHandler, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import React, { FormEventHandler, useCallback, useRef, useState } from 'react';
|
||||
import ViewBanner from '$app/components/_shared/ViewTitle/ViewBanner';
|
||||
import { Page, PageIcon } from '$app_reducers/pages/slice';
|
||||
import { ViewIconTypePB } from '@/services/backend';
|
||||
@ -37,13 +37,6 @@ function ViewTitle({ view, onTitleChange: onTitleChangeProp, onUpdateIcon: onUpd
|
||||
[onUpdateIconProp]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.focus();
|
||||
// set the cursor to the end of the text
|
||||
textareaRef.current.setSelectionRange(textareaRef.current.value.length, textareaRef.current.value.length);
|
||||
}
|
||||
}, []);
|
||||
return (
|
||||
<div className={'flex flex-col'} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}>
|
||||
<ViewBanner icon={icon} hover={hover} onUpdateIcon={onUpdateIcon} />
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import { Button, DialogActions } from '@mui/material';
|
||||
import { Button, DialogActions, Divider } from '@mui/material';
|
||||
import Dialog from '@mui/material/Dialog';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@ -18,10 +18,11 @@ function ConfirmDialog({ open, title, subtitle, onOk, onClose }: Props) {
|
||||
return (
|
||||
<Dialog keepMounted={false} onMouseDown={(e) => e.stopPropagation()} open={open} onClose={onClose}>
|
||||
<DialogContent className={'flex w-[540px] flex-col items-center justify-center'}>
|
||||
<div className={'text-md m-2 font-bold'}>{title}</div>
|
||||
<div className={'m-1 text-sm text-text-caption'}>{subtitle}</div>
|
||||
<div className={'text-md font-medium'}>{title}</div>
|
||||
{subtitle && <div className={'m-1 text-sm text-text-caption'}>{subtitle}</div>}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Divider className={'mb-4'} />
|
||||
<DialogActions className={'p-4 pt-0'}>
|
||||
<Button variant={'outlined'} onClick={onClose}>
|
||||
{t('button.cancel')}
|
||||
</Button>
|
||||
|
@ -1,11 +1,19 @@
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { proxy, useSnapshot } from 'valtio';
|
||||
|
||||
import { DatabaseLayoutPB, DatabaseNotification, FieldVisibility } from '@/services/backend';
|
||||
import { subscribeNotifications } from '$app/hooks';
|
||||
import { Database, databaseService, fieldListeners, fieldService, rowListeners, sortListeners } from './application';
|
||||
import { didUpdateFilter } from '$app/components/database/application/filter/filter_listeners';
|
||||
import { didUpdateViewRowsVisibility } from '$app/components/database/application/row/row_listeners';
|
||||
import {
|
||||
Cell,
|
||||
Database,
|
||||
databaseService,
|
||||
cellListeners,
|
||||
fieldListeners,
|
||||
rowListeners,
|
||||
sortListeners,
|
||||
filterListeners,
|
||||
} from './application';
|
||||
|
||||
export function useSelectDatabaseView({ viewId }: { viewId?: string }) {
|
||||
const key = 'v';
|
||||
@ -36,16 +44,73 @@ const DatabaseContext = createContext<Database>({
|
||||
sorts: [],
|
||||
groupSettings: [],
|
||||
groups: [],
|
||||
typeOptions: {},
|
||||
cells: {},
|
||||
});
|
||||
|
||||
export const DatabaseProvider = DatabaseContext.Provider;
|
||||
|
||||
export const useDatabase = () => useSnapshot(useContext(DatabaseContext));
|
||||
|
||||
export const useSelectorCell = (rowId: string, fieldId: string) => {
|
||||
const database = useContext(DatabaseContext);
|
||||
const cells = useSnapshot(database.cells);
|
||||
|
||||
return cells[`${rowId}:${fieldId}`];
|
||||
};
|
||||
|
||||
export const useDispatchCell = () => {
|
||||
const database = useContext(DatabaseContext);
|
||||
|
||||
const setCell = useCallback(
|
||||
(cell: Cell) => {
|
||||
const id = `${cell.rowId}:${cell.fieldId}`;
|
||||
|
||||
database.cells[id] = cell;
|
||||
},
|
||||
[database]
|
||||
);
|
||||
|
||||
const deleteCells = useCallback(
|
||||
({ rowId, fieldId }: { rowId: string; fieldId?: string }) => {
|
||||
cellListeners.didDeleteCells({ database, rowId, fieldId });
|
||||
},
|
||||
[database]
|
||||
);
|
||||
|
||||
return {
|
||||
deleteCells,
|
||||
setCell,
|
||||
};
|
||||
};
|
||||
|
||||
export const useTypeOptions = () => {
|
||||
const context = useContext(DatabaseContext);
|
||||
|
||||
return useSnapshot(context.typeOptions);
|
||||
};
|
||||
|
||||
export const useFiltersCount = () => {
|
||||
const { filters, fields } = useDatabase();
|
||||
|
||||
// filter fields: if the field is deleted, it will not be displayed
|
||||
return useMemo(
|
||||
() => filters?.map((filter) => fields.find((field) => field.id === filter.fieldId)).filter(Boolean).length,
|
||||
[filters, fields]
|
||||
);
|
||||
};
|
||||
|
||||
export function useTypeOption<T>(fieldId: string) {
|
||||
const context = useContext(DatabaseContext);
|
||||
const typeOptions = useSnapshot(context.typeOptions);
|
||||
|
||||
return typeOptions[fieldId] as T;
|
||||
}
|
||||
|
||||
export const useDatabaseVisibilityRows = () => {
|
||||
const { rowMetas } = useDatabase();
|
||||
|
||||
return useMemo(() => rowMetas.filter((row) => !row.isHidden), [rowMetas]);
|
||||
return useMemo(() => rowMetas.filter((row) => row && !row.isHidden), [rowMetas]);
|
||||
};
|
||||
|
||||
export const useDatabaseVisibilityFields = () => {
|
||||
@ -69,6 +134,8 @@ export const useConnectDatabase = (viewId: string) => {
|
||||
sorts: [],
|
||||
groupSettings: [],
|
||||
groups: [],
|
||||
typeOptions: {},
|
||||
cells: {},
|
||||
});
|
||||
|
||||
void databaseService.openDatabase(viewId).then((value) => Object.assign(proxyDatabase, value));
|
||||
@ -79,10 +146,10 @@ export const useConnectDatabase = (viewId: string) => {
|
||||
useEffect(() => {
|
||||
const unsubscribePromise = subscribeNotifications(
|
||||
{
|
||||
[DatabaseNotification.DidUpdateFields]: async () => {
|
||||
database.fields = await fieldService.getFields(viewId);
|
||||
[DatabaseNotification.DidUpdateFields]: async (changeset) => {
|
||||
await fieldListeners.didUpdateFields(viewId, database, changeset);
|
||||
},
|
||||
[DatabaseNotification.DidUpdateFieldSettings]: async (changeset) => {
|
||||
[DatabaseNotification.DidUpdateFieldSettings]: (changeset) => {
|
||||
fieldListeners.didUpdateFieldSettings(database, changeset);
|
||||
},
|
||||
[DatabaseNotification.DidUpdateViewRows]: (changeset) => {
|
||||
@ -100,10 +167,10 @@ export const useConnectDatabase = (viewId: string) => {
|
||||
},
|
||||
|
||||
[DatabaseNotification.DidUpdateFilter]: (changeset) => {
|
||||
didUpdateFilter(database, changeset);
|
||||
filterListeners.didUpdateFilter(database, changeset);
|
||||
},
|
||||
[DatabaseNotification.DidUpdateViewRowsVisibility]: (changeset) => {
|
||||
didUpdateViewRowsVisibility(database, changeset);
|
||||
rowListeners.didUpdateViewRowsVisibility(database, changeset);
|
||||
},
|
||||
},
|
||||
{ id: viewId }
|
||||
@ -115,9 +182,10 @@ export const useConnectDatabase = (viewId: string) => {
|
||||
return database;
|
||||
};
|
||||
|
||||
export function useDatabaseResize() {
|
||||
export function useDatabaseResize(selectedViewId?: string) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const collectionRef = useRef<HTMLDivElement>(null);
|
||||
const [openCollections, setOpenCollections] = useState<string[]>([]);
|
||||
|
||||
const [tableHeight, setTableHeight] = useState(0);
|
||||
|
||||
@ -140,23 +208,23 @@ export function useDatabaseResize() {
|
||||
};
|
||||
|
||||
handleResize();
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
handleResize();
|
||||
});
|
||||
const resizeObserver = new ResizeObserver(handleResize);
|
||||
|
||||
resizeObserver.observe(element);
|
||||
if (collectionElement) {
|
||||
resizeObserver.observe(collectionRef.current);
|
||||
resizeObserver.observe(collectionElement);
|
||||
}
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, []);
|
||||
}, [selectedViewId]);
|
||||
|
||||
return {
|
||||
ref,
|
||||
collectionRef,
|
||||
tableHeight,
|
||||
openCollections,
|
||||
setOpenCollections,
|
||||
};
|
||||
}
|
||||
|
@ -24,8 +24,7 @@ export const Database = ({ selectedViewId, setSelectedViewId }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const [notFound, setNotFound] = useState(false);
|
||||
const [childViewIds, setChildViewIds] = useState<string[]>([]);
|
||||
const { ref, collectionRef, tableHeight } = useDatabaseResize();
|
||||
const [openCollections, setOpenCollections] = useState<string[]>([]);
|
||||
const { ref, collectionRef, tableHeight, openCollections, setOpenCollections } = useDatabaseResize(selectedViewId);
|
||||
|
||||
useEffect(() => {
|
||||
const onPageChanged = () => {
|
||||
@ -54,7 +53,7 @@ export const Database = ({ selectedViewId, setSelectedViewId }: Props) => {
|
||||
};
|
||||
}, [viewId]);
|
||||
|
||||
const index = useMemo(() => {
|
||||
const value = useMemo(() => {
|
||||
return Math.max(0, childViewIds.indexOf(selectedViewId ?? viewId));
|
||||
}, [childViewIds, selectedViewId, viewId]);
|
||||
|
||||
@ -77,7 +76,7 @@ export const Database = ({ selectedViewId, setSelectedViewId }: Props) => {
|
||||
setOpenCollections((prev) => [...prev, id]);
|
||||
}
|
||||
},
|
||||
[openCollections]
|
||||
[openCollections, setOpenCollections]
|
||||
);
|
||||
|
||||
if (notFound) {
|
||||
@ -102,10 +101,10 @@ export const Database = ({ selectedViewId, setSelectedViewId }: Props) => {
|
||||
}}
|
||||
className={'flex-1 overflow-hidden'}
|
||||
axis={'x'}
|
||||
index={index}
|
||||
index={value}
|
||||
>
|
||||
{childViewIds.map((id) => (
|
||||
<TabPanel key={id} index={index} value={index}>
|
||||
{childViewIds.map((id, index) => (
|
||||
<TabPanel key={id} index={index} value={value}>
|
||||
<DatabaseLoader viewId={id}>
|
||||
{selectedViewId === id && (
|
||||
<>
|
||||
|
@ -9,7 +9,7 @@ export const CellText = React.forwardRef<HTMLDivElement, PropsWithChildren<HTMLA
|
||||
const { children, className, ...other } = props;
|
||||
|
||||
return (
|
||||
<div ref={ref} className={['flex w-full p-2', className].join(' ')} {...other}>
|
||||
<div ref={ref} className={['flex h-full w-full p-2', className].join(' ')} {...other}>
|
||||
<span className='flex-1 truncate text-sm'>{children}</span>
|
||||
</div>
|
||||
);
|
||||
|
@ -33,10 +33,11 @@ export const VirtualizedList: FC<VirtualizedListProps> = ({
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
ref={virtualizer.measureElement}
|
||||
className={itemClassName}
|
||||
style={{
|
||||
[sizeProp]: size,
|
||||
...getItemStyle?.(index),
|
||||
...(horizontal ? { [sizeProp]: size } : undefined),
|
||||
}}
|
||||
data-key={key}
|
||||
data-index={index}
|
||||
|
@ -0,0 +1,48 @@
|
||||
import { Database } from '$app/components/database/application';
|
||||
import { getCell } from './cell_service';
|
||||
|
||||
export function didDeleteCells({ database, rowId, fieldId }: { database: Database; rowId?: string; fieldId?: string }) {
|
||||
const ids = Object.keys(database.cells);
|
||||
|
||||
ids.forEach((id) => {
|
||||
const cell = database.cells[id];
|
||||
|
||||
if (rowId && cell.rowId !== rowId) return;
|
||||
if (fieldId && cell.fieldId !== fieldId) return;
|
||||
|
||||
delete database.cells[id];
|
||||
});
|
||||
}
|
||||
|
||||
export async function didUpdateCells({
|
||||
viewId,
|
||||
database,
|
||||
rowId,
|
||||
fieldId,
|
||||
}: {
|
||||
viewId: string;
|
||||
database: Database;
|
||||
rowId?: string;
|
||||
fieldId?: string;
|
||||
}) {
|
||||
const field = database.fields.find((field) => field.id === fieldId);
|
||||
|
||||
if (!field) {
|
||||
delete database.cells[`${rowId}:${fieldId}`];
|
||||
return;
|
||||
}
|
||||
|
||||
const ids = Object.keys(database.cells);
|
||||
|
||||
ids.forEach((id) => {
|
||||
const cell = database.cells[id];
|
||||
|
||||
if (rowId && cell.rowId !== rowId) return;
|
||||
if (fieldId && cell.fieldId !== fieldId) return;
|
||||
|
||||
void getCell(viewId, cell.rowId, cell.fieldId, field.type).then((data) => {
|
||||
// cache cell
|
||||
database.cells[id] = data;
|
||||
});
|
||||
});
|
||||
}
|
@ -102,10 +102,17 @@ export async function updateDateCell(
|
||||
rowId: string,
|
||||
fieldId: string,
|
||||
data: {
|
||||
// 10-digit timestamp
|
||||
date?: number;
|
||||
// time string in format HH:mm
|
||||
time?: string;
|
||||
// 10-digit timestamp
|
||||
endDate?: number;
|
||||
// time string in format HH:mm
|
||||
endTime?: string;
|
||||
includeTime?: boolean;
|
||||
clearFlag?: boolean;
|
||||
isRange?: boolean;
|
||||
}
|
||||
): Promise<void> {
|
||||
const payload = DateChangesetPB.fromObject({
|
||||
@ -118,6 +125,9 @@ export async function updateDateCell(
|
||||
time: data.time,
|
||||
include_time: data.includeTime,
|
||||
clear_flag: data.clearFlag,
|
||||
end_date: data.endDate,
|
||||
end_time: data.endTime,
|
||||
is_range: data.isRange,
|
||||
});
|
||||
|
||||
const result = await DatabaseEventUpdateDateCell(payload);
|
||||
|
@ -4,8 +4,13 @@ import {
|
||||
DateCellDataPB,
|
||||
FieldType,
|
||||
SelectOptionCellDataPB,
|
||||
TimestampCellDataPB,
|
||||
URLCellDataPB,
|
||||
} from '@/services/backend';
|
||||
import {
|
||||
SelectOption,
|
||||
pbToSelectOption,
|
||||
} from '$app/components/database/application/field/select_option/select_option_types';
|
||||
|
||||
export interface Cell {
|
||||
rowId: string;
|
||||
@ -53,11 +58,25 @@ export interface DateTimeCell extends Cell {
|
||||
data: DateTimeCellData;
|
||||
}
|
||||
|
||||
export interface TimeStampCell extends Cell {
|
||||
fieldType: FieldType.LastEditedTime | FieldType.CreatedTime;
|
||||
data: TimestampCellData;
|
||||
}
|
||||
|
||||
export interface DateTimeCellData {
|
||||
date?: string;
|
||||
time?: string;
|
||||
timestamp?: number;
|
||||
includeTime?: boolean;
|
||||
endDate?: string;
|
||||
endTime?: string;
|
||||
endTimestamp?: number;
|
||||
isRange?: boolean;
|
||||
}
|
||||
|
||||
export interface TimestampCellData {
|
||||
dataTime?: string;
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
export interface ChecklistCell extends Cell {
|
||||
@ -71,20 +90,37 @@ export interface ChecklistCellData {
|
||||
*/
|
||||
selectedOptions?: string[];
|
||||
percentage?: number;
|
||||
options?: SelectOption[];
|
||||
}
|
||||
|
||||
export type UndeterminedCell = TextCell | NumberCell | DateTimeCell | SelectCell | CheckboxCell | UrlCell | ChecklistCell;
|
||||
export type UndeterminedCell =
|
||||
| TextCell
|
||||
| NumberCell
|
||||
| DateTimeCell
|
||||
| SelectCell
|
||||
| CheckboxCell
|
||||
| UrlCell
|
||||
| ChecklistCell;
|
||||
|
||||
const pbToDateCellData = (pb: DateCellDataPB): DateTimeCellData => ({
|
||||
const pbToDateTimeCellData = (pb: DateCellDataPB): DateTimeCellData => ({
|
||||
date: pb.date,
|
||||
time: pb.time,
|
||||
timestamp: pb.timestamp,
|
||||
includeTime: pb.include_time,
|
||||
endDate: pb.end_date,
|
||||
endTime: pb.end_time,
|
||||
endTimestamp: pb.end_timestamp,
|
||||
isRange: pb.is_range,
|
||||
});
|
||||
|
||||
const pbToTimestampCellData = (pb: TimestampCellDataPB): TimestampCellData => ({
|
||||
dataTime: pb.date_time,
|
||||
timestamp: pb.timestamp,
|
||||
});
|
||||
|
||||
export const pbToSelectCellData = (pb: SelectOptionCellDataPB): SelectCellData => {
|
||||
return {
|
||||
selectedOptionIds: pb.select_options.map(option => option.id),
|
||||
selectedOptionIds: pb.select_options.map((option) => option.id),
|
||||
};
|
||||
};
|
||||
|
||||
@ -96,6 +132,7 @@ const pbToURLCellData = (pb: URLCellDataPB): UrlCellData => ({
|
||||
export const pbToChecklistCellData = (pb: ChecklistCellDataPB): ChecklistCellData => ({
|
||||
selectedOptions: pb.selected_options.map(({ id }) => id),
|
||||
percentage: pb.percentage,
|
||||
options: pb.options.map(pbToSelectOption),
|
||||
});
|
||||
|
||||
function bytesToCellData(bytes: Uint8Array, fieldType: FieldType) {
|
||||
@ -105,9 +142,10 @@ function bytesToCellData(bytes: Uint8Array, fieldType: FieldType) {
|
||||
case FieldType.Checkbox:
|
||||
return new TextDecoder().decode(bytes);
|
||||
case FieldType.DateTime:
|
||||
return pbToDateTimeCellData(DateCellDataPB.deserialize(bytes));
|
||||
case FieldType.LastEditedTime:
|
||||
case FieldType.CreatedTime:
|
||||
return pbToDateCellData(DateCellDataPB.deserialize(bytes));
|
||||
return pbToTimestampCellData(TimestampCellDataPB.deserialize(bytes));
|
||||
case FieldType.SingleSelect:
|
||||
case FieldType.MultiSelect:
|
||||
return pbToSelectCellData(SelectOptionCellDataPB.deserialize(bytes));
|
||||
|
@ -1,2 +1,3 @@
|
||||
export * from './cell_types';
|
||||
export * as cellService from './cell_service';
|
||||
export * as cellListeners from './cell_listeners';
|
||||
|
@ -16,10 +16,9 @@ export async function getDatabaseId(viewId: string): Promise<string> {
|
||||
|
||||
const result = await DatabaseEventGetDatabaseId(payload);
|
||||
|
||||
return result.map(value => value.value).unwrap();
|
||||
return result.map((value) => value.value).unwrap();
|
||||
}
|
||||
|
||||
|
||||
export async function getDatabase(viewId: string) {
|
||||
const payload = DatabaseViewIdPB.fromObject({
|
||||
value: viewId,
|
||||
@ -27,15 +26,17 @@ export async function getDatabase(viewId: string) {
|
||||
|
||||
const result = await DatabaseEventGetDatabase(payload);
|
||||
|
||||
return result.map(value => {
|
||||
return {
|
||||
id: value.id,
|
||||
isLinked: value.is_linked,
|
||||
layoutType: value.layout_type,
|
||||
fieldIds: value.fields.map(field => field.field_id),
|
||||
rowMetas: value.rows.map(pbToRowMeta),
|
||||
};
|
||||
}).unwrap();
|
||||
return result
|
||||
.map((value) => {
|
||||
return {
|
||||
id: value.id,
|
||||
isLinked: value.is_linked,
|
||||
layoutType: value.layout_type,
|
||||
fieldIds: value.fields.map((field) => field.field_id),
|
||||
rowMetas: value.rows.map(pbToRowMeta),
|
||||
};
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
export async function getDatabaseSetting(viewId: string) {
|
||||
@ -45,31 +46,23 @@ export async function getDatabaseSetting(viewId: string) {
|
||||
|
||||
const result = await DatabaseEventGetDatabaseSetting(payload);
|
||||
|
||||
return result.map(value => {
|
||||
return {
|
||||
filters: value.filters.items.map(pbToFilter),
|
||||
sorts: value.sorts.items.map(pbToSort),
|
||||
groupSettings: value.group_settings.items.map(pbToGroupSetting),
|
||||
};
|
||||
}).unwrap();
|
||||
return result
|
||||
.map((value) => {
|
||||
return {
|
||||
filters: value.filters.items.map(pbToFilter),
|
||||
sorts: value.sorts.items.map(pbToSort),
|
||||
groupSettings: value.group_settings.items.map(pbToGroupSetting),
|
||||
};
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
export async function openDatabase(viewId: string): Promise<Database> {
|
||||
const {
|
||||
id,
|
||||
isLinked,
|
||||
layoutType,
|
||||
fieldIds,
|
||||
rowMetas,
|
||||
} = await getDatabase(viewId);
|
||||
const { id, isLinked, layoutType, fieldIds, rowMetas } = await getDatabase(viewId);
|
||||
|
||||
const {
|
||||
filters,
|
||||
sorts,
|
||||
groupSettings,
|
||||
} = await getDatabaseSetting(viewId);
|
||||
const { filters, sorts, groupSettings } = await getDatabaseSetting(viewId);
|
||||
|
||||
const fields = await fieldService.getFields(viewId, fieldIds);
|
||||
const { fields, typeOptions } = await fieldService.getFields(viewId, fieldIds);
|
||||
|
||||
const groups = await groupService.getGroups(viewId);
|
||||
|
||||
@ -83,5 +76,7 @@ export async function openDatabase(viewId: string): Promise<Database> {
|
||||
sorts,
|
||||
groups,
|
||||
groupSettings,
|
||||
typeOptions,
|
||||
cells: {},
|
||||
};
|
||||
}
|
||||
|
@ -1,18 +1,21 @@
|
||||
import { DatabaseLayoutPB } from '@/services/backend';
|
||||
import { Field } from '../field';
|
||||
import { Field, UndeterminedTypeOptionData } from '../field';
|
||||
import { Filter } from '../filter';
|
||||
import { GroupSetting, Group } from '../group';
|
||||
import { RowMeta } from '../row';
|
||||
import { Sort } from '../sort';
|
||||
import { Cell } from '../cell';
|
||||
|
||||
export interface Database {
|
||||
id: string;
|
||||
isLinked: boolean;
|
||||
layoutType: DatabaseLayoutPB,
|
||||
layoutType: DatabaseLayoutPB;
|
||||
fields: Field[];
|
||||
rowMetas: RowMeta[];
|
||||
filters: Filter[];
|
||||
sorts: Sort[];
|
||||
groupSettings: GroupSetting[];
|
||||
groups: Group[];
|
||||
typeOptions: Record<string, UndeterminedTypeOptionData>;
|
||||
cells: Record<string, Cell>;
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { FieldSettingsPB } from '@/services/backend';
|
||||
import { Database } from '$app/components/database/application';
|
||||
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';
|
||||
|
||||
export function didUpdateFieldSettings(database: Database, settings: FieldSettingsPB) {
|
||||
const { field_id: fieldId, visibility, width } = settings;
|
||||
@ -8,4 +9,32 @@ export function didUpdateFieldSettings(database: Database, settings: FieldSettin
|
||||
if (!field) return;
|
||||
field.visibility = visibility;
|
||||
field.width = width;
|
||||
// delete cells if field is hidden
|
||||
if (visibility === FieldVisibility.AlwaysHidden) {
|
||||
didDeleteCells({ database, fieldId });
|
||||
}
|
||||
}
|
||||
|
||||
export async function didUpdateFields(viewId: string, database: Database, changeset: DatabaseFieldChangesetPB) {
|
||||
const { fields, typeOptions } = await fieldService.getFields(viewId);
|
||||
|
||||
database.fields = fields;
|
||||
const deletedFieldIds = Object.keys(changeset.deleted_fields);
|
||||
const updatedFieldIds = changeset.updated_fields.map((field) => field.id);
|
||||
|
||||
Object.assign(database.typeOptions, typeOptions);
|
||||
deletedFieldIds.forEach(
|
||||
(fieldId) => {
|
||||
// delete cache cells
|
||||
didDeleteCells({ database, fieldId });
|
||||
// delete cache type options
|
||||
delete database.typeOptions[fieldId];
|
||||
},
|
||||
[database.typeOptions]
|
||||
);
|
||||
|
||||
updatedFieldIds.forEach((fieldId) => {
|
||||
// delete cache cells
|
||||
void didUpdateCells({ viewId, database, fieldId });
|
||||
});
|
||||
}
|
||||
|
@ -24,9 +24,16 @@ import {
|
||||
DatabaseEventGetAllFieldSettings,
|
||||
} from '@/services/backend/events/flowy-database2';
|
||||
import { Field, pbToField } from './field_types';
|
||||
import { bytesToTypeOption, getTypeOption } from './type_option';
|
||||
import { getTypeOption } from './type_option';
|
||||
import { Database } from '$app/components/database/application';
|
||||
|
||||
export async function getFields(viewId: string, fieldIds?: string[]): Promise<Field[]> {
|
||||
export async function getFields(
|
||||
viewId: string,
|
||||
fieldIds?: string[]
|
||||
): Promise<{
|
||||
fields: Field[];
|
||||
typeOptions: Database['typeOptions'];
|
||||
}> {
|
||||
const payload = GetFieldPayloadPB.fromObject({
|
||||
view_id: viewId,
|
||||
field_ids: fieldIds
|
||||
@ -48,22 +55,29 @@ export async function getFields(viewId: string, fieldIds?: string[]): Promise<Fi
|
||||
return Promise.reject('Failed to get fields');
|
||||
}
|
||||
|
||||
const typeOptions: Database['typeOptions'] = {};
|
||||
|
||||
const fields = await Promise.all(
|
||||
result.val.items.map(async (item) => {
|
||||
const setting = settings.val.items.find((setting) => setting.field_id === item.id);
|
||||
|
||||
const field = pbToField(item);
|
||||
const typeOption = await getTypeOption(viewId, field.id, field.type);
|
||||
|
||||
const typeOption = await getTypeOption(viewId, item.id, item.field_type);
|
||||
|
||||
if (typeOption) {
|
||||
typeOptions[item.id] = typeOption;
|
||||
}
|
||||
|
||||
return {
|
||||
...field,
|
||||
visibility: setting?.visibility,
|
||||
width: setting?.width,
|
||||
typeOption,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return fields;
|
||||
return { fields, typeOptions };
|
||||
}
|
||||
|
||||
export async function createField(viewId: string, fieldType?: FieldType, data?: Uint8Array): Promise<Field> {
|
||||
@ -79,10 +93,7 @@ export async function createField(viewId: string, fieldType?: FieldType, data?:
|
||||
return Promise.reject('Failed to create field');
|
||||
}
|
||||
|
||||
const field = pbToField(result.val.field);
|
||||
|
||||
field.typeOption = bytesToTypeOption(result.val.type_option_data, field.type);
|
||||
return field;
|
||||
return pbToField(result.val.field);
|
||||
}
|
||||
|
||||
export async function duplicateField(viewId: string, fieldId: string): Promise<void> {
|
||||
|
@ -1,11 +1,9 @@
|
||||
import { FieldPB, FieldType, FieldVisibility } from '@/services/backend';
|
||||
import { DateTimeTypeOption, NumberTypeOption, SelectTypeOption } from './type_option/type_option_types';
|
||||
|
||||
export interface Field {
|
||||
id: string;
|
||||
name: string;
|
||||
type: FieldType;
|
||||
typeOption?: unknown;
|
||||
visibility?: FieldVisibility;
|
||||
width?: number;
|
||||
isPrimary: boolean;
|
||||
@ -13,24 +11,41 @@ export interface Field {
|
||||
|
||||
export interface NumberField extends Field {
|
||||
type: FieldType.Number;
|
||||
typeOption: NumberTypeOption;
|
||||
}
|
||||
|
||||
export interface DateTimeField extends Field {
|
||||
type: FieldType.DateTime;
|
||||
typeOption: DateTimeTypeOption;
|
||||
}
|
||||
|
||||
export interface LastEditedTimeField extends Field {
|
||||
type: FieldType.LastEditedTime;
|
||||
}
|
||||
|
||||
export interface CreatedTimeField extends Field {
|
||||
type: FieldType.CreatedTime;
|
||||
}
|
||||
|
||||
export type UndeterminedDateField = DateTimeField | CreatedTimeField | LastEditedTimeField;
|
||||
|
||||
export interface SelectField extends Field {
|
||||
type: FieldType.SingleSelect | FieldType.MultiSelect;
|
||||
typeOption: SelectTypeOption;
|
||||
}
|
||||
|
||||
export interface ChecklistField extends Field {
|
||||
type: FieldType.Checklist;
|
||||
}
|
||||
|
||||
export interface DateTimeField extends Field {
|
||||
type: FieldType.DateTime;
|
||||
}
|
||||
|
||||
export type UndeterminedField = NumberField | DateTimeField | SelectField | Field;
|
||||
|
||||
export const pbToField = (pb: FieldPB): Field => ({
|
||||
id: pb.id,
|
||||
name: pb.name,
|
||||
type: pb.field_type,
|
||||
isPrimary: pb.is_primary,
|
||||
});
|
||||
export const pbToField = (pb: FieldPB): Field => {
|
||||
return {
|
||||
id: pb.id,
|
||||
name: pb.name,
|
||||
type: pb.field_type,
|
||||
isPrimary: pb.is_primary,
|
||||
};
|
||||
};
|
||||
|
@ -1,7 +1,4 @@
|
||||
import {
|
||||
CreateSelectOptionPayloadPB,
|
||||
RepeatedSelectOptionPayload,
|
||||
} from '@/services/backend';
|
||||
import { CreateSelectOptionPayloadPB, RepeatedSelectOptionPayload } from '@/services/backend';
|
||||
import {
|
||||
DatabaseEventCreateSelectOption,
|
||||
DatabaseEventInsertOrUpdateSelectOption,
|
||||
@ -28,7 +25,7 @@ export async function insertOrUpdateSelectOption(
|
||||
viewId: string,
|
||||
fieldId: string,
|
||||
items: Partial<SelectOption>[],
|
||||
rowId?: string,
|
||||
rowId?: string
|
||||
): Promise<void> {
|
||||
const payload = RepeatedSelectOptionPayload.fromObject({
|
||||
view_id: viewId,
|
||||
@ -46,13 +43,13 @@ export async function deleteSelectOption(
|
||||
viewId: string,
|
||||
fieldId: string,
|
||||
items: Partial<SelectOption>[],
|
||||
rowId?: string,
|
||||
rowId?: string
|
||||
): Promise<void> {
|
||||
const payload = RepeatedSelectOptionPayload.fromObject({
|
||||
view_id: viewId,
|
||||
field_id: fieldId,
|
||||
row_id: rowId,
|
||||
items: items,
|
||||
items,
|
||||
});
|
||||
|
||||
const result = await DatabaseEventDeleteSelectOption(payload);
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { FieldType, TypeOptionPathPB, TypeOptionChangesetPB } from '@/services/backend';
|
||||
import {
|
||||
FieldType,
|
||||
TypeOptionPathPB,
|
||||
} from '@/services/backend';
|
||||
import { DatabaseEventGetTypeOption } from '@/services/backend/events/flowy-database2';
|
||||
import { bytesToTypeOption } from './type_option_types';
|
||||
DatabaseEventGetTypeOption,
|
||||
DatabaseEventUpdateFieldTypeOption,
|
||||
} from '@/services/backend/events/flowy-database2';
|
||||
import { bytesToTypeOption, UndeterminedTypeOptionData, typeOptionDataToPB } from './type_option_types';
|
||||
|
||||
export async function getTypeOption(viewId: string, fieldId: string, fieldType: FieldType) {
|
||||
const payload = TypeOptionPathPB.fromObject({
|
||||
@ -14,5 +14,32 @@ export async function getTypeOption(viewId: string, fieldId: string, fieldType:
|
||||
|
||||
const result = await DatabaseEventGetTypeOption(payload);
|
||||
|
||||
return result.map(value => bytesToTypeOption(value.type_option_data, fieldType)).unwrap();
|
||||
if (!result.ok) {
|
||||
return Promise.reject(result.val);
|
||||
}
|
||||
|
||||
const value = result.val;
|
||||
|
||||
return bytesToTypeOption(value.type_option_data, fieldType);
|
||||
}
|
||||
|
||||
export async function updateTypeOption(
|
||||
viewId: string,
|
||||
fieldId: string,
|
||||
fieldType: FieldType,
|
||||
data: UndeterminedTypeOptionData
|
||||
) {
|
||||
const payload = TypeOptionChangesetPB.fromObject({
|
||||
view_id: viewId,
|
||||
field_id: fieldId,
|
||||
type_option_data: typeOptionDataToPB(data, fieldType)?.serialize(),
|
||||
});
|
||||
|
||||
const result = await DatabaseEventUpdateFieldTypeOption(payload);
|
||||
|
||||
if (!result.ok) {
|
||||
return Promise.reject(result.val);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
@ -8,6 +8,9 @@ import {
|
||||
RichTextTypeOptionPB,
|
||||
SingleSelectTypeOptionPB,
|
||||
TimeFormatPB,
|
||||
ChecklistTypeOptionPB,
|
||||
DateTypeOptionPB,
|
||||
TimestampTypeOptionPB,
|
||||
} from '@/services/backend';
|
||||
import { pbToSelectOption, SelectOption } from '../select_option';
|
||||
|
||||
@ -26,6 +29,9 @@ export interface DateTimeTypeOption {
|
||||
dateFormat?: DateFormatPB;
|
||||
timeFormat?: TimeFormatPB;
|
||||
timezoneId?: string;
|
||||
}
|
||||
export interface TimeStampTypeOption extends DateTimeTypeOption {
|
||||
includeTime?: boolean;
|
||||
fieldType?: FieldType;
|
||||
}
|
||||
|
||||
@ -38,6 +44,50 @@ export interface CheckboxTypeOption {
|
||||
isSelected?: boolean;
|
||||
}
|
||||
|
||||
export interface ChecklistTypeOption {
|
||||
config?: string;
|
||||
}
|
||||
|
||||
export type UndeterminedTypeOptionData =
|
||||
| TextTypeOption
|
||||
| NumberTypeOption
|
||||
| SelectTypeOption
|
||||
| CheckboxTypeOption
|
||||
| ChecklistTypeOption
|
||||
| DateTimeTypeOption
|
||||
| TimeStampTypeOption;
|
||||
|
||||
export function typeOptionDataToPB(data: UndeterminedTypeOptionData, fieldType: FieldType) {
|
||||
switch (fieldType) {
|
||||
case FieldType.Number:
|
||||
return NumberTypeOptionPB.fromObject(data as NumberTypeOption);
|
||||
case FieldType.DateTime:
|
||||
return dateTimeTypeOptionToPB(data as DateTimeTypeOption);
|
||||
case FieldType.CreatedTime:
|
||||
case FieldType.LastEditedTime:
|
||||
return timestampTypeOptionToPB(data as TimeStampTypeOption);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function dateTimeTypeOptionToPB(data: DateTimeTypeOption): DateTypeOptionPB {
|
||||
return DateTypeOptionPB.fromObject({
|
||||
time_format: data.timeFormat,
|
||||
date_format: data.dateFormat,
|
||||
timezone_id: data.timezoneId,
|
||||
});
|
||||
}
|
||||
|
||||
function timestampTypeOptionToPB(data: TimeStampTypeOption): TimestampTypeOptionPB {
|
||||
return TimestampTypeOptionPB.fromObject({
|
||||
include_time: data.includeTime,
|
||||
date_format: data.dateFormat,
|
||||
time_format: data.timeFormat,
|
||||
field_type: data.fieldType,
|
||||
});
|
||||
}
|
||||
|
||||
function pbToSelectTypeOption(pb: SingleSelectTypeOptionPB | MultiSelectTypeOptionPB): SelectTypeOption {
|
||||
return {
|
||||
@ -52,6 +102,29 @@ function pbToCheckboxTypeOption(pb: CheckboxTypeOptionPB): CheckboxTypeOption {
|
||||
};
|
||||
}
|
||||
|
||||
function pbToChecklistTypeOption(pb: ChecklistTypeOptionPB): ChecklistTypeOption {
|
||||
return {
|
||||
config: pb.config,
|
||||
};
|
||||
}
|
||||
|
||||
function pbToDateTypeOption(pb: DateTypeOptionPB): DateTimeTypeOption {
|
||||
return {
|
||||
dateFormat: pb.date_format,
|
||||
timezoneId: pb.timezone_id,
|
||||
timeFormat: pb.time_format,
|
||||
};
|
||||
}
|
||||
|
||||
function pbToTimeStampTypeOption(pb: TimestampTypeOptionPB): TimeStampTypeOption {
|
||||
return {
|
||||
includeTime: pb.include_time,
|
||||
dateFormat: pb.date_format,
|
||||
timeFormat: pb.time_format,
|
||||
fieldType: pb.field_type,
|
||||
};
|
||||
}
|
||||
|
||||
export function bytesToTypeOption(data: Uint8Array, fieldType: FieldType) {
|
||||
switch (fieldType) {
|
||||
case FieldType.RichText:
|
||||
@ -64,5 +137,12 @@ export function bytesToTypeOption(data: Uint8Array, fieldType: FieldType) {
|
||||
return pbToSelectTypeOption(MultiSelectTypeOptionPB.deserialize(data));
|
||||
case FieldType.Checkbox:
|
||||
return pbToCheckboxTypeOption(CheckboxTypeOptionPB.deserialize(data));
|
||||
case FieldType.Checklist:
|
||||
return pbToChecklistTypeOption(ChecklistTypeOptionPB.deserialize(data));
|
||||
case FieldType.DateTime:
|
||||
return pbToDateTypeOption(DateTypeOptionPB.deserialize(data));
|
||||
case FieldType.CreatedTime:
|
||||
case FieldType.LastEditedTime:
|
||||
return pbToTimeStampTypeOption(TimestampTypeOptionPB.deserialize(data));
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,32 @@
|
||||
import { TextFilterConditionPB, FieldType } from '@/services/backend';
|
||||
import {
|
||||
CheckboxFilterConditionPB,
|
||||
ChecklistFilterConditionPB,
|
||||
FieldType,
|
||||
NumberFilterConditionPB,
|
||||
TextFilterConditionPB,
|
||||
} from '@/services/backend';
|
||||
import { UndeterminedFilter } from '$app/components/database/application';
|
||||
|
||||
export function getDefaultFilter(fieldType: FieldType): UndeterminedFilter['data'] | undefined {
|
||||
switch (fieldType) {
|
||||
case FieldType.RichText:
|
||||
case FieldType.URL:
|
||||
return {
|
||||
condition: TextFilterConditionPB.Contains,
|
||||
content: '',
|
||||
};
|
||||
case FieldType.Number:
|
||||
return {
|
||||
condition: NumberFilterConditionPB.NumberIsNotEmpty,
|
||||
};
|
||||
case FieldType.Checkbox:
|
||||
return {
|
||||
condition: CheckboxFilterConditionPB.IsUnChecked,
|
||||
};
|
||||
case FieldType.Checklist:
|
||||
return {
|
||||
condition: ChecklistFilterConditionPB.IsIncomplete,
|
||||
};
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ export async function insertFilter({
|
||||
viewId: string;
|
||||
fieldId: string;
|
||||
fieldType: FieldType;
|
||||
data: UndeterminedFilter['data'];
|
||||
data?: UndeterminedFilter['data'];
|
||||
filterId?: string;
|
||||
}): Promise<void> {
|
||||
const payload = DatabaseSettingChangesetPB.fromObject({
|
||||
@ -36,7 +36,7 @@ export async function insertFilter({
|
||||
field_id: fieldId,
|
||||
field_type: fieldType,
|
||||
filter_id: filterId,
|
||||
data: filterDataToPB(data, fieldType)?.serialize(),
|
||||
data: data ? filterDataToPB(data, fieldType)?.serialize() : undefined,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -1,10 +1,18 @@
|
||||
import {
|
||||
CheckboxFilterConditionPB,
|
||||
CheckboxFilterPB,
|
||||
FieldType,
|
||||
TextFilterConditionPB,
|
||||
SelectOptionConditionPB,
|
||||
TextFilterPB,
|
||||
SelectOptionFilterPB,
|
||||
FilterPB,
|
||||
NumberFilterConditionPB,
|
||||
NumberFilterPB,
|
||||
SelectOptionConditionPB,
|
||||
SelectOptionFilterPB,
|
||||
TextFilterConditionPB,
|
||||
TextFilterPB,
|
||||
ChecklistFilterConditionPB,
|
||||
ChecklistFilterPB,
|
||||
DateFilterConditionPB,
|
||||
DateFilterPB,
|
||||
} from '@/services/backend';
|
||||
|
||||
export interface Filter {
|
||||
@ -29,16 +37,63 @@ export interface SelectFilter extends Filter {
|
||||
data: SelectFilterData;
|
||||
}
|
||||
|
||||
export interface NumberFilter extends Filter {
|
||||
fieldType: FieldType.Number;
|
||||
data: NumberFilterData;
|
||||
}
|
||||
|
||||
export interface CheckboxFilter extends Filter {
|
||||
fieldType: FieldType.Checkbox;
|
||||
data: CheckboxFilterData;
|
||||
}
|
||||
|
||||
export interface CheckboxFilterData {
|
||||
condition?: CheckboxFilterConditionPB;
|
||||
}
|
||||
|
||||
export interface ChecklistFilter extends Filter {
|
||||
fieldType: FieldType.Checklist;
|
||||
data: ChecklistFilterData;
|
||||
}
|
||||
|
||||
export interface DateFilter extends Filter {
|
||||
fieldType: FieldType.DateTime | FieldType.CreatedTime | FieldType.LastEditedTime;
|
||||
data: DateFilterData;
|
||||
}
|
||||
|
||||
export interface ChecklistFilterData {
|
||||
condition?: ChecklistFilterConditionPB;
|
||||
}
|
||||
|
||||
export interface SelectFilterData {
|
||||
condition?: SelectOptionConditionPB;
|
||||
optionIds?: string[];
|
||||
}
|
||||
|
||||
export type UndeterminedFilter = TextFilter | SelectFilter;
|
||||
export interface NumberFilterData {
|
||||
condition: NumberFilterConditionPB;
|
||||
content?: string;
|
||||
}
|
||||
|
||||
export interface DateFilterData {
|
||||
condition: DateFilterConditionPB;
|
||||
start?: number;
|
||||
end?: number;
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
export type UndeterminedFilter =
|
||||
| TextFilter
|
||||
| SelectFilter
|
||||
| NumberFilter
|
||||
| CheckboxFilter
|
||||
| ChecklistFilter
|
||||
| DateFilter;
|
||||
|
||||
export function filterDataToPB(data: UndeterminedFilter['data'], fieldType: FieldType) {
|
||||
switch (fieldType) {
|
||||
case FieldType.RichText:
|
||||
case FieldType.URL:
|
||||
return TextFilterPB.fromObject({
|
||||
condition: (data as TextFilterData).condition,
|
||||
content: (data as TextFilterData).content,
|
||||
@ -49,6 +104,28 @@ export function filterDataToPB(data: UndeterminedFilter['data'], fieldType: Fiel
|
||||
condition: (data as SelectFilterData).condition,
|
||||
option_ids: (data as SelectFilterData).optionIds,
|
||||
});
|
||||
case FieldType.Number:
|
||||
return NumberFilterPB.fromObject({
|
||||
condition: (data as NumberFilterData).condition,
|
||||
content: (data as NumberFilterData).content,
|
||||
});
|
||||
case FieldType.Checkbox:
|
||||
return CheckboxFilterPB.fromObject({
|
||||
condition: (data as CheckboxFilterData).condition,
|
||||
});
|
||||
case FieldType.Checklist:
|
||||
return ChecklistFilterPB.fromObject({
|
||||
condition: (data as ChecklistFilterData).condition,
|
||||
});
|
||||
case FieldType.DateTime:
|
||||
case FieldType.CreatedTime:
|
||||
case FieldType.LastEditedTime:
|
||||
return DateFilterPB.fromObject({
|
||||
condition: (data as DateFilterData).condition,
|
||||
start: (data as DateFilterData).start,
|
||||
end: (data as DateFilterData).end,
|
||||
timestamp: (data as DateFilterData).timestamp,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -66,13 +143,52 @@ export function pbToSelectFilterData(pb: SelectOptionFilterPB): SelectFilterData
|
||||
};
|
||||
}
|
||||
|
||||
export function pbToNumberFilterData(pb: NumberFilterPB): NumberFilterData {
|
||||
return {
|
||||
condition: pb.condition,
|
||||
content: pb.content,
|
||||
};
|
||||
}
|
||||
|
||||
export function pbToCheckboxFilterData(pb: CheckboxFilterPB): CheckboxFilterData {
|
||||
return {
|
||||
condition: pb.condition,
|
||||
};
|
||||
}
|
||||
|
||||
export function pbToChecklistFilterData(pb: ChecklistFilterPB): ChecklistFilterData {
|
||||
return {
|
||||
condition: pb.condition,
|
||||
};
|
||||
}
|
||||
|
||||
export function pbToDateFilterData(pb: DateFilterPB): DateFilterData {
|
||||
return {
|
||||
condition: pb.condition,
|
||||
start: pb.start,
|
||||
end: pb.end,
|
||||
timestamp: pb.timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
export function bytesToFilterData(bytes: Uint8Array, fieldType: FieldType) {
|
||||
switch (fieldType) {
|
||||
case FieldType.RichText:
|
||||
case FieldType.URL:
|
||||
return pbToTextFilterData(TextFilterPB.deserialize(bytes));
|
||||
case FieldType.SingleSelect:
|
||||
case FieldType.MultiSelect:
|
||||
return pbToSelectFilterData(SelectOptionFilterPB.deserialize(bytes));
|
||||
case FieldType.Number:
|
||||
return pbToNumberFilterData(NumberFilterPB.deserialize(bytes));
|
||||
case FieldType.Checkbox:
|
||||
return pbToCheckboxFilterData(CheckboxFilterPB.deserialize(bytes));
|
||||
case FieldType.Checklist:
|
||||
return pbToChecklistFilterData(ChecklistFilterPB.deserialize(bytes));
|
||||
case FieldType.DateTime:
|
||||
case FieldType.CreatedTime:
|
||||
case FieldType.LastEditedTime:
|
||||
return pbToDateFilterData(DateFilterPB.deserialize(bytes));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,2 +1,3 @@
|
||||
export * from './filter_types';
|
||||
export * as filterService from './filter_service';
|
||||
export * as filterListeners from './filter_listeners';
|
||||
|
@ -1,6 +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';
|
||||
|
||||
const deleteRowsFromChangeset = (database: Database, changeset: RowsChangePB) => {
|
||||
changeset.deleted_rows.forEach((rowId) => {
|
||||
@ -8,6 +9,8 @@ const deleteRowsFromChangeset = (database: Database, changeset: RowsChangePB) =>
|
||||
|
||||
if (index !== -1) {
|
||||
database.rowMetas.splice(index, 1);
|
||||
// delete cells
|
||||
didDeleteCells({ database, rowId });
|
||||
}
|
||||
});
|
||||
};
|
||||
@ -23,7 +26,7 @@ const updateRowsFromChangeset = (database: Database, changeset: RowsChangePB) =>
|
||||
const found = database.rowMetas.find((rowMeta) => rowMeta.id === rowId);
|
||||
|
||||
if (found) {
|
||||
Object.assign(found, pbToRowMeta(rowMetaPB));
|
||||
Object.assign(found, rowMetaPB ? pbToRowMeta(rowMetaPB) : {});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -1,23 +1,63 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { DatabaseNotification, FieldType } from '@/services/backend';
|
||||
import { DatabaseNotification } from '@/services/backend';
|
||||
import { useNotification, useViewId } from '$app/hooks';
|
||||
import { cellService, Cell } from '../../application';
|
||||
import { cellService, Cell, Field } from '../../application';
|
||||
import { useDispatchCell, useSelectorCell } from '$app/components/database';
|
||||
|
||||
export const useCell = (rowId: string, fieldId: string, fieldType: FieldType) => {
|
||||
export const useCell = (rowId: string, field: Field) => {
|
||||
const viewId = useViewId();
|
||||
const [cell, setCell] = useState<Cell | undefined>(undefined);
|
||||
const { setCell } = useDispatchCell();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const cell = useSelectorCell(rowId, field.id);
|
||||
|
||||
const fetchCell = useCallback(() => {
|
||||
void cellService.getCell(viewId, rowId, fieldId, fieldType).then((data) => {
|
||||
setLoading(true);
|
||||
void cellService.getCell(viewId, rowId, field.id, field.type).then((data) => {
|
||||
// cache cell
|
||||
setCell(data);
|
||||
setLoading(false);
|
||||
});
|
||||
}, [viewId, rowId, fieldId, fieldType]);
|
||||
}, [viewId, rowId, field.id, field.type, setCell]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCell();
|
||||
}, [fetchCell]);
|
||||
// fetch cell if not cached
|
||||
if (!cell && !loading) {
|
||||
// fetch cell in next tick to avoid blocking
|
||||
const timeout = setTimeout(fetchCell, 0);
|
||||
|
||||
useNotification(DatabaseNotification.DidUpdateCell, fetchCell, { id: `${rowId}:${fieldId}` });
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}
|
||||
}, [fetchCell, cell, loading]);
|
||||
|
||||
useNotification(DatabaseNotification.DidUpdateCell, fetchCell, { id: `${rowId}:${field.id}` });
|
||||
|
||||
return cell;
|
||||
};
|
||||
|
||||
export const useInputCell = (cell?: Cell) => {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [value, setValue] = useState('');
|
||||
const viewId = useViewId();
|
||||
const updateCell = useCallback(() => {
|
||||
if (!cell) return;
|
||||
const { rowId, fieldId } = cell;
|
||||
|
||||
if (editing) {
|
||||
if (value !== cell.data) {
|
||||
void cellService.updateCell(viewId, rowId, fieldId, value);
|
||||
}
|
||||
|
||||
setEditing(false);
|
||||
}
|
||||
}, [cell, editing, value, viewId]);
|
||||
|
||||
return {
|
||||
updateCell,
|
||||
editing,
|
||||
setEditing,
|
||||
value,
|
||||
setValue,
|
||||
};
|
||||
};
|
||||
|
@ -6,6 +6,11 @@ import { useCell } from './Cell.hooks';
|
||||
import { TextCell } from './TextCell';
|
||||
import { SelectCell } from './SelectCell';
|
||||
import { CheckboxCell } from './CheckboxCell';
|
||||
import NumberCell from '$app/components/database/components/cell/NumberCell';
|
||||
import URLCell from '$app/components/database/components/cell/URLCell';
|
||||
import ChecklistCell from '$app/components/database/components/cell/ChecklistCell';
|
||||
import DateTimeCell from '$app/components/database/components/cell/DateTimeCell';
|
||||
import TimestampCell from '$app/components/database/components/cell/TimestampCell';
|
||||
|
||||
export interface CellProps {
|
||||
rowId: string;
|
||||
@ -15,25 +20,44 @@ export interface CellProps {
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
interface CellComponentProps {
|
||||
field: Field;
|
||||
cell: CellType;
|
||||
}
|
||||
const getCellComponent = (fieldType: FieldType) => {
|
||||
switch (fieldType) {
|
||||
case FieldType.RichText:
|
||||
return TextCell as FC<{ field: Field; cell?: CellType }>;
|
||||
return TextCell as FC<CellComponentProps>;
|
||||
case FieldType.SingleSelect:
|
||||
case FieldType.MultiSelect:
|
||||
return SelectCell as FC<{ field: Field; cell?: CellType }>;
|
||||
return SelectCell as FC<CellComponentProps>;
|
||||
case FieldType.Checkbox:
|
||||
return CheckboxCell as FC<{ field: Field; cell?: CellType }>;
|
||||
return CheckboxCell as FC<CellComponentProps>;
|
||||
case FieldType.Checklist:
|
||||
return ChecklistCell as FC<CellComponentProps>;
|
||||
case FieldType.Number:
|
||||
return NumberCell as FC<CellComponentProps>;
|
||||
case FieldType.URL:
|
||||
return URLCell as FC<CellComponentProps>;
|
||||
case FieldType.DateTime:
|
||||
return DateTimeCell as FC<CellComponentProps>;
|
||||
case FieldType.LastEditedTime:
|
||||
case FieldType.CreatedTime:
|
||||
return TimestampCell as FC<CellComponentProps>;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const Cell: FC<CellProps> = ({ rowId, field, ...props }) => {
|
||||
const cell = useCell(rowId, field.id, field.type);
|
||||
const cell = useCell(rowId, field);
|
||||
|
||||
const Component = getCellComponent(field.type);
|
||||
|
||||
if (!cell) {
|
||||
return <div className={`h-[36px] w-[${field.width}px]`} />;
|
||||
}
|
||||
|
||||
if (!Component) {
|
||||
return null;
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Checkbox } from '@mui/material';
|
||||
import { FC, useCallback } from 'react';
|
||||
import React, { FC, useCallback } from 'react';
|
||||
import { ReactComponent as CheckboxCheckSvg } from '$app/assets/database/checkbox-check.svg';
|
||||
import { ReactComponent as CheckboxUncheckSvg } from '$app/assets/database/checkbox-uncheck.svg';
|
||||
import { useViewId } from '$app/hooks';
|
||||
@ -7,25 +6,18 @@ import { cellService, CheckboxCell as CheckboxCellType, Field } from '../../appl
|
||||
|
||||
export const CheckboxCell: FC<{
|
||||
field: Field;
|
||||
cell?: CheckboxCellType;
|
||||
cell: CheckboxCellType;
|
||||
}> = ({ field, cell }) => {
|
||||
const viewId = useViewId();
|
||||
const checked = cell?.data === 'Yes';
|
||||
const checked = cell.data === 'Yes';
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (!cell) return;
|
||||
void cellService.updateCell(viewId, cell.rowId, field.id, !checked ? 'Yes' : 'No');
|
||||
}, [viewId, cell, field.id, checked]);
|
||||
|
||||
return (
|
||||
<div className='flex w-full cursor-pointer items-center px-2' onClick={handleClick}>
|
||||
<Checkbox
|
||||
disableRipple
|
||||
style={{ padding: 0 }}
|
||||
checked={checked}
|
||||
icon={<CheckboxUncheckSvg />}
|
||||
checkedIcon={<CheckboxCheckSvg />}
|
||||
/>
|
||||
<div className='relative flex w-full cursor-pointer items-center px-2 text-fill-default' onClick={handleClick}>
|
||||
{checked ? <CheckboxCheckSvg /> : <CheckboxUncheckSvg />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,55 @@
|
||||
import React, { useState, Suspense, useMemo } from 'react';
|
||||
import { ChecklistCell as ChecklistCellType, ChecklistField } from '$app/components/database/application';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import ChecklistCellActions from '$app/components/database/components/field_types/checklist/ChecklistCellActions';
|
||||
|
||||
interface Props {
|
||||
field: ChecklistField;
|
||||
cell: ChecklistCellType;
|
||||
}
|
||||
|
||||
function ChecklistCell({ cell }: Props) {
|
||||
const value = cell?.data.percentage ?? 0;
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLDivElement | undefined>(undefined);
|
||||
const open = Boolean(anchorEl);
|
||||
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
setAnchorEl(e.currentTarget);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(undefined);
|
||||
};
|
||||
|
||||
const result = useMemo(() => `${Math.round(value * 100)}%`, [value]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='flex w-full cursor-pointer items-center px-2' onClick={handleClick}>
|
||||
<Typography variant='body2' color='text.secondary'>
|
||||
{result}
|
||||
</Typography>
|
||||
</div>
|
||||
<Suspense>
|
||||
{open && (
|
||||
<ChecklistCellActions
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleClose}
|
||||
cell={cell}
|
||||
/>
|
||||
)}
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChecklistCell;
|
@ -0,0 +1,55 @@
|
||||
import React, { Suspense, useRef, useState, useMemo } from 'react';
|
||||
import { DateTimeCell as DateTimeCellType, DateTimeField } from '$app/components/database/application';
|
||||
import DateTimeCellActions from '$app/components/database/components/field_types/date/DateTimeCellActions';
|
||||
|
||||
interface Props {
|
||||
field: DateTimeField;
|
||||
cell: DateTimeCellType;
|
||||
placeholder?: string;
|
||||
}
|
||||
function DateTimeCell({ field, cell, placeholder }: Props) {
|
||||
const isRange = cell.data.isRange;
|
||||
const includeTime = cell.data.includeTime;
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const content = useMemo(() => {
|
||||
const { date, time, endDate, endTime } = cell.data;
|
||||
|
||||
if (date) {
|
||||
return (
|
||||
<>
|
||||
{date}
|
||||
{includeTime && time ? ' ' + time : ''}
|
||||
{isRange && endDate ? ' - ' + endDate : ''}
|
||||
{isRange && includeTime && endTime ? ' ' + endTime : ''}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className={'text-sm text-text-placeholder'}>{placeholder}</div>;
|
||||
}, [cell, includeTime, isRange, placeholder]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={ref} className={'flex h-full w-full items-center px-2 text-xs font-medium'} onClick={handleClick}>
|
||||
{content}
|
||||
</div>
|
||||
<Suspense>
|
||||
{open && (
|
||||
<DateTimeCellActions field={field} onClose={handleClose} anchorEl={ref.current} cell={cell} open={open} />
|
||||
)}
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default DateTimeCell;
|
@ -21,7 +21,12 @@ function ExpandButton({ cell, documentId, icon, visible }: Props) {
|
||||
return (
|
||||
<>
|
||||
{visible && (
|
||||
<div className={`mr-4 flex items-center justify-center`}>
|
||||
<div
|
||||
style={{
|
||||
transform: 'translateY(-50%) translateZ(0)',
|
||||
}}
|
||||
className={`absolute right-0 top-1/2 mr-4 flex items-center justify-center`}
|
||||
>
|
||||
<IconButton onClick={() => setOpen(true)} className={'h-6 w-6 text-sm'}>
|
||||
<OpenIcon />
|
||||
</IconButton>
|
||||
|
@ -0,0 +1,50 @@
|
||||
import React, { Suspense, useCallback, useMemo, useRef } from 'react';
|
||||
import { Field, NumberCell as NumberCellType } from '$app/components/database/application';
|
||||
import { CellText } from '$app/components/database/_shared';
|
||||
import EditNumberCellInput from '$app/components/database/components/field_types/number/EditNumberCellInput';
|
||||
import { useInputCell } from '$app/components/database/components/cell/Cell.hooks';
|
||||
|
||||
interface Props {
|
||||
field: Field;
|
||||
cell: NumberCellType;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
function NumberCell({ field, cell, placeholder }: Props) {
|
||||
const cellRef = useRef<HTMLDivElement>(null);
|
||||
const { value, editing, updateCell, setEditing, setValue } = useInputCell(cell);
|
||||
const content = useMemo(() => {
|
||||
if (typeof cell.data === 'string' && cell.data) {
|
||||
return cell.data;
|
||||
}
|
||||
|
||||
return <div className={'text-sm text-text-placeholder'}>{placeholder}</div>;
|
||||
}, [cell, placeholder]);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
setValue(cell.data);
|
||||
setEditing(true);
|
||||
}, [cell, setEditing, setValue]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CellText className={'min-h-[36px]'} ref={cellRef} onClick={handleClick}>
|
||||
<div className='flex h-full w-full items-center'>{content}</div>
|
||||
</CellText>
|
||||
<Suspense>
|
||||
{editing && (
|
||||
<EditNumberCellInput
|
||||
editing={editing}
|
||||
anchorEl={cellRef.current}
|
||||
width={field?.width}
|
||||
onClose={updateCell}
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
/>
|
||||
)}
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default NumberCell;
|
@ -0,0 +1,76 @@
|
||||
import { FC, useCallback, useMemo, useState, Suspense, lazy } from 'react';
|
||||
import { MenuProps, Menu } from '@mui/material';
|
||||
import { SelectField, SelectCell as SelectCellType, SelectTypeOption } from '../../application';
|
||||
import { Tag } from '../field_types/select/Tag';
|
||||
import { useTypeOption } from '$app/components/database';
|
||||
|
||||
const SelectCellActions = lazy(
|
||||
() => import('$app/components/database/components/field_types/select/select_cell_actions/SelectCellActions')
|
||||
);
|
||||
const menuProps: Partial<MenuProps> = {
|
||||
classes: {
|
||||
list: 'py-5',
|
||||
},
|
||||
anchorOrigin: {
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
},
|
||||
transformOrigin: {
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
},
|
||||
};
|
||||
|
||||
export const SelectCell: FC<{
|
||||
field: SelectField;
|
||||
cell: SelectCellType;
|
||||
placeholder?: string;
|
||||
}> = ({ field, cell, placeholder }) => {
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
|
||||
const selectedIds = useMemo(() => cell.data?.selectedOptionIds ?? [], [cell]);
|
||||
const open = Boolean(anchorEl);
|
||||
const handleClose = useCallback(() => {
|
||||
setAnchorEl(null);
|
||||
}, []);
|
||||
|
||||
const typeOption = useTypeOption<SelectTypeOption>(field.id);
|
||||
|
||||
const renderSelectedOptions = useCallback(
|
||||
(selected: string[]) =>
|
||||
selected
|
||||
.map((id) => typeOption.options?.find((option) => option.id === id))
|
||||
.map((option) => option && <Tag key={option.id} size='small' color={option.color} label={option.name} />),
|
||||
[typeOption]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={'relative w-full'}>
|
||||
<div
|
||||
onClick={(e) => {
|
||||
setAnchorEl(e.currentTarget);
|
||||
}}
|
||||
className={'flex h-full w-full cursor-pointer items-center gap-2 overflow-x-hidden px-2 py-1'}
|
||||
>
|
||||
{selectedIds.length === 0 ? (
|
||||
<div className={'text-sm text-text-placeholder'}>{placeholder}</div>
|
||||
) : (
|
||||
renderSelectedOptions(selectedIds)
|
||||
)}
|
||||
</div>
|
||||
<Suspense>
|
||||
{open ? (
|
||||
<Menu
|
||||
keepMounted={false}
|
||||
className='h-full w-full'
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
{...menuProps}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<SelectCellActions field={field} cell={cell} />
|
||||
</Menu>
|
||||
) : null}
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,22 +0,0 @@
|
||||
import { MenuItem, MenuItemProps } from '@mui/material';
|
||||
import { FC } from 'react';
|
||||
import { Tag } from './Tag';
|
||||
|
||||
export interface CreateOptionProps {
|
||||
label: React.ReactNode;
|
||||
onClick?: MenuItemProps['onClick'];
|
||||
}
|
||||
|
||||
export const CreateOption: FC<CreateOptionProps> = ({
|
||||
label,
|
||||
onClick,
|
||||
}) => {
|
||||
return (
|
||||
<MenuItem
|
||||
className="mt-2"
|
||||
onClick={onClick}
|
||||
>
|
||||
<Tag className="ml-2" size="small" label={label} />
|
||||
</MenuItem>
|
||||
);
|
||||
};
|
@ -1,146 +0,0 @@
|
||||
import { FC, FormEvent, useCallback, useMemo, useState } from 'react';
|
||||
import { t } from 'i18next';
|
||||
import { ListSubheader, Select, OutlinedInput, SelectChangeEvent, InputBase, MenuProps, MenuItem } from '@mui/material';
|
||||
import { FieldType } from '@/services/backend';
|
||||
import { useViewId } from '$app/hooks';
|
||||
import { cellService, SelectField, SelectCell as SelectCellType } from '../../../application';
|
||||
import { Tag } from './Tag';
|
||||
import { CreateOption } from './CreateOption';
|
||||
import { SelectOptionItem } from './SelectOptionItem';
|
||||
|
||||
const menuProps: Partial<MenuProps> = {
|
||||
classes: {
|
||||
list: 'py-5',
|
||||
},
|
||||
anchorOrigin: {
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
},
|
||||
transformOrigin: {
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
},
|
||||
};
|
||||
|
||||
export const SelectCell: FC<{
|
||||
field: SelectField;
|
||||
cell?: SelectCellType;
|
||||
}> = ({ field, cell }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const rowId = cell?.rowId;
|
||||
const viewId = useViewId();
|
||||
const options = useMemo(() => field.typeOption.options ?? [], [field.typeOption.options]);
|
||||
const selectedIds = useMemo(() => cell?.data.selectedOptionIds ?? [], [cell?.data.selectedOptionIds]);
|
||||
const [newOptionName, setNewOptionName] = useState('');
|
||||
const filteredOptions = useMemo(
|
||||
() =>
|
||||
options.filter((option) => {
|
||||
return option.name.toLowerCase().includes(newOptionName.toLowerCase());
|
||||
}),
|
||||
[options, newOptionName]
|
||||
);
|
||||
|
||||
const shouldCreateOption = !!newOptionName && filteredOptions.length === 0;
|
||||
|
||||
const handleInput = useCallback((event: FormEvent) => {
|
||||
const value = (event.target as HTMLInputElement).value;
|
||||
|
||||
setNewOptionName(value);
|
||||
}, []);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setNewOptionName('');
|
||||
setOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleChange = (event: SelectChangeEvent<string | string[]>) => {
|
||||
if (!cell || !rowId) return;
|
||||
const {
|
||||
target: { value },
|
||||
} = event;
|
||||
|
||||
const current = Array.isArray(value) ? value : [value];
|
||||
const prev = cell.data.selectedOptionIds;
|
||||
const deleteOptionIds = prev?.filter((id) => current.find((cur) => cur === id) === undefined);
|
||||
|
||||
void cellService.updateSelectCell(viewId, rowId, field.id, {
|
||||
insertOptionIds: current,
|
||||
deleteOptionIds,
|
||||
});
|
||||
};
|
||||
|
||||
const handleNewTagClick = async () => {
|
||||
if (!cell || !rowId) return;
|
||||
const exist = options.find((option) => option.name.toLowerCase() === newOptionName.toLowerCase());
|
||||
|
||||
if (exist) {
|
||||
return cellService.updateSelectCell(viewId, rowId, field.id, {
|
||||
insertOptionIds: [exist.id],
|
||||
});
|
||||
}
|
||||
|
||||
// const option = await cellService.createSelectOption(viewId, field.id, newOptionName);
|
||||
|
||||
// await cellService.insertOrUpdateSelectOption(viewId, field.id, [option], rowId);
|
||||
};
|
||||
|
||||
const searchInput = (
|
||||
<ListSubheader className='flex'>
|
||||
<OutlinedInput
|
||||
size='small'
|
||||
value={newOptionName}
|
||||
onInput={handleInput}
|
||||
placeholder={t('grid.selectOption.searchOrCreateOption')}
|
||||
/>
|
||||
</ListSubheader>
|
||||
);
|
||||
|
||||
const renderSelectedOptions = useCallback(
|
||||
(selected: string[]) =>
|
||||
selected
|
||||
.map((id) => options.find((option) => option.id === id))
|
||||
.map((option) => option && <Tag key={option.id} size='small' color={option.color} label={option.name} />),
|
||||
[options]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={'relative w-full'}>
|
||||
<div
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
className={'absolute left-0 top-0 flex h-full w-full items-center gap-2 px-4 py-1'}
|
||||
>
|
||||
{renderSelectedOptions(selectedIds)}
|
||||
</div>
|
||||
{open ? (
|
||||
<Select
|
||||
className='h-full w-full'
|
||||
size='small'
|
||||
value={selectedIds}
|
||||
open={open}
|
||||
multiple={field.type === FieldType.MultiSelect}
|
||||
input={<InputBase />}
|
||||
IconComponent={() => null}
|
||||
MenuProps={menuProps}
|
||||
onChange={handleChange}
|
||||
onClose={handleClose}
|
||||
>
|
||||
{searchInput}
|
||||
<ListSubheader className='mb-2 mt-4 text-xs'>
|
||||
{shouldCreateOption ? t('grid.selectOption.createNew') : t('grid.selectOption.orSelectOne')}
|
||||
</ListSubheader>
|
||||
{shouldCreateOption ? (
|
||||
<CreateOption label={newOptionName} onClick={handleNewTagClick} />
|
||||
) : (
|
||||
filteredOptions.map((option, index) => (
|
||||
<MenuItem className={index === 0 ? '' : 'mt-2'} key={option.id} value={option.id}>
|
||||
<SelectOptionItem option={option} />
|
||||
</MenuItem>
|
||||
))
|
||||
)}
|
||||
</Select>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,49 +0,0 @@
|
||||
import { FC, MouseEventHandler, useCallback, useRef, useState } from 'react';
|
||||
import { IconButton } from '@mui/material';
|
||||
import { ReactComponent as DetailsSvg } from '$app/assets/details.svg';
|
||||
import { SelectOption } from '../../../application';
|
||||
import { SelectOptionMenu } from './SelectOptionMenu';
|
||||
import { Tag } from './Tag';
|
||||
|
||||
export interface SelectOptionItemProps {
|
||||
option: SelectOption;
|
||||
}
|
||||
|
||||
export const SelectOptionItem: FC<SelectOptionItemProps> = ({
|
||||
option,
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const anchorEl = useRef<HTMLButtonElement | null>(null);
|
||||
|
||||
const handleClick = useCallback<MouseEventHandler<HTMLButtonElement>>((event) => {
|
||||
event.stopPropagation();
|
||||
anchorEl.current = event.target as HTMLButtonElement;
|
||||
setOpen(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex-1">
|
||||
<Tag
|
||||
key={option.id}
|
||||
size="small"
|
||||
color={option.color}
|
||||
label={option.name}
|
||||
/>
|
||||
</div>
|
||||
<IconButton onClick={handleClick}>
|
||||
<DetailsSvg className="text-base" />
|
||||
</IconButton>
|
||||
{open && (
|
||||
<SelectOptionMenu
|
||||
open={open}
|
||||
option={option}
|
||||
MenuProps={{
|
||||
anchorEl: anchorEl.current,
|
||||
onClose: () => setOpen(false),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,78 +0,0 @@
|
||||
import { FC } from 'react';
|
||||
import { t } from 'i18next';
|
||||
import {
|
||||
Divider,
|
||||
ListSubheader,
|
||||
Menu,
|
||||
MenuItem,
|
||||
MenuProps,
|
||||
OutlinedInput,
|
||||
} from '@mui/material';
|
||||
import { SelectOptionColorPB } from '@/services/backend';
|
||||
import { ReactComponent as DeleteSvg } from '$app/assets/delete.svg';
|
||||
import { ReactComponent as SelectCheckSvg } from '$app/assets/database/select-check.svg';
|
||||
import { SelectOption } from '../../../application';
|
||||
import { SelectOptionColorMap, SelectOptionColorTextMap } from './constants';
|
||||
|
||||
interface SelectOptionMenuProps {
|
||||
option: SelectOption;
|
||||
open: boolean;
|
||||
MenuProps?: Partial<MenuProps>;
|
||||
}
|
||||
|
||||
const Colors = [
|
||||
SelectOptionColorPB.Purple,
|
||||
SelectOptionColorPB.Pink,
|
||||
SelectOptionColorPB.LightPink,
|
||||
SelectOptionColorPB.Orange,
|
||||
SelectOptionColorPB.Yellow,
|
||||
SelectOptionColorPB.Lime,
|
||||
SelectOptionColorPB.Green,
|
||||
SelectOptionColorPB.Aqua,
|
||||
SelectOptionColorPB.Blue,
|
||||
];
|
||||
|
||||
export const SelectOptionMenu: FC<SelectOptionMenuProps> = ({
|
||||
open,
|
||||
option,
|
||||
MenuProps: menuProps,
|
||||
}) => {
|
||||
return (
|
||||
<Menu
|
||||
classes={{
|
||||
paper: 'w-52',
|
||||
}}
|
||||
anchorOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'center',
|
||||
horizontal: -32,
|
||||
}}
|
||||
{...menuProps}
|
||||
open={open}
|
||||
>
|
||||
<ListSubheader className="leading-tight">
|
||||
<OutlinedInput size="small" />
|
||||
</ListSubheader>
|
||||
<MenuItem>
|
||||
<DeleteSvg className="mr-2 text-base" />
|
||||
{t('grid.selectOption.deleteTag')}
|
||||
</MenuItem>
|
||||
<Divider />
|
||||
<MenuItem disabled>{t('grid.selectOption.colorPanelTitle')}</MenuItem>
|
||||
{Colors.map(color => (
|
||||
<MenuItem key={color} value={color}>
|
||||
<span className={`inline-flex w-4 h-4 mr-2 rounded-full ${SelectOptionColorMap[color]}`} />
|
||||
<span className="flex-1">
|
||||
{t(`grid.selectOption.${SelectOptionColorTextMap[color]}`)}
|
||||
</span>
|
||||
{option.color === color && (
|
||||
<SelectCheckSvg />
|
||||
)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
};
|
@ -1 +0,0 @@
|
||||
export * from './SelectCell';
|
@ -1,57 +1,45 @@
|
||||
import { FC, FormEventHandler, Suspense, lazy, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { useViewId } from '$app/hooks';
|
||||
import { cellService, Field, TextCell as TextCellType } from '../../application';
|
||||
import { FC, FormEventHandler, Suspense, lazy, useCallback, useEffect, useRef, useMemo } from 'react';
|
||||
import { Field, TextCell as TextCellType } from '../../application';
|
||||
import { CellText } from '../../_shared';
|
||||
import { useGridUIStateDispatcher, useGridUIStateSelector } from '$app/components/database/proxy/grid/ui_state/actions';
|
||||
import { useInputCell } from '$app/components/database/components/cell/Cell.hooks';
|
||||
|
||||
const ExpandButton = lazy(() => import('$app/components/database/components/cell/ExpandButton'));
|
||||
const EditTextCellInput = lazy(() => import('$app/components/database/components/cell/EditTextCellInput'));
|
||||
const EditTextCellInput = lazy(() => import('$app/components/database/components/field_types/text/EditTextCellInput'));
|
||||
|
||||
export const TextCell: FC<{
|
||||
field: Field;
|
||||
cell?: TextCellType;
|
||||
cell: TextCellType;
|
||||
documentId?: string;
|
||||
icon?: string;
|
||||
placeholder?: string;
|
||||
}> = ({ field, cell, documentId, icon, placeholder }) => {
|
||||
}> = ({ field, documentId, icon, placeholder, cell }) => {
|
||||
const isPrimary = field.isPrimary;
|
||||
const viewId = useViewId();
|
||||
const cellRef = useRef<HTMLDivElement>(null);
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [text, setText] = useState('');
|
||||
const [width, setWidth] = useState<number | undefined>(undefined);
|
||||
const { value, editing, updateCell, setEditing, setValue } = useInputCell(cell);
|
||||
|
||||
const { hoverRowId } = useGridUIStateSelector();
|
||||
const isHover = hoverRowId === cell?.rowId;
|
||||
const { setRowHover } = useGridUIStateDispatcher();
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const showExpandIcon = cell && !editing && isHover && isPrimary;
|
||||
const handleClose = () => {
|
||||
if (!cell) return;
|
||||
if (editing) {
|
||||
if (text !== cell.data) {
|
||||
void cellService.updateCell(viewId, cell.rowId, field.id, text);
|
||||
}
|
||||
|
||||
setEditing(false);
|
||||
}
|
||||
updateCell();
|
||||
};
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (!cell) return;
|
||||
setText(cell.data);
|
||||
setValue(cell.data);
|
||||
setEditing(true);
|
||||
}, [cell]);
|
||||
}, [cell, setEditing, setValue]);
|
||||
|
||||
const handleInput = useCallback<FormEventHandler<HTMLTextAreaElement>>((event) => {
|
||||
setText((event.target as HTMLTextAreaElement).value);
|
||||
}, []);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (cellRef.current) {
|
||||
setWidth(cellRef.current.clientWidth);
|
||||
}
|
||||
}, [editing]);
|
||||
const handleInput = useCallback<FormEventHandler<HTMLTextAreaElement>>(
|
||||
(event) => {
|
||||
setValue((event.target as HTMLTextAreaElement).value);
|
||||
},
|
||||
[setValue]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (editing) {
|
||||
@ -59,20 +47,27 @@ export const TextCell: FC<{
|
||||
}
|
||||
}, [editing, setRowHover]);
|
||||
|
||||
useEffect(() => {
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.focus();
|
||||
// set the cursor to the end of the text
|
||||
textareaRef.current.setSelectionRange(textareaRef.current.value.length, textareaRef.current.value.length);
|
||||
const content = useMemo(() => {
|
||||
if (cell && typeof cell.data === 'string' && cell.data) {
|
||||
return cell.data;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return <div className={'text-text-placeholder'}>{placeholder}</div>;
|
||||
}, [cell, placeholder]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CellText ref={cellRef} onClick={handleClick}>
|
||||
<div className='flex w-full items-center'>
|
||||
<div className={'relative h-full'}>
|
||||
<CellText
|
||||
style={{
|
||||
width: `${field.width}px`,
|
||||
minHeight: 37,
|
||||
}}
|
||||
ref={cellRef}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div className={`flex h-full w-full items-center whitespace-break-spaces break-all`}>
|
||||
{icon && <div className={'mr-2'}>{icon}</div>}
|
||||
{cell?.data || <div className={'text-text-placeholder'}>{placeholder}</div>}
|
||||
{content}
|
||||
</div>
|
||||
</CellText>
|
||||
<Suspense>
|
||||
@ -81,13 +76,13 @@ export const TextCell: FC<{
|
||||
<EditTextCellInput
|
||||
editing={editing}
|
||||
anchorEl={cellRef.current}
|
||||
width={width}
|
||||
width={field.width}
|
||||
onClose={handleClose}
|
||||
text={text}
|
||||
text={value}
|
||||
onInput={handleInput}
|
||||
/>
|
||||
)}
|
||||
</Suspense>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import { CreatedTimeField, LastEditedTimeField, TimeStampCell } from '$app/components/database/application';
|
||||
|
||||
interface Props {
|
||||
field: LastEditedTimeField | CreatedTimeField;
|
||||
cell: TimeStampCell;
|
||||
}
|
||||
|
||||
function TimestampCell({ cell }: Props) {
|
||||
return <div className={'flex h-full w-full items-center p-2 text-xs font-medium'}>{cell.data.dataTime}</div>;
|
||||
}
|
||||
|
||||
export default TimestampCell;
|
@ -0,0 +1,92 @@
|
||||
import React, { FormEventHandler, lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useInputCell } from '$app/components/database/components/cell/Cell.hooks';
|
||||
import { Field, UrlCell as URLCellType } from '$app/components/database/application';
|
||||
import { CellText } from '$app/components/database/_shared';
|
||||
|
||||
const EditTextCellInput = lazy(() => import('$app/components/database/components/field_types/text/EditTextCellInput'));
|
||||
|
||||
const pattern = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w.-]*)*\/?$/;
|
||||
|
||||
interface Props {
|
||||
field: Field;
|
||||
cell: URLCellType;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
function UrlCell({ field, cell, placeholder }: Props) {
|
||||
const [isUrl, setIsUrl] = useState(false);
|
||||
const cellRef = useRef<HTMLDivElement>(null);
|
||||
const { value, editing, updateCell, setEditing, setValue } = useInputCell(cell);
|
||||
const handleClick = useCallback(() => {
|
||||
setValue(cell.data.content || '');
|
||||
setEditing(true);
|
||||
}, [cell, setEditing, setValue]);
|
||||
|
||||
const handleClose = () => {
|
||||
updateCell();
|
||||
};
|
||||
|
||||
const handleInput = useCallback<FormEventHandler<HTMLTextAreaElement>>(
|
||||
(event) => {
|
||||
setValue((event.target as HTMLTextAreaElement).value);
|
||||
},
|
||||
[setValue]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (editing) return;
|
||||
const str = cell.data.content;
|
||||
|
||||
if (!str) return;
|
||||
const isUrl = pattern.test(str);
|
||||
|
||||
setIsUrl(isUrl);
|
||||
}, [cell, editing]);
|
||||
|
||||
const content = useMemo(() => {
|
||||
const str = cell.data.content;
|
||||
|
||||
if (str) {
|
||||
if (isUrl) {
|
||||
return (
|
||||
<a href={str} target={'_blank'} className={'cursor-pointer text-content-blue-400 underline'}>
|
||||
{str}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
return <div className={'text-sm text-text-placeholder'}>{placeholder}</div>;
|
||||
}, [isUrl, cell, placeholder]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CellText
|
||||
style={{
|
||||
width: `${field.width}px`,
|
||||
minHeight: 37,
|
||||
}}
|
||||
ref={cellRef}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<div className={`flex w-full items-center whitespace-break-spaces break-all `}>{content}</div>
|
||||
</CellText>
|
||||
<Suspense>
|
||||
{editing && (
|
||||
<EditTextCellInput
|
||||
editing={editing}
|
||||
anchorEl={cellRef.current}
|
||||
width={field.width}
|
||||
onClose={handleClose}
|
||||
text={value}
|
||||
onInput={handleInput}
|
||||
/>
|
||||
)}
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default UrlCell;
|
@ -31,4 +31,4 @@ function DatabaseSettings(props: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(DatabaseSettings);
|
||||
export default DatabaseSettings;
|
||||
|
@ -1,13 +1,13 @@
|
||||
import React, { useState } from 'react';
|
||||
import { TextButton } from '$app/components/database/components/tab_bar/TextButton';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDatabase } from '$app/components/database';
|
||||
import { useFiltersCount } from '$app/components/database';
|
||||
import FilterFieldsMenu from '$app/components/database/components/filter/FilterFieldsMenu';
|
||||
|
||||
function FilterSettings({ onToggleCollection }: { onToggleCollection: (forceOpen?: boolean) => void }) {
|
||||
const { t } = useTranslation();
|
||||
const { filters } = useDatabase();
|
||||
const highlight = filters && filters.length > 0;
|
||||
const filtersCount = useFiltersCount();
|
||||
const highlight = filtersCount > 0;
|
||||
|
||||
const [filterAnchorEl, setFilterAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const open = Boolean(filterAnchorEl);
|
||||
@ -31,6 +31,10 @@ function FilterSettings({ onToggleCollection }: { onToggleCollection: (forceOpen
|
||||
open={open}
|
||||
anchorEl={filterAnchorEl}
|
||||
onClose={() => setFilterAnchorEl(null)}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@ -19,10 +19,13 @@ function Properties({ onItemClick }: PropertiesProps) {
|
||||
<MenuItem
|
||||
disabled={field.isPrimary}
|
||||
onClick={() => onItemClick(field)}
|
||||
className={'flex w-full items-center justify-between'}
|
||||
className={'flex w-full items-center justify-between overflow-hidden px-1.5'}
|
||||
key={field.id}
|
||||
>
|
||||
<Field field={field} />
|
||||
<div className={'w-[100px] overflow-hidden text-ellipsis'}>
|
||||
<Field field={field} />
|
||||
</div>
|
||||
|
||||
<div className={'ml-2'}>{field.visibility !== FieldVisibility.AlwaysHidden ? <EyeOpen /> : <EyeClosed />}</div>
|
||||
</MenuItem>
|
||||
))}
|
||||
|
@ -39,6 +39,10 @@ function SortSettings({ onToggleCollection }: Props) {
|
||||
open={open}
|
||||
anchorEl={sortAnchorEl}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@ -4,7 +4,8 @@ import RecordDocument from '$app/components/database/components/edit_record/Reco
|
||||
import RecordHeader from '$app/components/database/components/edit_record/RecordHeader';
|
||||
import { Page } from '$app_reducers/pages/slice';
|
||||
import { PageController } from '$app/stores/effects/workspace/page/page_controller';
|
||||
import { ViewLayoutPB } from '@/services/backend';
|
||||
import { ErrorCode, ViewLayoutPB } from '@/services/backend';
|
||||
import { Log } from '$app/utils/log';
|
||||
|
||||
interface Props {
|
||||
cell: TextCell;
|
||||
@ -26,7 +27,7 @@ function EditRecord({ documentId: id, cell, icon }: Props) {
|
||||
// Record not found
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
if (e.code === 3) {
|
||||
if (e.code === ErrorCode.RecordNotFound) {
|
||||
try {
|
||||
const page = await controller.createOrphanPage({
|
||||
name: '',
|
||||
@ -35,7 +36,7 @@ function EditRecord({ documentId: id, cell, icon }: Props) {
|
||||
|
||||
setPage(page);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
Log.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,25 +1,28 @@
|
||||
import React, { MouseEvent, useCallback, useState } from 'react';
|
||||
import { Field, fieldService } from '$app/components/database/application';
|
||||
import React, { MouseEvent, useCallback, useMemo, useState } from 'react';
|
||||
import { fieldService } from '$app/components/database/application';
|
||||
import { FieldType } from '@/services/backend';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Button from '@mui/material/Button';
|
||||
import { ReactComponent as AddSvg } from '$app/assets/add.svg';
|
||||
import { useViewId } from '$app/hooks';
|
||||
import { FieldMenu } from '$app/components/database/components/field/FieldMenu';
|
||||
import { useDatabase } from '$app/components/database';
|
||||
|
||||
function NewProperty() {
|
||||
const viewId = useViewId();
|
||||
const { t } = useTranslation();
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
||||
const open = Boolean(anchorEl);
|
||||
const [updateField, setUpdateField] = useState<Field | null>(null);
|
||||
const [updateFieldId, setUpdateFieldId] = useState<string>('');
|
||||
const { fields } = useDatabase();
|
||||
const updateField = useMemo(() => fields.find((field) => field.id === updateFieldId), [fields, updateFieldId]);
|
||||
|
||||
const handleClick = useCallback(
|
||||
async (e: MouseEvent<HTMLButtonElement>) => {
|
||||
try {
|
||||
const field = await fieldService.createField(viewId, FieldType.RichText);
|
||||
|
||||
setUpdateField(field);
|
||||
setUpdateFieldId(field.id);
|
||||
setAnchorEl(e.target as HTMLButtonElement);
|
||||
} catch (e) {
|
||||
// toast.error(t('grid.field.newPropertyFail'));
|
||||
@ -39,7 +42,7 @@ function NewProperty() {
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={() => {
|
||||
setUpdateField(null);
|
||||
setUpdateFieldId('');
|
||||
setAnchorEl(null);
|
||||
}}
|
||||
/>
|
||||
|
@ -32,7 +32,7 @@ function Property({ field, rowId, ishovered, onHover, ...props }: Props, ref: Re
|
||||
onMouseLeave={() => {
|
||||
onHover(null);
|
||||
}}
|
||||
className={'relative flex gap-6 rounded hover:bg-content-blue-50'}
|
||||
className={'relative flex items-start gap-6 rounded hover:bg-content-blue-50'}
|
||||
key={field.id}
|
||||
{...props}
|
||||
>
|
||||
|
@ -21,7 +21,7 @@ function PropertyName({ field, openMenu, onOpenMenu, onCloseMenu }: Props) {
|
||||
e.preventDefault();
|
||||
onOpenMenu();
|
||||
}}
|
||||
className={'flex w-[200px] cursor-pointer items-center'}
|
||||
className={'flex min-h-[36px] w-[200px] cursor-pointer items-center'}
|
||||
onClick={onOpenMenu}
|
||||
>
|
||||
<Field field={field} />
|
||||
@ -31,4 +31,4 @@ function PropertyName({ field, openMenu, onOpenMenu, onCloseMenu }: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(PropertyName);
|
||||
export default PropertyName;
|
||||
|
@ -1,14 +1,31 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Cell } from '$app/components/database/components';
|
||||
import { Field } from '$app/components/database/application';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function PropertyValue(props: { rowId: string; field: Field }) {
|
||||
const { t } = useTranslation();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [width, setWidth] = useState(props.field.width);
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
|
||||
if (!el) return;
|
||||
const width = el.getBoundingClientRect().width;
|
||||
|
||||
setWidth(width);
|
||||
}, []);
|
||||
return (
|
||||
<div className={'flex h-9 flex-1 items-center'}>
|
||||
<Cell placeholder={t('grid.row.textPlaceholder')} {...props} />
|
||||
<div ref={ref} className={'flex min-h-[36px] flex-1 items-center'}>
|
||||
<Cell
|
||||
placeholder={t('grid.row.textPlaceholder')}
|
||||
{...props}
|
||||
field={{
|
||||
...props.field,
|
||||
width,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Field, fieldService, TextCell } from '$app/components/database/application';
|
||||
import { useDatabase } from '$app/components/database';
|
||||
import { FieldVisibility } from '@/services/backend';
|
||||
@ -11,6 +11,7 @@ import PropertyList from '$app/components/database/components/edit_record/record
|
||||
import NewProperty from '$app/components/database/components/edit_record/record_properties/NewProperty';
|
||||
import { useViewId } from '$app/hooks';
|
||||
import { DragDropContext, Droppable, DropResult, OnDragEndResponder } from 'react-beautiful-dnd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface Props {
|
||||
documentId?: string;
|
||||
@ -28,6 +29,7 @@ const reorder = (list: Field[], startIndex: number, endIndex: number) => {
|
||||
};
|
||||
|
||||
function RecordProperties({ documentId, cell }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const viewId = useViewId();
|
||||
const { fieldId, rowId } = cell;
|
||||
const { fields } = useDatabase();
|
||||
@ -41,8 +43,18 @@ function RecordProperties({ documentId, cell }: Props) {
|
||||
});
|
||||
}, [fieldId, fields, showHiddenFields]);
|
||||
|
||||
const hiddenFieldsCount = useMemo(() => {
|
||||
return fields.filter((field) => {
|
||||
return field.visibility === FieldVisibility.AlwaysHidden;
|
||||
}).length;
|
||||
}, [fields]);
|
||||
|
||||
const [state, setState] = useState<Field[]>(properties);
|
||||
|
||||
useEffect(() => {
|
||||
setState(properties);
|
||||
}, [properties]);
|
||||
|
||||
// move the field in the database
|
||||
const onMoveProperty = useCallback(
|
||||
async (fieldId: string, prevId?: string) => {
|
||||
@ -103,19 +115,31 @@ function RecordProperties({ documentId, cell }: Props) {
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowHiddenFields((prev) => !prev);
|
||||
}}
|
||||
className={'w-full justify-start'}
|
||||
startIcon={showHiddenFields ? <EyeClosedSvg /> : <EyeOpenSvg />}
|
||||
color={'inherit'}
|
||||
>
|
||||
{showHiddenFields ? 'Hide hidden fields' : 'Show hidden fields'}
|
||||
</Button>
|
||||
{
|
||||
// show the button only if there are hidden fields
|
||||
hiddenFieldsCount > 0 && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowHiddenFields((prev) => !prev);
|
||||
}}
|
||||
className={'w-full justify-start'}
|
||||
startIcon={showHiddenFields ? <EyeClosedSvg /> : <EyeOpenSvg />}
|
||||
color={'inherit'}
|
||||
>
|
||||
{showHiddenFields
|
||||
? t('grid.rowPage.hideHiddenFields', {
|
||||
count: hiddenFieldsCount,
|
||||
})
|
||||
: t('grid.rowPage.showHiddenFields', {
|
||||
count: hiddenFieldsCount,
|
||||
})}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
<NewProperty />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(RecordProperties);
|
||||
export default RecordProperties;
|
||||
|
@ -6,15 +6,11 @@ export interface FieldProps {
|
||||
field: FieldType;
|
||||
}
|
||||
|
||||
export const Field: FC<FieldProps> = ({
|
||||
field,
|
||||
}) => {
|
||||
export const Field: FC<FieldProps> = ({ field }) => {
|
||||
return (
|
||||
<div className="flex items-center px-2 w-full">
|
||||
<FieldTypeSvg className="text-base mr-1" type={field.type} />
|
||||
<span className="flex-1 text-left text-xs truncate">
|
||||
{field.name}
|
||||
</span>
|
||||
<div className='flex w-full items-center px-2'>
|
||||
<FieldTypeSvg className='mr-1 text-base' type={field.type} />
|
||||
<span className='flex-1 truncate text-left text-xs'>{field.name}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { Input, MenuItem } from '@mui/material';
|
||||
import { OutlinedInput, MenuItem, MenuList } from '@mui/material';
|
||||
import { Field } from '$app/components/database/components/field/Field';
|
||||
import { Field as FieldType } from '../../application';
|
||||
import { useDatabase } from '$app/components/database';
|
||||
@ -26,34 +26,37 @@ function FieldList({ showSearch, onItemClick, searchPlaceholder }: FieldListProp
|
||||
|
||||
const searchInput = useMemo(() => {
|
||||
return showSearch ? (
|
||||
<div className={'w-full px-8 py-2'}>
|
||||
<Input placeholder={searchPlaceholder} onChange={onInputChange} />
|
||||
<div className={'w-[220px] px-4 pt-2'}>
|
||||
<OutlinedInput size={'small'} autoFocus={true} placeholder={searchPlaceholder} onChange={onInputChange} />
|
||||
</div>
|
||||
) : null;
|
||||
}, [onInputChange, searchPlaceholder, showSearch]);
|
||||
|
||||
const emptyList = useMemo(() => {
|
||||
return fieldsResult.length === 0 ? (
|
||||
<div className={'px-8 py-4 text-center text-gray-500'}>No fields found</div>
|
||||
<div className={'px-4 pt-3 text-center text-sm font-medium text-gray-500'}>No fields found</div>
|
||||
) : null;
|
||||
}, [fieldsResult]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'pt-2'}>
|
||||
{searchInput}
|
||||
{emptyList}
|
||||
{fieldsResult.map((field) => (
|
||||
<MenuItem
|
||||
key={field.id}
|
||||
value={field.id}
|
||||
onClick={(event) => {
|
||||
onItemClick?.(event, field);
|
||||
}}
|
||||
>
|
||||
<Field field={field} />
|
||||
</MenuItem>
|
||||
))}
|
||||
</>
|
||||
<MenuList className={'max-h-[300px] overflow-y-auto overflow-x-hidden'}>
|
||||
{fieldsResult.map((field) => (
|
||||
<MenuItem
|
||||
className={'overflow-hidden text-ellipsis px-1'}
|
||||
key={field.id}
|
||||
value={field.id}
|
||||
onClick={(event) => {
|
||||
onItemClick?.(event, field);
|
||||
}}
|
||||
>
|
||||
<Field field={field} />
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,14 @@
|
||||
import { Divider, Menu, MenuItem, MenuProps, OutlinedInput } from '@mui/material';
|
||||
import { Divider, MenuList, MenuProps } from '@mui/material';
|
||||
import { ChangeEventHandler, FC, useCallback, useState } from 'react';
|
||||
import { ReactComponent as MoreSvg } from '$app/assets/more.svg';
|
||||
import { useViewId } from '$app/hooks';
|
||||
import { Field, fieldService } from '../../application';
|
||||
import { FieldMenuActions } from './FieldMenuActions';
|
||||
import { FieldTypeText, FieldTypeSvg } from '$app/components/database/components/field/index';
|
||||
import FieldTypeMenuExtension from '$app/components/database/components/field/FieldTypeMenuExtension';
|
||||
import FieldTypeSelect from '$app/components/database/components/field/FieldTypeSelect';
|
||||
import { FieldType } from '@/services/backend';
|
||||
import { Log } from '$app/utils/log';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import Popover from '@mui/material/Popover';
|
||||
|
||||
export interface GridFieldMenuProps {
|
||||
field: Field;
|
||||
@ -29,44 +33,60 @@ export const FieldMenu: FC<GridFieldMenuProps> = ({ field, anchorEl, open, onClo
|
||||
});
|
||||
} catch (e) {
|
||||
// TODO
|
||||
console.error(`change field ${field.id} name from '${field.name}' to ${inputtingName} fail`, e);
|
||||
Log.error(`change field ${field.id} name from '${field.name}' to ${inputtingName} fail`, e);
|
||||
}
|
||||
}
|
||||
}, [viewId, field, inputtingName]);
|
||||
|
||||
const fieldNameInput = (
|
||||
<OutlinedInput
|
||||
className='mx-3 mb-5 mt-1 !rounded-[10px]'
|
||||
size='small'
|
||||
value={inputtingName}
|
||||
onChange={handleInput}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
);
|
||||
|
||||
const fieldTypeSelect = (
|
||||
<MenuItem dense>
|
||||
<FieldTypeSvg type={field.type} className='mr-2 text-base' />
|
||||
<span className='flex-1 text-xs font-medium'>
|
||||
<FieldTypeText type={field.type} />
|
||||
</span>
|
||||
<MoreSvg className='text-base' />
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
const isPrimary = field.isPrimary;
|
||||
|
||||
return (
|
||||
<Menu keepMounted={false} anchorEl={anchorEl} open={open} onClose={onClose}>
|
||||
{fieldNameInput}
|
||||
{!isPrimary && (
|
||||
<div>
|
||||
{fieldTypeSelect}
|
||||
<Divider />
|
||||
</div>
|
||||
)}
|
||||
const onUpdateFieldType = useCallback(
|
||||
async (type: FieldType) => {
|
||||
try {
|
||||
await fieldService.updateFieldType(viewId, field.id, type);
|
||||
} catch (e) {
|
||||
// TODO
|
||||
Log.error(`change field ${field.id} type from '${field.type}' to ${type} fail`, e);
|
||||
}
|
||||
},
|
||||
[viewId, field]
|
||||
);
|
||||
|
||||
<FieldMenuActions isPrimary={isPrimary} onMenuItemClick={() => onClose()} fieldId={field.id} />
|
||||
</Menu>
|
||||
return (
|
||||
<Popover
|
||||
transformOrigin={{
|
||||
vertical: -4,
|
||||
horizontal: 'left',
|
||||
}}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
keepMounted={false}
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
>
|
||||
<TextField
|
||||
className='mx-3 mt-3 rounded-[10px]'
|
||||
size='small'
|
||||
autoFocus={true}
|
||||
value={inputtingName}
|
||||
onChange={handleInput}
|
||||
onBlur={handleBlur}
|
||||
/>
|
||||
<MenuList>
|
||||
<div>
|
||||
{!isPrimary && (
|
||||
<>
|
||||
<FieldTypeSelect field={field} onUpdateFieldType={onUpdateFieldType} />
|
||||
<Divider className={'my-2'} />
|
||||
</>
|
||||
)}
|
||||
<FieldTypeMenuExtension field={field} />
|
||||
<FieldMenuActions isPrimary={isPrimary} onMenuItemClick={() => onClose()} fieldId={field.id} />
|
||||
</div>
|
||||
</MenuList>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
@ -30,9 +30,12 @@ export const FieldSelect: FC<FieldSelectProps> = ({ onChange, ...props }) => {
|
||||
alignItems: 'center',
|
||||
},
|
||||
}}
|
||||
MenuProps={{
|
||||
className: 'max-w-[150px]',
|
||||
}}
|
||||
>
|
||||
{fields.map((field) => (
|
||||
<MenuItem key={field.id} value={field.id}>
|
||||
<MenuItem className={'overflow-hidden text-ellipsis px-1.5'} key={field.id} value={field.id}>
|
||||
<Field field={field} />
|
||||
</MenuItem>
|
||||
))}
|
||||
|
@ -2,6 +2,8 @@ import { Divider, Menu, MenuItem, MenuProps } from '@mui/material';
|
||||
import { FC, useMemo } from 'react';
|
||||
import { FieldType } from '@/services/backend';
|
||||
import { FieldTypeText, FieldTypeSvg } from '$app/components/database/components/field/index';
|
||||
import { Field } from '$app/components/database/application';
|
||||
import { ReactComponent as SelectCheckSvg } from '$app/assets/database/select-check.svg';
|
||||
|
||||
const FieldTypeGroup = [
|
||||
{
|
||||
@ -14,15 +16,21 @@ const FieldTypeGroup = [
|
||||
FieldType.DateTime,
|
||||
FieldType.Checkbox,
|
||||
FieldType.Checklist,
|
||||
FieldType.URL,
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Advanced',
|
||||
types: [FieldType.LastEditedTime],
|
||||
types: [FieldType.LastEditedTime, FieldType.CreatedTime],
|
||||
},
|
||||
];
|
||||
|
||||
export const FieldTypeMenu: FC<MenuProps> = (props) => {
|
||||
export const FieldTypeMenu: FC<
|
||||
MenuProps & {
|
||||
field: Field;
|
||||
onClickItem?: (type: FieldType) => void;
|
||||
}
|
||||
> = ({ field, onClickItem, ...props }) => {
|
||||
const PopoverClasses = useMemo(
|
||||
() => ({
|
||||
...props.PopoverClasses,
|
||||
@ -38,11 +46,12 @@ export const FieldTypeMenu: FC<MenuProps> = (props) => {
|
||||
{group.name}
|
||||
</MenuItem>,
|
||||
group.types.map((type) => (
|
||||
<MenuItem key={type} dense>
|
||||
<MenuItem onClick={() => onClickItem?.(type)} key={type} dense className={'flex justify-between'}>
|
||||
<FieldTypeSvg className='mr-2 text-base' type={type} />
|
||||
<span className='font-medium'>
|
||||
<span className='flex-1 font-medium'>
|
||||
<FieldTypeText type={type} />
|
||||
</span>
|
||||
{type === field.type && <SelectCheckSvg />}
|
||||
</MenuItem>
|
||||
)),
|
||||
index < FieldTypeGroup.length - 1 && <Divider key={`Divider-${group.name}`} />,
|
||||
|
@ -0,0 +1,26 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { FieldType } from '@/services/backend';
|
||||
import { DateTimeField, Field, NumberField, SelectField } from '$app/components/database/application';
|
||||
import SelectFieldActions from '$app/components/database/components/field_types/select/select_field_actions/SelectFieldActions';
|
||||
import NumberFieldActions from '$app/components/database/components/field_types/number/NumberFieldActions';
|
||||
import DateTimeFieldActions from '$app/components/database/components/field_types/date/DateTimeFieldActions';
|
||||
|
||||
function FieldTypeMenuExtension({ field }: { field: Field }) {
|
||||
return useMemo(() => {
|
||||
switch (field.type) {
|
||||
case FieldType.SingleSelect:
|
||||
case FieldType.MultiSelect:
|
||||
return <SelectFieldActions field={field as SelectField} />;
|
||||
case FieldType.Number:
|
||||
return <NumberFieldActions field={field as NumberField} />;
|
||||
case FieldType.DateTime:
|
||||
case FieldType.CreatedTime:
|
||||
case FieldType.LastEditedTime:
|
||||
return <DateTimeFieldActions field={field as DateTimeField} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}, [field]);
|
||||
}
|
||||
|
||||
export default FieldTypeMenuExtension;
|
@ -0,0 +1,57 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { FieldTypeSvg } from '$app/components/database/components/field/FieldTypeSvg';
|
||||
import { MenuItem } from '@mui/material';
|
||||
import { Field } from '$app/components/database/application';
|
||||
import { ReactComponent as MoreSvg } from '$app/assets/more.svg';
|
||||
import { FieldTypeMenu } from '$app/components/database/components/field/FieldTypeMenu';
|
||||
import { FieldType } from '@/services/backend';
|
||||
import { FieldTypeText } from '$app/components/database/components/field/FieldTypeText';
|
||||
|
||||
interface Props {
|
||||
field: Field;
|
||||
onUpdateFieldType: (type: FieldType) => void;
|
||||
}
|
||||
function FieldTypeSelect({ field, onUpdateFieldType }: Props) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const ref = useRef<HTMLLIElement>(null);
|
||||
|
||||
return (
|
||||
<div className={'px-1'}>
|
||||
<MenuItem
|
||||
ref={ref}
|
||||
onClick={() => {
|
||||
setExpanded(!expanded);
|
||||
}}
|
||||
className={'px-23 mx-0'}
|
||||
>
|
||||
<FieldTypeSvg type={field.type} className='mr-2 text-base' />
|
||||
<span className='flex-1 text-xs font-medium'>
|
||||
<FieldTypeText type={field.type} />
|
||||
</span>
|
||||
<MoreSvg className={`transform text-base ${expanded ? '' : 'rotate-90'}`} />
|
||||
</MenuItem>
|
||||
{expanded && (
|
||||
<FieldTypeMenu
|
||||
keepMounted={false}
|
||||
field={field}
|
||||
onClickItem={onUpdateFieldType}
|
||||
open={expanded}
|
||||
anchorEl={ref.current}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
anchorOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
onClose={() => {
|
||||
setExpanded(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FieldTypeSelect;
|
@ -1,39 +1,26 @@
|
||||
import { FieldType } from '@/services/backend';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCallback } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const FieldTypeText = ({ type }: { type: FieldType }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const getText = useCallback(
|
||||
(type: FieldType) => {
|
||||
switch (type) {
|
||||
case FieldType.RichText:
|
||||
return t('grid.field.textFieldName');
|
||||
case FieldType.Number:
|
||||
return t('grid.field.numberFieldName');
|
||||
case FieldType.DateTime:
|
||||
return t('grid.field.dateFieldName');
|
||||
case FieldType.SingleSelect:
|
||||
return t('grid.field.singleSelectFieldName');
|
||||
case FieldType.MultiSelect:
|
||||
return t('grid.field.multiSelectFieldName');
|
||||
case FieldType.Checkbox:
|
||||
return t('grid.field.checkboxFieldName');
|
||||
case FieldType.URL:
|
||||
return t('grid.field.urlFieldName');
|
||||
case FieldType.Checklist:
|
||||
return t('grid.field.checklistFieldName');
|
||||
case FieldType.LastEditedTime:
|
||||
return t('grid.field.updatedAtFieldName');
|
||||
case FieldType.CreatedTime:
|
||||
return t('grid.field.createdAtFieldName');
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
},
|
||||
[t]
|
||||
);
|
||||
const text = useMemo(() => {
|
||||
const map = {
|
||||
[FieldType.RichText]: t('grid.field.textFieldName'),
|
||||
[FieldType.Number]: t('grid.field.numberFieldName'),
|
||||
[FieldType.DateTime]: t('grid.field.dateFieldName'),
|
||||
[FieldType.SingleSelect]: t('grid.field.singleSelectFieldName'),
|
||||
[FieldType.MultiSelect]: t('grid.field.multiSelectFieldName'),
|
||||
[FieldType.Checkbox]: t('grid.field.checkboxFieldName'),
|
||||
[FieldType.URL]: t('grid.field.urlFieldName'),
|
||||
[FieldType.Checklist]: t('grid.field.checklistFieldName'),
|
||||
[FieldType.LastEditedTime]: t('grid.field.updatedAtFieldName'),
|
||||
[FieldType.CreatedTime]: t('grid.field.createdAtFieldName'),
|
||||
};
|
||||
|
||||
return <>{getText(type)}</>;
|
||||
return map[type] || 'unknown';
|
||||
}, [t, type]);
|
||||
|
||||
return <div>{text}</div>;
|
||||
};
|
||||
|
@ -0,0 +1,41 @@
|
||||
import React, { useState } from 'react';
|
||||
import { updateChecklistCell } from '$app/components/database/application/cell/cell_service';
|
||||
import { useViewId } from '$app/hooks';
|
||||
import { ReactComponent as AddIcon } from '$app/assets/add.svg';
|
||||
import { IconButton } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function AddNewOption({ rowId, fieldId }: { rowId: string; fieldId: string }) {
|
||||
const { t } = useTranslation();
|
||||
const [value, setValue] = useState('');
|
||||
const viewId = useViewId();
|
||||
const createOption = async () => {
|
||||
await updateChecklistCell(viewId, rowId, fieldId, {
|
||||
insertOptions: [value],
|
||||
});
|
||||
setValue('');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={'flex items-center justify-between p-2 px-4 text-sm'}>
|
||||
<input
|
||||
placeholder={t('grid.checklist.addNew')}
|
||||
className={'flex-1'}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
void createOption();
|
||||
}
|
||||
}}
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
setValue(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<IconButton size={'small'} disabled={!value} onClick={createOption}>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddNewOption;
|
@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import Popover, { PopoverProps } from '@mui/material/Popover';
|
||||
import { LinearProgressWithLabel } from '$app/components/database/components/field_types/checklist/LinearProgressWithLabel';
|
||||
import { Divider } from '@mui/material';
|
||||
import { ChecklistCell as ChecklistCellType } from '$app/components/database/application';
|
||||
import ChecklistItem from '$app/components/database/components/field_types/checklist/ChecklistItem';
|
||||
import AddNewOption from '$app/components/database/components/field_types/checklist/AddNewOption';
|
||||
|
||||
function ChecklistCellActions({
|
||||
cell,
|
||||
...props
|
||||
}: PopoverProps & {
|
||||
cell: ChecklistCellType;
|
||||
}) {
|
||||
const { fieldId, rowId } = cell;
|
||||
const { percentage, selectedOptions = [], options } = cell.data;
|
||||
|
||||
return (
|
||||
<Popover {...props}>
|
||||
<LinearProgressWithLabel className={'m-4'} value={percentage || 0} />
|
||||
<div className={'p-1'}>
|
||||
{options?.map((option) => {
|
||||
return (
|
||||
<ChecklistItem
|
||||
fieldId={fieldId}
|
||||
rowId={rowId}
|
||||
key={option.id}
|
||||
option={option}
|
||||
checked={selectedOptions?.includes(option.id) || false}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
<AddNewOption fieldId={fieldId} rowId={rowId} />
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChecklistCellActions;
|
@ -0,0 +1,80 @@
|
||||
import React, { useState } from 'react';
|
||||
import { SelectOption } from '$app/components/database/application';
|
||||
import { Checkbox, IconButton } from '@mui/material';
|
||||
import { updateChecklistCell } from '$app/components/database/application/cell/cell_service';
|
||||
import { useViewId } from '$app/hooks';
|
||||
import { ReactComponent as DeleteIcon } from '$app/assets/delete.svg';
|
||||
import { ReactComponent as CheckboxCheckSvg } from '$app/assets/database/checkbox-check.svg';
|
||||
import { ReactComponent as CheckboxUncheckSvg } from '$app/assets/database/checkbox-uncheck.svg';
|
||||
|
||||
function ChecklistItem({
|
||||
checked,
|
||||
option,
|
||||
rowId,
|
||||
fieldId,
|
||||
}: {
|
||||
checked: boolean;
|
||||
option: SelectOption;
|
||||
rowId: string;
|
||||
fieldId: string;
|
||||
}) {
|
||||
const [hover, setHover] = useState(false);
|
||||
const [value, setValue] = useState(option.name);
|
||||
const viewId = useViewId();
|
||||
const updateText = async () => {
|
||||
await updateChecklistCell(viewId, rowId, fieldId, {
|
||||
updateOptions: [
|
||||
{
|
||||
...option,
|
||||
name: value,
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const onCheckedChange = async () => {
|
||||
void updateChecklistCell(viewId, rowId, fieldId, {
|
||||
selectedOptionIds: [option.id],
|
||||
});
|
||||
};
|
||||
|
||||
const deleteOption = async () => {
|
||||
await updateChecklistCell(viewId, rowId, fieldId, {
|
||||
deleteOptionIds: [option.id],
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={() => {
|
||||
setHover(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHover(false);
|
||||
}}
|
||||
className={`flex items-center justify-between gap-2 rounded p-1 text-sm ${hover ? 'bg-fill-list-hover' : ''}`}
|
||||
>
|
||||
<Checkbox
|
||||
onClick={onCheckedChange}
|
||||
checked={checked}
|
||||
disableRipple
|
||||
style={{ padding: 4 }}
|
||||
icon={<CheckboxUncheckSvg />}
|
||||
checkedIcon={<CheckboxCheckSvg />}
|
||||
/>
|
||||
<input
|
||||
className={'flex-1'}
|
||||
onBlur={updateText}
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
setValue(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<IconButton size={'small'} className={`mx-2 ${hover ? 'visible' : 'invisible'}`} onClick={deleteOption}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ChecklistItem;
|
@ -0,0 +1,17 @@
|
||||
import * as React from 'react';
|
||||
import LinearProgress, { LinearProgressProps } from '@mui/material/LinearProgress';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
export function LinearProgressWithLabel(props: LinearProgressProps & { value: number }) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Box sx={{ width: '100%', mr: 1 }}>
|
||||
<LinearProgress variant='determinate' {...props} value={props.value * 100} />
|
||||
</Box>
|
||||
<Box sx={{ minWidth: 35 }}>
|
||||
<Typography variant='body2' color='text.secondary'>{`${Math.round(props.value * 100)}%`}</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import DatePicker, { ReactDatePickerCustomHeaderProps } from 'react-datepicker';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
import dayjs from 'dayjs';
|
||||
import { ReactComponent as LeftSvg } from '$app/assets/arrow-left.svg';
|
||||
import { ReactComponent as RightSvg } from '$app/assets/arrow-right.svg';
|
||||
import { IconButton } from '@mui/material';
|
||||
|
||||
function CustomCalendar({
|
||||
handleChange,
|
||||
isRange,
|
||||
timestamp,
|
||||
endTimestamp,
|
||||
}: {
|
||||
handleChange: (params: { date?: number; endDate?: number }) => void;
|
||||
isRange: boolean;
|
||||
timestamp: number;
|
||||
endTimestamp: number;
|
||||
}) {
|
||||
const [startDate, setStartDate] = useState<Date | null>(new Date(timestamp * 1000));
|
||||
const [endDate, setEndDate] = useState<Date | null>(new Date(endTimestamp * 1000));
|
||||
|
||||
useEffect(() => {
|
||||
if (!isRange) return;
|
||||
setEndDate(new Date(endTimestamp * 1000));
|
||||
}, [isRange, endTimestamp]);
|
||||
|
||||
useEffect(() => {
|
||||
setStartDate(new Date(timestamp * 1000));
|
||||
}, [timestamp]);
|
||||
|
||||
return (
|
||||
<div className={'flex w-full items-center justify-center'}>
|
||||
<DatePicker
|
||||
calendarClassName={
|
||||
'appflowy-date-picker-calendar bg-bg-body h-full border-none rounded-none flex w-full items-center justify-center'
|
||||
}
|
||||
renderCustomHeader={(props: ReactDatePickerCustomHeaderProps) => {
|
||||
return (
|
||||
<div className={'flex w-full justify-between pb-1.5 pt-0'}>
|
||||
<div className={'flex-1 px-4 text-left text-sm font-medium text-text-title'}>
|
||||
{dayjs(props.date).format('MMMM YYYY')}
|
||||
</div>
|
||||
|
||||
<div className={'flex items-center gap-[10px] pr-2'}>
|
||||
<IconButton size={'small'} onClick={props.decreaseMonth}>
|
||||
<LeftSvg />
|
||||
</IconButton>
|
||||
<IconButton size={'small'} onClick={props.increaseMonth}>
|
||||
<RightSvg />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
selected={startDate}
|
||||
onChange={(dates) => {
|
||||
if (!dates) return;
|
||||
if (isRange) {
|
||||
const [start, end] = dates as [Date | null, Date | null];
|
||||
|
||||
setStartDate(start);
|
||||
setEndDate(end);
|
||||
if (!start || !end) return;
|
||||
handleChange({
|
||||
date: start.getTime() / 1000,
|
||||
endDate: end.getTime() / 1000,
|
||||
});
|
||||
} else {
|
||||
const date = dates as Date;
|
||||
|
||||
setStartDate(date);
|
||||
handleChange({
|
||||
date: date.getTime() / 1000,
|
||||
});
|
||||
}
|
||||
}}
|
||||
startDate={isRange ? startDate : null}
|
||||
endDate={isRange ? endDate : null}
|
||||
selectsRange={isRange}
|
||||
inline
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CustomCalendar;
|
@ -0,0 +1,72 @@
|
||||
import React, { useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { MenuItem, Menu } from '@mui/material';
|
||||
import { ReactComponent as MoreSvg } from '$app/assets/more.svg';
|
||||
import { ReactComponent as SelectCheckSvg } from '$app/assets/database/select-check.svg';
|
||||
|
||||
import { DateFormatPB } from '@/services/backend';
|
||||
|
||||
interface Props {
|
||||
value: DateFormatPB;
|
||||
onChange: (value: DateFormatPB) => void;
|
||||
}
|
||||
|
||||
function DateFormat({ value, onChange }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef<HTMLLIElement>(null);
|
||||
const dateFormatMap = useMemo(
|
||||
() => ({
|
||||
[DateFormatPB.Friendly]: t('grid.field.dateFormatFriendly'),
|
||||
[DateFormatPB.ISO]: t('grid.field.dateFormatISO'),
|
||||
[DateFormatPB.US]: t('grid.field.dateFormatUS'),
|
||||
[DateFormatPB.Local]: t('grid.field.dateFormatLocal'),
|
||||
[DateFormatPB.DayMonthYear]: t('grid.field.dateFormatDayMonthYear'),
|
||||
}),
|
||||
[t]
|
||||
);
|
||||
|
||||
const handleClick = (option: DateFormatPB) => {
|
||||
onChange(option);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuItem
|
||||
className={'mx-0 flex w-full justify-between text-xs font-medium'}
|
||||
ref={ref}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
{t('grid.field.dateFormat')}
|
||||
<MoreSvg className={`transform text-base ${open ? '' : 'rotate-90'}`} />
|
||||
</MenuItem>
|
||||
<Menu
|
||||
anchorOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
open={open}
|
||||
anchorEl={ref.current}
|
||||
onClose={() => setOpen(false)}
|
||||
>
|
||||
{Object.keys(dateFormatMap).map((option) => {
|
||||
const optionValue = Number(option) as DateFormatPB;
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
className={'min-w-[180px] justify-between'}
|
||||
key={optionValue}
|
||||
onClick={() => handleClick(optionValue)}
|
||||
>
|
||||
{dateFormatMap[optionValue]}
|
||||
{value === optionValue && <SelectCheckSvg />}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default DateFormat;
|
@ -0,0 +1,153 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import Popover, { PopoverProps } from '@mui/material/Popover';
|
||||
import { DateTimeCell, DateTimeField, DateTimeTypeOption } from '$app/components/database/application';
|
||||
import { useViewId } from '$app/hooks';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { updateDateCell } from '$app/components/database/application/cell/cell_service';
|
||||
import { Divider, MenuItem, MenuList } from '@mui/material';
|
||||
import dayjs from 'dayjs';
|
||||
import RangeSwitch from '$app/components/database/components/field_types/date/RangeSwitch';
|
||||
import CustomCalendar from '$app/components/database/components/field_types/date/CustomCalendar';
|
||||
import IncludeTimeSwitch from '$app/components/database/components/field_types/date/IncludeTimeSwitch';
|
||||
import DateTimeFormatSelect from '$app/components/database/components/field_types/date/DateTimeFormatSelect';
|
||||
import DateTimeSet from '$app/components/database/components/field_types/date/DateTimeSet';
|
||||
import { useTypeOption } from '$app/components/database';
|
||||
import { getDateFormat, getTimeFormat } from '$app/components/database/components/field_types/date/utils';
|
||||
|
||||
function DateTimeCellActions({
|
||||
cell,
|
||||
field,
|
||||
...props
|
||||
}: PopoverProps & {
|
||||
field: DateTimeField;
|
||||
cell: DateTimeCell;
|
||||
}) {
|
||||
const typeOption = useTypeOption<DateTimeTypeOption>(field.id);
|
||||
|
||||
const timeFormat = useMemo(() => {
|
||||
return getTimeFormat(typeOption.timeFormat);
|
||||
}, [typeOption.timeFormat]);
|
||||
|
||||
const dateFormat = useMemo(() => {
|
||||
return getDateFormat(typeOption.dateFormat);
|
||||
}, [typeOption.dateFormat]);
|
||||
|
||||
const { includeTime } = cell.data;
|
||||
|
||||
const timestamp = useMemo(() => cell.data.timestamp || dayjs().unix(), [cell.data.timestamp]);
|
||||
const endTimestamp = useMemo(() => cell.data.endTimestamp || dayjs().unix(), [cell.data.endTimestamp]);
|
||||
const time = useMemo(() => cell.data.time || dayjs().format(timeFormat), [cell.data.time, timeFormat]);
|
||||
const endTime = useMemo(() => cell.data.endTime || dayjs().format(timeFormat), [cell.data.endTime, timeFormat]);
|
||||
|
||||
const viewId = useViewId();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleChange = useCallback(
|
||||
async (params: {
|
||||
includeTime?: boolean;
|
||||
date?: number;
|
||||
endDate?: number;
|
||||
time?: string;
|
||||
endTime?: string;
|
||||
isRange?: boolean;
|
||||
clearFlag?: boolean;
|
||||
}) => {
|
||||
try {
|
||||
const isRange = params.isRange ?? cell.data.isRange;
|
||||
|
||||
await updateDateCell(viewId, cell.rowId, cell.fieldId, {
|
||||
date: params.date ?? timestamp,
|
||||
endDate: isRange ? params.endDate ?? endTimestamp : undefined,
|
||||
time: params.time ?? time,
|
||||
endTime: isRange ? params.endTime ?? endTime : undefined,
|
||||
includeTime: params.includeTime ?? includeTime,
|
||||
isRange,
|
||||
clearFlag: params.clearFlag,
|
||||
});
|
||||
} catch (e) {
|
||||
// toast.error(e.message);
|
||||
}
|
||||
},
|
||||
[cell, endTime, endTimestamp, includeTime, time, timestamp, viewId]
|
||||
);
|
||||
|
||||
const isRange = cell.data.isRange || false;
|
||||
|
||||
return (
|
||||
<Popover
|
||||
keepMounted={false}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
{...props}
|
||||
PaperProps={{
|
||||
className: 'pt-4 transform transition-all',
|
||||
}}
|
||||
>
|
||||
<DateTimeSet
|
||||
date={timestamp}
|
||||
endTime={endTime}
|
||||
endDate={endTimestamp}
|
||||
dateFormat={dateFormat}
|
||||
time={time}
|
||||
timeFormat={timeFormat}
|
||||
onChange={handleChange}
|
||||
isRange={isRange}
|
||||
includeTime={includeTime}
|
||||
/>
|
||||
|
||||
<CustomCalendar isRange={isRange} timestamp={timestamp} endTimestamp={endTimestamp} handleChange={handleChange} />
|
||||
|
||||
<Divider className={'my-0'} />
|
||||
<div className={'flex flex-col gap-1 px-4 py-2'}>
|
||||
<RangeSwitch
|
||||
onIsRangeChange={(val) => {
|
||||
void handleChange({
|
||||
isRange: val,
|
||||
// reset endTime when isRange is changed
|
||||
endTime: time,
|
||||
endDate: timestamp,
|
||||
});
|
||||
}}
|
||||
checked={isRange}
|
||||
/>
|
||||
<IncludeTimeSwitch
|
||||
onIncludeTimeChange={(val) => {
|
||||
void handleChange({
|
||||
includeTime: val,
|
||||
// reset time when includeTime is changed
|
||||
time: val ? dayjs().format(timeFormat) : undefined,
|
||||
endTime: val && isRange ? dayjs().format(timeFormat) : undefined,
|
||||
});
|
||||
}}
|
||||
checked={includeTime}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Divider className={'my-0'} />
|
||||
|
||||
<MenuList>
|
||||
<DateTimeFormatSelect field={field} />
|
||||
<MenuItem
|
||||
className={'text-xs font-medium'}
|
||||
onClick={async () => {
|
||||
await handleChange({
|
||||
clearFlag: true,
|
||||
});
|
||||
|
||||
props.onClose?.({}, 'backdropClick');
|
||||
}}
|
||||
>
|
||||
{t('grid.field.clearDate')}
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
export default DateTimeCellActions;
|
@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import { UndeterminedDateField } from '$app/components/database/application';
|
||||
import DateTimeFormat from '$app/components/database/components/field_types/date/DateTimeFormat';
|
||||
import { Divider } from '@mui/material';
|
||||
|
||||
function DateTimeFieldActions({ field }: { field: UndeterminedDateField }) {
|
||||
return (
|
||||
<>
|
||||
<div className={'px-1'}>
|
||||
<DateTimeFormat field={field} />
|
||||
</div>
|
||||
<Divider className={'my-2'} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default DateTimeFieldActions;
|
@ -0,0 +1,75 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import DateFormat from '$app/components/database/components/field_types/date/DateFormat';
|
||||
import TimeFormat from '$app/components/database/components/field_types/date/TimeFormat';
|
||||
import { TimeStampTypeOption, UndeterminedDateField, updateTypeOption } from '$app/components/database/application';
|
||||
import { DateFormatPB, FieldType, TimeFormatPB } from '@/services/backend';
|
||||
import { useViewId } from '$app/hooks';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import IncludeTimeSwitch from '$app/components/database/components/field_types/date/IncludeTimeSwitch';
|
||||
import { useTypeOption } from '$app/components/database';
|
||||
|
||||
interface Props {
|
||||
field: UndeterminedDateField;
|
||||
showLabel?: boolean;
|
||||
}
|
||||
|
||||
function DateTimeFormat({ field, showLabel = true }: Props) {
|
||||
const viewId = useViewId();
|
||||
const { t } = useTranslation();
|
||||
const showIncludeTime = field.type === FieldType.CreatedTime || field.type === FieldType.LastEditedTime;
|
||||
const typeOption = useTypeOption<TimeStampTypeOption>(field.id);
|
||||
const { timeFormat = TimeFormatPB.TwentyFourHour, dateFormat = DateFormatPB.Friendly, includeTime } = typeOption;
|
||||
const handleChange = useCallback(
|
||||
async (params: { timeFormat?: TimeFormatPB; dateFormat?: DateFormatPB; includeTime?: boolean }) => {
|
||||
try {
|
||||
await updateTypeOption(viewId, field.id, field.type, {
|
||||
timeFormat: params.timeFormat ?? timeFormat,
|
||||
dateFormat: params.dateFormat ?? dateFormat,
|
||||
includeTime: params.includeTime ?? includeTime,
|
||||
fieldType: field.type,
|
||||
});
|
||||
} catch (e) {
|
||||
// toast.error(e.message);
|
||||
}
|
||||
},
|
||||
[dateFormat, field.id, field.type, includeTime, timeFormat, viewId]
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{showLabel && (
|
||||
<Typography className={'py-1 pl-3'} color={'text.secondary'}>
|
||||
{t('grid.field.format')}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<DateFormat
|
||||
value={dateFormat}
|
||||
onChange={(val) => {
|
||||
void handleChange({ dateFormat: val });
|
||||
}}
|
||||
/>
|
||||
<TimeFormat
|
||||
value={timeFormat}
|
||||
onChange={(val) => {
|
||||
void handleChange({ timeFormat: val });
|
||||
}}
|
||||
/>
|
||||
|
||||
{showIncludeTime && (
|
||||
<div className={'px-3 py-1'}>
|
||||
<IncludeTimeSwitch
|
||||
size={'small'}
|
||||
checked={includeTime}
|
||||
onIncludeTimeChange={(checked) => {
|
||||
void handleChange({ includeTime: checked });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DateTimeFormat;
|
@ -0,0 +1,47 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { Menu, MenuItem } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DateTimeField } from '$app/components/database/application';
|
||||
import DateTimeFormat from '$app/components/database/components/field_types/date/DateTimeFormat';
|
||||
import { ReactComponent as MoreSvg } from '$app/assets/more.svg';
|
||||
|
||||
interface Props {
|
||||
field: DateTimeField;
|
||||
}
|
||||
|
||||
function DateTimeFormatSelect({ field }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef<HTMLLIElement>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuItem ref={ref} onClick={() => setOpen(true)} className={'text-xs font-medium'}>
|
||||
<div className={'flex-1'}>
|
||||
{t('grid.field.dateFormat')} & {t('grid.field.timeFormat')}
|
||||
</div>
|
||||
<MoreSvg className={`transform text-base ${open ? '' : 'rotate-90'}`} />
|
||||
</MenuItem>
|
||||
<Menu
|
||||
anchorOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
open={open}
|
||||
anchorEl={ref.current}
|
||||
onClose={() => setOpen(false)}
|
||||
MenuListProps={{
|
||||
className: 'px-2',
|
||||
}}
|
||||
>
|
||||
<DateTimeFormat showLabel={false} field={field} />
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default DateTimeFormatSelect;
|
@ -0,0 +1,81 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { DateField, TimeField } from '@mui/x-date-pickers-pro';
|
||||
import dayjs from 'dayjs';
|
||||
import { Divider } from '@mui/material';
|
||||
|
||||
interface Props {
|
||||
onChange: (params: { date?: number; time?: string }) => void;
|
||||
date?: number;
|
||||
time?: string;
|
||||
timeFormat: string;
|
||||
dateFormat: string;
|
||||
includeTime?: boolean;
|
||||
}
|
||||
|
||||
const sx = {
|
||||
'& .MuiOutlinedInput-notchedOutline': {
|
||||
border: 'none',
|
||||
},
|
||||
'& .MuiOutlinedInput-input': {
|
||||
padding: '0',
|
||||
},
|
||||
};
|
||||
|
||||
function DateTimeInput({ includeTime, dateFormat, timeFormat, ...props }: Props) {
|
||||
const date = useMemo(() => {
|
||||
return dayjs.unix(props.date || dayjs().unix());
|
||||
}, [props.date]);
|
||||
|
||||
const time = useMemo(() => {
|
||||
return dayjs(dayjs().format('YYYY/MM/DD ') + props.time);
|
||||
}, [props.time]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
'flex transform items-center justify-between rounded-lg border border-line-divider px-1 py-2 transition-all'
|
||||
}
|
||||
>
|
||||
<DateField
|
||||
value={date}
|
||||
onChange={(date) => {
|
||||
if (!date) return;
|
||||
props.onChange({
|
||||
date: date.unix(),
|
||||
});
|
||||
}}
|
||||
inputProps={{
|
||||
className: 'text-[12px]',
|
||||
}}
|
||||
format={dateFormat}
|
||||
size={'small'}
|
||||
sx={sx}
|
||||
className={'flex-1 pl-2'}
|
||||
/>
|
||||
|
||||
{includeTime && (
|
||||
<>
|
||||
<Divider orientation={'vertical'} className={'mx-3'} flexItem />
|
||||
<TimeField
|
||||
value={time}
|
||||
inputProps={{
|
||||
className: 'text-[12px]',
|
||||
}}
|
||||
onChange={(time) => {
|
||||
if (!time) return;
|
||||
props.onChange({
|
||||
time: time.format(timeFormat),
|
||||
});
|
||||
}}
|
||||
format={timeFormat}
|
||||
size={'small'}
|
||||
sx={sx}
|
||||
className={'w-[70px] pl-1'}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DateTimeInput;
|
@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import { LocalizationProvider } from '@mui/x-date-pickers-pro';
|
||||
import { AdapterDayjs } from '@mui/x-date-pickers-pro/AdapterDayjs';
|
||||
import DateTimeInput from '$app/components/database/components/field_types/date/DateTimeInput';
|
||||
|
||||
interface Props {
|
||||
onChange: (params: { date?: number; endDate?: number; time?: string; endTime?: string }) => void;
|
||||
date?: number;
|
||||
endDate?: number;
|
||||
time?: string;
|
||||
endTime?: string;
|
||||
isRange?: boolean;
|
||||
timeFormat: string;
|
||||
dateFormat: string;
|
||||
includeTime?: boolean;
|
||||
}
|
||||
function DateTimeSet({ onChange, date, endDate, time, endTime, isRange, timeFormat, dateFormat, includeTime }: Props) {
|
||||
return (
|
||||
<div className={'mx-4 flex w-[216px] transform flex-col gap-2 transition-all'}>
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||
<DateTimeInput
|
||||
onChange={({ date, time }) => {
|
||||
onChange({
|
||||
date,
|
||||
time,
|
||||
});
|
||||
}}
|
||||
date={date}
|
||||
time={time}
|
||||
timeFormat={timeFormat}
|
||||
dateFormat={dateFormat}
|
||||
includeTime={includeTime}
|
||||
/>
|
||||
{isRange && (
|
||||
<DateTimeInput
|
||||
date={endDate}
|
||||
time={endTime}
|
||||
onChange={({ date, time }) => {
|
||||
onChange({
|
||||
endDate: date,
|
||||
endTime: time,
|
||||
});
|
||||
}}
|
||||
timeFormat={timeFormat}
|
||||
dateFormat={dateFormat}
|
||||
includeTime={includeTime}
|
||||
/>
|
||||
)}
|
||||
</LocalizationProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DateTimeSet;
|
@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import { Switch, SwitchProps } from '@mui/material';
|
||||
import { ReactComponent as TimeSvg } from '$app/assets/database/field-type-last-edited-time.svg';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function IncludeTimeSwitch({
|
||||
checked,
|
||||
onIncludeTimeChange,
|
||||
...props
|
||||
}: SwitchProps & {
|
||||
onIncludeTimeChange: (checked: boolean) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onIncludeTimeChange(event.target.checked);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={'flex w-full items-center justify-between gap-[20px]'}>
|
||||
<div className={'flex flex-1 justify-start gap-1.5'}>
|
||||
<TimeSvg />
|
||||
<Typography className={'flex-1 text-xs font-medium'}>{t('grid.field.includeTime')}</Typography>
|
||||
</div>
|
||||
<Switch {...props} size={'small'} checked={checked} onChange={handleChange} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default IncludeTimeSwitch;
|
@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import { Switch, SwitchProps } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { ReactComponent as DateSvg } from '$app/assets/database/field-type-date.svg';
|
||||
|
||||
function RangeSwitch({
|
||||
checked,
|
||||
onIsRangeChange,
|
||||
...props
|
||||
}: SwitchProps & {
|
||||
onIsRangeChange: (checked: boolean) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onIsRangeChange(event.target.checked);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={'flex w-full items-center justify-between gap-[20px]'}>
|
||||
<div className={'flex flex-1 justify-start gap-1.5'}>
|
||||
<DateSvg />
|
||||
<Typography className={'flex-1 text-xs font-medium'}>{t('grid.field.isRange')}</Typography>
|
||||
</div>
|
||||
<Switch {...props} size={'small'} checked={checked} onChange={handleChange} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default RangeSwitch;
|
@ -0,0 +1,67 @@
|
||||
import React, { useMemo, useRef, useState } from 'react';
|
||||
import { TimeFormatPB } from '@/services/backend';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Menu, MenuItem } from '@mui/material';
|
||||
import { ReactComponent as MoreSvg } from '$app/assets/more.svg';
|
||||
import { ReactComponent as SelectCheckSvg } from '$app/assets/database/select-check.svg';
|
||||
|
||||
interface Props {
|
||||
value: TimeFormatPB;
|
||||
onChange: (value: TimeFormatPB) => void;
|
||||
}
|
||||
function TimeFormat({ value, onChange }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef<HTMLLIElement>(null);
|
||||
const timeFormatMap = useMemo(
|
||||
() => ({
|
||||
[TimeFormatPB.TwelveHour]: t('grid.field.timeFormatTwelveHour'),
|
||||
[TimeFormatPB.TwentyFourHour]: t('grid.field.timeFormatTwentyFourHour'),
|
||||
}),
|
||||
[t]
|
||||
);
|
||||
|
||||
const handleClick = (option: TimeFormatPB) => {
|
||||
onChange(option);
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuItem
|
||||
className={'mx-0 flex w-full justify-between text-xs font-medium'}
|
||||
ref={ref}
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
{t('grid.field.timeFormat')}
|
||||
<MoreSvg className={`transform text-base ${open ? '' : 'rotate-90'}`} />
|
||||
</MenuItem>
|
||||
<Menu
|
||||
anchorOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
open={open}
|
||||
anchorEl={ref.current}
|
||||
onClose={() => setOpen(false)}
|
||||
>
|
||||
{Object.keys(timeFormatMap).map((option) => {
|
||||
const optionValue = Number(option) as TimeFormatPB;
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
className={'min-w-[120px] justify-between'}
|
||||
key={optionValue}
|
||||
onClick={() => handleClick(optionValue)}
|
||||
>
|
||||
{timeFormatMap[optionValue]}
|
||||
{value === optionValue && <SelectCheckSvg />}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default TimeFormat;
|
@ -0,0 +1,29 @@
|
||||
import { DateFormatPB, TimeFormatPB } from '@/services/backend';
|
||||
|
||||
export function getTimeFormat(timeFormat?: TimeFormatPB) {
|
||||
switch (timeFormat) {
|
||||
case TimeFormatPB.TwelveHour:
|
||||
return 'h:mm A';
|
||||
case TimeFormatPB.TwentyFourHour:
|
||||
return 'HH:mm';
|
||||
default:
|
||||
return 'HH:mm';
|
||||
}
|
||||
}
|
||||
|
||||
export function getDateFormat(dateFormat?: DateFormatPB) {
|
||||
switch (dateFormat) {
|
||||
case DateFormatPB.Friendly:
|
||||
return 'MMM DD, YYYY';
|
||||
case DateFormatPB.ISO:
|
||||
return 'YYYY-MMM-DD';
|
||||
case DateFormatPB.US:
|
||||
return 'YYYY/MMM/DD';
|
||||
case DateFormatPB.Local:
|
||||
return 'MMM/DD/YYYY';
|
||||
case DateFormatPB.DayMonthYear:
|
||||
return 'DD/MMM/YYYY';
|
||||
default:
|
||||
return 'YYYY-MMM-DD';
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { Popover } from '@mui/material';
|
||||
import InputBase from '@mui/material/InputBase';
|
||||
|
||||
function EditNumberCellInput({
|
||||
editing,
|
||||
anchorEl,
|
||||
width,
|
||||
onClose,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
editing: boolean;
|
||||
anchorEl: HTMLDivElement | null;
|
||||
width: number | undefined;
|
||||
onClose: () => void;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}) {
|
||||
const handleInput = (e: React.FormEvent<HTMLInputElement>) => {
|
||||
const value = (e.target as HTMLInputElement).value;
|
||||
|
||||
onChange(value);
|
||||
};
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'Enter') {
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
[onClose]
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
keepMounted={false}
|
||||
open={editing}
|
||||
anchorEl={anchorEl}
|
||||
PaperProps={{
|
||||
className: 'flex p-2 border border-blue-400',
|
||||
style: { width, minHeight: anchorEl?.offsetHeight, borderRadius: 0, boxShadow: 'none' },
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
transitionDuration={0}
|
||||
onClose={onClose}
|
||||
>
|
||||
<InputBase
|
||||
inputProps={{
|
||||
sx: {
|
||||
padding: 0,
|
||||
},
|
||||
}}
|
||||
autoFocus={true}
|
||||
value={value}
|
||||
onInput={handleInput}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditNumberCellInput;
|
@ -0,0 +1,34 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { NumberField, NumberTypeOption, updateTypeOption } from '$app/components/database/application';
|
||||
import { Divider } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import NumberFormatSelect from '$app/components/database/components/field_types/number/NumberFormatSelect';
|
||||
import { NumberFormatPB } from '@/services/backend';
|
||||
import { useViewId } from '$app/hooks';
|
||||
import { useTypeOption } from '$app/components/database';
|
||||
|
||||
function NumberFieldActions({ field }: { field: NumberField }) {
|
||||
const viewId = useViewId();
|
||||
const { t } = useTranslation();
|
||||
const typeOption = useTypeOption<NumberTypeOption>(field.id);
|
||||
const onChange = useCallback(
|
||||
async (value: NumberFormatPB) => {
|
||||
await updateTypeOption(viewId, field.id, field.type, {
|
||||
format: value,
|
||||
});
|
||||
},
|
||||
[field.id, field.type, viewId]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'flex flex-col pr-3 pt-1'}>
|
||||
<div className={'mb-2 px-5 text-sm text-text-caption'}>{t('grid.field.format')}</div>
|
||||
<NumberFormatSelect value={typeOption.format || NumberFormatPB.Num} onChange={onChange} />
|
||||
</div>
|
||||
<Divider className={'my-2'} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default NumberFieldActions;
|
@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { NumberFormatPB } from '@/services/backend';
|
||||
import { Menu, MenuItem, MenuProps } from '@mui/material';
|
||||
import { formats } from '$app/components/database/components/field_types/number/const';
|
||||
import { ReactComponent as SelectCheckSvg } from '$app/assets/database/select-check.svg';
|
||||
|
||||
function NumberFormatMenu({
|
||||
value,
|
||||
onChangeFormat,
|
||||
...props
|
||||
}: MenuProps & {
|
||||
value: NumberFormatPB;
|
||||
onChangeFormat: (value: NumberFormatPB) => void;
|
||||
}) {
|
||||
return (
|
||||
<Menu {...props}>
|
||||
{formats.map((format) => (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
onChangeFormat(format.value as NumberFormatPB);
|
||||
props.onClose?.({}, 'backdropClick');
|
||||
}}
|
||||
className={'flex justify-between text-xs font-medium'}
|
||||
key={format.value}
|
||||
>
|
||||
<div className={'flex-1'}>{format.key}</div>
|
||||
{value === format.value && <SelectCheckSvg />}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
export default NumberFormatMenu;
|
@ -0,0 +1,49 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { MenuItem } from '@mui/material';
|
||||
import { NumberFormatPB } from '@/services/backend';
|
||||
import { ReactComponent as MoreSvg } from '$app/assets/more.svg';
|
||||
import { formatText } from '$app/components/database/components/field_types/number/const';
|
||||
import NumberFormatMenu from '$app/components/database/components/field_types/number/NumberFormatMenu';
|
||||
|
||||
function NumberFormatSelect({ value, onChange }: { value: NumberFormatPB; onChange: (value: NumberFormatPB) => void }) {
|
||||
const ref = useRef<HTMLLIElement>(null);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuItem
|
||||
ref={ref}
|
||||
onClick={() => {
|
||||
setExpanded(!expanded);
|
||||
}}
|
||||
className={'flex w-full justify-between'}
|
||||
>
|
||||
<div className='flex-1 text-xs font-medium'>{formatText(value)}</div>
|
||||
<MoreSvg className={`transform text-base ${expanded ? '' : 'rotate-90'}`} />
|
||||
</MenuItem>
|
||||
<NumberFormatMenu
|
||||
keepMounted={false}
|
||||
anchorOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
PaperProps={{
|
||||
style: {
|
||||
maxHeight: 500,
|
||||
},
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
value={value}
|
||||
open={expanded}
|
||||
anchorEl={ref.current}
|
||||
onClose={() => setExpanded(false)}
|
||||
onChangeFormat={onChange}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default NumberFormatSelect;
|
@ -0,0 +1,14 @@
|
||||
import { NumberFormatPB } from '@/services/backend';
|
||||
|
||||
export const formats = Object.entries(NumberFormatPB)
|
||||
.filter(([, value]) => typeof value !== 'string')
|
||||
.map(([key, value]) => {
|
||||
return {
|
||||
key,
|
||||
value,
|
||||
};
|
||||
});
|
||||
|
||||
export const formatText = (format: NumberFormatPB) => {
|
||||
return formats.find((item) => item.value === format)?.key;
|
||||
};
|
@ -0,0 +1,130 @@
|
||||
import { FC, useState } from 'react';
|
||||
import { t } from 'i18next';
|
||||
import { Divider, ListSubheader, MenuItem, MenuList, MenuProps, OutlinedInput } from '@mui/material';
|
||||
import { SelectOptionColorPB } from '@/services/backend';
|
||||
import { ReactComponent as DeleteSvg } from '$app/assets/delete.svg';
|
||||
import { ReactComponent as SelectCheckSvg } from '$app/assets/database/select-check.svg';
|
||||
import { SelectOption } from '../../../application';
|
||||
import { SelectOptionColorMap, SelectOptionColorTextMap } from './constants';
|
||||
import Button from '@mui/material/Button';
|
||||
import {
|
||||
deleteSelectOption,
|
||||
insertOrUpdateSelectOption,
|
||||
} from '$app/components/database/application/field/select_option/select_option_service';
|
||||
import { useViewId } from '$app/hooks';
|
||||
import Popover from '@mui/material/Popover';
|
||||
|
||||
interface SelectOptionMenuProps {
|
||||
fieldId: string;
|
||||
option: SelectOption;
|
||||
MenuProps: MenuProps;
|
||||
}
|
||||
|
||||
const Colors = [
|
||||
SelectOptionColorPB.Purple,
|
||||
SelectOptionColorPB.Pink,
|
||||
SelectOptionColorPB.LightPink,
|
||||
SelectOptionColorPB.Orange,
|
||||
SelectOptionColorPB.Yellow,
|
||||
SelectOptionColorPB.Lime,
|
||||
SelectOptionColorPB.Green,
|
||||
SelectOptionColorPB.Aqua,
|
||||
SelectOptionColorPB.Blue,
|
||||
];
|
||||
|
||||
export const SelectOptionMenu: FC<SelectOptionMenuProps> = ({ fieldId, option, MenuProps: menuProps }) => {
|
||||
const [tagName, setTagName] = useState(option.name);
|
||||
const viewId = useViewId();
|
||||
const updateColor = async (color: SelectOptionColorPB) => {
|
||||
await insertOrUpdateSelectOption(viewId, fieldId, [
|
||||
{
|
||||
...option,
|
||||
color,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const updateName = async () => {
|
||||
if (tagName === option.name) return;
|
||||
await insertOrUpdateSelectOption(viewId, fieldId, [
|
||||
{
|
||||
...option,
|
||||
name: tagName,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
menuProps.onClose?.({}, 'backdropClick');
|
||||
};
|
||||
|
||||
const deleteOption = async () => {
|
||||
await deleteSelectOption(viewId, fieldId, [option]);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover
|
||||
keepMounted={false}
|
||||
classes={{
|
||||
paper: 'w-52',
|
||||
}}
|
||||
anchorOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'center',
|
||||
horizontal: -32,
|
||||
}}
|
||||
{...menuProps}
|
||||
onClose={onClose}
|
||||
>
|
||||
<ListSubheader className='my-2 leading-tight'>
|
||||
<OutlinedInput
|
||||
value={tagName}
|
||||
onChange={(e) => {
|
||||
setTagName(e.target.value);
|
||||
}}
|
||||
onBlur={updateName}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
void updateName();
|
||||
}
|
||||
}}
|
||||
autoFocus={true}
|
||||
placeholder={t('grid.selectOption.tagName')}
|
||||
size='small'
|
||||
/>
|
||||
</ListSubheader>
|
||||
<div className={'mb-2 px-3'}>
|
||||
<Button
|
||||
className={'flex w-full justify-start'}
|
||||
onClick={deleteOption}
|
||||
color={'inherit'}
|
||||
startIcon={<DeleteSvg />}
|
||||
>
|
||||
{t('grid.selectOption.deleteTag')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
<MenuItem disabled>{t('grid.selectOption.colorPanelTitle')}</MenuItem>
|
||||
<MenuList className={'max-h-[300px] overflow-y-auto overflow-x-hidden'}>
|
||||
{Colors.map((color) => (
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
void updateColor(color);
|
||||
}}
|
||||
key={color}
|
||||
value={color}
|
||||
>
|
||||
<span className={`mr-2 inline-flex h-4 w-4 rounded-full ${SelectOptionColorMap[color]}`} />
|
||||
<span className='flex-1'>{t(`grid.selectOption.${SelectOptionColorTextMap[color]}`)}</span>
|
||||
{option.color === color && <SelectCheckSvg />}
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuList>
|
||||
</Popover>
|
||||
);
|
||||
};
|
@ -0,0 +1,16 @@
|
||||
import { MenuItem, MenuItemProps } from '@mui/material';
|
||||
import { FC } from 'react';
|
||||
import { Tag } from '../Tag';
|
||||
|
||||
export interface CreateOptionProps {
|
||||
label: React.ReactNode;
|
||||
onClick?: MenuItemProps['onClick'];
|
||||
}
|
||||
|
||||
export const CreateOption: FC<CreateOptionProps> = ({ label, onClick }) => {
|
||||
return (
|
||||
<MenuItem className='mt-2' onClick={onClick}>
|
||||
<Tag className='ml-2' size='small' label={label} />
|
||||
</MenuItem>
|
||||
);
|
||||
};
|
@ -0,0 +1,41 @@
|
||||
import React, { FormEvent, useCallback } from 'react';
|
||||
import { ListSubheader, OutlinedInput } from '@mui/material';
|
||||
import { t } from 'i18next';
|
||||
|
||||
function SearchInput({
|
||||
setNewOptionName,
|
||||
newOptionName,
|
||||
onEnter,
|
||||
}: {
|
||||
newOptionName: string;
|
||||
setNewOptionName: (value: string) => void;
|
||||
onEnter: () => void;
|
||||
}) {
|
||||
const handleInput = useCallback(
|
||||
(event: FormEvent) => {
|
||||
const value = (event.target as HTMLInputElement).value;
|
||||
|
||||
setNewOptionName(value);
|
||||
},
|
||||
[setNewOptionName]
|
||||
);
|
||||
|
||||
return (
|
||||
<ListSubheader className='flex'>
|
||||
<OutlinedInput
|
||||
size='small'
|
||||
autoFocus={true}
|
||||
value={newOptionName}
|
||||
onInput={handleInput}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
onEnter();
|
||||
}
|
||||
}}
|
||||
placeholder={t('grid.selectOption.searchOrCreateOption')}
|
||||
/>
|
||||
</ListSubheader>
|
||||
);
|
||||
}
|
||||
|
||||
export default SearchInput;
|
@ -0,0 +1,152 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { MenuItem } from '@mui/material';
|
||||
import { t } from 'i18next';
|
||||
import { CreateOption } from '$app/components/database/components/field_types/select/select_cell_actions/CreateOption';
|
||||
import { SelectOptionItem } from '$app/components/database/components/field_types/select/select_cell_actions/SelectOptionItem';
|
||||
import {
|
||||
cellService,
|
||||
SelectCell as SelectCellType,
|
||||
SelectField,
|
||||
SelectTypeOption,
|
||||
} from '$app/components/database/application';
|
||||
import { useViewId } from '$app/hooks';
|
||||
import {
|
||||
createSelectOption,
|
||||
insertOrUpdateSelectOption,
|
||||
} from '$app/components/database/application/field/select_option/select_option_service';
|
||||
import { FieldType } from '@/services/backend';
|
||||
import { useTypeOption } from '$app/components/database';
|
||||
import SearchInput from './SearchInput';
|
||||
|
||||
function SelectCellActions({
|
||||
field,
|
||||
cell,
|
||||
onUpdated,
|
||||
}: {
|
||||
field: SelectField;
|
||||
cell: SelectCellType;
|
||||
onUpdated?: () => void;
|
||||
}) {
|
||||
const rowId = cell?.rowId;
|
||||
const viewId = useViewId();
|
||||
const typeOption = useTypeOption<SelectTypeOption>(field.id);
|
||||
const options = useMemo(() => typeOption.options ?? [], [typeOption.options]);
|
||||
|
||||
const selectedOptionIds = useMemo(() => cell?.data?.selectedOptionIds ?? [], [cell]);
|
||||
const [newOptionName, setNewOptionName] = useState('');
|
||||
const filteredOptions = useMemo(
|
||||
() =>
|
||||
options.filter((option) => {
|
||||
return option.name.toLowerCase().includes(newOptionName.toLowerCase());
|
||||
}),
|
||||
[options, newOptionName]
|
||||
);
|
||||
|
||||
const shouldCreateOption = !!newOptionName && filteredOptions.length === 0;
|
||||
|
||||
const updateCell = useCallback(
|
||||
async (optionIds: string[]) => {
|
||||
if (!cell || !rowId) return;
|
||||
const prev = selectedOptionIds;
|
||||
const deleteOptionIds = prev?.filter((id) => optionIds.find((cur) => cur === id) === undefined);
|
||||
|
||||
await cellService.updateSelectCell(viewId, rowId, field.id, {
|
||||
insertOptionIds: optionIds,
|
||||
deleteOptionIds,
|
||||
});
|
||||
onUpdated?.();
|
||||
},
|
||||
[cell, field.id, onUpdated, rowId, selectedOptionIds, viewId]
|
||||
);
|
||||
|
||||
const createOption = useCallback(async () => {
|
||||
const option = await createSelectOption(viewId, field.id, newOptionName);
|
||||
|
||||
if (!option) return;
|
||||
await insertOrUpdateSelectOption(viewId, field.id, [option]);
|
||||
setNewOptionName('');
|
||||
return option;
|
||||
}, [viewId, field.id, newOptionName]);
|
||||
|
||||
const handleClickOption = useCallback(
|
||||
(optionId: string) => {
|
||||
if (field.type === FieldType.SingleSelect) {
|
||||
void updateCell([optionId]);
|
||||
return;
|
||||
}
|
||||
|
||||
const prev = selectedOptionIds;
|
||||
let newOptionIds = [];
|
||||
|
||||
if (!prev) {
|
||||
newOptionIds.push(optionId);
|
||||
} else {
|
||||
const isSelected = prev.includes(optionId);
|
||||
|
||||
if (isSelected) {
|
||||
newOptionIds = prev.filter((id) => id !== optionId);
|
||||
} else {
|
||||
newOptionIds = [...prev, optionId];
|
||||
}
|
||||
}
|
||||
|
||||
void updateCell(newOptionIds);
|
||||
},
|
||||
[field.type, selectedOptionIds, updateCell]
|
||||
);
|
||||
|
||||
const handleNewTagClick = useCallback(async () => {
|
||||
if (!cell || !rowId) return;
|
||||
const option = await createOption();
|
||||
|
||||
if (!option) return;
|
||||
handleClickOption(option.id);
|
||||
}, [cell, createOption, handleClickOption, rowId]);
|
||||
|
||||
const handleEnter = useCallback(() => {
|
||||
if (shouldCreateOption) {
|
||||
void handleNewTagClick();
|
||||
} else {
|
||||
if (field.type === FieldType.SingleSelect) {
|
||||
const firstOption = filteredOptions[0];
|
||||
|
||||
if (!firstOption) return;
|
||||
|
||||
void updateCell([firstOption.id]);
|
||||
} else {
|
||||
void updateCell(filteredOptions.map((option) => option.id));
|
||||
}
|
||||
}
|
||||
|
||||
setNewOptionName('');
|
||||
}, [field.type, filteredOptions, handleNewTagClick, shouldCreateOption, updateCell]);
|
||||
|
||||
return (
|
||||
<div className={'text-base'}>
|
||||
<SearchInput setNewOptionName={setNewOptionName} newOptionName={newOptionName} onEnter={handleEnter} />
|
||||
<div className='mx-4 mb-2 mt-4 text-xs'>
|
||||
{shouldCreateOption ? t('grid.selectOption.createNew') : t('grid.selectOption.orSelectOne')}
|
||||
</div>
|
||||
{shouldCreateOption ? (
|
||||
<CreateOption label={newOptionName} onClick={handleNewTagClick} />
|
||||
) : (
|
||||
<div className={'max-h-[300px] overflow-y-auto overflow-x-hidden'}>
|
||||
{filteredOptions.map((option) => (
|
||||
<MenuItem className={'px-2'} key={option.id} value={option.id}>
|
||||
<SelectOptionItem
|
||||
onClick={() => {
|
||||
handleClickOption(option.id);
|
||||
}}
|
||||
isSelected={selectedOptionIds?.includes(option.id)}
|
||||
fieldId={cell?.fieldId || ''}
|
||||
option={option}
|
||||
/>
|
||||
</MenuItem>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectCellActions;
|
@ -0,0 +1,57 @@
|
||||
import { FC, MouseEventHandler, useCallback, useRef, useState } from 'react';
|
||||
import { IconButton } from '@mui/material';
|
||||
import { ReactComponent as DetailsSvg } from '$app/assets/details.svg';
|
||||
import { SelectOption } from '../../../../application';
|
||||
import { SelectOptionMenu } from '../SelectOptionMenu';
|
||||
import { Tag } from '../Tag';
|
||||
import { ReactComponent as SelectCheckSvg } from '$app/assets/database/select-check.svg';
|
||||
|
||||
export interface SelectOptionItemProps {
|
||||
option: SelectOption;
|
||||
fieldId: string;
|
||||
isSelected?: boolean;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export const SelectOptionItem: FC<SelectOptionItemProps> = ({ onClick, isSelected, fieldId, option }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const anchorEl = useRef<HTMLDivElement | null>(null);
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const handleClick = useCallback<MouseEventHandler<HTMLButtonElement>>((event) => {
|
||||
event.stopPropagation();
|
||||
setOpen(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
onClick={onClick}
|
||||
ref={anchorEl}
|
||||
className={'flex w-full items-center justify-between'}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
>
|
||||
<div className='flex-1'>
|
||||
<Tag key={option.id} size='small' color={option.color} label={option.name} />
|
||||
</div>
|
||||
{isSelected && !hovered && <SelectCheckSvg />}
|
||||
{hovered && (
|
||||
<IconButton onClick={handleClick}>
|
||||
<DetailsSvg className='text-base' />
|
||||
</IconButton>
|
||||
)}
|
||||
</div>
|
||||
{open && (
|
||||
<SelectOptionMenu
|
||||
fieldId={fieldId}
|
||||
option={option}
|
||||
MenuProps={{
|
||||
open,
|
||||
anchorEl: anchorEl.current,
|
||||
onClose: () => setOpen(false),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,54 @@
|
||||
import React, { useState } from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ReactComponent as AddSvg } from '$app/assets/add.svg';
|
||||
import { OutlinedInput } from '@mui/material';
|
||||
import {
|
||||
createSelectOption,
|
||||
insertOrUpdateSelectOption,
|
||||
} from '$app/components/database/application/field/select_option/select_option_service';
|
||||
import { useViewId } from '$app/hooks';
|
||||
|
||||
function AddAnOption({ fieldId }: { fieldId: string }) {
|
||||
const viewId = useViewId();
|
||||
const { t } = useTranslation();
|
||||
const [edit, setEdit] = useState(false);
|
||||
const [newOptionName, setNewOptionName] = useState('');
|
||||
const exitEdit = () => {
|
||||
setNewOptionName('');
|
||||
setEdit(false);
|
||||
};
|
||||
|
||||
const createOption = async () => {
|
||||
const option = await createSelectOption(viewId, fieldId, newOptionName);
|
||||
|
||||
if (!option) return;
|
||||
await insertOrUpdateSelectOption(viewId, fieldId, [option]);
|
||||
setNewOptionName('');
|
||||
};
|
||||
|
||||
return edit ? (
|
||||
<OutlinedInput
|
||||
onBlur={exitEdit}
|
||||
autoFocus={true}
|
||||
onChange={(e) => {
|
||||
setNewOptionName(e.target.value);
|
||||
}}
|
||||
value={newOptionName}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
void createOption();
|
||||
}
|
||||
}}
|
||||
className={'mx-2 mb-1'}
|
||||
placeholder={t('grid.selectOption.typeANewOption')}
|
||||
size='small'
|
||||
/>
|
||||
) : (
|
||||
<Button onClick={() => setEdit(true)} color={'inherit'} startIcon={<AddSvg />}>
|
||||
<div className={'w-full text-left'}>{t('grid.field.addSelectOption')}</div>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddAnOption;
|
@ -0,0 +1,46 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { ReactComponent as MoreIcon } from '$app/assets/more.svg';
|
||||
import { SelectOption } from '$app/components/database/application';
|
||||
// import { ReactComponent as DragIcon } from '$app/assets/drag.svg';
|
||||
|
||||
import { SelectOptionMenu } from '$app/components/database/components/field_types/select/SelectOptionMenu';
|
||||
import Button from '@mui/material/Button';
|
||||
import { SelectOptionColorMap } from '$app/components/database/components/field_types/select/constants';
|
||||
|
||||
function Option({ option, fieldId }: { option: SelectOption; fieldId: string }) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const ref = useRef<HTMLButtonElement>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
color={'inherit'}
|
||||
// startIcon={<DragIcon />}
|
||||
endIcon={<MoreIcon className={`transform ${expanded ? '' : 'rotate-90'}`} />}
|
||||
ref={ref}
|
||||
className={'flex w-full items-center justify-between'}
|
||||
>
|
||||
<div className={`flex flex-1 justify-start`}>
|
||||
<div className={`${SelectOptionColorMap[option.color]} rounded-lg px-1.5 py-1`}>{option.name}</div>
|
||||
</div>
|
||||
</Button>
|
||||
<SelectOptionMenu
|
||||
fieldId={fieldId}
|
||||
MenuProps={{
|
||||
anchorEl: ref.current,
|
||||
onClose: () => setExpanded(false),
|
||||
open: expanded,
|
||||
transformOrigin: {
|
||||
vertical: 'center',
|
||||
horizontal: 'left',
|
||||
},
|
||||
anchorOrigin: { vertical: 'center', horizontal: 'right' },
|
||||
}}
|
||||
option={option}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Option;
|
@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { SelectOption } from '$app/components/database/application';
|
||||
import Option from './Option';
|
||||
|
||||
interface Props {
|
||||
options: SelectOption[];
|
||||
fieldId: string;
|
||||
}
|
||||
function Options({ options, fieldId }: Props) {
|
||||
return (
|
||||
<div className={'max-h-[300px] overflow-y-auto overflow-x-hidden'}>
|
||||
{options.map((option) => {
|
||||
return <Option fieldId={fieldId} key={option.id} option={option} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Options;
|
@ -0,0 +1,26 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import AddAnOption from '$app/components/database/components/field_types/select/select_field_actions/AddAnOption';
|
||||
import Options from '$app/components/database/components/field_types/select/select_field_actions/Options';
|
||||
import { SelectField, SelectTypeOption } from '$app/components/database/application';
|
||||
import { Divider } from '@mui/material';
|
||||
import { useTypeOption } from '$app/components/database';
|
||||
|
||||
function SelectFieldActions({ field }: { field: SelectField }) {
|
||||
const typeOption = useTypeOption<SelectTypeOption>(field.id);
|
||||
const options = useMemo(() => typeOption.options ?? [], [typeOption.options]);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={'flex flex-col px-3 pt-1'}>
|
||||
<div className={'mb-2 px-2 text-sm text-text-caption'}>{t('grid.field.optionTitle')}</div>
|
||||
<AddAnOption fieldId={field.id} />
|
||||
<Options fieldId={field.id} options={options} />
|
||||
</div>
|
||||
<Divider className={'my-2'} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectFieldActions;
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import React from 'react';
|
||||
import { Popover, TextareaAutosize } from '@mui/material';
|
||||
|
||||
interface Props {
|
||||
@ -10,25 +10,24 @@ interface Props {
|
||||
onInput: (event: React.FormEvent<HTMLTextAreaElement>) => void;
|
||||
}
|
||||
function EditTextCellInput({ editing, anchorEl, width, onClose, text, onInput }: Props) {
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const handleEnter = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
const shift = e.shiftKey;
|
||||
|
||||
useEffect(() => {
|
||||
if (textareaRef.current) {
|
||||
textareaRef.current.focus();
|
||||
// set the cursor to the end of the text
|
||||
const length = textareaRef.current.value.length;
|
||||
|
||||
textareaRef.current.setSelectionRange(length, length);
|
||||
// If shift is pressed, allow the user to enter a new line, otherwise close the popover
|
||||
if (!shift && e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onClose();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [textareaRef.current]);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={editing}
|
||||
anchorEl={anchorEl}
|
||||
PaperProps={{
|
||||
className: 'flex p-2 border border-blue-400',
|
||||
style: { width, borderRadius: 0, boxShadow: 'none' },
|
||||
style: { width, minHeight: anchorEl?.offsetHeight, borderRadius: 0, boxShadow: 'none' },
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 1,
|
||||
@ -36,14 +35,15 @@ function EditTextCellInput({ editing, anchorEl, width, onClose, text, onInput }:
|
||||
}}
|
||||
transitionDuration={0}
|
||||
onClose={onClose}
|
||||
keepMounted={false}
|
||||
>
|
||||
<TextareaAutosize
|
||||
ref={textareaRef}
|
||||
className='resize-none text-sm'
|
||||
className='w-full resize-none whitespace-break-spaces break-all text-sm'
|
||||
autoFocus
|
||||
autoCorrect='off'
|
||||
value={text}
|
||||
onInput={onInput}
|
||||
onKeyDown={handleEnter}
|
||||
/>
|
||||
</Popover>
|
||||
);
|
@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import Select from '@mui/material/Select';
|
||||
import { FormControl, MenuItem, SelectProps } from '@mui/material';
|
||||
|
||||
function ConditionSelect({
|
||||
conditions,
|
||||
...props
|
||||
}: SelectProps & {
|
||||
conditions: {
|
||||
value: number;
|
||||
text: string;
|
||||
}[];
|
||||
}) {
|
||||
return (
|
||||
<FormControl size={'small'} variant={'outlined'}>
|
||||
<Select
|
||||
{...props}
|
||||
sx={{
|
||||
'& .MuiSelect-select': {
|
||||
padding: 0,
|
||||
fontSize: '12px',
|
||||
},
|
||||
'& .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: 'transparent !important',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{conditions.map((condition) => {
|
||||
return (
|
||||
<MenuItem key={condition.value} value={condition.value}>
|
||||
{condition.text}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
);
|
||||
}
|
||||
|
||||
export default ConditionSelect;
|
@ -1,27 +1,54 @@
|
||||
import React, { FC, useState } from 'react';
|
||||
import { Filter as FilterType, Field as FieldData, UndeterminedFilter } from '$app/components/database/application';
|
||||
import React, { FC, useMemo, useState } from 'react';
|
||||
import {
|
||||
Filter as FilterType,
|
||||
Field as FieldData,
|
||||
UndeterminedFilter,
|
||||
TextFilterData,
|
||||
SelectFilterData,
|
||||
NumberFilterData,
|
||||
CheckboxFilterData,
|
||||
ChecklistFilterData,
|
||||
DateFilterData,
|
||||
} from '$app/components/database/application';
|
||||
import { Chip, Popover } from '@mui/material';
|
||||
import { Field } from '$app/components/database/components/field';
|
||||
import { ReactComponent as DropDownSvg } from '$app/assets/dropdown.svg';
|
||||
import TextFilter from '$app/components/database/components/filter/field_filter/TextFilter';
|
||||
import TextFilter from './text_filter/TextFilter';
|
||||
import { FieldType } from '@/services/backend';
|
||||
import FilterActions from '$app/components/database/components/filter/FilterActions';
|
||||
import { updateFilter } from '$app/components/database/application/filter/filter_service';
|
||||
import { useViewId } from '$app/hooks';
|
||||
import SelectFilter from './select_filter/SelectFilter';
|
||||
|
||||
import DateFilter from '$app/components/database/components/filter/date_filter/DateFilter';
|
||||
import FilterConditionSelect from '$app/components/database/components/filter/FilterConditionSelect';
|
||||
|
||||
interface Props {
|
||||
filter: FilterType;
|
||||
field: FieldData;
|
||||
}
|
||||
|
||||
interface FilterComponentProps {
|
||||
filter: FilterType;
|
||||
field: FieldData;
|
||||
onChange: (data: UndeterminedFilter['data']) => void;
|
||||
}
|
||||
|
||||
type FilterComponent = FC<FilterComponentProps>;
|
||||
const getFilterComponent = (field: FieldData) => {
|
||||
switch (field.type) {
|
||||
case FieldType.RichText:
|
||||
return TextFilter as FC<{
|
||||
filter: FilterType;
|
||||
field: FieldData;
|
||||
onChange: (data: UndeterminedFilter['data']) => void;
|
||||
}>;
|
||||
case FieldType.URL:
|
||||
case FieldType.Number:
|
||||
return TextFilter as FilterComponent;
|
||||
case FieldType.SingleSelect:
|
||||
case FieldType.MultiSelect:
|
||||
return SelectFilter as FilterComponent;
|
||||
|
||||
case FieldType.DateTime:
|
||||
case FieldType.LastEditedTime:
|
||||
case FieldType.CreatedTime:
|
||||
return DateFilter as FilterComponent;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@ -54,6 +81,29 @@ function Filter({ filter, field }: Props) {
|
||||
|
||||
const Component = getFilterComponent(field);
|
||||
|
||||
const condition = useMemo(() => {
|
||||
switch (field.type) {
|
||||
case FieldType.RichText:
|
||||
case FieldType.URL:
|
||||
return (filter.data as TextFilterData).condition;
|
||||
case FieldType.SingleSelect:
|
||||
case FieldType.MultiSelect:
|
||||
return (filter.data as SelectFilterData).condition;
|
||||
case FieldType.Number:
|
||||
return (filter.data as NumberFilterData).condition;
|
||||
case FieldType.Checkbox:
|
||||
return (filter.data as CheckboxFilterData).condition;
|
||||
case FieldType.Checklist:
|
||||
return (filter.data as ChecklistFilterData).condition;
|
||||
case FieldType.DateTime:
|
||||
case FieldType.LastEditedTime:
|
||||
case FieldType.CreatedTime:
|
||||
return (filter.data as DateFilterData).condition;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}, [field, filter]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Chip
|
||||
@ -67,25 +117,37 @@ function Filter({ filter, field }: Props) {
|
||||
}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
<Popover
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'center',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'center',
|
||||
}}
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleClose}
|
||||
keepMounted={false}
|
||||
>
|
||||
<div className={'flex items-start justify-between p-4'}>
|
||||
{condition !== undefined && open && (
|
||||
<Popover
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'center',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'center',
|
||||
}}
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleClose}
|
||||
keepMounted={false}
|
||||
>
|
||||
<div className={'flex items-center justify-between'}>
|
||||
<FilterConditionSelect
|
||||
name={field.name}
|
||||
condition={condition}
|
||||
fieldType={field.type}
|
||||
onChange={(condition) => {
|
||||
void onDataChange({
|
||||
condition,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<FilterActions filter={filter} />
|
||||
</div>
|
||||
{Component && <Component filter={filter} field={field} onChange={onDataChange} />}
|
||||
<FilterActions filter={filter} />
|
||||
</div>
|
||||
</Popover>
|
||||
</Popover>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ function FilterActions({ filter }: { filter: Filter }) {
|
||||
onClick={(e) => {
|
||||
setAnchorEl(e.currentTarget);
|
||||
}}
|
||||
className={'mx-2 my-1.5'}
|
||||
>
|
||||
<MoreSvg />
|
||||
</IconButton>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user