From 5a3f00967df11051f8c4c024a3ef245332db2877 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Tue, 13 Oct 2026 11:31:38 -0700 Subject: [PATCH] feat(empty-states): add tests and changelog empty-state graphics --- .../[workspaceId]/changelog/changelog.tsx | 7 +- .../changelog-empty-state.tsx | 111 ++++++++++++++++ .../components/resource-empty-state/index.ts | 2 + .../tests-empty-state.tsx | 125 ++++++++++++++++++ .../workspace/[workspaceId]/tests/tests.tsx | 7 +- 5 files changed, 246 insertions(+), 6 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/changelog-empty-state.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/tests-empty-state.tsx diff --git a/apps/sim/app/workspace/[workspaceId]/changelog/changelog.tsx b/apps/sim/app/workspace/[workspaceId]/changelog/changelog.tsx index faf3f8857d1..48bda52d9da 100644 --- a/apps/sim/app/workspace/[workspaceId]/changelog/changelog.tsx +++ b/apps/sim/app/workspace/[workspaceId]/changelog/changelog.tsx @@ -3,13 +3,13 @@ import { useState } from 'react' import { Chip, cn, scrollFadeAttributes, scrollFadeClass, useScrollEdges } from '@sim/emcn' import { Rss } from '@sim/emcn/icons' -import { EmptyState } from '@/components/empty-state/empty-state' import type { ChangelogRelease } from '@/lib/api/contracts/changelog' import { ReleaseDetailsModal, ReleaseEntry, } from '@/app/workspace/[workspaceId]/changelog/components' import ChangelogLoading from '@/app/workspace/[workspaceId]/changelog/loading' +import { ChangelogEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state' import { Resource } from '@/app/workspace/[workspaceId]/components/resource/resource' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { PermissionAccessBoundary } from '@/ee/access-requests/components/permission-access-boundary' @@ -47,10 +47,7 @@ function ChangelogContent({ workspaceId }: ChangelogProps) { query.refetch()}>Retry ) : releases.length === 0 ? ( - + ) : (
{ + const y = release.top + 26 + index * NOTE_PITCH + return ( + + + + + ) + }) +} + +/** A release timeline: the latest release ringed at the head, earlier ones falling away below. */ +function ChangelogGraphic() { + const latestY = LATEST.top + 8 + return ( + + ) +} + +/** Empty state for a workspace that has no published releases yet. */ +export function ChangelogEmptyState() { + return ( + } + title='Changelog' + description='Sim publishes a release here when it ships changes to this workspace.' + /> + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/index.ts b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/index.ts index 488ce28e54e..cd37a146c47 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/index.ts @@ -1,3 +1,4 @@ +export { ChangelogEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/changelog-empty-state' export { DashboardEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/dashboard-empty-state' export { DocumentsEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/documents-empty-state' export { FilesEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/files-empty-state' @@ -5,3 +6,4 @@ export { KnowledgeEmptyState } from '@/app/workspace/[workspaceId]/components/re export { LogsEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/logs-empty-state' export { ResourceNoResults } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/resource-no-results' export { TablesEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/tables-empty-state' +export { TestsEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/tests-empty-state' diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/tests-empty-state.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/tests-empty-state.tsx new file mode 100644 index 00000000000..946fa1cfc79 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/tests-empty-state.tsx @@ -0,0 +1,125 @@ +import { cn } from '@sim/emcn' +import { EmptyState } from '@/components/empty-state/empty-state' +import { HAIRLINE } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/hairline' +import { MASK_NO_REPEAT } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/mask' + +/** + * Skeleton ink — see the `INK` note in `tables-empty-state.tsx` for why not the surface + * ramp. `mark` is the tables selection ring's opaque mix, so a status glyph reads as + * drawn rather than as more placeholder. + */ +const INK = { + title: 'color-mix(in srgb, var(--text-secondary) 30%, transparent)', + detail: 'color-mix(in srgb, var(--text-secondary) 15%, transparent)', + mark: 'color-mix(in srgb, var(--text-secondary) 46%, var(--bg))', +} as const + +/** The logs feed's falloff — a run report is the same repeating structure. */ +const REPORT_FADE = + '[-webkit-mask-image:linear-gradient(to_bottom,#000_44%,transparent_100%)] [mask-image:linear-gradient(to_bottom,#000_44%,transparent_100%)]' + +type TestStatus = 'pass' | 'fail' + +interface ReportRow { + status: TestStatus + title: number + detail: number +} + +/** + * The failure sits second, inside the fade's opaque head, so the one row held in a card + * never dissolves mid-stroke. + */ +const ROWS: readonly ReportRow[] = [ + { status: 'pass', title: 72, detail: 110 }, + { status: 'fail', title: 64, detail: 124 }, + { status: 'pass', title: 80, detail: 96 }, + { status: 'pass', title: 58, detail: 116 }, +] as const + +const ROW_PITCH = 38 +const BADGE_RADIUS = 8 +const GLYPH = BADGE_RADIUS * 0.42 + +const MARK_STROKE = { + stroke: INK.mark, + strokeWidth: 1.4, + strokeLinecap: 'round', + strokeLinejoin: 'round', +} as const + +interface StatusBadgeProps { + cx: number + cy: number + status: TestStatus +} + +/** A pass ticks on a hairline ring; a fail crosses on the heavier ring it shares with its card. */ +function StatusBadge({ cx, cy, status }: StatusBadgeProps) { + const glyph = + status === 'pass' + ? `M ${cx - GLYPH} ${cy} L ${cx - GLYPH * 0.25} ${cy + GLYPH * 0.75} L ${cx + GLYPH} ${cy - GLYPH * 0.7}` + : `M ${cx - GLYPH * 0.75} ${cy - GLYPH * 0.75} L ${cx + GLYPH * 0.75} ${cy + GLYPH * 0.75} M ${cx + GLYPH * 0.75} ${cy - GLYPH * 0.75} L ${cx - GLYPH * 0.75} ${cy + GLYPH * 0.75}` + return ( + + + + + ) +} + +/** A test run report: passing rows, the one failure held in a card like the logs feed's latest run. */ +function TestsGraphic() { + return ( + + ) +} + +/** Empty state for a workspace with no tests yet. */ +export function TestsEmptyState() { + return ( + } + title='Tests' + description='Write tests to check your workflows behave before you deploy.' + /> + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/tests/tests.tsx b/apps/sim/app/workspace/[workspaceId]/tests/tests.tsx index 4960ec0ba57..bcdb1450070 100644 --- a/apps/sim/app/workspace/[workspaceId]/tests/tests.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tests/tests.tsx @@ -8,7 +8,10 @@ import { useQueryStates } from 'nuqs' import type { WorkflowTestRecord } from '@/lib/api/contracts/workflow-tests' import { SEARCH_DEBOUNCE_MS } from '@/lib/url-state' import { ownerCell } from '@/app/workspace/[workspaceId]/components/resource/components/owner-cell' -import { ResourceNoResults } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state' +import { + ResourceNoResults, + TestsEmptyState, +} from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state' import type { SearchConfig, SortConfig, @@ -154,6 +157,8 @@ export function Tests({ workspaceId }: TestsProps) {

{query.error.message}

query.refetch()}>Retry
+ ) : query.data?.length === 0 ? ( + ) : debouncedSearch.trim() && query.data?.length ? (