Skip to content

Commit 4f71964

Browse files
committed
feat(dashboard): Enhance team onboarding with member avatar and framework selection
- Add OrgMemberAvatarInput component with improved member selection using Popover and Command - Implement ComboboxDemo for framework selection - Update Button variant and add cmdk package - Improve UI components with more flexible selection and filtering Tool: gitpod/catfood.gitpod.cloud
1 parent 0e2cafa commit 4f71964

File tree

6 files changed

+542
-47
lines changed

6 files changed

+542
-47
lines changed

components/dashboard/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"buffer": "^4.3.0",
3232
"class-variance-authority": "^0.7.0",
3333
"classnames": "^2.3.1",
34+
"cmdk": "^1.0.4",
3435
"configcat-js": "^6.0.0",
3536
"countries-list": "^2.6.1",
3637
"crypto-browserify": "3.12.0",

components/dashboard/src/components/podkit/buttons/Button.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export const buttonVariants = cva(
1717
default:
1818
"bg-gray-900 hover:bg-gray-800 dark:bg-kumquat-base dark:hover:bg-kumquat-ripe text-gray-50 dark:text-gray-900",
1919
destructive: "bg-red-600 hover:bg-red-700 text-gray-100 dark:text-red-100",
20-
outline: "border border-input bg-transparent hover:bg-kumquat-ripe hover:text-gray-600",
20+
outline: "border border-input bg-transparent hover:bg-pk-surface-secondary hover:text-gray-600",
2121
secondary:
2222
"bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 text-gray-500 dark:text-gray-100 hover:text-gray-600",
2323
ghost: "bg-transparent hover:opacity-50",
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/**
2+
* Copyright (c) 2025 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License.AGPL.txt in the project root for license information.
5+
*/
6+
7+
import * as React from "react";
8+
import { Command as CommandPrimitive } from "cmdk";
9+
import { Search } from "lucide-react";
10+
11+
import { cn } from "@podkit/lib/cn";
12+
13+
const Command = React.forwardRef<
14+
React.ElementRef<typeof CommandPrimitive>,
15+
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
16+
>(({ className, ...props }, ref) => (
17+
<CommandPrimitive
18+
ref={ref}
19+
className={cn(
20+
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
21+
className,
22+
)}
23+
{...props}
24+
/>
25+
));
26+
Command.displayName = CommandPrimitive.displayName;
27+
28+
// todo(ft): Add when needed
29+
// const CommandDialog = ({ children, ...props }: DialogProps) => {
30+
// return (
31+
// <Dialog {...props}>
32+
// <DialogContent className="overflow-hidden p-0 shadow-lg">
33+
// <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
34+
// {children}
35+
// </Command>
36+
// </DialogContent>
37+
// </Dialog>
38+
// )
39+
// }
40+
41+
const CommandInput = React.forwardRef<
42+
React.ElementRef<typeof CommandPrimitive.Input>,
43+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
44+
>(({ className, ...props }, ref) => (
45+
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
46+
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
47+
<CommandPrimitive.Input
48+
ref={ref}
49+
className={cn(
50+
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
51+
className,
52+
)}
53+
{...props}
54+
/>
55+
</div>
56+
));
57+
58+
CommandInput.displayName = CommandPrimitive.Input.displayName;
59+
60+
const CommandList = React.forwardRef<
61+
React.ElementRef<typeof CommandPrimitive.List>,
62+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
63+
>(({ className, ...props }, ref) => (
64+
<CommandPrimitive.List
65+
ref={ref}
66+
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
67+
{...props}
68+
/>
69+
));
70+
71+
CommandList.displayName = CommandPrimitive.List.displayName;
72+
73+
const CommandEmpty = React.forwardRef<
74+
React.ElementRef<typeof CommandPrimitive.Empty>,
75+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
76+
>((props, ref) => <CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />);
77+
78+
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
79+
80+
const CommandGroup = React.forwardRef<
81+
React.ElementRef<typeof CommandPrimitive.Group>,
82+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
83+
>(({ className, ...props }, ref) => (
84+
<CommandPrimitive.Group
85+
ref={ref}
86+
className={cn(
87+
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
88+
className,
89+
)}
90+
{...props}
91+
/>
92+
));
93+
94+
CommandGroup.displayName = CommandPrimitive.Group.displayName;
95+
96+
const CommandSeparator = React.forwardRef<
97+
React.ElementRef<typeof CommandPrimitive.Separator>,
98+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
99+
>(({ className, ...props }, ref) => (
100+
<CommandPrimitive.Separator ref={ref} className={cn("-mx-1 h-px bg-border", className)} {...props} />
101+
));
102+
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
103+
104+
const CommandItem = React.forwardRef<
105+
React.ElementRef<typeof CommandPrimitive.Item>,
106+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
107+
>(({ className, ...props }, ref) => (
108+
<CommandPrimitive.Item
109+
ref={ref}
110+
className={cn(
111+
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
112+
className,
113+
)}
114+
{...props}
115+
/>
116+
));
117+
118+
CommandItem.displayName = CommandPrimitive.Item.displayName;
119+
120+
const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
121+
return <span className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />;
122+
};
123+
CommandShortcut.displayName = "CommandShortcut";
124+
125+
export {
126+
Command,
127+
CommandInput,
128+
CommandList,
129+
CommandEmpty,
130+
CommandGroup,
131+
CommandItem,
132+
CommandShortcut,
133+
CommandSeparator,
134+
};

