Files
simstudioai--sim/apps/sim/hooks/queries/folders.ts
T
wehub-resource-sync d25d482dc2
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

372 lines
11 KiB
TypeScript

import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { requestJson } from '@/lib/api/client/request'
import {
createFolderContract,
deleteFolderContract,
duplicateFolderContract,
type FolderApi,
listFoldersContract,
reorderFoldersContract,
restoreFolderContract,
updateFolderContract,
} from '@/lib/api/contracts'
import { getFolderMap } from '@/hooks/queries/utils/folder-cache'
import { type FolderQueryScope, folderKeys } from '@/hooks/queries/utils/folder-keys'
import { invalidateWorkflowLists } from '@/hooks/queries/utils/invalidate-workflow-lists'
import {
createOptimisticMutationHandlers,
generateTempId,
} from '@/hooks/queries/utils/optimistic-mutation'
import { getTopInsertionSortOrder } from '@/hooks/queries/utils/top-insertion-sort-order'
import { getWorkflows } from '@/hooks/queries/utils/workflow-cache'
import type { WorkflowFolder } from '@/stores/folders/types'
const logger = createLogger('FolderQueries')
export const FOLDER_LIST_STALE_TIME = 60 * 1000
/**
* Maps a wire folder row to the client `WorkflowFolder` shape (string dates →
* `Date`, color default). Exported so the server-side home prefetch produces
* the exact cached value `useFolders` stores, keeping the hydrated entry in
* sync with a client fetch.
*/
export function mapFolder(folder: FolderApi): WorkflowFolder {
return {
id: folder.id,
name: folder.name,
userId: folder.userId,
workspaceId: folder.workspaceId,
parentId: folder.parentId,
color: folder.color ?? '#6B7280',
isExpanded: folder.isExpanded,
locked: folder.locked,
sortOrder: folder.sortOrder,
createdAt: new Date(folder.createdAt),
updatedAt: new Date(folder.updatedAt),
archivedAt: folder.archivedAt ? new Date(folder.archivedAt) : null,
}
}
async function fetchFolders(
workspaceId: string,
scope: FolderQueryScope = 'active',
signal?: AbortSignal
): Promise<WorkflowFolder[]> {
const { folders } = await requestJson(listFoldersContract, {
query: { workspaceId, scope },
signal,
})
return folders.map(mapFolder)
}
export function useFolders(workspaceId?: string, options?: { scope?: FolderQueryScope }) {
const scope = options?.scope ?? 'active'
return useQuery({
queryKey: folderKeys.list(workspaceId, scope),
queryFn: ({ signal }) => fetchFolders(workspaceId as string, scope, signal),
enabled: Boolean(workspaceId),
placeholderData: keepPreviousData,
staleTime: FOLDER_LIST_STALE_TIME,
})
}
const selectFolderMap = (folders: WorkflowFolder[]): Record<string, WorkflowFolder> =>
Object.fromEntries(folders.map((folder) => [folder.id, folder]))
export function useFolderMap(workspaceId?: string) {
return useQuery({
queryKey: folderKeys.list(workspaceId),
queryFn: ({ signal }) => fetchFolders(workspaceId as string, 'active', signal),
enabled: Boolean(workspaceId),
placeholderData: keepPreviousData,
staleTime: FOLDER_LIST_STALE_TIME,
select: selectFolderMap,
})
}
interface CreateFolderVariables {
workspaceId: string
name: string
parentId?: string
color?: string
sortOrder?: number
id?: string
}
interface UpdateFolderVariables {
workspaceId: string
id: string
updates: Partial<Pick<WorkflowFolder, 'name' | 'parentId' | 'color' | 'sortOrder' | 'locked'>>
}
interface DeleteFolderVariables {
workspaceId: string
id: string
}
interface DuplicateFolderVariables {
workspaceId: string
id: string
name: string
parentId?: string | null
color?: string
newId?: string
}
/**
* Creates optimistic mutation handlers for folder operations
*/
function createFolderMutationHandlers<TVariables extends { workspaceId: string }>(
queryClient: ReturnType<typeof useQueryClient>,
name: string,
createOptimisticFolder: (
variables: TVariables,
tempId: string,
previousFolders: Record<string, WorkflowFolder>
) => WorkflowFolder,
customGenerateTempId?: (variables: TVariables) => string
) {
return createOptimisticMutationHandlers<WorkflowFolder, TVariables, WorkflowFolder>(queryClient, {
name,
getQueryKey: (variables) => folderKeys.list(variables.workspaceId),
getSnapshot: (variables) => ({ ...getFolderMap(variables.workspaceId) }),
generateTempId: customGenerateTempId ?? (() => generateTempId('temp-folder')),
createOptimisticItem: (variables, tempId) => {
const previousFolders = getFolderMap(variables.workspaceId)
return createOptimisticFolder(variables, tempId, previousFolders)
},
applyOptimisticUpdate: (tempId, item) => {
queryClient.setQueryData<WorkflowFolder[]>(folderKeys.list(item.workspaceId), (old) => [
...(old ?? []),
item,
])
},
replaceOptimisticEntry: (tempId, data) => {
queryClient.setQueryData<WorkflowFolder[]>(folderKeys.list(data.workspaceId), (old) =>
(old ?? []).map((folder) => (folder.id === tempId ? data : folder))
)
},
rollback: (snapshot, variables) => {
queryClient.setQueryData(folderKeys.list(variables.workspaceId), Object.values(snapshot))
},
})
}
export function useCreateFolder() {
const queryClient = useQueryClient()
const handlers = createFolderMutationHandlers<CreateFolderVariables>(
queryClient,
'CreateFolder',
(variables, tempId, previousFolders) => {
const currentWorkflows = Object.fromEntries(
getWorkflows(variables.workspaceId).map((w) => [w.id, w])
)
return {
id: tempId,
name: variables.name,
userId: '',
workspaceId: variables.workspaceId,
parentId: variables.parentId || null,
color: variables.color || '#808080',
isExpanded: false,
locked: false,
sortOrder:
variables.sortOrder ??
getTopInsertionSortOrder(
currentWorkflows,
previousFolders,
variables.workspaceId,
variables.parentId
),
createdAt: new Date(),
updatedAt: new Date(),
archivedAt: null,
}
},
(variables) => variables.id ?? generateId()
)
return useMutation({
mutationFn: async ({ workspaceId, sortOrder, ...payload }: CreateFolderVariables) => {
const { folder } = await requestJson(createFolderContract, {
body: { ...payload, workspaceId, sortOrder },
})
return mapFolder(folder)
},
...handlers,
})
}
export function useUpdateFolder() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({ workspaceId, id, updates }: UpdateFolderVariables) => {
const { folder } = await requestJson(updateFolderContract, {
params: { id },
body: updates,
})
return mapFolder(folder)
},
onSettled: (_data, _error, variables) => {
queryClient.invalidateQueries({ queryKey: folderKeys.list(variables.workspaceId) })
},
})
}
export function useDeleteFolderMutation() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({ workspaceId: _workspaceId, id }: DeleteFolderVariables) => {
return requestJson(deleteFolderContract, { params: { id } })
},
onSettled: (_data, _error, variables) => {
queryClient.invalidateQueries({ queryKey: folderKeys.lists() })
return invalidateWorkflowLists(queryClient, variables.workspaceId, ['active', 'archived'])
},
})
}
interface RestoreFolderVariables {
workspaceId: string
folderId: string
}
export function useRestoreFolder() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({ workspaceId, folderId }: RestoreFolderVariables) => {
return requestJson(restoreFolderContract, {
params: { id: folderId },
body: { workspaceId },
})
},
onSettled: (_data, _error, variables) => {
queryClient.invalidateQueries({ queryKey: folderKeys.lists() })
return invalidateWorkflowLists(queryClient, variables.workspaceId, ['active', 'archived'])
},
})
}
export function useDuplicateFolderMutation() {
const queryClient = useQueryClient()
const handlers = createFolderMutationHandlers<DuplicateFolderVariables>(
queryClient,
'DuplicateFolder',
(variables, tempId, previousFolders) => {
const currentWorkflows = Object.fromEntries(
getWorkflows(variables.workspaceId).map((w) => [w.id, w])
)
const sourceFolder = previousFolders[variables.id]
const targetParentId = variables.parentId ?? sourceFolder?.parentId ?? null
return {
id: tempId,
name: variables.name,
userId: sourceFolder?.userId || '',
workspaceId: variables.workspaceId,
parentId: targetParentId,
color: variables.color || sourceFolder?.color || '#808080',
isExpanded: false,
locked: false,
sortOrder: getTopInsertionSortOrder(
currentWorkflows,
previousFolders,
variables.workspaceId,
targetParentId
),
createdAt: new Date(),
updatedAt: new Date(),
archivedAt: null,
}
},
(variables) => variables.newId ?? generateId()
)
return useMutation({
mutationFn: async ({
id,
workspaceId,
name,
parentId,
color,
newId,
}: DuplicateFolderVariables): Promise<WorkflowFolder> => {
const { folder } = await requestJson(duplicateFolderContract, {
params: { id },
body: {
workspaceId,
name,
parentId: parentId ?? null,
color,
newId,
},
})
return mapFolder(folder)
},
...handlers,
onSettled: (_data, _error, variables) => {
queryClient.invalidateQueries({ queryKey: folderKeys.list(variables.workspaceId) })
return invalidateWorkflowLists(queryClient, variables.workspaceId)
},
})
}
interface ReorderFoldersVariables {
workspaceId: string
updates: Array<{
id: string
sortOrder: number
parentId?: string | null
}>
}
export function useReorderFolders() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (variables: ReorderFoldersVariables): Promise<void> => {
await requestJson(reorderFoldersContract, { body: variables })
},
onMutate: async (variables) => {
await queryClient.cancelQueries({ queryKey: folderKeys.list(variables.workspaceId) })
const snapshot = queryClient.getQueryData<WorkflowFolder[]>(
folderKeys.list(variables.workspaceId)
)
const updatesById = new Map(variables.updates.map((update) => [update.id, update]))
queryClient.setQueryData<WorkflowFolder[]>(folderKeys.list(variables.workspaceId), (old) => {
if (!old?.length) return old
return old.map((folder) => {
const update = updatesById.get(folder.id)
if (!update) return folder
return {
...folder,
sortOrder: update.sortOrder,
parentId: update.parentId !== undefined ? update.parentId : folder.parentId,
}
})
})
return { snapshot }
},
onError: (_error, variables, context) => {
if (context?.snapshot) {
queryClient.setQueryData(folderKeys.list(variables.workspaceId), context.snapshot)
}
},
onSettled: (_data, _error, variables) => {
queryClient.invalidateQueries({ queryKey: folderKeys.list(variables.workspaceId) })
},
})
}