mirror of
https://github.com/enricoros/big-AGI.git
synced 2026-05-11 14:10:15 -07:00
Display Folders
This commit is contained in:
Generated
+117
@@ -35,6 +35,7 @@
|
||||
"plantuml-encoder": "^1.4.0",
|
||||
"prismjs": "^1.29.0",
|
||||
"react": "^18.2.0",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-katex": "^3.0.1",
|
||||
"react-markdown": "^9.0.1",
|
||||
@@ -54,6 +55,7 @@
|
||||
"@types/plantuml-encoder": "^1.4.2",
|
||||
"@types/prismjs": "^1.26.3",
|
||||
"@types/react": "^18.2.46",
|
||||
"@types/react-beautiful-dnd": "^13.1.8",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/react-katex": "^3.0.4",
|
||||
"@types/react-timeago": "^4.1.7",
|
||||
@@ -1326,6 +1328,15 @@
|
||||
"@types/unist": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/hoist-non-react-statics": {
|
||||
"version": "3.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz",
|
||||
"integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==",
|
||||
"dependencies": {
|
||||
"@types/react": "*",
|
||||
"hoist-non-react-statics": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/json5": {
|
||||
"version": "0.0.29",
|
||||
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
||||
@@ -1395,6 +1406,15 @@
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-beautiful-dnd": {
|
||||
"version": "13.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.8.tgz",
|
||||
"integrity": "sha512-E3TyFsro9pQuK4r8S/OL6G99eq7p8v29sX0PM7oT8Z+PJfZvSQTx4zTQbUJ+QZXioAF0e7TGBEcA1XhYhCweyQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "18.2.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.18.tgz",
|
||||
@@ -1413,6 +1433,17 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-redux": {
|
||||
"version": "7.1.33",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.33.tgz",
|
||||
"integrity": "sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==",
|
||||
"dependencies": {
|
||||
"@types/hoist-non-react-statics": "^3.3.0",
|
||||
"@types/react": "*",
|
||||
"hoist-non-react-statics": "^3.3.0",
|
||||
"redux": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-timeago": {
|
||||
"version": "4.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-timeago/-/react-timeago-4.1.7.tgz",
|
||||
@@ -2226,6 +2257,14 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/css-box-model": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz",
|
||||
"integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==",
|
||||
"dependencies": {
|
||||
"tiny-invariant": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
@@ -4585,6 +4624,11 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/memoize-one": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
||||
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
@@ -5835,6 +5879,11 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/raf-schd": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
|
||||
"integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ=="
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
|
||||
@@ -5846,6 +5895,24 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-beautiful-dnd": {
|
||||
"version": "13.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz",
|
||||
"integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.9.2",
|
||||
"css-box-model": "^1.2.0",
|
||||
"memoize-one": "^5.1.1",
|
||||
"raf-schd": "^4.0.2",
|
||||
"react-redux": "^7.2.0",
|
||||
"redux": "^4.0.4",
|
||||
"use-memo-one": "^1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.5 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
|
||||
@@ -5900,6 +5967,35 @@
|
||||
"react": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-redux": {
|
||||
"version": "7.2.9",
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz",
|
||||
"integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.15.4",
|
||||
"@types/react-redux": "^7.1.20",
|
||||
"hoist-non-react-statics": "^3.3.2",
|
||||
"loose-envify": "^1.4.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-is": "^17.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.3 || ^17 || ^18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-redux/node_modules/react-is": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
|
||||
},
|
||||
"node_modules/react-resizable-panels": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-1.0.5.tgz",
|
||||
@@ -5952,6 +6048,14 @@
|
||||
"string_decoder": "~0.10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/redux": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
|
||||
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.9.2"
|
||||
}
|
||||
},
|
||||
"node_modules/reflect.getprototypeof": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz",
|
||||
@@ -6672,6 +6776,11 @@
|
||||
"xtend": "~2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/tiny-invariant": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz",
|
||||
"integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw=="
|
||||
},
|
||||
"node_modules/to-fast-properties": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
|
||||
@@ -6994,6 +7103,14 @@
|
||||
"integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/use-memo-one": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz",
|
||||
"integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"plantuml-encoder": "^1.4.0",
|
||||
"prismjs": "^1.29.0",
|
||||
"react": "^18.2.0",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-katex": "^3.0.1",
|
||||
"react-markdown": "^9.0.1",
|
||||
@@ -58,6 +59,7 @@
|
||||
"@types/plantuml-encoder": "^1.4.2",
|
||||
"@types/prismjs": "^1.26.3",
|
||||
"@types/react": "^18.2.46",
|
||||
"@types/react-beautiful-dnd": "^13.1.8",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/react-katex": "^3.0.4",
|
||||
"@types/react-timeago": "^4.1.7",
|
||||
|
||||
@@ -11,6 +11,7 @@ import { OpenAIIcon } from '~/common/components/icons/OpenAIIcon';
|
||||
import { useOptimaDrawers } from '~/common/layout/optima/useOptimaDrawers';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
import { useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
import { useFolderStore } from '~/common/state/store-folders';
|
||||
|
||||
import { ChatNavigationItemMemo } from './ChatNavigationItem';
|
||||
import { ChatFolderList } from './folder/ChatFolderList';
|
||||
@@ -27,17 +28,21 @@ function ChatDrawerItems(props: {
|
||||
onConversationImportDialog: () => void,
|
||||
onConversationNew: () => void,
|
||||
onConversationsDeleteAll: () => void,
|
||||
selectedFolderId: string | null,
|
||||
setSelectedFolderId: (folderId: string | null) => void,
|
||||
}) {
|
||||
|
||||
// local state
|
||||
const { onConversationDelete, onConversationNew, onConversationActivate } = props;
|
||||
// const [grouping] = React.useState<ListGrouping>('off');
|
||||
const { selectedFolderId, setSelectedFolderId } = props;
|
||||
|
||||
// external state
|
||||
const { closeDrawer } = useOptimaDrawers();
|
||||
const conversations = useChatStore(state => state.conversations, shallow);
|
||||
const showSymbols = useUIPreferencesStore(state => state.zenMode !== 'cleaner');
|
||||
const labsEnhancedUI = useUXLabsStore(state => state.labsEnhancedUI);
|
||||
const createFolder = useFolderStore((state) => state.createFolder);
|
||||
|
||||
// derived state
|
||||
const maxChatMessages = conversations.reduce((longest, _c) => Math.max(longest, _c.messages.length), 1);
|
||||
@@ -62,6 +67,10 @@ function ChatDrawerItems(props: {
|
||||
!singleChat && conversationId && onConversationDelete(conversationId, true);
|
||||
}, [onConversationDelete, singleChat]);
|
||||
|
||||
const handleFolderSelect = (folderId: string | null) => {
|
||||
// Logic to handle folder selection
|
||||
setSelectedFolderId(folderId);
|
||||
};
|
||||
|
||||
// grouping
|
||||
/*let sortedIds = conversationIDs;
|
||||
@@ -91,8 +100,12 @@ function ChatDrawerItems(props: {
|
||||
{/* </Typography>*/}
|
||||
{/*</ListItem>*/}
|
||||
|
||||
<ChatFolderList />
|
||||
|
||||
<ChatFolderList
|
||||
onFolderSelect={handleFolderSelect}
|
||||
folders={useFolderStore((state) => state.folders)}
|
||||
selectedFolderId={selectedFolderId}
|
||||
conversationsByFolder={conversations}
|
||||
/>
|
||||
<ListDivider sx={{ mb: 0 }} />
|
||||
|
||||
<MenuItem disabled={props.disableNewButton} onClick={handleButtonNew}>
|
||||
|
||||
@@ -2,26 +2,143 @@ import * as React from 'react';
|
||||
|
||||
import Sheet, { sheetClasses } from '@mui/joy/Sheet';
|
||||
import Typography from '@mui/joy/Typography';
|
||||
import { List, ListItem, ListItemButton, ListItemContent, ListItemDecorator } from '@mui/joy';
|
||||
import OutlineFolderIcon from '@mui/icons-material/Folder';
|
||||
import { DragDropContext, Draggable, DropResult } from 'react-beautiful-dnd';
|
||||
|
||||
import { DFolder, useFolderStore } from '~/common/state/store-folders';
|
||||
import { DConversation } from '~/common/state/store-chats';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { AddFolderButton } from './AddFolderButton';
|
||||
import FolderListItem from './FolderListItem';
|
||||
|
||||
import { StrictModeDroppable } from './StrictModeDroppable';
|
||||
|
||||
export function ChatFolderList() {
|
||||
export function ChatFolderList({
|
||||
onFolderSelect,
|
||||
folders,
|
||||
selectedFolderId,
|
||||
}: {
|
||||
onFolderSelect: (folderId: string | null) => void;
|
||||
folders: DFolder[];
|
||||
selectedFolderId: string | null;
|
||||
conversationsByFolder: DConversation[];
|
||||
}) {
|
||||
// local state
|
||||
|
||||
// external state
|
||||
const { moveFolder } = useFolderStore((state) => ({
|
||||
moveFolder: state.moveFolder,
|
||||
}));
|
||||
|
||||
// handlers
|
||||
|
||||
const onDragEnd = (result: DropResult) => {
|
||||
if (!result.destination) return;
|
||||
moveFolder(result.source.index, result.destination.index);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Sheet variant="soft" sx={{ width: 343, p: 2, borderRadius: 'sm' }}>
|
||||
<Typography level="h3" fontSize="xl" fontWeight="xl" mb={1}>
|
||||
Folders
|
||||
</Typography>
|
||||
<div>Folder List</div>
|
||||
<List
|
||||
aria-labelledby="ios-example-demo"
|
||||
sx={(theme) => ({
|
||||
'& ul': {
|
||||
'--List-gap': '0px',
|
||||
bgcolor: 'background.surface',
|
||||
'& > li:first-of-type > [role="button"]': {
|
||||
borderTopRightRadius: 'var(--List-radius)',
|
||||
borderTopLeftRadius: 'var(--List-radius)',
|
||||
},
|
||||
'& > li:last-child > [role="button"]': {
|
||||
borderBottomRightRadius: 'var(--List-radius)',
|
||||
borderBottomLeftRadius: 'var(--List-radius)',
|
||||
},
|
||||
},
|
||||
'--List-radius': '8px',
|
||||
'--List-gap': '1rem',
|
||||
'--ListDivider-gap': '0px',
|
||||
'--ListItem-paddingY': '0.5rem',
|
||||
// override global variant tokens
|
||||
'--joy-palette-neutral-plainHoverBg': 'rgba(0 0 0 / 0.08)',
|
||||
'--joy-palette-neutral-plainActiveBg': 'rgba(0 0 0 / 0.12)',
|
||||
[theme.getColorSchemeSelector('light')]: {
|
||||
'--joy-palette-divider': 'rgba(0 0 0 / 0.08)',
|
||||
},
|
||||
[theme.getColorSchemeSelector('dark')]: {
|
||||
'--joy-palette-neutral-plainHoverBg': 'rgba(255 255 255 / 0.1)',
|
||||
'--joy-palette-neutral-plainActiveBg': 'rgba(255 255 255 / 0.16)',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<ListItem nested>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<StrictModeDroppable
|
||||
droppableId="folder"
|
||||
renderClone={(provided, snapshot, rubric) => (
|
||||
<FolderListItem
|
||||
folder={folders[rubric.source.index]}
|
||||
provided={provided}
|
||||
snapshot={snapshot}
|
||||
onFolderSelect={onFolderSelect}
|
||||
selectedFolderId={selectedFolderId}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
{(provided) => (
|
||||
<List ref={provided.innerRef} {...provided.droppableProps}>
|
||||
<ListItem>
|
||||
<ListItemButton
|
||||
// handle folder select
|
||||
onClick={(event) => {
|
||||
event.stopPropagation(); // Prevent the ListItemButton's onClick from firing
|
||||
onFolderSelect(null);
|
||||
}}
|
||||
selected={selectedFolderId === null}
|
||||
sx={{
|
||||
justifyContent: 'space-between',
|
||||
'&:hover .menu-icon': {
|
||||
visibility: 'visible', // Hide delete icon for default folder
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ListItemDecorator>
|
||||
<OutlineFolderIcon style={{ color: 'inherit' }} />
|
||||
</ListItemDecorator>
|
||||
|
||||
<ListItemContent>
|
||||
<Typography>All</Typography>
|
||||
</ListItemContent>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
|
||||
{folders.map((folder, index) => (
|
||||
<Draggable key={folder.id} draggableId={folder.id} index={index}>
|
||||
{(provided, snapshot) => (
|
||||
<React.Fragment>
|
||||
<FolderListItem
|
||||
folder={folder}
|
||||
provided={provided}
|
||||
snapshot={snapshot}
|
||||
onFolderSelect={onFolderSelect}
|
||||
selectedFolderId={selectedFolderId}
|
||||
/>
|
||||
</React.Fragment>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</List>
|
||||
)}
|
||||
</StrictModeDroppable>
|
||||
</DragDropContext>
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
<AddFolderButton />
|
||||
</Sheet>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,365 @@
|
||||
import React, { useState } from 'react';
|
||||
import { DraggableProvided, DraggableStateSnapshot } from 'react-beautiful-dnd';
|
||||
import {
|
||||
ListItem,
|
||||
ListItemButton,
|
||||
ListItemDecorator,
|
||||
ListItemContent,
|
||||
Typography,
|
||||
IconButton,
|
||||
Dropdown,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItem,
|
||||
FormLabel,
|
||||
RadioGroup,
|
||||
Sheet,
|
||||
Radio,
|
||||
radioClasses,
|
||||
} from '@mui/joy';
|
||||
import OutlineFolderIcon from '@mui/icons-material/Folder';
|
||||
import MoreVert from '@mui/icons-material/MoreVert';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import Done from '@mui/icons-material/Done';
|
||||
import { DFolder, useFolderStore } from '~/common/state/store-folders';
|
||||
import { DraggingStyle, NotDraggingStyle } from 'react-beautiful-dnd';
|
||||
|
||||
// Define the type for your props if you're using TypeScript
|
||||
type RenderItemProps = {
|
||||
folder: DFolder;
|
||||
provided: DraggableProvided;
|
||||
snapshot: DraggableStateSnapshot;
|
||||
onFolderSelect: (folderId: string | null) => void;
|
||||
selectedFolderId: string | null;
|
||||
// Include any other props that RenderItem needs
|
||||
};
|
||||
|
||||
const FolderListItem: React.FC<RenderItemProps> = ({ folder, provided, snapshot, onFolderSelect, selectedFolderId }) => {
|
||||
// internal state
|
||||
const [deleteArmed, setDeleteArmed] = useState(false);
|
||||
const [deleteArmedFolderId, setDeleteArmedFolderId] = useState<string | null>(null);
|
||||
const [editingFolderId, setEditingFolderId] = useState<string | null>(null);
|
||||
const [editingFolderName, setEditingFolderName] = useState<string>('');
|
||||
|
||||
// State to control the open state of the Menu
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
|
||||
// external state
|
||||
const { folders, moveFolder, updateFolderName, deleteFolder } = useFolderStore((state) => ({
|
||||
folders: state.folders,
|
||||
moveFolder: state.moveFolder,
|
||||
updateFolderName: state.updateFolderName,
|
||||
deleteFolder: state.deleteFolder,
|
||||
}));
|
||||
|
||||
const { setFolderColor } = useFolderStore((state) => ({
|
||||
setFolderColor: state.setFolderColor,
|
||||
}));
|
||||
|
||||
const handleColorChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFolderColor(folder.id, event.target.value);
|
||||
setMenuOpen(false);
|
||||
};
|
||||
|
||||
// Handlers for editing and deleting
|
||||
const handleEdit = (event: React.MouseEvent<HTMLElement, MouseEvent>, folderId: string, folderTitle: string) => {
|
||||
event.stopPropagation(); // Prevent the ListItemButton's onClick from firing
|
||||
setEditingFolderId(folderId);
|
||||
setEditingFolderName(folderTitle);
|
||||
};
|
||||
|
||||
const handleSaveFolder = (folderId: string) => {
|
||||
if (editingFolderName.trim() !== '') {
|
||||
updateFolderName(folderId, editingFolderName.trim());
|
||||
}
|
||||
setEditingFolderId(null); // Exit edit mode
|
||||
// Blur the input element if it's currently focused
|
||||
if (document.activeElement instanceof HTMLElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setEditingFolderName(event.target.value);
|
||||
};
|
||||
|
||||
const handleInputKeyUp = (event: React.KeyboardEvent<HTMLInputElement>, folderId: string) => {
|
||||
if (event.key === 'Enter') {
|
||||
handleSaveFolder(folderId);
|
||||
} else if (event.key === 'Escape') {
|
||||
handleCancelEdit();
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelEdit = () => {
|
||||
setEditingFolderId(null); // Exit edit mode without saving
|
||||
setEditingFolderName(''); // Reset editing name
|
||||
};
|
||||
|
||||
// Modified handler to arm the delete action and keep the menu open
|
||||
const handleDeleteButtonShow = (event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
setDeleteArmed(true);
|
||||
setMenuOpen(true); // Keep the menu open
|
||||
};
|
||||
|
||||
// Handler to close the menu
|
||||
const handleCloseMenu = () => {
|
||||
setMenuOpen(false);
|
||||
setDeleteArmed(false); // Reset delete armed state
|
||||
};
|
||||
|
||||
// Handler to disarm the delete action
|
||||
const handleDeleteButtonHide = () => setDeleteArmed(false);
|
||||
|
||||
// Handler to delete the folder
|
||||
const handleDeleteConfirmed = (event: React.MouseEvent) => {
|
||||
if (deleteArmed) {
|
||||
setDeleteArmed(false);
|
||||
event.stopPropagation();
|
||||
deleteFolder(folder.id);
|
||||
setMenuOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Toggle the menu's open state
|
||||
const toggleMenu = () => {
|
||||
setMenuOpen(!menuOpen);
|
||||
};
|
||||
|
||||
const getItemStyle = (isDragging: boolean, draggableStyle: DraggingStyle | NotDraggingStyle | undefined) => ({
|
||||
userSelect: 'none',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: isDragging ? 'rgba(0, 80, 80, 0.18)' : 'transparent',
|
||||
|
||||
...draggableStyle,
|
||||
|
||||
// Any additional styles you want to apply during dragging
|
||||
...(isDragging &&
|
||||
{
|
||||
// Apply any drag-specific styles here
|
||||
// marginLeft: '12px',
|
||||
}),
|
||||
});
|
||||
|
||||
const getListItemContentStyle = (isDragging: boolean, draggableStyle: DraggingStyle | NotDraggingStyle | undefined) => ({
|
||||
...(isDragging && {
|
||||
// Apply any drag-specific styles here
|
||||
marginLeft: '20px',
|
||||
}),
|
||||
});
|
||||
|
||||
const getListItemDecoratorStyle = (isDragging: boolean, draggableStyle: DraggingStyle | NotDraggingStyle | undefined) => ({
|
||||
...(isDragging && {
|
||||
// Apply any drag-specific styles here
|
||||
marginLeft: '12px',
|
||||
}),
|
||||
});
|
||||
|
||||
const handleFolderSelect = (folderId: string | null) => {
|
||||
onFolderSelect(folderId);
|
||||
};
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
style={{
|
||||
...getItemStyle(snapshot.isDragging, provided.draggableProps.style),
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
<ListItemButton
|
||||
// handle folder select
|
||||
onClick={(event) => {
|
||||
event.stopPropagation(); // Prevent the ListItemButton's onClick from firing
|
||||
handleFolderSelect(folder.id);
|
||||
}}
|
||||
selected={folder.id === selectedFolderId}
|
||||
sx={{
|
||||
justifyContent: 'space-between',
|
||||
'&:hover .menu-icon': {
|
||||
visibility: 'visible', // Hide delete icon for default folder
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ListItemDecorator
|
||||
style={{
|
||||
...getListItemDecoratorStyle(snapshot.isDragging, provided.draggableProps.style),
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
<OutlineFolderIcon style={{ color: folder.color || 'inherit' }} />
|
||||
</ListItemDecorator>
|
||||
|
||||
{editingFolderId === folder.id ? (
|
||||
<input
|
||||
type="text"
|
||||
value={editingFolderName}
|
||||
onChange={handleInputChange}
|
||||
onKeyUp={(event) => handleInputKeyUp(event, folder.id)}
|
||||
onBlur={() => handleSaveFolder(folder.id)}
|
||||
autoFocus
|
||||
style={{
|
||||
// Add styles for the input field
|
||||
fontSize: 'inherit',
|
||||
fontWeight: 'inherit',
|
||||
color: 'inherit',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
width: '100%', // Ensure the input field expands as needed
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ListItemContent
|
||||
style={{
|
||||
...getListItemContentStyle(snapshot.isDragging, provided.draggableProps.style),
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
<Typography>{folder.title}</Typography>
|
||||
</ListItemContent>
|
||||
)}
|
||||
|
||||
<Dropdown>
|
||||
<MenuButton
|
||||
className="menu-icon"
|
||||
sx={{ visibility: 'hidden' }}
|
||||
slots={{ root: IconButton }}
|
||||
slotProps={{ root: { variant: 'outlined', color: 'neutral' } }}
|
||||
onClick={toggleMenu}
|
||||
>
|
||||
<MoreVert />
|
||||
</MenuButton>
|
||||
<Menu open={menuOpen} onClose={handleCloseMenu}>
|
||||
<MenuItem
|
||||
onClick={(event) => {
|
||||
handleEdit(event, folder.id, folder.title); // Pass the folder's title here
|
||||
handleCloseMenu();
|
||||
}}
|
||||
>
|
||||
<EditIcon />
|
||||
Edit
|
||||
</MenuItem>
|
||||
{!deleteArmed ? (
|
||||
<MenuItem onClick={handleDeleteButtonShow}>
|
||||
<DeleteOutlineIcon />
|
||||
Delete
|
||||
</MenuItem>
|
||||
) : (
|
||||
<>
|
||||
<MenuItem onClick={handleDeleteConfirmed} color="danger" sx={{ color: 'danger' }}>
|
||||
<DeleteOutlineIcon />
|
||||
Confirm Delete
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleCloseMenu}>
|
||||
<CloseIcon />
|
||||
Cancel
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
<MenuItem
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
p: 2,
|
||||
minWidth: 200,
|
||||
}}
|
||||
>
|
||||
<FormLabel
|
||||
id="folder-color"
|
||||
sx={{
|
||||
mb: 1.5,
|
||||
fontWeight: 'xl',
|
||||
textTransform: 'uppercase',
|
||||
fontSize: 'xs',
|
||||
letterSpacing: '0.1em',
|
||||
}}
|
||||
>
|
||||
Color
|
||||
</FormLabel>
|
||||
<RadioGroup
|
||||
aria-labelledby="product-color-attribute"
|
||||
defaultValue={folder.color || 'warning'}
|
||||
onChange={handleColorChange}
|
||||
sx={{ gap: 2, flexWrap: 'wrap', flexDirection: 'row', maxWidth: 180 }}
|
||||
>
|
||||
{(
|
||||
[
|
||||
'#ff0000',
|
||||
'#ff8700',
|
||||
'#ffd300',
|
||||
'#deff0a',
|
||||
'#a1ff0a',
|
||||
'#8A0000',
|
||||
'#8A3700',
|
||||
'#8A5700',
|
||||
'#7C6A05',
|
||||
'#626906',
|
||||
'#0aff99',
|
||||
'#0aefff',
|
||||
'#147df5',
|
||||
'#580aff',
|
||||
'#be0aff',
|
||||
'#226D40',
|
||||
'#22656D',
|
||||
'#25346A',
|
||||
'#440669',
|
||||
'#6E0569',
|
||||
] as const
|
||||
).map((color, index) => (
|
||||
<Sheet
|
||||
key={index}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
width: 20,
|
||||
height: 20,
|
||||
flexShrink: 0,
|
||||
bgcolor: `${color}`,
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Radio
|
||||
overlay
|
||||
variant="solid"
|
||||
checkedIcon={<Done />}
|
||||
value={color}
|
||||
color="neutral"
|
||||
slotProps={{
|
||||
input: { 'aria-label': color },
|
||||
radio: {
|
||||
sx: {
|
||||
display: 'contents',
|
||||
'--variant-borderWidth': '2px',
|
||||
},
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
'--joy-focus-outlineOffset': '4px',
|
||||
'--joy-palette-focusVisible': color,
|
||||
[`& .${radioClasses.action}.${radioClasses.focusVisible}`]: {
|
||||
outlineWidth: '2px',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Sheet>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Dropdown>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default FolderListItem;
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Droppable, DroppableProps } from "react-beautiful-dnd";
|
||||
|
||||
export const StrictModeDroppable = ({ children, ...props }: DroppableProps) => {
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const animation = requestAnimationFrame(() => setEnabled(true));
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(animation);
|
||||
setEnabled(false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Droppable {...props}>{children}</Droppable>;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user