Skip to content

Commit abd0d0e

Browse files
Merge Marcus's feat/org-project-routes onto staging's Project model
Brings in the 9 branch-only commits from origin/feat/org-project-routes (the first 14, the version diff and fork preview, landed on staging as #8455): Kept, rewired onto staging's Project API (/api/projects): - project view as a pane on organization Home, the redesigned Environments tab with promote/refresh, sync review, Chats section, composer, settings rendered in the pane - create a mapped secret from a fork sync (POST /api/workspaces/[id]/fork/mapping) - sub-agent activity and cited sources in chat - hide-chat / expand-pane toggle on the resource panel, combined with staging's compact layout Dropped in favour of staging's model: - the prototype project, project_workspace and copilot_chat_project tables (migrations 0389-0391 and the 0030 backfill), and the projects API built on them - project-scoped permission groups, credential groups, data retention and usage - chat project marks and the manual environment position - the older copies of dashboards, charts and the #8455 diff work - the single-environment invite modal, which changed inviting outside the flag Project rows archive the project through DELETE /api/projects/[id]; sync review compares through staging's comparison modal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lrd1NLD9qx2KBLVnw32c3E
2 parents 90086de + fb86acf commit abd0d0e

131 files changed

Lines changed: 4905 additions & 1782 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.

‎apps/sim/app/api/workspaces/[id]/fork/mapping/route.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
createForkSecretMappingContract,
23
getForkMappingContract,
34
updateForkMappingContract,
45
} from '@/lib/api/contracts/workspace-fork'
@@ -8,6 +9,7 @@ import {
89
internalSessionAuth,
910
} from '@/lib/api/server/routes'
1011
import { internalForkErrorPolicy } from '@/ee/workspace-forking/api/route-policies'
12+
import { createWorkspaceForkSecretMapping } from '@/ee/workspace-forking/application/create-secret-mapping'
1113
import { getWorkspaceForkMappingDetails } from '@/ee/workspace-forking/application/mapping-details'
1214
import { forkOperations } from '@/ee/workspace-forking/application/operations'
1315
import { updateWorkspaceForkMappings } from '@/ee/workspace-forking/application/recovery-and-mappings'
@@ -38,3 +40,14 @@ export const PUT = defineInternalJsonRoute({
3840
useCase: updateWorkspaceForkMappings,
3941
present: (result) => ({ success: true as const, ...result }),
4042
})
43+
44+
export const POST = defineInternalJsonRoute({
45+
contract: createForkSecretMappingContract,
46+
auth: internalSessionAuth,
47+
operation: forkOperations.createSecretMapping,
48+
rateLimit: internalRateLimits.none({ reason: 'Preserve existing internal fork request policy' }),
49+
errorPolicy: internalForkErrorPolicy,
50+
mapInput: ({ params, body }) => ({ workspaceId: params.id, ...body }),
51+
useCase: createWorkspaceForkSecretMapping,
52+
present: ({ name }) => ({ name }),
53+
})
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { getWorkspaceSettingsNavigationContract } from '@/lib/api/contracts/workspace-settings'
2+
import {
3+
defineInternalJsonRoute,
4+
internalOrchestrationErrorPolicy,
5+
internalRateLimits,
6+
internalSessionAuth,
7+
} from '@/lib/api/server/routes'
8+
import {
9+
readWorkspaceSettingsNavigation,
10+
readWorkspaceSettingsNavigationOperation,
11+
} from '@/lib/settings/application/workspace-navigation'
12+
13+
export const GET = defineInternalJsonRoute({
14+
contract: getWorkspaceSettingsNavigationContract,
15+
auth: internalSessionAuth,
16+
operation: readWorkspaceSettingsNavigationOperation,
17+
rateLimit: internalRateLimits.none({
18+
reason: 'Read-only settings navigation, matching existing settings-page admission',
19+
}),
20+
errorPolicy: internalOrchestrationErrorPolicy,
21+
mapInput: ({ params }) => ({ workspaceId: params.id }),
22+
useCase: readWorkspaceSettingsNavigation,
23+
staticResponseHeaders: { 'Cache-Control': 'private, no-store' },
24+
})

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use client'
22

