@shipfox/client-workspace-settings 22.0.2 → 23.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 +23 -0
- package/dist/components/show-setup-guide-link.d.ts +4 -0
- package/dist/components/show-setup-guide-link.d.ts.map +1 -0
- package/dist/components/show-setup-guide-link.js +59 -0
- package/dist/components/show-setup-guide-link.js.map +1 -0
- package/dist/hooks/api/list-invitations.d.ts +4 -8
- package/dist/hooks/api/list-invitations.d.ts.map +1 -1
- package/dist/hooks/api/list-invitations.js.map +1 -1
- package/dist/hooks/api/list-members.d.ts +4 -8
- package/dist/hooks/api/list-members.d.ts.map +1 -1
- package/dist/hooks/api/list-members.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/pages/general-settings-page.d.ts.map +1 -1
- package/dist/pages/general-settings-page.js +4 -0
- package/dist/pages/general-settings-page.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/src/components/show-setup-guide-link.test.tsx +69 -0
- package/src/components/show-setup-guide-link.tsx +66 -0
- package/src/hooks/api/list-invitations.ts +9 -2
- package/src/hooks/api/list-members.ts +9 -2
- package/src/index.ts +2 -0
- package/src/pages/general-settings-page.tsx +3 -0
- package/src/public-api.test.ts +27 -0
- 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": "23.0.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
"@tanstack/react-form": "^1.32.0",
|
|
30
30
|
"@shipfox/api-workspaces-dto": "12.0.0",
|
|
31
31
|
"@shipfox/client-api": "6.0.1",
|
|
32
|
-
"@shipfox/client-auth": "
|
|
33
|
-
"@shipfox/client-shell": "
|
|
34
|
-
"@shipfox/client-ui": "22.0.
|
|
35
|
-
"@shipfox/react-ui": "2.
|
|
32
|
+
"@shipfox/client-auth": "23.0.0",
|
|
33
|
+
"@shipfox/client-shell": "23.0.0",
|
|
34
|
+
"@shipfox/client-ui": "22.0.3",
|
|
35
|
+
"@shipfox/react-ui": "2.2.0"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@tanstack/react-query": "^5.101.0",
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import {
|
|
2
|
+
clearWorkspaceSetupChecklistDismissal,
|
|
3
|
+
dismissWorkspaceSetupChecklist,
|
|
4
|
+
isWorkspaceSetupChecklistDismissed,
|
|
5
|
+
} from '@shipfox/client-shell/runtime';
|
|
6
|
+
import {fireEvent, render, screen} from '@testing-library/react';
|
|
7
|
+
import userEvent from '@testing-library/user-event';
|
|
8
|
+
import {ShowSetupGuideLink} from './show-setup-guide-link.js';
|
|
9
|
+
|
|
10
|
+
const CURRENT_WORKSPACE_ID = '11111111-1111-4111-8111-111111111111';
|
|
11
|
+
const OTHER_WORKSPACE_ID = '22222222-2222-4222-8222-222222222222';
|
|
12
|
+
|
|
13
|
+
describe('ShowSetupGuideLink', () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
window.localStorage.clear();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('is absent while the setup guide is not dismissed', () => {
|
|
19
|
+
render(<ShowSetupGuideLink workspaceId={CURRENT_WORKSPACE_ID} />);
|
|
20
|
+
|
|
21
|
+
expect(screen.queryByRole('button', {name: 'Show the setup guide'})).not.toBeInTheDocument();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('is present while the setup guide is dismissed for the current workspace', () => {
|
|
25
|
+
dismissWorkspaceSetupChecklist(CURRENT_WORKSPACE_ID);
|
|
26
|
+
|
|
27
|
+
render(<ShowSetupGuideLink workspaceId={CURRENT_WORKSPACE_ID} />);
|
|
28
|
+
|
|
29
|
+
expect(screen.getByRole('button', {name: 'Show the setup guide'})).toBeInTheDocument();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('does not use another workspace dismissal flag', () => {
|
|
33
|
+
dismissWorkspaceSetupChecklist(OTHER_WORKSPACE_ID);
|
|
34
|
+
|
|
35
|
+
render(<ShowSetupGuideLink workspaceId={CURRENT_WORKSPACE_ID} />);
|
|
36
|
+
|
|
37
|
+
expect(screen.queryByRole('button', {name: 'Show the setup guide'})).not.toBeInTheDocument();
|
|
38
|
+
expect(isWorkspaceSetupChecklistDismissed(OTHER_WORKSPACE_ID)).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('re-reads the dismissal flag when the window regains focus', () => {
|
|
42
|
+
render(<ShowSetupGuideLink workspaceId={CURRENT_WORKSPACE_ID} />);
|
|
43
|
+
dismissWorkspaceSetupChecklist(CURRENT_WORKSPACE_ID);
|
|
44
|
+
|
|
45
|
+
fireEvent.focus(window);
|
|
46
|
+
|
|
47
|
+
expect(screen.getByRole('button', {name: 'Show the setup guide'})).toBeInTheDocument();
|
|
48
|
+
|
|
49
|
+
clearWorkspaceSetupChecklistDismissal(CURRENT_WORKSPACE_ID);
|
|
50
|
+
fireEvent.focus(window);
|
|
51
|
+
|
|
52
|
+
expect(screen.queryByRole('button', {name: 'Show the setup guide'})).not.toBeInTheDocument();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('clears the dismissal flag and announces where the guide will appear', async () => {
|
|
56
|
+
dismissWorkspaceSetupChecklist(CURRENT_WORKSPACE_ID);
|
|
57
|
+
const user = userEvent.setup();
|
|
58
|
+
render(<ShowSetupGuideLink workspaceId={CURRENT_WORKSPACE_ID} />);
|
|
59
|
+
|
|
60
|
+
await user.click(screen.getByRole('button', {name: 'Show the setup guide'}));
|
|
61
|
+
|
|
62
|
+
expect(isWorkspaceSetupChecklistDismissed(CURRENT_WORKSPACE_ID)).toBe(false);
|
|
63
|
+
expect(screen.queryByRole('button', {name: 'Show the setup guide'})).not.toBeInTheDocument();
|
|
64
|
+
expect(screen.getByRole('status')).toHaveTextContent(
|
|
65
|
+
'The setup guide will appear on the Projects page.',
|
|
66
|
+
);
|
|
67
|
+
expect(document.activeElement).toBe(screen.getByRole('status'));
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {
|
|
2
|
+
clearWorkspaceSetupChecklistDismissal,
|
|
3
|
+
isWorkspaceSetupChecklistDismissed,
|
|
4
|
+
} from '@shipfox/client-shell/runtime';
|
|
5
|
+
import {ButtonLink} from '@shipfox/react-ui/button';
|
|
6
|
+
import {useEffect, useRef, useState} from 'react';
|
|
7
|
+
|
|
8
|
+
const SETUP_GUIDE_REOPENED_MESSAGE = 'The setup guide will appear on the Projects page.';
|
|
9
|
+
|
|
10
|
+
export function ShowSetupGuideLink({workspaceId}: {workspaceId: string}) {
|
|
11
|
+
const [dismissed, setDismissed] = useState(() => isWorkspaceSetupChecklistDismissed(workspaceId));
|
|
12
|
+
const [statusMessage, setStatusMessage] = useState<string>();
|
|
13
|
+
const statusRef = useRef<HTMLParagraphElement>(null);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const refreshDismissal = () => {
|
|
17
|
+
const nextDismissed = isWorkspaceSetupChecklistDismissed(workspaceId);
|
|
18
|
+
setDismissed(nextDismissed);
|
|
19
|
+
if (nextDismissed) setStatusMessage(undefined);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
refreshDismissal();
|
|
23
|
+
window.addEventListener('focus', refreshDismissal);
|
|
24
|
+
window.addEventListener('storage', refreshDismissal);
|
|
25
|
+
|
|
26
|
+
return () => {
|
|
27
|
+
window.removeEventListener('focus', refreshDismissal);
|
|
28
|
+
window.removeEventListener('storage', refreshDismissal);
|
|
29
|
+
};
|
|
30
|
+
}, [workspaceId]);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (statusMessage) statusRef.current?.focus();
|
|
34
|
+
}, [statusMessage]);
|
|
35
|
+
|
|
36
|
+
if (!dismissed && !statusMessage) return null;
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<>
|
|
40
|
+
{dismissed ? (
|
|
41
|
+
<ButtonLink asChild variant="interactive" underline>
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
onClick={() => {
|
|
45
|
+
clearWorkspaceSetupChecklistDismissal(workspaceId);
|
|
46
|
+
setDismissed(false);
|
|
47
|
+
setStatusMessage(SETUP_GUIDE_REOPENED_MESSAGE);
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
Show the setup guide
|
|
51
|
+
</button>
|
|
52
|
+
</ButtonLink>
|
|
53
|
+
) : null}
|
|
54
|
+
{statusMessage ? (
|
|
55
|
+
<p
|
|
56
|
+
ref={statusRef}
|
|
57
|
+
role="status"
|
|
58
|
+
tabIndex={-1}
|
|
59
|
+
className="text-sm text-foreground-neutral-muted outline-none"
|
|
60
|
+
>
|
|
61
|
+
{statusMessage}
|
|
62
|
+
</p>
|
|
63
|
+
) : null}
|
|
64
|
+
</>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import {listInvitationsResponseSchema} from '@shipfox/api-workspaces-dto';
|
|
2
2
|
import {checkedApiRequest} from '@shipfox/client-api';
|
|
3
|
-
import {queryOptions, useQuery} from '@tanstack/react-query';
|
|
3
|
+
import {queryOptions, type UseQueryOptions, useQuery} from '@tanstack/react-query';
|
|
4
4
|
import type {PendingInvitation} from '#core/membership.js';
|
|
5
5
|
import {toInvitation} from './invitation-mapper.js';
|
|
6
6
|
|
|
7
7
|
export const listInvitationsQueryKey = (workspaceId: string) =>
|
|
8
8
|
['workspaces', workspaceId, 'invitations'] as const;
|
|
9
9
|
|
|
10
|
+
type ListInvitationsQueryOptions = UseQueryOptions<
|
|
11
|
+
PendingInvitation[],
|
|
12
|
+
Error,
|
|
13
|
+
PendingInvitation[],
|
|
14
|
+
ReturnType<typeof listInvitationsQueryKey>
|
|
15
|
+
>;
|
|
16
|
+
|
|
10
17
|
async function listInvitations(workspaceId: string): Promise<PendingInvitation[]> {
|
|
11
18
|
const response = await checkedApiRequest(
|
|
12
19
|
listInvitationsResponseSchema,
|
|
@@ -15,7 +22,7 @@ async function listInvitations(workspaceId: string): Promise<PendingInvitation[]
|
|
|
15
22
|
return response.invitations.map(toInvitation);
|
|
16
23
|
}
|
|
17
24
|
|
|
18
|
-
export function listInvitationsQueryOptions(workspaceId: string) {
|
|
25
|
+
export function listInvitationsQueryOptions(workspaceId: string): ListInvitationsQueryOptions {
|
|
19
26
|
return queryOptions({
|
|
20
27
|
queryKey: listInvitationsQueryKey(workspaceId),
|
|
21
28
|
queryFn: () => listInvitations(workspaceId),
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import {listMembersResponseSchema} from '@shipfox/api-workspaces-dto';
|
|
2
2
|
import {checkedApiRequest} from '@shipfox/client-api';
|
|
3
|
-
import {queryOptions, useQuery} from '@tanstack/react-query';
|
|
3
|
+
import {queryOptions, type UseQueryOptions, useQuery} from '@tanstack/react-query';
|
|
4
4
|
import type {WorkspaceMember} from '#core/membership.js';
|
|
5
5
|
import {toWorkspaceMember} from './membership-mapper.js';
|
|
6
6
|
|
|
7
7
|
export const listMembersQueryKey = (workspaceId: string) =>
|
|
8
8
|
['workspaces', workspaceId, 'members'] as const;
|
|
9
9
|
|
|
10
|
+
type ListMembersQueryOptions = UseQueryOptions<
|
|
11
|
+
WorkspaceMember[],
|
|
12
|
+
Error,
|
|
13
|
+
WorkspaceMember[],
|
|
14
|
+
ReturnType<typeof listMembersQueryKey>
|
|
15
|
+
>;
|
|
16
|
+
|
|
10
17
|
async function listMembers(workspaceId: string): Promise<WorkspaceMember[]> {
|
|
11
18
|
const response = await checkedApiRequest(
|
|
12
19
|
listMembersResponseSchema,
|
|
@@ -15,7 +22,7 @@ async function listMembers(workspaceId: string): Promise<WorkspaceMember[]> {
|
|
|
15
22
|
return response.members.map(toWorkspaceMember);
|
|
16
23
|
}
|
|
17
24
|
|
|
18
|
-
export function listMembersQueryOptions(workspaceId: string) {
|
|
25
|
+
export function listMembersQueryOptions(workspaceId: string): ListMembersQueryOptions {
|
|
19
26
|
return queryOptions({
|
|
20
27
|
queryKey: listMembersQueryKey(workspaceId),
|
|
21
28
|
queryFn: () => listMembers(workspaceId),
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {useForm} from '@tanstack/react-form';
|
|
|
15
15
|
import {useNavigate} from '@tanstack/react-router';
|
|
16
16
|
import {useState} from 'react';
|
|
17
17
|
import {workspaceGeneralErrorToFormError} from '#components/general/form-errors.js';
|
|
18
|
+
import {ShowSetupGuideLink} from '#components/show-setup-guide-link.js';
|
|
18
19
|
import {WorkspaceSettingsShell} from '#components/workspace-settings-shell.js';
|
|
19
20
|
|
|
20
21
|
interface WorkspaceGeneralValues {
|
|
@@ -184,6 +185,8 @@ function WorkspaceGeneralForm({workspace}: {workspace: ReturnType<typeof useActi
|
|
|
184
185
|
</form>
|
|
185
186
|
</PanelBody>
|
|
186
187
|
</Panel>
|
|
188
|
+
|
|
189
|
+
<ShowSetupGuideLink workspaceId={workspace.id} />
|
|
187
190
|
</div>
|
|
188
191
|
|
|
189
192
|
<SlugChangeWarning
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {
|
|
2
|
+
listInvitationsQueryKey,
|
|
3
|
+
listInvitationsQueryOptions,
|
|
4
|
+
listMembersQueryKey,
|
|
5
|
+
listMembersQueryOptions,
|
|
6
|
+
} from './index.js';
|
|
7
|
+
|
|
8
|
+
const WORKSPACE_ID = '11111111-1111-4111-8111-111111111111';
|
|
9
|
+
|
|
10
|
+
describe('@shipfox/client-workspace-settings public query API', () => {
|
|
11
|
+
test('exports membership and invitation query contracts from the package root', () => {
|
|
12
|
+
expect(listMembersQueryKey(WORKSPACE_ID)).toEqual(['workspaces', WORKSPACE_ID, 'members']);
|
|
13
|
+
expect(listInvitationsQueryKey(WORKSPACE_ID)).toEqual([
|
|
14
|
+
'workspaces',
|
|
15
|
+
WORKSPACE_ID,
|
|
16
|
+
'invitations',
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
const members = listMembersQueryOptions(WORKSPACE_ID);
|
|
20
|
+
const invitations = listInvitationsQueryOptions(WORKSPACE_ID);
|
|
21
|
+
|
|
22
|
+
expect(members.queryKey).toEqual(['workspaces', WORKSPACE_ID, 'members']);
|
|
23
|
+
expect(members.queryFn).toEqual(expect.any(Function));
|
|
24
|
+
expect(invitations.queryKey).toEqual(['workspaces', WORKSPACE_ID, 'invitations']);
|
|
25
|
+
expect(invitations.queryFn).toEqual(expect.any(Function));
|
|
26
|
+
});
|
|
27
|
+
});
|