Skip to content

Commit

Permalink
Bug/12035 not possible to delete a data model in studio (#12115)
Browse files Browse the repository at this point in the history
* fixed removing  datamodell and old datamodell
  • Loading branch information
JamalAlabdullah authored Feb 9, 2024
1 parent f72dadf commit 97fef76
Show file tree
Hide file tree
Showing 7 changed files with 201 additions and 36 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);
24 changes: 14 additions & 10 deletions frontend/packages/shared/src/mocks/datamodelMetadataMocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
2 changes: 2 additions & 0 deletions frontend/packages/shared/src/mocks/modelPathMocks.ts
Original file line number Diff line number Diff line change
@@ -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`;
3 changes: 3 additions & 0 deletions frontend/testing/testUtils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { waitFor } from '@testing-library/react';
import { APP_DEVELOPMENT_BASENAME } from 'app-shared/constants';

export const TEST_DOMAIN = 'http://localhost';
Expand All @@ -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();

0 comments on commit 97fef76

Please sign in to comment.