From 7582936b36831f8f37bffc7d1f821ed32154757e Mon Sep 17 00:00:00 2001 From: Nina Kylstad Date: Tue, 14 May 2024 12:31:46 +0200 Subject: [PATCH 1/5] use correct property for app created by in about page (#12782) * use correct property for app created by in about page * cleanup * Update frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AboutTab/AboutTab.test.tsx Co-authored-by: andreastanderen <71079896+standeren@users.noreply.github.com> * remove all traces of fetchInitialCommit from frontend * update mockend --------- Co-authored-by: andreastanderen <71079896+standeren@users.noreply.github.com> --- .../overview/handleServiceInformationSagas.ts | 22 +---------- .../overview/handleServiceInformationSlice.ts | 18 +-------- .../features/overview/types.ts | 8 ---- frontend/app-development/layout/App.tsx | 12 +----- .../Tabs/AboutTab/AboutTab.test.tsx | 37 +++++++++--------- .../components/Tabs/AboutTab/AboutTab.tsx | 20 +++++----- frontend/app-development/sagas/index.ts | 2 - .../app-development/test/rootStateMock.ts | 1 - frontend/packages/shared/src/api/paths.js | 1 - frontend/packages/shared/src/api/queries.ts | 3 -- .../shared/src/hooks/queries/index.ts | 1 - .../queries/useRepoInitialCommitQuery.ts | 38 ------------------- .../packages/shared/src/mocks/queriesMock.ts | 3 -- .../shared/src/types/ApplicationMetadata.ts | 4 ++ .../packages/shared/src/types/QueryKey.ts | 1 - frontend/testing/mockend/src/index.js | 2 - 16 files changed, 35 insertions(+), 138 deletions(-) delete mode 100644 frontend/packages/shared/src/hooks/queries/useRepoInitialCommitQuery.ts diff --git a/frontend/app-development/features/overview/handleServiceInformationSagas.ts b/frontend/app-development/features/overview/handleServiceInformationSagas.ts index 5961dca69cb..9aaee3a56bb 100644 --- a/frontend/app-development/features/overview/handleServiceInformationSagas.ts +++ b/frontend/app-development/features/overview/handleServiceInformationSagas.ts @@ -5,7 +5,6 @@ import postMessages from 'app-shared/utils/postMessages'; import type { PayloadAction } from '@reduxjs/toolkit'; import { HandleServiceInformationActions } from './handleServiceInformationSlice'; import type { - IFetchInitialCommitAction, IFetchServiceAction, IFetchServiceConfigAction, IFetchServiceNameAction, @@ -72,25 +71,6 @@ export function* watchHandleSaveServiceNameSaga(): SagaIterator { yield takeLatest(HandleServiceInformationActions.saveServiceName, handleSaveServiceNameSaga); } -export function* handleFetchInitialCommitSaga({ - payload: { url }, -}: PayloadAction): SagaIterator { - try { - const result = yield call(get, url); - - yield put(HandleServiceInformationActions.fetchInitialCommitFulfilled({ result })); - } catch (error) { - yield put(HandleServiceInformationActions.fetchInitialCommitRejected({ error })); - } -} - -export function* watchHandleFetchInitialCommitSaga(): SagaIterator { - yield takeLatest( - HandleServiceInformationActions.fetchInitialCommit, - handleFetchInitialCommitSaga, - ); -} - export function* handleFetchServiceConfigSaga({ payload: { url }, }: PayloadAction): SagaIterator { @@ -103,7 +83,7 @@ export function* handleFetchServiceConfigSaga({ }), ); } catch (error) { - yield put(HandleServiceInformationActions.fetchInitialCommitRejected({ error })); + yield put(HandleServiceInformationActions.fetchServiceConfigRejected({ error })); } } diff --git a/frontend/app-development/features/overview/handleServiceInformationSlice.ts b/frontend/app-development/features/overview/handleServiceInformationSlice.ts index 9953cf631c3..626365d8779 100644 --- a/frontend/app-development/features/overview/handleServiceInformationSlice.ts +++ b/frontend/app-development/features/overview/handleServiceInformationSlice.ts @@ -1,8 +1,7 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createAction, createSlice } from '@reduxjs/toolkit'; -import type { ICommit, IServiceDescription, IServiceId, IServiceName } from '../../types/global'; +import type { IServiceDescription, IServiceId, IServiceName } from '../../types/global'; import type { - IFetchInitialCommitFulfilled, IHandleServiceInformationActionRejected, IFetchServiceFulfilled, IFetchServiceConfigFulfilled, @@ -14,7 +13,6 @@ import type { IFetchServiceAction, IFetchServiceConfigAction, IFetchServiceNameAction, - IFetchInitialCommitAction, } from './types'; import type { Repository } from 'app-shared/types/Repository'; @@ -23,7 +21,6 @@ export interface IHandleServiceInformationState { serviceNameObj: IServiceName; serviceDescriptionObj: IServiceDescription; serviceIdObj: IServiceId; - initialCommit: ICommit; error: Error; } @@ -41,7 +38,6 @@ const initialState: IHandleServiceInformationState = { serviceId: '', saving: false, }, - initialCommit: null, error: null, }; @@ -50,17 +46,6 @@ const handleServiceInformationSlice = createSlice({ name: moduleName, initialState, reducers: { - fetchInitialCommitFulfilled: (state, action: PayloadAction) => { - const { result } = action.payload; - state.initialCommit = result; - }, - fetchInitialCommitRejected: ( - state, - action: PayloadAction, - ) => { - const { error } = action.payload; - state.error = error; - }, fetchServiceFulfilled: (state, action: PayloadAction) => { const { repository } = action.payload; state.repositoryInfo = repository; @@ -148,7 +133,6 @@ const actions = { fetchService: createAction(`${moduleName}/fetchService`), fetchServiceConfig: createAction(`${moduleName}/fetchServiceConfig`), fetchServiceName: createAction(`${moduleName}/fetchServiceName`), - fetchInitialCommit: createAction(`${moduleName}/fetchInitialCommit`), }; export const HandleServiceInformationActions = { diff --git a/frontend/app-development/features/overview/types.ts b/frontend/app-development/features/overview/types.ts index 43388e02a43..0355cb2eaa3 100644 --- a/frontend/app-development/features/overview/types.ts +++ b/frontend/app-development/features/overview/types.ts @@ -1,13 +1,5 @@ import type { Repository } from 'app-shared/types/Repository'; -export interface IFetchInitialCommitAction { - url: string; -} - -export interface IFetchInitialCommitFulfilled { - result: any; -} - export interface IFetchServiceAction { url: string; } diff --git a/frontend/app-development/layout/App.tsx b/frontend/app-development/layout/App.tsx index f976ef124ef..95a3dd05695 100644 --- a/frontend/app-development/layout/App.tsx +++ b/frontend/app-development/layout/App.tsx @@ -13,12 +13,7 @@ import classes from './App.module.css'; import { useAppDispatch, useAppSelector } from '../hooks'; import { getRepositoryType } from 'app-shared/utils/repository'; import { RepositoryType } from 'app-shared/types/global'; -import { - repoInitialCommitPath, - repoMetaPath, - serviceConfigPath, - serviceNamePath, -} from 'app-shared/api/paths'; +import { repoMetaPath, serviceConfigPath, serviceNamePath } from 'app-shared/api/paths'; import i18next from 'i18next'; import { initReactI18next, useTranslation } from 'react-i18next'; import nb from '../../language/src/nb.json'; @@ -70,11 +65,6 @@ export function App() { url: repoMetaPath(org, app), }), ); - dispatch( - HandleServiceInformationActions.fetchInitialCommit({ - url: repoInitialCommitPath(org, app), - }), - ); if (repositoryType === RepositoryType.App) { dispatch( diff --git a/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AboutTab/AboutTab.test.tsx b/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AboutTab/AboutTab.test.tsx index 27e947dfe9e..0475389ba65 100644 --- a/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AboutTab/AboutTab.test.tsx +++ b/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AboutTab/AboutTab.test.tsx @@ -14,26 +14,17 @@ import { queriesMock } from 'app-shared/mocks/queriesMock'; import { mockRepository1, mockRepository2 } from '../../../mocks/repositoryMock'; import { mockAppConfig } from '../../../mocks/appConfigMock'; import { formatDateToDateAndTimeString } from 'app-development/utils/dateUtils'; -import type { Commit, CommitAuthor } from 'app-shared/types/Commit'; import { MemoryRouter } from 'react-router-dom'; +import type { ApplicationMetadata } from 'app-shared/types/ApplicationMetadata'; const mockApp: string = 'app'; const mockOrg: string = 'org'; const mockNewText: string = 'test'; -const mockCommitAuthor: CommitAuthor = { - email: '', - name: 'Mock Mockesen', - when: new Date(2023, 9, 22), -}; - -const mockInitialCommit: Commit = { - message: '', - author: mockCommitAuthor, - comitter: mockCommitAuthor, - sha: '', - messageShort: '', - encoding: '', +const mockAppMetadata: ApplicationMetadata = { + id: `${mockOrg}/${mockApp}`, + org: mockOrg, + createdBy: 'Test Testesen', }; jest.mock('../../../../../../hooks/mutations/useAppConfigMutation'); @@ -47,7 +38,7 @@ mockUpdateAppConfigMutation.mockReturnValue({ const getAppConfig = jest.fn().mockImplementation(() => Promise.resolve({})); const getRepoMetadata = jest.fn().mockImplementation(() => Promise.resolve({})); -const getRepoInitialCommit = jest.fn().mockImplementation(() => Promise.resolve({})); +const getAppMetadata = jest.fn().mockImplementation(() => Promise.resolve({})); const defaultProps: AboutTabProps = { org: mockOrg, @@ -73,12 +64,12 @@ describe('AboutTab', () => { expect(getRepoMetadata).toHaveBeenCalledTimes(1); }); - it('fetches commit data on mount', () => { + it('fetches applicationMetadata on mount', () => { render(); - expect(getRepoInitialCommit).toHaveBeenCalledTimes(1); + expect(getAppMetadata).toHaveBeenCalledTimes(1); }); - it.each(['getAppConfig', 'getRepoMetadata', 'getRepoInitialCommit'])( + it.each(['getAppConfig', 'getRepoMetadata', 'getAppMetadata'])( 'shows an error message if an error occured on the %s query', async (queryName) => { const errorMessage = 'error-message-test'; @@ -168,12 +159,18 @@ describe('AboutTab', () => { ), ).toBeInTheDocument(); }); + + it('displays the user that created the app correctly', async () => { + await resolveAndWaitForSpinnerToDisappear(); + + expect(screen.getByText(mockAppMetadata.createdBy)).toBeInTheDocument(); + }); }); const resolveAndWaitForSpinnerToDisappear = async (props: Partial = {}) => { getAppConfig.mockImplementation(() => Promise.resolve(mockAppConfig)); getRepoMetadata.mockImplementation(() => Promise.resolve(mockRepository1)); - getRepoInitialCommit.mockImplementation(() => Promise.resolve(mockInitialCommit)); + getAppMetadata.mockImplementation(() => Promise.resolve(mockAppMetadata)); render(props); await waitForElementToBeRemoved(() => @@ -190,7 +187,7 @@ const render = ( ...queriesMock, getAppConfig, getRepoMetadata, - getRepoInitialCommit, + getAppMetadata, ...queries, }; diff --git a/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AboutTab/AboutTab.tsx b/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AboutTab/AboutTab.tsx index 18dfa24dfba..ed13cb154c5 100644 --- a/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AboutTab/AboutTab.tsx +++ b/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AboutTab/AboutTab.tsx @@ -7,8 +7,8 @@ import { ErrorMessage } from '@digdir/design-system-react'; import { Divider } from 'app-shared/primitives'; import { getRepositoryType } from 'app-shared/utils/repository'; import { useAppConfigMutation } from 'app-development/hooks/mutations'; -import { useAppConfigQuery } from 'app-development/hooks/queries'; -import { useRepoInitialCommitQuery, useRepoMetadataQuery } from 'app-shared/hooks/queries'; +import { useAppConfigQuery, useAppMetadataQuery } from 'app-development/hooks/queries'; +import { useRepoMetadataQuery } from 'app-shared/hooks/queries'; import { mergeQueryStatuses } from 'app-shared/utils/tanstackQueryUtils'; import { LoadingTabData } from '../../LoadingTabData'; import { TabDataError } from '../../TabDataError'; @@ -46,10 +46,10 @@ export const AboutTab = ({ org, app }: AboutTabProps): ReactNode => { error: repositoryError, } = useRepoMetadataQuery(org, app); const { - status: initialCommitStatus, - data: initialCommitData, - error: initialCommitError, - } = useRepoInitialCommitQuery(org, app); + status: applicationMetadataStatus, + data: applicationMetadataData, + error: applicationMetadataError, + } = useAppMetadataQuery(org, app); const { mutate: updateAppConfigMutation } = useAppConfigMutation(org, app); @@ -58,7 +58,7 @@ export const AboutTab = ({ org, app }: AboutTabProps): ReactNode => { }; const displayContent = () => { - switch (mergeQueryStatuses(appConfigStatus, repositoryStatus, initialCommitStatus)) { + switch (mergeQueryStatuses(appConfigStatus, repositoryStatus, applicationMetadataStatus)) { case 'pending': { return ; } @@ -67,7 +67,9 @@ export const AboutTab = ({ org, app }: AboutTabProps): ReactNode => { {appConfigError && {appConfigError.message}} {repositoryError && {repositoryError.message}} - {initialCommitError && {initialCommitError.message}} + {applicationMetadataError && ( + {applicationMetadataError.message} + )} ); } @@ -79,7 +81,7 @@ export const AboutTab = ({ org, app }: AboutTabProps): ReactNode => { ); diff --git a/frontend/app-development/sagas/index.ts b/frontend/app-development/sagas/index.ts index 04054f1d6b3..fcb3babd749 100644 --- a/frontend/app-development/sagas/index.ts +++ b/frontend/app-development/sagas/index.ts @@ -2,7 +2,6 @@ import type { SagaIterator } from 'redux-saga'; import createSagaMiddleware from 'redux-saga'; import { fork } from 'redux-saga/effects'; import { - watchHandleFetchInitialCommitSaga, watchHandleFetchServiceConfigSaga, watchHandleFetchServiceNameSaga, watchHandleFetchServiceSaga, @@ -15,7 +14,6 @@ function* root(): SagaIterator { yield fork(watchHandleFetchServiceSaga); yield fork(watchHandleFetchServiceNameSaga); yield fork(watchHandleSaveServiceNameSaga); - yield fork(watchHandleFetchInitialCommitSaga); yield fork(watchHandleFetchServiceConfigSaga); yield fork(watchHandleSaveServiceConfigSaga); yield fork(userSagas); diff --git a/frontend/app-development/test/rootStateMock.ts b/frontend/app-development/test/rootStateMock.ts index fb011595332..0d50a6427f5 100644 --- a/frontend/app-development/test/rootStateMock.ts +++ b/frontend/app-development/test/rootStateMock.ts @@ -5,7 +5,6 @@ export const rootStateMock: RootState = { serviceInformation: { repositoryInfo: repository, error: null, - initialCommit: null, serviceDescriptionObj: { description: 'mockDescription', saving: false, diff --git a/frontend/packages/shared/src/api/paths.js b/frontend/packages/shared/src/api/paths.js index c9909a660bc..104052927c7 100644 --- a/frontend/packages/shared/src/api/paths.js +++ b/frontend/packages/shared/src/api/paths.js @@ -92,7 +92,6 @@ export const repoBranchesPath = (org, app) => `${basePath}/repos/repo/${org}/${a export const repoCommitPath = (org, app) => `${basePath}/repos/repo/${org}/${app}/commit`; // Post export const repoCommitPushPath = (org, app) => `${basePath}/repos/repo/${org}/${app}/commit-and-push`; // Post export const repoDownloadPath = (org, app, full) => `${basePath}/repos/repo/${org}/${app}/contents.zip?${s({ full })}`; -export const repoInitialCommitPath = (org, app) => `${basePath}/repos/repo/${org}/${app}/initial-commit`; // Get export const repoLatestCommitPath = (org, app) => `${basePath}/repos/repo/${org}/${app}/latest-commit`; // Get export const repoLogPath = (org, app) => `${basePath}/repos/repo/${org}/${app}/log`; // Get export const repoMetaPath = (org, app) => `${basePath}/repos/repo/${org}/${app}/metadata`; // Get diff --git a/frontend/packages/shared/src/api/queries.ts b/frontend/packages/shared/src/api/queries.ts index c81b69fa2e5..2f7fe759143 100644 --- a/frontend/packages/shared/src/api/queries.ts +++ b/frontend/packages/shared/src/api/queries.ts @@ -23,7 +23,6 @@ import { accessListPath, processEditorPath, releasesPath, - repoInitialCommitPath, repoMetaPath, repoPullPath, repoSearchPath, @@ -69,7 +68,6 @@ import type { JsonSchema } from 'app-shared/types/JsonSchema'; import type { PolicyAction, Policy, PolicySubject } from '@altinn/policy-editor'; import type { BrregPartySearchResult, BrregSubPartySearchResult, AccessList, Resource, ResourceListItem, ResourceVersionStatus, Validation, AccessListsResponse } from 'app-shared/types/ResourceAdm'; import type { AppConfig } from 'app-shared/types/AppConfig'; -import type { Commit } from 'app-shared/types/Commit'; 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'; @@ -98,7 +96,6 @@ export const getNewsList = (language: 'nb' | 'en') => get(newsListUrl( export const getOptionListIds = (owner: string, app: string) => get(optionListIdsPath(owner, app)); export const getOrgList = () => get(orgListUrl()); export const getOrganizations = () => get(orgsListPath()); -export const getRepoInitialCommit = (owner: string, app: string) => get(repoInitialCommitPath(owner, app)); export const getRepoMetadata = (owner: string, app: string) => get(repoMetaPath(owner, app)); export const getRepoPull = (owner: string, app: string) => get(repoPullPath(owner, app)); export const getRepoStatus = (owner: string, app: string) => get(repoStatusPath(owner, app)); diff --git a/frontend/packages/shared/src/hooks/queries/index.ts b/frontend/packages/shared/src/hooks/queries/index.ts index ac528c7ac50..e4261c3e4ff 100644 --- a/frontend/packages/shared/src/hooks/queries/index.ts +++ b/frontend/packages/shared/src/hooks/queries/index.ts @@ -2,7 +2,6 @@ export { useAppVersionQuery } from './useAppVersionQuery'; export { useDatamodelsJsonQuery } from './useDatamodelsJsonQuery'; export { useDatamodelsXsdQuery } from './useDatamodelsXsdQuery'; export { useInstanceIdQuery } from './useInstanceIdQuery'; -export { useRepoInitialCommitQuery } from './useRepoInitialCommitQuery'; export { useRepoMetadataQuery } from './useRepoMetadataQuery'; export { useRepoPullQuery } from './useRepoPullQuery'; export { useRepoStatusQuery } from './useRepoStatusQuery'; diff --git a/frontend/packages/shared/src/hooks/queries/useRepoInitialCommitQuery.ts b/frontend/packages/shared/src/hooks/queries/useRepoInitialCommitQuery.ts deleted file mode 100644 index 5254d3639eb..00000000000 --- a/frontend/packages/shared/src/hooks/queries/useRepoInitialCommitQuery.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { UseQueryResult } from '@tanstack/react-query'; -import { useQuery } from '@tanstack/react-query'; -import { useServicesContext } from 'app-shared/contexts/ServicesContext'; -import { QueryKey } from 'app-shared/types/QueryKey'; -import type { Commit, CommitAuthor } from 'app-shared/types/Commit'; -import type { AxiosError } from 'axios'; - -/** - * Query function to get the data of the intial commit of a repo - * - * @param owner the owner of the repo - * @param app the application - * - * @returns useQuery result with the Repository - */ -export const useRepoInitialCommitQuery = ( - owner: string, - app: string, -): UseQueryResult => { - const { getRepoInitialCommit } = useServicesContext(); - return useQuery({ - queryKey: [QueryKey.RepoInitialCommit, owner, app], - queryFn: () => getRepoInitialCommit(owner, app), - select: (data: Commit) => { - // Convert the 'when' property of the author and comitter to a Date - const author: CommitAuthor = { - ...data.author, - when: new Date(data.author.when), - }; - const comitter: CommitAuthor = { - ...data.comitter, - when: new Date(data.comitter.when), - }; - - return { ...data, author, comitter }; - }, - }); -}; diff --git a/frontend/packages/shared/src/mocks/queriesMock.ts b/frontend/packages/shared/src/mocks/queriesMock.ts index 9970ddc43a9..c9b96348858 100644 --- a/frontend/packages/shared/src/mocks/queriesMock.ts +++ b/frontend/packages/shared/src/mocks/queriesMock.ts @@ -4,7 +4,6 @@ import type { AppConfig } from 'app-shared/types/AppConfig'; import type { AppVersion } from 'app-shared/types/AppVersion'; import type { ApplicationMetadata } from 'app-shared/types/ApplicationMetadata'; import type { BranchStatus } from 'app-shared/types/BranchStatus'; -import type { Commit } from 'app-shared/types/Commit'; import type { DatamodelMetadataJson, DatamodelMetadataXsd, @@ -51,7 +50,6 @@ import { appReleasesResponse, applicationMetadata, branchStatus, - commit, createRepoCommitPayload, datamodelMetadataResponse, layoutSets, @@ -105,7 +103,6 @@ export const queriesMock: ServicesContextProps = { getOptionListIds: jest.fn().mockImplementation(() => Promise.resolve([])), getOrgList: jest.fn().mockImplementation(() => Promise.resolve(orgList)), getOrganizations: jest.fn().mockImplementation(() => Promise.resolve([])), - getRepoInitialCommit: jest.fn().mockImplementation(() => Promise.resolve(commit)), getRepoMetadata: jest.fn().mockImplementation(() => Promise.resolve(repository)), getRepoPull: jest.fn().mockImplementation(() => Promise.resolve(repoStatus)), getRepoStatus: jest.fn().mockImplementation(() => Promise.resolve(repoStatus)), diff --git a/frontend/packages/shared/src/types/ApplicationMetadata.ts b/frontend/packages/shared/src/types/ApplicationMetadata.ts index decd19e8aaa..2ffe7206693 100644 --- a/frontend/packages/shared/src/types/ApplicationMetadata.ts +++ b/frontend/packages/shared/src/types/ApplicationMetadata.ts @@ -3,10 +3,14 @@ import type { KeyValuePairs } from 'app-shared/types/KeyValuePairs'; export interface ApplicationMetadata { autoDeleteOnProcessEnd?: boolean; copyInstanceSettings?: CopyInstanceSettings; + created?: string; + createdBy?: string; dataFields?: DataFieldElement[]; dataTypes?: DataTypeElement[]; eFormidling?: EFormidling; id: string; + lastChanged?: string; + lastChangedBy?: string; messageBoxConfig?: MessageBoxConfig; onEntry?: OnEntry; org: string; diff --git a/frontend/packages/shared/src/types/QueryKey.ts b/frontend/packages/shared/src/types/QueryKey.ts index 99c1f553309..83a6e4af1b0 100644 --- a/frontend/packages/shared/src/types/QueryKey.ts +++ b/frontend/packages/shared/src/types/QueryKey.ts @@ -27,7 +27,6 @@ export enum QueryKey { OptionListIds = 'OptionListIds', OrgList = 'OrgList', Organizations = 'Organizations', - RepoInitialCommit = 'RepoInitialCommit', RepoMetaData = 'RepoMetaData', RepoPullData = 'RepoPullData', RepoReset = 'RepoReset', diff --git a/frontend/testing/mockend/src/index.js b/frontend/testing/mockend/src/index.js index 92c8a292da9..a20bbe28d12 100644 --- a/frontend/testing/mockend/src/index.js +++ b/frontend/testing/mockend/src/index.js @@ -9,7 +9,6 @@ const { datamodelsPath, createDatamodelPath, remainingSessionTimePath, - repoInitialCommitPath, branchStatusPath, repoStatusPath, frontendLangPath, @@ -35,7 +34,6 @@ module.exports = (middlewares, devServer) => { //prettier-ignore app.get(frontendLangPath(':locale'), (req, res) => res.json(require(`../../../language/src/${req.params.locale}.json`))); app.get(remainingSessionTimePath(), (req, res) => res.send('9999')); - app.get(repoInitialCommitPath(':org', ':app'), (req, res) => res.sendStatus(204)); app.get(repoMetaPath(':org', ':app'), require('./routes/get-repo-data')); app.get(branchStatusPath(':org', ':app', 'branch'), require('./routes/get-branch')); app.get(repoStatusPath(':org', ':app'), fixtureRoute('status')); From aa005834ca0c0925360171083069c255e03c2771 Mon Sep 17 00:00:00 2001 From: andreastanderen <71079896+standeren@users.noreply.github.com> Date: Tue, 14 May 2024 13:10:02 +0200 Subject: [PATCH 2/5] delete layoutset datatype ref and exclude appmetadata taskType ref when deleting and uploading datamodels respectively (#12793) * delete layoutset datatype ref and exclude appmetadata taskType ref when deleting and uploading datamodels respectively * Fix PR comments --- ...ProcessDataTypeChangedLayoutSetsHandler.cs | 2 +- .../ProcessTaskIdChangedLayoutSetsHandler.cs | 2 +- .../GitRepository/AltinnAppGitRepository.cs | 2 +- .../Implementation/AppDevelopmentService.cs | 6 +-- .../Implementation/SchemaModelService.cs | 20 ++++++--- .../Services/SchemaModelServiceTests.cs | 43 +++++++++++++++++++ .../mutations/useDeleteDatamodelMutation.ts | 1 + 7 files changed, 64 insertions(+), 12 deletions(-) diff --git a/backend/src/Designer/EventHandlers/ProcessDataTypeChanged/ProcessDataTypeChangedLayoutSetsHandler.cs b/backend/src/Designer/EventHandlers/ProcessDataTypeChanged/ProcessDataTypeChangedLayoutSetsHandler.cs index 8df346f149a..575186f99e4 100644 --- a/backend/src/Designer/EventHandlers/ProcessDataTypeChanged/ProcessDataTypeChangedLayoutSetsHandler.cs +++ b/backend/src/Designer/EventHandlers/ProcessDataTypeChanged/ProcessDataTypeChangedLayoutSetsHandler.cs @@ -41,7 +41,7 @@ await _fileSyncHandlerExecutor.ExecuteWithExceptionHandling( var layoutSets = await repository.GetLayoutSetsFile(cancellationToken); if (TryChangeDataType(layoutSets, notification.NewDataType, notification.ConnectedTaskId)) { - await repository.SaveLayoutSetsFile(layoutSets); + await repository.SaveLayoutSets(layoutSets); } }); } diff --git a/backend/src/Designer/EventHandlers/ProcessTaskIdChanged/ProcessTaskIdChangedLayoutSetsHandler.cs b/backend/src/Designer/EventHandlers/ProcessTaskIdChanged/ProcessTaskIdChangedLayoutSetsHandler.cs index 7db106f06af..16cf0423283 100644 --- a/backend/src/Designer/EventHandlers/ProcessTaskIdChanged/ProcessTaskIdChangedLayoutSetsHandler.cs +++ b/backend/src/Designer/EventHandlers/ProcessTaskIdChanged/ProcessTaskIdChangedLayoutSetsHandler.cs @@ -42,7 +42,7 @@ await _fileSyncHandlerExecutor.ExecuteWithExceptionHandling( var layoutSets = await repository.GetLayoutSetsFile(cancellationToken); if (TryChangeTaskIds(layoutSets, notification.OldId, notification.NewId)) { - await repository.SaveLayoutSetsFile(layoutSets); + await repository.SaveLayoutSets(layoutSets); } }); } diff --git a/backend/src/Designer/Infrastructure/GitRepository/AltinnAppGitRepository.cs b/backend/src/Designer/Infrastructure/GitRepository/AltinnAppGitRepository.cs index eb0ab044d6b..fa52a336c6f 100644 --- a/backend/src/Designer/Infrastructure/GitRepository/AltinnAppGitRepository.cs +++ b/backend/src/Designer/Infrastructure/GitRepository/AltinnAppGitRepository.cs @@ -584,7 +584,7 @@ public async Task GetLayoutSetsFile(CancellationToken cancellationTo throw new NotFoundException("No layout set was found for this app"); } - public async Task SaveLayoutSetsFile(LayoutSets layoutSets) + public async Task SaveLayoutSets(LayoutSets layoutSets) { if (AppUsesLayoutSets()) { diff --git a/backend/src/Designer/Services/Implementation/AppDevelopmentService.cs b/backend/src/Designer/Services/Implementation/AppDevelopmentService.cs index 657fae0a2dd..9f262203e00 100644 --- a/backend/src/Designer/Services/Implementation/AppDevelopmentService.cs +++ b/backend/src/Designer/Services/Implementation/AppDevelopmentService.cs @@ -365,7 +365,7 @@ private static async Task DeleteExistingLayoutSet(AltinnAppGitReposi { LayoutSetConfig layoutSetToDelete = layoutSets.Sets.Find(set => set.Id == layoutSetToDeleteId); layoutSets.Sets.Remove(layoutSetToDelete); - await altinnAppGitRepository.SaveLayoutSetsFile(layoutSets); + await altinnAppGitRepository.SaveLayoutSets(layoutSets); return layoutSets; } @@ -376,14 +376,14 @@ await altinnAppGitRepository.SaveLayout(layoutSet.Id, AltinnAppGitRepository.Ini altinnAppGitRepository.InitialLayout); await altinnAppGitRepository.SaveLayoutSettings(layoutSet.Id, altinnAppGitRepository.InitialLayoutSettings); - await altinnAppGitRepository.SaveLayoutSetsFile(layoutSets); + await altinnAppGitRepository.SaveLayoutSets(layoutSets); return layoutSets; } private async Task UpdateLayoutSetName(AltinnAppGitRepository altinnAppGitRepository, LayoutSets layoutSets, string oldLayoutSetName, string newLayoutSetName) { layoutSets.Sets.Find(set => set.Id == oldLayoutSetName).Id = newLayoutSetName; - await altinnAppGitRepository.SaveLayoutSetsFile(layoutSets); + await altinnAppGitRepository.SaveLayoutSets(layoutSets); return layoutSets; } diff --git a/backend/src/Designer/Services/Implementation/SchemaModelService.cs b/backend/src/Designer/Services/Implementation/SchemaModelService.cs index 4aaf7b7d869..959b3cb350b 100644 --- a/backend/src/Designer/Services/Implementation/SchemaModelService.cs +++ b/backend/src/Designer/Services/Implementation/SchemaModelService.cs @@ -247,7 +247,7 @@ public async Task DeleteSchema(AltinnRepoEditingContext altinnRepoEditingContext var altinnCoreFile = altinnGitRepository.GetAltinnCoreFileByRelativePath(relativeFilePath); var schemaName = altinnGitRepository.GetSchemaName(relativeFilePath); - await DeleteDatatypeFromApplicationMetadata(altinnAppGitRepository, schemaName); + await DeleteDatatypeFromApplicationMetadataAndLayoutSets(altinnAppGitRepository, schemaName); DeleteRelatedSchemaFiles(altinnAppGitRepository, schemaName, altinnCoreFile.Directory); } else @@ -338,7 +338,6 @@ private static void UpdateApplicationWithAppLogicModel(ApplicationMetadata appli application.DataTypes = new List(); } - DataType existingLogicElement = application.DataTypes.FirstOrDefault(d => d.AppLogic?.ClassRef != null); DataType logicElement = application.DataTypes.SingleOrDefault(d => d.Id == dataTypeId); if (logicElement == null) @@ -346,7 +345,7 @@ private static void UpdateApplicationWithAppLogicModel(ApplicationMetadata appli logicElement = new DataType { Id = dataTypeId, - TaskId = existingLogicElement == null ? "Task_1" : null, + TaskId = null, AllowedContentTypes = new List() { "application/xml" }, MaxCount = 1, MinCount = 1, @@ -407,17 +406,26 @@ private static IEnumerable GetRelatedSchemaFiles(string schemaName, stri return new List() { jsonSchemaFile, xsdFile, jsonMetadataFile, csharpModelFile }; } - private static async Task DeleteDatatypeFromApplicationMetadata(AltinnAppGitRepository altinnAppGitRepository, string id) + private static async Task DeleteDatatypeFromApplicationMetadataAndLayoutSets(AltinnAppGitRepository altinnAppGitRepository, string id) { var applicationMetadata = await altinnAppGitRepository.GetApplicationMetadata(); if (applicationMetadata.DataTypes != null) { DataType removeForm = applicationMetadata.DataTypes.Find(m => m.Id == id); + if (altinnAppGitRepository.AppUsesLayoutSets()) + { + var layoutSets = await altinnAppGitRepository.GetLayoutSetsFile(); + var layoutSet = layoutSets.Sets.Find(set => set.Tasks[0] == removeForm.TaskId); + if (layoutSet is not null) + { + layoutSet.DataType = null; + await altinnAppGitRepository.SaveLayoutSets(layoutSets); + } + } applicationMetadata.DataTypes.Remove(removeForm); + await altinnAppGitRepository.SaveApplicationMetadata(applicationMetadata); } - - await altinnAppGitRepository.SaveApplicationMetadata(applicationMetadata); } private async Task ProcessNewXsd(AltinnAppGitRepository altinnAppGitRepository, MemoryStream xsdMemoryStream, string filePath) diff --git a/backend/tests/Designer.Tests/Services/SchemaModelServiceTests.cs b/backend/tests/Designer.Tests/Services/SchemaModelServiceTests.cs index 828851e2d5c..a4febf21116 100644 --- a/backend/tests/Designer.Tests/Services/SchemaModelServiceTests.cs +++ b/backend/tests/Designer.Tests/Services/SchemaModelServiceTests.cs @@ -61,6 +61,49 @@ public async Task DeleteSchema_AppRepo_ShouldDelete() } } + [Fact] + public async Task DeleteSchema_AppRepoWithLayoutSets_ShouldDelete() + { + // Arrange + var org = "ttd"; + var sourceRepository = "app-with-layoutsets"; + var developer = "testUser"; + var targetRepository = TestDataHelper.GenerateTestRepoName(); + var editingContext = AltinnRepoEditingContext.FromOrgRepoDeveloper(org, targetRepository, developer); + + await TestDataHelper.CopyRepositoryForTest(org, sourceRepository, developer, targetRepository); + try + { + string dataModelName = "datamodel"; + var altinnGitRepositoryFactory = new AltinnGitRepositoryFactory(TestDataHelper.GetTestDataRepositoriesRootDirectory()); + + ISchemaModelService schemaModelService = new SchemaModelService(altinnGitRepositoryFactory, TestDataHelper.LogFactory, TestDataHelper.ServiceRepositorySettings, TestDataHelper.XmlSchemaToJsonSchemaConverter, TestDataHelper.JsonSchemaToXmlSchemaConverter, TestDataHelper.ModelMetadataToCsharpConverter); + var schemaFiles = schemaModelService.GetSchemaFiles(editingContext); + schemaFiles.Should().HaveCount(1); + + var altinnAppGitRepository = altinnGitRepositoryFactory.GetAltinnAppGitRepository(org, targetRepository, developer); + var applicationMetadataBefore = await altinnAppGitRepository.GetApplicationMetadata(); + var layoutSetsBefore = await altinnAppGitRepository.GetLayoutSetsFile(); + + // Act + var schemaToDelete = schemaFiles.First(s => s.FileName == $"{dataModelName}.schema.json"); + await schemaModelService.DeleteSchema(editingContext, schemaToDelete.RepositoryRelativeUrl); + + // Assert + schemaFiles = schemaModelService.GetSchemaFiles(editingContext); + schemaFiles.Should().HaveCount(0); + var applicationMetadataAfter = await altinnAppGitRepository.GetApplicationMetadata(); + applicationMetadataAfter.DataTypes.Should().HaveCount(applicationMetadataBefore.DataTypes.Count - 1); + var layoutSetsAfter = await altinnAppGitRepository.GetLayoutSetsFile(); + layoutSetsBefore.Sets.Exists(set => set.DataType == dataModelName).Should().BeTrue(); + layoutSetsAfter.Sets.Exists(set => set.DataType == dataModelName).Should().BeFalse(); + } + finally + { + TestDataHelper.DeleteAppRepository(org, targetRepository, developer); + } + } + [Fact] public async Task DeleteSchema_ModelsRepo_ShouldDelete() { diff --git a/frontend/app-development/hooks/mutations/useDeleteDatamodelMutation.ts b/frontend/app-development/hooks/mutations/useDeleteDatamodelMutation.ts index dcf1838e8cf..13c2198e817 100644 --- a/frontend/app-development/hooks/mutations/useDeleteDatamodelMutation.ts +++ b/frontend/app-development/hooks/mutations/useDeleteDatamodelMutation.ts @@ -32,6 +32,7 @@ export const useDeleteDatamodelMutation = () => { queryClient.removeQueries({ queryKey: [QueryKey.JsonSchema, org, app, xsdPath], }); + queryClient.invalidateQueries({ queryKey: [QueryKey.AppMetadataModelIds, org, app] }); }, }); }; From b26b300a7904ed513dd49eefcba587d488e35e0e Mon Sep 17 00:00:00 2001 From: andreastanderen <71079896+standeren@users.noreply.github.com> Date: Tue, 14 May 2024 13:10:17 +0200 Subject: [PATCH 3/5] Only add feature flag to session storage if present in url (#12796) * Only add feature flag to session storage if present in url * Fix tests --- .../src/utils/featureToggleUtils.test.ts | 40 ++++++++++++++----- .../shared/src/utils/featureToggleUtils.ts | 2 +- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/frontend/packages/shared/src/utils/featureToggleUtils.test.ts b/frontend/packages/shared/src/utils/featureToggleUtils.test.ts index 7628bb98cfb..02ac6cc56f3 100644 --- a/frontend/packages/shared/src/utils/featureToggleUtils.test.ts +++ b/frontend/packages/shared/src/utils/featureToggleUtils.test.ts @@ -1,4 +1,4 @@ -import { typedLocalStorage } from 'app-shared/utils/webStorage'; +import { typedLocalStorage, typedSessionStorage } from 'app-shared/utils/webStorage'; import { addFeatureFlagToLocalStorage, removeFeatureFlagFromLocalStorage, @@ -6,45 +6,67 @@ import { } from './featureToggleUtils'; describe('featureToggle localStorage', () => { + beforeEach(() => typedLocalStorage.removeItem('featureFlags')); + it('should return true if feature is enabled in the localStorage', () => { typedLocalStorage.setItem('featureFlags', ['shouldOverrideAppLibCheck']); - expect(shouldDisplayFeature('shouldOverrideAppLibCheck')).toBe(true); + expect(shouldDisplayFeature('shouldOverrideAppLibCheck')).toBeTruthy(); }); it('should return true if featureFlag includes in feature params', () => { typedLocalStorage.setItem('featureFlags', ['demo', 'shouldOverrideAppLibCheck']); - expect(shouldDisplayFeature('shouldOverrideAppLibCheck')).toBe(true); + expect(shouldDisplayFeature('shouldOverrideAppLibCheck')).toBeTruthy(); }); it('should return false if feature is not enabled in the localStorage', () => { typedLocalStorage.setItem('featureFlags', ['demo']); - expect(shouldDisplayFeature('shouldOverrideAppLibCheck')).toBe(false); + expect(shouldDisplayFeature('shouldOverrideAppLibCheck')).toBeFalsy(); }); it('should return false if feature is not enabled in the localStorage', () => { - expect(shouldDisplayFeature('shouldOverrideAppLibCheck')).toBe(false); + expect(shouldDisplayFeature('shouldOverrideAppLibCheck')).toBeFalsy(); }); }); describe('featureToggle url', () => { + beforeEach(() => { + typedLocalStorage.removeItem('featureFlags'); + typedSessionStorage.removeItem('featureFlags'); + }); it('should return true if feature is enabled in the url', () => { window.history.pushState({}, 'PageUrl', '/?featureFlags=shouldOverrideAppLibCheck'); - expect(shouldDisplayFeature('shouldOverrideAppLibCheck')).toBe(true); + expect(shouldDisplayFeature('shouldOverrideAppLibCheck')).toBeTruthy(); }); it('should return true if featureFlag includes in feature params', () => { window.history.pushState({}, 'PageUrl', '/?featureFlags=demo,shouldOverrideAppLibCheck'); - expect(shouldDisplayFeature('shouldOverrideAppLibCheck')).toBe(true); + expect(shouldDisplayFeature('shouldOverrideAppLibCheck')).toBeTruthy(); }); it('should return false if feature is not included in the url', () => { window.history.pushState({}, 'PageUrl', '/?featureFlags=demo'); - expect(shouldDisplayFeature('shouldOverrideAppLibCheck')).toBe(false); + expect(shouldDisplayFeature('shouldOverrideAppLibCheck')).toBeFalsy(); }); it('should return false if feature is not included in the url', () => { window.history.pushState({}, 'PageUrl', '/'); - expect(shouldDisplayFeature('shouldOverrideAppLibCheck')).toBe(false); + expect(shouldDisplayFeature('shouldOverrideAppLibCheck')).toBeFalsy(); + }); + + it('should persist features in sessionStorage when persistFeatureFlag is set in url', () => { + window.history.pushState( + {}, + 'PageUrl', + '/?featureFlags=customizeEndEvent,shouldOverrideAppLibCheck&persistFeatureFlag=true', + ); + expect(shouldDisplayFeature('componentConfigBeta')).toBeFalsy(); + expect(shouldDisplayFeature('shouldOverrideAppLibCheck')).toBeTruthy(); + expect(shouldDisplayFeature('customizeEndEvent')).toBeTruthy(); + expect(typedSessionStorage.getItem('featureFlags')).toEqual([ + 'shouldOverrideAppLibCheck', + 'customizeEndEvent', + ]); + expect(typedLocalStorage.getItem('featureFlags')).toBeUndefined(); }); }); diff --git a/frontend/packages/shared/src/utils/featureToggleUtils.ts b/frontend/packages/shared/src/utils/featureToggleUtils.ts index 896c97e9a7c..cabd6687e12 100644 --- a/frontend/packages/shared/src/utils/featureToggleUtils.ts +++ b/frontend/packages/shared/src/utils/featureToggleUtils.ts @@ -28,7 +28,7 @@ const defaultActiveFeatures: SupportedFeatureFlags[] = []; */ export const shouldDisplayFeature = (featureFlag: SupportedFeatureFlags): boolean => { // Check if feature should be persisted in session storage, (url)?persistFeatureFlag=true - if (shouldPersistInSession()) { + if (shouldPersistInSession() && isFeatureActivatedByUrl(featureFlag)) { addFeatureFlagToSessionStorage(featureFlag); } From 847a83b718ba65e90e84c49610501fa3bb9b5987 Mon Sep 17 00:00:00 2001 From: Lars <74791975+lassopicasso@users.noreply.github.com> Date: Tue, 14 May 2024 21:42:17 +0200 Subject: [PATCH 4/5] Fix expression crash when linked ID doesn't exist anymore (#12746) * add new text resource for expression * redo text * add initial error styling on border before edit mode, and remove the selected id if it doesnt exist anymore in edit mode * fix tests * don't display the error message "id doesn't exist anymore" when ID is not selected * set correct border color when initial rendering of expression container display an error * fix strange behaviour * fix styling when error message on combobox * add test * remove custom styling * set assertion to the test * move the use of dataLookupOptions closer to where its actually used. * fix tests --- frontend/language/src/nb.json | 1 + .../ComponentIdSelector.tsx | 10 ++++++---- .../SubexpressionValueSelector.test.tsx | 11 +++++++++++ .../SubExpression/Subexpression.tsx | 10 ++++++---- .../utils/findSubexpressionErrors.test.ts | 19 +++++++++++++------ .../utils/findSubexpressionErrors.ts | 17 ++++++++++++----- .../enums/ExpressionErrorKey.ts | 1 + .../StudioExpression/test-data/texts.ts | 1 + 8 files changed, 51 insertions(+), 19 deletions(-) diff --git a/frontend/language/src/nb.json b/frontend/language/src/nb.json index c3f99dcd389..f241dfaa0dc 100644 --- a/frontend/language/src/nb.json +++ b/frontend/language/src/nb.json @@ -237,6 +237,7 @@ "expression.confirmDeleteSubexpression": "Er du sikker på at du vil slette dette underuttrykket?", "expression.datamodelPath": "Datamodellfelt", "expression.disabledLogicalOperator": "Det må være minst to underuttrykk i listen før en logisk operator kan legges til.", + "expression.error.componentIDNoLongerExists": "Opprinnelig komponent-ID eksisterer ikke lenger. Velg en ny fra listen.", "expression.error.invalidComponentId": "Det finnes ingen komponent med denne ID-en. Velg en fra listen.", "expression.error.invalidDatamodelPath": "Det finnes intet datamodellfelt med denne pekeren. Velg en peker fra listen.", "expression.error.invalidFirstOperand": "Den første verdien er ikke gyldig.", diff --git a/frontend/libs/studio-components/src/components/StudioExpression/SimplifiedEditor/LogicalExpressionEditor/SubExpression/SubExpressionValueSelector/SubExpressionValueContentInput/ComponentIdSelector.tsx b/frontend/libs/studio-components/src/components/StudioExpression/SimplifiedEditor/LogicalExpressionEditor/SubExpression/SubExpressionValueSelector/SubExpressionValueContentInput/ComponentIdSelector.tsx index 935d7f6073b..f2e902b1547 100644 --- a/frontend/libs/studio-components/src/components/StudioExpression/SimplifiedEditor/LogicalExpressionEditor/SubExpression/SubExpressionValueSelector/SubExpressionValueContentInput/ComponentIdSelector.tsx +++ b/frontend/libs/studio-components/src/components/StudioExpression/SimplifiedEditor/LogicalExpressionEditor/SubExpression/SubExpressionValueSelector/SubExpressionValueContentInput/ComponentIdSelector.tsx @@ -11,10 +11,12 @@ export const ComponentIdSelector = ({ onChange, }: Props) => { const { dataLookupOptions, texts } = useStudioExpressionContext(); - const [errorKey, setErrorKey] = useState(null); - const [idValue, setIdValue] = useState(value.id); - const options = dataLookupOptions[DataLookupFuncName.Component]; + const idValueExist = options.includes(value.id) || value.id === ''; + const [errorKey, setErrorKey] = useState( + idValueExist ? null : ExpressionErrorKey.ComponentIDNoLongerExists, + ); + const [idValue, setIdValue] = useState(value.id); const handleChange = (values: string[]) => { if (values.length) { @@ -33,7 +35,7 @@ export const ComponentIdSelector = ({ label={texts.componentId} onValueChange={handleChange} size='small' - value={idValue ? [idValue] : []} + value={idValue && idValueExist ? [idValue] : []} > {options.map((option) => ( diff --git a/frontend/libs/studio-components/src/components/StudioExpression/SimplifiedEditor/LogicalExpressionEditor/SubExpression/SubExpressionValueSelector/SubexpressionValueSelector.test.tsx b/frontend/libs/studio-components/src/components/StudioExpression/SimplifiedEditor/LogicalExpressionEditor/SubExpression/SubExpressionValueSelector/SubexpressionValueSelector.test.tsx index 29126d8cf32..ae5619ec162 100644 --- a/frontend/libs/studio-components/src/components/StudioExpression/SimplifiedEditor/LogicalExpressionEditor/SubExpression/SubExpressionValueSelector/SubexpressionValueSelector.test.tsx +++ b/frontend/libs/studio-components/src/components/StudioExpression/SimplifiedEditor/LogicalExpressionEditor/SubExpression/SubExpressionValueSelector/SubexpressionValueSelector.test.tsx @@ -190,6 +190,17 @@ describe('SubexpressionValueSelector', () => { await user.click(document.body); screen.getByText(texts.errorMessages[ExpressionErrorKey.InvalidComponentId]); }); + + it('Displays initial error and handles non-existing component ID', () => { + const id = 'non-existing-id'; + renderSubexpressionValueSelector({ value: { ...componentValue, id }, isInEditMode: true }); + const errorMessage = screen.getByText( + texts.errorMessages[ExpressionErrorKey.ComponentIDNoLongerExists], + ); + expect(errorMessage).toBeInTheDocument(); + const input = screen.getByRole('combobox', { name: texts.componentId }); + expect(input).toHaveValue(''); + }); }); describe('When the value is an instance context reference', () => { diff --git a/frontend/libs/studio-components/src/components/StudioExpression/SimplifiedEditor/LogicalExpressionEditor/SubExpression/Subexpression.tsx b/frontend/libs/studio-components/src/components/StudioExpression/SimplifiedEditor/LogicalExpressionEditor/SubExpression/Subexpression.tsx index 88e83c24896..d4d3881e588 100644 --- a/frontend/libs/studio-components/src/components/StudioExpression/SimplifiedEditor/LogicalExpressionEditor/SubExpression/Subexpression.tsx +++ b/frontend/libs/studio-components/src/components/StudioExpression/SimplifiedEditor/LogicalExpressionEditor/SubExpression/Subexpression.tsx @@ -26,10 +26,12 @@ export type SubexpressionProps = { }; export const Subexpression = ({ expression, legend, onChange, onDelete }: SubexpressionProps) => { - const { texts } = useStudioExpressionContext(); + const { texts, dataLookupOptions } = useStudioExpressionContext(); const [isInEditMode, setIsInEditMode] = useState(false); const [expressionState, setExpressionState] = useState(expression); - const [errors, setErrors] = useState([]); + const [errors, setErrors] = useState( + findSubexpressionErrors(expression, dataLookupOptions), + ); useEffect(() => { setExpressionState(expression); @@ -40,7 +42,7 @@ export const Subexpression = ({ expression, legend, onChange, onDelete }: Subexp }; const handleSave = () => { - const errorList = findSubexpressionErrors(expressionState); + const errorList = findSubexpressionErrors(expressionState, dataLookupOptions); setErrors(errorList); if (!errorList.length) { onChange(expressionState); @@ -96,7 +98,7 @@ export const Subexpression = ({ expression, legend, onChange, onDelete }: Subexp onSave={handleSave} onEnableEditMode={handleEnableEditMode} /> - {!!errors.length && } + {!!errors.length && isInEditMode && } ); diff --git a/frontend/libs/studio-components/src/components/StudioExpression/SimplifiedEditor/LogicalExpressionEditor/SubExpression/utils/findSubexpressionErrors.test.ts b/frontend/libs/studio-components/src/components/StudioExpression/SimplifiedEditor/LogicalExpressionEditor/SubExpression/utils/findSubexpressionErrors.test.ts index 3cf9c8ff58b..dc65b95fd78 100644 --- a/frontend/libs/studio-components/src/components/StudioExpression/SimplifiedEditor/LogicalExpressionEditor/SubExpression/utils/findSubexpressionErrors.test.ts +++ b/frontend/libs/studio-components/src/components/StudioExpression/SimplifiedEditor/LogicalExpressionEditor/SubExpression/utils/findSubexpressionErrors.test.ts @@ -4,15 +4,21 @@ import { SimpleSubexpressionValueType } from '../../../../enums/SimpleSubexpress import type { SimpleSubexpression } from '../../../../types/SimpleSubexpression'; import { ExpressionErrorKey } from '../../../../enums/ExpressionErrorKey'; import { GeneralRelationOperator } from '../../../../enums/GeneralRelationOperator'; +import type { DataLookupOptions } from '../../../../types/DataLookupOptions'; +import { DataLookupFuncName } from '../../../../enums/DataLookupFuncName'; describe('findSubexpressionErrors', () => { + const dataLookupOptions: DataLookupOptions = { + [DataLookupFuncName.Component]: ['1', '2'], + [DataLookupFuncName.DataModel]: ['a', 'b'], + }; it('Returns an empty array when the subexpression is valid', () => { const subexpression: SimpleSubexpression = { relationalOperator: GeneralRelationOperator.Equals, firstOperand: { type: SimpleSubexpressionValueType.Number, value: 1 }, secondOperand: { type: SimpleSubexpressionValueType.Number, value: 2 }, }; - const result = findSubexpressionErrors(subexpression); + const result = findSubexpressionErrors(subexpression, dataLookupOptions); expect(result).toEqual([]); }); @@ -22,7 +28,8 @@ describe('findSubexpressionErrors', () => { firstOperand: { type: SimpleSubexpressionValueType.Number, value: 1 }, secondOperand: { type: SimpleSubexpressionValueType.Boolean, value: false }, }; - const result = findSubexpressionErrors(subexpression); + + const result = findSubexpressionErrors(subexpression, dataLookupOptions); expect(result).toEqual([ExpressionErrorKey.NumericRelationOperatorWithWrongType]); }); @@ -32,7 +39,7 @@ describe('findSubexpressionErrors', () => { firstOperand: { type: SimpleSubexpressionValueType.Datamodel, path: '' }, secondOperand: { type: SimpleSubexpressionValueType.Number, value: 2 }, }; - const result = findSubexpressionErrors(subexpression); + const result = findSubexpressionErrors(subexpression, dataLookupOptions); expect(result).toEqual([ExpressionErrorKey.InvalidFirstOperand]); }); @@ -42,7 +49,7 @@ describe('findSubexpressionErrors', () => { firstOperand: { type: SimpleSubexpressionValueType.Component, id: '' }, secondOperand: { type: SimpleSubexpressionValueType.Number, value: 2 }, }; - const result = findSubexpressionErrors(subexpression); + const result = findSubexpressionErrors(subexpression, dataLookupOptions); expect(result).toEqual([ExpressionErrorKey.InvalidFirstOperand]); }); @@ -52,7 +59,7 @@ describe('findSubexpressionErrors', () => { firstOperand: { type: SimpleSubexpressionValueType.Number, value: 1 }, secondOperand: { type: SimpleSubexpressionValueType.Datamodel, path: '' }, }; - const result = findSubexpressionErrors(subexpression); + const result = findSubexpressionErrors(subexpression, dataLookupOptions); expect(result).toEqual([ExpressionErrorKey.InvalidSecondOperand]); }); @@ -62,7 +69,7 @@ describe('findSubexpressionErrors', () => { firstOperand: { type: SimpleSubexpressionValueType.Component, id: '' }, secondOperand: { type: SimpleSubexpressionValueType.Datamodel, path: '' }, }; - const result = findSubexpressionErrors(subexpression); + const result = findSubexpressionErrors(subexpression, dataLookupOptions); expect(result).toEqual([ ExpressionErrorKey.InvalidFirstOperand, ExpressionErrorKey.InvalidSecondOperand, diff --git a/frontend/libs/studio-components/src/components/StudioExpression/SimplifiedEditor/LogicalExpressionEditor/SubExpression/utils/findSubexpressionErrors.ts b/frontend/libs/studio-components/src/components/StudioExpression/SimplifiedEditor/LogicalExpressionEditor/SubExpression/utils/findSubexpressionErrors.ts index 088e01c86c5..a8e1db81740 100644 --- a/frontend/libs/studio-components/src/components/StudioExpression/SimplifiedEditor/LogicalExpressionEditor/SubExpression/utils/findSubexpressionErrors.ts +++ b/frontend/libs/studio-components/src/components/StudioExpression/SimplifiedEditor/LogicalExpressionEditor/SubExpression/utils/findSubexpressionErrors.ts @@ -4,18 +4,21 @@ import type { RelationalOperator } from '../../../../types/RelationalOperator'; import { SimpleSubexpressionValueType } from '../../../../enums/SimpleSubexpressionValueType'; import { ExpressionErrorKey } from '../../../../enums/ExpressionErrorKey'; import type { SimpleSubexpressionValue } from '../../../../types/SimpleSubexpressionValue'; +import type { DataLookupOptions } from '../../../../types/DataLookupOptions'; +import { DataLookupFuncName } from '../../../../enums/DataLookupFuncName'; export const findSubexpressionErrors = ( subexpression: SimpleSubexpression, + dataLookupOptions: DataLookupOptions, ): ExpressionErrorKey[] => { const errors: ExpressionErrorKey[] = []; if (hasNumberOperator(subexpression) && hasBooleanValue(subexpression)) { errors.push(ExpressionErrorKey.NumericRelationOperatorWithWrongType); } - if (!isOperandValid(subexpression.firstOperand)) { + if (!isOperandValid(subexpression.firstOperand, dataLookupOptions)) { errors.push(ExpressionErrorKey.InvalidFirstOperand); } - if (!isOperandValid(subexpression.secondOperand)) { + if (!isOperandValid(subexpression.secondOperand, dataLookupOptions)) { errors.push(ExpressionErrorKey.InvalidSecondOperand); } return errors; @@ -33,12 +36,15 @@ const hasBooleanValue = ({ firstOperand, secondOperand }: SimpleSubexpression): (value) => value.type === SimpleSubexpressionValueType.Boolean, ); -const isOperandValid = (value: SimpleSubexpressionValue): boolean => { +const isOperandValid = ( + value: SimpleSubexpressionValue, + dataLookupOptions: DataLookupOptions, +): boolean => { switch (value.type) { case SimpleSubexpressionValueType.Datamodel: return isDatamodelValueValid(value); case SimpleSubexpressionValueType.Component: - return isComponentValueValid(value); + return isComponentValueValid(value, dataLookupOptions); default: return true; } @@ -50,4 +56,5 @@ const isDatamodelValueValid = ( const isComponentValueValid = ( value: SimpleSubexpressionValue, -): boolean => !!value.id; + dataLookupOptions: DataLookupOptions, +): boolean => !!value.id && dataLookupOptions[DataLookupFuncName.Component].includes(value.id); diff --git a/frontend/libs/studio-components/src/components/StudioExpression/enums/ExpressionErrorKey.ts b/frontend/libs/studio-components/src/components/StudioExpression/enums/ExpressionErrorKey.ts index 80b41b9d227..aafecc6918d 100644 --- a/frontend/libs/studio-components/src/components/StudioExpression/enums/ExpressionErrorKey.ts +++ b/frontend/libs/studio-components/src/components/StudioExpression/enums/ExpressionErrorKey.ts @@ -1,4 +1,5 @@ export enum ExpressionErrorKey { + ComponentIDNoLongerExists = 'componentIDNoLongerExists', InvalidComponentId = 'invalidComponentId', InvalidDatamodelPath = 'invalidDatamodelPath', InvalidFirstOperand = 'invalidFirstOperand', diff --git a/frontend/libs/studio-components/src/components/StudioExpression/test-data/texts.ts b/frontend/libs/studio-components/src/components/StudioExpression/test-data/texts.ts index f0c1c42bdf7..9db4da1894a 100644 --- a/frontend/libs/studio-components/src/components/StudioExpression/test-data/texts.ts +++ b/frontend/libs/studio-components/src/components/StudioExpression/test-data/texts.ts @@ -45,6 +45,7 @@ const errorMessages: Record = { [ExpressionErrorKey.InvalidSecondOperand]: 'The second operand is invalid.', [ExpressionErrorKey.NumericRelationOperatorWithWrongType]: 'The relational operator is invalid for the selected operand types.', + [ExpressionErrorKey.ComponentIDNoLongerExists]: 'The component ID no longer exists.', }; export const texts: ExpressionTexts = { From 240e2c7df0f8378ef959d12d4228cb2e48f63a04 Mon Sep 17 00:00:00 2001 From: Martin Gunnerud Date: Wed, 15 May 2024 12:33:23 +0200 Subject: [PATCH 5/5] Resourceadm: allow publishing resources and editing access lists in AT21 and AT24 (#12781) * add teams for publish resource and edit access lists for AT21 and AT24 * allow publish resource and edit access lists in AT21 and AT24 * show labels for test environments in import from Altinn 2 modal * only allow import Altinn 2 service from AT environments if user belongs to ttd organization --- development/data/gitea-teams.json | 24 +++++++++++++++++ development/setup.js | 8 ++++++ frontend/language/src/nb.json | 2 ++ .../ImportResourceModal.test.tsx | 27 ++++++++++++------- .../ImportResourceModal.tsx | 14 ++++++---- .../utils/resourceUtils/resourceUtils.ts | 19 +++++++++++-- 6 files changed, 78 insertions(+), 16 deletions(-) diff --git a/development/data/gitea-teams.json b/development/data/gitea-teams.json index 550ecc64eb2..b7eab90fa17 100644 --- a/development/data/gitea-teams.json +++ b/development/data/gitea-teams.json @@ -53,6 +53,12 @@ "description": "Tilgang til ressursregister", "includes_all_repositories": true }, + { + "name": "Resources-Publish-AT21", + "permission": "write", + "description": "Deploy av ressurser i AT21", + "includes_all_repositories": true + }, { "name": "Resources-Publish-AT22", "permission": "write", @@ -65,12 +71,24 @@ "description": "Deploy av ressurser i AT23", "includes_all_repositories": true }, + { + "name": "Resources-Publish-AT24", + "permission": "write", + "description": "Deploy av ressurser i AT24", + "includes_all_repositories": true + }, { "name": "Resources-Publish-TT02", "permission": "write", "description": "Deploy av ressurser i TT02", "includes_all_repositories": true }, + { + "name": "AccessLists-AT21", + "permission": "write", + "description": "Skriving av RRR tilgangslister i AT21", + "includes_all_repositories": true + }, { "name": "AccessLists-AT22", "permission": "write", @@ -83,6 +101,12 @@ "description": "Skriving av RRR tilgangslister i AT23", "includes_all_repositories": true }, + { + "name": "AccessLists-AT24", + "permission": "write", + "description": "Skriving av RRR tilgangslister i AT24", + "includes_all_repositories": true + }, { "name": "AccessLists-TT02", "permission": "write", diff --git a/development/setup.js b/development/setup.js index 5d4f3c9bcd0..4615401e276 100644 --- a/development/setup.js +++ b/development/setup.js @@ -84,11 +84,15 @@ const addUserToSomeTestDepTeams = async (env) => { 'Deploy-AT21', 'Deploy-AT22', 'Resources', + 'Resources-Publish-AT21', 'Resources-Publish-AT22', 'Resources-Publish-AT23', + 'Resources-Publish-AT24', 'Resources-Publish-TT02', + 'AccessLists-AT21', 'AccessLists-AT22', 'AccessLists-AT23', + 'AccessLists-AT24', 'AccessLists-TT02', ]) { const existing = teams.find((t) => t.name === teamName); @@ -106,11 +110,15 @@ const addUserToSomeTestDepTeams = async (env) => { 'Deploy-AT21', 'Deploy-AT22', 'Resources', + 'Resources-Publish-AT21', 'Resources-Publish-AT22', 'Resources-Publish-AT23', + 'Resources-Publish-AT24', 'Resources-Publish-TT02', + 'AccessLists-AT21', 'AccessLists-AT22', 'AccessLists-AT23', + 'AccessLists-AT24', 'AccessLists-TT02', ]) { const existing = teams.find((t) => t.name === teamName); diff --git a/frontend/language/src/nb.json b/frontend/language/src/nb.json index f241dfaa0dc..7450cb99a5e 100644 --- a/frontend/language/src/nb.json +++ b/frontend/language/src/nb.json @@ -969,8 +969,10 @@ "resourceadm.dashboard_table_row_missing_title": "Mangler tittel på Bokmål", "resourceadm.dashboard_userdata_error_body": "Vi beklager, men en feil oppstod ved henting av dine brukerdata.", "resourceadm.dashboard_userdata_error_header": "Feil oppstod ved innlasting av brukerdata", + "resourceadm.deploy_at21_env": "Testmiljø AT21", "resourceadm.deploy_at22_env": "Testmiljø AT22", "resourceadm.deploy_at23_env": "Testmiljø AT23", + "resourceadm.deploy_at24_env": "Testmiljø AT24", "resourceadm.deploy_card_arrow_icon": "Ny versjon for {{env}}", "resourceadm.deploy_card_publish": "Publiser til {{env}}", "resourceadm.deploy_deploying": "Publiserer", diff --git a/frontend/resourceadm/components/ImportResourceModal/ImportResourceModal.test.tsx b/frontend/resourceadm/components/ImportResourceModal/ImportResourceModal.test.tsx index a1f0ff98a2d..44129dcf1ce 100644 --- a/frontend/resourceadm/components/ImportResourceModal/ImportResourceModal.test.tsx +++ b/frontend/resourceadm/components/ImportResourceModal/ImportResourceModal.test.tsx @@ -30,6 +30,13 @@ const defaultProps: ImportResourceModalProps = { onClose: mockOnClose, }; +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: () => ({ + selectedContext: 'ttd', + }), +})); + describe('ImportResourceModal', () => { afterEach(jest.clearAllMocks); @@ -45,9 +52,11 @@ describe('ImportResourceModal', () => { textMock('resourceadm.dashboard_import_modal_select_env'), ); await user.click(environmentSelect); - await user.click(screen.getByRole('option', { name: 'AT21' })); + await user.click(screen.getByRole('option', { name: textMock('resourceadm.deploy_at21_env') })); - await waitFor(() => expect(environmentSelect).toHaveValue('AT21')); + await waitFor(() => + expect(environmentSelect).toHaveValue(textMock('resourceadm.deploy_at21_env')), + ); expect(importButton).toHaveAttribute('aria-disabled', 'true'); // wait for the second combobox to appear, instead of waiting for the spinner to disappear. @@ -92,7 +101,7 @@ describe('ImportResourceModal', () => { textMock('resourceadm.dashboard_import_modal_select_env'), ); await user.click(environmentSelect); - await user.click(screen.getByRole('option', { name: 'AT21' })); + await user.click(screen.getByRole('option', { name: textMock('resourceadm.deploy_at21_env') })); // wait for the second combobox to appear, instead of waiting for the spinner to disappear. // (sometimes the spinner disappears) too quick and the test will fail @@ -110,7 +119,7 @@ describe('ImportResourceModal', () => { await waitFor(() => expect(serviceSelect).toHaveValue(mockOption)); await user.click(environmentSelect); - await user.click(screen.getByRole('option', { name: 'AT22' })); + await user.click(screen.getByRole('option', { name: textMock('resourceadm.deploy_at22_env') })); await waitFor(() => expect( screen.queryByLabelText(textMock('resourceadm.dashboard_resource_name_and_id_resource_id')), @@ -126,7 +135,7 @@ describe('ImportResourceModal', () => { textMock('resourceadm.dashboard_import_modal_select_env'), ); await user.click(environmentSelect); - await user.click(screen.getByRole('option', { name: 'AT21' })); + await user.click(screen.getByRole('option', { name: textMock('resourceadm.deploy_at21_env') })); // wait for the second combobox to appear, instead of waiting for the spinner to disappear. // (sometimes the spinner disappears) too quick and the test will fail @@ -172,7 +181,7 @@ describe('ImportResourceModal', () => { textMock('resourceadm.dashboard_import_modal_select_env'), ); await user.click(environmentSelect); - await user.click(screen.getByRole('option', { name: 'AT21' })); + await user.click(screen.getByRole('option', { name: textMock('resourceadm.deploy_at21_env') })); // wait for the second combobox to appear, instead of waiting for the spinner to disappear. // (sometimes the spinner disappears) too quick and the test will fail @@ -204,7 +213,7 @@ describe('ImportResourceModal', () => { textMock('resourceadm.dashboard_import_modal_select_env'), ); await user.click(environmentSelect); - await user.click(screen.getByRole('option', { name: 'AT21' })); + await user.click(screen.getByRole('option', { name: textMock('resourceadm.deploy_at21_env') })); // wait for the second combobox to appear, instead of waiting for the spinner to disappear. // (sometimes the spinner disappears) too quick and the test will fail @@ -236,7 +245,7 @@ describe('ImportResourceModal', () => { textMock('resourceadm.dashboard_import_modal_select_env'), ); await user.click(environmentSelect); - await user.click(screen.getByRole('option', { name: 'AT21' })); + await user.click(screen.getByRole('option', { name: textMock('resourceadm.deploy_at21_env') })); // wait for the second combobox to appear, instead of waiting for the spinner to disappear. // (sometimes the spinner disappears) too quick and the test will fail @@ -274,7 +283,7 @@ describe('ImportResourceModal', () => { textMock('resourceadm.dashboard_import_modal_select_env'), ); await user.click(environmentSelect); - await user.click(screen.getByRole('option', { name: 'AT21' })); + await user.click(screen.getByRole('option', { name: textMock('resourceadm.deploy_at21_env') })); // wait for the second combobox to appear, instead of waiting for the spinner to disappear. // (sometimes the spinner disappears) too quick and the test will fail diff --git a/frontend/resourceadm/components/ImportResourceModal/ImportResourceModal.tsx b/frontend/resourceadm/components/ImportResourceModal/ImportResourceModal.tsx index d9164265235..d0091e43b9b 100644 --- a/frontend/resourceadm/components/ImportResourceModal/ImportResourceModal.tsx +++ b/frontend/resourceadm/components/ImportResourceModal/ImportResourceModal.tsx @@ -16,9 +16,10 @@ import { ServerCodes } from 'app-shared/enums/ServerCodes'; import { useUrlParams } from '../../hooks/useSelectedContext'; import { StudioButton } from '@studio/components'; import { formatIdString } from '../../utils/stringUtils'; -import { getResourceIdentifierErrorMessage } from '../../utils/resourceUtils'; - -const environmentOptions = ['AT21', 'AT22', 'AT23', 'AT24', 'TT02', 'PROD']; +import { + getAvailableEnvironments, + getResourceIdentifierErrorMessage, +} from '../../utils/resourceUtils'; export type ImportResourceModalProps = { isOpen: boolean; @@ -60,6 +61,9 @@ export const ImportResourceModal = ({ const idErrorMessage = getResourceIdentifierErrorMessage(id, resourceIdExists); const hasValidValues = selectedEnv && selectedService && id.length >= 4 && !idErrorMessage && !isImportingResource; + + const environmentOptions = getAvailableEnvironments(selectedContext); + /** * Reset fields on close */ @@ -113,8 +117,8 @@ export const ImportResourceModal = ({ }} > {environmentOptions.map((env) => ( - - {env} + + {t(env.label)} ))} diff --git a/frontend/resourceadm/utils/resourceUtils/resourceUtils.ts b/frontend/resourceadm/utils/resourceUtils/resourceUtils.ts index fa6ae0bd01c..e16757431b6 100644 --- a/frontend/resourceadm/utils/resourceUtils/resourceUtils.ts +++ b/frontend/resourceadm/utils/resourceUtils/resourceUtils.ts @@ -43,7 +43,7 @@ export const availableForTypeMap: Record SelfRegisteredUser: 'resourceadm.about_resource_available_for_type_self_registered', }; -export type EnvId = 'tt02' | 'prod' | 'at22' | 'at23'; +export type EnvId = 'tt02' | 'prod' | 'at21' | 'at22' | 'at23' | 'at24'; export type EnvType = 'test' | 'prod'; export type Environment = { id: EnvId; @@ -52,6 +52,11 @@ export type Environment = { }; const environments: Record = { + ['at21']: { + id: 'at21' as EnvId, + label: 'resourceadm.deploy_at21_env', + envType: 'test' as EnvType, + }, ['at22']: { id: 'at22' as EnvId, label: 'resourceadm.deploy_at22_env', @@ -62,6 +67,11 @@ const environments: Record = { label: 'resourceadm.deploy_at23_env', envType: 'test' as EnvType, }, + ['at24']: { + id: 'at24' as EnvId, + label: 'resourceadm.deploy_at24_env', + envType: 'test' as EnvType, + }, ['tt02']: { id: 'tt02' as EnvId, label: 'resourceadm.deploy_test_env', @@ -77,7 +87,12 @@ const environments: Record = { export const getAvailableEnvironments = (org: string): Environment[] => { const availableEnvs = [environments['tt02'], environments['prod']]; if (org === 'ttd') { - availableEnvs.push(environments['at22'], environments['at23']); + availableEnvs.push( + environments['at21'], + environments['at22'], + environments['at23'], + environments['at24'], + ); } return availableEnvs; };