@shipfox/client-projects 12.0.2 → 13.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 (111) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +49 -0
  3. package/dist/chrome.d.ts +5 -1
  4. package/dist/chrome.d.ts.map +1 -1
  5. package/dist/chrome.js +18 -26
  6. package/dist/chrome.js.map +1 -1
  7. package/dist/components/model-provider-reminder-banner.d.ts.map +1 -1
  8. package/dist/components/model-provider-reminder-banner.js +4 -2
  9. package/dist/components/model-provider-reminder-banner.js.map +1 -1
  10. package/dist/components/project-crumb.d.ts +3 -1
  11. package/dist/components/project-crumb.d.ts.map +1 -1
  12. package/dist/components/project-crumb.js +7 -5
  13. package/dist/components/project-crumb.js.map +1 -1
  14. package/dist/components/project-switcher.d.ts +2 -1
  15. package/dist/components/project-switcher.d.ts.map +1 -1
  16. package/dist/components/project-switcher.js +10 -8
  17. package/dist/components/project-switcher.js.map +1 -1
  18. package/dist/core/definition.d.ts +6 -0
  19. package/dist/core/definition.d.ts.map +1 -1
  20. package/dist/core/definition.js.map +1 -1
  21. package/dist/core/project.d.ts +7 -0
  22. package/dist/core/project.d.ts.map +1 -1
  23. package/dist/core/project.js.map +1 -1
  24. package/dist/feature.d.ts +29 -5
  25. package/dist/feature.d.ts.map +1 -1
  26. package/dist/feature.js +26 -5
  27. package/dist/feature.js.map +1 -1
  28. package/dist/hooks/api/mappers.d.ts.map +1 -1
  29. package/dist/hooks/api/mappers.js +9 -1
  30. package/dist/hooks/api/mappers.js.map +1 -1
  31. package/dist/hooks/api/projects.d.ts +21 -2
  32. package/dist/hooks/api/projects.d.ts.map +1 -1
  33. package/dist/hooks/api/projects.js +175 -2
  34. package/dist/hooks/api/projects.js.map +1 -1
  35. package/dist/index.d.ts +3 -2
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +2 -1
  38. package/dist/index.js.map +1 -1
  39. package/dist/pages/create-project-page.d.ts.map +1 -1
  40. package/dist/pages/create-project-page.js +97 -21
  41. package/dist/pages/create-project-page.js.map +1 -1
  42. package/dist/pages/create-project-page.stories.js +5 -3
  43. package/dist/pages/create-project-page.stories.js.map +1 -1
  44. package/dist/pages/project-settings-form-errors.d.ts +11 -0
  45. package/dist/pages/project-settings-form-errors.d.ts.map +1 -0
  46. package/dist/pages/project-settings-form-errors.js +16 -0
  47. package/dist/pages/project-settings-form-errors.js.map +1 -0
  48. package/dist/pages/project-settings-page.d.ts +2 -0
  49. package/dist/pages/project-settings-page.d.ts.map +1 -0
  50. package/dist/pages/project-settings-page.js +225 -0
  51. package/dist/pages/project-settings-page.js.map +1 -0
  52. package/dist/pages/project-settings-page.stories.d.ts +23 -0
  53. package/dist/pages/project-settings-page.stories.d.ts.map +1 -0
  54. package/dist/pages/project-settings-page.stories.js +206 -0
  55. package/dist/pages/project-settings-page.stories.js.map +1 -0
  56. package/dist/pages/projects-hub-page.js +11 -11
  57. package/dist/pages/projects-hub-page.js.map +1 -1
  58. package/dist/project-error.d.ts +1 -0
  59. package/dist/project-error.d.ts.map +1 -1
  60. package/dist/project-error.js +7 -0
  61. package/dist/project-error.js.map +1 -1
  62. package/dist/routes/inputs.d.ts +2 -2
  63. package/dist/routes/inputs.d.ts.map +1 -1
  64. package/dist/routes/inputs.js +5 -5
  65. package/dist/routes/inputs.js.map +1 -1
  66. package/dist/routes/project-index.d.ts +2 -2
  67. package/dist/routes/project-index.d.ts.map +1 -1
  68. package/dist/routes/project-index.js +3 -3
  69. package/dist/routes/project-index.js.map +1 -1
  70. package/dist/routes/project-settings-index.d.ts +10 -0
  71. package/dist/routes/project-settings-index.d.ts.map +1 -0
  72. package/dist/routes/project-settings-index.js +12 -0
  73. package/dist/routes/project-settings-index.js.map +1 -0
  74. package/dist/routes/project-settings.d.ts +6 -0
  75. package/dist/routes/project-settings.d.ts.map +1 -0
  76. package/dist/routes/project-settings.js +7 -0
  77. package/dist/routes/project-settings.js.map +1 -0
  78. package/dist/tsconfig.test.tsbuildinfo +1 -1
  79. package/package.json +11 -10
  80. package/src/chrome.tsx +27 -19
  81. package/src/components/model-provider-reminder-banner.tsx +3 -1
  82. package/src/components/project-crumb.tsx +18 -5
  83. package/src/components/project-switcher.test.tsx +7 -4
  84. package/src/components/project-switcher.tsx +16 -5
  85. package/src/core/definition.ts +7 -0
  86. package/src/core/project.ts +8 -0
  87. package/src/feature.ts +26 -4
  88. package/src/hooks/api/mappers.test.ts +19 -0
  89. package/src/hooks/api/mappers.ts +6 -0
  90. package/src/hooks/api/projects.test.ts +247 -1
  91. package/src/hooks/api/projects.ts +227 -2
  92. package/src/index.ts +9 -1
  93. package/src/pages/create-project-page.stories.tsx +5 -4
  94. package/src/pages/create-project-page.test.tsx +191 -12
  95. package/src/pages/create-project-page.tsx +102 -15
  96. package/src/pages/home-router.test.tsx +2 -2
  97. package/src/pages/project-settings-form-errors.ts +17 -0
  98. package/src/pages/project-settings-page.stories.tsx +216 -0
  99. package/src/pages/project-settings-page.test.tsx +74 -0
  100. package/src/pages/project-settings-page.tsx +220 -0
  101. package/src/pages/projects-hub-page.test.tsx +17 -11
  102. package/src/pages/projects-hub-page.tsx +9 -9
  103. package/src/project-error.test.ts +1 -0
  104. package/src/project-error.ts +8 -0
  105. package/src/routes/inputs.test.ts +8 -6
  106. package/src/routes/inputs.ts +7 -6
  107. package/src/routes/project-index.tsx +3 -3
  108. package/src/routes/project-settings-index.tsx +11 -0
  109. package/src/routes/project-settings.tsx +4 -0
  110. package/test/pages.tsx +26 -22
  111. package/tsconfig.build.tsbuildinfo +1 -1
