Skip to content

Commit 3a05bca

Browse files
committed
Refactor welcome message components and update dependencies
This commit introduces several improvements to the welcome message feature: - Split welcome message components into separate files for better organization - Replace @uiw/react-md-editor with react-markdown - Update TypeScript to version 5.7.3 - Modify organization member listing to exclude deleted and admin users - Add OrganizationJoinModal for new user onboarding flow - Simplify welcome message preview and editor components Tool: gitpod/catfood.gitpod.cloud
1 parent 6293167 commit 3a05bca

File tree

10 files changed

+304
-781
lines changed

10 files changed

+304
-781
lines changed

components/dashboard/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
"@tanstack/react-query": "^4.29.19",
2727
"@tanstack/react-query-devtools": "^4.29.19",
2828
"@tanstack/react-query-persist-client": "^4.29.19",
29-
"@uiw/react-md-editor": "^4.0.5",
3029
"@xterm/addon-fit": "^0.10.0",
3130
"@xterm/xterm": "^5.5.0",
3231
"buffer": "^4.3.0",
@@ -54,6 +53,7 @@
5453
"react-focus-on": "^3.8.1",
5554
"react-intl-tel-input": "^8.2.0",
5655
"react-linkedin-login-oauth2": "^2.0.1",
56+
"react-markdown": "^9.0.3",
5757
"react-popper": "^2.3.0",
5858
"react-portal": "^4.2.2",
5959
"react-router-dom": "^5.2.0",

components/dashboard/src/teams/TeamOnboarding.tsx

Lines changed: 7 additions & 169 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,10 @@
44
* See License.AGPL.txt in the project root for license information.
55
*/
66

7-
import {
8-
OnboardingSettings_WelcomeMessage,
9-
OrganizationSettings,
10-
} from "@gitpod/public-api/lib/gitpod/v1/organization_pb";
7+
import { OrganizationSettings } from "@gitpod/public-api/lib/gitpod/v1/organization_pb";
118
import { FormEvent, useCallback, useEffect, useState } from "react";
129
import { Heading2, Heading3, Subheading } from "../components/typography/headings";
13-
import { useIsOwner, useListOrganizationMembers } from "../data/organizations/members-query";
10+
import { useIsOwner } from "../data/organizations/members-query";
1411
import { useOrgSettingsQuery } from "../data/organizations/org-settings-query";
1512
import { useCurrentOrg } from "../data/organizations/orgs-query";
1613
import { useUpdateOrgSettingsMutation } from "../data/organizations/update-org-settings-mutation";
@@ -22,14 +19,12 @@ import type { PlainMessage } from "@bufbuild/protobuf";
2219
import { InputField } from "../components/forms/InputField";
2320
import { TextInput } from "../components/forms/TextInputField";
2421
import { LoadingButton } from "@podkit/buttons/LoadingButton";
25-
import MDEditor from "@uiw/react-md-editor";
26-
import { Textarea } from "@podkit/forms/TextArea";
27-
import Modal, { ModalFooter, ModalBody, ModalHeader } from "../components/Modal";
28-
import { Button } from "@podkit/buttons/Button";
2922
import { SwitchInputField } from "@podkit/switch/Switch";
30-
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@podkit/dropdown/DropDown";
23+
import { WelcomeMessagePreview } from "./onboarding/WelcomeMessagePreview";
24+
import { WelcomeMessageEditorModal } from "./onboarding/WelcomeMessageEditor";
3125

32-
const gitpodWelcomeSubheading = `Gitpod’s sandboxed, ephemeral development environments enable you to use your existing tools without worrying about vulnerabilities impacting their local machines.`;
26+
export const gitpodWelcomeSubheading =
27+
`Gitpod’s sandboxed, ephemeral development environments enable you to use your existing tools without worrying about vulnerabilities impacting their local machines.` as const;
3328

