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: added reference and combination #13455

Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
dcef150
added reference and combination
JamalAlabdullah Aug 30, 2024
74f634d
Merge remote-tracking branch 'origin/main' into 13317-add-combination…
JamalAlabdullah Sep 2, 2024
4775d0d
updated
JamalAlabdullah Sep 2, 2024
957b1c6
Fixed a part of comments --WIP
JamalAlabdullah Sep 13, 2024
d16e22d
Merge branch 'main' into 13317-add-combination-and-referanse-types-to…
JamalAlabdullah Sep 13, 2024
cb23621
updated test
JamalAlabdullah Sep 14, 2024
46fb75f
Merge branch '13317-add-combination-and-referanse-types-to-dropdown-m…
JamalAlabdullah Sep 14, 2024
6582777
created new component AddPropertiesMenu and fixe som issues
JamalAlabdullah Sep 16, 2024
6ad9030
Merge branch 'main' into 13317-add-combination-and-referanse-types-to…
JamalAlabdullah Sep 16, 2024
6807f43
Merge remote-tracking branch 'origin/main' into 13317-add-combination…
JamalAlabdullah Sep 17, 2024
4fbdeb8
Merge branch '13317-add-combination-and-referanse-types-to-dropdown-m…
JamalAlabdullah Sep 17, 2024
917aa22
fix typecheck
JamalAlabdullah Sep 17, 2024
a7ac55b
updated test
JamalAlabdullah Sep 17, 2024
4030d86
added test
JamalAlabdullah Sep 17, 2024
9f7ed40
Merge remote-tracking branch 'origin/main' into 13317-add-combination…
JamalAlabdullah Sep 17, 2024
93cd6ae
added test
JamalAlabdullah Sep 17, 2024
4a7cb79
fixed part of comments --WIP
JamalAlabdullah Sep 18, 2024
c7858c0
Fixed comments --WIP
JamalAlabdullah Sep 19, 2024
bb71c6b
Merge remote-tracking branch 'origin/main' into 13317-add-combination…
JamalAlabdullah Sep 19, 2024
d1609fd
update test
JamalAlabdullah Sep 19, 2024
b4d02ea
update test --WIP
JamalAlabdullah Sep 19, 2024
babcea6
updated css
JamalAlabdullah Sep 19, 2024
e108ad3
fixed part of comments --WIP
JamalAlabdullah Sep 24, 2024
16d6e6a
moved related hooks files to ItemFieldType folder
JamalAlabdullah Sep 24, 2024
8ef8b37
update test
JamalAlabdullah Sep 24, 2024
1e10d76
update test
JamalAlabdullah Sep 24, 2024
f2e90b8
Merge remote-tracking branch 'origin/main' into 13317-add-combination…
JamalAlabdullah Sep 24, 2024
e4be8e3
fixe comments
JamalAlabdullah Sep 27, 2024
0e7c2ae
Merge remote-tracking branch 'origin/main' into 13317-add-combination…
JamalAlabdullah Sep 27, 2024
466154d
Merge remote-tracking branch 'origin/main' into 13317-add-combination…
JamalAlabdullah Sep 30, 2024
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
1 change: 1 addition & 0 deletions frontend/language/src/nb.json
Original file line number Diff line number Diff line change
Expand Up @@ -909,6 +909,7 @@
"schema_editor.pattern_test_field": "Testfelt for regex",
"schema_editor.promote": "Konverter til type",
"schema_editor.properties": "Egenskaper",
"schema_editor.reference": "Referanse",
"schema_editor.reference_to": "Refererer til",
"schema_editor.regex": "Regex",
"schema_editor.required": "Påkrevd",
Expand Down
3 changes: 3 additions & 0 deletions frontend/libs/studio-components/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,6 @@ export * from './StudioToggleableTextfield';
export * from './StudioToggleableTextfieldSchema';
export * from './StudioTreeView';
export * from './StudioTabs';
export * from './StudioDivider';
export * from './StudioPageError';
export * from './StudioError';
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
CombinationIcon,
ReferenceIcon,
ObjectIcon,
StringIcon,
BooleanIcon,
NumberIcon,
PlusIcon,
} from '@studio/icons';
import { ObjectKind, FieldType } from '@altinn/schema-model';
import { type StudioButtonProps, StudioDropdownMenu } from '@studio/components';

export interface AddPropertiesMenuProps {
onItemClick?: (kind: ObjectKind, fieldType?: FieldType) => void;
anchorButtonProps?: StudioButtonProps;
}

