Skip to content

Commit 555a24d

Browse files
committed
feat: add error message when project not aligned to spaceId
1 parent d362c3c commit 555a24d

File tree

15 files changed

+148
-17
lines changed

15 files changed

+148
-17
lines changed

apps/vercel/frontend/src/clients/Vercel.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,12 @@ import {
66
ServerlessFunction,
77
AccessToken,
88
Deployment,
9+
ProjectEnv,
10+
Project,
911
} from '@customTypes/configPage';
1012

13+
const CONTENTFUL_SPACE_ID = 'CONTENTFUL_SPACE_ID';
14+
1115
interface GetToken {
1216
ok: boolean;
1317
data: AccessToken;
@@ -18,6 +22,12 @@ interface VercelAPIClient {
1822
getToken: () => Promise<Response>;
1923
listProjects: (teamId?: string) => Promise<ListProjectsResponse>;
2024
listApiPaths: (projectId: string, teamId?: string) => Promise<ApiPath[]>;
25+
validateProjectContentfulSpaceId: (
26+
currentSpaceId: string,
27+
projectId: string,
28+
teamId: string,
29+
envs: ProjectEnv[]
30+
) => Promise<boolean>;
2131
}
2232

2333
export default class VercelClient implements VercelAPIClient {
@@ -146,6 +156,54 @@ export default class VercelClient implements VercelAPIClient {
146156
return data.deployments[0];
147157
}
148158

159+
async validateProjectContentfulSpaceId(
160+
currentSpaceId: string,
161+
projectId: string,
162+
teamId: string
163+
) {
164+
try {
165+
const data = await this.listProject(projectId, teamId);
166+
const envs = data.env;
167+
const contentfulSpaceIdEnv = envs.find((env) => env.key === CONTENTFUL_SPACE_ID);
168+
if (contentfulSpaceIdEnv) {
169+
const res = await fetch(
170+
`${this.baseEndpoint}/v1/projects/${projectId}/env/${contentfulSpaceIdEnv.id}`,
171+
{
172+
headers: this.buildHeaders(),
173+
method: 'GET',
174+
}
175+
);
176+
177+
const data = await res.json();
178+
return data.value === currentSpaceId;
179+
}
180+
} catch (e) {
181+
// let user continue if there is an validating env value
182+
console.error(e);
183+
}
184+
185+
return false;
186+
}
187+
188+
private async listProject(projectId: string, teamId: string): Promise<Project> {
189+
let projectData: Response;
190+
try {
191+
projectData = await fetch(
192+
`${this.baseEndpoint}/v9/projects/${projectId}?${this.buildTeamIdQueryParam(teamId)}`,
193+
{
194+
headers: this.buildHeaders(),
195+
method: 'GET',
196+
}
197+
);
198+
} catch (e) {
199+
console.error(e);
200+
throw new Error('Cannot fetch project data.');
201+
}
202+
203+
const data = await projectData.json();
204+
return data;
205+
}
206+
149207
private filterServerlessFunctions(data: ServerlessFunction[]) {
150208
return data.filter(
151209
(file: ServerlessFunction) => file.type === 'Page' && file.path.includes('api')

apps/vercel/frontend/src/components/common/SelectSection/SelectSection.spec.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,11 @@ describe('SelectSection', () => {
7272

7373
it('renders error message when selected option no longer exists', () => {
7474
const ID = singleSelectionSections.PROJECT_SELECTION_SECTION;
75-
const projectSelectionError = { projectNotFound: true, cannotFetchProjects: false };
75+
const projectSelectionError = {
76+
projectNotFound: true,
77+
cannotFetchProjects: false,
78+
invalidSpaceId: false,
79+
};
7680
const mockHandleInvalidSelectionError = vi.fn(() => {});
7781
const { unmount } = render(
7882
<SelectSection

apps/vercel/frontend/src/components/common/SelectSection/SelectSection.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export const SelectSection = ({
3434
}: Props) => {
3535
const { placeholder, label, emptyMessage, helpText: helpTextCopy } = copies.configPage[section];
3636
const { isLoading } = useContext(ConfigPageContext);
37-
const { isError, message } = useError({ error });
37+
const { message } = useError({ error });
3838

3939
useEffect(() => {
4040
if (!isLoading) {
@@ -51,7 +51,7 @@ export const SelectSection = ({
5151
return (
5252
<FormControl marginBottom="spacingS" id={id} isRequired={true}>
5353
<Select
54-
value={isError ? '' : selectedOption}
54+
value={selectedOption}
5555
onChange={handleChange}
5656
placeholder={placeholder}
5757
emptyMessage={emptyMessage}

apps/vercel/frontend/src/components/config-screen/ApiPathSelectionSection/ApiPathSelectionSection.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export const ApiPathSelectionSection = ({ paths }: Props) => {
2222
useContext(ConfigPageContext);
2323
const { invalidDeploymentData, cannotFetchApiPaths } = errors.apiPathSelection;
2424

25+
const selectedOption = errors.apiPathSelection ? '' : parameters.selectedApiPath;
26+
2527
const handleChange = (event: ChangeEvent<HTMLSelectElement>) => {
2628
dispatchParameters({
2729
type: parametersActions.APPLY_API_PATH,
@@ -47,7 +49,7 @@ export const ApiPathSelectionSection = ({ paths }: Props) => {
4749

4850
return (
4951
<SelectSection
50-
selectedOption={parameters.selectedApiPath}
52+
selectedOption={selectedOption}
5153
options={paths}
5254
handleInvalidSelectionError={handleInvalidSelectionError}
5355
handleChange={handleChange}

apps/vercel/frontend/src/components/config-screen/ProjectSelectionSection/ProjectSelectionSection.tsx

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,31 @@ import {
1010
singleSelectionSections,
1111
} from '@constants/enums';
1212
import { ConfigPageContext } from '@contexts/ConfigPageProvider';
13+
import { useFetchData } from '@hooks/useFetchData/useFetchData';
1314

1415
interface Props {
1516
projects: Project[];
1617
}
1718

1819
export const ProjectSelectionSection = ({ projects }: Props) => {
1920
const sectionId = singleSelectionSections.PROJECT_SELECTION_SECTION;
20-
const { parameters, errors, dispatchErrors, dispatchParameters, handleAppConfigurationChange } =
21-
useContext(ConfigPageContext);
21+
const {
22+
parameters,
23+
errors,
24+
dispatchErrors,
25+
dispatchParameters,
26+
handleAppConfigurationChange,
27+
sdk,
28+
vercelClient,
29+
} = useContext(ConfigPageContext);
30+
const { validateProjectEnv } = useFetchData({
31+
dispatchErrors,
32+
dispatchParameters,
33+
vercelClient,
34+
teamId: parameters.teamId,
35+
});
2236

23-
const handleChange = (event: ChangeEvent<HTMLSelectElement>) => {
37+
const handleChange = async (event: ChangeEvent<HTMLSelectElement>) => {
2438
// indicate app config change when project has been re-selected
2539
handleAppConfigurationChange();
2640

@@ -35,9 +49,8 @@ export const ProjectSelectionSection = ({ projects }: Props) => {
3549
payload: event.target.value,
3650
});
3751

38-
dispatchErrors({
39-
type: errorsActions.RESET_PROJECT_SELECTION_ERRORS,
40-
});
52+
const currentSpaceId = sdk.ids.space;
53+
await validateProjectEnv(currentSpaceId, event.target.value);
4154
};
4255

4356
const handleInvalidSelectionError = () => {
@@ -47,10 +60,12 @@ export const ProjectSelectionSection = ({ projects }: Props) => {
4760
});
4861
};
4962

63+
const selectedOption = errors.projectSelection.projectNotFound ? '' : parameters.selectedProject;
64+
5065
return (
5166
<SectionWrapper testId={sectionId}>
5267
<SelectSection
53-
selectedOption={parameters.selectedProject}
68+
selectedOption={selectedOption}
5469
options={projects}
5570
handleInvalidSelectionError={handleInvalidSelectionError}
5671
section={sectionId}

apps/vercel/frontend/src/constants/defaultParams.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export const initialErrors: Errors = {
1717
projectSelection: {
1818
projectNotFound: false,
1919
cannotFetchProjects: false,
20+
invalidSpaceId: false,
2021
},
2122
apiPathSelection: {
2223
apiPathNotFound: false,

apps/vercel/frontend/src/constants/enums.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,5 @@ export enum errorTypes {
3636
EMPTY_PREVIEW_PATH_INPUT = 'emptyPreviewPathInput',
3737
API_PATHS_EMPTY = 'apiPathsEmpty',
3838
INVALID_DEPLOYMENT_DATA = 'invalidDeploymentData',
39+
INVALID_SPACE_ID = 'invalidSpaceId',
3940
}

apps/vercel/frontend/src/constants/errorMessages.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,6 @@ export const errorMessages = {
1212
invalidPreviewPathFormat: 'Path must start with a "/", and include a {token}.',
1313
emptyPreviewPathInput: 'Field is empty.',
1414
invalidDeploymentData: 'We had trouble fetching routes for this Vercel project.',
15+
invalidSpaceId:
16+
'It looks like you’ve configured an environment variable, CONTENTFUL_SPACE_ID, in the selected Vercel project that doesn’t match the id of the space where this app is installed.',
1517
};

apps/vercel/frontend/src/contexts/ConfigPageProvider.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { createContext, Dispatch } from 'react';
2-
import { ContentType } from '@contentful/app-sdk';
2+
import { ConfigAppSDK, ContentType } from '@contentful/app-sdk';
33
import { ParameterAction } from '@reducers/parameterReducer';
44
import { AppInstallationParameters, Errors } from '@customTypes/configPage';
55
import { ErrorAction } from '@reducers/errorsReducer';
6+
import VercelClient from '@clients/Vercel';
67

78
interface ConfigPageContextValue {
89
isAppConfigurationSaved: boolean;
@@ -13,6 +14,8 @@ interface ConfigPageContextValue {
1314
dispatchErrors: Dispatch<ErrorAction>;
1415
handleAppConfigurationChange: () => void;
1516
isLoading: boolean;
17+
sdk: ConfigAppSDK;
18+
vercelClient: VercelClient | null;
1619
}
1720

1821
export interface ChannelContextProviderProps extends ConfigPageContextValue {
@@ -32,6 +35,8 @@ export const ConfigPageProvider = (props: ChannelContextProviderProps) => {
3235
errors,
3336
handleAppConfigurationChange,
3437
isLoading,
38+
sdk,
39+
vercelClient,
3540
} = props;
3641

3742
return (
@@ -45,6 +50,8 @@ export const ConfigPageProvider = (props: ChannelContextProviderProps) => {
4550
errors,
4651
handleAppConfigurationChange,
4752
isLoading,
53+
sdk,
54+
vercelClient,
4855
}}>
4956
{children}
5057
</ConfigPageContext.Provider>

apps/vercel/frontend/src/customTypes/configPage.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ export interface AppInstallationParameters {
1717
teamId?: string;
1818
}
1919

20+
export type ProjectEnv = {
21+
key: string;
22+
id: string;
23+
value: string;
24+
};
2025
export interface Project {
2126
id: string;
2227
name: string;
@@ -25,6 +30,7 @@ export interface Project {
2530
id: string;
2631
};
2732
};
33+
env: ProjectEnv[];
2834
}
2935

3036
export interface Deployment {
@@ -86,6 +92,7 @@ export type Errors = {
8692
projectSelection: {
8793
projectNotFound: boolean;
8894
cannotFetchProjects: boolean;
95+
invalidSpaceId: boolean;
8996
};
9097
apiPathSelection: {
9198
apiPathNotFound: boolean;

apps/vercel/frontend/src/hooks/useFetchData/useFetchData.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,5 +102,30 @@ export const useFetchData = ({
102102
}
103103
};
104104

105-
return { validateToken, fetchProjects, fetchApiPaths };
105+
const validateProjectEnv = async (currentSpaceId: string, projectId: string) => {
106+
if (vercelClient && teamId)
107+
try {
108+
const isProjectSelectionValid = await vercelClient.validateProjectContentfulSpaceId(
109+
currentSpaceId,
110+
projectId,
111+
teamId
112+
);
113+
if (!isProjectSelectionValid) throw new Error(errorTypes.INVALID_SPACE_ID);
114+
else {
115+
dispatchErrors({
116+
type: errorsActions.RESET_PROJECT_SELECTION_ERRORS,
117+
});
118+
}
119+
} catch (e) {
120+
const err = e as Error;
121+
if (err.message === errorTypes.INVALID_SPACE_ID) {
122+
dispatchErrors({
123+
type: errorsActions.UPDATE_PROJECT_SELECTION_ERRORS,
124+
payload: errorTypes.INVALID_SPACE_ID,
125+
});
126+
}
127+
}
128+
};
129+
130+
return { validateToken, fetchProjects, fetchApiPaths, validateProjectEnv };
106131
};

apps/vercel/frontend/src/locations/ConfigScreen.spec.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ describe('ConfigScreen', () => {
3333
id: '',
3434
},
3535
},
36+
env: [],
3637
},
3738
],
3839
});

apps/vercel/frontend/src/locations/ConfigScreen.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,8 @@ const ConfigScreen = () => {
165165
dispatchErrors={dispatchErrors}
166166
isLoading={isLoading}
167167
parameters={parameters}
168+
sdk={sdk}
169+
vercelClient={vercelClient}
168170
errors={errors}>
169171
<Box className={styles.body}>
170172
<Box>

apps/vercel/frontend/test/helpers/renderConfigPageComponent.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import React from 'react';
22
import { render } from '@testing-library/react';
33
import { mockContentTypes } from '../mocks/mockContentTypes';
44
import { mockContentTypePreviewPathSelections } from '../mocks/mockContentTypePreviewPathSelections';
5-
import { ConfigPageContext } from '../../src/contexts/ConfigPageProvider';
6-
import { AppInstallationParameters, Errors } from '../../src/customTypes/configPage';
5+
import { ConfigPageContext } from '@contexts/ConfigPageProvider';
6+
import { AppInstallationParameters, Errors } from '@customTypes/configPage';
77
import { initialErrors } from '@constants/defaultParams';
8+
import { mockSdk } from '@test/mocks';
89

910
export const renderConfigPageComponent = (
1011
children: React.ReactNode,
@@ -26,6 +27,8 @@ export const renderConfigPageComponent = (
2627
isAppConfigurationSaved: false,
2728
handleAppConfigurationChange: () => null,
2829
isLoading: false,
30+
sdk: mockSdk,
31+
vercelClient: null,
2932
...overrides,
3033
}}>
3134
{children}

apps/vercel/frontend/test/helpers/renderConfigPageHook.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import React from 'react';
22
import { mockContentTypes } from '../mocks/mockContentTypes';
33
import { mockContentTypePreviewPathSelections } from '../mocks/mockContentTypePreviewPathSelections';
4-
import { ConfigPageContext } from '../../src/contexts/ConfigPageProvider';
5-
import { AppInstallationParameters, Errors } from '../../src/customTypes/configPage';
4+
import { ConfigPageContext } from '@contexts/ConfigPageProvider';
5+
import { AppInstallationParameters, Errors } from '@customTypes/configPage';
66
import { initialErrors } from '@constants/defaultParams';
7+
import { mockSdk } from '@test/mocks';
78

89
export const renderConfigPageHook = ({ children }: { children: React.ReactNode }) => (
910
<ConfigPageContext.Provider
@@ -20,6 +21,8 @@ export const renderConfigPageHook = ({ children }: { children: React.ReactNode }
2021
isAppConfigurationSaved: false,
2122
handleAppConfigurationChange: () => null,
2223
isLoading: false,
24+
sdk: mockSdk,
25+
vercelClient: null,
2326
}}>
2427
{children}
2528
</ConfigPageContext.Provider>

0 commit comments

Comments
 (0)