Skip to content

Commit 3dfac32

Browse files
committed
improvement(sim-links): illustrated empty state, server-safe page column class, stable audit poll
1 parent 549988a commit 3dfac32

7 files changed

Lines changed: 142 additions & 11 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/**
2+
* The home surface's reading column, so every organization page shares its width. Kept out of
3+
* the client component module so server-rendered loading views receive the class string itself.
4+
*/
5+
export const PAGE_COLUMN_CLASS = 'mx-auto w-full max-w-chat px-6'
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
export { PAGE_COLUMN_CLASS } from '@/app/o/[organizationId]/components/organization-page/constants'
12
export {
23
OrganizationPage,
34
type OrganizationPageTab,
4-
PAGE_COLUMN_CLASS,
55
} from '@/app/o/[organizationId]/components/organization-page/organization-page'
66
export { useOrganizationPageFilters } from '@/app/o/[organizationId]/components/organization-page/use-organization-page-filters'

‎apps/sim/app/o/[organizationId]/components/organization-page/organization-page.tsx‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,13 @@ import {
1313
} from '@sim/emcn'
1414
import { Search, X } from '@sim/emcn/icons'
1515
import { HEADER_ACTION_CLUSTER, PAGE_HEADER_BAR } from '@/components/page-header-bar'
16+
import { PAGE_COLUMN_CLASS } from '@/app/o/[organizationId]/components/organization-page/constants'
1617
import { useOrganizationPageFilters } from '@/app/o/[organizationId]/components/organization-page/use-organization-page-filters'
1718
import {
1819
SIDEBAR_DIVIDER_PAD_ABOVE_CLASS,
1920
SIDEBAR_DIVIDER_PAD_BELOW_CLASS,
2021
} from '@/app/workspace/[workspaceId]/w/components/sidebar/constants'
2122

