feat: new grid (#3167)

* feat: implement database database service by functions

* feat: define database data type

* feat: basic grid store and component

* feat: data communication mechanism and simple table ui. Can add new filed and update field name.

* feat: add grid text cell and grid checkbox cell

* feat: single select cell and multiselect cell

* refactor: fix code review problems

* feat: add new row

* feat: fix tsc error
This commit is contained in:
fangwufeng-v 2023-09-11 10:27:56 +08:00 committed by GitHub
parent 35f84e42cc
commit a81670c447
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
76 changed files with 2987 additions and 12 deletions

View File

@ -7,7 +7,7 @@
<title>AppFlowy: The Open Source Alternative To Notion</title>
</head>
<body>
<body id="body">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>

View File

@ -62,9 +62,11 @@
"slate-react": "^0.94.2",
"ts-results": "^3.3.0",
"utf8": "^3.0.0",
"valtio": "^1.11.1",
"yjs": "^13.5.51"
},
"devDependencies": {
"@svgr/plugin-svgo": "^8.0.1",
"@tauri-apps/cli": "^1.2.2",
"@types/google-protobuf": "^3.15.6",
"@types/is-hotkey": "^0.1.7",
@ -94,6 +96,7 @@
"tailwindcss": "^3.2.7",
"typescript": "^4.6.4",
"uuid": "^9.0.0",
"vite": "^4.0.0"
"vite": "^4.0.0",
"vite-plugin-svgr": "^3.2.0"
}
}

View File

@ -136,11 +136,17 @@ dependencies:
utf8:
specifier: ^3.0.0
version: 3.0.0
valtio:
specifier: ^1.11.1
version: 1.11.1(react@18.2.0)
yjs:
specifier: ^13.5.51
version: 13.6.1
devDependencies:
'@svgr/plugin-svgo':
specifier: ^8.0.1
version: 8.0.1(@svgr/core@7.0.0)
'@tauri-apps/cli':
specifier: ^1.2.2
version: 1.3.1
@ -231,6 +237,9 @@ devDependencies:
vite:
specifier: ^4.0.0
version: 4.3.5(@types/node@18.16.9)
vite-plugin-svgr:
specifier: ^3.2.0
version: 3.2.0(vite@4.3.5)
packages:
@ -1483,6 +1492,20 @@ packages:
engines: {node: '>=14'}
dev: false
/@rollup/pluginutils@5.0.2:
resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==}
engines: {node: '>=14.0.0'}
peerDependencies:
rollup: ^1.20.0||^2.0.0||^3.0.0
peerDependenciesMeta:
rollup:
optional: true
dependencies:
'@types/estree': 1.0.1
estree-walker: 2.0.2
picomatch: 2.3.1
dev: true
/@sinclair/typebox@0.25.24:
resolution: {integrity: sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==}
dev: false
@ -1510,6 +1533,139 @@ packages:
yjs: 13.6.1
dev: false
/@svgr/babel-plugin-add-jsx-attribute@7.0.0(@babel/core@7.21.8):
resolution: {integrity: sha512-khWbXesWIP9v8HuKCl2NU2HNAyqpSQ/vkIl36Nbn4HIwEYSRWL0H7Gs6idJdha2DkpFDWlsqMELvoCE8lfFY6Q==}
engines: {node: '>=14'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.21.8
dev: true
/@svgr/babel-plugin-remove-jsx-attribute@7.0.0(@babel/core@7.21.8):
resolution: {integrity: sha512-iiZaIvb3H/c7d3TH2HBeK91uI2rMhZNwnsIrvd7ZwGLkFw6mmunOCoVnjdYua662MqGFxlN9xTq4fv9hgR4VXQ==}
engines: {node: '>=14'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.21.8
dev: true
/@svgr/babel-plugin-remove-jsx-empty-expression@7.0.0(@babel/core@7.21.8):
resolution: {integrity: sha512-sQQmyo+qegBx8DfFc04PFmIO1FP1MHI1/QEpzcIcclo5OAISsOJPW76ZIs0bDyO/DBSJEa/tDa1W26pVtt0FRw==}
engines: {node: '>=14'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.21.8
dev: true
/@svgr/babel-plugin-replace-jsx-attribute-value@7.0.0(@babel/core@7.21.8):
resolution: {integrity: sha512-i6MaAqIZXDOJeikJuzocByBf8zO+meLwfQ/qMHIjCcvpnfvWf82PFvredEZElErB5glQFJa2KVKk8N2xV6tRRA==}
engines: {node: '>=14'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.21.8
dev: true
/@svgr/babel-plugin-svg-dynamic-title@7.0.0(@babel/core@7.21.8):
resolution: {integrity: sha512-BoVSh6ge3SLLpKC0pmmN9DFlqgFy4NxNgdZNLPNJWBUU7TQpDWeBuyVuDW88iXydb5Cv0ReC+ffa5h3VrKfk1w==}
engines: {node: '>=14'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.21.8
dev: true
/@svgr/babel-plugin-svg-em-dimensions@7.0.0(@babel/core@7.21.8):
resolution: {integrity: sha512-tNDcBa+hYn0gO+GkP/AuNKdVtMufVhU9fdzu+vUQsR18RIJ9RWe7h/pSBY338RO08wArntwbDk5WhQBmhf2PaA==}
engines: {node: '>=14'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.21.8
dev: true
/@svgr/babel-plugin-transform-react-native-svg@7.0.0(@babel/core@7.21.8):
resolution: {integrity: sha512-qw54u8ljCJYL2KtBOjI5z7Nzg8LnSvQOP5hPKj77H4VQL4+HdKbAT5pnkkZLmHKYwzsIHSYKXxHouD8zZamCFQ==}
engines: {node: '>=14'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.21.8
dev: true
/@svgr/babel-plugin-transform-svg-component@7.0.0(@babel/core@7.21.8):
resolution: {integrity: sha512-CcFECkDj98daOg9jE3Bh3uyD9kzevCAnZ+UtzG6+BQG/jOQ2OA3jHnX6iG4G1MCJkUQFnUvEv33NvQfqrb/F3A==}
engines: {node: '>=12'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.21.8
dev: true
/@svgr/babel-preset@7.0.0(@babel/core@7.21.8):
resolution: {integrity: sha512-EX/NHeFa30j5UjldQGVQikuuQNHUdGmbh9kEpBKofGUtF0GUPJ4T4rhoYiqDAOmBOxojyot36JIFiDUHUK1ilQ==}
engines: {node: '>=14'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.21.8
'@svgr/babel-plugin-add-jsx-attribute': 7.0.0(@babel/core@7.21.8)
'@svgr/babel-plugin-remove-jsx-attribute': 7.0.0(@babel/core@7.21.8)
'@svgr/babel-plugin-remove-jsx-empty-expression': 7.0.0(@babel/core@7.21.8)
'@svgr/babel-plugin-replace-jsx-attribute-value': 7.0.0(@babel/core@7.21.8)
'@svgr/babel-plugin-svg-dynamic-title': 7.0.0(@babel/core@7.21.8)
'@svgr/babel-plugin-svg-em-dimensions': 7.0.0(@babel/core@7.21.8)
'@svgr/babel-plugin-transform-react-native-svg': 7.0.0(@babel/core@7.21.8)
'@svgr/babel-plugin-transform-svg-component': 7.0.0(@babel/core@7.21.8)
dev: true
/@svgr/core@7.0.0:
resolution: {integrity: sha512-ztAoxkaKhRVloa3XydohgQQCb0/8x9T63yXovpmHzKMkHO6pkjdsIAWKOS4bE95P/2quVh1NtjSKlMRNzSBffw==}
engines: {node: '>=14'}
dependencies:
'@babel/core': 7.21.8
'@svgr/babel-preset': 7.0.0(@babel/core@7.21.8)
camelcase: 6.3.0
cosmiconfig: 8.2.0
transitivePeerDependencies:
- supports-color
dev: true
/@svgr/hast-util-to-babel-ast@7.0.0:
resolution: {integrity: sha512-42Ej9sDDEmsJKjrfQ1PHmiDiHagh/u9AHO9QWbeNx4KmD9yS5d1XHmXUNINfUcykAU+4431Cn+k6Vn5mWBYimQ==}
engines: {node: '>=14'}
dependencies:
'@babel/types': 7.21.5
entities: 4.5.0
dev: true
/@svgr/plugin-jsx@7.0.0:
resolution: {integrity: sha512-SWlTpPQmBUtLKxXWgpv8syzqIU8XgFRvyhfkam2So8b3BE0OS0HPe5UfmlJ2KIC+a7dpuuYovPR2WAQuSyMoPw==}
engines: {node: '>=14'}
dependencies:
'@babel/core': 7.21.8
'@svgr/babel-preset': 7.0.0(@babel/core@7.21.8)
'@svgr/hast-util-to-babel-ast': 7.0.0
svg-parser: 2.0.4
transitivePeerDependencies:
- supports-color
dev: true
/@svgr/plugin-svgo@8.0.1(@svgr/core@7.0.0):
resolution: {integrity: sha512-29OJ1QmJgnohQHDAgAuY2h21xWD6TZiXji+hnx+W635RiXTAlHTbjrZDktfqzkN0bOeQEtNe+xgq73/XeWFfSg==}
engines: {node: '>=14'}
peerDependencies:
'@svgr/core': '*'
dependencies:
'@svgr/core': 7.0.0
cosmiconfig: 8.2.0
deepmerge: 4.3.1
svgo: 3.0.2
dev: true
/@tanstack/react-virtual@3.0.0-beta.54(react@18.2.0):
resolution: {integrity: sha512-D1mDMf4UPbrtHRZZriCly5bXTBMhylslm4dhcHqTtDJ6brQcgGmk8YD9JdWBGWfGSWPKoh2x1H3e7eh+hgPXtQ==}
peerDependencies:
@ -1625,6 +1781,11 @@ packages:
'@tauri-apps/cli-win32-x64-msvc': 1.3.1
dev: true
/@trysound/sax@0.2.0:
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
engines: {node: '>=10.13.0'}
dev: true
/@types/babel__core@7.20.0:
resolution: {integrity: sha512-+n8dL/9GWblDO0iU6eZAwEIJVr5DWigtle+Q6HLOrh/pdbXOhOtqzq8VPPE2zvNJzSKY4vH/z3iT3tn0A3ypiQ==}
dependencies:
@ -1654,6 +1815,10 @@ packages:
'@babel/types': 7.21.5
dev: false
/@types/estree@1.0.1:
resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==}
dev: true
/@types/google-protobuf@3.15.6:
resolution: {integrity: sha512-pYVNNJ+winC4aek+lZp93sIKxnXt5qMkuKmaqS3WGuTq0Bw1ZDYNBgzG5kkdtwcv+GmYJGo3yEg6z2cKKAiEdw==}
dev: true
@ -2216,6 +2381,10 @@ packages:
engines: {node: '>=8'}
dev: true
/boolbase@1.0.0:
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
dev: true
/brace-expansion@1.1.11:
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
dependencies:
@ -2278,7 +2447,6 @@ packages:
/camelcase@6.3.0:
resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
engines: {node: '>=10'}
dev: false
/caniuse-lite@1.0.30001487:
resolution: {integrity: sha512-83564Z3yWGqXsh2vaH/mhXfEM0wX+NlBCm1jYHOb97TrTWJEmPTccZgeLTPBUUb0PNVo+oomb7wkimZBIERClA==}
@ -2407,6 +2575,11 @@ packages:
engines: {node: '>= 6'}
dev: true
/commander@7.2.0:
resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
engines: {node: '>= 10'}
dev: true
/commander@8.3.0:
resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
engines: {node: '>= 12'}
@ -2444,6 +2617,16 @@ packages:
yaml: 1.10.2
dev: false
/cosmiconfig@8.2.0:
resolution: {integrity: sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==}
engines: {node: '>=14'}
dependencies:
import-fresh: 3.3.0
js-yaml: 4.1.0
parse-json: 5.2.0
path-type: 4.0.0
dev: true
/cross-spawn@7.0.3:
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
engines: {node: '>= 8'}
@ -2458,12 +2641,50 @@ packages:
tiny-invariant: 1.3.1
dev: false
/css-select@5.1.0:
resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==}
dependencies:
boolbase: 1.0.0
css-what: 6.1.0
domhandler: 5.0.3
domutils: 3.1.0
nth-check: 2.1.1
dev: true
/css-tree@2.2.1:
resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
dependencies:
mdn-data: 2.0.28
source-map-js: 1.0.2
dev: true
/css-tree@2.3.1:
resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
dependencies:
mdn-data: 2.0.30
source-map-js: 1.0.2
dev: true
/css-what@6.1.0:
resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==}
engines: {node: '>= 6'}
dev: true
/cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'}
hasBin: true
dev: true
/csso@5.0.5:
resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
dependencies:
css-tree: 2.2.1
dev: true
/csstype@3.1.2:
resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==}
@ -2504,7 +2725,6 @@ packages:
/deepmerge@4.3.1:
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
engines: {node: '>=0.10.0'}
dev: false
/define-properties@1.2.0:
resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==}
@ -2564,6 +2784,33 @@ packages:
csstype: 3.1.2
dev: false
/dom-serializer@2.0.0:
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
dependencies:
domelementtype: 2.3.0
domhandler: 5.0.3
entities: 4.5.0
dev: true
/domelementtype@2.3.0:
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
dev: true
/domhandler@5.0.3:
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
engines: {node: '>= 4'}
dependencies:
domelementtype: 2.3.0
dev: true
/domutils@3.1.0:
resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==}
dependencies:
dom-serializer: 2.0.0
domelementtype: 2.3.0
domhandler: 5.0.3
dev: true
/dot-case@3.0.4:
resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}
dependencies:
@ -2591,11 +2838,15 @@ packages:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
dev: false
/entities@4.5.0:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
dev: true
/error-ex@1.3.2:
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
dependencies:
is-arrayish: 0.2.1
dev: false
/es-abstract@1.21.2:
resolution: {integrity: sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==}
@ -2850,6 +3101,10 @@ packages:
engines: {node: '>=4.0'}
dev: true
/estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
dev: true
/esutils@2.0.3:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
@ -3307,7 +3562,6 @@ packages:
/is-arrayish@0.2.1:
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
dev: false
/is-bigint@1.0.4:
resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==}
@ -3947,7 +4201,6 @@ packages:
/json-parse-even-better-errors@2.3.1:
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
dev: false
/json-schema-traverse@0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
@ -4105,6 +4358,14 @@ packages:
resolution: {integrity: sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==}
dev: false
/mdn-data@2.0.28:
resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==}
dev: true
/mdn-data@2.0.30:
resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
dev: true
/memoize-one@5.2.1:
resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==}
dev: false
@ -4195,6 +4456,12 @@ packages:
path-key: 3.1.1
dev: false
/nth-check@2.1.1:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
dependencies:
boolbase: 1.0.0
dev: true
/object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
@ -4344,7 +4611,6 @@ packages:
error-ex: 1.3.2
json-parse-even-better-errors: 2.3.1
lines-and-columns: 1.2.4
dev: false
/pascal-case@3.1.2:
resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==}
@ -4575,6 +4841,10 @@ packages:
typescript: 4.9.5
dev: false
/proxy-compare@2.5.1:
resolution: {integrity: sha512-oyfc0Tx87Cpwva5ZXezSp5V9vht1c7dZBhvuV/y3ctkgMVUmiAGDVeeB0dKhGSyT0v1ZTEQYpe/RXlBVBNuCLA==}
dev: false
/punycode@2.3.0:
resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==}
engines: {node: '>=6'}
@ -5256,6 +5526,23 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
/svg-parser@2.0.4:
resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==}
dev: true
/svgo@3.0.2:
resolution: {integrity: sha512-Z706C1U2pb1+JGP48fbazf3KxHrWOsLme6Rv7imFBn5EnuanDW1GPaA/P1/dvObE670JDePC3mnj0k0B7P0jjQ==}
engines: {node: '>=14.0.0'}
hasBin: true
dependencies:
'@trysound/sax': 0.2.0
commander: 7.2.0
css-select: 5.1.0
css-tree: 2.3.1
csso: 5.0.5
picocolors: 1.0.0
dev: true
/tailwindcss@3.3.2:
resolution: {integrity: sha512-9jPkMiIBXvPc2KywkraqsUfbfj+dHDb+JPWtSJa9MLFdrPyazI7q6WX2sUrm7R9eVR7qqv3Pas7EvQFzxKnI6w==}
engines: {node: '>=14.0.0'}
@ -5483,6 +5770,34 @@ packages:
convert-source-map: 1.9.0
dev: false
/valtio@1.11.1(react@18.2.0):
resolution: {integrity: sha512-sTKWY1e1AVUu4sY9CimoSZpufAsAXO+fzZrw0X5xtijEmDDQaPPLHZxlpONUpTLtvxPjpQURCSdUuUyBszoEOg==}
engines: {node: '>=12.20.0'}
peerDependencies:
react: '>=16.8'
peerDependenciesMeta:
react:
optional: true
dependencies:
proxy-compare: 2.5.1
react: 18.2.0
use-sync-external-store: 1.2.0(react@18.2.0)
dev: false
/vite-plugin-svgr@3.2.0(vite@4.3.5):
resolution: {integrity: sha512-Uvq6niTvhqJU6ga78qLKBFJSDvxWhOnyfQSoKpDPMAGxJPo5S3+9hyjExE5YDj6Lpa4uaLkGc1cBgxXov+LjSw==}
peerDependencies:
vite: ^2.6.0 || 3 || 4
dependencies:
'@rollup/pluginutils': 5.0.2
'@svgr/core': 7.0.0
'@svgr/plugin-jsx': 7.0.0
vite: 4.3.5(@types/node@18.16.9)
transitivePeerDependencies:
- rollup
- supports-color
dev: true
/vite@4.3.5(@types/node@18.16.9):
resolution: {integrity: sha512-0gEnL9wiRFxgz40o/i/eTBwm+NEbpUeTWhzKrZDSdKm6nplj+z4lKz8ANDgildxHm47Vg8EUia0aicKbawUVVA==}
engines: {node: ^14.18.0 || >=16.0.0}

