Skip to content

Commit 8e500b9

Browse files
committed
refactor: folder management
1 parent 9e9d3fb commit 8e500b9

6 files changed

Lines changed: 521 additions & 269 deletions

File tree

apps/desktop/src/entities/database/store/helpers.ts

Lines changed: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -164,19 +164,28 @@ export function toggleFolder(id: string, schema: string, folder: string) {
164164
})
165165
}
166166

167-
export function addTableToGroup(id: string, schema: string, folder: string, table: string) {
167+
export function addTableToFolder(id: string, schema: string, folder: string, table: string) {
168168
const store = databaseStore(id)
169169
store.setState((state) => {
170-
const existingGroup = state.tableGroups.find(g => g.schema === schema && g.folder === folder)
170+
// First, remove the table from any other folder in the same schema
171+
const updatedFolders = state.tableFolders
172+
.map(g =>
173+
g.schema === schema && g.tables.includes(table)
174+
? { ...g, tables: g.tables.filter(t => t !== table) }
175+
: g,
176+
)
177+
.filter(g => g.tables.length > 0)
178+
179+
const existingFolder = updatedFolders.find(g => g.schema === schema && g.folder === folder)
171180

172-
if (existingGroup) {
173-
if (existingGroup.tables.includes(table)) {
181+
if (existingFolder) {
182+
if (existingFolder.tables.includes(table)) {
174183
return state
175184
}
176185

177186
return {
178187
...state,
179-
tableGroups: state.tableGroups.map(g =>
188+
tableFolders: updatedFolders.map(g =>
180189
g.schema === schema && g.folder === folder
181190
? { ...g, tables: [...g.tables, table] }
182191
: g,
@@ -186,16 +195,16 @@ export function addTableToGroup(id: string, schema: string, folder: string, tabl
186195

187196
return {
188197
...state,
189-
tableGroups: [...state.tableGroups, { schema, folder, tables: [table] }],
198+
tableFolders: [...updatedFolders, { schema, folder, tables: [table] }],
190199
} satisfies typeof state
191200
})
192201
}
193202

194-
export function removeTableFromGroup(id: string, schema: string, folder: string, table: string) {
203+
export function removeTableFromFolder(id: string, schema: string, folder: string, table: string) {
195204
const store = databaseStore(id)
196205
store.setState(state => ({
197206
...state,
198-
tableGroups: state.tableGroups
207+
tableFolders: state.tableFolders
199208
.map(g =>
200209
g.schema === schema && g.folder === folder
201210
? { ...g, tables: g.tables.filter(t => t !== table) }
@@ -205,11 +214,22 @@ export function removeTableFromGroup(id: string, schema: string, folder: string,
205214
} satisfies typeof state))
206215
}
207216

208-
export function renameGroup(id: string, schema: string, oldFolder: string, newFolder: string) {
217+
export function folderExists(id: string, schema: string, folder: string): boolean {
209218
const store = databaseStore(id)
219+
return store.state.tableFolders.some(g => g.schema === schema && g.folder === folder)
220+
}
221+
222+
export function renameFolder(id: string, schema: string, oldFolder: string, newFolder: string): boolean {
223+
const store = databaseStore(id)
224+
225+
// Check if a folder with the new name already exists
226+
if (folderExists(id, schema, newFolder)) {
227+
return false
228+
}
229+
210230
store.setState(state => ({
211231
...state,
212-
tableGroups: state.tableGroups.map(g =>
232+
tableFolders: state.tableFolders.map(g =>
213233
g.schema === schema && g.folder === oldFolder
214234
? { ...g, folder: newFolder }
215235
: g,
@@ -220,13 +240,15 @@ export function renameGroup(id: string, schema: string, oldFolder: string, newFo
220240
: f,
221241
),
222242
} satisfies typeof state))
243+
244+
return true
223245
}
224246

225-
export function deleteGroup(id: string, schema: string, folder: string) {
247+
export function deleteFolder(id: string, schema: string, folder: string) {
226248
const store = databaseStore(id)
227249
store.setState(state => ({
228250
...state,
229-
tableGroups: state.tableGroups.filter(g => !(g.schema === schema && g.folder === folder)),
251+
tableFolders: state.tableFolders.filter(g => !(g.schema === schema && g.folder === folder)),
230252
tablesTreeOpenedFolders: state.tablesTreeOpenedFolders.filter(f => !(f.schema === schema && f.folder === folder)),
231253
} satisfies typeof state))
232254
}
@@ -262,20 +284,32 @@ export function selectMultipleTables(id: string, tables: { schema: string, table
262284
} satisfies typeof state))
263285
}
264286

265-
export function addMultipleTablesToGroup(id: string, schema: string, folder: string, tables: string[]) {
287+
export function addMultipleTablesToFolder(id: string, schema: string, folder: string, tables: string[]) {
266288
const store = databaseStore(id)
267289
store.setState((state) => {
268-
const existingGroup = state.tableGroups.find(g => g.schema === schema && g.folder === folder)
290+
// First, remove all these tables from any other folder in the same schema
291+
const updatedFolders = state.tableFolders
292+
.map(g =>
293+
g.schema === schema
294+
? { ...g, tables: g.tables.filter(t => !tables.includes(t)) }
295+
: g,
296+
)
297+
.filter(g => g.tables.length > 0)
269298

270-
if (existingGroup) {
271-
const newTables = tables.filter(t => !existingGroup.tables.includes(t))
299+
const existingFolder = updatedFolders.find(g => g.schema === schema && g.folder === folder)
300+
301+
if (existingFolder) {
302+
const newTables = tables.filter(t => !existingFolder.tables.includes(t))
272303
if (newTables.length === 0) {
273-
return state
304+
return {
305+
...state,
306+
tableFolders: updatedFolders,
307+
} satisfies typeof state
274308
}
275309

276310
return {
277311
...state,
278-
tableGroups: state.tableGroups.map(g =>
312+
tableFolders: updatedFolders.map(g =>
279313
g.schema === schema && g.folder === folder
280314
? { ...g, tables: [...g.tables, ...newTables] }
281315
: g,
@@ -285,7 +319,7 @@ export function addMultipleTablesToGroup(id: string, schema: string, folder: str
285319

286320
return {
287321
...state,
288-
tableGroups: [...state.tableGroups, { schema, folder, tables }],
322+
tableFolders: [...updatedFolders, { schema, folder, tables }],
289323
} satisfies typeof state
290324
})
291325
}

apps/desktop/src/entities/database/store/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export const databaseStoreType = type({
4848
schema: 'string',
4949
folder: 'string',
5050
}).array(),
51-
tableGroups: type({
51+
tableFolders: type({
5252
schema: 'string',
5353
folder: 'string',
5454
tables: 'string[]',
@@ -94,7 +94,7 @@ const defaultState: typeof databaseStoreType.infer = {
9494
tablesSearch: '',
9595
tablesTreeOpenedSchemas: null,
9696
tablesTreeOpenedFolders: [],
97-
tableGroups: [],
97+
tableFolders: [],
9898
selectedTables: [],
9999
pinnedTables: [],
100100
layout: {
@@ -150,7 +150,7 @@ export function databaseStore(id: string) {
150150
tablesSearch: currentVal.tablesSearch,
151151
tablesTreeOpenedSchemas: currentVal.tablesTreeOpenedSchemas,
152152
tablesTreeOpenedFolders: currentVal.tablesTreeOpenedFolders,
153-
tableGroups: currentVal.tableGroups,
153+
tableFolders: currentVal.tableFolders,
154154
selectedTables: currentVal.selectedTables,
155155
pinnedTables: currentVal.pinnedTables,
156156
layout: currentVal.layout,

apps/desktop/src/routes/(protected)/_protected/database/$id/table/-components/add-to-folder-dialog.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { RiAddLine, RiFolderLine } from '@remixicon/react'
1414
import { useStore } from '@tanstack/react-store'
1515
import { useImperativeHandle, useState } from 'react'
1616
import { toast } from 'sonner'
17-
import { addTableToGroup, databaseStore } from '~/entities/database/store'
17+
import { addTableToFolder, databaseStore } from '~/entities/database/store'
1818

1919
interface AddToFolderDialogProps {
2020
ref: React.RefObject<{
@@ -31,7 +31,7 @@ export function AddToFolderDialog({ ref, database }: AddToFolderDialogProps) {
3131
const [showNewFolder, setShowNewFolder] = useState(false)
3232

3333
const store = databaseStore(database.id)
34-
const tableGroups = useStore(store, state => state.tableGroups)
34+
const tableFolders = useStore(store, state => state.tableFolders)
3535

3636
useImperativeHandle(ref, () => ({
3737
addToFolder: (schema: string, table: string) => {
@@ -43,12 +43,12 @@ export function AddToFolderDialog({ ref, database }: AddToFolderDialogProps) {
4343
},
4444
}))
4545

46-
const existingFolders = tableGroups
46+
const existingFolders = tableFolders
4747
.filter(g => g.schema === schema)
4848
.map(g => g.folder)
4949

5050
const handleAddToExistingFolder = (folder: string) => {
51-
addTableToGroup(database.id, schema, folder, table)
51+
addTableToFolder(database.id, schema, folder, table)
5252
toast.success(`Table "${table}" added to folder "${folder}"`)
5353
setOpen(false)
5454
}
@@ -64,7 +64,7 @@ export function AddToFolderDialog({ ref, database }: AddToFolderDialogProps) {
6464
return
6565
}
6666

67-
addTableToGroup(database.id, schema, newFolderName.trim(), table)
67+
addTableToFolder(database.id, schema, newFolderName.trim(), table)
6868
toast.success(`Table "${table}" added to new folder "${newFolderName.trim()}"`)
6969
setOpen(false)
7070
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import type { databases } from '~/drizzle'
2+
import { Alert, AlertDescription, AlertTitle } from '@conar/ui/components/alert'
3+
import { Button } from '@conar/ui/components/button'
4+
import {
5+
Dialog,
6+
DialogClose,
7+
DialogContent,
8+
DialogFooter,
9+
DialogHeader,
10+
DialogTitle,
11+
} from '@conar/ui/components/dialog'
12+
import { Input } from '@conar/ui/components/input'
13+
import { Label } from '@conar/ui/components/label'
14+
import { RiAlertLine } from '@remixicon/react'
15+
import { useImperativeHandle, useState } from 'react'
16+
import { toast } from 'sonner'
17+
import { deleteFolder } from '~/entities/database/store'
18+
19+
interface DeleteFolderDialogProps {
20+
ref: React.RefObject<{
21+
deleteFolder: (schema: string, folder: string) => void
22+
} | null>
23+
database: typeof databases.$inferSelect
24+
}
25+
26+
export function DeleteFolderDialog({ ref, database }: DeleteFolderDialogProps) {
27+
const [confirmationText, setConfirmationText] = useState('')
28+
const [schema, setSchema] = useState('')
29+
const [folder, setFolder] = useState('')
30+
const [open, setOpen] = useState(false)
31+
32+
useImperativeHandle(ref, () => ({
33+
deleteFolder: (schema, folder) => {
34+
setSchema(schema)
35+
setFolder(folder)
36+
setConfirmationText('')
37+
setOpen(true)
38+
},
39+
}))
40+
41+
const handleDelete = () => {
42+
deleteFolder(database.id, schema, folder)
43+
toast.success(`Folder "${folder}" successfully deleted`)
44+
setOpen(false)
45+
setConfirmationText('')
46+
}
47+
48+
const canConfirm = confirmationText === folder
49+
50+
return (
51+
<Dialog open={open} onOpenChange={setOpen}>
52+
<DialogContent>
53+
<DialogHeader>
54+
<DialogTitle>
55+
Delete Folder
56+
</DialogTitle>
57+
<div className="space-y-4">
58+
<Alert variant="destructive">
59+
<RiAlertLine className="size-5 text-destructive" />
60+
<AlertTitle>This will delete the folder organization.</AlertTitle>
61+
<AlertDescription>
62+
The tables inside this folder will not be deleted, only the folder organization will be removed.
63+
</AlertDescription>
64+
</Alert>
65+
<div className="space-y-2">
66+
<Label htmlFor="confirmation">
67+
Type
68+
{' '}
69+
<span className="font-mono font-semibold">
70+
{folder}
71+
</span>
72+
{' '}
73+
to confirm
74+
</Label>
75+
<Input
76+
id="confirmation"
77+
value={confirmationText}
78+
onChange={e => setConfirmationText(e.target.value)}
79+
placeholder={folder}
80+
autoComplete="off"
81+
/>
82+
</div>
83+
</div>
84+
</DialogHeader>
85+
<DialogFooter>
86+
<DialogClose asChild>
87+
<Button variant="outline">
88+
Cancel
89+
</Button>
90+
</DialogClose>
91+
<Button
92+
variant="destructive"
93+
disabled={!canConfirm}
94+
onClick={handleDelete}
95+
>
96+
Delete Folder
97+
</Button>
98+
</DialogFooter>
99+
</DialogContent>
100+
</Dialog>
101+
)
102+
}

apps/desktop/src/routes/(protected)/_protected/database/$id/table/-components/rename-folder-dialog.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { Input } from '@conar/ui/components/input'
1212
import { Label } from '@conar/ui/components/label'
1313
import { useImperativeHandle, useState } from 'react'
1414
import { toast } from 'sonner'
15-
import { renameGroup } from '~/entities/database/store'
15+
import { folderExists, renameFolder } from '~/entities/database/store'
1616

1717
interface RenameFolderDialogProps {
1818
ref: React.RefObject<{
@@ -47,9 +47,20 @@ export function RenameFolderDialog({ ref, database }: RenameFolderDialogProps) {
4747
return
4848
}
4949

50-
renameGroup(database.id, schema, folder, newFolderName.trim())
51-
toast.success(`Folder "${folder}" renamed to "${newFolderName.trim()}"`)
52-
setOpen(false)
50+
if (folderExists(database.id, schema, newFolderName.trim())) {
51+
toast.error(`A folder named "${newFolderName.trim()}" already exists in this schema`)
52+
return
53+
}
54+
55+
const success = renameFolder(database.id, schema, folder, newFolderName.trim())
56+
57+
if (success) {
58+
toast.success(`Folder "${folder}" renamed to "${newFolderName.trim()}"`)
59+
setOpen(false)
60+
}
61+
else {
62+
toast.error('Failed to rename folder')
63+
}
5364
}
5465

5566
const canConfirm = newFolderName.trim() !== '' && newFolderName.trim() !== folder

0 commit comments

Comments
 (0)