const propertyItems = [
{ kind: ObjectKind.Field, fieldType: FieldType.Object, icon: ObjectIcon },
{ kind: ObjectKind.Field, fieldType: FieldType.String, icon: StringIcon },
{ kind: ObjectKind.Field, fieldType: FieldType.Integer, icon: NumberIcon },
{ kind: ObjectKind.Field, fieldType: FieldType.Number, icon: NumberIcon },
{ kind: ObjectKind.Field, fieldType: FieldType.Boolean, icon: BooleanIcon },
{ kind: ObjectKind.Combination, icon: CombinationIcon },
{ kind: ObjectKind.Reference, icon: ReferenceIcon },
];

export const AddPropertiesMenu = ({ onItemClick, anchorButtonProps }: AddPropertiesMenuProps) => {
const { t } = useTranslation();

return (
<StudioDropdownMenu
anchorButtonProps={{
children: t('schema_editor.add_property'),
color: 'second',
icon: <PlusIcon />,
variant: 'secondary',
...anchorButtonProps,
}}
size='small'
placement='bottom-start'
>
{propertyItems.map(({ kind, fieldType, icon: Icon }) => (
<StudioDropdownMenu.Item
key={`${kind}-${fieldType}`}
onClick={() => onItemClick(kind, fieldType)}
icon={<Icon />}
>
{t(`schema_editor.add_${fieldType || kind}`)}
</StudioDropdownMenu.Item>
))}
</StudioDropdownMenu>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { AddPropertiesMenu } from './AddPropertiesMenu';
TomasEng marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -95,49 +95,67 @@ describe('ItemFieldsTab', () => {
expect(await screen.findByText(textAdd)).toBeDefined();
});

test('Model is saved with correct payload when a name is changed', async () => {
test('Should save the model when user clicks the dropdown menu items', async () => {
renderItemFieldsTab();
const suffix = 'Duck';
for (const fieldName of fieldNames) {
await user.type(screen.getByDisplayValue(fieldName), suffix);
await user.tab();
}
expect(saveDataModel).toHaveBeenCalledTimes(numberOfFields);
const selectMenuItem = async (item: string) => {
await user.click(screen.getByText(textAdd));
await user.click(screen.getByRole('menuitem', { name: item }));
};
await selectMenuItem(textMock('schema_editor.add_number'));
expect(saveDataModel).toHaveBeenCalledTimes(1);
await selectMenuItem(textMock('schema_editor.add_string'));
expect(saveDataModel).toHaveBeenCalledTimes(2);
await selectMenuItem(textMock('schema_editor.add_integer'));
expect(saveDataModel).toHaveBeenCalledTimes(3);
await selectMenuItem(textMock('schema_editor.add_boolean'));
expect(saveDataModel).toHaveBeenCalledTimes(4);
await selectMenuItem(textMock('schema_editor.add_object'));
expect(saveDataModel).toHaveBeenCalledTimes(5);
});

test('Should show dropdown menu items when the "Add field" button is clicked', async () => {
renderItemFieldsTab();
await user.click(screen.getByText(textAdd));
expect(screen.getByRole('dialog')).toBeInTheDocument();
TomasEng marked this conversation as resolved.
Show resolved Hide resolved
expect(
screen.getByRole('menuitem', { name: textMock('schema_editor.number') }),
screen.getByRole('menuitem', { name: textMock('schema_editor.add_number') }),
).toBeInTheDocument();
expect(screen.getByRole('menuitem', { name: textMock('schema_editor.string') }))
.toBeInTheDocument;
expect(
screen.getByRole('menuitem', { name: textMock('schema_editor.integer') }),
screen.getByRole('menuitem', { name: textMock('schema_editor.add_string') }),
).toBeInTheDocument();
expect(
screen.getByRole('menuitem', { name: textMock('schema_editor.boolean') }),
screen.getByRole('menuitem', { name: textMock('schema_editor.add_integer') }),
).toBeInTheDocument();
expect(
screen.getByRole('menuitem', { name: textMock('schema_editor.object') }),
screen.getByRole('menuitem', { name: textMock('schema_editor.add_boolean') }),
).toBeInTheDocument();
expect(
screen.getByRole('menuitem', { name: textMock('schema_editor.add_object') }),
).toBeInTheDocument();
expect(
screen.getByRole('menuitem', { name: textMock('schema_editor.add_combination') }),
).toBeInTheDocument();
expect(
screen.getByRole('menuitem', { name: textMock('schema_editor.add_reference') }),
).toBeInTheDocument();
});

test('should save the model when user clicks the dropdown menu items', async () => {
test('Should close the dropdown menu when user clicks outside the menu', async () => {
renderItemFieldsTab();
await user.click(screen.getByText(textAdd));
await user.click(screen.getByRole('menuitem', { name: textMock('schema_editor.number') }));
expect(saveDataModel).toHaveBeenCalledTimes(1);
await user.click(screen.getByRole('menuitem', { name: textMock('schema_editor.string') }));
expect(saveDataModel).toHaveBeenCalledTimes(2);
await user.click(screen.getByRole('menuitem', { name: textMock('schema_editor.integer') }));
expect(saveDataModel).toHaveBeenCalledTimes(3);
await user.click(screen.getByRole('menuitem', { name: textMock('schema_editor.boolean') }));
expect(saveDataModel).toHaveBeenCalledTimes(4);
await user.click(screen.getByRole('menuitem', { name: textMock('schema_editor.object') }));
expect(saveDataModel).toHaveBeenCalledTimes(5);
expect(screen.getByRole('dialog')).toBeInTheDocument();
await user.click(document.body);
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});

test('Model is saved with correct payload when a name is changed', async () => {
renderItemFieldsTab();
const suffix = 'Duck';
for (const fieldName of fieldNames) {
await user.type(screen.getByDisplayValue(fieldName), suffix);
await user.tab();
}
expect(saveDataModel).toHaveBeenCalledTimes(numberOfFields);
});

test('Model is saved correctly when a field is focused and the Enter key is clicked', async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import type { BaseSyntheticEvent } from 'react';
import React, { useEffect, useState } from 'react';
import type { FieldType, FieldNode } from '@altinn/schema-model';
import { isField, isReference, ObjectKind } from '@altinn/schema-model';
import React, { useEffect } from 'react';
import type { FieldType, FieldNode, ObjectKind } from '@altinn/schema-model';
import { isField, isReference } from '@altinn/schema-model';
import classes from './ItemFieldsTab.module.css';
import { StudioButton, usePrevious } from '@studio/components';
import { PlusIcon } from '@studio/icons';
import { useTranslation } from 'react-i18next';
import { usePrevious } from '@studio/components';
import { ItemFieldsTable } from './ItemFieldsTable';
import { useAddProperty } from '@altinn/schema-editor/hooks/useAddProperty';
import { getLastNameField } from '@altinn/schema-editor/components/SchemaInspector/ItemFieldsTab/domUtils';
import { DropdownMenu } from '@digdir/designsystemet-react';
import { useTypeOptions } from '../hooks/useTypeOptions';
import { AddPropertiesMenu } from '../../AddPropertiesMenu';

export interface ItemFieldsTabProps {
selectedItem: FieldNode;
Expand All @@ -21,7 +17,6 @@ export const ItemFieldsTab = ({ selectedItem }: ItemFieldsTabProps) => {

const numberOfChildNodes = selectedItem.children.length;
const prevNumberOfChildNodes = usePrevious<number>(numberOfChildNodes) ?? 0;
const typeOptions = useTypeOptions();

useEffect(() => {
// If the number of fields has increased, a new field has been added and should get focus
Expand All @@ -32,55 +27,18 @@ export const ItemFieldsTab = ({ selectedItem }: ItemFieldsTabProps) => {
}
}, [numberOfChildNodes, prevNumberOfChildNodes]);

const { t } = useTranslation();
const [isAddDropdownOpen, setIsAddDropdownOpen] = useState(false);

const onAddPropertyClicked = (event: BaseSyntheticEvent, fieldType: FieldType) => {
const onAddPropertyClicked = (kind: ObjectKind, fieldType?: FieldType) => {
event.preventDefault();

addProperty(ObjectKind.Field, fieldType, selectedItem.schemaPointer);
addProperty(kind, fieldType, selectedItem.schemaPointer);
};
const readonly = isReference(selectedItem);

const closeDropdown = () => setIsAddDropdownOpen(false);
return (
<div className={classes.root}>
{isField(selectedItem) && numberOfChildNodes > 0 && (
<ItemFieldsTable readonly={readonly} selectedItem={selectedItem} />
)}
<DropdownMenu
open={isAddDropdownOpen}
onClose={closeDropdown}
size='small'
portal
placement='bottom-start'
>
<DropdownMenu.Trigger asChild>
{!readonly && (
<StudioButton
color='second'
icon={<PlusIcon />}
onClick={() => setIsAddDropdownOpen(true)}
variant='secondary'
>
{t('schema_editor.add_property')}
</StudioButton>
)}
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Group>
{typeOptions.map(({ value: fieldType, label }) => (
<DropdownMenu.Item
key={fieldType}
value={fieldType}
onClick={(e) => onAddPropertyClicked(e, fieldType)}
>
{label}
</DropdownMenu.Item>
))}
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu>
<AddPropertiesMenu onItemClick={onAddPropertyClicked} />
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.linkButton {
border: none;
background-color: inherit;
font-size: inherit;
font-family: inherit;
padding: 0;
text-decoration: none;
TomasEng marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from 'react';
import { screen } from '@testing-library/react';
import { ItemFieldType } from './ItemFieldType';
import type { ItemFieldTypeProps } from './ItemFieldType';
import { type UiSchemaNode } from '@altinn/schema-model';
import {
combinationNodeMock,
referenceNodeMock,
stringDefinitionNodeMock,
objectNodeMock,
} from '../../../../../../test/mocks/uiSchemaMock';
import { renderWithProviders } from '../../../../../../test/renderWithProviders';
import { textMock } from '@studio/testing/mocks/i18nMock';
import userEvent from '@testing-library/user-event';

const defaultNode: UiSchemaNode = combinationNodeMock;
const setSelectedTypePointer = jest.fn();

const stringTypeLabel = textMock('schema_editor.string');
const objectTypeLabel = textMock('schema_editor.object');

const combinationKindLabel = textMock('schema_editor.combination');

describe('ItemFieldType', () => {
afterEach(jest.clearAllMocks);

it('should render string type label', () => {
renderItemFieldType({ fieldNode: stringDefinitionNodeMock });
expect(screen.getByText(stringTypeLabel)).toBeInTheDocument();
});

it('should render object type label', () => {
renderItemFieldType({ fieldNode: objectNodeMock });
expect(screen.getByText(objectTypeLabel)).toBeInTheDocument();
});

it('should render combination kind label', () => {
renderItemFieldType({ fieldNode: defaultNode });
expect(screen.getByText(combinationKindLabel)).toBeInTheDocument();
});

it('Should render reference link when fieldNode is a reference', async () => {
const user = userEvent.setup();
renderItemFieldType({ fieldNode: referenceNodeMock });
const linkButton = screen.getByRole('button');
await user.click(linkButton);
expect(setSelectedTypePointer).toHaveBeenCalledTimes(1);
});
TomasEng marked this conversation as resolved.
Show resolved Hide resolved
});

const renderItemFieldType = (props: ItemFieldTypeProps = { fieldNode: defaultNode }) => {
renderWithProviders({
appContextProps: {
setSelectedTypePointer,
},
})(<ItemFieldType {...props} />);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react';
import {
extractNameFromPointer,
type FieldType,
isField,
isReference,
type ReferenceNode,
type UiSchemaNode,
} from '@altinn/schema-model/index';
import { useSavableSchemaModel } from '../../../../../hooks/useSavableSchemaModel';
import { useSchemaEditorAppContext } from '../../../../../hooks/useSchemaEditorAppContext';
import { useTypeName } from './hooks/useTypeName';
import { useKindName } from './hooks/useKindName';
import { ObjectKind } from '@altinn/schema-model';
import { Link } from '@digdir/designsystemet-react';
import classes from './ItemFieldType.module.css';

export type ItemFieldTypeProps = {
fieldNode: UiSchemaNode;
};

export const ItemFieldType = ({ fieldNode }: ItemFieldTypeProps) => {
const typeName = useTypeName(isField(fieldNode) ? (fieldNode.fieldType as FieldType) : undefined);
const typeLabel = isField(fieldNode) && typeName;

const kindName = useKindName(fieldNode.objectKind);
const notReferenceKind = fieldNode.objectKind !== ObjectKind.Reference;
const kindLabel = notReferenceKind && kindName;

if (typeLabel) return <>{typeLabel}</>;
if (kindLabel) return <>{kindLabel}</>;
if (isReference(fieldNode)) return <ReferenceLink fieldNode={fieldNode} />;
return null;

Check warning on line 33 in frontend/packages/schema-editor/src/components/SchemaInspector/ItemFieldsTab/ItemFieldsTable/ItemFieldType/ItemFieldType.tsx

View check run for this annotation

Codecov / codecov/patch

frontend/packages/schema-editor/src/components/SchemaInspector/ItemFieldsTab/ItemFieldsTable/ItemFieldType/ItemFieldType.tsx#L33

Added line #L33 was not covered by tests
};

const ReferenceLink = ({ fieldNode }: { fieldNode: UiSchemaNode }) => {
const savableModel = useSavableSchemaModel();
const { setSelectedTypePointer } = useSchemaEditorAppContext();
const referredNode = savableModel.getReferredNode(fieldNode as ReferenceNode);
const name = extractNameFromPointer(referredNode.schemaPointer);

const handleClick = () => {
isReference(fieldNode) && setSelectedTypePointer(fieldNode.reference);
};

return (
<Link asChild onClick={handleClick}>
<button className={classes.linkButton}>{name}</button>
</Link>
);
};
TomasEng marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ObjectKind } from '@altinn/schema-model/types';
import { useTranslation } from 'react-i18next';

export function useKindName(objectKind: ObjectKind): string {
const { t } = useTranslation();

const kindNames = {
[ObjectKind.Field]: t('schema_editor.field'),
[ObjectKind.Combination]: t('schema_editor.combination'),
[ObjectKind.Reference]: t('schema_editor.reference'),
};

return kindNames[objectKind];
}
Loading