components/dashboard/src/teams/TeamOnboarding.tsx

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ import { RepositoryListItem } from "../repositories/list/RepoListItem";
2626
import { LoadingState } from "@podkit/loading/LoadingState";
2727
import { Table, TableHeader, TableRow, TableHead, TableBody } from "@podkit/tables/Table";
2828
import { WelcomeMessageConfigurationField } from "./onboarding/WelcomeMessageConfigurationField";
29+
import { OrgMemberAvatarInput } from "./onboarding/OrgMemberAvatarInput";
30+
import { Popover, PopoverContent, PopoverTrigger } from "@podkit/popover/Popover";
31+
import { Button } from "@podkit/buttons/Button";
32+
import { Check, ChevronsUpDown } from "lucide-react";
33+
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@podkit/command/Command";
34+
import { cn } from "@podkit/lib/cn";
2935

3036
export type UpdateTeamSettingsOptions = {
3137
throwMutateError?: boolean;
@@ -44,6 +50,9 @@ export default function TeamOnboardingPage() {
4450

4551
const [internalLink, setInternalLink] = useState<string | undefined>(undefined);
4652

53+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
54+
const [featuredMemberId, setFeaturedMemberId] = useState<string | undefined>(undefined);
55+
4756
const handleUpdateTeamSettings = useCallback(
4857
async (newSettings: UpdateOrganizationSettingsArgs, options?: UpdateTeamSettingsOptions) => {
4958
if (!org?.id) {
@@ -163,8 +172,181 @@ export default function TeamOnboardingPage() {
163172
)}
164173
</ConfigurationSettingsField>
165174

175+
<OrgMemberAvatarInput
176+
settings={settings?.onboardingSettings?.welcomeMessage}
177+
setFeaturedMemberId={setFeaturedMemberId}
178+
/>
179+
<ComboboxDemo />
180+
166181
<WelcomeMessageConfigurationField handleUpdateTeamSettings={handleUpdateTeamSettings} />
167182
</div>
168183
</OrgSettingsPage>
169184
);
170185
}
186+
187+
const frameworks = [
188+
{
189+
value: "next.js",
190+
label: "AAAANext.js",
191+
},
192+
{
193+
value: "sveltekit",
194+
label: "SvelteKit",
195+
},
196+
{
197+
value: "nuxt.js",
198+
label: "Nuxt.js",
199+
},
200+
{
201+
value: "remix",
202+
label: "Remix",
203+
},
204+
{
205+
value: "astro",
206+
label: "Astro",
207+
},
208+
{
209+
value: "gatsby",
210+
label: "Gatsby",
211+
},
212+
{
213+
value: "angular",
214+
label: "Angular",
215+
},
216+
{
217+
value: "ember",
218+
label: "Ember.js",
219+
},
220+
{
221+
value: "qwik",
222+
label: "Qwik",
223+
},
224+
{
225+
value: "solid",
226+
label: "SolidJS",
227+
},
228+
{
229+
value: "vite",
230+
label: "Vite",
231+
},
232+
{
233+
value: "eleventy",
234+
label: "Eleventy",
235+
},
236+
{
237+
value: "redwood",
238+
label: "RedwoodJS",
239+
},
240+
{
241+
value: "fresh",
242+
label: "Fresh",
243+
},
244+
{
245+
value: "nest",
246+
label: "NestJS",
247+
},
248+
{
249+
value: "vue",
250+
label: "Vue.js",
251+
},
252+
{
253+
value: "create-react-app",
254+
label: "Create React App",
255+
},
256+
{
257+
value: "preact",
258+
label: "Preact",
259+
},
260+
{
261+
value: "gridsome",
262+
label: "Gridsome",
263+
},
264+
{
265+
value: "blitz",
266+
label: "Blitz.js",
267+
},
268+
{
269+
value: "hydrogen",
270+
label: "Hydrogen",
271+
},
272+
{
273+
value: "remix-indie",
274+
label: "Remix Indie Stack",
275+
},
276+
{
277+
value: "expo",
278+
label: "Expo",
279+
},
280+
{
281+
value: "docusaurus",
282+
label: "Docusaurus",
283+
},
284+
{
285+
value: "react-native",
286+
label: "React Native",
287+
},
288+
{
289+
value: "t3-app",
290+
label: "Create T3 App",
291+
},
292+
{
293+
value: "ionic",
294+
label: "Ionic",
295+
},
296+
{
297+
value: "vitepress",
298+
label: "VitePress",
299+
},
300+
{
301+
value: "nextra",
302+
label: "Nextra",
303+
},
304+
{
305+
value: "adonis",
306+
label: "AdonisJS",
307+
},
308+
];
309+
310+
export function ComboboxDemo() {
311+
const [open, setOpen] = useState(false);
312+
const [value, setValue] = useState("");
313+
314+
return (
315+
<Popover open={open} onOpenChange={setOpen}>
316+
<PopoverTrigger asChild>
317+
<Button variant="outline" role="combobox" aria-expanded={open} className="w-[200px] justify-between">
318+
{value ? frameworks.find((framework) => framework.value === value)?.label : "Select framework..."}
319+
<ChevronsUpDown className="opacity-50" />
320+
</Button>
321+
</PopoverTrigger>
322+
<PopoverContent className="w-[200px] p-0">
323+
<Command>
324+
<CommandInput placeholder="Search framework..." className="h-9" />
325+
<CommandList>
326+
<CommandEmpty>No framework found.</CommandEmpty>
327+
<CommandGroup>
328+
{frameworks.map((framework) => (
329+
<CommandItem
330+
key={framework.value}
331+
value={framework.value}
332+
onSelect={(currentValue) => {
333+
setValue(currentValue === value ? "" : currentValue);
334+
setOpen(false);
335+
}}
336+
>
337+
{framework.label}
338+
<Check
339+
className={cn(
340+
"ml-auto",
341+
value === framework.value ? "opacity-100" : "opacity-0",
342+
)}
343+
/>
344+
</CommandItem>
345+
))}
346+
</CommandGroup>
347+
</CommandList>
348+
</Command>
349+
</PopoverContent>
350+
</Popover>
351+
);
352+
}

0 commit comments

Comments
 (0)