Skip to content

Commit

Permalink
merge main
Browse files Browse the repository at this point in the history
  • Loading branch information
wrt95 committed Feb 9, 2024
2 parents 4e58765 + 97fef76 commit 99e8e74
Show file tree
Hide file tree
Showing 157 changed files with 1,862 additions and 1,405 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,37 @@ import type { ServicesContextProps } from 'app-shared/contexts/ServicesContext';
import { act, screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react';
import { textMock } from '../../../../testing/mocks/i18nMock';
import { createQueryClientMock } from 'app-shared/mocks/queryClientMock';
import { datamodelNameMock } from 'app-shared/mocks/datamodelMetadataMocks';
import {
createJsonMetadataMock,
createXsdMetadataMock,
} from 'app-shared/mocks/datamodelMetadataMocks';
import userEvent from '@testing-library/user-event';
import { dataMock } from '@altinn/schema-editor/mockData';
import { AUTOSAVE_DEBOUNCE_INTERVAL_MILLISECONDS } from 'app-shared/constants';
import type { SchemaEditorAppProps } from '@altinn/schema-editor/SchemaEditorApp';
import { QueryKey } from 'app-shared/types/QueryKey';
import { createApiErrorMock } from 'app-shared/mocks/apiErrorMock';
import { createJsonModelPathMock } from 'app-shared/mocks/modelPathMocks';
import type {
DatamodelMetadataJson,
DatamodelMetadataXsd,
} from 'app-shared/types/DatamodelMetadata';
import { verifyNeverOccurs } from '../../../../testing/testUtils';

const user = userEvent.setup();

// Test data:
const modelPath = datamodelNameMock;
const model1Name = 'model1';
const model2name = 'model2';
const model1Path = createJsonModelPathMock(model1Name);
const model2Path = createJsonModelPathMock(model2name);
const model1MetadataJson: DatamodelMetadataJson = createJsonMetadataMock(model1Name);
const model1MetadataXsd: DatamodelMetadataXsd = createXsdMetadataMock(model1Name);
const model2MetadataJson: DatamodelMetadataJson = createJsonMetadataMock(model2name);
const model2MetadataXsd: DatamodelMetadataXsd = createXsdMetadataMock(model2name);

const defaultProps: SelectedSchemaEditorProps = {
modelPath,
modelPath: model1Path,
};
const org = 'org';
const app = 'app';
Expand Down Expand Up @@ -72,6 +89,7 @@ describe('SelectedSchemaEditor', () => {
const getDatamodel = jest.fn().mockImplementation(() => Promise.resolve(dataMock));

render({ getDatamodel, saveDatamodel });

await waitForElementToBeRemoved(() => screen.queryByTitle(textMock('general.loading')));

const button = screen.getByTestId(saveButtonTestId);
Expand All @@ -80,7 +98,7 @@ describe('SelectedSchemaEditor', () => {

act(() => jest.advanceTimersByTime(AUTOSAVE_DEBOUNCE_INTERVAL_MILLISECONDS));
await waitFor(() => expect(saveDatamodel).toHaveBeenCalledTimes(1));
expect(saveDatamodel).toHaveBeenCalledWith(org, app, modelPath, dataMock);
expect(saveDatamodel).toHaveBeenCalledWith(org, app, model1Path, dataMock);
});

it('Autosaves when changing between models that are not present in the cache', async () => {
Expand All @@ -92,22 +110,19 @@ describe('SelectedSchemaEditor', () => {
await waitForElementToBeRemoved(() => screen.queryByTitle(textMock('general.loading')));
expect(saveDatamodel).not.toHaveBeenCalled();

const updatedProps = {
...defaultProps,
modelPath: 'newModel',
};
const updatedProps = { ...defaultProps, modelPath: model2Path };
rerender(<SelectedSchemaEditor {...updatedProps} />);
jest.advanceTimersByTime(AUTOSAVE_DEBOUNCE_INTERVAL_MILLISECONDS);
await waitFor(() => expect(saveDatamodel).toHaveBeenCalledTimes(1));
expect(saveDatamodel).toHaveBeenCalledWith(org, app, datamodelNameMock, dataMock);
expect(saveDatamodel).toHaveBeenCalledWith(org, app, model1Path, dataMock);
});

it('Autosaves when changing between models that are already present in the cache', async () => {
const saveDatamodel = jest.fn();
const queryClient = createQueryClientMock();
const newModelPath = 'newModel';
queryClient.setQueryData([QueryKey.JsonSchema, org, app, datamodelNameMock], dataMock);
queryClient.setQueryData([QueryKey.JsonSchema, org, app, newModelPath], dataMock);
queryClient.setQueryData([QueryKey.JsonSchema, org, app, model1Path], dataMock);
queryClient.setQueryData([QueryKey.JsonSchema, org, app, model1Path], dataMock);
const {
renderResult: { rerender },
} = render({ saveDatamodel }, queryClient);
Expand All @@ -120,17 +135,48 @@ describe('SelectedSchemaEditor', () => {
rerender(<SelectedSchemaEditor {...updatedProps} />);
jest.advanceTimersByTime(AUTOSAVE_DEBOUNCE_INTERVAL_MILLISECONDS);
await waitFor(() => expect(saveDatamodel).toHaveBeenCalledTimes(1));
expect(saveDatamodel).toHaveBeenCalledWith(org, app, datamodelNameMock, dataMock);
expect(saveDatamodel).toHaveBeenCalledWith(org, app, model1Path, dataMock);
});

it('Does not save when model is deleted', async () => {
const saveDatamodel = jest.fn();
const queryClient = createQueryClientMock();

queryClient.setQueryData([QueryKey.JsonSchema, org, app, model1Path], dataMock);
queryClient.setQueryData([QueryKey.JsonSchema, org, app, model2Path], dataMock);
const {
renderResult: { rerender },
} = render({ saveDatamodel }, queryClient);
expect(saveDatamodel).not.toHaveBeenCalled();

const updatedProps = {
...defaultProps,
modelPath: model2Path,
};
queryClient.setQueryData([QueryKey.DatamodelsJson, org, app], [model2MetadataJson]);
queryClient.setQueryData([QueryKey.DatamodelsXsd, org, app], [model2MetadataXsd]);
rerender(<SelectedSchemaEditor {...updatedProps} />);
jest.advanceTimersByTime(AUTOSAVE_DEBOUNCE_INTERVAL_MILLISECONDS);
await verifyNeverOccurs(() => expect(saveDatamodel).toHaveBeenCalled());
});
});

const render = (
queries: Partial<ServicesContextProps> = {},
queryClient = createQueryClientMock(),
props: Partial<SelectedSchemaEditorProps> = {},
) =>
renderWithMockStore(
) => {
queryClient.setQueryData(
[QueryKey.DatamodelsJson, org, app],
[model1MetadataJson, model2MetadataJson],
);
queryClient.setQueryData(
[QueryKey.DatamodelsXsd, org, app],
[model1MetadataXsd, model2MetadataXsd],
);
return renderWithMockStore(
{},
queries,
queryClient,
)(<SelectedSchemaEditor {...defaultProps} {...props} />);
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,15 @@ import { useTranslation } from 'react-i18next';
import { AUTOSAVE_DEBOUNCE_INTERVAL_MILLISECONDS } from 'app-shared/constants';
import type { JsonSchema } from 'app-shared/types/JsonSchema';
import { useOnUnmount } from 'app-shared/hooks/useOnUnmount';
import type {
DatamodelMetadataJson,
DatamodelMetadataXsd,
} from 'app-shared/types/DatamodelMetadata';
import { useQueryClient } from '@tanstack/react-query';
import { QueryKey } from 'app-shared/types/QueryKey';
import { useStudioUrlParams } from 'app-shared/hooks/useStudioUrlParams';
import { mergeJsonAndXsdData } from 'app-development/utils/metadataUtils';
import { extractFilename, removeSchemaExtension } from 'app-shared/utils/filenameUtils';

export interface SelectedSchemaEditorProps {
modelPath: string;
}
Expand Down Expand Up @@ -46,7 +53,9 @@ interface SchemaEditorWithDebounceProps {
}

const SchemaEditorWithDebounce = ({ jsonSchema, modelPath }: SchemaEditorWithDebounceProps) => {
const { org, app } = useStudioUrlParams();
const { mutate } = useSchemaMutation();
const queryClient = useQueryClient();
const [model, setModel] = useState<JsonSchema>(jsonSchema);
const saveTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
const updatedModel = useRef<JsonSchema>(jsonSchema);
Expand All @@ -68,9 +77,24 @@ const SchemaEditorWithDebounce = ({ jsonSchema, modelPath }: SchemaEditorWithDeb
[saveFunction],
);

const doesModelExist = useCallback(() => {
const jsonModels: DatamodelMetadataJson[] = queryClient.getQueryData([
QueryKey.DatamodelsJson,
org,
app,
]);
const xsdModels: DatamodelMetadataXsd[] = queryClient.getQueryData([
QueryKey.DatamodelsXsd,
org,
app,
]);
const metadataList = mergeJsonAndXsdData(jsonModels, xsdModels);
return metadataList.some((datamodel) => datamodel.repositoryRelativeUrl === modelPath);
}, [queryClient, org, app, modelPath]);

useOnUnmount(() => {
clearTimeout(saveTimeoutRef.current);
saveFunction();
if (doesModelExist()) saveFunction();
});

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { renderHookWithMockStore } from '../../test/mocks';
import { useDeleteDatamodelMutation } from './useDeleteDatamodelMutation';
import type { ServicesContextProps } from 'app-shared/contexts/ServicesContext';
import type { QueryClient } from '@tanstack/react-query';
import { createQueryClientMock } from 'app-shared/mocks/queryClientMock';
import { waitFor } from '@testing-library/react';
import { QueryKey } from 'app-shared/types/QueryKey';
import { queriesMock } from 'app-shared/mocks/queriesMock';
import { createJsonModelPathMock } from 'app-shared/mocks/modelPathMocks';
import {
createJsonMetadataMock,
createXsdMetadataMock,
} from 'app-shared/mocks/datamodelMetadataMocks';

const modelName = 'modelName';
const modelPath = createJsonModelPathMock(modelName);
const org = 'org';
const app = 'app';
const modelMetadataJson = createJsonMetadataMock(modelName);
const modelMetadataXsd = createXsdMetadataMock(modelName);

describe('useDeleteDatamodelMutation', () => {
beforeEach(jest.clearAllMocks);

it('Calls deleteDatamodel with correct parameters', async () => {
const client = createQueryClientMock();
client.setQueryData([QueryKey.DatamodelsJson, org, app], [modelMetadataJson]);
client.setQueryData([QueryKey.DatamodelsXsd, org, app], [modelMetadataXsd]);
const {
renderHookResult: { result },
} = render({}, client);
expect(result.current).toBeDefined();
result.current.mutate(modelPath);
await waitFor(() => result.current.isSuccess);
expect(queriesMock.deleteDatamodel).toHaveBeenCalledTimes(1);
expect(queriesMock.deleteDatamodel).toHaveBeenCalledWith(org, app, modelPath);
});

it('Removes the metadata instances from the query cache', async () => {
const client = createQueryClientMock();
client.setQueryData([QueryKey.DatamodelsJson, org, app], [modelMetadataJson]);
client.setQueryData([QueryKey.DatamodelsXsd, org, app], [modelMetadataXsd]);
const {
renderHookResult: { result },
} = render({}, client);
result.current.mutate(modelPath);
await waitFor(() => result.current.isSuccess);
expect(client.getQueryData([QueryKey.DatamodelsJson, org, app])).toEqual([]);
expect(client.getQueryData([QueryKey.DatamodelsXsd, org, app])).toEqual([]);
});

it('Removes the schema queries from the query cache', async () => {
const client = createQueryClientMock();
client.setQueryData([QueryKey.DatamodelsJson, org, app], [modelMetadataJson]);
client.setQueryData([QueryKey.DatamodelsXsd, org, app], [modelMetadataXsd]);
const {
renderHookResult: { result },
} = render({}, client);
result.current.mutate(modelPath);
await waitFor(() => result.current.isSuccess);
expect(client.getQueryData([QueryKey.JsonSchema, org, app, modelPath])).toBeUndefined();
expect(
client.getQueryData([QueryKey.JsonSchema, org, app, modelMetadataXsd.repositoryRelativeUrl]),
).toBeUndefined();
});
});

const render = (
queries: Partial<ServicesContextProps> = {},
queryClient: QueryClient = createQueryClientMock(),
) => renderHookWithMockStore({}, queries, queryClient)(() => useDeleteDatamodelMutation());
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,41 @@ import { useServicesContext } from 'app-shared/contexts/ServicesContext';
import { QueryKey } from 'app-shared/types/QueryKey';
import { useStudioUrlParams } from 'app-shared/hooks/useStudioUrlParams';
import { isXsdFile } from 'app-shared/utils/filenameUtils';
import type { DatamodelMetadata } from 'app-shared/types/DatamodelMetadata';

export const useDeleteDatamodelMutation = () => {
const { deleteDatamodel } = useServicesContext();
const { org, app } = useStudioUrlParams();
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (modelPath: string) => {
await deleteDatamodel(org, app, modelPath);
const respectiveFileNameInXsdOrJson = isXsdFile(modelPath)
const jsonSchemaPath = isXsdFile(modelPath)
? modelPath.replace('.xsd', '.schema.json')
: modelPath.replace('.schema.json', '.xsd');
queryClient.setQueryData([QueryKey.JsonSchema, org, app, modelPath], undefined);
: modelPath;
const xsdPath = isXsdFile(modelPath) ? modelPath : modelPath.replace('.schema.json', '.xsd');
queryClient.setQueryData(
[QueryKey.JsonSchema, org, app, respectiveFileNameInXsdOrJson],
undefined,
[QueryKey.DatamodelsJson, org, app],
(oldData: DatamodelMetadata[]) => removeDatamodelFromList(oldData, jsonSchemaPath),
);
queryClient.setQueryData([QueryKey.DatamodelsXsd, org, app], (oldData: DatamodelMetadata[]) =>
removeDatamodelFromList(oldData, xsdPath),
);
await Promise.all([
queryClient.invalidateQueries({ queryKey: [QueryKey.DatamodelsJson, org, app] }),
queryClient.invalidateQueries({ queryKey: [QueryKey.DatamodelsXsd, org, app] }),
]);
await deleteDatamodel(org, app, modelPath);
return { jsonSchemaPath, xsdPath };
},
onSuccess: ({ jsonSchemaPath, xsdPath }) => {
queryClient.removeQueries({
queryKey: [QueryKey.JsonSchema, org, app, jsonSchemaPath],
});
queryClient.removeQueries({
queryKey: [QueryKey.JsonSchema, org, app, xsdPath],
});
},
});
};

export const removeDatamodelFromList = (
datamodels: DatamodelMetadata[],
relativeUrl: string,
): DatamodelMetadata[] =>
datamodels.filter((datamodel) => datamodel.repositoryRelativeUrl !== relativeUrl);
11 changes: 8 additions & 3 deletions frontend/language/src/nb.json
Original file line number Diff line number Diff line change
Expand Up @@ -1383,7 +1383,8 @@
"ux_editor.component_properties.action": "Aksjon",
"ux_editor.component_properties.align": "Plassering*",
"ux_editor.component_properties.attribution": "Opphav",
"ux_editor.component_properties.autocomplete": "HTML autocomplete",
"ux_editor.component_properties.autocomplete": "HTML autofullfør",
"ux_editor.component_properties.autocomplete_default": "Standard",
"ux_editor.component_properties.breakAfter": "Sideskift ETTER komponenten",
"ux_editor.component_properties.breakBefore": "Sideskift FØR komponenten",
"ux_editor.component_properties.buttonStyle": "Knappens stil",
Expand Down Expand Up @@ -1446,7 +1447,9 @@
"ux_editor.component_title.AttachmentList": "Liste over vedlegg",
"ux_editor.component_title.Button": "Knapp",
"ux_editor.component_title.ButtonGroup": "Knappegruppe",
"ux_editor.component_title.Checkboxes": "Avkrysningsboks",
"ux_editor.component_title.Checkboxes": "Avkrysningsbokser",
"ux_editor.component_title.Custom": "Egendefinert",
"ux_editor.component_title.CustomButton": "Egendefinert kanpp",
"ux_editor.component_title.Datepicker": "Dato",
"ux_editor.component_title.Dropdown": "Nedtrekksliste",
"ux_editor.component_title.FileUpload": "Vedlegg",
Expand All @@ -1460,6 +1463,7 @@
"ux_editor.component_title.InstanceInformation": "Informasjon om instans",
"ux_editor.component_title.InstantiationButton": "Instansieringsknapp",
"ux_editor.component_title.Likert": "Likert",
"ux_editor.component_title.LikertItem": "Likertelement",
"ux_editor.component_title.Link": "Lenke",
"ux_editor.component_title.List": "Liste",
"ux_editor.component_title.Map": "Stedfeste i kart",
Expand All @@ -1469,7 +1473,8 @@
"ux_editor.component_title.Panel": "Informativ melding",
"ux_editor.component_title.Paragraph": "Paragraf",
"ux_editor.component_title.PrintButton": "Utskriftsknapp",
"ux_editor.component_title.RadioButtons": "Radioknapp",
"ux_editor.component_title.RadioButtons": "Radioknapper",
"ux_editor.component_title.RepeatingGroup": "Repeterende gruppe",
"ux_editor.component_title.Summary": "Oppsummering",
"ux_editor.component_title.TextArea": "Langt svar",
"ux_editor.component_unknown": "Ukjent komponent",
Expand Down
2 changes: 2 additions & 0 deletions frontend/packages/shared/src/api/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import type { CreateDeploymentPayload } from 'app-shared/types/api/CreateDeploym
import type { CreateReleasePayload } from 'app-shared/types/api/CreateReleasePayload';
import type { CreateRepoCommitPayload } from 'app-shared/types/api/CreateRepoCommitPayload';
import type { ExternalFormLayout } from 'app-shared/types/api/FormLayoutsResponse';
import type { ExternalFormLayoutV3 } from 'app-shared/types/api/FormLayoutsResponseV3';
import type { LayoutSetConfig, LayoutSets } from 'app-shared/types/api/LayoutSetsResponse';
import type { ILayoutSettings, ITextResourcesObjectFormat } from 'app-shared/types/global';
import type { RuleConfig } from 'app-shared/types/RuleConfig';
Expand Down Expand Up @@ -83,6 +84,7 @@ export const pushRepoChanges = (org: string, app: string) => post(repoPushPath(o
export const resetRepoChanges = (org: string, app: string) => get(repoResetPath(org, app)); //Technically a mutation, but currently only implemented as a GET
export const saveDatamodel = (org: string, app: string, modelPath: string, payload: JsonSchema) => put<void, JsonSchema>(datamodelPath(org, app, modelPath, true), payload);
export const saveFormLayout = (org: string, app: string, layoutName: string, layoutSetName: string, payload: ExternalFormLayout) => post<void, ExternalFormLayout>(formLayoutPath(org, app, layoutName, layoutSetName), payload);
export const saveFormLayoutV3 = (org: string, app: string, layoutName: string, layoutSetName: string, payload: ExternalFormLayoutV3) => post<void, ExternalFormLayoutV3>(formLayoutPath(org, app, layoutName, layoutSetName), payload);
export const saveFormLayoutSettings = (org: string, app: string, layoutSetName: string, payload: ILayoutSettings) => post<ILayoutSettings>(layoutSettingsPath(org, app, layoutSetName), payload);
export const saveRuleConfig = (org: string, app: string, layoutSetName: string, payload: RuleConfig) => post<RuleConfig>(ruleConfigPath(org, app, layoutSetName), payload);
export const setStarredRepo = (org: string, app: string) => put(userStarredRepoPath(org, app), {});
Expand Down
2 changes: 2 additions & 0 deletions frontend/packages/shared/src/api/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import type { ApplicationMetadata } from 'app-shared/types/ApplicationMetadata';
import type { Altinn2LinkService } from 'app-shared/types/Altinn2LinkService';
import type { NewsList } from 'app-shared/types/api/NewsList';
import type { AppVersion } from 'app-shared/types/AppVersion';
import type { FormLayoutsResponseV3 } from 'app-shared/types/api/FormLayoutsResponseV3';

export const getAppReleases = (owner: string, app: string) => get<AppReleasesResponse>(releasesPath(owner, app, 'Descending'));
export const getAppVersion = (org: string, app: string) => get<AppVersion>(appVersionPath(org, app));
Expand All @@ -87,6 +88,7 @@ export const getEnvironments = () => get<DeployEnvironment[]>(envConfigPath());
export const getExpressionSchema = () => get<string[]>(expressionSchemaUrl());
export const getFormLayoutSettings = (owner: string, app: string, layoutSetName: string) => get<ILayoutSettings>(layoutSettingsPath(owner, app, layoutSetName));
export const getFormLayouts = (owner: string, app: string, layoutSetName: string) => get<FormLayoutsResponse>(formLayoutsPath(owner, app, layoutSetName));
export const getFormLayoutsV3 = (owner: string, app: string, layoutSetName: string) => get<FormLayoutsResponseV3>(formLayoutsPath(owner, app, layoutSetName));
export const getFrontEndSettings = (owner: string, app: string) => get<IFrontEndSettings>(frontEndSettingsPath(owner, app));
export const getInstanceIdForPreview = (owner: string, app: string) => get<string>(instanceIdForPreviewPath(owner, app));
export const getLayoutSchema = () => get<string[]>(layoutSchemaUrl());
Expand Down
Loading

0 comments on commit 99e8e74

Please sign in to comment.