22-
/** The home surface's reading column, so every organization page shares its width. */
23-
export const PAGE_COLUMN_CLASS = 'mx-auto w-full max-w-chat px-6'
24-
2523
export interface OrganizationPageTab {
2624
id: string
2725
label: string

‎apps/sim/app/o/[organizationId]/links/sim-links.tsx‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
} from '@/app/o/[organizationId]/components/organization-page'
2828
import { simLinkViewParsers } from '@/app/o/[organizationId]/links/search-params'
2929
import { SimLinkModal } from '@/app/o/[organizationId]/links/sim-link-modal'
30+
import { SimLinksEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state'
3031
import {
3132
SettingsEmptyState,
3233
SettingsQueryErrorState,
@@ -116,12 +117,10 @@ export function SimLinks({ organizationId }: SimLinksProps) {
116117
/>
117118
) : !links.data ? (
118119
<SettingsEmptyState variant='inline'>Loading Sim links</SettingsEmptyState>
120+
) : !rows.length && query ? (
121+
<SettingsEmptyState variant='inline'>No matching Sim links</SettingsEmptyState>
119122
) : !rows.length ? (
120-
<SettingsEmptyState variant='inline'>
121-
{query
122-
? 'No matching Sim links'
123-
: 'No Sim links yet. Create one for a page your team opens often, like sim/handbook.'}
124-
</SettingsEmptyState>
123+
<SimLinksEmptyState onCreate={() => void setView({ create: '' })} />
125124
) : (
126125
rows.map((link) => {
127126
const path = simLinkPath(link.name)

‎apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ export { FilesEmptyState } from '@/app/workspace/[workspaceId]/components/resour
55
export { KnowledgeEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/knowledge-empty-state'
66
export { LogsEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/logs-empty-state'
77
export { ResourceNoResults } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/resource-no-results'
8+
export { SimLinksEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/sim-links-empty-state'
89
export { TablesEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/tables-empty-state'
910
export { TestsEmptyState } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/tests-empty-state'
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { Chip, cn } from '@sim/emcn'
2+
import { EmptyState } from '@/components/empty-state/empty-state'
3+
import { HAIRLINE } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/hairline'
4+
import { MASK_NO_REPEAT } from '@/app/workspace/[workspaceId]/components/resource/components/resource-empty-state/mask'
5+
6+
/**
7+
* Skeleton ink — see the `INK` note in `tables-empty-state.tsx` for why not the surface
8+
* ramp. `mark` is the tables selection ring's opaque mix, so the held link reads as the
9+
* one drawn shortcut rather than more placeholder.
10+
*/
11+
const INK = {
12+
title: 'color-mix(in srgb, var(--text-secondary) 30%, transparent)',
13+
detail: 'color-mix(in srgb, var(--text-secondary) 15%, transparent)',
14+
mark: 'color-mix(in srgb, var(--text-secondary) 46%, var(--bg))',
15+
} as const
16+
17+
/** The logs feed's falloff — a link directory is the same repeating structure. */
18+
const DIRECTORY_FADE =
19+
'[-webkit-mask-image:linear-gradient(to_bottom,#000_44%,transparent_100%)] [mask-image:linear-gradient(to_bottom,#000_44%,transparent_100%)]'
20+
21+
interface LinkRow {
22+
name: number
23+
destination: number
24+
}
25+
26+
/** The held link sits second, inside the fade's opaque head, so its card never dissolves. */
27+
const ROWS: readonly LinkRow[] = [
28+
{ name: 54, destination: 132 },
29+
{ name: 70, destination: 108 },
30+
{ name: 46, destination: 140 },
31+
{ name: 62, destination: 118 },
32+
] as const
33+
34+
const HELD_ROW = 1
35+
const ROW_PITCH = 38
36+
const PILL_X = 12
37+
const PILL_PADDING = 10
38+
39+
/** A link directory: short names pointing at their destinations, one held in a card. */
40+
function SimLinksGraphic() {
41+
return (
42+
<svg
43+
viewBox='0 0 286 148'
44+
width={286}
45+
height={148}
46+
fill='none'
47+
aria-hidden='true'
48+
focusable='false'
49+
className={cn('block max-w-none shrink-0', DIRECTORY_FADE, MASK_NO_REPEAT)}
50+
>
51+
{ROWS.map((row, index) => {
52+
const y = 6 + index * ROW_PITCH
53+
const held = index === HELD_ROW
54+
const ink = held ? INK.mark : INK.title
55+
const arrowX = PILL_X + row.name + PILL_PADDING * 2 + 10
56+
return (
57+
<g key={y}>
58+
{held ? (
59+
<rect
60+
x='0.5'
61+
y={y - 2}
62+
width='285'
63+
height='36'
64+
rx='8'
65+
fill='var(--surface-2)'
66+
{...HAIRLINE}
67+
/>
68+
) : null}
69+
<rect
70+
x={PILL_X}
71+
y={y + 8}
72+
width={row.name + PILL_PADDING * 2}
73+
height='16'
74+
rx='8'
75+
fill='var(--surface-2)'
76+
stroke={held ? INK.mark : 'var(--border)'}
77+
strokeWidth={held ? 1.4 : HAIRLINE.strokeWidth}
78+
/>
79+
<rect
80+
x={PILL_X + PILL_PADDING}
81+
y={y + 14}
82+
width={row.name}
83+
height='4'
84+
rx='2'
85+
fill={ink}
86+
/>
87+
<path
88+
d={`M ${arrowX} ${y + 16} H ${arrowX + 18} M ${arrowX + 13} ${y + 11.5} L ${arrowX + 18} ${y + 16} L ${arrowX + 13} ${y + 20.5}`}
89+
stroke={ink}
90+
strokeWidth={1.4}
91+
strokeLinecap='round'
92+
strokeLinejoin='round'
93+
/>
94+
<rect
95+
x={arrowX + 28}
96+
y={y + 14}
97+
width={row.destination}
98+
height='4'
99+
rx='2'
100+
fill={INK.detail}
101+
/>
102+
</g>
103+
)
104+
})}
105+
</svg>
106+
)
107+
}
108+
109+
interface SimLinksEmptyStateProps {
110+
onCreate: () => void
111+
}
112+
113+
/** Empty state for an organization with no Sim links yet. */
114+
export function SimLinksEmptyState({ onCreate }: SimLinksEmptyStateProps) {
115+
return (
116+
<EmptyState
117+
graphic={<SimLinksGraphic />}
118+
title='Sim Links'
119+
description='Create links like sim/handbook for the pages your team opens most.'
120+
action={
121+
<Chip variant='primary' onClick={onCreate}>
122+
New link
123+
</Chip>
124+
}
125+
/>
126+
)
127+
}

‎apps/sim/scripts/test-sim-links-e2e.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,13 +337,14 @@ async function run() {
337337
await check('deleting a link stops it resolving and every change is audited', async () => {
338338
await api(owner, `/${handbook}`, { method: 'DELETE' })
339339
assert.match((await open(member, 'team-handbook')).text, /doesn.*t exist yet/)
340+
const expected = ['sim_link.created', 'sim_link.updated', 'sim_link.deleted']
340341
let actions: string[] = []
341342
for (let attempt = 0; attempt < 20; attempt++) {
342343
actions = await auditActions(orgId)
343-
if (actions.includes('sim_link.deleted')) break
344+
if (expected.every((action) => actions.includes(action))) break
344345
await sleep(250)
345346
}
346-
for (const action of ['sim_link.created', 'sim_link.updated', 'sim_link.deleted']) {
347+
for (const action of expected) {
347348
assert(actions.includes(action), `Expected an ${action} audit entry, got ${actions}`)
348349
}
349350
})

0 commit comments

Comments
 (0)