diff --git a/frontend/app-development/features/dataModelling/SchemaEditorWithToolbar/SelectedSchemaEditor.test.tsx b/frontend/app-development/features/dataModelling/SchemaEditorWithToolbar/SelectedSchemaEditor.test.tsx
index 81a0b5d62f2..5d4c0b11845 100644
--- a/frontend/app-development/features/dataModelling/SchemaEditorWithToolbar/SelectedSchemaEditor.test.tsx
+++ b/frontend/app-development/features/dataModelling/SchemaEditorWithToolbar/SelectedSchemaEditor.test.tsx
@@ -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';
@@ -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);
@@ -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 () => {
@@ -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();
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);
@@ -120,7 +135,29 @@ describe('SelectedSchemaEditor', () => {
rerender();
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();
+ jest.advanceTimersByTime(AUTOSAVE_DEBOUNCE_INTERVAL_MILLISECONDS);
+ await verifyNeverOccurs(() => expect(saveDatamodel).toHaveBeenCalled());
});
});
@@ -128,9 +165,18 @@ const render = (
queries: Partial = {},
queryClient = createQueryClientMock(),
props: Partial = {},
-) =>
- renderWithMockStore(
+) => {
+ queryClient.setQueryData(
+ [QueryKey.DatamodelsJson, org, app],
+ [model1MetadataJson, model2MetadataJson],
+ );
+ queryClient.setQueryData(
+ [QueryKey.DatamodelsXsd, org, app],
+ [model1MetadataXsd, model2MetadataXsd],
+ );
+ return renderWithMockStore(
{},
queries,
queryClient,
)();
+};
diff --git a/frontend/app-development/features/dataModelling/SchemaEditorWithToolbar/SelectedSchemaEditor.tsx b/frontend/app-development/features/dataModelling/SchemaEditorWithToolbar/SelectedSchemaEditor.tsx
index 3f58ee05bb8..3aedb20ac35 100644
--- a/frontend/app-development/features/dataModelling/SchemaEditorWithToolbar/SelectedSchemaEditor.tsx
+++ b/frontend/app-development/features/dataModelling/SchemaEditorWithToolbar/SelectedSchemaEditor.tsx
@@ -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;
}
@@ -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);
const saveTimeoutRef = useRef>();
const updatedModel = useRef(jsonSchema);
@@ -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 (
diff --git a/frontend/app-development/hooks/mutations/useDeleteDatamodelMutation.test.ts b/frontend/app-development/hooks/mutations/useDeleteDatamodelMutation.test.ts
new file mode 100644
index 00000000000..d61c5df3a03
--- /dev/null
+++ b/frontend/app-development/hooks/mutations/useDeleteDatamodelMutation.test.ts
@@ -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 = {},
+ queryClient: QueryClient = createQueryClientMock(),
+) => renderHookWithMockStore({}, queries, queryClient)(() => useDeleteDatamodelMutation());
diff --git a/frontend/app-development/hooks/mutations/useDeleteDatamodelMutation.ts b/frontend/app-development/hooks/mutations/useDeleteDatamodelMutation.ts
index f292aa4af31..dcf1838e8cf 100644
--- a/frontend/app-development/hooks/mutations/useDeleteDatamodelMutation.ts
+++ b/frontend/app-development/hooks/mutations/useDeleteDatamodelMutation.ts
@@ -3,6 +3,7 @@ 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();
@@ -10,19 +11,33 @@ export const useDeleteDatamodelMutation = () => {
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);
diff --git a/frontend/packages/shared/src/mocks/datamodelMetadataMocks.ts b/frontend/packages/shared/src/mocks/datamodelMetadataMocks.ts
index 6fbae30be5f..22ff72523a3 100644
--- a/frontend/packages/shared/src/mocks/datamodelMetadataMocks.ts
+++ b/frontend/packages/shared/src/mocks/datamodelMetadataMocks.ts
@@ -2,6 +2,7 @@ import type {
DatamodelMetadataJson,
DatamodelMetadataXsd,
} from 'app-shared/types/DatamodelMetadata';
+import { createJsonModelPathMock, createXsdModelPathMock } from 'app-shared/mocks/modelPathMocks';
export const datamodelNameMock = 'model1';
const description = null;
@@ -15,18 +16,21 @@ const metadataMockBase = {
lastChanged,
};
-export const jsonMetadataMock: DatamodelMetadataJson = {
+export const createJsonMetadataMock = (modelName: string): DatamodelMetadataJson => ({
...metadataMockBase,
- fileName: `${datamodelNameMock}.schema.json`,
- filePath: `${directory}/${datamodelNameMock}.schema.json`,
+ fileName: `${modelName}.schema.json`,
+ filePath: `${directory}/${modelName}.schema.json`,
fileType: '.json',
- repositoryRelativeUrl: `/App/models/${datamodelNameMock}.schema.json`,
-};
+ repositoryRelativeUrl: createJsonModelPathMock(modelName),
+});
-export const xsdMetadataMock: DatamodelMetadataXsd = {
+export const createXsdMetadataMock = (modelName: string): DatamodelMetadataXsd => ({
...metadataMockBase,
- fileName: `${datamodelNameMock}.xsd`,
- filePath: `${directory}/${datamodelNameMock}.xsd`,
+ fileName: `${modelName}.xsd`,
+ filePath: `${directory}/${modelName}.xsd`,
fileType: '.xsd',
- repositoryRelativeUrl: `/App/models/${datamodelNameMock}.xsd`,
-};
+ repositoryRelativeUrl: createXsdModelPathMock(modelName),
+});
+
+export const jsonMetadataMock: DatamodelMetadataJson = createJsonMetadataMock(datamodelNameMock);
+export const xsdMetadataMock: DatamodelMetadataXsd = createXsdMetadataMock(datamodelNameMock);
diff --git a/frontend/packages/shared/src/mocks/modelPathMocks.ts b/frontend/packages/shared/src/mocks/modelPathMocks.ts
new file mode 100644
index 00000000000..b098721ac3d
--- /dev/null
+++ b/frontend/packages/shared/src/mocks/modelPathMocks.ts
@@ -0,0 +1,2 @@
+export const createJsonModelPathMock = (name: string): string => `/App/models/${name}.schema.json`;
+export const createXsdModelPathMock = (name: string): string => `/App/models/${name}.xsd`;
diff --git a/frontend/testing/testUtils.ts b/frontend/testing/testUtils.ts
index 0f2c2e5cdbb..61363927f41 100644
--- a/frontend/testing/testUtils.ts
+++ b/frontend/testing/testUtils.ts
@@ -1,3 +1,4 @@
+import { waitFor } from '@testing-library/react';
import { APP_DEVELOPMENT_BASENAME } from 'app-shared/constants';
export const TEST_DOMAIN = 'http://localhost';
@@ -8,3 +9,5 @@ export const setWindowLocationForTests = (org: string, app: string) => {
`${TEST_DOMAIN}${APP_DEVELOPMENT_BASENAME}/${org}/${app}`,
) as unknown as Location;
};
+
+export const verifyNeverOccurs = (fn: () => void) => expect(waitFor(fn)).rejects.toThrow();