View File

@ -6,7 +6,7 @@ import { ColorPalette } from '$app/components/tests/ColorPalette';
import { TestAPI } from '$app/components/tests/TestAPI';
import { DocumentPage } from '$app/views/DocumentPage';
import { BoardPage } from '$app/views/BoardPage';
import { GridPage } from '$app/views/GridPage';
import { DatabasePage } from '$app/views/DatabasePage';
import { LoginPage } from '$app/views/LoginPage';
import { GetStarted } from '$app/components/auth/GetStarted/GetStarted';
import { SignUpPage } from '$app/views/SignUpPage';
@ -29,7 +29,7 @@ function AppMain() {
<Route path={'/page/api-test'} element={<TestAPI />} />
<Route path={'/page/document/:id'} element={<DocumentPage />} />
<Route path={'/page/board/:id'} element={<BoardPage />} />
<Route path={'/page/grid/:id'} element={<GridPage />} />
<Route path={'/page/grid/:id'} element={<DatabasePage />} />
<Route path={'/trash'} id={'trash'} element={<TrashPage />} />
</Route>
<Route path={'/auth/login'} element={<LoginPage />}></Route>

View File

@ -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="M8 4C7.72386 4 7.5 4.22386 7.5 4.5V7.5H4.5C4.22386 7.5 4 7.72386 4 8C4 8.27614 4.22386 8.5 4.5 8.5H7.5V11.5C7.5 11.7761 7.72386 12 8 12C8.27614 12 8.5 11.7761 8.5 11.5V8.5H11.5C11.7761 8.5 12 8.27614 12 8C12 7.72386 11.7761 7.5 11.5 7.5H8.5V4.5C8.5 4.22386 8.27614 4 8 4Z" fill="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 402 B

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.9743 6.33203H7.35889C6.79245 6.33203 6.33325 6.79123 6.33325 7.35767V11.9731C6.33325 12.5395 6.79245 12.9987 7.35889 12.9987H11.9743C12.5407 12.9987 12.9999 12.5395 12.9999 11.9731V7.35767C12.9999 6.79123 12.5407 6.33203 11.9743 6.33203Z" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4.53846 9.66667H4.02564C3.75362 9.66667 3.49275 9.55861 3.3004 9.36626C3.10806 9.17392 3 8.91304 3 8.64103V4.02564C3 3.75362 3.10806 3.49275 3.3004 3.3004C3.49275 3.10806 3.75362 3 4.02564 3H8.64103C8.91304 3 9.17392 3.10806 9.36626 3.3004C9.55861 3.49275 9.66667 3.75362 9.66667 4.02564V4.53846" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 794 B

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="2" y="2" width="12" height="12" rx="4" fill="currentColor"/>
<path d="M6 8L7.61538 9.5L10.5 6.5" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 273 B

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="2.5" y="2.5" width="11" height="11" rx="3.5" stroke="#BDBDBD"/>
</svg>

After

Width:  |  Height:  |  Size: 176 B

View File

@ -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="M13 7.688L8.27223 12.1469C7.69304 12.6931 6.90749 13 6.0884 13C5.26931 13 4.48376 12.6931 3.90457 12.1469C3.32538 11.6006 3 10.8598 3 10.0873C3 9.31474 3.32538 8.57387 3.90457 8.02763L8.63234 3.56875C9.01847 3.20459 9.54216 3 10.0882 3C10.6343 3 11.158 3.20459 11.5441 3.56875C11.9302 3.93291 12.1472 4.42683 12.1472 4.94183C12.1472 5.45684 11.9302 5.95075 11.5441 6.31491L6.8112 10.7738C6.61814 10.9559 6.35629 11.0582 6.08326 11.0582C5.81022 11.0582 5.54838 10.9559 5.35531 10.7738C5.16225 10.5917 5.05379 10.3448 5.05379 10.0873C5.05379 9.82975 5.16225 9.58279 5.35531 9.40071L9.72297 5.28632" stroke="#333333" stroke-width="0.9989" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 797 B

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.5 8L8.11538 9.5L13.5 4.5" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13 8.5V11.8889C13 12.1836 12.8829 12.4662 12.6746 12.6746C12.4662 12.8829 12.1836 13 11.8889 13H4.11111C3.81643 13 3.53381 12.8829 3.32544 12.6746C3.11706 12.4662 3 12.1836 3 11.8889V4.11111C3 3.81643 3.11706 3.53381 3.32544 3.32544C3.53381 3.11706 3.81643 3 4.11111 3H10.2222" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 561 B

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.5 8L8.11538 9.5L13.5 4.5" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.5 8C13.5 11.0376 11.0376 13.5 8 13.5C4.96243 13.5 2.5 11.0376 2.5 8C2.5 4.96243 4.96243 2.5 8 2.5C8.81896 2.5 9.59612 2.679 10.2945 3" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 421 B

View File

@ -0,0 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.8889 3.5H4.11111C3.49746 3.5 3 3.94772 3 4.5V11.5C3 12.0523 3.49746 12.5 4.11111 12.5H11.8889C12.5025 12.5 13 12.0523 13 11.5V4.5C13 3.94772 12.5025 3.5 11.8889 3.5Z" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 2.5V4.58181" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 2.5V4.58181" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 6.5H13" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 618 B

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 13C10.7614 13 13 10.7614 13 8C13 5.23858 10.7614 3 8 3C5.23858 3 3 5.23858 3 8C3 10.7614 5.23858 13 8 13Z" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 5V8L10 9" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 376 B

View File

@ -0,0 +1,8 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.5 4L12.5 4" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.5 8H12.5" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.5 12H12.5" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="4" cy="4" r="0.5" fill="#333333"/>
<circle cx="4" cy="8" r="0.5" fill="#333333"/>
<circle cx="4" cy="12" r="0.5" fill="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 512 B

View File

@ -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="M2.201 6.4H3.001V12H2.081V7.384L0.953 7.704L0.729 6.92L2.201 6.4ZM3.91156 12V11.1L6.35156 8.61C6.9449 8.01667 7.24156 7.50333 7.24156 7.07C7.24156 6.73 7.13823 6.46667 6.93156 6.28C6.73156 6.08667 6.4749 5.99 6.16156 5.99C5.5749 5.99 5.14156 6.28 4.86156 6.86L3.89156 6.29C4.11156 5.82333 4.42156 5.47 4.82156 5.23C5.22156 4.99 5.6649 4.87 6.15156 4.87C6.7649 4.87 7.29156 5.06333 7.73156 5.45C8.17156 5.83667 8.39156 6.36333 8.39156 7.03C8.39156 7.74333 7.9949 8.50333 7.20156 9.31L5.62156 10.89H8.52156V12H3.91156ZM12.9025 7.032C13.5105 7.176 14.0025 7.46 14.3785 7.884C14.7625 8.3 14.9545 8.824 14.9545 9.456C14.9545 10.296 14.6705 10.956 14.1025 11.436C13.5345 11.916 12.8385 12.156 12.0145 12.156C11.3745 12.156 10.7985 12.008 10.2865 11.712C9.78253 11.416 9.41853 10.984 9.19453 10.416L10.3705 9.732C10.6185 10.452 11.1665 10.812 12.0145 10.812C12.4945 10.812 12.8745 10.692 13.1545 10.452C13.4345 10.204 13.5745 9.872 13.5745 9.456C13.5745 9.04 13.4345 8.712 13.1545 8.472C12.8745 8.232 12.4945 8.112 12.0145 8.112H11.7025L11.1505 7.284L12.9625 4.896H9.44653V3.6H14.6065V4.776L12.9025 7.032Z" fill="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="5" r="2.5" stroke="#333333"/>
<path d="M3 13C3 10.2386 5.23858 8 8 8C10.7614 8 13 10.2386 13 13" stroke="#333333" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 261 B

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.78787 8.78787L6.51213 7.51213C6.32314 7.32314 6.45699 7 6.72426 7H9.27574C9.54301 7 9.67686 7.32314 9.48787 7.51213L8.21213 8.78787C8.09497 8.90503 7.90503 8.90503 7.78787 8.78787Z" fill="#333333"/>
<path d="M8 13C10.7614 13 13 10.7614 13 8C13 5.23858 10.7614 3 8 3C5.23858 3 3 5.23858 3 8C3 10.7614 5.23858 13 8 13Z" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 499 B

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.15625 11.8359L6.43768 9.85414H2.46662L1.74805 11.8359H0.5L3.7903 3H5.11399L8.4043 11.8359H7.15625ZM2.87003 8.75596H6.03427L4.44584 4.40112L2.87003 8.75596Z" fill="#333333"/>
<path d="M14.4032 5.52454H15.5V11.8359H14.4032V10.7504C13.8569 11.5835 13.0627 12 12.0206 12C11.1381 12 10.386 11.6802 9.76403 11.0407C9.14211 10.3927 8.83114 9.60589 8.83114 8.68022C8.83114 7.75456 9.14211 6.97195 9.76403 6.3324C10.386 5.68443 11.1381 5.36045 12.0206 5.36045C13.0627 5.36045 13.8569 5.777 14.4032 6.6101V5.52454ZM12.1593 10.9397C12.798 10.9397 13.3317 10.7251 13.7603 10.2959C14.1889 9.85835 14.4032 9.31978 14.4032 8.68022C14.4032 8.04067 14.1889 7.50631 13.7603 7.07714C13.3317 6.63955 12.798 6.42076 12.1593 6.42076C11.5289 6.42076 10.9995 6.63955 10.5708 7.07714C10.1422 7.50631 9.92791 8.04067 9.92791 8.68022C9.92791 9.31978 10.1422 9.85835 10.5708 10.2959C10.9995 10.7251 11.5289 10.9397 12.1593 10.9397Z" fill="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -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="M13 7.688L8.27223 12.1469C7.69304 12.6931 6.90749 13 6.0884 13C5.26931 13 4.48376 12.6931 3.90457 12.1469C3.32538 11.6006 3 10.8598 3 10.0873C3 9.31474 3.32538 8.57387 3.90457 8.02763L8.63234 3.56875C9.01847 3.20459 9.54216 3 10.0882 3C10.6343 3 11.158 3.20459 11.5441 3.56875C11.9302 3.93291 12.1472 4.42683 12.1472 4.94183C12.1472 5.45684 11.9302 5.95075 11.5441 6.31491L6.8112 10.7738C6.61814 10.9559 6.35629 11.0582 6.08326 11.0582C5.81022 11.0582 5.54838 10.9559 5.35531 10.7738C5.16225 10.5917 5.05379 10.3448 5.05379 10.0873C5.05379 9.82975 5.16225 9.58279 5.35531 9.40071L9.72297 5.28632" stroke="#333333" stroke-width="0.9989" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 797 B

View File

@ -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="M5 8.2L6.84615 10L13 4" stroke="#00BCF0" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 202 B

View File

@ -0,0 +1,6 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 4.40039H4.11111H13" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.77775 4.4V3.2C5.77775 2.88174 5.89481 2.57652 6.10319 2.35147C6.31156 2.12643 6.59418 2 6.88886 2H9.11108C9.40577 2 9.68838 2.12643 9.89676 2.35147C10.1051 2.57652 10.2222 2.88174 10.2222 3.2V4.4M11.8889 4.4V12.8C11.8889 13.1183 11.7718 13.4235 11.5634 13.6485C11.3551 13.8736 11.0724 14 10.7778 14H5.2222C4.92751 14 4.64489 13.8736 4.43652 13.6485C4.22815 13.4235 4.11108 13.1183 4.11108 12.8V4.4H11.8889Z" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.88892 7.40039V11.0004" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.11108 7.40039V11.0004" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 889 B

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="6" r="1" fill="#333333"/>
<circle cx="8" cy="10" r="1" fill="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 194 B

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.1224 11.5847C5.92231 12.1165 6.88514 12.5 8 12.5C10.484 12.5 12.2132 10.596 13.0673 9.39083C13.6621 8.55146 13.6621 7.44854 13.0673 6.60917C12.7339 6.13867 12.2671 5.56168 11.6741 5.03305L10.9658 5.74127C11.4906 6.20089 11.9223 6.72296 12.2514 7.18736C12.6008 7.68035 12.6008 8.31965 12.2514 8.81264C11.4273 9.97552 9.95966 11.5 8 11.5C7.19594 11.5 6.47471 11.2434 5.84677 10.8603L5.1224 11.5847ZM5.03417 10.2587L4.32594 10.967C3.73292 10.4383 3.26612 9.86133 2.93269 9.39083C2.33787 8.55146 2.33787 7.44854 2.93269 6.60917C3.78677 5.40397 5.51603 3.5 8 3.5C9.11486 3.5 10.0777 3.88354 10.8776 4.4153L10.1532 5.13966C9.52529 4.75665 8.80406 4.5 8 4.5C6.04034 4.5 4.57268 6.02448 3.74859 7.18736C3.39923 7.68035 3.39923 8.31965 3.74859 8.81264C4.0777 9.27704 4.50944 9.79911 5.03417 10.2587ZM6.99245 9.71466C7.28524 9.8954 7.62928 10 8.00012 10C9.09398 10 9.95466 9.08996 9.95466 8C9.95466 7.64165 9.86163 7.30275 9.69786 7.00924L8.93093 7.77618C8.94644 7.84779 8.95466 7.92265 8.95466 8C8.95466 8.5669 8.51291 9 8.00012 9C7.912 9 7.82599 8.98721 7.74395 8.96316L6.99245 9.71466ZM7.06926 8.22363L6.30229 8.99061C6.13857 8.69713 6.04557 8.35829 6.04557 8C6.04557 6.91005 6.90626 6 8.00012 6C8.3709 6 8.71489 6.10456 9.00764 6.28525L8.2561 7.03679C8.17412 7.01277 8.08817 7 8.00012 7C7.48733 7 7.04557 7.4331 7.04557 8C7.04557 8.07728 7.05378 8.15208 7.06926 8.22363Z" fill="#333333"/>
<path d="M11.6667 3.33398L3.33341 11.6673" stroke="#333333" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 11.7778L3 4" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.5 4.5L6 8L9.5 11.5" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 8L13 8" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 378 B

View File

@ -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="M9.39568 7.6963L6.91032 5.56599C6.65085 5.34358 6.25 5.52795 6.25 5.86969L6.25 10.1303C6.25 10.4721 6.65085 10.6564 6.91032 10.434L9.39568 8.3037C9.58192 8.14406 9.58192 7.85594 9.39568 7.6963Z" fill="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 324 B

View File

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13 11.7778L13 4" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.5 4.5L10 8L6.5 11.5" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 8L3 8" stroke="#333333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 381 B

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="7.5" cy="7.5" r="5" stroke="#4F4F4F"/>
<path d="M12.6464 13.354C12.8416 13.5493 13.1582 13.5493 13.3535 13.3541C13.5488 13.1588 13.5488 12.8422 13.3536 12.647L12.6464 13.354ZM10.6464 11.3535L12.6464 13.354L13.3536 12.647L11.3536 10.6465L10.6464 11.3535Z" fill="#4F4F4F"/>
</svg>

After

Width:  |  Height:  |  Size: 387 B

View File

@ -0,0 +1,4 @@
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.01471 2.15012C7.62441 1.7981 8.37559 1.7981 8.98529 2.15012L12.5769 4.22372C13.1866 4.57573 13.5622 5.22628 13.5622 5.9303V10.0775C13.5622 10.7815 13.1866 11.4321 12.5769 11.7841L8.98529 13.8577C8.37559 14.2097 7.62441 14.2097 7.01471 13.8577L3.42312 11.7841C2.81341 11.4321 2.43782 10.7815 2.43782 10.0775V5.9303C2.43782 5.22628 2.81341 4.57573 3.42312 4.22372L7.01471 2.15012Z" stroke="#333333"/>
<circle cx="8" cy="8.00391" r="2.5" stroke="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 569 B

View File

@ -0,0 +1,16 @@
import { Database } from '$app/interfaces/database';
import { DatabaseLayoutPB } from '@/services/backend';
import { RefObject, createContext, createRef } from 'react';
import { proxy } from 'valtio';
export const VerticalScrollElementRefContext = createContext<RefObject<Element>>(createRef());
export const DatabaseContext = createContext<Database>(proxy({
id: '',
viewId: '',
name: '',
fields: [],
rows: [],
layoutType: DatabaseLayoutPB.Grid,
layoutSetting: {},
isLinked: false,
}));

View File

@ -0,0 +1,138 @@
import { useContext, useEffect, useMemo } from 'react';
import { proxy, useSnapshot } from 'valtio';
import { useParams } from 'react-router-dom';
import { DatabaseLayoutPB, DatabaseNotification } from '@/services/backend';
import { type Database, fieldPbToField } from '$app/interfaces/database';
import { subscribeNotifications } from '$app/hooks';
import { DatabaseContext } from './database.context';
import * as service from './database_bd_svc';
export const useDatabase = () => useSnapshot(useContext(DatabaseContext));
export const useViewId = () => useParams().id!;
const fetchDatabase = async (viewId: string) => {
const [
databasePb,
settingPb,
] = await Promise.all([
service.getDatabase(viewId),
service.getDatabaseSetting(viewId),
]);
const fieldsPb = await service.getFields(viewId, databasePb.fields.map(field => field.field_id));
const database: Database = {
id: databasePb.id,
viewId: viewId,
name: '',
layoutType: databasePb.layout_type,
layoutSetting: {},
isLinked: databasePb.is_linked,
fields: fieldsPb.map(fieldPbToField),
rows: databasePb.rows.map(row => ({
id: row.id,
documentId: row.document_id,
icon: row.icon,
cover: row.cover,
})),
};
if (settingPb.layout_type === DatabaseLayoutPB.Grid) {
const layoutSetting: Database.GridLayoutSetting = {};
if (settingPb.has_filters) {
layoutSetting.filters = settingPb.filters.items.map(filter => ({
id: filter.id,
fieldId: filter.field_id,
fieldType: filter.field_type,
data: filter.data,
}));
}
if (settingPb.has_sorts) {
layoutSetting.sorts = settingPb.sorts.items.map(sort => ({
id: sort.id,
fieldId: sort.field_id,
fieldType: sort.field_type,
condition: sort.condition,
}));
}
if (settingPb.has_group_settings) {
layoutSetting.groups = settingPb.group_settings.items.map(group => ({
id: group.id,
fieldId: group.field_id,
}));
}
database.layoutSetting = layoutSetting;
}
return database;
};
export const useConnectDatabase = (viewId: string) => {
const database = useMemo(() => {
const proxyDatabase = proxy<Database>({
id: '',
viewId,
name: '',
isLinked: false,
layoutType: DatabaseLayoutPB.Grid,
layoutSetting: {},
rows: [],
fields: [],
});
void fetchDatabase(viewId).then(value => Object.assign(proxyDatabase, value));
return proxyDatabase;
}, [viewId]);
useEffect(() => {
const unsubscribePromise = subscribeNotifications({
[DatabaseNotification.DidUpdateFields]: async (result) => {
if (result.err) {
return;
}
const { fields: fieldIds } = await service.getDatabase(viewId);
const newFieldsPb = await service.getFields(viewId, fieldIds.map(field => field.field_id));
database.fields = newFieldsPb.map(fieldPbToField);
},
[DatabaseNotification.DidUpdateViewRows]:async (result) => {
if (result.err) {
return;
}
const {
deleted_rows: deletedRowIds,
inserted_rows: insertedRows,
// TODO: updated_rows: updatedRows,
} = result.val;
deletedRowIds.forEach(rowId => {
const index = database.rows.findIndex(row => row.id === rowId);
if (index !== -1) {
database.rows.splice(index, 1);
}
});
insertedRows.forEach(({ index, row_meta: rowMeta }) => {
database.rows.splice(index, 0, {
id: rowMeta.id,
documentId: rowMeta.document_id,
cover: rowMeta.cover,
icon: rowMeta.icon,
});
});
}
}, { id: viewId });
return () => void unsubscribePromise.then(unsubscribe => unsubscribe());
}, [viewId, database]);
return database;
};

View File

@ -0,0 +1,697 @@
import { Database } from '$app/interfaces/database';
import {
DatabaseDescriptionPB,
DatabasePB,
DatabaseViewIdPB,
DatabaseViewSettingPB,
DatabaseLayoutPB,
CreateDatabaseViewPayloadPB,
DatabaseLayoutMetaPB,
LayoutSettingChangesetPB,
GroupPB,
DatabaseGroupIdPB,
GroupByFieldPayloadPB,
UpdateGroupPB,
MoveGroupPayloadPB,
FieldPB,
GetFieldPayloadPB,
RepeatedFieldIdPB,
DuplicateFieldPayloadPB,
FieldChangesetPB,
FieldType,
UpdateFieldTypePayloadPB,
MoveFieldPayloadPB,
DeleteFieldPayloadPB,
TypeOptionPB,
TypeOptionPathPB,
CreateFieldPayloadPB,
TypeOptionChangesetPB,
SelectOptionPB,
CreateSelectOptionPayloadPB,
RepeatedSelectOptionPayload,
RowIdPB,
RowMetaPB,
CreateRowPayloadPB,
MoveGroupRowPayloadPB,
UpdateRowMetaChangesetPB,
CellPB,
CellIdPB,
CellChangesetPB,
SelectOptionCellDataPB,
SelectOptionCellChangesetPB,
ChecklistCellDataPB,
ChecklistCellDataChangesetPB,
DateChangesetPB,
DatabaseLayoutSettingPB,
RowPB,
DatabaseExportDataPB,
DatabaseSnapshotPB,
FilterPB,
SortPB,
} from '@/services/backend';
import {
DatabaseEventGetDatabases,
DatabaseEventGetDatabase,
DatabaseEventGetDatabaseSetting,
DatabaseEventCreateDatabaseView,
DatabaseEventGetLayoutSetting,
DatabaseEventSetLayoutSetting,
DatabaseEventGetGroups,
DatabaseEventGetGroup,
DatabaseEventSetGroupByField,
DatabaseEventUpdateGroup,
DatabaseEventMoveGroup,
DatabaseEventGetFields,
DatabaseEventGetPrimaryField,
DatabaseEventDuplicateField,
DatabaseEventUpdateField,
DatabaseEventUpdateFieldType,
DatabaseEventMoveField,
DatabaseEventDeleteField,
DatabaseEventGetTypeOption,
DatabaseEventCreateTypeOption,
DatabaseEventUpdateFieldTypeOption,
DatabaseEventCreateSelectOption,
DatabaseEventInsertOrUpdateSelectOption,
DatabaseEventDeleteSelectOption,
DatabaseEventGetRow,
DatabaseEventCreateRow,
DatabaseEventDuplicateRow,
DatabaseEventDeleteRow,
DatabaseEventMoveGroupRow,
DatabaseEventGetRowMeta,
DatabaseEventUpdateRowMeta,
DatabaseEventGetCell,
DatabaseEventUpdateCell,
DatabaseEventGetSelectOptionCellData,
DatabaseEventUpdateSelectOptionCell,
DatabaseEventGetChecklistCellData,
DatabaseEventUpdateChecklistCell,
DatabaseEventUpdateDateCell,
DatabaseEventExportCSV,
DatabaseEventGetDatabaseSnapshots,
DatabaseEventGetAllFilters,
DatabaseEventGetAllSorts,
} from "@/services/backend/events/flowy-database2";
export async function getDatabases(): Promise<DatabaseDescriptionPB[]> {
const result = await DatabaseEventGetDatabases();
return result.map(value => value.items).unwrap();
}
export async function getDatabase(viewId: string): Promise<DatabasePB> {
const payload = DatabaseViewIdPB.fromObject({
value: viewId,
});
const result = await DatabaseEventGetDatabase(payload);
return result.unwrap();
}
export async function getDatabaseSetting(viewId: string): Promise<DatabaseViewSettingPB> {
const payload = DatabaseViewIdPB.fromObject({
value: viewId,
});
const result = await DatabaseEventGetDatabaseSetting(payload);
return result.unwrap();
}
export async function createDatabaseView(
viewId: string,
data?: {
name?: string;
layout?: DatabaseLayoutPB;
},
): Promise<void> {
const payload = CreateDatabaseViewPayloadPB.fromObject({
view_id: viewId,
name: data?.name,
layout_type: data?.layout,
});
const result = await DatabaseEventCreateDatabaseView(payload);
return result.unwrap();
}
export async function getLayoutSetting(viewId: string, layout: DatabaseLayoutPB): Promise<DatabaseLayoutSettingPB> {
const payload = DatabaseLayoutMetaPB.fromObject({
view_id: viewId,
layout: layout,
});
const result = await DatabaseEventGetLayoutSetting(payload);
return result.unwrap();
}
export async function setLayoutSetting(viewId: string, setting: {
layoutType?: DatabaseLayoutPB;
calendar?: Database.CalendarLayoutSetting;
}): Promise<void> {
const payload = LayoutSettingChangesetPB.fromObject({
view_id: viewId,
layout_type: setting.layoutType,
calendar: setting.calendar ? {
field_id: setting.calendar.fieldId,
layout_ty: setting.calendar.layoutTy,
first_day_of_week: setting.calendar.firstDayOfWeek,
show_weekends: setting.calendar.showWeekends,
show_week_numbers: setting.calendar.showWeekNumbers,
} : undefined,
});
const result = await DatabaseEventSetLayoutSetting(payload);
return result.unwrap();
}
export async function getGroups(viewId: string): Promise<GroupPB[]> {
const payload = DatabaseViewIdPB.fromObject({
value: viewId,
});
const result = await DatabaseEventGetGroups(payload);
return result.map(value => value.items).unwrap();
}
export async function getGroup(viewId: string, groupId: string): Promise<GroupPB> {
const payload = DatabaseGroupIdPB.fromObject({
view_id: viewId,
group_id: groupId,
});
const result = await DatabaseEventGetGroup(payload);
return result.unwrap();
}
export async function setGroupByField(viewId: string, fieldId: string): Promise<void> {
const payload = GroupByFieldPayloadPB.fromObject({
view_id: viewId,
field_id: fieldId,
});
const result = await DatabaseEventSetGroupByField(payload);
return result.unwrap();
}
export async function updateGroup(
viewId: string,
groupId: string,
data: {
name?: string;
visible?: boolean;
},
): Promise<void> {
const payload = UpdateGroupPB.fromObject({
view_id: viewId,
group_id: groupId,
name: data.name,
visible: data.visible,
});
const result = await DatabaseEventUpdateGroup(payload);
return result.unwrap();
}
export async function moveGroup(viewId: string, fromGroupId: string, toGroupId: string): Promise<void> {
const payload = MoveGroupPayloadPB.fromObject({
view_id: viewId,
from_group_id: fromGroupId,
to_group_id: toGroupId,
});
const result = await DatabaseEventMoveGroup(payload);
return result.unwrap();
}
export async function getFilters(viewId: string): Promise<FilterPB[]> {
const payload = DatabaseViewIdPB.fromObject({
value: viewId,
});
const result = await DatabaseEventGetAllFilters(payload);
return result.map(value => value.items).unwrap();
}
export async function getSorts(viewId: string): Promise<SortPB[]> {
const payload = DatabaseViewIdPB.fromObject({
value: viewId,
});
const result = await DatabaseEventGetAllSorts(payload);
return result.map(value => value.items).unwrap();
}
export async function getFields(viewId: string, fieldIds?: string[]): Promise<FieldPB[]> {
const payload = GetFieldPayloadPB.fromObject({
view_id: viewId,
field_ids: fieldIds ? RepeatedFieldIdPB.fromObject({
items: fieldIds.map(fieldId => ({ field_id: fieldId })),
}) : undefined,
});
const result = await DatabaseEventGetFields(payload);
return result.map((value) => value.items).unwrap();
}
export async function getPrimaryField(viewId: string): Promise<FieldPB> {
const payload = DatabaseViewIdPB.fromObject({
value: viewId,
});
const result = await DatabaseEventGetPrimaryField(payload);
return result.unwrap();
}
export async function duplicateField(viewId: string, fieldId: string): Promise<void> {
const payload = DuplicateFieldPayloadPB.fromObject({
view_id: viewId,
field_id: fieldId,
});
const result = await DatabaseEventDuplicateField(payload);
return result.unwrap();
}
export async function updateField(viewId: string, fieldId: string, data: {
name?: string;
desc?: string;
frozen?: boolean;
visibility?: boolean;
width?: number;
}): Promise<void> {
const payload = FieldChangesetPB.fromObject({
view_id: viewId,
field_id: fieldId,
...data,
});
const result = await DatabaseEventUpdateField(payload);
return result.unwrap();
}
export async function updateFieldType(viewId: string, fieldId: string, fieldType: FieldType): Promise<void> {
const payload = UpdateFieldTypePayloadPB.fromObject({
view_id: viewId,
field_id: fieldId,
field_type: fieldType,
});
const result = await DatabaseEventUpdateFieldType(payload);
return result.unwrap();
}
export async function moveField(viewId: string, fieldId: string, fromIndex: number, toIndex: number): Promise<void> {
const payload = MoveFieldPayloadPB.fromObject({
view_id: viewId,
field_id: fieldId,
from_index: fromIndex,
to_index: toIndex,
});
const result = await DatabaseEventMoveField(payload);
return result.unwrap();
}
export async function deleteField(viewId: string, fieldId: string): Promise<void> {
const payload = DeleteFieldPayloadPB.fromObject({
view_id: viewId,
field_id: fieldId,
});
const result = await DatabaseEventDeleteField(payload);
return result.unwrap();
}
export async function getFieldTypeOption(viewId: string, fieldId: string, fieldType?: FieldType): Promise<TypeOptionPB> {
const payload = TypeOptionPathPB.fromObject({
view_id: viewId,
field_id: fieldId,
field_type: fieldType,
});
const result = await DatabaseEventGetTypeOption(payload);
return result.unwrap();
}
/**
* TODO data type need to clarify
*/
export async function createFieldTypeOption(viewId: string, fieldType: FieldType, data?: Uint8Array): Promise<TypeOptionPB> {
const payload = CreateFieldPayloadPB.fromObject({
view_id: viewId,
field_type: fieldType,
type_option_data: data,
});
const result = await DatabaseEventCreateTypeOption(payload);
return result.unwrap();
}
/**
* TODO data type need to clarify
*/
export async function updateFieldTypeOption(viewId: string, fieldId: string, data: Uint8Array): Promise<void> {
const payload = TypeOptionChangesetPB.fromObject({
view_id: viewId,
field_id: fieldId,
type_option_data: data,
});
const result = await DatabaseEventUpdateFieldTypeOption(payload);
return result.unwrap();
}
export async function createSelectOption(viewId: string, fieldId: string, optionName: string): Promise<SelectOptionPB> {
const payload = CreateSelectOptionPayloadPB.fromObject({
view_id: viewId,
field_id: fieldId,
option_name: optionName,
});
const result = await DatabaseEventCreateSelectOption(payload);
return result.unwrap();
}
/**
* @param [rowId] If pass the rowId, the cell will select this option after insert or update.
*/
export async function insertOrUpdateSelectOption(
viewId: string,
fieldId: string,
items: Partial<Database.SelectOption>[],
rowId?: string,
): Promise<void> {
const payload = RepeatedSelectOptionPayload.fromObject({
view_id: viewId,
field_id: fieldId,
row_id: rowId,
items: items,
});
const result = await DatabaseEventInsertOrUpdateSelectOption(payload);
return result.unwrap();
}
export async function deleteSelectOption(
viewId: string,
fieldId: string,
items: Partial<Database.SelectOption>[],
rowId?: string,
): Promise<void> {
const payload = RepeatedSelectOptionPayload.fromObject({
view_id: viewId,
field_id: fieldId,
row_id: rowId,
items: items,
});
const result = await DatabaseEventDeleteSelectOption(payload);
return result.unwrap();
}
export async function getRow(viewId: string, rowId: string, groupId?: string): Promise<RowPB | undefined> {
const payload = RowIdPB.fromObject({
view_id: viewId,
row_id: rowId,
group_id: groupId,
});
const result = await DatabaseEventGetRow(payload);
return result.map(value => value.row).unwrap();
}
export async function createRow(viewId: string, params?: {
startRowId?: string;
groupId?: string;
data?: Record<string, string>;
}): Promise<RowMetaPB> {
const payload = CreateRowPayloadPB.fromObject({
view_id: viewId,
start_row_id: params?.startRowId,
group_id: params?.groupId,
data: params?.data ? { cell_data_by_field_id: params.data } : undefined,
});
const result = await DatabaseEventCreateRow(payload);
return result.unwrap();
}
export async function duplicateRow(viewId: string, rowId: string, groupId?: string): Promise<void> {
const payload = RowIdPB.fromObject({
view_id: viewId,
row_id: rowId,
group_id: groupId,
});
const result = await DatabaseEventDuplicateRow(payload);
return result.unwrap();
}
export async function deleteRow(viewId: string, rowId: string, groupId?: string): Promise<void> {
const payload = RowIdPB.fromObject({
view_id: viewId,
row_id: rowId,
group_id: groupId,
});
const result = await DatabaseEventDeleteRow(payload);
return result.unwrap();
}
/**
* Move the row from one group to another group
*
* @param fromRowId
* @param toGroupId
* @param toRowId used to locate the moving row location.
* @returns
*/
export async function moveGroupRow(viewId: string, fromRowId: string, toGroupId: string, toRowId?: string): Promise<void> {
const payload = MoveGroupRowPayloadPB.fromObject({
view_id: viewId,
from_row_id: fromRowId,
to_group_id: toGroupId,
to_row_id: toRowId,
});
const result = await DatabaseEventMoveGroupRow(payload);
return result.unwrap();
}
export async function getRowMeta(viewId: string, rowId: string, groupId?: string): Promise<RowMetaPB> {
const payload = RowIdPB.fromObject({
view_id: viewId,
row_id: rowId,
group_id: groupId,
});
const result = await DatabaseEventGetRowMeta(payload);
return result.unwrap();
}
export async function updateRowMeta(
viewId: string,
rowId: string,
meta: {
iconUrl?: string;
coverUrl?: string;
},
): Promise<void> {
const payload = UpdateRowMetaChangesetPB.fromObject({
view_id: viewId,
id: rowId,
icon_url: meta.iconUrl,
cover_url: meta.coverUrl,
});
const result = await DatabaseEventUpdateRowMeta(payload);
return result.unwrap();
}
export async function getCell(viewId: string, rowId: string, fieldId: string): Promise<CellPB> {
const payload = CellIdPB.fromObject({
view_id: viewId,
row_id: rowId,
field_id: fieldId,
});
const result = await DatabaseEventGetCell(payload);
return result.unwrap();
}
export async function updateCell(viewId: string, rowId: string, fieldId: string, changeset: string): Promise<void> {
const payload = CellChangesetPB.fromObject({
view_id: viewId,
row_id: rowId,
field_id: fieldId,
cell_changeset: changeset,
});
const result = await DatabaseEventUpdateCell(payload);
return result.unwrap();
}
export async function getSelectOptionCell(viewId: string, rowId: string, fieldId: string): Promise<SelectOptionCellDataPB> {
const payload = CellIdPB.fromObject({
view_id: viewId,
row_id: rowId,
field_id: fieldId,
});
const result = await DatabaseEventGetSelectOptionCellData(payload);
return result.unwrap();
}
export async function updateSelectOptionCell(
viewId: string,
rowId: string,
fieldId: string,
data: {
insertOptionIds?: string[];
deleteOptionIds?: string[];
},
): Promise<void> {
const payload = SelectOptionCellChangesetPB.fromObject({
cell_identifier: {
view_id: viewId,
row_id: rowId,
field_id: fieldId,
},
insert_option_ids: data.insertOptionIds,
delete_option_ids: data.deleteOptionIds,
});
const result = await DatabaseEventUpdateSelectOptionCell(payload);
return result.unwrap();
}
export async function getChecklistCell(viewId: string, rowId: string, fieldId: string): Promise<ChecklistCellDataPB> {
const payload = CellIdPB.fromObject({
view_id: viewId,
row_id: rowId,
field_id: fieldId,
});
const result = await DatabaseEventGetChecklistCellData(payload);
return result.unwrap();
}
export async function updateChecklistCell(
viewId: string,
rowId: string,
fieldId: string,
data: {
insertOptions?: string[];
selectedOptionIds?: string[];
deleteOptionIds?: string[];
updateOptions?: Partial<Database.SelectOption>[];
},
): Promise<void> {
const payload = ChecklistCellDataChangesetPB.fromObject({
view_id: viewId,
row_id: rowId,
field_id: fieldId,
insert_options: data.insertOptions,
selected_option_ids: data.selectedOptionIds,
delete_option_ids: data.deleteOptionIds,
update_options: data.updateOptions,
});
const result = await DatabaseEventUpdateChecklistCell(payload);
return result.unwrap();
}
export async function updateDateCell(
viewId: string,
rowId: string,
fieldId: string,
data: {
date?: number;
time?: string;
includeTime?: boolean;
clearFlag?: boolean;
},
): Promise<void> {
const payload = DateChangesetPB.fromObject({
cell_id: {
view_id: viewId,
row_id: rowId,
field_id: fieldId,
},
date: data.date,
time: data.time,
include_time: data.includeTime,
clear_flag: data.clearFlag,
});
const result = await DatabaseEventUpdateDateCell(payload);
return result.unwrap();
}
export async function exportCSV(viewId: string): Promise<DatabaseExportDataPB> {
const payload = DatabaseViewIdPB.fromObject({
value: viewId,
});
const result = await DatabaseEventExportCSV(payload);
return result.unwrap();
}
export async function getDatabaseSnapshots(viewId: string): Promise<DatabaseSnapshotPB[]> {
const payload = DatabaseViewIdPB.fromObject({
value: viewId,
});
const result = await DatabaseEventGetDatabaseSnapshots(payload);
return result.map(value => value.items).unwrap();
}

View File

@ -0,0 +1,12 @@
import { FC } from 'react';
import { GridToolbar } from '../GridToolbar';
import { GridTable } from '../GridTable/GridTable';
export const Grid: FC = () => {
return (
<>
<GridToolbar />
<GridTable />
</>
);
};

View File

@ -0,0 +1 @@
export * from './Grid';

View File

@ -0,0 +1,34 @@
import { useCallback, useEffect, useState } from 'react';
import { Result } from 'ts-results';
import { DatabaseNotification, FieldType, FlowyError } from '@/services/backend';
import { Database, cellPbToCell } from '$app/interfaces/database';
import * as service from '$app/components/database/database_bd_svc';
import { useNotification } from '$app/hooks';
import { useViewId } from '../../database.hooks';
export const useCell = (rowId: string, fieldId: string, fieldType: FieldType) => {
const viewId = useViewId();
const [cell, setCell] = useState<Database.Cell | null>(null);
const fetchCell = useCallback(() => {
void service.getCell(viewId, rowId, fieldId).then(data => {
setCell(cellPbToCell(data, fieldType));
});
}, [viewId, rowId, fieldId, fieldType]);
useEffect(() => {
void fetchCell();
}, [fetchCell]);
const didUpdateCell = useCallback((result: Result<void, FlowyError>) => {
if (result.err) {
return;
}
void fetchCell();
}, [fetchCell]);
useNotification(DatabaseNotification.DidUpdateCell, didUpdateCell, { id: `${rowId}:${fieldId}` });
return cell;
};

View File

@ -0,0 +1,38 @@
import { FC, useMemo } from 'react';
import { Database } from '$app/interfaces/database';
import { FieldType } from '@/services/backend';
import { GridTextCell } from './GridTextCell';
import { GridNotSupportedCell } from './GridNotSupportedCell';
import { GridSelectCell } from './GridSelectCell';
import { useCell } from './GridCell.hooks';
import { GridCheckboxCell } from './GridCheckboxCell';
interface GridCellProps {
rowId: string;
field: Database.Field;
}
export const GridCell: FC<GridCellProps> = ({
rowId,
field,
}) => {
const cell = useCell(rowId, field.id, field.type);
const RenderCell = useMemo(() => {
switch (field.type) {
case FieldType.RichText:
return GridTextCell;
case FieldType.SingleSelect:
case FieldType.MultiSelect:
return GridSelectCell;
case FieldType.Checkbox:
return GridCheckboxCell;
default:
return GridNotSupportedCell;
}
}, [field.type]);
// TODO: find a better way to check cell type.
return <RenderCell rowId={rowId} field={field} cell={cell as any} />
};

View File

@ -0,0 +1,31 @@
import { Database } from '$app/interfaces/database';
import { Checkbox } from '@mui/material';
import { 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 * as service from '$app/components/database/database_bd_svc';
import { useViewId } from '../../database.hooks';
export const GridCheckboxCell: FC<{
rowId: string;
cell: Database.CheckboxCell | null,
field: Database.Field,
}> = ({ rowId, field, cell }) => {
const viewId = useViewId();
const handleChange = useCallback((event: React.ChangeEvent<HTMLInputElement>, checked: boolean) => {
void service.updateCell(viewId, rowId, field.id, checked ? 'Yes' : 'No');
}, [viewId, rowId, field.id ]);
return (
<div className="flex h-full items-center px-3">
<Checkbox
disableRipple
style={{ padding: 0 }}
checked={cell?.data === 'Yes'}
icon={<CheckboxUncheckSvg />}
checkedIcon={<CheckboxCheckSvg />}
onChange={handleChange}
/>
</div>
);
};

View File

@ -0,0 +1,5 @@
import { FC } from 'react';
export const GridNotSupportedCell: FC = () => {
return null
};

View File

@ -0,0 +1,22 @@
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>
);
};

