diff --git a/frontend/language/src/nb.json b/frontend/language/src/nb.json
index 7d337cfac78..6fd1dd1471d 100644
--- a/frontend/language/src/nb.json
+++ b/frontend/language/src/nb.json
@@ -849,6 +849,8 @@
"resourceadm.about_resource_error_usage_string_title": "tittel",
"resourceadm.about_resource_homepage_label": "Hjemmeside",
"resourceadm.about_resource_homepage_text": "Lenke til informasjon om hvor sluttbruker kan finne tjenesten og informasjon om den.",
+ "resourceadm.about_resource_identifier_description": "Dette er en unik ID for ressursen både i URler og i APIer.",
+ "resourceadm.about_resource_identifier_label": "Ressurs-id",
"resourceadm.about_resource_keywords_label": "Nøkkelord",
"resourceadm.about_resource_keywords_text": "Ord som er enkle å søke på. Separer hvert ord med komma \",\".",
"resourceadm.about_resource_langauge_error_missing_1": "Du mangler oversettelse for {{usageString}} på {{lang}}.",
@@ -877,6 +879,7 @@
"resourceadm.about_resource_resource_title_label": "Navn på tjenesten (Bokmål)",
"resourceadm.about_resource_resource_title_text": "Navnet vil synes for brukere, og må være gjenkjennelig og beskrivende for hva tjenesten handler om. Pass på å skille tjenester som har like eller nesten like navn, slik at det blir lett for brukere å forstå.",
"resourceadm.about_resource_resource_type": "Ressurstype",
+ "resourceadm.about_resource_resource_type_brokerservice": "Formidlingstjeneste",
"resourceadm.about_resource_resource_type_error": "Du mangler å legge til ressurstype.",
"resourceadm.about_resource_resource_type_generic_access_resource": "Generisk tilgangsressurs",
"resourceadm.about_resource_resource_type_label": "Velg en ressurstype fra listen under.",
@@ -938,6 +941,7 @@
"resourceadm.dashboard_table_header_last_changed": "Sist endret",
"resourceadm.dashboard_table_header_name": "Navn",
"resourceadm.dashboard_table_header_policy_rules": "Tilgangsregler",
+ "resourceadm.dashboard_table_header_resourceid": "Ressurs-id",
"resourceadm.dashboard_table_row_edit": "Rediger ressurs",
"resourceadm.dashboard_table_row_has_policy": "Har tilgangsregler",
"resourceadm.dashboard_table_row_missing_policy": "Mangler tilgangsregler",
@@ -981,6 +985,7 @@
"resourceadm.left_nav_bar_organization_access": "Organisasjonstilganger",
"resourceadm.left_nav_bar_policy": "Tilgangsregler",
"resourceadm.listadmin_add_to_list": "Legg til i liste",
+ "resourceadm.listadmin_add_to_list_org": "Legg til {{org}} i liste",
"resourceadm.listadmin_back": "Tilbake til dashboard",
"resourceadm.listadmin_confirm_create_list": "Opprett tilgangsliste",
"resourceadm.listadmin_create_list": "Opprett ny tilgangsliste",
@@ -1014,6 +1019,7 @@
"resourceadm.listadmin_parties": "Enheter",
"resourceadm.listadmin_party": "Enhet",
"resourceadm.listadmin_remove_from_list": "Fjern fra liste",
+ "resourceadm.listadmin_remove_from_list_org": "Fjern {{org}} fra liste",
"resourceadm.listadmin_resource_header": "Tilgangslister for {{resourceTitle}} - {{env}}",
"resourceadm.listadmin_resource_list_checkbox_header": "Alle enheter og underenheter i valgte liste(r) kan bruke ressursen",
"resourceadm.listadmin_search": "Søk for å legge til ny enhet eller underenhet",
diff --git a/frontend/packages/shared/src/types/Altinn2LinkService.ts b/frontend/packages/shared/src/types/Altinn2LinkService.ts
index dec93277f47..e0247e66581 100644
--- a/frontend/packages/shared/src/types/Altinn2LinkService.ts
+++ b/frontend/packages/shared/src/types/Altinn2LinkService.ts
@@ -1,4 +1,5 @@
export interface Altinn2LinkService {
+ serviceOwnerCode: string;
serviceName: string;
externalServiceCode: string;
externalServiceEditionCode: string;
diff --git a/frontend/packages/shared/src/types/ResourceAdm.ts b/frontend/packages/shared/src/types/ResourceAdm.ts
index c05a9e269ae..5baf13a46cc 100644
--- a/frontend/packages/shared/src/types/ResourceAdm.ts
+++ b/frontend/packages/shared/src/types/ResourceAdm.ts
@@ -33,7 +33,11 @@ export interface ResourceContactPoint {
contactPage: string;
}
-export type ResourceTypeOption = 'GenericAccessResource' | 'Systemresource' | 'MaskinportenSchema';
+export type ResourceTypeOption =
+ | 'GenericAccessResource'
+ | 'Systemresource'
+ | 'MaskinportenSchema'
+ | 'BrokerService';
export type ResourceStatusOption = 'Completed' | 'Deprecated' | 'UnderDevelopment' | 'Withdrawn';
diff --git a/frontend/resourceadm/components/AccessListDetails/AccessListDetail.tsx b/frontend/resourceadm/components/AccessListDetails/AccessListDetail.tsx
index c4acb02981b..c4a11c5e7f5 100644
--- a/frontend/resourceadm/components/AccessListDetails/AccessListDetail.tsx
+++ b/frontend/resourceadm/components/AccessListDetails/AccessListDetail.tsx
@@ -84,7 +84,7 @@ export const AccessListDetail = ({
label={t('resourceadm.listadmin_list_id')}
description={t('resourceadm.listadmin_list_id_description')}
>
-
+
- {t('resourceadm.listadmin_remove_from_list')}
-
- >
- }
onButtonClick={(item: AccessListMember) => handleRemoveMember(item.orgNr)}
/>
{listItems.length === 0 && (
@@ -115,15 +109,8 @@ export const AccessListMembers = ({
- !!listItems.find((listItem) => disableItem.orgNr === listItem.orgNr)
- }
- buttonNode={
- <>
- {t('resourceadm.listadmin_add_to_list')}
-
- >
- }
+ disabledItems={listItems}
+ isAdd
onButtonClick={handleAddMember}
/>
{(isLoadingParties || isLoadingSubParties) && (
diff --git a/frontend/resourceadm/components/AccessListMembers/AccessListMembersTable.tsx b/frontend/resourceadm/components/AccessListMembers/AccessListMembersTable.tsx
index bf3d4826fff..8e4025878c3 100644
--- a/frontend/resourceadm/components/AccessListMembers/AccessListMembersTable.tsx
+++ b/frontend/resourceadm/components/AccessListMembers/AccessListMembersTable.tsx
@@ -4,24 +4,57 @@ import { Table } from '@digdir/design-system-react';
import type { AccessListMember } from 'app-shared/types/ResourceAdm';
import { StudioButton } from '@studio/components';
import classes from './AccessListMembers.module.css';
+import { PlusCircleIcon, MinusCircleIcon } from '@studio/icons';
+import { stringNumberToAriaLabel } from '../../utils/stringUtils';
interface AccessListMembersTableProps {
listItems: AccessListMember[];
- buttonNode: React.JSX.Element;
+ isAdd?: boolean;
isHeaderHidden?: boolean;
- disableButtonFn?: (member: AccessListMember) => boolean;
+ disabledItems?: AccessListMember[];
onButtonClick: (member: AccessListMember) => void;
}
export const AccessListMembersTable = ({
listItems,
- buttonNode,
+ isAdd,
isHeaderHidden,
- disableButtonFn,
+ disabledItems,
onButtonClick,
}: AccessListMembersTableProps): React.JSX.Element => {
const { t } = useTranslation();
+ const renderActionButton = (item: AccessListMember): React.JSX.Element => {
+ let buttonAriaLabel: string;
+ let buttonIcon: React.JSX.Element;
+ let buttonText: string;
+ if (isAdd) {
+ buttonAriaLabel = t('resourceadm.listadmin_add_to_list_org', { org: item.orgName });
+ buttonIcon = ;
+ buttonText = t('resourceadm.listadmin_add_to_list');
+ } else {
+ buttonAriaLabel = t('resourceadm.listadmin_remove_from_list_org', {
+ org: item.orgName,
+ });
+ buttonIcon = ;
+ buttonText = t('resourceadm.listadmin_remove_from_list');
+ }
+ return (
+ onButtonClick(item)}
+ disabled={
+ disabledItems && disabledItems.some((existingItem) => existingItem.orgNr === item.orgNr)
+ }
+ variant='tertiary'
+ size='small'
+ >
+ {buttonText}
+ {buttonIcon}
+
+ );
+ };
+
return (
@@ -42,23 +75,14 @@ export const AccessListMembersTable = ({
{listItems.map((item) => {
return (
- {item.orgNr}
+ {item.orgNr}
{item.orgName || t('resourceadm.listadmin_empty_name')}
{item.isSubParty
? t('resourceadm.listadmin_sub_party')
: t('resourceadm.listadmin_party')}
-
- onButtonClick(item)}
- disabled={disableButtonFn ? disableButtonFn(item) : false}
- variant='tertiary'
- size='small'
- >
- {buttonNode}
-
-
+ {renderActionButton(item)}
);
})}
diff --git a/frontend/resourceadm/components/ImportResourceModal/ImportResourceModal.test.tsx b/frontend/resourceadm/components/ImportResourceModal/ImportResourceModal.test.tsx
index 2e917b3a6c5..16ea5aa2b67 100644
--- a/frontend/resourceadm/components/ImportResourceModal/ImportResourceModal.test.tsx
+++ b/frontend/resourceadm/components/ImportResourceModal/ImportResourceModal.test.tsx
@@ -13,12 +13,13 @@ import type { Altinn2LinkService } from 'app-shared/types/Altinn2LinkService';
import { ServerCodes } from 'app-shared/enums/ServerCodes';
const mockAltinn2LinkService: Altinn2LinkService = {
+ serviceOwnerCode: 'ttd',
externalServiceCode: 'code1',
externalServiceEditionCode: 'edition1',
serviceName: 'TestService',
};
const mockAltinn2LinkServices: Altinn2LinkService[] = [mockAltinn2LinkService];
-const mockOption: string = `${mockAltinn2LinkService.externalServiceCode}-${mockAltinn2LinkService.externalServiceEditionCode}-${mockAltinn2LinkService.serviceName}`;
+const mockOption: string = `${mockAltinn2LinkService.serviceOwnerCode}: ${mockAltinn2LinkService.externalServiceCode}-${mockAltinn2LinkService.externalServiceEditionCode}-${mockAltinn2LinkService.serviceName}`;
const mockOnClose = jest.fn();
const getAltinn2LinkServices = jest
diff --git a/frontend/resourceadm/components/ImportResourceModal/ServiceContent/ServiceContent.test.tsx b/frontend/resourceadm/components/ImportResourceModal/ServiceContent/ServiceContent.test.tsx
index 12937896521..5620b1c6f60 100644
--- a/frontend/resourceadm/components/ImportResourceModal/ServiceContent/ServiceContent.test.tsx
+++ b/frontend/resourceadm/components/ImportResourceModal/ServiceContent/ServiceContent.test.tsx
@@ -15,11 +15,13 @@ const mockSelectedContext: string = 'selectedContext';
const mockEnv: string = 'env1';
const mockAltinn2LinkService: Altinn2LinkService = {
+ serviceOwnerCode: 'ttd',
externalServiceCode: 'code1',
externalServiceEditionCode: 'edition1',
serviceName: 'TestService',
};
const mockAltinn2HyphenLinkService: Altinn2LinkService = {
+ serviceOwnerCode: 'ttd',
externalServiceCode: 'code2',
externalServiceEditionCode: 'edition2',
serviceName: 'test-med---hyphens',
@@ -28,8 +30,8 @@ const mockAltinn2LinkServices: Altinn2LinkService[] = [
mockAltinn2LinkService,
mockAltinn2HyphenLinkService,
];
-const mockOption: string = `${mockAltinn2LinkService.externalServiceCode}-${mockAltinn2LinkService.externalServiceEditionCode}-${mockAltinn2LinkService.serviceName}`;
-const mockHyphenOption: string = `${mockAltinn2HyphenLinkService.externalServiceCode}-${mockAltinn2HyphenLinkService.externalServiceEditionCode}-${mockAltinn2HyphenLinkService.serviceName}`;
+const mockOption: string = `${mockAltinn2LinkService.serviceOwnerCode}: ${mockAltinn2LinkService.externalServiceCode}-${mockAltinn2LinkService.externalServiceEditionCode}-${mockAltinn2LinkService.serviceName}`;
+const mockHyphenOption: string = `${mockAltinn2HyphenLinkService.serviceOwnerCode}: ${mockAltinn2HyphenLinkService.externalServiceCode}-${mockAltinn2HyphenLinkService.externalServiceEditionCode}-${mockAltinn2HyphenLinkService.serviceName}`;
const mockOnSelectService = jest.fn();
diff --git a/frontend/resourceadm/components/ResourcePageInputs/ResourceTextField.tsx b/frontend/resourceadm/components/ResourcePageInputs/ResourceTextField.tsx
index 719c604d286..01f02ebaab7 100644
--- a/frontend/resourceadm/components/ResourcePageInputs/ResourceTextField.tsx
+++ b/frontend/resourceadm/components/ResourcePageInputs/ResourceTextField.tsx
@@ -44,6 +44,10 @@ type ResourceTextFieldProps = {
* Whether this field is required or not
*/
required?: boolean;
+ /**
+ * Whether this field is read only or not
+ */
+ readOnly?: boolean;
};
/**
@@ -59,6 +63,7 @@ type ResourceTextFieldProps = {
* @property {boolean}[showErrorMessage] - Flag for if the error message should be shown
* @property {string}[errorText] - The text to be shown
* @property {boolean}[required] - Whether this field is required or not
+ * @property {boolean}[readOnly] - Whether this field is read only or not
*
* @returns {React.JSX.Element} - The rendered component
*/
@@ -74,6 +79,7 @@ export const ResourceTextField = forwardRef {
@@ -94,6 +100,7 @@ export const ResourceTextField = forwardRef onBlur(val)}
required={required}
+ readOnly={readOnly}
/>
{showErrorMessage && }
diff --git a/frontend/resourceadm/components/ResourceTable/ResourceTable.tsx b/frontend/resourceadm/components/ResourceTable/ResourceTable.tsx
index 27e8d4b95ca..033dd865c0e 100644
--- a/frontend/resourceadm/components/ResourceTable/ResourceTable.tsx
+++ b/frontend/resourceadm/components/ResourceTable/ResourceTable.tsx
@@ -71,6 +71,11 @@ export const ResourceTable = ({
headerName: t('resourceadm.dashboard_table_header_name'),
width: 200,
},
+ {
+ field: 'identifier',
+ headerName: t('resourceadm.dashboard_table_header_resourceid'),
+ width: 200,
+ },
{
field: 'createdBy',
headerName: t('resourceadm.dashboard_table_header_createdby'),
diff --git a/frontend/resourceadm/pages/AboutResourcePage/AboutResourcePage.test.tsx b/frontend/resourceadm/pages/AboutResourcePage/AboutResourcePage.test.tsx
index a290706b53f..4f5991274dd 100644
--- a/frontend/resourceadm/pages/AboutResourcePage/AboutResourcePage.test.tsx
+++ b/frontend/resourceadm/pages/AboutResourcePage/AboutResourcePage.test.tsx
@@ -86,6 +86,17 @@ describe('AboutResourcePage', () => {
id: mockId,
};
+ it('handles resource id field blur', async () => {
+ render();
+
+ const idInput = screen.getByLabelText(textMock('resourceadm.about_resource_identifier_label'));
+
+ await act(() => idInput.focus());
+ await act(() => idInput.blur());
+
+ expect(mockOnSaveResource).not.toHaveBeenCalled();
+ });
+
it('handles resource type change', async () => {
const user = userEvent.setup();
render();
diff --git a/frontend/resourceadm/pages/AboutResourcePage/AboutResourcePage.tsx b/frontend/resourceadm/pages/AboutResourcePage/AboutResourcePage.tsx
index d2b981d914f..d9ea74c2ea4 100644
--- a/frontend/resourceadm/pages/AboutResourcePage/AboutResourcePage.tsx
+++ b/frontend/resourceadm/pages/AboutResourcePage/AboutResourcePage.tsx
@@ -102,6 +102,14 @@ export const AboutResourcePage = ({
{t('resourceadm.about_resource_title')}
+ setTranslationType('none')}
+ onBlur={() => {}}
+ />
{
const { t } = useTranslation();
diff --git a/frontend/resourceadm/pages/ResourcePage/ResourcePage.tsx b/frontend/resourceadm/pages/ResourcePage/ResourcePage.tsx
index 363ae6ee18f..c8f4f852ce6 100644
--- a/frontend/resourceadm/pages/ResourcePage/ResourcePage.tsx
+++ b/frontend/resourceadm/pages/ResourcePage/ResourcePage.tsx
@@ -286,7 +286,7 @@ export const ResourcePage = (): React.JSX.Element => {
onSaveVersion={(version: string) =>
handleSaveResource({
...resourceData,
- version,
+ version: version?.trim(), // empty version is not allowed
})
}
id='page-content-deploy'
diff --git a/frontend/resourceadm/utils/mapperUtils/mapperUtils.test.ts b/frontend/resourceadm/utils/mapperUtils/mapperUtils.test.ts
index 23e01921ee0..38eff0b40fc 100644
--- a/frontend/resourceadm/utils/mapperUtils/mapperUtils.test.ts
+++ b/frontend/resourceadm/utils/mapperUtils/mapperUtils.test.ts
@@ -5,23 +5,25 @@ describe('mapperUtils', () => {
describe('mapAltinn2LinkServiceToSelectOption', () => {
const mockLinkServices: Altinn2LinkService[] = [
{
+ serviceOwnerCode: 'ttd',
externalServiceCode: 'code1',
externalServiceEditionCode: 'edition1',
serviceName: 'name1',
},
{
+ serviceOwnerCode: 'acn',
externalServiceCode: 'code2',
externalServiceEditionCode: 'edition2',
serviceName: 'name2',
},
];
- it('should map Altinn2LinkService to SelectOption correctly', () => {
+ it('should map and sort Altinn2LinkService to SelectOption correctly', () => {
const result = mapAltinn2LinkServiceToSelectOption(mockLinkServices);
expect(result).toHaveLength(mockLinkServices.length);
- expect(result[0].value).toBe(JSON.stringify(mockLinkServices[0]));
- expect(result[0].label).toBe('code1-edition1-name1');
+ expect(result[0].value).toBe(JSON.stringify(mockLinkServices[1]));
+ expect(result[0].label).toBe('acn: code2-edition2-name2');
});
});
});
diff --git a/frontend/resourceadm/utils/mapperUtils/mapperUtils.ts b/frontend/resourceadm/utils/mapperUtils/mapperUtils.ts
index 486a27a1e41..421b83a3e5c 100644
--- a/frontend/resourceadm/utils/mapperUtils/mapperUtils.ts
+++ b/frontend/resourceadm/utils/mapperUtils/mapperUtils.ts
@@ -23,9 +23,15 @@ export const sortResourceListByDate = (resourceList: ResourceListItem[]): Resour
* @returns an object that looks like this: { value: string, label: string }
*/
export const mapAltinn2LinkServiceToSelectOption = (linkServices: Altinn2LinkService[]) => {
- return linkServices.map((ls: Altinn2LinkService) => ({
+ const sortedServices = [...linkServices].sort((a, b) => {
+ const serviceOwnerValue = a.serviceOwnerCode.localeCompare(b.serviceOwnerCode);
+ return serviceOwnerValue === 0
+ ? a.externalServiceCode.localeCompare(b.externalServiceCode)
+ : serviceOwnerValue;
+ });
+ return sortedServices.map((ls: Altinn2LinkService) => ({
value: JSON.stringify(ls),
- label: `${ls.externalServiceCode}-${ls.externalServiceEditionCode}-${ls.serviceName}`,
+ label: `${ls.serviceOwnerCode}: ${ls.externalServiceCode}-${ls.externalServiceEditionCode}-${ls.serviceName}`,
}));
};
diff --git a/frontend/resourceadm/utils/resourceUtils/resourceUtils.ts b/frontend/resourceadm/utils/resourceUtils/resourceUtils.ts
index 5c0da2b5f55..fa6ae0bd01c 100644
--- a/frontend/resourceadm/utils/resourceUtils/resourceUtils.ts
+++ b/frontend/resourceadm/utils/resourceUtils/resourceUtils.ts
@@ -19,6 +19,7 @@ export const resourceTypeMap: Record = {
GenericAccessResource: 'resourceadm.about_resource_resource_type_generic_access_resource',
Systemresource: 'resourceadm.about_resource_resource_type_system_resource',
MaskinportenSchema: 'resourceadm.about_resource_resource_type_maskinporten',
+ BrokerService: 'resourceadm.about_resource_resource_type_brokerservice',
};
/**
diff --git a/frontend/resourceadm/utils/stringUtils/index.ts b/frontend/resourceadm/utils/stringUtils/index.ts
index ed5b957f98a..75bf35bfce0 100644
--- a/frontend/resourceadm/utils/stringUtils/index.ts
+++ b/frontend/resourceadm/utils/stringUtils/index.ts
@@ -1 +1 @@
-export { formatIdString, isAppPrefix, isSePrefix } from './stringUtils';
+export { formatIdString, isAppPrefix, isSePrefix, stringNumberToAriaLabel } from './stringUtils';
diff --git a/frontend/resourceadm/utils/stringUtils/stringUtils.ts b/frontend/resourceadm/utils/stringUtils/stringUtils.ts
index b52664fc057..58758509a9c 100644
--- a/frontend/resourceadm/utils/stringUtils/stringUtils.ts
+++ b/frontend/resourceadm/utils/stringUtils/stringUtils.ts
@@ -15,3 +15,14 @@ export const isAppPrefix = (s: string): boolean => {
export const isSePrefix = (s: string): boolean => {
return s.substring(0, 3) === 'se_';
};
+
+/**
+ * Numbers with a specific meaning (postal numbers, phone numbers etc) should not be
+ * read by screen readers as millons-thousands etc, but rather as groups of numbers.
+ *
+ * @param s the string to format
+ * @returns the string formatted
+ */
+export const stringNumberToAriaLabel = (s: string): string => {
+ return s.split('').join(' ');
+};