Skip to content

Commit

Permalink
feat(ui-editor): poc a modal for adding components to form (#13568)
Browse files Browse the repository at this point in the history
Co-authored-by: David Ovrelid <46874830+framitdavid@users.noreply.github.com>
  • Loading branch information
nkylstad and framitdavid authored Oct 25, 2024
1 parent 3deec23 commit 0808b19
Show file tree
Hide file tree
Showing 18 changed files with 538 additions and 12 deletions.
11 changes: 11 additions & 0 deletions frontend/language/src/nb.json
Original file line number Diff line number Diff line change
Expand Up @@ -1060,6 +1060,17 @@
"ux_editor.collapsable_text_advanced_components": "Avansert",
"ux_editor.collapsable_text_components": "Tekst",
"ux_editor.collapsable_text_widgets": "Widgets",
"ux_editor.component_add_item.info_component_selected": "Du har valgt komponenten {{componentName}}",
"ux_editor.component_add_item.info_heading": "Legg til komponent",
"ux_editor.component_add_item.info_no_component_selected": "Velg en komponent for å se informasjon og legge til",
"ux_editor.component_category.advanced": "Avansert",
"ux_editor.component_category.attachment": "Vedlegg",
"ux_editor.component_category.button": "Knapper",
"ux_editor.component_category.container": "Gruppering",
"ux_editor.component_category.form": "Skjema",
"ux_editor.component_category.info": "Informasjon",
"ux_editor.component_category.select": "Flervalg",
"ux_editor.component_category.text": "Tekst",
"ux_editor.component_deletion_confirm": "Ja, slett komponenten",
"ux_editor.component_deletion_text": "Er du sikker på at du vil slette denne komponenten?",
"ux_editor.component_dropdown_set_preselected": "Sett forhåndsvalgt verdi for nedtrekksliste",
Expand Down
1 change: 1 addition & 0 deletions frontend/packages/shared/src/utils/featureToggleUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type SupportedFeatureFlags =
| 'resourceMigration'
| 'multipleDataModelsPerTask'
| 'exportForm'
| 'addComponentModal'
| 'subform'
| 'summary2';

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
.allComponentsWrapper {
display: flex;
flex-direction: row;
flex-wrap: wrap;
height: 100%;
flex: 3;
padding: 20px;
overflow-y: scroll;
}

.componentButton {
height: 50px;
width: 180px;
margin: 8px;
}

.componentCategory {
padding-top: 12px;
}

.componentHelpText {
width: 100%;
}

.componentsInfoWrapper {
flex: 1;
background-color: var(--fds-semantic-surface-info-subtle);
padding: 20px;
height: 600px;
position: sticky;
}

.root {
display: flex;
flex-direction: row;
overflow-y: hidden;
height: 100%;
/* min-width: 70vw; */
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React from 'react';
import type { ComponentType } from 'app-shared/types/ComponentType';
import type { KeyValuePairs } from 'app-shared/types/KeyValuePairs';
import type { IToolbarElement } from '../../../types/global';
import classes from './AddItemContent.module.css';
import { ItemCategory } from './ItemCategory';
import type { AddedItem } from './types';
import { ItemInfo } from './ItemInfo';
import { useFormLayouts } from '../../../hooks';
import { generateComponentId } from '../../../utils/generateId';
import { StudioParagraph } from '@studio/components';

export type AddItemContentProps = {
item: AddedItem | null;
setItem: (item: AddedItem | null) => void;
onAddItem: (addedItem: AddedItem) => void;
onCancel: () => void;
availableComponents: KeyValuePairs<IToolbarElement[]>;
};

export const AddItemContent = ({
item,
setItem,
onAddItem,
onCancel,
availableComponents,
}: AddItemContentProps) => {
const layouts = useFormLayouts();

return (
<div className={classes.root}>
<div className={classes.allComponentsWrapper}>
<StudioParagraph spacing size='small' style={{ width: '100%' }}>
Klikk på en komponent for å se mer informasjon om den.
</StudioParagraph>
{Object.keys(availableComponents).map((key) => {
return (
<ItemCategory
key={key}
category={key}
items={availableComponents[key]}
selectedItemType={item?.componentType}
setAddedItem={setItem}
generateComponentId={(type: ComponentType) => generateComponentId(type, layouts)}
/>
);
})}
</div>
<div className={classes.componentsInfoWrapper}>
<ItemInfo
onAddItem={onAddItem}
onCancel={onCancel}
generateComponentId={(type: ComponentType) => generateComponentId(type, layouts)}
item={item}
setItem={setItem}
/>
</div>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import React, { useCallback, useRef } from 'react';
import {
addItemOfType,
getAvailableChildComponentsForContainer,
getItem,
} from '../../../utils/formLayoutUtils';
import { useAddItemToLayoutMutation } from '../../../hooks/mutations/useAddItemToLayoutMutation';
import { useFormItemContext } from '../../FormItemContext';
import { useAppContext } from '../../../hooks';
import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams';
import type { IInternalLayout } from '../../../types/global';
import type { ComponentType } from 'app-shared/types/ComponentType';
import { StudioButton, StudioModal } from '@studio/components';
import type { AddedItem } from './types';
import { BASE_CONTAINER_ID } from 'app-shared/constants';
import { AddItemContent } from './AddItemContent';
import { PlusCircleIcon } from '@studio/icons';
import { usePreviewContext } from 'app-development/contexts/PreviewContext';

export type AddItemProps = {
containerId: string;
layout: IInternalLayout;
};

export const AddItemModal = ({ containerId, layout }: AddItemProps) => {
const [selectedItem, setSelectedItem] = React.useState<AddedItem | null>(null);

const { doReloadPreview } = usePreviewContext();
const handleCloseModal = () => {
setSelectedItem(null);
modalRef.current?.close();
};
const { handleEdit } = useFormItemContext();

const { org, app } = useStudioEnvironmentParams();
const { selectedFormLayoutSetName } = useAppContext();

const { mutate: addItemToLayout } = useAddItemToLayoutMutation(
org,
app,
selectedFormLayoutSetName,
);

const modalRef = useRef<HTMLDialogElement>(null);

const addItem = (type: ComponentType, parentId: string, index: number, newId: string) => {
const updatedLayout = addItemOfType(layout, type, newId, parentId, index);

addItemToLayout(
{ componentType: type, newId, parentId, index },
{
onSuccess: () => {
doReloadPreview();
},
},
);
handleEdit(getItem(updatedLayout, newId));
};

const onAddComponent = (addedItem: AddedItem) => {
addItem(
addedItem.componentType,
containerId,
layout.order[containerId].length,
addedItem.componentId,
);
handleCloseModal();
};

const handleOpenModal = useCallback(() => {
modalRef.current?.showModal();
}, []);

return (
<div style={{ display: 'flex', justifyContent: 'center', marginLeft: 12, marginRight: 12 }}>
<StudioModal.Root>
<StudioButton
icon={<PlusCircleIcon />}
onClick={handleOpenModal}
variant='tertiary'
fullWidth
>
Legg til komponent
</StudioButton>
<StudioModal.Dialog
onClose={handleCloseModal}
heading={'Velg komponent'}
closeButtonTitle='Lukk'
style={{ minWidth: '80vw', overflowY: 'hidden' }}
ref={modalRef}
>
<AddItemContent
item={selectedItem}
setItem={setSelectedItem}
onAddItem={onAddComponent}
onCancel={handleCloseModal}
availableComponents={getAvailableChildComponentsForContainer(layout, BASE_CONTAINER_ID)}
/>
</StudioModal.Dialog>
</StudioModal.Root>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
.componentButton {
margin: 8px;
justify-content: start;
width: 270px;
}

.componentsWrapper {
display: flex;
flex-direction: column;
height: 100%;
flex-wrap: wrap;
justify-content: start;
align-items: start;
}

.itemCategory {
margin-bottom: 12px;
margin-right: 12px;
max-width: calc(50% - 12px);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React from 'react';
import { StudioButton, StudioCard, StudioHeading } from '@studio/components';
import classes from './ItemCategory.module.css';
import { useTranslation } from 'react-i18next';
import type { IToolbarElement } from '../../../../types/global';
import type { AddedItem } from '../types';
import type { ComponentType } from 'app-shared/types/ComponentType';
import { getComponentTitleByComponentType } from '../../../../utils/language';

export type ItemCategoryProps = {
items: IToolbarElement[];
category: string;
selectedItemType: ComponentType;
setAddedItem(addedItem: AddedItem): void;
generateComponentId: (type: string) => string;
};

export const ItemCategory = ({
items,
category,
selectedItemType,
setAddedItem,
generateComponentId,
}: ItemCategoryProps) => {
const { t } = useTranslation();

return (
<StudioCard color='subtle' className={classes.itemCategory}>
<StudioHeading level={2} size='small'>
{t(`ux_editor.component_category.${category}`)}
</StudioHeading>
<div className={classes.componentsWrapper}>
{items.map((item: IToolbarElement) => (
<ComponentButton
tooltipContent={getComponentTitleByComponentType(item.type, t) || item.label}
selected={selectedItemType === item.type}
key={item.type}
icon={item.icon}
onClick={() => {
setAddedItem({
componentType: item.type,
componentId: generateComponentId(item.type),
});
}}
/>
))}
</div>
</StudioCard>
);
};

type ComponentButtonProps = {
tooltipContent: string;
selected: boolean;
icon: React.ComponentType;
onClick: () => void;
};
function ComponentButton({ tooltipContent, selected, icon, onClick }: ComponentButtonProps) {
return (
<StudioButton
variant={selected ? 'primary' : 'tertiary'}
onClick={onClick}
size='sm'
aria-label={tooltipContent}
className={classes.componentButton}
title={tooltipContent}
icon={React.createElement(icon, { fontSize: '1.5rem' } as any)}
>
{tooltipContent}
</StudioButton>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ItemCategory } from './ItemCategory';
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
.allComponentsWrapper {
display: flex;
flex-direction: column;
height: 100%;
flex: 3;
padding: 20px;
}

.componentButton {
height: 50px;
width: 180px;
margin: 8px;
}

.componentCategory {
padding-top: 12px;
}

.componentHelpText {
width: 100%;
}

.componentsInfoWrapper {
flex: 1;
background-color: var(--fds-semantic-surface-info-subtle);
padding: 20px;
}

.componentsWrapper {
display: flex;
flex-direction: row;
height: 100%;
flex-wrap: wrap;
}

.root {
min-width: 360px;
}
Loading

0 comments on commit 0808b19

Please sign in to comment.