View File

@ -0,0 +1,143 @@
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 { Database } from '$app/interfaces/database';
import * as service from '../../../database_bd_svc';
import { useViewId } from '../../../database.hooks';
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 GridSelectCell: FC<{
rowId: string;
field: Database.Field;
cell: Database.SelectCell | null;
}> = ({ rowId, field, cell }) => {
const viewId = useViewId();
const options = useMemo(() => cell?.data?.options ?? [], [cell?.data.options]);
const selectedIds = useMemo(() => cell?.data.selectOptions?.map(({ id }) => id) ?? [], [cell?.data.selectOptions]);
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('');
}, []);
const handleChange = (event: SelectChangeEvent<string | string[]>) => {
const { target: { value } } = event;
const current = Array.isArray(value) ? value : [value];
const prev = cell?.data.selectOptions?.map(({ id }) => id);
const deleteOptionIds = prev?.filter(id => current.find(cur => cur === id) === undefined);
void service.updateSelectOptionCell(viewId, rowId, field.id, {
insertOptionIds: current,
deleteOptionIds,
});
};
const handleNewTagClick = async () => {
const exist = options.find(option => option.name.toLowerCase() === newOptionName.toLowerCase());
if (exist) {
return service.updateSelectOptionCell(viewId, rowId, field.id, {
insertOptionIds: [exist.id],
});
}
const option = await service.createSelectOption(viewId, field.id, newOptionName);
await service.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 (
<Select
className="w-full"
classes={{
select: 'flex items-center gap-2 px-4 py-2 h-6',
}}
size="small"
value={selectedIds}
multiple={field.type === FieldType.MultiSelect}
input={<InputBase />}
IconComponent={() => null}
MenuProps={menuProps}
renderValue={renderSelectedOptions}
onChange={handleChange}
onClose={handleClose}
>
{searchInput}
<ListSubheader className="text-xs mt-4 mb-2">
{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>
);
};

View File

@ -0,0 +1,49 @@
import { FC, MouseEventHandler, useCallback, useRef, useState } from 'react';
import { IconButton } from '@mui/material';
import { Database } from '$app/interfaces/database';
import { ReactComponent as DetailsSvg } from '$app/assets/details.svg';
import { SelectOptionMenu } from './SelectOptionMenu';
import { Tag } from './Tag';
export interface SelectOptionItemProps {
option: Database.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),
}}
/>
)}
</>
);
};

