Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 125 additions & 5 deletions packages/app/src/app/components/WorkspaceSetup/steps/Create.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useEffect, useRef, useState } from 'react';
import { useActions, useAppState } from 'app/overmind';
import { useActions, useAppState, useEffects } from 'app/overmind';
import { useHistory } from 'react-router-dom';
import {
Stack,
Button,
Expand Down Expand Up @@ -34,6 +35,8 @@ export const Create: React.FC<StepProps> = ({
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string>('');
const inputRef = useRef<HTMLInputElement>(null);
const [disableButton, setDisableButton] = useState(false);
const [loadingButton, setLoadingButton] = useState(false);

const urlWorkspaceId = getQueryParam('workspace');
const teamIsAlreadyCreated = !!urlWorkspaceId;
Expand Down Expand Up @@ -143,6 +146,7 @@ export const Create: React.FC<StepProps> = ({
defaultValue={teamIsAlreadyCreated ? activeTeamInfo.name : ''}
onChange={handleInput}
ref={inputRef}
disabled={disableButton || loading}
/>

{error && (
Expand All @@ -157,25 +161,141 @@ export const Create: React.FC<StepProps> = ({
</Stack>

<Button
loading={loading}
disabled={loading || !!error}
loading={loadingButton || loading}
disabled={disableButton || loading || !!error}
type="submit"
size="large"
>
Next
</Button>
</Stack>

<JoinWorkspace
onStart={() => setLoadingButton(true)}
onDidFinish={() => setLoadingButton(false)}
onDidFindWorkspace={() => setDisableButton(true)}
onRejectWorkspace={() => setDisableButton(false)}
/>

<Link
href={docsUrl('/learn/plans/workspace')}
target="_blank"
rel="noreferrer"
>
<Stack gap={1} align="center">
<Stack gap={2} align="center">
<Text>More about teams and workspaces</Text>
<Icon name="external" size={16} />
<Icon name="external" size={14} />
</Stack>
</Link>
</Stack>
</AnimatedStep>
);
};

const JoinWorkspace: React.FC<{
onDidFindWorkspace: () => void;
onRejectWorkspace: () => void;
onStart: () => void;
onDidFinish: () => void;
}> = ({ onDidFindWorkspace, onStart, onDidFinish, onRejectWorkspace }) => {
const [hidden, setHidden] = useState(true);
const effects = useEffects();
const actions = useActions();
const [eligibleWorkspace, setEligibleWorkspace] = useState<{
id: string;
name: string;
}>(null);
const history = useHistory();
const [loading, setLoading] = useState(false);

useEffect(() => {
onStart();

effects.gql.queries
.getEligibleWorkspaces({})
.then(result => {
onDidFindWorkspace();
setEligibleWorkspace(result.me.eligibleWorkspaces[0]);
})
.catch(e => {})
.finally(() => {
onDidFinish();
});
}, []);

const joinWorkspace = () => {
setLoading(true);
effects.gql.mutations
.joinEligibleWorkspace({
workspaceId: eligibleWorkspace.id,
})
.then(async () => {
await actions.setActiveTeam({ id: eligibleWorkspace.id });
await actions.dashboard.getTeams();

history.push(`/dashboard/recent?workspace=${eligibleWorkspace.id}`);
});
};

if (eligibleWorkspace && hidden) {
return (
<>
<Text css={{ textAlign: 'center' }}>or</Text>
<Stack
direction="vertical"
css={{
background: '#1A1A1A',
padding: 18,
marginLeft: -18,
marginRight: -18,
borderRadius: 4,
}}
>
<Text
margin={0}
as="h1"
color="#fff"
weight="medium"
fontFamily="everett"
size={24}
>
You have been invited to join the {eligibleWorkspace.name} workspace
</Text>
<p>
Your email matches the domain of the {eligibleWorkspace.name}{' '}
workspace.
</p>

<Stack gap={4} css={{ marginTop: 32 }}>
<Button
autoWidth
loading={loading}
type="submit"
size="large"
onClick={joinWorkspace}
css={{ flex: 1 }}
>
Join workspace
</Button>

<Button
autoWidth
type="submit"
size="large"
css={{ flex: 1 }}
variant="secondary"
onClick={() => {
setHidden(false);
onRejectWorkspace();
}}
>
Reject
</Button>
</Stack>
</Stack>
</>
);
}

return null;
};
2 changes: 1 addition & 1 deletion packages/app/src/app/components/dashboard/InputText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const StyledInput = styled.input<{ isInvalid?: boolean }>`

${props => (props.isInvalid ? 'outline: 1px solid #EB5E5E;' : '')}

&:hover {
&:hover:not(:disabled) {
box-shadow: 0 0 0 2px #e5e5e51a;
}

Expand Down
71 changes: 70 additions & 1 deletion packages/app/src/app/graphql/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,24 @@ export type RootQueryType = {
* ```
*/
recentTeamsByRepository: Array<TeamPreview>;
/** Get a sandbox */
/**
* Get a sandbox by its (short) ID
*
* Requires the current user have read authorization (or that the sandbox is public). Otherwise
* returns an error (`"unauthorized"`).
*/
sandbox: Maybe<Sandbox>;
/**
* Returns a sandbox's team ID if the current user is eligible to join that workspace
*
* This query is designed for use in 404 experience where the current user does not have access
* to the resource but *might* have access if they accept an open invitation to its workspace.
* Returns null if no such open invitation exists, or an error if no user is authenticated.
*
* For a list of all workspaces the user is eligible to join, see `query eligibleWorkspaces`.
* The ID returned by this query can be used in `mutation joinEligibleWorkspace`.
*/
sandboxEligibleWorkspace: Maybe<TeamPreview>;
/** A team from an invite token */
teamByToken: Maybe<TeamPreview>;
};
Expand Down Expand Up @@ -227,6 +243,10 @@ export type RootQueryTypeSandboxArgs = {
sandboxId: Scalars['ID'];
};

export type RootQueryTypeSandboxEligibleWorkspaceArgs = {
sandboxId: Scalars['ID'];
};

export type RootQueryTypeTeamByTokenArgs = {
inviteToken: Scalars['String'];
};
Expand Down Expand Up @@ -390,6 +410,8 @@ export type Team = {
legacy: Scalars['Boolean'];
limits: TeamLimits;
members: Array<TeamMember>;
/** Additional user-provided metadata about the workspace */
metadata: TeamMetadata;
name: Scalars['String'];
privateRegistry: Maybe<PrivateRegistry>;
/**
Expand Down Expand Up @@ -535,6 +557,13 @@ export type TeamMember = {
username: Scalars['String'];
};

/** Additional user-provided metadata about a workspace */
export type TeamMetadata = {
__typename?: 'TeamMetadata';
/** Use-cases for the workspace provided during creation */
useCases: Array<Scalars['String']>;
};

/** A private package registry */
export type PrivateRegistry = {
__typename?: 'PrivateRegistry';
Expand Down Expand Up @@ -2071,6 +2100,8 @@ export type RootMutationType = {
setTeamDescription: Team;
/** Set user-editable limits for the workspace */
setTeamLimits: Scalars['String'];
/** Set user-provided metadata about the workspace */
setTeamMetadata: Team;
/** Set minimum privacy level for workspace */
setTeamMinimumPrivacy: WorkspaceSandboxSettings;
/** Set the name of the team */
Expand Down Expand Up @@ -2595,6 +2626,11 @@ export type RootMutationTypeSetTeamLimitsArgs = {
teamId: Scalars['UUID4'];
};

export type RootMutationTypeSetTeamMetadataArgs = {
metadata: TeamMetadataInput;
teamId: Scalars['UUID4'];
};

export type RootMutationTypeSetTeamMinimumPrivacyArgs = {
minimumPrivacy: Scalars['Int'];
teamId: Scalars['UUID4'];
Expand Down Expand Up @@ -2812,6 +2848,12 @@ export type BillingDetails = {
date: Scalars['String'];
};

/** Additional user-provided metadata about a workspace */
export type TeamMetadataInput = {
/** Use-cases for the workspace */
useCases: Array<Scalars['String']>;
};

export type RootSubscriptionType = {
__typename?: 'RootSubscriptionType';
/** Receive updates for events related to the specified branch. */
Expand Down Expand Up @@ -5603,6 +5645,15 @@ export type UpdateProjectVmTierMutation = {
};
};

export type JoinEligibleWorkspaceMutationVariables = Exact<{
workspaceId: Scalars['ID'];
}>;

export type JoinEligibleWorkspaceMutation = {
__typename?: 'RootMutationType';
joinEligibleWorkspace: { __typename?: 'Team'; id: any };
};

export type RecentlyDeletedTeamSandboxesQueryVariables = Exact<{
teamId: Scalars['UUID4'];
}>;
Expand Down Expand Up @@ -6549,6 +6600,24 @@ export type GetSandboxWithTemplateQuery = {
} | null;
};

export type GetEligibleWorkspacesQueryVariables = Exact<{
[key: string]: never;
}>;

export type GetEligibleWorkspacesQuery = {
__typename?: 'RootQueryType';
me: {
__typename?: 'CurrentUser';
eligibleWorkspaces: Array<{
__typename?: 'TeamPreview';
id: any;
avatarUrl: string | null;
name: string;
shortid: string;
}>;
} | null;
};

export type RecentNotificationFragment = {
__typename?: 'Notification';
id: any;
Expand Down
13 changes: 13 additions & 0 deletions packages/app/src/app/overmind/effects/gql/dashboard/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ import {
UpdateProjectVmTierMutation,
UpdateUsageSubscriptionMutationVariables,
UpdateUsageSubscriptionMutation,
JoinEligibleWorkspaceMutation,
JoinEligibleWorkspaceMutationVariables,
} from 'app/graphql/types';
import { gql, Query } from 'overmind-graphql';

Expand Down Expand Up @@ -476,3 +478,14 @@ export const updateProjectVmTier: Query<
}
}
`;

export const joinEligibleWorkspace: Query<
JoinEligibleWorkspaceMutation,
JoinEligibleWorkspaceMutationVariables
> = gql`
mutation JoinEligibleWorkspace($workspaceId: ID!) {
joinEligibleWorkspace(workspaceId: $workspaceId) {
id
}
}
`;
18 changes: 18 additions & 0 deletions packages/app/src/app/overmind/effects/gql/dashboard/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ import {
GetFullGitHubOrganizationReposQueryVariables,
GetSandboxWithTemplateQuery,
GetSandboxWithTemplateQueryVariables,
GetEligibleWorkspacesQuery,
GetEligibleWorkspacesQueryVariables,
} from 'app/graphql/types';
import { gql, Query } from 'overmind-graphql';

Expand Down Expand Up @@ -399,3 +401,19 @@ export const getSandboxWithTemplate: Query<
}
}
`;

export const getEligibleWorkspaces: Query<
GetEligibleWorkspacesQuery,
GetEligibleWorkspacesQueryVariables
> = gql`
query GetEligibleWorkspaces {
me {
eligibleWorkspaces {
id
avatarUrl
name
shortid
}
}
}
`;