@shipfox/client-projects 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 +42 -0
- package/dist/components/source-strip.d.ts.map +1 -1
- package/dist/components/source-strip.js +49 -45
- package/dist/components/source-strip.js.map +1 -1
- package/dist/pages/create-project-page.d.ts.map +1 -1
- package/dist/pages/create-project-page.js +161 -175
- package/dist/pages/create-project-page.js.map +1 -1
- package/dist/pages/create-project-page.stories.js +1 -1
- package/dist/pages/create-project-page.stories.js.map +1 -1
- package/dist/pages/project-settings-page.d.ts.map +1 -1
- package/dist/pages/project-settings-page.js +82 -84
- package/dist/pages/project-settings-page.js.map +1 -1
- package/dist/pages/project-settings-page.stories.js +1 -1
- package/dist/pages/project-settings-page.stories.js.map +1 -1
- package/dist/pages/projects-hub-page.d.ts.map +1 -1
- package/dist/pages/projects-hub-page.js +160 -159
- package/dist/pages/projects-hub-page.js.map +1 -1
- package/dist/pages/projects-hub-page.stories.d.ts +22 -0
- package/dist/pages/projects-hub-page.stories.d.ts.map +1 -0
- package/dist/pages/projects-hub-page.stories.js +214 -0
- package/dist/pages/projects-hub-page.stories.js.map +1 -0
- package/dist/routes/create-project.d.ts +3 -0
- package/dist/routes/create-project.js +3 -0
- package/dist/routes/create-project.js.map +1 -1
- package/dist/routes/home.d.ts +3 -0
- package/dist/routes/home.js +3 -0
- package/dist/routes/home.js.map +1 -1
- package/dist/routes/project-index.d.ts +3 -0
- package/dist/routes/project-index.d.ts.map +1 -1
- package/dist/routes/project-index.js +3 -0
- package/dist/routes/project-index.js.map +1 -1
- package/dist/routes/project-settings-index.d.ts +3 -0
- package/dist/routes/project-settings-index.d.ts.map +1 -1
- package/dist/routes/project-settings-index.js +3 -0
- package/dist/routes/project-settings-index.js.map +1 -1
- package/dist/routes/project-settings.d.ts +3 -0
- package/dist/routes/project-settings.js +3 -0
- package/dist/routes/project-settings.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/src/components/source-strip.tsx +29 -26
- package/src/pages/create-project-page.stories.tsx +1 -1
- package/src/pages/create-project-page.test.tsx +85 -1
- package/src/pages/create-project-page.tsx +165 -153
- package/src/pages/home-router.test.tsx +2 -1
- package/src/pages/project-settings-page.stories.tsx +1 -1
- package/src/pages/project-settings-page.test.tsx +10 -1
- package/src/pages/project-settings-page.tsx +88 -76
- package/src/pages/projects-hub-page.stories.tsx +203 -0
- package/src/pages/projects-hub-page.test.tsx +90 -10
- package/src/pages/projects-hub-page.tsx +136 -125
- package/src/routes/create-project.tsx +4 -1
- package/src/routes/home.tsx +1 -0
- package/src/routes/project-index.tsx +1 -0
- package/src/routes/project-settings-index.tsx +1 -0
- package/src/routes/project-settings.tsx +4 -1
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -1 +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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"}
|
|
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-subtle-base 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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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"projects-hub-page.d.ts","sourceRoot":"","sources":["../../src/pages/projects-hub-page.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"projects-hub-page.d.ts","sourceRoot":"","sources":["../../src/pages/projects-hub-page.tsx"],"names":[],"mappings":"AA6BA,wBAAgB,eAAe,CAAC,EAAC,MAAW,EAAC,EAAE;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAC,+BAwH/D"}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useActiveWorkspace } from '@shipfox/client-auth';
|
|
3
3
|
import { ConnectionStatusBadge, IntegrationIcon, useIntegrationConnectionsQuery } from '@shipfox/client-integrations';
|
|
4
4
|
import { QueryLoadError } from '@shipfox/client-ui';
|
|
5
5
|
import { Button } from '@shipfox/react-ui/button';
|
|
6
6
|
import { Callout } from '@shipfox/react-ui/callout';
|
|
7
|
-
import { Card } from '@shipfox/react-ui/card';
|
|
8
7
|
import { EmptyState } from '@shipfox/react-ui/empty-state';
|
|
9
8
|
import { Icon } from '@shipfox/react-ui/icon';
|
|
10
9
|
import { Input } from '@shipfox/react-ui/input';
|
|
10
|
+
import { Panel, PanelActions, PanelBody, PanelCell, PanelCellAction, PanelGrid, PanelHeader } from '@shipfox/react-ui/panel';
|
|
11
11
|
import { Skeleton } from '@shipfox/react-ui/skeleton';
|
|
12
|
-
import {
|
|
12
|
+
import { Text } from '@shipfox/react-ui/typography';
|
|
13
13
|
import { Link, useNavigate } from '@tanstack/react-router';
|
|
14
14
|
import { ModelProviderReminderBanner } from '#components/model-provider-reminder-banner.js';
|
|
15
15
|
import { useProjectsInfiniteQuery } from '#hooks/api/projects.js';
|
|
@@ -32,120 +32,125 @@ export function ProjectsHubPage({ search = '' }) {
|
|
|
32
32
|
return /*#__PURE__*/ _jsxs("div", {
|
|
33
33
|
className: "flex w-full flex-col gap-section",
|
|
34
34
|
children: [
|
|
35
|
-
/*#__PURE__*/ _jsxs("header", {
|
|
36
|
-
className: "flex flex-col gap-group",
|
|
37
|
-
children: [
|
|
38
|
-
/*#__PURE__*/ _jsx("div", {
|
|
39
|
-
className: "flex items-start justify-between gap-section max-[640px]:flex-col",
|
|
40
|
-
children: /*#__PURE__*/ _jsx(Header, {
|
|
41
|
-
variant: "h2",
|
|
42
|
-
children: "Projects"
|
|
43
|
-
})
|
|
44
|
-
}),
|
|
45
|
-
/*#__PURE__*/ _jsxs("div", {
|
|
46
|
-
className: "flex items-center gap-cluster max-[640px]:flex-col max-[640px]:items-stretch",
|
|
47
|
-
children: [
|
|
48
|
-
/*#__PURE__*/ _jsx(Input, {
|
|
49
|
-
type: "search",
|
|
50
|
-
value: search,
|
|
51
|
-
onChange: (event)=>{
|
|
52
|
-
const next = event.target.value.trim();
|
|
53
|
-
navigate({
|
|
54
|
-
search: next ? {
|
|
55
|
-
search: next
|
|
56
|
-
} : {},
|
|
57
|
-
replace: true
|
|
58
|
-
});
|
|
59
|
-
},
|
|
60
|
-
placeholder: "Search projects…",
|
|
61
|
-
"aria-label": "Search projects",
|
|
62
|
-
className: "flex-1",
|
|
63
|
-
iconLeft: /*#__PURE__*/ _jsx(Icon, {
|
|
64
|
-
name: "searchLine",
|
|
65
|
-
className: "size-16"
|
|
66
|
-
}),
|
|
67
|
-
iconRight: isSearching ? /*#__PURE__*/ _jsx(Icon, {
|
|
68
|
-
name: "spinner",
|
|
69
|
-
size: 16,
|
|
70
|
-
className: "text-foreground-neutral-muted",
|
|
71
|
-
"aria-hidden": "true"
|
|
72
|
-
}) : undefined
|
|
73
|
-
}),
|
|
74
|
-
/*#__PURE__*/ _jsx(Button, {
|
|
75
|
-
asChild: true,
|
|
76
|
-
iconLeft: "addLine",
|
|
77
|
-
className: "shrink-0 max-[640px]:w-full",
|
|
78
|
-
children: /*#__PURE__*/ _jsx(Link, {
|
|
79
|
-
to: "/w/$workspaceSlug/projects/new",
|
|
80
|
-
params: {
|
|
81
|
-
workspaceSlug: workspace.slug
|
|
82
|
-
},
|
|
83
|
-
children: "New project"
|
|
84
|
-
})
|
|
85
|
-
})
|
|
86
|
-
]
|
|
87
|
-
})
|
|
88
|
-
]
|
|
89
|
-
}),
|
|
90
35
|
/*#__PURE__*/ _jsx(ModelProviderReminderBanner, {
|
|
91
36
|
workspaceId: workspace.id
|
|
92
37
|
}),
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
38
|
+
/*#__PURE__*/ _jsx("section", {
|
|
39
|
+
"aria-label": "Projects",
|
|
40
|
+
children: /*#__PURE__*/ _jsxs(Panel, {
|
|
41
|
+
children: [
|
|
42
|
+
/*#__PURE__*/ _jsxs(PanelHeader, {
|
|
43
|
+
className: "flex-wrap max-[640px]:items-stretch",
|
|
44
|
+
children: [
|
|
45
|
+
/*#__PURE__*/ _jsx("div", {
|
|
46
|
+
className: "min-w-0 flex-1 max-[640px]:basis-full",
|
|
47
|
+
children: /*#__PURE__*/ _jsx(Input, {
|
|
48
|
+
type: "search",
|
|
49
|
+
value: search,
|
|
50
|
+
onChange: (event)=>{
|
|
51
|
+
const next = event.target.value.trim();
|
|
52
|
+
navigate({
|
|
53
|
+
search: next ? {
|
|
54
|
+
search: next
|
|
55
|
+
} : {},
|
|
56
|
+
replace: true
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
placeholder: "Search projects…",
|
|
60
|
+
"aria-label": "Search projects",
|
|
61
|
+
iconLeft: /*#__PURE__*/ _jsx(Icon, {
|
|
62
|
+
name: "searchLine",
|
|
63
|
+
className: "size-16"
|
|
64
|
+
}),
|
|
65
|
+
iconRight: isSearching ? /*#__PURE__*/ _jsx(Icon, {
|
|
66
|
+
name: "spinner",
|
|
67
|
+
size: 16,
|
|
68
|
+
className: "text-foreground-neutral-muted",
|
|
69
|
+
"aria-hidden": "true"
|
|
70
|
+
}) : undefined
|
|
71
|
+
})
|
|
72
|
+
}),
|
|
73
|
+
/*#__PURE__*/ _jsx(PanelActions, {
|
|
74
|
+
className: "max-[640px]:ml-0 max-[640px]:w-full",
|
|
75
|
+
children: /*#__PURE__*/ _jsx(Button, {
|
|
76
|
+
asChild: true,
|
|
77
|
+
iconLeft: "addLine",
|
|
78
|
+
className: "max-[640px]:w-full",
|
|
79
|
+
children: /*#__PURE__*/ _jsx(Link, {
|
|
80
|
+
to: "/w/$workspaceSlug/projects/new",
|
|
81
|
+
params: {
|
|
82
|
+
workspaceSlug: workspace.slug
|
|
83
|
+
},
|
|
84
|
+
children: "New project"
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
]
|
|
89
|
+
}),
|
|
90
|
+
/*#__PURE__*/ _jsxs(PanelBody, {
|
|
91
|
+
children: [
|
|
92
|
+
isInitialLoading || search && hasNoData && query.isFetching ? /*#__PURE__*/ _jsx(ProjectsSkeleton, {}) : null,
|
|
93
|
+
query.isError && hasNoData ? /*#__PURE__*/ _jsx(QueryLoadError, {
|
|
94
|
+
query: query,
|
|
95
|
+
subject: "projects",
|
|
96
|
+
variant: "panel"
|
|
97
|
+
}) : null,
|
|
98
|
+
!isInitialLoading && !query.isError && projects.length === 0 && !search ? /*#__PURE__*/ _jsx(EmptyProjects, {
|
|
99
|
+
workspaceSlug: workspace.slug
|
|
100
|
+
}) : null,
|
|
101
|
+
!query.isFetching && !query.isError && projects.length === 0 && search ? /*#__PURE__*/ _jsx(NoSearchResults, {
|
|
102
|
+
search: search,
|
|
103
|
+
onClear: ()=>navigate({
|
|
104
|
+
search: {},
|
|
105
|
+
replace: true
|
|
106
|
+
})
|
|
107
|
+
}) : null,
|
|
108
|
+
projects.length > 0 ? /*#__PURE__*/ _jsxs(_Fragment, {
|
|
109
|
+
children: [
|
|
110
|
+
/*#__PURE__*/ _jsx(PanelGrid, {
|
|
111
|
+
"aria-label": "Projects list",
|
|
112
|
+
children: projects.map((project)=>/*#__PURE__*/ _jsx(ProjectCell, {
|
|
113
|
+
project: project,
|
|
114
|
+
connection: connectionsById.get(project.source.connectionId),
|
|
115
|
+
connectionsResolved: connectionsQuery.isSuccess,
|
|
116
|
+
connectionsSettled: connectionsQuery.isSuccess || connectionsQuery.isError,
|
|
117
|
+
workspaceSlug: workspace.slug
|
|
118
|
+
}, project.id))
|
|
119
|
+
}),
|
|
120
|
+
query.error && query.data ? /*#__PURE__*/ _jsx("div", {
|
|
121
|
+
className: "border-t border-border-neutral-base p-panel-compact",
|
|
122
|
+
children: /*#__PURE__*/ _jsx(Callout, {
|
|
123
|
+
role: "alert",
|
|
124
|
+
type: "error",
|
|
125
|
+
children: /*#__PURE__*/ _jsx(Text, {
|
|
126
|
+
size: "sm",
|
|
127
|
+
children: "Could not load the next page. Existing projects are still shown."
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
}) : null,
|
|
131
|
+
query.hasNextPage ? /*#__PURE__*/ _jsx("div", {
|
|
132
|
+
className: "flex justify-center border-t border-border-neutral-base p-panel-compact",
|
|
133
|
+
children: /*#__PURE__*/ _jsx(Button, {
|
|
134
|
+
variant: "secondary",
|
|
135
|
+
isLoading: query.isFetchingNextPage,
|
|
136
|
+
onClick: ()=>query.fetchNextPage(),
|
|
137
|
+
children: "Load more"
|
|
138
|
+
})
|
|
139
|
+
}) : null
|
|
140
|
+
]
|
|
141
|
+
}) : null
|
|
142
|
+
]
|
|
137
143
|
})
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
})
|
|
144
|
+
]
|
|
145
|
+
})
|
|
146
|
+
})
|
|
141
147
|
]
|
|
142
148
|
});
|
|
143
149
|
}
|
|
144
150
|
function ProjectsSkeleton() {
|
|
145
|
-
return /*#__PURE__*/ _jsx(
|
|
151
|
+
return /*#__PURE__*/ _jsx(PanelGrid, {
|
|
146
152
|
role: "status",
|
|
147
153
|
"aria-label": "Loading projects",
|
|
148
|
-
className: "grid grid-cols-2 gap-cluster max-[760px]:grid-cols-1",
|
|
149
154
|
children: [
|
|
150
155
|
0,
|
|
151
156
|
1,
|
|
@@ -153,20 +158,17 @@ function ProjectsSkeleton() {
|
|
|
153
158
|
3,
|
|
154
159
|
4,
|
|
155
160
|
5
|
|
156
|
-
].map((row)=>/*#__PURE__*/ _jsx(
|
|
157
|
-
children: /*#__PURE__*/
|
|
158
|
-
className: "
|
|
159
|
-
children:
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
})
|
|
168
|
-
]
|
|
169
|
-
})
|
|
161
|
+
].map((row)=>/*#__PURE__*/ _jsx(PanelCell, {
|
|
162
|
+
children: /*#__PURE__*/ _jsxs("div", {
|
|
163
|
+
className: "flex items-center gap-cluster px-row py-row",
|
|
164
|
+
children: [
|
|
165
|
+
/*#__PURE__*/ _jsx(Skeleton, {
|
|
166
|
+
className: "size-24 shrink-0"
|
|
167
|
+
}),
|
|
168
|
+
/*#__PURE__*/ _jsx(Skeleton, {
|
|
169
|
+
className: "h-16 w-1/2"
|
|
170
|
+
})
|
|
171
|
+
]
|
|
170
172
|
})
|
|
171
173
|
}, row))
|
|
172
174
|
});
|
|
@@ -176,6 +178,7 @@ function EmptyProjects({ workspaceSlug }) {
|
|
|
176
178
|
icon: "folderLine",
|
|
177
179
|
title: "Create your first project",
|
|
178
180
|
description: "Connect a repository-backed project to start building workflows.",
|
|
181
|
+
variant: "panel",
|
|
179
182
|
action: /*#__PURE__*/ _jsx(Button, {
|
|
180
183
|
asChild: true,
|
|
181
184
|
iconRight: "chevronRight",
|
|
@@ -194,6 +197,7 @@ function NoSearchResults({ search, onClear }) {
|
|
|
194
197
|
icon: "searchLine",
|
|
195
198
|
title: `No projects match “${search}”`,
|
|
196
199
|
description: "Try a different search, or clear it to see all projects.",
|
|
200
|
+
variant: "panel",
|
|
197
201
|
action: /*#__PURE__*/ _jsx(Button, {
|
|
198
202
|
size: "sm",
|
|
199
203
|
variant: "secondary",
|
|
@@ -202,48 +206,45 @@ function NoSearchResults({ search, onClear }) {
|
|
|
202
206
|
})
|
|
203
207
|
});
|
|
204
208
|
}
|
|
205
|
-
function
|
|
209
|
+
function ProjectCell({ project, connection, connectionsResolved, connectionsSettled, workspaceSlug }) {
|
|
206
210
|
// On a resolved fetch, `active` carries no badge while a missing connection
|
|
207
211
|
// reads as an error so a broken source is still flagged. An unresolved or
|
|
208
212
|
// failed fetch shows nothing, so a fetch failure never flags every card.
|
|
209
213
|
const status = connectionsResolved ? connection?.lifecycleStatus ?? 'error' : undefined;
|
|
210
|
-
return /*#__PURE__*/ _jsx(
|
|
211
|
-
children: /*#__PURE__*/ _jsx(
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
workspaceSlug,
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
})
|
|
245
|
-
]
|
|
246
|
-
})
|
|
214
|
+
return /*#__PURE__*/ _jsx(PanelCell, {
|
|
215
|
+
children: /*#__PURE__*/ _jsx(PanelCellAction, {
|
|
216
|
+
asChild: true,
|
|
217
|
+
children: /*#__PURE__*/ _jsxs(Link, {
|
|
218
|
+
to: "/w/$workspaceSlug/p/$projectSlug",
|
|
219
|
+
params: {
|
|
220
|
+
workspaceSlug,
|
|
221
|
+
projectSlug: project.slug
|
|
222
|
+
},
|
|
223
|
+
children: [
|
|
224
|
+
connectionsSettled ? /*#__PURE__*/ _jsx(IntegrationIcon, {
|
|
225
|
+
source: connection?.provider,
|
|
226
|
+
"aria-hidden": true,
|
|
227
|
+
className: "size-24 shrink-0 text-foreground-neutral-base"
|
|
228
|
+
}) : /*#__PURE__*/ _jsx(Skeleton, {
|
|
229
|
+
className: "size-24 shrink-0"
|
|
230
|
+
}),
|
|
231
|
+
/*#__PURE__*/ _jsxs("span", {
|
|
232
|
+
className: "flex min-w-0 flex-1 items-center gap-inline",
|
|
233
|
+
children: [
|
|
234
|
+
/*#__PURE__*/ _jsx(Text, {
|
|
235
|
+
as: "span",
|
|
236
|
+
size: "md",
|
|
237
|
+
bold: true,
|
|
238
|
+
className: "truncate",
|
|
239
|
+
children: project.name
|
|
240
|
+
}),
|
|
241
|
+
status ? /*#__PURE__*/ _jsx(ConnectionStatusBadge, {
|
|
242
|
+
status: status,
|
|
243
|
+
className: "shrink-0"
|
|
244
|
+
}) : null
|
|
245
|
+
]
|
|
246
|
+
})
|
|
247
|
+
]
|
|
247
248
|
})
|
|
248
249
|
})
|
|
249
250
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/pages/projects-hub-page.tsx"],"sourcesContent":["import {useActiveWorkspace} from '@shipfox/client-auth';\nimport {\n ConnectionStatusBadge,\n type IntegrationConnection,\n IntegrationIcon,\n useIntegrationConnectionsQuery,\n} from '@shipfox/client-integrations';\nimport {QueryLoadError} from '@shipfox/client-ui';\nimport {Button} from '@shipfox/react-ui/button';\nimport {Callout} from '@shipfox/react-ui/callout';\nimport {Card} from '@shipfox/react-ui/card';\nimport {EmptyState} from '@shipfox/react-ui/empty-state';\nimport {Icon} from '@shipfox/react-ui/icon';\nimport {Input} from '@shipfox/react-ui/input';\nimport {Skeleton} from '@shipfox/react-ui/skeleton';\nimport {Header, Text} from '@shipfox/react-ui/typography';\nimport {Link, useNavigate} from '@tanstack/react-router';\nimport {ModelProviderReminderBanner} from '#components/model-provider-reminder-banner.js';\nimport type {Project} from '#core/project.js';\nimport {useProjectsInfiniteQuery} from '#hooks/api/projects.js';\n\nexport function ProjectsHubPage({search = ''}: {search?: string}) {\n const workspace = useActiveWorkspace();\n const navigate = useNavigate();\n const query = useProjectsInfiniteQuery(workspace.id, search || undefined);\n const projects = query.data?.pages.flatMap((page) => page.projects) ?? [];\n\n // The provider logo and connection health live on the integration connection,\n // not the project, so resolve them once for the whole list and index by id.\n // Skip the fetch when there are no cards to annotate.\n const connectionsQuery = useIntegrationConnectionsQuery(\n projects.length > 0 ? workspace.id : undefined,\n );\n const connectionsById = new Map(\n (connectionsQuery.data ?? []).map((connection) => [connection.id, connection]),\n );\n\n const isInitialLoading = query.isPending;\n const isSearching = Boolean(search) && query.isFetching && !query.isFetchingNextPage;\n const hasNoData = !query.data;\n\n return (\n <div className=\"flex w-full flex-col gap-section\">\n <header className=\"flex flex-col gap-group\">\n <div className=\"flex items-start justify-between gap-section max-[640px]:flex-col\">\n <Header variant=\"h2\">Projects</Header>\n </div>\n <div className=\"flex items-center gap-cluster max-[640px]:flex-col max-[640px]:items-stretch\">\n <Input\n type=\"search\"\n value={search}\n onChange={(event) => {\n const next = event.target.value.trim();\n navigate({search: (next ? {search: next} : {}) as never, replace: true});\n }}\n placeholder=\"Search projects…\"\n aria-label=\"Search projects\"\n className=\"flex-1\"\n iconLeft={<Icon name=\"searchLine\" className=\"size-16\" />}\n iconRight={\n isSearching ? (\n <Icon\n name=\"spinner\"\n size={16}\n className=\"text-foreground-neutral-muted\"\n aria-hidden=\"true\"\n />\n ) : undefined\n }\n />\n <Button asChild iconLeft=\"addLine\" className=\"shrink-0 max-[640px]:w-full\">\n <Link to=\"/w/$workspaceSlug/projects/new\" params={{workspaceSlug: workspace.slug}}>\n New project\n </Link>\n </Button>\n </div>\n </header>\n\n <ModelProviderReminderBanner workspaceId={workspace.id} />\n\n {isInitialLoading || (search && hasNoData && query.isFetching) ? <ProjectsSkeleton /> : null}\n\n {query.isError && hasNoData ? <QueryLoadError query={query} subject=\"projects\" /> : null}\n\n {!isInitialLoading && !query.isError && projects.length === 0 && !search ? (\n <EmptyProjects workspaceSlug={workspace.slug} />\n ) : null}\n\n {!query.isFetching && !query.isError && projects.length === 0 && search ? (\n <NoSearchResults\n search={search}\n onClear={() => navigate({search: {} as never, replace: true})}\n />\n ) : null}\n\n {projects.length > 0 ? (\n <section className=\"flex flex-col gap-group\" aria-label=\"Projects list\">\n <ul className=\"grid grid-cols-2 gap-cluster max-[760px]:grid-cols-1\">\n {projects.map((project) => (\n <ProjectCard\n project={project}\n connection={connectionsById.get(project.source.connectionId)}\n connectionsResolved={connectionsQuery.isSuccess}\n connectionsSettled={connectionsQuery.isSuccess || connectionsQuery.isError}\n key={project.id}\n workspaceSlug={workspace.slug}\n />\n ))}\n </ul>\n {query.error && query.data ? (\n <Callout role=\"alert\" type=\"error\">\n <Text size=\"sm\">\n Could not load the next page. Existing projects are still shown.\n </Text>\n </Callout>\n ) : null}\n {query.hasNextPage ? (\n <div className=\"flex justify-center\">\n <Button\n variant=\"secondary\"\n isLoading={query.isFetchingNextPage}\n onClick={() => query.fetchNextPage()}\n >\n Load more\n </Button>\n </div>\n ) : null}\n </section>\n ) : null}\n </div>\n );\n}\n\nfunction ProjectsSkeleton() {\n return (\n <ul\n role=\"status\"\n aria-label=\"Loading projects\"\n className=\"grid grid-cols-2 gap-cluster max-[760px]:grid-cols-1\"\n >\n {[0, 1, 2, 3, 4, 5].map((row) => (\n <li key={row}>\n <Card className=\"h-full p-panel-compact\">\n <div className=\"flex items-center gap-cluster\">\n <Skeleton className=\"size-24 shrink-0\" />\n <Skeleton className=\"h-16 w-1/2\" />\n </div>\n </Card>\n </li>\n ))}\n </ul>\n );\n}\n\nfunction EmptyProjects({workspaceSlug}: {workspaceSlug: string}) {\n return (\n <EmptyState\n icon=\"folderLine\"\n title=\"Create your first project\"\n description=\"Connect a repository-backed project to start building workflows.\"\n action={\n <Button asChild iconRight=\"chevronRight\">\n <Link to=\"/w/$workspaceSlug/projects/new\" params={{workspaceSlug}}>\n Create project\n </Link>\n </Button>\n }\n />\n );\n}\n\nfunction NoSearchResults({search, onClear}: {search: string; onClear: () => void}) {\n return (\n <EmptyState\n icon=\"searchLine\"\n title={`No projects match “${search}”`}\n description=\"Try a different search, or clear it to see all projects.\"\n action={\n <Button size=\"sm\" variant=\"secondary\" onClick={onClear}>\n Clear search\n </Button>\n }\n />\n );\n}\n\nfunction ProjectCard({\n project,\n connection,\n connectionsResolved,\n connectionsSettled,\n workspaceSlug,\n}: {\n project: Project;\n connection: IntegrationConnection | undefined;\n connectionsResolved: boolean;\n connectionsSettled: boolean;\n workspaceSlug: string;\n}) {\n // On a resolved fetch, `active` carries no badge while a missing connection\n // reads as an error so a broken source is still flagged. An unresolved or\n // failed fetch shows nothing, so a fetch failure never flags every card.\n const status = connectionsResolved ? (connection?.lifecycleStatus ?? 'error') : undefined;\n\n return (\n <li>\n <Link\n to=\"/w/$workspaceSlug/p/$projectSlug\"\n params={{workspaceSlug, projectSlug: project.slug}}\n className=\"block h-full rounded-8 focus-visible:shadow-button-neutral-focus focus-visible:outline-none\"\n >\n <Card className=\"h-full p-panel-compact transition-colors hover:bg-background-components-hover\">\n <div className=\"flex min-w-0 items-center gap-cluster\">\n {/* Settle on success or error: a failed fetch falls back to the\n neutral provider icon rather than spinning forever. */}\n {connectionsSettled ? (\n <IntegrationIcon\n source={connection?.provider}\n aria-hidden\n className=\"size-24 shrink-0 text-foreground-neutral-base\"\n />\n ) : (\n <Skeleton className=\"size-24 shrink-0\" />\n )}\n <div className=\"flex min-w-0 flex-1 items-center gap-inline\">\n <Text size=\"md\" bold className=\"truncate\">\n {project.name}\n </Text>\n {status ? <ConnectionStatusBadge status={status} className=\"shrink-0\" /> : null}\n </div>\n </div>\n </Card>\n </Link>\n </li>\n );\n}\n"],"names":["useActiveWorkspace","ConnectionStatusBadge","IntegrationIcon","useIntegrationConnectionsQuery","QueryLoadError","Button","Callout","Card","EmptyState","Icon","Input","Skeleton","Header","Text","Link","useNavigate","ModelProviderReminderBanner","useProjectsInfiniteQuery","ProjectsHubPage","search","workspace","navigate","query","id","undefined","projects","data","pages","flatMap","page","connectionsQuery","length","connectionsById","Map","map","connection","isInitialLoading","isPending","isSearching","Boolean","isFetching","isFetchingNextPage","hasNoData","div","className","header","variant","type","value","onChange","event","next","target","trim","replace","placeholder","aria-label","iconLeft","name","iconRight","size","aria-hidden","asChild","to","params","workspaceSlug","slug","workspaceId","ProjectsSkeleton","isError","subject","EmptyProjects","NoSearchResults","onClear","section","ul","project","ProjectCard","get","source","connectionId","connectionsResolved","isSuccess","connectionsSettled","error","role","hasNextPage","isLoading","onClick","fetchNextPage","row","li","icon","title","description","action","status","lifecycleStatus","projectSlug","provider","bold"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/pages/projects-hub-page.tsx"],"sourcesContent":["import {useActiveWorkspace} from '@shipfox/client-auth';\nimport {\n ConnectionStatusBadge,\n type IntegrationConnection,\n IntegrationIcon,\n useIntegrationConnectionsQuery,\n} from '@shipfox/client-integrations';\nimport {QueryLoadError} 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 {Icon} from '@shipfox/react-ui/icon';\nimport {Input} from '@shipfox/react-ui/input';\nimport {\n Panel,\n PanelActions,\n PanelBody,\n PanelCell,\n PanelCellAction,\n PanelGrid,\n PanelHeader,\n} from '@shipfox/react-ui/panel';\nimport {Skeleton} from '@shipfox/react-ui/skeleton';\nimport {Text} from '@shipfox/react-ui/typography';\nimport {Link, useNavigate} from '@tanstack/react-router';\nimport {ModelProviderReminderBanner} from '#components/model-provider-reminder-banner.js';\nimport type {Project} from '#core/project.js';\nimport {useProjectsInfiniteQuery} from '#hooks/api/projects.js';\n\nexport function ProjectsHubPage({search = ''}: {search?: string}) {\n const workspace = useActiveWorkspace();\n const navigate = useNavigate();\n const query = useProjectsInfiniteQuery(workspace.id, search || undefined);\n const projects = query.data?.pages.flatMap((page) => page.projects) ?? [];\n\n // The provider logo and connection health live on the integration connection,\n // not the project, so resolve them once for the whole list and index by id.\n // Skip the fetch when there are no cards to annotate.\n const connectionsQuery = useIntegrationConnectionsQuery(\n projects.length > 0 ? workspace.id : undefined,\n );\n const connectionsById = new Map(\n (connectionsQuery.data ?? []).map((connection) => [connection.id, connection]),\n );\n\n const isInitialLoading = query.isPending;\n const isSearching = Boolean(search) && query.isFetching && !query.isFetchingNextPage;\n const hasNoData = !query.data;\n\n return (\n <div className=\"flex w-full flex-col gap-section\">\n <ModelProviderReminderBanner workspaceId={workspace.id} />\n\n <section aria-label=\"Projects\">\n <Panel>\n <PanelHeader className=\"flex-wrap max-[640px]:items-stretch\">\n <div className=\"min-w-0 flex-1 max-[640px]:basis-full\">\n <Input\n type=\"search\"\n value={search}\n onChange={(event) => {\n const next = event.target.value.trim();\n navigate({search: (next ? {search: next} : {}) as never, replace: true});\n }}\n placeholder=\"Search projects…\"\n aria-label=\"Search projects\"\n iconLeft={<Icon name=\"searchLine\" className=\"size-16\" />}\n iconRight={\n isSearching ? (\n <Icon\n name=\"spinner\"\n size={16}\n className=\"text-foreground-neutral-muted\"\n aria-hidden=\"true\"\n />\n ) : undefined\n }\n />\n </div>\n <PanelActions className=\"max-[640px]:ml-0 max-[640px]:w-full\">\n <Button asChild iconLeft=\"addLine\" className=\"max-[640px]:w-full\">\n <Link to=\"/w/$workspaceSlug/projects/new\" params={{workspaceSlug: workspace.slug}}>\n New project\n </Link>\n </Button>\n </PanelActions>\n </PanelHeader>\n\n <PanelBody>\n {isInitialLoading || (search && hasNoData && query.isFetching) ? (\n <ProjectsSkeleton />\n ) : null}\n\n {query.isError && hasNoData ? (\n <QueryLoadError query={query} subject=\"projects\" variant=\"panel\" />\n ) : null}\n\n {!isInitialLoading && !query.isError && projects.length === 0 && !search ? (\n <EmptyProjects workspaceSlug={workspace.slug} />\n ) : null}\n\n {!query.isFetching && !query.isError && projects.length === 0 && search ? (\n <NoSearchResults\n search={search}\n onClear={() => navigate({search: {} as never, replace: true})}\n />\n ) : null}\n\n {projects.length > 0 ? (\n <>\n <PanelGrid aria-label=\"Projects list\">\n {projects.map((project) => (\n <ProjectCell\n project={project}\n connection={connectionsById.get(project.source.connectionId)}\n connectionsResolved={connectionsQuery.isSuccess}\n connectionsSettled={connectionsQuery.isSuccess || connectionsQuery.isError}\n key={project.id}\n workspaceSlug={workspace.slug}\n />\n ))}\n </PanelGrid>\n {query.error && query.data ? (\n <div className=\"border-t border-border-neutral-base p-panel-compact\">\n <Callout role=\"alert\" type=\"error\">\n <Text size=\"sm\">\n Could not load the next page. Existing projects are still shown.\n </Text>\n </Callout>\n </div>\n ) : null}\n {query.hasNextPage ? (\n <div className=\"flex justify-center border-t border-border-neutral-base p-panel-compact\">\n <Button\n variant=\"secondary\"\n isLoading={query.isFetchingNextPage}\n onClick={() => query.fetchNextPage()}\n >\n Load more\n </Button>\n </div>\n ) : null}\n </>\n ) : null}\n </PanelBody>\n </Panel>\n </section>\n </div>\n );\n}\n\nfunction ProjectsSkeleton() {\n return (\n <PanelGrid role=\"status\" aria-label=\"Loading projects\">\n {[0, 1, 2, 3, 4, 5].map((row) => (\n <PanelCell key={row}>\n <div className=\"flex items-center gap-cluster px-row py-row\">\n <Skeleton className=\"size-24 shrink-0\" />\n <Skeleton className=\"h-16 w-1/2\" />\n </div>\n </PanelCell>\n ))}\n </PanelGrid>\n );\n}\n\nfunction EmptyProjects({workspaceSlug}: {workspaceSlug: string}) {\n return (\n <EmptyState\n icon=\"folderLine\"\n title=\"Create your first project\"\n description=\"Connect a repository-backed project to start building workflows.\"\n variant=\"panel\"\n action={\n <Button asChild iconRight=\"chevronRight\">\n <Link to=\"/w/$workspaceSlug/projects/new\" params={{workspaceSlug}}>\n Create project\n </Link>\n </Button>\n }\n />\n );\n}\n\nfunction NoSearchResults({search, onClear}: {search: string; onClear: () => void}) {\n return (\n <EmptyState\n icon=\"searchLine\"\n title={`No projects match “${search}”`}\n description=\"Try a different search, or clear it to see all projects.\"\n variant=\"panel\"\n action={\n <Button size=\"sm\" variant=\"secondary\" onClick={onClear}>\n Clear search\n </Button>\n }\n />\n );\n}\n\nfunction ProjectCell({\n project,\n connection,\n connectionsResolved,\n connectionsSettled,\n workspaceSlug,\n}: {\n project: Project;\n connection: IntegrationConnection | undefined;\n connectionsResolved: boolean;\n connectionsSettled: boolean;\n workspaceSlug: string;\n}) {\n // On a resolved fetch, `active` carries no badge while a missing connection\n // reads as an error so a broken source is still flagged. An unresolved or\n // failed fetch shows nothing, so a fetch failure never flags every card.\n const status = connectionsResolved ? (connection?.lifecycleStatus ?? 'error') : undefined;\n\n return (\n <PanelCell>\n <PanelCellAction asChild>\n <Link\n to=\"/w/$workspaceSlug/p/$projectSlug\"\n params={{workspaceSlug, projectSlug: project.slug}}\n >\n {/* Settle on success or error: a failed fetch falls back to the\n neutral provider icon rather than spinning forever. */}\n {connectionsSettled ? (\n <IntegrationIcon\n source={connection?.provider}\n aria-hidden\n className=\"size-24 shrink-0 text-foreground-neutral-base\"\n />\n ) : (\n <Skeleton className=\"size-24 shrink-0\" />\n )}\n <span className=\"flex min-w-0 flex-1 items-center gap-inline\">\n <Text as=\"span\" size=\"md\" bold className=\"truncate\">\n {project.name}\n </Text>\n {status ? <ConnectionStatusBadge status={status} className=\"shrink-0\" /> : null}\n </span>\n </Link>\n </PanelCellAction>\n </PanelCell>\n );\n}\n"],"names":["useActiveWorkspace","ConnectionStatusBadge","IntegrationIcon","useIntegrationConnectionsQuery","QueryLoadError","Button","Callout","EmptyState","Icon","Input","Panel","PanelActions","PanelBody","PanelCell","PanelCellAction","PanelGrid","PanelHeader","Skeleton","Text","Link","useNavigate","ModelProviderReminderBanner","useProjectsInfiniteQuery","ProjectsHubPage","search","workspace","navigate","query","id","undefined","projects","data","pages","flatMap","page","connectionsQuery","length","connectionsById","Map","map","connection","isInitialLoading","isPending","isSearching","Boolean","isFetching","isFetchingNextPage","hasNoData","div","className","workspaceId","section","aria-label","type","value","onChange","event","next","target","trim","replace","placeholder","iconLeft","name","iconRight","size","aria-hidden","asChild","to","params","workspaceSlug","slug","ProjectsSkeleton","isError","subject","variant","EmptyProjects","NoSearchResults","onClear","project","ProjectCell","get","source","connectionId","connectionsResolved","isSuccess","connectionsSettled","error","role","hasNextPage","isLoading","onClick","fetchNextPage","row","icon","title","description","action","status","lifecycleStatus","projectSlug","provider","span","as","bold"],"mappings":";AAAA,SAAQA,kBAAkB,QAAO,uBAAuB;AACxD,SACEC,qBAAqB,EAErBC,eAAe,EACfC,8BAA8B,QACzB,+BAA+B;AACtC,SAAQC,cAAc,QAAO,qBAAqB;AAClD,SAAQC,MAAM,QAAO,2BAA2B;AAChD,SAAQC,OAAO,QAAO,4BAA4B;AAClD,SAAQC,UAAU,QAAO,gCAAgC;AACzD,SAAQC,IAAI,QAAO,yBAAyB;AAC5C,SAAQC,KAAK,QAAO,0BAA0B;AAC9C,SACEC,KAAK,EACLC,YAAY,EACZC,SAAS,EACTC,SAAS,EACTC,eAAe,EACfC,SAAS,EACTC,WAAW,QACN,0BAA0B;AACjC,SAAQC,QAAQ,QAAO,6BAA6B;AACpD,SAAQC,IAAI,QAAO,+BAA+B;AAClD,SAAQC,IAAI,EAAEC,WAAW,QAAO,yBAAyB;AACzD,SAAQC,2BAA2B,QAAO,gDAAgD;AAE1F,SAAQC,wBAAwB,QAAO,yBAAyB;AAEhE,OAAO,SAASC,gBAAgB,EAACC,SAAS,EAAE,EAAoB;IAC9D,MAAMC,YAAYzB;IAClB,MAAM0B,WAAWN;IACjB,MAAMO,QAAQL,yBAAyBG,UAAUG,EAAE,EAAEJ,UAAUK;IAC/D,MAAMC,WAAWH,MAAMI,IAAI,EAAEC,MAAMC,QAAQ,CAACC,OAASA,KAAKJ,QAAQ,KAAK,EAAE;IAEzE,8EAA8E;IAC9E,4EAA4E;IAC5E,sDAAsD;IACtD,MAAMK,mBAAmBhC,+BACvB2B,SAASM,MAAM,GAAG,IAAIX,UAAUG,EAAE,GAAGC;IAEvC,MAAMQ,kBAAkB,IAAIC,IAC1B,AAACH,CAAAA,iBAAiBJ,IAAI,IAAI,EAAE,AAAD,EAAGQ,GAAG,CAAC,CAACC,aAAe;YAACA,WAAWZ,EAAE;YAAEY;SAAW;IAG/E,MAAMC,mBAAmBd,MAAMe,SAAS;IACxC,MAAMC,cAAcC,QAAQpB,WAAWG,MAAMkB,UAAU,IAAI,CAAClB,MAAMmB,kBAAkB;IACpF,MAAMC,YAAY,CAACpB,MAAMI,IAAI;IAE7B,qBACE,MAACiB;QAAIC,WAAU;;0BACb,KAAC5B;gBAA4B6B,aAAazB,UAAUG,EAAE;;0BAEtD,KAACuB;gBAAQC,cAAW;0BAClB,cAAA,MAAC1C;;sCACC,MAACM;4BAAYiC,WAAU;;8CACrB,KAACD;oCAAIC,WAAU;8CACb,cAAA,KAACxC;wCACC4C,MAAK;wCACLC,OAAO9B;wCACP+B,UAAU,CAACC;4CACT,MAAMC,OAAOD,MAAME,MAAM,CAACJ,KAAK,CAACK,IAAI;4CACpCjC,SAAS;gDAACF,QAASiC,OAAO;oDAACjC,QAAQiC;gDAAI,IAAI,CAAC;gDAAaG,SAAS;4CAAI;wCACxE;wCACAC,aAAY;wCACZT,cAAW;wCACXU,wBAAU,KAACtD;4CAAKuD,MAAK;4CAAad,WAAU;;wCAC5Ce,WACErB,4BACE,KAACnC;4CACCuD,MAAK;4CACLE,MAAM;4CACNhB,WAAU;4CACViB,eAAY;6CAEZrC;;;8CAIV,KAAClB;oCAAasC,WAAU;8CACtB,cAAA,KAAC5C;wCAAO8D,OAAO;wCAACL,UAAS;wCAAUb,WAAU;kDAC3C,cAAA,KAAC9B;4CAAKiD,IAAG;4CAAiCC,QAAQ;gDAACC,eAAe7C,UAAU8C,IAAI;4CAAA;sDAAG;;;;;;sCAOzF,MAAC3D;;gCACE6B,oBAAqBjB,UAAUuB,aAAapB,MAAMkB,UAAU,iBAC3D,KAAC2B,wBACC;gCAEH7C,MAAM8C,OAAO,IAAI1B,0BAChB,KAAC3C;oCAAeuB,OAAOA;oCAAO+C,SAAQ;oCAAWC,SAAQ;qCACvD;gCAEH,CAAClC,oBAAoB,CAACd,MAAM8C,OAAO,IAAI3C,SAASM,MAAM,KAAK,KAAK,CAACZ,uBAChE,KAACoD;oCAAcN,eAAe7C,UAAU8C,IAAI;qCAC1C;gCAEH,CAAC5C,MAAMkB,UAAU,IAAI,CAAClB,MAAM8C,OAAO,IAAI3C,SAASM,MAAM,KAAK,KAAKZ,uBAC/D,KAACqD;oCACCrD,QAAQA;oCACRsD,SAAS,IAAMpD,SAAS;4CAACF,QAAQ,CAAC;4CAAYoC,SAAS;wCAAI;qCAE3D;gCAEH9B,SAASM,MAAM,GAAG,kBACjB;;sDACE,KAACrB;4CAAUqC,cAAW;sDACnBtB,SAASS,GAAG,CAAC,CAACwC,wBACb,KAACC;oDACCD,SAASA;oDACTvC,YAAYH,gBAAgB4C,GAAG,CAACF,QAAQG,MAAM,CAACC,YAAY;oDAC3DC,qBAAqBjD,iBAAiBkD,SAAS;oDAC/CC,oBAAoBnD,iBAAiBkD,SAAS,IAAIlD,iBAAiBsC,OAAO;oDAE1EH,eAAe7C,UAAU8C,IAAI;mDADxBQ,QAAQnD,EAAE;;wCAKpBD,MAAM4D,KAAK,IAAI5D,MAAMI,IAAI,iBACxB,KAACiB;4CAAIC,WAAU;sDACb,cAAA,KAAC3C;gDAAQkF,MAAK;gDAAQnC,MAAK;0DACzB,cAAA,KAACnC;oDAAK+C,MAAK;8DAAK;;;6CAKlB;wCACHtC,MAAM8D,WAAW,iBAChB,KAACzC;4CAAIC,WAAU;sDACb,cAAA,KAAC5C;gDACCsE,SAAQ;gDACRe,WAAW/D,MAAMmB,kBAAkB;gDACnC6C,SAAS,IAAMhE,MAAMiE,aAAa;0DACnC;;6CAID;;qCAEJ;;;;;;;;AAMhB;AAEA,SAASpB;IACP,qBACE,KAACzD;QAAUyE,MAAK;QAASpC,cAAW;kBACjC;YAAC;YAAG;YAAG;YAAG;YAAG;YAAG;SAAE,CAACb,GAAG,CAAC,CAACsD,oBACvB,KAAChF;0BACC,cAAA,MAACmC;oBAAIC,WAAU;;sCACb,KAAChC;4BAASgC,WAAU;;sCACpB,KAAChC;4BAASgC,WAAU;;;;eAHR4C;;AASxB;AAEA,SAASjB,cAAc,EAACN,aAAa,EAA0B;IAC7D,qBACE,KAAC/D;QACCuF,MAAK;QACLC,OAAM;QACNC,aAAY;QACZrB,SAAQ;QACRsB,sBACE,KAAC5F;YAAO8D,OAAO;YAACH,WAAU;sBACxB,cAAA,KAAC7C;gBAAKiD,IAAG;gBAAiCC,QAAQ;oBAACC;gBAAa;0BAAG;;;;AAO7E;AAEA,SAASO,gBAAgB,EAACrD,MAAM,EAAEsD,OAAO,EAAwC;IAC/E,qBACE,KAACvE;QACCuF,MAAK;QACLC,OAAO,CAAC,mBAAmB,EAAEvE,OAAO,CAAC,CAAC;QACtCwE,aAAY;QACZrB,SAAQ;QACRsB,sBACE,KAAC5F;YAAO4D,MAAK;YAAKU,SAAQ;YAAYgB,SAASb;sBAAS;;;AAMhE;AAEA,SAASE,YAAY,EACnBD,OAAO,EACPvC,UAAU,EACV4C,mBAAmB,EACnBE,kBAAkB,EAClBhB,aAAa,EAOd;IACC,4EAA4E;IAC5E,0EAA0E;IAC1E,yEAAyE;IACzE,MAAM4B,SAASd,sBAAuB5C,YAAY2D,mBAAmB,UAAWtE;IAEhF,qBACE,KAAChB;kBACC,cAAA,KAACC;YAAgBqD,OAAO;sBACtB,cAAA,MAAChD;gBACCiD,IAAG;gBACHC,QAAQ;oBAACC;oBAAe8B,aAAarB,QAAQR,IAAI;gBAAA;;oBAIhDe,mCACC,KAACpF;wBACCgF,QAAQ1C,YAAY6D;wBACpBnC,aAAW;wBACXjB,WAAU;uCAGZ,KAAChC;wBAASgC,WAAU;;kCAEtB,MAACqD;wBAAKrD,WAAU;;0CACd,KAAC/B;gCAAKqF,IAAG;gCAAOtC,MAAK;gCAAKuC,IAAI;gCAACvD,WAAU;0CACtC8B,QAAQhB,IAAI;;4BAEdmC,uBAAS,KAACjG;gCAAsBiG,QAAQA;gCAAQjD,WAAU;iCAAgB;;;;;;;AAMvF"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { StoryObj } from '@storybook/react';
|
|
2
|
+
type Scenario = 'list' | 'empty' | 'loading' | 'error';
|
|
3
|
+
declare function ProjectsHubPageStory({ scenario }: {
|
|
4
|
+
scenario: Scenario;
|
|
5
|
+
}): import("react").JSX.Element;
|
|
6
|
+
declare const meta: {
|
|
7
|
+
title: string;
|
|
8
|
+
component: typeof ProjectsHubPageStory;
|
|
9
|
+
parameters: {
|
|
10
|
+
layout: string;
|
|
11
|
+
};
|
|
12
|
+
args: {
|
|
13
|
+
scenario: "list";
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
export default meta;
|
|
17
|
+
type Story = StoryObj<typeof meta>;
|
|
18
|
+
export declare const Playground: Story;
|
|
19
|
+
export declare const Empty: Story;
|
|
20
|
+
export declare const Loading: Story;
|
|
21
|
+
export declare const ErrorState: Story;
|
|
22
|
+
//# sourceMappingURL=projects-hub-page.stories.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projects-hub-page.stories.d.ts","sourceRoot":"","sources":["../../src/pages/projects-hub-page.stories.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAO,QAAQ,EAAC,MAAM,kBAAkB,CAAC;AAiBrD,KAAK,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,OAAO,CAAC;AAcvD,iBAAS,oBAAoB,CAAC,EAAC,QAAQ,EAAC,EAAE;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAAC,+BA6B7D;AAED,QAAA,MAAM,IAAI;;;;QAGK,MAAM;;;;;CAEwB,CAAC;eAE/B,IAAI;AACnB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,IAAI,CAAC,CAAC;AAEnC,eAAO,MAAM,UAAU,EAAE,KAAU,CAAC;AAEpC,eAAO,MAAM,KAAK,EAAE,KAEnB,CAAC;AAEF,eAAO,MAAM,OAAO,EAAE,KAErB,CAAC;AAEF,eAAO,MAAM,UAAU,EAAE,KAExB,CAAC"}
|