Display Folders

This commit is contained in:
Joris Kalz
2024-01-01 23:28:31 +01:00
parent d0ac1d8e1a
commit dbcdbaa893
6 changed files with 641 additions and 5 deletions
+117
View File
@@ -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",
+2
View File
@@ -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>;
};