Skip to content

Commit 0d7d880

Browse files
committed
Update organization onboarding settings to support optional welcome message fields
This change modifies the organization onboarding settings to make welcome message fields optional, including: - Making message, featured member ID, and avatar URL optional - Removing the footer field - Adding validation to prevent enabling an empty welcome message Tool: gitpod/catfood.gitpod.cloud
1 parent d42f22d commit 0d7d880

File tree

7 files changed

+952
-977
lines changed

7 files changed

+952
-977
lines changed

components/dashboard/src/teams/TeamOnboarding.tsx

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

7-
import { OrganizationSettings } from "@gitpod/public-api/lib/gitpod/v1/organization_pb";
7+
import {
8+
OnboardingSettings_WelcomeMessage,
9+
OrganizationSettings,
10+
} from "@gitpod/public-api/lib/gitpod/v1/organization_pb";
811
import { FormEvent, useCallback, useEffect, useState } from "react";
912
import { Heading2, Heading3, Subheading } from "../components/typography/headings";
1013
import { useIsOwner, useListOrganizationMembers } from "../data/organizations/members-query";
@@ -26,10 +29,6 @@ import { Button } from "@podkit/buttons/Button";
2629
import { SwitchInputField } from "@podkit/switch/Switch";
2730
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@podkit/dropdown/DropDown";
2831

29-
const sampleMarkdown = `“With Gitpod, you'll boost your productivity and streamline your workflow, giving you the freedom to work how you want while helping our team meet its efficiency goals.”
30-
31-
**Hannah Cole** - CTO Acme, Inc.
32-
`;
3332
const gitpodWelcomeSubheading = `Gitpod’s sandboxed, ephemeral development environments enable you to use your existing tools without worrying about vulnerabilities impacting their local machines.`;
3433