@@ -0,0 +1,225 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { slugSchema } from '@shipfox/api-common-dto';
3
+ import { createProjectBodySchema } from '@shipfox/api-projects-dto';
4
+ import { ApiError } from '@shipfox/client-api';
5
+ import { useActiveWorkspace } from '@shipfox/client-auth';
6
+ import { displayNameFieldError, QueryLoadError, SlugChangeWarning, SlugField } from '@shipfox/client-ui';
7
+ import { Button } from '@shipfox/react-ui/button';
8
+ import { Callout } from '@shipfox/react-ui/callout';
9
+ import { EmptyState } from '@shipfox/react-ui/empty-state';
10
+ import { FormField, FormFieldInput, fieldError } from '@shipfox/react-ui/form-field';
11
+ import { FullPageLoader } from '@shipfox/react-ui/loader';
12
+ import { toast } from '@shipfox/react-ui/toast';
13
+ import { Header, Text } from '@shipfox/react-ui/typography';
14
+ import { useForm } from '@tanstack/react-form';
15
+ import { useNavigate } from '@tanstack/react-router';
16
+ import { useState } from 'react';
17
+ import { useMaybeActiveProjectQuery } from '#chrome.js';
18
+ import { useProjectSlugAvailability, useUpdateProjectMutation } from '#hooks/api/projects.js';
19
+ import { projectSettingsErrorToFormError } from './project-settings-form-errors.js';
20
+ function isSlugValid(value) {
21
+ return slugSchema.safeParse(value).success;
22
+ }
23
+ export function ProjectSettingsPage() {
24
+ const projectQuery = useMaybeActiveProjectQuery();
25
+ if (projectQuery.isPending) return /*#__PURE__*/ _jsx(FullPageLoader, {});
26
+ if (projectQuery.isError && projectQuery.data === undefined) {
27
+ if (projectQuery.error instanceof ApiError && projectQuery.error.status === 404) {
28
+ return /*#__PURE__*/ _jsx(EmptyState, {
29
+ icon: "errorWarningLine",
30
+ tone: "error",
31
+ title: "Project not found",
32
+ description: "This project doesn't exist, or you don't have access to it."
33
+ });
34
+ }
35
+ return /*#__PURE__*/ _jsx(QueryLoadError, {
36
+ query: projectQuery,
37
+ subject: "project"
38
+ });
39
+ }
40
+ const project = projectQuery.data;
41
+ if (!project) {
42
+ return /*#__PURE__*/ _jsx(EmptyState, {
43
+ icon: "errorWarningLine",
44
+ tone: "error",
45
+ title: "Project not found",
46
+ description: "This project doesn't exist, or you don't have access to it."
47
+ });
48
+ }
49
+ return /*#__PURE__*/ _jsx(ProjectSettingsForm, {
50
+ project: project
51
+ }, `${project.id}:${project.name}:${project.slug}`);
52
+ }
53
+ function ProjectSettingsForm({ project }) {
54
+ const workspace = useActiveWorkspace();
55
+ const navigate = useNavigate();
56
+ const updateProject = useUpdateProjectMutation();
57
+ const [warningOpen, setWarningOpen] = useState(false);
58
+ const [pendingValues, setPendingValues] = useState();
59
+ const [formError, setFormError] = useState();
60
+ const checkProjectSlugAvailability = useProjectSlugAvailability(workspace.id, project.id);
61
+ const form = useForm({
62
+ defaultValues: {
63
+ name: project.name,
64
+ slug: project.slug
65
+ },
66
+ onSubmit: async ({ value })=>{
67
+ if (value.slug !== project.slug) {
68
+ setPendingValues(value);
69
+ setWarningOpen(true);
70
+ return;
71
+ }
72
+ await save(value);
73
+ }
74
+ });
75
+ async function save(values) {
76
+ setFormError(undefined);
77
+ const nameChanged = values.name !== project.name;
78
+ const slugChanged = values.slug !== project.slug;
79
+ if (!nameChanged && !slugChanged) return;
80
+ const command = {
81
+ projectId: project.id,
82
+ ...nameChanged ? {
83
+ name: values.name
84
+ } : {},
85
+ ...slugChanged ? {
86
+ slug: values.slug
87
+ } : {}
88
+ };
89
+ try {
90
+ const updated = await updateProject.mutateAsync(command);
91
+ setPendingValues(undefined);
92
+ setWarningOpen(false);
93
+ toast.success('Project settings saved.');
94
+ await navigate({
95
+ to: '/w/$workspaceSlug/p/$projectSlug/settings/general',
96
+ params: {
97
+ workspaceSlug: workspace.slug,
98
+ projectSlug: updated.slug
99
+ }
100
+ });
101
+ } catch (error) {
102
+ setPendingValues(undefined);
103
+ setWarningOpen(false);
104
+ const mapped = projectSettingsErrorToFormError(error);
105
+ if (mapped.kind === 'field') {
106
+ form.setFieldMeta(mapped.field, (previous)=>({
107
+ ...previous,
108
+ errorMap: {
109
+ ...previous.errorMap,
110
+ onServer: mapped.message
111
+ }
112
+ }));
113
+ } else {
114
+ setFormError(mapped.message);
115
+ }
116
+ }
117
+ }
118
+ return /*#__PURE__*/ _jsxs(_Fragment, {
119
+ children: [
120
+ /*#__PURE__*/ _jsxs("div", {
121
+ className: "flex min-w-0 flex-col gap-24",
122
+ children: [
123
+ /*#__PURE__*/ _jsxs("header", {
124
+ className: "flex flex-col gap-6",
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
+ ]
136
+ }),
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-16",
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, {
162
+ 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
+ ]
192
+ }),
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
+ ]
209
+ })
210
+ ]
211
+ }),
212
+ /*#__PURE__*/ _jsx(SlugChangeWarning, {
213
+ open: warningOpen,
214
+ onOpenChange: setWarningOpen,
215
+ entityLabel: "project",
216
+ isLoading: updateProject.isPending,
217
+ onConfirm: ()=>{
218
+ if (pendingValues) void save(pendingValues);
219
+ }
220
+ })
221
+ ]
222
+ });
223
+ }
224
+
225
+ //# sourceMappingURL=project-settings-page.js.map
@@ -0,0 +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-24\">\n <header className=\"flex flex-col gap-6\">\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-16\"\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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"}
@@ -0,0 +1,23 @@
1
+ import type { StoryObj } from '@storybook/react';
2
+ type Scenario = 'default' | 'long-values' | 'taken-slug';
3
+ interface ProjectSettingsPageStoryProps {
4
+ scenario: Scenario;
5
+ }
6
+ declare function ProjectSettingsPageStory({ scenario }: ProjectSettingsPageStoryProps): import("react").JSX.Element;
7
+ declare const meta: {
8
+ title: string;
9
+ component: typeof ProjectSettingsPageStory;
10
+ parameters: {
11
+ layout: string;
12
+ };
13
+ args: {
14
+ scenario: "default";
15
+ };
16
+ };
17
+ export default meta;
18
+ type Story = StoryObj<typeof meta>;
19
+ export declare const Playground: Story;
20
+ export declare const LongValues: Story;
21
+ export declare const TakenSlug: Story;
22
+ export declare const SlugChangeWarning: Story;
23
+ //# sourceMappingURL=project-settings-page.stories.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-settings-page.stories.d.ts","sourceRoot":"","sources":["../../src/pages/project-settings-page.stories.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAO,QAAQ,EAAC,MAAM,kBAAkB,CAAC;AAmBrD,KAAK,QAAQ,GAAG,SAAS,GAAG,aAAa,GAAG,YAAY,CAAC;AAezD,UAAU,6BAA6B;IACrC,QAAQ,EAAE,QAAQ,CAAC;CACpB;AAcD,iBAAS,wBAAwB,CAAC,EAAC,QAAQ,EAAC,EAAE,6BAA6B,+BA6B1E;AAED,QAAA,MAAM,IAAI;;;;;;;;;CAKuC,CAAC;AAElD,eAAe,IAAI,CAAC;AACpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAEnC,eAAO,MAAM,UAAU,EAAE,KAAU,CAAC;AAEpC,eAAO,MAAM,UAAU,EAAE,KAExB,CAAC;AAEF,eAAO,MAAM,SAAS,EAAE,KAUvB,CAAC;AAEF,eAAO,MAAM,iBAAiB,EAAE,KAgB/B,CAAC"}
@@ -0,0 +1,206 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { configureApiClient } from '@shipfox/client-api';
3
+ import { authStateAtom } from '@shipfox/client-auth';
4
+ import { Toaster } from '@shipfox/react-ui/toast';
5
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
6
+ import { createMemoryHistory, createRootRoute, createRoute, createRouter, Outlet, RouterProvider } from '@tanstack/react-router';
7
+ import { createStore, Provider as JotaiProvider } from 'jotai';
8
+ import { useMemo } from 'react';
9
+ import { expect, screen, userEvent, within } from 'storybook/test';
10
+ import { ProjectSettingsPage } from './project-settings-page.js';
11
+ const WORKSPACE_ID = '11111111-1111-4111-8111-111111111111';
12
+ const PROJECT_ID = '99999999-9999-4999-8999-999999999999';
13
+ const OTHER_PROJECT_ID = '88888888-8888-4888-8888-888888888888';
14
+ const authState = {
15
+ status: 'authenticated',
16
+ token: 'token',
17
+ user: {
18
+ id: '22222222-2222-4222-8222-222222222222',
19
+ email: 'platform@example.com',
20
+ name: 'Platform Engineer',
21
+ emailVerifiedAt: '2026-01-01T00:00:00.000Z'
22
+ },
23
+ workspaces: [
24
+ {
25
+ id: WORKSPACE_ID,
26
+ name: 'Acme',
27
+ slug: 'acme',
28
+ membershipId: 'membership-1'
29
+ }
30
+ ]
31
+ };
32
+ function ProjectSettingsPageStory({ scenario }) {
33
+ configureApiClient({
34
+ baseUrl: 'https://api.example.test',
35
+ fetchImpl: fetchForScenario(scenario)
36
+ });
37
+ const queryClient = useMemo(()=>new QueryClient({
38
+ defaultOptions: {
39
+ queries: {
40
+ retry: false
41
+ }
42
+ }
43
+ }), []);
44
+ const store = useMemo(()=>{
45
+ const nextStore = createStore();
46
+ nextStore.set(authStateAtom, authState);
47
+ return nextStore;
48
+ }, []);
49
+ const router = useMemo(()=>createStoryRouter(scenario), [
50
+ scenario
51
+ ]);
52
+ return /*#__PURE__*/ _jsx(QueryClientProvider, {
53
+ client: queryClient,
54
+ children: /*#__PURE__*/ _jsxs(JotaiProvider, {
55
+ store: store,
56
+ children: [
57
+ /*#__PURE__*/ _jsx("div", {
58
+ className: "min-h-screen bg-background-neutral-background px-24 py-32",
59
+ children: /*#__PURE__*/ _jsx("div", {
60
+ className: "mx-auto w-full max-w-[1120px]",
61
+ children: /*#__PURE__*/ _jsx(RouterProvider, {
62
+ router: router
63
+ })
64
+ })
65
+ }),
66
+ /*#__PURE__*/ _jsx(Toaster, {})
67
+ ]
68
+ })
69
+ });
70
+ }
71
+ const meta = {
72
+ title: 'Projects/ProjectSettingsPage',
73
+ component: ProjectSettingsPageStory,
74
+ parameters: {
75
+ layout: 'fullscreen'
76
+ },
77
+ args: {
78
+ scenario: 'default'
79
+ }
80
+ };
81
+ export default meta;
82
+ export const Playground = {};
83
+ export const LongValues = {
84
+ args: {
85
+ scenario: 'long-values'
86
+ }
87
+ };
88
+ export const TakenSlug = {
89
+ args: {
90
+ scenario: 'taken-slug'
91
+ },
92
+ play: async ({ canvasElement })=>{
93
+ const canvas = within(canvasElement);
94
+ const slugInput = await canvas.findByLabelText('Project slug');
95
+ await userEvent.clear(slugInput);
96
+ await userEvent.type(slugInput, 'taken-platform');
97
+ expect(await canvas.findByText('This slug is already taken.')).toBeInTheDocument();
98
+ }
99
+ };
100
+ export const SlugChangeWarning = {
101
+ play: async ({ canvasElement })=>{
102
+ const canvas = within(canvasElement);
103
+ const slugInput = await canvas.findByLabelText('Project slug');
104
+ await userEvent.clear(slugInput);
105
+ await userEvent.type(slugInput, 'renamed-platform');
106
+ await canvas.findByText('Slug is available.');
107
+ await userEvent.click(canvas.getByRole('button', {
108
+ name: 'Save changes'
109
+ }));
110
+ const dialog = await screen.findByRole('dialog');
111
+ expect(dialog).toHaveTextContent('Change project slug?');
112
+ expect(dialog).toHaveTextContent('Links and bookmarks pointing at the old URL stop working.');
113
+ expect(dialog).toHaveTextContent('Workflows that reference this project slug may stop working.');
114
+ }
115
+ };
116
+ function createStoryRouter(scenario) {
117
+ const project = projectForScenario(scenario);
118
+ const rootRoute = createRootRoute({
119
+ component: Outlet
120
+ });
121
+ const projectSettingsRoute = createRoute({
122
+ getParentRoute: ()=>rootRoute,
123
+ path: '/w/$workspaceSlug/p/$projectSlug/settings/general',
124
+ component: ProjectSettingsPage
125
+ });
126
+ return createRouter({
127
+ history: createMemoryHistory({
128
+ initialEntries: [
129
+ `/w/acme/p/${project.slug}/settings/general`
130
+ ]
131
+ }),
132
+ routeTree: rootRoute.addChildren([
133
+ projectSettingsRoute
134
+ ])
135
+ });
136
+ }
137
+ function fetchForScenario(scenario) {
138
+ return async (input, init)=>{
139
+ const url = requestUrl(input);
140
+ const method = input instanceof Request ? input.method : init?.method ?? 'GET';
141
+ if (url.pathname === '/projects' && method === 'GET') {
142
+ const search = url.searchParams.get('search');
143
+ if (scenario === 'taken-slug' && search === 'taken-platform') {
144
+ return jsonResponse({
145
+ projects: [
146
+ projectForScenario(scenario, {
147
+ id: OTHER_PROJECT_ID,
148
+ slug: 'taken-platform'
149
+ })
150
+ ],
151
+ next_cursor: null
152
+ });
153
+ }
154
+ return jsonResponse({
155
+ projects: [
156
+ projectForScenario(scenario)
157
+ ],
158
+ next_cursor: null
159
+ });
160
+ }
161
+ if (url.pathname === `/projects/${PROJECT_ID}` && method === 'PATCH') {
162
+ const body = await input.clone().json();
163
+ const project = projectForScenario(scenario);
164
+ return jsonResponse({
165
+ ...project,
166
+ name: body.name ?? project.name,
167
+ slug: body.slug ?? project.slug
168
+ });
169
+ }
170
+ return jsonResponse({}, {
171
+ status: 404
172
+ });
173
+ };
174
+ }
175
+ function projectForScenario(scenario, overrides = {}) {
176
+ const project = {
177
+ id: PROJECT_ID,
178
+ workspace_id: WORKSPACE_ID,
179
+ name: scenario === 'long-values' ? 'Infrastructure Control Plane and Workflow Automation' : 'Platform',
180
+ slug: scenario === 'long-values' ? 'infrastructure-control-plane' : 'platform',
181
+ source: {
182
+ connection_id: '33333333-3333-4333-8333-333333333333',
183
+ external_repository_id: 'platform'
184
+ },
185
+ created_at: '2026-01-01T00:00:00.000Z',
186
+ updated_at: '2026-01-01T00:00:00.000Z',
187
+ ...overrides
188
+ };
189
+ return project;
190
+ }
191
+ function requestUrl(input) {
192
+ if (input instanceof Request) return new URL(input.url);
193
+ if (input instanceof URL) return input;
194
+ return new URL(input, 'https://api.example.test');
195
+ }
196
+ function jsonResponse(body, init = {}) {
197
+ return new Response(JSON.stringify(body), {
198
+ status: 200,
199
+ headers: {
200
+ 'content-type': 'application/json'
201
+ },
202
+ ...init
203
+ });
204
+ }
205
+
206
+ //# sourceMappingURL=project-settings-page.stories.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/pages/project-settings-page.stories.tsx"],"sourcesContent":["import {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 {expect, screen, userEvent, within} from 'storybook/test';\nimport {ProjectSettingsPage} from './project-settings-page.js';\n\nconst WORKSPACE_ID = '11111111-1111-4111-8111-111111111111';\nconst PROJECT_ID = '99999999-9999-4999-8999-999999999999';\nconst OTHER_PROJECT_ID = '88888888-8888-4888-8888-888888888888';\n\ntype Scenario = 'default' | 'long-values' | 'taken-slug';\n\ninterface ProjectStoryResponse {\n id: string;\n workspace_id: string;\n name: string;\n slug: string;\n source: {\n connection_id: string;\n external_repository_id: string;\n };\n created_at: string;\n updated_at: string;\n}\n\ninterface ProjectSettingsPageStoryProps {\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', slug: 'acme', membershipId: 'membership-1'}],\n};\n\nfunction ProjectSettingsPageStory({scenario}: ProjectSettingsPageStoryProps) {\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(scenario), [scenario]);\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/ProjectSettingsPage',\n component: ProjectSettingsPageStory,\n parameters: {layout: 'fullscreen'},\n args: {scenario: 'default'},\n} satisfies Meta<typeof ProjectSettingsPageStory>;\n\nexport default meta;\ntype Story = StoryObj<typeof meta>;\n\nexport const Playground: Story = {};\n\nexport const LongValues: Story = {\n args: {scenario: 'long-values'},\n};\n\nexport const TakenSlug: Story = {\n args: {scenario: 'taken-slug'},\n play: async ({canvasElement}) => {\n const canvas = within(canvasElement);\n const slugInput = await canvas.findByLabelText('Project slug');\n await userEvent.clear(slugInput);\n await userEvent.type(slugInput, 'taken-platform');\n\n expect(await canvas.findByText('This slug is already taken.')).toBeInTheDocument();\n },\n};\n\nexport const SlugChangeWarning: Story = {\n play: async ({canvasElement}) => {\n const canvas = within(canvasElement);\n const slugInput = await canvas.findByLabelText('Project slug');\n await userEvent.clear(slugInput);\n await userEvent.type(slugInput, 'renamed-platform');\n await canvas.findByText('Slug is available.');\n await userEvent.click(canvas.getByRole('button', {name: 'Save changes'}));\n\n const dialog = await screen.findByRole('dialog');\n expect(dialog).toHaveTextContent('Change project slug?');\n expect(dialog).toHaveTextContent('Links and bookmarks pointing at the old URL stop working.');\n expect(dialog).toHaveTextContent(\n 'Workflows that reference this project slug may stop working.',\n );\n },\n};\n\nfunction createStoryRouter(scenario: Scenario) {\n const project = projectForScenario(scenario);\n const rootRoute = createRootRoute({component: Outlet});\n const projectSettingsRoute = createRoute({\n getParentRoute: () => rootRoute,\n path: '/w/$workspaceSlug/p/$projectSlug/settings/general',\n component: ProjectSettingsPage,\n });\n\n return createRouter({\n history: createMemoryHistory({\n initialEntries: [`/w/acme/p/${project.slug}/settings/general`],\n }),\n routeTree: rootRoute.addChildren([projectSettingsRoute]),\n });\n}\n\nfunction fetchForScenario(scenario: Scenario): typeof fetch {\n return async (input, init) => {\n const url = requestUrl(input);\n const method = input instanceof Request ? input.method : (init?.method ?? 'GET');\n\n if (url.pathname === '/projects' && method === 'GET') {\n const search = url.searchParams.get('search');\n if (scenario === 'taken-slug' && search === 'taken-platform') {\n return jsonResponse({\n projects: [projectForScenario(scenario, {id: OTHER_PROJECT_ID, slug: 'taken-platform'})],\n next_cursor: null,\n });\n }\n return jsonResponse({projects: [projectForScenario(scenario)], next_cursor: null});\n }\n\n if (url.pathname === `/projects/${PROJECT_ID}` && method === 'PATCH') {\n const body = (await (input as Request).clone().json()) as {\n name?: string;\n slug?: string;\n };\n const project = projectForScenario(scenario);\n return jsonResponse({\n ...project,\n name: body.name ?? project.name,\n slug: body.slug ?? project.slug,\n });\n }\n\n return jsonResponse({}, {status: 404});\n };\n}\n\nfunction projectForScenario(\n scenario: Scenario,\n overrides: Partial<ProjectStoryResponse> = {},\n): ProjectStoryResponse {\n const project = {\n id: PROJECT_ID,\n workspace_id: WORKSPACE_ID,\n name:\n scenario === 'long-values'\n ? 'Infrastructure Control Plane and Workflow Automation'\n : 'Platform',\n slug: scenario === 'long-values' ? 'infrastructure-control-plane' : 'platform',\n source: {\n connection_id: '33333333-3333-4333-8333-333333333333',\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 ...overrides,\n };\n return project;\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"],"names":["configureApiClient","authStateAtom","Toaster","QueryClient","QueryClientProvider","createMemoryHistory","createRootRoute","createRoute","createRouter","Outlet","RouterProvider","createStore","Provider","JotaiProvider","useMemo","expect","screen","userEvent","within","ProjectSettingsPage","WORKSPACE_ID","PROJECT_ID","OTHER_PROJECT_ID","authState","status","token","user","id","email","name","emailVerifiedAt","workspaces","slug","membershipId","ProjectSettingsPageStory","scenario","baseUrl","fetchImpl","fetchForScenario","queryClient","defaultOptions","queries","retry","store","nextStore","set","router","createStoryRouter","client","div","className","meta","title","component","parameters","layout","args","Playground","LongValues","TakenSlug","play","canvasElement","canvas","slugInput","findByLabelText","clear","type","findByText","toBeInTheDocument","SlugChangeWarning","click","getByRole","dialog","findByRole","toHaveTextContent","project","projectForScenario","rootRoute","projectSettingsRoute","getParentRoute","path","history","initialEntries","routeTree","addChildren","input","init","url","requestUrl","method","Request","pathname","search","searchParams","get","jsonResponse","projects","next_cursor","body","clone","json","overrides","workspace_id","source","connection_id","external_repository_id","created_at","updated_at","URL","Response","JSON","stringify","headers"],"mappings":"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"}
@@ -76,9 +76,9 @@ export function ProjectsHubPage({ search = '' }) {
76
76
  iconLeft: "addLine",
77
77
  className: "shrink-0 max-[640px]:w-full",
78
78
  children: /*#__PURE__*/ _jsx(Link, {
79
- to: "/workspaces/$wid/projects/new",
79
+ to: "/w/$workspaceSlug/projects/new",
80
80
  params: {
81
- wid: workspace.id
81
+ workspaceSlug: workspace.slug
82
82
  },
83
83
  children: "New project"
84
84
  })
@@ -96,7 +96,7 @@ export function ProjectsHubPage({ search = '' }) {
96
96
  subject: "projects"
97
97
  }) : null,
