diff --git a/packages/common/assets/src/images/icon_file.png b/packages/common/assets/src/images/icon_file.png new file mode 100644 index 000000000..b7d06d287 Binary files /dev/null and b/packages/common/assets/src/images/icon_file.png differ diff --git a/packages/common/assets/src/images/icon_folder_off.png b/packages/common/assets/src/images/icon_folder_off.png new file mode 100644 index 000000000..67a608a73 Binary files /dev/null and b/packages/common/assets/src/images/icon_folder_off.png differ diff --git a/packages/common/assets/src/images/icon_folder_on.png b/packages/common/assets/src/images/icon_folder_on.png new file mode 100644 index 000000000..ca89a4830 Binary files /dev/null and b/packages/common/assets/src/images/icon_folder_on.png differ diff --git a/packages/common/assets/src/images/index.ts b/packages/common/assets/src/images/index.ts index dd4ccb0b2..7abbeb673 100644 --- a/packages/common/assets/src/images/index.ts +++ b/packages/common/assets/src/images/index.ts @@ -1,6 +1,10 @@ export const images = { logo: require("./logo.png"), menuBtn: require("./mob_menuBtn.svg"), + menuBtnClose: require("./mob_menuBtn_close.svg"), + iconFolderOff: require("./icon_folder_off.png"), + iconFolderOn: require("./icon_folder_on.png"), + iconFile: require("./icon_file.png"), } as const; export type ImageType = keyof typeof images; diff --git a/packages/common/components/package.json b/packages/common/components/package.json index fedb50ec5..a2f9889ec 100644 --- a/packages/common/components/package.json +++ b/packages/common/components/package.json @@ -26,6 +26,7 @@ }, "dependencies": { "@common/assets": "workspace:*", + "@minoru/react-dnd-treeview": "^3.5.0", "@radix-ui/react-accordion": "^1.2.3", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.6", @@ -47,6 +48,8 @@ "lucide-react": "^0.474.0", "react-chartjs-2": "^5.3.0", "react-day-picker": "^8.10.1", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", "tailwind-merge": "^3.0.1", "tailwindcss-animate": "^1.0.7" }, diff --git a/packages/common/components/src/ui/tree/CustomNode.tsx b/packages/common/components/src/ui/tree/CustomNode.tsx new file mode 100644 index 000000000..75974e8e0 --- /dev/null +++ b/packages/common/components/src/ui/tree/CustomNode.tsx @@ -0,0 +1,39 @@ +import React from "react"; +import { type NodeModel } from "@minoru/react-dnd-treeview"; +import { type CustomData } from "./types"; +import { TypeIcon } from "./TypeIcon"; + +type Props = { + node: NodeModel; + depth: number; + isOpen: boolean; + onToggle: (id: NodeModel["id"]) => void; +}; + +export const CustomNode: React.FC = (props) => { + const { droppable, id, text } = props.node; + const indent = props.depth * 24; + + const handleToggle = (e: React.MouseEvent) => { + if (!droppable) return; + e.stopPropagation(); + props.onToggle(id); + }; + + return ( +
+
+
+ +
+
+
+ {text} +
+
+ ); +}; diff --git a/packages/common/components/src/ui/tree/TypeIcon.tsx b/packages/common/components/src/ui/tree/TypeIcon.tsx new file mode 100644 index 000000000..f99a91639 --- /dev/null +++ b/packages/common/components/src/ui/tree/TypeIcon.tsx @@ -0,0 +1,19 @@ +import React from "react"; +import { images } from "@common/assets"; + +type Props = { + droppable?: boolean; + isOpen?: boolean; +}; + +export const TypeIcon: React.FC = (props) => { + if (props.droppable) { + return ( + + ); + } + return ; +}; diff --git a/packages/common/components/src/ui/tree/__mock__.json b/packages/common/components/src/ui/tree/__mock__.json new file mode 100644 index 000000000..f86fa3ad7 --- /dev/null +++ b/packages/common/components/src/ui/tree/__mock__.json @@ -0,0 +1,38 @@ +[ + { + "id": 1, + "parent": 0, + "droppable": true, + "text": "Folder 1" + }, + { + "id": 2, + "parent": 1, + "droppable": false, + "text": "File 1-1" + }, + { + "id": 3, + "parent": 1, + "droppable": false, + "text": "File 1-2" + }, + { + "id": 4, + "parent": 0, + "droppable": true, + "text": "Folder 2" + }, + { + "id": 5, + "parent": 4, + "droppable": true, + "text": "Folder 2-1" + }, + { + "id": 6, + "parent": 5, + "droppable": false, + "text": "File 2-1-1" + } +] diff --git a/packages/common/components/src/ui/tree/tree.stories.tsx b/packages/common/components/src/ui/tree/tree.stories.tsx new file mode 100644 index 000000000..6678f756b --- /dev/null +++ b/packages/common/components/src/ui/tree/tree.stories.tsx @@ -0,0 +1,68 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { useState } from "react"; +import { DndProvider } from "react-dnd"; +import { + Tree, + MultiBackend, + getBackendOptions, + type NodeModel, +} from "@minoru/react-dnd-treeview"; +import { type CustomData } from "./types"; +import { CustomNode } from "./CustomNode"; +import SampleData from "./__mock__.json"; + +const meta: Meta = { + title: "UI/Tree", + component: Tree, + parameters: { + docs: { + description: { + component: ` +- [Docs](https://github.com/minop1205/react-dnd-treeview/blob/next/README.md) + `, + }, + }, + }, +}; +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: () => , +}; + +function TreeView() { + const [treeData, setTreeData] = useState[]>(SampleData); + const handleDrop = (newTree: NodeModel[]) => setTreeData(newTree); + + return ( + +
+ + tree={treeData} + rootId={0} + render={( + node: NodeModel, + { depth, isOpen, onToggle }, + ) => ( + + )} + onDrop={handleDrop} + classes={{ + root: "", + container: "flex flex-col", + listItem: "", + dropTarget: "bg-primary rounded-sm", + draggingSource: "", + placeholder: "", + }} + /> +
+
+ ); +} diff --git a/packages/common/components/src/ui/tree/types.ts b/packages/common/components/src/ui/tree/types.ts new file mode 100644 index 000000000..36cfd322b --- /dev/null +++ b/packages/common/components/src/ui/tree/types.ts @@ -0,0 +1,4 @@ +export type CustomData = { + fileType: string; + fileSize: string; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e72143d63..762d9fae0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -121,6 +121,9 @@ importers: '@common/assets': specifier: workspace:* version: link:../assets + '@minoru/react-dnd-treeview': + specifier: ^3.5.0 + version: 3.5.0(dnd-core@16.0.1)(react-dnd@16.0.1)(react-dom@18.3.1)(react@18.3.1) '@radix-ui/react-accordion': specifier: ^1.2.3 version: 1.2.3(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) @@ -184,6 +187,12 @@ importers: react-day-picker: specifier: ^8.10.1 version: 8.10.1(date-fns@3.6.0)(react@18.3.1) + react-dnd: + specifier: ^16.0.1 + version: 16.0.1(@types/node@22.13.5)(@types/react@18.3.18)(react@18.3.1) + react-dnd-html5-backend: + specifier: ^16.0.1 + version: 16.0.1 tailwind-merge: specifier: ^3.0.1 version: 3.0.2 @@ -228,7 +237,7 @@ importers: specifier: ^8.5.5 version: 8.6.0(storybook@8.6.0) '@types/node': - specifier: ^22.13.5 + specifier: ^22.13.1 version: 22.13.5 '@vitejs/plugin-react': specifier: ^4.3.4 @@ -457,7 +466,6 @@ packages: engines: {node: '>=6.9.0'} dependencies: regenerator-runtime: 0.14.1 - dev: true /@babel/template@7.26.9: resolution: {integrity: sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==} @@ -1360,6 +1368,10 @@ packages: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + /@juggle/resize-observer@3.4.0: + resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} + dev: false + /@kurkle/color@0.3.4: resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} dev: false @@ -1375,6 +1387,27 @@ packages: react: 18.3.1 dev: true + /@minoru/react-dnd-treeview@3.5.0(dnd-core@16.0.1)(react-dnd@16.0.1)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-kDHGQ0UK18w6ie5EH68NynATSP9BAzNLmWBXuVSoaGkraWWFdEdAWqQxAZiMorAixFu40MzYkn2N9/Gam6q17w==} + peerDependencies: + react: ^18.3.1 + react-dnd: 15.x || 16.x + react-dom: ^18.3.1 + dependencies: + '@juggle/resize-observer': 3.4.0 + dnd-multi-backend: 8.1.2(dnd-core@16.0.1) + framer-motion: 11.18.2(react-dom@18.3.1)(react@18.3.1) + react: 18.3.1 + react-dnd: 16.0.1(@types/node@22.13.5)(@types/react@18.3.18)(react@18.3.1) + react-dnd-html5-backend: 16.0.1 + react-dnd-touch-backend: 16.0.1 + react-dom: 18.3.1(react@18.3.1) + react-use-measure: 2.1.7(react-dom@18.3.1)(react@18.3.1) + transitivePeerDependencies: + - '@emotion/is-prop-valid' + - dnd-core + dev: false + /@next/env@15.1.7: resolution: {integrity: sha512-d9jnRrkuOH7Mhi+LHav2XW91HOgTAWHxjMPkXMGBc9B2b7614P7kjt8tAplRvJpbSt4nbO1lugcT/kAaWzjlLQ==} dev: false @@ -2225,6 +2258,18 @@ packages: resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} dev: false + /@react-dnd/asap@5.0.2: + resolution: {integrity: sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==} + dev: false + + /@react-dnd/invariant@4.0.2: + resolution: {integrity: sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==} + dev: false + + /@react-dnd/shallowequal@4.0.2: + resolution: {integrity: sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==} + dev: false + /@rollup/pluginutils@5.1.4: resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==} engines: {node: '>=14.0.0'} @@ -2928,7 +2973,6 @@ packages: resolution: {integrity: sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==} dependencies: undici-types: 6.20.0 - dev: true /@types/node@22.13.9: resolution: {integrity: sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==} @@ -4098,6 +4142,22 @@ packages: /dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + /dnd-core@16.0.1: + resolution: {integrity: sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==} + dependencies: + '@react-dnd/asap': 5.0.2 + '@react-dnd/invariant': 4.0.2 + redux: 4.2.1 + dev: false + + /dnd-multi-backend@8.1.2(dnd-core@16.0.1): + resolution: {integrity: sha512-KPDVEsiM+6gNEegqZYTWJQgJxYV4vB91tUrvoKJjaS0wwWqT/jNU0P7xJAwCue/cbasJNvk2dFZH7tC+bjX1Rg==} + peerDependencies: + dnd-core: ^16.0.1 + dependencies: + dnd-core: 16.0.1 + dev: false + /doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} @@ -4745,7 +4805,6 @@ packages: /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - dev: true /fast-glob@3.3.1: resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} @@ -4872,6 +4931,27 @@ packages: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} dev: true + /framer-motion@11.18.2(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.3.1 + react-dom: ^18.3.1 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + dependencies: + motion-dom: 11.18.1 + motion-utils: 11.18.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tslib: 2.8.1 + dev: false + /fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -5083,6 +5163,12 @@ packages: dependencies: function-bind: 1.1.2 + /hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + dependencies: + react-is: 16.13.1 + dev: false + /ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -5653,6 +5739,16 @@ packages: node-source-walk: 7.0.1 dev: true + /motion-dom@11.18.1: + resolution: {integrity: sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==} + dependencies: + motion-utils: 11.18.1 + dev: false + + /motion-utils@11.18.1: + resolution: {integrity: sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==} + dev: false + /ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} dev: true @@ -6216,6 +6312,44 @@ packages: react: 18.3.1 dev: false + /react-dnd-html5-backend@16.0.1: + resolution: {integrity: sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==} + dependencies: + dnd-core: 16.0.1 + dev: false + + /react-dnd-touch-backend@16.0.1: + resolution: {integrity: sha512-NonoCABzzjyWGZuDxSG77dbgMZ2Wad7eQiCd/ECtsR2/NBLTjGksPUx9UPezZ1nQ/L7iD130Tz3RUshL/ClKLA==} + dependencies: + '@react-dnd/invariant': 4.0.2 + dnd-core: 16.0.1 + dev: false + + /react-dnd@16.0.1(@types/node@22.13.5)(@types/react@18.3.18)(react@18.3.1): + resolution: {integrity: sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==} + peerDependencies: + '@types/hoist-non-react-statics': '>= 3.3.1' + '@types/node': '>= 12' + '@types/react': ^18.3.18 + react: ^18.3.1 + peerDependenciesMeta: + '@types/hoist-non-react-statics': + optional: true + '@types/node': + optional: true + '@types/react': + optional: true + dependencies: + '@react-dnd/invariant': 4.0.2 + '@react-dnd/shallowequal': 4.0.2 + '@types/node': 22.13.5 + '@types/react': 18.3.18 + dnd-core: 16.0.1 + fast-deep-equal: 3.1.3 + hoist-non-react-statics: 3.3.2 + react: 18.3.1 + dev: false + /react-docgen-typescript@2.2.2(typescript@5.6.3): resolution: {integrity: sha512-tvg2ZtOpOi6QDwsb3GZhOjDkkX0h8Z2gipvTg6OVMUyoYoURhEiRNePT8NZItTVCDh39JJHnLdfCOkzoLbFnTg==} peerDependencies: @@ -6253,7 +6387,6 @@ packages: /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - dev: true /react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} @@ -6315,6 +6448,19 @@ packages: tslib: 2.8.1 dev: false + /react-use-measure@2.1.7(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==} + peerDependencies: + react: ^18.3.1 + react-dom: ^18.3.1 + peerDependenciesMeta: + react-dom: + optional: true + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + /react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -6356,6 +6502,12 @@ packages: strip-indent: 3.0.0 dev: true + /redux@4.2.1: + resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==} + dependencies: + '@babel/runtime': 7.26.9 + dev: false + /reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -6372,7 +6524,6 @@ packages: /regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} - dev: true /regexp.prototype.flags@1.5.4: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} @@ -7305,7 +7456,6 @@ packages: /undici-types@6.20.0: resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} - dev: true /unicorn-magic@0.3.0: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==}