3534
export default function TeamOnboardingPage() {
@@ -42,10 +41,8 @@ export default function TeamOnboardingPage() {
4241
const updateTeamSettings = useUpdateOrgSettingsMutation();
4342

4443
const [internalLink, setInternalLink] = useState<string | undefined>(undefined);
45-
const [welcomeMessage, setWelcomeMessage] = useState<string>(sampleMarkdown);
46-
const [showWelcomeMessage, setShowWelcomeMessage] = useState<boolean>(true);
4744
const [welcomeMessageEditorOpen, setWelcomeMessageEditorOpen] = useState<boolean>(false);
48-
const [avatarURL, setAvatarURL] = useState<string | undefined>(undefined);
45+
4946
const handleUpdateTeamSettings = useCallback(
5047
async (newSettings: Partial<PlainMessage<OrganizationSettings>>, options?: { throwMutateError?: boolean }) => {
5148
if (!org?.id) {
@@ -129,65 +126,69 @@ export default function TeamOnboardingPage() {
129126
>
130127
<SwitchInputField
131128
id="show-welcome-message"
132-
checked={showWelcomeMessage}
129+
checked={settings?.onboardingSettings?.welcomeMessage?.enabled ?? false}
133130
disabled={!isOwner || updateTeamSettings.isLoading}
134-
onCheckedChange={setShowWelcomeMessage}
131+
onCheckedChange={(checked) => {
132+
if (checked) {
133+
if (!settings?.onboardingSettings?.welcomeMessage?.message) {
134+
toast("Please set up a welcome message first.");
135+
return;
136+
}
137+
}
138+
139+
updateTeamSettings.mutate({
140+
onboardingSettings: {
141+
welcomeMessage: {
142+
enabled: checked,
143+
message: settings?.onboardingSettings?.welcomeMessage?.message,
144+
featuredMemberId:
145+
settings?.onboardingSettings?.welcomeMessage?.featuredMemberId,
146+
},
147+
},
148+
});
149+
}}
135150
label=""
136151
/>
137152
</InputField>
138153

154+
<WelcomeMessageEditor
155+
isLoading={updateTeamSettings.isLoading}
156+
isOwner={isOwner}
157+
isOpen={welcomeMessageEditorOpen}
158+
setIsOpen={setWelcomeMessageEditorOpen}
159+
handleUpdateTeamSettings={handleUpdateTeamSettings}
160+
settings={settings?.onboardingSettings?.welcomeMessage}
161+
/>
162+
163+
{/* todo: add a warning if the welcome message is empty */}
164+
139165
<span className="text-pk-content-secondary text-sm">
140166
Here's a preview of the welcome message that will be shown to your organization members:
141167
</span>
142168
<WelcomeMessagePreview
143-
welcomeMessage={welcomeMessage}
169+
welcomeMessage={settings?.onboardingSettings?.welcomeMessage?.message}
144170
setWelcomeMessageEditorOpen={setWelcomeMessageEditorOpen}
145-
avatarURL={avatarURL}
146-
disabled={!showWelcomeMessage}
171+
disabled={!isOwner || updateTeamSettings.isLoading}
147172
/>
148-
149-
<Modal
150-
onClose={() => setWelcomeMessageEditorOpen(false)}
151-
visible={welcomeMessageEditorOpen}
152-
containerClassName="min-[576px]:max-w-[650px]"
153-
>
154-
<ModalHeader>Edit welcome message</ModalHeader>
155-
<ModalBody>
156-
<WelcomeMessageEditor
157-
welcomeMessage={welcomeMessage}
158-
setWelcomeMessage={setWelcomeMessage}
159-
disabled={!showWelcomeMessage}
160-
avatarURL={avatarURL}
161-
setAvatarURL={setAvatarURL}
162-
/>
163-
</ModalBody>
164-
<ModalFooter>
165-
<Button variant="secondary" onClick={() => setWelcomeMessageEditorOpen(false)}>
166-
Cancel
167-
</Button>
168-
<LoadingButton type="submit" loading={updateTeamSettings.isLoading} disabled={!isOwner}>
169-
Save
170-
</LoadingButton>
171-
</ModalFooter>
172-
</Modal>
173173
</ConfigurationSettingsField>
174174
</div>
175175
</OrgSettingsPage>
176176
);
177177
}
178178

179179
type WelcomeMessagePreviewProps = {
180-
welcomeMessage: string;
181-
avatarURL: string | undefined;
180+
welcomeMessage: string | undefined;
182181
disabled?: boolean;
183182
setWelcomeMessageEditorOpen: (open: boolean) => void;
184183
};
185184
const WelcomeMessagePreview = ({
186185
welcomeMessage,
187-
avatarURL,
188186
disabled,
189187
setWelcomeMessageEditorOpen,
190188
}: WelcomeMessagePreviewProps) => {
189+
const { data: settings } = useOrgSettingsQuery();
190+
const avatarUrl = settings?.onboardingSettings?.welcomeMessage?.featuredMemberResolvedAvatarUrl;
191+
191192
return (
192193
<div className="max-w-2xl mx-auto">
193194
<div className="flex justify-between gap-2 items-center">
@@ -198,82 +199,129 @@ const WelcomeMessagePreview = ({
198199
</div>
199200
<Subheading>{gitpodWelcomeSubheading}</Subheading>
200201
{/* todo: sanitize md */}
201-
<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">
202-
{avatarURL && <img src={avatarURL} alt="" className="w-12 h-12 rounded-full" />}
203-
<MDEditor.Markdown
204-
source={welcomeMessage}
205-
className="md-preview space-y-4 text-center bg-pk-surface-secondary"
206-
/>
207-
</div>
202+
{welcomeMessage && (
203+
<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">
204+
{avatarUrl && <img src={avatarUrl} alt="" className="w-12 h-12 rounded-full" />}
205+
<MDEditor.Markdown
206+
source={welcomeMessage}
207+
className="md-preview space-y-4 text-center bg-pk-surface-secondary"
208+
/>
209+
</div>
210+
)}
208211
</div>
209212
);
210213
};
211214

212215
type WelcomeMessageEditorProps = {
213-
welcomeMessage: string;
214-
setWelcomeMessage: (welcomeMessage: string) => void;
215-
disabled?: boolean;
216-
avatarURL: string | undefined;
217-
setAvatarURL: (avatarURL: string | undefined) => void;
216+
settings: OnboardingSettings_WelcomeMessage | undefined;
217+
handleUpdateTeamSettings: (
218+
newSettings: Partial<PlainMessage<OrganizationSettings>>,
219+
options?: { throwMutateError?: boolean },
220+
) => Promise<void>;
221+
isLoading: boolean;
222+
isOwner: boolean;
223+
isOpen: boolean;
224+
setIsOpen: (isOpen: boolean) => void;
218225
};
219226
const WelcomeMessageEditor = ({
220-
welcomeMessage,
221-
setWelcomeMessage,
222-
disabled,
223-
avatarURL,
224-
setAvatarURL,
227+
handleUpdateTeamSettings,
228+
isLoading,
229+
isOwner,
230+
settings,
231+
isOpen,
232+
setIsOpen,
225233
}: WelcomeMessageEditorProps) => {
234+
const [message, setMessage] = useState<string | undefined>(settings?.message);
235+
const [featuredMemberId, setFeaturedMemberId] = useState<string | undefined>(settings?.featuredMemberId);
236+
237+
const updateWelcomeMessage = useCallback(
238+
async (e: FormEvent) => {
239+
e.preventDefault();
240+
await handleUpdateTeamSettings({
241+
onboardingSettings: {
242+
welcomeMessage: { message, featuredMemberId, enabled: settings?.enabled ?? false },
243+
},
244+
});
245+
},
246+
[handleUpdateTeamSettings, message, featuredMemberId, settings?.enabled],
247+
);
248+
226249
return (
227-
<div className="space-y-4">
228-
<TextInput readOnly value="Welcome to Gitpod" className="cursor-default"></TextInput>
229-
<Textarea value={gitpodWelcomeSubheading} readOnly className="cursor-default resize-none" />
230-
<div className="w-full flex justify-center">
231-
<OrgMemberInput avatarURL={avatarURL} setAvatarURL={setAvatarURL} disabled={disabled} />
232-
</div>
233-
<InputField label="Welcome message" error={undefined} className="mb-4" labelHidden>
234-
<Textarea
235-
className="bg-pk-surface-secondary text-pk-content-primary w-full p-4 rounded-xl min-h-[150px]"
236-
value={welcomeMessage}
237-
placeholder="Write a welcome message to your organization members. Markdown formatting is supported."
238-
onChange={(e) => setWelcomeMessage(e.target.value)}
239-
disabled={disabled}
240-
/>
241-
</InputField>
242-
</div>
250+
<Modal onClose={() => setIsOpen(false)} visible={isOpen} containerClassName="min-[576px]:max-w-[650px]">
251+
<ModalHeader>Edit welcome message</ModalHeader>
252+
<ModalBody>
253+
<form id="welcome-message-editor" onSubmit={updateWelcomeMessage} className="space-y-4">
254+
<TextInput readOnly value="Welcome to Gitpod" className="cursor-default"></TextInput>
255+
<Textarea value={gitpodWelcomeSubheading} readOnly className="cursor-default resize-none" />
256+
<div className="w-full flex justify-center">
257+
<OrgMemberInput settings={settings} setFeaturedMemberId={setFeaturedMemberId} />
258+
</div>
259+
<InputField label="Welcome message" error={undefined} className="mb-4" labelHidden>
260+
<Textarea
261+
className="bg-pk-surface-secondary text-pk-content-primary w-full p-4 rounded-xl min-h-[150px]"
262+
value={message}
263+
placeholder="Write a welcome message to your organization members. Markdown formatting is supported."
264+
onChange={(e) => setMessage(e.target.value)}
265+
/>
266+
</InputField>
267+
</form>
268+
</ModalBody>
269+
<ModalFooter>
270+
<Button variant="secondary" onClick={() => setIsOpen(false)}>
271+
Cancel
272+
</Button>
273+
<LoadingButton type="submit" loading={isLoading} disabled={!isOwner} form="welcome-message-editor">
274+
Save
275+
</LoadingButton>
276+
</ModalFooter>
277+
</Modal>
243278
);
244279
};
245280

246281
type OrgMemberSelectProps = {
247-
avatarURL: string | undefined;
248-
setAvatarURL: (avatarURL: string | undefined) => void;
249-
disabled?: boolean;
282+
settings: OnboardingSettings_WelcomeMessage | undefined;
283+
setFeaturedMemberId: (featuredMemberId: string | undefined) => void;
250284
};
251-
const OrgMemberInput = ({ avatarURL, setAvatarURL, disabled }: OrgMemberSelectProps) => {
285+
const OrgMemberInput = ({ settings, setFeaturedMemberId }: OrgMemberSelectProps) => {
252286
const { data: members } = useListOrganizationMembers();
253287

288+
const [avatarUrl, setAvatarUrl] = useState<string | undefined>(settings?.featuredMemberResolvedAvatarUrl);
289+
254290
return (
255291
<DropdownMenu>
256292
<DropdownMenuTrigger>
257293
<div className="flex flex-col justify-center items-center gap-2">
258-
{avatarURL ? (
259-
<img src={avatarURL} alt="" className="w-16 h-16 rounded-full" />
294+
{avatarUrl ? (
295+
<img src={avatarUrl} alt="" className="w-16 h-16 rounded-full" />
260296
) : (
261297
<div className="w-16 h-16 rounded-full bg-[#EA71DE]" />
262298
)}
263-
<Button variant="secondary" size="sm">
299+
<Button variant="secondary" size="sm" type="button">
264300
Change Photo
265301
</Button>
266302
</div>
267303
</DropdownMenuTrigger>
268304
<DropdownMenuContent>
269-
<DropdownMenuItem key="disabled" onClick={() => setAvatarURL(undefined)}>
305+
<DropdownMenuItem
306+
key="disabled"
307+
onClick={() => {
308+
setFeaturedMemberId(undefined);
309+
setAvatarUrl(undefined);
310+
}}
311+
>
270312
<div className="flex items-center gap-2">
271313
<div className="w-4 h-4 rounded-full bg-pk-surface-tertiary" />
272314
Disable image
273315
</div>
274316
</DropdownMenuItem>
275317
{members?.map((member) => (
276-
<DropdownMenuItem key={member.userId} onClick={() => setAvatarURL(member.avatarUrl)}>
318+
<DropdownMenuItem
319+
key={member.userId}
320+
onClick={() => {
321+
setFeaturedMemberId(member.userId);
322+
setAvatarUrl(member.avatarUrl);
323+
}}
324+
>
277325
<div className="flex items-center gap-2">
278326
<img src={member.avatarUrl} alt={member.fullName} className="w-4 h-4 rounded-full" />
279327
{member.fullName}

components/gitpod-protocol/src/teams-projects-protocol.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ export interface OnboardingSettings {
277277
*/
278278
welcomeMessage?: {
279279
featuredMemberId?: string;
280-
message: string;
280+
message?: string;
281281
footer?: string;
282282
};
283283
}

components/public-api/gitpod/v1/organization.proto

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,16 +54,13 @@ message OnboardingSettings {
5454
bool enabled = 1;
5555

5656
// message is the welcome message for the organization
57-
string message = 2;
58-
59-
// footer is the footer message for the welcome message
60-
string footer = 3;
57+
optional string message = 2;
6158

6259
// featured_member_id is the ID of the member to show in the welcome message
63-
string featured_member_id = 4;
60+
optional string featured_member_id = 4;
6461

6562
// featured_member_resolved_avatar_url is the avatar URL that is resolved from the featured_member_id by the server. Do not set this field manually.
66-
string featured_member_resolved_avatar_url = 5;
63+
optional string featured_member_resolved_avatar_url = 5;
6764
}
6865

6966
// internal_link is the link to an internal onboarding page for the organization, possibly featuring a custom onboarding guide and other resources

0 commit comments

Comments
 (0)