-
Notifications
You must be signed in to change notification settings - Fork 166
Expand file tree
/
Copy pathorg-menu-slot.tsx
More file actions
188 lines (174 loc) · 6.58 KB
/
Copy pathorg-menu-slot.tsx
File metadata and controls
188 lines (174 loc) · 6.58 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
import { useState } from "react";
import { useAtomValue } from "@effect/atom-react";
import * as AsyncResult from "effect/unstable/reactivity/AsyncResult";
import { trackEvent } from "@executor-js/react/api/analytics";
import { Button } from "@executor-js/react/components/button";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@executor-js/react/components/dialog";
import {
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
} from "@executor-js/react/components/dropdown-menu";
import { useAuth } from "../auth";
import { organizationsAtom } from "../auth";
import { CreateOrganizationFields, useCreateOrganizationForm } from "./create-organization-form";
// ---------------------------------------------------------------------------
// Cloud-only org-switcher slot for the shared shell's account dropdown.
//
// The shared `Shell` renders this `orgMenuSlot` ABOVE its API-keys link. Cloud
// is the only product with multiple organizations, so the switcher + create-org
// dialog live here and are injected, keeping the shared shell provider-neutral.
// The create-org dialog is controlled by local state so its `DialogContent`
// can live outside the dropdown menu while the trigger sits inside it.
// ---------------------------------------------------------------------------
function CheckIcon() {
return (
<svg viewBox="0 0 16 16" fill="none" className="ml-auto size-3 text-muted-foreground">
<path
d="M3.5 8.5L6.5 11.5L12.5 5"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
function OrganizationSwitcherItems(props: { activeOrganizationId: string | null }) {
const organizations = useAtomValue(organizationsAtom);
// Switching orgs is now a pure URL navigation: the session authenticates the
// user to ALL their orgs, and the slug in the path scopes every request (the
// `x-executor-organization` header). No cookie to rewrite, no server switch
// call — just land on the other org's URL root and the whole app re-scopes.
const handleSwitch = (organization: { id: string; slug: string }) => {
if (organization.id === props.activeOrganizationId) return;
trackEvent("org_switched", { success: true });
window.location.href = `/${organization.slug}`;
};
return AsyncResult.match(organizations, {
onInitial: () => <DropdownMenuItem disabled>Loading…</DropdownMenuItem>,
onFailure: () => <DropdownMenuItem disabled>Failed to load organizations</DropdownMenuItem>,
onSuccess: ({ value }) =>
value.organizations.length === 0 ? (
<DropdownMenuItem disabled>No organizations</DropdownMenuItem>
) : (
<>
{value.organizations.map((organization: { id: string; name: string; slug: string }) => {
const isActive = organization.id === props.activeOrganizationId;
return (
<DropdownMenuItem
key={organization.id}
disabled={isActive}
onClick={() => {
handleSwitch(organization);
}}
className="text-xs"
>
<span className="min-w-0 flex-1 truncate">{organization.name}</span>
{isActive && <CheckIcon />}
</DropdownMenuItem>
);
})}
</>
),
});
}
export function OrgMenuSlot() {
const auth = useAuth();
const [createOrganizationOpen, setCreateOrganizationOpen] = useState(false);
const suggestedOrganizationName =
auth.status === "authenticated" && auth.user.name?.trim() !== "" && auth.user.name != null
? `${auth.user.name}'s Organization`
: "New Organization";
const form = useCreateOrganizationForm({
defaultName: suggestedOrganizationName,
// Land on the new org's URL root — a reload would keep the old slug and
// the slug gate would switch the session right back.
onSuccess: (org) => {
window.location.href = `/${org.slug}`;
},
});
if (auth.status !== "authenticated") return null;
const openCreateOrganization = () => {
form.reset(suggestedOrganizationName);
setCreateOrganizationOpen(true);
};
return (
<>
<DropdownMenuLabel className="text-xs font-normal text-muted-foreground">
Organization
</DropdownMenuLabel>
<DropdownMenuSub>
<DropdownMenuSubTrigger className="text-xs">
<span className="min-w-0 flex-1 truncate">
{auth.organization?.name ?? "No organization"}
</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent className="w-56">
<OrganizationSwitcherItems activeOrganizationId={auth.organization?.id ?? null} />
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-xs"
onSelect={(event) => {
event.preventDefault();
openCreateOrganization();
}}
>
Create organization
</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSeparator />
<Dialog
open={createOrganizationOpen}
onOpenChange={(open) => {
setCreateOrganizationOpen(open);
if (!open) form.reset(suggestedOrganizationName);
}}
>
<DialogContent className="sm:max-w-[420px]">
<DialogHeader>
<DialogTitle className="font-display text-xl">Create organization</DialogTitle>
<DialogDescription className="text-sm leading-relaxed">
Add another organization under your current account and switch into it immediately.
</DialogDescription>
</DialogHeader>
<CreateOrganizationFields
name={form.name}
onNameChange={(name) => {
form.setName(name);
if (form.error) form.setError(null);
}}
error={form.error}
onSubmit={() => void form.submit()}
/>
<DialogFooter>
<DialogClose asChild>
<Button variant="ghost" size="sm" disabled={form.creating}>
Cancel
</Button>
</DialogClose>
<Button
size="sm"
onClick={() => void form.submit()}
disabled={!form.canSubmit || form.creating}
>
{form.creating ? "Creating…" : "Create organization"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}