Skip to content

Commit 49fc9bd

Browse files
committed
Fix Project file UI review regressions and sharing owner
1 parent c3bb018 commit 49fc9bd

13 files changed

Lines changed: 95 additions & 62 deletions

File tree

‎apps/sim/app/o/[organizationId]/home/organization-home.test.tsx‎

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/** @vitest-environment jsdom */
22

33
import { act, type ComponentProps, type ReactNode } from 'react'
4+
import { toast } from '@sim/emcn'
45
import { authClientMock, authClientMockFns } from '@sim/testing/mocks/auth-client.mock'
56
import {
67
createMockDeploymentShape,
@@ -9,7 +10,6 @@ import {
910
} from '@sim/testing/mocks/deployment-shape.mock'
1011
import { integrationMatcherMock } from '@sim/testing/mocks/integration-matcher.mock'
1112
import { kbConnectorsQueriesMock } from '@sim/testing/mocks/kb-connectors-queries.mock'
12-
import { nextNavigationMock } from '@sim/testing/mocks/next-navigation.mock'
1313
import {
1414
organizationProviderMock,
1515
organizationProviderMockFns,
@@ -24,6 +24,8 @@ import { useOrganizationChatModeStore } from '@/stores/organization-chat-mode/st
2424

2525
const mocks = vi.hoisted(() => ({
2626
plan: false,
27+
projects: true,
28+
projectFiles: true,
2729
resourcePanel: vi.fn(),
2830
chat: vi.fn(),
2931
composer: vi.fn(),
@@ -40,11 +42,21 @@ const mocks = vi.hoisted(() => ({
4042
activeResource: null as string | null,
4143
}))
4244
vi.mock('@/app/workspace/[workspaceId]/providers/feature-flags-provider', () => ({
43-
useFeatureFlag: (name: string) => (name === 'mothership-plan-mode' ? mocks.plan : false),
45+
useFeatureFlag: (name: string) =>
46+
name === 'mothership-plan-mode'
47+
? mocks.plan
48+
: name === 'projects'
49+
? mocks.projects
50+
: name === 'project-files'
51+
? mocks.projectFiles
52+
: false,
4453
}))
4554
vi.mock('@/lib/core/config/deployment-shape', () => deploymentShapeMock)
4655
vi.mock('@/blocks/integration-matcher', () => integrationMatcherMock)
47-
vi.mock('next/navigation', () => nextNavigationMock)
56+
vi.mock(
57+
'next/navigation',
58+
async () => (await import('@sim/testing/mocks/next-navigation.mock')).nextNavigationMock
59+
)
4860
vi.mock('@tanstack/react-query', () => reactQueryMock)
4961
vi.mock('@/app/workspace/[workspaceId]/home/hooks/use-resource-panel', () => ({
5062
useResourcePanelController: () => ({
@@ -103,6 +115,8 @@ let root: Root
103115
let container: HTMLDivElement
104116
beforeEach(() => {
105117
useMothershipDraftsStore.setState({ drafts: {} })
118+
mocks.projects = true
119+
mocks.projectFiles = true
106120
mocks.plan = false
107121
mocks.activeResource = null
108122
mockSession.mockReturnValue({ data: { user: { id: 'reader' } } })
@@ -224,3 +238,31 @@ it('isolates saved drafts by user, organization, and conversation', async () =>
224238
await act(async () => renderHome(<OrganizationHome chatId='chat-a' requestMode='assistant' />))
225239
expect(composerProps().value).toBe('')
226240
})
241+
242+
it.each([
243+
[false, true],
244+
[true, false],
245+
[false, false],
246+
])(
247+
'does not look up saved Project references with flags projects=%s, files=%s',
248+
async (projects, projectFiles) => {
249+
mocks.projects = projects
250+
mocks.projectFiles = projectFiles
251+
vi.spyOn(toast, 'error').mockReturnValue('lookup-error')
252+
const fetch = vi.fn().mockRejectedValue(new Error('Project lookup is disabled'))
253+
vi.stubGlobal('fetch', fetch)
254+
await act(async () => renderHome(<OrganizationHome chatId='chat-a' />))
255+
const selectResource = mocks.renderer.mock.lastCall?.[0].onWorkspaceResourceSelect
256+
if (!selectResource) throw new Error('Chat resource selection is unavailable')
257+
await act(async () =>
258+
selectResource({
259+
type: 'file',
260+
id: 'file-a',
261+
title: 'Saved Project file',
262+
owner: { entityType: 'project', entityId: 'project-a' },
263+
})
264+
)
265+
expect(fetch).not.toHaveBeenCalled()
266+
expect(mocks.addResource).not.toHaveBeenCalled()
267+
}
268+
)

‎apps/sim/app/o/[organizationId]/home/organization-home.tsx‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ function OrganizationHomeContent({
9797
const rememberMode = useOrganizationChatModeStore((state) => state.setMode)
9898
const [selectedMode, setSelectedMode] = useState<ChatRequestMode | null>(null)
9999
const planEnabled = useFeatureFlag('mothership-plan-mode')
100+
const projectsEnabled = useFeatureFlag('projects')
101+
const projectFilesEnabled = useFeatureFlag('project-files')
100102
const requestMode =
101103
selectedMode ??
102104
(urlSearchLevel && searchAccess.memberScoped && !chatId ? 'assistant' : undefined) ??
@@ -175,6 +177,7 @@ function OrganizationHomeContent({
175177
async (ref: WorkspaceResourceRef) => {
176178
try {
177179
if (ref.type === 'file' && ref.owner?.entityType === 'project') {
180+
if (!projectsEnabled || !projectFilesEnabled) return
178181
if (!ref.id) throw new Error('Missing Project file identity')
179182
await requestJson(getProjectContract, {
180183
params: { id: ref.owner.entityId },
@@ -217,7 +220,7 @@ function OrganizationHomeContent({
217220
toast.error(`Couldn't open "${ref.title}". Check your access and try again.`)
218221
}
219222
},
220-
[queryClient, addResource, organization.id]
223+
[queryClient, addResource, organization.id, projectsEnabled, projectFilesEnabled]
221224
)
222225
const files = useFileAttachments({
223226
userId: session?.user?.id,

‎apps/sim/app/workspace/[workspaceId]/files/browser.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ interface FileOwnerTabsProps {
8080
}
8181

8282
function FileOwnerTabs({ owner, projectId, workspaceId }: FileOwnerTabsProps) {
83-
const { navigate } = useFileNavigation(owner)
83+
const { navigate } = useFileNavigation({ owner })
8484
const { fileId } = useParams<{ fileId?: string }>()
8585
const [, setLocation] = useQueryStates({
8686
...fileOwnerParsers,

‎apps/sim/app/workspace/[workspaceId]/files/components/file-detail/detail.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ interface FileDetailProps {
2828
/** Shared editor chrome; owner-specific queries and commands remain in their browser adapters. */
2929
export function FileDetail({ viewer, header, showSaveAction = false }: FileDetailProps) {
3030
const owner = viewer.owner ?? { entityType: 'workspace' as const, entityId: viewer.workspaceId }
31-
const navigation = useFileNavigation(owner)
31+
const navigation = useFileNavigation({ owner })
3232
const [previewMode, setPreviewMode] = useState<PreviewMode>(() =>
3333
!viewer.autoFocus && isPreviewable(viewer.file) ? 'preview' : 'editor'
3434
)

‎apps/sim/app/workspace/[workspaceId]/files/components/file-detail/navigation.tsx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,11 @@ export function FileNavigationProvider({ owner, fileId, children }: FileNavigati
103103
)
104104
}
105105

106-
export function useFileNavigation(owner: EditableFileOwner) {
106+
interface UseFileNavigationProps {
107+
owner: EditableFileOwner
108+
}
109+
110+
export function useFileNavigation({ owner }: UseFileNavigationProps) {
107111
const controller = useContext(FileNavigationContext)
108112
if (
109113
!controller ||

‎apps/sim/app/workspace/[workspaceId]/files/components/file-history-modal.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export function FileHistoryModal({
4747
const history = useFileHistory(owner, fileId, true)
4848
const revert = useRevertFileVersion(owner, fileId)
4949
const download = useDownloadFileVersion(owner, fileId)
50-
const navigation = useFileNavigation(owner)
50+
const navigation = useFileNavigation({ owner })
5151
const [scrollElement, setScrollElement] = useState<HTMLDivElement | null>(null)
5252
const [target, setTarget] = useState<{ version: number; expectedRevision: string } | null>(null)
5353
const edges = useScrollEdges(scrollElement)

‎apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/editor-lifecycle.test.tsx‎

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@ import { LoadedRichMarkdownEditor } from '@/app/workspace/[workspaceId]/files/co
2525

2626
nextNavigationMockFns.mockUsePathname.mockReturnValue('/workspace/workspace-1/files')
2727

28-
const { collaborationRef, uploadFile } = vi.hoisted(() => ({
28+
const { collaborationRef, uploadFile, uploadProjectFile, fileSource } = vi.hoisted(() => ({
2929
collaborationRef: { current: null as unknown },
3030
uploadFile: vi.fn(),
31+
uploadProjectFile: vi.fn(),
32+
fileSource: { owner: undefined as { entityType: 'project'; entityId: string } | undefined },
3133
}))
3234

3335
vi.mock(
@@ -38,9 +40,12 @@ vi.mock('@/lib/auth/auth-client', () => authClientMock)
3840
vi.mock('@/hooks/queries/workspace-files', () => ({
3941
useUploadWorkspaceFile: () => ({ mutateAsync: uploadFile }),
4042
}))
43+
vi.mock('@/hooks/queries/project-files', () => ({
44+
useUploadProjectFile: () => ({ mutateAsync: uploadProjectFile }),
45+
}))
4146
vi.mock('@/hooks/use-add-to-chat', () => ({ useAddToChat: () => vi.fn() }))
4247
vi.mock('@/hooks/use-file-content-source', () => ({
43-
useFileContentSource: () => ({ resolveImageSrc: (src: string) => src }),
48+
useFileContentSource: () => ({ owner: fileSource.owner, resolveImageSrc: (src: string) => src }),
4449
}))
4550
vi.mock('@/app/workspace/[workspaceId]/components', () => ({ FindBar: () => null }))
4651
vi.mock(
@@ -174,7 +179,7 @@ async function render(
174179
<Suspense fallback='Loading editor'>
175180
<LoadedRichMarkdownEditor
176181
file={FILE}
177-
workspaceId={FILE.workspaceId}
182+
workspaceId={fileSource.owner ? undefined : FILE.workspaceId}
178183
content={content}
179184
acceptedBaselineContent={acceptedBaselineContent}
180185
isStreaming={options.isStreaming ?? false}
@@ -207,6 +212,8 @@ function getEditor() {
207212

208213
beforeEach(() => {
209214
uploadFile.mockReset()
215+
uploadProjectFile.mockReset()
216+
fileSource.owner = undefined
210217
collaborationRef.current = null
211218
vi.spyOn(toast, 'warning').mockReturnValue('test-toast')
212219
vi.spyOn(toast, 'info').mockReturnValue('uploading-toast')
@@ -484,3 +491,17 @@ describe('loaded rich editor lifecycle', () => {
484491
expect(abandonedSave).not.toHaveBeenCalled()
485492
})
486493
})
494+
495+
it('inserts an uploaded inline image using its Project content address', async () => {
496+
fileSource.owner = { entityType: 'project', entityId: 'project-a' }
497+
uploadProjectFile.mockResolvedValue({ file: { id: 'image-a' } })
498+
await render('Project document')
499+
await act(async () => getEditor().storage.slashCommand.insertImage(1))
500+
const input = container.querySelector<HTMLInputElement>('input[type="file"]')
501+
if (!input) throw new Error('Editor image picker is unavailable')
502+
Object.defineProperty(input, 'files', {
503+
value: [new File(['image'], 'diagram.png', { type: 'image/png' })],
504+
})
505+
await act(async () => input.dispatchEvent(new Event('change', { bubbles: true })))
506+
expect(getEditor().getHTML()).toContain('src="/api/projects/project-a/files/image-a/content"')
507+
})

‎apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.tsx‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -689,7 +689,10 @@ export function LoadedRichMarkdownEditor({
689689
folderId: file.folderId ?? null,
690690
signal: controller.signal,
691691
})
692-
.then((result) => `/api/files/view/${encodeURIComponent(result.file.id)}`)
692+
.then(
693+
(result) =>
694+
`/api/projects/${encodeURIComponent(projectId)}/files/${encodeURIComponent(result.file.id)}/content`
695+
)
693696
: workspaceId
694697
? uploadFile
695698
.mutateAsync({

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ function FilesContent() {
218218
setIsDirty,
219219
setSaveStatus,
220220
navigate: handleNavigateFromFileDetail,
221-
} = useFileNavigation({ entityType: 'workspace', entityId: workspaceId })
221+
} = useFileNavigation({ owner: { entityType: 'workspace', entityId: workspaceId } })
222222

223223
const posthog = usePostHog()
224224
const posthogRef = useRef(posthog)

‎apps/sim/app/workspace/[workspaceId]/files/project-files.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ function ProjectFilesContent({ project, workspaceId }: ProjectFilesProps) {
110110
useFileListRoom({ owner: { entityType: 'project', entityId: project.id } })
111111
const uploadInput = useRef<HTMLInputElement>(null)
112112
const downloadInFlight = useRef(false)
113-
const navigation = useFileNavigation({ entityType: 'project', entityId: project.id })
113+
const navigation = useFileNavigation({ owner: { entityType: 'project', entityId: project.id } })
114114
const [isDownloading, setIsDownloading] = useState(false)
115115
const [extractTarget, setExtractTarget] = useState<{ id: string; name: string } | null>(null)
116116
const [contextRowId, setContextRowId] = useState<string | null>(null)
@@ -196,7 +196,7 @@ function ProjectFilesContent({ project, workspaceId }: ProjectFilesProps) {
196196
: contextFile
197197
? { kind: 'file' as const, ...contextFile }
198198
: null
199-
const currentFolder = allFolders.find((folder) => folder.id === folderId)
199+
const currentFolder = archived ? undefined : allFolders.find((folder) => folder.id === folderId)
200200
const base = `/workspace/${encodeURIComponent(workspaceId)}/files`
201201
const locationState = {
202202
owner: 'project' as const,

0 commit comments

Comments
 (0)