Skip to content

Commit

Permalink
resourceadm: change available fields for resource type "MaskinportenS…
Browse files Browse the repository at this point in the history
…chema" (#11933)

* refactor: create a common fieldset wrapper component to use for contact points and resource references

* remove self_identified user, enterprise user and available for type fields from about resource page when resource type is MaskinportenSchema

* add fields for resource references fields for resourceType MaskinportenSchema

* replace Select component with radio buttons for resource type and resource status

* hide translation panel when any non-translateable field is focused

* hide migrate tab unless resource has a reference with Altinn2 source
  • Loading branch information
mgunnerud authored Jan 8, 2024
1 parent 464ab50 commit 350fed5
Show file tree
Hide file tree
Showing 21 changed files with 825 additions and 377 deletions.
12 changes: 11 additions & 1 deletion frontend/language/src/nb.json
Original file line number Diff line number Diff line change
Expand Up @@ -712,7 +712,7 @@
"resourceadm.about_resource_contact_label_email": "E-post",
"resourceadm.about_resource_contact_label_telephone": "Telefon",
"resourceadm.about_resource_contact_legend": "Kontakt informasjon",
"resourceadm.about_resource_contact_point_error": "Du mangler å legge til et av felte for kontakt informasjon.",
"resourceadm.about_resource_contact_point_error": "Du mangler å legge til et av feltene for kontakt informasjon.",
"resourceadm.about_resource_contact_remove_button": "Slett kontaktinformasjon",
"resourceadm.about_resource_delegable_label": "Delegerbar",
"resourceadm.about_resource_delegable_show_text": "Ressursen {{showText}} være delegerbar.",
Expand All @@ -734,6 +734,16 @@
"resourceadm.about_resource_public_service_show": "skal",
"resourceadm.about_resource_public_service_show_text": "Ressursen {{showText}} vises i offentlige kataloger.",
"resourceadm.about_resource_public_service_text": "Etter publisering blir ressursen tilgjengelig i kataloger, blant annet i altinn, på norge.no og data.norge.no.",
"resourceadm.about_resource_reference": "Referanse",
"resourceadm.about_resource_reference_add_reference": "Legg til ny referanse",
"resourceadm.about_resource_reference_confirm_delete": "Er du sikker på at du vil slette denne referansen?",
"resourceadm.about_resource_reference_confirm_delete_button": "Slett referansen",
"resourceadm.about_resource_reference_delete": "Slett referanse",
"resourceadm.about_resource_reference_error": "Du må fylle ut alle referanse-feltene",
"resourceadm.about_resource_reference_source": "Referansekilde",
"resourceadm.about_resource_reference_type": "Referansetype",
"resourceadm.about_resource_references": "Ressursreferanser",
"resourceadm.about_resource_references_description": "Brukes kun av maskinporten",
"resourceadm.about_resource_resource_description_label": "Beskrivelse (Bokmål)",
"resourceadm.about_resource_resource_description_text": "Her må du beskrive tjenesten. Teksten kan bli synlig på flere områder på tvers av offentlige nettløsninger.",
"resourceadm.about_resource_resource_title_label": "Navn på tjenesten (Bokmål)",
Expand Down
18 changes: 10 additions & 8 deletions frontend/packages/shared/src/types/ResourceAdm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,16 @@ export interface Validation {
errors: any;
}

export type ResourceReferenceSource = 'Default' | 'Altinn2' | 'Altinn3' | 'ExternalPlatform';
export type ResourceReferenceType =
| 'Default'
| 'Uri'
| 'DelegationSchemeId'
| 'MaskinportenScope'
| 'ServiceCode'
| 'ServiceEditionCode';
export interface ResourceReference {
referenceSource?: 'Default' | 'Altinn1' | 'Altinn2' | 'Altinn3' | 'ExternalPlatform';
referenceSource?: ResourceReferenceSource;
reference?: string;
referenceType?:
| 'Default'
| 'Uri'
| 'DelegationSchemeId'
| 'MaskinportenScope'
| 'ServiceCode'
| 'ServiceEditionCode';
referenceType?: ResourceReferenceType;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
width: 32rem;
margin-top: var(--fds-spacing-2);
margin-bottom: var(--fds-spacing-3);
position: relative;
}