View File

@ -0,0 +1,71 @@
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 { Database } from '$app/interfaces/database';
import { SelectOptionColorMap, SelectOptionColorTextMap } from './constants';
import { ReactComponent as SelectCheckSvg } from '$app/assets/database/select-check.svg';
interface SelectOptionMenuProps {
option: Database.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>
)
}

View File

@ -0,0 +1,27 @@
import { FC } from 'react';
import { Chip, ChipProps } from '@mui/material';
import { SelectOptionColorPB } from '@/services/backend';
import { SelectOptionColorMap } from './constants';
export interface TagProps extends Omit<ChipProps, 'color'> {
color?: SelectOptionColorPB | ChipProps['color'];
}
export const Tag: FC<TagProps> = ({ color, classes, ...props }) => {
return (
<Chip
{...props}
color={typeof color === 'number' ? undefined : color}
classes={{
...classes,
root: [
'rounded-md',
typeof color === 'number' ? SelectOptionColorMap[color] : '',
classes?.root,
].join(' '),
label: ['font-medium text-xs', classes?.label].join(' '),
}}
/>
)
}

View File

@ -0,0 +1,25 @@
import { SelectOptionColorPB } from "@/services/backend";
export const SelectOptionColorMap = {
[SelectOptionColorPB.Purple]: 'bg-tint-purple',
[SelectOptionColorPB.Pink]: 'bg-tint-pink',
[SelectOptionColorPB.LightPink]: 'bg-tint-red',
[SelectOptionColorPB.Orange]: 'bg-tint-orange',
[SelectOptionColorPB.Yellow]: 'bg-tint-yellow',
[SelectOptionColorPB.Lime]: 'bg-tint-lime',
[SelectOptionColorPB.Green]: 'bg-tint-green',
[SelectOptionColorPB.Aqua]: 'bg-tint-aqua',
[SelectOptionColorPB.Blue]: 'bg-tint-blue',
};
export const SelectOptionColorTextMap = {
[SelectOptionColorPB.Purple]: 'purpleColor',
[SelectOptionColorPB.Pink]: 'pinkColor',
[SelectOptionColorPB.LightPink]: 'lightPinkColor',
[SelectOptionColorPB.Orange]: 'orangeColor',
[SelectOptionColorPB.Yellow]: 'yellowColor',
[SelectOptionColorPB.Lime]: 'limeColor',
[SelectOptionColorPB.Green]: 'greenColor',
[SelectOptionColorPB.Aqua]: 'aquaColor',
[SelectOptionColorPB.Blue]: 'blueColor',
} as const;

