Skip to content

Commit 9c398c1

Browse files
mzxchandraclaude
andcommitted
feat(org): project view moves onto the organization surface behind org-project-view
The prototype leaves /playground/org for the real routes. With the flag on: - /o/<org>/home is the product's Home plus the project picker. - The org sidebar lists Projects (one per fork lineage, with their chats and the row menu, rename, pin and drag) in place of Workspaces, and Home reads as New chat. - /o/<org>/p/<workspaceId>/{dashboard,changelog,issues,environments} is the project main view: real dashboards and Runs, the Environments tab, and empty states for Changelog and Issues until those entities exist. - The Build sidebar's back chip and project links point at the org project routes. With the flag off the org surface is unchanged. The playground shell, its fixtures and the mock-only pieces are deleted. The workflow location bar mounts from the canvas page instead of its layout, and reads the workflow through the list query options and opens search through the global command, so the layout's module graph stays at its baseline (it had grown by about 1,150 modules). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent bc5bf6e commit 9c398c1

86 files changed

Lines changed: 962 additions & 8084 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export { ChatsSection } from './chats-section'
22
export { OrganizationFooter } from './organization-footer'
33
export { OrganizationHeader } from './organization-header'
4+
export { ProjectsSection } from './projects-section'
45
export { WorkspacesSection } from './workspaces-section'
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export { ProjectRow } from './project-row'
2+
export { ProjectsSection } from './projects-section'
3+
export { useProjectActions } from './use-project-actions'
4+
export { type ProjectDragProps, useProjectOrder } from './use-project-order'

