@shipfox/client-projects 0.2.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 (125) hide show
  1. package/.storybook/main.ts +1 -0
  2. package/.storybook/preview.css +7 -0
  3. package/.storybook/preview.tsx +56 -0
  4. package/.turbo/turbo-build.log +2 -0
  5. package/.turbo/turbo-type$colon$emit.log +1 -0
  6. package/.turbo/turbo-type.log +1 -0
  7. package/CHANGELOG.md +207 -0
  8. package/LICENSE +21 -0
  9. package/dist/chrome.d.ts +3 -0
  10. package/dist/chrome.d.ts.map +1 -0
  11. package/dist/chrome.js +42 -0
  12. package/dist/chrome.js.map +1 -0
  13. package/dist/components/model-provider-reminder-banner.d.ts +4 -0
  14. package/dist/components/model-provider-reminder-banner.d.ts.map +1 -0
  15. package/dist/components/model-provider-reminder-banner.js +73 -0
  16. package/dist/components/model-provider-reminder-banner.js.map +1 -0
  17. package/dist/components/project-crumb.d.ts +7 -0
  18. package/dist/components/project-crumb.d.ts.map +1 -0
  19. package/dist/components/project-crumb.js +93 -0
  20. package/dist/components/project-crumb.js.map +1 -0
  21. package/dist/components/project-switcher.d.ts +7 -0
  22. package/dist/components/project-switcher.d.ts.map +1 -0
  23. package/dist/components/project-switcher.js +138 -0
  24. package/dist/components/project-switcher.js.map +1 -0
  25. package/dist/components/source-strip.d.ts +14 -0
  26. package/dist/components/source-strip.d.ts.map +1 -0
  27. package/dist/components/source-strip.js +98 -0
  28. package/dist/components/source-strip.js.map +1 -0
  29. package/dist/components/workspace-setup-guard.d.ts +9 -0
  30. package/dist/components/workspace-setup-guard.d.ts.map +1 -0
  31. package/dist/components/workspace-setup-guard.js +160 -0
  32. package/dist/components/workspace-setup-guard.js.map +1 -0
  33. package/dist/feature.d.ts +35 -0
  34. package/dist/feature.d.ts.map +1 -0
  35. package/dist/feature.js +45 -0
  36. package/dist/feature.js.map +1 -0
  37. package/dist/hooks/api/definitions.d.ts +97 -0
  38. package/dist/hooks/api/definitions.d.ts.map +1 -0
  39. package/dist/hooks/api/definitions.js +55 -0
  40. package/dist/hooks/api/definitions.js.map +1 -0
  41. package/dist/hooks/api/projects.d.ts +93 -0
  42. package/dist/hooks/api/projects.d.ts.map +1 -0
  43. package/dist/hooks/api/projects.js +79 -0
  44. package/dist/hooks/api/projects.js.map +1 -0
  45. package/dist/index.d.ts +13 -0
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +13 -0
  48. package/dist/index.js.map +1 -0
  49. package/dist/pages/create-project-page.d.ts +2 -0
  50. package/dist/pages/create-project-page.d.ts.map +1 -0
  51. package/dist/pages/create-project-page.js +406 -0
  52. package/dist/pages/create-project-page.js.map +1 -0
  53. package/dist/pages/create-project-page.stories.d.ts +24 -0
  54. package/dist/pages/create-project-page.stories.d.ts.map +1 -0
  55. package/dist/pages/create-project-page.stories.js +345 -0
  56. package/dist/pages/create-project-page.stories.js.map +1 -0
  57. package/dist/pages/home-router.d.ts +2 -0
  58. package/dist/pages/home-router.d.ts.map +1 -0
  59. package/dist/pages/home-router.js +7 -0
  60. package/dist/pages/home-router.js.map +1 -0
  61. package/dist/pages/project-workflows-page.d.ts +4 -0
  62. package/dist/pages/project-workflows-page.d.ts.map +1 -0
  63. package/dist/pages/project-workflows-page.js +481 -0
  64. package/dist/pages/project-workflows-page.js.map +1 -0
  65. package/dist/pages/projects-hub-page.d.ts +2 -0
  66. package/dist/pages/projects-hub-page.d.ts.map +1 -0
  67. package/dist/pages/projects-hub-page.js +256 -0
  68. package/dist/pages/projects-hub-page.js.map +1 -0
  69. package/dist/project-error.d.ts +7 -0
  70. package/dist/project-error.d.ts.map +1 -0
  71. package/dist/project-error.js +93 -0
  72. package/dist/project-error.js.map +1 -0
  73. package/dist/routes/create-project.d.ts +6 -0
  74. package/dist/routes/create-project.d.ts.map +1 -0
  75. package/dist/routes/create-project.js +7 -0
  76. package/dist/routes/create-project.js.map +1 -0
  77. package/dist/routes/home.d.ts +6 -0
  78. package/dist/routes/home.d.ts.map +1 -0
  79. package/dist/routes/home.js +7 -0
  80. package/dist/routes/home.js.map +1 -0
  81. package/dist/routes/project-index.d.ts +10 -0
  82. package/dist/routes/project-index.d.ts.map +1 -0
  83. package/dist/routes/project-index.js +15 -0
  84. package/dist/routes/project-index.js.map +1 -0
  85. package/dist/routes/workflows.d.ts +5 -0
  86. package/dist/routes/workflows.d.ts.map +1 -0
  87. package/dist/routes/workflows.js +16 -0
  88. package/dist/routes/workflows.js.map +1 -0
  89. package/dist/tsconfig.test.tsbuildinfo +1 -0
  90. package/package.json +120 -0
  91. package/src/chrome.tsx +26 -0
  92. package/src/components/model-provider-reminder-banner.tsx +78 -0
  93. package/src/components/project-crumb.tsx +70 -0
  94. package/src/components/project-switcher.test.tsx +91 -0
  95. package/src/components/project-switcher.tsx +127 -0
  96. package/src/components/source-strip.tsx +91 -0
  97. package/src/components/workspace-setup-guard.test.tsx +534 -0
  98. package/src/components/workspace-setup-guard.tsx +185 -0
  99. package/src/feature.ts +44 -0
  100. package/src/hooks/api/definitions.ts +50 -0
  101. package/src/hooks/api/projects.test.ts +78 -0
  102. package/src/hooks/api/projects.ts +75 -0
  103. package/src/index.ts +15 -0
  104. package/src/pages/create-project-page.stories.tsx +344 -0
  105. package/src/pages/create-project-page.test.tsx +317 -0
  106. package/src/pages/create-project-page.tsx +386 -0
  107. package/src/pages/home-router.test.tsx +27 -0
  108. package/src/pages/home-router.tsx +5 -0
  109. package/src/pages/project-workflows-page.test.tsx +258 -0
  110. package/src/pages/project-workflows-page.tsx +445 -0
  111. package/src/pages/projects-hub-page.test.tsx +313 -0
  112. package/src/pages/projects-hub-page.tsx +246 -0
  113. package/src/project-error.test.ts +59 -0
  114. package/src/project-error.ts +106 -0
  115. package/src/routes/create-project.tsx +4 -0
  116. package/src/routes/home.tsx +4 -0
  117. package/src/routes/project-index.tsx +11 -0
  118. package/src/routes/workflows.tsx +10 -0
  119. package/test/pages.tsx +151 -0
  120. package/test/setup.ts +3 -0
  121. package/tsconfig.build.json +9 -0
  122. package/tsconfig.build.tsbuildinfo +1 -0
  123. package/tsconfig.json +3 -0
  124. package/tsconfig.test.json +8 -0
  125. package/vitest.config.ts +70 -0
