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" ;
811import { FormEvent , useCallback , useEffect , useState } from "react" ;
912import { Heading2 , Heading3 , Subheading } from "../components/typography/headings" ;
1013import { useIsOwner , useListOrganizationMembers } from "../data/organizations/members-query" ;
@@ -26,10 +29,6 @@ import { Button } from "@podkit/buttons/Button";
2629import { SwitchInputField } from "@podkit/switch/Switch" ;
2730import { 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- ` ;
3332const gitpodWelcomeSubheading = `Gitpod’s sandboxed, ephemeral development environments enable you to use your existing tools without worrying about vulnerabilities impacting their local machines.` ;
3433
3534export 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
179179type WelcomeMessagePreviewProps = {
180- welcomeMessage : string ;
181- avatarURL : string | undefined ;
180+ welcomeMessage : string | undefined ;
182181 disabled ?: boolean ;
183182 setWelcomeMessageEditorOpen : ( open : boolean ) => void ;
184183} ;
185184const 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
212215type 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} ;
219226const 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
246281type 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 }
0 commit comments