@shipfox/client-auth 17.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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +34 -0
- package/dist/pages/workspace-onboarding-page.d.ts.map +1 -1
- package/dist/pages/workspace-onboarding-page.js +100 -235
- package/dist/pages/workspace-onboarding-page.js.map +1 -1
- package/dist/routes/index.d.ts +3 -0
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/index.js +3 -0
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/login.d.ts +3 -0
- package/dist/routes/login.js +3 -0
- package/dist/routes/login.js.map +1 -1
- package/dist/routes/logout.d.ts +3 -0
- package/dist/routes/logout.js +3 -0
- package/dist/routes/logout.js.map +1 -1
- package/dist/routes/reset.d.ts +3 -0
- package/dist/routes/reset.js +3 -0
- package/dist/routes/reset.js.map +1 -1
- package/dist/routes/signup.d.ts +3 -0
- package/dist/routes/signup.js +3 -0
- package/dist/routes/signup.js.map +1 -1
- package/dist/routes/workspace-onboarding.d.ts +3 -0
- package/dist/routes/workspace-onboarding.d.ts.map +1 -1
- package/dist/routes/workspace-onboarding.js +3 -0
- package/dist/routes/workspace-onboarding.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/src/pages/workspace-onboarding-page.test.tsx +16 -6
- package/src/pages/workspace-onboarding-page.tsx +104 -174
- package/src/routes/index.tsx +1 -0
- package/src/routes/login.tsx +1 -0
- package/src/routes/logout.tsx +5 -1
- package/src/routes/reset.tsx +1 -0
- package/src/routes/signup.tsx +1 -0
- package/src/routes/workspace-onboarding.tsx +1 -0
- package/tsconfig.build.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipfox/client-auth",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "22.0.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"@shipfox/api-auth-dto": "12.0.0",
|
|
40
40
|
"@shipfox/api-workspaces-dto": "12.0.0",
|
|
41
41
|
"@shipfox/client-api": "6.0.1",
|
|
42
|
-
"@shipfox/client-invitations": "
|
|
43
|
-
"@shipfox/client-
|
|
44
|
-
"@shipfox/
|
|
45
|
-
"@shipfox/
|
|
42
|
+
"@shipfox/client-invitations": "22.0.0",
|
|
43
|
+
"@shipfox/client-shell": "22.0.0",
|
|
44
|
+
"@shipfox/client-ui": "22.0.0",
|
|
45
|
+
"@shipfox/react-ui": "2.1.0"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"@tanstack/react-query": "^5.101.0",
|
|
@@ -66,7 +66,7 @@ describe('WorkspaceOnboardingPage', () => {
|
|
|
66
66
|
});
|
|
67
67
|
configureApiClient({fetchImpl});
|
|
68
68
|
|
|
69
|
-
renderAuthPage(
|
|
69
|
+
const {container} = renderAuthPage(
|
|
70
70
|
'/',
|
|
71
71
|
<AuthGuard>
|
|
72
72
|
<WorkspaceGuard>
|
|
@@ -74,7 +74,13 @@ describe('WorkspaceOnboardingPage', () => {
|
|
|
74
74
|
</WorkspaceGuard>
|
|
75
75
|
</AuthGuard>,
|
|
76
76
|
);
|
|
77
|
-
|
|
77
|
+
const workspaceName = await screen.findByLabelText('Workspace name');
|
|
78
|
+
expect(container.querySelector('[data-slot="panel-header"]')).toHaveAttribute(
|
|
79
|
+
'data-variant',
|
|
80
|
+
'plain',
|
|
81
|
+
);
|
|
82
|
+
expect(container.querySelector('[data-slot="panel-body"]')).toBeInTheDocument();
|
|
83
|
+
fireEvent.change(workspaceName, {
|
|
78
84
|
target: {value: ' Acme '},
|
|
79
85
|
});
|
|
80
86
|
fireEvent.click(screen.getByRole('button', {name: 'Create workspace'}));
|
|
@@ -111,13 +117,17 @@ describe('WorkspaceOnboardingPage', () => {
|
|
|
111
117
|
const slug = screen.getByLabelText('Workspace slug');
|
|
112
118
|
expect(screen.getByText(`${window.location.origin}/w/acme`)).toBeInTheDocument();
|
|
113
119
|
fireEvent.change(name, {target: {value: 'Acme Labs'}});
|
|
114
|
-
|
|
115
|
-
|
|
120
|
+
await waitFor(() => {
|
|
121
|
+
expect(slug).toHaveValue('acme-labs');
|
|
122
|
+
expect(screen.getByText(`${window.location.origin}/w/acme-labs`)).toBeInTheDocument();
|
|
123
|
+
});
|
|
116
124
|
|
|
117
125
|
fireEvent.change(slug, {target: {value: 'custom-workspace'}});
|
|
118
126
|
fireEvent.change(name, {target: {value: 'Renamed Labs'}});
|
|
119
|
-
|
|
120
|
-
|
|
127
|
+
await waitFor(() => {
|
|
128
|
+
expect(slug).toHaveValue('custom-workspace');
|
|
129
|
+
expect(screen.getByText(`${window.location.origin}/w/custom-workspace`)).toBeInTheDocument();
|
|
130
|
+
});
|
|
121
131
|
});
|
|
122
132
|
|
|
123
133
|
test('checks a manually edited workspace slug for availability', async () => {
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import {slugifyName, slugSchema} from '@shipfox/api-common-dto';
|
|
2
2
|
import {createWorkspaceBodySchema} from '@shipfox/api-workspaces-dto';
|
|
3
|
+
import {FocusedFrame} from '@shipfox/client-shell/runtime';
|
|
3
4
|
import {displayNameFieldError, SlugField} from '@shipfox/client-ui';
|
|
4
5
|
import {Button} from '@shipfox/react-ui/button';
|
|
5
6
|
import {Callout} from '@shipfox/react-ui/callout';
|
|
6
|
-
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from '@shipfox/react-ui/card';
|
|
7
7
|
import {FormField, FormFieldInput, fieldError} from '@shipfox/react-ui/form-field';
|
|
8
8
|
import {Icon} from '@shipfox/react-ui/icon';
|
|
9
|
+
import {Panel, PanelBody, PanelHeader, PanelTitle} from '@shipfox/react-ui/panel';
|
|
9
10
|
import {toast} from '@shipfox/react-ui/toast';
|
|
10
11
|
import {Text} from '@shipfox/react-ui/typography';
|
|
11
12
|
import {useForm} from '@tanstack/react-form';
|
|
@@ -17,23 +18,6 @@ import {useAuthState} from '#hooks/use-auth-state.js';
|
|
|
17
18
|
import {lastWorkspaceIdAtom, rememberLastWorkspaceId} from '#state/last-workspace.js';
|
|
18
19
|
import {workspaceOnboardingErrorToFormError} from './form-errors.js';
|
|
19
20
|
|
|
20
|
-
const previewMetrics = [
|
|
21
|
-
{label: 'Runs', value: '--'},
|
|
22
|
-
{label: 'Passed', value: '--'},
|
|
23
|
-
{label: 'Failed', value: '--'},
|
|
24
|
-
{label: 'Duration', value: '--'},
|
|
25
|
-
];
|
|
26
|
-
const previewBars = [
|
|
27
|
-
{id: 'runs-start', height: 32},
|
|
28
|
-
{id: 'runs-mid-low', height: 48},
|
|
29
|
-
{id: 'runs-dip', height: 28},
|
|
30
|
-
{id: 'runs-mid-high', height: 66},
|
|
31
|
-
{id: 'runs-mid', height: 54},
|
|
32
|
-
{id: 'runs-peak', height: 82},
|
|
33
|
-
{id: 'runs-late-low', height: 44},
|
|
34
|
-
{id: 'runs-late-high', height: 74},
|
|
35
|
-
];
|
|
36
|
-
|
|
37
21
|
function isSlugValid(value: string): boolean {
|
|
38
22
|
return slugSchema.safeParse(value).success;
|
|
39
23
|
}
|
|
@@ -78,8 +62,8 @@ export function WorkspaceOnboardingPage() {
|
|
|
78
62
|
});
|
|
79
63
|
|
|
80
64
|
return (
|
|
81
|
-
<main className="min-h-screen
|
|
82
|
-
<
|
|
65
|
+
<main className="min-h-screen px-frame py-frame max-[520px]:px-row">
|
|
66
|
+
<FocusedFrame className="flex min-h-[calc(100vh-64px)] flex-col gap-section">
|
|
83
67
|
<header className="flex items-center justify-between">
|
|
84
68
|
<div className="flex items-center gap-inline">
|
|
85
69
|
<div className="flex size-36 items-center justify-center rounded-8 border border-border-neutral-base bg-background-neutral-base shadow-button-neutral">
|
|
@@ -91,9 +75,9 @@ export function WorkspaceOnboardingPage() {
|
|
|
91
75
|
</div>
|
|
92
76
|
</header>
|
|
93
77
|
|
|
94
|
-
<section className="
|
|
78
|
+
<section className="flex flex-1 items-center">
|
|
95
79
|
<form
|
|
96
|
-
className="
|
|
80
|
+
className="w-full"
|
|
97
81
|
noValidate
|
|
98
82
|
aria-labelledby="workspace-onboarding-title"
|
|
99
83
|
onSubmit={(event) => {
|
|
@@ -102,168 +86,114 @@ export function WorkspaceOnboardingPage() {
|
|
|
102
86
|
void form.handleSubmit();
|
|
103
87
|
}}
|
|
104
88
|
>
|
|
105
|
-
<
|
|
106
|
-
<
|
|
107
|
-
<
|
|
89
|
+
<Panel>
|
|
90
|
+
<PanelHeader variant="plain" className="flex-col items-start gap-inline">
|
|
91
|
+
<PanelTitle id="workspace-onboarding-title" variant="h1">
|
|
108
92
|
Create your workspace
|
|
109
|
-
</
|
|
110
|
-
<
|
|
111
|
-
|
|
93
|
+
</PanelTitle>
|
|
94
|
+
<Text size="sm" className="text-foreground-neutral-muted">
|
|
95
|
+
Give your team a place to collaborate.
|
|
96
|
+
</Text>
|
|
97
|
+
</PanelHeader>
|
|
112
98
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
99
|
+
<PanelBody className="flex flex-col gap-section p-panel">
|
|
100
|
+
{formError ? (
|
|
101
|
+
<Callout role="alert" type="error">
|
|
102
|
+
{formError}
|
|
103
|
+
</Callout>
|
|
104
|
+
) : null}
|
|
118
105
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
106
|
+
<div className="flex flex-col gap-inline">
|
|
107
|
+
<form.Field
|
|
108
|
+
name="name"
|
|
109
|
+
validators={{
|
|
110
|
+
onBlur: ({value}) =>
|
|
111
|
+
displayNameFieldError(
|
|
112
|
+
value,
|
|
113
|
+
'Workspace name',
|
|
114
|
+
createWorkspaceBodySchema.shape.name,
|
|
115
|
+
),
|
|
116
|
+
onSubmit: ({value}) =>
|
|
117
|
+
displayNameFieldError(
|
|
118
|
+
value,
|
|
119
|
+
'Workspace name',
|
|
120
|
+
createWorkspaceBodySchema.shape.name,
|
|
121
|
+
),
|
|
122
|
+
}}
|
|
123
|
+
>
|
|
124
|
+
{(field) => (
|
|
125
|
+
<FormField
|
|
126
|
+
label="Workspace name"
|
|
127
|
+
id="workspace-name"
|
|
128
|
+
error={fieldError(field)}
|
|
129
|
+
>
|
|
130
|
+
<FormFieldInput
|
|
131
|
+
autoComplete="organization"
|
|
132
|
+
name="name"
|
|
133
|
+
placeholder="Acme"
|
|
134
|
+
type="text"
|
|
135
|
+
value={field.state.value}
|
|
136
|
+
onChange={(event) => {
|
|
137
|
+
const name = event.target.value;
|
|
138
|
+
field.handleChange(name);
|
|
139
|
+
if (!slugTouched) {
|
|
140
|
+
form.setFieldValue(
|
|
141
|
+
'slug',
|
|
142
|
+
name ? slugifyName(name, {fallback: 'workspace'}) : '',
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}}
|
|
146
|
+
onBlur={field.handleBlur}
|
|
147
|
+
/>
|
|
148
|
+
</FormField>
|
|
149
|
+
)}
|
|
150
|
+
</form.Field>
|
|
151
|
+
<form.Field
|
|
152
|
+
name="slug"
|
|
153
|
+
validators={{
|
|
154
|
+
onBlur: createWorkspaceBodySchema.shape.slug,
|
|
155
|
+
onSubmit: createWorkspaceBodySchema.shape.slug,
|
|
156
|
+
}}
|
|
157
|
+
>
|
|
158
|
+
{(field) => (
|
|
159
|
+
<SlugField
|
|
160
|
+
id="workspace-slug"
|
|
161
|
+
label="Workspace slug"
|
|
162
|
+
name="slug"
|
|
144
163
|
value={field.state.value}
|
|
145
|
-
onChange={(
|
|
146
|
-
|
|
147
|
-
field.handleChange(
|
|
148
|
-
if (!slugTouched) {
|
|
149
|
-
form.setFieldValue(
|
|
150
|
-
'slug',
|
|
151
|
-
name ? slugifyName(name, {fallback: 'workspace'}) : '',
|
|
152
|
-
);
|
|
153
|
-
}
|
|
164
|
+
onChange={(value) => {
|
|
165
|
+
setSlugTouched(true);
|
|
166
|
+
field.handleChange(value);
|
|
154
167
|
}}
|
|
155
168
|
onBlur={field.handleBlur}
|
|
169
|
+
error={fieldError(field)}
|
|
170
|
+
description={
|
|
171
|
+
<span className="break-all font-code">
|
|
172
|
+
{`${window.location.origin}/w/${field.state.value || 'acme'}`}
|
|
173
|
+
</span>
|
|
174
|
+
}
|
|
175
|
+
placeholder="acme"
|
|
176
|
+
checkEnabled={slugTouched}
|
|
177
|
+
isValid={isSlugValid}
|
|
178
|
+
checkAvailability={checkWorkspaceSlugAvailability}
|
|
156
179
|
/>
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
</
|
|
160
|
-
<form.Field
|
|
161
|
-
name="slug"
|
|
162
|
-
validators={{
|
|
163
|
-
onBlur: createWorkspaceBodySchema.shape.slug,
|
|
164
|
-
onSubmit: createWorkspaceBodySchema.shape.slug,
|
|
165
|
-
}}
|
|
166
|
-
>
|
|
167
|
-
{(field) => (
|
|
168
|
-
<SlugField
|
|
169
|
-
id="workspace-slug"
|
|
170
|
-
label="Workspace slug"
|
|
171
|
-
name="slug"
|
|
172
|
-
value={field.state.value}
|
|
173
|
-
onChange={(value) => {
|
|
174
|
-
setSlugTouched(true);
|
|
175
|
-
field.handleChange(value);
|
|
176
|
-
}}
|
|
177
|
-
onBlur={field.handleBlur}
|
|
178
|
-
error={fieldError(field)}
|
|
179
|
-
description={
|
|
180
|
-
<span className="break-all font-code">
|
|
181
|
-
{`${window.location.origin}/w/${field.state.value || 'acme'}`}
|
|
182
|
-
</span>
|
|
183
|
-
}
|
|
184
|
-
placeholder="acme"
|
|
185
|
-
checkEnabled={slugTouched}
|
|
186
|
-
isValid={isSlugValid}
|
|
187
|
-
checkAvailability={checkWorkspaceSlugAvailability}
|
|
188
|
-
/>
|
|
189
|
-
)}
|
|
190
|
-
</form.Field>
|
|
191
|
-
</CardContent>
|
|
192
|
-
|
|
193
|
-
<Button
|
|
194
|
-
className="w-full"
|
|
195
|
-
iconRight="chevronRight"
|
|
196
|
-
isLoading={createWorkspace.isPending}
|
|
197
|
-
type="submit"
|
|
198
|
-
>
|
|
199
|
-
{createWorkspace.isPending ? 'Creating workspace...' : 'Create workspace'}
|
|
200
|
-
</Button>
|
|
201
|
-
</Card>
|
|
202
|
-
</form>
|
|
180
|
+
)}
|
|
181
|
+
</form.Field>
|
|
182
|
+
</div>
|
|
203
183
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
key={metric.label}
|
|
184
|
+
<Button
|
|
185
|
+
className="w-full"
|
|
186
|
+
iconRight="chevronRight"
|
|
187
|
+
isLoading={createWorkspace.isPending}
|
|
188
|
+
type="submit"
|
|
210
189
|
>
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
</Text>
|
|
217
|
-
</div>
|
|
218
|
-
))}
|
|
219
|
-
</div>
|
|
220
|
-
<div className="grid grid-cols-2 gap-group">
|
|
221
|
-
<PreviewPanel title="Performance over time" />
|
|
222
|
-
<PreviewPanel title="Duration distribution" bars />
|
|
223
|
-
</div>
|
|
224
|
-
<div className="flex flex-col gap-cluster rounded-8 border border-border-neutral-base bg-background-neutral-base p-panel-compact shadow-button-neutral">
|
|
225
|
-
<Text size="sm" bold>
|
|
226
|
-
Jobs breakdown
|
|
227
|
-
</Text>
|
|
228
|
-
<div className="flex flex-col gap-inline">
|
|
229
|
-
{[0, 1, 2, 3].map((row) => (
|
|
230
|
-
<div
|
|
231
|
-
className="grid grid-cols-[1fr_80px_80px] gap-cluster border-t border-border-neutral-base pt-[10px]"
|
|
232
|
-
key={row}
|
|
233
|
-
>
|
|
234
|
-
<div className="h-12 rounded-full bg-background-neutral-disabled" />
|
|
235
|
-
<div className="h-12 rounded-full bg-background-neutral-disabled" />
|
|
236
|
-
<div className="h-12 rounded-full bg-background-neutral-disabled" />
|
|
237
|
-
</div>
|
|
238
|
-
))}
|
|
239
|
-
</div>
|
|
240
|
-
</div>
|
|
241
|
-
</div>
|
|
190
|
+
{createWorkspace.isPending ? 'Creating workspace...' : 'Create workspace'}
|
|
191
|
+
</Button>
|
|
192
|
+
</PanelBody>
|
|
193
|
+
</Panel>
|
|
194
|
+
</form>
|
|
242
195
|
</section>
|
|
243
|
-
</
|
|
196
|
+
</FocusedFrame>
|
|
244
197
|
</main>
|
|
245
198
|
);
|
|
246
199
|
}
|
|
247
|
-
|
|
248
|
-
function PreviewPanel({title, bars = false}: {title: string; bars?: boolean}) {
|
|
249
|
-
return (
|
|
250
|
-
<div className="flex flex-col gap-group rounded-8 border border-border-neutral-base bg-background-neutral-base p-panel-compact shadow-button-neutral">
|
|
251
|
-
<Text size="sm" bold>
|
|
252
|
-
{title}
|
|
253
|
-
</Text>
|
|
254
|
-
<div className="flex h-[220px] items-end gap-inline border-b border-l border-border-neutral-base px-row pb-[10px]">
|
|
255
|
-
{previewBars.map((bar) => (
|
|
256
|
-
<div
|
|
257
|
-
className={
|
|
258
|
-
bars
|
|
259
|
-
? 'w-full rounded-t-4 bg-background-neutral-disabled'
|
|
260
|
-
: 'w-full rounded-full bg-background-neutral-disabled'
|
|
261
|
-
}
|
|
262
|
-
key={`${title}-${bar.id}`}
|
|
263
|
-
style={{height: `${bar.height}%`}}
|
|
264
|
-
/>
|
|
265
|
-
))}
|
|
266
|
-
</div>
|
|
267
|
-
</div>
|
|
268
|
-
);
|
|
269
|
-
}
|
package/src/routes/index.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import {FullPageLoader} from '@shipfox/react-ui/loader';
|
|
|
4
4
|
import {redirect} from '@tanstack/react-router';
|
|
5
5
|
|
|
6
6
|
export default defineRoute({
|
|
7
|
+
staticData: {frame: 'focused'},
|
|
7
8
|
beforeLoad: ({context}: {context: RouterContext}) => {
|
|
8
9
|
const auth = context.auth;
|
|
9
10
|
if (!auth || auth.isLoading) return;
|
package/src/routes/login.tsx
CHANGED
package/src/routes/logout.tsx
CHANGED
|
@@ -2,4 +2,8 @@ import {defineRoute} from '@shipfox/client-shell/runtime';
|
|
|
2
2
|
import {LogoutPage} from '#pages/logout-page.js';
|
|
3
3
|
import {validateRedirectSearch} from './inputs.js';
|
|
4
4
|
|
|
5
|
-
export default defineRoute({
|
|
5
|
+
export default defineRoute({
|
|
6
|
+
staticData: {frame: 'focused'},
|
|
7
|
+
validateSearch: validateRedirectSearch,
|
|
8
|
+
component: LogoutPage,
|
|
9
|
+
});
|
package/src/routes/reset.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import {PasswordResetPage} from '#pages/password-reset-page.js';
|
|
|
4
4
|
import {validatePasswordResetSearch} from './inputs.js';
|
|
5
5
|
|
|
6
6
|
export default defineRoute({
|
|
7
|
+
staticData: {frame: 'focused'},
|
|
7
8
|
validateSearch: validatePasswordResetSearch,
|
|
8
9
|
component: () => (
|
|
9
10
|
<GuestGuard>
|
package/src/routes/signup.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import {redirect} from '@tanstack/react-router';
|
|
|
4
4
|
import {WorkspaceOnboardingPage} from '#pages/workspace-onboarding-page.js';
|
|
5
5
|
|
|
6
6
|
export default defineRoute({
|
|
7
|
+
staticData: {frame: 'focused'},
|
|
7
8
|
beforeLoad: ({context, location}: {context: RouterContext; location: {href: string}}) => {
|
|
8
9
|
const auth = context.auth;
|
|
9
10
|
if (!auth || auth.isLoading) return;
|