View File

@ -0,0 +1 @@
export * from './GridSelectCell';

View File

@ -0,0 +1,78 @@
import { Popover, TextareaAutosize } from '@mui/material';
import { FC, FormEventHandler, useCallback, useEffect, useRef, useState } from 'react';
import { Database } from '$app/interfaces/database';
import * as service from '$app/components/database/database_bd_svc';
import { useViewId } from '../../database.hooks';
export const GridTextCell: FC<{
rowId: string;
field: Database.Field,
cell: Database.TextCell | null;
}> = ({ rowId, field, cell }) => {
const viewId = useViewId();
const [ editing, setEditing ] = useState(false);
const [ text, setText ] = useState('');
const [ width, setWidth ] = useState<number | undefined>(undefined);
const cellRef = useRef<HTMLDivElement>(null);
const handleClose = () => {
if (editing) {
if (text !== cell?.data) {
void service.updateCell(viewId, rowId, field.id, text);
}
setEditing(false);
}
};
const handleDoubleClick = useCallback(() => {
setText(cell?.data ?? '');
setEditing(true);
}, [cell?.data]);
const handleInput = useCallback<FormEventHandler<HTMLTextAreaElement>>((event) => {
setText((event.target as HTMLTextAreaElement).value);
}, []);
useEffect(() => {
if (cellRef.current) {
setWidth(cellRef.current.clientWidth);
}
}, [editing]);
return (
<>
<div
ref={cellRef}
className="relative flex h-full items-center p-3 text-xs font-medium"
onDoubleClick={handleDoubleClick}
>
{cell?.data}
</div>
{editing && (
<Popover
open={editing}
anchorEl={cellRef.current}
PaperProps={{
className: 'flex',
style: { width, borderRadius: 0, boxShadow: 'none' },
}}
transformOrigin={{
vertical: 1,
horizontal: 'left',
}}
transitionDuration={0}
onClose={handleClose}
>
<TextareaAutosize
className="resize-none p-3 text-xs font-medium border border-blue-400"
autoFocus
autoCorrect="off"
value={text}
onInput={handleInput}
/>
</Popover>
)}
</>
);
};