apps/sim/app/playground/org/components/project-row.tsx renamed to apps/sim/app/o/[organizationId]/components/organization-sidebar/components/projects-section/project-row.tsx

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import { useCallback, useRef, useState } from 'react'
44
import {
5-
ChipTag,
65
chipVariants,
76
cn,
87
DropdownMenu,
@@ -26,9 +25,9 @@ import {
2625
} from '@sim/emcn/icons'
2726
import Link from 'next/link'
2827
import { IdentityTile } from '@/components/identity-tile/identity-tile'
29-
import type { Project } from '@/app/playground/org/lib/project'
30-
import { useProjectActions } from '@/app/playground/org/lib/use-project-actions'
31-
import type { ProjectDragProps } from '@/app/playground/org/lib/use-project-order'
28+
import { useProjectActions } from '@/app/o/[organizationId]/components/organization-sidebar/components/projects-section/use-project-actions'
29+
import type { ProjectDragProps } from '@/app/o/[organizationId]/components/organization-sidebar/components/projects-section/use-project-order'
30+
import type { Project } from '@/app/o/[organizationId]/p/hooks/use-projects'
3231
import { SidebarTooltip } from '@/app/workspace/[workspaceId]/w/components/sidebar/components'
3332
import { SidebarRowAction } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-row-actions'
3433
import { DeleteModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/delete-modal/delete-modal'
@@ -47,7 +46,6 @@ interface ProjectRowProps {
4746
active: boolean
4847
/** Whether the chats under the row are shown; undefined when there are none to show. */
4948
expanded?: boolean
50-
needsYou: number
5149
railCollapsed: boolean
5250
drag: ProjectDragProps
5351
isAnyDragActive: boolean
@@ -65,7 +63,6 @@ export function ProjectRow({
6563
newChatHref,
6664
active,
6765
expanded,
68-
needsYou,
6966
railCollapsed,
7067
drag,
7168
isAnyDragActive,
@@ -137,7 +134,7 @@ export function ProjectRow({
137134
onDragOver={drag.onDragOver}
138135
onDragLeave={drag.onDragLeave}
139136
onDrop={drag.onDrop}
140-
onContextMenu={project.isMock ? undefined : handleContextMenu}
137+
onContextMenu={handleContextMenu}
141138
>
142139
{drag.indicator && (
143140
<span
@@ -178,7 +175,6 @@ export function ProjectRow({
178175
onToggleExpand()
179176
}}
180177
onDoubleClick={(event) => {
181-
if (project.isMock) return
182178
event.preventDefault()
183179
rename.handleStartEdit()
184180
}}
@@ -198,11 +194,7 @@ export function ProjectRow({
198194
open={menuOpen}
199195
revealOnHover={!isAnyDragActive}
200196
indicator={
201-
needsYou > 0 ? (
202-
<ChipTag variant='gray' aria-label={`${needsYou} waiting on you`}>
203-
{needsYou}
204-
</ChipTag>
205-
) : pinned ? (
197+
pinned ? (
206198
<Pin className='size-[12px] text-[var(--text-icon)]' aria-label='Pinned' />
207199
) : undefined
208200
}
@@ -215,11 +207,9 @@ export function ProjectRow({
215207
>
216208
<Plus className='size-[12px] text-[var(--text-icon)]' />
217209
</Link>
218-
{!project.isMock && (
219-
<SidebarRowAction aria-label='Project options' onClick={openMenuFromButton}>
220-
<MoreHorizontal className='size-[16px] text-[var(--text-icon)]' />
221-
</SidebarRowAction>
222-
)}
210+
<SidebarRowAction aria-label='Project options' onClick={openMenuFromButton}>
211+
<MoreHorizontal className='size-[16px] text-[var(--text-icon)]' />
212+
</SidebarRowAction>
223213
</RowActions>
224214
)}
225215
</div>
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
'use client'
2+
3+
import { useState } from 'react'
4+
import { chipVariants, cn, OverflowText } from '@sim/emcn'
5+
import { formatRelativeTime } from '@sim/utils/formatting'
6+
import Link from 'next/link'
7+
import { useQueryStates } from 'nuqs'
8+
import { organizationRoutes } from '@/lib/navigation/paths'
9+
import { ProjectRow } from '@/app/o/[organizationId]/components/organization-sidebar/components/projects-section/project-row'
10+
import {
11+
type ProjectDragProps,
12+
useProjectOrder,
13+
} from '@/app/o/[organizationId]/components/organization-sidebar/components/projects-section/use-project-order'
14+
import { type Project, useProjects } from '@/app/o/[organizationId]/p/hooks/use-projects'
15+
import { projectParsers } from '@/app/o/[organizationId]/p/search-params'
16+
import { SidebarSection } from '@/app/workspace/[workspaceId]/w/components/sidebar/components'
17+
import { SIDEBAR_ITEM_GAP_CLASS } from '@/app/workspace/[workspaceId]/w/components/sidebar/constants'
18+
import { useMothershipChats } from '@/hooks/queries/mothership-chats'
19+
20+
/** Chats listed under an open project before "Show more". */
21+
const CHAT_PREVIEW = 3
22+
23+
interface ProjectsSectionProps {
24+
organizationId: string
25+
isCollapsed: boolean
26+
pathname: string | null
27+
}
28+
29+
/** The organization's projects, one per fork lineage, each with its chats underneath. */
30+
export function ProjectsSection({ organizationId, isCollapsed, pathname }: ProjectsSectionProps) {
31+
const { roots } = useProjects(organizationId)
32+
const { projects, dragProps, isAnyDragActive } = useProjectOrder(roots)
33+
const [{ chat: openChat }] = useQueryStates(projectParsers)
34+
return (
35+
<SidebarSection title='Projects' railCollapsed={isCollapsed} className='shrink-0'>
36+
<div className={cn(SIDEBAR_ITEM_GAP_CLASS, 'flex flex-col px-2')}>
37+
{projects.map((project) => (
38+
<ProjectTree
39+
key={project.id}
40+
organizationId={organizationId}
41+
project={project}
42+
pathname={pathname}
43+
openChat={openChat}
44+
railCollapsed={isCollapsed}
45+
drag={dragProps(project)}
46+
isAnyDragActive={isAnyDragActive}
47+
/>
48+
))}
49+
</div>
50+
</SidebarSection>
51+
)
52+
}
53+
54+
interface ProjectTreeProps {
55+
organizationId: string
56+
project: Project
57+
pathname: string | null
58+
openChat: string
59+
railCollapsed: boolean
60+
drag: ProjectDragProps
61+
isAnyDragActive: boolean
62+
}
63+
64+
/**
65+
* A project row with its chats listed underneath. A project spans every environment in its
66+
* lineage; the chats shown belong to the environment you are in.
67+
*/
68+
function ProjectTree({
69+
organizationId,
70+
project,
71+
pathname,
72+
openChat,
73+
railCollapsed,
74+
drag,
75+
isAnyDragActive,
76+
}: ProjectTreeProps) {
77+
const routes = organizationRoutes(organizationId)
78+
const [showAll, setShowAll] = useState(false)
79+
const activeId =
80+
project.environments.find((environment) => {
81+
const base = `${routes.root}/p/${environment.workspaceId}`
82+
return pathname === base || pathname?.startsWith(`${base}/`)
83+
})?.workspaceId ?? null
84+
const inProject = activeId !== null
85+
/** null follows navigation (open while you're in the project); a click pins it open or shut. */
86+
const [override, setOverride] = useState<boolean | null>(null)
87+
const expanded = override ?? inProject
88+
const { data: chats = [] } = useMothershipChats(activeId ?? project.id)
89+
const visible = showAll ? chats : chats.slice(0, CHAT_PREVIEW)
90+
const panelHref = (chatId: string) =>
91+
`${inProject && pathname ? pathname : routes.project(project.id)}?chat=${chatId}`
92+
93+
return (
94+
<div className='flex flex-col gap-[1px]'>
95+
<ProjectRow
96+
project={project}
97+
href={routes.project(project.id)}
98+
newChatHref={panelHref('new')}
99+
active={inProject}
100+
expanded={chats.length > 0 ? expanded : undefined}
101+
railCollapsed={railCollapsed}
102+
drag={drag}
103+
isAnyDragActive={isAnyDragActive}
104+
onToggleExpand={() => setOverride(inProject ? !expanded : true)}
105+
/>
106+
{!railCollapsed && expanded && chats.length > 0 && (
107+
<div className='flex flex-col gap-[1px] pl-[22px]'>
108+
{visible.map((chat) => (
109+
<Link
110+
key={chat.id}
111+
href={panelHref(chat.id)}
112+
className={cn(
113+
chipVariants({ active: openChat === chat.id && inProject, fullWidth: true }),
114+
'h-[26px] gap-2'
115+
)}
116+
>
117+
<OverflowText
118+
label={chat.name}
119+
className='sidebar-collapse-hide flex-1 text-[var(--text-body)] text-small'
120+
focusTarget='nearest-interactive'
121+
/>
122+
{chat.isActive ? (
123+
<span
124+
role='img'
125+
aria-label='Running'
126+
className='inline-flex size-[14px] shrink-0 items-center justify-center'
127+
>
128+
<span className='size-[7px] rounded-full bg-[var(--caution)]' />
129+
</span>
130+
) : (
131+
<span className='shrink-0 text-[var(--text-muted)] text-caption'>
132+
{formatRelativeTime(chat.updatedAt.toISOString())
133+
.replace(' ago', '')
134+
.replace('just now', 'now')}
135+
</span>
136+
)}
137+
</Link>
138+
))}
139+
{chats.length > CHAT_PREVIEW && (
140+
<button
141+
type='button'
142+
onClick={() => setShowAll((prev) => !prev)}
143+
className={cn(
144+
chipVariants({ fullWidth: true }),
145+
'h-[26px] text-[var(--text-muted)] text-small'
146+
)}
147+
>
148+
{showAll ? 'Show less' : 'Show more'}
149+
</button>
150+
)}
151+
</div>
152+
)}
153+
</div>
154+
)
155+
}

apps/sim/app/playground/org/lib/use-project-actions.ts renamed to apps/sim/app/o/[organizationId]/components/organization-sidebar/components/projects-section/use-project-actions.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { useCallback } from 'react'
44
import { parseEnvironmentName } from '@/lib/projects'
5-
import type { Project } from '@/app/playground/org/lib/project'
5+
import type { Project } from '@/app/o/[organizationId]/p/hooks/use-projects'
66
import {
77
useDeleteWorkspace,
88
usePinnedWorkspaceIds,
@@ -31,7 +31,6 @@ export function useProjectActions() {
3131

3232
const setPinned = useCallback(
3333
(project: Project, pinned: boolean) => {
34-
if (project.isMock) return
3534
togglePin({ workspaceId: project.rootId, pinned })
3635
},
3736
[togglePin]
@@ -40,7 +39,7 @@ export function useProjectActions() {
4039
const rename = useCallback(
4140
async (project: Project, name: string) => {
4241
const base = name.trim()
43-
if (project.isMock || !base || base === project.name) return
42+
if (!base || base === project.name) return
4443
for (const environment of project.environments) {
4544
const workspace = workspaces?.find((candidate) => candidate.id === environment.workspaceId)
4645
if (!workspace) continue
@@ -56,7 +55,6 @@ export function useProjectActions() {
5655

5756
const remove = useCallback(
5857
async (project: Project) => {
59-
if (project.isMock) return
6058
/** Forks first: the root is the last workspace to go, so a failure leaves a valid lineage. */
6159
for (const environment of [...project.environments].reverse()) {
6260
await deleteWorkspace({ workspaceId: environment.workspaceId })

apps/sim/app/playground/org/lib/use-project-order.ts renamed to apps/sim/app/o/[organizationId]/components/organization-sidebar/components/projects-section/use-project-order.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client'
22

33
import { useCallback, useState } from 'react'
4-
import type { Project } from '@/app/playground/org/lib/project'
4+
import type { Project } from '@/app/o/[organizationId]/p/hooks/use-projects'
55
import { usePinnedWorkspaceIds } from '@/hooks/queries/workspace'
66
import { orderProjectIds, useProjectOrderStore } from '@/stores/project-order/store'
77

‎apps/sim/app/o/[organizationId]/components/organization-sidebar/navigation.ts‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Home, Integration, Search } from '@sim/emcn/icons'
1+
import { Home, Integration, Search, SquarePen } from '@sim/emcn/icons'
22
import { organizationRoutes } from '@/lib/navigation/paths'
33
import type { SidebarNavItemData } from '@/app/workspace/[workspaceId]/w/components/sidebar/components'
44

@@ -21,16 +21,24 @@ const ORGANIZATION_NAV_ENTRIES: readonly OrganizationNavEntry[] = [
2121
{ id: 'integrations', label: 'Integrations', icon: Integration, route: 'integrations' },
2222
]
2323

24+
/** With the org project view, Home is where a new chat starts, so it reads as one. */
25+
const PROJECT_VIEW_HOME: Pick<OrganizationNavEntry, 'label' | 'icon'> = {
26+
label: 'New chat',
27+
icon: SquarePen,
28+
}
29+
2430
export function buildOrganizationNavItems(
2531
organizationId: string,
2632
searchAvailable: boolean,
27-
mothershipAvailable: boolean
33+
mothershipAvailable: boolean,
34+
projectViewEnabled = false
2835
): SidebarNavItemData[] {
2936
const routes = organizationRoutes(organizationId)
3037
return ORGANIZATION_NAV_ENTRIES.filter(({ route }) =>
3138
route === 'home' ? mothershipAvailable : searchAvailable
3239
).map(({ route, ...entry }) => ({
3340
...entry,
41+
...(route === 'home' && projectViewEnabled ? PROJECT_VIEW_HOME : {}),
3442
href: routes[route],
3543
}))
3644
}

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

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ import {
1414
ChatsSection,
1515
OrganizationFooter,
1616
OrganizationHeader,
17+
ProjectsSection,
1718
WorkspacesSection,
1819
} from '@/app/o/[organizationId]/components/organization-sidebar/components'
1920
import { useOrganizationChats } from '@/app/o/[organizationId]/components/organization-sidebar/hooks'
2021
import { buildOrganizationNavItems } from '@/app/o/[organizationId]/components/organization-sidebar/navigation'
2122
import { useOrganizationContext } from '@/app/o/[organizationId]/providers/organization-provider'
2223
import { OrganizationSettingsSidebar } from '@/app/o/[organizationId]/settings/organization-settings-sidebar'
2324
import { useSidebarChrome } from '@/app/workspace/[workspaceId]/components/workspace-chrome'
25+
import { useFeatureFlag } from '@/app/workspace/[workspaceId]/providers/feature-flags-provider'
2426
import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
2527
import { createCommands } from '@/app/workspace/[workspaceId]/utils/commands-utils'
2628
import {
@@ -90,7 +92,13 @@ export const OrganizationSidebar = memo(function OrganizationSidebar() {
9092

9193
const isMac = isMacPlatform()
9294
const canUseHome = mothershipAvailable && (canBuild || searchAccess.memberScoped)
93-
const navItems = buildOrganizationNavItems(organization.id, searchAccess.memberScoped, canUseHome)
95+
const projectViewEnabled = useFeatureFlag('org-project-view')
96+
const navItems = buildOrganizationNavItems(
97+
organization.id,
98+
searchAccess.memberScoped,
99+
canUseHome,
100+
projectViewEnabled
101+
)
94102
const settingsPath = organizationRoutes(organization.id).settings
95103
const isSettings = pathname === settingsPath || pathname?.startsWith(`${settingsPath}/`)
96104

@@ -258,11 +266,19 @@ export const OrganizationSidebar = memo(function OrganizationSidebar() {
258266
{...scrollFadeAttributes(scrollEdges)}
259267
>
260268
<div ref={scrollContentRef} className='flex flex-col'>
261-
<WorkspacesSection
262-
organizationId={organization.id}
263-
isCollapsed={isCollapsed}
264-
pathname={pathname}
265-
/>
269+
{projectViewEnabled ? (
270+
<ProjectsSection
271+
organizationId={organization.id}
272+
isCollapsed={isCollapsed}
273+
pathname={pathname}
274+
/>
275+
) : (
276+
<WorkspacesSection
277+
organizationId={organization.id}
278+
isCollapsed={isCollapsed}
279+
pathname={pathname}
280+
/>
281+
)}
266282
{canUseHome && (
267283
<OrganizationChats
268284
key={organization.id}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { ProjectHome } from './project-home'

0 commit comments

Comments
 (0)