Skip to content

Commit 59c5995

Browse files
fix(dashboards): cap dashboard folders and reject folder name collisions
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6aTRnkiu7PxYZPNPXYEMV
1 parent bac1877 commit 59c5995

3 files changed

Lines changed: 27 additions & 7 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/dashboards/dashboard-dialog.tsx‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,14 @@ export function DashboardDialog({
7575
if (!row) return false
7676
return row.id === ancestorId || (row.parentId !== null && isWithin(row.parentId, ancestorId))
7777
}
78+
const folderNameTaken =
79+
(state.kind === 'createFolder' || state.kind === 'editFolder') &&
80+
folders.some(
81+
(folder) =>
82+
('folder' in state ? folder.id !== state.folder.id : true) &&
83+
(folder.parentId ?? '') === target &&
84+
folder.name === name.trim()
85+
)
7886
function save() {
7987
const callbacks = {
8088
onSuccess: (result: Awaited<ReturnType<typeof mutation.mutateAsync>>) => {
@@ -136,7 +144,13 @@ export function DashboardDialog({
136144
</p>
137145
) : (
138146
<>
139-
<ChipModalField type='input' title='Name' value={name} onChange={setName} />
147+
<ChipModalField
148+
type='input'
149+
title='Name'
150+
value={name}
151+
onChange={setName}
152+
error={folderNameTaken ? 'A folder with this name already exists here' : undefined}
153+
/>
140154
{(state.kind === 'edit' || state.kind === 'editFolder') && (
141155
<ChipModalField
142156
type='dropdown'
@@ -166,7 +180,7 @@ export function DashboardDialog({
166180
primaryAction={{
167181
label: deleting ? 'Delete' : 'Save',
168182
variant: deleting ? 'destructive' : 'primary',
169-
disabled: mutation.isPending || (!deleting && !name.trim()),
183+
disabled: mutation.isPending || (!deleting && (!name.trim() || folderNameTaken)),
170184
onClick: save,
171185
}}
172186
/>

‎apps/sim/lib/dashboards/application/dashboards.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { OrchestrationError } from '@/lib/core/orchestration/types'
44
import { dashboardOperations } from '@/lib/dashboards/application/operations'
55
import { requireDashboardsEnabled } from '@/lib/dashboards/feature-flag'
66
import { DASHBOARD_CONTENT_TYPE, dashboardDisplayName } from '@/lib/dashboards/resource'
7-
import { parseDashboardSpec } from '@/lib/dashboards/spec'
7+
import { MAX_DASHBOARD_SOURCE_BYTES, parseDashboardSpec } from '@/lib/dashboards/spec'
88
import { notifyWorkspaceFilesChanged } from '@/lib/realtime/notify'
99
import {
1010
ContentVersionConflictError,
@@ -109,7 +109,7 @@ export const readDashboard = defineAuthorizedWorkspaceFileUseCase({
109109
authorizeResource: ({ context }) => requireDashboardsEnabled(context.workspaceOrganizationId),
110110
async execute({ context }) {
111111
const file = await requireDashboardFile(context.workspaceId, context.fileId)
112-
const content = await fetchWorkspaceFileBuffer(file, { maxBytes: 128 * 1024 })
112+
const content = await fetchWorkspaceFileBuffer(file, { maxBytes: MAX_DASHBOARD_SOURCE_BYTES })
113113
return { dashboard: dashboardRecord(file), content: content.toString('utf-8') }
114114
},
115115
})

‎apps/sim/lib/dashboards/application/folders.ts‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@ import { OrchestrationError } from '@/lib/core/orchestration/types'
44
import { dashboardWorkspace } from '@/lib/dashboards/application/dashboards'
55
import { dashboardOperations } from '@/lib/dashboards/application/operations'
66
import { requireDashboardsEnabled } from '@/lib/dashboards/feature-flag'
7+
import { MAX_FOLDERS_PER_WORKSPACE } from '@/lib/folders/constants'
78
import {
89
createFolderAtPathTransition,
910
deleteFolderByPathTransition,
1011
relocateFolderByPathTransition,
1112
} from '@/lib/folders/orchestration'
12-
import { listFoldersForWorkspace, toFolderApi } from '@/lib/folders/queries'
13+
import { listActiveFolderRows, toFolderApi } from '@/lib/folders/queries'
1314
import { notifyWorkspaceFilesChanged } from '@/lib/realtime/notify'
1415
import { defineAuthorizedWorkspaceFileUseCase } from '@/lib/workspace-files/application/authorized-workspace-file-use-case'
1516

@@ -24,8 +25,10 @@ export const listDashboardFolders = defineAuthorizedWorkspaceFileUseCase({
2425
dashboardWorkspace(input.workspaceId),
2526
authorizeResource: ({ context }) => requireDashboardsEnabled(context.workspaceOrganizationId),
2627
async execute({ context }) {
27-
const folders = await listFoldersForWorkspace(context.workspaceId, 'active', 'dashboard')
28-
return { folders }
28+
const rows = await listActiveFolderRows(context.workspaceId, 'dashboard', {
29+
maxRows: MAX_FOLDERS_PER_WORKSPACE,
30+
})
31+
return { folders: rows.map(toFolderApi) }
2932
},
3033
})
3134

@@ -39,6 +42,7 @@ export const createDashboardFolder = defineAuthorizedWorkspaceFileUseCase({
3942
resourceType: 'dashboard',
4043
userId: requirePrincipalSubjectUserId(principal),
4144
path: input.path,
45+
maxFolderRows: MAX_FOLDERS_PER_WORKSPACE,
4246
effects: false,
4347
throwInfrastructure: true,
4448
})
@@ -71,6 +75,7 @@ export const moveDashboardFolder = defineAuthorizedWorkspaceFileUseCase({
7175
userId: requirePrincipalSubjectUserId(principal),
7276
path: input.path,
7377
destinationPath: input.destinationPath,
78+
maxFolderRows: MAX_FOLDERS_PER_WORKSPACE,
7479
effects: false,
7580
throwInfrastructure: true,
7681
})
@@ -102,6 +107,7 @@ export const deleteDashboardFolder = defineAuthorizedWorkspaceFileUseCase({
102107
userId: requirePrincipalSubjectUserId(principal),
103108
path: input.path,
104109
recursive: true,
110+
maxFolderRows: MAX_FOLDERS_PER_WORKSPACE,
105111
effects: false,
106112
throwInfrastructure: true,
107113
})

0 commit comments

Comments
 (0)