Skip to content

Commit 6d13c65

Browse files
authored
feat(dashboard): remove from codesandbox (#7012)
* update base api to support beta * fixup * feat: api calls * wip * remove branches * feat(components): disabled MenuItem * feat: disabled state on branch and repository menu * fix typecheck * handle branch removal in different pages * typecheck * improvements * fixup
1 parent 780cfb5 commit 6d13c65

File tree

16 files changed

+237
-64
lines changed

16 files changed

+237
-64
lines changed

packages/app/src/app/overmind/effects/api/apiFactory.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22
import axios, { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios';
33
import { camelizeKeys, decamelizeKeys } from 'humps';
44

5-
export const API_ROOT = '/api/v1';
5+
const API_ROOT = '/api';
6+
7+
// If the path starts with `/beta`, do not append
8+
// `/v1` to the api root url.
9+
const getBaseApi = (path: string) =>
10+
path.startsWith('/beta') ? API_ROOT : `${API_ROOT}/v1`;
611

712
export type ApiError = AxiosError<
813
{ errors: string[] } | { error: string } | any
@@ -41,36 +46,36 @@ export default (config: ApiConfig) => {
4146
const api: Api = {
4247
get(path, params, options) {
4348
return axios
44-
.get(API_ROOT + path, {
49+
.get(getBaseApi(path) + path, {
4550
params,
4651
headers: createHeaders(config.provideJwtToken),
4752
})
4853
.then(response => handleResponse(response, options));
4954
},
5055
post(path, body, options) {
5156
return axios
52-
.post(API_ROOT + path, decamelizeKeys(body), {
57+
.post(getBaseApi(path) + path, decamelizeKeys(body), {
5358
headers: createHeaders(config.provideJwtToken),
5459
})
5560
.then(response => handleResponse(response, options));
5661
},
5762
patch(path, body, options) {
5863
return axios
59-
.patch(API_ROOT + path, decamelizeKeys(body), {
64+
.patch(getBaseApi(path) + path, decamelizeKeys(body), {
6065
headers: createHeaders(config.provideJwtToken),
6166
})
6267
.then(response => handleResponse(response, options));
6368
},
6469
put(path, body, options) {
6570
return axios
66-
.put(API_ROOT + path, decamelizeKeys(body), {
71+
.put(getBaseApi(path) + path, decamelizeKeys(body), {
6772
headers: createHeaders(config.provideJwtToken),
6873
})
6974
.then(response => handleResponse(response, options));
7075
},
7176
delete(path, params, options) {
7277
return axios
73-
.delete(API_ROOT + path, {
78+
.delete(getBaseApi(path) + path, {
7479
params,
7580
headers: createHeaders(config.provideJwtToken),
7681
})
@@ -80,7 +85,7 @@ export default (config: ApiConfig) => {
8085
return axios
8186
.request(
8287
Object.assign(requestConfig, {
83-
url: API_ROOT + requestConfig.url,
88+
url: getBaseApi(requestConfig.url ?? '') + requestConfig.url,
8489
data: requestConfig.data ? camelizeKeys(requestConfig.data) : null,
8590
headers: createHeaders(config.provideJwtToken),
8691
})

packages/app/src/app/overmind/effects/api/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -691,4 +691,10 @@ export default {
691691
`/teams/${teamId}/customer_portal?return_path=${return_path}`
692692
);
693693
},
694+
removeBranchFromRepository(owner: string, repo: string, branch: string) {
695+
return api.delete(`/beta/sandboxes/github/${owner}/${repo}/${branch}`);
696+
},
697+
removeRepositoryFromTeam(owner: string, repo: string, teamId: string) {
698+
return api.delete(`/beta/repos/link/github/${owner}/${repo}/${teamId}`);
699+
},
694700
};

packages/app/src/app/overmind/namespaces/dashboard/actions.ts

Lines changed: 113 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
DeleteNpmRegistryMutationVariables,
1414
} from 'app/graphql/types';
1515
import { getDecoratedCollection, sortByNameAscending } from './utils';
16-
import { OrderBy, sandboxesTypes } from './types';
16+
import { OrderBy, PageTypes, sandboxesTypes } from './types';
1717
import * as internalActions from './internalActions';
1818

1919
export const internal = internalActions;
@@ -1968,25 +1968,38 @@ export const getContributionBranches = async ({ state, effects }: Context) => {
19681968
}
19691969
};
19701970

1971-
export const getRepositoriesByTeam = async ({ state, effects }: Context) => {
1971+
type RepositoriesActionOptions = {
1972+
bypassLoading: boolean;
1973+
};
1974+
export const getRepositoriesByTeam = async (
1975+
{ state, effects }: Context,
1976+
options?: RepositoriesActionOptions
1977+
) => {
19721978
const { activeTeam, dashboard } = state;
1973-
try {
1974-
dashboard.repositories = null;
1979+
const { bypassLoading = false } = options ?? {};
19751980

1976-
// First fetch data without syncing with GitHub
1977-
// to decrease waiting time.
1978-
const unsyncedRepositoriesData = await effects.gql.queries.getRepositoriesByTeam(
1979-
{
1980-
teamId: activeTeam,
1981-
syncData: false,
1981+
// If we should bypass the loading state, we don't need to make two
1982+
// queries (synced and unsynced) since the loading time won't be
1983+
// perceived by the user.
1984+
try {
1985+
if (!bypassLoading) {
1986+
dashboard.repositories = null;
1987+
1988+
// First fetch data without syncing with GitHub
1989+
// to decrease waiting time.
1990+
const unsyncedRepositoriesData = await effects.gql.queries.getRepositoriesByTeam(
1991+
{
1992+
teamId: activeTeam,
1993+
syncData: false,
1994+
}
1995+
);
1996+
const unsyncedRepositories = unsyncedRepositoriesData?.me?.team?.projects;
1997+
if (!unsyncedRepositories) {
1998+
return;
19821999
}
1983-
);
1984-
const unsyncedRepositories = unsyncedRepositoriesData?.me?.team?.projects;
1985-
if (!unsyncedRepositories) {
1986-
return;
1987-
}
19882000

1989-
dashboard.repositories = unsyncedRepositories.sort(sortByNameAscending);
2001+
dashboard.repositories = unsyncedRepositories.sort(sortByNameAscending);
2002+
}
19902003

19912004
// Then fetch data synced with GitHub to make sure
19922005
// what we show is up-to-date.
@@ -2080,3 +2093,87 @@ export const unstarRepo = (
20802093
dashboard.starredRepos
20812094
);
20822095
};
2096+
2097+
type BranchToRemove = {
2098+
owner: string;
2099+
repoName: string;
2100+
name: string;
2101+
id: string;
2102+
page: PageTypes;
2103+
};
2104+
export const removeBranchFromRepository = async (
2105+
context: Context,
2106+
branch: BranchToRemove
2107+
) => {
2108+
const { actions, effects, state } = context;
2109+
const { id, owner, repoName, name, page } = branch;
2110+
2111+
state.dashboard.removingBranch = { id };
2112+
2113+
try {
2114+
await effects.api.removeBranchFromRepository(owner, repoName, name);
2115+
2116+
if (page === 'repositories') {
2117+
const repository = state.dashboard.repositories?.find(
2118+
r => r.repository.owner === owner && r.repository.name === repoName
2119+
);
2120+
2121+
if (repository) {
2122+
// Manually remove the data from the state.
2123+
repository.branches = repository.branches.filter(b => b.id !== id);
2124+
}
2125+
2126+
// Then sync in the background.
2127+
actions.dashboard.getRepositoriesByTeam({ bypassLoading: true });
2128+
}
2129+
2130+
if (page === 'recent') {
2131+
// First, manually remove the data from the state.
2132+
state.dashboard.sandboxes.RECENT_BRANCHES =
2133+
state.dashboard.sandboxes.RECENT_BRANCHES?.filter(b => b.id !== id) ??
2134+
[];
2135+
2136+
// Then sync in the background.
2137+
actions.dashboard.getStartPageSandboxes();
2138+
}
2139+
} catch (error) {
2140+
effects.notificationToast.error(
2141+
`Failed to remove branch ${name} from ${owner}/${repoName}`
2142+
);
2143+
} finally {
2144+
state.dashboard.removingBranch = null;
2145+
}
2146+
};
2147+
2148+
type ProjectToRemove = {
2149+
owner: string;
2150+
name: string;
2151+
teamId: string;
2152+
};
2153+
export const removeRepositoryFromTeam = async (
2154+
context: Context,
2155+
project: ProjectToRemove
2156+
) => {
2157+
const { actions, state, effects } = context;
2158+
const { owner, name, teamId } = project;
2159+
2160+
state.dashboard.removingRepository = { owner, name };
2161+
2162+
try {
2163+
await effects.api.removeRepositoryFromTeam(owner, name, teamId);
2164+
2165+
// First, manually remove the data from the state.
2166+
state.dashboard.repositories =
2167+
state.dashboard.repositories?.filter(
2168+
r => r.repository.owner !== owner || r.repository.name !== name
2169+
) ?? [];
2170+
// Then sync in the background.
2171+
actions.dashboard.getRepositoriesByTeam({ bypassLoading: true });
2172+
} catch (error) {
2173+
effects.notificationToast.error(
2174+
`Failed to remove project ${owner}/${name}`
2175+
);
2176+
} finally {
2177+
state.dashboard.removingRepository = null;
2178+
}
2179+
};

packages/app/src/app/overmind/namespaces/dashboard/state.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,13 @@ export type State = {
6969
/** v2 repositories (formerly projects) */
7070
repositories: Repository[] | null;
7171
starredRepos: Array<{ owner: string; name: string }>;
72+
/**
73+
* Use these variables to track if items are being removed. This way
74+
* we don't have to manipulate the state directly to let the components
75+
* know what to render.
76+
*/
77+
removingRepository: { owner: string; name: string } | null;
78+
removingBranch: { id: string } | null;
7279
};
7380

7481
export const DEFAULT_DASHBOARD_SANDBOXES: DashboardSandboxStructure = {
@@ -184,4 +191,6 @@ export const state: State = {
184191
contributions: null,
185192
repositories: null,
186193
starredRepos: [],
194+
removingRepository: null,
195+
removingBranch: null,
187196
};

packages/app/src/app/pages/Dashboard/Components/Branch/BranchCard.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ import { css } from '@styled-system/css';
1010
import { BranchProps } from './types';
1111

1212
export const BranchCard: React.FC<BranchProps> = ({
13-
onContextMenu,
1413
branch,
1514
branchUrl,
15+
isBeingRemoved,
1616
selected,
17+
onContextMenu,
1718
...props
1819
}) => {
1920
const { name: branchName, project, contribution } = branch;
@@ -33,7 +34,9 @@ export const BranchCard: React.FC<BranchProps> = ({
3334
border: '1px solid',
3435
borderColor: selected ? 'focusBorder' : 'transparent',
3536
backgroundColor: selected ? 'card.backgroundHover' : 'card.background',
36-
transition: 'background ease-in-out',
37+
opacity: isBeingRemoved ? 0.5 : 1,
38+
pointerEvents: isBeingRemoved ? 'none' : 'all',
39+
transition: 'background ease-in-out, opacity ease-in-out',
3740
transitionDuration: theme => theme.speeds[2],
3841
textDecoration: 'none',
3942
outline: 'none',
@@ -45,7 +48,7 @@ export const BranchCard: React.FC<BranchProps> = ({
4548
},
4649
})}
4750
direction="vertical"
48-
href={branchUrl}
51+
href={isBeingRemoved ? undefined : branchUrl}
4952
onContextMenu={onContextMenu}
5053
{...props}
5154
>

packages/app/src/app/pages/Dashboard/Components/Branch/BranchListItem.tsx

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import { BranchProps } from './types';
1616
export const BranchListItem = ({
1717
branch,
1818
branchUrl,
19+
selected,
20+
isBeingRemoved,
1921
onContextMenu,
2022
}: BranchProps) => {
2123
const { name: branchName, project, contribution } = branch;
@@ -29,10 +31,18 @@ export const BranchListItem = ({
2931
borderBottom: '1px solid',
3032
borderBottomColor: 'grays.600',
3133
overflow: 'hidden',
32-
backgroundColor: 'transparent',
33-
color: 'inherit',
34+
backgroundColor:
35+
selected && !isBeingRemoved ? 'purpleOpaque' : 'transparent',
36+
color: selected && !isBeingRemoved ? 'white' : 'inherit',
37+
transition: 'background ease-in-out, opacity ease-in-out',
38+
opacity: isBeingRemoved ? 0.5 : 1,
39+
pointerEvents: isBeingRemoved ? 'none' : 'all',
3440
':hover, :focus, :focus-within': {
35-
backgroundColor: 'list.hoverBackground',
41+
cursor: 'default',
42+
backgroundColor:
43+
selected && !isBeingRemoved
44+
? 'purpleOpaque'
45+
: 'list.hoverBackground',
3646
},
3747
})}
3848
>
@@ -45,12 +55,12 @@ export const BranchListItem = ({
4555
width: '100%',
4656
textDecoration: 'none',
4757
}}
48-
href={branchUrl}
58+
href={isBeingRemoved ? undefined : branchUrl}
4959
onContextMenu={onContextMenu}
5060
>
5161
<Grid css={{ width: 'calc(100% - 26px - 8px)' }}>
5262
<Column
53-
span={[12, 5, 5]}
63+
span={[12, 5, 5]}
5464
css={{
5565
display: 'block',
5666
overflow: 'hidden',
@@ -60,7 +70,12 @@ export const BranchListItem = ({
6070
>
6171
<Stack gap={4} align="center" marginLeft={2}>
6272
{contribution ? (
63-
<Icon color="#EDFFA5" name="contribution" size={16} width="32px" />
73+
<Icon
74+
color="#EDFFA5"
75+
name="contribution"
76+
size={16}
77+
width="32px"
78+
/>
6479
) : (
6580
<Icon name="branch" color="#999" size={16} width="32px" />
6681
)}

packages/app/src/app/pages/Dashboard/Components/Branch/index.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ type BranchProps = DashboardBranch & {
2020
};
2121
export const Branch: React.FC<BranchProps> = ({ branch, page }) => {
2222
const {
23-
dashboard: { viewMode },
23+
dashboard: { removingBranch, viewMode },
2424
} = useAppState();
2525
const { selectedIds, onRightClick, onMenuEvent } = useSelection();
2626
const { name, project } = branch;
@@ -38,14 +38,13 @@ export const Branch: React.FC<BranchProps> = ({ branch, page }) => {
3838
trackImprovedDashboardEvent(MAP_BRANCH_EVENT_TO_PAGE_TYPE[page]);
3939
};
4040

41-
const selected = selectedIds.includes(branch.id);
42-
4341
const props = {
4442
branch,
4543
branchUrl,
44+
selected: selectedIds.includes(branch.id),
45+
isBeingRemoved: removingBranch?.id === branch.id,
4646
onContextMenu: handleContextMenu,
4747
onClick: handleClick,
48-
selected,
4948
/**
5049
* If we ever need selection for branch entries, `data-selection-id` must be set
5150
* 'data-selection-id': branch.id,

packages/app/src/app/pages/Dashboard/Components/Branch/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { BranchFragment as Branch } from 'app/graphql/types';
33
export type BranchProps = {
44
branch: Branch;
55
branchUrl: string;
6+
isBeingRemoved: boolean;
7+
selected: boolean;
68
onContextMenu: (evt: React.MouseEvent) => void;
79
onClick: (evt: React.MouseEvent) => void;
8-
selected: boolean;
910
};

0 commit comments

Comments
 (0)