@shipfox/client-projects 5.0.0 → 6.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +57 -0
- package/dist/chrome.d.ts.map +1 -1
- package/dist/chrome.js +7 -10
- package/dist/chrome.js.map +1 -1
- package/dist/components/model-provider-reminder-banner.d.ts.map +1 -1
- package/dist/components/model-provider-reminder-banner.js +1 -1
- package/dist/components/model-provider-reminder-banner.js.map +1 -1
- package/dist/components/source-strip.d.ts +2 -2
- package/dist/components/source-strip.d.ts.map +1 -1
- package/dist/components/source-strip.js +2 -2
- package/dist/components/source-strip.js.map +1 -1
- package/dist/core/definition.d.ts +32 -0
- package/dist/core/definition.d.ts.map +1 -0
- package/dist/core/definition.js +3 -0
- package/dist/core/definition.js.map +1 -0
- package/dist/core/project.d.ts +26 -0
- package/dist/core/project.d.ts.map +1 -0
- package/dist/core/project.js +12 -0
- package/dist/core/project.js.map +1 -0
- package/dist/feature.d.ts +8 -0
- package/dist/feature.d.ts.map +1 -1
- package/dist/feature.js +11 -10
- package/dist/feature.js.map +1 -1
- package/dist/hooks/api/definitions.d.ts +12 -88
- package/dist/hooks/api/definitions.d.ts.map +1 -1
- package/dist/hooks/api/definitions.js +18 -10
- package/dist/hooks/api/definitions.js.map +1 -1
- package/dist/hooks/api/mappers.d.ts +17 -0
- package/dist/hooks/api/mappers.d.ts.map +1 -0
- package/dist/hooks/api/mappers.js +56 -0
- package/dist/hooks/api/mappers.js.map +1 -0
- package/dist/hooks/api/projects.d.ts +18 -84
- package/dist/hooks/api/projects.d.ts.map +1 -1
- package/dist/hooks/api/projects.js +72 -29
- package/dist/hooks/api/projects.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/pages/create-project-page.d.ts.map +1 -1
- package/dist/pages/create-project-page.js +12 -37
- package/dist/pages/create-project-page.js.map +1 -1
- package/dist/pages/create-project-page.stories.js +3 -1
- package/dist/pages/create-project-page.stories.js.map +1 -1
- package/dist/pages/home-router.d.ts +4 -1
- package/dist/pages/home-router.d.ts.map +1 -1
- package/dist/pages/home-router.js +4 -2
- package/dist/pages/home-router.js.map +1 -1
- package/dist/pages/projects-hub-page.d.ts +3 -1
- package/dist/pages/projects-hub-page.d.ts.map +1 -1
- package/dist/pages/projects-hub-page.js +26 -30
- package/dist/pages/projects-hub-page.js.map +1 -1
- package/dist/routes/home.d.ts +3 -2
- package/dist/routes/home.d.ts.map +1 -1
- package/dist/routes/home.js +7 -2
- package/dist/routes/home.js.map +1 -1
- package/dist/routes/inputs.d.ts +6 -0
- package/dist/routes/inputs.d.ts.map +1 -0
- package/dist/routes/inputs.js +14 -0
- package/dist/routes/inputs.js.map +1 -0
- package/dist/routes/search.d.ts +5 -0
- package/dist/routes/search.d.ts.map +1 -0
- package/dist/routes/search.js +8 -0
- package/dist/routes/search.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +14 -15
- package/src/chrome.tsx +11 -6
- package/src/components/model-provider-reminder-banner.tsx +1 -3
- package/src/components/project-switcher.test.tsx +2 -1
- package/src/components/source-strip.tsx +5 -5
- package/src/core/definition.ts +31 -0
- package/src/core/project.ts +47 -0
- package/src/feature.ts +13 -11
- package/src/hooks/api/definitions.ts +54 -19
- package/src/hooks/api/mappers.ts +67 -0
- package/src/hooks/api/projects.test.ts +11 -4
- package/src/hooks/api/projects.ts +114 -39
- package/src/index.ts +9 -0
- package/src/pages/create-project-page.stories.tsx +6 -1
- package/src/pages/create-project-page.test.tsx +3 -1
- package/src/pages/create-project-page.tsx +16 -51
- package/src/pages/home-router.tsx +3 -2
- package/src/pages/projects-hub-page.test.tsx +13 -8
- package/src/pages/projects-hub-page.tsx +24 -34
- package/src/routes/home.tsx +6 -2
- package/src/routes/inputs.test.ts +14 -0
- package/src/routes/inputs.ts +15 -0
- package/src/routes/search.ts +8 -0
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/pages/create-project-page.tsx"],"sourcesContent":["import type {IntegrationConnectionDto} from '@shipfox/api-integration-core-dto';\nimport {\n createProjectBodySchema,\n type ListProjectsResponseDto,\n type ProjectResponseDto,\n} from '@shipfox/api-projects-dto';\nimport {useMaybeActiveWorkspace} from '@shipfox/client-auth';\nimport {\n ConnectionPicker,\n IntegrationIcon,\n RepositoryPicker,\n useRepositoriesInfiniteQuery,\n useSourceConnectionsQuery,\n} from '@shipfox/client-integrations';\nimport {displayNameFieldError} from '@shipfox/client-ui';\nimport {Button} from '@shipfox/react-ui/button';\nimport {Callout} from '@shipfox/react-ui/callout';\nimport {FormField, FormFieldInput, fieldError} from '@shipfox/react-ui/form-field';\nimport {FullPageLoader} from '@shipfox/react-ui/loader';\nimport {toast} from '@shipfox/react-ui/toast';\nimport {Header, Text} from '@shipfox/react-ui/typography';\nimport {useForm} from '@tanstack/react-form';\nimport {type QueryClient, useQueryClient} from '@tanstack/react-query';\nimport {Link, Navigate, useNavigate} from '@tanstack/react-router';\nimport {useEffect, useRef, useState} from 'react';\nimport {ModelProviderReminderBanner} from '#components/model-provider-reminder-banner.js';\nimport {projectsQueryKeys, useCreateProjectMutation} from '#hooks/api/projects.js';\nimport {projectErrorCopy} from '#project-error.js';\n\nconst REPOSITORY_NAME_SPLIT_RE = /[/-]/;\n\nexport function CreateProjectPage() {\n const workspace = useMaybeActiveWorkspace();\n const queryClient = useQueryClient();\n const navigate = useNavigate();\n const createProject = useCreateProjectMutation();\n const errorRef = useRef<HTMLDivElement>(null);\n\n const connectionsQuery = useSourceConnectionsQuery(workspace?.id);\n const connections = connectionsQuery.data?.connections ?? [];\n\n const [selectedConnectionId, setSelectedConnectionId] = useState<string | undefined>();\n const singleConnectionId = connections.length === 1 ? connections[0]?.id : undefined;\n const effectiveSelectedConnectionId = selectedConnectionId ?? singleConnectionId;\n useEffect(() => {\n if (singleConnectionId && selectedConnectionId !== singleConnectionId) {\n setSelectedConnectionId(singleConnectionId);\n }\n }, [singleConnectionId, selectedConnectionId]);\n\n const selectedConnection: IntegrationConnectionDto | undefined = connections.find(\n (connection) => connection.id === effectiveSelectedConnectionId,\n );\n\n const [repoFilter, setRepoFilter] = useState('');\n const debouncedRepoFilter = useDebouncedValue(repoFilter, 250);\n const trimmedFilter = debouncedRepoFilter.trim();\n\n const repositoriesQuery = useRepositoriesInfiniteQuery(\n effectiveSelectedConnectionId,\n trimmedFilter ? {search: trimmedFilter} : undefined,\n );\n const repositories = repositoriesQuery.data?.pages.flatMap((page) => page.repositories) ?? [];\n\n const [selectedRepositoryId, setSelectedRepositoryId] = useState<string | undefined>();\n useEffect(() => {\n if (!selectedRepositoryId && repositories[0]) {\n setSelectedRepositoryId(repositories[0].external_repository_id);\n }\n }, [repositories, selectedRepositoryId]);\n const selectedRepository = repositories.find(\n (repository) => repository.external_repository_id === selectedRepositoryId,\n );\n\n const [nameTouched, setNameTouched] = useState(false);\n const defaultProjectName = projectNameFromRepository(\n selectedRepository?.name ?? selectedRepositoryId ?? '',\n );\n\n const [formError, setFormError] = useState<string | undefined>();\n\n const form = useForm({\n defaultValues: {name: defaultProjectName},\n onSubmit: async ({value}) => {\n await createProjectFromForm(nameTouched ? value.name : defaultProjectName);\n },\n });\n\n useEffect(() => {\n if (!nameTouched && form.state.values.name !== defaultProjectName) {\n form.setFieldValue('name', defaultProjectName);\n }\n }, [defaultProjectName, form, nameTouched]);\n\n function selectConnection(connectionId: string) {\n setSelectedConnectionId(connectionId);\n setSelectedRepositoryId(undefined);\n }\n\n if (connectionsQuery.isPending) {\n return <FullPageLoader />;\n }\n\n if (!workspace) {\n return <FullPageLoader />;\n }\n\n if (!connectionsQuery.isError && connections.length === 0) {\n return <Navigate to=\"/workspaces/$wid/integrations\" params={{wid: workspace.id}} replace />;\n }\n\n async function createProjectFromForm(projectName: string) {\n setFormError(undefined);\n if (!workspace) {\n setFormError('Workspace is still loading. Try again in a moment.');\n errorRef.current?.focus();\n return;\n }\n if (!selectedConnection) {\n setFormError('Choose a source integration before creating a project.');\n errorRef.current?.focus();\n return;\n }\n if (!selectedRepository) {\n setFormError('Choose a repository before creating a project.');\n errorRef.current?.focus();\n return;\n }\n\n try {\n const projectBody = createProjectBodySchema.parse({\n workspace_id: workspace.id,\n name: projectName,\n source: {\n connection_id: selectedConnection.id,\n external_repository_id: selectedRepository.external_repository_id,\n },\n });\n const project = await createProject.mutateAsync(projectBody);\n setWorkspaceProjectExists(queryClient, workspace.id, project);\n queryClient.setQueryData(projectsQueryKeys.detail(project.id), project);\n await queryClient.invalidateQueries({queryKey: projectsQueryKeys.list(workspace.id)});\n toast.success('Project created.');\n await navigate({\n to: '/workspaces/$wid/projects/$pid',\n params: {wid: workspace.id, pid: project.id},\n });\n } catch (error) {\n const copy = projectErrorCopy(error);\n if (copy.existingProjectId) {\n await queryClient.invalidateQueries({\n queryKey: projectsQueryKeys.exists(workspace.id),\n refetchType: 'active',\n });\n await queryClient.invalidateQueries({queryKey: projectsQueryKeys.list(workspace.id)});\n toast.info('Project already exists.');\n await navigate({\n to: '/workspaces/$wid/projects/$pid',\n params: {wid: workspace.id, pid: copy.existingProjectId},\n });\n return;\n }\n setFormError(`${copy.title}: ${copy.message}`);\n requestAnimationFrame(() => errorRef.current?.focus());\n }\n }\n\n const showRepoPicker = Boolean(selectedConnection);\n const filteredEmptyMessage = trimmedFilter\n ? `No repositories matching \"${repoFilter.trim()}\".`\n : 'No repositories visible to this connection.';\n\n return (\n <div className=\"flex w-full flex-col gap-24\">\n <header className=\"flex flex-col gap-6\">\n <Header id=\"create-project-title\" variant=\"h1\">\n Create project\n </Header>\n <Text size=\"sm\" className=\"text-foreground-neutral-muted\">\n A Shipfox project starts from a Git repository. Choose the repository Shipfox should\n track, then give the project a name.\n </Text>\n </header>\n\n <ModelProviderReminderBanner workspaceId={workspace.id} />\n\n {connectionsQuery.isError ? (\n <Callout role=\"alert\" type=\"error\">\n <div className=\"flex w-full flex-wrap items-center justify-between gap-12\">\n <Text size=\"sm\" className=\"min-w-[240px] flex-1\">\n Could not load source integrations. Refresh the integrations list to continue.\n </Text>\n <Button\n size=\"sm\"\n variant=\"secondary\"\n onClick={() => connectionsQuery.refetch()}\n className=\"shrink-0\"\n >\n Refresh integrations\n </Button>\n </div>\n </Callout>\n ) : null}\n\n <form\n onSubmit={(event) => {\n event.preventDefault();\n event.stopPropagation();\n void form.handleSubmit();\n }}\n noValidate\n aria-labelledby=\"create-project-title\"\n className=\"grid items-start gap-32 lg:grid-cols-[minmax(0,1fr)_340px]\"\n >\n <div className=\"flex min-w-0 flex-col gap-32\">\n <section className=\"flex flex-col gap-16\" aria-label=\"Source integration\">\n <div className=\"flex items-start justify-between gap-16\">\n <div className=\"flex flex-col gap-4\">\n <Header variant=\"h3\">Source integration</Header>\n <Text size=\"sm\" className=\"text-foreground-neutral-muted\">\n Choose the integration that can access the repository.\n </Text>\n </div>\n\n {connections.length === 1 ? (\n <Button asChild variant=\"transparent\" size=\"sm\" className=\"shrink-0\">\n <Link to=\"/workspaces/$wid/integrations\" params={{wid: workspace.id}}>\n Add another integration\n </Link>\n </Button>\n ) : null}\n </div>\n\n {connections.length > 0 ? (\n <ConnectionPicker\n connections={connections}\n selectedConnectionId={effectiveSelectedConnectionId}\n onSelect={selectConnection}\n />\n ) : null}\n </section>\n\n {showRepoPicker ? (\n <section className=\"flex flex-col gap-16\" aria-label=\"Repository\">\n <div className=\"flex flex-col gap-4\">\n <Header variant=\"h3\">Repository</Header>\n <Text size=\"sm\" className=\"text-foreground-neutral-muted\">\n Select the repository this project tracks.\n </Text>\n </div>\n\n <RepositoryPicker\n repositories={repositories}\n selectedRepositoryId={selectedRepositoryId}\n onSelect={setSelectedRepositoryId}\n isLoading={repositoriesQuery.isPending}\n isFetchingNextPage={repositoriesQuery.isFetchingNextPage}\n hasNextPage={repositoriesQuery.hasNextPage}\n onLoadMore={() => repositoriesQuery.fetchNextPage()}\n emptyMessage={filteredEmptyMessage}\n searchValue={repoFilter}\n onSearchChange={setRepoFilter}\n />\n </section>\n ) : null}\n </div>\n\n <aside className=\"lg:sticky lg:top-32\">\n <div className=\"flex flex-col gap-18 rounded-8 border border-border-neutral-base bg-background-neutral-base p-20\">\n <div className=\"flex flex-col gap-4\">\n <Header variant=\"h3\">Project details</Header>\n <Text size=\"sm\" className=\"text-foreground-neutral-muted\">\n Confirm the source and create the project.\n </Text>\n </div>\n\n {formError ? (\n <Callout role=\"alert\" type=\"error\">\n <div ref={errorRef} tabIndex={-1}>\n {formError}\n </div>\n </Callout>\n ) : null}\n\n <ProjectSummary\n connection={selectedConnection}\n repositoryName={selectedRepository?.full_name}\n />\n\n <form.Field\n name=\"name\"\n validators={{\n onBlur: ({value}) =>\n displayNameFieldError(value, 'Project name', createProjectBodySchema.shape.name),\n onSubmit: ({value}) =>\n displayNameFieldError(value, 'Project name', createProjectBodySchema.shape.name),\n }}\n >\n {(field) => (\n <FormField label=\"Project name\" id=\"project-name\" error={fieldError(field)}>\n <FormFieldInput\n name=\"name\"\n type=\"text\"\n value={field.state.value}\n onChange={(event) => {\n setNameTouched(true);\n field.handleChange(event.target.value);\n }}\n onBlur={field.handleBlur}\n placeholder=\"Platform\"\n />\n </FormField>\n )}\n </form.Field>\n\n <Button\n type=\"submit\"\n iconRight=\"chevronRight\"\n isLoading={createProject.isPending}\n disabled={!selectedConnection || !selectedRepository}\n className=\"w-full\"\n >\n Create project\n </Button>\n </div>\n </aside>\n </form>\n </div>\n );\n}\n\nfunction ProjectSummary({\n connection,\n repositoryName,\n}: {\n connection: IntegrationConnectionDto | undefined;\n repositoryName: string | undefined;\n}) {\n return (\n <div className=\"flex min-w-0 items-center gap-10\">\n {repositoryName ? (\n <IntegrationIcon\n source={connection?.provider}\n aria-hidden\n className=\"size-20 shrink-0 text-foreground-neutral-base\"\n />\n ) : null}\n <Text\n size=\"sm\"\n bold\n className={repositoryName ? 'min-w-0 truncate' : 'text-foreground-neutral-muted'}\n >\n {repositoryName ?? 'Pick a repository'}\n </Text>\n </div>\n );\n}\n\nfunction projectNameFromRepository(repositoryId: string): string {\n return repositoryId\n .trim()\n .split(REPOSITORY_NAME_SPLIT_RE)\n .filter(Boolean)\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join(' ');\n}\n\nfunction useDebouncedValue<T>(value: T, delayMs: number): T {\n const [debounced, setDebounced] = useState(value);\n useEffect(() => {\n const handle = setTimeout(() => setDebounced(value), delayMs);\n return () => clearTimeout(handle);\n }, [value, delayMs]);\n return debounced;\n}\n\nfunction setWorkspaceProjectExists(\n queryClient: QueryClient,\n workspaceId: string,\n project: ProjectResponseDto,\n) {\n queryClient.setQueryData<ListProjectsResponseDto>(projectsQueryKeys.exists(workspaceId), {\n projects: [project],\n next_cursor: null,\n });\n}\n"],"names":["createProjectBodySchema","useMaybeActiveWorkspace","ConnectionPicker","IntegrationIcon","RepositoryPicker","useRepositoriesInfiniteQuery","useSourceConnectionsQuery","displayNameFieldError","Button","Callout","FormField","FormFieldInput","fieldError","FullPageLoader","toast","Header","Text","useForm","useQueryClient","Link","Navigate","useNavigate","useEffect","useRef","useState","ModelProviderReminderBanner","projectsQueryKeys","useCreateProjectMutation","projectErrorCopy","REPOSITORY_NAME_SPLIT_RE","CreateProjectPage","workspace","queryClient","navigate","createProject","errorRef","connectionsQuery","id","connections","data","selectedConnectionId","setSelectedConnectionId","singleConnectionId","length","undefined","effectiveSelectedConnectionId","selectedConnection","find","connection","repoFilter","setRepoFilter","debouncedRepoFilter","useDebouncedValue","trimmedFilter","trim","repositoriesQuery","search","repositories","pages","flatMap","page","selectedRepositoryId","setSelectedRepositoryId","external_repository_id","selectedRepository","repository","nameTouched","setNameTouched","defaultProjectName","projectNameFromRepository","name","formError","setFormError","form","defaultValues","onSubmit","value","createProjectFromForm","state","values","setFieldValue","selectConnection","connectionId","isPending","isError","to","params","wid","replace","projectName","current","focus","projectBody","parse","workspace_id","source","connection_id","project","mutateAsync","setWorkspaceProjectExists","setQueryData","detail","invalidateQueries","queryKey","list","success","pid","error","copy","existingProjectId","exists","refetchType","info","title","message","requestAnimationFrame","showRepoPicker","Boolean","filteredEmptyMessage","div","className","header","variant","size","workspaceId","role","type","onClick","refetch","event","preventDefault","stopPropagation","handleSubmit","noValidate","aria-labelledby","section","aria-label","asChild","onSelect","isLoading","isFetchingNextPage","hasNextPage","onLoadMore","fetchNextPage","emptyMessage","searchValue","onSearchChange","aside","ref","tabIndex","ProjectSummary","repositoryName","full_name","Field","validators","onBlur","shape","field","label","onChange","handleChange","target","handleBlur","placeholder","iconRight","disabled","provider","aria-hidden","bold","repositoryId","split","filter","map","part","charAt","toUpperCase","slice","join","delayMs","debounced","setDebounced","handle","setTimeout","clearTimeout","projects","next_cursor"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/pages/create-project-page.tsx"],"sourcesContent":["import {createProjectBodySchema} from '@shipfox/api-projects-dto';\nimport {useMaybeActiveWorkspace} from '@shipfox/client-auth';\nimport {\n ConnectionPicker,\n type IntegrationConnection,\n IntegrationIcon,\n RepositoryPicker,\n useRepositoriesInfiniteQuery,\n useSourceConnectionsQuery,\n} from '@shipfox/client-integrations';\nimport {displayNameFieldError} from '@shipfox/client-ui';\nimport {Button} from '@shipfox/react-ui/button';\nimport {Callout} from '@shipfox/react-ui/callout';\nimport {FormField, FormFieldInput, fieldError} from '@shipfox/react-ui/form-field';\nimport {FullPageLoader} from '@shipfox/react-ui/loader';\nimport {toast} from '@shipfox/react-ui/toast';\nimport {Header, Text} from '@shipfox/react-ui/typography';\nimport {useForm} from '@tanstack/react-form';\nimport {Link, Navigate, useNavigate} from '@tanstack/react-router';\nimport {useEffect, useRef, useState} from 'react';\nimport {ModelProviderReminderBanner} from '#components/model-provider-reminder-banner.js';\nimport {type CreateProjectCommand, projectNameFromRepository} from '#core/project.js';\nimport {useCreateProjectMutation} from '#hooks/api/projects.js';\nimport {projectErrorCopy} from '#project-error.js';\n\nexport function CreateProjectPage() {\n const workspace = useMaybeActiveWorkspace();\n const navigate = useNavigate();\n const createProject = useCreateProjectMutation();\n const errorRef = useRef<HTMLDivElement>(null);\n\n const connectionsQuery = useSourceConnectionsQuery(workspace?.id);\n const connections = connectionsQuery.data ?? [];\n\n const [selectedConnectionId, setSelectedConnectionId] = useState<string | undefined>();\n const singleConnectionId = connections.length === 1 ? connections[0]?.id : undefined;\n const effectiveSelectedConnectionId = selectedConnectionId ?? singleConnectionId;\n useEffect(() => {\n if (singleConnectionId && selectedConnectionId !== singleConnectionId) {\n setSelectedConnectionId(singleConnectionId);\n }\n }, [singleConnectionId, selectedConnectionId]);\n\n const selectedConnection: IntegrationConnection | undefined = connections.find(\n (connection) => connection.id === effectiveSelectedConnectionId,\n );\n\n const [repoFilter, setRepoFilter] = useState('');\n const debouncedRepoFilter = useDebouncedValue(repoFilter, 250);\n const trimmedFilter = debouncedRepoFilter.trim();\n\n const repositoriesQuery = useRepositoriesInfiniteQuery(\n effectiveSelectedConnectionId,\n trimmedFilter ? {search: trimmedFilter} : undefined,\n );\n const repositories = repositoriesQuery.data?.pages.flatMap((page) => page.repositories) ?? [];\n\n const [selectedRepositoryId, setSelectedRepositoryId] = useState<string | undefined>();\n useEffect(() => {\n if (!selectedRepositoryId && repositories[0]) {\n setSelectedRepositoryId(repositories[0].externalRepositoryId);\n }\n }, [repositories, selectedRepositoryId]);\n const selectedRepository = repositories.find(\n (repository) => repository.externalRepositoryId === selectedRepositoryId,\n );\n\n const [nameTouched, setNameTouched] = useState(false);\n const defaultProjectName = projectNameFromRepository(\n selectedRepository?.name ?? selectedRepositoryId ?? '',\n );\n\n const [formError, setFormError] = useState<string | undefined>();\n\n const form = useForm({\n defaultValues: {name: defaultProjectName},\n onSubmit: async ({value}) => {\n await createProjectFromForm(nameTouched ? value.name : defaultProjectName);\n },\n });\n\n useEffect(() => {\n if (!nameTouched && form.state.values.name !== defaultProjectName) {\n form.setFieldValue('name', defaultProjectName);\n }\n }, [defaultProjectName, form, nameTouched]);\n\n function selectConnection(connectionId: string) {\n setSelectedConnectionId(connectionId);\n setSelectedRepositoryId(undefined);\n }\n\n if (connectionsQuery.isPending) {\n return <FullPageLoader />;\n }\n\n if (!workspace) {\n return <FullPageLoader />;\n }\n\n if (!connectionsQuery.isError && connections.length === 0) {\n return <Navigate to=\"/workspaces/$wid/integrations\" params={{wid: workspace.id}} replace />;\n }\n\n async function createProjectFromForm(projectName: string) {\n setFormError(undefined);\n if (!workspace) {\n setFormError('Workspace is still loading. Try again in a moment.');\n errorRef.current?.focus();\n return;\n }\n if (!selectedConnection) {\n setFormError('Choose a source integration before creating a project.');\n errorRef.current?.focus();\n return;\n }\n if (!selectedRepository) {\n setFormError('Choose a repository before creating a project.');\n errorRef.current?.focus();\n return;\n }\n\n try {\n const command: CreateProjectCommand = {\n workspaceId: workspace.id,\n name: projectName,\n source: {\n connectionId: selectedConnection.id,\n externalRepositoryId: selectedRepository.externalRepositoryId,\n },\n };\n const project = await createProject.mutateAsync(command);\n toast.success('Project created.');\n await navigate({\n to: '/workspaces/$wid/projects/$pid',\n params: {wid: workspace.id, pid: project.id},\n });\n } catch (error) {\n const copy = projectErrorCopy(error);\n if (copy.existingProjectId) {\n toast.info('Project already exists.');\n await navigate({\n to: '/workspaces/$wid/projects/$pid',\n params: {wid: workspace.id, pid: copy.existingProjectId},\n });\n return;\n }\n setFormError(`${copy.title}: ${copy.message}`);\n requestAnimationFrame(() => errorRef.current?.focus());\n }\n }\n\n const showRepoPicker = Boolean(selectedConnection);\n const filteredEmptyMessage = trimmedFilter\n ? `No repositories matching \"${repoFilter.trim()}\".`\n : 'No repositories visible to this connection.';\n\n return (\n <div className=\"flex w-full flex-col gap-24\">\n <header className=\"flex flex-col gap-6\">\n <Header id=\"create-project-title\" variant=\"h1\">\n Create project\n </Header>\n <Text size=\"sm\" className=\"text-foreground-neutral-muted\">\n A Shipfox project starts from a Git repository. Choose the repository Shipfox should\n track, then give the project a name.\n </Text>\n </header>\n\n <ModelProviderReminderBanner workspaceId={workspace.id} />\n\n {connectionsQuery.isError ? (\n <Callout role=\"alert\" type=\"error\">\n <div className=\"flex w-full flex-wrap items-center justify-between gap-12\">\n <Text size=\"sm\" className=\"min-w-[240px] flex-1\">\n Could not load source integrations. Refresh the integrations list to continue.\n </Text>\n <Button\n size=\"sm\"\n variant=\"secondary\"\n onClick={() => connectionsQuery.refetch()}\n className=\"shrink-0\"\n >\n Refresh integrations\n </Button>\n </div>\n </Callout>\n ) : null}\n\n <form\n onSubmit={(event) => {\n event.preventDefault();\n event.stopPropagation();\n void form.handleSubmit();\n }}\n noValidate\n aria-labelledby=\"create-project-title\"\n className=\"grid items-start gap-32 lg:grid-cols-[minmax(0,1fr)_340px]\"\n >\n <div className=\"flex min-w-0 flex-col gap-32\">\n <section className=\"flex flex-col gap-16\" aria-label=\"Source integration\">\n <div className=\"flex items-start justify-between gap-16\">\n <div className=\"flex flex-col gap-4\">\n <Header variant=\"h3\">Source integration</Header>\n <Text size=\"sm\" className=\"text-foreground-neutral-muted\">\n Choose the integration that can access the repository.\n </Text>\n </div>\n\n {connections.length === 1 ? (\n <Button asChild variant=\"transparent\" size=\"sm\" className=\"shrink-0\">\n <Link to=\"/workspaces/$wid/integrations\" params={{wid: workspace.id}}>\n Add another integration\n </Link>\n </Button>\n ) : null}\n </div>\n\n {connections.length > 0 ? (\n <ConnectionPicker\n connections={connections}\n selectedConnectionId={effectiveSelectedConnectionId}\n onSelect={selectConnection}\n />\n ) : null}\n </section>\n\n {showRepoPicker ? (\n <section className=\"flex flex-col gap-16\" aria-label=\"Repository\">\n <div className=\"flex flex-col gap-4\">\n <Header variant=\"h3\">Repository</Header>\n <Text size=\"sm\" className=\"text-foreground-neutral-muted\">\n Select the repository this project tracks.\n </Text>\n </div>\n\n <RepositoryPicker\n repositories={repositories}\n selectedRepositoryId={selectedRepositoryId}\n onSelect={setSelectedRepositoryId}\n isLoading={repositoriesQuery.isPending}\n isFetchingNextPage={repositoriesQuery.isFetchingNextPage}\n hasNextPage={repositoriesQuery.hasNextPage}\n onLoadMore={() => repositoriesQuery.fetchNextPage()}\n emptyMessage={filteredEmptyMessage}\n searchValue={repoFilter}\n onSearchChange={setRepoFilter}\n />\n </section>\n ) : null}\n </div>\n\n <aside className=\"lg:sticky lg:top-32\">\n <div className=\"flex flex-col gap-18 rounded-8 border border-border-neutral-base bg-background-neutral-base p-20\">\n <div className=\"flex flex-col gap-4\">\n <Header variant=\"h3\">Project details</Header>\n <Text size=\"sm\" className=\"text-foreground-neutral-muted\">\n Confirm the source and create the project.\n </Text>\n </div>\n\n {formError ? (\n <Callout role=\"alert\" type=\"error\">\n <div ref={errorRef} tabIndex={-1}>\n {formError}\n </div>\n </Callout>\n ) : null}\n\n <ProjectSummary\n connection={selectedConnection}\n repositoryName={selectedRepository?.fullName}\n />\n\n <form.Field\n name=\"name\"\n validators={{\n onBlur: ({value}) =>\n displayNameFieldError(value, 'Project name', createProjectBodySchema.shape.name),\n onSubmit: ({value}) =>\n displayNameFieldError(value, 'Project name', createProjectBodySchema.shape.name),\n }}\n >\n {(field) => (\n <FormField label=\"Project name\" id=\"project-name\" error={fieldError(field)}>\n <FormFieldInput\n name=\"name\"\n type=\"text\"\n value={field.state.value}\n onChange={(event) => {\n setNameTouched(true);\n field.handleChange(event.target.value);\n }}\n onBlur={field.handleBlur}\n placeholder=\"Platform\"\n />\n </FormField>\n )}\n </form.Field>\n\n <Button\n type=\"submit\"\n iconRight=\"chevronRight\"\n isLoading={createProject.isPending}\n disabled={!selectedConnection || !selectedRepository}\n className=\"w-full\"\n >\n Create project\n </Button>\n </div>\n </aside>\n </form>\n </div>\n );\n}\n\nfunction ProjectSummary({\n connection,\n repositoryName,\n}: {\n connection: IntegrationConnection | undefined;\n repositoryName: string | undefined;\n}) {\n return (\n <div className=\"flex min-w-0 items-center gap-10\">\n {repositoryName ? (\n <IntegrationIcon\n source={connection?.provider}\n aria-hidden\n className=\"size-20 shrink-0 text-foreground-neutral-base\"\n />\n ) : null}\n <Text\n size=\"sm\"\n bold\n className={repositoryName ? 'min-w-0 truncate' : 'text-foreground-neutral-muted'}\n >\n {repositoryName ?? 'Pick a repository'}\n </Text>\n </div>\n );\n}\n\nfunction useDebouncedValue<T>(value: T, delayMs: number): T {\n const [debounced, setDebounced] = useState(value);\n useEffect(() => {\n const handle = setTimeout(() => setDebounced(value), delayMs);\n return () => clearTimeout(handle);\n }, [value, delayMs]);\n return debounced;\n}\n"],"names":["createProjectBodySchema","useMaybeActiveWorkspace","ConnectionPicker","IntegrationIcon","RepositoryPicker","useRepositoriesInfiniteQuery","useSourceConnectionsQuery","displayNameFieldError","Button","Callout","FormField","FormFieldInput","fieldError","FullPageLoader","toast","Header","Text","useForm","Link","Navigate","useNavigate","useEffect","useRef","useState","ModelProviderReminderBanner","projectNameFromRepository","useCreateProjectMutation","projectErrorCopy","CreateProjectPage","workspace","navigate","createProject","errorRef","connectionsQuery","id","connections","data","selectedConnectionId","setSelectedConnectionId","singleConnectionId","length","undefined","effectiveSelectedConnectionId","selectedConnection","find","connection","repoFilter","setRepoFilter","debouncedRepoFilter","useDebouncedValue","trimmedFilter","trim","repositoriesQuery","search","repositories","pages","flatMap","page","selectedRepositoryId","setSelectedRepositoryId","externalRepositoryId","selectedRepository","repository","nameTouched","setNameTouched","defaultProjectName","name","formError","setFormError","form","defaultValues","onSubmit","value","createProjectFromForm","state","values","setFieldValue","selectConnection","connectionId","isPending","isError","to","params","wid","replace","projectName","current","focus","command","workspaceId","source","project","mutateAsync","success","pid","error","copy","existingProjectId","info","title","message","requestAnimationFrame","showRepoPicker","Boolean","filteredEmptyMessage","div","className","header","variant","size","role","type","onClick","refetch","event","preventDefault","stopPropagation","handleSubmit","noValidate","aria-labelledby","section","aria-label","asChild","onSelect","isLoading","isFetchingNextPage","hasNextPage","onLoadMore","fetchNextPage","emptyMessage","searchValue","onSearchChange","aside","ref","tabIndex","ProjectSummary","repositoryName","fullName","Field","validators","onBlur","shape","field","label","onChange","handleChange","target","handleBlur","placeholder","iconRight","disabled","provider","aria-hidden","bold","delayMs","debounced","setDebounced","handle","setTimeout","clearTimeout"],"mappings":"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"}
|
|
@@ -151,7 +151,8 @@ function fetchForScenario(scenario) {
|
|
|
151
151
|
configs: [
|
|
152
152
|
modelProviderConfig()
|
|
153
153
|
],
|
|
154
|
-
default_provider_id: 'openai'
|
|
154
|
+
default_provider_id: 'openai',
|
|
155
|
+
default_harness_id: null
|
|
155
156
|
}));
|
|
156
157
|
}
|
|
157
158
|
if (url.pathname === '/integration-connections') {
|
|
@@ -306,6 +307,7 @@ function repository(overrides) {
|
|
|
306
307
|
}
|
|
307
308
|
function modelProviderConfig() {
|
|
308
309
|
return {
|
|
310
|
+
kind: 'builtin',
|
|
309
311
|
workspace_id: WORKSPACE_ID,
|
|
310
312
|
provider_id: 'openai',
|
|
311
313
|
default_model: 'gpt-5.5-pro',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/pages/create-project-page.stories.tsx"],"sourcesContent":["import type {IntegrationConnectionDto, RepositoryDto} from '@shipfox/api-integration-core-dto';\nimport {configureApiClient} from '@shipfox/client-api';\nimport {type AuthState, authStateAtom} from '@shipfox/client-auth';\nimport {Toaster} from '@shipfox/react-ui/toast';\nimport type {Meta, StoryObj} from '@storybook/react';\nimport {QueryClient, QueryClientProvider} from '@tanstack/react-query';\nimport {\n createMemoryHistory,\n createRootRoute,\n createRoute,\n createRouter,\n Outlet,\n RouterProvider,\n} from '@tanstack/react-router';\nimport {createStore, Provider as JotaiProvider} from 'jotai';\nimport {useMemo} from 'react';\nimport {CreateProjectPage} from './create-project-page.js';\n\nconst WORKSPACE_ID = '11111111-1111-4111-8111-111111111111';\nconst WORKSPACE_PATH = `/workspaces/${WORKSPACE_ID}/projects/new`;\nconst GITHUB_CONNECTION_ID = '33333333-3333-4333-8333-333333333333';\nconst GITEA_CONNECTION_ID = '44444444-4444-4444-8444-444444444444';\n\ntype Scenario =\n | 'playground'\n | 'long-names'\n | 'repository-loading'\n | 'empty-repositories'\n | 'connections-error';\n\ninterface CreateProjectPageStoryProps {\n scenario: Scenario;\n}\n\nconst authState: AuthState = {\n status: 'authenticated',\n token: 'token',\n user: {\n id: '22222222-2222-4222-8222-222222222222',\n email: 'platform@example.com',\n name: 'Platform Engineer',\n emailVerifiedAt: '2026-01-01T00:00:00.000Z',\n },\n workspaces: [{id: WORKSPACE_ID, name: 'Acme', membershipId: 'membership-1'}],\n};\n\nfunction CreateProjectPageStory({scenario}: CreateProjectPageStoryProps) {\n configureApiClient({\n baseUrl: 'https://api.example.test',\n fetchImpl: fetchForScenario(scenario),\n });\n\n const queryClient = useMemo(\n () => new QueryClient({defaultOptions: {queries: {retry: false}}}),\n [],\n );\n const store = useMemo(() => {\n const nextStore = createStore();\n nextStore.set(authStateAtom, authState);\n return nextStore;\n }, []);\n const router = useMemo(() => createStoryRouter(), []);\n\n return (\n <QueryClientProvider client={queryClient}>\n <JotaiProvider store={store}>\n <div className=\"min-h-screen bg-background-neutral-background px-24 py-32\">\n <div className=\"mx-auto w-full max-w-[1120px]\">\n <RouterProvider router={router} />\n </div>\n </div>\n <Toaster />\n </JotaiProvider>\n </QueryClientProvider>\n );\n}\n\nconst meta = {\n title: 'Projects/CreateProjectPage',\n component: CreateProjectPageStory,\n parameters: {layout: 'fullscreen'},\n args: {scenario: 'playground'},\n} satisfies Meta<typeof CreateProjectPageStory>;\n\nexport default meta;\ntype Story = StoryObj<typeof meta>;\n\nexport const Playground: Story = {};\n\nexport const LongNames: Story = {\n args: {scenario: 'long-names'},\n};\n\nexport const RepositoryLoading: Story = {\n args: {scenario: 'repository-loading'},\n};\n\nexport const EmptyRepositories: Story = {\n args: {scenario: 'empty-repositories'},\n};\n\nexport const ConnectionsError: Story = {\n args: {scenario: 'connections-error'},\n};\n\nfunction createStoryRouter() {\n const rootRoute = createRootRoute({component: Outlet});\n const workspaceRoute = createRoute({\n getParentRoute: () => rootRoute,\n path: '/workspaces/$wid',\n component: Outlet,\n });\n const createProjectRoute = createRoute({\n getParentRoute: () => workspaceRoute,\n path: 'projects/new',\n component: CreateProjectPage,\n });\n const integrationsRoute = createRoute({\n getParentRoute: () => workspaceRoute,\n path: 'integrations',\n component: () => <div />,\n });\n const modelProvidersRoute = createRoute({\n getParentRoute: () => workspaceRoute,\n path: 'settings/agents',\n component: () => <div />,\n });\n const projectRoute = createRoute({\n getParentRoute: () => workspaceRoute,\n path: 'projects/$pid',\n component: () => <div />,\n });\n\n return createRouter({\n history: createMemoryHistory({initialEntries: [WORKSPACE_PATH]}),\n routeTree: rootRoute.addChildren([\n workspaceRoute.addChildren([\n createProjectRoute,\n integrationsRoute,\n modelProvidersRoute,\n projectRoute,\n ]),\n ]),\n });\n}\n\nfunction fetchForScenario(scenario: Scenario): typeof fetch {\n return (input, init) => {\n const url = requestUrl(input);\n const method = input instanceof Request ? input.method : (init?.method ?? 'GET');\n if (url.pathname === `/workspaces/${WORKSPACE_ID}/agent/model-providers`) {\n return Promise.resolve(\n jsonResponse({configs: [modelProviderConfig()], default_provider_id: 'openai'}),\n );\n }\n if (url.pathname === '/integration-connections') {\n if (scenario === 'connections-error') return Promise.resolve(errorResponse());\n return Promise.resolve(jsonResponse({connections: connectionsForScenario(scenario)}));\n }\n if (\n url.pathname.startsWith('/integration-connections/') &&\n url.pathname.endsWith('/repositories')\n ) {\n if (scenario === 'repository-loading') return new Promise<Response>(() => undefined);\n return Promise.resolve(\n jsonResponse({repositories: repositoriesForScenario(scenario), next_cursor: null}),\n );\n }\n if (url.pathname === '/projects' && method === 'POST') {\n return Promise.resolve(\n jsonResponse({\n id: '99999999-9999-4999-8999-999999999999',\n workspace_id: WORKSPACE_ID,\n name: 'Platform',\n source: {\n connection_id: GITHUB_CONNECTION_ID,\n external_repository_id: 'platform',\n },\n created_at: '2026-01-01T00:00:00.000Z',\n updated_at: '2026-01-01T00:00:00.000Z',\n }),\n );\n }\n return Promise.resolve(jsonResponse({}, {status: 404}));\n };\n}\n\nfunction connectionsForScenario(scenario: Scenario): IntegrationConnectionDto[] {\n if (scenario === 'long-names') {\n return [\n connection({\n id: GITHUB_CONNECTION_ID,\n provider: 'github',\n display_name: 'acme-platform-engineering-production',\n external_account_id: 'acme-platform-engineering-production',\n }),\n connection({\n id: GITEA_CONNECTION_ID,\n provider: 'gitea',\n display_name: 'git.internal.acme.example/platform-tools',\n external_account_id: 'git.internal.acme.example/platform-tools',\n }),\n ];\n }\n\n return [\n connection({\n id: GITHUB_CONNECTION_ID,\n provider: 'github',\n display_name: 'acme',\n external_account_id: 'acme',\n }),\n connection({\n id: GITEA_CONNECTION_ID,\n provider: 'gitea',\n display_name: 'git.acme.internal',\n external_account_id: 'git.acme.internal',\n }),\n ];\n}\n\nfunction repositoriesForScenario(scenario: Scenario): RepositoryDto[] {\n if (scenario === 'empty-repositories') return [];\n if (scenario === 'long-names') {\n return [\n repository({\n external_repository_id: 'acme-platform-infrastructure-control-plane',\n name: 'acme-platform-infrastructure-control-plane',\n full_name: 'acme-engineering/acme-platform-infrastructure-control-plane',\n default_branch: 'production',\n }),\n repository({\n external_repository_id: 'workflow-automation-and-runner-orchestration',\n name: 'workflow-automation-and-runner-orchestration',\n full_name: 'acme-engineering/workflow-automation-and-runner-orchestration',\n default_branch: 'main',\n }),\n repository({\n external_repository_id: 'customer-deployment-pipelines',\n name: 'customer-deployment-pipelines',\n full_name: 'acme-solutions/customer-deployment-pipelines',\n default_branch: 'release',\n }),\n ];\n }\n\n return [\n repository({\n external_repository_id: 'platform',\n name: 'platform',\n full_name: 'acme/platform',\n default_branch: 'main',\n }),\n repository({\n external_repository_id: 'api',\n name: 'api',\n full_name: 'acme/api',\n default_branch: 'main',\n }),\n repository({\n external_repository_id: 'client',\n name: 'client',\n full_name: 'acme/client',\n default_branch: 'main',\n }),\n repository({\n external_repository_id: 'runners',\n name: 'runners',\n full_name: 'acme/runners',\n default_branch: 'main',\n }),\n repository({\n external_repository_id: 'workflows',\n name: 'workflows',\n full_name: 'acme/workflows',\n default_branch: 'develop',\n }),\n ];\n}\n\nfunction connection(\n overrides: Partial<IntegrationConnectionDto> & Pick<IntegrationConnectionDto, 'id'>,\n): IntegrationConnectionDto {\n return {\n workspace_id: WORKSPACE_ID,\n provider: 'github',\n external_account_id: 'acme',\n slug: 'github_acme',\n display_name: 'GitHub',\n lifecycle_status: 'active',\n capabilities: ['source_control'],\n created_at: '2026-01-01T00:00:00.000Z',\n updated_at: '2026-01-01T00:00:00.000Z',\n ...overrides,\n };\n}\n\nfunction repository(overrides: Partial<RepositoryDto>): RepositoryDto {\n return {\n connection_id: GITHUB_CONNECTION_ID,\n external_repository_id: 'platform',\n owner: 'acme',\n name: 'platform',\n full_name: 'acme/platform',\n default_branch: 'main',\n visibility: 'private',\n clone_url: 'https://github.example.test/acme/platform.git',\n html_url: 'https://github.example.test/acme/platform',\n ...overrides,\n };\n}\n\nfunction modelProviderConfig() {\n return {\n workspace_id: WORKSPACE_ID,\n provider_id: 'openai',\n default_model: 'gpt-5.5-pro',\n credential_fields: ['api_key'],\n lifecycle_status: 'active',\n created_at: '2026-01-01T00:00:00.000Z',\n updated_at: '2026-01-01T00:00:00.000Z',\n };\n}\n\nfunction requestUrl(input: RequestInfo | URL): URL {\n if (input instanceof Request) return new URL(input.url);\n if (input instanceof URL) return input;\n return new URL(input, 'https://api.example.test');\n}\n\nfunction jsonResponse(body: unknown, init: ResponseInit = {}) {\n return new Response(JSON.stringify(body), {\n status: 200,\n headers: {'content-type': 'application/json'},\n ...init,\n });\n}\n\nfunction errorResponse() {\n return jsonResponse({code: 'server-error'}, {status: 500});\n}\n"],"names":["configureApiClient","authStateAtom","Toaster","QueryClient","QueryClientProvider","createMemoryHistory","createRootRoute","createRoute","createRouter","Outlet","RouterProvider","createStore","Provider","JotaiProvider","useMemo","CreateProjectPage","WORKSPACE_ID","WORKSPACE_PATH","GITHUB_CONNECTION_ID","GITEA_CONNECTION_ID","authState","status","token","user","id","email","name","emailVerifiedAt","workspaces","membershipId","CreateProjectPageStory","scenario","baseUrl","fetchImpl","fetchForScenario","queryClient","defaultOptions","queries","retry","store","nextStore","set","router","createStoryRouter","client","div","className","meta","title","component","parameters","layout","args","Playground","LongNames","RepositoryLoading","EmptyRepositories","ConnectionsError","rootRoute","workspaceRoute","getParentRoute","path","createProjectRoute","integrationsRoute","modelProvidersRoute","projectRoute","history","initialEntries","routeTree","addChildren","input","init","url","requestUrl","method","Request","pathname","Promise","resolve","jsonResponse","configs","modelProviderConfig","default_provider_id","errorResponse","connections","connectionsForScenario","startsWith","endsWith","undefined","repositories","repositoriesForScenario","next_cursor","workspace_id","source","connection_id","external_repository_id","created_at","updated_at","connection","provider","display_name","external_account_id","repository","full_name","default_branch","overrides","slug","lifecycle_status","capabilities","owner","visibility","clone_url","html_url","provider_id","default_model","credential_fields","URL","body","Response","JSON","stringify","headers","code"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/pages/create-project-page.stories.tsx"],"sourcesContent":["import type {IntegrationConnectionDto, RepositoryDto} from '@shipfox/api-integration-core-dto';\nimport {configureApiClient} from '@shipfox/client-api';\nimport {type AuthState, authStateAtom} from '@shipfox/client-auth';\nimport {Toaster} from '@shipfox/react-ui/toast';\nimport type {Meta, StoryObj} from '@storybook/react';\nimport {QueryClient, QueryClientProvider} from '@tanstack/react-query';\nimport {\n createMemoryHistory,\n createRootRoute,\n createRoute,\n createRouter,\n Outlet,\n RouterProvider,\n} from '@tanstack/react-router';\nimport {createStore, Provider as JotaiProvider} from 'jotai';\nimport {useMemo} from 'react';\nimport {CreateProjectPage} from './create-project-page.js';\n\nconst WORKSPACE_ID = '11111111-1111-4111-8111-111111111111';\nconst WORKSPACE_PATH = `/workspaces/${WORKSPACE_ID}/projects/new`;\nconst GITHUB_CONNECTION_ID = '33333333-3333-4333-8333-333333333333';\nconst GITEA_CONNECTION_ID = '44444444-4444-4444-8444-444444444444';\n\ntype Scenario =\n | 'playground'\n | 'long-names'\n | 'repository-loading'\n | 'empty-repositories'\n | 'connections-error';\n\ninterface CreateProjectPageStoryProps {\n scenario: Scenario;\n}\n\nconst authState: AuthState = {\n status: 'authenticated',\n token: 'token',\n user: {\n id: '22222222-2222-4222-8222-222222222222',\n email: 'platform@example.com',\n name: 'Platform Engineer',\n emailVerifiedAt: '2026-01-01T00:00:00.000Z',\n },\n workspaces: [{id: WORKSPACE_ID, name: 'Acme', membershipId: 'membership-1'}],\n};\n\nfunction CreateProjectPageStory({scenario}: CreateProjectPageStoryProps) {\n configureApiClient({\n baseUrl: 'https://api.example.test',\n fetchImpl: fetchForScenario(scenario),\n });\n\n const queryClient = useMemo(\n () => new QueryClient({defaultOptions: {queries: {retry: false}}}),\n [],\n );\n const store = useMemo(() => {\n const nextStore = createStore();\n nextStore.set(authStateAtom, authState);\n return nextStore;\n }, []);\n const router = useMemo(() => createStoryRouter(), []);\n\n return (\n <QueryClientProvider client={queryClient}>\n <JotaiProvider store={store}>\n <div className=\"min-h-screen bg-background-neutral-background px-24 py-32\">\n <div className=\"mx-auto w-full max-w-[1120px]\">\n <RouterProvider router={router} />\n </div>\n </div>\n <Toaster />\n </JotaiProvider>\n </QueryClientProvider>\n );\n}\n\nconst meta = {\n title: 'Projects/CreateProjectPage',\n component: CreateProjectPageStory,\n parameters: {layout: 'fullscreen'},\n args: {scenario: 'playground'},\n} satisfies Meta<typeof CreateProjectPageStory>;\n\nexport default meta;\ntype Story = StoryObj<typeof meta>;\n\nexport const Playground: Story = {};\n\nexport const LongNames: Story = {\n args: {scenario: 'long-names'},\n};\n\nexport const RepositoryLoading: Story = {\n args: {scenario: 'repository-loading'},\n};\n\nexport const EmptyRepositories: Story = {\n args: {scenario: 'empty-repositories'},\n};\n\nexport const ConnectionsError: Story = {\n args: {scenario: 'connections-error'},\n};\n\nfunction createStoryRouter() {\n const rootRoute = createRootRoute({component: Outlet});\n const workspaceRoute = createRoute({\n getParentRoute: () => rootRoute,\n path: '/workspaces/$wid',\n component: Outlet,\n });\n const createProjectRoute = createRoute({\n getParentRoute: () => workspaceRoute,\n path: 'projects/new',\n component: CreateProjectPage,\n });\n const integrationsRoute = createRoute({\n getParentRoute: () => workspaceRoute,\n path: 'integrations',\n component: () => <div />,\n });\n const modelProvidersRoute = createRoute({\n getParentRoute: () => workspaceRoute,\n path: 'settings/agents',\n component: () => <div />,\n });\n const projectRoute = createRoute({\n getParentRoute: () => workspaceRoute,\n path: 'projects/$pid',\n component: () => <div />,\n });\n\n return createRouter({\n history: createMemoryHistory({initialEntries: [WORKSPACE_PATH]}),\n routeTree: rootRoute.addChildren([\n workspaceRoute.addChildren([\n createProjectRoute,\n integrationsRoute,\n modelProvidersRoute,\n projectRoute,\n ]),\n ]),\n });\n}\n\nfunction fetchForScenario(scenario: Scenario): typeof fetch {\n return (input, init) => {\n const url = requestUrl(input);\n const method = input instanceof Request ? input.method : (init?.method ?? 'GET');\n if (url.pathname === `/workspaces/${WORKSPACE_ID}/agent/model-providers`) {\n return Promise.resolve(\n jsonResponse({\n configs: [modelProviderConfig()],\n default_provider_id: 'openai',\n default_harness_id: null,\n }),\n );\n }\n if (url.pathname === '/integration-connections') {\n if (scenario === 'connections-error') return Promise.resolve(errorResponse());\n return Promise.resolve(jsonResponse({connections: connectionsForScenario(scenario)}));\n }\n if (\n url.pathname.startsWith('/integration-connections/') &&\n url.pathname.endsWith('/repositories')\n ) {\n if (scenario === 'repository-loading') return new Promise<Response>(() => undefined);\n return Promise.resolve(\n jsonResponse({repositories: repositoriesForScenario(scenario), next_cursor: null}),\n );\n }\n if (url.pathname === '/projects' && method === 'POST') {\n return Promise.resolve(\n jsonResponse({\n id: '99999999-9999-4999-8999-999999999999',\n workspace_id: WORKSPACE_ID,\n name: 'Platform',\n source: {\n connection_id: GITHUB_CONNECTION_ID,\n external_repository_id: 'platform',\n },\n created_at: '2026-01-01T00:00:00.000Z',\n updated_at: '2026-01-01T00:00:00.000Z',\n }),\n );\n }\n return Promise.resolve(jsonResponse({}, {status: 404}));\n };\n}\n\nfunction connectionsForScenario(scenario: Scenario): IntegrationConnectionDto[] {\n if (scenario === 'long-names') {\n return [\n connection({\n id: GITHUB_CONNECTION_ID,\n provider: 'github',\n display_name: 'acme-platform-engineering-production',\n external_account_id: 'acme-platform-engineering-production',\n }),\n connection({\n id: GITEA_CONNECTION_ID,\n provider: 'gitea',\n display_name: 'git.internal.acme.example/platform-tools',\n external_account_id: 'git.internal.acme.example/platform-tools',\n }),\n ];\n }\n\n return [\n connection({\n id: GITHUB_CONNECTION_ID,\n provider: 'github',\n display_name: 'acme',\n external_account_id: 'acme',\n }),\n connection({\n id: GITEA_CONNECTION_ID,\n provider: 'gitea',\n display_name: 'git.acme.internal',\n external_account_id: 'git.acme.internal',\n }),\n ];\n}\n\nfunction repositoriesForScenario(scenario: Scenario): RepositoryDto[] {\n if (scenario === 'empty-repositories') return [];\n if (scenario === 'long-names') {\n return [\n repository({\n external_repository_id: 'acme-platform-infrastructure-control-plane',\n name: 'acme-platform-infrastructure-control-plane',\n full_name: 'acme-engineering/acme-platform-infrastructure-control-plane',\n default_branch: 'production',\n }),\n repository({\n external_repository_id: 'workflow-automation-and-runner-orchestration',\n name: 'workflow-automation-and-runner-orchestration',\n full_name: 'acme-engineering/workflow-automation-and-runner-orchestration',\n default_branch: 'main',\n }),\n repository({\n external_repository_id: 'customer-deployment-pipelines',\n name: 'customer-deployment-pipelines',\n full_name: 'acme-solutions/customer-deployment-pipelines',\n default_branch: 'release',\n }),\n ];\n }\n\n return [\n repository({\n external_repository_id: 'platform',\n name: 'platform',\n full_name: 'acme/platform',\n default_branch: 'main',\n }),\n repository({\n external_repository_id: 'api',\n name: 'api',\n full_name: 'acme/api',\n default_branch: 'main',\n }),\n repository({\n external_repository_id: 'client',\n name: 'client',\n full_name: 'acme/client',\n default_branch: 'main',\n }),\n repository({\n external_repository_id: 'runners',\n name: 'runners',\n full_name: 'acme/runners',\n default_branch: 'main',\n }),\n repository({\n external_repository_id: 'workflows',\n name: 'workflows',\n full_name: 'acme/workflows',\n default_branch: 'develop',\n }),\n ];\n}\n\nfunction connection(\n overrides: Partial<IntegrationConnectionDto> & Pick<IntegrationConnectionDto, 'id'>,\n): IntegrationConnectionDto {\n return {\n workspace_id: WORKSPACE_ID,\n provider: 'github',\n external_account_id: 'acme',\n slug: 'github_acme',\n display_name: 'GitHub',\n lifecycle_status: 'active',\n capabilities: ['source_control'],\n created_at: '2026-01-01T00:00:00.000Z',\n updated_at: '2026-01-01T00:00:00.000Z',\n ...overrides,\n };\n}\n\nfunction repository(overrides: Partial<RepositoryDto>): RepositoryDto {\n return {\n connection_id: GITHUB_CONNECTION_ID,\n external_repository_id: 'platform',\n owner: 'acme',\n name: 'platform',\n full_name: 'acme/platform',\n default_branch: 'main',\n visibility: 'private',\n clone_url: 'https://github.example.test/acme/platform.git',\n html_url: 'https://github.example.test/acme/platform',\n ...overrides,\n };\n}\n\nfunction modelProviderConfig() {\n return {\n kind: 'builtin',\n workspace_id: WORKSPACE_ID,\n provider_id: 'openai',\n default_model: 'gpt-5.5-pro',\n credential_fields: ['api_key'],\n lifecycle_status: 'active',\n created_at: '2026-01-01T00:00:00.000Z',\n updated_at: '2026-01-01T00:00:00.000Z',\n };\n}\n\nfunction requestUrl(input: RequestInfo | URL): URL {\n if (input instanceof Request) return new URL(input.url);\n if (input instanceof URL) return input;\n return new URL(input, 'https://api.example.test');\n}\n\nfunction jsonResponse(body: unknown, init: ResponseInit = {}) {\n return new Response(JSON.stringify(body), {\n status: 200,\n headers: {'content-type': 'application/json'},\n ...init,\n });\n}\n\nfunction errorResponse() {\n return jsonResponse({code: 'server-error'}, {status: 500});\n}\n"],"names":["configureApiClient","authStateAtom","Toaster","QueryClient","QueryClientProvider","createMemoryHistory","createRootRoute","createRoute","createRouter","Outlet","RouterProvider","createStore","Provider","JotaiProvider","useMemo","CreateProjectPage","WORKSPACE_ID","WORKSPACE_PATH","GITHUB_CONNECTION_ID","GITEA_CONNECTION_ID","authState","status","token","user","id","email","name","emailVerifiedAt","workspaces","membershipId","CreateProjectPageStory","scenario","baseUrl","fetchImpl","fetchForScenario","queryClient","defaultOptions","queries","retry","store","nextStore","set","router","createStoryRouter","client","div","className","meta","title","component","parameters","layout","args","Playground","LongNames","RepositoryLoading","EmptyRepositories","ConnectionsError","rootRoute","workspaceRoute","getParentRoute","path","createProjectRoute","integrationsRoute","modelProvidersRoute","projectRoute","history","initialEntries","routeTree","addChildren","input","init","url","requestUrl","method","Request","pathname","Promise","resolve","jsonResponse","configs","modelProviderConfig","default_provider_id","default_harness_id","errorResponse","connections","connectionsForScenario","startsWith","endsWith","undefined","repositories","repositoriesForScenario","next_cursor","workspace_id","source","connection_id","external_repository_id","created_at","updated_at","connection","provider","display_name","external_account_id","repository","full_name","default_branch","overrides","slug","lifecycle_status","capabilities","owner","visibility","clone_url","html_url","kind","provider_id","default_model","credential_fields","URL","body","Response","JSON","stringify","headers","code"],"mappings":"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"}
|
|
@@ -1,2 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import type { ProjectsSearch } from '#routes/search.js';
|
|
2
|
+
export declare function HomeRouter({ search }: {
|
|
3
|
+
search?: ProjectsSearch;
|
|
4
|
+
}): import("react").JSX.Element;
|
|
2
5
|
//# sourceMappingURL=home-router.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"home-router.d.ts","sourceRoot":"","sources":["../../src/pages/home-router.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"home-router.d.ts","sourceRoot":"","sources":["../../src/pages/home-router.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,mBAAmB,CAAC;AAEtD,wBAAgB,UAAU,CAAC,EAAC,MAAW,EAAC,EAAE;IAAC,MAAM,CAAC,EAAE,cAAc,CAAA;CAAC,+BAElE"}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { ProjectsHubPage } from '#pages/projects-hub-page.js';
|
|
3
|
-
export function HomeRouter() {
|
|
4
|
-
return /*#__PURE__*/ _jsx(ProjectsHubPage, {
|
|
3
|
+
export function HomeRouter({ search = {} }) {
|
|
4
|
+
return /*#__PURE__*/ _jsx(ProjectsHubPage, {
|
|
5
|
+
search: search.search ?? ''
|
|
6
|
+
});
|
|
5
7
|
}
|
|
6
8
|
|
|
7
9
|
//# sourceMappingURL=home-router.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/pages/home-router.tsx"],"sourcesContent":["import {ProjectsHubPage} from '#pages/projects-hub-page.js';\n\nexport function HomeRouter() {\n return <ProjectsHubPage />;\n}\n"],"names":["ProjectsHubPage","HomeRouter"],"mappings":";AAAA,SAAQA,eAAe,QAAO,8BAA8B;
|
|
1
|
+
{"version":3,"sources":["../../src/pages/home-router.tsx"],"sourcesContent":["import {ProjectsHubPage} from '#pages/projects-hub-page.js';\nimport type {ProjectsSearch} from '#routes/search.js';\n\nexport function HomeRouter({search = {}}: {search?: ProjectsSearch}) {\n return <ProjectsHubPage search={search.search ?? ''} />;\n}\n"],"names":["ProjectsHubPage","HomeRouter","search"],"mappings":";AAAA,SAAQA,eAAe,QAAO,8BAA8B;AAG5D,OAAO,SAASC,WAAW,EAACC,SAAS,CAAC,CAAC,EAA4B;IACjE,qBAAO,KAACF;QAAgBE,QAAQA,OAAOA,MAAM,IAAI;;AACnD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"projects-hub-page.d.ts","sourceRoot":"","sources":["../../src/pages/projects-hub-page.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"projects-hub-page.d.ts","sourceRoot":"","sources":["../../src/pages/projects-hub-page.tsx"],"names":[],"mappings":"AAqBA,wBAAgB,eAAe,CAAC,EAAC,MAAW,EAAC,EAAE;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAC,+BA8G/D"}
|
|
@@ -10,28 +10,24 @@ import { Icon } from '@shipfox/react-ui/icon';
|
|
|
10
10
|
import { Input } from '@shipfox/react-ui/input';
|
|
11
11
|
import { Skeleton } from '@shipfox/react-ui/skeleton';
|
|
12
12
|
import { Header, Text } from '@shipfox/react-ui/typography';
|
|
13
|
-
import { Link } from '@tanstack/react-router';
|
|
14
|
-
import { useEffect, useState } from 'react';
|
|
13
|
+
import { Link, useNavigate } from '@tanstack/react-router';
|
|
15
14
|
import { ModelProviderReminderBanner } from '#components/model-provider-reminder-banner.js';
|
|
16
15
|
import { useProjectsInfiniteQuery } from '#hooks/api/projects.js';
|
|
17
|
-
export function ProjectsHubPage() {
|
|
16
|
+
export function ProjectsHubPage({ search = '' }) {
|
|
18
17
|
const workspace = useActiveWorkspace();
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const debouncedSearch = useDebouncedValue(trimmedInput, 250);
|
|
22
|
-
const query = useProjectsInfiniteQuery(workspace.id, debouncedSearch || undefined);
|
|
18
|
+
const navigate = useNavigate();
|
|
19
|
+
const query = useProjectsInfiniteQuery(workspace.id, search || undefined);
|
|
23
20
|
const projects = query.data?.pages.flatMap((page)=>page.projects) ?? [];
|
|
24
21
|
// The provider logo and connection health live on the integration connection,
|
|
25
22
|
// not the project, so resolve them once for the whole list and index by id.
|
|
26
23
|
// Skip the fetch when there are no cards to annotate.
|
|
27
24
|
const connectionsQuery = useIntegrationConnectionsQuery(projects.length > 0 ? workspace.id : undefined);
|
|
28
|
-
const connectionsById = new Map((connectionsQuery.data
|
|
25
|
+
const connectionsById = new Map((connectionsQuery.data ?? []).map((connection)=>[
|
|
29
26
|
connection.id,
|
|
30
27
|
connection
|
|
31
28
|
]));
|
|
32
29
|
const isInitialLoading = query.isPending;
|
|
33
|
-
const
|
|
34
|
-
const isSearching = Boolean(trimmedInput) && (isDebouncePending || query.isFetching && !query.isFetchingNextPage);
|
|
30
|
+
const isSearching = Boolean(search) && query.isFetching && !query.isFetchingNextPage;
|
|
35
31
|
const hasNoData = !query.data;
|
|
36
32
|
return /*#__PURE__*/ _jsxs("div", {
|
|
37
33
|
className: "flex w-full flex-col gap-24",
|
|
@@ -51,8 +47,16 @@ export function ProjectsHubPage() {
|
|
|
51
47
|
children: [
|
|
52
48
|
/*#__PURE__*/ _jsx(Input, {
|
|
53
49
|
type: "search",
|
|
54
|
-
value:
|
|
55
|
-
onChange: (event)=>
|
|
50
|
+
value: search,
|
|
51
|
+
onChange: (event)=>{
|
|
52
|
+
const next = event.target.value.trim();
|
|
53
|
+
navigate({
|
|
54
|
+
search: next ? {
|
|
55
|
+
search: next
|
|
56
|
+
} : {},
|
|
57
|
+
replace: true
|
|
58
|
+
});
|
|
59
|
+
},
|
|
56
60
|
placeholder: "Search projects…",
|
|
57
61
|
"aria-label": "Search projects",
|
|
58
62
|
className: "flex-1",
|
|
@@ -86,17 +90,20 @@ export function ProjectsHubPage() {
|
|
|
86
90
|
/*#__PURE__*/ _jsx(ModelProviderReminderBanner, {
|
|
87
91
|
workspaceId: workspace.id
|
|
88
92
|
}),
|
|
89
|
-
isInitialLoading ||
|
|
93
|
+
isInitialLoading || search && hasNoData && query.isFetching ? /*#__PURE__*/ _jsx(ProjectsSkeleton, {}) : null,
|
|
90
94
|
query.isError && hasNoData ? /*#__PURE__*/ _jsx(QueryLoadError, {
|
|
91
95
|
query: query,
|
|
92
96
|
subject: "projects"
|
|
93
97
|
}) : null,
|
|
94
|
-
!isInitialLoading && !query.isError && projects.length === 0 && !
|
|
98
|
+
!isInitialLoading && !query.isError && projects.length === 0 && !search ? /*#__PURE__*/ _jsx(EmptyProjects, {
|
|
95
99
|
workspaceId: workspace.id
|
|
96
100
|
}) : null,
|
|
97
|
-
!query.isFetching && !query.isError && projects.length === 0 &&
|
|
98
|
-
search:
|
|
99
|
-
onClear: ()=>
|
|
101
|
+
!query.isFetching && !query.isError && projects.length === 0 && search ? /*#__PURE__*/ _jsx(NoSearchResults, {
|
|
102
|
+
search: search,
|
|
103
|
+
onClear: ()=>navigate({
|
|
104
|
+
search: {},
|
|
105
|
+
replace: true
|
|
106
|
+
})
|
|
100
107
|
}) : null,
|
|
101
108
|
projects.length > 0 ? /*#__PURE__*/ _jsxs("section", {
|
|
102
109
|
"aria-label": "Projects list",
|
|
@@ -105,7 +112,7 @@ export function ProjectsHubPage() {
|
|
|
105
112
|
className: "grid grid-cols-2 gap-12 max-[760px]:grid-cols-1",
|
|
106
113
|
children: projects.map((project)=>/*#__PURE__*/ _jsx(ProjectCard, {
|
|
107
114
|
project: project,
|
|
108
|
-
connection: connectionsById.get(project.source.
|
|
115
|
+
connection: connectionsById.get(project.source.connectionId),
|
|
109
116
|
connectionsResolved: connectionsQuery.isSuccess,
|
|
110
117
|
connectionsSettled: connectionsQuery.isSuccess || connectionsQuery.isError,
|
|
111
118
|
workspaceId: workspace.id
|
|
@@ -134,17 +141,6 @@ export function ProjectsHubPage() {
|
|
|
134
141
|
]
|
|
135
142
|
});
|
|
136
143
|
}
|
|
137
|
-
function useDebouncedValue(value, delayMs) {
|
|
138
|
-
const [debounced, setDebounced] = useState(value);
|
|
139
|
-
useEffect(()=>{
|
|
140
|
-
const handle = setTimeout(()=>setDebounced(value), delayMs);
|
|
141
|
-
return ()=>clearTimeout(handle);
|
|
142
|
-
}, [
|
|
143
|
-
value,
|
|
144
|
-
delayMs
|
|
145
|
-
]);
|
|
146
|
-
return debounced;
|
|
147
|
-
}
|
|
148
144
|
function ProjectsSkeleton() {
|
|
149
145
|
return /*#__PURE__*/ _jsx("ul", {
|
|
150
146
|
role: "status",
|
|
@@ -210,7 +206,7 @@ function ProjectCard({ project, connection, connectionsResolved, connectionsSett
|
|
|
210
206
|
// On a resolved fetch, `active` carries no badge while a missing connection
|
|
211
207
|
// reads as an error so a broken source is still flagged. An unresolved or
|
|
212
208
|
// failed fetch shows nothing, so a fetch failure never flags every card.
|
|
213
|
-
const status = connectionsResolved ? connection?.
|
|
209
|
+
const status = connectionsResolved ? connection?.lifecycleStatus ?? 'error' : undefined;
|
|
214
210
|
return /*#__PURE__*/ _jsx("li", {
|
|
215
211
|
children: /*#__PURE__*/ _jsx(Link, {
|
|
216
212
|
to: "/workspaces/$wid/projects/$pid",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/pages/projects-hub-page.tsx"],"sourcesContent":["import type {IntegrationConnectionDto} from '@shipfox/api-integration-core-dto';\nimport type {ProjectResponseDto} from '@shipfox/api-projects-dto';\nimport {useActiveWorkspace} from '@shipfox/client-auth';\nimport {\n ConnectionStatusBadge,\n IntegrationIcon,\n useIntegrationConnectionsQuery,\n} from '@shipfox/client-integrations';\nimport {QueryLoadError} from '@shipfox/client-ui';\nimport {Button} from '@shipfox/react-ui/button';\nimport {Callout} from '@shipfox/react-ui/callout';\nimport {Card} from '@shipfox/react-ui/card';\nimport {EmptyState} from '@shipfox/react-ui/empty-state';\nimport {Icon} from '@shipfox/react-ui/icon';\nimport {Input} from '@shipfox/react-ui/input';\nimport {Skeleton} from '@shipfox/react-ui/skeleton';\nimport {Header, Text} from '@shipfox/react-ui/typography';\nimport {Link} from '@tanstack/react-router';\nimport {useEffect, useState} from 'react';\nimport {ModelProviderReminderBanner} from '#components/model-provider-reminder-banner.js';\nimport {useProjectsInfiniteQuery} from '#hooks/api/projects.js';\n\nexport function ProjectsHubPage() {\n const workspace = useActiveWorkspace();\n const [searchInput, setSearchInput] = useState('');\n const trimmedInput = searchInput.trim();\n const debouncedSearch = useDebouncedValue(trimmedInput, 250);\n const query = useProjectsInfiniteQuery(workspace.id, debouncedSearch || undefined);\n const projects = query.data?.pages.flatMap((page) => page.projects) ?? [];\n\n // The provider logo and connection health live on the integration connection,\n // not the project, so resolve them once for the whole list and index by id.\n // Skip the fetch when there are no cards to annotate.\n const connectionsQuery = useIntegrationConnectionsQuery(\n projects.length > 0 ? workspace.id : undefined,\n );\n const connectionsById = new Map(\n (connectionsQuery.data?.connections ?? []).map((connection) => [connection.id, connection]),\n );\n\n const isInitialLoading = query.isPending;\n const isDebouncePending = trimmedInput !== debouncedSearch;\n const isSearching =\n Boolean(trimmedInput) && (isDebouncePending || (query.isFetching && !query.isFetchingNextPage));\n const hasNoData = !query.data;\n\n return (\n <div className=\"flex w-full flex-col gap-24\">\n <header className=\"flex flex-col gap-16\">\n <div className=\"flex items-start justify-between gap-24 max-[640px]:flex-col\">\n <Header variant=\"h2\">Projects</Header>\n </div>\n <div className=\"flex items-center gap-12 max-[640px]:flex-col max-[640px]:items-stretch\">\n <Input\n type=\"search\"\n value={searchInput}\n onChange={(event) => setSearchInput(event.target.value)}\n placeholder=\"Search projects…\"\n aria-label=\"Search projects\"\n className=\"flex-1\"\n iconLeft={<Icon name=\"searchLine\" className=\"size-16\" />}\n iconRight={\n isSearching ? (\n <Icon\n name=\"spinner\"\n size={16}\n className=\"text-foreground-neutral-muted\"\n aria-hidden=\"true\"\n />\n ) : undefined\n }\n />\n <Button asChild iconLeft=\"addLine\" className=\"shrink-0 max-[640px]:w-full\">\n <Link to=\"/workspaces/$wid/projects/new\" params={{wid: workspace.id}}>\n New project\n </Link>\n </Button>\n </div>\n </header>\n\n <ModelProviderReminderBanner workspaceId={workspace.id} />\n\n {isInitialLoading || (debouncedSearch && hasNoData && query.isFetching) ? (\n <ProjectsSkeleton />\n ) : null}\n\n {query.isError && hasNoData ? <QueryLoadError query={query} subject=\"projects\" /> : null}\n\n {!isInitialLoading && !query.isError && projects.length === 0 && !debouncedSearch ? (\n <EmptyProjects workspaceId={workspace.id} />\n ) : null}\n\n {!query.isFetching && !query.isError && projects.length === 0 && debouncedSearch ? (\n <NoSearchResults search={debouncedSearch} onClear={() => setSearchInput('')} />\n ) : null}\n\n {projects.length > 0 ? (\n <section aria-label=\"Projects list\">\n <ul className=\"grid grid-cols-2 gap-12 max-[760px]:grid-cols-1\">\n {projects.map((project) => (\n <ProjectCard\n project={project}\n connection={connectionsById.get(project.source.connection_id)}\n connectionsResolved={connectionsQuery.isSuccess}\n connectionsSettled={connectionsQuery.isSuccess || connectionsQuery.isError}\n key={project.id}\n workspaceId={workspace.id}\n />\n ))}\n </ul>\n {query.error && query.data ? (\n <Callout role=\"alert\" type=\"error\" className=\"mt-16\">\n <Text size=\"sm\">\n Could not load the next page. Existing projects are still shown.\n </Text>\n </Callout>\n ) : null}\n {query.hasNextPage ? (\n <div className=\"mt-16 flex justify-center\">\n <Button\n variant=\"secondary\"\n isLoading={query.isFetchingNextPage}\n onClick={() => query.fetchNextPage()}\n >\n Load more\n </Button>\n </div>\n ) : null}\n </section>\n ) : null}\n </div>\n );\n}\n\nfunction useDebouncedValue<T>(value: T, delayMs: number): T {\n const [debounced, setDebounced] = useState(value);\n useEffect(() => {\n const handle = setTimeout(() => setDebounced(value), delayMs);\n return () => clearTimeout(handle);\n }, [value, delayMs]);\n return debounced;\n}\n\nfunction ProjectsSkeleton() {\n return (\n <ul\n role=\"status\"\n aria-label=\"Loading projects\"\n className=\"grid grid-cols-2 gap-12 max-[760px]:grid-cols-1\"\n >\n {[0, 1, 2, 3, 4, 5].map((row) => (\n <li key={row}>\n <Card className=\"h-full p-16\">\n <div className=\"flex items-center gap-12\">\n <Skeleton className=\"size-24 shrink-0\" />\n <Skeleton className=\"h-16 w-1/2\" />\n </div>\n </Card>\n </li>\n ))}\n </ul>\n );\n}\n\nfunction EmptyProjects({workspaceId}: {workspaceId: string}) {\n return (\n <EmptyState\n icon=\"folderLine\"\n title=\"Create your first project\"\n description=\"Connect a repository-backed project to start building workflows.\"\n action={\n <Button asChild iconRight=\"chevronRight\">\n <Link to=\"/workspaces/$wid/projects/new\" params={{wid: workspaceId}}>\n Create project\n </Link>\n </Button>\n }\n />\n );\n}\n\nfunction NoSearchResults({search, onClear}: {search: string; onClear: () => void}) {\n return (\n <EmptyState\n icon=\"searchLine\"\n title={`No projects match “${search}”`}\n description=\"Try a different search, or clear it to see all projects.\"\n action={\n <Button size=\"sm\" variant=\"secondary\" onClick={onClear}>\n Clear search\n </Button>\n }\n />\n );\n}\n\nfunction ProjectCard({\n project,\n connection,\n connectionsResolved,\n connectionsSettled,\n workspaceId,\n}: {\n project: ProjectResponseDto;\n connection: IntegrationConnectionDto | undefined;\n connectionsResolved: boolean;\n connectionsSettled: boolean;\n workspaceId: string;\n}) {\n // On a resolved fetch, `active` carries no badge while a missing connection\n // reads as an error so a broken source is still flagged. An unresolved or\n // failed fetch shows nothing, so a fetch failure never flags every card.\n const status = connectionsResolved ? (connection?.lifecycle_status ?? 'error') : undefined;\n\n return (\n <li>\n <Link\n to=\"/workspaces/$wid/projects/$pid\"\n params={{wid: workspaceId, pid: project.id}}\n className=\"block h-full rounded-8 focus-visible:shadow-button-neutral-focus focus-visible:outline-none\"\n >\n <Card className=\"h-full p-16 transition-colors hover:bg-background-components-hover\">\n <div className=\"flex min-w-0 items-center gap-12\">\n {/* Settle on success or error: a failed fetch falls back to the\n neutral provider icon rather than spinning forever. */}\n {connectionsSettled ? (\n <IntegrationIcon\n source={connection?.provider}\n aria-hidden\n className=\"size-24 shrink-0 text-foreground-neutral-base\"\n />\n ) : (\n <Skeleton className=\"size-24 shrink-0\" />\n )}\n <div className=\"flex min-w-0 flex-1 items-center gap-8\">\n <Text size=\"md\" bold className=\"truncate\">\n {project.name}\n </Text>\n {status ? <ConnectionStatusBadge status={status} className=\"shrink-0\" /> : null}\n </div>\n </div>\n </Card>\n </Link>\n </li>\n );\n}\n"],"names":["useActiveWorkspace","ConnectionStatusBadge","IntegrationIcon","useIntegrationConnectionsQuery","QueryLoadError","Button","Callout","Card","EmptyState","Icon","Input","Skeleton","Header","Text","Link","useEffect","useState","ModelProviderReminderBanner","useProjectsInfiniteQuery","ProjectsHubPage","workspace","searchInput","setSearchInput","trimmedInput","trim","debouncedSearch","useDebouncedValue","query","id","undefined","projects","data","pages","flatMap","page","connectionsQuery","length","connectionsById","Map","connections","map","connection","isInitialLoading","isPending","isDebouncePending","isSearching","Boolean","isFetching","isFetchingNextPage","hasNoData","div","className","header","variant","type","value","onChange","event","target","placeholder","aria-label","iconLeft","name","iconRight","size","aria-hidden","asChild","to","params","wid","workspaceId","ProjectsSkeleton","isError","subject","EmptyProjects","NoSearchResults","search","onClear","section","ul","project","ProjectCard","get","source","connection_id","connectionsResolved","isSuccess","connectionsSettled","error","role","hasNextPage","isLoading","onClick","fetchNextPage","delayMs","debounced","setDebounced","handle","setTimeout","clearTimeout","row","li","icon","title","description","action","status","lifecycle_status","pid","provider","bold"],"mappings":";AAEA,SAAQA,kBAAkB,QAAO,uBAAuB;AACxD,SACEC,qBAAqB,EACrBC,eAAe,EACfC,8BAA8B,QACzB,+BAA+B;AACtC,SAAQC,cAAc,QAAO,qBAAqB;AAClD,SAAQC,MAAM,QAAO,2BAA2B;AAChD,SAAQC,OAAO,QAAO,4BAA4B;AAClD,SAAQC,IAAI,QAAO,yBAAyB;AAC5C,SAAQC,UAAU,QAAO,gCAAgC;AACzD,SAAQC,IAAI,QAAO,yBAAyB;AAC5C,SAAQC,KAAK,QAAO,0BAA0B;AAC9C,SAAQC,QAAQ,QAAO,6BAA6B;AACpD,SAAQC,MAAM,EAAEC,IAAI,QAAO,+BAA+B;AAC1D,SAAQC,IAAI,QAAO,yBAAyB;AAC5C,SAAQC,SAAS,EAAEC,QAAQ,QAAO,QAAQ;AAC1C,SAAQC,2BAA2B,QAAO,gDAAgD;AAC1F,SAAQC,wBAAwB,QAAO,yBAAyB;AAEhE,OAAO,SAASC;IACd,MAAMC,YAAYpB;IAClB,MAAM,CAACqB,aAAaC,eAAe,GAAGN,SAAS;IAC/C,MAAMO,eAAeF,YAAYG,IAAI;IACrC,MAAMC,kBAAkBC,kBAAkBH,cAAc;IACxD,MAAMI,QAAQT,yBAAyBE,UAAUQ,EAAE,EAAEH,mBAAmBI;IACxE,MAAMC,WAAWH,MAAMI,IAAI,EAAEC,MAAMC,QAAQ,CAACC,OAASA,KAAKJ,QAAQ,KAAK,EAAE;IAEzE,8EAA8E;IAC9E,4EAA4E;IAC5E,sDAAsD;IACtD,MAAMK,mBAAmBhC,+BACvB2B,SAASM,MAAM,GAAG,IAAIhB,UAAUQ,EAAE,GAAGC;IAEvC,MAAMQ,kBAAkB,IAAIC,IAC1B,AAACH,CAAAA,iBAAiBJ,IAAI,EAAEQ,eAAe,EAAE,AAAD,EAAGC,GAAG,CAAC,CAACC,aAAe;YAACA,WAAWb,EAAE;YAAEa;SAAW;IAG5F,MAAMC,mBAAmBf,MAAMgB,SAAS;IACxC,MAAMC,oBAAoBrB,iBAAiBE;IAC3C,MAAMoB,cACJC,QAAQvB,iBAAkBqB,CAAAA,qBAAsBjB,MAAMoB,UAAU,IAAI,CAACpB,MAAMqB,kBAAkB;IAC/F,MAAMC,YAAY,CAACtB,MAAMI,IAAI;IAE7B,qBACE,MAACmB;QAAIC,WAAU;;0BACb,MAACC;gBAAOD,WAAU;;kCAChB,KAACD;wBAAIC,WAAU;kCACb,cAAA,KAACvC;4BAAOyC,SAAQ;sCAAK;;;kCAEvB,MAACH;wBAAIC,WAAU;;0CACb,KAACzC;gCACC4C,MAAK;gCACLC,OAAOlC;gCACPmC,UAAU,CAACC,QAAUnC,eAAemC,MAAMC,MAAM,CAACH,KAAK;gCACtDI,aAAY;gCACZC,cAAW;gCACXT,WAAU;gCACVU,wBAAU,KAACpD;oCAAKqD,MAAK;oCAAaX,WAAU;;gCAC5CY,WACElB,4BACE,KAACpC;oCACCqD,MAAK;oCACLE,MAAM;oCACNb,WAAU;oCACVc,eAAY;qCAEZpC;;0CAGR,KAACxB;gCAAO6D,OAAO;gCAACL,UAAS;gCAAUV,WAAU;0CAC3C,cAAA,KAACrC;oCAAKqD,IAAG;oCAAgCC,QAAQ;wCAACC,KAAKjD,UAAUQ,EAAE;oCAAA;8CAAG;;;;;;;0BAO5E,KAACX;gBAA4BqD,aAAalD,UAAUQ,EAAE;;YAErDc,oBAAqBjB,mBAAmBwB,aAAatB,MAAMoB,UAAU,iBACpE,KAACwB,wBACC;YAEH5C,MAAM6C,OAAO,IAAIvB,0BAAY,KAAC7C;gBAAeuB,OAAOA;gBAAO8C,SAAQ;iBAAgB;YAEnF,CAAC/B,oBAAoB,CAACf,MAAM6C,OAAO,IAAI1C,SAASM,MAAM,KAAK,KAAK,CAACX,gCAChE,KAACiD;gBAAcJ,aAAalD,UAAUQ,EAAE;iBACtC;YAEH,CAACD,MAAMoB,UAAU,IAAI,CAACpB,MAAM6C,OAAO,IAAI1C,SAASM,MAAM,KAAK,KAAKX,gCAC/D,KAACkD;gBAAgBC,QAAQnD;gBAAiBoD,SAAS,IAAMvD,eAAe;iBACtE;YAEHQ,SAASM,MAAM,GAAG,kBACjB,MAAC0C;gBAAQlB,cAAW;;kCAClB,KAACmB;wBAAG5B,WAAU;kCACXrB,SAASU,GAAG,CAAC,CAACwC,wBACb,KAACC;gCACCD,SAASA;gCACTvC,YAAYJ,gBAAgB6C,GAAG,CAACF,QAAQG,MAAM,CAACC,aAAa;gCAC5DC,qBAAqBlD,iBAAiBmD,SAAS;gCAC/CC,oBAAoBpD,iBAAiBmD,SAAS,IAAInD,iBAAiBqC,OAAO;gCAE1EF,aAAalD,UAAUQ,EAAE;+BADpBoD,QAAQpD,EAAE;;oBAKpBD,MAAM6D,KAAK,IAAI7D,MAAMI,IAAI,iBACxB,KAACzB;wBAAQmF,MAAK;wBAAQnC,MAAK;wBAAQH,WAAU;kCAC3C,cAAA,KAACtC;4BAAKmD,MAAK;sCAAK;;yBAIhB;oBACHrC,MAAM+D,WAAW,iBAChB,KAACxC;wBAAIC,WAAU;kCACb,cAAA,KAAC9C;4BACCgD,SAAQ;4BACRsC,WAAWhE,MAAMqB,kBAAkB;4BACnC4C,SAAS,IAAMjE,MAAMkE,aAAa;sCACnC;;yBAID;;iBAEJ;;;AAGV;AAEA,SAASnE,kBAAqB6B,KAAQ,EAAEuC,OAAe;IACrD,MAAM,CAACC,WAAWC,aAAa,GAAGhF,SAASuC;IAC3CxC,UAAU;QACR,MAAMkF,SAASC,WAAW,IAAMF,aAAazC,QAAQuC;QACrD,OAAO,IAAMK,aAAaF;IAC5B,GAAG;QAAC1C;QAAOuC;KAAQ;IACnB,OAAOC;AACT;AAEA,SAASxB;IACP,qBACE,KAACQ;QACCU,MAAK;QACL7B,cAAW;QACXT,WAAU;kBAET;YAAC;YAAG;YAAG;YAAG;YAAG;YAAG;SAAE,CAACX,GAAG,CAAC,CAAC4D,oBACvB,KAACC;0BACC,cAAA,KAAC9F;oBAAK4C,WAAU;8BACd,cAAA,MAACD;wBAAIC,WAAU;;0CACb,KAACxC;gCAASwC,WAAU;;0CACpB,KAACxC;gCAASwC,WAAU;;;;;eAJjBiD;;AAWjB;AAEA,SAAS1B,cAAc,EAACJ,WAAW,EAAwB;IACzD,qBACE,KAAC9D;QACC8F,MAAK;QACLC,OAAM;QACNC,aAAY;QACZC,sBACE,KAACpG;YAAO6D,OAAO;YAACH,WAAU;sBACxB,cAAA,KAACjD;gBAAKqD,IAAG;gBAAgCC,QAAQ;oBAACC,KAAKC;gBAAW;0BAAG;;;;AAO/E;AAEA,SAASK,gBAAgB,EAACC,MAAM,EAAEC,OAAO,EAAwC;IAC/E,qBACE,KAACrE;QACC8F,MAAK;QACLC,OAAO,CAAC,mBAAmB,EAAE3B,OAAO,CAAC,CAAC;QACtC4B,aAAY;QACZC,sBACE,KAACpG;YAAO2D,MAAK;YAAKX,SAAQ;YAAYuC,SAASf;sBAAS;;;AAMhE;AAEA,SAASI,YAAY,EACnBD,OAAO,EACPvC,UAAU,EACV4C,mBAAmB,EACnBE,kBAAkB,EAClBjB,WAAW,EAOZ;IACC,4EAA4E;IAC5E,0EAA0E;IAC1E,yEAAyE;IACzE,MAAMoC,SAASrB,sBAAuB5C,YAAYkE,oBAAoB,UAAW9E;IAEjF,qBACE,KAACwE;kBACC,cAAA,KAACvF;YACCqD,IAAG;YACHC,QAAQ;gBAACC,KAAKC;gBAAasC,KAAK5B,QAAQpD,EAAE;YAAA;YAC1CuB,WAAU;sBAEV,cAAA,KAAC5C;gBAAK4C,WAAU;0BACd,cAAA,MAACD;oBAAIC,WAAU;;wBAGZoC,mCACC,KAACrF;4BACCiF,QAAQ1C,YAAYoE;4BACpB5C,aAAW;4BACXd,WAAU;2CAGZ,KAACxC;4BAASwC,WAAU;;sCAEtB,MAACD;4BAAIC,WAAU;;8CACb,KAACtC;oCAAKmD,MAAK;oCAAK8C,IAAI;oCAAC3D,WAAU;8CAC5B6B,QAAQlB,IAAI;;gCAEd4C,uBAAS,KAACzG;oCAAsByG,QAAQA;oCAAQvD,WAAU;qCAAgB;;;;;;;;AAOzF"}
|
|
1
|
+
{"version":3,"sources":["../../src/pages/projects-hub-page.tsx"],"sourcesContent":["import {useActiveWorkspace} from '@shipfox/client-auth';\nimport {\n ConnectionStatusBadge,\n type IntegrationConnection,\n IntegrationIcon,\n useIntegrationConnectionsQuery,\n} from '@shipfox/client-integrations';\nimport {QueryLoadError} from '@shipfox/client-ui';\nimport {Button} from '@shipfox/react-ui/button';\nimport {Callout} from '@shipfox/react-ui/callout';\nimport {Card} from '@shipfox/react-ui/card';\nimport {EmptyState} from '@shipfox/react-ui/empty-state';\nimport {Icon} from '@shipfox/react-ui/icon';\nimport {Input} from '@shipfox/react-ui/input';\nimport {Skeleton} from '@shipfox/react-ui/skeleton';\nimport {Header, Text} from '@shipfox/react-ui/typography';\nimport {Link, useNavigate} from '@tanstack/react-router';\nimport {ModelProviderReminderBanner} from '#components/model-provider-reminder-banner.js';\nimport type {Project} from '#core/project.js';\nimport {useProjectsInfiniteQuery} from '#hooks/api/projects.js';\n\nexport function ProjectsHubPage({search = ''}: {search?: string}) {\n const workspace = useActiveWorkspace();\n const navigate = useNavigate();\n const query = useProjectsInfiniteQuery(workspace.id, search || undefined);\n const projects = query.data?.pages.flatMap((page) => page.projects) ?? [];\n\n // The provider logo and connection health live on the integration connection,\n // not the project, so resolve them once for the whole list and index by id.\n // Skip the fetch when there are no cards to annotate.\n const connectionsQuery = useIntegrationConnectionsQuery(\n projects.length > 0 ? workspace.id : undefined,\n );\n const connectionsById = new Map(\n (connectionsQuery.data ?? []).map((connection) => [connection.id, connection]),\n );\n\n const isInitialLoading = query.isPending;\n const isSearching = Boolean(search) && query.isFetching && !query.isFetchingNextPage;\n const hasNoData = !query.data;\n\n return (\n <div className=\"flex w-full flex-col gap-24\">\n <header className=\"flex flex-col gap-16\">\n <div className=\"flex items-start justify-between gap-24 max-[640px]:flex-col\">\n <Header variant=\"h2\">Projects</Header>\n </div>\n <div className=\"flex items-center gap-12 max-[640px]:flex-col max-[640px]:items-stretch\">\n <Input\n type=\"search\"\n value={search}\n onChange={(event) => {\n const next = event.target.value.trim();\n navigate({search: (next ? {search: next} : {}) as never, replace: true});\n }}\n placeholder=\"Search projects…\"\n aria-label=\"Search projects\"\n className=\"flex-1\"\n iconLeft={<Icon name=\"searchLine\" className=\"size-16\" />}\n iconRight={\n isSearching ? (\n <Icon\n name=\"spinner\"\n size={16}\n className=\"text-foreground-neutral-muted\"\n aria-hidden=\"true\"\n />\n ) : undefined\n }\n />\n <Button asChild iconLeft=\"addLine\" className=\"shrink-0 max-[640px]:w-full\">\n <Link to=\"/workspaces/$wid/projects/new\" params={{wid: workspace.id}}>\n New project\n </Link>\n </Button>\n </div>\n </header>\n\n <ModelProviderReminderBanner workspaceId={workspace.id} />\n\n {isInitialLoading || (search && hasNoData && query.isFetching) ? <ProjectsSkeleton /> : null}\n\n {query.isError && hasNoData ? <QueryLoadError query={query} subject=\"projects\" /> : null}\n\n {!isInitialLoading && !query.isError && projects.length === 0 && !search ? (\n <EmptyProjects workspaceId={workspace.id} />\n ) : null}\n\n {!query.isFetching && !query.isError && projects.length === 0 && search ? (\n <NoSearchResults\n search={search}\n onClear={() => navigate({search: {} as never, replace: true})}\n />\n ) : null}\n\n {projects.length > 0 ? (\n <section aria-label=\"Projects list\">\n <ul className=\"grid grid-cols-2 gap-12 max-[760px]:grid-cols-1\">\n {projects.map((project) => (\n <ProjectCard\n project={project}\n connection={connectionsById.get(project.source.connectionId)}\n connectionsResolved={connectionsQuery.isSuccess}\n connectionsSettled={connectionsQuery.isSuccess || connectionsQuery.isError}\n key={project.id}\n workspaceId={workspace.id}\n />\n ))}\n </ul>\n {query.error && query.data ? (\n <Callout role=\"alert\" type=\"error\" className=\"mt-16\">\n <Text size=\"sm\">\n Could not load the next page. Existing projects are still shown.\n </Text>\n </Callout>\n ) : null}\n {query.hasNextPage ? (\n <div className=\"mt-16 flex justify-center\">\n <Button\n variant=\"secondary\"\n isLoading={query.isFetchingNextPage}\n onClick={() => query.fetchNextPage()}\n >\n Load more\n </Button>\n </div>\n ) : null}\n </section>\n ) : null}\n </div>\n );\n}\n\nfunction ProjectsSkeleton() {\n return (\n <ul\n role=\"status\"\n aria-label=\"Loading projects\"\n className=\"grid grid-cols-2 gap-12 max-[760px]:grid-cols-1\"\n >\n {[0, 1, 2, 3, 4, 5].map((row) => (\n <li key={row}>\n <Card className=\"h-full p-16\">\n <div className=\"flex items-center gap-12\">\n <Skeleton className=\"size-24 shrink-0\" />\n <Skeleton className=\"h-16 w-1/2\" />\n </div>\n </Card>\n </li>\n ))}\n </ul>\n );\n}\n\nfunction EmptyProjects({workspaceId}: {workspaceId: string}) {\n return (\n <EmptyState\n icon=\"folderLine\"\n title=\"Create your first project\"\n description=\"Connect a repository-backed project to start building workflows.\"\n action={\n <Button asChild iconRight=\"chevronRight\">\n <Link to=\"/workspaces/$wid/projects/new\" params={{wid: workspaceId}}>\n Create project\n </Link>\n </Button>\n }\n />\n );\n}\n\nfunction NoSearchResults({search, onClear}: {search: string; onClear: () => void}) {\n return (\n <EmptyState\n icon=\"searchLine\"\n title={`No projects match “${search}”`}\n description=\"Try a different search, or clear it to see all projects.\"\n action={\n <Button size=\"sm\" variant=\"secondary\" onClick={onClear}>\n Clear search\n </Button>\n }\n />\n );\n}\n\nfunction ProjectCard({\n project,\n connection,\n connectionsResolved,\n connectionsSettled,\n workspaceId,\n}: {\n project: Project;\n connection: IntegrationConnection | undefined;\n connectionsResolved: boolean;\n connectionsSettled: boolean;\n workspaceId: string;\n}) {\n // On a resolved fetch, `active` carries no badge while a missing connection\n // reads as an error so a broken source is still flagged. An unresolved or\n // failed fetch shows nothing, so a fetch failure never flags every card.\n const status = connectionsResolved ? (connection?.lifecycleStatus ?? 'error') : undefined;\n\n return (\n <li>\n <Link\n to=\"/workspaces/$wid/projects/$pid\"\n params={{wid: workspaceId, pid: project.id}}\n className=\"block h-full rounded-8 focus-visible:shadow-button-neutral-focus focus-visible:outline-none\"\n >\n <Card className=\"h-full p-16 transition-colors hover:bg-background-components-hover\">\n <div className=\"flex min-w-0 items-center gap-12\">\n {/* Settle on success or error: a failed fetch falls back to the\n neutral provider icon rather than spinning forever. */}\n {connectionsSettled ? (\n <IntegrationIcon\n source={connection?.provider}\n aria-hidden\n className=\"size-24 shrink-0 text-foreground-neutral-base\"\n />\n ) : (\n <Skeleton className=\"size-24 shrink-0\" />\n )}\n <div className=\"flex min-w-0 flex-1 items-center gap-8\">\n <Text size=\"md\" bold className=\"truncate\">\n {project.name}\n </Text>\n {status ? <ConnectionStatusBadge status={status} className=\"shrink-0\" /> : null}\n </div>\n </div>\n </Card>\n </Link>\n </li>\n );\n}\n"],"names":["useActiveWorkspace","ConnectionStatusBadge","IntegrationIcon","useIntegrationConnectionsQuery","QueryLoadError","Button","Callout","Card","EmptyState","Icon","Input","Skeleton","Header","Text","Link","useNavigate","ModelProviderReminderBanner","useProjectsInfiniteQuery","ProjectsHubPage","search","workspace","navigate","query","id","undefined","projects","data","pages","flatMap","page","connectionsQuery","length","connectionsById","Map","map","connection","isInitialLoading","isPending","isSearching","Boolean","isFetching","isFetchingNextPage","hasNoData","div","className","header","variant","type","value","onChange","event","next","target","trim","replace","placeholder","aria-label","iconLeft","name","iconRight","size","aria-hidden","asChild","to","params","wid","workspaceId","ProjectsSkeleton","isError","subject","EmptyProjects","NoSearchResults","onClear","section","ul","project","ProjectCard","get","source","connectionId","connectionsResolved","isSuccess","connectionsSettled","error","role","hasNextPage","isLoading","onClick","fetchNextPage","row","li","icon","title","description","action","status","lifecycleStatus","pid","provider","bold"],"mappings":"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"}
|
package/dist/routes/home.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { validateProjectsSearch } from './search.js';
|
|
2
2
|
declare const _default: import("@shipfox/client-shell/runtime").RouteImpl<{
|
|
3
|
-
readonly
|
|
3
|
+
readonly validateSearch: typeof validateProjectsSearch;
|
|
4
|
+
readonly component: () => import("react").JSX.Element;
|
|
4
5
|
}>;
|
|
5
6
|
export default _default;
|
|
6
7
|
//# sourceMappingURL=home.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"home.d.ts","sourceRoot":"","sources":["../../src/routes/home.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"home.d.ts","sourceRoot":"","sources":["../../src/routes/home.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAC,sBAAsB,EAAC,MAAM,aAAa,CAAC;;;;;AAEnD,wBAGG"}
|
package/dist/routes/home.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { defineRoute, useRouteSearch } from '@shipfox/client-shell/runtime';
|
|
2
3
|
import { HomeRouter } from '#pages/home-router.js';
|
|
4
|
+
import { validateProjectsSearch } from './search.js';
|
|
3
5
|
export default defineRoute({
|
|
4
|
-
|
|
6
|
+
validateSearch: validateProjectsSearch,
|
|
7
|
+
component: ()=>/*#__PURE__*/ _jsx(HomeRouter, {
|
|
8
|
+
search: useRouteSearch(validateProjectsSearch)
|
|
9
|
+
})
|
|
5
10
|
});
|
|
6
11
|
|
|
7
12
|
//# sourceMappingURL=home.js.map
|
package/dist/routes/home.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/routes/home.tsx"],"sourcesContent":["import {defineRoute} from '@shipfox/client-shell/runtime';\nimport {HomeRouter} from '#pages/home-router.js';\n\nexport default defineRoute({component: HomeRouter});\n"],"names":["defineRoute","HomeRouter","component"],"mappings":"AAAA,SAAQA,WAAW,QAAO,gCAAgC;
|
|
1
|
+
{"version":3,"sources":["../../src/routes/home.tsx"],"sourcesContent":["import {defineRoute, useRouteSearch} from '@shipfox/client-shell/runtime';\nimport {HomeRouter} from '#pages/home-router.js';\nimport {validateProjectsSearch} from './search.js';\n\nexport default defineRoute({\n validateSearch: validateProjectsSearch,\n component: () => <HomeRouter search={useRouteSearch(validateProjectsSearch)} />,\n});\n"],"names":["defineRoute","useRouteSearch","HomeRouter","validateProjectsSearch","validateSearch","component","search"],"mappings":";AAAA,SAAQA,WAAW,EAAEC,cAAc,QAAO,gCAAgC;AAC1E,SAAQC,UAAU,QAAO,wBAAwB;AACjD,SAAQC,sBAAsB,QAAO,cAAc;AAEnD,eAAeH,YAAY;IACzBI,gBAAgBD;IAChBE,WAAW,kBAAM,KAACH;YAAWI,QAAQL,eAAeE;;AACtD,GAAG"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inputs.d.ts","sourceRoot":"","sources":["../../src/routes/inputs.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;CACb;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,kBAAkB,CAKrF"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function projectRouteParams(input) {
|
|
2
|
+
const wid = stringParam(input.wid);
|
|
3
|
+
const pid = stringParam(input.pid);
|
|
4
|
+
if (!wid || !pid) throw new Error('Project route is missing required path parameters.');
|
|
5
|
+
return {
|
|
6
|
+
wid,
|
|
7
|
+
pid
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
function stringParam(value) {
|
|
11
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
//# sourceMappingURL=inputs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/routes/inputs.ts"],"sourcesContent":["export interface ProjectRouteParams {\n wid: string;\n pid: string;\n}\n\nexport function projectRouteParams(input: Record<string, unknown>): ProjectRouteParams {\n const wid = stringParam(input.wid);\n const pid = stringParam(input.pid);\n if (!wid || !pid) throw new Error('Project route is missing required path parameters.');\n return {wid, pid};\n}\n\nfunction stringParam(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\n"],"names":["projectRouteParams","input","wid","stringParam","pid","Error","value","length","undefined"],"mappings":"AAKA,OAAO,SAASA,mBAAmBC,KAA8B;IAC/D,MAAMC,MAAMC,YAAYF,MAAMC,GAAG;IACjC,MAAME,MAAMD,YAAYF,MAAMG,GAAG;IACjC,IAAI,CAACF,OAAO,CAACE,KAAK,MAAM,IAAIC,MAAM;IAClC,OAAO;QAACH;QAAKE;IAAG;AAClB;AAEA,SAASD,YAAYG,KAAc;IACjC,OAAO,OAAOA,UAAU,YAAYA,MAAMC,MAAM,GAAG,IAAID,QAAQE;AACjE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/routes/search.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,cAAc,CAGrF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/routes/search.ts"],"sourcesContent":["export interface ProjectsSearch {\n search?: string;\n}\n\nexport function validateProjectsSearch(input: Record<string, unknown>): ProjectsSearch {\n const search = typeof input.search === 'string' ? input.search.trim() : '';\n return search ? {search} : {};\n}\n"],"names":["validateProjectsSearch","input","search","trim"],"mappings":"AAIA,OAAO,SAASA,uBAAuBC,KAA8B;IACnE,MAAMC,SAAS,OAAOD,MAAMC,MAAM,KAAK,WAAWD,MAAMC,MAAM,CAACC,IAAI,KAAK;IACxE,OAAOD,SAAS;QAACA;IAAM,IAAI,CAAC;AAC9B"}
|