View File

@ -0,0 +1 @@
export * from './GridCell';

View File

@ -0,0 +1,51 @@
import { Divider, Menu, MenuItem, MenuProps } from '@mui/material';
import { FC, useMemo } from 'react';
import { FieldType } from '@/services/backend';
import { FieldTypeSvg } from './FieldTypeSvg';
import { FieldTypeText } from './FieldTypeText';
const FieldTypeGroup = [
{
name: 'Basic',
types: [
FieldType.RichText,
FieldType.Number,
FieldType.SingleSelect,
FieldType.MultiSelect,
FieldType.DateTime,
FieldType.Checkbox,
FieldType.Checklist,
],
},
{
name: 'Advanced',
types: [
FieldType.LastEditedTime,
],
},
];
export const FieldTypeMenu: FC<MenuProps> = (props) => {
const PopoverClasses = useMemo(() => ({
...props.PopoverClasses,
paper: ['w-56', props.PopoverClasses?.paper].join(' '),
}), [props.PopoverClasses]);
return (
<Menu
{...props}
PopoverClasses={PopoverClasses}
>
{FieldTypeGroup.map((group, index) => [
<MenuItem key={group.name} dense disabled>{group.name}</MenuItem>,
group.types.map(type => (
<MenuItem key={type} dense>
<FieldTypeSvg className="mr-2 text-base" type={type} />
<span className="font-medium">{FieldTypeText(type)}</span>
</MenuItem>
)),
index < FieldTypeGroup.length - 1 && <Divider key={`Divider-${group.name}`} />,
])}
</Menu>
);
};

View File

@ -0,0 +1,30 @@
import { FC } from 'react';
import { FieldType } from '@/services/backend';
import { ReactComponent as TextSvg } from '$app/assets/database/field-type-text.svg';
import { ReactComponent as NumberSvg } from '$app/assets/database/field-type-number.svg';
import { ReactComponent as DateSvg } from '$app/assets/database/field-type-date.svg';
import { ReactComponent as SingleSelectSvg } from '$app/assets/database/field-type-single-select.svg';
import { ReactComponent as MultiSelectSvg } from '$app/assets/database/field-type-multi-select.svg';
import { ReactComponent as ChecklistSvg } from '$app/assets/database/field-type-checklist.svg';
import { ReactComponent as CheckboxSvg } from '$app/assets/database/field-type-checkbox.svg';
import { ReactComponent as URLSvg } from '$app/assets/database/field-type-url.svg';
import { ReactComponent as LastEditedTimeSvg } from '$app/assets/database/field-type-last-edited-time.svg';
export const FieldTypeSvgMap: Record<FieldType, FC<any>> = {
[FieldType.RichText]: TextSvg,
[FieldType.Number]: NumberSvg,
[FieldType.DateTime]: DateSvg,
[FieldType.SingleSelect]: SingleSelectSvg,
[FieldType.MultiSelect]: MultiSelectSvg,
[FieldType.Checkbox]: CheckboxSvg,
[FieldType.URL]: URLSvg,
[FieldType.Checklist]: ChecklistSvg,
[FieldType.LastEditedTime]: LastEditedTimeSvg,
[FieldType.CreatedTime]: LastEditedTimeSvg,
};
export const FieldTypeSvg: FC<{ type: FieldType, className?: string }> = ({ type, ...props }) => {
const Svg = FieldTypeSvgMap[type];
return <Svg {...props} />;
}

View File

@ -0,0 +1,19 @@
import { t } from 'i18next';
import { FieldType } from '@/services/backend';
export const FieldTypeTextMap = {
[FieldType.RichText]: 'textFieldName',
[FieldType.Number]: 'numberFieldName',
[FieldType.DateTime]: 'dateFieldName',
[FieldType.SingleSelect]: 'singleSelectFieldName',
[FieldType.MultiSelect]: 'multiSelectFieldName',
[FieldType.Checkbox]: 'checkboxFieldName',
[FieldType.URL]: 'urlFieldName',
[FieldType.Checklist]: 'checklistFieldName',
[FieldType.LastEditedTime]: 'updatedAtFieldName',
[FieldType.CreatedTime]: 'createdAtFieldName',
} as const;
export const FieldTypeText = (type: FieldType) => {
return t(`grid.field.${FieldTypeTextMap[type]}`);
}

View File

@ -0,0 +1,46 @@
import { IconButton } from '@mui/material';
import { FC, useCallback, useRef, useState } from 'react';
import { ReactComponent as DetailsSvg } from '$app/assets/details.svg';
import { Database } from '$app/interfaces/database';
import { FieldTypeSvg } from './FieldTypeSvg';
import { GridFieldMenu } from './GridFieldMenu';
export interface GridFieldProps {
field: Database.Field;
}
export const GridField: FC<GridFieldProps> = ({ field }) => {
const anchorEl = useRef<HTMLDivElement>(null);
const [open, setOpen] = useState(false);
const handleClick = useCallback(() => {
setOpen(true);
}, []);
const handleClose = useCallback(() => {
setOpen(false);
}, []);
return (
<div
ref={anchorEl}
className="flex items-center p-3 h-full"
>
<div className="flex flex-1 items-center">
<FieldTypeSvg type={field.type} className="text-base mr-2" />
<span className="text-xs font-medium">
{field.name}
</span>
</div>
<IconButton size="small" onClick={handleClick}>
<DetailsSvg />
</IconButton>
<GridFieldMenu
field={field}
open={open}
anchorEl={anchorEl.current}
onClose={handleClose}
/>
</div>
);
};

View File

@ -0,0 +1,77 @@
import { Divider, Menu, MenuItem, MenuProps, OutlinedInput } from '@mui/material';
import { ChangeEventHandler, FC, useCallback, useState } from 'react';
import { ReactComponent as MoreSvg } from '$app/assets/more.svg';
import { Database } from '$app/interfaces/database';
import * as service from '$app/components/database/database_bd_svc';
import { useViewId } from '../../database.hooks';
import { FieldTypeSvg } from './FieldTypeSvg';
import { FieldTypeText } from './FieldTypeText';
import { GridFieldMenuActions } from './GridFieldMenuActions';
export interface GridFieldMenuProps {
field: Database.Field;
anchorEl: MenuProps['anchorEl'];
open: boolean;
onClose: MenuProps['onClose'];
}
export const GridFieldMenu: FC<GridFieldMenuProps> = ({
field,
anchorEl,
open,
onClose,
}) => {
const viewId = useViewId();
const [inputtingName, setInputtingName] = useState(field.name);
const handleInput = useCallback<ChangeEventHandler<HTMLInputElement>>((e) => {
setInputtingName(e.target.value);
}, []);
const handleBlur = useCallback(async () => {
if (inputtingName !== field.name) {
try {
await service.updateField(viewId, field.id, {
name: inputtingName,
});
} catch (e) {
// TODO
console.error(`change field ${field.id} name from '${field.name}' to ${inputtingName} fail`, e);
}
}
}, [viewId, field, inputtingName]);
const fieldNameInput = (
<OutlinedInput
className="mx-3 mt-1 mb-5 !rounded-[10px]"
size="small"
value={inputtingName}
onChange={handleInput}
onBlur={handleBlur}
/>
);
const fieldTypeSelect = (
<MenuItem dense>
<FieldTypeSvg type={field.type} className="text-base mr-2" />
<span className="flex-1 text-xs font-medium">
{FieldTypeText(field.type)}
</span>
<MoreSvg className="text-base" />
</MenuItem>
);
return (
<Menu
anchorEl={anchorEl}
open={open}
onClose={onClose}
>
{fieldNameInput}
{fieldTypeSelect}
<Divider />
<GridFieldMenuActions />
</Menu>
);
};

View File

@ -0,0 +1,50 @@
import { Grid, MenuItem } from '@mui/material';
import { t } from 'i18next';
import { ReactComponent as HideSvg } from '$app/assets/hide.svg';
import { ReactComponent as CopySvg } from '$app/assets/copy.svg';
import { ReactComponent as DeleteSvg } from '$app/assets/delete.svg';
import { ReactComponent as LeftSvg } from '$app/assets/left.svg';
import { ReactComponent as RightSvg } from '$app/assets/right.svg';
enum FieldAction {
Hide = 'hide',
Duplicate = 'duplicate',
Delete = 'delete',
InsertLeft = 'insertLeft',
InsertRight = 'insertRight',
}
const FieldActionSvgMap = {
[FieldAction.Hide]: HideSvg,
[FieldAction.Duplicate]: CopySvg,
[FieldAction.Delete]: DeleteSvg,
[FieldAction.InsertLeft]: LeftSvg,
[FieldAction.InsertRight]: RightSvg,
};
const TwoColumnActions: FieldAction[][] = [
[FieldAction.Hide, FieldAction.Duplicate, FieldAction.Delete],
[FieldAction.InsertLeft, FieldAction.InsertRight],
];
export const GridFieldMenuActions = () => {
return (
<Grid container spacing={2}>
{TwoColumnActions.map((column, index) => (
<Grid key={index} item xs={6}>
{column.map(action => {
const ActionSvg = FieldActionSvgMap[action];
return (
<MenuItem key={action} dense>
<ActionSvg className="mr-2 text-base" />
{t(`grid.field.${action}`)}
</MenuItem>
);
})}
</Grid>
))}
</Grid>
);
};