3+
import type { ReactNode } from 'react'
34
import {
45
chipVariants,
56
cn,
@@ -28,6 +29,8 @@ import {
2829

2930
interface ChatRowProps {
3031
chat: OrganizationChat
32+
/** Shown before the title, such as the projects the chat worked in. */
33+
leading?: ReactNode
3134
isCurrentRoute: boolean
3235
isSelected: boolean
3336
isMenuOpen: boolean
@@ -39,6 +42,7 @@ interface ChatRowProps {
3942

4043
function ChatRow({
4144
chat,
45+
leading,
4246
isCurrentRoute,
4347
isSelected,
4448
isMenuOpen,
@@ -66,6 +70,7 @@ function ChatRow({
6670
onContextMenu={(e) => onContextMenu(e, chat.id)}
6771
onSelectChat={onSelectChat}
6872
>
73+
{leading}
6974
<OverflowText label={chat.name} className='flex-1 text-[var(--text-body)]' />
7075
<RowActions
7176
open={isMenuOpen}
@@ -103,6 +108,8 @@ interface ChatsSectionProps {
103108
isLoading: boolean
104109
isCollapsed: boolean
105110
pathname: string | null
111+
/** Renders before each chat's title; the org project view shows the chat's projects. */
112+
leadingFor?: (chat: OrganizationChat) => ReactNode
106113
}
107114

108115
export function ChatsSection({
@@ -111,6 +118,7 @@ export function ChatsSection({
111118
isLoading,
112119
isCollapsed,
113120
pathname,
121+
leadingFor,
114122
}: ChatsSectionProps) {
115123
const actions = useOrganizationChatActions({ organizationId, chats })
116124
const { menu, hover, rename, selectedChat, selectedChats, menuOpenChatId } = actions
@@ -140,7 +148,7 @@ export function ChatsSection({
140148
<CollapsedChatFlyoutItem
141149
key={chat.id}
142150
chat={chat}
143-
isCurrentRoute={pathname === chat.href}
151+
isCurrentRoute={pathname === chat.href.split('?')[0]}
144152
isSelected={selectedChats.size > 1 && selectedChats.has(chat.id)}
145153
onSelectChat={actions.handleChatClick}
146154
isMenuOpen={menuOpenChatId === chat.id}
@@ -184,7 +192,8 @@ export function ChatsSection({
184192
<ChatRow
185193
key={chat.id}
186194
chat={chat}
187-
isCurrentRoute={pathname === chat.href}
195+
leading={leadingFor?.(chat)}
196+
isCurrentRoute={pathname === chat.href.split('?')[0]}
188197
isSelected={selectedChats.size > 1 && selectedChats.has(chat.id)}
189198
isMenuOpen={menuOpenChatId === chat.id}
190199
onSelectChat={actions.handleChatClick}

‎apps/sim/app/o/[organizationId]/components/organization-sidebar/components/projects-section/project-row.tsx‎

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ import {
2323
SquareArrowUpRight,
2424
Trash,
2525
} from '@sim/emcn/icons'
26-
import Link from 'next/link'
2726
import { IdentityTile } from '@/components/identity-tile/identity-tile'
27+
import { SettingsGuardedLink as Link } from '@/components/settings/settings-guarded-link'
2828
import { useProjectActions } from '@/app/o/[organizationId]/components/organization-sidebar/components/projects-section/use-project-actions'
2929
import type { ProjectDragProps } from '@/app/o/[organizationId]/components/organization-sidebar/components/projects-section/use-project-order'
3030
import type { Project } from '@/app/o/[organizationId]/p/hooks/use-projects'
@@ -44,12 +44,9 @@ interface ProjectRowProps {
4444
href: string
4545
newChatHref: string
4646
active: boolean
47-
/** Whether the chats under the row are shown; undefined when there are none to show. */
48-
expanded?: boolean
4947
railCollapsed: boolean
5048
drag: ProjectDragProps
5149
isAnyDragActive: boolean
52-
onToggleExpand: () => void
5350
}
5451

5552
/**
@@ -62,11 +59,9 @@ export function ProjectRow({
6259
href,
6360
newChatHref,
6461
active,
65-
expanded,
6662
railCollapsed,
6763
drag,
6864
isAnyDragActive,
69-
onToggleExpand,
7065
}: ProjectRowProps) {
7166
const actions = useProjectActions()
7267
const pinned = actions.isPinned(project)
@@ -172,13 +167,11 @@ export function ProjectRow({
172167
event.preventDefault()
173168
return
174169
}
175-
onToggleExpand()
176170
}}
177171
onDoubleClick={(event) => {
178172
event.preventDefault()
179173
if (project.canAdminister) rename.handleStartEdit()
180174
}}
181-
aria-expanded={expanded}
182175
className='flex min-w-0 flex-1 items-center gap-2'
183176
>
184177
<IdentityTile initial={project.name[0]} />
Lines changed: 28 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,38 @@
11
'use client'
22

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'
3+
import { cn } from '@sim/emcn'
4+
import { useSearchParams } from 'next/navigation'
75
import { useQueryStates } from 'nuqs'
86
import { organizationRoutes } from '@/lib/navigation/paths'
97
import { ProjectRow } from '@/app/o/[organizationId]/components/organization-sidebar/components/projects-section/project-row'
108
import {
119
type ProjectDragProps,
1210
useProjectOrder,
1311
} from '@/app/o/[organizationId]/components/organization-sidebar/components/projects-section/use-project-order'
14-
import {
15-
defaultEnvironment,
16-
type Project,
17-
useProjects,
18-
} from '@/app/o/[organizationId]/p/hooks/use-projects'
19-
import { projectParsers } from '@/app/o/[organizationId]/p/search-params'
12+
import { projectPaneParsers } from '@/app/o/[organizationId]/home/components/project-pane/search-params'
13+
import { type Project, useProjects } from '@/app/o/[organizationId]/p/hooks/use-projects'
2014
import { SidebarSection } from '@/app/workspace/[workspaceId]/w/components/sidebar/components'
2115
import { SIDEBAR_ITEM_GAP_CLASS } from '@/app/workspace/[workspaceId]/w/components/sidebar/constants'
22-
import { useMothershipChats } from '@/hooks/queries/mothership-chats'
23-
24-
/** Chats listed under an open project before "Show more". */
25-
const CHAT_PREVIEW = 3
2616

2717
interface ProjectsSectionProps {
2818
organizationId: string
2919
isCollapsed: boolean
3020
pathname: string | null
3121
}
3222

33-
/** The organization's projects, each with its chats underneath. */
23+
/** Projects select the pinned pane; chats remain independent organization conversations. */
3424
export function ProjectsSection({ organizationId, isCollapsed, pathname }: ProjectsSectionProps) {
35-
const { projects: unordered, error } = useProjects(organizationId)
36-
const { projects, dragProps, isAnyDragActive } = useProjectOrder(unordered)
37-
const [{ chat: openChat }] = useQueryStates(projectParsers)
25+
const { roots } = useProjects(organizationId)
26+
const { projects, dragProps, isAnyDragActive } = useProjectOrder(roots)
3827
return (
3928
<SidebarSection title='Projects' railCollapsed={isCollapsed} className='shrink-0'>
4029
<div className={cn(SIDEBAR_ITEM_GAP_CLASS, 'flex flex-col px-2')}>
41-
{error && !isCollapsed && (
42-
<p role='alert' className='px-2 py-1 text-[var(--text-error)] text-caption'>
43-
{error.message}
44-
</p>
45-
)}
4630
{projects.map((project) => (
4731
<ProjectTree
4832
key={project.id}
4933
organizationId={organizationId}
5034
project={project}
5135
pathname={pathname}
52-
openChat={openChat}
5336
railCollapsed={isCollapsed}
5437
drag={dragProps(project)}
5538
isAnyDragActive={isAnyDragActive}
@@ -64,102 +47,41 @@ interface ProjectTreeProps {
6447
organizationId: string
6548
project: Project
6649
pathname: string | null
67-
openChat: string
6850
railCollapsed: boolean
6951
drag: ProjectDragProps
7052
isAnyDragActive: boolean
7153
}
7254

73-
/**
74-
* A project row with its chats listed underneath. The chats shown belong to the environment
75-
* you are in, else to the project's default environment.
76-
*/
7755
function ProjectTree({
7856
organizationId,
7957
project,
8058
pathname,
81-
openChat,
8259
railCollapsed,
8360
drag,
8461
isAnyDragActive,
8562
}: ProjectTreeProps) {
8663
const routes = organizationRoutes(organizationId)
87-
const [showAll, setShowAll] = useState(false)
88-
const activeId =
89-
project.environments.find((environment) => {
90-
const base = `${routes.root}/p/${environment.workspaceId}`
91-
return pathname === base || pathname?.startsWith(`${base}/`)
92-
})?.workspaceId ?? null
93-
const inProject = activeId !== null
94-
const homeWorkspaceId = defaultEnvironment(project).workspaceId
95-
/** null follows navigation (open while you're in the project); a click pins it open or shut. */
96-
const [override, setOverride] = useState<boolean | null>(null)
97-
const expanded = override ?? inProject
98-
const { data: chats = [] } = useMothershipChats(activeId ?? homeWorkspaceId)
99-
const visible = showAll ? chats : chats.slice(0, CHAT_PREVIEW)
100-
const panelHref = (chatId: string) =>
101-
`${inProject && pathname ? pathname : routes.project(homeWorkspaceId)}?chat=${chatId}`
102-
64+
const searchParams = useSearchParams()
65+
const [{ project: selectedWorkspace }] = useQueryStates(projectPaneParsers)
66+
const inProject = project.environments.some(
67+
(environment) => environment.workspaceId === selectedWorkspace
68+
)
69+
const query = new URLSearchParams(searchParams.toString())
70+
if (inProject) query.delete('project')
71+
else query.set('project', project.id)
72+
query.delete('section')
73+
query.set('pane', 'project')
74+
const chatPath = pathname?.startsWith(`/o/${organizationId}/chat/`) ? pathname : routes.home
75+
const href = `${chatPath}?${query}`
10376
return (
104-
<div className='flex flex-col gap-[1px]'>
105-
<ProjectRow
106-
project={project}
107-
href={routes.project(homeWorkspaceId)}
108-
newChatHref={panelHref('new')}
109-
active={inProject}
110-
expanded={chats.length > 0 ? expanded : undefined}
111-
railCollapsed={railCollapsed}
112-
drag={drag}
113-
isAnyDragActive={isAnyDragActive}
114-
onToggleExpand={() => setOverride(inProject ? !expanded : true)}
115-
/>
116-
{!railCollapsed && expanded && chats.length > 0 && (
117-
<div className='flex flex-col gap-[1px] pl-[22px]'>
118-
{visible.map((chat) => (
119-
<Link
120-
key={chat.id}
121-
href={panelHref(chat.id)}
122-
className={cn(
123-
chipVariants({ active: openChat === chat.id && inProject, fullWidth: true }),
124-
'h-[26px] gap-2'
125-
)}
126-
>
127-
<OverflowText
128-
label={chat.name}
129-
className='sidebar-collapse-hide flex-1 text-[var(--text-body)] text-small'
130-
focusTarget='nearest-interactive'
131-
/>
132-
{chat.isActive ? (
133-
<span
134-
role='img'
135-
aria-label='Running'
136-
className='inline-flex size-[14px] shrink-0 items-center justify-center'
137-
>
138-
<span className='size-[7px] rounded-full bg-[var(--caution)]' />
139-
</span>
140-
) : (
141-
<span className='shrink-0 text-[var(--text-muted)] text-caption'>
142-
{formatRelativeTime(chat.updatedAt.toISOString())
143-
.replace(' ago', '')
144-
.replace('just now', 'now')}
145-
</span>
146-
)}
147-
</Link>
148-
))}
149-
{chats.length > CHAT_PREVIEW && (
150-
<button
151-
type='button'
152-
onClick={() => setShowAll((prev) => !prev)}
153-
className={cn(
154-
chipVariants({ fullWidth: true }),
155-
'h-[26px] text-[var(--text-muted)] text-small'
156-
)}
157-
>
158-
{showAll ? 'Show less' : 'Show more'}
159-
</button>
160-
)}
161-
</div>
162-
)}
163-
</div>
77+
<ProjectRow
78+
project={project}
79+
href={href}
80+
newChatHref={`${routes.home}?project=${encodeURIComponent(project.id)}&pane=project`}
81+
active={inProject}
82+
railCollapsed={railCollapsed}
83+
drag={drag}
84+
isAnyDragActive={isAnyDragActive}
85+
/>
16486
)
16587
}

0 commit comments

Comments
 (0)