@shipfox/client-projects 12.0.1 → 13.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.
Files changed (111) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +59 -0
  3. package/dist/chrome.d.ts +5 -1
  4. package/dist/chrome.d.ts.map +1 -1
  5. package/dist/chrome.js +18 -26
  6. package/dist/chrome.js.map +1 -1
  7. package/dist/components/model-provider-reminder-banner.d.ts.map +1 -1
  8. package/dist/components/model-provider-reminder-banner.js +4 -2
  9. package/dist/components/model-provider-reminder-banner.js.map +1 -1
  10. package/dist/components/project-crumb.d.ts +3 -1
  11. package/dist/components/project-crumb.d.ts.map +1 -1
  12. package/dist/components/project-crumb.js +7 -5
  13. package/dist/components/project-crumb.js.map +1 -1
  14. package/dist/components/project-switcher.d.ts +2 -1
  15. package/dist/components/project-switcher.d.ts.map +1 -1
  16. package/dist/components/project-switcher.js +10 -8
  17. package/dist/components/project-switcher.js.map +1 -1
  18. package/dist/core/definition.d.ts +6 -0
  19. package/dist/core/definition.d.ts.map +1 -1
  20. package/dist/core/definition.js.map +1 -1
  21. package/dist/core/project.d.ts +7 -0
  22. package/dist/core/project.d.ts.map +1 -1
  23. package/dist/core/project.js.map +1 -1
  24. package/dist/feature.d.ts +29 -5
  25. package/dist/feature.d.ts.map +1 -1
  26. package/dist/feature.js +26 -5
  27. package/dist/feature.js.map +1 -1
  28. package/dist/hooks/api/mappers.d.ts.map +1 -1
  29. package/dist/hooks/api/mappers.js +9 -1
  30. package/dist/hooks/api/mappers.js.map +1 -1
  31. package/dist/hooks/api/projects.d.ts +21 -2
  32. package/dist/hooks/api/projects.d.ts.map +1 -1
  33. package/dist/hooks/api/projects.js +175 -2
  34. package/dist/hooks/api/projects.js.map +1 -1
  35. package/dist/index.d.ts +3 -2
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +2 -1
  38. package/dist/index.js.map +1 -1
  39. package/dist/pages/create-project-page.d.ts.map +1 -1
  40. package/dist/pages/create-project-page.js +97 -21
  41. package/dist/pages/create-project-page.js.map +1 -1
  42. package/dist/pages/create-project-page.stories.js +5 -3
  43. package/dist/pages/create-project-page.stories.js.map +1 -1
  44. package/dist/pages/project-settings-form-errors.d.ts +11 -0
  45. package/dist/pages/project-settings-form-errors.d.ts.map +1 -0
  46. package/dist/pages/project-settings-form-errors.js +16 -0
  47. package/dist/pages/project-settings-form-errors.js.map +1 -0
  48. package/dist/pages/project-settings-page.d.ts +2 -0
  49. package/dist/pages/project-settings-page.d.ts.map +1 -0
  50. package/dist/pages/project-settings-page.js +225 -0
  51. package/dist/pages/project-settings-page.js.map +1 -0
  52. package/dist/pages/project-settings-page.stories.d.ts +23 -0
  53. package/dist/pages/project-settings-page.stories.d.ts.map +1 -0
  54. package/dist/pages/project-settings-page.stories.js +206 -0
  55. package/dist/pages/project-settings-page.stories.js.map +1 -0
  56. package/dist/pages/projects-hub-page.js +11 -11
  57. package/dist/pages/projects-hub-page.js.map +1 -1
  58. package/dist/project-error.d.ts +1 -0
  59. package/dist/project-error.d.ts.map +1 -1
  60. package/dist/project-error.js +7 -0
  61. package/dist/project-error.js.map +1 -1
  62. package/dist/routes/inputs.d.ts +2 -2
  63. package/dist/routes/inputs.d.ts.map +1 -1
  64. package/dist/routes/inputs.js +5 -5
  65. package/dist/routes/inputs.js.map +1 -1
  66. package/dist/routes/project-index.d.ts +2 -2
  67. package/dist/routes/project-index.d.ts.map +1 -1
  68. package/dist/routes/project-index.js +3 -3
  69. package/dist/routes/project-index.js.map +1 -1
  70. package/dist/routes/project-settings-index.d.ts +10 -0
  71. package/dist/routes/project-settings-index.d.ts.map +1 -0
  72. package/dist/routes/project-settings-index.js +12 -0
  73. package/dist/routes/project-settings-index.js.map +1 -0
  74. package/dist/routes/project-settings.d.ts +6 -0
  75. package/dist/routes/project-settings.d.ts.map +1 -0
  76. package/dist/routes/project-settings.js +7 -0
  77. package/dist/routes/project-settings.js.map +1 -0
  78. package/dist/tsconfig.test.tsbuildinfo +1 -1
  79. package/package.json +11 -10
  80. package/src/chrome.tsx +27 -19
  81. package/src/components/model-provider-reminder-banner.tsx +3 -1
  82. package/src/components/project-crumb.tsx +18 -5
  83. package/src/components/project-switcher.test.tsx +7 -4
  84. package/src/components/project-switcher.tsx +16 -5
  85. package/src/core/definition.ts +7 -0
  86. package/src/core/project.ts +8 -0
  87. package/src/feature.ts +26 -4
  88. package/src/hooks/api/mappers.test.ts +19 -0
  89. package/src/hooks/api/mappers.ts +6 -0
  90. package/src/hooks/api/projects.test.ts +247 -1
  91. package/src/hooks/api/projects.ts +227 -2
  92. package/src/index.ts +9 -1
  93. package/src/pages/create-project-page.stories.tsx +5 -4
  94. package/src/pages/create-project-page.test.tsx +191 -12
  95. package/src/pages/create-project-page.tsx +102 -15
  96. package/src/pages/home-router.test.tsx +2 -2
  97. package/src/pages/project-settings-form-errors.ts +17 -0
  98. package/src/pages/project-settings-page.stories.tsx +216 -0
  99. package/src/pages/project-settings-page.test.tsx +74 -0
  100. package/src/pages/project-settings-page.tsx +220 -0
  101. package/src/pages/projects-hub-page.test.tsx +17 -11
  102. package/src/pages/projects-hub-page.tsx +9 -9
  103. package/src/project-error.test.ts +1 -0
  104. package/src/project-error.ts +8 -0
  105. package/src/routes/inputs.test.ts +8 -6
  106. package/src/routes/inputs.ts +7 -6
  107. package/src/routes/project-index.tsx +3 -3
  108. package/src/routes/project-settings-index.tsx +11 -0
  109. package/src/routes/project-settings.tsx +4 -0
  110. package/test/pages.tsx +26 -22
  111. package/tsconfig.build.tsbuildinfo +1 -1