98
98
  !isInitialLoading && !query.isError && projects.length === 0 && !search ? /*#__PURE__*/ _jsx(EmptyProjects, {
99
- workspaceId: workspace.id
99
+ workspaceSlug: workspace.slug
100
100
  }) : null,
101
101
  !query.isFetching && !query.isError && projects.length === 0 && search ? /*#__PURE__*/ _jsx(NoSearchResults, {
102
102
  search: search,
@@ -115,7 +115,7 @@ export function ProjectsHubPage({ search = '' }) {
115
115
  connection: connectionsById.get(project.source.connectionId),
116
116
  connectionsResolved: connectionsQuery.isSuccess,
117
117
  connectionsSettled: connectionsQuery.isSuccess || connectionsQuery.isError,
118
- workspaceId: workspace.id
118
+ workspaceSlug: workspace.slug
119
119
  }, project.id))
120
120
  }),
121
121
  query.error && query.data ? /*#__PURE__*/ _jsx(Callout, {
@@ -171,7 +171,7 @@ function ProjectsSkeleton() {
171
171
  }, row))
172
172
  });
173
173
  }
174
- function EmptyProjects({ workspaceId }) {
174
+ function EmptyProjects({ workspaceSlug }) {
175
175
  return /*#__PURE__*/ _jsx(EmptyState, {
176
176
  icon: "folderLine",
177
177
  title: "Create your first project",
@@ -180,9 +180,9 @@ function EmptyProjects({ workspaceId }) {
180
180
  asChild: true,
181
181
  iconRight: "chevronRight",
182
182
  children: /*#__PURE__*/ _jsx(Link, {
183
- to: "/workspaces/$wid/projects/new",
183
+ to: "/w/$workspaceSlug/projects/new",
184
184
  params: {
185
- wid: workspaceId
185
+ workspaceSlug
186
186
  },
187
187
  children: "Create project"
188
188
  })
@@ -202,17 +202,17 @@ function NoSearchResults({ search, onClear }) {
202
202
  })
203
203
  });
204
204
  }
205
- function ProjectCard({ project, connection, connectionsResolved, connectionsSettled, workspaceId }) {
205
+ function ProjectCard({ project, connection, connectionsResolved, connectionsSettled, workspaceSlug }) {
206
206
  // On a resolved fetch, `active` carries no badge while a missing connection
207
207
  // reads as an error so a broken source is still flagged. An unresolved or
208
208
  // failed fetch shows nothing, so a fetch failure never flags every card.
209
209
  const status = connectionsResolved ? connection?.lifecycleStatus ?? 'error' : undefined;
210
210
  return /*#__PURE__*/ _jsx("li", {
211
211
  children: /*#__PURE__*/ _jsx(Link, {
212
- to: "/workspaces/$wid/projects/$pid",
212
+ to: "/w/$workspaceSlug/p/$projectSlug",
213
213
  params: {
214
- wid: workspaceId,
215
- pid: project.id
214
+ workspaceSlug,
215
+ projectSlug: project.slug
216
216
  },
217
217
  className: "block h-full rounded-8 focus-visible:shadow-button-neutral-focus focus-visible:outline-none",
218
218
  children: /*#__PURE__*/ _jsx(Card, {