Skip to content

Commit bac1877

Browse files
fix(dashboards): address review round on the restored resource
Fork payloads accept file discovery, dashboard queries are keyed and invalidated per workspace, dashboard contexts open tabs and resolve in organization chats, archived dashboard folders keep their paths, and the folder dialog skips no-op moves and hides the edited subtree. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S6aTRnkiu7PxYZPNPXYEMV
1 parent 6f27ddd commit bac1877

17 files changed

Lines changed: 120 additions & 62 deletions

File tree

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

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,16 @@ export function DashboardDialog({
6565
if (!row) return ''
6666
return `${row.parentId ? pathFor(row.parentId) : ''}/${encodeFolderPathSegment(row.name)}`
6767
}
68+
function labelFor(id: string): string {
69+
const row = folders.find((folder) => folder.id === id)
70+
if (!row) return ''
71+
return row.parentId ? `${labelFor(row.parentId)} / ${row.name}` : row.name
72+
}
73+
function isWithin(id: string, ancestorId: string): boolean {
74+
const row = folders.find((folder) => folder.id === id)
75+
if (!row) return false
76+
return row.id === ancestorId || (row.parentId !== null && isWithin(row.parentId, ancestorId))
77+
}
6878
function save() {
6979
const callbacks = {
7080
onSuccess: (result: Awaited<ReturnType<typeof mutation.mutateAsync>>) => {
@@ -95,16 +105,15 @@ export function DashboardDialog({
95105
callbacks
96106
)
97107
break
98-
case 'editFolder':
99-
mutation.mutate(
100-
{
101-
operation: 'moveFolder',
102-
path: state.path,
103-
destinationPath: `${pathFor(target)}/${encodeFolderPathSegment(name)}`,
104-
},
105-
callbacks
106-
)
108+
case 'editFolder': {
109+
const destinationPath = `${pathFor(target)}/${encodeFolderPathSegment(name)}`
110+
if (destinationPath === state.path) {
111+
onClose()
112+
break
113+
}
114+
mutation.mutate({ operation: 'moveFolder', path: state.path, destinationPath }, callbacks)
107115
break
116+
}
108117
case 'deleteFolder':
109118
mutation.mutate({ operation: 'deleteFolder', path: state.path }, callbacks)
110119
break
@@ -137,8 +146,10 @@ export function DashboardDialog({
137146
options={[
138147
{ value: '', label: 'Dashboards' },
139148
...folders
140-
.filter((folder) => !('folder' in state) || folder.id !== state.folder.id)
141-
.map((folder) => ({ value: folder.id, label: pathFor(folder.id) })),
149+
.filter(
150+
(folder) => !('folder' in state) || !isWithin(folder.id, state.folder.id)
151+
)
152+
.map((folder) => ({ value: folder.id, label: labelFor(folder.id) })),
142153
]}
143154
/>
144155
)}

‎apps/sim/app/workspace/[workspaceId]/files/hooks/use-workspace-files-room.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export function useWorkspaceFilesRoom(workspaceId: string): void {
2121
ROOM_TYPES.WORKSPACE_FILES,
2222
() => {
2323
invalidateWorkspaceFileBrowsers(queryClient, workspaceId)
24-
void queryClient.invalidateQueries({ queryKey: dashboardKeys.all })
24+
void queryClient.invalidateQueries({ queryKey: dashboardKeys.workspace(workspaceId) })
2525
},
2626
WORKSPACE_FILE_BROWSER_INVALIDATION_KEY
2727
)

‎apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.tsx‎

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ import type {
9696
// ConnectServiceAccountModal, and that edge would pull the modal into this
9797
// chunk and defeat the lazy() split below.
9898
import { useServiceAccountConnectTarget } from '@/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/use-service-account-connect'
99+
import { useFeatureFlag } from '@/app/workspace/[workspaceId]/providers/feature-flags-provider'
99100
import { useOptionalWorkspaceHostContext } from '@/app/workspace/[workspaceId]/providers/workspace-host-provider'
100101
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
101102
import { BrandIcon } from '@/blocks/brand-icon'
@@ -105,6 +106,7 @@ import {
105106
useWorkspaceCredential,
106107
useWorkspaceCredentials,
107108
} from '@/hooks/queries/credentials'
109+
import { useDashboards } from '@/hooks/queries/dashboards'
108110
import {
109111
usePersonalEnvironment,
110112
useSavePersonalEnvironment,
@@ -1994,6 +1996,14 @@ function WorkspaceResourceDisplayContent({
19941996
const { data: tables = [] } = useTablesList(workspaceId)
19951997
const { data: files = [] } = useWorkspaceFiles(workspaceId)
19961998
const { data: knowledgeBases = [] } = useKnowledgeBasesQuery(workspaceId)
1999+
const dashboardsEnabled = useFeatureFlag('dashboards')
2000+
const { data: dashboardList } = useDashboards(
2001+
workspaceId,
2002+
{},
2003+
{
2004+
enabled: dashboardsEnabled && data.type === 'dashboard',
2005+
}
2006+
)
19972007

19982008
const resource = useMemo<WorkspaceResourceRef>(() => {
19992009
const fileFromPath =
@@ -2005,13 +2015,17 @@ function WorkspaceResourceDisplayContent({
20052015
: data.type === 'table'
20062016
? (tables.find((table) => table.id === data.id)?.name ??
20072017
fallbackWorkspaceResourceTitle(data.type))
2008-
: data.type === 'file' || data.type === 'dashboard'
2009-
? (files.find((file) => file.id === data.id)?.name ??
2010-
fileFromPath?.name ??
2018+
: data.type === 'dashboard'
2019+
? (dashboardList?.dashboards.find((dashboard) => dashboard.id === data.id)?.name ??
20112020
data.title ??
20122021
fallbackWorkspaceResourceTitle(data.type))
2013-
: (knowledgeBases.find((knowledgeBase) => knowledgeBase.id === data.id)?.name ??
2014-
fallbackWorkspaceResourceTitle(data.type))
2022+
: data.type === 'file'
2023+
? (files.find((file) => file.id === data.id)?.name ??
2024+
fileFromPath?.name ??
2025+
data.title ??
2026+
fallbackWorkspaceResourceTitle(data.type))
2027+
: (knowledgeBases.find((knowledgeBase) => knowledgeBase.id === data.id)?.name ??
2028+
fallbackWorkspaceResourceTitle(data.type))
20152029

20162030
const id = data.id ?? fileFromPath?.id
20172031
return {
@@ -2026,6 +2040,7 @@ function WorkspaceResourceDisplayContent({
20262040
data.path,
20272041
data.title,
20282042
data.type,
2043+
dashboardList,
20292044
files,
20302045
knowledgeBases,
20312046
tables,

‎apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/resource-invalidation.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ const RESOURCE_INVALIDATORS: Record<
2828
invalidate(qc, id ? tableKeys.views(id) : tableKeys.viewsRoot())
2929
},
3030
dashboard: (qc, wId, id) => {
31-
invalidate(qc, dashboardKeys.lists())
31+
invalidate(qc, dashboardKeys.lists(wId))
3232
invalidate(qc, dashboardKeys.folders(wId))
33-
invalidate(qc, id ? dashboardKeys.detail(wId, id) : dashboardKeys.details())
33+
invalidate(qc, id ? dashboardKeys.detail(wId, id) : dashboardKeys.details(wId))
3434
},
3535
file: (qc, wId, id) => {
3636
invalidate(qc, workspaceFilesKeys.lists())

‎apps/sim/app/workspace/[workspaceId]/home/home.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,8 @@ function HomeContent({ chatId, userName, userId }: HomeProps) {
314314
return context.fileId ? { type: 'file', id: context.fileId } : null
315315
case 'file_selection':
316316
return context.fileId ? { type: 'file', id: context.fileId } : null
317+
case 'dashboard':
318+
return { type: 'dashboard', id: context.fileId }
317319
default:
318320
return null
319321
}

‎apps/sim/ee/workspace-forking/application/content-outbox.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { db } from '@sim/db'
2-
import { workspaceOperationReceipt } from '@sim/db/schema'
2+
import { fileDiscoveryEnum, workspaceOperationReceipt } from '@sim/db/schema'
33
import { and, eq } from 'drizzle-orm'
44
import { z } from 'zod'
55
import { OrchestrationError } from '@/lib/core/orchestration/types'
@@ -107,6 +107,7 @@ const contentPayloadSchema = z
107107
'logs',
108108
'workspace-logos',
109109
]),
110+
discovery: z.enum(fileDiscoveryEnum.enumValues).optional(),
110111
fileName: z.string().max(4096),
111112
contentType: z.string().max(1024),
112113
size: z.number().min(0),

‎apps/sim/hooks/queries/dashboards.ts‎

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
1+
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
22
import { requestJson } from '@/lib/api/client/request'
33
import {
44
type CreateDashboardBody,
@@ -21,13 +21,13 @@ import { workspaceFilesKeys } from '@/hooks/queries/workspace-files'
2121
export const DASHBOARD_STALE_TIME = 30_000
2222
export const dashboardKeys = {
2323
all: ['dashboards'] as const,
24-
lists: () => [...dashboardKeys.all, 'list'] as const,
24+
workspace: (workspaceId: string) => [...dashboardKeys.all, workspaceId] as const,
25+
lists: (workspaceId: string) => [...dashboardKeys.workspace(workspaceId), 'list'] as const,
2526
list: (workspaceId: string, query: ListDashboardsQuery = {}) =>
26-
[...dashboardKeys.lists(), workspaceId, query.search ?? '', query.folder ?? ''] as const,
27-
details: () => [...dashboardKeys.all, 'detail'] as const,
28-
detail: (workspaceId: string, id: string) =>
29-
[...dashboardKeys.details(), workspaceId, id] as const,
30-
folders: (workspaceId: string) => [...dashboardKeys.all, 'folders', workspaceId] as const,
27+
[...dashboardKeys.lists(workspaceId), query.search ?? '', query.folder ?? ''] as const,
28+
details: (workspaceId: string) => [...dashboardKeys.workspace(workspaceId), 'detail'] as const,
29+
detail: (workspaceId: string, id: string) => [...dashboardKeys.details(workspaceId), id] as const,
30+
folders: (workspaceId: string) => [...dashboardKeys.workspace(workspaceId), 'folders'] as const,
3131
}
3232
export function useDashboards(
3333
workspaceId: string,
@@ -44,7 +44,12 @@ export function useDashboards(
4444
}),
4545
enabled: Boolean(workspaceId) && (options?.enabled ?? true),
4646
staleTime: DASHBOARD_STALE_TIME,
47-
placeholderData: keepPreviousData,
47+
placeholderData: (previous, previousQuery) =>
48+
dashboardKeys
49+
.lists(workspaceId)
50+
.every((part, index) => previousQuery?.queryKey[index] === part)
51+
? previous
52+
: undefined,
4853
})
4954
}
5055
export function useDashboard(workspaceId: string, dashboardId: string) {
@@ -115,7 +120,7 @@ export function useDashboardMutation(workspaceId: string) {
115120
}
116121
},
117122
onSuccess: (_data, input) => {
118-
void client.invalidateQueries({ queryKey: dashboardKeys.lists() })
123+
void client.invalidateQueries({ queryKey: dashboardKeys.lists(workspaceId) })
119124
void client.invalidateQueries({ queryKey: dashboardKeys.folders(workspaceId) })
120125
void client.invalidateQueries({ queryKey: workspaceFilesKeys.lists() })
121126
if ('dashboardId' in input)

‎apps/sim/lib/api/contracts/dashboards.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export const createDashboardBodySchema = z
3232
.object({
3333
name: dashboardNameSchema,
3434
content: dashboardContentSchema,
35-
folderId: z.string().min(1).nullable().optional(),
35+
folderId: folderIdSchema.nullable().optional(),
3636
})
3737
.strict()
3838
export const updateDashboardBodySchema = z
@@ -41,7 +41,7 @@ export const updateDashboardBodySchema = z
4141
export const moveDashboardBodySchema = z
4242
.object({
4343
name: dashboardNameSchema.optional(),
44-
folderId: z.string().min(1).nullable().optional(),
44+
folderId: folderIdSchema.nullable().optional(),
4545
})
4646
.strict()
4747
export const listDashboardsQuerySchema = z.object({

‎apps/sim/lib/api/contracts/mothership-management-tools.test.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,20 @@ const examples = {
3232
{ action: 'setup', connectorType: 'google_drive', accessMode: 'admin' },
3333
{ action: 'approve', connectorType: 'google_drive', approved: true },
3434
],
35+
dashboards: [
36+
{ action: 'list', search: 'support' },
37+
{ action: 'get', dashboardId: 'dash-1' },
38+
{ action: 'create', name: 'Support', content: 'title: Support\nblocks: []', folderId: null },
39+
{ action: 'update', dashboardId: 'dash-1', content: 'title: Support', expectedRevision: 'r1' },
40+
{ action: 'move', dashboardId: 'dash-1', folderId: null },
41+
{ action: 'delete', dashboardId: 'dash-1' },
42+
],
43+
dashboard_folders: [
44+
{ action: 'list' },
45+
{ action: 'create', path: '/Operations' },
46+
{ action: 'move', path: '/Operations', destinationPath: '/Ops' },
47+
{ action: 'delete', path: '/Operations' },
48+
],
3549
}
3650

3751
describe('management tool provider contract', () => {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ function validateContent(content: string) {
7171
}
7272

7373
function storageName(name: string) {
74-
const clean = name.trim().replace(/\.dashboard$/, '')
74+
const clean = name.trim().replace(/\.dashboard$/i, '')
7575
if (!clean || clean.length > 220 || /[/\\]/.test(clean) || clean === '.' || clean === '..') {
7676
throw new OrchestrationError(
7777
'validation',

0 commit comments

Comments
 (0)