@shipfox/client-projects 21.0.0 → 22.0.0

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.
Files changed (30) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +21 -0
  3. package/dist/components/source-strip.d.ts.map +1 -1
  4. package/dist/components/source-strip.js +49 -45
  5. package/dist/components/source-strip.js.map +1 -1
  6. package/dist/pages/create-project-page.d.ts.map +1 -1
  7. package/dist/pages/create-project-page.js +161 -175
  8. package/dist/pages/create-project-page.js.map +1 -1
  9. package/dist/pages/project-settings-page.d.ts.map +1 -1
  10. package/dist/pages/project-settings-page.js +82 -84
  11. package/dist/pages/project-settings-page.js.map +1 -1
  12. package/dist/pages/projects-hub-page.d.ts.map +1 -1
  13. package/dist/pages/projects-hub-page.js +160 -159
  14. package/dist/pages/projects-hub-page.js.map +1 -1
  15. package/dist/pages/projects-hub-page.stories.d.ts +22 -0
  16. package/dist/pages/projects-hub-page.stories.d.ts.map +1 -0
  17. package/dist/pages/projects-hub-page.stories.js +214 -0
  18. package/dist/pages/projects-hub-page.stories.js.map +1 -0
  19. package/dist/tsconfig.test.tsbuildinfo +1 -1
  20. package/package.json +7 -7
  21. package/src/components/source-strip.tsx +29 -26
  22. package/src/pages/create-project-page.test.tsx +85 -1
  23. package/src/pages/create-project-page.tsx +165 -153
  24. package/src/pages/home-router.test.tsx +2 -1
  25. package/src/pages/project-settings-page.test.tsx +10 -1
  26. package/src/pages/project-settings-page.tsx +88 -76
  27. package/src/pages/projects-hub-page.stories.tsx +203 -0
  28. package/src/pages/projects-hub-page.test.tsx +90 -10
  29. package/src/pages/projects-hub-page.tsx +136 -125
  30. package/tsconfig.build.tsbuildinfo +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/pages/create-project-page.tsx"],"sourcesContent":["import {slugifyName, slugSchema} from '@shipfox/api-common-dto';\nimport {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, SlugField} 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 {\n getProject,\n useCreateProjectMutation,\n useProjectSlugAvailability,\n} from '#hooks/api/projects.js';\nimport {projectErrorCopy} from '#project-error.js';\n\nfunction isSlugValid(value: string): boolean {\n return slugSchema.safeParse(value).success;\n}\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 const defaultProjectSlug = slugifyName(defaultProjectName, {fallback: 'project'});\n\n const [formError, setFormError] = useState<string | undefined>();\n const [slugManuallyEdited, setSlugManuallyEdited] = useState(false);\n const [slugCheckEnabled, setSlugCheckEnabled] = useState(false);\n const [slugConflict, setSlugConflict] = useState(false);\n const workspaceId = workspace?.id;\n const checkProjectSlugAvailability = useProjectSlugAvailability(workspaceId);\n\n const form = useForm({\n defaultValues: {name: defaultProjectName, slug: defaultProjectSlug},\n onSubmit: async ({value}) => {\n const projectSlug = slugManuallyEdited\n ? value.slug\n : slugifyName(value.name, {fallback: 'project'});\n await createProjectFromForm(nameTouched ? value.name : defaultProjectName, projectSlug);\n },\n });\n\n useEffect(() => {\n if (!nameTouched && form.state.values.name !== defaultProjectName) {\n form.setFieldValue('name', defaultProjectName);\n if (!slugManuallyEdited) {\n setSlugConflict(false);\n form.setFieldValue('slug', slugifyName(defaultProjectName, {fallback: 'project'}));\n }\n }\n }, [defaultProjectName, form, nameTouched, slugManuallyEdited]);\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 (\n <Navigate\n to=\"/w/$workspaceSlug/integrations\"\n params={{workspaceSlug: workspace.slug}}\n replace\n />\n );\n }\n\n async function createProjectFromForm(projectName: string, projectSlug: string) {\n setFormError(undefined);\n setSlugConflict(false);\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 slug: projectSlug,\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: '/w/$workspaceSlug/p/$projectSlug',\n params: {workspaceSlug: workspace.slug, projectSlug: project.slug},\n });\n } catch (error) {\n const copy = projectErrorCopy(error);\n if (copy.existingProjectId) {\n toast.info('Project already exists.');\n try {\n const project = await getProject(copy.existingProjectId);\n await navigate({\n to: '/w/$workspaceSlug/p/$projectSlug',\n params: {workspaceSlug: workspace.slug, projectSlug: project.slug},\n });\n } catch (recoveryError) {\n const recoveryCopy = projectErrorCopy(recoveryError);\n setFormError(`${recoveryCopy.title}: ${recoveryCopy.message}`);\n requestAnimationFrame(() => errorRef.current?.focus());\n }\n return;\n }\n if (copy.slugConflict) {\n setSlugConflict(true);\n requestAnimationFrame(() => document.getElementById('project-slug')?.focus());\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-section\">\n <header className=\"flex flex-col gap-inline\">\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-cluster\">\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-region lg:grid-cols-[minmax(0,1fr)_340px]\"\n >\n <div className=\"flex min-w-0 flex-col gap-region\">\n <section className=\"flex flex-col gap-group\" aria-label=\"Source integration\">\n <div className=\"flex items-start justify-between gap-group\">\n <div className=\"flex flex-col gap-tight\">\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\n to=\"/w/$workspaceSlug/integrations\"\n params={{workspaceSlug: workspace.slug}}\n >\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-group\" aria-label=\"Repository\">\n <div className=\"flex flex-col gap-tight\">\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-group rounded-8 border border-border-neutral-base bg-background-neutral-base p-panel\">\n <div className=\"flex flex-col gap-tight\">\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 const nextName = event.target.value;\n setNameTouched(true);\n field.handleChange(nextName);\n if (!slugManuallyEdited) {\n setSlugCheckEnabled(true);\n setSlugConflict(false);\n form.setFieldValue('slug', slugifyName(nextName, {fallback: 'project'}));\n }\n }}\n onBlur={field.handleBlur}\n placeholder=\"Platform\"\n />\n </FormField>\n )}\n </form.Field>\n\n <form.Field\n name=\"slug\"\n validators={{\n onBlur: createProjectBodySchema.shape.slug,\n onSubmit: createProjectBodySchema.shape.slug,\n }}\n >\n {(field) => (\n <SlugField\n id=\"project-slug\"\n label=\"Project slug\"\n name=\"slug\"\n value={field.state.value}\n onChange={(value) => {\n setSlugManuallyEdited(true);\n setSlugCheckEnabled(true);\n setSlugConflict(false);\n field.handleChange(value);\n }}\n onBlur={field.handleBlur}\n error={slugConflict ? 'This project slug is already in use.' : fieldError(field)}\n description={\n <span className=\"font-code\">\n {`/w/${workspace.slug}/p/${field.state.value}`}\n </span>\n }\n placeholder=\"platform\"\n className=\"font-code\"\n checkEnabled={slugCheckEnabled}\n debounceMs={0}\n isValid={isSlugValid}\n checkAvailability={checkProjectSlugAvailability}\n />\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-inline\">\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":["slugifyName","slugSchema","createProjectBodySchema","useMaybeActiveWorkspace","ConnectionPicker","IntegrationIcon","RepositoryPicker","useRepositoriesInfiniteQuery","useSourceConnectionsQuery","displayNameFieldError","SlugField","Button","Callout","FormField","FormFieldInput","fieldError","FullPageLoader","toast","Header","Text","useForm","Link","Navigate","useNavigate","useEffect","useRef","useState","ModelProviderReminderBanner","projectNameFromRepository","getProject","useCreateProjectMutation","useProjectSlugAvailability","projectErrorCopy","isSlugValid","value","safeParse","success","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","defaultProjectSlug","fallback","formError","setFormError","slugManuallyEdited","setSlugManuallyEdited","slugCheckEnabled","setSlugCheckEnabled","slugConflict","setSlugConflict","workspaceId","checkProjectSlugAvailability","form","defaultValues","slug","onSubmit","projectSlug","createProjectFromForm","state","values","setFieldValue","selectConnection","connectionId","isPending","isError","to","params","workspaceSlug","replace","projectName","current","focus","command","source","project","mutateAsync","error","copy","existingProjectId","info","recoveryError","recoveryCopy","title","message","requestAnimationFrame","document","getElementById","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","nextName","target","handleChange","handleBlur","placeholder","description","span","checkEnabled","debounceMs","isValid","checkAvailability","iconRight","disabled","provider","aria-hidden","bold","delayMs","debounced","setDebounced","handle","setTimeout","clearTimeout"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/pages/create-project-page.tsx"],"sourcesContent":["import {slugifyName, slugSchema} from '@shipfox/api-common-dto';\nimport {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, SlugField} 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 {Input} from '@shipfox/react-ui/input';\nimport {FullPageLoader} from '@shipfox/react-ui/loader';\nimport {Panel, PanelActions, PanelBody, PanelHeader, PanelTitle} from '@shipfox/react-ui/panel';\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 {\n getProject,\n useCreateProjectMutation,\n useProjectSlugAvailability,\n} from '#hooks/api/projects.js';\nimport {projectErrorCopy} from '#project-error.js';\n\nfunction isSlugValid(value: string): boolean {\n return slugSchema.safeParse(value).success;\n}\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 const defaultProjectSlug = slugifyName(defaultProjectName, {fallback: 'project'});\n\n const [formError, setFormError] = useState<string | undefined>();\n const [slugManuallyEdited, setSlugManuallyEdited] = useState(false);\n const [slugCheckEnabled, setSlugCheckEnabled] = useState(false);\n const [slugConflict, setSlugConflict] = useState(false);\n const workspaceId = workspace?.id;\n const checkProjectSlugAvailability = useProjectSlugAvailability(workspaceId);\n\n const form = useForm({\n defaultValues: {name: defaultProjectName, slug: defaultProjectSlug},\n onSubmit: async ({value}) => {\n const projectSlug = slugManuallyEdited\n ? value.slug\n : slugifyName(value.name, {fallback: 'project'});\n await createProjectFromForm(nameTouched ? value.name : defaultProjectName, projectSlug);\n },\n });\n\n useEffect(() => {\n if (!nameTouched && form.state.values.name !== defaultProjectName) {\n form.setFieldValue('name', defaultProjectName);\n if (!slugManuallyEdited) {\n setSlugConflict(false);\n form.setFieldValue('slug', slugifyName(defaultProjectName, {fallback: 'project'}));\n }\n }\n }, [defaultProjectName, form, nameTouched, slugManuallyEdited]);\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 (\n <Navigate\n to=\"/w/$workspaceSlug/integrations\"\n params={{workspaceSlug: workspace.slug}}\n replace\n />\n );\n }\n\n async function createProjectFromForm(projectName: string, projectSlug: string) {\n setFormError(undefined);\n setSlugConflict(false);\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 slug: projectSlug,\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: '/w/$workspaceSlug/p/$projectSlug',\n params: {workspaceSlug: workspace.slug, projectSlug: project.slug},\n });\n } catch (error) {\n const copy = projectErrorCopy(error);\n if (copy.existingProjectId) {\n toast.info('Project already exists.');\n try {\n const project = await getProject(copy.existingProjectId);\n await navigate({\n to: '/w/$workspaceSlug/p/$projectSlug',\n params: {workspaceSlug: workspace.slug, projectSlug: project.slug},\n });\n } catch (recoveryError) {\n const recoveryCopy = projectErrorCopy(recoveryError);\n setFormError(`${recoveryCopy.title}: ${recoveryCopy.message}`);\n requestAnimationFrame(() => errorRef.current?.focus());\n }\n return;\n }\n if (copy.slugConflict) {\n setSlugConflict(true);\n requestAnimationFrame(() => document.getElementById('project-slug')?.focus());\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-section\">\n <Header id=\"create-project-title\" variant=\"h1\">\n Create project\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-cluster\">\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-region lg:grid-cols-[minmax(0,1fr)_340px]\"\n >\n <div className=\"flex min-w-0 flex-col gap-region\">\n {connections.length > 0 ? (\n <section aria-label=\"Source integration\">\n <Panel>\n <PanelHeader>\n <PanelTitle>Source integration</PanelTitle>\n {connections.length === 1 ? (\n <PanelActions>\n <Button asChild variant=\"transparent\" size=\"sm\" className=\"shrink-0\">\n <Link\n to=\"/w/$workspaceSlug/integrations\"\n params={{workspaceSlug: workspace.slug}}\n >\n Add another integration\n </Link>\n </Button>\n </PanelActions>\n ) : null}\n </PanelHeader>\n\n <PanelBody>\n <ConnectionPicker\n connections={connections}\n selectedConnectionId={effectiveSelectedConnectionId}\n onSelect={selectConnection}\n />\n </PanelBody>\n </Panel>\n </section>\n ) : null}\n\n {showRepoPicker ? (\n <section aria-label=\"Repository\">\n <Panel>\n <PanelHeader className=\"flex-wrap\">\n <PanelTitle>Repository</PanelTitle>\n <PanelActions className=\"min-w-0 max-[640px]:ml-0 max-[640px]:basis-full\">\n <Input\n type=\"search\"\n placeholder=\"Search repositories…\"\n aria-label=\"Search repositories\"\n value={repoFilter}\n onChange={(event) => setRepoFilter(event.target.value)}\n className=\"w-full max-w-[320px]\"\n />\n </PanelActions>\n </PanelHeader>\n <PanelBody>\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 />\n </PanelBody>\n </Panel>\n </section>\n ) : null}\n </div>\n\n <aside className=\"lg:sticky lg:top-32\">\n <Panel>\n <PanelHeader>\n <PanelTitle>Project details</PanelTitle>\n </PanelHeader>\n <PanelBody className=\"gap-group p-panel\">\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(\n value,\n 'Project name',\n createProjectBodySchema.shape.name,\n ),\n onSubmit: ({value}) =>\n displayNameFieldError(\n value,\n 'Project name',\n createProjectBodySchema.shape.name,\n ),\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 const nextName = event.target.value;\n setNameTouched(true);\n field.handleChange(nextName);\n if (!slugManuallyEdited) {\n setSlugCheckEnabled(true);\n setSlugConflict(false);\n form.setFieldValue('slug', slugifyName(nextName, {fallback: 'project'}));\n }\n }}\n onBlur={field.handleBlur}\n placeholder=\"Platform\"\n />\n </FormField>\n )}\n </form.Field>\n\n <form.Field\n name=\"slug\"\n validators={{\n onBlur: createProjectBodySchema.shape.slug,\n onSubmit: createProjectBodySchema.shape.slug,\n }}\n >\n {(field) => (\n <SlugField\n id=\"project-slug\"\n label=\"Project slug\"\n name=\"slug\"\n value={field.state.value}\n onChange={(value) => {\n setSlugManuallyEdited(true);\n setSlugCheckEnabled(true);\n setSlugConflict(false);\n field.handleChange(value);\n }}\n onBlur={field.handleBlur}\n error={\n slugConflict ? 'This project slug is already in use.' : fieldError(field)\n }\n description={\n <span className=\"font-code\">\n {`/w/${workspace.slug}/p/${field.state.value}`}\n </span>\n }\n placeholder=\"platform\"\n className=\"font-code\"\n checkEnabled={slugCheckEnabled}\n debounceMs={0}\n isValid={isSlugValid}\n checkAvailability={checkProjectSlugAvailability}\n />\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 </PanelBody>\n </Panel>\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-inline\">\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":["slugifyName","slugSchema","createProjectBodySchema","useMaybeActiveWorkspace","ConnectionPicker","IntegrationIcon","RepositoryPicker","useRepositoriesInfiniteQuery","useSourceConnectionsQuery","displayNameFieldError","SlugField","Button","Callout","FormField","FormFieldInput","fieldError","Input","FullPageLoader","Panel","PanelActions","PanelBody","PanelHeader","PanelTitle","toast","Header","Text","useForm","Link","Navigate","useNavigate","useEffect","useRef","useState","ModelProviderReminderBanner","projectNameFromRepository","getProject","useCreateProjectMutation","useProjectSlugAvailability","projectErrorCopy","isSlugValid","value","safeParse","success","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","defaultProjectSlug","fallback","formError","setFormError","slugManuallyEdited","setSlugManuallyEdited","slugCheckEnabled","setSlugCheckEnabled","slugConflict","setSlugConflict","workspaceId","checkProjectSlugAvailability","form","defaultValues","slug","onSubmit","projectSlug","createProjectFromForm","state","values","setFieldValue","selectConnection","connectionId","isPending","isError","to","params","workspaceSlug","replace","projectName","current","focus","command","source","project","mutateAsync","error","copy","existingProjectId","info","recoveryError","recoveryCopy","title","message","requestAnimationFrame","document","getElementById","showRepoPicker","Boolean","filteredEmptyMessage","div","className","variant","role","type","size","onClick","refetch","event","preventDefault","stopPropagation","handleSubmit","noValidate","aria-labelledby","section","aria-label","asChild","onSelect","placeholder","onChange","target","isLoading","isFetchingNextPage","hasNextPage","onLoadMore","fetchNextPage","emptyMessage","aside","ref","tabIndex","ProjectSummary","repositoryName","fullName","Field","validators","onBlur","shape","field","label","nextName","handleChange","handleBlur","description","span","checkEnabled","debounceMs","isValid","checkAvailability","iconRight","disabled","provider","aria-hidden","bold","delayMs","debounced","setDebounced","handle","setTimeout","clearTimeout"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"project-settings-page.d.ts","sourceRoot":"","sources":["../../src/pages/project-settings-page.tsx"],"names":[],"mappings":"AAkCA,wBAAgB,mBAAmB,gCA8BlC"}
1
+ {"version":3,"file":"project-settings-page.d.ts","sourceRoot":"","sources":["../../src/pages/project-settings-page.tsx"],"names":[],"mappings":"AAmCA,wBAAgB,mBAAmB,gCA8BlC"}
@@ -9,8 +9,9 @@ import { Callout } from '@shipfox/react-ui/callout';
9
9
  import { EmptyState } from '@shipfox/react-ui/empty-state';
10
10
  import { FormField, FormFieldInput, fieldError } from '@shipfox/react-ui/form-field';
11
11
  import { FullPageLoader } from '@shipfox/react-ui/loader';
12
+ import { Panel, PanelBody } from '@shipfox/react-ui/panel';
12
13
  import { toast } from '@shipfox/react-ui/toast';
13
- import { Header, Text } from '@shipfox/react-ui/typography';
14
+ import { Header } from '@shipfox/react-ui/typography';
14
15
  import { useForm } from '@tanstack/react-form';
15
16
  import { useNavigate } from '@tanstack/react-router';
16
17
  import { useState } from 'react';
@@ -120,92 +121,89 @@ function ProjectSettingsForm({ project }) {
120
121
  /*#__PURE__*/ _jsxs("div", {
121
122
  className: "flex min-w-0 flex-col gap-section",
122
123
  children: [
123
- /*#__PURE__*/ _jsxs("header", {
124
- className: "flex flex-col gap-inline",
125
- children: [
126
- /*#__PURE__*/ _jsx(Header, {
127
- variant: "h1",
128
- children: "General"
129
- }),
130
- /*#__PURE__*/ _jsx(Text, {
131
- size: "sm",
132
- className: "text-foreground-neutral-muted",
133
- children: "Update the project name and the slug used in its URLs."
134
- })
135
- ]
124
+ /*#__PURE__*/ _jsx(Header, {
125
+ variant: "h1",
126
+ children: "General"
136
127
  }),
137
- formError ? /*#__PURE__*/ _jsx(Callout, {
138
- role: "alert",
139
- type: "error",
140
- children: formError
141
- }) : null,
142
- /*#__PURE__*/ _jsxs("form", {
143
- className: "flex max-w-[560px] flex-col gap-group",
144
- noValidate: true,
145
- onSubmit: (event)=>{
146
- event.preventDefault();
147
- event.stopPropagation();
148
- void form.handleSubmit();
149
- },
150
- children: [
151
- /*#__PURE__*/ _jsx(form.Field, {
152
- name: "name",
153
- validators: {
154
- onBlur: ({ value })=>displayNameFieldError(value, 'Project name', createProjectBodySchema.shape.name),
155
- onSubmit: ({ value })=>displayNameFieldError(value, 'Project name', createProjectBodySchema.shape.name)
156
- },
157
- children: (field)=>/*#__PURE__*/ _jsx(FormField, {
158
- label: "Project name",
159
- id: "project-settings-name",
160
- error: fieldError(field),
161
- children: /*#__PURE__*/ _jsx(FormFieldInput, {
128
+ /*#__PURE__*/ _jsx(Panel, {
129
+ children: /*#__PURE__*/ _jsxs(PanelBody, {
130
+ className: "gap-group p-panel",
131
+ children: [
132
+ formError ? /*#__PURE__*/ _jsx(Callout, {
133
+ role: "alert",
134
+ type: "error",
135
+ children: formError
136
+ }) : null,
137
+ /*#__PURE__*/ _jsxs("form", {
138
+ className: "flex w-full max-w-[560px] flex-col gap-group",
139
+ noValidate: true,
140
+ onSubmit: (event)=>{
141
+ event.preventDefault();
142
+ event.stopPropagation();
143
+ void form.handleSubmit();
144
+ },
145
+ children: [
146
+ /*#__PURE__*/ _jsx(form.Field, {
162
147
  name: "name",
163
- type: "text",
164
- value: field.state.value,
165
- onChange: (event)=>field.handleChange(event.target.value),
166
- onBlur: field.handleBlur
167
- })
168
- })
169
- }),
170
- /*#__PURE__*/ _jsx(form.Field, {
171
- name: "slug",
172
- validators: {
173
- onBlur: createProjectBodySchema.shape.slug,
174
- onSubmit: createProjectBodySchema.shape.slug
175
- },
176
- children: (field)=>/*#__PURE__*/ _jsx(SlugField, {
177
- id: "project-settings-slug",
178
- label: "Project slug",
179
- name: "slug",
180
- value: field.state.value,
181
- onChange: (value)=>field.handleChange(value),
182
- onBlur: field.handleBlur,
183
- error: fieldError(field),
184
- description: /*#__PURE__*/ _jsxs("span", {
185
- className: "break-all font-code",
186
- children: [
187
- "/w/",
188
- workspace.slug,
189
- "/p/",
190
- field.state.value
191
- ]
148
+ validators: {
149
+ onBlur: ({ value })=>displayNameFieldError(value, 'Project name', createProjectBodySchema.shape.name),
150
+ onSubmit: ({ value })=>displayNameFieldError(value, 'Project name', createProjectBodySchema.shape.name)
151
+ },
152
+ children: (field)=>/*#__PURE__*/ _jsx(FormField, {
153
+ label: "Project name",
154
+ id: "project-settings-name",
155
+ error: fieldError(field),
156
+ children: /*#__PURE__*/ _jsx(FormFieldInput, {
157
+ name: "name",
158
+ type: "text",
159
+ value: field.state.value,
160
+ onChange: (event)=>field.handleChange(event.target.value),
161
+ onBlur: field.handleBlur
162
+ })
163
+ })
164
+ }),
165
+ /*#__PURE__*/ _jsx(form.Field, {
166
+ name: "slug",
167
+ validators: {
168
+ onBlur: createProjectBodySchema.shape.slug,
169
+ onSubmit: createProjectBodySchema.shape.slug
170
+ },
171
+ children: (field)=>/*#__PURE__*/ _jsx(SlugField, {
172
+ id: "project-settings-slug",
173
+ label: "Project slug",
174
+ name: "slug",
175
+ value: field.state.value,
176
+ onChange: (value)=>field.handleChange(value),
177
+ onBlur: field.handleBlur,
178
+ error: fieldError(field),
179
+ description: /*#__PURE__*/ _jsxs("span", {
180
+ className: "break-all font-code",
181
+ children: [
182
+ "/w/",
183
+ workspace.slug,
184
+ "/p/",
185
+ field.state.value
186
+ ]
187
+ }),
188
+ placeholder: "platform",
189
+ className: "font-code",
190
+ currentSlug: project.slug,
191
+ checkEnabled: true,
192
+ debounceMs: 0,
193
+ isValid: isSlugValid,
194
+ checkAvailability: checkProjectSlugAvailability
195
+ })
192
196
  }),
193
- placeholder: "platform",
194
- className: "font-code",
195
- currentSlug: project.slug,
196
- checkEnabled: true,
197
- debounceMs: 0,
198
- isValid: isSlugValid,
199
- checkAvailability: checkProjectSlugAvailability
200
- })
201
- }),
202
- /*#__PURE__*/ _jsx(Button, {
203
- type: "submit",
204
- isLoading: updateProject.isPending,
205
- className: "self-start",
206
- children: "Save changes"
207
- })
208
- ]
197
+ /*#__PURE__*/ _jsx(Button, {
198
+ type: "submit",
199
+ isLoading: updateProject.isPending,
200
+ className: "self-start",
201
+ children: "Save changes"
202
+ })
203
+ ]
204
+ })
205
+ ]
206
+ })
209
207
  })
210
208
  ]
211
209
  }),
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/pages/project-settings-page.tsx"],"sourcesContent":["import {slugSchema} from '@shipfox/api-common-dto';\nimport {createProjectBodySchema} from '@shipfox/api-projects-dto';\nimport {ApiError} from '@shipfox/client-api';\nimport {useActiveWorkspace} from '@shipfox/client-auth';\nimport {\n displayNameFieldError,\n QueryLoadError,\n SlugChangeWarning,\n SlugField,\n} from '@shipfox/client-ui';\nimport {Button} from '@shipfox/react-ui/button';\nimport {Callout} from '@shipfox/react-ui/callout';\nimport {EmptyState} from '@shipfox/react-ui/empty-state';\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 {useNavigate} from '@tanstack/react-router';\nimport {useState} from 'react';\nimport {useMaybeActiveProjectQuery} from '#chrome.js';\nimport type {Project} from '#core/project.js';\nimport {useProjectSlugAvailability, useUpdateProjectMutation} from '#hooks/api/projects.js';\nimport {projectSettingsErrorToFormError} from './project-settings-form-errors.js';\n\ninterface ProjectSettingsValues {\n name: string;\n slug: string;\n}\n\nfunction isSlugValid(value: string): boolean {\n return slugSchema.safeParse(value).success;\n}\n\nexport function ProjectSettingsPage() {\n const projectQuery = useMaybeActiveProjectQuery();\n if (projectQuery.isPending) return <FullPageLoader />;\n if (projectQuery.isError && projectQuery.data === undefined) {\n if (projectQuery.error instanceof ApiError && projectQuery.error.status === 404) {\n return (\n <EmptyState\n icon=\"errorWarningLine\"\n tone=\"error\"\n title=\"Project not found\"\n description=\"This project doesn't exist, or you don't have access to it.\"\n />\n );\n }\n return <QueryLoadError query={projectQuery} subject=\"project\" />;\n }\n const project = projectQuery.data;\n if (!project) {\n return (\n <EmptyState\n icon=\"errorWarningLine\"\n tone=\"error\"\n title=\"Project not found\"\n description=\"This project doesn't exist, or you don't have access to it.\"\n />\n );\n }\n return (\n <ProjectSettingsForm key={`${project.id}:${project.name}:${project.slug}`} project={project} />\n );\n}\n\nfunction ProjectSettingsForm({project}: {project: Project}) {\n const workspace = useActiveWorkspace();\n const navigate = useNavigate();\n const updateProject = useUpdateProjectMutation();\n const [warningOpen, setWarningOpen] = useState(false);\n const [pendingValues, setPendingValues] = useState<ProjectSettingsValues>();\n const [formError, setFormError] = useState<string>();\n\n const checkProjectSlugAvailability = useProjectSlugAvailability(workspace.id, project.id);\n\n const form = useForm({\n defaultValues: {name: project.name, slug: project.slug},\n onSubmit: async ({value}) => {\n if (value.slug !== project.slug) {\n setPendingValues(value);\n setWarningOpen(true);\n return;\n }\n await save(value);\n },\n });\n\n async function save(values: ProjectSettingsValues) {\n setFormError(undefined);\n const nameChanged = values.name !== project.name;\n const slugChanged = values.slug !== project.slug;\n if (!nameChanged && !slugChanged) return;\n const command = {\n projectId: project.id,\n ...(nameChanged ? {name: values.name} : {}),\n ...(slugChanged ? {slug: values.slug} : {}),\n };\n\n try {\n const updated = await updateProject.mutateAsync(command);\n setPendingValues(undefined);\n setWarningOpen(false);\n toast.success('Project settings saved.');\n await navigate({\n to: '/w/$workspaceSlug/p/$projectSlug/settings/general',\n params: {workspaceSlug: workspace.slug, projectSlug: updated.slug},\n });\n } catch (error) {\n setPendingValues(undefined);\n setWarningOpen(false);\n const mapped = projectSettingsErrorToFormError(error);\n if (mapped.kind === 'field') {\n form.setFieldMeta(mapped.field, (previous) => ({\n ...previous,\n errorMap: {...previous.errorMap, onServer: mapped.message},\n }));\n } else {\n setFormError(mapped.message);\n }\n }\n }\n\n return (\n <>\n <div className=\"flex min-w-0 flex-col gap-section\">\n <header className=\"flex flex-col gap-inline\">\n <Header variant=\"h1\">General</Header>\n <Text size=\"sm\" className=\"text-foreground-neutral-muted\">\n Update the project name and the slug used in its URLs.\n </Text>\n </header>\n\n {formError ? (\n <Callout role=\"alert\" type=\"error\">\n {formError}\n </Callout>\n ) : null}\n\n <form\n className=\"flex max-w-[560px] flex-col gap-group\"\n noValidate\n onSubmit={(event) => {\n event.preventDefault();\n event.stopPropagation();\n void form.handleSubmit();\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-settings-name\" error={fieldError(field)}>\n <FormFieldInput\n name=\"name\"\n type=\"text\"\n value={field.state.value}\n onChange={(event) => field.handleChange(event.target.value)}\n onBlur={field.handleBlur}\n />\n </FormField>\n )}\n </form.Field>\n\n <form.Field\n name=\"slug\"\n validators={{\n onBlur: createProjectBodySchema.shape.slug,\n onSubmit: createProjectBodySchema.shape.slug,\n }}\n >\n {(field) => (\n <SlugField\n id=\"project-settings-slug\"\n label=\"Project slug\"\n name=\"slug\"\n value={field.state.value}\n onChange={(value) => field.handleChange(value)}\n onBlur={field.handleBlur}\n error={fieldError(field)}\n description={\n <span className=\"break-all font-code\">\n /w/{workspace.slug}/p/{field.state.value}\n </span>\n }\n placeholder=\"platform\"\n className=\"font-code\"\n currentSlug={project.slug}\n checkEnabled\n debounceMs={0}\n isValid={isSlugValid}\n checkAvailability={checkProjectSlugAvailability}\n />\n )}\n </form.Field>\n\n <Button type=\"submit\" isLoading={updateProject.isPending} className=\"self-start\">\n Save changes\n </Button>\n </form>\n </div>\n\n <SlugChangeWarning\n open={warningOpen}\n onOpenChange={setWarningOpen}\n entityLabel=\"project\"\n isLoading={updateProject.isPending}\n onConfirm={() => {\n if (pendingValues) void save(pendingValues);\n }}\n />\n </>\n );\n}\n"],"names":["slugSchema","createProjectBodySchema","ApiError","useActiveWorkspace","displayNameFieldError","QueryLoadError","SlugChangeWarning","SlugField","Button","Callout","EmptyState","FormField","FormFieldInput","fieldError","FullPageLoader","toast","Header","Text","useForm","useNavigate","useState","useMaybeActiveProjectQuery","useProjectSlugAvailability","useUpdateProjectMutation","projectSettingsErrorToFormError","isSlugValid","value","safeParse","success","ProjectSettingsPage","projectQuery","isPending","isError","data","undefined","error","status","icon","tone","title","description","query","subject","project","ProjectSettingsForm","id","name","slug","workspace","navigate","updateProject","warningOpen","setWarningOpen","pendingValues","setPendingValues","formError","setFormError","checkProjectSlugAvailability","form","defaultValues","onSubmit","save","values","nameChanged","slugChanged","command","projectId","updated","mutateAsync","to","params","workspaceSlug","projectSlug","mapped","kind","setFieldMeta","field","previous","errorMap","onServer","message","div","className","header","variant","size","role","type","noValidate","event","preventDefault","stopPropagation","handleSubmit","Field","validators","onBlur","shape","label","state","onChange","handleChange","target","handleBlur","span","placeholder","currentSlug","checkEnabled","debounceMs","isValid","checkAvailability","isLoading","open","onOpenChange","entityLabel","onConfirm"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/pages/project-settings-page.tsx"],"sourcesContent":["import {slugSchema} from '@shipfox/api-common-dto';\nimport {createProjectBodySchema} from '@shipfox/api-projects-dto';\nimport {ApiError} from '@shipfox/client-api';\nimport {useActiveWorkspace} from '@shipfox/client-auth';\nimport {\n displayNameFieldError,\n QueryLoadError,\n SlugChangeWarning,\n SlugField,\n} from '@shipfox/client-ui';\nimport {Button} from '@shipfox/react-ui/button';\nimport {Callout} from '@shipfox/react-ui/callout';\nimport {EmptyState} from '@shipfox/react-ui/empty-state';\nimport {FormField, FormFieldInput, fieldError} from '@shipfox/react-ui/form-field';\nimport {FullPageLoader} from '@shipfox/react-ui/loader';\nimport {Panel, PanelBody} from '@shipfox/react-ui/panel';\nimport {toast} from '@shipfox/react-ui/toast';\nimport {Header} from '@shipfox/react-ui/typography';\nimport {useForm} from '@tanstack/react-form';\nimport {useNavigate} from '@tanstack/react-router';\nimport {useState} from 'react';\nimport {useMaybeActiveProjectQuery} from '#chrome.js';\nimport type {Project} from '#core/project.js';\nimport {useProjectSlugAvailability, useUpdateProjectMutation} from '#hooks/api/projects.js';\nimport {projectSettingsErrorToFormError} from './project-settings-form-errors.js';\n\ninterface ProjectSettingsValues {\n name: string;\n slug: string;\n}\n\nfunction isSlugValid(value: string): boolean {\n return slugSchema.safeParse(value).success;\n}\n\nexport function ProjectSettingsPage() {\n const projectQuery = useMaybeActiveProjectQuery();\n if (projectQuery.isPending) return <FullPageLoader />;\n if (projectQuery.isError && projectQuery.data === undefined) {\n if (projectQuery.error instanceof ApiError && projectQuery.error.status === 404) {\n return (\n <EmptyState\n icon=\"errorWarningLine\"\n tone=\"error\"\n title=\"Project not found\"\n description=\"This project doesn't exist, or you don't have access to it.\"\n />\n );\n }\n return <QueryLoadError query={projectQuery} subject=\"project\" />;\n }\n const project = projectQuery.data;\n if (!project) {\n return (\n <EmptyState\n icon=\"errorWarningLine\"\n tone=\"error\"\n title=\"Project not found\"\n description=\"This project doesn't exist, or you don't have access to it.\"\n />\n );\n }\n return (\n <ProjectSettingsForm key={`${project.id}:${project.name}:${project.slug}`} project={project} />\n );\n}\n\nfunction ProjectSettingsForm({project}: {project: Project}) {\n const workspace = useActiveWorkspace();\n const navigate = useNavigate();\n const updateProject = useUpdateProjectMutation();\n const [warningOpen, setWarningOpen] = useState(false);\n const [pendingValues, setPendingValues] = useState<ProjectSettingsValues>();\n const [formError, setFormError] = useState<string>();\n\n const checkProjectSlugAvailability = useProjectSlugAvailability(workspace.id, project.id);\n\n const form = useForm({\n defaultValues: {name: project.name, slug: project.slug},\n onSubmit: async ({value}) => {\n if (value.slug !== project.slug) {\n setPendingValues(value);\n setWarningOpen(true);\n return;\n }\n await save(value);\n },\n });\n\n async function save(values: ProjectSettingsValues) {\n setFormError(undefined);\n const nameChanged = values.name !== project.name;\n const slugChanged = values.slug !== project.slug;\n if (!nameChanged && !slugChanged) return;\n const command = {\n projectId: project.id,\n ...(nameChanged ? {name: values.name} : {}),\n ...(slugChanged ? {slug: values.slug} : {}),\n };\n\n try {\n const updated = await updateProject.mutateAsync(command);\n setPendingValues(undefined);\n setWarningOpen(false);\n toast.success('Project settings saved.');\n await navigate({\n to: '/w/$workspaceSlug/p/$projectSlug/settings/general',\n params: {workspaceSlug: workspace.slug, projectSlug: updated.slug},\n });\n } catch (error) {\n setPendingValues(undefined);\n setWarningOpen(false);\n const mapped = projectSettingsErrorToFormError(error);\n if (mapped.kind === 'field') {\n form.setFieldMeta(mapped.field, (previous) => ({\n ...previous,\n errorMap: {...previous.errorMap, onServer: mapped.message},\n }));\n } else {\n setFormError(mapped.message);\n }\n }\n }\n\n return (\n <>\n <div className=\"flex min-w-0 flex-col gap-section\">\n <Header variant=\"h1\">General</Header>\n\n <Panel>\n <PanelBody className=\"gap-group p-panel\">\n {formError ? (\n <Callout role=\"alert\" type=\"error\">\n {formError}\n </Callout>\n ) : null}\n\n <form\n className=\"flex w-full max-w-[560px] flex-col gap-group\"\n noValidate\n onSubmit={(event) => {\n event.preventDefault();\n event.stopPropagation();\n void form.handleSubmit();\n }}\n >\n <form.Field\n name=\"name\"\n validators={{\n onBlur: ({value}) =>\n displayNameFieldError(\n value,\n 'Project name',\n createProjectBodySchema.shape.name,\n ),\n onSubmit: ({value}) =>\n displayNameFieldError(\n value,\n 'Project name',\n createProjectBodySchema.shape.name,\n ),\n }}\n >\n {(field) => (\n <FormField\n label=\"Project name\"\n id=\"project-settings-name\"\n error={fieldError(field)}\n >\n <FormFieldInput\n name=\"name\"\n type=\"text\"\n value={field.state.value}\n onChange={(event) => field.handleChange(event.target.value)}\n onBlur={field.handleBlur}\n />\n </FormField>\n )}\n </form.Field>\n\n <form.Field\n name=\"slug\"\n validators={{\n onBlur: createProjectBodySchema.shape.slug,\n onSubmit: createProjectBodySchema.shape.slug,\n }}\n >\n {(field) => (\n <SlugField\n id=\"project-settings-slug\"\n label=\"Project slug\"\n name=\"slug\"\n value={field.state.value}\n onChange={(value) => field.handleChange(value)}\n onBlur={field.handleBlur}\n error={fieldError(field)}\n description={\n <span className=\"break-all font-code\">\n /w/{workspace.slug}/p/{field.state.value}\n </span>\n }\n placeholder=\"platform\"\n className=\"font-code\"\n currentSlug={project.slug}\n checkEnabled\n debounceMs={0}\n isValid={isSlugValid}\n checkAvailability={checkProjectSlugAvailability}\n />\n )}\n </form.Field>\n\n <Button type=\"submit\" isLoading={updateProject.isPending} className=\"self-start\">\n Save changes\n </Button>\n </form>\n </PanelBody>\n </Panel>\n </div>\n\n <SlugChangeWarning\n open={warningOpen}\n onOpenChange={setWarningOpen}\n entityLabel=\"project\"\n isLoading={updateProject.isPending}\n onConfirm={() => {\n if (pendingValues) void save(pendingValues);\n }}\n />\n </>\n );\n}\n"],"names":["slugSchema","createProjectBodySchema","ApiError","useActiveWorkspace","displayNameFieldError","QueryLoadError","SlugChangeWarning","SlugField","Button","Callout","EmptyState","FormField","FormFieldInput","fieldError","FullPageLoader","Panel","PanelBody","toast","Header","useForm","useNavigate","useState","useMaybeActiveProjectQuery","useProjectSlugAvailability","useUpdateProjectMutation","projectSettingsErrorToFormError","isSlugValid","value","safeParse","success","ProjectSettingsPage","projectQuery","isPending","isError","data","undefined","error","status","icon","tone","title","description","query","subject","project","ProjectSettingsForm","id","name","slug","workspace","navigate","updateProject","warningOpen","setWarningOpen","pendingValues","setPendingValues","formError","setFormError","checkProjectSlugAvailability","form","defaultValues","onSubmit","save","values","nameChanged","slugChanged","command","projectId","updated","mutateAsync","to","params","workspaceSlug","projectSlug","mapped","kind","setFieldMeta","field","previous","errorMap","onServer","message","div","className","variant","role","type","noValidate","event","preventDefault","stopPropagation","handleSubmit","Field","validators","onBlur","shape","label","state","onChange","handleChange","target","handleBlur","span","placeholder","currentSlug","checkEnabled","debounceMs","isValid","checkAvailability","isLoading","open","onOpenChange","entityLabel","onConfirm"],"mappings":"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"}
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"projects-hub-page.d.ts","sourceRoot":"","sources":["../../src/pages/projects-hub-page.tsx"],"names":[],"mappings":"AA6BA,wBAAgB,eAAe,CAAC,EAAC,MAAW,EAAC,EAAE;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAC,+BAwH/D"}