Skip to content

Commit 82a72bb

Browse files
committed
feat(home): hold Needs your attention and Workspaces behind platform-preview
Needs your attention was hidden by a code constant, HOME_HIDDEN_SECTIONS, and the Workspaces widget had been deleted. Both now wait for the design pass behind the platform-preview flag instead: HomeView carries `attention` and `workspaces` only while Home offers them, so the dashboard renders a section, and Customize Home lists Workspaces, only when it is present. useLiveHome reads neither section's data while the flag is off. The widget is restored from 5b97c03^ with its activity read and key, moved to the shared FailureNotice, and without the "…" detail it showed while its activity loaded. Example data always shows both sections.
1 parent 4ab2cec commit 82a72bb

13 files changed

Lines changed: 301 additions & 31 deletions

File tree

‎apps/sim/app/(product)/_components/model.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { SettingsSearchEntry } from '@/platform/shared/settings-search'
55
import type { ShellDestination, ShellItem, ShellQueryStatus } from '@/platform/shared/shell'
66

77
export type HomeMode = 'Build' | 'Search'
8-
export type HomeWidget = 'Activity' | 'Email' | 'Calendar'
8+
export type HomeWidget = 'Activity' | 'Workspaces' | 'Email' | 'Calendar'
99

1010
export interface InboxItem extends ShellItem {
1111
description: string

‎apps/sim/app/(product)/o/[organizationId]/home/_components/fixture-home/fixture-home.tsx‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { useState } from 'react'
44
import type { RecentItem } from '@sim/emcn-platform/recent-items'
55
import { useRouter, useSearchParams } from 'next/navigation'
6+
import type { ShellWorkspace } from '@/app/(product)/_components/model'
67
import {
78
useInterfacePreferences,
89
useRecentHistory,
@@ -17,6 +18,7 @@ import type {
1718
import { surfaceHref } from '@/platform/lib/data/organization-model'
1819
import { platformHref } from '@/platform/lib/navigation'
1920
import { useSurfaceScope } from '@/platform/lib/surface-scope'
21+
import { workspaceHomePath } from '@/platform/lib/workspace-navigation'
2022
import type {
2123
HomeCalendarDay,
2224
HomeConnectOption,
@@ -71,10 +73,12 @@ function metricPoints(data: HomeData | null, field: 'tasks' | 'searches') {
7173
interface FixtureHomeProps {
7274
data: HomeData | null
7375
recentLinks: readonly RecentItem[]
76+
/** The sidebar's example workspaces, which Home's Workspaces lists without claiming any activity. */
77+
workspaces: readonly ShellWorkspace[]
7478
}
7579

7680
/** The session implementation of Home's view: edits last as long as the product session. */
77-
export function FixtureHome({ data, recentLinks }: FixtureHomeProps) {
81+
export function FixtureHome({ data, recentLinks, workspaces }: FixtureHomeProps) {
7882
const router = useRouter()
7983
const search = useSearchParams()
8084
const scope = useSurfaceScope()
@@ -115,6 +119,14 @@ export function FixtureHome({ data, recentLinks }: FixtureHomeProps) {
115119
calendarForOffset(data, dayOffset)
116120
)
117121
: { status: 'not_connected', options: CONNECT_OPTIONS.Calendar },
122+
workspaces: {
123+
status: 'ready',
124+
items: workspaces.map(({ id, name }) => ({
125+
id,
126+
name,
127+
href: platformHref(workspaceHomePath(id, 'fixture'), search),
128+
})),
129+
},
118130
recents: { status: 'ready', items: recents },
119131
getStarted: [],
120132
composerInventory: data?.composerInventory,
@@ -127,6 +139,7 @@ export function FixtureHome({ data, recentLinks }: FixtureHomeProps) {
127139
returnToToday: () => setDayOffset(0),
128140
retryAttention: () => {},
129141
retryActivity: () => {},
142+
retryWorkspaces: () => {},
130143
retryRecents: () => {},
131144
removeRecent,
132145
}

‎apps/sim/app/(product)/o/[organizationId]/home/_components/home-dashboard/home-dashboard.tsx‎

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ import {
1818
import { CalendarDay } from '@/app/(product)/o/[organizationId]/home/_components/calendar/calendar-day'
1919
import { ConnectedWidget } from '@/app/(product)/o/[organizationId]/home/_components/connected-widget/connected-widget'
2020
import styles from '@/app/(product)/o/[organizationId]/home/_components/home-dashboard/home-dashboard.module.css'
21-
import {
22-
HOME_HIDDEN_SECTIONS,
23-
type HomeActions,
24-
type HomeView,
21+
import { HomeWorkspaces } from '@/app/(product)/o/[organizationId]/home/_components/home-workspaces/home-workspaces'
22+
import type {
23+
HomeActions,
24+
HomeView,
2525
} from '@/app/(product)/o/[organizationId]/home/_components/model'
2626

27-
const WIDGETS: readonly HomeWidget[] = ['Activity', 'Email', 'Calendar']
27+
const WIDGETS: readonly HomeWidget[] = ['Activity', 'Workspaces', 'Email', 'Calendar']
2828
const ACTIVITY_METRIC_TITLES: Record<HomeMode, string> = { Build: 'Tasks run', Search: 'Searches' }
2929

3030
interface HomeDashboardProps {
@@ -58,8 +58,12 @@ export function HomeDashboard({
5858
const metricTitle = ACTIVITY_METRIC_TITLES[mode]
5959
const activity = view.activity[mode]
6060
const widgetLabel = (widget: HomeWidget) => (widget === 'Activity' ? metricTitle : widget)
61-
const customizable = WIDGETS.filter(
62-
(widget) => widget !== 'Activity' || activity.status !== 'hidden'
61+
const workspaces = view.workspaces
62+
const noWorkspaces = workspaces?.status === 'ready' && workspaces.items.length === 0
63+
const customizable = WIDGETS.filter((widget) =>
64+
widget === 'Activity'
65+
? activity.status !== 'hidden'
66+
: widget !== 'Workspaces' || (workspaces !== undefined && !noWorkspaces)
6367
)
6468
function setVisible(widget: HomeWidget, visible: boolean) {
6569
const id = `hide-${widget}`
@@ -143,6 +147,13 @@ export function HomeDashboard({
143147
onRetry={actions.retryActivity}
144148
hidden={hiddenWidgets.includes('Activity')}
145149
/>
150+
{workspaces && !hiddenWidgets.includes('Workspaces') && (
151+
<HomeWorkspaces
152+
state={workspaces}
153+
asOf={view.asOf}
154+
onRetry={actions.retryWorkspaces}
155+
/>
156+
)}
146157
{!hiddenWidgets.includes('Email') && (
147158
<ConnectedWidget
148159
title='Email'
@@ -157,7 +168,7 @@ export function HomeDashboard({
157168
)}
158169
</div>
159170
<div className={styles.column}>
160-
{!HOME_HIDDEN_SECTIONS.has('Needs your attention') && (
171+
{view.attention && (
161172
<AttentionList
162173
state={view.attention}
163174
asOf={view.asOf}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
.viewport {
2+
display: grid;
3+
grid-template-columns: minmax(0, 1fr);
4+
grid-template-rows: minmax(0, 1fr) auto;
5+
gap: var(--ui-list-gap);
6+
min-block-size: 0;
7+
margin-inline: calc(-1 * var(--ui-row-inset));
8+
}
9+
.entries {
10+
display: grid;
11+
gap: var(--ui-list-gap);
12+
min-inline-size: 0;
13+
}
14+
/* The button's hit area sits on the rows' outer edge, so its label shares their text edge. */
15+
.more {
16+
display: flex;
17+
justify-self: start;
18+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
'use client'
2+
3+
import { useState } from 'react'
4+
import { ActivityRow } from '@sim/emcn/activity-row'
5+
import { Button } from '@sim/emcn/button'
6+
import { ScrollArea } from '@sim/emcn/scroll-area'
7+
import { FailureNotice, Spinner } from '@sim/emcn/status'
8+
import { HomeSection } from '@/app/(product)/_components/home-section/home-section'
9+
import styles from '@/app/(product)/o/[organizationId]/home/_components/home-workspaces/home-workspaces.module.css'
10+
import type { HomeListState, HomeWorkspaceItem } from '@/platform/shared/home'
11+
import { formatRelativeTime } from '@/platform/shared/time'
12+
13+
/** The sidebar's own page size, so See more advances the same way in both places. */
14+
const PAGE_SIZE = 5
15+
16+
const STATUS_LABEL = { review: 'Review', running: 'Running' } as const
17+
18+
function metadataOf(item: HomeWorkspaceItem, asOf: number) {
19+
if (item.status) return STATUS_LABEL[item.status]
20+
return item.lastRunAt === undefined ? undefined : formatRelativeTime(item.lastRunAt, asOf)
21+
}
22+
23+
interface HomeWorkspacesProps {
24+
state: HomeListState<HomeWorkspaceItem>
25+
asOf: number
26+
onRetry: () => void
27+
}
28+
29+
/** The viewer's workspaces with what their runs are doing. Renders nothing for a viewer with none. */
30+
export function HomeWorkspaces({ state, asOf, onRetry }: HomeWorkspacesProps) {
31+
const [visibleCount, setVisibleCount] = useState(PAGE_SIZE)
32+
if (state.status === 'ready' && state.items.length === 0) return null
33+
return (
34+
<HomeSection title='Workspaces'>
35+
{state.status === 'error' && (
36+
<FailureNotice
37+
message='Workspaces could not load.'
38+
retrying={state.retrying}
39+
onRetry={onRetry}
40+
/>
41+
)}
42+
{state.status === 'ready' && (
43+
<>
44+
{state.partial && (
45+
<FailureNotice
46+
intent='warning'
47+
message='What these workspaces are doing could not load.'
48+
onRetry={onRetry}
49+
/>
50+
)}
51+
<div className={styles.viewport} aria-busy={state.settling || undefined}>
52+
<ScrollArea label='Workspace list'>
53+
<ul className={styles.entries}>
54+
{state.items.slice(0, visibleCount).map((item) => (
55+
<ActivityRow
56+
key={item.id}
57+
title={item.name}
58+
detail={state.settling ? undefined : item.detail}
59+
textMetadata={state.settling ? undefined : metadataOf(item, asOf)}
60+
metadataLeading={item.status === 'running' ? <Spinner /> : undefined}
61+
href={item.href}
62+
/>
63+
))}
64+
</ul>
65+
</ScrollArea>
66+
{state.items.length > PAGE_SIZE && (
67+
<span className={styles.more}>
68+
<Button
69+
variant='quiet'
70+
size='compact'
71+
onClick={() =>
72+
setVisibleCount(
73+
state.items.length > visibleCount ? visibleCount + PAGE_SIZE : PAGE_SIZE
74+
)
75+
}
76+
>
77+
{state.items.length > visibleCount ? 'See more' : 'See less'}
78+
</Button>
79+
</span>
80+
)}
81+
</div>
82+
</>
83+
)}
84+
</HomeSection>
85+
)
86+
}

‎apps/sim/app/(product)/o/[organizationId]/home/_components/live-home/live-home.tsx‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@ import type { HomeWidget } from '@/app/(product)/_components/model'
44
import { SavedAccountModal } from '@/app/(product)/_components/service-account-modal/saved-account-modal'
55
import { useInterfacePreferences } from '@/app/(product)/_components/session/interface-preferences'
66
import { ZoneHomeDashboard } from '@/app/(product)/o/[organizationId]/home/_components/home-dashboard/zone-home-dashboard'
7-
import { HOME_HIDDEN_SECTIONS } from '@/app/(product)/o/[organizationId]/home/_components/model'
87
import { useHiddenHomeWidgets, useLiveHome } from '@/platform/lib/data/home'
98

109
/** Until the viewer's Customize Home choices are known, every widget they could hide stays unread. */
11-
const UNKNOWN_CHOICES: readonly HomeWidget[] = ['Activity', 'Email', 'Calendar']
10+
const UNKNOWN_CHOICES: readonly HomeWidget[] = ['Activity', 'Workspaces', 'Email', 'Calendar']
1211

1312
function keepChoices() {}
1413

@@ -24,8 +23,7 @@ export function LiveHome() {
2423
const hiddenWidgets = widgets?.hiddenWidgets ?? UNKNOWN_CHOICES
2524
const { view, actions, savedConnection } = useLiveHome(
2625
preferences?.homeMode === 'Search',
27-
hiddenWidgets,
28-
HOME_HIDDEN_SECTIONS.has('Needs your attention')
26+
hiddenWidgets
2927
)
3028
return (
3129
<>

‎apps/sim/app/(product)/o/[organizationId]/home/_components/model.ts‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,9 @@ import type {
1010
HomeListState,
1111
HomeWidgetActions,
1212
HomeWidgetState,
13+
HomeWorkspaceItem,
1314
} from '@/platform/shared/home'
1415

15-
/**
16-
* Home sections held back by design until the designer's pass. Their views stay; Home renders none
17-
* of them and the live Home skips their reads.
18-
*/
19-
export const HOME_HIDDEN_SECTIONS: ReadonlySet<'Needs your attention'> = new Set([
20-
'Needs your attention',
21-
])
22-
2316
export interface AttentionItem {
2417
id: string
2518
title: string
@@ -64,10 +57,16 @@ export interface HomeView {
6457
asOf: number
6558
greeting: string
6659
firstName?: string
67-
attention: HomeListState<AttentionItem>
60+
/**
61+
* Needs your attention and Workspaces wait for their design pass: absent while Home does not offer
62+
* them, so nothing renders or reads for either.
63+
*/
64+
attention?: HomeListState<AttentionItem>
6865
activity: Record<HomeMode, HomeActivityState>
6966
email: HomeWidgetState<readonly EmailItem[]>
7067
calendar: HomeWidgetState<HomeCalendarDay>
68+
/** The viewer's workspaces, the sidebar's rows. Nothing renders for a viewer with none. */
69+
workspaces?: HomeListState<HomeWorkspaceItem>
7170
recents: HomeListState<RecentItem>
7271
getStarted: readonly HomeGetStartedStep[]
7372
composerInventory?: ComposerInventory
@@ -81,6 +80,7 @@ export interface HomeActions {
8180
returnToToday: () => void
8281
retryAttention: () => void
8382
retryActivity: () => void
83+
retryWorkspaces: () => void
8484
retryRecents: () => void
8585
removeRecent: (id: string) => void
8686
}

‎apps/sim/app/(product)/o/[organizationId]/home/page.tsx‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,13 @@ export default async function Page({ searchParams }: ProductPageProps<{ organiza
1717
loadConversationData(scenario),
1818
loadHomeRecentLinks(scenario),
1919
])
20-
return <FixtureHome data={data?.home ?? null} recentLinks={recentLinks ?? []} />
20+
return (
21+
<FixtureHome
22+
data={data?.home ?? null}
23+
recentLinks={recentLinks ?? []}
24+
workspaces={data?.workspaces ?? []}
25+
/>
26+
)
2127
}
2228
// Loaded only here, so a fixture build never evaluates the data seam.
2329
const { LiveHomeRoute } = await import(

‎apps/sim/hooks/queries/organization-home.ts‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@ import { requestJson } from '@/lib/api/client/request'
33
import {
44
getOrganizationHomeCalendarContract,
55
getOrganizationHomeEmailContract,
6+
getOrganizationHomeWorkspaceActivityContract,
7+
type OrganizationHomeWorkspaceActivity,
68
} from '@/lib/api/contracts/organization-home'
79
import { organizationAccountsKeys } from '@/hooks/queries/utils/organization-accounts-keys'
810

911
/** Each read costs a request per thread at the member's mail provider, so focus re-reads are spaced. */
1012
const EMAIL_STALE_TIME = 5 * 60 * 1000
1113
/** A day's meetings change less often than a mailbox, and every day browsed is its own read. */
1214
const CALENDAR_STALE_TIME = 5 * 60 * 1000
15+
/** A run's state moves quickly, but no one is waiting on it to the second. */
16+
const WORKSPACE_ACTIVITY_STALE_TIME = 30 * 1000
1317

1418
/** Nested under the organization's accounts, so anything that changes a connection refreshes Home. */
1519
const organizationHomeKeys = {
@@ -20,6 +24,8 @@ const organizationHomeKeys = {
2024
[...organizationHomeKeys.organization(organizationId), 'email'] as const,
2125
calendar: (organizationId: string, date: string, timeZone: string) =>
2226
[...organizationHomeKeys.organization(organizationId), 'calendar', date, timeZone] as const,
27+
workspaceActivity: (organizationId: string) =>
28+
[...organizationHomeKeys.organization(organizationId), 'workspace-activity'] as const,
2329
}
2430

2531
interface HomeWidgetQueryOptions {
@@ -60,3 +66,30 @@ export function useOrganizationHomeCalendar(
6066
refetchOnWindowFocus: true,
6167
})
6268
}
69+
70+
function hasRunningWorkspace(data: OrganizationHomeWorkspaceActivity | undefined) {
71+
return data?.workspaces.some((workspace) => workspace.status === 'running') ?? false
72+
}
73+
74+
/**
75+
* Polls only while a run is on screen, so a finished one does not keep saying Running. Everything
76+
* else refreshes on focus, and a hidden tab polls nothing.
77+
*/
78+
export function useOrganizationHomeWorkspaceActivity(
79+
organizationId: string,
80+
{ enabled }: HomeWidgetQueryOptions
81+
) {
82+
return useQuery({
83+
queryKey: organizationHomeKeys.workspaceActivity(organizationId),
84+
queryFn: ({ signal }) =>
85+
requestJson(getOrganizationHomeWorkspaceActivityContract, {
86+
params: { id: organizationId },
87+
signal,
88+
}),
89+
enabled,
90+
staleTime: WORKSPACE_ACTIVITY_STALE_TIME,
91+
refetchOnWindowFocus: true,
92+
refetchInterval: (query) =>
93+
hasRunningWorkspace(query.state.data) ? WORKSPACE_ACTIVITY_STALE_TIME : false,
94+
})
95+
}

‎apps/sim/platform/fixtures/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ export const loadConversationData = fixture(async (scenario) => {
9797
return {
9898
home: { ...home, composerInventory: inventory },
9999
chat: { ...chat, composerInventory: inventory },
100+
workspaces: shellScenarios[scenario].workspaces,
100101
}
101102
})
102103
export const loadIntegrationsData = fixture(

0 commit comments

Comments
 (0)