@shipfox/client-triggers 12.0.2 → 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 (49) hide show
  1. package/.storybook/preview.tsx +3 -3
  2. package/.turbo/turbo-build.log +1 -1
  3. package/CHANGELOG.md +23 -0
  4. package/dist/components/events-list-states.d.ts +2 -2
  5. package/dist/components/events-list-states.d.ts.map +1 -1
  6. package/dist/components/events-list-states.js +5 -5
  7. package/dist/components/events-list-states.js.map +1 -1
  8. package/dist/components/events-list.d.ts +1 -1
  9. package/dist/components/events-list.d.ts.map +1 -1
  10. package/dist/components/events-list.js +2 -2
  11. package/dist/components/events-list.js.map +1 -1
  12. package/dist/components/events-list.stories.d.ts +1 -1
  13. package/dist/components/events-list.stories.js +1 -1
  14. package/dist/components/events-list.stories.js.map +1 -1
  15. package/dist/components/trigger-event-detail.d.ts +6 -4
  16. package/dist/components/trigger-event-detail.d.ts.map +1 -1
  17. package/dist/components/trigger-event-detail.js +80 -35
  18. package/dist/components/trigger-event-detail.js.map +1 -1
  19. package/dist/components/trigger-event-detail.stories.d.ts +1 -0
  20. package/dist/components/trigger-event-detail.stories.d.ts.map +1 -1
  21. package/dist/components/trigger-event-detail.stories.js +40 -6
  22. package/dist/components/trigger-event-detail.stories.js.map +1 -1
  23. package/dist/components/types.d.ts +1 -1
  24. package/dist/components/types.d.ts.map +1 -1
  25. package/dist/components/types.js.map +1 -1
  26. package/dist/feature.d.ts +1 -1
  27. package/dist/feature.js +1 -1
  28. package/dist/feature.js.map +1 -1
  29. package/dist/pages/events-page.d.ts +2 -1
  30. package/dist/pages/events-page.d.ts.map +1 -1
  31. package/dist/pages/events-page.js +3 -2
  32. package/dist/pages/events-page.js.map +1 -1
  33. package/dist/routes/events-settings.d.ts.map +1 -1
  34. package/dist/routes/events-settings.js +2 -1
  35. package/dist/routes/events-settings.js.map +1 -1
  36. package/dist/tsconfig.test.tsbuildinfo +1 -1
  37. package/package.json +6 -5
  38. package/src/components/events-list-states.tsx +8 -6
  39. package/src/components/events-list.stories.tsx +1 -1
  40. package/src/components/events-list.test.tsx +0 -1
  41. package/src/components/events-list.tsx +2 -2
  42. package/src/components/trigger-event-detail.stories.tsx +25 -7
  43. package/src/components/trigger-event-detail.test.tsx +42 -10
  44. package/src/components/trigger-event-detail.tsx +137 -28
  45. package/src/components/types.ts +1 -1
  46. package/src/feature.ts +1 -1
  47. package/src/pages/events-page.tsx +9 -2
  48. package/src/routes/events-settings.tsx +7 -2
  49. package/tsconfig.build.tsbuildinfo +1 -1
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipfox/client-triggers",
3
3
  "license": "MIT",
4
- "version": "12.0.2",
4
+ "version": "13.0.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -28,10 +28,11 @@
28
28
  "@swc/helpers": "^0.5.17",
29
29
  "@shipfox/api-triggers-dto": "9.0.2",
30
30
  "@shipfox/client-api": "6.0.1",
31
- "@shipfox/client-shell": "12.0.2",
32
- "@shipfox/client-ui": "6.0.2",
33
- "@shipfox/integration-icons": "0.2.2",
34
- "@shipfox/react-ui": "0.3.7"
31
+ "@shipfox/client-projects": "13.0.0",
32
+ "@shipfox/client-shell": "13.0.0",
33
+ "@shipfox/client-ui": "13.0.0",
34
+ "@shipfox/integration-icons": "0.2.3",
35
+ "@shipfox/react-ui": "0.4.0"
35
36
  },