@@ -0,0 +1,106 @@
1
+ import {ApiError} from '@shipfox/client-api';
2
+
3
+ export interface ProjectErrorCopy {
4
+ title: string;
5
+ message: string;
6
+ existingProjectId?: string | undefined;
7
+ }
8
+
9
+ function isRecord(value: unknown): value is Record<string, unknown> {
10
+ return typeof value === 'object' && value !== null;
11
+ }
12
+
13
+ function apiDetails(error: ApiError): Record<string, unknown> {
14
+ if (!isRecord(error.details)) return {};
15
+ const details = error.details.details;
16
+ return isRecord(details) ? details : {};
17
+ }
18
+
19
+ function stringDetail(error: ApiError, key: string): string | undefined {
20
+ const value = apiDetails(error)[key];
21
+ return typeof value === 'string' ? value : undefined;
22
+ }
23
+
24
+ function numberDetail(error: ApiError, key: string): number | undefined {
25
+ const value = apiDetails(error)[key];
26
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
27
+ }
28
+
29
+ export function projectErrorCopy(error: unknown): ProjectErrorCopy {
30
+ if (!(error instanceof ApiError)) {
31
+ return {
32
+ title: 'Something went wrong',
33
+ message: 'Try again in a moment.',
34
+ };
35
+ }
36
+
37
+ if (error.code === 'network-error') {
38
+ return {
39
+ title: 'Network problem',
40
+ message: 'We could not reach the API. Check your connection and try again.',
41
+ };
42
+ }
43
+ if (error.code === 'repository-not-found') {
44
+ return {
45
+ title: 'Repository not found',
46
+ message: 'Choose another repository and try again.',
47
+ };
48
+ }
49
+ if (error.code === 'source-connection-not-found') {
50
+ return {
51
+ title: 'Source connection not found',
52
+ message: 'Reconnect source control and try again.',
53
+ };
54
+ }
55
+ if (error.code === 'source-connection-inactive') {
56
+ return {
57
+ title: 'Source connection inactive',
58
+ message: 'Reconnect or choose another source-control connection.',
59
+ };
60
+ }
61
+ if (error.code === 'access-denied') {
62
+ return {
63
+ title: 'Repository access denied',
64
+ message: 'The selected connection cannot access this repository.',
65
+ };
66
+ }
67
+ if (error.code === 'rate-limited') {
68
+ const retryAfter = numberDetail(error, 'retry_after_seconds');
69
+ return {
70
+ title: 'Provider rate limited',
71
+ message: retryAfter
72
+ ? `Try again in about ${retryAfter} seconds.`
73
+ : 'The provider is asking us to slow down. Try again shortly.',
74
+ };
75
+ }
76
+ if (error.code === 'timeout') {
77
+ return {
78
+ title: 'Provider timed out',
79
+ message: 'The provider did not respond in time. Try again.',
80
+ };
81
+ }
82
+ if (error.code === 'provider-unavailable') {
83
+ return {
84
+ title: 'Provider unavailable',
85
+ message: 'The provider is unavailable right now. Try again shortly.',
86
+ };
87
+ }
88
+ if (error.code === 'malformed-provider-response') {
89
+ return {
90
+ title: 'Provider response changed',
91
+ message: 'The provider returned data Shipfox could not understand.',
92
+ };
93
+ }
94
+ if (error.code === 'project-already-exists') {
95
+ return {
96
+ title: 'Project already exists',
97
+ message: 'This repository is already connected to a Shipfox project.',
98
+ existingProjectId: stringDetail(error, 'existing_project_id'),
99
+ };
100
+ }
101
+
102
+ return {
103
+ title: 'Project request failed',
104
+ message: error.message || 'Try again in a moment.',
105
+ };
106
+ }
@@ -0,0 +1,4 @@
1
+ import {defineRoute} from '@shipfox/client-shell/runtime';
2
+ import {CreateProjectPage} from '#pages/create-project-page.js';
3
+
4
+ export default defineRoute({component: CreateProjectPage});
@@ -0,0 +1,4 @@
1
+ import {defineRoute} from '@shipfox/client-shell/runtime';
2
+ import {HomeRouter} from '#pages/home-router.js';
3
+
4
+ export default defineRoute({component: HomeRouter});
@@ -0,0 +1,11 @@
1
+ import {defineRoute} from '@shipfox/client-shell/runtime';
2
+ import {redirect} from '@tanstack/react-router';
3
+
4
+ export default defineRoute({
5
+ beforeLoad: ({params}: {params: {wid: string; pid: string}}) => {
6
+ throw redirect({
7
+ to: '/workspaces/$wid/projects/$pid/runs',
8
+ params: {wid: params.wid, pid: params.pid},
9
+ });
10
+ },
11
+ });
@@ -0,0 +1,10 @@
1
+ import {defineRoute} from '@shipfox/client-shell/runtime';
2
+ import {useParams} from '@tanstack/react-router';
3
+ import {ProjectWorkflowsPage} from '#pages/project-workflows-page.js';
4
+
5
+ export default defineRoute({
6
+ component: () => {
7
+ const {pid} = useParams({strict: false}) as {pid: string};
8
+ return <ProjectWorkflowsPage projectId={pid} />;
9
+ },
10
+ });
package/test/pages.tsx ADDED
@@ -0,0 +1,151 @@
1
+ import {configureApiClient} from '@shipfox/client-api';
2
+ import {type AuthState, authStateAtom} from '@shipfox/client-auth';
3
+ import {Toaster} from '@shipfox/react-ui/toast';
4
+ import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
5
+ import {
6
+ createMemoryHistory,
7
+ createRootRoute,
8
+ createRoute,
9
+ createRouter,
10
+ Outlet,
11
+ RouterProvider,
12
+ useParams,
13
+ } from '@tanstack/react-router';
14
+ import {type RenderResult, render} from '@testing-library/react';
15
+ import {createStore, Provider as JotaiProvider} from 'jotai';
16
+ import type {ReactElement} from 'react';
17
+ import {CreateProjectPage} from '#pages/create-project-page.js';
18
+ import {ProjectWorkflowsPage} from '#pages/project-workflows-page.js';
19
+ import {ProjectsHubPage} from '#pages/projects-hub-page.js';
20
+
21
+ // All test renders that exercise pages requiring `useActiveWorkspace()` mount
22
+ // under `/workspaces/$wid`. The seeded workspace id (see authState) is the wid
23
+ // used in routes.
24
+ export const PROJECT_TEST_WID = '11111111-1111-4111-8111-111111111111';
25
+
26
+ const authState: AuthState = {
27
+ status: 'authenticated',
28
+ token: 'token',
29
+ user: {
30
+ id: '22222222-2222-4222-8222-222222222222',
31
+ email: 'user@example.com',
32
+ name: null,
33
+ email_verified_at: new Date().toISOString(),
34
+ status: 'active',
35
+ created_at: new Date().toISOString(),
36
+ updated_at: new Date().toISOString(),
37
+ },
38
+ workspaces: [{id: PROJECT_TEST_WID, name: 'Acme', membershipId: 'm-1'}],
39
+ };
40
+
41
+ export function jsonResponse(body: unknown, init: ResponseInit = {}) {
42
+ return new Response(JSON.stringify(body), {
43
+ status: 200,
44
+ headers: {'content-type': 'application/json'},
45
+ ...init,
46
+ });
47
+ }
48
+
49
+ // The param-carrying routes below declare `$pid`/`$runId` in their path, so the value is
50
+ // always present when their fallback page renders. A missing param means the harness
51
+ // wired a route wrong — fail loudly instead of rendering a page with a fabricated id.
52
+ function requireParam(value: string | undefined, name: string): string {
53
+ if (value === undefined) {
54
+ throw new Error(`Test router: route param "${name}" is missing`);
55
+ }
56
+ return value;
57
+ }
58
+
59
+ function createTestRouter(path: string, element: ReactElement) {
60
+ const initialPath = path.split('?')[0] ?? path;
61
+ const rootRoute = createRootRoute({component: Outlet});
62
+ const indexRoute = createRoute({
63
+ getParentRoute: () => rootRoute,
64
+ path: '/',
65
+ component: () => (initialPath === '/' ? element : <ProjectsHubPage />),
66
+ });
67
+ const workspaceRoute = createRoute({
68
+ getParentRoute: () => rootRoute,
69
+ path: '/workspaces/$wid',
70
+ component: () =>
71
+ initialPath === `/workspaces/${PROJECT_TEST_WID}` ? element : <ProjectsHubPage />,
72
+ });
73
+ const workspaceNewProjectRoute = createRoute({
74
+ getParentRoute: () => rootRoute,
75
+ path: '/workspaces/$wid/projects/new',
76
+ component: () =>
77
+ initialPath === `/workspaces/${PROJECT_TEST_WID}/projects/new` ? (
78
+ element
79
+ ) : (
80
+ <CreateProjectPage />
81
+ ),
82
+ });
83
+ const integrationsRoute = createRoute({
84
+ getParentRoute: () => rootRoute,
85
+ path: '/workspaces/$wid/integrations',
86
+ component: () => <div>Integrations gallery placeholder</div>,
87
+ });
88
+ const modelProviderSettingsRoute = createRoute({
89
+ getParentRoute: () => rootRoute,
90
+ path: '/workspaces/$wid/settings/agents',
91
+ component: () => <div>Agent settings placeholder</div>,
92
+ });
93
+ const projectDetailRoute = createRoute({
94
+ getParentRoute: () => rootRoute,
95
+ path: '/workspaces/$wid/projects/$pid',
96
+ component: () => {
97
+ const params = useParams({strict: false}) as {pid?: string};
98
+ if (initialPath === `/workspaces/${PROJECT_TEST_WID}/projects/${params.pid}`) {
99
+ return element;
100
+ }
101
+
102
+ // The harness only needs to prove project-detail navigation landed on the
103
+ // Runs surface, so it renders a minimal stand-in rather than depending on
104
+ // @shipfox/client-workflows.
105
+ return <h1>Runs</h1>;
106
+ },
107
+ });
108
+ const projectWorkflowsRoute = createRoute({
109
+ getParentRoute: () => rootRoute,
110
+ path: '/workspaces/$wid/projects/$pid/workflows',
111
+ component: () => {
112
+ const params = useParams({strict: false}) as {pid?: string};
113
+ if (initialPath === `/workspaces/${PROJECT_TEST_WID}/projects/${params.pid}/workflows`) {
114
+ return element;
115
+ }
116
+
117
+ return <ProjectWorkflowsPage projectId={requireParam(params.pid, 'pid')} />;
118
+ },
119
+ });
120
+
121
+ return createRouter({
122
+ history: createMemoryHistory({initialEntries: [path]}),
123
+ routeTree: rootRoute.addChildren([
124
+ indexRoute,
125
+ workspaceRoute,
126
+ workspaceNewProjectRoute,
127
+ integrationsRoute,
128
+ modelProviderSettingsRoute,
129
+ projectDetailRoute,
130
+ projectWorkflowsRoute,
131
+ ]),
132
+ });
133
+ }
134
+
135
+ export function renderProjectPage(path: string, element: ReactElement): RenderResult {
136
+ const queryClient = new QueryClient({defaultOptions: {queries: {retry: false}}});
137
+ const router = createTestRouter(path, element);
138
+ const store = createStore();
139
+ store.set(authStateAtom, authState);
140
+
141
+ configureApiClient({baseUrl: 'https://api.example.test'});
142
+
143
+ return render(
144
+ <QueryClientProvider client={queryClient}>
145
+ <JotaiProvider store={store}>
146
+ <RouterProvider router={router} />
147
+ <Toaster />
148
+ </JotaiProvider>
149
+ </QueryClientProvider>,
150
+ );
151
+ }
package/test/setup.ts ADDED
@@ -0,0 +1,3 @@
1
+ import {installClientDomTestEnv} from '@shipfox/client-test-setup';
2
+
3
+ installClientDomTestEnv();
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "@shipfox/ts-config/react",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "dist"
6
+ },
7
+ "include": ["src"],
8
+ "exclude": ["**/*.test.tsx", "**/*.test.ts"]
9
+ }