Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enable visualization of code list usage in library #14308

Merged
merged 7 commits into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ describe('AppContentLibrary', () => {
});

it('renders a spinner when waiting for option lists', () => {
renderAppContentLibrary({ optionListsData: [] });
renderAppContentLibrary({ shouldPutDataOnCache: false });
const spinner = screen.getByText(textMock('general.loading'));
expect(spinner).toBeInTheDocument();
});
Expand Down Expand Up @@ -146,16 +146,19 @@ const goToLibraryPage = async (user: UserEvent, libraryPage: string) => {

type renderAppContentLibraryProps = {
queries?: Partial<ServicesContextProps>;
shouldPutDataOnCache?: boolean;
optionListsData?: OptionsListsResponse;
};

const renderAppContentLibrary = ({
queries = {},
shouldPutDataOnCache = true,
optionListsData = optionListsDataMock,
}: renderAppContentLibraryProps = {}) => {
const queryClientMock = createQueryClientMock();
if (optionListsData.length) {
if (shouldPutDataOnCache) {
queryClientMock.setQueryData([QueryKey.OptionLists, org, app], optionListsData);
queryClientMock.setQueryData([QueryKey.OptionListsUsage, org, app], []);
}
renderWithProviders(queries, queryClientMock)(<AppContentLibrary />);
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { CodeListWithMetadata } from '@studio/content-library';
import type { CodeListReference, CodeListWithMetadata } from '@studio/content-library';
import { ResourceContentLibraryImpl } from '@studio/content-library';
import React from 'react';
import { useOptionListsQuery } from 'app-shared/hooks/queries';
import { useOptionListsQuery, useOptionListsReferencesQuery } from 'app-shared/hooks/queries';
import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams';
import { convertOptionsListsDataToCodeListsData } from './utils/convertOptionsListsDataToCodeListsData';
import { StudioPageSpinner } from '@studio/components';
Expand All @@ -15,6 +15,7 @@ import {
useUpdateOptionListMutation,
useUpdateOptionListIdMutation,
} from 'app-shared/hooks/mutations';
import { mapToCodeListsUsage } from './utils/mapToCodeListsUsage';

export function AppContentLibrary(): React.ReactElement {
const { org, app } = useStudioEnvironmentParams();
Expand All @@ -28,12 +29,16 @@ export function AppContentLibrary(): React.ReactElement {
});
const { mutate: updateOptionList } = useUpdateOptionListMutation(org, app);
const { mutate: updateOptionListId } = useUpdateOptionListIdMutation(org, app);
const { data: optionListsUsages, isPending: optionListsUsageIsPending } =
useOptionListsReferencesQuery(org, app);

if (optionListsDataPending)
if (optionListsDataPending || optionListsUsageIsPending)
return <StudioPageSpinner spinnerTitle={t('general.loading')}></StudioPageSpinner>;

const codeListsData = convertOptionsListsDataToCodeListsData(optionListsData);

const codeListsUsages: CodeListReference[] = mapToCodeListsUsage({ optionListsUsages });

const handleUpdateCodeListId = (optionListId: string, newOptionListId: string) => {
updateOptionListId({ optionListId, newOptionListId });
};
Expand Down Expand Up @@ -63,6 +68,7 @@ export function AppContentLibrary(): React.ReactElement {
onUpdateCodeListId: handleUpdateCodeListId,
onUpdateCodeList: handleUpdate,
onUploadCodeList: handleUpload,
codeListsUsages,
},
},
images: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { CodeListIdSource } from '@studio/content-library';
import { mapToCodeListsUsage } from './mapToCodeListsUsage';
import type { OptionListsReferences } from 'app-shared/types/api/OptionsLists';

const optionListId: string = 'optionListId';
const optionListIdSources: CodeListIdSource[] = [
{
layoutSetId: 'layoutSetId',
layoutName: 'layoutName',
componentIds: ['componentId1', 'componentId2'],
},
];
const optionListsUsages: OptionListsReferences = [
{
optionListId,
optionListIdSources,
},
];

describe('mapToCodeListsUsage', () => {
it('maps optionListsUsage to codeListUsage', () => {
const codeListUsage = mapToCodeListsUsage({ optionListsUsages });
expect(codeListUsage).toEqual([
{
codeListId: optionListId,
codeListIdSources: optionListIdSources,
},
]);
});

it('maps undefined optionListsUsage to empty array', () => {
const codeListUsage = mapToCodeListsUsage({ optionListsUsages: undefined });
expect(codeListUsage).toEqual([]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { OptionListsReferences } from 'app-shared/types/api/OptionsLists';
import type { CodeListReference } from '@studio/content-library';

type MapToCodeListsUsageProps = {
optionListsUsages: OptionListsReferences;
};

export const mapToCodeListsUsage = ({
optionListsUsages,
}: MapToCodeListsUsageProps): CodeListReference[] => {
const codeListsUsages: CodeListReference[] = [];
if (!optionListsUsages) return codeListsUsages;
optionListsUsages.map((optionListsUsage) =>
codeListsUsages.push({
codeListId: optionListsUsage.optionListId,
codeListIdSources: optionListsUsage.optionListIdSources,
}),
);
return codeListsUsages;
};
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const mockPagesConfig: PagesConfig = {
onUpdateCodeListId: () => {},
onUpdateCodeList: () => {},
onUploadCodeList: () => {},
codeListsUsages: [],
},
},
images: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ const defaultCodeListPageProps: CodeListPageProps = {
onUpdateCodeListId: onUpdateCodeListIdMock,
onUpdateCodeList: onUpdateCodeListMock,
onUploadCodeList: onUploadCodeListMock,
codeListsUsages: [],
};

const renderCodeListPage = (props: Partial<CodeListPageProps> = {}) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { CodeLists } from './CodeLists';
import { CodeListsCounterMessage } from './CodeListsCounterMessage';
import classes from './CodeListPage.module.css';
import { ArrayUtils, FileNameUtils } from '@studio/pure-functions';
import type { CodeListReference } from './types/CodeListReference';

export type CodeListWithMetadata = {
codeList: CodeList;
Expand All @@ -24,13 +25,15 @@ export type CodeListPageProps = {
onUpdateCodeListId: (codeListId: string, newCodeListId: string) => void;
onUpdateCodeList: (updatedCodeList: CodeListWithMetadata) => void;
onUploadCodeList: (uploadedCodeList: File) => void;
codeListsUsages: CodeListReference[];
};

export function CodeListPage({
codeListsData,
onUpdateCodeListId,
onUpdateCodeList,
onUploadCodeList,
codeListsUsages,
}: CodeListPageProps): React.ReactElement {
const { t } = useTranslation();
const [codeListInEditMode, setCodeListInEditMode] = useState<string>(undefined);
Expand Down Expand Up @@ -62,6 +65,7 @@ export function CodeListPage({
onUpdateCodeList={onUpdateCodeList}
codeListInEditMode={codeListInEditMode}
codeListNames={codeListTitles}
codeListsUsages={codeListsUsages}
/>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import type { CodeListsProps } from './CodeLists';
import { updateCodeListWithMetadata, CodeLists } from './CodeLists';
import { getCodeListSourcesById, updateCodeListWithMetadata, CodeLists } from './CodeLists';
import { textMock } from '@studio/testing/mocks/i18nMock';
import type { CodeListWithMetadata } from '../CodeListPage';
import type { RenderResult } from '@testing-library/react';
import type { UserEvent } from '@testing-library/user-event';
import userEvent from '@testing-library/user-event';
import type { CodeList as StudioComponentsCodeList } from '@studio/components';
import { codeListsDataMock } from '../../../../../../mocks/mockPagesConfig';
import type { CodeListIdSource, CodeListReference } from '../types/CodeListReference';

const codeListName = codeListsDataMock[0].title;
const onUpdateCodeListIdMock = jest.fn();
Expand Down Expand Up @@ -124,6 +125,7 @@ const defaultProps: CodeListsProps = {
onUpdateCodeList: onUpdateCodeListMock,
codeListInEditMode: undefined,
codeListNames: [],
codeListsUsages: [],
};

const renderCodeLists = (props: Partial<CodeListsProps> = {}): RenderResult => {
Expand Down Expand Up @@ -156,3 +158,37 @@ describe('updateCodeListWithMetadata', () => {
});
});
});

const codeListId1: string = 'codeListId1';
const codeListId2: string = 'codeListId2';
const componentIds: string[] = ['componentId1', 'componentId2'];
const codeListIdSources1: CodeListIdSource[] = [
{ layoutSetId: 'layoutSetId', layoutName: 'layoutName', componentIds },
];
const codeListIdSources2: CodeListIdSource[] = [...codeListIdSources1];

describe('getCodeListSourcesById', () => {
it('returns an array of CodeListSources if given Id is present in codeListsUsages array', () => {
const codeListUsages: CodeListReference[] = [
{ codeListId: codeListId1, codeListIdSources: codeListIdSources1 },
{ codeListId: codeListId2, codeListIdSources: codeListIdSources2 },
];
const codeListSources = getCodeListSourcesById(codeListUsages, codeListId1);

expect(codeListSources).toBe(codeListIdSources1);
expect(codeListSources).not.toBe(codeListIdSources2);
});

it('returns an empty array if given Id is not present in codeListsUsages array', () => {
const codeListUsages: CodeListReference[] = [
{ codeListId: codeListId2, codeListIdSources: codeListIdSources2 },
];
const codeListSources = getCodeListSourcesById(codeListUsages, codeListId1);
expect(codeListSources).toEqual([]);
});

it('returns an empty array if codeListsUsages array is empty', () => {
const codeListSources = getCodeListSourcesById([], codeListId1);
expect(codeListSources).toEqual([]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import { Accordion } from '@digdir/designsystemet-react';
import { StudioAlert, type CodeList as StudioComponentsCodeList } from '@studio/components';
import { EditCodeList } from './EditCodeList/EditCodeList';
import { useTranslation } from 'react-i18next';
import type { CodeListIdSource, CodeListReference } from '../types/CodeListReference';

export type CodeListsProps = {
codeListsData: CodeListData[];
onUpdateCodeListId: (codeListId: string, newCodeListId: string) => void;
onUpdateCodeList: (updatedCodeList: CodeListWithMetadata) => void;
codeListInEditMode: string | undefined;
codeListNames: string[];
codeListsUsages: CodeListReference[];
};

export function CodeLists({
Expand All @@ -19,27 +21,46 @@ export function CodeLists({
onUpdateCodeList,
codeListInEditMode,
codeListNames,
codeListsUsages,
}: CodeListsProps) {
return codeListsData.map((codeListData) => (
<CodeList
key={codeListData.title}
codeListData={codeListData}
onUpdateCodeListId={onUpdateCodeListId}
onUpdateCodeList={onUpdateCodeList}
codeListInEditMode={codeListInEditMode}
codeListNames={codeListNames}
/>
));
return codeListsData.map((codeListData) => {
const codeListSources = getCodeListSourcesById(codeListsUsages, codeListData.title);
return (
<CodeList
key={codeListData.title}
codeListData={codeListData}
onUpdateCodeListId={onUpdateCodeListId}
onUpdateCodeList={onUpdateCodeList}
codeListInEditMode={codeListInEditMode}
codeListNames={codeListNames}
codeListSources={codeListSources}
/>
);
});
}

type CodeListProps = Omit<CodeListsProps, 'codeListsData'> & { codeListData: CodeListData };
export const getCodeListSourcesById = (
codeListsUsages: CodeListReference[],
codeListTitle: string,
): CodeListIdSource[] => {
const codeListUsages: CodeListReference | undefined = codeListsUsages.find(
(codeListUsage) => codeListUsage.codeListId === codeListTitle,
);
return codeListUsages?.codeListIdSources ?? [];
};

type CodeListProps = Omit<CodeListsProps, 'codeListsData' | 'codeListsUsages'> & {
codeListData: CodeListData;
codeListSources: CodeListIdSource[];
};

function CodeList({
codeListData,
onUpdateCodeListId,
onUpdateCodeList,
codeListInEditMode,
codeListNames,
codeListSources,
}: CodeListProps) {
const { t } = useTranslation();

Expand All @@ -58,6 +79,7 @@ function CodeList({
onUpdateCodeListId={onUpdateCodeListId}
onUpdateCodeList={onUpdateCodeList}
codeListNames={codeListNames}
codeListSources={codeListSources}
/>
</Accordion.Item>
</Accordion>
Expand All @@ -71,6 +93,7 @@ function CodeListAccordionContent({
onUpdateCodeListId,
onUpdateCodeList,
codeListNames,
codeListSources,
}: CodeListAccordionContentProps): React.ReactElement {
const { t } = useTranslation();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { CodeListPage } from './CodeListPage';
export type { CodeListWithMetadata, CodeListData, CodeListPageProps } from './CodeListPage';
export type { CodeListIdSource, CodeListReference } from './types/CodeListReference';
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export type CodeListReference = {
codeListId: string;
codeListIdSources: CodeListIdSource[];
};

export type CodeListIdSource = {
layoutSetId: string;
layoutName: string;
componentIds: string[];
};
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
export type { CodeListWithMetadata, CodeListData } from './CodeListPage';
export type {
CodeListWithMetadata,
CodeListData,
CodeListIdSource,
CodeListReference,
} from './CodeListPage';
7 changes: 6 additions & 1 deletion frontend/libs/studio-content-library/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
export { ResourceContentLibraryImpl } from './config/ContentResourceLibraryImpl';
export type { CodeListWithMetadata, CodeListData } from './ContentLibrary/LibraryBody/pages';
export type {
CodeListWithMetadata,
CodeListData,
CodeListIdSource,
CodeListReference,
} from './ContentLibrary/LibraryBody/pages';
1 change: 1 addition & 0 deletions frontend/packages/shared/src/api/paths.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const ruleHandlerPath = (org, app, layoutSetName) => `${basePath}/${org}/
export const widgetSettingsPath = (org, app) => `${basePath}/${org}/${app}/app-development/widget-settings`; // Get
export const optionListPath = (org, app, optionsListId) => `${basePath}/${org}/${app}/options/${optionsListId}`; // Get
export const optionListsPath = (org, app) => `${basePath}/${org}/${app}/options/option-lists`; // Get
export const optionListReferencesPath = (org, app) => `${basePath}/${org}/${app}/options/usage`; // Get
export const optionListIdsPath = (org, app) => `${basePath}/${org}/${app}/app-development/option-list-ids`; // Get
export const optionListUpdatePath = (org, app, optionsListId) => `${basePath}/${org}/${app}/options/${optionsListId}`; // Put
export const optionListIdUpdatePath = (org, app, optionsListId) => `${basePath}/${org}/${app}/options/change-name/${optionsListId}`; // Put
Expand Down
4 changes: 3 additions & 1 deletion frontend/packages/shared/src/api/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import {
selectedMaskinportenScopesPath,
resourceAccessPackageServicesPath,
optionListPath,
optionListReferencesPath,
} from './paths';

import type { AppReleasesResponse, DataModelMetadataResponse, SearchRepoFilterParams, SearchRepositoryResponse } from 'app-shared/types/api';
Expand Down Expand Up @@ -89,7 +90,7 @@ import type { Policy } from 'app-shared/types/Policy';
import type { RepoDiffResponse } from 'app-shared/types/api/RepoDiffResponse';
import type { ExternalImageUrlValidationResponse } from 'app-shared/types/api/ExternalImageUrlValidationResponse';
import type { MaskinportenScopes } from 'app-shared/types/MaskinportenScope';
import type { OptionsList, OptionsListsResponse } from 'app-shared/types/api/OptionsLists';
import type { OptionListsReferences, OptionsList, OptionsListsResponse } from 'app-shared/types/api/OptionsLists';
import type { LayoutSetsModel } from '../types/api/dto/LayoutSetsModel';
import type { AccessPackageResource, PolicyAccessPackageAreaGroup } from 'app-shared/types/PolicyAccessPackages';

Expand Down Expand Up @@ -118,6 +119,7 @@ export const getLayoutSets = (owner: string, app: string) => get<LayoutSets>(lay
export const getLayoutSetsExtended = (owner: string, app: string) => get<LayoutSetsModel>(layoutSetsPath(owner, app) + '/extended');
export const getOptionList = (owner: string, app: string, optionsListId: string) => get<OptionsList>(optionListPath(owner, app, optionsListId));
export const getOptionLists = (owner: string, app: string) => get<OptionsListsResponse>(optionListsPath(owner, app));
export const getOptionListsReferences = (owner: string, app: string) => get<OptionListsReferences>(optionListReferencesPath(owner, app));
export const getOptionListIds = (owner: string, app: string) => get<string[]>(optionListIdsPath(owner, app));
export const getOrgList = () => get<OrgList>(orgListUrl());
export const getOrganizations = () => get<Organization[]>(orgsListPath());
Expand Down
Loading
Loading