36
37
  "peerDependencies": {
37
38
  "@tanstack/react-query": "^5.101.0",
@@ -25,7 +25,7 @@ export function EventsListSkeleton() {
25
25
  );
26
26
  }
27
27
 
28
- export function EventsListEmpty({workspaceId}: {workspaceId: string}) {
28
+ export function EventsListEmpty({workspaceSlug}: {workspaceSlug?: string | undefined}) {
29
29
  return (
30
30
  <div className="p-16">
31
31
  <EmptyState
@@ -33,11 +33,13 @@ export function EventsListEmpty({workspaceId}: {workspaceId: string}) {
33
33
  title="No events yet"
34
34
  description="Events appear here once a connected integration delivers one or you fire a trigger."
35
35
  action={
36
- <Button asChild size="sm" variant="secondary">
37
- <Link to="/workspaces/$wid/settings/integrations" params={{wid: workspaceId}}>
38
- Configure integrations
39
- </Link>
40
- </Button>
36
+ workspaceSlug ? (
37
+ <Button asChild size="sm" variant="secondary">
38
+ <Link to="/w/$workspaceSlug/settings/integrations" params={{workspaceSlug}}>
39
+ Configure integrations
40
+ </Link>
41
+ </Button>
42
+ ) : undefined
41
43
  }
42
44
  />
43
45
  </div>
@@ -90,12 +90,12 @@ const meta = {
90
90
  parameters: {layout: 'padded'},
91
91
  decorators: [withWidth],
92
92
  args: {
93
+ workspaceSlug: 'acme',
93
94
  events: SAMPLE_EVENTS,
94
95
  query: makeQuery(),
95
96
  facets: SAMPLE_FACETS,
96
97
  filters: {},
97
98
  onFiltersChange: () => undefined,
98
- workspaceId: 'ws-demo',
99
99
  hasNextPage: false,
100
100
  isFetchingNextPage: false,
101
101
  onLoadMore: () => undefined,
@@ -40,7 +40,6 @@ function makeProps(overrides: Partial<EventsListProps> = {}): EventsListProps {
40
40
  facets: {sources: [{value: 'github_acme', count: 1}], events: [{value: 'push', count: 1}]},
41
41
  filters: {},
42
42
  onFiltersChange: vi.fn(),
43
- workspaceId: 'ws-1',
44
43
  hasNextPage: false,
45
44
  isFetchingNextPage: false,
46
45
  onLoadMore: vi.fn(),
@@ -18,7 +18,7 @@ export function EventsList({
18
18
  facets,
19
19
  filters,
20
20
  onFiltersChange,
21
- workspaceId,
21
+ workspaceSlug,
22
22
  hasNextPage,
23
23
  isFetchingNextPage,
24
24
  onLoadMore,
@@ -53,7 +53,7 @@ export function EventsList({
53
53
  {query.isPending ? <EventsListSkeleton /> : null}
54
54
  {!query.isPending ? <QueryLoadError query={query} subject="events" icon="pulseLine" /> : null}
55
55
  {!query.isPending && refreshFailed ? <EventsListStaleError query={query} /> : null}
56
- {showEmptyState && !activeFilters ? <EventsListEmpty workspaceId={workspaceId} /> : null}
56
+ {showEmptyState && !activeFilters ? <EventsListEmpty workspaceSlug={workspaceSlug} /> : null}
57
57
  {showEmptyState && activeFilters ? <EventsListNoMatches onClear={clearFilters} /> : null}
58
58
 
59
59
  {events.length > 0 ? (
@@ -1,7 +1,9 @@
1
1
  import {argosScreenshot} from '@argos-ci/storybook/vitest';
2
2
  import type {TriggerEventDetailResponseDto} from '@shipfox/api-triggers-dto';
3
+ import {projectsQueryKeys} from '@shipfox/client-projects';
3
4
  import {RelativeTimeProvider} from '@shipfox/react-ui/relative-time';
4
5
  import type {Decorator, Meta, StoryObj} from '@storybook/react';
6
+ import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
5
7
  import {
6
8
  createMemoryHistory,
7
9
  createRootRoute,
@@ -22,16 +24,31 @@ function minutesAgo(minutes: number): string {
22
24
  return new Date(Date.now() - minutes * 60_000).toISOString();
23
25
  }
24
26
 
27
+ function createStoryQueryClient() {
28
+ const queryClient = new QueryClient({
29
+ defaultOptions: {queries: {staleTime: Number.POSITIVE_INFINITY, retry: false}},
30
+ });
31
+ queryClient.setQueryData(projectsQueryKeys.list(WORKSPACE_ID), {
32
+ pages: [{projects: [{id: PROJECT_ID, slug: 'checkout-api'}], nextCursor: null}],
33
+ pageParams: [undefined],
34
+ });
35
+ return queryClient;
36
+ }
37
+
25
38
  const withRouter: Decorator = (Story) => {
39
+ const queryClient = createStoryQueryClient();
40
+
26
41
  function StoryRoute() {
27
42
  return (
28
- <RelativeTimeProvider>
29
- <div className="min-h-screen bg-background-subtle-base p-24 [--app-content-h:calc(100dvh_-_96px)]">
30
- <div className="@container ml-auto w-[860px]">
31
- <Story />
43
+ <QueryClientProvider client={queryClient}>
44
+ <RelativeTimeProvider>
45
+ <div className="min-h-screen bg-background-subtle-base p-24 [--app-content-h:calc(100dvh_-_96px)]">
46
+ <div className="@container ml-auto w-[860px]">
47
+ <Story />
48
+ </div>
32
49
  </div>
33
- </div>
34
- </RelativeTimeProvider>
50
+ </RelativeTimeProvider>
51
+ </QueryClientProvider>
35
52
  );
36
53
  }
37
54
 
@@ -43,7 +60,7 @@ const withRouter: Decorator = (Story) => {
43
60
  });
44
61
  const runRoute = createRoute({
45
62
  getParentRoute: () => rootRoute,
46
- path: '/workspaces/$wid/projects/$pid/runs/$workflowRunId',
63
+ path: '/w/$workspaceSlug/p/$projectSlug/runs/$workflowRunId',
47
64
  component: StoryRoute,
48
65
  });
49
66
  const router = createRouter({
@@ -146,6 +163,7 @@ const meta = {
146
163
  decorators: [withRouter],
147
164
  args: {
148
165
  workspaceId: WORKSPACE_ID,
166
+ workspaceSlug: 'acme',
149
167
  event: routedEvent,
150
168
  onBack: () => undefined,
151
169
  },
@@ -1,5 +1,7 @@
1
1
  import type {TriggerEventDetailResponseDto} from '@shipfox/api-triggers-dto';
2
+ import {projectsQueryKeys} from '@shipfox/client-projects';
2
3
  import {RelativeTimeProvider} from '@shipfox/react-ui/relative-time';
4
+ import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
3
5
  import {fireEvent, render, screen} from '@testing-library/react';
4
6
  import userEvent from '@testing-library/user-event';
5
7
  import type {ReactElement} from 'react';
@@ -60,17 +62,39 @@ function makeEvent(
60
62
  };
61
63
  }
62
64
 
63
- function renderWithProviders(ui: ReactElement) {
64
- return render(<RelativeTimeProvider>{ui}</RelativeTimeProvider>);
65
+ function renderWithProviders(
66
+ ui: ReactElement,
67
+ {includeListProject = true, includeDetailProject = false} = {},
68
+ ) {
69
+ const queryClient = new QueryClient({
70
+ defaultOptions: {queries: {retry: false, staleTime: Infinity}},
71
+ });
72
+ const project = {id: PROJECT_ID, slug: 'checkout-api'};
73
+ queryClient.setQueryData(projectsQueryKeys.list(WORKSPACE_ID), {
74
+ pages: [{projects: includeListProject ? [project] : [], nextCursor: null}],
75
+ pageParams: [undefined],
76
+ });
77
+ if (includeDetailProject) queryClient.setQueryData(projectsQueryKeys.detail(PROJECT_ID), project);
78
+ return render(
79
+ <QueryClientProvider client={queryClient}>
80
+ <RelativeTimeProvider>{ui}</RelativeTimeProvider>
81
+ </QueryClientProvider>,
82
+ );
65
83
  }
66
84
 
67
- function renderDetailView(event: TriggerEventDetailResponseDto, onBack = vi.fn()) {
85
+ function renderDetailView(
86
+ event: TriggerEventDetailResponseDto,
87
+ onBack = vi.fn(),
88
+ options?: {includeListProject?: boolean; includeDetailProject?: boolean},
89
+ ) {
68
90
  return renderWithProviders(
69
91
  <TriggerEventDetailView
70
92
  workspaceId={WORKSPACE_ID}
93
+ workspaceSlug="acme"
71
94
  event={toTriggerEventDetail(event)}
72
95
  onBack={onBack}
73
96
  />,
97
+ options,
74
98
  );
75
99
  }
76
100
 
@@ -82,7 +106,7 @@ describe('TriggerEventDetailView', () => {
82
106
  expect(screen.getByText('Deploy production')).toBeInTheDocument();
83
107
  expect(screen.getByRole('link', {name: DEPLOY_RUN_LINK_NAME})).toHaveAttribute(
84
108
  'href',
85
- `/workspaces/${WORKSPACE_ID}/projects/${PROJECT_ID}/runs/${RUN_ID}`,
109
+ `/w/acme/p/checkout-api/runs/${RUN_ID}`,
86
110
  );
87
111
  expect(screen.getByText(PAYLOAD_REF_LINE)).toBeInTheDocument();
88
112
  });
@@ -157,6 +181,18 @@ describe('TriggerEventDetailView', () => {
157
181
  expect(screen.queryByRole('link')).not.toBeInTheDocument();
158
182
  });
159
183
 
184
+ test('resolves a matched project from its detail query when it is not in the list page', async () => {
185
+ renderDetailView(makeEvent(), vi.fn(), {
186
+ includeListProject: false,
187
+ includeDetailProject: true,
188
+ });
189
+
190
+ expect(await screen.findByRole('link', {name: DEPLOY_RUN_LINK_NAME})).toHaveAttribute(
191
+ 'href',
192
+ `/w/acme/p/checkout-api/runs/${RUN_ID}`,
193
+ );
194
+ });
195
+
160
196
  test('calls onBack from the focused detail panel control', async () => {
161
197
  const onBack = vi.fn();
162
198
  renderDetailView(makeEvent(), onBack);
@@ -179,9 +215,7 @@ describe('TriggerEventDetail', () => {
179
215
  refetch: vi.fn(),
180
216
  } as unknown as ReturnType<typeof useTriggerEventQuery>);
181
217
 
182
- renderWithProviders(
183
- <TriggerEventDetail workspaceId={WORKSPACE_ID} eventId={EVENT_ID} onBack={vi.fn()} />,
184
- );
218
+ renderWithProviders(<TriggerEventDetail eventId={EVENT_ID} onBack={vi.fn()} />);
185
219
 
186
220
  expect(await screen.findByRole('button', {name: 'Back to events'})).toBeInTheDocument();
187
221
  });
@@ -194,9 +228,7 @@ describe('TriggerEventDetail', () => {
194
228
  refetch,
195
229
  } as unknown as ReturnType<typeof useTriggerEventQuery>);
196
230
 
197
- renderWithProviders(
198
- <TriggerEventDetail workspaceId={WORKSPACE_ID} eventId={EVENT_ID} onBack={vi.fn()} />,
199
- );
231
+ renderWithProviders(<TriggerEventDetail eventId={EVENT_ID} onBack={vi.fn()} />);
200
232
 
201
233
  fireEvent.click(await screen.findByRole('button', {name: 'Retry'}));
202
234
 
@@ -1,3 +1,4 @@
1
+ import {useProjectQuery, useProjectsInfiniteQuery} from '@shipfox/client-projects';
1
2
  import {Badge} from '@shipfox/react-ui/badge';
2
3
  import {Button} from '@shipfox/react-ui/button';
3
4
  import {Callout} from '@shipfox/react-ui/callout';
@@ -34,17 +35,30 @@ const DETAIL_RAIL_CLASS =
34
35
  '@min-[820px]:sticky @min-[820px]:top-16 @min-[820px]:max-h-[calc(var(--app-content-h,100dvh_-_96px)_-_32px)] @min-[820px]:min-h-[min(320px,calc(var(--app-content-h,100dvh_-_96px)_-_32px))]';
35
36
 
36
37
  export interface TriggerEventDetailProps {
37
- workspaceId: string;
38
+ workspaceId?: string | undefined;
39
+ workspaceSlug?: string | undefined;
38
40
  eventId?: string | undefined;
39
41
  onBack: () => void;
40
42
  }
41
43
 
42
- export function TriggerEventDetail({workspaceId, eventId, onBack}: TriggerEventDetailProps) {
44
+ export function TriggerEventDetail({
45
+ workspaceId,
46
+ workspaceSlug,
47
+ eventId,
48
+ onBack,
49
+ }: TriggerEventDetailProps) {
43
50
  const query = useTriggerEventQuery(eventId);
44
51
 
45
52
  if (!eventId) return <TriggerEventDetailPlaceholder />;
46
53
  if (query.data) {
47
- return <TriggerEventDetailView workspaceId={workspaceId} event={query.data} onBack={onBack} />;
54
+ return (
55
+ <TriggerEventDetailView
56
+ workspaceId={workspaceId}
57
+ workspaceSlug={workspaceSlug}
58
+ event={query.data}
59
+ onBack={onBack}
60
+ />
61
+ );
48
62
  }
49
63
  if (query.isError)
50
64
  return <TriggerEventDetailError onBack={onBack} onRetry={() => query.refetch()} />;
@@ -53,10 +67,12 @@ export function TriggerEventDetail({workspaceId, eventId, onBack}: TriggerEventD
53
67
 
54
68
  export function TriggerEventDetailView({
55
69
  workspaceId,
70
+ workspaceSlug,
56
71
  event,
57
72
  onBack,
58
73
  }: {
59
- workspaceId: string;
74
+ workspaceId?: string | undefined;
75
+ workspaceSlug?: string | undefined;
60
76
  event: TriggerEventDetailModel;
61
77
  onBack: () => void;
62
78
  }) {
@@ -124,7 +140,7 @@ export function TriggerEventDetailView({
124
140
  key={event.id}
125
141
  className="flex min-h-0 flex-1 flex-col gap-20 overflow-y-auto p-16 scrollbar"
126
142
  >
127
- <EventRuns workspaceId={workspaceId} event={event} />
143
+ <EventRuns workspaceId={workspaceId} workspaceSlug={workspaceSlug} event={event} />
128
144
  <EventPayload payload={formattedPayload} />
129
145
  </div>
130
146
  </aside>
@@ -207,7 +223,15 @@ function TriggerEventDetailError({onBack, onRetry}: {onBack: () => void; onRetry
207
223
  );
208
224
  }
209
225
 
210
- function EventRuns({workspaceId, event}: {workspaceId: string; event: TriggerEventDetailModel}) {
226
+ function EventRuns({
227
+ workspaceId,
228
+ workspaceSlug,
229
+ event,
230
+ }: {
231
+ workspaceId?: string | undefined;
232
+ workspaceSlug?: string | undefined;
233
+ event: TriggerEventDetailModel;
234
+ }) {
211
235
  if (event.decisions.length === 0) {
212
236
  if (event.outcome === 'discarded') {
213
237
  return (
@@ -219,6 +243,55 @@ function EventRuns({workspaceId, event}: {workspaceId: string; event: TriggerEve
219
243
  return null;
220
244
  }
221
245
 
246
+ if (!workspaceId) {
247
+ return <EventRunsList workspaceSlug={workspaceSlug} projectSlugs={new Map()} event={event} />;
248
+ }
249
+
250
+ return (
251
+ <EventRunsWithProjects workspaceId={workspaceId} workspaceSlug={workspaceSlug} event={event} />
252
+ );
253
+ }
254
+
255
+ function EventRunsWithProjects({
256
+ workspaceId,
257
+ workspaceSlug,
258
+ event,
259
+ }: {
260
+ workspaceId: string;
261
+ workspaceSlug?: string | undefined;
262
+ event: TriggerEventDetailModel;
263
+ }) {
264
+ const projectsQuery = useProjectsInfiniteQuery(workspaceId);
265
+ const projectSlugs = useMemo(
266
+ () =>
267
+ new Map(
268
+ projectsQuery.data?.pages
269
+ .flatMap((page) => page.projects)
270
+ .map((project) => [project.id, project.slug] as const),
271
+ ),
272
+ [projectsQuery.data],
273
+ );
274
+ return (
275
+ <EventRunsList
276
+ workspaceSlug={workspaceSlug}
277
+ projectSlugs={projectSlugs}
278
+ projectDetailLookupEnabled={!projectsQuery.isPending}
279
+ event={event}
280
+ />
281
+ );
282
+ }
283
+
284
+ function EventRunsList({
285
+ workspaceSlug,
286
+ projectSlugs,
287
+ projectDetailLookupEnabled = false,
288
+ event,
289
+ }: {
290
+ workspaceSlug?: string | undefined;
291
+ projectSlugs: ReadonlyMap<string, string>;
292
+ projectDetailLookupEnabled?: boolean;
293
+ event: TriggerEventDetailModel;
294
+ }) {
222
295
  return (
223
296
  <section aria-labelledby="trigger-event-runs-heading" className="flex flex-col gap-6">
224
297
  <Text id="trigger-event-runs-heading" size="sm" bold>
@@ -226,7 +299,14 @@ function EventRuns({workspaceId, event}: {workspaceId: string; event: TriggerEve
226
299
  </Text>
227
300
  <ul className="-mx-8 flex flex-col gap-1">
228
301
  {event.decisions.map((decision) => (
229
- <DecisionRow key={decision.id} workspaceId={workspaceId} decision={decision} />
302
+ <DecisionRow
303
+ key={decision.id}
304
+ workspaceSlug={workspaceSlug}
305
+ projectId={decision.projectId ?? undefined}
306
+ projectSlug={decision.projectId ? projectSlugs.get(decision.projectId) : undefined}
307
+ projectDetailLookupEnabled={projectDetailLookupEnabled}
308
+ decision={decision}
309
+ />
230
310
  ))}
231
311
  </ul>
232
312
  </section>
@@ -234,12 +314,25 @@ function EventRuns({workspaceId, event}: {workspaceId: string; event: TriggerEve
234
314
  }
235
315
 
236
316
  function DecisionRow({
237
- workspaceId,
317
+ projectId,
318
+ projectSlug,
319
+ workspaceSlug,
320
+ projectDetailLookupEnabled,
238
321
  decision,
239
322
  }: {
240
- workspaceId: string;
323
+ projectId?: string | undefined;
324
+ projectSlug?: string | undefined;
325
+ workspaceSlug?: string | undefined;
326
+ projectDetailLookupEnabled: boolean;
241
327
  decision: TriggerEventMatchedWorkflowResult;
242
328
  }) {
329
+ const canRenderRunLink =
330
+ decision.decision === 'triggered' && Boolean(decision.runId && decision.runName);
331
+ const projectQuery = useProjectQuery(
332
+ canRenderRunLink && projectDetailLookupEnabled && !projectSlug ? projectId : undefined,
333
+ );
334
+ const resolvedProjectSlug = projectSlug ?? projectQuery.data?.slug;
335
+
243
336
  if (decision.decision !== 'triggered' || !decision.runId || !decision.runName) {
244
337
  return (
245
338
  <li className="flex min-w-0 items-start gap-8 rounded-6 px-8 py-6">
@@ -266,27 +359,43 @@ function DecisionRow({
266
359
  );
267
360
  }
268
361
 
362
+ const row = (
363
+ <>
364
+ <Icon
365
+ name="cornerDownRightLine"
366
+ className="mt-3 size-14 shrink-0 text-foreground-neutral-muted"
367
+ aria-hidden="true"
368
+ />
369
+ <span className="flex min-w-0 flex-col gap-1">
370
+ <Text as="span" size="sm" className="min-w-0 truncate text-foreground-neutral-base">
371
+ {decision.subscriptionName}
372
+ </Text>
373
+ <Code as="span" variant="label" className="truncate text-foreground-neutral-muted">
374
+ {decision.runName}
375
+ </Code>
376
+ </span>
377
+ </>
378
+ );
379
+ const rowClassName =
380
+ 'flex min-w-0 items-start gap-8 rounded-6 px-8 py-6 hover:bg-background-components-hover focus-visible:outline-none focus-visible:shadow-button-neutral-focus';
381
+
269
382
  return (
270
383
  <li>
271
- <Link
272
- to="/workspaces/$wid/projects/$pid/runs/$workflowRunId"
273
- params={{wid: workspaceId, pid: decision.projectId ?? '', workflowRunId: decision.runId}}
274
- className="flex min-w-0 items-start gap-8 rounded-6 px-8 py-6 transition-colors hover:bg-background-components-hover focus-visible:shadow-border-interactive-with-active focus-visible:outline-none"
275
- >
276
- <Icon
277
- name="cornerDownRightLine"
278
- className="mt-3 size-14 shrink-0 text-foreground-neutral-muted"
279
- aria-hidden="true"
280
- />
281
- <span className="flex min-w-0 flex-col gap-1">
282
- <Text as="span" size="sm" className="min-w-0 truncate text-foreground-neutral-base">
283
- {decision.subscriptionName}
284
- </Text>
285
- <Code as="span" variant="label" className="truncate text-foreground-neutral-muted">
286
- {decision.runName}
287
- </Code>
288
- </span>
289
- </Link>
384
+ {workspaceSlug && resolvedProjectSlug ? (
385
+ <Link
386
+ to="/w/$workspaceSlug/p/$projectSlug/runs/$workflowRunId"
387
+ params={{
388
+ workspaceSlug,
389
+ projectSlug: resolvedProjectSlug,
390
+ workflowRunId: decision.runId,
391
+ }}
392
+ className={rowClassName}
393
+ >
394
+ {row}
395
+ </Link>
396
+ ) : (
397
+ <div className={rowClassName}>{row}</div>
398
+ )}
290
399
  </li>
291
400
  );
292
401
  }
@@ -13,7 +13,7 @@ export interface EventsListProps {
13
13
  facets: TriggerEventFacets | undefined;
14
14
  filters: TriggerEventFilters;
15
15
  onFiltersChange: (patch: Partial<TriggerEventFilters>) => void;
16
- workspaceId: string;
16
+ workspaceSlug?: string | undefined;
17
17
  hasNextPage: boolean;
18
18
  isFetchingNextPage: boolean;
19
19
  onLoadMore: () => void;
package/src/feature.ts CHANGED
@@ -8,7 +8,7 @@ export const triggersFeature = defineClientFeature({
8
8
  id: 'shipfox.triggers',
9
9
  routes: [
10
10
  {
11
- path: '/workspaces/$wid/settings/events',
11
+ path: '/w/$workspaceSlug/settings/events',
12
12
  parent: 'workspaceSettings',
13
13
  impl: '@shipfox/client-triggers/routes/events-settings',
14
14
  },
@@ -12,6 +12,7 @@ import {
12
12
 
13
13
  export interface EventsPageProps {
14
14
  workspaceId: string;
15
+ workspaceSlug?: string | undefined;
15
16
  filters: TriggerEventFilters;
16
17
  onFiltersChange: (patch: Partial<TriggerEventFilters>) => void;
17
18
  }
@@ -20,7 +21,12 @@ export interface EventsPageProps {
20
21
  * Workspace-scoped Events list. Router-agnostic: filters and their setter come in as props
21
22
  * (the settings wrapper binds them to the URL), so a story can drive it with local state.
22
23
  */
23
- export function EventsPage({workspaceId, filters, onFiltersChange}: EventsPageProps) {
24
+ export function EventsPage({
25
+ workspaceId,
26
+ workspaceSlug,
27
+ filters,
28
+ onFiltersChange,
29
+ }: EventsPageProps) {
24
30
  const query = useTriggerEventsInfiniteQuery(workspaceId, filters);
25
31
  const facetsQuery = useTriggerEventFacetsQuery(workspaceId);
26
32
  const [selectedEventId, setSelectedEventId] = useState<string | undefined>();
@@ -59,7 +65,7 @@ export function EventsPage({workspaceId, filters, onFiltersChange}: EventsPagePr
59
65
  facets={facetsQuery.data}
60
66
  filters={filters}
61
67
  onFiltersChange={onFiltersChange}
62
- workspaceId={workspaceId}
68
+ workspaceSlug={workspaceSlug}
63
69
  hasNextPage={query.hasNextPage}
64
70
  isFetchingNextPage={query.isFetchingNextPage}
65
71
  onLoadMore={() => {
@@ -71,6 +77,7 @@ export function EventsPage({workspaceId, filters, onFiltersChange}: EventsPagePr
71
77
  </div>
72
78
  <TriggerEventDetail
73
79
  workspaceId={workspaceId}
80
+ workspaceSlug={workspaceSlug}
74
81
  eventId={selectedEventId}
75
82
  onBack={() => setSelectedEventId(undefined)}
76
83
  />
@@ -4,7 +4,7 @@ import type {TriggerEventFilters} from '#core/trigger-event.js';
4
4
  import {EventsPage} from '#pages/events-page.js';
5
5
  import {type TriggerEventsSearch, validateTriggerEventsSearch} from '#search.js';
6
6
 
7
- const routeApi = getRouteApi('/workspaces/$wid/settings/events');
7
+ const routeApi = getRouteApi('/w/$workspaceSlug/settings/events');
8
8
 
9
9
  // Wrapped (not just re-typed) so the search validator's own type is portable outside this
10
10
  // package: TanStack Router's `const` generics capture the exact declared function, and a
@@ -24,7 +24,12 @@ export default defineRoute({
24
24
  void navigate({search: {...search, ...patch}, replace: true});
25
25
  };
26
26
  return (
27
- <EventsPage workspaceId={workspace.id} filters={search} onFiltersChange={onFiltersChange} />
27
+ <EventsPage
28
+ workspaceId={workspace.id}
29
+ workspaceSlug={workspace.slug}
30
+ filters={search}
31
+ onFiltersChange={onFiltersChange}
32
+ />
28
33
  );
29
34
  },
30
35
  });