From b2edb7620770c4a2315f272df16ed83a4ceba85a Mon Sep 17 00:00:00 2001 From: blinko Date: Wed, 18 Dec 2024 09:25:44 +0800 Subject: [PATCH 1/7] fix: disable dragging for image thumbnails in AttachmentRender component #331 - Updated the ImageThumbnailRender component to set the draggable property to false, preventing users from dragging images unintentionally. - This change enhances the user experience by ensuring that image thumbnails behave as expected within the application. --- src/components/Common/AttachmentRender/imageRender.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Common/AttachmentRender/imageRender.tsx b/src/components/Common/AttachmentRender/imageRender.tsx index bcc2969..a020dab 100644 --- a/src/components/Common/AttachmentRender/imageRender.tsx +++ b/src/components/Common/AttachmentRender/imageRender.tsx @@ -32,6 +32,7 @@ const ImageThumbnailRender = ({ file, className }: { file: FileType, className?: classNames={{ wrapper: '!max-w-full', }} + draggable={false} onError={() => { if (file.preview === currentSrc) { return setIsOriginalError(true) From 4183c9f964c6e851a0c2331c4e78d79eea4421e8 Mon Sep 17 00:00:00 2001 From: blinko Date: Wed, 18 Dec 2024 09:29:55 +0800 Subject: [PATCH 2/7] fix: update dialog store reference in DeleteIcon component #332 - Replaced the DialogStore reference with DialogStandaloneStore in the DeleteIcon component to ensure proper dialog management. - This change improves the functionality of the delete action by correctly closing the standalone dialog after a file deletion. --- src/components/Common/AttachmentRender/icons.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/Common/AttachmentRender/icons.tsx b/src/components/Common/AttachmentRender/icons.tsx index ba4248f..e742970 100644 --- a/src/components/Common/AttachmentRender/icons.tsx +++ b/src/components/Common/AttachmentRender/icons.tsx @@ -10,6 +10,7 @@ import { PromiseState } from '@/store/standard/PromiseState'; import { BlinkoStore } from '@/store/blinkoStore'; import { helper } from '@/lib/helper'; import { FileType } from '../Editor/type'; +import { DialogStandaloneStore } from '@/store/module/DialogStandalone'; export const DeleteIcon = observer(({ className, file, files, size = 20 }: { className: string, file: FileType, files: FileType[], size?: number }) => { const store = RootStore.Local(() => ({ @@ -21,7 +22,7 @@ export const DeleteIcon = observer(({ className, file, files, size = 20 }: { cla }); const index = files.findIndex(i => i.name == file.name) files.splice(index, 1) - RootStore.Get(DialogStore).close() + RootStore.Get(DialogStandaloneStore).close() RootStore.Get(ToastPlugin).success(t('delete-success')) RootStore.Get(BlinkoStore).updateTicker++ } From dc41a44883e5f4a0913c7b956fc911baa7351415 Mon Sep 17 00:00:00 2001 From: blinko Date: Wed, 18 Dec 2024 10:02:38 +0800 Subject: [PATCH 3/7] fix: pwa refresh language lose --- src/store/user.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/store/user.ts b/src/store/user.ts index 7291bd6..3d375b3 100644 --- a/src/store/user.ts +++ b/src/store/user.ts @@ -94,10 +94,10 @@ export class UserStore implements User, Store { } async setupUserPreferences(setTheme: (theme: string) => void, i18n: any) { - if (this.isSetup) return - await this.blinko.config.call(); - const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + await this.blinko.config.getOrCall(); const config = this.blinko.config.value + if (this.isSetup && RootStore.Get(BaseStore).locale.value == config?.language) return + const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; const newTheme = config?.theme == 'system' ? systemTheme : (config?.theme ?? systemTheme); setTheme(newTheme); RootStore.Get(BaseStore).changeLanugage(i18n, config?.language ?? 'en'); From 5b69f2b99f45c69165034c5d8132b9cea5656839 Mon Sep 17 00:00:00 2001 From: blinko Date: Wed, 18 Dec 2024 11:49:50 +0800 Subject: [PATCH 4/7] fix: enhance attachment management and sorting functionality - Added the ability to reorder attachments in the AttachmentRender component, improving user experience by allowing drag-and-drop functionality. - Introduced a new sortOrder field in the attachments model to facilitate ordering. - Updated the Prisma schema and Zod validation to include sortOrder for attachments and tags. - Enhanced the note router to return attachments sorted by sortOrder. - Implemented a new mutation to update the order of attachments in the database. - Refactored the Editor and AttachmentsRender components to support file reordering, ensuring a seamless integration with the existing file management system. --- package.json | 1 + pnpm-lock.yaml | 130 +++++++++++++ .../20241218023101_0_27_0/migration.sql | 5 + prisma/schema.prisma | 54 +++--- .../AttachmentRender/DraggableFileGrid.tsx | 97 ++++++++++ .../Common/AttachmentRender/imageRender.tsx | 84 +++++---- .../Common/AttachmentRender/index.tsx | 175 ++++++++++++------ src/components/Common/Editor/editorStore.tsx | 4 + src/components/Common/Editor/index.tsx | 8 +- src/lib/prismaZodType.ts | 2 + src/server/routers/note.ts | 42 ++++- 11 files changed, 482 insertions(+), 120 deletions(-) create mode 100644 prisma/migrations/20241218023101_0_27_0/migration.sql create mode 100644 src/components/Common/AttachmentRender/DraggableFileGrid.tsx diff --git a/package.json b/package.json index 2df7c23..013877f 100644 --- a/package.json +++ b/package.json @@ -116,6 +116,7 @@ "react-accessible-treeview": "^2.9.1", "react-audio-visualize": "^1.2.0", "react-audio-voice-recorder": "^2.2.0", + "react-beautiful-dnd-next": "^11.0.5", "react-burger-menu": "^3.0.9", "react-collapsed": "^4.1.2", "react-dev-inspector": "^2.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d70aa3b..3cf7be8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -266,6 +266,9 @@ importers: react-audio-voice-recorder: specifier: ^2.2.0 version: 2.2.0(encoding@0.1.13)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-beautiful-dnd-next: + specifier: ^11.0.5 + version: 11.0.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-burger-menu: specifier: ^3.0.9 version: 3.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1219,6 +1222,10 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 + '@babel/runtime-corejs2@7.26.0': + resolution: {integrity: sha512-AQKSxUdaM7uTEGFmLZj1LOgX3LaLdt4udjqywaVdN6R5P2KAgqtBkDW4TS2ySRYNqcKmEe8Xv96jegHJNNb7Gg==} + engines: {node: '>=6.9.0'} + '@babel/runtime-corejs3@7.25.7': resolution: {integrity: sha512-gMmIEhg35sXk9Te5qbGp3W9YKrvLt3HV658/d3odWrHSqT0JeG5OzsJWFHRLiOohRyjRsJc/x03DhJm3i8VJxg==} engines: {node: '>=6.9.0'} @@ -4072,6 +4079,9 @@ packages: '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/hoist-non-react-statics@3.3.6': + resolution: {integrity: sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -4132,6 +4142,9 @@ packages: '@types/react-reconciler@0.28.8': resolution: {integrity: sha512-SN9c4kxXZonFhbX4hJrZy37yw9e7EIxcpHCxQv5JUS18wDE5ovkQKlqQEkufdJCCMfuI9BnjUJvhYeJ9x5Ra7g==} + '@types/react-redux@7.1.34': + resolution: {integrity: sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ==} + '@types/react@18.2.8': resolution: {integrity: sha512-lTyWUNrd8ntVkqycEEplasWy2OxNlShj3zqS0LuB1ENUGis5HodmhM7DtCoUGbxj3VW/WsGA0DUhpG6XrM7gPA==} @@ -4843,6 +4856,10 @@ packages: core-js-pure@3.38.1: resolution: {integrity: sha512-BY8Etc1FZqdw1glX0XNOq2FDwfrg/VGqoZOZCdaL+UmdaqDwQwYXkMJT4t6In+zfEfOJDcM9T0KdbBeJg8KKCQ==} + core-js@2.6.12: + resolution: {integrity: sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==} + deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js. + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -4893,6 +4910,9 @@ packages: resolution: {integrity: sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==} engines: {node: '>=12'} + css-box-model@1.2.1: + resolution: {integrity: sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==} + css-declaration-sorter@6.4.1: resolution: {integrity: sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g==} engines: {node: ^10 || ^12 || >=14} @@ -5986,6 +6006,9 @@ packages: highlightjs-vue@1.0.0: resolution: {integrity: sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==} + hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + hook-std@3.0.0: resolution: {integrity: sha512-jHRQzjSDzMtFy34AGj1DN+vq54WVuhSvKgrHf0OMiFQTwDD4L/qqofVEWjLOBMTn5+lCD3fPg32W9yOfnEJTTw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -6879,6 +6902,9 @@ packages: resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} engines: {node: '>= 4.0.0'} + memoize-one@5.2.1: + resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} + meow@13.2.0: resolution: {integrity: sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==} engines: {node: '>=18'} @@ -8189,6 +8215,9 @@ packages: radix3@1.1.2: resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + raf-schd@4.0.3: + resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==} + ramda-adjunct@5.1.0: resolution: {integrity: sha512-8qCpl2vZBXEJyNbi4zqcgdfHtcdsWjOGbiNSEnEBrM6Y0OKOT8UxJbIVGm1TIcjaSu2MxaWcgtsNlKlCk7o7qg==} engines: {node: '>=0.10.3'} @@ -8244,6 +8273,11 @@ packages: react: '>=16.2.0' react-dom: '>=16.2.0' + react-beautiful-dnd-next@11.0.5: + resolution: {integrity: sha512-kM5Mob41HkA3ShS9uXqeMkW51L5bVsfttxfrwwHucu7I6SdnRKCyN78t6QiLH/UJQQ8T4ukI6NeQAQQpGwolkg==} + peerDependencies: + react: ^16.8.5 + react-burger-menu@3.0.9: resolution: {integrity: sha512-Qy15hkCxwxNEKfqdAv43F+8ZSl+/c6KkqrBwGP0CesFYJ02onHtiUFUbuhSWCMtBH8/n0HhfekFlp/NyCdKYzQ==} engines: {node: '>=4.0.0'} @@ -8366,6 +8400,9 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-markdown@9.0.1: resolution: {integrity: sha512-186Gw/vF1uRkydbsOIkcGXw7aHq0sZOCRFFjGrr7b9+nVZg4UfA4enXCaxm4fUzecU38sWfrNDitGhshuU7rdg==} peerDependencies: @@ -8392,6 +8429,18 @@ packages: react: '>=16.8.0' react-dom: '>=16.8.0' + react-redux@7.2.9: + resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==} + peerDependencies: + react: ^16.8.3 || ^17 || ^18 + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + react-redux@9.1.2: resolution: {integrity: sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==} peerDependencies: @@ -8503,6 +8552,9 @@ packages: peerDependencies: immutable: ^3.8.1 || ^4.0.0-rc.1 + redux@4.2.1: + resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==} + redux@5.0.1: resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} @@ -9202,6 +9254,9 @@ packages: resolution: {integrity: sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==} engines: {node: '>=12'} + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tiny-warning@1.0.3: resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} @@ -9585,6 +9640,11 @@ packages: '@types/react': optional: true + use-memo-one@1.1.3: + resolution: {integrity: sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + use-sidecar@1.1.2: resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} engines: {node: '>=10'} @@ -11295,6 +11355,11 @@ snapshots: '@babel/types': 7.26.0 esutils: 2.0.3 + '@babel/runtime-corejs2@7.26.0': + dependencies: + core-js: 2.6.12 + regenerator-runtime: 0.14.1 + '@babel/runtime-corejs3@7.25.7': dependencies: core-js-pure: 3.38.1 @@ -15427,6 +15492,11 @@ snapshots: dependencies: '@types/unist': 3.0.3 + '@types/hoist-non-react-statics@3.3.6': + dependencies: + '@types/react': 18.2.8 + hoist-non-react-statics: 3.3.2 + '@types/json-schema@7.0.15': {} '@types/katex@0.16.7': {} @@ -15486,6 +15556,13 @@ snapshots: dependencies: '@types/react': 18.2.8 + '@types/react-redux@7.1.34': + dependencies: + '@types/hoist-non-react-statics': 3.3.6 + '@types/react': 18.2.8 + hoist-non-react-statics: 3.3.2 + redux: 4.2.1 + '@types/react@18.2.8': dependencies: '@types/prop-types': 15.7.13 @@ -16258,6 +16335,8 @@ snapshots: core-js-pure@3.38.1: {} + core-js@2.6.12: {} + core-util-is@1.0.3: {} cors@2.8.5: @@ -16319,6 +16398,10 @@ snapshots: dependencies: type-fest: 1.4.0 + css-box-model@1.2.1: + dependencies: + tiny-invariant: 1.3.3 + css-declaration-sorter@6.4.1(postcss@8.4.47): dependencies: postcss: 8.4.47 @@ -17583,6 +17666,10 @@ snapshots: highlightjs-vue@1.0.0: {} + hoist-non-react-statics@3.3.2: + dependencies: + react-is: 16.13.1 + hook-std@3.0.0: {} hosted-git-info@7.0.2: @@ -18604,6 +18691,8 @@ snapshots: dependencies: fs-monkey: 1.0.6 + memoize-one@5.2.1: {} + meow@13.2.0: {} merge-descriptors@1.0.3: {} @@ -19908,6 +19997,8 @@ snapshots: radix3@1.1.2: {} + raf-schd@4.0.3: {} + ramda-adjunct@5.1.0(ramda@0.30.1): dependencies: ramda: 0.30.1 @@ -19971,6 +20062,21 @@ snapshots: transitivePeerDependencies: - encoding + react-beautiful-dnd-next@11.0.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime-corejs2': 7.26.0 + css-box-model: 1.2.1 + memoize-one: 5.2.1 + raf-schd: 4.0.3 + react: 18.3.1 + react-redux: 7.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + redux: 4.2.1 + tiny-invariant: 1.3.3 + use-memo-one: 1.1.3(react@18.3.1) + transitivePeerDependencies: + - react-dom + - react-native + react-burger-menu@3.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: browserify-optional: 1.0.1 @@ -20141,6 +20247,8 @@ snapshots: react-is@16.13.1: {} + react-is@17.0.2: {} + react-markdown@9.0.1(@types/react@18.2.8)(react@18.3.1): dependencies: '@types/hast': 3.0.4 @@ -20176,6 +20284,18 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + react-redux@7.2.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.25.7 + '@types/react-redux': 7.1.34 + hoist-non-react-statics: 3.3.2 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.3.1 + react-is: 17.0.2 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + react-redux@9.1.2(@types/react@18.2.8)(react@18.3.1)(redux@5.0.1): dependencies: '@types/use-sync-external-store': 0.0.3 @@ -20313,6 +20433,10 @@ snapshots: dependencies: immutable: 3.8.2 + redux@4.2.1: + dependencies: + '@babel/runtime': 7.25.7 + redux@5.0.1: {} reflect-metadata@0.2.2: {} @@ -21214,6 +21338,8 @@ snapshots: dependencies: convert-hrtime: 5.0.0 + tiny-invariant@1.3.3: {} + tiny-warning@1.0.3: {} tinyexec@0.3.1: {} @@ -21584,6 +21710,10 @@ snapshots: optionalDependencies: '@types/react': 18.2.8 + use-memo-one@1.1.3(react@18.3.1): + dependencies: + react: 18.3.1 + use-sidecar@1.1.2(@types/react@18.2.8)(react@18.3.1): dependencies: detect-node-es: 1.1.0 diff --git a/prisma/migrations/20241218023101_0_27_0/migration.sql b/prisma/migrations/20241218023101_0_27_0/migration.sql new file mode 100644 index 0000000..5db902f --- /dev/null +++ b/prisma/migrations/20241218023101_0_27_0/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "attachments" ADD COLUMN "sortOrder" INTEGER NOT NULL DEFAULT 0; + +-- AlterTable +ALTER TABLE "tag" ADD COLUMN "sortOrder" INTEGER NOT NULL DEFAULT 0; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a708c67..d6a69f3 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -36,6 +36,7 @@ model attachments { size Decimal @default(0) @db.Decimal type String @default("") @db.VarChar noteId Int @default(0) + sortOrder Int @default(0) createdAt DateTime @default(now()) @db.Timestamptz(6) updatedAt DateTime @updatedAt @db.Timestamptz(6) @@ -43,30 +44,30 @@ model attachments { } model config { - id Int @id @default(autoincrement()) - key String @default("") @db.VarChar - config Json? @db.Json - userId Int? + id Int @id @default(autoincrement()) + key String @default("") @db.VarChar + config Json? @db.Json + userId Int? user accounts? @relation(fields: [userId], references: [id]) } model notes { - id Int @id @default(autoincrement()) - type Int @default(0) - content String @default("") @db.VarChar - isArchived Boolean @default(false) - isRecycle Boolean @default(false) - isShare Boolean @default(false) - isTop Boolean @default(false) - isReviewed Boolean @default(false) - sharePassword String @default("") @db.VarChar - metadata Json? @db.Json - accountId Int? - createdAt DateTime @default(now()) @db.Timestamptz(6) - updatedAt DateTime @updatedAt @db.Timestamptz(6) + id Int @id @default(autoincrement()) + type Int @default(0) + content String @default("") @db.VarChar + isArchived Boolean @default(false) + isRecycle Boolean @default(false) + isShare Boolean @default(false) + isTop Boolean @default(false) + isReviewed Boolean @default(false) + sharePassword String @default("") @db.VarChar + metadata Json? @db.Json + accountId Int? + createdAt DateTime @default(now()) @db.Timestamptz(6) + updatedAt DateTime @updatedAt @db.Timestamptz(6) attachments attachments[] tags tagsToNote[] - account accounts? @relation(fields: [accountId], references: [id]) + account accounts? @relation(fields: [accountId], references: [id]) referencedBy noteReference[] @relation("ReferencedNote") references noteReference[] @relation("ReferencingNote") } @@ -76,10 +77,11 @@ model tag { name String @default("") @db.VarChar icon String @default("") @db.VarChar parent Int @default(0) - accountId Int? + accountId Int? createdAt DateTime @default(now()) @db.Timestamptz(6) updatedAt DateTime @updatedAt @db.Timestamptz(6) tagsToNote tagsToNote[] + sortOrder Int @default(0) account accounts? @relation(fields: [accountId], references: [id]) } @@ -104,13 +106,13 @@ model scheduledTask { } model noteReference { - id Int @id @default(autoincrement()) - fromNoteId Int - toNoteId Int - createdAt DateTime @default(now()) @db.Timestamptz(6) - - fromNote notes @relation("ReferencingNote", fields: [fromNoteId], references: [id]) - toNote notes @relation("ReferencedNote", fields: [toNoteId], references: [id]) + id Int @id @default(autoincrement()) + fromNoteId Int + toNoteId Int + createdAt DateTime @default(now()) @db.Timestamptz(6) + + fromNote notes @relation("ReferencingNote", fields: [fromNoteId], references: [id]) + toNote notes @relation("ReferencedNote", fields: [toNoteId], references: [id]) @@unique([fromNoteId, toNoteId]) } diff --git a/src/components/Common/AttachmentRender/DraggableFileGrid.tsx b/src/components/Common/AttachmentRender/DraggableFileGrid.tsx new file mode 100644 index 0000000..b4e7922 --- /dev/null +++ b/src/components/Common/AttachmentRender/DraggableFileGrid.tsx @@ -0,0 +1,97 @@ +import React from 'react'; +import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd-next'; +import { FileType } from '../Editor/type'; +import { FileIcons } from '../FileIcon'; +import { DeleteIcon } from './icons'; +import { helper } from '@/lib/helper'; +import { api } from '@/lib/trpc'; + +type DraggableFileGridProps = { + files: FileType[]; + preview?: boolean; + columns?: number; + onReorder?: (newFiles: FileType[]) => void; + type: 'image' | 'other'; + className?: string; + renderItem?: (file: FileType) => React.ReactNode; +}; + +export const DraggableFileGrid = ({ + files, + preview = false, + onReorder, + type, + className, + renderItem +}: DraggableFileGridProps) => { + const handleDragEnd = async (result: any) => { + if (!result.destination) return; + + const { source, destination } = result; + const filteredFiles = files.filter(i => i.previewType === type); + const allFiles = Array.from(files); + + const [reorderedItem] = filteredFiles.splice(source.index, 1); + if (reorderedItem) { + filteredFiles.splice(destination.index, 0, reorderedItem); + + const newFiles = allFiles.map(file => { + if (file.previewType === type) { + return filteredFiles.shift() || file; + } + return file; + }); + + onReorder?.(newFiles); + + try { + await api.notes.updateAttachmentsOrder.mutate({ + attachments: newFiles.map((file, index) => ({ + name: file.name, + sortOrder: index + })) + }); + } catch (error) { + console.error('Failed to update attachments order:', error); + } + } + }; + + return ( + + + {(provided, snapshot) => ( +
+ {files.filter(i => i.previewType === type).map((file, index) => ( + + {(provided, snapshot) => ( +
+ {renderItem?.(file)} +
+ )} +
+ ))} + {provided.placeholder} +
+ )} +
+
+ ); +}; \ No newline at end of file diff --git a/src/components/Common/AttachmentRender/imageRender.tsx b/src/components/Common/AttachmentRender/imageRender.tsx index a020dab..ab06806 100644 --- a/src/components/Common/AttachmentRender/imageRender.tsx +++ b/src/components/Common/AttachmentRender/imageRender.tsx @@ -1,18 +1,18 @@ import React, { useEffect, useMemo, useState } from 'react'; import { FileType } from '../Editor/type'; -import { Image, Skeleton } from '@nextui-org/react'; +import { Image } from '@nextui-org/react'; import { PhotoProvider, PhotoView } from 'react-photo-view'; import { Icon } from '@iconify/react'; import { DeleteIcon, DownloadIcon } from './icons'; import { observer } from 'mobx-react-lite'; -import { RootStore } from '@/store'; import { useMediaQuery } from 'usehooks-ts'; -import { api } from '@/lib/trpc'; +import { DraggableFileGrid } from './DraggableFileGrid'; type IProps = { files: FileType[] preview?: boolean columns?: number + onReorder?: (newFiles: FileType[]) => void } const ImageThumbnailRender = ({ file, className }: { file: FileType, className?: string }) => { const [isOriginalError, setIsOriginalError] = useState(false); @@ -49,13 +49,14 @@ const ImageRender = observer((props: IProps) => { const images = files?.filter(i => i.previewType == 'image') const imageRenderClassName = useMemo(() => { - const imageLength = files?.filter(i => i.previewType == 'image')?.length + if (!preview) { + return 'flex flex-row gap-2 overflow-x-auto pb-2' + } + + const imageLength = images?.length if (columns) { return `grid grid-cols-${columns} gap-2` } - if (!preview && !isPc) { - return `flex items-center overflow-x-scroll gap-2` - } if (imageLength == 1) { return `grid grid-cols-2 gap-2` } @@ -66,16 +67,17 @@ const ImageRender = observer((props: IProps) => { return `grid grid-cols-3 gap-3` } return '' - }, [images]) + }, [images, preview, columns]) const imageHeight = useMemo(() => { - const imageLength = files?.filter(i => i.previewType == 'image')?.length + if (!preview) { + return 'h-[160px] w-[160px]' + } + + const imageLength = images?.length if (columns) { return `max-h-[100px] w-auto` } - if (!preview && !isPc) { - return `h-[80px] min-w-[80px]` - } if (imageLength == 1) { return `h-[200px] max-h-[200px] md:max-w-[200px]` } @@ -86,31 +88,45 @@ const ImageRender = observer((props: IProps) => { return `lg:h-[160px] md:h-[120px] h-[100px]` } return '' - }, [images]) + }, [images, preview, columns]) - return
- - {images.map((file, index) => ( -
- {file.uploadPromise?.loading?.value &&
- -
} -
- -
- -
-
-
- {!file.uploadPromise?.loading?.value && !preview && - - } - {preview && - } + const renderImage = (file: FileType) => ( +
+ {file.uploadPromise?.loading?.value && ( +
+
- ))} + )} +
+ +
+ +
+
+
+ {!file.uploadPromise?.loading?.value && !preview && + + } + {preview && } +
+ ) + + return ( + + -
+ ) }) export { ImageRender } \ No newline at end of file diff --git a/src/components/Common/AttachmentRender/index.tsx b/src/components/Common/AttachmentRender/index.tsx index baf829c..6739911 100644 --- a/src/components/Common/AttachmentRender/index.tsx +++ b/src/components/Common/AttachmentRender/index.tsx @@ -8,15 +8,12 @@ import { DeleteIcon, DownloadIcon } from './icons'; import { ImageRender } from './imageRender'; import { HandleFileType } from '../Editor/editorUtils'; import { Icon } from '@iconify/react'; -import { RootStore } from '@/store'; -import { BlinkoStore } from '@/store/blinkoStore'; import { Popover, PopoverContent, PopoverTrigger } from '@nextui-org/popover'; import { BlinkoCard } from '@/components/BlinkoCard'; import { api } from '@/lib/trpc'; -import { PromiseState } from '@/store/standard/PromiseState'; -import { cache } from '@/lib/cache'; -import { reaction } from 'mobx'; import { EditorStore } from '../Editor/editorStore'; +import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd-next'; +import { DraggableFileGrid } from './DraggableFileGrid'; //https://www.npmjs.com/package/browser-thumbnail-generator @@ -24,65 +21,131 @@ type IProps = { files: FileType[] preview?: boolean columns?: number + onReorder?: (newFiles: FileType[]) => void } const AttachmentsRender = observer((props: IProps) => { const { files, preview = false, columns = 3 } = props - return <> - {/* image render */} - - {/* video render todo:improve style*/} -
- {files?.filter(i => i.previewType == 'video').map((file, index) => ( -
-
- ))} -
- {/* audio render todo:improve style*/} -
- {files?.filter(i => i.previewType == 'audio').map((file, index) => ( -
-
- ))} -
- - - {/* other file render */} -
- {files?.filter(i => i.previewType == 'other').map((file, index) => ( -
{ - if (preview) { - helper.download.downloadByLink(file.uploadPromise.value) - } - }} className='relative flex p-2 w-full items-center gap-2 cursor-pointer bg-sencondbackground hover:bg-hover tansition-all rounded-md group'> - -
{file.name}
- {!file.uploadPromise?.loading?.value && !preview && - } -
- ))} -
- + const gridClassName = preview + ? `grid grid-cols-${(columns - 1) < 1 ? 1 : (columns - 1)} md:grid-cols-${columns} gap-2 mt-3` + : 'flex flex-row gap-2 overflow-x-auto pb-2 mt-3'; + + return ( +
+ {/* image render */} + + + {/* video render */} +
+ {files?.filter(i => i.previewType == 'video').map((file, index) => ( +
+
+ ))} +
+ + {/* audio render - */} +
+ {files?.filter(i => i.previewType == 'audio').map((file, index) => ( +
+
+ ))} +
+ + {/* other file render */} + ( +
{ + if (preview) { + helper.download.downloadByLink(file.uploadPromise.value) + } + }} + > + +
{file.name}
+ {!file.uploadPromise?.loading?.value && !preview && + + } +
+ )} + /> +
+ ) }) -const FilesAttachmentRender = observer(({ files, preview, columns }: { files: Attachment[], preview?: boolean, columns?: number }) => { - const [handledFiles, setFiles] = useState([]) +const FilesAttachmentRender = observer(({ + files, + preview, + columns, + onReorder +}: { + files: Attachment[], + preview?: boolean, + columns?: number, + onReorder?: (newFiles: Attachment[]) => void +}) => { + const [handledFiles, setFiles] = useState([]); + useEffect(() => { - setFiles(HandleFileType(files)) - }, [files]) - return -}) + setFiles(HandleFileType(files)); + }, [files]); + + const handleReorder = (newFiles: FileType[]) => { + const newAttachments = files.slice().sort((a, b) => { + const aIndex = newFiles.findIndex(f => f.name === a.name); + const bIndex = newFiles.findIndex(f => f.name === b.name); + return aIndex - bIndex; + }); + onReorder?.(newAttachments); + }; + + return ( + + ); +}); const ReferenceRender = observer(({ store }: { store: EditorStore }) => { diff --git a/src/components/Common/Editor/editorStore.tsx b/src/components/Common/Editor/editorStore.tsx index b2f0079..f5d77b7 100644 --- a/src/components/Common/Editor/editorStore.tsx +++ b/src/components/Common/Editor/editorStore.tsx @@ -51,6 +51,10 @@ export class EditorStore { } catch (error) { } } + updateFileOrder = (newFiles: FileType[]) => { + this.files = newFiles; + } + insertMarkdown = (text) => { this.vditor?.insertValue(text) this.focus() diff --git a/src/components/Common/Editor/index.tsx b/src/components/Common/Editor/index.tsx index 7bb1367..293d725 100644 --- a/src/components/Common/Editor/index.tsx +++ b/src/components/Common/Editor/index.tsx @@ -5,7 +5,7 @@ import { useTheme } from 'next-themes'; import React, { ReactElement, useEffect, useRef, useState } from 'react'; import { useDropzone } from 'react-dropzone'; import { observer, useLocalObservable } from 'mobx-react-lite'; -import { OnSendContentType } from './type'; +import { FileType, OnSendContentType } from './type'; import { BlinkoStore } from '@/store/blinkoStore'; import { _ } from '@/lib/lodash'; import { useTranslation } from 'react-i18next'; @@ -72,6 +72,10 @@ const Editor = observer(({ content, onChange, onSend, isSendLoading, originFiles } }); + const handleFileReorder = (newFiles: FileType[]) => { + store.updateFileOrder(newFiles); + }; + return 0 && (
- +
)} diff --git a/src/lib/prismaZodType.ts b/src/lib/prismaZodType.ts index 6225232..9e3e9a3 100644 --- a/src/lib/prismaZodType.ts +++ b/src/lib/prismaZodType.ts @@ -33,6 +33,7 @@ export const attachmentsSchema = z.object({ size: z.instanceof(Prisma.Decimal, { message: "Field 'size' must be a Decimal. Location: ['Models', 'attachments']" }), noteId: z.number().int(), createdAt: z.coerce.date(), + sortOrder: z.number().int(), updatedAt: z.coerce.date(), type: z.string(), }) @@ -82,6 +83,7 @@ export const tagSchema = z.object({ name: z.string(), icon: z.string(), parent: z.number().int(), + sortOrder: z.number().int(), createdAt: z.coerce.date(), updatedAt: z.coerce.date(), }) diff --git a/src/server/routers/note.ts b/src/server/routers/note.ts index a125c24..2174c0d 100644 --- a/src/server/routers/note.ts +++ b/src/server/routers/note.ts @@ -117,7 +117,12 @@ export const noteRouter = router({ take: size, include: { tags: { include: { tag: true } }, - attachments: true, + attachments: { + orderBy: [ + { sortOrder: 'asc' }, + { id: 'asc' } + ] + }, references: { select: { toNoteId: true @@ -173,7 +178,12 @@ export const noteRouter = router({ where: { id: { in: ids }, accountId: Number(ctx.id) }, include: { tags: { include: { tag: true } }, - attachments: true, + attachments: { + orderBy: [ + { sortOrder: 'asc' }, + { id: 'asc' } + ] + }, references: { select: { toNoteId: true @@ -526,6 +536,34 @@ export const noteRouter = router({ return await deleteNotes(noteIds, ctx); }), + updateAttachmentsOrder: authProcedure + .meta({ openapi: { method: 'POST', path: '/v1/note/update-attachments-order', summary: 'Update attachments order', protect: true, tags: ['Note'] } }) + .input(z.object({ + attachments: z.array(z.object({ + name: z.string(), + sortOrder: z.number() + })) + })) + .output(z.any()) + .mutation(async function ({ input, ctx }) { + const { attachments } = input; + + await Promise.all( + attachments.map(({ name, sortOrder }) => + prisma.attachments.updateMany({ + where: { + name, + note: { + accountId: Number(ctx.id) + } + }, + data: { sortOrder } + }) + ) + ); + + return { success: true }; + }), }) let insertNoteReference = async ({ fromNoteId, toNoteId, accountId }) => { From 91229c6addd1b4ca25547fe831a675f5b188642c Mon Sep 17 00:00:00 2001 From: blinko Date: Wed, 18 Dec 2024 12:57:24 +0800 Subject: [PATCH 5/7] fix: improve BlinkoAiChat responsiveness and enhance file name handling - Added support for iOS-specific styling in the BlinkoAiChat component to improve responsiveness on iOS devices. - Updated the EditorStore to correctly update the file name in the files array after a successful upload. - Refactored Dialog and DialogStandalone components to ensure consistent content rendering and layout adjustments, enhancing user experience. --- src/components/BlinkoAi/index.tsx | 6 +++--- src/components/Common/Editor/editorStore.tsx | 6 ++++++ src/store/module/Dialog/Provider.tsx | 15 ++++++++++----- src/store/module/DialogStandalone/Provider.tsx | 8 ++++++-- 4 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/components/BlinkoAi/index.tsx b/src/components/BlinkoAi/index.tsx index 132a609..316ee26 100644 --- a/src/components/BlinkoAi/index.tsx +++ b/src/components/BlinkoAi/index.tsx @@ -8,17 +8,16 @@ import { motion } from "motion/react" import { AiStore } from "@/store/aiStore"; import { useEffect, useRef } from "react"; import { useMediaQuery } from "usehooks-ts"; -import { DialogStore } from "@/store/module/Dialog"; import { UserStore } from "@/store/user"; import { useTranslation } from "react-i18next"; import { ScrollArea, ScrollAreaHandles } from "../Common/ScrollArea"; import Link from "next/link"; -import DraggableDiv from "../Common/DragContainer"; import dayjs from "@/lib/dayjs"; import { FilesAttachmentRender } from "../Common/AttachmentRender"; import { ResizableWrapper } from "../Common/ResizableWrapper"; import { useRouter } from "next/router"; import { MarkdownRender } from "../Common/MarkdownRender"; +import { useIsIOS } from "@/lib/hooks"; export const BlinkoAiChat = observer(() => { const ai = RootStore.Get(AiStore) @@ -26,6 +25,7 @@ export const BlinkoAiChat = observer(() => { const router = useRouter() const scrollAreaRef = useRef(null); const { t } = useTranslation() + const isIOS = useIsIOS() useEffect(() => { scrollAreaRef.current?.scrollToBottom() }, [ai.scrollTicker]) @@ -35,7 +35,7 @@ export const BlinkoAiChat = observer(() => { onBottom={() => { }} ref={scrollAreaRef} key='BlinkoAiChat' - className={`mx-1 w-full flex-1`}> + className={`mx-1 w-full flex-1 ${isIOS ? 'max-h-[70vh]' : ''}`}> { ai.chatHistory.list.length == 0 &&
diff --git a/src/components/Common/Editor/editorStore.tsx b/src/components/Common/Editor/editorStore.tsx index f5d77b7..080c360 100644 --- a/src/components/Common/Editor/editorStore.tsx +++ b/src/components/Common/Editor/editorStore.tsx @@ -146,6 +146,12 @@ export class EditorStore { onUploadProgress }); const data = response.data; + if (data.fileName) { + const fileIndex = this.files.findIndex(f => f.name === file.name); + if (fileIndex !== -1) { + this.files[fileIndex]!.name = data.fileName; + } + } this.speechToText(data.filePath) if (data.filePath) { return data.filePath diff --git a/src/store/module/Dialog/Provider.tsx b/src/store/module/Dialog/Provider.tsx index 23ed3cc..4479b49 100644 --- a/src/store/module/Dialog/Provider.tsx +++ b/src/store/module/Dialog/Provider.tsx @@ -8,6 +8,7 @@ import { useMediaQuery } from "usehooks-ts"; import { motion } from "motion/react"; import { Icon } from "@iconify/react"; import { CancelIcon } from "@/components/Common/Icons"; +import { ScrollArea } from "@/components/Common/ScrollArea"; const CloseButton = ({ onClose }: { onClose: () => void }) => (
{ className="w-full bg-background border-sencondbackground p-1 rounded-t-lg shadow-lg pointer-events-auto" {...motionConfig} > -
+
{title ?? ''}
-
- +
+ +
)} @@ -123,12 +126,14 @@ const Dialog = observer(() => { className="w-full pointer-events-auto " {...motionConfig} > -
+
{ showOnlyContentCloseButton && modal.close()} /> } - +
+ +
} diff --git a/src/store/module/DialogStandalone/Provider.tsx b/src/store/module/DialogStandalone/Provider.tsx index 77d0741..cf5d932 100644 --- a/src/store/module/DialogStandalone/Provider.tsx +++ b/src/store/module/DialogStandalone/Provider.tsx @@ -115,7 +115,9 @@ const Dialog = observer(() => {
- +
+ +
)} @@ -129,7 +131,9 @@ const Dialog = observer(() => { showOnlyContentCloseButton && modal.close()} /> } - +
+ +
} From 6194067d5de9f8ed95a2b850b747448d188ea539 Mon Sep 17 00:00:00 2001 From: blinko Date: Wed, 18 Dec 2024 13:21:04 +0800 Subject: [PATCH 6/7] fix: refactor sidebar state management and enhance resizing functionality - Introduced StorageState for sidebar width and collapsed state, allowing persistent storage and validation of sidebar dimensions. - Updated Sidebar component to conditionally apply transition effects based on resizing state. - Enhanced resizing functionality with mouse event handlers for better user interaction. - Improved code readability and maintainability by refactoring sidebar-related logic in BaseStore. --- src/components/Layout/Sidebar.tsx | 18 ++++++-- src/store/baseStore.ts | 67 +++++++++++++++++++++++++++--- src/store/standard/StorageState.ts | 6 ++- 3 files changed, 80 insertions(+), 11 deletions(-) diff --git a/src/components/Layout/Sidebar.tsx b/src/components/Layout/Sidebar.tsx index 2bcab31..abd634a 100644 --- a/src/components/Layout/Sidebar.tsx +++ b/src/components/Layout/Sidebar.tsx @@ -27,15 +27,27 @@ export const Sidebar = observer(({ onItemClick }: SidebarProps) => { useEffect(() => { if (!isPc) { - base.collapseSidebar() + base.collapseSidebar(); } - }, [isPc]) + }, [isPc]); return (
+ {!base.isSidebarCollapsed && ( +
e.stopPropagation()} + style={{ touchAction: 'none' }} + /> + )}
{!base.isSidebarCollapsed && ( theme == 'dark' ? ( diff --git a/src/store/baseStore.ts b/src/store/baseStore.ts index fd63491..db14e8f 100644 --- a/src/store/baseStore.ts +++ b/src/store/baseStore.ts @@ -106,15 +106,70 @@ export class BaseStore implements Store { }, [this.currentRouter, router.pathname]) } - isSidebarCollapsed: boolean = false; - sideBarWidth = 288 + sidebarWidth = new StorageState({ + key: 'sidebar-width', + default: 288, + validate: (value: number) => { + if (value < 220) return 220; + if (value > 400) return 400; + return value; + } + }); + + sidebarCollapsed = new StorageState({ + key: 'sidebar-collapsed', + default: false + }); + + isResizing = false; + isDragging = false; + + get isSidebarCollapsed() { + return this.sidebarCollapsed.value; + } + + get sideBarWidth() { + return this.isSidebarCollapsed ? 80 : this.sidebarWidth.value; + } + + set sideBarWidth(value: number) { + if (!this.isSidebarCollapsed) { + this.sidebarWidth.save(value); + } + } + + startResizing = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + this.isResizing = true; + this.isDragging = true; + document.addEventListener('mousemove', this.handleMouseMove); + document.addEventListener('mouseup', this.stopResizing); + }; + + handleMouseMove = (e: MouseEvent) => { + if (!this.isResizing || this.isSidebarCollapsed) return; + + e.preventDefault(); + const newWidth = Math.max(80, Math.min(400, e.clientX)); + this.sidebarWidth.save(newWidth); + }; + + stopResizing = () => { + this.isResizing = false; + setTimeout(() => { + this.isDragging = false; + }, 50); + document.removeEventListener('mousemove', this.handleMouseMove); + document.removeEventListener('mouseup', this.stopResizing); + }; toggleSidebar = () => { - this.isSidebarCollapsed = !this.isSidebarCollapsed; - this.sideBarWidth = this.isSidebarCollapsed ? 80 : 288 + const newCollapsed = !this.isSidebarCollapsed; + this.sidebarCollapsed.save(newCollapsed); } + collapseSidebar = () => { - this.isSidebarCollapsed = false; - this.sideBarWidth = 288 + this.sidebarCollapsed.save(false); } } diff --git a/src/store/standard/StorageState.ts b/src/store/standard/StorageState.ts index acd9b74..e57f98d 100644 --- a/src/store/standard/StorageState.ts +++ b/src/store/standard/StorageState.ts @@ -4,6 +4,8 @@ export class StorageState { key: string; value: T | any = null; default: T | any = null; + validate?: (value: T) => T; + constructor(args: Partial>) { Object.assign(this, args); makeAutoObservable(this); @@ -37,9 +39,9 @@ export class StorageState { try { if (typeof window == 'undefined') return if (value !== null || value !== undefined) { - this.value = value; + this.value = this.validate ? this.validate(value!) : value; } - window?.localStorage.setItem(this.key, JSON.stringify(value)); + window?.localStorage.setItem(this.key, JSON.stringify(this.value)); } catch (error) { console.error(error); return null; From 9de5e8e8786d1fec3e6781467af9bb4a565333e4 Mon Sep 17 00:00:00 2001 From: blinko Date: Wed, 18 Dec 2024 14:48:52 +0800 Subject: [PATCH 7/7] fix: enhance layout components and improve user interaction - Refactored FilterPop component to wrap the filter icon in a button for better accessibility and interaction. - Updated CommonLayout to integrate BarSearchInput and improve layout structure, enhancing the overall user experience. - Adjusted MobileNavBar styles for better text visibility and alignment, ensuring a more consistent appearance across devices. - Improved responsiveness and overflow handling in layout components to enhance usability on mobile devices. --- .../Common/PopoverFloat/filterPop.tsx | 4 +- src/components/Layout/BarSearchInput.tsx | 122 ++++++++++++++++ src/components/Layout/MobileNavBar.tsx | 8 +- src/components/Layout/index.tsx | 138 +++++++----------- 4 files changed, 183 insertions(+), 89 deletions(-) create mode 100644 src/components/Layout/BarSearchInput.tsx diff --git a/src/components/Common/PopoverFloat/filterPop.tsx b/src/components/Common/PopoverFloat/filterPop.tsx index 73ab948..972ae46 100644 --- a/src/components/Common/PopoverFloat/filterPop.tsx +++ b/src/components/Common/PopoverFloat/filterPop.tsx @@ -69,7 +69,9 @@ export default function FilterPop() { return ( - +
diff --git a/src/components/Layout/BarSearchInput.tsx b/src/components/Layout/BarSearchInput.tsx new file mode 100644 index 0000000..5da4fdb --- /dev/null +++ b/src/components/Layout/BarSearchInput.tsx @@ -0,0 +1,122 @@ +import React, { useRef, useState } from "react"; +import { Button, Input, Tooltip } from "@nextui-org/react"; +import { Icon } from "@iconify/react"; +import { motion, AnimatePresence } from "framer-motion"; +import { useTranslation } from "react-i18next"; +import { _ } from "@/lib/lodash"; +import { useRouter } from "next/router"; +import { RootStore } from "@/store"; +import { BlinkoStore } from "@/store/blinkoStore"; +import { observer } from "mobx-react-lite"; + +interface BarSearchInputProps { + isPc: boolean; +} + +export const BarSearchInput = observer(({ isPc }: BarSearchInputProps) => { + const { t } = useTranslation(); + const router = useRouter(); + const searchInputRef = useRef(null); + const blinkoStore = RootStore.Get(BlinkoStore); + const [showSearchInput, setShowSearchInput] = useState(false); + + const throttleSearchRef = useRef(_.throttle(() => { + if (router.pathname == '/resources') { + return blinkoStore.resourceList.resetAndCall({ searchText: searchInputRef.current?.value }) + } + blinkoStore.noteList.resetAndCall({}) + }, 1000, { trailing: true, leading: false })); + + const handleClose = () => { + setShowSearchInput(false); + blinkoStore.noteListFilterConfig.searchText = ''; + throttleSearchRef.current(); + } + + return ( + <> + {!isPc && !showSearchInput ? ( + + + + ) : ( + + + { + blinkoStore.noteListFilterConfig.searchText = e.target.value + throttleSearchRef.current() + }} + startContent={ + g]:stroke-[2px] ${!isPc ? 'cursor-pointer' : ''}`} + icon={!isPc && showSearchInput ? "material-symbols:close" : "lets-icons:search"} + width="24" + height="24" + onClick={() => !isPc && handleClose()} + /> + } + endContent={router.pathname != '/resources' && ( + + { + searchInputRef.current?.focus() + blinkoStore.noteListFilterConfig.isUseAiQuery = !blinkoStore.noteListFilterConfig.isUseAiQuery + if (blinkoStore.noteListFilterConfig.searchText != '') { + throttleSearchRef.current() + } + }} + /> + + )} + /> + + + )} + + ); +}); \ No newline at end of file diff --git a/src/components/Layout/MobileNavBar.tsx b/src/components/Layout/MobileNavBar.tsx index a1b5a8e..3c48099 100644 --- a/src/components/Layout/MobileNavBar.tsx +++ b/src/components/Layout/MobileNavBar.tsx @@ -24,7 +24,7 @@ export const MobileNavBar = observer(({ onItemClick }: MobileNavBarProps) => { } return ( -
+
{base.routerList.filter(i => !i.hiddenMobile).map(i => ( { >
- -
{t(i.title)}
+ +
{t(i.title)}
))} diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx index c4551df..2d3ac7c 100644 --- a/src/components/Layout/index.tsx +++ b/src/components/Layout/index.tsx @@ -29,6 +29,8 @@ import { api } from "@/lib/trpc"; import { showTipsDialog } from "../Common/TipsDialog"; import { DialogStandaloneStore } from "@/store/module/DialogStandalone"; import { ToastPlugin } from "@/store/module/Toast/Toast"; +import { motion, AnimatePresence } from "framer-motion"; +import { BarSearchInput } from "./BarSearchInput"; export const SideBarItem = "p-2 flex flex-row items-center cursor-pointer gap-2 hover:bg-hover rounded-xl transition-all" @@ -48,14 +50,6 @@ export const CommonLayout = observer(({ const user = RootStore.Get(UserStore) const blinkoStore = RootStore.Get(BlinkoStore) const base = RootStore.Get(BaseStore) - const searchInputRef = useRef(null); - - const throttleSearchRef = useRef(_.throttle(() => { - if (base.currentRouter?.href == '/resources') { - return blinkoStore.resourceList.resetAndCall({ searchText: searchInputRef.current?.value }) - } - blinkoStore.noteList.resetAndCall({}) - }, 1000, { trailing: true, leading: false })); blinkoStore.use() user.use() @@ -84,9 +78,7 @@ export const CommonLayout = observer(({ return (
{blinkoStore.showAi && createPortal(, document.body)} - - setisOpen(false)} @@ -124,88 +116,66 @@ export const CommonLayout = observer(({ width={24} /> } -
-
-
-
{t(base.currentTitle)}
- { - router.pathname != '/trash' - ? blinkoStore.refreshData()} icon="fluent:arrow-sync-12-filled" width="20" height="20" /> - : { - showTipsDialog({ - size: 'sm', - title: t('confirm-to-delete'), - content: t('this-operation-removes-the-associated-label-and-cannot-be-restored-please-confirm'), - onConfirm: async () => { - await RootStore.Get(ToastPlugin).promise( - api.notes.clearRecycleBin.mutate(), - { - loading: t('in-progress'), - success: {t('your-changes-have-been-saved')}, - error: {t('operation-failed')}, - }) - blinkoStore.refreshData() - RootStore.Get(DialogStandaloneStore).close() - } - }) - }} icon="mingcute:delete-2-line" width="20" height="20" /> - } +
+
+
+
+
{t(base.currentTitle)}
+ { + router.pathname != '/trash' + ? blinkoStore.refreshData()} + icon="fluent:arrow-sync-12-filled" + width="20" + height="20" + /> + : { + showTipsDialog({ + size: 'sm', + title: t('confirm-to-delete'), + content: t('this-operation-removes-the-associated-label-and-cannot-be-restored-please-confirm'), + onConfirm: async () => { + await RootStore.Get(ToastPlugin).promise( + api.notes.clearRecycleBin.mutate(), + { + loading: t('in-progress'), + success: {t('your-changes-have-been-saved')}, + error: {t('operation-failed')}, + }) + blinkoStore.refreshData() + RootStore.Get(DialogStandaloneStore).close() + } + }) + }} + icon="mingcute:delete-2-line" + width="20" + height="20" + /> + } +
- { - blinkoStore.noteListFilterConfig.searchText = e.target.value - throttleSearchRef.current() - }} - startContent={ - +
+ + + {blinkoStore.dailyReviewNoteList.value?.length != 0 && + + + + + } - endContent={router.pathname != '/resources' && - { - searchInputRef.current?.focus() - blinkoStore.noteListFilterConfig.isUseAiQuery = !blinkoStore.noteListFilterConfig.isUseAiQuery - if (blinkoStore.noteListFilterConfig.searchText != '') { - throttleSearchRef.current() - } - }} - /> - } - /> - - {blinkoStore.dailyReviewNoteList.value?.length != 0 && - - - - - } +
{header} {/* backdrop pt-6 -mt-6 to fix the editor tooltip position */} - { }} className="flex h-[calc(100%_-_70px)] overflow-y-scroll"> + { }} className="flex h-[calc(100%_-_70px)] overflow-y-scroll overflow-x-hidden">
{children}