Skip to content

Commit f51110a

Browse files
mzxchandraclaude
andcommitted
feat(playground): org sidebar account footer, fork from any environment
The org view's footer is now the shared account and help footer: clicking your name opens Settings (the organization's settings) and Sign out, and help offers docs, Slack and support. The fork modal takes an optional list of sources; the Environments tab passes the lineage, so Create fork can start from Prod, Staging or Sandbox, with the default name and resource list following the chosen source. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent d921f6c commit f51110a

3 files changed

Lines changed: 62 additions & 25 deletions

File tree

‎apps/sim/app/playground/org/components/environments/lineage-strip.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ export function LineageStrip({ project, columns, lineageByEnv, canManage }: Line
212212
onOpenChange={setForkModalOpen}
213213
sourceWorkspaceId={project.id}
214214
sourceWorkspaceName={current?.name || project.name}
215+
sources={columns.map((column) => ({ id: column.id, name: column.name }))}
215216
canFork={creationPolicy?.canCreate ?? true}
216217
onUpgrade={() => {
217218
if (billingEnabled) navigateToSettings({ section: 'billing' })

‎apps/sim/app/playground/org/components/proto-sidebar.tsx‎

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

33
import { useRef, useState } from 'react'
44
import {
5-
Avatar,
65
Chip,
76
ChipLink,
87
chipVariants,
@@ -19,15 +18,16 @@ import {
1918
PanelLeft,
2019
Plus,
2120
Search,
22-
Settings,
2321
SquarePen,
2422
} from '@sim/emcn/icons'
2523
import { formatRelativeTime } from '@sim/utils/formatting'
2624
import Link from 'next/link'
27-
import { usePathname } from 'next/navigation'
25+
import { usePathname, useRouter } from 'next/navigation'
2826
import { useQueryStates } from 'nuqs'
2927
import { IdentityTile } from '@/components/identity-tile/identity-tile'
3028
import { useActiveOrganization, useSession } from '@/lib/auth/auth-client'
29+
import { DOCS_URL, SLACK_COMMUNITY_URL } from '@/lib/help-links'
30+
import { organizationRoutes, WORKSPACE_SETTINGS_PATH } from '@/lib/navigation/paths'
3131
import { RunningDot } from '@/app/playground/org/components/glyphs'
3232
import { ProjectRow } from '@/app/playground/org/components/project-row'
3333
import { type Project, useProjects } from '@/app/playground/org/lib/project'
@@ -51,7 +51,9 @@ import {
5151
import { type ProjectDragProps, useProjectOrder } from '@/app/playground/org/lib/use-project-order'
5252
import { useSidebarChrome } from '@/app/workspace/[workspaceId]/components/workspace-chrome'
5353
import {
54+
HelpModal,
5455
isNavItemActive,
56+
SidebarFooter,
5557
SidebarNavChip,
5658
type SidebarNavItemData,
5759
SidebarSection,
@@ -121,6 +123,8 @@ export function ProtoSidebar() {
121123
const [{ chat: openChat }] = useQueryStates(protoParsers)
122124
const { projects, roots } = useProjects()
123125
const { projects: orderedProjects, dragProps, isAnyDragActive } = useProjectOrder(roots)
126+
const router = useRouter()
127+
const [helpOpen, setHelpOpen] = useState(false)
124128
const { data: organization } = useActiveOrganization()
125129
const { data: session } = useSession()
126130
/** Only a mock project has a full view here; a real project's Build is its workspace pages. */
@@ -129,6 +133,10 @@ export function ProtoSidebar() {
129133
)
130134
const inSettings = Boolean(fullProject && settingsProject(pathname))
131135
const organizationId = projects.find((project) => project.organizationId)?.organizationId
136+
/** The organization's settings; without an organization, the viewer's workspace settings. */
137+
const accountSettingsHref = organizationId
138+
? organizationRoutes(organizationId).settingsSection('general')
139+
: WORKSPACE_SETTINGS_PATH
132140
const toggleCollapsed = useSidebarStore((state) => state.toggleCollapsed)
133141
const scrollContainerRef = useRef<HTMLDivElement>(null)
134142
const scrollContentRef = useRef<HTMLDivElement>(null)
@@ -275,24 +283,19 @@ export function ProtoSidebar() {
275283
</div>
276284
</div>
277285

278-
<div
279-
className={cn(
280-
'flex shrink-0 items-center gap-[1px] border-t px-2 py-2 transition-colors',
281-
!scrollEdges.bottom && 'border-transparent'
282-
)}
283-
>
284-
<div className='min-w-0 flex-1'>
285-
<Chip
286-
fullWidth
287-
className={SIDEBAR_RAIL_CHIP_CLASS}
288-
leftAdornment={<Avatar size='xs' name={userName} />}
289-
>
290-
{userName}
291-
</Chip>
292-
</div>
293-
{!isCollapsed && <Chip leftIcon={Settings} aria-label='Settings' />}
294-
</div>
286+
<SidebarFooter
287+
showDivider={scrollEdges.bottom}
288+
isCollapsed={isCollapsed}
289+
showCollapsedTooltips={isCollapsed}
290+
accountSettingsHref={accountSettingsHref}
291+
onOpenAccountSettings={() => router.push(accountSettingsHref)}
292+
navigationLinks={[]}
293+
onOpenDocs={() => window.open(DOCS_URL, '_blank', 'noopener,noreferrer')}
294+
onJoinSlack={() => window.open(SLACK_COMMUNITY_URL, '_blank', 'noopener,noreferrer')}
295+
onContactSupport={() => setHelpOpen(true)}
296+
/>
295297
</div>
298+
<HelpModal open={helpOpen} onOpenChange={setHelpOpen} />
296299
</aside>
297300
)
298301
}

‎apps/sim/ee/workspace-forking/components/fork-workspace-modal/fork-workspace-modal.tsx‎

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
ChipModalError,
1010
ChipModalFooter,
1111
ChipModalHeader,
12+
ChipSelect,
1213
toast,
1314
} from '@sim/emcn'
1415
import { TriangleAlert } from '@sim/emcn/icons'
@@ -26,6 +27,11 @@ interface ForkWorkspaceModalProps {
2627
onOpenChange: (open: boolean) => void
2728
sourceWorkspaceId: string
2829
sourceWorkspaceName: string
30+
/**
31+
* Other workspaces the fork may be taken from, such as the environments of a project. With
32+
* more than one, "Forking from" becomes a picker that starts on `sourceWorkspaceId`.
33+
*/
34+
sources?: readonly { id: string; name: string }[]
2935
/** Whether the user is under their workspace cap; creating a fork is gated on this. */
3036
canFork: boolean
3137
/** Sends the user to upgrade (billing) when they try to fork at the cap. */
@@ -75,25 +81,39 @@ export function ForkWorkspaceModal({
7581
onOpenChange,
7682
sourceWorkspaceId,
7783
sourceWorkspaceName,
84+
sources,
7885
canFork,
7986
onUpgrade,
8087
}: ForkWorkspaceModalProps) {
8188
const router = useRouter()
8289
const forkWorkspace = useForkWorkspace()
83-
const resources = useForkResources(sourceWorkspaceId, open)
90+
const [sourceId, setSourceId] = useState(sourceWorkspaceId)
91+
const sourceName = sources?.find((source) => source.id === sourceId)?.name ?? sourceWorkspaceName
92+
const resources = useForkResources(sourceId, open)
8493
const [name, setName] = useState('')
8594
const [selected, setSelected] = useState<ResourceSelection>(emptySelection)
8695
const [defaulted, setDefaulted] = useState(false)
8796
const [error, setError] = useState<string | null>(null)
8897

8998
useEffect(() => {
9099
if (open) {
91-
setName(`${sourceWorkspaceName} (fork)`)
100+
setSourceId(sourceWorkspaceId)
92101
setSelected(emptySelection())
93102
setDefaulted(false)
94103
setError(null)
95104
}
96-
}, [open, sourceWorkspaceName])
105+
}, [open, sourceWorkspaceId])
106+
107+
/** The default name follows the chosen source until the user types one of their own. */
108+
useEffect(() => {
109+
if (open) setName(`${sourceName} (fork)`)
110+
}, [open, sourceName])
111+
112+
const handleSourceChange = (nextSourceId: string) => {
113+
setSourceId(nextSourceId)
114+
setSelected(emptySelection())
115+
setDefaulted(false)
116+
}
97117

98118
useEffect(() => {
99119
if (!open || !resources.data || defaulted) return
@@ -141,7 +161,7 @@ export function ForkWorkspaceModal({
141161
RESOURCE_KINDS.map((kind) => [kind.key, Array.from(selected[kind.key])])
142162
)
143163
forkWorkspace.mutate(
144-
{ workspaceId: sourceWorkspaceId, body: { name: trimmed, copy } },
164+
{ workspaceId: sourceId, body: { name: trimmed, copy } },
145165
{
146166
onSuccess: (result) => {
147167
// The copy job's progress lands in the page's Activity log; the toast action
@@ -165,7 +185,20 @@ export function ForkWorkspaceModal({
165185
<ChipModalBody>
166186
<div className='flex flex-col gap-7 px-2'>
167187
<SettingsSection label='Forking from'>
168-
<ChipCopyInput value={sourceWorkspaceName} aria-label='Forking from' />
188+
{sources && sources.length > 1 ? (
189+
<ChipSelect
190+
aria-label='Forking from'
191+
align='start'
192+
fullWidth
193+
dropdownWidth='trigger'
194+
value={sourceId}
195+
onChange={handleSourceChange}
196+
disabled={isForking}
197+
options={sources.map((source) => ({ value: source.id, label: source.name }))}
198+
/>
199+
) : (
200+
<ChipCopyInput value={sourceName} aria-label='Forking from' />
201+
)}
169202
</SettingsSection>
170203

171204
<SettingsSection

0 commit comments

Comments
 (0)