View File

@ -0,0 +1 @@
export * from './GridField';

View File

@ -0,0 +1,36 @@
import { Database } from '$app/interfaces/database';
import { VirtualItem } from '@tanstack/react-virtual';
import { FC } from 'react';
import { useDatabase } from '../../database.hooks';
import { GridCell } from '../GridCell';
export const GridCellRow: FC<{
columnVirtualItems: VirtualItem[];
row: Database.Row;
before: number;
after: number;
}> = ({ columnVirtualItems, row, before, after }) => {
const { fields } = useDatabase();
return (
<>
<div className="flex">
{before > 0 && <div style={{ width: before }} />}
{columnVirtualItems.map(virtualColumn => (
<div
key={virtualColumn.key}
className="border-r border-line-divider overflow-hidden"
data-index={virtualColumn.index}
style={{
width: virtualColumn.size,
}}
>
<GridCell rowId={row.id} field={fields[virtualColumn.index]} />
</div>
))}
{after > 0 && <div style={{ width: after }} />}
</div>
<div className="w-44 grow" />
</>
);
}

View File

@ -0,0 +1,57 @@
import { VirtualItem } from '@tanstack/react-virtual';
import { t } from 'i18next';
import { FC } from 'react';
import { Button } from '@mui/material';
import { FieldType } from '@/services/backend';
import { ReactComponent as AddSvg } from '$app/assets/add.svg';
import * as service from '$app/components/database/database_bd_svc';
import { useDatabase } from '../../database.hooks';
import { GridField } from '../GridField';
export const GridFieldRow: FC<{
columnVirtualItems: VirtualItem[];
before: number;
after: number;
}> = ({ columnVirtualItems, before, after }) => {
const { viewId, fields } = useDatabase();
const handleClick = async () => {
await service.createFieldTypeOption(viewId, FieldType.RichText);
};
return (
<>
<div
className="flex border-t border-line-divider"
style={{
height: 41,
}}
>
{before > 0 && <div style={{ width: before }} />}
{columnVirtualItems.map(virtualColumn => (
<div
key={virtualColumn.key}
className="border-r border-line-divider"
data-index={virtualColumn.index}
style={{
width: `${virtualColumn.size}px`,
}}
>
<GridField field={fields[virtualColumn.index]} />
</div>
))}
{after > 0 && <div style={{ width: after }} />}
</div>
<div className="w-44 grow flex items-center pl-2 border-t border-line-divider">
<Button
variant="text"
color="inherit"
size="small"
startIcon={<AddSvg />}
onClick={handleClick}
>
{t('grid.field.newColumn')}
</Button>
</div>
</>
);
}

View File

@ -0,0 +1,29 @@
import { useCallback } from 'react';
import { t } from 'i18next';
import { ReactComponent as AddSvg } from '$app/assets/add.svg';
import * as service from '$app/components/database/database_bd_svc';
import { useDatabase, useViewId } from '../../database.hooks';
export const GridNewRow = () => {
const viewId = useViewId();
const { rows } = useDatabase();
const lastRowId = rows.at(-1)?.id;
const handleClick = useCallback(() => {
void service.createRow(viewId, {
startRowId: lastRowId,
});
}, [viewId, lastRowId]);
return (
<div
className="flex flex-1 h-9 items-center px-1 py-2 cursor-pointer"
onClick={handleClick}
>
<AddSvg className="text-base mr-1" />
<span className="text-xs font-medium">
{t('grid.row.newRow')}
</span>
</div>
);
};

View File

@ -0,0 +1,38 @@
import { VirtualItem } from '@tanstack/react-virtual';
import { FC } from 'react';
import { RenderRow, RenderRowType } from './constants';
import { GridCellRow } from './GridCellRow';
import { GridFieldRow } from './GridFieldRow';
import { GridNewRow } from './GridNewRow';
export const GridRow: FC<{
row: RenderRow;
columnVirtualItems: VirtualItem[];
before: number;
after: number;
}> = ({ row, columnVirtualItems, before, after }) => {
switch (row.type) {
case RenderRowType.Row:
return (
<GridCellRow
row={row.data}
columnVirtualItems={columnVirtualItems}
before={before}
after={after}
/>
);
case RenderRowType.Fields:
return (
<GridFieldRow
columnVirtualItems={columnVirtualItems}
before={before}
after={after}
/>
);
case RenderRowType.NewRow:
return <GridNewRow />;
default:
return null;
}
};

View File

@ -0,0 +1,22 @@
import { Database } from '$app/interfaces/database';
export enum RenderRowType {
Fields = 'fields',
Row = 'row',
NewRow = 'new-row',
}
export interface FieldRenderRow {
type: RenderRowType.Fields;
}
export interface CellRenderRow {
type: RenderRowType.Row;
data: Database.Row;
}
export interface NewRenderRow {
type: RenderRowType.NewRow;
}
export type RenderRow = FieldRenderRow | CellRenderRow | NewRenderRow;

View File

@ -0,0 +1,2 @@
export * from './GridRow';
export * from './constants';

View File

@ -0,0 +1,75 @@
import { Virtualizer, useVirtualizer } from '@tanstack/react-virtual';
import { FC, useContext, useMemo, useRef } from 'react';
import { VerticalScrollElementRefContext } from '../../database.context';
import { useDatabase } from '../../database.hooks';
import { GridRow, RenderRow, RenderRowType } from '../GridRow';
import { VirtualizedRows } from './VirtualizedRows';
const calculateBeforeAfter = (columnVirtualizer: Virtualizer<HTMLDivElement, Element>) => {
const columnVirtualItems = columnVirtualizer.getVirtualItems();
return columnVirtualItems.length > 0
? [
columnVirtualItems[0].start,
columnVirtualizer.getTotalSize() - columnVirtualItems[columnVirtualItems.length - 1].end,
]
: [0, 0];
};
export const GridTable: FC = () => {
const verticalScrollElementRef = useContext(VerticalScrollElementRefContext);
const { rows, fields } = useDatabase();
const horizontalScrollElementRef = useRef<HTMLDivElement>(null);
const renderRows = useMemo<RenderRow[]>(() => {
return [
{
type: RenderRowType.Fields,
},
...rows.map(row => ({
type: RenderRowType.Row,
data: row,
})),
{
type: RenderRowType.NewRow,
},
];
}, [rows]);
const defaultColumnWidth = 221;
const columnVirtualizer = useVirtualizer({
horizontal: true,
count: fields.length,
overscan: 5,
getItemKey: i => fields[i].id,
getScrollElement: () => horizontalScrollElementRef.current,
estimateSize: (i) => fields[i].width ?? defaultColumnWidth,
});
const columnVirtualItems = columnVirtualizer.getVirtualItems();
const [before, after] = calculateBeforeAfter(columnVirtualizer);
return (
<div
ref={horizontalScrollElementRef}
className="overflow-y-hidden overflow-x-auto"
>
<div className='px-16'>
<VirtualizedRows
scrollElementRef={verticalScrollElementRef}
rows={renderRows}
renderRow={(row) => (
<GridRow
row={row}
columnVirtualItems={columnVirtualItems}
before={before}
after={after}
/>
)}
/>
</div>
</div>
);
};

View File

@ -0,0 +1,78 @@
import { useVirtualizer } from '@tanstack/react-virtual';
import { FC, RefObject } from 'react';
import { RenderRow, RenderRowType } from '../GridRow';
export interface VirtualizedRowsProps {
rows: RenderRow[];
scrollElementRef: RefObject<Element>;
defaultHeight?: number;
renderRow: (row: RenderRow, index: number) => React.ReactNode;
}
const getRenderRowKey = (row: RenderRow) => {
switch (row.type) {
case RenderRowType.Row:
return `row:${row.data.id}`;
case RenderRowType.Fields:
return 'fields';
case RenderRowType.NewRow:
return 'new-row';
default:
return '';
}
};
const getRenderRowHeight = (row: RenderRow) => {
switch (row.type) {
case RenderRowType.Row:
return row.data.height ?? 41;
case RenderRowType.Fields:
return 41;
case RenderRowType.NewRow:
return 36;
default:
return 0;
}
};
export const VirtualizedRows: FC<VirtualizedRowsProps> = ({
rows,
scrollElementRef,
renderRow,
}) => {
const virtualizer = useVirtualizer({
count: rows.length,
overscan: 5,
getItemKey: i => getRenderRowKey(rows[i]),
getScrollElement: () => scrollElementRef.current,
estimateSize: i => getRenderRowHeight(rows[i]),
});
const virtualItems = virtualizer.getVirtualItems();
return (
<div
style={{
position: 'relative',
height: virtualizer.getTotalSize(),
}}
>
{virtualItems.map((virtualRow) => {
return (
<div
key={virtualRow.key}
className='absolute top-0 left-0 flex min-w-full border-b border-line-divider'
style={{
height: virtualRow.size,
transform: `translateY(${virtualRow.start}px)`,
}}
data-key={virtualRow.key}
data-index={virtualRow.index}
>
{renderRow(rows[virtualRow.index], virtualRow.index)}
</div>
);
})}
</div>
);
};

View File

@ -0,0 +1 @@
export * from './GridTable';

View File

@ -0,0 +1,41 @@
import { t } from 'i18next';
import { Button, Input, IconButton } from '@mui/material';
import { ReactComponent as AddSvg } from '$app/assets/add.svg';
import { ReactComponent as SearchSvg } from '$app/assets/search.svg';
import { ReactComponent as SettingsSvg } from '$app/assets/settings.svg';
export const GridToolbar = () => {
// TODO: get view title
const title = 'My plans on week';
return (
<div className="database-grid-toolbar flex items-center h-10 px-16">
<div className="flex flex-1 items-center font-semibold">
<span className="text-base">
{title}
</span>
<span className="ml-2">
<IconButton size="small">
<SettingsSvg />
</IconButton>
</span>
</div>
<div className="flex items-center">
<Button
variant="text"
color="inherit"
size="small"
startIcon={<AddSvg />}
>
{t('grid.createView')}
</Button>
<Input
className="ml-8 w-36"
placeholder={t('search.label')}
disableUnderline
startAdornment={<SearchSvg className="w-4 h-4 mr-2" />}
/>
</div>
</div>
);
};

View File

@ -0,0 +1 @@
export * from './GridToolbar';

View File

@ -0,0 +1 @@
export { Grid } from './Grid';

View File

@ -0,0 +1,3 @@
export * from './database.context';
export * from './database.hooks';
export * from './grid';

View File

@ -0,0 +1 @@
export * from './notification.hooks';

View File

