@@ -54,11 +54,13 @@ import Menu from "../menu/Menu";
5454import { useOrgSettingsQuery } from "../data/organizations/org-settings-query" ;
5555import { useAllowedWorkspaceEditorsMemo } from "../data/ide-options/ide-options-query" ;
5656import { isGitpodIo } from "../utils" ;
57- import { useListConfigurations } from "../data/configurations/configuration-queries" ;
57+ import { useConfiguration , useListConfigurations } from "../data/configurations/configuration-queries" ;
5858import { flattenPagedConfigurations } from "../data/git-providers/unified-repositories-search-query" ;
5959import { Configuration } from "@gitpod/public-api/lib/gitpod/v1/configuration_pb" ;
6060import { useMemberRole } from "../data/organizations/members-query" ;
6161import { OrganizationPermission } from "@gitpod/public-api/lib/gitpod/v1/organization_pb" ;
62+ import { createParser , useQueryState } from "nuqs" ;
63+ import { validate as validateUUID } from "uuid" ;
6264import { useInstallationConfiguration } from "../data/installation/installation-config-query" ;
6365
6466type NextLoadOption = "searchParams" | "autoStart" | "allDone" ;
@@ -76,10 +78,9 @@ export function CreateWorkspacePage() {
7678 const [ autostart , setAutostart ] = useState < boolean | undefined > ( props . autostart ) ;
7779 const createWorkspaceMutation = useCreateWorkspaceMutation ( ) ;
7880
79- // Currently this tracks if the user has selected a project from the dropdown
80- // Need to make sure we initialize this to a project if the url hash value maps to a project's repo url
81- // Will need to handle multiple projects w/ same repo url
82- const [ selectedProjectID , setSelectedProjectID ] = useState < string | undefined > ( undefined ) ;
81+ // This stores the configurationId corresponding to a context URL
82+ // it can either be resolved from the context URL [lossy context URL -> configuration conversion] or it can itself resolve the context URL when specified [lossless configurationId -> context URL conversion]
83+ const [ selectedProjectID , setSelectedProjectID ] = useQueryState ( "configurationId" , parseAsUUIDv4 ) ;
8384
8485 const defaultLatestIde =
8586 props . ideSettings ?. useLatestVersion !== undefined
@@ -91,12 +92,12 @@ export function CreateWorkspacePage() {
9192 // Note: it has data fetching and UI rendering race between the updating of `selectedProjectId` and `selectedIde`
9293 // We have to stored the using repositoryId locally so that we can know selectedIde is updated because if which repo
9394 // so that it doesn't show ide error messages in middle state
94- const [ defaultIdeSource , setDefaultIdeSource ] = useState < string | undefined > ( selectedProjectID ) ;
95+ const [ defaultIdeSource , setDefaultIdeSource ] = useState < string | undefined > ( selectedProjectID ?? undefined ) ;
9596 const {
9697 computedDefault : computedDefaultEditor ,
9798 usingConfigurationId,
9899 availableOptions : availableEditorOptions ,
99- } = useAllowedWorkspaceEditorsMemo ( selectedProjectID , {
100+ } = useAllowedWorkspaceEditorsMemo ( selectedProjectID ?? undefined , {
100101 userDefault : user ?. editorSettings ?. name ,
101102 filterOutDisabled : true ,
102103 } ) ;
@@ -106,7 +107,7 @@ export function CreateWorkspacePage() {
106107 computedDefaultClass,
107108 data : allowedWorkspaceClasses ,
108109 isLoading : isLoadingWorkspaceClasses ,
109- } = useAllowedWorkspaceClassesMemo ( selectedProjectID ) ;
110+ } = useAllowedWorkspaceClassesMemo ( selectedProjectID ?? undefined ) ;
110111 const defaultWorkspaceClass = props . workspaceClass ?? computedDefaultClass ;
111112 const showExamples = props . showExamples ?? false ;
112113 const { data : orgSettings } = useOrgSettingsQuery ( ) ;
@@ -123,9 +124,13 @@ export function CreateWorkspacePage() {
123124 const needsGitAuthorization = useNeedsGitAuthorization ( ) ;
124125
125126 useEffect ( ( ) => {
126- setContextURL ( StartWorkspaceOptions . parseContextUrl ( location . hash ) ) ;
127- setSelectedProjectID ( undefined ) ;
128- setNextLoadOption ( "searchParams" ) ;
127+ // if we have a context URL in the hash, we can proceed with resolving info based on it.
128+ if ( location . hash ) {
129+ setContextURL ( StartWorkspaceOptions . parseContextUrl ( location . hash ) ) ;
130+ setNextLoadOption ( "searchParams" ) ;
131+ }
132+
133+ // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to depend on setSelectedProjectID
129134 } , [ location . hash ] ) ;
130135
131136 const cloneURL = workspaceContext . data ?. cloneUrl ;
@@ -189,24 +194,43 @@ export function CreateWorkspacePage() {
189194 setUser ,
190195 ] ) ;
191196
192- // see if we have a matching configuration based on context url and configuration's repo url
193- const configuration = useMemo ( ( ) => {
197+ // see if we have a matching configuration based on configuration id / context url and configuration's repo url
198+ const configurationFromURL = useMemo ( ( ) => {
194199 if ( ! workspaceContext . data || configurations . length === 0 ) {
195200 return undefined ;
196201 }
197202 if ( ! cloneURL ) {
198203 return ;
199204 }
200- // TODO: Account for multiple configurations w/ the same cloneUrl
205+
201206 return configurations . find ( ( p ) => p . cloneUrl === cloneURL ) ;
202207 } , [ workspaceContext . data , configurations , cloneURL ] ) ;
208+ const { data : configurationFromId , isLoading : isLoadingConfigurationFromId } = useConfiguration (
209+ selectedProjectID ?? undefined ,
210+ ) ;
211+ const configuration = useMemo (
212+ ( ) => {
213+ if ( configurationFromId && ( ! cloneURL || configurationFromId . cloneUrl === cloneURL ) ) {
214+ return configurationFromId ;
215+ }
216+ if ( configurationFromURL && ! isLoadingConfigurationFromId ) {
217+ setSelectedProjectID ( configurationFromURL . id ) ; // idk about this
218+
219+ return configurationFromURL ;
220+ }
203221
204- // Handle the case where the context url in the hash matches a project and we don't have that project selected yet
222+ return undefined ;
223+ } ,
224+ // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to depend on setSelectedProjectID
225+ [ configurationFromId , configurationFromURL , selectedProjectID ] ,
226+ ) ;
227+
228+ // when the user comes in with a configuration id in the search params, we want to set the context URL to the configuration's clone URL
205229 useEffect ( ( ) => {
206- if ( configuration && ! selectedProjectID ) {
207- setSelectedProjectID ( configuration . id ) ;
230+ if ( configuration && ! contextURL ) {
231+ setContextURL ( configuration . cloneUrl ) ;
208232 }
209- } , [ configuration , selectedProjectID ] ) ;
233+ } , [ configuration , contextURL ] ) ;
210234
211235 // In addition to updating state, we want to update the url hash as well
212236 // This allows the contextURL to persist if user changes orgs, or copies/shares url
@@ -222,6 +246,8 @@ export function CreateWorkspacePage() {
222246 // reset load options
223247 setNextLoadOption ( "searchParams" ) ;
224248 } ,
249+
250+ // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to depend on setSelectedProjectID
225251 [ history ] ,
226252 ) ;
227253
@@ -291,7 +317,7 @@ export function CreateWorkspacePage() {
291317 opts . metadata = { } ;
292318 }
293319 opts . metadata . organizationId = organizationId ;
294- opts . metadata . configurationId = selectedProjectID ;
320+ opts . metadata . configurationId = selectedProjectID ?? undefined ;
295321
296322 const contextUrlSource : PartialMessage < CreateAndStartWorkspaceRequest_ContextURL > =
297323 opts . source ?. case === "contextUrl" ? opts . source ?. value ?? { } : { } ;
@@ -563,8 +589,8 @@ export function CreateWorkspacePage() {
563589 < RepositoryFinder
564590 onChange = { handleContextURLChange }
565591 selectedContextURL = { contextURL }
566- selectedConfigurationId = { selectedProjectID }
567- expanded = { ! contextURL }
592+ selectedConfigurationId = { selectedProjectID ?? undefined }
593+ expanded = { ! contextURL && ! selectedProjectID }
568594 onlyConfigurations = {
569595 orgSettings ?. roleRestrictions . some (
570596 ( roleRestriction ) =>
@@ -583,12 +609,14 @@ export function CreateWorkspacePage() {
583609 < SelectIDEComponent
584610 onSelectionChange = { onSelectEditorChange }
585611 availableOptions = {
586- defaultIdeSource === selectedProjectID ? availableEditorOptions : undefined
612+ defaultIdeSource === ( selectedProjectID ?? undefined )
613+ ? availableEditorOptions
614+ : undefined
587615 }
588616 setError = { setErrorIde }
589617 setWarning = { setWarningIde }
590618 selectedIdeOption = { selectedIde }
591- selectedConfigurationId = { selectedProjectID }
619+ selectedConfigurationId = { selectedProjectID ?? undefined }
592620 pinnedEditorVersions = {
593621 orgSettings ?. pinnedEditorVersions &&
594622 new Map < string , string > ( Object . entries ( orgSettings . pinnedEditorVersions ) )
@@ -602,7 +630,7 @@ export function CreateWorkspacePage() {
602630
603631 < InputField error = { errorWsClass } >
604632 < SelectWorkspaceClassComponent
605- selectedConfigurationId = { selectedProjectID }
633+ selectedConfigurationId = { selectedProjectID ?? undefined }
606634 onSelectionChange = { setSelectedWsClass }
607635 setError = { setErrorWsClass }
608636 selectedWorkspaceClass = { selectedWsClass }
@@ -886,3 +914,15 @@ export function LimitReachedModal(p: { children: ReactNode }) {
886914 </ Modal >
887915 ) ;
888916}
917+
918+ export const parseAsUUIDv4 = createParser ( {
919+ parse ( queryValue ) {
920+ if ( validateUUID ( queryValue ) ) {
921+ return queryValue ;
922+ }
923+ return null ;
924+ } ,
925+ serialize ( value ) {
926+ return value ;
927+ } ,
928+ } ) ;
0 commit comments