@@ -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-24\">\n <header className=\"flex flex-col gap-16\">\n <div className=\"flex items-start justify-between gap-24 max-[640px]:flex-col\">\n <Header variant=\"h2\">Projects</Header>\n </div>\n <div className=\"flex items-center gap-12 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=\"/workspaces/$wid/projects/new\" params={{wid: workspace.id}}>\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 workspaceId={workspace.id} />\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 aria-label=\"Projects list\">\n <ul className=\"grid grid-cols-2 gap-12 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 workspaceId={workspace.id}\n />\n ))}\n </ul>\n {query.error && query.data ? (\n <Callout role=\"alert\" type=\"error\" className=\"mt-16\">\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=\"mt-16 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-12 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-16\">\n <div className=\"flex items-center gap-12\">\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({workspaceId}: {workspaceId: 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=\"/workspaces/$wid/projects/new\" params={{wid: workspaceId}}>\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 workspaceId,\n}: {\n project: Project;\n connection: IntegrationConnection | undefined;\n connectionsResolved: boolean;\n connectionsSettled: boolean;\n workspaceId: 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=\"/workspaces/$wid/projects/$pid\"\n params={{wid: workspaceId, pid: project.id}}\n className=\"block h-full rounded-8 focus-visible:shadow-button-neutral-focus focus-visible:outline-none\"\n >\n <Card className=\"h-full p-16 transition-colors hover:bg-background-components-hover\">\n <div className=\"flex min-w-0 items-center gap-12\">\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-8\">\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","wid","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","pid","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 {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-24\">\n <header className=\"flex flex-col gap-16\">\n <div className=\"flex items-start justify-between gap-24 max-[640px]:flex-col\">\n <Header variant=\"h2\">Projects</Header>\n </div>\n <div className=\"flex items-center gap-12 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 aria-label=\"Projects list\">\n <ul className=\"grid grid-cols-2 gap-12 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\" className=\"mt-16\">\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=\"mt-16 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-12 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-16\">\n <div className=\"flex items-center gap-12\">\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-16 transition-colors hover:bg-background-components-hover\">\n <div className=\"flex min-w-0 items-center gap-12\">\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-8\">\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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"}
@@ -2,6 +2,7 @@ export interface ProjectErrorCopy {
2
2
  title: string;
3
3
  message: string;
4
4
  existingProjectId?: string | undefined;
5
+ slugConflict?: boolean | undefined;
5
6
  }