.buttonWrapper {
Expand Down
152 changes: 152 additions & 0 deletions frontend/resourceadm/components/FieldsetWrapper/FieldsetWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import React, { useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Modal } from '@digdir/design-system-react';
import classes from './FieldsetWrapper.module.css';

const DELETE_ID_NOT_SET = -1;

export type FieldsetWrapperProps<T> = {
/**
* The current list
*/
list: T[];
/**
* Function to be executed when list is changed
* @param list the updated list
* @returns void
*/
onListFieldChanged: (list: T[]) => void;
/**
* Translation keys for texts displayed by the wrapper
*/
translations: {
deleteButton: string;
deleteHeader: string;
deleteConfirmation: string;
deleteConfirmationButton: string;
addButton: string;
};
/**
* List object where all values are default values
*/
emptyItem: T;
/**
* Render function for rendering a list item.
* @param listItem the list item to render
* @param onChange function to call when item is changed. Call this function from child fieldset render on change
*/
renderItem: (listItem: T, onChange: (item: T) => void) => React.ReactNode;
};

/**
* @component
* Renders the list and calls the renderItem prop function for each list item
*
* @property {T[]}[list] - The current list
* @property {function}[onListFieldChanged] - Function to be executed when list is changed
* @property {Object}[translations] - Translation keys for texts displayed by the wrapper
* @property {T}[emptyItem] - List object where all values are default values
* @property {function}[renderItem] - Render function for rendering a list item.
*
* @returns {React.ReactNode} - The rendered component
*/
export const FieldsetWrapper = <T,>({
list,
onListFieldChanged,
emptyItem,
translations,
renderItem,
}: FieldsetWrapperProps<T>): React.ReactNode => {
const { t } = useTranslation();
const deleteModalRef = useRef<HTMLDialogElement>(null);

const [deleteId, setDeleteId] = useState<number>(DELETE_ID_NOT_SET);
const [listItems, setListItems] = useState<T[]>(list ?? [emptyItem]);

/**
* Adds a new empty list item to the list
*/
const handleClickAddButton = () => {
const updatedList = [...listItems, emptyItem];
setListItems(updatedList);
onListFieldChanged(updatedList);
};

/**
* Removes a list item in the deleteId position from the list
*/
const handleClickRemoveButton = () => {
const updatedList = listItems.filter((_item, index) => index !== deleteId);
onCloseDeleteModal();
setListItems(updatedList);
onListFieldChanged(updatedList);
};

/**
* Updates the list item when a field is changed
* @param listItem
* @param pos
*/
const onChangeListItemField = (listItem: T, pos: number) => {
const updatedList = [...listItems];
updatedList[pos] = listItem;
setListItems(updatedList);
onListFieldChanged(updatedList);
};

/**
* Closes the delete list item modal
*/
const onCloseDeleteModal = (): void => {
deleteModalRef.current?.close();
setDeleteId(DELETE_ID_NOT_SET);
};
/**
* Render each list item with renderItem() and display a delete button for each
*/
const displayFields = listItems.map((listItem: T, pos: number) => (
<div key={`${pos}/${listItems.length}`} className={classes.fieldset}>
<div className={classes.divider} />
{renderItem(listItem, (item: T) => {
onChangeListItemField(item, pos);
})}
<div className={classes.buttonWrapper}>
<Button
size='small'
color='danger'
aria-disabled={listItems.length < 2}
onClick={() => {
if (listItems.length > 1) {
deleteModalRef.current?.showModal();
setDeleteId(pos);
}
}}
>
{t(translations.deleteButton)}
</Button>
</div>
</div>
));

return (
<>
<Modal ref={deleteModalRef} onClose={onCloseDeleteModal}>
<Modal.Header>{t(translations.deleteHeader)}</Modal.Header>
<Modal.Content>{t(translations.deleteConfirmation)}</Modal.Content>
<Modal.Footer>
<Button color='danger' size='small' onClick={handleClickRemoveButton}>
{t(translations.deleteConfirmationButton)}
</Button>
<Button size='small' variant='tertiary' onClick={onCloseDeleteModal}>
{t('general.cancel')}
</Button>
</Modal.Footer>
</Modal>
<div className={classes.divider} />
{displayFields}
<Button size='small' onClick={handleClickAddButton}>
{t(translations.addButton)}
</Button>
</>
);
};
1 change: 1 addition & 0 deletions frontend/resourceadm/components/FieldsetWrapper/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { FieldsetWrapper } from './FieldsetWrapper';
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ describe('ResourceContactPointFields', () => {
const defaultProps: ResourceContactPointFieldsProps = {
contactPointList: mockContactPointList,
onContactPointsChanged: mockOnContactPointsChanged,
onFocus: jest.fn(),
showErrors: false,
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import React, { useRef, useState } from 'react';
import classes from './ResourceContactPointFields.module.css';
import React from 'react';
import type { ResourceContactPoint } from 'app-shared/types/ResourceAdm';
import { Button, Modal } from '@digdir/design-system-react';
import { useTranslation } from 'react-i18next';
import { ResourceContactPointFieldset } from './ResourceContactPointFieldset';

const DELETE_ID_NOT_SET = -1;
import { FieldsetWrapper } from '../FieldsetWrapper';

// Empty value for when adding a new field
const emptyContactPoint: ResourceContactPoint = {
Expand All @@ -26,6 +22,11 @@ export type ResourceContactPointFieldsProps = {
* @returns void
*/
onContactPointsChanged: (contactPoints: ResourceContactPoint[]) => void;
/**
* Function to be executed when the field is focused
* @returns void
*/
onFocus: () => void;
/**
* If the error should be shown
*/
Expand All @@ -39,110 +40,42 @@ export type ResourceContactPointFieldsProps = {
*
* @property {ResourceContactPoint[]}[contactPointList] - The current contact point list
* @property {function}[onContactPointsChanged] - Function to be executed when contact points are changed
* @property {function}[onFocus] - Function to be executed when the field is focused
* @property {boolean}[showErrors] - If the error should be shown
*
* @returns {React.ReactNode} - The rendered component
*/
export const ResourceContactPointFields = ({
contactPointList,
onContactPointsChanged,
onFocus,
showErrors,
}: ResourceContactPointFieldsProps): React.ReactNode => {
const { t } = useTranslation();
const deleteModalRef = useRef<HTMLDialogElement>(null);

const [deleteId, setDeleteId] = useState<number>(DELETE_ID_NOT_SET);
const [contactPoints, setContactPoints] = useState<ResourceContactPoint[]>(
contactPointList ?? [emptyContactPoint],
);

/**
* Adds the contact point to the list
*/
const handleClickAddButton = () => {
const updatedList = [...contactPoints, emptyContactPoint];
setContactPoints(updatedList);
onContactPointsChanged(updatedList);
};

/**
* Removes the contact point from the list
*/
const handleClickRemoveButton = () => {
const updatedList = contactPoints.filter((_cp, index) => index !== deleteId);
onCloseDeleteModal();
setContactPoints(updatedList);
onContactPointsChanged(updatedList);
};

/**
* Updates the contact points when leaving a field
* @param contactPoint
* @param pos
*/
const handleLeaveTextFields = (contactPoint: ResourceContactPoint, pos: number) => {
const updatedList = [...contactPoints];
updatedList[pos] = contactPoint;
setContactPoints(updatedList);
onContactPointsChanged(updatedList);
};

/**
* Closes the delete contact point modal
*/
const onCloseDeleteModal = (): void => {
deleteModalRef.current?.close();
setDeleteId(DELETE_ID_NOT_SET);
};
/**
* Displays each contact point as a group of 4 elements
*/
const displayContactFields = contactPoints.map(
(contactPoint: ResourceContactPoint, pos: number) => (
<div key={JSON.stringify(contactPoint)}>
<ResourceContactPointFieldset
contactPoint={contactPoint}
onLeaveTextFields={(cp: ResourceContactPoint) => handleLeaveTextFields(cp, pos)}
showErrors={showErrors}
/>
<Button
size='small'
color='danger'
aria-disabled={contactPoints.length < 2}
onClick={() => {
if (contactPoints.length > 1) {
deleteModalRef.current?.showModal();
setDeleteId(pos);
}
}}
>
{t('resourceadm.about_resource_contact_remove_button')}
</Button>
</div>
),
);

return (
<>
<Modal ref={deleteModalRef} onClose={onCloseDeleteModal}>
<Modal.Header>{t('resourceadm.about_resource_contact_remove_button')}</Modal.Header>
<Modal.Content>{t('resourceadm.about_resource_contact_confirm_remove')}</Modal.Content>
<Modal.Footer>
<Button color='danger' size='small' onClick={handleClickRemoveButton}>
{t('resourceadm.about_resource_contact_confirm_remove_button')}
</Button>
<Button size='small' variant='tertiary' onClick={onCloseDeleteModal}>
{t('general.cancel')}
</Button>
</Modal.Footer>
</Modal>
<div className={classes.divider} />
{displayContactFields}
<div className={classes.buttonWrapper}>
<Button size='small' onClick={handleClickAddButton}>
{t('resourceadm.about_resource_contact_add_button')}
</Button>
</div>
</>
<FieldsetWrapper<ResourceContactPoint>
list={contactPointList}
onListFieldChanged={onContactPointsChanged}
translations={{
deleteButton: 'resourceadm.about_resource_contact_remove_button',
deleteHeader: 'resourceadm.about_resource_contact_remove_button',
deleteConfirmation: 'resourceadm.about_resource_contact_confirm_remove',
deleteConfirmationButton: 'resourceadm.about_resource_contact_confirm_remove_button',
addButton: 'resourceadm.about_resource_contact_add_button',
}}
emptyItem={emptyContactPoint}
renderItem={(
contactPoint: ResourceContactPoint,
onChange: (changedItem: ResourceContactPoint) => void,
) => {
return (
<ResourceContactPointFieldset
contactPoint={contactPoint}
onLeaveTextFields={onChange}
onFocus={onFocus}
showErrors={showErrors}
/>
);
}}
/>
);
};
Loading

0 comments on commit 350fed5

Please sign in to comment.