3429
export default function TeamOnboardingPage() {
3530
useDocumentTitle("Organization Settings - Onboarding");
@@ -151,7 +146,7 @@ export default function TeamOnboardingPage() {
151146
/>
152147
</InputField>
153148

154-
<WelcomeMessageEditor
149+
<WelcomeMessageEditorModal
155150
isLoading={updateTeamSettings.isLoading}
156151
isOwner={isOwner}
157152
isOpen={welcomeMessageEditorOpen}
@@ -160,8 +155,6 @@ export default function TeamOnboardingPage() {
160155
settings={settings?.onboardingSettings?.welcomeMessage}
161156
/>
162157

163-
{/* todo: add a warning if the welcome message is empty */}
164-
165158
<span className="text-pk-content-secondary text-sm">
166159
Here's a preview of the welcome message that will be shown to your organization members:
167160
</span>
@@ -174,158 +167,3 @@ export default function TeamOnboardingPage() {
174167
</OrgSettingsPage>
175168
);
176169
}
177-
178-
type WelcomeMessagePreviewProps = {
179-
disabled?: boolean;
180-
setWelcomeMessageEditorOpen?: (open: boolean) => void;
181-
};
182-
export const WelcomeMessagePreview = ({ disabled, setWelcomeMessageEditorOpen }: WelcomeMessagePreviewProps) => {
183-
const { data: settings } = useOrgSettingsQuery();
184-
const avatarUrl = settings?.onboardingSettings?.welcomeMessage?.featuredMemberResolvedAvatarUrl;
185-
const welcomeMessage = settings?.onboardingSettings?.welcomeMessage?.message;
186-
187-
return (
188-
<div className="max-w-2xl mx-auto">
189-
<div className="flex justify-between gap-2 items-center">
190-
<Heading2 className="py-6">Welcome to Gitpod</Heading2>
191-
{setWelcomeMessageEditorOpen && (
192-
<Button variant="secondary" onClick={() => setWelcomeMessageEditorOpen(true)} disabled={disabled}>
193-
Edit
194-
</Button>
195-
)}
196-
</div>
197-
<Subheading>{gitpodWelcomeSubheading}</Subheading>
198-
{/* todo: sanitize md */}
199-
{welcomeMessage && (
200-
<div className="p-8 my-4 bg-pk-surface-secondary text-pk-content-primary rounded-xl flex flex-col gap-5 items-center justify-center">
201-
{avatarUrl && <img src={avatarUrl} alt="" className="w-12 h-12 rounded-full" />}
202-
<MDEditor.Markdown
203-
source={welcomeMessage}
204-
className="md-preview space-y-4 text-center bg-pk-surface-secondary"
205-
/>
206-
</div>
207-
)}
208-
</div>
209-
);
210-
};
211-
212-
type WelcomeMessageEditorProps = {
213-
settings: OnboardingSettings_WelcomeMessage | undefined;
214-
handleUpdateTeamSettings: (
215-
newSettings: Partial<PlainMessage<OrganizationSettings>>,
216-
options?: { throwMutateError?: boolean },
217-
) => Promise<void>;
218-
isLoading: boolean;
219-
isOwner: boolean;
220-
isOpen: boolean;
221-
setIsOpen: (isOpen: boolean) => void;
222-
};
223-
const WelcomeMessageEditor = ({
224-
handleUpdateTeamSettings,
225-
isLoading,
226-
isOwner,
227-
settings,
228-
isOpen,
229-
setIsOpen,
230-
}: WelcomeMessageEditorProps) => {
231-
const [message, setMessage] = useState<string | undefined>(settings?.message);
232-
const [featuredMemberId, setFeaturedMemberId] = useState<string | undefined>(settings?.featuredMemberId);
233-
234-
const updateWelcomeMessage = useCallback(
235-
async (e: FormEvent) => {
236-
e.preventDefault();
237-
await handleUpdateTeamSettings({
238-
onboardingSettings: {
239-
welcomeMessage: { message, featuredMemberId, enabled: settings?.enabled ?? false },
240-
},
241-
});
242-
},
243-
[handleUpdateTeamSettings, message, featuredMemberId, settings?.enabled],
244-
);
245-
246-
return (
247-
<Modal onClose={() => setIsOpen(false)} visible={isOpen} containerClassName="min-[576px]:max-w-[650px]">
248-
<ModalHeader>Edit welcome message</ModalHeader>
249-
<ModalBody>
250-
<form id="welcome-message-editor" onSubmit={updateWelcomeMessage} className="space-y-4">
251-
<TextInput readOnly value="Welcome to Gitpod" className="cursor-default"></TextInput>
252-
<Textarea value={gitpodWelcomeSubheading} readOnly className="cursor-default resize-none" />
253-
<div className="w-full flex justify-center">
254-
<OrgMemberInput settings={settings} setFeaturedMemberId={setFeaturedMemberId} />
255-
</div>
256-
<InputField label="Welcome message" error={undefined} className="mb-4" labelHidden>
257-
<Textarea
258-
className="bg-pk-surface-secondary text-pk-content-primary w-full p-4 rounded-xl min-h-[150px]"
259-
value={message}
260-
placeholder="Write a welcome message to your organization members. Markdown formatting is supported."
261-
onChange={(e) => setMessage(e.target.value)}
262-
/>
263-
</InputField>
264-
</form>
265-
</ModalBody>
266-
<ModalFooter>
267-
<Button variant="secondary" onClick={() => setIsOpen(false)}>
268-
Cancel
269-
</Button>
270-
<LoadingButton type="submit" loading={isLoading} disabled={!isOwner} form="welcome-message-editor">
271-
Save
272-
</LoadingButton>
273-
</ModalFooter>
274-
</Modal>
275-
);
276-
};
277-
278-
type OrgMemberSelectProps = {
279-
settings: OnboardingSettings_WelcomeMessage | undefined;
280-
setFeaturedMemberId: (featuredMemberId: string | undefined) => void;
281-
};
282-
const OrgMemberInput = ({ settings, setFeaturedMemberId }: OrgMemberSelectProps) => {
283-
const { data: members } = useListOrganizationMembers();
284-
285-
const [avatarUrl, setAvatarUrl] = useState<string | undefined>(settings?.featuredMemberResolvedAvatarUrl);
286-
287-
return (
288-
<DropdownMenu>
289-
<DropdownMenuTrigger>
290-
<div className="flex flex-col justify-center items-center gap-2">
291-
{avatarUrl ? (
292-
<img src={avatarUrl} alt="" className="w-16 h-16 rounded-full" />
293-
) : (
294-
<div className="w-16 h-16 rounded-full bg-[#EA71DE]" />
295-
)}
296-
<Button variant="secondary" size="sm" type="button">
297-
Change Photo
298-
</Button>
299-
</div>
300-
</DropdownMenuTrigger>
301-
<DropdownMenuContent>
302-
<DropdownMenuItem
303-
key="disabled"
304-
onClick={() => {
305-
setFeaturedMemberId(undefined);
306-
setAvatarUrl(undefined);
307-
}}
308-
>
309-
<div className="flex items-center gap-2">
310-
<div className="w-4 h-4 rounded-full bg-pk-surface-tertiary" />
311-
Disable image
312-
</div>
313-
</DropdownMenuItem>
314-
{members?.map((member) => (
315-
<DropdownMenuItem
316-
key={member.userId}
317-
onClick={() => {
318-
setFeaturedMemberId(member.userId);
319-
setAvatarUrl(member.avatarUrl);
320-
}}
321-
>
322-
<div className="flex items-center gap-2">
323-
<img src={member.avatarUrl} alt={member.fullName} className="w-4 h-4 rounded-full" />
324-
{member.fullName}
325-
</div>
326-
</DropdownMenuItem>
327-
))}
328-
</DropdownMenuContent>
329-
</DropdownMenu>
330-
);
331-
};
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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 { useState } from "react";
8+
import { useListOrganizationMembers } from "../../data/organizations/members-query";
9+
10+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@podkit/dropdown/DropDown";
11+
import { Button } from "@podkit/buttons/Button";
12+
import type { OnboardingSettings_WelcomeMessage } from "@gitpod/public-api/lib/gitpod/v1/organization_pb";
13+
14+
type Props = {
15+
settings: OnboardingSettings_WelcomeMessage | undefined;
16+
setFeaturedMemberId: (featuredMemberId: string | undefined) => void;
17+
};
18+
export const OrgMemberAvatarInput = ({ settings, setFeaturedMemberId }: Props) => {
19+
const { data: members } = useListOrganizationMembers();
20+
21+
const [avatarUrl, setAvatarUrl] = useState<string | undefined>(settings?.featuredMemberResolvedAvatarUrl);
22+
23+
return (
24+
<DropdownMenu>
25+
<DropdownMenuTrigger>
26+
<div className="flex flex-col justify-center items-center gap-2">
27+
{avatarUrl ? (
28+
<img src={avatarUrl} alt="" className="w-16 h-16 rounded-full" />
29+
) : (
30+
<div className="w-16 h-16 rounded-full bg-[#EA71DE]" />
31+
)}
32+
<Button variant="secondary" size="sm" type="button">
33+
Change Photo
34+
</Button>
35+
</div>
36+
</DropdownMenuTrigger>
37+
<DropdownMenuContent>
38+
<DropdownMenuItem
39+
key="disabled"
40+
onClick={() => {
41+
setFeaturedMemberId(undefined);
42+
setAvatarUrl(undefined);
43+
}}
44+
>
45+
<div className="flex items-center gap-2">
46+
<div className="w-4 h-4 rounded-full bg-pk-surface-tertiary" />
47+
Disable image
48+
</div>
49+
</DropdownMenuItem>
50+
{members?.map((member) => (
51+
<DropdownMenuItem
52+
key={member.userId}
53+
onClick={() => {
54+
setFeaturedMemberId(member.userId);
55+
setAvatarUrl(member.avatarUrl);
56+
}}
57+
>
58+
<div className="flex items-center gap-2">
59+
<img src={member.avatarUrl} alt={member.fullName} className="w-4 h-4 rounded-full" />
60+
{member.fullName}
61+
</div>
62+
</DropdownMenuItem>
63+
))}
64+
</DropdownMenuContent>
65+
</DropdownMenu>
66+
);
67+
};
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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 { Button } from "@podkit/buttons/Button";
8+
import { ModalHeader } from "../../components/Modal";
9+
import { ModalBody } from "../../components/Modal";
10+
import { ModalFooter } from "../../components/Modal";
11+
import { Modal } from "../../components/Modal";
12+
import { useMemo, useCallback, useState, useEffect } from "react";
13+
import { storageAvailable } from "../../utils";
14+
import { OrganizationSettings } from "@gitpod/public-api/lib/gitpod/v1/organization_pb";
15+
import { WelcomeMessagePreview } from "./WelcomeMessagePreview";
16+
17+
type Props = {
18+
orgSettings: OrganizationSettings;
19+
};
20+
export const OrganizationJoinModal = ({ orgSettings }: Props) => {
21+
const initialOrgOnboardingPending = useMemo(() => {
22+
if (storageAvailable("localStorage")) {
23+
return localStorage.getItem("newUserOnboardingPending") === "true";
24+
}
25+
}, []);
26+
const dismissOrgOnboardingPending = useCallback(() => {
27+
if (storageAvailable("localStorage")) {
28+
localStorage.removeItem("newUserOnboardingPending");
29+
}
30+
31+
setOrgOnboardingPending(false);
32+
}, []);
33+
const [orgOnboardingPending, setOrgOnboardingPending] = useState(initialOrgOnboardingPending ?? false);
34+
35+
// if the org-wide welcome message is not enabled, prevent showing it in the future
36+
useEffect(() => {
37+
if (!orgSettings?.onboardingSettings?.welcomeMessage?.enabled) {
38+
dismissOrgOnboardingPending();
39+
}
40+
}, [orgSettings?.onboardingSettings?.welcomeMessage?.enabled, dismissOrgOnboardingPending]);
41+
42+
if (!orgSettings?.onboardingSettings?.welcomeMessage?.enabled) {
43+
return null;
44+
}
45+
46+
return (
47+
<Modal
48+
visible={orgOnboardingPending}
49+
onClose={dismissOrgOnboardingPending}
50+
containerClassName="min-[576px]:max-w-[650px]"
51+
>
52+
<ModalHeader>Welcome to Gitpod</ModalHeader>
53+
<ModalBody>
54+
<WelcomeMessagePreview hideHeader />
55+
</ModalBody>
56+
<ModalFooter>
57+
<Button onClick={dismissOrgOnboardingPending}>Get Started</Button>
58+
</ModalFooter>
59+
</Modal>
60+
);
61+
};

0 commit comments

Comments
 (0)