6
7
  export declare function projectErrorCopy(error: unknown): ProjectErrorCopy;
7
8
  //# sourceMappingURL=project-error.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"project-error.d.ts","sourceRoot":"","sources":["../src/project-error.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACxC;AAsBD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,gBAAgB,CA6EjE"}
1
+ {"version":3,"file":"project-error.d.ts","sourceRoot":"","sources":["../src/project-error.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,iBAAiB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,YAAY,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACpC;AAsBD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,gBAAgB,CAoFjE"}
@@ -84,6 +84,13 @@ export function projectErrorCopy(error) {
84
84
  existingProjectId: stringDetail(error, 'existing_project_id')
85
85
  };
86
86
  }
87
+ if (error.code === 'slug-conflict') {
88
+ return {
89
+ title: 'Project slug already exists',
90
+ message: 'Choose another slug and try again.',
91
+ slugConflict: true
92
+ };
93
+ }
87
94
  return {
88
95
  title: 'Project request failed',
89
96
  message: error.message || 'Try again in a moment.'
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/project-error.ts"],"sourcesContent":["import {ApiError} from '@shipfox/client-api';\n\nexport interface ProjectErrorCopy {\n title: string;\n message: string;\n existingProjectId?: string | undefined;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nfunction apiDetails(error: ApiError): Record<string, unknown> {\n if (!isRecord(error.details)) return {};\n const details = error.details.details;\n return isRecord(details) ? details : {};\n}\n\nfunction stringDetail(error: ApiError, key: string): string | undefined {\n const value = apiDetails(error)[key];\n return typeof value === 'string' ? value : undefined;\n}\n\nfunction numberDetail(error: ApiError, key: string): number | undefined {\n const value = apiDetails(error)[key];\n return typeof value === 'number' && Number.isFinite(value) ? value : undefined;\n}\n\nexport function projectErrorCopy(error: unknown): ProjectErrorCopy {\n if (!(error instanceof ApiError)) {\n return {\n title: 'Something went wrong',\n message: 'Try again in a moment.',\n };\n }\n\n if (error.code === 'network-error') {\n return {\n title: 'Network problem',\n message: 'We could not reach the API. Check your connection and try again.',\n };\n }\n if (error.code === 'repository-not-found') {\n return {\n title: 'Repository not found',\n message: 'Choose another repository and try again.',\n };\n }\n if (error.code === 'source-connection-not-found') {\n return {\n title: 'Source connection not found',\n message: 'Reconnect source control and try again.',\n };\n }\n if (error.code === 'source-connection-inactive') {\n return {\n title: 'Source connection inactive',\n message: 'Reconnect or choose another source-control connection.',\n };\n }\n if (error.code === 'access-denied') {\n return {\n title: 'Repository access denied',\n message: 'The selected connection cannot access this repository.',\n };\n }\n if (error.code === 'rate-limited') {\n const retryAfter = numberDetail(error, 'retry_after_seconds');\n return {\n title: 'Provider rate limited',\n message: retryAfter\n ? `Try again in about ${retryAfter} seconds.`\n : 'The provider is asking us to slow down. Try again shortly.',\n };\n }\n if (error.code === 'timeout') {\n return {\n title: 'Provider timed out',\n message: 'The provider did not respond in time. Try again.',\n };\n }\n if (error.code === 'provider-unavailable') {\n return {\n title: 'Provider unavailable',\n message: 'The provider is unavailable right now. Try again shortly.',\n };\n }\n if (error.code === 'malformed-provider-response') {\n return {\n title: 'Provider response changed',\n message: 'The provider returned data Shipfox could not understand.',\n };\n }\n if (error.code === 'project-already-exists') {\n return {\n title: 'Project already exists',\n message: 'This repository is already connected to a Shipfox project.',\n existingProjectId: stringDetail(error, 'existing_project_id'),\n };\n }\n\n return {\n title: 'Project request failed',\n message: error.message || 'Try again in a moment.',\n };\n}\n"],"names":["ApiError","isRecord","value","apiDetails","error","details","stringDetail","key","undefined","numberDetail","Number","isFinite","projectErrorCopy","title","message","code","retryAfter","existingProjectId"],"mappings":"AAAA,SAAQA,QAAQ,QAAO,sBAAsB;AAQ7C,SAASC,SAASC,KAAc;IAC9B,OAAO,OAAOA,UAAU,YAAYA,UAAU;AAChD;AAEA,SAASC,WAAWC,KAAe;IACjC,IAAI,CAACH,SAASG,MAAMC,OAAO,GAAG,OAAO,CAAC;IACtC,MAAMA,UAAUD,MAAMC,OAAO,CAACA,OAAO;IACrC,OAAOJ,SAASI,WAAWA,UAAU,CAAC;AACxC;AAEA,SAASC,aAAaF,KAAe,EAAEG,GAAW;IAChD,MAAML,QAAQC,WAAWC,MAAM,CAACG,IAAI;IACpC,OAAO,OAAOL,UAAU,WAAWA,QAAQM;AAC7C;AAEA,SAASC,aAAaL,KAAe,EAAEG,GAAW;IAChD,MAAML,QAAQC,WAAWC,MAAM,CAACG,IAAI;IACpC,OAAO,OAAOL,UAAU,YAAYQ,OAAOC,QAAQ,CAACT,SAASA,QAAQM;AACvE;AAEA,OAAO,SAASI,iBAAiBR,KAAc;IAC7C,IAAI,CAAEA,CAAAA,iBAAiBJ,QAAO,GAAI;QAChC,OAAO;YACLa,OAAO;YACPC,SAAS;QACX;IACF;IAEA,IAAIV,MAAMW,IAAI,KAAK,iBAAiB;QAClC,OAAO;YACLF,OAAO;YACPC,SAAS;QACX;IACF;IACA,IAAIV,MAAMW,IAAI,KAAK,wBAAwB;QACzC,OAAO;YACLF,OAAO;YACPC,SAAS;QACX;IACF;IACA,IAAIV,MAAMW,IAAI,KAAK,+BAA+B;QAChD,OAAO;YACLF,OAAO;YACPC,SAAS;QACX;IACF;IACA,IAAIV,MAAMW,IAAI,KAAK,8BAA8B;QAC/C,OAAO;YACLF,OAAO;YACPC,SAAS;QACX;IACF;IACA,IAAIV,MAAMW,IAAI,KAAK,iBAAiB;QAClC,OAAO;YACLF,OAAO;YACPC,SAAS;QACX;IACF;IACA,IAAIV,MAAMW,IAAI,KAAK,gBAAgB;QACjC,MAAMC,aAAaP,aAAaL,OAAO;QACvC,OAAO;YACLS,OAAO;YACPC,SAASE,aACL,CAAC,mBAAmB,EAAEA,WAAW,SAAS,CAAC,GAC3C;QACN;IACF;IACA,IAAIZ,MAAMW,IAAI,KAAK,WAAW;QAC5B,OAAO;YACLF,OAAO;YACPC,SAAS;QACX;IACF;IACA,IAAIV,MAAMW,IAAI,KAAK,wBAAwB;QACzC,OAAO;YACLF,OAAO;YACPC,SAAS;QACX;IACF;IACA,IAAIV,MAAMW,IAAI,KAAK,+BAA+B;QAChD,OAAO;YACLF,OAAO;YACPC,SAAS;QACX;IACF;IACA,IAAIV,MAAMW,IAAI,KAAK,0BAA0B;QAC3C,OAAO;YACLF,OAAO;YACPC,SAAS;YACTG,mBAAmBX,aAAaF,OAAO;QACzC;IACF;IAEA,OAAO;QACLS,OAAO;QACPC,SAASV,MAAMU,OAAO,IAAI;IAC5B;AACF"}
1
+ {"version":3,"sources":["../src/project-error.ts"],"sourcesContent":["import {ApiError} from '@shipfox/client-api';\n\nexport interface ProjectErrorCopy {\n title: string;\n message: string;\n existingProjectId?: string | undefined;\n slugConflict?: boolean | undefined;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nfunction apiDetails(error: ApiError): Record<string, unknown> {\n if (!isRecord(error.details)) return {};\n const details = error.details.details;\n return isRecord(details) ? details : {};\n}\n\nfunction stringDetail(error: ApiError, key: string): string | undefined {\n const value = apiDetails(error)[key];\n return typeof value === 'string' ? value : undefined;\n}\n\nfunction numberDetail(error: ApiError, key: string): number | undefined {\n const value = apiDetails(error)[key];\n return typeof value === 'number' && Number.isFinite(value) ? value : undefined;\n}\n\nexport function projectErrorCopy(error: unknown): ProjectErrorCopy {\n if (!(error instanceof ApiError)) {\n return {\n title: 'Something went wrong',\n message: 'Try again in a moment.',\n };\n }\n\n if (error.code === 'network-error') {\n return {\n title: 'Network problem',\n message: 'We could not reach the API. Check your connection and try again.',\n };\n }\n if (error.code === 'repository-not-found') {\n return {\n title: 'Repository not found',\n message: 'Choose another repository and try again.',\n };\n }\n if (error.code === 'source-connection-not-found') {\n return {\n title: 'Source connection not found',\n message: 'Reconnect source control and try again.',\n };\n }\n if (error.code === 'source-connection-inactive') {\n return {\n title: 'Source connection inactive',\n message: 'Reconnect or choose another source-control connection.',\n };\n }\n if (error.code === 'access-denied') {\n return {\n title: 'Repository access denied',\n message: 'The selected connection cannot access this repository.',\n };\n }\n if (error.code === 'rate-limited') {\n const retryAfter = numberDetail(error, 'retry_after_seconds');\n return {\n title: 'Provider rate limited',\n message: retryAfter\n ? `Try again in about ${retryAfter} seconds.`\n : 'The provider is asking us to slow down. Try again shortly.',\n };\n }\n if (error.code === 'timeout') {\n return {\n title: 'Provider timed out',\n message: 'The provider did not respond in time. Try again.',\n };\n }\n if (error.code === 'provider-unavailable') {\n return {\n title: 'Provider unavailable',\n message: 'The provider is unavailable right now. Try again shortly.',\n };\n }\n if (error.code === 'malformed-provider-response') {\n return {\n title: 'Provider response changed',\n message: 'The provider returned data Shipfox could not understand.',\n };\n }\n if (error.code === 'project-already-exists') {\n return {\n title: 'Project already exists',\n message: 'This repository is already connected to a Shipfox project.',\n existingProjectId: stringDetail(error, 'existing_project_id'),\n };\n }\n if (error.code === 'slug-conflict') {\n return {\n title: 'Project slug already exists',\n message: 'Choose another slug and try again.',\n slugConflict: true,\n };\n }\n\n return {\n title: 'Project request failed',\n message: error.message || 'Try again in a moment.',\n };\n}\n"],"names":["ApiError","isRecord","value","apiDetails","error","details","stringDetail","key","undefined","numberDetail","Number","isFinite","projectErrorCopy","title","message","code","retryAfter","existingProjectId","slugConflict"],"mappings":"AAAA,SAAQA,QAAQ,QAAO,sBAAsB;AAS7C,SAASC,SAASC,KAAc;IAC9B,OAAO,OAAOA,UAAU,YAAYA,UAAU;AAChD;AAEA,SAASC,WAAWC,KAAe;IACjC,IAAI,CAACH,SAASG,MAAMC,OAAO,GAAG,OAAO,CAAC;IACtC,MAAMA,UAAUD,MAAMC,OAAO,CAACA,OAAO;IACrC,OAAOJ,SAASI,WAAWA,UAAU,CAAC;AACxC;AAEA,SAASC,aAAaF,KAAe,EAAEG,GAAW;IAChD,MAAML,QAAQC,WAAWC,MAAM,CAACG,IAAI;IACpC,OAAO,OAAOL,UAAU,WAAWA,QAAQM;AAC7C;AAEA,SAASC,aAAaL,KAAe,EAAEG,GAAW;IAChD,MAAML,QAAQC,WAAWC,MAAM,CAACG,IAAI;IACpC,OAAO,OAAOL,UAAU,YAAYQ,OAAOC,QAAQ,CAACT,SAASA,QAAQM;AACvE;AAEA,OAAO,SAASI,iBAAiBR,KAAc;IAC7C,IAAI,CAAEA,CAAAA,iBAAiBJ,QAAO,GAAI;QAChC,OAAO;YACLa,OAAO;YACPC,SAAS;QACX;IACF;IAEA,IAAIV,MAAMW,IAAI,KAAK,iBAAiB;QAClC,OAAO;YACLF,OAAO;YACPC,SAAS;QACX;IACF;IACA,IAAIV,MAAMW,IAAI,KAAK,wBAAwB;QACzC,OAAO;YACLF,OAAO;YACPC,SAAS;QACX;IACF;IACA,IAAIV,MAAMW,IAAI,KAAK,+BAA+B;QAChD,OAAO;YACLF,OAAO;YACPC,SAAS;QACX;IACF;IACA,IAAIV,MAAMW,IAAI,KAAK,8BAA8B;QAC/C,OAAO;YACLF,OAAO;YACPC,SAAS;QACX;IACF;IACA,IAAIV,MAAMW,IAAI,KAAK,iBAAiB;QAClC,OAAO;YACLF,OAAO;YACPC,SAAS;QACX;IACF;IACA,IAAIV,MAAMW,IAAI,KAAK,gBAAgB;QACjC,MAAMC,aAAaP,aAAaL,OAAO;QACvC,OAAO;YACLS,OAAO;YACPC,SAASE,aACL,CAAC,mBAAmB,EAAEA,WAAW,SAAS,CAAC,GAC3C;QACN;IACF;IACA,IAAIZ,MAAMW,IAAI,KAAK,WAAW;QAC5B,OAAO;YACLF,OAAO;YACPC,SAAS;QACX;IACF;IACA,IAAIV,MAAMW,IAAI,KAAK,wBAAwB;QACzC,OAAO;YACLF,OAAO;YACPC,SAAS;QACX;IACF;IACA,IAAIV,MAAMW,IAAI,KAAK,+BAA+B;QAChD,OAAO;YACLF,OAAO;YACPC,SAAS;QACX;IACF;IACA,IAAIV,MAAMW,IAAI,KAAK,0BAA0B;QAC3C,OAAO;YACLF,OAAO;YACPC,SAAS;YACTG,mBAAmBX,aAAaF,OAAO;QACzC;IACF;IACA,IAAIA,MAAMW,IAAI,KAAK,iBAAiB;QAClC,OAAO;YACLF,OAAO;YACPC,SAAS;YACTI,cAAc;QAChB;IACF;IAEA,OAAO;QACLL,OAAO;QACPC,SAASV,MAAMU,OAAO,IAAI;IAC5B;AACF"}
@@ -1,6 +1,6 @@
1
1
  export interface ProjectRouteParams {
2
- wid: string;
3
- pid: string;
2
+ workspaceSlug: string;
3
+ projectSlug: string;
4
4
  }
5
5
  export declare function projectRouteParams(input: Record<string, unknown>): ProjectRouteParams;
6
6
  //# sourceMappingURL=inputs.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"inputs.d.ts","sourceRoot":"","sources":["../../src/routes/inputs.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;CACb;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,kBAAkB,CAKrF"}
1
+ {"version":3,"file":"inputs.d.ts","sourceRoot":"","sources":["../../src/routes/inputs.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,kBAAkB;IACjC,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,kBAAkB,CAMrF"}
@@ -1,10 +1,10 @@
1
1
  export function projectRouteParams(input) {
2
- const wid = stringParam(input.wid);
3
- const pid = stringParam(input.pid);
4
- if (!wid || !pid) throw new Error('Project route is missing required path parameters.');
2
+ const workspaceSlug = stringParam(input.workspaceSlug);
3
+ const projectSlug = stringParam(input.projectSlug);
4
+ if (!workspaceSlug || !projectSlug) throw new Error('Project route is missing required path parameters.');
5
5
  return {
6
- wid,
7
- pid
6
+ workspaceSlug,
7
+ projectSlug
8
8
  };
9
9
  }
10
10
  function stringParam(value) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/routes/inputs.ts"],"sourcesContent":["export interface ProjectRouteParams {\n wid: string;\n pid: string;\n}\n\nexport function projectRouteParams(input: Record<string, unknown>): ProjectRouteParams {\n const wid = stringParam(input.wid);\n const pid = stringParam(input.pid);\n if (!wid || !pid) throw new Error('Project route is missing required path parameters.');\n return {wid, pid};\n}\n\nfunction stringParam(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\n"],"names":["projectRouteParams","input","wid","stringParam","pid","Error","value","length","undefined"],"mappings":"AAKA,OAAO,SAASA,mBAAmBC,KAA8B;IAC/D,MAAMC,MAAMC,YAAYF,MAAMC,GAAG;IACjC,MAAME,MAAMD,YAAYF,MAAMG,GAAG;IACjC,IAAI,CAACF,OAAO,CAACE,KAAK,MAAM,IAAIC,MAAM;IAClC,OAAO;QAACH;QAAKE;IAAG;AAClB;AAEA,SAASD,YAAYG,KAAc;IACjC,OAAO,OAAOA,UAAU,YAAYA,MAAMC,MAAM,GAAG,IAAID,QAAQE;AACjE"}
1
+ {"version":3,"sources":["../../src/routes/inputs.ts"],"sourcesContent":["export interface ProjectRouteParams {\n workspaceSlug: string;\n projectSlug: string;\n}\n\nexport function projectRouteParams(input: Record<string, unknown>): ProjectRouteParams {\n const workspaceSlug = stringParam(input.workspaceSlug);\n const projectSlug = stringParam(input.projectSlug);\n if (!workspaceSlug || !projectSlug)\n throw new Error('Project route is missing required path parameters.');\n return {workspaceSlug, projectSlug};\n}\n\nfunction stringParam(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined;\n}\n"],"names":["projectRouteParams","input","workspaceSlug","stringParam","projectSlug","Error","value","length","undefined"],"mappings":"AAKA,OAAO,SAASA,mBAAmBC,KAA8B;IAC/D,MAAMC,gBAAgBC,YAAYF,MAAMC,aAAa;IACrD,MAAME,cAAcD,YAAYF,MAAMG,WAAW;IACjD,IAAI,CAACF,iBAAiB,CAACE,aACrB,MAAM,IAAIC,MAAM;IAClB,OAAO;QAACH;QAAeE;IAAW;AACpC;AAEA,SAASD,YAAYG,KAAc;IACjC,OAAO,OAAOA,UAAU,YAAYA,MAAMC,MAAM,GAAG,IAAID,QAAQE;AACjE"}
@@ -1,8 +1,8 @@
1
1
  declare const _default: import("@shipfox/client-shell/runtime").RouteImpl<{
2
2
  readonly beforeLoad: ({ params }: {
3
3
  params: {
4
- wid: string;
5
- pid: string;
4
+ workspaceSlug: string;
5
+ projectSlug: string;
6
6
  };
7
7
  }) => never;
8
8
  }>;
@@ -1 +1 @@
1
- {"version":3,"file":"project-index.d.ts","sourceRoot":"","sources":["../../src/routes/project-index.tsx"],"names":[],"mappings":";sCAIyB;QAAC,MAAM,EAAE;YAAC,GAAG,EAAE,MAAM,CAAC;YAAC,GAAG,EAAE,MAAM,CAAA;SAAC,CAAA;KAAC;;AAD7D,wBAOG"}
1
+ {"version":3,"file":"project-index.d.ts","sourceRoot":"","sources":["../../src/routes/project-index.tsx"],"names":[],"mappings":";sCAIyB;QAAC,MAAM,EAAE;YAAC,aAAa,EAAE,MAAM,CAAC;YAAC,WAAW,EAAE,MAAM,CAAA;SAAC,CAAA;KAAC;;AAD/E,wBAOG"}
@@ -3,10 +3,10 @@ import { redirect } from '@tanstack/react-router';
3
3
  export default defineRoute({
4
4
  beforeLoad: ({ params })=>{
5
5
  throw redirect({
6
- to: '/workspaces/$wid/projects/$pid/runs',
6
+ to: '/w/$workspaceSlug/p/$projectSlug/runs',
7
7
  params: {
8
- wid: params.wid,
9
- pid: params.pid
8
+ workspaceSlug: params.workspaceSlug,
9
+ projectSlug: params.projectSlug
10
10
  }
11
11
  });
12
12
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/routes/project-index.tsx"],"sourcesContent":["import {defineRoute} from '@shipfox/client-shell/runtime';\nimport {redirect} from '@tanstack/react-router';\n\nexport default defineRoute({\n beforeLoad: ({params}: {params: {wid: string; pid: string}}) => {\n throw redirect({\n to: '/workspaces/$wid/projects/$pid/runs',\n params: {wid: params.wid, pid: params.pid},\n });\n },\n});\n"],"names":["defineRoute","redirect","beforeLoad","params","to","wid","pid"],"mappings":"AAAA,SAAQA,WAAW,QAAO,gCAAgC;AAC1D,SAAQC,QAAQ,QAAO,yBAAyB;AAEhD,eAAeD,YAAY;IACzBE,YAAY,CAAC,EAACC,MAAM,EAAuC;QACzD,MAAMF,SAAS;YACbG,IAAI;YACJD,QAAQ;gBAACE,KAAKF,OAAOE,GAAG;gBAAEC,KAAKH,OAAOG,GAAG;YAAA;QAC3C;IACF;AACF,GAAG"}
1
+ {"version":3,"sources":["../../src/routes/project-index.tsx"],"sourcesContent":["import {defineRoute} from '@shipfox/client-shell/runtime';\nimport {redirect} from '@tanstack/react-router';\n\nexport default defineRoute({\n beforeLoad: ({params}: {params: {workspaceSlug: string; projectSlug: string}}) => {\n throw redirect({\n to: '/w/$workspaceSlug/p/$projectSlug/runs',\n params: {workspaceSlug: params.workspaceSlug, projectSlug: params.projectSlug},\n });\n },\n});\n"],"names":["defineRoute","redirect","beforeLoad","params","to","workspaceSlug","projectSlug"],"mappings":"AAAA,SAAQA,WAAW,QAAO,gCAAgC;AAC1D,SAAQC,QAAQ,QAAO,yBAAyB;AAEhD,eAAeD,YAAY;IACzBE,YAAY,CAAC,EAACC,MAAM,EAAyD;QAC3E,MAAMF,SAAS;YACbG,IAAI;YACJD,QAAQ;gBAACE,eAAeF,OAAOE,aAAa;gBAAEC,aAAaH,OAAOG,WAAW;YAAA;QAC/E;IACF;AACF,GAAG"}
@@ -0,0 +1,10 @@
1
+ declare const _default: import("@shipfox/client-shell/runtime").RouteImpl<{
2
+ readonly beforeLoad: ({ params }: {
3
+ params: {
4
+ workspaceSlug: string;
5
+ projectSlug: string;
6
+ };
7
+ }) => never;
8
+ }>;
9
+ export default _default;
10
+ //# sourceMappingURL=project-settings-index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-settings-index.d.ts","sourceRoot":"","sources":["../../src/routes/project-settings-index.tsx"],"names":[],"mappings":";sCAIyB;QAAC,MAAM,EAAE;YAAC,aAAa,EAAE,MAAM,CAAC;YAAC,WAAW,EAAE,MAAM,CAAA;SAAC,CAAA;KAAC;;AAD/E,wBAOG"}
@@ -0,0 +1,12 @@
1
+ import { defineRoute } from '@shipfox/client-shell/runtime';
2
+ import { redirect } from '@tanstack/react-router';
3
+ export default defineRoute({
4
+ beforeLoad: ({ params })=>{
5
+ throw redirect({
6
+ to: '/w/$workspaceSlug/p/$projectSlug/settings/general',
7
+ params
8
+ });
9
+ }
10
+ });
11
+
12
+ //# sourceMappingURL=project-settings-index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/routes/project-settings-index.tsx"],"sourcesContent":["import {defineRoute} from '@shipfox/client-shell/runtime';\nimport {redirect} from '@tanstack/react-router';\n\nexport default defineRoute({\n beforeLoad: ({params}: {params: {workspaceSlug: string; projectSlug: string}}) => {\n throw redirect({\n to: '/w/$workspaceSlug/p/$projectSlug/settings/general',\n params,\n });\n },\n});\n"],"names":["defineRoute","redirect","beforeLoad","params","to"],"mappings":"AAAA,SAAQA,WAAW,QAAO,gCAAgC;AAC1D,SAAQC,QAAQ,QAAO,yBAAyB;AAEhD,eAAeD,YAAY;IACzBE,YAAY,CAAC,EAACC,MAAM,EAAyD;QAC3E,MAAMF,SAAS;YACbG,IAAI;YACJD;QACF;IACF;AACF,GAAG"}
@@ -0,0 +1,6 @@
1
+ import { ProjectSettingsPage } from '#pages/project-settings-page.js';
2
+ declare const _default: import("@shipfox/client-shell/runtime").RouteImpl<{
3
+ readonly component: typeof ProjectSettingsPage;
4
+ }>;
5
+ export default _default;
6
+ //# sourceMappingURL=project-settings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-settings.d.ts","sourceRoot":"","sources":["../../src/routes/project-settings.tsx"],"names":[],"mappings":"AACA,OAAO,EAAC,mBAAmB,EAAC,MAAM,iCAAiC,CAAC;;;;AAEpE,wBAA6D"}
@@ -0,0 +1,7 @@
1
+ import { defineRoute } from '@shipfox/client-shell/runtime';
2
+ import { ProjectSettingsPage } from '#pages/project-settings-page.js';
3
+ export default defineRoute({
4
+ component: ProjectSettingsPage
5
+ });
6
+
7
+ //# sourceMappingURL=project-settings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/routes/project-settings.tsx"],"sourcesContent":["import {defineRoute} from '@shipfox/client-shell/runtime';\nimport {ProjectSettingsPage} from '#pages/project-settings-page.js';\n\nexport default defineRoute({component: ProjectSettingsPage});\n"],"names":["defineRoute","ProjectSettingsPage","component"],"mappings":"AAAA,SAAQA,WAAW,QAAO,gCAAgC;AAC1D,SAAQC,mBAAmB,QAAO,kCAAkC;AAEpE,eAAeD,YAAY;IAACE,WAAWD;AAAmB,GAAG"}