@shipfox/client-workspace-settings 12.0.2 → 14.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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +41 -0
- package/dist/components/general/form-errors.d.ts +11 -0
- package/dist/components/general/form-errors.d.ts.map +1 -0
- package/dist/components/general/form-errors.js +16 -0
- package/dist/components/general/form-errors.js.map +1 -0
- package/dist/components/members/workspace-members-section.js +2 -2
- package/dist/components/members/workspace-members-section.js.map +1 -1
- package/dist/components/workspace-settings-shell.d.ts +3 -3
- package/dist/components/workspace-settings-shell.d.ts.map +1 -1
- package/dist/components/workspace-settings-shell.js +5 -2
- package/dist/components/workspace-settings-shell.js.map +1 -1
- package/dist/feature.d.ts +20 -4
- package/dist/feature.d.ts.map +1 -1
- package/dist/feature.js +15 -3
- package/dist/feature.js.map +1 -1
- package/dist/pages/general-settings-page.d.ts +2 -0
- package/dist/pages/general-settings-page.d.ts.map +1 -0
- package/dist/pages/general-settings-page.js +191 -0
- package/dist/pages/general-settings-page.js.map +1 -0
- package/dist/routes/general.d.ts +6 -0
- package/dist/routes/general.d.ts.map +1 -0
- package/dist/routes/general.js +7 -0
- package/dist/routes/general.js.map +1 -0
- package/dist/routes/index.d.ts +1 -1
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/index.js +2 -2
- package/dist/routes/index.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/src/components/general/form-errors.ts +17 -0
- package/src/components/members/workspace-members-section.tsx +2 -2
- package/src/components/workspace-settings-shell.tsx +5 -3
- package/src/feature.ts +15 -3
- package/src/pages/general-settings-page.test.tsx +91 -0
- package/src/pages/general-settings-page.tsx +198 -0
- package/src/routes/general.tsx +4 -0
- package/src/routes/index.tsx +5 -2
- package/test/pages.tsx +11 -5
- package/tsconfig.build.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipfox/client-workspace-settings",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "14.0.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -27,12 +27,12 @@
|
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@swc/helpers": "^0.5.17",
|
|
29
29
|
"@tanstack/react-form": "^1.32.0",
|
|
30
|
+
"@shipfox/api-workspaces-dto": "12.0.0",
|
|
30
31
|
"@shipfox/client-api": "6.0.1",
|
|
31
|
-
"@shipfox/client-auth": "
|
|
32
|
-
"@shipfox/
|
|
33
|
-
"@shipfox/client-
|
|
34
|
-
"@shipfox/
|
|
35
|
-
"@shipfox/react-ui": "0.3.7"
|
|
32
|
+
"@shipfox/client-auth": "14.0.0",
|
|
33
|
+
"@shipfox/client-shell": "14.0.0",
|
|
34
|
+
"@shipfox/client-ui": "14.0.0",
|
|
35
|
+
"@shipfox/react-ui": "0.5.0"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@tanstack/react-query": "^5.101.0",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {ApiError} from '@shipfox/client-api';
|
|
2
|
+
|
|
3
|
+
export type WorkspaceGeneralField = 'name' | 'slug';
|
|
4
|
+
|
|
5
|
+
export type WorkspaceGeneralFormError =
|
|
6
|
+
| {kind: 'field'; field: WorkspaceGeneralField; message: string}
|
|
7
|
+
| {kind: 'form'; message: string};
|
|
8
|
+
|
|
9
|
+
export function workspaceGeneralErrorToFormError(error: unknown): WorkspaceGeneralFormError {
|
|
10
|
+
if (error instanceof ApiError && error.code === 'slug-conflict') {
|
|
11
|
+
return {kind: 'field', field: 'slug', message: 'That workspace slug is already taken.'};
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
kind: 'form',
|
|
15
|
+
message: error instanceof Error ? error.message : 'Could not update workspace settings.',
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -144,7 +144,7 @@ function MemberRow({
|
|
|
144
144
|
|
|
145
145
|
return (
|
|
146
146
|
<TableRow className={remove.isPending ? 'opacity-60' : undefined}>
|
|
147
|
-
<TableCell className="font-medium">{member.name ?? '
|
|
147
|
+
<TableCell className="font-medium">{member.name ?? 'N/A'}</TableCell>
|
|
148
148
|
<TableCell>
|
|
149
149
|
<Code variant="paragraph">{member.email}</Code>
|
|
150
150
|
</TableCell>
|
|
@@ -283,7 +283,7 @@ function InvitationRow({
|
|
|
283
283
|
<TableCell>
|
|
284
284
|
<Code variant="paragraph">{invitation.email}</Code>
|
|
285
285
|
</TableCell>
|
|
286
|
-
<TableCell>{invitation.invitedByDisplay ?? '
|
|
286
|
+
<TableCell>{invitation.invitedByDisplay ?? 'N/A'}</TableCell>
|
|
287
287
|
<TableCell>
|
|
288
288
|
<div className="flex items-center gap-8">
|
|
289
289
|
<Text size="sm">{formatDate(invitation.expiresAt)}</Text>
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {useMaybeActiveWorkspace} from '@shipfox/client-shell/runtime';
|
|
2
|
+
import {FullPageLoader} from '@shipfox/react-ui/loader';
|
|
2
3
|
import type {ReactNode} from 'react';
|
|
3
4
|
|
|
4
5
|
interface WorkspaceSettingsShellProps {
|
|
5
|
-
children: (workspace: ReturnType<typeof
|
|
6
|
+
children: (workspace: NonNullable<ReturnType<typeof useMaybeActiveWorkspace>>) => ReactNode;
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
export function WorkspaceSettingsShell({children}: WorkspaceSettingsShellProps) {
|
|
9
|
-
const workspace =
|
|
10
|
+
const workspace = useMaybeActiveWorkspace();
|
|
11
|
+
if (!workspace) return <FullPageLoader />;
|
|
10
12
|
|
|
11
13
|
return children(workspace);
|
|
12
14
|
}
|
package/src/feature.ts
CHANGED
|
@@ -9,12 +9,19 @@ export const workspaceSettingsNavigation = [
|
|
|
9
9
|
id: 'nav.settings',
|
|
10
10
|
scope: 'workspace',
|
|
11
11
|
label: 'Settings',
|
|
12
|
-
to: '/
|
|
12
|
+
to: '/w/$workspaceSlug/settings',
|
|
13
13
|
order: 200,
|
|
14
14
|
},
|
|
15
15
|
] as const satisfies readonly NavTabEntry[];
|
|
16
16
|
|
|
17
17
|
export const workspaceSettingsSections = [
|
|
18
|
+
{
|
|
19
|
+
id: 'settings.general',
|
|
20
|
+
pathSegment: 'general',
|
|
21
|
+
label: 'General',
|
|
22
|
+
icon: 'settings3Line',
|
|
23
|
+
order: 50,
|
|
24
|
+
},
|
|
18
25
|
{
|
|
19
26
|
id: 'settings.members',
|
|
20
27
|
pathSegment: 'members',
|
|
@@ -28,15 +35,20 @@ export const workspaceSettingsFeature = defineClientFeature({
|
|
|
28
35
|
id: 'shipfox.workspace-settings',
|
|
29
36
|
routes: [
|
|
30
37
|
{
|
|
31
|
-
path: '/
|
|
38
|
+
path: '/w/$workspaceSlug/settings',
|
|
32
39
|
parent: 'workspaceSettings',
|
|
33
40
|
impl: '@shipfox/client-workspace-settings/routes/index',
|
|
34
41
|
},
|
|
35
42
|
{
|
|
36
|
-
path: '/
|
|
43
|
+
path: '/w/$workspaceSlug/settings/members',
|
|
37
44
|
parent: 'workspaceSettings',
|
|
38
45
|
impl: '@shipfox/client-workspace-settings/routes/members',
|
|
39
46
|
},
|
|
47
|
+
{
|
|
48
|
+
path: '/w/$workspaceSlug/settings/general',
|
|
49
|
+
parent: 'workspaceSettings',
|
|
50
|
+
impl: '@shipfox/client-workspace-settings/routes/general',
|
|
51
|
+
},
|
|
40
52
|
],
|
|
41
53
|
navigation: workspaceSettingsNavigation,
|
|
42
54
|
settingsSections: workspaceSettingsSections,
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import {configureApiClient} from '@shipfox/client-api';
|
|
2
|
+
import {fireEvent, screen, waitFor} from '@testing-library/react';
|
|
3
|
+
import {jsonResponse, renderWorkspaceSettingsPage} from '#test/pages.js';
|
|
4
|
+
import {GeneralSettingsPage} from './general-settings-page.js';
|
|
5
|
+
|
|
6
|
+
describe('GeneralSettingsPage', () => {
|
|
7
|
+
test('saves a name change without opening the slug warning', async () => {
|
|
8
|
+
let patchBody: unknown;
|
|
9
|
+
const fetchImpl = vi.fn(async (input: RequestInfo | URL) => {
|
|
10
|
+
const request = input as Request;
|
|
11
|
+
if (request.method === 'PATCH') {
|
|
12
|
+
patchBody = await request.clone().json();
|
|
13
|
+
return jsonResponse(workspaceDto({name: 'Acme Labs'}));
|
|
14
|
+
}
|
|
15
|
+
return jsonResponse({available: true});
|
|
16
|
+
});
|
|
17
|
+
configureApiClient({baseUrl: 'https://api.example.test', fetchImpl});
|
|
18
|
+
|
|
19
|
+
renderWorkspaceSettingsPage('/w/acme/settings/general', <GeneralSettingsPage />);
|
|
20
|
+
fireEvent.change(await screen.findByLabelText('Workspace name'), {
|
|
21
|
+
target: {value: 'Acme Labs'},
|
|
22
|
+
});
|
|
23
|
+
fireEvent.click(screen.getByRole('button', {name: 'Save changes'}));
|
|
24
|
+
|
|
25
|
+
await waitFor(() => expect(patchBody).toEqual({name: 'Acme Labs'}));
|
|
26
|
+
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('requires confirmation before saving a slug change', async () => {
|
|
30
|
+
let patchBody: unknown;
|
|
31
|
+
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
32
|
+
try {
|
|
33
|
+
const fetchImpl = vi.fn(async (input: RequestInfo | URL) => {
|
|
34
|
+
const request = input as Request;
|
|
35
|
+
if (request.url.includes('/slug-availability')) {
|
|
36
|
+
return jsonResponse({available: true});
|
|
37
|
+
}
|
|
38
|
+
if (request.method === 'PATCH') {
|
|
39
|
+
patchBody = await request.clone().json();
|
|
40
|
+
return jsonResponse(workspaceDto({slug: 'acme-labs'}));
|
|
41
|
+
}
|
|
42
|
+
return jsonResponse({});
|
|
43
|
+
});
|
|
44
|
+
configureApiClient({baseUrl: 'https://api.example.test', fetchImpl});
|
|
45
|
+
|
|
46
|
+
renderWorkspaceSettingsPage('/w/acme/settings/general', <GeneralSettingsPage />);
|
|
47
|
+
fireEvent.change(await screen.findByLabelText('Workspace slug'), {
|
|
48
|
+
target: {value: 'acme-labs'},
|
|
49
|
+
});
|
|
50
|
+
await waitFor(() =>
|
|
51
|
+
expect(
|
|
52
|
+
fetchImpl.mock.calls.some(([input]) =>
|
|
53
|
+
(input as Request).url.includes('/workspaces/slug-availability'),
|
|
54
|
+
),
|
|
55
|
+
).toBe(true),
|
|
56
|
+
);
|
|
57
|
+
fireEvent.click(screen.getByRole('button', {name: 'Save changes'}));
|
|
58
|
+
|
|
59
|
+
const dialog = await screen.findByRole('dialog');
|
|
60
|
+
expect(dialog).toHaveTextContent('old URL stop working');
|
|
61
|
+
expect(
|
|
62
|
+
fetchImpl.mock.calls.some(([input]) =>
|
|
63
|
+
(input as Request).url.includes('/workspaces/slug-availability'),
|
|
64
|
+
),
|
|
65
|
+
).toBe(true);
|
|
66
|
+
expect(patchBody).toBeUndefined();
|
|
67
|
+
|
|
68
|
+
fireEvent.click(screen.getByRole('button', {name: 'Change slug'}));
|
|
69
|
+
await waitFor(() => expect(patchBody).toEqual({slug: 'acme-labs'}));
|
|
70
|
+
expect(
|
|
71
|
+
consoleError.mock.calls.some((args) =>
|
|
72
|
+
args.some((argument) => String(argument).includes('useActiveWorkspace called outside')),
|
|
73
|
+
),
|
|
74
|
+
).toBe(false);
|
|
75
|
+
} finally {
|
|
76
|
+
consoleError.mockRestore();
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
function workspaceDto({name = 'Acme', slug = 'acme'}: {name?: string; slug?: string} = {}) {
|
|
82
|
+
return {
|
|
83
|
+
id: '11111111-1111-4111-8111-111111111111',
|
|
84
|
+
name,
|
|
85
|
+
slug,
|
|
86
|
+
status: 'active',
|
|
87
|
+
settings: {},
|
|
88
|
+
created_at: '2026-04-27T00:00:00.000Z',
|
|
89
|
+
updated_at: '2026-04-27T00:00:00.000Z',
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import {createWorkspaceBodySchema} from '@shipfox/api-workspaces-dto';
|
|
2
|
+
import {
|
|
3
|
+
checkWorkspaceSlugAvailability,
|
|
4
|
+
type useActiveWorkspace,
|
|
5
|
+
useUpdateWorkspaceMutation,
|
|
6
|
+
} from '@shipfox/client-auth';
|
|
7
|
+
import {displayNameFieldError, SlugChangeWarning, SlugField} from '@shipfox/client-ui';
|
|
8
|
+
import {Button} from '@shipfox/react-ui/button';
|
|
9
|
+
import {Callout} from '@shipfox/react-ui/callout';
|
|
10
|
+
import {FormField, FormFieldInput, fieldError} from '@shipfox/react-ui/form-field';
|
|
11
|
+
import {toast} from '@shipfox/react-ui/toast';
|
|
12
|
+
import {Header, Text} from '@shipfox/react-ui/typography';
|
|
13
|
+
import {useForm} from '@tanstack/react-form';
|
|
14
|
+
import {useNavigate} from '@tanstack/react-router';
|
|
15
|
+
import {useState} from 'react';
|
|
16
|
+
import {workspaceGeneralErrorToFormError} from '#components/general/form-errors.js';
|
|
17
|
+
import {WorkspaceSettingsShell} from '#components/workspace-settings-shell.js';
|
|
18
|
+
|
|
19
|
+
interface WorkspaceGeneralValues {
|
|
20
|
+
name: string;
|
|
21
|
+
slug: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isSlugValid(value: string): boolean {
|
|
25
|
+
return createWorkspaceBodySchema.shape.slug.safeParse(value).success;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function GeneralSettingsPage() {
|
|
29
|
+
return (
|
|
30
|
+
<WorkspaceSettingsShell>
|
|
31
|
+
{(workspace) => (
|
|
32
|
+
<WorkspaceGeneralForm
|
|
33
|
+
key={`${workspace.id}:${workspace.name}:${workspace.slug}`}
|
|
34
|
+
workspace={workspace}
|
|
35
|
+
/>
|
|
36
|
+
)}
|
|
37
|
+
</WorkspaceSettingsShell>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function WorkspaceGeneralForm({workspace}: {workspace: ReturnType<typeof useActiveWorkspace>}) {
|
|
42
|
+
const updateWorkspace = useUpdateWorkspaceMutation();
|
|
43
|
+
const navigate = useNavigate();
|
|
44
|
+
const [warningOpen, setWarningOpen] = useState(false);
|
|
45
|
+
const [pendingValues, setPendingValues] = useState<WorkspaceGeneralValues>();
|
|
46
|
+
const [formError, setFormError] = useState<string>();
|
|
47
|
+
|
|
48
|
+
const form = useForm({
|
|
49
|
+
defaultValues: {name: workspace.name, slug: workspace.slug},
|
|
50
|
+
onSubmit: async ({value}) => {
|
|
51
|
+
if (value.slug !== workspace.slug) {
|
|
52
|
+
setPendingValues(value);
|
|
53
|
+
setWarningOpen(true);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
await save(value);
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
async function save(values: WorkspaceGeneralValues) {
|
|
61
|
+
setFormError(undefined);
|
|
62
|
+
const nameChanged = values.name !== workspace.name;
|
|
63
|
+
const slugChanged = values.slug !== workspace.slug;
|
|
64
|
+
if (!nameChanged && !slugChanged) return;
|
|
65
|
+
const command = {
|
|
66
|
+
workspaceId: workspace.id,
|
|
67
|
+
...(nameChanged ? {name: values.name} : {}),
|
|
68
|
+
...(slugChanged ? {slug: values.slug} : {}),
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const updated = await updateWorkspace.mutateAsync(command);
|
|
73
|
+
setPendingValues(undefined);
|
|
74
|
+
setWarningOpen(false);
|
|
75
|
+
toast.success('Workspace settings saved.');
|
|
76
|
+
await navigate({
|
|
77
|
+
to: '/w/$workspaceSlug/settings/general',
|
|
78
|
+
params: {workspaceSlug: updated.slug},
|
|
79
|
+
});
|
|
80
|
+
} catch (error) {
|
|
81
|
+
setPendingValues(undefined);
|
|
82
|
+
setWarningOpen(false);
|
|
83
|
+
const mapped = workspaceGeneralErrorToFormError(error);
|
|
84
|
+
if (mapped.kind === 'field') {
|
|
85
|
+
form.setFieldMeta(mapped.field, (previous) => ({
|
|
86
|
+
...previous,
|
|
87
|
+
errorMap: {...previous.errorMap, onServer: mapped.message},
|
|
88
|
+
}));
|
|
89
|
+
} else {
|
|
90
|
+
setFormError(mapped.message);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<>
|
|
97
|
+
<div className="flex min-w-0 flex-col gap-24">
|
|
98
|
+
<header className="flex flex-col gap-6">
|
|
99
|
+
<Header variant="h1">General</Header>
|
|
100
|
+
<Text size="sm" className="text-foreground-neutral-muted">
|
|
101
|
+
Update the workspace name and the slug used in its URLs.
|
|
102
|
+
</Text>
|
|
103
|
+
</header>
|
|
104
|
+
|
|
105
|
+
{formError ? (
|
|
106
|
+
<Callout role="alert" type="error">
|
|
107
|
+
{formError}
|
|
108
|
+
</Callout>
|
|
109
|
+
) : null}
|
|
110
|
+
|
|
111
|
+
<form
|
|
112
|
+
className="flex max-w-[560px] flex-col gap-16"
|
|
113
|
+
noValidate
|
|
114
|
+
onSubmit={(event) => {
|
|
115
|
+
event.preventDefault();
|
|
116
|
+
event.stopPropagation();
|
|
117
|
+
void form.handleSubmit();
|
|
118
|
+
}}
|
|
119
|
+
>
|
|
120
|
+
<form.Field
|
|
121
|
+
name="name"
|
|
122
|
+
validators={{
|
|
123
|
+
onBlur: ({value}) =>
|
|
124
|
+
displayNameFieldError(
|
|
125
|
+
value,
|
|
126
|
+
'Workspace name',
|
|
127
|
+
createWorkspaceBodySchema.shape.name,
|
|
128
|
+
),
|
|
129
|
+
onSubmit: ({value}) =>
|
|
130
|
+
displayNameFieldError(
|
|
131
|
+
value,
|
|
132
|
+
'Workspace name',
|
|
133
|
+
createWorkspaceBodySchema.shape.name,
|
|
134
|
+
),
|
|
135
|
+
}}
|
|
136
|
+
>
|
|
137
|
+
{(field) => (
|
|
138
|
+
<FormField
|
|
139
|
+
label="Workspace name"
|
|
140
|
+
id="workspace-settings-name"
|
|
141
|
+
error={fieldError(field)}
|
|
142
|
+
>
|
|
143
|
+
<FormFieldInput
|
|
144
|
+
name="name"
|
|
145
|
+
type="text"
|
|
146
|
+
value={field.state.value}
|
|
147
|
+
onChange={(event) => field.handleChange(event.target.value)}
|
|
148
|
+
onBlur={field.handleBlur}
|
|
149
|
+
/>
|
|
150
|
+
</FormField>
|
|
151
|
+
)}
|
|
152
|
+
</form.Field>
|
|
153
|
+
|
|
154
|
+
<form.Field
|
|
155
|
+
name="slug"
|
|
156
|
+
validators={{
|
|
157
|
+
onBlur: createWorkspaceBodySchema.shape.slug,
|
|
158
|
+
onSubmit: createWorkspaceBodySchema.shape.slug,
|
|
159
|
+
}}
|
|
160
|
+
>
|
|
161
|
+
{(field) => (
|
|
162
|
+
<SlugField
|
|
163
|
+
id="workspace-settings-slug"
|
|
164
|
+
label="Workspace slug"
|
|
165
|
+
name="slug"
|
|
166
|
+
value={field.state.value}
|
|
167
|
+
onChange={(value) => field.handleChange(value)}
|
|
168
|
+
onBlur={field.handleBlur}
|
|
169
|
+
error={fieldError(field)}
|
|
170
|
+
description={<span className="break-all font-code">/w/{field.state.value}</span>}
|
|
171
|
+
placeholder="acme"
|
|
172
|
+
className="font-code"
|
|
173
|
+
currentSlug={workspace.slug}
|
|
174
|
+
checkEnabled
|
|
175
|
+
isValid={isSlugValid}
|
|
176
|
+
checkAvailability={checkWorkspaceSlugAvailability}
|
|
177
|
+
/>
|
|
178
|
+
)}
|
|
179
|
+
</form.Field>
|
|
180
|
+
|
|
181
|
+
<Button type="submit" isLoading={updateWorkspace.isPending} className="self-start">
|
|
182
|
+
Save changes
|
|
183
|
+
</Button>
|
|
184
|
+
</form>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
<SlugChangeWarning
|
|
188
|
+
open={warningOpen}
|
|
189
|
+
onOpenChange={setWarningOpen}
|
|
190
|
+
entityLabel="workspace"
|
|
191
|
+
isLoading={updateWorkspace.isPending}
|
|
192
|
+
onConfirm={() => {
|
|
193
|
+
if (pendingValues) void save(pendingValues);
|
|
194
|
+
}}
|
|
195
|
+
/>
|
|
196
|
+
</>
|
|
197
|
+
);
|
|
198
|
+
}
|
package/src/routes/index.tsx
CHANGED
|
@@ -2,7 +2,10 @@ import {defineRoute} from '@shipfox/client-shell/runtime';
|
|
|
2
2
|
import {redirect} from '@tanstack/react-router';
|
|
3
3
|
|
|
4
4
|
export default defineRoute({
|
|
5
|
-
beforeLoad: ({params}: {params: {
|
|
6
|
-
throw redirect({
|
|
5
|
+
beforeLoad: ({params}: {params: {workspaceSlug: string}}) => {
|
|
6
|
+
throw redirect({
|
|
7
|
+
to: '/w/$workspaceSlug/settings/members',
|
|
8
|
+
params: {workspaceSlug: params.workspaceSlug},
|
|
9
|
+
});
|
|
7
10
|
},
|
|
8
11
|
});
|
package/test/pages.tsx
CHANGED
|
@@ -24,7 +24,7 @@ const authState: AuthState = {
|
|
|
24
24
|
email: 'user@example.com',
|
|
25
25
|
emailVerifiedAt: new Date().toISOString(),
|
|
26
26
|
},
|
|
27
|
-
workspaces: [{id: WORKSPACE_SETTINGS_TEST_WID, name: 'Acme', membershipId: 'm-1'}],
|
|
27
|
+
workspaces: [{id: WORKSPACE_SETTINGS_TEST_WID, name: 'Acme', slug: 'acme', membershipId: 'm-1'}],
|
|
28
28
|
};
|
|
29
29
|
|
|
30
30
|
export function jsonResponse(body: unknown, init: ResponseInit = {}) {
|
|
@@ -39,22 +39,27 @@ function createTestRouter(path: string, element: ReactElement) {
|
|
|
39
39
|
const rootRoute = createRootRoute({component: Outlet});
|
|
40
40
|
const runnersRoute = createRoute({
|
|
41
41
|
getParentRoute: () => rootRoute,
|
|
42
|
-
path: '/
|
|
42
|
+
path: '/w/$workspaceSlug/settings/runners',
|
|
43
43
|
component: () => element,
|
|
44
44
|
});
|
|
45
45
|
const provisionersRoute = createRoute({
|
|
46
46
|
getParentRoute: () => rootRoute,
|
|
47
|
-
path: '/
|
|
47
|
+
path: '/w/$workspaceSlug/settings/provisioners',
|
|
48
48
|
component: () => element,
|
|
49
49
|
});
|
|
50
50
|
const modelProvidersRoute = createRoute({
|
|
51
51
|
getParentRoute: () => rootRoute,
|
|
52
|
-
path: '/
|
|
52
|
+
path: '/w/$workspaceSlug/settings/agents',
|
|
53
53
|
component: () => element,
|
|
54
54
|
});
|
|
55
55
|
const integrationsRoute = createRoute({
|
|
56
56
|
getParentRoute: () => rootRoute,
|
|
57
|
-
path: '/
|
|
57
|
+
path: '/w/$workspaceSlug/settings/integrations',
|
|
58
|
+
component: () => element,
|
|
59
|
+
});
|
|
60
|
+
const generalRoute = createRoute({
|
|
61
|
+
getParentRoute: () => rootRoute,
|
|
62
|
+
path: '/w/$workspaceSlug/settings/general',
|
|
58
63
|
component: () => element,
|
|
59
64
|
});
|
|
60
65
|
|
|
@@ -65,6 +70,7 @@ function createTestRouter(path: string, element: ReactElement) {
|
|
|
65
70
|
provisionersRoute,
|
|
66
71
|
modelProvidersRoute,
|
|
67
72
|
integrationsRoute,
|
|
73
|
+
generalRoute,
|
|
68
74
|
]),
|
|
69
75
|
});
|
|
70
76
|
}
|