@ -0,0 +1,137 @@
/* eslint-disable no-redeclare */
import { useEffect } from 'react';
import { listen } from '@tauri-apps/api/event';
import { Ok, Err, Result } from 'ts-results';
import { SubscribeObject } from '@/services/backend/models/flowy-notification';
import { FlowyError } from '@/services/backend/models/flowy-error';
import {
DatabaseFieldChangesetPB,
DatabaseNotification,
FieldPB,
GroupChangesPB,
GroupRowsNotificationPB,
ReorderAllRowsPB,
ReorderSingleRowPB,
RowsChangePB,
RowsVisibilityChangePB,
} from '@/services/backend';
const NotificationPBMap = {
[DatabaseNotification.DidUpdateViewRowsVisibility]: RowsVisibilityChangePB,
[DatabaseNotification.DidUpdateViewRows]: RowsChangePB,
[DatabaseNotification.DidReorderRows]: ReorderAllRowsPB,
[DatabaseNotification.DidReorderSingleRow]:ReorderSingleRowPB,
[DatabaseNotification.DidUpdateFields]:DatabaseFieldChangesetPB,
[DatabaseNotification.DidGroupByField]:GroupChangesPB,
[DatabaseNotification.DidUpdateNumOfGroups]:GroupChangesPB,
[DatabaseNotification.DidUpdateGroupRow]: GroupRowsNotificationPB,
[DatabaseNotification.DidUpdateField]: FieldPB,
[DatabaseNotification.DidUpdateCell]: null,
};
type NotificationMap = typeof NotificationPBMap;
type NotificationEnum = keyof NotificationMap;
type NullableInstanceType<K extends ((abstract new (...args: any) => any) | null)> = K extends (abstract new (...args: any) => any) ? InstanceType<K> : void;
type NotificationHandler<K extends NotificationEnum> = (result: Result<NullableInstanceType<NotificationMap[K]>, FlowyError>) => void;
/**
* Subscribes to a set of notifications.
*
* This function subscribes to notifications defined by the `NotificationEnum` and
* calls the appropriate `NotificationHandler` when each type of notification is received.
*
* @param {Object} callbacks - An object containing handlers for various notification types.
* Each key is a `NotificationEnum` value, and the corresponding value is a `NotificationHandler` function.
*
* @param {Object} [options] - Optional settings for the subscription.
* @param {string} [options.id] - An optional ID. If provided, only notifications with a matching ID will be processed.
*
* @returns {Promise<() => void>} A Promise that resolves to an unsubscribe function.
*
* @example
* subscribeNotifications({
* [DatabaseNotification.DidUpdateField]: (result) => {
* if (result.err) {
* // process error
* return;
* }
*
* console.log(result.val); // result.val is FieldPB
* },
* [DatabaseNotification.DidReorderRows]: (result) => {
* if (result.err) {
* // process error
* return;
* }
*
* console.log(result.val); // result.val is ReorderAllRowsPB
* },
* }, { id: '123' })
* .then(unsubscribe => {
* // Do something
* // ...
* // To unsubscribe, call `unsubscribe()`
* });
*
* @throws {Error} Throws an error if unable to subscribe.
*/
export function subscribeNotifications(
callbacks: {
[K in NotificationEnum]?: NotificationHandler<K>;
},
options?: { id?: string },
): Promise<() => void> {
return listen<ReturnType<typeof SubscribeObject.prototype.toObject>>('af-notification', event => {
const subject = SubscribeObject.fromObject(event.payload);
const { id, ty } = subject;
if (options?.id !== undefined && id !== options.id) {
return;
}
const notification = ty as NotificationEnum;
const pb = NotificationPBMap[notification];
const callback = callbacks[notification] as NotificationHandler<NotificationEnum>;
if (pb === undefined || !callback) {
return;
}
if (subject.has_error) {
const error = FlowyError.deserializeBinary(subject.error);
callback(Err(error));
} else {
const { payload } = subject;
callback(pb ? Ok(pb.deserializeBinary(payload)) : Ok.EMPTY);
}
});
}
export function subscribeNotification<K extends NotificationEnum>(
notification: K,
callback: NotificationHandler<K>,
options?: { id?: string },
): Promise<() => void> {
return subscribeNotifications({ [notification]: callback }, options);
}
export function useNotification<K extends NotificationEnum>(
notification: K,
callback: NotificationHandler<K>,
options: { id?: string },
): void {
const { id } = options;
useEffect(() => {
const unsubscribePromise = subscribeNotification(notification, callback, { id });
return () => {
void unsubscribePromise.then(fn => fn());
};
}, [callback, id, notification]);
}

View File

@ -0,0 +1,2 @@
export * from './types';
export * from './transform';

View File

@ -0,0 +1,72 @@
import { CellPB, ChecklistCellDataPB, DateCellDataPB, FieldPB, FieldType, SelectOptionCellDataPB, URLCellDataPB } from '@/services/backend';
import type { Database } from './types';
export const fieldPbToField = (fieldPb: FieldPB): Database.Field => ({
id: fieldPb.id,
name: fieldPb.name,
type: fieldPb.field_type,
visibility: fieldPb.visibility,
width: fieldPb.width,
isPrimary: fieldPb.is_primary,
});
const toDateCellData = (pb: DateCellDataPB): Database.DateTimeCellData => ({
date: pb.date,
time: pb.time,
timestamp: pb.timestamp,
includeTime: pb.include_time,
});
const toSelectCellData = (pb: SelectOptionCellDataPB): Database.SelectCellData => {
return {
options: pb.options.map(option => ({
id: option.id,
name: option.name,
color: option.color,
})),
selectOptions: pb.select_options.map(option => ({
id: option.id,
name: option.name,
color: option.color,
})),
};
};
const toURLCellData = (pb: URLCellDataPB): Database.UrlCellData => ({
url: pb.url,
content: pb.content,
});
const toChecklistCellData = (pb: ChecklistCellDataPB): Database.ChecklistCellData => ({
selectedOptions: pb.selected_options.map(({ id }) => id),
percentage: pb.percentage,
});
function parseCellData(fieldType: FieldType, data: Uint8Array) {
switch (fieldType) {
case FieldType.RichText:
case FieldType.Number:
case FieldType.Checkbox:
return new TextDecoder().decode(data);
case FieldType.DateTime:
case FieldType.LastEditedTime:
case FieldType.CreatedTime:
return toDateCellData(DateCellDataPB.deserializeBinary(data));
case FieldType.SingleSelect:
case FieldType.MultiSelect:
return toSelectCellData(SelectOptionCellDataPB.deserializeBinary(data));
case FieldType.URL:
return toURLCellData(URLCellDataPB.deserializeBinary(data));
case FieldType.Checklist:
return toChecklistCellData(ChecklistCellDataPB.deserializeBinary(data));
}
}
export const cellPbToCell = (cellPb: CellPB, fieldType: FieldType): Database.Cell => {
return {
rowId: cellPb.row_id,
fieldId: cellPb.field_id,
fieldType: fieldType,
data: parseCellData(fieldType, cellPb.data),
};
};

View File

@ -0,0 +1,227 @@
import {
CalendarLayoutPB,
DatabaseLayoutPB,
DateFormatPB,
FieldType,
NumberFormatPB,
SelectOptionColorPB,
SelectOptionConditionPB,
SortConditionPB,
TextFilterConditionPB,
TimeFormatPB,
} from '@/services/backend';
export interface Database {
id: string;
viewId: string;
name: string;
fields: Database.UndeterminedField[];
rows: Database.Row[];
layoutType: DatabaseLayoutPB;
layoutSetting: Database.GridLayoutSetting | Database.CalendarLayoutSetting;
isLinked: boolean;
}
// eslint-disable-next-line @typescript-eslint/no-namespace, no-redeclare
export namespace Database {
export interface GridLayoutSetting {
filters?: UndeterminedFilter[];
groups?: Group[];
sorts?: Sort[];
}
export interface CalendarLayoutSetting {
fieldId?: string;
layoutTy?: CalendarLayoutPB;
firstDayOfWeek?: number;
showWeekends?: boolean;
showWeekNumbers?: boolean;
}
export interface Field {
id: string;
name: string;
type: FieldType;
typeOption?: unknown;
visibility?: boolean;
width?: number;
isPrimary?: boolean;
}
export interface NumberTypeOption {
format?: NumberFormatPB;
scale?: number;
symbol?: string;
name?: string;
}
export interface NumberField extends Field {
type: FieldType.Number;
typeOption: NumberTypeOption;
}
export interface DateTimeTypeOption {
dateFormat?: DateFormatPB;
timeFormat?: TimeFormatPB;
timezoneId?: string;
fieldType?: FieldType;
}
export interface DateTimeField extends Field {
type: FieldType.DateTime;
typeOption: DateTimeTypeOption;
}
export interface SelectOption {
id: string;
name: string;
color: SelectOptionColorPB;
}
export interface SelectTypeOption {
options?: SelectOption[];
disableColor?: boolean;
}
export interface SelectField extends Field {
type: FieldType.SingleSelect | FieldType.MultiSelect;
typeOption: SelectTypeOption;
}
export interface ChecklistTypeOption {
config?: string;
}
export interface ChecklistField extends Field {
type: FieldType.Checklist;
typeOption: ChecklistTypeOption;
}
export type UndeterminedField = NumberField | DateTimeField | SelectField | ChecklistField | Field;
export interface Sort {
id: string;
fieldId: string;
fieldType: FieldType;
condition: SortConditionPB;
}
export interface Group {
id: string;
fieldId: string;
}
export interface Filter {
id: string;
fieldId: string;
fieldType: FieldType;
data: unknown;
}
export interface TextFilter extends Filter {
fieldType: FieldType.RichText;
data: TextFilterCondition;
}
export interface TextFilterCondition {
condition?: TextFilterConditionPB;
content?: string;
}
export interface SelectFilter extends Filter {
fieldType: FieldType.SingleSelect | FieldType.MultiSelect;
data: SelectFilterCondition;
}
export interface SelectFilterCondition {
condition?: SelectOptionConditionPB;
/**
* link to [SelectOption's id property]{@link SelectOption#id}.
*/
optionIds?: string[];
}
export type UndeterminedFilter = TextFilter | SelectFilter | Filter;
export interface Row {
id: string;
documentId?: string;
icon?: string;
cover?: string;
createdAt?: number;
modifiedAt?: number;
height?: number;
visibility?: boolean;
}
export interface Cell {
rowId: string;
fieldId: string;
fieldType: FieldType;
data: unknown;
}
export interface TextCell extends Cell {
fieldType: FieldType.RichText;
data: string;
}
export interface NumberCell extends Cell {
fieldType: FieldType.Number;
data: string;
}
export interface CheckboxCell extends Cell {
fieldType: FieldType.Checkbox;
data: 'Yes' | 'No';
}
export interface UrlCell extends Cell {
fieldType: FieldType.URL;
data: UrlCellData;
}
export interface UrlCellData {
url: string;
content?: string;
}
export interface SelectCell extends Cell {
fieldType: FieldType.SingleSelect | FieldType.MultiSelect;
data: SelectCellData;
}
export interface SelectCellData {
options?: SelectOption[];
selectOptions?: SelectOption[];
}
export interface DateTimeCell extends Cell {
fieldType: FieldType.DateTime;
data: DateTimeCellData;
}
export interface DateTimeCellData {
date?: string;
time?: string;
timestamp?: number;
includeTime?: boolean;
}
export interface ChecklistCell extends Cell {
fieldType: FieldType.Checklist;
data: ChecklistCellData;
}
export interface ChecklistCellData {
/**
* link to [SelectOption's id property]{@link SelectOption#id}.
*/
selectedOptions?: string[];
percentage?: number;
}
export type UndeterminedCell = TextCell | NumberCell | DateTimeCell | SelectCell | CheckboxCell | UrlCell | ChecklistCell;
}

View File

@ -0,0 +1,38 @@
import { useRef } from 'react';
import { useSnapshot } from 'valtio';
import { DatabaseLayoutPB } from '@/services/backend';
import {
VerticalScrollElementRefContext,
DatabaseContext,
Grid,
useViewId,
useConnectDatabase,
} from '../components/database';
export const DatabasePage = () => {
const viewId = useViewId();
const scrollElementRef = useRef<HTMLDivElement>(null);
const database = useConnectDatabase(viewId);
const snapshot = useSnapshot(database);
return (
<div
ref={scrollElementRef}
className="scroll-container flex flex-col overflow-y-auto overflow-x-hidden h-full"
>
<div>
<div className="px-16 pt-8">
<h1 className="text-3xl font-semibold mb-6">Grid</h1>
<div className="text-lg font-semibold mb-9">
👋 Welcome to AppFlowy
</div>
</div>
<VerticalScrollElementRefContext.Provider value={scrollElementRef}>
<DatabaseContext.Provider value={database}>
{snapshot.layoutType === DatabaseLayoutPB.Grid ? <Grid /> : null}
</DatabaseContext.Provider>
</VerticalScrollElementRefContext.Provider>
</div>
</div>
);
};

View File

@ -1 +1,2 @@
/// <reference types="vite/client" />
/// <reference types="vite-plugin-svgr/client" />

View File

@ -8,6 +8,7 @@ module.exports = {
'./src/**/*.{js,ts,jsx,tsx}',
'./node_modules/react-tailwindcss-datepicker/dist/index.esm.js',
],
important: '#body',
darkMode: 'class',
theme: {
extend: {

View File

@ -1,9 +1,38 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import svgr from 'vite-plugin-svgr';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
plugins: [
react(),
svgr({
svgrOptions: {
prettier: false,
plugins: ['@svgr/plugin-svgo', '@svgr/plugin-jsx'],
icon: true,
svgoConfig: {
multipass: true,
plugins: [
{
name: 'preset-default',
params: {
overrides: {
removeViewBox: false,
}
},
},
],
},
svgProps: {
role: 'img',
},
replaceAttrValues: {
'#333': 'currentColor',
},
},
}),
],
publicDir: '../appflowy_flutter/assets',
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
// prevent vite from obscuring rust errors

View File

@ -452,7 +452,10 @@
"deleteTag": "Delete tag",
"colorPanelTitle": "Colors",
"panelTitle": "Select an option or create one",
"searchOption": "Search for an option"
"searchOption": "Search for an option",
"searchOrCreateOption": "Search or create an option...",
"createNew": "Create an new",
"orSelectOne": "Or select an option"
},
"checklist": {
"panelTitle": "Add an item"