-
-
- {t('policy_editor.rules')}
-
-
-
-
- {t('policy_editor.alert', {
- usageType:
- usageType === 'app'
- ? t('policy_editor.alert_app')
- : t('policy_editor.alert_resource'),
- })}
-
-
-
- {displayRules}
-
-
+
+
+
+
+ {t('policy_editor.rules')}
+
+
+
+
- setVerificationModalOpen(false)}
- text={t('policy_editor.verification_modal_text')}
- closeButtonText={t('policy_editor.verification_modal_close_button')}
- actionButtonText={t('policy_editor.verification_modal_action_button')}
- onPerformAction={() => handleDeleteRule(ruleIdToDelete)}
- />
-
+
);
};
+
+// TODO - Find out how this should be set. Issue: #10880
+const getResourceType = (usageType: PolicyEditorUsage): string => {
+ return usageType === 'app' ? 'urn:altinn' : 'urn:altinn:resource';
+};
diff --git a/frontend/packages/policy-editor/src/components/CardButton/CardButton.module.css b/frontend/packages/policy-editor/src/components/AddPolicyRuleButton/AddPolicyRuleButton.module.css
similarity index 86%
rename from frontend/packages/policy-editor/src/components/CardButton/CardButton.module.css
rename to frontend/packages/policy-editor/src/components/AddPolicyRuleButton/AddPolicyRuleButton.module.css
index dc1dba661bf..ce247a80bce 100644
--- a/frontend/packages/policy-editor/src/components/CardButton/CardButton.module.css
+++ b/frontend/packages/policy-editor/src/components/AddPolicyRuleButton/AddPolicyRuleButton.module.css
@@ -3,10 +3,10 @@
display: flex;
align-items: center;
justify-content: space-between;
- padding: 20px;
+ padding: var(--fds-spacing-5);
margin: 0;
background-color: var(--fds-semantic-surface-neutral-default);
- border-radius: 12px;
+ border-radius: var(--fds-sizing-3);
border: solid 1px var(--fds-semantic-border-divider-default);
cursor: pointer;
}
@@ -23,3 +23,7 @@
outline-offset: var(--focus-border-width);
box-shadow: 0 0 0 var(--focus-border-width) var(--focus-border-inner-color);
}
+
+.icon {
+ font-size: var(--fds-sizing-6);
+}
diff --git a/frontend/packages/policy-editor/src/components/AddPolicyRuleButton/AddPolicyRuleButton.test.tsx b/frontend/packages/policy-editor/src/components/AddPolicyRuleButton/AddPolicyRuleButton.test.tsx
new file mode 100644
index 00000000000..a9aade3a396
--- /dev/null
+++ b/frontend/packages/policy-editor/src/components/AddPolicyRuleButton/AddPolicyRuleButton.test.tsx
@@ -0,0 +1,49 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import { AddPolicyRuleButton, type AddPolicyRuleButtonProps } from './AddPolicyRuleButton';
+import { textMock } from '@studio/testing/mocks/i18nMock';
+import userEvent from '@testing-library/user-event';
+import { PolicyEditorContext } from '../../contexts/PolicyEditorContext';
+import { mockPolicyEditorContextValue } from '../../../test/mocks/policyEditorContextMock';
+
+const mockOnClick = jest.fn();
+const defaultProps: AddPolicyRuleButtonProps = {
+ onClick: mockOnClick,
+};
+
+describe('AddPolicyRuleButton', () => {
+ afterEach(jest.clearAllMocks);
+
+ it('calls the onClick function when clicked', async () => {
+ const user = userEvent.setup();
+ renderAddPolicyRuleButton();
+
+ const buttonElement = screen.getByRole('button', {
+ name: textMock('policy_editor.card_button_text'),
+ });
+ await user.click(buttonElement);
+
+ expect(mockOnClick).toHaveBeenCalledTimes(1);
+ });
+
+ it('calls "savePolicy" when a new rule is added', async () => {
+ const user = userEvent.setup();
+ renderAddPolicyRuleButton();
+
+ const addButton = screen.getByRole('button', {
+ name: textMock('policy_editor.card_button_text'),
+ });
+
+ await user.click(addButton);
+
+ expect(mockPolicyEditorContextValue.savePolicy).toHaveBeenCalledTimes(1);
+ });
+});
+
+const renderAddPolicyRuleButton = () => {
+ return render(
+
+
+ ,
+ );
+};
diff --git a/frontend/packages/policy-editor/src/components/AddPolicyRuleButton/AddPolicyRuleButton.tsx b/frontend/packages/policy-editor/src/components/AddPolicyRuleButton/AddPolicyRuleButton.tsx
new file mode 100644
index 00000000000..a8c3af05f41
--- /dev/null
+++ b/frontend/packages/policy-editor/src/components/AddPolicyRuleButton/AddPolicyRuleButton.tsx
@@ -0,0 +1,45 @@
+import React from 'react';
+import classes from './AddPolicyRuleButton.module.css';
+import { PlusIcon } from '@studio/icons';
+import { Paragraph } from '@digdir/design-system-react';
+import { usePolicyEditorContext } from '../../contexts/PolicyEditorContext';
+import type { PolicyRuleResource, PolicyRuleCard } from '../../types';
+import { emptyPolicyRule, createNewPolicyResource, getNewRuleId } from '../../utils';
+import { useTranslation } from 'react-i18next';
+
+export type AddPolicyRuleButtonProps = {
+ onClick: () => void;
+};
+
+export const AddPolicyRuleButton = ({ onClick }: AddPolicyRuleButtonProps): React.ReactNode => {
+ const { policyRules, setPolicyRules, savePolicy, usageType, resourceId, resourceType } =
+ usePolicyEditorContext();
+ const { t } = useTranslation();
+
+ const handleAddCardClick = () => {
+ onClick();
+
+ const newResource: PolicyRuleResource[][] = [
+ createNewPolicyResource(usageType, resourceType, resourceId),
+ ];
+ const newRuleId: string = getNewRuleId(policyRules);
+
+ const newRule: PolicyRuleCard = {
+ ...emptyPolicyRule,
+ ruleId: newRuleId,
+ resources: newResource,
+ };
+
+ const updatedRules: PolicyRuleCard[] = [...policyRules, ...[newRule]];
+
+ setPolicyRules(updatedRules);
+ savePolicy(updatedRules);
+ };
+
+ return (
+
+ );
+};
diff --git a/frontend/packages/policy-editor/src/components/AddPolicyRuleButton/index.ts b/frontend/packages/policy-editor/src/components/AddPolicyRuleButton/index.ts
new file mode 100644
index 00000000000..ead818f34b6
--- /dev/null
+++ b/frontend/packages/policy-editor/src/components/AddPolicyRuleButton/index.ts
@@ -0,0 +1 @@
+export { AddPolicyRuleButton } from './AddPolicyRuleButton';
diff --git a/frontend/packages/policy-editor/src/components/CardButton/CardButton.test.tsx b/frontend/packages/policy-editor/src/components/CardButton/CardButton.test.tsx
deleted file mode 100644
index ff098b632c8..00000000000
--- a/frontend/packages/policy-editor/src/components/CardButton/CardButton.test.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import React from 'react';
-import { render, screen } from '@testing-library/react';
-import type { CardButtonProps } from './CardButton';
-import { CardButton } from './CardButton';
-import { textMock } from '@studio/testing/mocks/i18nMock';
-import userEvent from '@testing-library/user-event';
-
-const mockButtonText = textMock('policy_editor.card_button_text');
-
-describe('CardButton', () => {
- afterEach(jest.clearAllMocks);
-
- const mockOnClick = jest.fn();
-
- const defaultProps: CardButtonProps = {
- buttonText: mockButtonText,
- onClick: mockOnClick,
- };
-
- it('calls the onClick function when clicked', async () => {
- const user = userEvent.setup();
- render(
);
-
- const buttonElement = screen.getByRole('button', { name: mockButtonText });
- await user.click(buttonElement);
-
- expect(mockOnClick).toHaveBeenCalledTimes(1);
- });
-});
diff --git a/frontend/packages/policy-editor/src/components/CardButton/CardButton.tsx b/frontend/packages/policy-editor/src/components/CardButton/CardButton.tsx
deleted file mode 100644
index 2ccd9488cb1..00000000000
--- a/frontend/packages/policy-editor/src/components/CardButton/CardButton.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import React from 'react';
-import classes from './CardButton.module.css';
-import { PlusIcon } from '@studio/icons';
-import { Paragraph } from '@digdir/design-system-react';
-
-export type CardButtonProps = {
- /**
- * The text to display on the button
- */
- buttonText: string;
- /**
- * Function to handle buttonclick
- * @returns void
- */
- onClick: () => void;
-};
-
-/**
- * @component
- * Button component that displays a text and a plus icon.
- *
- * @property {string}[buttonText] - The text to display on the button
- * @property {function}[onClick] - Function to handle buttonclick
- *
- * @returns {React.ReactNode} - The rendered component
- */
-export const CardButton = ({ buttonText, onClick }: CardButtonProps): React.ReactNode => {
- return (
-
- );
-};
diff --git a/frontend/packages/policy-editor/src/components/CardButton/index.ts b/frontend/packages/policy-editor/src/components/CardButton/index.ts
deleted file mode 100644
index 38a74ae5245..00000000000
--- a/frontend/packages/policy-editor/src/components/CardButton/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { CardButton } from './CardButton';
diff --git a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ActionAndSubjectListItem/ActionAndSubjectListItem.module.css b/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ActionAndSubjectListItem/ActionAndSubjectListItem.module.css
deleted file mode 100644
index 4a5b86e1a8f..00000000000
--- a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ActionAndSubjectListItem/ActionAndSubjectListItem.module.css
+++ /dev/null
@@ -1,5 +0,0 @@
-.wrapper {
- margin: 0;
- margin-right: 5px;
- margin-top: 5px;
-}
diff --git a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ActionAndSubjectListItem/ActionAndSubjectListItem.test.tsx b/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ActionAndSubjectListItem/ActionAndSubjectListItem.test.tsx
deleted file mode 100644
index 7e2259cf56f..00000000000
--- a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ActionAndSubjectListItem/ActionAndSubjectListItem.test.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import React from 'react';
-import { render, screen } from '@testing-library/react';
-import type { ActionAndSubjectListItemProps } from './ActionAndSubjectListItem';
-import { ActionAndSubjectListItem } from './ActionAndSubjectListItem';
-import userEvent from '@testing-library/user-event';
-
-const mockChipTitle: string = 'Test';
-
-describe('ActionAndSubjectListItem', () => {
- afterEach(jest.clearAllMocks);
-
- const mockOnRemove = jest.fn();
-
- const defaultProps: ActionAndSubjectListItemProps = {
- title: mockChipTitle,
- onRemove: mockOnRemove,
- };
-
- it('calls the onRemove function when the chip is clicked', async () => {
- const user = userEvent.setup();
- render(
);
-
- const chipElement = screen.getByText(mockChipTitle);
- await user.click(chipElement);
-
- expect(mockOnRemove).toHaveBeenCalledTimes(1);
- });
-});
diff --git a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ActionAndSubjectListItem/ActionAndSubjectListItem.tsx b/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ActionAndSubjectListItem/ActionAndSubjectListItem.tsx
deleted file mode 100644
index 0cf00f60f5d..00000000000
--- a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ActionAndSubjectListItem/ActionAndSubjectListItem.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import React from 'react';
-import classes from './ActionAndSubjectListItem.module.css';
-import { Chip } from '@digdir/design-system-react';
-import { useTranslation } from 'react-i18next';
-
-export type ActionAndSubjectListItemProps = {
- /**
- * The title to display
- */
- title: string;
- /**
- * Function that removes the element from the list
- */
- onRemove: () => void;
-};
-
-/**
- * @component
- * Displays the list of subjects that belongs to a rule in a card in the policy editor.
- *
- * @example
- *
- *
- * @property {string}[title] - The title to display
- * @property {function}[onRemove] - Function that removes the element from the list
- *
- * @returns {React.ReactNode} - The rendered Chip element
- */
-export const ActionAndSubjectListItem = ({
- title,
- onRemove,
-}: ActionAndSubjectListItemProps): React.ReactNode => {
- const { t } = useTranslation();
-
- return (
-
-
- {title}
-
-
- );
-};
diff --git a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ActionAndSubjectListItem/index.ts b/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ActionAndSubjectListItem/index.ts
deleted file mode 100644
index 76dcd5512f4..00000000000
--- a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ActionAndSubjectListItem/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { ActionAndSubjectListItem } from './ActionAndSubjectListItem';
diff --git a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ExpandablePolicyCard.module.css b/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ExpandablePolicyCard.module.css
deleted file mode 100644
index 412f416d4f1..00000000000
--- a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ExpandablePolicyCard.module.css
+++ /dev/null
@@ -1,39 +0,0 @@
-.cardWrapper {
- width: 595px;
- min-width: 595px;
-}
-
-.chipWrapper {
- display: flex;
- flex-wrap: wrap;
-}
-
-.textAreaWrapper {
- margin-top: 10px;
-}
-
-.addResourceButton {
- width: 100%;
- margin-bottom: 10px;
-}
-
-.label {
- margin-top: 30px;
- margin-bottom: 5px;
-}
-
-.label:first-child {
- margin-top: 10px;
-}
-
-.dropdownWrapper {
- margin-bottom: 10px;
-}
-
-.inputParagraph {
- margin-bottom: 5px;
-}
-
-.descriptionInput {
- margin-top: 30px;
-}
diff --git a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ExpandablePolicyCard.test.tsx b/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ExpandablePolicyCard.test.tsx
deleted file mode 100644
index a22ce730a3b..00000000000
--- a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ExpandablePolicyCard.test.tsx
+++ /dev/null
@@ -1,285 +0,0 @@
-import React from 'react';
-import { render, screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import type { ExpandablePolicyCardProps } from './ExpandablePolicyCard';
-import { ExpandablePolicyCard } from './ExpandablePolicyCard';
-import { textMock } from '@studio/testing/mocks/i18nMock';
-import type { PolicyEditorUsage } from '../../types';
-import {
- mockActionId1,
- mockActionId2,
- mockActionId3,
- mockActionId4,
- mockActions,
- mockPolicyRuleCard1,
- mockPolicyRuleCards,
- mockResourceType1,
- mockResourecId1,
- mockSubjectTitle1,
- mockSubjectTitle2,
- mockSubjectTitle3,
- mockSubjects,
-} from '../../data-mocks';
-
-const mockUsageType: PolicyEditorUsage = 'app';
-
-const mockActionOption1: string = textMock(`policy_editor.action_${mockActionId1}`);
-const mockActionOption2: string = textMock(`policy_editor.action_${mockActionId2}`);
-const mockActionOption3: string = textMock(`policy_editor.action_${mockActionId3}`);
-const mockActionOption4: string = mockActionId4;
-
-describe('ExpandablePolicyCard', () => {
- afterEach(jest.clearAllMocks);
-
- const mockSetPolicyRules = jest.fn();
- const mockHandleCloneRule = jest.fn();
- const mockHandleDeleteRule = jest.fn();
- const mockSavePolicy = jest.fn();
-
- const defaultProps: ExpandablePolicyCardProps = {
- policyRule: mockPolicyRuleCard1,
- actions: mockActions,
- subjects: mockSubjects,
- rules: mockPolicyRuleCards,
- setPolicyRules: mockSetPolicyRules,
- resourceId: mockResourecId1,
- resourceType: mockResourceType1,
- handleCloneRule: mockHandleCloneRule,
- handleDeleteRule: mockHandleDeleteRule,
- showErrors: false,
- savePolicy: mockSavePolicy,
- usageType: mockUsageType,
- };
-
- it('calls "handleCloneRule" when the clone button is clicked', async () => {
- const user = userEvent.setup();
- render(
);
-
- const [moreButton] = screen.getAllByRole('button', { name: textMock('policy_editor.more') });
- await user.click(moreButton);
-
- const [cloneButton] = screen.getAllByRole('menuitem', {
- name: textMock('policy_editor.expandable_card_dropdown_copy'),
- });
- await user.click(cloneButton);
-
- expect(mockHandleCloneRule).toHaveBeenCalledTimes(1);
- });
-
- it('calls "handleDeleteRule" when the delete button is clicked', async () => {
- const user = userEvent.setup();
- render(
);
-
- const [moreButton] = screen.getAllByRole('button', { name: textMock('policy_editor.more') });
- await user.click(moreButton);
-
- const [deleteButton] = screen.getAllByRole('menuitem', { name: textMock('general.delete') });
- await user.click(deleteButton);
-
- expect(mockHandleDeleteRule).toHaveBeenCalledTimes(1);
- });
-
- it('calls "setPolicyRules" when sub-resource fields are edited', async () => {
- const user = userEvent.setup();
- render(
);
-
- const [typeInput] = screen.getAllByLabelText(
- textMock('policy_editor.narrowing_list_field_type'),
- );
- const [idInput] = screen.getAllByLabelText(textMock('policy_editor.narrowing_list_field_id'));
-
- const newWord: string = 'test';
-
- await user.type(typeInput, newWord);
- expect(mockSetPolicyRules).toHaveBeenCalledTimes(newWord.length);
-
- mockSetPolicyRules.mockClear();
-
- await user.type(idInput, newWord);
- expect(mockSetPolicyRules).toHaveBeenCalledTimes(newWord.length);
- });
-
- it('displays the selected actions as Chips', async () => {
- const user = userEvent.setup();
- render(
);
-
- // Check that the selected actions are present
- const selectedAction1 = screen.getByLabelText(
- `${textMock('general.delete')} ${mockActionOption1}`,
- );
- const selectedAction2 = screen.getByLabelText(
- `${textMock('general.delete')} ${mockActionOption2}`,
- );
- const selectedAction3 = screen.queryByLabelText(
- `${textMock('general.delete')} ${mockActionOption3}`,
- );
- const selectedAction4 = screen.getByLabelText(
- `${textMock('general.delete')} ${mockActionOption4}`,
- );
- expect(selectedAction1).toBeInTheDocument();
- expect(selectedAction2).toBeInTheDocument();
- expect(selectedAction3).not.toBeInTheDocument(); // 3 is not in the resource
- expect(selectedAction4).toBeInTheDocument();
-
- // Open the select
- const [actionSelect] = screen.getAllByLabelText(
- textMock('policy_editor.rule_card_actions_title'),
- );
- await user.click(actionSelect);
-
- // Check that the selected actions are not in the document
- const optionAction1 = screen.queryByRole('option', { name: mockActionOption1 });
- const optionAction2 = screen.queryByRole('option', { name: mockActionOption2 });
- const optionAction3 = screen.getByRole('option', { name: mockActionOption3 });
- const optionAction4 = screen.queryByRole('option', { name: mockActionOption4 });
-
- expect(optionAction1).not.toBeInTheDocument();
- expect(optionAction2).not.toBeInTheDocument();
- expect(optionAction3).toBeInTheDocument(); // 3 is in the resource
- expect(optionAction4).not.toBeInTheDocument();
-
- // Click the final action
- await user.click(screen.getByRole('option', { name: mockActionOption3 }));
-
- expect(mockSetPolicyRules).toHaveBeenCalledTimes(1);
-
- // Expect the option clicked to be removed from the screen
- expect(
- screen.queryByLabelText(`${textMock('general.delete')} ${mockActionOption3}`),
- ).not.toBeInTheDocument();
-
- // Expect the label with all selected to be present
- const [inputAllSelected] = screen.getAllByText(
- textMock('policy_editor.rule_card_actions_select_all_selected'),
- );
- expect(inputAllSelected).toBeInTheDocument();
- });
-
- it('calls "setPolicyRules" when subjects are edited', async () => {
- const user = userEvent.setup();
- render(
);
-
- // Check that the selected subjects are present
- const selectedSubject1 = screen.getByLabelText(
- `${textMock('general.delete')} ${mockSubjectTitle1}`,
- );
- const selectedSubject2 = screen.queryByLabelText(
- `${textMock('general.delete')} ${mockSubjectTitle2}`,
- );
- const selectedSubject3 = screen.getByLabelText(
- `${textMock('general.delete')} ${mockSubjectTitle3}`,
- );
- expect(selectedSubject1).toBeInTheDocument();
- expect(selectedSubject2).not.toBeInTheDocument(); // 2 is not in the resource
- expect(selectedSubject3).toBeInTheDocument();
-
- // Open the select
- const [subjectSelect] = screen.getAllByLabelText(
- textMock('policy_editor.rule_card_subjects_title'),
- );
- await user.click(subjectSelect);
-
- // Check that the selected subjects are not in the document
- const optionSubject1 = screen.queryByRole('option', { name: mockSubjectTitle1 });
- const optionSubject2 = screen.getByRole('option', { name: mockSubjectTitle2 });
- const optionSubject3 = screen.queryByRole('option', { name: mockSubjectTitle3 });
-
- expect(optionSubject1).not.toBeInTheDocument();
- expect(optionSubject2).toBeInTheDocument(); // 2 is in the resource
- expect(optionSubject3).not.toBeInTheDocument();
-
- // Click the final subject
- await user.click(screen.getByRole('option', { name: mockSubjectTitle2 }));
-
- expect(mockSetPolicyRules).toHaveBeenCalledTimes(1);
-
- // Expect the option clicked to be removed from the screen
- expect(
- screen.queryByLabelText(`${textMock('general.delete')} ${mockSubjectTitle2}`),
- ).not.toBeInTheDocument();
-
- // Expect the label with all selected to be present
- const [inputAllSelected] = screen.getAllByText(
- textMock('policy_editor.rule_card_subjects_select_all_selected'),
- );
- expect(inputAllSelected).toBeInTheDocument();
- });
-
- it('should append subject to selectable subject options list when selected subject is removed', async () => {
- const user = userEvent.setup();
- render(
);
-
- const [subjectSelect] = screen.getAllByLabelText(
- textMock('policy_editor.rule_card_subjects_title'),
- );
- await user.click(subjectSelect);
-
- // Check that already selected options does not be included within selectable list.
- expect(screen.queryByRole('option', { name: mockSubjectTitle1 })).toBeNull();
-
- // Remove the selected subject
- const selectedSubject = screen.getByLabelText(
- `${textMock('general.delete')} ${mockSubjectTitle1}`,
- );
- await user.click(selectedSubject);
-
- // Open the select and verify that the removed subject is now appended to the selectable list
- await user.click(subjectSelect);
- expect(screen.getByRole('option', { name: mockSubjectTitle1 })).toBeInTheDocument();
- });
-
- it('calls "setPolicyRules" when description field is edited', async () => {
- const user = userEvent.setup();
- render(
);
-
- const [descriptionField] = screen.getAllByLabelText(
- textMock('policy_editor.rule_card_description_title'),
- );
- expect(descriptionField).toHaveValue(mockPolicyRuleCard1.description);
- await user.type(descriptionField, '1');
-
- expect(mockSetPolicyRules).toHaveBeenCalledTimes(1);
- });
-
- it('calls "savePolicy" when input fields are blurred', async () => {
- const user = userEvent.setup();
- render(
);
-
- const [typeInput] = screen.getAllByLabelText(
- textMock('policy_editor.narrowing_list_field_type'),
- );
- const [idInput] = screen.getAllByLabelText(textMock('policy_editor.narrowing_list_field_id'));
-
- const newWord: string = 'test';
- await user.type(typeInput, newWord);
- await user.tab();
- await user.type(idInput, newWord);
- await user.tab();
-
- const [actionSelect] = screen.getAllByLabelText(
- textMock('policy_editor.rule_card_actions_title'),
- );
- await user.click(actionSelect);
-
- const actionOption: string = textMock(`policy_editor.action_${mockActionId3}`);
- await user.click(screen.getByRole('option', { name: actionOption }));
- await user.tab();
-
- const [subjectSelect] = screen.getAllByLabelText(
- textMock('policy_editor.rule_card_subjects_title'),
- );
- await user.click(subjectSelect);
- await user.click(screen.getByRole('option', { name: mockSubjectTitle2 }));
- await user.tab();
-
- const [descriptionField] = screen.getAllByLabelText(
- textMock('policy_editor.rule_card_description_title'),
- );
- expect(descriptionField).toHaveValue(mockPolicyRuleCard1.description);
- await user.type(descriptionField, newWord);
- await user.tab();
-
- const numFields = 5;
- expect(mockSavePolicy).toHaveBeenCalledTimes(numFields);
- });
-});
diff --git a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ExpandablePolicyCard.tsx b/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ExpandablePolicyCard.tsx
deleted file mode 100644
index 2a8ebb09b4e..00000000000
--- a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ExpandablePolicyCard.tsx
+++ /dev/null
@@ -1,530 +0,0 @@
-import React, { useState, useId } from 'react';
-import {
- Label,
- ErrorMessage,
- Paragraph,
- Textarea,
- LegacySelect,
-} from '@digdir/design-system-react';
-import { PlusIcon } from '@studio/icons';
-import classes from './ExpandablePolicyCard.module.css';
-import { ActionAndSubjectListItem } from './ActionAndSubjectListItem';
-import { ResourceNarrowingList } from './ResourceNarrowingList';
-import { ExpandablePolicyElement } from './ExpandablePolicyElement';
-import type {
- PolicyAction,
- PolicyRuleCard,
- PolicyRuleResource,
- PolicySubject,
- PolicyEditorUsage,
-} from '../../types';
-import { createNewPolicyResource, findSubjectByPolicyRuleSubject } from '../../utils';
-import {
- getActionOptions,
- getPolicyRuleIdString,
- getSubjectOptions,
- getUpdatedRules,
-} from '../../utils/ExpandablePolicyCardUtils';
-import { useTranslation } from 'react-i18next';
-import { StudioButton, StudioLabelAsParagraph } from '@studio/components';
-
-const wellKnownActionsIds: string[] = [
- 'complete',
- 'confirm',
- 'delete',
- 'instantiate',
- 'read',
- 'sign',
- 'write',
-];
-
-export type ExpandablePolicyCardProps = {
- policyRule: PolicyRuleCard;
- actions: PolicyAction[];
- subjects: PolicySubject[];
- rules: PolicyRuleCard[];
- setPolicyRules: React.Dispatch
>;
- resourceId: string;
- resourceType: string;
- handleCloneRule: () => void;
- handleDeleteRule: () => void;
- showErrors: boolean;
- savePolicy: (rules: PolicyRuleCard[]) => void;
- usageType: PolicyEditorUsage;
-};
-
-/**
- * @component
- * Component that displays a card where a user can view and update a policy rule
- * for a resource.
- *
- * @property {PolicyRuleCard}[policyRule] - The rule to display in the card
- * @property {PolicyAction[]}[actions] - The possible actions to select from
- * @property {PolicySubject[]}[subjects] - The possible subjects to select from
- * @property {PolicyRuleCard[]}[rules] - The list of all the rules
- * @property {React.Dispatch>}[setPolicyRules] - useState function to update the list of rules
- * @property {string}[resourceId] - The ID of the resource
- * @property {string}[resourceType] - The type of the resource
- * @property {function}[handleCloneRule] - Function to be executed when clicking clone rule
- * @property {function}[handleDeleteRule] - Function to be executed when clicking delete rule
- * @property {boolean}[showErrors] - Flag to decide if errors should be shown or not
- * @property {function}[savePolicy] - Function to save the policy
- * @property {PolicyEditorUsage}[usageType] - The usage type of the policy editor
- *
- * @returns {React.ReactNode} - The rendered component
- */
-export const ExpandablePolicyCard = ({
- policyRule,
- actions,
- subjects,
- rules,
- setPolicyRules,
- resourceId,
- resourceType,
- handleCloneRule,
- handleDeleteRule,
- showErrors,
- savePolicy,
- usageType,
-}: ExpandablePolicyCardProps): React.ReactNode => {
- const { t } = useTranslation();
-
- const uniqueId = useId();
-
- const [hasResourceError, setHasResourceError] = useState(policyRule.resources.length === 0);
- const [hasRightsError, setHasRightsErrors] = useState(policyRule.actions.length === 0);
- const [hasSubjectsError, setHasSubjectsError] = useState(policyRule.subject.length === 0);
- const [subjectOptions, setSubjectOptions] = useState(getSubjectOptions(subjects, policyRule));
- const [actionOptions, setActionOptions] = useState(getActionOptions(actions, policyRule));
-
- /**
- * Handles the changes in the input fields inside the resource blocks
- *
- * @param index the index of the element in the resource block
- * @param field the type of textfield to update
- * @param value the value types in the textfield
- * @param ruleIndex the index of the rule
- */
- const handleInputChange = (
- index: number,
- field: 'id' | 'type',
- value: string,
- ruleIndex: number,
- ) => {
- const updatedResources = [...policyRule.resources];
- updatedResources[ruleIndex][index] = {
- ...updatedResources[ruleIndex][index],
- [field]: value,
- };
-
- const updatedRules = getUpdatedRules(
- { ...policyRule, resources: updatedResources },
- policyRule.ruleId,
- rules,
- );
- setPolicyRules(updatedRules);
- };
-
- /**
- * Adds a resource block to the list of resources. The first element in the
- * resource block is set to the resource's ID and type.
- */
- const handleClickAddResource = () => {
- const newResource: PolicyRuleResource[] = createNewPolicyResource(
- usageType,
- resourceType,
- resourceId,
- );
-
- const updatedResources = [...policyRule.resources, newResource];
- const updatedRules = getUpdatedRules(
- { ...policyRule, resources: updatedResources },
- policyRule.ruleId,
- rules,
- );
- setPolicyRules(updatedRules);
- savePolicy(updatedRules);
- setHasResourceError(false);
- };
-
- /**
- * Displays a list of resource blocks, which each contains a list of the resources
- * and the list narrowing down the elements.
- */
- const displayResources = policyRule.resources.map((r, i) => {
- return (
-
- handleInputChange(narrowResourceIndex, field, s, i)
- }
- handleRemoveResource={(narrowResourceIndex) =>
- handleRemoveNarrowingResource(narrowResourceIndex, i)
- }
- handleClickAddResource={() => handleClickAddResourceNarrowing(i)}
- handleCloneElement={() => handleCloneResourceGroup(i)}
- handleRemoveElement={() => handleDeleteResourceGroup(i)}
- onBlur={() => savePolicy(rules)}
- usageType={usageType}
- />
- );
- });
-
- /**
- * Handles the addition of more resources
- */
- const handleClickAddResourceNarrowing = (resourceIndex: number) => {
- const newResource: PolicyRuleResource = {
- type: '',
- id: '',
- };
- const updatedResources = [...policyRule.resources];
- updatedResources[resourceIndex].push(newResource);
-
- const updatedRules = getUpdatedRules(
- { ...policyRule, resources: updatedResources },
- policyRule.ruleId,
- rules,
- );
- setPolicyRules(updatedRules);
- savePolicy(updatedRules);
- };
-
- /**
- * Handles the removal of the narrowed resources
- */
- const handleRemoveNarrowingResource = (index: number, ruleIndex: number) => {
- const updatedResources = [...policyRule.resources];
- updatedResources[ruleIndex].splice(index, 1);
- const updatedRules = getUpdatedRules(
- { ...policyRule, resources: updatedResources },
- policyRule.ruleId,
- rules,
- );
- setPolicyRules(updatedRules);
- savePolicy(updatedRules);
- };
-
- const getTranslationByActionId = (actionId: string): string => {
- return wellKnownActionsIds.includes(actionId)
- ? t(`policy_editor.action_${actionId}`)
- : actionId;
- };
-
- const displayActions = policyRule.actions.map((actionId, i) => {
- return (
- handleRemoveAction(i, actionId)}
- />
- );
- });
-
- const handleRemoveAction = (index: number, actionTitle: string) => {
- // Remove from selected list
- const updatedActions = [...policyRule.actions];
- updatedActions.splice(index, 1);
-
- // Add to options list
- setActionOptions([...actionOptions, { value: actionTitle, label: actionTitle }]);
-
- const updatedRules = getUpdatedRules(
- { ...policyRule, actions: updatedActions },
- policyRule.ruleId,
- rules,
- );
- setPolicyRules(updatedRules);
- savePolicy(updatedRules);
- setHasRightsErrors(updatedActions.length === 0);
- };
-
- /**
- * Displays the selected subjects
- */
- const displaySubjects = policyRule.subject.map((s, i) => {
- const subject: PolicySubject = findSubjectByPolicyRuleSubject(subjects, s);
- return (
- handleRemoveSubject(i, subject)}
- />
- );
- });
-
- /**
- * Handles the removal of subjects
- */
- const handleRemoveSubject = (index: number, subject: PolicySubject): void => {
- // Remove from selected list
- const updatedSubjects = [...policyRule.subject];
- updatedSubjects.splice(index, 1);
-
- // Add to options list
- setSubjectOptions((prevSubjectOptions) => [
- ...prevSubjectOptions,
- {
- value: subject.subjectId,
- label: subject.subjectTitle,
- },
- ]);
-
- const updatedRules = getUpdatedRules(
- { ...policyRule, subject: updatedSubjects },
- policyRule.ruleId,
- rules,
- );
-
- setPolicyRules(updatedRules);
- savePolicy(updatedRules);
- setHasSubjectsError(updatedSubjects.length === 0);
- };
-
- /**
- * Handles the click on a subject in the select list. It removes the clicked element
- * from the options list, and adds it to the selected subject title list.
- */
- const handleClickSubjectInList = (option: string) => {
- // As the input field is multiple, the onchance function uses string[], but
- // we are removing the element from the options list before it is displayed, so
- // it will only ever be a first value in the array.
- const clickedOption = option;
-
- // Remove from options list
- const index = subjectOptions.findIndex((o) => o.value === clickedOption);
- const updatedOptions = [...subjectOptions];
- updatedOptions.splice(index, 1);
- setSubjectOptions(updatedOptions);
-
- const updatedSubjectTitles = [...policyRule.subject, clickedOption];
- const updatedRules = getUpdatedRules(
- { ...policyRule, subject: updatedSubjectTitles },
- policyRule.ruleId,
- rules,
- );
- setPolicyRules(updatedRules);
- savePolicy(updatedRules);
- setHasSubjectsError(false);
- };
-
- /**
- * Handles the click on an action in the select list. It removes the clicked element
- * from the options list, and adds it to the selected action title list.
- */
- const handleClickActionInList = (option: string) => {
- // As the input field is multiple, the onChange function uses string[], but
- // we are removing the element from the options list before it is displayed, so
- // it will only ever be a first value in the array.
- const clickedOption = option;
-
- // Remove from options list
- const index = actionOptions.findIndex((o) => o.value === clickedOption);
- const updatedOptions = [...actionOptions];
- updatedOptions.splice(index, 1);
- setActionOptions(updatedOptions);
-
- const updatedActionTitles = [...policyRule.actions, clickedOption];
- const updatedRules = getUpdatedRules(
- { ...policyRule, actions: updatedActionTitles },
- policyRule.ruleId,
- rules,
- );
- setPolicyRules(updatedRules);
- savePolicy(updatedRules);
- setHasRightsErrors(false);
- };
-
- /**
- * Updates the description of the rule
- */
- const handleChangeDescription = (description: string) => {
- const updatedRules = getUpdatedRules({ ...policyRule, description }, policyRule.ruleId, rules);
- setPolicyRules(updatedRules);
- };
-
- /**
- * Duplicates a resource group and all the content in it.
- *
- * @param resourceIndex the index of the resource group to duplicate
- */
- const handleCloneResourceGroup = (resourceIndex: number) => {
- const resourceGroupToDuplicate: PolicyRuleResource[] = policyRule.resources[resourceIndex];
-
- // Create a deep copy of the object so the objects don't share same object reference
- const deepCopiedResourceGroupToDuplicate: PolicyRuleResource[] = JSON.parse(
- JSON.stringify(resourceGroupToDuplicate),
- );
-
- const updatedResources = [...policyRule.resources, deepCopiedResourceGroupToDuplicate];
- const updatedRules = getUpdatedRules(
- { ...policyRule, resources: updatedResources },
- policyRule.ruleId,
- rules,
- );
- setPolicyRules(updatedRules);
- savePolicy(updatedRules);
- };
-
- /**
- * Removes a resource group and all the content in it.
- *
- * @param resourceIndex the index of the resource group to remove
- */
- const handleDeleteResourceGroup = (resourceIndex: number) => {
- const updatedResources = [...policyRule.resources];
- updatedResources.splice(resourceIndex, 1);
- const updatedRules = getUpdatedRules(
- { ...policyRule, resources: updatedResources },
- policyRule.ruleId,
- rules,
- );
- setPolicyRules(updatedRules);
- savePolicy(updatedRules);
- setHasResourceError(updatedResources.length === 0);
- };
-
- /**
- * Displays the given text in a warning card
- *
- * @param text the text to display
- */
- const displayWarningCard = (text: string) => {
- return (
-
- {text}
-
- );
- };
-
- /**
- * Gets if there is an error in the rule card
- */
- const getHasRuleError = () => {
- return hasResourceError || hasRightsError || hasSubjectsError;
- };
-
- /**
- * Gets the correct text to display for a rule with missing values
- */
- const getRuleErrorText = (): string => {
- const arr: string[] = [];
- if (hasResourceError) arr.push(t('policy_editor.policy_rule_missing_sub_resource'));
- if (hasRightsError) arr.push(t('policy_editor.policy_rule_missing_actions'));
- if (hasSubjectsError) arr.push(t('policy_editor.policy_rule_missing_subjects'));
-
- if (arr.length === 1) {
- return t('policy_editor.policy_rule_missing_1', {
- ruleId: policyRule.ruleId,
- missing: arr[0],
- });
- }
- if (arr.length === 2) {
- return t('policy_editor.policy_rule_missing_2', {
- ruleId: policyRule.ruleId,
- missing1: arr[0],
- missing2: arr[1],
- });
- }
- if (arr.length === 3) {
- return t('policy_editor.policy_rule_missing_3', {
- ruleId: policyRule.ruleId,
- missing1: arr[0],
- missing2: arr[1],
- missing3: arr[2],
- });
- }
- return '';
- };
-
- return (
-
-
-
- {t('policy_editor.rule_card_sub_resource_title')}
-
- {displayResources}
-
- }
- >
- {t('policy_editor.rule_card_sub_resource_button')}
-
-
- {showErrors &&
- hasResourceError &&
- displayWarningCard(t('policy_editor.rule_card_sub_resource_error'))}
-
-
- {actionOptions.length === 0
- ? t('policy_editor.rule_card_actions_select_all_selected')
- : t('policy_editor.rule_card_actions_select_add')}
-
-
- ({
- ...option,
- label: getTranslationByActionId(option.label),
- }))}
- onChange={(value: string) => value !== null && handleClickActionInList(value)}
- disabled={actionOptions.length === 0}
- error={showErrors && hasRightsError}
- inputId={`selectAction-${uniqueId}`}
- />
-
- {displayActions}
- {showErrors &&
- hasRightsError &&
- displayWarningCard(t('policy_editor.rule_card_actions_error'))}
-
-
- {subjectOptions.length === 0
- ? t('policy_editor.rule_card_subjects_select_all_selected')
- : t('policy_editor.rule_card_subjects_select_add')}
-
-
- value !== null && handleClickSubjectInList(value)}
- disabled={subjectOptions.length === 0}
- error={showErrors && hasSubjectsError}
- inputId={`selectSubject-${uniqueId}`}
- />
-
- {displaySubjects}
- {showErrors &&
- hasSubjectsError &&
- displayWarningCard(t('policy_editor.rule_card_subjects_error'))}
-
-
-
- {showErrors && displayWarningCard(getRuleErrorText())}
-
- );
-};
diff --git a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ResourceNarrowingList/PolicyResourceFields/PolicyResourceFields.tsx b/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ResourceNarrowingList/PolicyResourceFields/PolicyResourceFields.tsx
deleted file mode 100644
index fd9c1354c92..00000000000
--- a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ResourceNarrowingList/PolicyResourceFields/PolicyResourceFields.tsx
+++ /dev/null
@@ -1,122 +0,0 @@
-import React from 'react';
-import classes from './PolicyResourceFields.module.css';
-import { Textfield } from '@digdir/design-system-react';
-import { MultiplyIcon } from '@studio/icons';
-import { useTranslation } from 'react-i18next';
-import { StudioButton, StudioLabelAsParagraph } from '@studio/components';
-
-export type PolicyResourceFieldsProps = {
- /**
- * Flag for if the fields are ediable or not
- */
- canEditTypeAndId: boolean;
- /**
- * Function to be executed when the remove button is clicked
- * @returns void
- */
- onRemove: () => void;
- /**
- * The value of the id field
- */
- valueId: string;
- /**
- * Function to be executed when the id value changes
- * @param s the string typed
- * @returns void
- */
- onChangeId: (s: string) => void;
- /**
- * The value of the type field
- */
- valueType: string;
- /**
- * Function to be executed when the type value changes
- * @param s the string typed
- * @returns void
- */
- onChangeType: (s: string) => void;
- /**
- * Function to be executed on blur
- * @returns
- */
- onBlur: () => void;
-};
-
-/**
- * @component
- * Component that displays two input fields next to each other, and a button
- * to remove the component from the list it belongs to. It is used to display
- * resources for a policy rules in the policy editor.
- *
- * @property {boolean}[canEditTypeAndId] - Flag for if the fields are ediable or not
- * @property {function}[onRemove] - Function to be executed when the remove button is clicked
- * @property {string}[valueId] - The value of the id field
- * @property {function}[onChangeId] - Function to be executed when the id value changes
- * @property {string}[valueType] - The value of the type field
- * @property {function}[onChangeType] - Function to be executed when the type value changes
- * @property {function}[onBlur] - Function to be executed on blur
- *
- * @returns {React.ReactNode} - The rendered component
- */
-export const PolicyResourceFields = ({
- canEditTypeAndId,
- onRemove,
- valueId,
- valueType,
- onChangeId,
- onChangeType,
- onBlur,
-}: PolicyResourceFieldsProps): React.ReactNode => {
- const { t } = useTranslation();
-
- return (
-
-
-
- {!canEditTypeAndId && (
-
- Type
-
- )}
- onChangeType(e.target.value)}
- readOnly={!canEditTypeAndId}
- onBlur={onBlur}
- aria-label={t('policy_editor.narrowing_list_field_type')}
- />
-
-
- {!canEditTypeAndId && (
-
- Id
-
- )}
- onChangeId(e.target.value)}
- readOnly={!canEditTypeAndId}
- onBlur={onBlur}
- aria-label={t('policy_editor.narrowing_list_field_id')}
- />
-
-
-
- {canEditTypeAndId && (
- }
- onClick={onRemove}
- size='small'
- title={t('policy_editor.narrowing_list_field_delete')}
- variant='tertiary'
- />
- )}
-
-
- );
-};
diff --git a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ResourceNarrowingList/ResourceNarrowingList.module.css b/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ResourceNarrowingList/ResourceNarrowingList.module.css
deleted file mode 100644
index 119d02dc3d4..00000000000
--- a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ResourceNarrowingList/ResourceNarrowingList.module.css
+++ /dev/null
@@ -1,8 +0,0 @@
-.wrapper {
- margin-top: 10px;
- margin-bottom: 20px;
-}
-
-.buttonWrapper {
- margin-bottom: 10px;
-}
diff --git a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ResourceNarrowingList/ResourceNarrowingList.tsx b/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ResourceNarrowingList/ResourceNarrowingList.tsx
deleted file mode 100644
index 33a0964eb04..00000000000
--- a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ResourceNarrowingList/ResourceNarrowingList.tsx
+++ /dev/null
@@ -1,132 +0,0 @@
-import React from 'react';
-import classes from './ResourceNarrowingList.module.css';
-import { PolicyResourceFields } from './PolicyResourceFields';
-import { ExpandablePolicyElement } from '../ExpandablePolicyElement';
-import { StudioButton } from '@studio/components';
-import { PlusIcon } from '@studio/icons';
-import type { PolicyEditorUsage, PolicyRuleResource } from '../../../types';
-import { useTranslation } from 'react-i18next';
-
-export type ResourceNarrowingListProps = {
- /**
- * The list of policy resources to display
- */
- resources: PolicyRuleResource[];
- /**
- * Function to update the values when the text fields changes value
- * @param i the index position
- * @param field if it is the id or the type field
- * @param s the string in the field
- * @returns void
- */
- handleInputChange: (i: number, field: 'id' | 'type', s: string) => void;
- /**
- * Function that removes a resource from the list
- * @param index the index position to remove
- * @returns void
- */
- handleRemoveResource: (index: number) => void;
- /**
- * Function that adds a resource to the list
- * @returns void
- */
- handleClickAddResource: () => void;
- /**
- * Function to be executed when the element is to be removed
- * @returns void
- */
- handleRemoveElement: () => void;
- /**
- * Function to be executed when the element is duplicated
- * @returns void
- */
- handleCloneElement: () => void;
- /**
- * Function to be executed on blur
- * @returns
- */
- onBlur: () => void;
- /**
- * The usage type of the policy editor
- */
- usageType: PolicyEditorUsage;
-};
-
-/**
- * @component
- * Displays the narrowing list of the resources. The component is expandable, and
- * has a button to add elements to the list.
- *
- * @property {PolicyRuleResource[]}[resources] - The list of policy resources to display
- * @property {function}[handleInputChange] - Function to update the values when the text fields changes value
- * @property {function}[handleRemoveResource] - Function that removes a resource from the list
- * @property {function}[handleClickAddResource] - Function that adds a resource to the list
- * @property {function}[handleRemoveElement] - Function to be executed when the element is to be removed
- * @property {function}[handleCloneElement] - Function to be executed when the element is cloned
- * @property {function}[onBlur] - Function to be executed on blur
- * @property {PolicyEditorUsage}[usageType] - The usage type of the policy editor
- *
- * @returns {React.ReactNode} - The rendered component
- */
-export const ResourceNarrowingList = ({
- resources,
- handleInputChange,
- handleRemoveResource,
- handleClickAddResource,
- handleRemoveElement,
- handleCloneElement,
- onBlur,
- usageType,
-}: ResourceNarrowingListProps): React.ReactNode => {
- const { t } = useTranslation();
-
- /**
- * Displays the list of resources
- */
- const displayResources = resources.map((r, i) => {
- return (
- 0}
- onRemove={() => handleRemoveResource(i)}
- valueId={r.id}
- valueType={r.type}
- onChangeId={(s: string) => handleInputChange(i, 'id', s)}
- onChangeType={(s: string) => handleInputChange(i, 'type', s)}
- onBlur={onBlur}
- />
- );
- });
-
- /**
- * Creates a name for the resourcegroup based on the id of the resource
- */
- const getResourceName = (): string => {
- return resources.map((r) => r.id).join(' - ');
- };
-
- return (
-
-
- {displayResources}
-
- }
- >
- {t('policy_editor.narrowing_list_add_button')}
-
-
-
-
- );
-};
diff --git a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/index.ts b/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/index.ts
deleted file mode 100644
index df6c56b03c9..00000000000
--- a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { ExpandablePolicyCard } from './ExpandablePolicyCard';
diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyCardRules.module.css b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyCardRules.module.css
new file mode 100644
index 00000000000..faa8d89d6b9
--- /dev/null
+++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyCardRules.module.css
@@ -0,0 +1,3 @@
+.space {
+ margin-block: var(--fds-sizing-5);
+}
diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyCardRules.test.tsx b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyCardRules.test.tsx
new file mode 100644
index 00000000000..e2d05081184
--- /dev/null
+++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyCardRules.test.tsx
@@ -0,0 +1,56 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { PolicyCardRules, type PolicyCardRulesProps } from './PolicyCardRules';
+import { textMock } from '@studio/testing/mocks/i18nMock';
+import { mockPolicyEditorContextValue } from '../../../test/mocks/policyEditorContextMock';
+import {
+ PolicyEditorContext,
+ type PolicyEditorContextProps,
+} from '../../contexts/PolicyEditorContext';
+
+const defaultProps: PolicyCardRulesProps = {
+ showErrorsOnAllRulesAboveNew: false,
+};
+
+describe('PolicyCardRule', () => {
+ afterEach(jest.clearAllMocks);
+
+ it('displays the rules when there are more than 0 rules', async () => {
+ const user = userEvent.setup();
+ renderPolicyCardRules();
+
+ const subResourceLabel = screen.getAllByText(
+ textMock('policy_editor.rule_card_sub_resource_title'),
+ );
+
+ await user.tab();
+
+ expect(subResourceLabel.length).toEqual(mockPolicyEditorContextValue.policyRules.length);
+ });
+
+ it('displays no rules when the policy has no rules', async () => {
+ const user = userEvent.setup();
+ renderPolicyCardRules({ policyRules: [] });
+
+ const subResourceLabel = screen.queryAllByText(
+ textMock('policy_editor.rule_card_sub_resource_title'),
+ );
+
+ await user.tab();
+
+ expect(subResourceLabel.length).toEqual(0);
+ });
+});
+
+const renderPolicyCardRules = (
+ policyEditorContextProps: Partial = {},
+) => {
+ return render(
+
+
+ ,
+ );
+};
diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyCardRules.tsx b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyCardRules.tsx
new file mode 100644
index 00000000000..52414c51b82
--- /dev/null
+++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyCardRules.tsx
@@ -0,0 +1,26 @@
+import React from 'react';
+import classes from './PolicyCardRules.module.css';
+import { usePolicyEditorContext } from '../../contexts/PolicyEditorContext';
+import { PolicyRule } from './PolicyRule';
+import { type PolicyRuleCard } from '../../types';
+
+export type PolicyCardRulesProps = {
+ showErrorsOnAllRulesAboveNew: boolean;
+};
+
+export const PolicyCardRules = ({
+ showErrorsOnAllRulesAboveNew,
+}: PolicyCardRulesProps): React.ReactElement[] => {
+ const { policyRules, showAllErrors } = usePolicyEditorContext();
+
+ const showErrors = (index: number): boolean =>
+ showAllErrors || (showErrorsOnAllRulesAboveNew && policyRules.length - 1 !== index);
+
+ return policyRules.map((policyRuleCard: PolicyRuleCard, index: number) => {
+ return (
+
+ );
+ });
+};
diff --git a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ExpandablePolicyElement/ExpandablePolicyElement.module.css b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/ExpandablePolicyElement/ExpandablePolicyElement.module.css
similarity index 75%
rename from frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ExpandablePolicyElement/ExpandablePolicyElement.module.css
rename to frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/ExpandablePolicyElement/ExpandablePolicyElement.module.css
index 085319e79b9..eda6439af18 100644
--- a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ExpandablePolicyElement/ExpandablePolicyElement.module.css
+++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/ExpandablePolicyElement/ExpandablePolicyElement.module.css
@@ -15,11 +15,11 @@
}
.cardWrapper {
- border-radius: 12px;
+ border-radius: var(--fds-sizing-3);
}
.elementWrapper {
- border-radius: 5px;
+ border-radius: var(--fds-sizing-1);
background-color: var(--fds-semantic-background-subtle);
}
@@ -36,11 +36,11 @@
display: flex;
justify-content: center;
align-items: center;
- padding-right: 15px;
+ padding-right: var(--fds-spacing-4);
}
.topWrapperCard {
- border-radius: 10px;
+ border-radius: var(--fds-sizing-3);
}
.topWrapperCardOpen {
@@ -49,7 +49,7 @@
}
.topWrapperElement {
- border-radius: 4px;
+ border-radius: var(--fds-sizing-1);
}
.topWrapperElementOpen {
@@ -58,7 +58,7 @@
}
.topWrapperError {
- border-radius: 10px;
+ border-radius: var(--fds-sizing-3);
background-color: var(--fds-semantic-surface-danger-subtle);
}
@@ -75,8 +75,8 @@
justify-content: space-between;
align-items: center;
width: 100%;
- padding: 20px;
- padding-right: 5px;
+ padding: var(--fds-spacing-5);
+ padding-right: var(--fds-spacing-1);
margin: 0;
}
@@ -88,9 +88,9 @@
justify-content: space-between;
align-items: center;
width: 100%;
- padding-block: 10px;
- padding-left: 20px;
- padding-right: 5px;
+ padding-block: var(--fds-spacing-3);
+ padding-left: var(--fds-spacing-5);
+ padding-right: var(--fds-spacing-1);
margin: 0;
}
@@ -100,11 +100,11 @@
}
.cardBottomWrapper {
- padding-inline: 20px;
- padding-bottom: 20px;
+ padding-inline: var(--fds-spacing-5);
+ padding-bottom: var(--fds-spacing-5);
}
.elementBottomWrapper {
- padding-inline: 20px;
- padding-bottom: 10px;
+ padding-inline: var(--fds-spacing-5);
+ padding-bottom: var(--fds-spacing-3);
}
diff --git a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ExpandablePolicyElement/ExpandablePolicyElement.test.tsx b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/ExpandablePolicyElement/ExpandablePolicyElement.test.tsx
similarity index 100%
rename from frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ExpandablePolicyElement/ExpandablePolicyElement.test.tsx
rename to frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/ExpandablePolicyElement/ExpandablePolicyElement.test.tsx
diff --git a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ExpandablePolicyElement/ExpandablePolicyElement.tsx b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/ExpandablePolicyElement/ExpandablePolicyElement.tsx
similarity index 76%
rename from frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ExpandablePolicyElement/ExpandablePolicyElement.tsx
rename to frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/ExpandablePolicyElement/ExpandablePolicyElement.tsx
index 4f02516961e..6ce24c64b49 100644
--- a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ExpandablePolicyElement/ExpandablePolicyElement.tsx
+++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/ExpandablePolicyElement/ExpandablePolicyElement.tsx
@@ -15,31 +15,6 @@ export type ExpandablePolicyElementProps = {
hasError?: boolean;
};
-/**
- * @component
- * Displays a wrapper component that can be expanded and collapsed. The wrapper
- * component is wrapped around the content that can be collapsed.
- *
- * @example
- *
- * ...
- *
- *
- * @property {string}[title] - The title to display on the element.
- * @property {ReactNode}[children] - The React childrens to display inside it.
- * @property {boolean}[isCard] - Optional flag for if the component is a card or an element
- * @property {function}[handleRemoveElement] - Function to be executed when the element is to be removed
- * @property {function}[handleCloneElement] - Function to be executed when the element is cloned
- * @property {boolean}[hasError] - Optional flag for if the component has error
- *
- * @returns {React.ReactNode} - The rendered component
- */
export const ExpandablePolicyElement = ({
title,
children,
diff --git a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ExpandablePolicyElement/PolicyEditorDropdownMenu/PolicyEditorDropdownMenu.module.css b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/ExpandablePolicyElement/PolicyEditorDropdownMenu/PolicyEditorDropdownMenu.module.css
similarity index 73%
rename from frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ExpandablePolicyElement/PolicyEditorDropdownMenu/PolicyEditorDropdownMenu.module.css
rename to frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/ExpandablePolicyElement/PolicyEditorDropdownMenu/PolicyEditorDropdownMenu.module.css
index 7274ffc1636..2d9da4a6437 100644
--- a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ExpandablePolicyElement/PolicyEditorDropdownMenu/PolicyEditorDropdownMenu.module.css
+++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/ExpandablePolicyElement/PolicyEditorDropdownMenu/PolicyEditorDropdownMenu.module.css
@@ -3,5 +3,5 @@
}
.icon {
- font-size: 1.3rem;
+ font-size: var(--fds-sizing-5);
}
diff --git a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ExpandablePolicyElement/PolicyEditorDropdownMenu/PolicyEditorDropdownMenu.test.tsx b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/ExpandablePolicyElement/PolicyEditorDropdownMenu/PolicyEditorDropdownMenu.test.tsx
similarity index 100%
rename from frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ExpandablePolicyElement/PolicyEditorDropdownMenu/PolicyEditorDropdownMenu.test.tsx
rename to frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/ExpandablePolicyElement/PolicyEditorDropdownMenu/PolicyEditorDropdownMenu.test.tsx
diff --git a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ExpandablePolicyElement/PolicyEditorDropdownMenu/PolicyEditorDropdownMenu.tsx b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/ExpandablePolicyElement/PolicyEditorDropdownMenu/PolicyEditorDropdownMenu.tsx
similarity index 74%
rename from frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ExpandablePolicyElement/PolicyEditorDropdownMenu/PolicyEditorDropdownMenu.tsx
rename to frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/ExpandablePolicyElement/PolicyEditorDropdownMenu/PolicyEditorDropdownMenu.tsx
index 64e49ce387e..66247f4bdc6 100644
--- a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ExpandablePolicyElement/PolicyEditorDropdownMenu/PolicyEditorDropdownMenu.tsx
+++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/ExpandablePolicyElement/PolicyEditorDropdownMenu/PolicyEditorDropdownMenu.tsx
@@ -14,19 +14,6 @@ export type PolicyEditorDropdownMenuProps = {
isError?: boolean;
};
-/**
- * @component
- * Dropdown menu component that displays a clone and a delete button
- *
- * @property {boolean}[isOpen] - Boolean for if the menu is open or not
- * @property {function}[handleClickMoreIcon] - Function to be executed when the menu icon is clicked
- * @property {function}[handleCloseMenu] - Function to be executed when closing the menu
- * @property {function}[handleClone] - Function to handle the click of the clone button
- * @property {function}[handleDelete] - Function to handle the click of the delete button
- * @property {boolean}[isError] - Optional flag for if there is an error or not
- *
- * @returns {React.ReactNode} - The rendered component
- */
export const PolicyEditorDropdownMenu = ({
isOpen,
handleClickMoreIcon,
diff --git a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ExpandablePolicyElement/PolicyEditorDropdownMenu/index.ts b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/ExpandablePolicyElement/PolicyEditorDropdownMenu/index.ts
similarity index 100%
rename from frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ExpandablePolicyElement/PolicyEditorDropdownMenu/index.ts
rename to frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/ExpandablePolicyElement/PolicyEditorDropdownMenu/index.ts
diff --git a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ExpandablePolicyElement/index.ts b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/ExpandablePolicyElement/index.ts
similarity index 100%
rename from frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ExpandablePolicyElement/index.ts
rename to frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/ExpandablePolicyElement/index.ts
diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyActions/PolicyActions.module.css b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyActions/PolicyActions.module.css
new file mode 100644
index 00000000000..7ea36e29e6c
--- /dev/null
+++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyActions/PolicyActions.module.css
@@ -0,0 +1,23 @@
+.label {
+ margin-top: var(--fds-spacing-7);
+ margin-bottom: var(--fds-spacing-1);
+}
+
+.chipWrapper {
+ display: flex;
+ flex-wrap: wrap;
+}
+
+.dropdownWrapper {
+ margin-bottom: var(--fds-spacing-3);
+}
+
+.inputParagraph {
+ margin-bottom: var(--fds-spacing-1);
+}
+
+.chip {
+ margin: 0;
+ margin-right: var(--fds-spacing-1);
+ margin-top: var(--fds-spacing-1);
+}
diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyActions/PolicyActions.test.tsx b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyActions/PolicyActions.test.tsx
new file mode 100644
index 00000000000..93ed4fe57e9
--- /dev/null
+++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyActions/PolicyActions.test.tsx
@@ -0,0 +1,115 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { PolicyActions } from './PolicyActions';
+import { textMock } from '@studio/testing/mocks/i18nMock';
+import {
+ mockActionId1,
+ mockActionId2,
+ mockActionId3,
+ mockActionId4,
+} from '../../../../../test/mocks/policyActionMocks';
+import { PolicyEditorContext } from '../../../../contexts/PolicyEditorContext';
+import { PolicyRuleContext } from '../../../../contexts/PolicyRuleContext';
+import { mockPolicyEditorContextValue } from '../../../../../test/mocks/policyEditorContextMock';
+import { mockPolicyRuleContextValue } from '../../../../../test/mocks/policyRuleContextMock';
+
+const mockActionOption1: string = textMock(`policy_editor.action_${mockActionId1}`);
+const mockActionOption2: string = textMock(`policy_editor.action_${mockActionId2}`);
+const mockActionOption3: string = textMock(`policy_editor.action_${mockActionId3}`);
+const mockActionOption4: string = mockActionId4;
+
+describe('PolicyActions', () => {
+ afterEach(jest.clearAllMocks);
+
+ it('displays the selected actions as Chips', async () => {
+ const user = userEvent.setup();
+ renderPolicyActions();
+
+ const selectedAction1 = screen.getByLabelText(
+ `${textMock('general.delete')} ${mockActionOption1}`,
+ );
+ const selectedAction2 = screen.getByLabelText(
+ `${textMock('general.delete')} ${mockActionOption2}`,
+ );
+ const selectedAction3 = screen.queryByLabelText(
+ `${textMock('general.delete')} ${mockActionOption3}`,
+ );
+ const selectedAction4 = screen.getByLabelText(
+ `${textMock('general.delete')} ${mockActionOption4}`,
+ );
+ expect(selectedAction1).toBeInTheDocument();
+ expect(selectedAction2).toBeInTheDocument();
+ expect(selectedAction3).not.toBeInTheDocument();
+ expect(selectedAction4).toBeInTheDocument();
+
+ const [actionSelect] = screen.getAllByLabelText(
+ textMock('policy_editor.rule_card_actions_title'),
+ );
+ await user.click(actionSelect);
+
+ const optionAction1 = screen.queryByRole('option', { name: mockActionOption1 });
+ const optionAction2 = screen.queryByRole('option', { name: mockActionOption2 });
+ const optionAction3 = screen.getByRole('option', { name: mockActionOption3 });
+ const optionAction4 = screen.queryByRole('option', { name: mockActionOption4 });
+
+ expect(optionAction1).not.toBeInTheDocument();
+ expect(optionAction2).not.toBeInTheDocument();
+ expect(optionAction3).toBeInTheDocument();
+ expect(optionAction4).not.toBeInTheDocument();
+
+ await user.click(screen.getByRole('option', { name: mockActionOption3 }));
+
+ expect(mockPolicyEditorContextValue.setPolicyRules).toHaveBeenCalledTimes(1);
+ expect(
+ screen.queryByLabelText(`${textMock('general.delete')} ${mockActionOption3}`),
+ ).not.toBeInTheDocument();
+
+ const [inputAllSelected] = screen.getAllByText(
+ textMock('policy_editor.rule_card_actions_select_all_selected'),
+ );
+ expect(inputAllSelected).toBeInTheDocument();
+ });
+
+ it('should append action to selectable actions options list when selected action is removed', async () => {
+ const user = userEvent.setup();
+ renderPolicyActions();
+
+ const [actionSelect] = screen.getAllByLabelText(
+ textMock('policy_editor.rule_card_actions_title'),
+ );
+ await user.click(actionSelect);
+
+ expect(screen.queryByRole('option', { name: mockActionOption1 })).toBeNull();
+
+ const selectedSubject = screen.getByLabelText(
+ `${textMock('general.delete')} ${mockActionOption1}`,
+ );
+ await user.click(selectedSubject);
+
+ await user.click(actionSelect);
+ expect(screen.getByRole('option', { name: mockActionOption1 })).toBeInTheDocument();
+ });
+
+ it('calls the "setPolicyRules", "savePolicy", and "setPolicyError" function when the chip is clicked', async () => {
+ const user = userEvent.setup();
+ renderPolicyActions();
+
+ const chipElement = screen.getByText(textMock('policy_editor.action_write'));
+ await user.click(chipElement);
+
+ expect(mockPolicyEditorContextValue.setPolicyRules).toHaveBeenCalledTimes(1);
+ expect(mockPolicyEditorContextValue.savePolicy).toHaveBeenCalledTimes(1);
+ expect(mockPolicyRuleContextValue.setPolicyError).toHaveBeenCalledTimes(1);
+ });
+});
+
+const renderPolicyActions = () => {
+ return render(
+
+
+
+
+ ,
+ );
+};
diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyActions/PolicyActions.tsx b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyActions/PolicyActions.tsx
new file mode 100644
index 00000000000..8c578e7edb0
--- /dev/null
+++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyActions/PolicyActions.tsx
@@ -0,0 +1,108 @@
+import React, { useState } from 'react';
+import classes from './PolicyActions.module.css';
+import { getActionOptions, getUpdatedRules } from '../../../../utils/PolicyRuleUtils';
+import { usePolicyEditorContext } from '../../../../contexts/PolicyEditorContext';
+import { usePolicyRuleContext } from '../../../../contexts/PolicyRuleContext';
+import { useTranslation } from 'react-i18next';
+import { Label, ErrorMessage, Paragraph, LegacySelect, Chip } from '@digdir/design-system-react';
+
+const wellKnownActionsIds: string[] = [
+ 'complete',
+ 'confirm',
+ 'delete',
+ 'instantiate',
+ 'read',
+ 'sign',
+ 'write',
+];
+
+export const PolicyActions = (): React.ReactElement => {
+ const { t } = useTranslation();
+ const { policyRules: rules, setPolicyRules, actions, savePolicy } = usePolicyEditorContext();
+ const { policyRule, uniqueId, showAllErrors, policyError, setPolicyError } =
+ usePolicyRuleContext();
+
+ const [actionOptions, setActionOptions] = useState(getActionOptions(actions, policyRule));
+
+ const getTranslationByActionId = (actionId: string): string => {
+ return wellKnownActionsIds.includes(actionId)
+ ? t(`policy_editor.action_${actionId}`)
+ : actionId;
+ };
+
+ const handleRemoveAction = (index: number, actionTitle: string) => {
+ const updatedActions = [...policyRule.actions];
+ updatedActions.splice(index, 1);
+
+ setActionOptions([...actionOptions, { value: actionTitle, label: actionTitle }]);
+
+ const updatedRules = getUpdatedRules(
+ { ...policyRule, actions: updatedActions },
+ policyRule.ruleId,
+ rules,
+ );
+ setPolicyRules(updatedRules);
+ savePolicy(updatedRules);
+ setPolicyError({ ...policyError, actionsError: updatedActions.length === 0 });
+ };
+
+ const handleClickActionInList = (clickedOption: string) => {
+ const index = actionOptions.findIndex((o) => o.value === clickedOption);
+ const updatedOptions = [...actionOptions];
+ updatedOptions.splice(index, 1);
+ setActionOptions(updatedOptions);
+
+ const updatedActionTitles = [...policyRule.actions, clickedOption];
+ const updatedRules = getUpdatedRules(
+ { ...policyRule, actions: updatedActionTitles },
+ policyRule.ruleId,
+ rules,
+ );
+ setPolicyRules(updatedRules);
+ savePolicy(updatedRules);
+ setPolicyError({ ...policyError, actionsError: false });
+ };
+
+ const displayActions = policyRule.actions.map((actionId, i) => {
+ return (
+ handleRemoveAction(i, actionId)}
+ >
+ {getTranslationByActionId(actionId)}
+
+ );
+ });
+
+ return (
+ <>
+
+
+ {actionOptions.length === 0
+ ? t('policy_editor.rule_card_actions_select_all_selected')
+ : t('policy_editor.rule_card_actions_select_add')}
+
+
+ ({
+ ...option,
+ label: getTranslationByActionId(option.label),
+ }))}
+ onChange={(value: string) => value !== null && handleClickActionInList(value)}
+ disabled={actionOptions.length === 0}
+ error={showAllErrors && policyError.actionsError}
+ inputId={`selectAction-${uniqueId}`}
+ />
+
+ {displayActions}
+ {showAllErrors && policyError.actionsError && (
+ {t('policy_editor.rule_card_actions_error')}
+ )}
+ >
+ );
+};
diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyActions/index.ts b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyActions/index.ts
new file mode 100644
index 00000000000..4bf0d203bd6
--- /dev/null
+++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyActions/index.ts
@@ -0,0 +1 @@
+export { PolicyActions } from './PolicyActions';
diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyDescription/PolicyDescription.module.css b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyDescription/PolicyDescription.module.css
new file mode 100644
index 00000000000..1728ac8e768
--- /dev/null
+++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyDescription/PolicyDescription.module.css
@@ -0,0 +1,7 @@
+.textAreaWrapper {
+ margin-top: var(--fds-spacing-3);
+}
+
+.descriptionInput {
+ margin-top: var(--fds-spacing-7);
+}
diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyDescription/PolicyDescription.test.tsx b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyDescription/PolicyDescription.test.tsx
new file mode 100644
index 00000000000..3eda33e4031
--- /dev/null
+++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyDescription/PolicyDescription.test.tsx
@@ -0,0 +1,35 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { PolicyDescription } from './PolicyDescription';
+import { textMock } from '@studio/testing/mocks/i18nMock';
+import { PolicyEditorContext } from '../../../../contexts/PolicyEditorContext';
+import { PolicyRuleContext } from '../../../../contexts/PolicyRuleContext';
+import { mockPolicyEditorContextValue } from '../../../../../test/mocks/policyEditorContextMock';
+import { mockPolicyRuleContextValue } from '../../../../../test/mocks/policyRuleContextMock';
+import { mockPolicyRuleCard1 } from '../../../../../test/mocks/policyRuleMocks';
+
+describe('PolicyDescription', () => {
+ it('calls "setPolicyRules" when description field is edited', async () => {
+ const user = userEvent.setup();
+ renderPolicyDescription();
+
+ const [descriptionField] = screen.getAllByLabelText(
+ textMock('policy_editor.rule_card_description_title'),
+ );
+ expect(descriptionField).toHaveValue(mockPolicyRuleCard1.description);
+ await user.type(descriptionField, '1');
+
+ expect(mockPolicyEditorContextValue.setPolicyRules).toHaveBeenCalledTimes(1);
+ });
+});
+
+const renderPolicyDescription = () => {
+ return render(
+
+
+
+
+ ,
+ );
+};
diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyDescription/PolicyDescription.tsx b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyDescription/PolicyDescription.tsx
new file mode 100644
index 00000000000..4b39b226ca9
--- /dev/null
+++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyDescription/PolicyDescription.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import classes from './PolicyDescription.module.css';
+import { usePolicyEditorContext } from '../../../../contexts/PolicyEditorContext';
+import { usePolicyRuleContext } from '../../../../contexts/PolicyRuleContext';
+import { getUpdatedRules } from '../../../../utils/PolicyRuleUtils';
+import { useTranslation } from 'react-i18next';
+import { StudioTextarea } from '@studio/components';
+
+export const PolicyDescription = (): React.ReactElement => {
+ const { t } = useTranslation();
+ const { policyRules, setPolicyRules, savePolicy } = usePolicyEditorContext();
+ const { policyRule, uniqueId } = usePolicyRuleContext();
+
+ const handleChangeDescription = (event: React.FormEvent) => {
+ const description: string = event.currentTarget.value;
+ const updatedRules = getUpdatedRules(
+ { ...policyRule, description },
+ policyRule.ruleId,
+ policyRules,
+ );
+ setPolicyRules(updatedRules);
+ };
+
+ return (
+
+ savePolicy(policyRules)}
+ id={`description-${uniqueId}`}
+ className={classes.descriptionInput}
+ />
+
+ );
+};
diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyDescription/index.ts b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyDescription/index.ts
new file mode 100644
index 00000000000..5fb0d6addac
--- /dev/null
+++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyDescription/index.ts
@@ -0,0 +1 @@
+export { PolicyDescription } from './PolicyDescription';
diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyRule.test.tsx b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyRule.test.tsx
new file mode 100644
index 00000000000..22194faa5b6
--- /dev/null
+++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyRule.test.tsx
@@ -0,0 +1,106 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { PolicyRule, type PolicyRuleProps } from './PolicyRule';
+import { textMock } from '@studio/testing/mocks/i18nMock';
+import { mockActionId3 } from '../../../../test/mocks/policyActionMocks';
+import { mockPolicyRuleCard1 } from '../../../../test/mocks/policyRuleMocks';
+import { mockSubjectTitle2 } from '../../../../test/mocks/policySubjectMocks';
+import {
+ PolicyEditorContext,
+ type PolicyEditorContextProps,
+} from '../../../contexts/PolicyEditorContext';
+import { mockPolicyEditorContextValue } from '../../../../test/mocks/policyEditorContextMock';
+
+const defaultProps: PolicyRuleProps = {
+ policyRule: mockPolicyRuleCard1,
+ showErrors: false,
+ ruleIndex: 0,
+};
+
+describe('PolicyRule', () => {
+ afterEach(jest.clearAllMocks);
+
+ it('calls "setPolicyRules" and "savePolicy" when the clone button is clicked', async () => {
+ const user = userEvent.setup();
+ renderPolicyRule();
+
+ const [moreButton] = screen.getAllByRole('button', { name: textMock('policy_editor.more') });
+ await user.click(moreButton);
+
+ const [cloneButton] = screen.getAllByRole('menuitem', {
+ name: textMock('policy_editor.expandable_card_dropdown_copy'),
+ });
+ await user.click(cloneButton);
+
+ expect(mockPolicyEditorContextValue.setPolicyRules).toHaveBeenCalledTimes(1);
+ expect(mockPolicyEditorContextValue.savePolicy).toHaveBeenCalledTimes(1);
+ });
+
+ it('calls "setPolicyRules" and "savePolicy" when the delete button is clicked', async () => {
+ const user = userEvent.setup();
+ renderPolicyRule();
+ jest.spyOn(window, 'confirm').mockImplementation(() => true);
+
+ const [moreButton] = screen.getAllByRole('button', { name: textMock('policy_editor.more') });
+ await user.click(moreButton);
+
+ const [deleteButton] = screen.getAllByRole('menuitem', { name: textMock('general.delete') });
+ await user.click(deleteButton);
+
+ expect(mockPolicyEditorContextValue.setPolicyRules).toHaveBeenCalledTimes(1);
+ expect(mockPolicyEditorContextValue.savePolicy).toHaveBeenCalledTimes(1);
+ });
+
+ it('calls "savePolicy" when input fields are blurred', async () => {
+ const user = userEvent.setup();
+ renderPolicyRule();
+
+ const [typeInput] = screen.getAllByLabelText(
+ textMock('policy_editor.narrowing_list_field_type'),
+ );
+ const [idInput] = screen.getAllByLabelText(textMock('policy_editor.narrowing_list_field_id'));
+
+ const newWord: string = 'test';
+ await user.type(typeInput, newWord);
+ await user.tab();
+ await user.type(idInput, newWord);
+ await user.tab();
+
+ const [actionSelect] = screen.getAllByLabelText(
+ textMock('policy_editor.rule_card_actions_title'),
+ );
+ await user.click(actionSelect);
+
+ const actionOption: string = textMock(`policy_editor.action_${mockActionId3}`);
+ await user.click(screen.getByRole('option', { name: actionOption }));
+ await user.tab();
+
+ const [subjectSelect] = screen.getAllByLabelText(
+ textMock('policy_editor.rule_card_subjects_title'),
+ );
+ await user.click(subjectSelect);
+ await user.click(screen.getByRole('option', { name: mockSubjectTitle2 }));
+ await user.tab();
+
+ const [descriptionField] = screen.getAllByLabelText(
+ textMock('policy_editor.rule_card_description_title'),
+ );
+ expect(descriptionField).toHaveValue(mockPolicyRuleCard1.description);
+ await user.type(descriptionField, newWord);
+ await user.tab();
+
+ const numFields = 5;
+ expect(mockPolicyEditorContextValue.savePolicy).toHaveBeenCalledTimes(numFields);
+ });
+});
+
+const renderPolicyRule = (policyEditorContextProps: Partial = {}) => {
+ return render(
+
+
+ ,
+ );
+};
diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyRule.tsx b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyRule.tsx
new file mode 100644
index 00000000000..11c34063201
--- /dev/null
+++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyRule.tsx
@@ -0,0 +1,92 @@
+import React, { useState, useId } from 'react';
+import { ExpandablePolicyElement } from './ExpandablePolicyElement';
+import type { PolicyRuleCard, PolicyError } from '../../../types';
+import { getPolicyRuleIdString } from '../../../utils/PolicyRuleUtils';
+import { useTranslation } from 'react-i18next';
+import { SubResources } from './SubResources';
+import { PolicyRuleContextProvider } from '../../../contexts/PolicyRuleContext';
+import { PolicyActions } from './PolicyActions';
+import { PolicySubjects } from './PolicySubjects';
+import { PolicyDescription } from './PolicyDescription';
+import { PolicyRuleErrorMessage } from './PolicyRuleErrorMessage';
+import { getNewRuleId } from '../../../utils';
+import { usePolicyEditorContext } from '../../../contexts/PolicyEditorContext';
+import { ObjectUtils } from '@studio/pure-functions';
+
+export type PolicyRuleProps = {
+ policyRule: PolicyRuleCard;
+ showErrors: boolean;
+ ruleIndex: number;
+};
+
+export const PolicyRule = ({
+ policyRule,
+ showErrors,
+ ruleIndex,
+}: PolicyRuleProps): React.ReactNode => {
+ const { t } = useTranslation();
+ const { policyRules, setPolicyRules, savePolicy } = usePolicyEditorContext();
+
+ const uniqueId = useId();
+
+ const [policyError, setPolicyError] = useState({
+ resourceError: policyRule.resources.length === 0,
+ actionsError: policyRule.actions.length === 0,
+ subjectsError: policyRule.subject.length === 0,
+ });
+ const { resourceError, actionsError, subjectsError } = policyError;
+
+ const getHasRuleError = () => {
+ return resourceError || actionsError || subjectsError;
+ };
+
+ const handleCloneRule = () => {
+ const newRuleId: string = getNewRuleId(policyRules);
+
+ const ruleToDuplicate: PolicyRuleCard = {
+ ...policyRules[ruleIndex],
+ ruleId: newRuleId,
+ };
+ const deepCopiedRuleToDuplicate: PolicyRuleCard = ObjectUtils.deepCopy(ruleToDuplicate);
+
+ const updatedRules = [...policyRules, deepCopiedRuleToDuplicate];
+ setPolicyRules(updatedRules);
+ savePolicy(updatedRules);
+ };
+
+ const handleDeleteRule = () => {
+ if (confirm(t('policy_editor.verification_modal_text'))) {
+ const updatedRules = [...policyRules];
+ const indexToRemove = updatedRules.findIndex((a) => a.ruleId === policyRule.ruleId);
+ updatedRules.splice(indexToRemove, 1);
+ setPolicyRules(updatedRules);
+ savePolicy(updatedRules);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ {showErrors &&
}
+
+
+ );
+};
diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyRuleErrorMessage/PolicyRuleErrorMessage.test.tsx b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyRuleErrorMessage/PolicyRuleErrorMessage.test.tsx
new file mode 100644
index 00000000000..2f5fff5fe34
--- /dev/null
+++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyRuleErrorMessage/PolicyRuleErrorMessage.test.tsx
@@ -0,0 +1,86 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import { PolicyRuleErrorMessage } from './PolicyRuleErrorMessage';
+import { PolicyEditorContext } from '../../../../contexts/PolicyEditorContext';
+import {
+ PolicyRuleContext,
+ type PolicyRuleContextProps,
+} from '../../../../contexts/PolicyRuleContext';
+import { mockPolicyEditorContextValue } from '../../../../../test/mocks/policyEditorContextMock';
+import { mockPolicyRuleContextValue } from '../../../../../test/mocks/policyRuleContextMock';
+import { textMock } from '@studio/testing/mocks/i18nMock';
+import { type PolicyError } from '../../../../types';
+
+const policyError: PolicyError = mockPolicyRuleContextValue.policyError;
+const ruleId: string = mockPolicyRuleContextValue.policyRule.ruleId;
+
+const errorText1 = textMock('policy_editor.policy_rule_missing_1', {
+ ruleId,
+ missing: textMock('policy_editor.policy_rule_missing_sub_resource'),
+});
+const errorText2 = textMock('policy_editor.policy_rule_missing_2', {
+ ruleId,
+ missing1: textMock('policy_editor.policy_rule_missing_sub_resource'),
+ missing2: textMock('policy_editor.policy_rule_missing_actions'),
+});
+const errorText3 = textMock('policy_editor.policy_rule_missing_3', {
+ ruleId,
+ missing1: textMock('policy_editor.policy_rule_missing_sub_resource'),
+ missing2: textMock('policy_editor.policy_rule_missing_actions'),
+ missing3: textMock('policy_editor.policy_rule_missing_subjects'),
+});
+
+describe('PolicyRuleErrorMessage', () => {
+ afterEach(jest.clearAllMocks);
+
+ it('renders error message when only one error exists', () => {
+ renderPolicyRuleErrorMessage({
+ policyError: { ...policyError, resourceError: true },
+ });
+ expect(screen.getByText(errorText1)).toBeInTheDocument();
+ });
+
+ it('renders error message when two errors exist', () => {
+ renderPolicyRuleErrorMessage({
+ policyError: {
+ ...policyError,
+ resourceError: true,
+ actionsError: true,
+ },
+ });
+ expect(screen.getByText(errorText2)).toBeInTheDocument();
+ });
+
+ it('renders error message when three errors exist', () => {
+ renderPolicyRuleErrorMessage({
+ policyError: {
+ resourceError: true,
+ actionsError: true,
+ subjectsError: true,
+ },
+ });
+ expect(screen.getByText(errorText3)).toBeInTheDocument();
+ });
+
+ it('does not render error message when no errors exist', () => {
+ renderPolicyRuleErrorMessage();
+
+ expect(screen.queryByText(errorText1)).not.toBeInTheDocument();
+ expect(screen.queryByText(errorText2)).not.toBeInTheDocument();
+ expect(screen.queryByText(errorText3)).not.toBeInTheDocument();
+ });
+});
+
+const renderPolicyRuleErrorMessage = (
+ policyRuleContextProps: Partial = {},
+) => {
+ return render(
+
+
+
+
+ ,
+ );
+};
diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyRuleErrorMessage/PolicyRuleErrorMessage.tsx b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyRuleErrorMessage/PolicyRuleErrorMessage.tsx
new file mode 100644
index 00000000000..dae9f46e807
--- /dev/null
+++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyRuleErrorMessage/PolicyRuleErrorMessage.tsx
@@ -0,0 +1,42 @@
+import React from 'react';
+import { usePolicyRuleContext } from '../../../../contexts/PolicyRuleContext';
+import { useTranslation } from 'react-i18next';
+import { ErrorMessage } from '@digdir/design-system-react';
+
+export const PolicyRuleErrorMessage = (): React.ReactElement => {
+ const { t } = useTranslation();
+ const { policyRule, policyError } = usePolicyRuleContext();
+ const { resourceError, actionsError, subjectsError } = policyError;
+
+ const getRuleErrorText = (): string => {
+ const arr: string[] = [];
+ if (resourceError) arr.push(t('policy_editor.policy_rule_missing_sub_resource'));
+ if (actionsError) arr.push(t('policy_editor.policy_rule_missing_actions'));
+ if (subjectsError) arr.push(t('policy_editor.policy_rule_missing_subjects'));
+
+ if (arr.length === 1) {
+ return t('policy_editor.policy_rule_missing_1', {
+ ruleId: policyRule.ruleId,
+ missing: arr[0],
+ });
+ }
+ if (arr.length === 2) {
+ return t('policy_editor.policy_rule_missing_2', {
+ ruleId: policyRule.ruleId,
+ missing1: arr[0],
+ missing2: arr[1],
+ });
+ }
+ if (arr.length === 3) {
+ return t('policy_editor.policy_rule_missing_3', {
+ ruleId: policyRule.ruleId,
+ missing1: arr[0],
+ missing2: arr[1],
+ missing3: arr[2],
+ });
+ }
+ return '';
+ };
+
+ return {getRuleErrorText()};
+};
diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyRuleErrorMessage/index.ts b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyRuleErrorMessage/index.ts
new file mode 100644
index 00000000000..2adc5d7cd49
--- /dev/null
+++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicyRuleErrorMessage/index.ts
@@ -0,0 +1 @@
+export { PolicyRuleErrorMessage } from './PolicyRuleErrorMessage';
diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicySubjects/PolicySubjects.module.css b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicySubjects/PolicySubjects.module.css
new file mode 100644
index 00000000000..7ea36e29e6c
--- /dev/null
+++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicySubjects/PolicySubjects.module.css
@@ -0,0 +1,23 @@
+.label {
+ margin-top: var(--fds-spacing-7);
+ margin-bottom: var(--fds-spacing-1);
+}
+
+.chipWrapper {
+ display: flex;
+ flex-wrap: wrap;
+}
+
+.dropdownWrapper {
+ margin-bottom: var(--fds-spacing-3);
+}
+
+.inputParagraph {
+ margin-bottom: var(--fds-spacing-1);
+}
+
+.chip {
+ margin: 0;
+ margin-right: var(--fds-spacing-1);
+ margin-top: var(--fds-spacing-1);
+}
diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicySubjects/PolicySubjects.test.tsx b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicySubjects/PolicySubjects.test.tsx
new file mode 100644
index 00000000000..e310b91b9d2
--- /dev/null
+++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicySubjects/PolicySubjects.test.tsx
@@ -0,0 +1,104 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { PolicySubjects } from './PolicySubjects';
+import { textMock } from '@studio/testing/mocks/i18nMock';
+import {
+ mockSubjectTitle1,
+ mockSubjectTitle2,
+ mockSubjectTitle3,
+} from '../../../../../test/mocks/policySubjectMocks';
+import { PolicyEditorContext } from '../../../../contexts/PolicyEditorContext';
+import { PolicyRuleContext } from '../../../../contexts/PolicyRuleContext';
+import { mockPolicyEditorContextValue } from '../../../../../test/mocks/policyEditorContextMock';
+import { mockPolicyRuleContextValue } from '../../../../../test/mocks/policyRuleContextMock';
+
+describe('PolicySubjects', () => {
+ afterEach(jest.clearAllMocks);
+
+ it('calls "setPolicyRules" when subjects are edited', async () => {
+ const user = userEvent.setup();
+ renderPolicySubjects();
+
+ const selectedSubject1 = screen.getByLabelText(
+ `${textMock('general.delete')} ${mockSubjectTitle1}`,
+ );
+ const selectedSubject2 = screen.queryByLabelText(
+ `${textMock('general.delete')} ${mockSubjectTitle2}`,
+ );
+ const selectedSubject3 = screen.getByLabelText(
+ `${textMock('general.delete')} ${mockSubjectTitle3}`,
+ );
+ expect(selectedSubject1).toBeInTheDocument();
+ expect(selectedSubject2).not.toBeInTheDocument();
+ expect(selectedSubject3).toBeInTheDocument();
+
+ const [subjectSelect] = screen.getAllByLabelText(
+ textMock('policy_editor.rule_card_subjects_title'),
+ );
+ await user.click(subjectSelect);
+
+ const optionSubject1 = screen.queryByRole('option', { name: mockSubjectTitle1 });
+ const optionSubject2 = screen.getByRole('option', { name: mockSubjectTitle2 });
+ const optionSubject3 = screen.queryByRole('option', { name: mockSubjectTitle3 });
+
+ expect(optionSubject1).not.toBeInTheDocument();
+ expect(optionSubject2).toBeInTheDocument();
+ expect(optionSubject3).not.toBeInTheDocument();
+
+ await user.click(screen.getByRole('option', { name: mockSubjectTitle2 }));
+
+ expect(mockPolicyEditorContextValue.setPolicyRules).toHaveBeenCalledTimes(1);
+
+ expect(
+ screen.queryByLabelText(`${textMock('general.delete')} ${mockSubjectTitle2}`),
+ ).not.toBeInTheDocument();
+
+ const [inputAllSelected] = screen.getAllByText(
+ textMock('policy_editor.rule_card_subjects_select_all_selected'),
+ );
+ expect(inputAllSelected).toBeInTheDocument();
+ });
+
+ it('should append subject to selectable subject options list when selected subject is removed', async () => {
+ const user = userEvent.setup();
+ renderPolicySubjects();
+
+ const [subjectSelect] = screen.getAllByLabelText(
+ textMock('policy_editor.rule_card_subjects_title'),
+ );
+ await user.click(subjectSelect);
+
+ expect(screen.queryByRole('option', { name: mockSubjectTitle1 })).toBeNull();
+
+ const selectedSubject = screen.getByLabelText(
+ `${textMock('general.delete')} ${mockSubjectTitle1}`,
+ );
+ await user.click(selectedSubject);
+
+ await user.click(subjectSelect);
+ expect(screen.getByRole('option', { name: mockSubjectTitle1 })).toBeInTheDocument();
+ });
+
+ it('calls the "setPolicyRules", "savePolicy", and "setPolicyError" function when the chip is clicked', async () => {
+ const user = userEvent.setup();
+ renderPolicySubjects();
+
+ const chipElement = screen.getByText(mockSubjectTitle2);
+ await user.click(chipElement);
+
+ expect(mockPolicyEditorContextValue.setPolicyRules).toHaveBeenCalledTimes(1);
+ expect(mockPolicyEditorContextValue.savePolicy).toHaveBeenCalledTimes(1);
+ expect(mockPolicyRuleContextValue.setPolicyError).toHaveBeenCalledTimes(1);
+ });
+});
+
+const renderPolicySubjects = () => {
+ return render(
+
+
+
+
+ ,
+ );
+};
diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicySubjects/PolicySubjects.tsx b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicySubjects/PolicySubjects.tsx
new file mode 100644
index 00000000000..4f07b842f0d
--- /dev/null
+++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicySubjects/PolicySubjects.tsx
@@ -0,0 +1,100 @@
+import React, { useState } from 'react';
+import classes from './PolicySubjects.module.css';
+import { Label, ErrorMessage, Paragraph, LegacySelect, Chip } from '@digdir/design-system-react';
+import type { PolicySubject } from '../../../../types';
+import { findSubjectByPolicyRuleSubject } from '../../../../utils';
+import { getSubjectOptions, getUpdatedRules } from '../../../../utils/PolicyRuleUtils';
+import { useTranslation } from 'react-i18next';
+import { usePolicyEditorContext } from '../../../../contexts/PolicyEditorContext';
+import { usePolicyRuleContext } from '../../../../contexts/PolicyRuleContext';
+
+export const PolicySubjects = (): React.ReactElement => {
+ const { t } = useTranslation();
+ const { policyRules, subjects, setPolicyRules, savePolicy } = usePolicyEditorContext();
+ const { policyRule, uniqueId, showAllErrors, policyError, setPolicyError } =
+ usePolicyRuleContext();
+
+ const [subjectOptions, setSubjectOptions] = useState(getSubjectOptions(subjects, policyRule));
+
+ const handleRemoveSubject = (index: number, subject: PolicySubject): void => {
+ const updatedSubjects = [...policyRule.subject];
+ updatedSubjects.splice(index, 1);
+
+ setSubjectOptions((prevSubjectOptions) => [
+ ...prevSubjectOptions,
+ {
+ value: subject.subjectId,
+ label: subject.subjectTitle,
+ },
+ ]);
+
+ const updatedRules = getUpdatedRules(
+ { ...policyRule, subject: updatedSubjects },
+ policyRule.ruleId,
+ policyRules,
+ );
+
+ setPolicyRules(updatedRules);
+ savePolicy(updatedRules);
+ setPolicyError({ ...policyError, subjectsError: updatedSubjects.length === 0 });
+ };
+
+ const handleClickSubjectInList = (clickedOption: string) => {
+ // Remove from options list
+ const index = subjectOptions.findIndex((o) => o.value === clickedOption);
+ const updatedOptions = [...subjectOptions];
+ updatedOptions.splice(index, 1);
+ setSubjectOptions(updatedOptions);
+
+ const updatedSubjectTitles = [...policyRule.subject, clickedOption];
+ const updatedRules = getUpdatedRules(
+ { ...policyRule, subject: updatedSubjectTitles },
+ policyRule.ruleId,
+ policyRules,
+ );
+ setPolicyRules(updatedRules);
+ savePolicy(updatedRules);
+ setPolicyError({ ...policyError, subjectsError: false });
+ };
+
+ const displaySubjects = policyRule.subject.map((s, i) => {
+ const subject: PolicySubject = findSubjectByPolicyRuleSubject(subjects, s);
+ return (
+ handleRemoveSubject(i, subject)}
+ >
+ {subject.subjectTitle}
+
+ );
+ });
+
+ return (
+ <>
+
+
+ {subjectOptions.length === 0
+ ? t('policy_editor.rule_card_subjects_select_all_selected')
+ : t('policy_editor.rule_card_subjects_select_add')}
+
+
+ value !== null && handleClickSubjectInList(value)}
+ disabled={subjectOptions.length === 0}
+ error={showAllErrors && policyError.subjectsError}
+ inputId={`selectSubject-${uniqueId}`}
+ />
+
+ {displaySubjects}
+ {showAllErrors && policyError.subjectsError && (
+ {t('policy_editor.rule_card_subjects_error')}
+ )}
+ >
+ );
+};
diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicySubjects/index.ts b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicySubjects/index.ts
new file mode 100644
index 00000000000..5e50d115d1d
--- /dev/null
+++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/PolicySubjects/index.ts
@@ -0,0 +1 @@
+export { PolicySubjects } from './PolicySubjects';
diff --git a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ResourceNarrowingList/PolicyResourceFields/PolicyResourceFields.module.css b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/SubResources/ResourceNarrowingList/PolicyResourceFields/PolicyResourceFields.module.css
similarity index 59%
rename from frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ResourceNarrowingList/PolicyResourceFields/PolicyResourceFields.module.css
rename to frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/SubResources/ResourceNarrowingList/PolicyResourceFields/PolicyResourceFields.module.css
index 2051ac57812..99e340eb062 100644
--- a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ResourceNarrowingList/PolicyResourceFields/PolicyResourceFields.module.css
+++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/SubResources/ResourceNarrowingList/PolicyResourceFields/PolicyResourceFields.module.css
@@ -1,14 +1,14 @@
.wrapper {
display: flex;
justify-content: space-between;
- margin-block: 10px;
+ margin-block: var(--fds-spacing-3);
}
.inputWrapper {
width: 100%;
display: flex;
justify-content: space-between;
- margin-right: 5px;
+ margin-right: var(--fds-spacing-1);
}
.textfieldWrapper {
@@ -16,9 +16,9 @@
}
.buttonWrapper {
- width: 32px;
+ width: var(--fds-spacing-8);
}
.label {
- margin-bottom: 0.5rem;
+ margin-bottom: var(--fds-spacing-2);
}
diff --git a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ResourceNarrowingList/PolicyResourceFields/PolicyResourceFields.test.tsx b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/SubResources/ResourceNarrowingList/PolicyResourceFields/PolicyResourceFields.test.tsx
similarity index 52%
rename from frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ResourceNarrowingList/PolicyResourceFields/PolicyResourceFields.test.tsx
rename to frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/SubResources/ResourceNarrowingList/PolicyResourceFields/PolicyResourceFields.test.tsx
index 4e11f61f2c3..5538e967151 100644
--- a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ResourceNarrowingList/PolicyResourceFields/PolicyResourceFields.test.tsx
+++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/SubResources/ResourceNarrowingList/PolicyResourceFields/PolicyResourceFields.test.tsx
@@ -4,31 +4,26 @@ import userEvent from '@testing-library/user-event';
import type { PolicyResourceFieldsProps } from './PolicyResourceFields';
import { PolicyResourceFields } from './PolicyResourceFields';
import { textMock } from '@studio/testing/mocks/i18nMock';
+import { PolicyEditorContext } from '../../../../../../contexts/PolicyEditorContext';
+import { PolicyRuleContext } from '../../../../../../contexts/PolicyRuleContext';
+import { mockPolicyEditorContextValue } from '../../../../../../../test/mocks/policyEditorContextMock';
+import { mockPolicyRuleContextValue } from '../../../../../../../test/mocks/policyRuleContextMock';
+import { mockResource11 } from '../../../../../../../test/mocks/policySubResourceMocks';
-const mockValueId: string = 'Test123';
-const mockValueType: string = '123Test';
const mockValudNewText = '45';
+const defaultProps: PolicyResourceFieldsProps = {
+ resource: mockResource11,
+ canEditTypeAndId: true,
+ resourceIndex: 0,
+ resourceNarrowingIndex: 0,
+};
+
describe('PolicyResourceFields', () => {
afterEach(jest.clearAllMocks);
- const mockOnRemove = jest.fn();
- const mockOnChangeId = jest.fn();
- const mockOnChangeType = jest.fn();
- const mockOnBlur = jest.fn();
-
- const defaultProps: PolicyResourceFieldsProps = {
- canEditTypeAndId: true,
- onRemove: mockOnRemove,
- valueId: mockValueId,
- onChangeId: mockOnChangeId,
- valueType: mockValueType,
- onChangeType: mockOnChangeType,
- onBlur: mockOnBlur,
- };
-
it('sets text fields to readonly when "canEditTypeAndId" is false', () => {
- render();
+ renderPolicyResourceFields({ canEditTypeAndId: false });
const idInput = screen.getByLabelText(textMock('policy_editor.narrowing_list_field_id'));
expect(idInput).toHaveAttribute('readonly');
@@ -38,7 +33,7 @@ describe('PolicyResourceFields', () => {
});
it('sets text fields to not be readonly when "canEditTypeAndId" is true', () => {
- render();
+ renderPolicyResourceFields();
const idInput = screen.getByLabelText(textMock('policy_editor.narrowing_list_field_id'));
expect(idInput).not.toHaveAttribute('readonly');
@@ -47,41 +42,45 @@ describe('PolicyResourceFields', () => {
expect(typeInput).not.toHaveAttribute('readonly');
});
- it('calls "onChangeId" when id input values change', async () => {
+ it('calls "setPolicyRules" when id input values change', async () => {
const user = userEvent.setup();
- render();
+ renderPolicyResourceFields();
const idInput = screen.getByLabelText(textMock('policy_editor.narrowing_list_field_id'));
await user.type(idInput, mockValudNewText);
- expect(mockOnChangeId).toHaveBeenCalledTimes(mockValudNewText.length);
+ expect(mockPolicyEditorContextValue.setPolicyRules).toHaveBeenCalledTimes(
+ mockValudNewText.length,
+ );
});
- it('calls "onChangeType" when type input values change', async () => {
+ it('calls "setPolicyRules" when type input values change', async () => {
const user = userEvent.setup();
- render();
+ renderPolicyResourceFields();
const typeInput = screen.getByLabelText(textMock('policy_editor.narrowing_list_field_type'));
await user.type(typeInput, mockValudNewText);
- expect(mockOnChangeType).toHaveBeenCalledTimes(mockValudNewText.length);
+ expect(mockPolicyEditorContextValue.setPolicyRules).toHaveBeenCalledTimes(
+ mockValudNewText.length,
+ );
});
- it('calls "onBlur" when input fields lose focus', async () => {
+ it('calls "savePolicy" when input fields lose focus', async () => {
const user = userEvent.setup();
- render();
+ renderPolicyResourceFields();
const typeInput = screen.getByLabelText(textMock('policy_editor.narrowing_list_field_type'));
await user.type(typeInput, mockValudNewText);
await user.tab();
- expect(mockOnBlur).toHaveBeenCalledTimes(1);
+ expect(mockPolicyEditorContextValue.savePolicy).toHaveBeenCalledTimes(1);
});
it('hides the delete button when "canEditTypeAndId" is false', () => {
- render();
+ renderPolicyResourceFields({ canEditTypeAndId: false });
const deleteButton = screen.queryByRole('button', {
name: textMock('policy_editor.narrowing_list_field_delete'),
@@ -90,9 +89,9 @@ describe('PolicyResourceFields', () => {
expect(deleteButton).not.toBeInTheDocument();
});
- it('calls "onRemove" when delete button is clicked', async () => {
+ it('calls "setPolicyRules" and "savePolicy" when delete button is clicked', async () => {
const user = userEvent.setup();
- render();
+ renderPolicyResourceFields();
const deleteButton = screen.getByRole('button', {
name: textMock('policy_editor.narrowing_list_field_delete'),
@@ -102,6 +101,19 @@ describe('PolicyResourceFields', () => {
await user.click(deleteButton);
- expect(mockOnRemove).toHaveBeenCalledTimes(1);
+ expect(mockPolicyEditorContextValue.setPolicyRules).toHaveBeenCalledTimes(1);
+ expect(mockPolicyEditorContextValue.savePolicy).toHaveBeenCalledTimes(1);
});
});
+
+const renderPolicyResourceFields = (
+ policyResourceFieldsProps: Partial = {},
+) => {
+ return render(
+
+
+
+
+ ,
+ );
+};
diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/SubResources/ResourceNarrowingList/PolicyResourceFields/PolicyResourceFields.tsx b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/SubResources/ResourceNarrowingList/PolicyResourceFields/PolicyResourceFields.tsx
new file mode 100644
index 00000000000..b43559ed879
--- /dev/null
+++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/SubResources/ResourceNarrowingList/PolicyResourceFields/PolicyResourceFields.tsx
@@ -0,0 +1,113 @@
+import React from 'react';
+import classes from './PolicyResourceFields.module.css';
+import { MultiplyIcon } from '@studio/icons';
+import { useTranslation } from 'react-i18next';
+import { StudioButton, StudioLabelAsParagraph, StudioTextfield } from '@studio/components';
+import { usePolicyEditorContext } from '../../../../../../contexts/PolicyEditorContext';
+import { usePolicyRuleContext } from '../../../../../../contexts/PolicyRuleContext';
+import { getUpdatedRules } from '../../../../../../utils/PolicyRuleUtils';
+import { type PolicyRuleResource } from '../../../../../../types';
+
+export type PolicyResourceFieldsProps = {
+ resource: PolicyRuleResource;
+ canEditTypeAndId: boolean;
+ resourceIndex: number;
+ resourceNarrowingIndex: number;
+};
+
+export const PolicyResourceFields = ({
+ resource,
+ canEditTypeAndId,
+ resourceIndex,
+ resourceNarrowingIndex,
+}: PolicyResourceFieldsProps): React.ReactNode => {
+ const { t } = useTranslation();
+ const { savePolicy, setPolicyRules, policyRules } = usePolicyEditorContext();
+ const { policyRule } = usePolicyRuleContext();
+
+ const handleInputChange = (field: 'id' | 'type', value: string) => {
+ const updatedResources = [...policyRule.resources];
+ updatedResources[resourceIndex][resourceNarrowingIndex] = {
+ ...updatedResources[resourceIndex][resourceNarrowingIndex],
+ [field]: value,
+ };
+
+ const updatedRules = getUpdatedRules(
+ { ...policyRule, resources: updatedResources },
+ policyRule.ruleId,
+ policyRules,
+ );
+ setPolicyRules(updatedRules);
+ };
+
+ const handleBlur = () => {
+ savePolicy(policyRules);
+ };
+
+ const handleRemoveNarrowingResource = () => {
+ const updatedResources = [...policyRule.resources];
+ updatedResources[resourceIndex].splice(resourceNarrowingIndex, 1);
+ const updatedRules = getUpdatedRules(
+ { ...policyRule, resources: updatedResources },
+ policyRule.ruleId,
+ policyRules,
+ );
+ setPolicyRules(updatedRules);
+ savePolicy(updatedRules);
+ };
+
+ return (
+
+
+
+ {!canEditTypeAndId && (
+
+ Type
+
+ )}
+ ) =>
+ handleInputChange('type', event.target.value)
+ }
+ readOnly={!canEditTypeAndId}
+ onBlur={handleBlur}
+ aria-label={t('policy_editor.narrowing_list_field_type')}
+ />
+
+
+ {!canEditTypeAndId && (
+
+ Id
+
+ )}
+ ) =>
+ handleInputChange('id', event.target.value)
+ }
+ readOnly={!canEditTypeAndId}
+ onBlur={handleBlur}
+ aria-label={t('policy_editor.narrowing_list_field_id')}
+ />
+
+
+
+ {canEditTypeAndId && (
+ }
+ onClick={handleRemoveNarrowingResource}
+ size='small'
+ title={t('policy_editor.narrowing_list_field_delete')}
+ variant='tertiary'
+ />
+ )}
+
+
+ );
+};
diff --git a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ResourceNarrowingList/PolicyResourceFields/index.ts b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/SubResources/ResourceNarrowingList/PolicyResourceFields/index.ts
similarity index 100%
rename from frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ResourceNarrowingList/PolicyResourceFields/index.ts
rename to frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/SubResources/ResourceNarrowingList/PolicyResourceFields/index.ts
diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/SubResources/ResourceNarrowingList/ResourceNarrowingList.module.css b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/SubResources/ResourceNarrowingList/ResourceNarrowingList.module.css
new file mode 100644
index 00000000000..21c43cc143a
--- /dev/null
+++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/SubResources/ResourceNarrowingList/ResourceNarrowingList.module.css
@@ -0,0 +1,8 @@
+.wrapper {
+ margin-top: var(--fds-spacing-3);
+ margin-bottom: var(--fds-spacing-5);
+}
+
+.buttonWrapper {
+ margin-bottom: var(--fds-spacing-3);
+}
diff --git a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ResourceNarrowingList/ResourceNarrowingList.test.tsx b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/SubResources/ResourceNarrowingList/ResourceNarrowingList.test.tsx
similarity index 55%
rename from frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ResourceNarrowingList/ResourceNarrowingList.test.tsx
rename to frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/SubResources/ResourceNarrowingList/ResourceNarrowingList.test.tsx
index 4d0cb75982c..82fd109615d 100644
--- a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ResourceNarrowingList/ResourceNarrowingList.test.tsx
+++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/SubResources/ResourceNarrowingList/ResourceNarrowingList.test.tsx
@@ -4,39 +4,31 @@ import userEvent from '@testing-library/user-event';
import type { ResourceNarrowingListProps } from './ResourceNarrowingList';
import { ResourceNarrowingList } from './ResourceNarrowingList';
import { textMock } from '@studio/testing/mocks/i18nMock';
-import type { PolicyRuleResource, PolicyEditorUsage } from '../../../types';
+import type { PolicyRuleResource } from '../../../../../types';
+import {
+ PolicyEditorContext,
+ type PolicyEditorContextProps,
+} from '../../../../../contexts/PolicyEditorContext';
+import { PolicyRuleContext } from '../../../../../contexts/PolicyRuleContext';
+import { mockPolicyEditorContextValue } from '../../../../../../test/mocks/policyEditorContextMock';
+import { mockPolicyRuleContextValue } from '../../../../../../test/mocks/policyRuleContextMock';
const mockResource1: PolicyRuleResource = { type: 'type1', id: 'id1' };
const mockResource2: PolicyRuleResource = { type: 'type2', id: 'id2' };
const mockResources: PolicyRuleResource[] = [mockResource1, mockResource2];
-const mockUsageType: PolicyEditorUsage = 'app';
-
const mockNewText: string = 'test';
+const defaultProps: ResourceNarrowingListProps = {
+ resources: mockResources,
+ resourceIndex: 0,
+};
+
describe('ResourceNarrowingList', () => {
afterEach(jest.clearAllMocks);
- const mockHandleInputChange = jest.fn();
- const mockHandleRemoveResource = jest.fn();
- const mockHandleClickAddResource = jest.fn();
- const mockHandleRemoveElement = jest.fn();
- const mockHandleCloneElement = jest.fn();
- const mockOnBlur = jest.fn();
-
- const defaultProps: ResourceNarrowingListProps = {
- resources: mockResources,
- handleInputChange: mockHandleInputChange,
- handleRemoveResource: mockHandleRemoveResource,
- handleClickAddResource: mockHandleClickAddResource,
- handleRemoveElement: mockHandleRemoveElement,
- handleCloneElement: mockHandleCloneElement,
- onBlur: mockOnBlur,
- usageType: mockUsageType,
- };
-
it('renders the list of resources', () => {
- render();
+ renderResourceNarrowingList();
const removeButtons = screen.getAllByRole('button', {
name: textMock('policy_editor.narrowing_list_field_delete'),
@@ -56,7 +48,7 @@ describe('ResourceNarrowingList', () => {
});
it('does not show the delete button for the first resource when "usageType" is resource', () => {
- render();
+ renderResourceNarrowingList({ usageType: 'resource' });
const removeButtons = screen.getAllByRole('button', {
name: textMock('policy_editor.narrowing_list_field_delete'),
@@ -65,26 +57,9 @@ describe('ResourceNarrowingList', () => {
expect(removeButtons).toHaveLength(numItems - 1); // Minus first element which is readonly
});
- it('calls "handleInputChange" when id or type is edited', async () => {
- const user = userEvent.setup();
- render();
-
- const [idInput] = screen.getAllByLabelText(textMock('policy_editor.narrowing_list_field_id'));
- await user.type(idInput, mockNewText);
- expect(mockHandleInputChange).toHaveBeenCalledTimes(mockNewText.length);
-
- mockHandleInputChange.mockClear();
-
- const [typeInput] = screen.getAllByLabelText(
- textMock('policy_editor.narrowing_list_field_type'),
- );
- await user.type(typeInput, mockNewText);
- expect(mockHandleInputChange).toHaveBeenCalledTimes(mockNewText.length);
- });
-
- it('calls "handleRemoveResource" when remove resource button is clicked', async () => {
+ it('calls "setPolicyRules" and "savePolicy" when remove resource button is clicked', async () => {
const user = userEvent.setup();
- render();
+ renderResourceNarrowingList();
const [deleteResourceButton] = screen.getAllByRole('button', {
name: textMock('policy_editor.narrowing_list_field_delete'),
@@ -92,12 +67,13 @@ describe('ResourceNarrowingList', () => {
await user.click(deleteResourceButton);
- expect(mockHandleRemoveResource).toHaveBeenCalledTimes(1);
+ expect(mockPolicyEditorContextValue.setPolicyRules).toHaveBeenCalledTimes(1);
+ expect(mockPolicyEditorContextValue.savePolicy).toHaveBeenCalledTimes(1);
});
- it('calls "handleClickAddResource" when add button is clicked', async () => {
+ it('calls "setPolicyRules" and "savePolicy" when add button is clicked', async () => {
const user = userEvent.setup();
- render();
+ renderResourceNarrowingList();
const addResourceButton = screen.getByRole('button', {
name: textMock('policy_editor.narrowing_list_add_button'),
@@ -105,12 +81,13 @@ describe('ResourceNarrowingList', () => {
await user.click(addResourceButton);
- expect(mockHandleClickAddResource).toHaveBeenCalledTimes(1);
+ expect(mockPolicyEditorContextValue.setPolicyRules).toHaveBeenCalledTimes(1);
+ expect(mockPolicyEditorContextValue.savePolicy).toHaveBeenCalledTimes(1);
});
- it('calls "handleRemoveElement" when remove element button is clicked', async () => {
+ it('calls "setPolicyRules" and "savePolicy" when remove element button is clicked', async () => {
const user = userEvent.setup();
- render();
+ renderResourceNarrowingList();
const [moreButton] = screen.getAllByRole('button', {
name: textMock('policy_editor.more'),
@@ -122,12 +99,13 @@ describe('ResourceNarrowingList', () => {
});
await user.click(deleteElementButton);
- expect(mockHandleRemoveElement).toHaveBeenCalledTimes(1);
+ expect(mockPolicyEditorContextValue.setPolicyRules).toHaveBeenCalledTimes(1);
+ expect(mockPolicyEditorContextValue.savePolicy).toHaveBeenCalledTimes(1);
});
- it('calls "handleCloneElement" when clone element button is clicked', async () => {
+ it('calls "setPolicyRules" and "savePolicy" when clone element button is clicked', async () => {
const user = userEvent.setup();
- render();
+ renderResourceNarrowingList();
const [moreButton] = screen.getAllByRole('button', {
name: textMock('policy_editor.more'),
@@ -139,12 +117,13 @@ describe('ResourceNarrowingList', () => {
});
await user.click(cloneElementButton);
- expect(mockHandleCloneElement).toHaveBeenCalledTimes(1);
+ expect(mockPolicyEditorContextValue.setPolicyRules).toHaveBeenCalledTimes(1);
+ expect(mockPolicyEditorContextValue.savePolicy).toHaveBeenCalledTimes(1);
});
- it('calls "onBlur" when a textfield is left', async () => {
+ it('calls "savePolicy" when a textfield is left', async () => {
const user = userEvent.setup();
- render();
+ renderResourceNarrowingList();
const [typeInput] = screen.getAllByLabelText(
textMock('policy_editor.narrowing_list_field_type'),
@@ -152,6 +131,20 @@ describe('ResourceNarrowingList', () => {
await user.type(typeInput, mockNewText);
await user.tab();
- expect(mockOnBlur).toHaveBeenCalledTimes(1);
+ expect(mockPolicyEditorContextValue.savePolicy).toHaveBeenCalledTimes(1);
});
});
+
+const renderResourceNarrowingList = (
+ policyEditorContextProps: Partial = {},
+) => {
+ return render(
+
+
+
+
+ ,
+ );
+};
diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/SubResources/ResourceNarrowingList/ResourceNarrowingList.tsx b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/SubResources/ResourceNarrowingList/ResourceNarrowingList.tsx
new file mode 100644
index 00000000000..ee03431204d
--- /dev/null
+++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/SubResources/ResourceNarrowingList/ResourceNarrowingList.tsx
@@ -0,0 +1,104 @@
+import React from 'react';
+import classes from './ResourceNarrowingList.module.css';
+import { PolicyResourceFields } from './PolicyResourceFields';
+import { ExpandablePolicyElement } from '../../ExpandablePolicyElement';
+import { StudioButton } from '@studio/components';
+import { PlusIcon } from '@studio/icons';
+import type { PolicyRuleResource } from '../../../../../types';
+import { useTranslation } from 'react-i18next';
+import { usePolicyEditorContext } from '../../../../../contexts/PolicyEditorContext';
+import { usePolicyRuleContext } from '../../../../../contexts/PolicyRuleContext';
+import { getUpdatedRules } from '../../../../../utils/PolicyRuleUtils';
+import { ObjectUtils } from '@studio/pure-functions';
+
+export type ResourceNarrowingListProps = {
+ resources: PolicyRuleResource[];
+ resourceIndex: number;
+};
+
+export const ResourceNarrowingList = ({
+ resources,
+ resourceIndex,
+}: ResourceNarrowingListProps): React.ReactNode => {
+ const { usageType, setPolicyRules, policyRules, savePolicy } = usePolicyEditorContext();
+ const { policyRule, setPolicyError, policyError } = usePolicyRuleContext();
+
+ const { t } = useTranslation();
+
+ const handleDeleteResourceGroup = () => {
+ const updatedResources = [...policyRule.resources];
+ updatedResources.splice(resourceIndex, 1);
+ updatePolicyStates(updatedResources);
+ setPolicyError({ ...policyError, resourceError: updatedResources.length === 0 });
+ };
+
+ const handleCloneResourceGroup = () => {
+ const resourceGroupToDuplicate: PolicyRuleResource[] = policyRule.resources[resourceIndex];
+ const deepCopiedResourceGroupToDuplicate: PolicyRuleResource[] =
+ ObjectUtils.deepCopy(resourceGroupToDuplicate);
+
+ const updatedResources = [...policyRule.resources, deepCopiedResourceGroupToDuplicate];
+ updatePolicyStates(updatedResources);
+ };
+
+ const handleClickAddResourceNarrowing = () => {
+ const newResource: PolicyRuleResource = {
+ type: '',
+ id: '',
+ };
+ const updatedResources = [...policyRule.resources];
+ updatedResources[resourceIndex].push(newResource);
+ updatePolicyStates(updatedResources);
+ };
+
+ const updatePolicyStates = (updatedResources: PolicyRuleResource[][]) => {
+ const updatedRules = getUpdatedRules(
+ { ...policyRule, resources: updatedResources },
+ policyRule.ruleId,
+ policyRules,
+ );
+ setPolicyRules(updatedRules);
+ savePolicy(updatedRules);
+ };
+
+ const displayResources = resources.map((resource: PolicyRuleResource, i) => {
+ return (
+ 0}
+ resourceIndex={resourceIndex}
+ resourceNarrowingIndex={i}
+ />
+ );
+ });
+
+ const getResourceName = (): string => {
+ return resources.map((r) => r.id).join(' - ');
+ };
+
+ return (
+
+
+ {displayResources}
+
+ }
+ >
+ {t('policy_editor.narrowing_list_add_button')}
+
+
+
+
+ );
+};
diff --git a/frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ResourceNarrowingList/index.ts b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/SubResources/ResourceNarrowingList/index.ts
similarity index 100%
rename from frontend/packages/policy-editor/src/components/ExpandablePolicyCard/ResourceNarrowingList/index.ts
rename to frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/SubResources/ResourceNarrowingList/index.ts
diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/SubResources/SubResources.module.css b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/SubResources/SubResources.module.css
new file mode 100644
index 00000000000..eeacd57d90f
--- /dev/null
+++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/SubResources/SubResources.module.css
@@ -0,0 +1,9 @@
+.addResourceButton {
+ width: 100%;
+ margin-bottom: var(--fds-spacing-3);
+}
+
+.label {
+ margin-top: var(--fds-spacing-7);
+ margin-bottom: var(--fds-spacing-1);
+}
diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/SubResources/SubResources.test.tsx b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/SubResources/SubResources.test.tsx
new file mode 100644
index 00000000000..1fdf96e0bff
--- /dev/null
+++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/SubResources/SubResources.test.tsx
@@ -0,0 +1,48 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { textMock } from '@studio/testing/mocks/i18nMock';
+import {
+ PolicyEditorContext,
+ type PolicyEditorContextProps,
+} from '../../../../contexts/PolicyEditorContext';
+import { PolicyRuleContext } from '../../../../contexts/PolicyRuleContext';
+import { mockPolicyEditorContextValue } from '../../../../../test/mocks/policyEditorContextMock';
+import { mockPolicyRuleContextValue } from '../../../../../test/mocks/policyRuleContextMock';
+import { SubResources } from './SubResources';
+
+describe('SubResources', () => {
+ it('calls "setPolicyRules" when sub-resource fields are edited', async () => {
+ const user = userEvent.setup();
+
+ const mockSetPolicyRules = jest.fn();
+ renderSubResources({ setPolicyRules: mockSetPolicyRules });
+
+ const [typeInput] = screen.getAllByLabelText(
+ textMock('policy_editor.narrowing_list_field_type'),
+ );
+ const [idInput] = screen.getAllByLabelText(textMock('policy_editor.narrowing_list_field_id'));
+
+ const newWord: string = 'test';
+
+ await user.type(typeInput, newWord);
+ expect(mockSetPolicyRules).toHaveBeenCalledTimes(newWord.length);
+
+ mockSetPolicyRules.mockClear();
+
+ await user.type(idInput, newWord);
+ expect(mockSetPolicyRules).toHaveBeenCalledTimes(newWord.length);
+ });
+});
+
+const renderSubResources = (policyEditorContextProps: Partial = {}) => {
+ return render(
+
+
+
+
+ ,
+ );
+};
diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/SubResources/SubResources.tsx b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/SubResources/SubResources.tsx
new file mode 100644
index 00000000000..d31282aca90
--- /dev/null
+++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/SubResources/SubResources.tsx
@@ -0,0 +1,74 @@
+import React from 'react';
+import classes from './SubResources.module.css';
+import { StudioButton, StudioLabelAsParagraph } from '@studio/components';
+import { PlusIcon } from '@studio/icons';
+import { ResourceNarrowingList } from './ResourceNarrowingList';
+import { useTranslation } from 'react-i18next';
+import { createNewPolicyResource } from '../../../../utils';
+import { usePolicyEditorContext } from '../../../../contexts/PolicyEditorContext';
+import { usePolicyRuleContext } from '../../../../contexts/PolicyRuleContext';
+import { getUpdatedRules } from '../../../../utils/PolicyRuleUtils';
+import type { PolicyRuleResource } from '../../../../types';
+import { ErrorMessage } from '@digdir/design-system-react';
+
+export const SubResources = (): React.ReactElement => {
+ const { t } = useTranslation();
+ const {
+ policyRules: rules,
+ setPolicyRules,
+ usageType,
+ resourceType,
+ resourceId,
+ savePolicy,
+ } = usePolicyEditorContext();
+
+ const { policyRule, setPolicyError, policyError, showAllErrors } = usePolicyRuleContext();
+
+ const handleClickAddResource = () => {
+ const newResource: PolicyRuleResource[] = createNewPolicyResource(
+ usageType,
+ resourceType,
+ resourceId,
+ );
+
+ const updatedResources = [...policyRule.resources, newResource];
+ const updatedRules = getUpdatedRules(
+ { ...policyRule, resources: updatedResources },
+ policyRule.ruleId,
+ rules,
+ );
+ setPolicyRules(updatedRules);
+ savePolicy(updatedRules);
+ setPolicyError({ ...policyError, resourceError: false });
+ };
+
+ const displayResources = policyRule.resources.map((r, i) => {
+ return (
+
+ );
+ });
+
+ return (
+ <>
+
+ {t('policy_editor.rule_card_sub_resource_title')}
+
+ {displayResources}
+
+ }
+ >
+ {t('policy_editor.rule_card_sub_resource_button')}
+
+
+ {showAllErrors && policyError.resourceError && (
+ {t('policy_editor.rule_card_sub_resource_error')}
+ )}
+ >
+ );
+};
diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/SubResources/index.ts b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/SubResources/index.ts
new file mode 100644
index 00000000000..38244682cc0
--- /dev/null
+++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/SubResources/index.ts
@@ -0,0 +1 @@
+export { SubResources } from './SubResources';
diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/index.ts b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/index.ts
new file mode 100644
index 00000000000..298361f093e
--- /dev/null
+++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/PolicyRule/index.ts
@@ -0,0 +1 @@
+export { PolicyRule } from './PolicyRule';
diff --git a/frontend/packages/policy-editor/src/components/PolicyCardRules/index.ts b/frontend/packages/policy-editor/src/components/PolicyCardRules/index.ts
new file mode 100644
index 00000000000..82de803eae5
--- /dev/null
+++ b/frontend/packages/policy-editor/src/components/PolicyCardRules/index.ts
@@ -0,0 +1 @@
+export { PolicyCardRules } from './PolicyCardRules';
diff --git a/frontend/packages/policy-editor/src/components/PolicyEditorAlert/PolicyEditorAlert.module.css b/frontend/packages/policy-editor/src/components/PolicyEditorAlert/PolicyEditorAlert.module.css
new file mode 100644
index 00000000000..b2e449722f3
--- /dev/null
+++ b/frontend/packages/policy-editor/src/components/PolicyEditorAlert/PolicyEditorAlert.module.css
@@ -0,0 +1,4 @@
+.alert {
+ display: flex;
+ align-items: center;
+}
diff --git a/frontend/packages/policy-editor/src/components/PolicyEditorAlert/PolicyEditorAlert.test.tsx b/frontend/packages/policy-editor/src/components/PolicyEditorAlert/PolicyEditorAlert.test.tsx
new file mode 100644
index 00000000000..c7f96f1ef08
--- /dev/null
+++ b/frontend/packages/policy-editor/src/components/PolicyEditorAlert/PolicyEditorAlert.test.tsx
@@ -0,0 +1,60 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { PolicyEditorAlert } from './PolicyEditorAlert';
+import {
+ PolicyEditorContext,
+ type PolicyEditorContextProps,
+} from '../../contexts/PolicyEditorContext';
+import { mockPolicyEditorContextValue } from '../../../test/mocks/policyEditorContextMock';
+import { textMock } from '@studio/testing/mocks/i18nMock';
+
+describe('PolicyEditorAlert', () => {
+ afterEach(jest.clearAllMocks);
+
+ it('displays the alert title for app when usagetype is app', async () => {
+ const user = userEvent.setup();
+ renderPolicyEditorAlert();
+
+ const alertTextApp = screen.getByText(
+ textMock('policy_editor.alert', { usageType: textMock('policy_editor.alert_app') }),
+ );
+ const alertTextResource = screen.queryByText(
+ textMock('policy_editor.alert', { usageType: textMock('policy_editor.alert_resource') }),
+ );
+
+ await user.tab();
+
+ expect(alertTextApp).toBeInTheDocument();
+ expect(alertTextResource).not.toBeInTheDocument();
+ });
+
+ it('displays the alert title for resource when usagetype is not app', async () => {
+ const user = userEvent.setup();
+ renderPolicyEditorAlert({ usageType: 'resource' });
+
+ const alertTextApp = screen.queryByText(
+ textMock('policy_editor.alert', { usageType: textMock('policy_editor.alert_app') }),
+ );
+ const alertTextResource = screen.getByText(
+ textMock('policy_editor.alert', { usageType: textMock('policy_editor.alert_resource') }),
+ );
+
+ await user.tab();
+
+ expect(alertTextApp).not.toBeInTheDocument();
+ expect(alertTextResource).toBeInTheDocument();
+ });
+});
+
+const renderPolicyEditorAlert = (
+ policyEditorContextProps: Partial = {},
+) => {
+ return render(
+
+
+ ,
+ );
+};
diff --git a/frontend/packages/policy-editor/src/components/PolicyEditorAlert/PolicyEditorAlert.tsx b/frontend/packages/policy-editor/src/components/PolicyEditorAlert/PolicyEditorAlert.tsx
new file mode 100644
index 00000000000..92c7be802ef
--- /dev/null
+++ b/frontend/packages/policy-editor/src/components/PolicyEditorAlert/PolicyEditorAlert.tsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import classes from './PolicyEditorAlert.module.css';
+import { Alert, Paragraph } from '@digdir/design-system-react';
+import { usePolicyEditorContext } from '../../contexts/PolicyEditorContext';
+import { useTranslation } from 'react-i18next';
+
+export const PolicyEditorAlert = (): React.ReactElement => {
+ const { usageType } = usePolicyEditorContext();
+ const { t } = useTranslation();
+
+ return (
+
+
+ {t('policy_editor.alert', {
+ usageType:
+ usageType === 'app' ? t('policy_editor.alert_app') : t('policy_editor.alert_resource'),
+ })}
+
+
+ );
+};
diff --git a/frontend/packages/policy-editor/src/components/PolicyEditorAlert/index.ts b/frontend/packages/policy-editor/src/components/PolicyEditorAlert/index.ts
new file mode 100644
index 00000000000..630f70248c9
--- /dev/null
+++ b/frontend/packages/policy-editor/src/components/PolicyEditorAlert/index.ts
@@ -0,0 +1 @@
+export { PolicyEditorAlert } from './PolicyEditorAlert';
diff --git a/frontend/packages/policy-editor/src/components/SecurityLevelSelect/SecurityLevelSelect.module.css b/frontend/packages/policy-editor/src/components/SecurityLevelSelect/SecurityLevelSelect.module.css
index b2706ffb740..bda443acec8 100644
--- a/frontend/packages/policy-editor/src/components/SecurityLevelSelect/SecurityLevelSelect.module.css
+++ b/frontend/packages/policy-editor/src/components/SecurityLevelSelect/SecurityLevelSelect.module.css
@@ -1,23 +1,15 @@
-.selectAuthLevel {
- width: 500px;
-}
-
.labelAndHelpTextWrapper {
display: flex;
align-items: center;
- gap: 10px;
- padding-bottom: 5px;
+ gap: var(--fds-spacing-2);
+ padding-bottom: var(--fds-spacing-1);
}
.paragraph {
- margin-bottom: 20px;
+ margin-bottom: var(--fds-spacing-5);
}
.link {
display: inline;
font: var(--fds-typography-paragraph-small);
}
-
-.securityLevelContainer {
- max-width: 650px;
-}
diff --git a/frontend/packages/policy-editor/src/components/SecurityLevelSelect/SecurityLevelSelect.tsx b/frontend/packages/policy-editor/src/components/SecurityLevelSelect/SecurityLevelSelect.tsx
index 293c407ac82..f944ef90e37 100644
--- a/frontend/packages/policy-editor/src/components/SecurityLevelSelect/SecurityLevelSelect.tsx
+++ b/frontend/packages/policy-editor/src/components/SecurityLevelSelect/SecurityLevelSelect.tsx
@@ -29,15 +29,6 @@ export type SecurityLevelSelectProps = {
onSave: (authLevel: RequiredAuthLevel) => void;
};
-/**
- * @component
- * Displays the security level area in the policy editor
- *
- * @property {RequiredAuthLevel}[requiredAuthenticationLevelEndUser] - The required auth level in the policy
- * @property {function}[onSave] - Function to be executed when saving the policy
- *
- * @returns {ReactNode} - The rendered component
- */
export const SecurityLevelSelect = ({
requiredAuthenticationLevelEndUser,
onSave,
@@ -52,14 +43,14 @@ export const SecurityLevelSelect = ({
}, [t]);
return (
-
+
{t('policy_editor.security_level_label')}
{t('policy_editor.security_level_description')}
-
+
{/* This is added because the 'label' in the Select component is not bold */}
- }
- closeButtonLabel={t('policy_editor.close_verification_modal_button')}
- >
-
-
{text}
-
-
-
- {closeButtonText}
-
-
-
- {actionButtonText}
-
-
-
-
- );
-};
diff --git a/frontend/packages/policy-editor/src/components/VerificationModal/index.ts b/frontend/packages/policy-editor/src/components/VerificationModal/index.ts
deleted file mode 100644
index 5d375f859ae..00000000000
--- a/frontend/packages/policy-editor/src/components/VerificationModal/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { VerificationModal } from './VerificationModal';
diff --git a/frontend/packages/policy-editor/src/contexts/PolicyEditorContext/PolicyEditorContext.test.tsx b/frontend/packages/policy-editor/src/contexts/PolicyEditorContext/PolicyEditorContext.test.tsx
new file mode 100644
index 00000000000..f4fec4e6f49
--- /dev/null
+++ b/frontend/packages/policy-editor/src/contexts/PolicyEditorContext/PolicyEditorContext.test.tsx
@@ -0,0 +1,45 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import { PolicyEditorContext, usePolicyEditorContext } from './PolicyEditorContext';
+import { mockPolicyEditorContextValue } from '../../../test/mocks/policyEditorContextMock';
+
+describe('PolicyEditorContext', () => {
+ it('should render children', () => {
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByRole('button', { name: 'My button' })).toBeInTheDocument();
+ });
+
+ it('should provide a usePolicyEditorContext hook', () => {
+ const TestComponent = () => {
+ const {} = usePolicyEditorContext();
+ return
;
+ };
+
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByTestId('context')).toHaveTextContent('');
+ });
+
+ it('should throw an error when usePolicyEditorContext is used outside of a PolicyEditorContextProvider', () => {
+ // Mock console error to check if it has been called
+ const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
+ const TestComponent = () => {
+ usePolicyEditorContext();
+ return
Test
;
+ };
+
+ expect(() => render(
)).toThrow(
+ 'usePolicyEditorContext must be used within a PolicyEditorContextProvider',
+ );
+ expect(consoleError).toHaveBeenCalled();
+ });
+});
diff --git a/frontend/packages/policy-editor/src/contexts/PolicyEditorContext/PolicyEditorContext.tsx b/frontend/packages/policy-editor/src/contexts/PolicyEditorContext/PolicyEditorContext.tsx
new file mode 100644
index 00000000000..a93948441c1
--- /dev/null
+++ b/frontend/packages/policy-editor/src/contexts/PolicyEditorContext/PolicyEditorContext.tsx
@@ -0,0 +1,43 @@
+import React, { createContext, useContext } from 'react';
+import type { PolicyAction, PolicyEditorUsage, PolicyRuleCard, PolicySubject } from '../../types';
+
+export type PolicyEditorContextProps = {
+ policyRules: PolicyRuleCard[];
+ setPolicyRules: React.Dispatch
>;
+ actions: PolicyAction[];
+ subjects: PolicySubject[];
+ usageType: PolicyEditorUsage;
+ resourceType: string;
+ resourceId: string;
+ showAllErrors: boolean;
+ savePolicy: (rules: PolicyRuleCard[]) => void;
+};
+
+export const PolicyEditorContext = createContext>(undefined);
+
+export type PolicyEditorContextProviderProps = {
+ children: React.ReactNode;
+} & PolicyEditorContextProps;
+
+export const PolicyEditorContextProvider = ({
+ children,
+ ...rest
+}: PolicyEditorContextProviderProps) => {
+ return (
+
+ {children}
+
+ );
+};
+
+export const usePolicyEditorContext = (): Partial => {
+ const context = useContext(PolicyEditorContext);
+ if (context === undefined) {
+ throw new Error('usePolicyEditorContext must be used within a PolicyEditorContextProvider');
+ }
+ return context;
+};
diff --git a/frontend/packages/policy-editor/src/contexts/PolicyEditorContext/index.ts b/frontend/packages/policy-editor/src/contexts/PolicyEditorContext/index.ts
new file mode 100644
index 00000000000..489975ccd74
--- /dev/null
+++ b/frontend/packages/policy-editor/src/contexts/PolicyEditorContext/index.ts
@@ -0,0 +1,6 @@
+export {
+ usePolicyEditorContext,
+ PolicyEditorContextProvider,
+ PolicyEditorContext,
+ type PolicyEditorContextProps,
+} from './PolicyEditorContext';
diff --git a/frontend/packages/policy-editor/src/contexts/PolicyRuleContext/PolicyRuleContext.test.tsx b/frontend/packages/policy-editor/src/contexts/PolicyRuleContext/PolicyRuleContext.test.tsx
new file mode 100644
index 00000000000..11858bedf4a
--- /dev/null
+++ b/frontend/packages/policy-editor/src/contexts/PolicyRuleContext/PolicyRuleContext.test.tsx
@@ -0,0 +1,45 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import { PolicyRuleContext, usePolicyRuleContext } from './PolicyRuleContext';
+import { mockPolicyRuleContextValue } from '../../../test/mocks/policyRuleContextMock';
+
+describe('PolicyRuleContext', () => {
+ it('should render children', () => {
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByRole('button', { name: 'My button' })).toBeInTheDocument();
+ });
+
+ it('should provide a usePolicyRuleContext hook', () => {
+ const TestComponent = () => {
+ const {} = usePolicyRuleContext();
+ return ;
+ };
+
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByTestId('context')).toHaveTextContent('');
+ });
+
+ it('should throw an error when usePolicyRuleContext is used outside of a PolicyRuleContextProvider', () => {
+ // Mock console error to check if it has been called
+ const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
+ const TestComponent = () => {
+ usePolicyRuleContext();
+ return Test
;
+ };
+
+ expect(() => render()).toThrow(
+ 'usePolicyRuleContext must be used within a PolicyRuleContextProvider',
+ );
+ expect(consoleError).toHaveBeenCalled();
+ });
+});
diff --git a/frontend/packages/policy-editor/src/contexts/PolicyRuleContext/PolicyRuleContext.tsx b/frontend/packages/policy-editor/src/contexts/PolicyRuleContext/PolicyRuleContext.tsx
new file mode 100644
index 00000000000..a8c9d29fe8e
--- /dev/null
+++ b/frontend/packages/policy-editor/src/contexts/PolicyRuleContext/PolicyRuleContext.tsx
@@ -0,0 +1,39 @@
+import React, { createContext, useContext } from 'react';
+import type { PolicyRuleCard, PolicyError } from '../../types';
+
+export type PolicyRuleContextProps = {
+ policyRule: PolicyRuleCard;
+ showAllErrors: boolean;
+ uniqueId: string;
+ policyError: PolicyError;
+ setPolicyError: React.Dispatch>;
+};
+
+export const PolicyRuleContext = createContext>(undefined);
+
+export type PolicyRuleContextProviderProps = {
+ children: React.ReactNode;
+} & PolicyRuleContextProps;
+
+export const PolicyRuleContextProvider = ({
+ children,
+ ...rest
+}: PolicyRuleContextProviderProps) => {
+ return (
+
+ {children}
+
+ );
+};
+
+export const usePolicyRuleContext = (): Partial => {
+ const context = useContext(PolicyRuleContext);
+ if (context === undefined) {
+ throw new Error('usePolicyRuleContext must be used within a PolicyRuleContextProvider');
+ }
+ return context;
+};
diff --git a/frontend/packages/policy-editor/src/contexts/PolicyRuleContext/index.ts b/frontend/packages/policy-editor/src/contexts/PolicyRuleContext/index.ts
new file mode 100644
index 00000000000..5c98dcd4bb6
--- /dev/null
+++ b/frontend/packages/policy-editor/src/contexts/PolicyRuleContext/index.ts
@@ -0,0 +1,6 @@
+export {
+ usePolicyRuleContext,
+ PolicyRuleContextProvider,
+ PolicyRuleContext,
+ type PolicyRuleContextProps,
+} from './PolicyRuleContext';
diff --git a/frontend/packages/policy-editor/src/data-mocks/index.ts b/frontend/packages/policy-editor/src/data-mocks/index.ts
deleted file mode 100644
index 073b4eac03e..00000000000
--- a/frontend/packages/policy-editor/src/data-mocks/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export * from './policyRuleMocks';
-export * from './policyActionMocks';
-export * from './policySubjectMocks';
-export * from './policySubResourceMocks';
diff --git a/frontend/packages/policy-editor/src/types/index.ts b/frontend/packages/policy-editor/src/types/index.ts
index 2bd660ec277..aaede8e41ac 100644
--- a/frontend/packages/policy-editor/src/types/index.ts
+++ b/frontend/packages/policy-editor/src/types/index.ts
@@ -41,3 +41,9 @@ export interface Policy {
}
export type PolicyEditorUsage = 'app' | 'resource';
+
+export type PolicyError = {
+ resourceError: boolean;
+ actionsError: boolean;
+ subjectsError: boolean;
+};
diff --git a/frontend/packages/policy-editor/src/utils/PolicyEditorUtils.test.ts b/frontend/packages/policy-editor/src/utils/PolicyEditorUtils.test.ts
index 42f6c4f0172..c8c4e1ff6b9 100644
--- a/frontend/packages/policy-editor/src/utils/PolicyEditorUtils.test.ts
+++ b/frontend/packages/policy-editor/src/utils/PolicyEditorUtils.test.ts
@@ -16,16 +16,15 @@ import {
mockAction2,
mockAction3,
mockAction4,
- mockPolicyResourceBackendString1,
+} from '../../test/mocks/policyActionMocks';
+import {
mockPolicyRule1,
mockPolicyRuleCard1,
mockPolicyRuleCards,
mockPolicyRules,
- mockResource11,
- mockResource3,
- mockResourceType1,
- mockResourecId1,
mockRuleId1,
+} from '../../test/mocks/policyRuleMocks';
+import {
mockSubject1,
mockSubject2,
mockSubject3,
@@ -34,7 +33,14 @@ import {
mockSubjectTitle1,
mockSubjects,
mockSubjectId3,
-} from '../data-mocks';
+} from '../../test/mocks/policySubjectMocks';
+import {
+ mockResource11,
+ mockResource3,
+ mockResourceType1,
+ mockResourecId1,
+ mockPolicyResourceBackendString1,
+} from '../../test/mocks/policySubResourceMocks';
import type { PolicySubject } from '../types';
describe('PolicyEditorUtils', () => {
diff --git a/frontend/packages/policy-editor/src/utils/ExpandablePolicyCardUtils/ExpandablePolicyCardUtils.test.ts b/frontend/packages/policy-editor/src/utils/PolicyRuleUtils/PolicyRuleUtils.test.ts
similarity index 94%
rename from frontend/packages/policy-editor/src/utils/ExpandablePolicyCardUtils/ExpandablePolicyCardUtils.test.ts
rename to frontend/packages/policy-editor/src/utils/PolicyRuleUtils/PolicyRuleUtils.test.ts
index aedc9bd81fe..fb1c0429f3a 100644
--- a/frontend/packages/policy-editor/src/utils/ExpandablePolicyCardUtils/ExpandablePolicyCardUtils.test.ts
+++ b/frontend/packages/policy-editor/src/utils/PolicyRuleUtils/PolicyRuleUtils.test.ts
@@ -10,17 +10,21 @@ import {
mockActionId3,
mockActionId4,
mockActions,
+} from '../../../test/mocks/policyActionMocks';
+import {
mockPolicyRuleCard1,
mockPolicyRuleCard2,
mockPolicyRuleCards,
mockRuleId1,
+} from '../../../test/mocks/policyRuleMocks';
+import {
mockSubjects,
mockSubjectId1,
mockSubjectId2,
mockSubjectId3,
-} from '../../data-mocks';
+} from '../../../test/mocks/policySubjectMocks';
-describe('ExpandablePolicyCardUtils', () => {
+describe('PolicyRuleUtils', () => {
describe('getUpdatedRules', () => {
it('should update a rule in the list', () => {
const updatedRule = { ...mockPolicyRuleCard1, description: 'Updated Rule' };
diff --git a/frontend/packages/policy-editor/src/utils/ExpandablePolicyCardUtils/index.ts b/frontend/packages/policy-editor/src/utils/PolicyRuleUtils/index.ts
similarity index 100%
rename from frontend/packages/policy-editor/src/utils/ExpandablePolicyCardUtils/index.ts
rename to frontend/packages/policy-editor/src/utils/PolicyRuleUtils/index.ts
diff --git a/frontend/packages/policy-editor/src/utils/index.ts b/frontend/packages/policy-editor/src/utils/index.ts
index 509f1c055c3..2b6be6cf9bb 100644
--- a/frontend/packages/policy-editor/src/utils/index.ts
+++ b/frontend/packages/policy-editor/src/utils/index.ts
@@ -237,3 +237,8 @@ export const findSubjectByPolicyRuleSubject = (
(subject) => subject.subjectId.toLowerCase() === policyRuleSubject.toLowerCase(),
);
};
+
+export const getNewRuleId = (rules: PolicyRuleCard[]): string => {
+ const lastId: number = Number(rules[rules.length - 1]?.ruleId ?? 0) + 1;
+ return String(lastId);
+};
diff --git a/frontend/packages/policy-editor/src/data-mocks/policyActionMocks.ts b/frontend/packages/policy-editor/test/mocks/policyActionMocks.ts
similarity index 94%
rename from frontend/packages/policy-editor/src/data-mocks/policyActionMocks.ts
rename to frontend/packages/policy-editor/test/mocks/policyActionMocks.ts
index 828699d6dba..fe781f971e5 100644
--- a/frontend/packages/policy-editor/src/data-mocks/policyActionMocks.ts
+++ b/frontend/packages/policy-editor/test/mocks/policyActionMocks.ts
@@ -1,4 +1,4 @@
-import type { PolicyAction } from '../types';
+import type { PolicyAction } from '../../src/types';
export const mockActionId1: string = 'read';
export const mockActionId2: string = 'write';
diff --git a/frontend/packages/policy-editor/test/mocks/policyEditorContextMock.ts b/frontend/packages/policy-editor/test/mocks/policyEditorContextMock.ts
new file mode 100644
index 00000000000..75c394dc674
--- /dev/null
+++ b/frontend/packages/policy-editor/test/mocks/policyEditorContextMock.ts
@@ -0,0 +1,21 @@
+import { type PolicyEditorContextProps } from '../../src/contexts/PolicyEditorContext';
+import { mockActions } from './policyActionMocks';
+import { mockPolicyRuleCards } from './policyRuleMocks';
+import { mockSubjects } from './policySubjectMocks';
+import { type PolicyEditorUsage } from '../../src/types';
+import { mockResourecId1 } from './policySubResourceMocks';
+
+const mockUsageType: PolicyEditorUsage = 'app';
+const mockResourceType: string = 'urn:altinn';
+
+export const mockPolicyEditorContextValue: PolicyEditorContextProps = {
+ policyRules: mockPolicyRuleCards,
+ setPolicyRules: jest.fn(),
+ actions: mockActions,
+ subjects: mockSubjects,
+ usageType: mockUsageType,
+ resourceType: mockResourceType,
+ showAllErrors: false,
+ resourceId: mockResourecId1,
+ savePolicy: jest.fn(),
+};
diff --git a/frontend/packages/policy-editor/test/mocks/policyRuleContextMock.ts b/frontend/packages/policy-editor/test/mocks/policyRuleContextMock.ts
new file mode 100644
index 00000000000..e57401be3b0
--- /dev/null
+++ b/frontend/packages/policy-editor/test/mocks/policyRuleContextMock.ts
@@ -0,0 +1,17 @@
+import { type PolicyRuleContextProps } from '../../src/contexts/PolicyRuleContext';
+import { mockPolicyRuleCard1 } from './policyRuleMocks';
+import { type PolicyError } from '../../src/types';
+
+const policyError: PolicyError = {
+ resourceError: false,
+ subjectsError: false,
+ actionsError: false,
+};
+
+export const mockPolicyRuleContextValue: PolicyRuleContextProps = {
+ policyRule: mockPolicyRuleCard1,
+ showAllErrors: false,
+ uniqueId: 'id',
+ policyError,
+ setPolicyError: jest.fn(),
+};
diff --git a/frontend/packages/policy-editor/src/data-mocks/policyRuleMocks.ts b/frontend/packages/policy-editor/test/mocks/policyRuleMocks.ts
similarity index 95%
rename from frontend/packages/policy-editor/src/data-mocks/policyRuleMocks.ts
rename to frontend/packages/policy-editor/test/mocks/policyRuleMocks.ts
index 86b0f8b26c5..a0578609edf 100644
--- a/frontend/packages/policy-editor/src/data-mocks/policyRuleMocks.ts
+++ b/frontend/packages/policy-editor/test/mocks/policyRuleMocks.ts
@@ -1,4 +1,4 @@
-import type { PolicyRule, PolicyRuleCard } from '../types';
+import type { PolicyRule, PolicyRuleCard } from '../../src/types';
import { mockAction1, mockAction2, mockAction4 } from './policyActionMocks';
import { mockPolicyResources, mockPolicyRuleResources } from './policySubResourceMocks';
import {
diff --git a/frontend/packages/policy-editor/src/data-mocks/policySubResourceMocks.ts b/frontend/packages/policy-editor/test/mocks/policySubResourceMocks.ts
similarity index 97%
rename from frontend/packages/policy-editor/src/data-mocks/policySubResourceMocks.ts
rename to frontend/packages/policy-editor/test/mocks/policySubResourceMocks.ts
index f174267e993..381a32aa6e6 100644
--- a/frontend/packages/policy-editor/src/data-mocks/policySubResourceMocks.ts
+++ b/frontend/packages/policy-editor/test/mocks/policySubResourceMocks.ts
@@ -1,4 +1,4 @@
-import type { PolicyRuleResource } from '../types';
+import type { PolicyRuleResource } from '../../src/types';
export const mockResourecId1: string = 'resource-1';
export const mockResourecId2: string = '1.2';
diff --git a/frontend/packages/policy-editor/src/data-mocks/policySubjectMocks.ts b/frontend/packages/policy-editor/test/mocks/policySubjectMocks.ts
similarity index 95%
rename from frontend/packages/policy-editor/src/data-mocks/policySubjectMocks.ts
rename to frontend/packages/policy-editor/test/mocks/policySubjectMocks.ts
index 7b95416b5fc..9ab1423f323 100644
--- a/frontend/packages/policy-editor/src/data-mocks/policySubjectMocks.ts
+++ b/frontend/packages/policy-editor/test/mocks/policySubjectMocks.ts
@@ -1,4 +1,4 @@
-import type { PolicySubject } from '../types';
+import type { PolicySubject } from '../../src/types';
export const mockSubjectId1: string = 's1';
export const mockSubjectId2: string = 's2';
From 4454eff7a7dbb3518c2e50d8382c0755de046522 Mon Sep 17 00:00:00 2001
From: Martin Gunnerud
Date: Thu, 30 May 2024 11:51:31 +0200
Subject: [PATCH 06/27] Resourceadm: change add and remove access list members
endpoints (#12846)
* send lists of orgnr when adding or removing access list members (in case we need to support batch updates later)
* allow adding Tenor test organizations if they exists in registry + hide radio label
* nextPage in resource admin should be a string + add paging to access list members
* show error message if list member cannot be added to list
* add missing translation
* remove button spinner
* update invalid orgnr error code from resource registry
* show 10 results from brreg for each page + fix jumping paging buttons
* PR changes
* ResourceError type extends Error type
---
frontend/language/src/nb.json | 6 +-
frontend/packages/shared/src/api/mutations.ts | 6 +-
frontend/packages/shared/src/api/paths.js | 6 +-
frontend/packages/shared/src/api/queries.ts | 8 +-
.../packages/shared/src/mocks/queriesMock.ts | 1 +
.../packages/shared/src/types/QueryKey.ts | 1 +
.../packages/shared/src/types/ResourceAdm.ts | 27 ++++-
.../AccessListDetail.test.tsx | 43 ++++++-
.../AccessListDetails/AccessListDetail.tsx | 30 ++++-
.../AccessListErrorMessage.tsx | 4 +-
.../AccessListMembers.module.css | 16 ++-
.../AccessListMembers.test.tsx | 113 +++++++++++++-----
.../AccessListMembers/AccessListMembers.tsx | 91 +++++++++-----
.../AccessListMembersTable.tsx | 19 ++-
.../ImportResourceModal.tsx | 7 +-
.../ResourceAccessLists.test.tsx | 20 +++-
.../ResourceAccessLists.tsx | 9 +-
.../ResourceDeployEnvCard.tsx | 4 +-
.../useAddAccessListMemberMutation.ts | 7 +-
.../mutations/useCreateAccessListMutation.ts | 1 +
.../mutations/useDeleteAccessListMutation.ts | 1 +
.../useRemoveAccessListMemberMutation.ts | 7 +-
.../queries/useGetAccessListMembersQuery.ts | 33 +++++
.../hooks/queries/useGetAccessListsQuery.ts | 4 +-
.../queries/useGetAltinn2LinkServicesQuery.ts | 6 +-
.../queries/useGetResourceAccessListsQuery.ts | 4 +-
.../hooks/queries/usePartiesRegistryQuery.ts | 12 +-
.../useResourcePolicyPublishStatusQuery.ts | 7 +-
.../queries/useSubPartiesRegistryQuery.ts | 12 +-
.../hooks/queries/useValidatePolicyQuery.ts | 7 +-
.../hooks/queries/useValidateResourceQuery.ts | 7 +-
.../ListAdminPage/ListAdminPage.test.tsx | 20 +++-
.../pages/ListAdminPage/ListAdminPage.tsx | 8 +-
.../resourceadm/utils/stringUtils/index.ts | 8 +-
.../utils/stringUtils/stringUtils.ts | 4 +
.../resourceadm/utils/urlUtils/urlUtils.ts | 5 +-
36 files changed, 425 insertions(+), 139 deletions(-)
create mode 100644 frontend/resourceadm/hooks/queries/useGetAccessListMembersQuery.ts
diff --git a/frontend/language/src/nb.json b/frontend/language/src/nb.json
index 6787c81a4a7..c70d3d267ac 100644
--- a/frontend/language/src/nb.json
+++ b/frontend/language/src/nb.json
@@ -1076,6 +1076,7 @@
"resourceadm.listadmin_empty_name": "",
"resourceadm.listadmin_header": "Administrer tilgangslister",
"resourceadm.listadmin_identifier_conflict": "En tilgangsliste med denne id-en finnes fra før",
+ "resourceadm.listadmin_invalid_org": "Denne enheten kan ikke legges til i listen",
"resourceadm.listadmin_list_description": "Beskrivelse",
"resourceadm.listadmin_list_description_description": "Her kan du beskrive listen",
"resourceadm.listadmin_list_detail_header": "Administrer tilgangsliste",
@@ -1086,9 +1087,12 @@
"resourceadm.listadmin_list_name_description": "Gi listen et beskrivende navn, f.eks \"Godkjente banker\"",
"resourceadm.listadmin_list_organizations": "Registrerte enheter",
"resourceadm.listadmin_list_organizations_description": "Enheter i denne listen vil ha tilgang til ressursen",
+ "resourceadm.listadmin_list_tenor_org": "Tenor testorganisasjon",
+ "resourceadm.listadmin_list_unit": "tilgangslister",
"resourceadm.listadmin_lists_in": "Lister i {{environment}}",
"resourceadm.listadmin_load_list_error": "Kunne ikke laste tilgangslister",
- "resourceadm.listadmin_load_more": "Last flere",
+ "resourceadm.listadmin_load_more": "Vis flere {{unit}}",
+ "resourceadm.listadmin_member_unit": "registrerte enheter",
"resourceadm.listadmin_navn": "Navn",
"resourceadm.listadmin_orgnr": "Orgnr",
"resourceadm.listadmin_parties": "Enheter",
diff --git a/frontend/packages/shared/src/api/mutations.ts b/frontend/packages/shared/src/api/mutations.ts
index b37be560954..8bdbe0f8900 100644
--- a/frontend/packages/shared/src/api/mutations.ts
+++ b/frontend/packages/shared/src/api/mutations.ts
@@ -55,7 +55,7 @@ import { buildQueryParams } from 'app-shared/utils/urlUtils';
import type { JsonSchema } from 'app-shared/types/JsonSchema';
import type { CreateDataModelPayload } from 'app-shared/types/api/CreateDataModelPayload';
import type { Policy } from '@altinn/policy-editor';
-import type { NewResource, AccessList, Resource } from 'app-shared/types/ResourceAdm';
+import type { NewResource, AccessList, Resource, AccessListOrganizationNumbers } from 'app-shared/types/ResourceAdm';
import type { ApplicationMetadata } from 'app-shared/types/ApplicationMetadata';
import type { AppConfig } from 'app-shared/types/AppConfig';
import type { Repository } from 'app-shared/types/Repository';
@@ -117,8 +117,8 @@ export const importResourceFromAltinn3 = (org: string, resourceId: string, envir
export const createAccessList = (org: string, environment: string, payload: Partial) => post(createAccessListsPath(org, environment), payload);
export const updateAccessList = (org: string, listId: string, environment: string, payload: AccessList) => put(accessListPath(org, listId, environment), payload);
export const deleteAccessList = (org: string, listId: string, environment: string) => del(accessListPath(org, listId, environment));
-export const addAccessListMember = (org: string, listId: string, orgnr: string, environment: string) => post(accessListMemberPath(org, listId, orgnr, environment));
-export const removeAccessListMember = (org: string, listId: string, orgnr: string, environment: string) => del(accessListMemberPath(org, listId, orgnr, environment));
+export const addAccessListMember = (org: string, listId: string, environment: string, payload: AccessListOrganizationNumbers) => post(accessListMemberPath(org, listId, environment), payload);
+export const removeAccessListMember = (org: string, listId: string, environment: string, payload: AccessListOrganizationNumbers) => del(accessListMemberPath(org, listId, environment), { data: payload });
export const addResourceAccessList = (org: string, resourceId: string, listId: string, environment: string) => post(resourceAccessListPath(org, resourceId, listId, environment));
export const removeResourceAccessList = (org: string, resourceId: string, listId: string, environment: string) => del(resourceAccessListPath(org, resourceId, listId, environment));
export const publishResource = (org: string, repo: string, id: string, env: string) => post(publishResourcePath(org, repo, id, env), { headers: { 'Content-Type': 'application/json' } });
diff --git a/frontend/packages/shared/src/api/paths.js b/frontend/packages/shared/src/api/paths.js
index 9403804087d..9d0744538d8 100644
--- a/frontend/packages/shared/src/api/paths.js
+++ b/frontend/packages/shared/src/api/paths.js
@@ -138,12 +138,12 @@ export const resourceValidateResourcePath = (org, repo, id) => `${basePath}/${or
export const publishResourcePath = (org, repo, id, env) => `${basePath}/${org}/resources/publish/${repo}/${id}?env=${env}`; // Get
export const altinn2LinkServicesPath = (org, env) => `${basePath}/${org}/resources/altinn2linkservices/${env}`; // Get
export const importResourceFromAltinn2Path = (org, env, serviceCode, serviceEdition) => `${basePath}/${org}/resources/importresource/${serviceCode}/${serviceEdition}/${env}`; // Post
+export const accessListsPath = (org, env, page) => `${basePath}/${org}/resources/accesslist/?env=${env}${page ? `&page=${page}` : ''}`; // Get
export const importResourceFromAltinn3Path = (org, resourceId, env) => `${basePath}/${org}/resources/addexistingresource/${resourceId}/${env}`; // Post
-export const accessListsPath = (org, env, page) => `${basePath}/${org}/resources/accesslist/?env=${env}&page=${page}`; // Get
export const createAccessListsPath = (org, env) => `${basePath}/${org}/resources/accesslist/?env=${env}`; // Post
export const accessListPath = (org, listId, env) => `${basePath}/${org}/resources/accesslist/${listId}?env=${env}`; // Get, Patch, Delete
-export const accessListMemberPath = (org, listId, orgnr, env) => `${basePath}/${org}/resources/accesslist/${listId}/members/${orgnr}?env=${env}`; // Post, Delete
-export const resourceAccessListsPath = (org, resourceId, env, page) => `${basePath}/${org}/resources/${resourceId}/accesslists/?env=${env}&page=${page}`; // Get
+export const accessListMemberPath = (org, listId, env, page) => `${basePath}/${org}/resources/accesslist/${listId}/members/?env=${env}${page ? `&page=${page}` : ''}`; // Get, Post, Delete
+export const resourceAccessListsPath = (org, resourceId, env, page) => `${basePath}/${org}/resources/${resourceId}/accesslists/?env=${env}${page ? `&page=${page}` : ''}`; // Get
export const resourceAccessListPath = (org, resourceId, listId, env) => `${basePath}/${org}/resources/${resourceId}/accesslists/${listId}?env=${env}`; // Post, Delete, Patch
// Process Editor
diff --git a/frontend/packages/shared/src/api/queries.ts b/frontend/packages/shared/src/api/queries.ts
index 5114b0c4550..e738667c1e5 100644
--- a/frontend/packages/shared/src/api/queries.ts
+++ b/frontend/packages/shared/src/api/queries.ts
@@ -21,6 +21,7 @@ import {
orgsListPath,
accessListsPath,
accessListPath,
+ accessListMemberPath,
processEditorPath,
releasesPath,
repoMetaPath,
@@ -66,7 +67,7 @@ import { buildQueryParams } from 'app-shared/utils/urlUtils';
import { newsListUrl, orgListUrl } from '../cdn-paths';
import type { JsonSchema } from 'app-shared/types/JsonSchema';
import type { PolicyAction, PolicySubject } from '@altinn/policy-editor';
-import type { BrregPartySearchResult, BrregSubPartySearchResult, AccessList, Resource, ResourceListItem, ResourceVersionStatus, Validation, AccessListsResponse } from 'app-shared/types/ResourceAdm';
+import type { BrregPartySearchResult, BrregSubPartySearchResult, AccessList, Resource, ResourceListItem, ResourceVersionStatus, Validation, AccessListsResponse, AccessListMembersResponse } from 'app-shared/types/ResourceAdm';
import type { AppConfig } from 'app-shared/types/AppConfig';
import type { ApplicationMetadata } from 'app-shared/types/ApplicationMetadata';
import type { Altinn2LinkService } from 'app-shared/types/Altinn2LinkService';
@@ -124,9 +125,10 @@ export const getResourceList = (org: string) => get(resource
export const getResourcePublishStatus = (org: string, repo: string, id: string) => get(resourcePublishStatusPath(org, repo, id));
export const getValidatePolicy = (org: string, repo: string, id: string) => get(resourceValidatePolicyPath(org, repo, id));
export const getValidateResource = (org: string, repo: string, id: string) => get(resourceValidateResourcePath(org, repo, id));
-export const getAccessLists = (org: string, environment: string, page?: number) => get(accessListsPath(org, environment, page));
+export const getAccessLists = (org: string, environment: string, page?: string) => get(accessListsPath(org, environment, page));
export const getAccessList = (org: string, listId: string, environment: string) => get(accessListPath(org, listId, environment));
-export const getResourceAccessLists = (org: string, resourceId: string, environment: string, page?: number) => get(resourceAccessListsPath(org, resourceId, environment, page));
+export const getAccessListMembers = (org: string, listId: string, environment: string, page?: string) => get(accessListMemberPath(org, listId, environment, page));
+export const getResourceAccessLists = (org: string, resourceId: string, environment: string, page?: string) => get(resourceAccessListsPath(org, resourceId, environment, page));
export const getParties = (url: string) => get(url);
export const getSubParties = (url: string) => get(url);
diff --git a/frontend/packages/shared/src/mocks/queriesMock.ts b/frontend/packages/shared/src/mocks/queriesMock.ts
index 06cf5a74a0c..307878af284 100644
--- a/frontend/packages/shared/src/mocks/queriesMock.ts
+++ b/frontend/packages/shared/src/mocks/queriesMock.ts
@@ -155,6 +155,7 @@ export const queriesMock: ServicesContextProps = {
getSubParties: jest
.fn()
.mockImplementation(() => Promise.resolve(null)),
+ getAccessListMembers: jest.fn().mockImplementation(() => Promise.resolve({ data: [] })),
// Queries - PrgetBpmnFile
getBpmnFile: jest.fn().mockImplementation(() => Promise.resolve('')),
diff --git a/frontend/packages/shared/src/types/QueryKey.ts b/frontend/packages/shared/src/types/QueryKey.ts
index b031f4375c0..b451687a25a 100644
--- a/frontend/packages/shared/src/types/QueryKey.ts
+++ b/frontend/packages/shared/src/types/QueryKey.ts
@@ -57,6 +57,7 @@ export enum QueryKey {
ImportAltinn2Resource = 'ImportAltinn2Resource',
AccessLists = 'AccessLists',
AccessList = 'AccessList',
+ AccessListMembers = 'AccessListMembers',
ResourceAccessLists = 'ResourceAccessLists',
PartiesRegistrySearch = 'PartiesRegistrySearch',
SubPartiesRegistrySearch = 'SubPartiesRegistrySearch',
diff --git a/frontend/packages/shared/src/types/ResourceAdm.ts b/frontend/packages/shared/src/types/ResourceAdm.ts
index ba8b38c34ef..76a969fffa8 100644
--- a/frontend/packages/shared/src/types/ResourceAdm.ts
+++ b/frontend/packages/shared/src/types/ResourceAdm.ts
@@ -124,8 +124,8 @@ export interface BrregSubPartySearchResult {
export interface BrregSearchResult {
parties: AccessListMember[];
- links: BrregPagination;
- page: BrregPageInfo;
+ links?: BrregPagination;
+ page?: BrregPageInfo;
}
export interface BrregParty {
@@ -144,7 +144,6 @@ export interface AccessList {
identifier: string;
name: string;
description?: string;
- members?: AccessListMember[];
resourceConnections?: {
resourceIdentifier: string;
}[];
@@ -152,7 +151,20 @@ export interface AccessList {
export interface AccessListsResponse {
data: AccessList[];
- nextPage?: number;
+ nextPage?: string;
+}
+
+export interface AccessListMembersResponse {
+ data: AccessListMember[];
+ nextPage?: string;
+}
+
+export interface AccessListOrganizationNumbers {
+ data: string[];
+}
+
+export interface AccessListOrganizationNumbers {
+ data: string[];
}
export interface JsonPatch {
@@ -160,3 +172,10 @@ export interface JsonPatch {
path: string;
value?: string | number;
}
+
+export interface ResourceError extends Error {
+ response?: {
+ status: number;
+ data?: any;
+ };
+}
diff --git a/frontend/resourceadm/components/AccessListDetails/AccessListDetail.test.tsx b/frontend/resourceadm/components/AccessListDetails/AccessListDetail.test.tsx
index a6a3f0aea28..9b938f9abe4 100644
--- a/frontend/resourceadm/components/AccessListDetails/AccessListDetail.test.tsx
+++ b/frontend/resourceadm/components/AccessListDetails/AccessListDetail.test.tsx
@@ -28,13 +28,22 @@ const defaultProps: AccessListDetailProps = {
identifier: testListIdentifier,
name: 'Test-list',
description: 'This is a description',
- members: [],
},
backUrl: '/listadmin',
};
+const membersPage2OrgNr = '987654321';
+const membersResults = {
+ data: [{ orgNr: '123456789', orgName: 'Skatteetaten', isSubParty: false }],
+ nextPage: 'http://at22-next-page',
+};
+
+const membersResultsPage2 = {
+ data: [{ orgNr: membersPage2OrgNr, orgName: 'Digitaliseringsdirektoratet', isSubParty: false }],
+ nextPage: '',
+};
+
const updateAccessListMock = jest.fn();
-const addAccessListMemberMock = jest.fn();
describe('AccessListDetail', () => {
afterEach(jest.clearAllMocks);
@@ -87,7 +96,7 @@ describe('AccessListDetail', () => {
it('should navigate back after list is deleted', async () => {
const user = userEvent.setup();
- renderAccessListDetail({}, { addAccessListMember: addAccessListMemberMock });
+ renderAccessListDetail();
const deleteListButton = screen.getByText(textMock('resourceadm.listadmin_delete_list'));
await user.click(deleteListButton);
@@ -100,7 +109,7 @@ describe('AccessListDetail', () => {
it('should close modal on cancel delete', async () => {
const user = userEvent.setup();
- renderAccessListDetail({}, { addAccessListMember: addAccessListMemberMock });
+ renderAccessListDetail();
const deleteListButton = screen.getByText(textMock('resourceadm.listadmin_delete_list'));
await user.click(deleteListButton);
@@ -112,6 +121,32 @@ describe('AccessListDetail', () => {
screen.queryByText(textMock('resourceadm.listadmin_delete_list_header')),
).not.toBeInTheDocument();
});
+
+ it('should show more members when load more button is clicked', async () => {
+ const user = userEvent.setup();
+ const getAccessListMembersMock = jest
+ .fn()
+ .mockImplementationOnce(() => Promise.resolve(membersResults))
+ .mockImplementationOnce(() => Promise.resolve(membersResultsPage2));
+ renderAccessListDetail({}, { getAccessListMembers: getAccessListMembersMock });
+
+ await waitFor(() =>
+ screen.findByText(
+ textMock('resourceadm.listadmin_load_more', {
+ unit: textMock('resourceadm.listadmin_member_unit'),
+ }),
+ ),
+ );
+
+ const loadMoreButton = screen.getByText(
+ textMock('resourceadm.listadmin_load_more', {
+ unit: textMock('resourceadm.listadmin_member_unit'),
+ }),
+ );
+ await user.click(loadMoreButton);
+
+ expect(await screen.findByText(membersPage2OrgNr)).toBeInTheDocument();
+ });
});
const renderAccessListDetail = (
diff --git a/frontend/resourceadm/components/AccessListDetails/AccessListDetail.tsx b/frontend/resourceadm/components/AccessListDetails/AccessListDetail.tsx
index c4a11c5e7f5..8a5ed870e5c 100644
--- a/frontend/resourceadm/components/AccessListDetails/AccessListDetail.tsx
+++ b/frontend/resourceadm/components/AccessListDetails/AccessListDetail.tsx
@@ -8,6 +8,7 @@ import type { AccessList } from 'app-shared/types/ResourceAdm';
import { FieldWrapper } from '../FieldWrapper';
import { useEditAccessListMutation } from '../../hooks/mutations/useEditAccessListMutation';
import { useDeleteAccessListMutation } from '../../hooks/mutations/useDeleteAccessListMutation';
+import { useGetAccessListMembersQuery } from '../../hooks/queries/useGetAccessListMembersQuery';
import { AccessListMembers } from '../AccessListMembers';
import { TrashIcon } from '@studio/icons';
import { StudioButton } from '@studio/components';
@@ -33,6 +34,12 @@ export const AccessListDetail = ({
const [listName, setListName] = useState(list.name || '');
const [listDescription, setListDescription] = useState(list.description || '');
+ const {
+ data: membersData,
+ hasNextPage,
+ isFetchingNextPage,
+ fetchNextPage,
+ } = useGetAccessListMembersQuery(org, list.identifier, env);
const { mutate: editAccessList } = useEditAccessListMutation(org, list.identifier, env);
const { mutate: deleteAccessList, isPending: isDeletingAccessList } = useDeleteAccessListMutation(
org,
@@ -114,7 +121,28 @@ export const AccessListDetail = ({
onBlur={(event) => handleSave({ ...list, description: event.target.value })}
/>
-
+ {membersData && (
+ fetchNextPage()}
+ >
+ {t('resourceadm.listadmin_load_more', {
+ unit: t('resourceadm.listadmin_member_unit'),
+ })}
+
+ )
+ }
+ />
+ )}
{
@@ -47,11 +48,11 @@ describe('AccessListMembers', () => {
});
it('should show message when list is empty', () => {
- renderAccessListMembers({ list: { ...defaultProps.list, members: undefined } });
+ renderAccessListMembers({ members: [] });
expect(screen.getByText(textMock('resourceadm.listadmin_empty_list'))).toBeInTheDocument();
});
- it('should call service to remove member', async () => {
+ it('should remove member from table when remove member button is clicked', async () => {
const user = userEvent.setup();
const removeAccessListMemberMock = jest.fn();
renderAccessListMembers({}, { removeAccessListMember: removeAccessListMemberMock });
@@ -59,15 +60,12 @@ describe('AccessListMembers', () => {
const removeButtons = screen.getAllByText(textMock('resourceadm.listadmin_remove_from_list'));
await user.click(removeButtons[0]);
- expect(removeAccessListMemberMock).toHaveBeenCalledWith(
- testOrg,
- testListIdentifier,
- testMemberPartyId,
- testEnv,
- );
+ expect(removeAccessListMemberMock).toHaveBeenCalledWith(testOrg, testListIdentifier, testEnv, {
+ data: [testMemberPartyId],
+ });
});
- it('should call service to add member', async () => {
+ it('should show new member in list after member is added', async () => {
const user = userEvent.setup();
const addAccessListMemberMock = jest.fn();
const searchResultText = 'Digdir';
@@ -95,15 +93,12 @@ describe('AccessListMembers', () => {
await waitFor(() => screen.findByText(searchResultText));
- const searchResultsButton = screen.getByText(textMock('resourceadm.listadmin_add_to_list'));
- await user.click(searchResultsButton);
+ const addMemberButton = screen.getByText(textMock('resourceadm.listadmin_add_to_list'));
+ await user.click(addMemberButton);
- expect(addAccessListMemberMock).toHaveBeenCalledWith(
- testOrg,
- testListIdentifier,
- searchResultOrgNr,
- testEnv,
- );
+ expect(addAccessListMemberMock).toHaveBeenCalledWith(testOrg, testListIdentifier, testEnv, {
+ data: [searchResultOrgNr],
+ });
});
it('should show message when no parties are found', async () => {
@@ -122,7 +117,7 @@ describe('AccessListMembers', () => {
await user.click(addMoreButton);
const textField = screen.getByLabelText(textMock('resourceadm.listadmin_search'));
- await user.type(textField, '123456789');
+ await user.type(textField, 'test');
await screen.findByText(textMock('resourceadm.listadmin_search_no_parties'));
});
@@ -153,6 +148,64 @@ describe('AccessListMembers', () => {
await screen.findByText(textMock('resourceadm.listadmin_search_no_sub_parties'));
});
+ it('should show special organization from tenor when search for orgnr is not found', async () => {
+ const user = userEvent.setup();
+
+ renderAccessListMembers(
+ {},
+ {
+ getParties: jest.fn().mockImplementation(() => Promise.resolve({})),
+ },
+ );
+
+ const addMoreButton = screen.getByRole('button', {
+ name: textMock('resourceadm.listadmin_search_add_more'),
+ });
+ await user.click(addMoreButton);
+
+ const textField = screen.getByLabelText(textMock('resourceadm.listadmin_search'));
+ await user.type(textField, '123456789');
+
+ await screen.findByText(textMock('resourceadm.listadmin_list_tenor_org'));
+ });
+
+ it('should show error message if organization cannot be added to list', async () => {
+ const user = userEvent.setup();
+ const searchResultText = 'Digdir';
+ const searchResultOrgNr = '987654321';
+
+ renderAccessListMembers(
+ {},
+ {
+ addAccessListMember: jest
+ .fn()
+ .mockImplementation(() => Promise.reject({ response: { data: { code: 'RR-00001' } } })),
+ getParties: jest.fn().mockImplementation(() =>
+ Promise.resolve({
+ _embedded: {
+ enheter: [{ organisasjonsnummer: searchResultOrgNr, navn: searchResultText }],
+ },
+ }),
+ ),
+ },
+ );
+
+ const addMoreButton = screen.getByRole('button', {
+ name: textMock('resourceadm.listadmin_search_add_more'),
+ });
+ await user.click(addMoreButton);
+
+ const textField = screen.getByLabelText(textMock('resourceadm.listadmin_search'));
+ await user.type(textField, searchResultOrgNr);
+
+ await waitFor(() => screen.findByText(searchResultText));
+
+ const addMemberButton = screen.getByText(textMock('resourceadm.listadmin_add_to_list'));
+ await user.click(addMemberButton);
+
+ expect(screen.getByText(textMock('resourceadm.listadmin_invalid_org'))).toBeInTheDocument();
+ });
+
it('should go to next page when paging button is clicked', async () => {
const user = userEvent.setup();
const nextPageUrl = 'brreg/next';
diff --git a/frontend/resourceadm/components/AccessListMembers/AccessListMembers.tsx b/frontend/resourceadm/components/AccessListMembers/AccessListMembers.tsx
index 9e6e727c249..62701e4f1bb 100644
--- a/frontend/resourceadm/components/AccessListMembers/AccessListMembers.tsx
+++ b/frontend/resourceadm/components/AccessListMembers/AccessListMembers.tsx
@@ -2,7 +2,7 @@ import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Alert, Textfield, Radio } from '@digdir/design-system-react';
import classes from './AccessListMembers.module.css';
-import type { AccessList, AccessListMember } from 'app-shared/types/ResourceAdm';
+import type { AccessList, AccessListMember, ResourceError } from 'app-shared/types/ResourceAdm';
import { FieldWrapper } from '../FieldWrapper';
import { useRemoveAccessListMemberMutation } from '../../hooks/mutations/useRemoveAccessListMemberMutation';
import { useAddAccessListMemberMutation } from '../../hooks/mutations/useAddAccessListMemberMutation';
@@ -10,29 +10,35 @@ import { useDebounce } from 'react-use';
import { usePartiesRegistryQuery } from '../../hooks/queries/usePartiesRegistryQuery';
import { useSubPartiesRegistryQuery } from '../../hooks/queries/useSubPartiesRegistryQuery';
import { getPartiesQueryUrl } from '../../utils/urlUtils';
-import { StudioSpinner, StudioButton } from '@studio/components';
+import { StudioButton } from '@studio/components';
import { PlusIcon } from '@studio/icons';
import { AccessListMembersPaging } from './AccessListMembersPaging';
import { AccessListMembersTable } from './AccessListMembersTable';
+import { isOrgNrString } from '../../utils/stringUtils';
const PARTY_SEARCH_TYPE = 'PARTY';
const SUBPARTY_SEARCH_TYPE = 'SUBPARTY';
+const INVALID_ORG_ERROR_CODE = 'RR-00001';
export interface AccessListMembersProps {
org: string;
env: string;
list: AccessList;
+ members: AccessListMember[];
+ loadMoreButton: React.JSX.Element;
}
export const AccessListMembers = ({
org,
env,
list,
+ members,
+ loadMoreButton,
}: AccessListMembersProps): React.JSX.Element => {
const { t } = useTranslation();
- const [listItems, setListItems] = useState(list.members ?? []);
- const [isAddMode, setIsAddMode] = useState((list.members ?? []).length === 0);
+ const [invalidOrgnrs, setInvalidOrgnrs] = useState([]);
+ const [isAddMode, setIsAddMode] = useState(members.length === 0);
const [isSubPartySearch, setIsSubPartySearch] = useState(false);
const [searchText, setSearchText] = useState('');
const [searchUrl, setSearchUrl] = useState('');
@@ -42,27 +48,58 @@ export const AccessListMembers = ({
[searchText, isSubPartySearch],
);
- const { mutate: removeListMember } = useRemoveAccessListMemberMutation(org, list.identifier, env);
- const { mutate: addListMember } = useAddAccessListMemberMutation(org, list.identifier, env);
+ const { mutate: removeListMember, isPending: isRemovingMember } =
+ useRemoveAccessListMemberMutation(org, list.identifier, env);
+ const { mutate: addListMember, isPending: isAddingNewListMember } =
+ useAddAccessListMemberMutation(org, list.identifier, env);
- const { data: partiesSearchData, isLoading: isLoadingParties } = usePartiesRegistryQuery(
- !isSubPartySearch ? searchUrl : '',
- );
- const { data: subPartiesSearchData, isLoading: isLoadingSubParties } = useSubPartiesRegistryQuery(
+ const { data: partiesSearchData } = usePartiesRegistryQuery(!isSubPartySearch ? searchUrl : '');
+ const { data: subPartiesSearchData } = useSubPartiesRegistryQuery(
isSubPartySearch ? searchUrl : '',
);
const handleAddMember = (memberToAdd: AccessListMember): void => {
- addListMember(memberToAdd.orgNr);
- setListItems((old) => [...old, memberToAdd]);
+ addListMember([memberToAdd.orgNr], {
+ onError: (error: Error) => {
+ if (
+ ((error as ResourceError).response?.data as { code: string }).code ===
+ INVALID_ORG_ERROR_CODE
+ ) {
+ setInvalidOrgnrs((old) => [...old, memberToAdd.orgNr]);
+ }
+ },
+ });
};
const handleRemoveMember = (memberIdToRemove: string): void => {
- removeListMember(memberIdToRemove);
- setListItems((old) => old.filter((x) => x.orgNr !== memberIdToRemove));
+ removeListMember([memberIdToRemove]);
+ };
+
+ const getResultData = () => {
+ if (
+ (partiesSearchData?.parties?.length === 0 || subPartiesSearchData?.parties?.length === 0) &&
+ isOrgNrString(searchText) &&
+ env !== 'prod'
+ ) {
+ return {
+ parties: [
+ {
+ orgNr: searchText,
+ orgName: t('resourceadm.listadmin_list_tenor_org'),
+ isSubParty: false,
+ },
+ ],
+ };
+ } else if (partiesSearchData) {
+ return partiesSearchData;
+ } else if (subPartiesSearchData) {
+ return subPartiesSearchData;
+ } else {
+ return undefined;
+ }
};
- const resultData = partiesSearchData ?? subPartiesSearchData ?? undefined;
+ const resultData = getResultData();
return (
handleRemoveMember(item.orgNr)}
/>
- {listItems.length === 0 && (
+ {loadMoreButton}
+ {members.length === 0 && (
{t('resourceadm.listadmin_empty_list')}
)}
{isAddMode && (
@@ -100,7 +139,11 @@ export const AccessListMembers = ({
onChange={() => setIsSubPartySearch((old) => !old)}
value={isSubPartySearch ? SUBPARTY_SEARCH_TYPE : PARTY_SEARCH_TYPE}
inline
- legend={t('resourceadm.listadmin_search_party_type')}
+ legend={
+
+ {t('resourceadm.listadmin_search_party_type')}
+
+ }
>
{t('resourceadm.listadmin_parties')}
{t('resourceadm.listadmin_sub_parties')}
@@ -109,18 +152,12 @@ export const AccessListMembers = ({
- {(isLoadingParties || isLoadingSubParties) && (
-
-
-
- )}
>
)}
diff --git a/frontend/resourceadm/components/AccessListMembers/AccessListMembersTable.tsx b/frontend/resourceadm/components/AccessListMembers/AccessListMembersTable.tsx
index 8e4025878c3..16cb96ab6f5 100644
--- a/frontend/resourceadm/components/AccessListMembers/AccessListMembersTable.tsx
+++ b/frontend/resourceadm/components/AccessListMembers/AccessListMembersTable.tsx
@@ -1,6 +1,6 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
-import { Table } from '@digdir/design-system-react';
+import { ErrorMessage, 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';
@@ -9,17 +9,21 @@ import { stringNumberToAriaLabel } from '../../utils/stringUtils';
interface AccessListMembersTableProps {
listItems: AccessListMember[];
+ isLoading: boolean;
isAdd?: boolean;
isHeaderHidden?: boolean;
disabledItems?: AccessListMember[];
+ invalidItems?: string[];
onButtonClick: (member: AccessListMember) => void;
}
export const AccessListMembersTable = ({
listItems,
+ isLoading,
isAdd,
isHeaderHidden,
disabledItems,
+ invalidItems,
onButtonClick,
}: AccessListMembersTableProps): React.JSX.Element => {
const { t } = useTranslation();
@@ -28,6 +32,9 @@ export const AccessListMembersTable = ({
let buttonAriaLabel: string;
let buttonIcon: React.JSX.Element;
let buttonText: string;
+ if (invalidItems?.indexOf(item.orgNr) > -1) {
+ return {t('resourceadm.listadmin_invalid_org')};
+ }
if (isAdd) {
buttonAriaLabel = t('resourceadm.listadmin_add_to_list_org', { org: item.orgName });
buttonIcon = ;
@@ -44,7 +51,8 @@ export const AccessListMembersTable = ({
aria-label={buttonAriaLabel}
onClick={() => onButtonClick(item)}
disabled={
- disabledItems && disabledItems.some((existingItem) => existingItem.orgNr === item.orgNr)
+ isLoading ||
+ (disabledItems && disabledItems.some((existingItem) => existingItem.orgNr === item.orgNr))
}
variant='tertiary'
size='small'
@@ -75,7 +83,12 @@ export const AccessListMembersTable = ({
{listItems.map((item) => {
return (
- {item.orgNr}
+
+ {item.orgNr}
+
{item.orgName || t('resourceadm.listadmin_empty_name')}
{item.isSubParty
diff --git a/frontend/resourceadm/components/ImportResourceModal/ImportResourceModal.tsx b/frontend/resourceadm/components/ImportResourceModal/ImportResourceModal.tsx
index d0091e43b9b..224c20e756d 100644
--- a/frontend/resourceadm/components/ImportResourceModal/ImportResourceModal.tsx
+++ b/frontend/resourceadm/components/ImportResourceModal/ImportResourceModal.tsx
@@ -9,9 +9,8 @@ import { useNavigate } from 'react-router-dom';
import { ServiceContent } from './ServiceContent';
import type { Altinn2LinkService } from 'app-shared/types/Altinn2LinkService';
import { useImportResourceFromAltinn2Mutation } from '../../hooks/mutations';
-import type { Resource } from 'app-shared/types/ResourceAdm';
+import type { Resource, ResourceError } from 'app-shared/types/ResourceAdm';
import { getResourcePageURL } from '../../utils/urlUtils';
-import type { AxiosError } from 'axios';
import { ServerCodes } from 'app-shared/enums/ServerCodes';
import { useUrlParams } from '../../hooks/useSelectedContext';
import { StudioButton } from '@studio/components';
@@ -90,8 +89,8 @@ export const ImportResourceModal = ({
toast.success(t('resourceadm.dashboard_import_success'));
navigate(getResourcePageURL(selectedContext, repo, resource.identifier, 'about'));
},
- onError: (error: AxiosError) => {
- if (error.response.status === ServerCodes.Conflict) {
+ onError: (error: Error) => {
+ if ((error as ResourceError).response?.status === ServerCodes.Conflict) {
setResourceIdExists(true);
}
},
diff --git a/frontend/resourceadm/components/ResourceAccessLists/ResourceAccessLists.test.tsx b/frontend/resourceadm/components/ResourceAccessLists/ResourceAccessLists.test.tsx
index 5394e0d46fa..fd39d3c23d4 100644
--- a/frontend/resourceadm/components/ResourceAccessLists/ResourceAccessLists.test.tsx
+++ b/frontend/resourceadm/components/ResourceAccessLists/ResourceAccessLists.test.tsx
@@ -36,7 +36,7 @@ const accessListResults = {
resourceConnections: [{ resourceIdentifier: resourceId }],
},
],
- nextPage: 1,
+ nextPage: 'http://at22-next-page',
};
const accessListResultsPage2 = {
@@ -49,7 +49,7 @@ const accessListResultsPage2 = {
resourceConnections: [],
},
],
- nextPage: null,
+ nextPage: '',
};
const defaultProps: ResourceAccessListsProps = {
@@ -152,8 +152,20 @@ describe('ResourceAccessLists', () => {
const spinnerTitle = screen.queryByText(textMock('resourceadm.loading_lists'));
await waitForElementToBeRemoved(spinnerTitle);
- await waitFor(() => screen.findByText(textMock('resourceadm.listadmin_load_more')));
- await user.click(screen.getByText(textMock('resourceadm.listadmin_load_more')));
+ await waitFor(() =>
+ screen.findByText(
+ textMock('resourceadm.listadmin_load_more', {
+ unit: textMock('resourceadm.listadmin_list_unit'),
+ }),
+ ),
+ );
+ await user.click(
+ screen.getByText(
+ textMock('resourceadm.listadmin_load_more', {
+ unit: textMock('resourceadm.listadmin_list_unit'),
+ }),
+ ),
+ );
expect(await screen.findByText(page2ListName)).toBeInTheDocument();
});
diff --git a/frontend/resourceadm/components/ResourceAccessLists/ResourceAccessLists.tsx b/frontend/resourceadm/components/ResourceAccessLists/ResourceAccessLists.tsx
index 860d254cd67..6a20f6d46e2 100644
--- a/frontend/resourceadm/components/ResourceAccessLists/ResourceAccessLists.tsx
+++ b/frontend/resourceadm/components/ResourceAccessLists/ResourceAccessLists.tsx
@@ -1,7 +1,6 @@
import React, { useEffect, useState, useRef } from 'react';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
-import type { AxiosError } from 'axios';
import { Checkbox, Heading, Link as DigdirLink, Button } from '@digdir/design-system-react';
import classes from './ResourceAccessLists.module.css';
import { StudioSpinner, StudioButton } from '@studio/components';
@@ -11,7 +10,7 @@ import { useAddResourceAccessListMutation } from '../../hooks/mutations/useAddRe
import { useRemoveResourceAccessListMutation } from '../../hooks/mutations/useRemoveResourceAccessListMutation';
import { getResourcePageURL } from '../../utils/urlUtils';
import { NewAccessListModal } from '../NewAccessListModal';
-import type { Resource } from 'app-shared/types/ResourceAdm';
+import type { Resource, ResourceError } from 'app-shared/types/ResourceAdm';
import { useUrlParams } from '../../hooks/useSelectedContext';
import type { EnvId } from '../../utils/resourceUtils';
import { AccessListErrorMessage } from '../AccessListErrorMessage';
@@ -77,7 +76,7 @@ export const ResourceAccessLists = ({
}
if (accessListsError) {
- return ;
+ return ;
}
return (
@@ -151,7 +150,9 @@ export const ResourceAccessLists = ({
variant='tertiary'
onClick={() => fetchNextPage()}
>
- {t('resourceadm.listadmin_load_more')}
+ {t('resourceadm.listadmin_load_more', {
+ unit: t('resourceadm.listadmin_list_unit'),
+ })}
)}
diff --git a/frontend/resourceadm/components/ResourceDeployEnvCard/ResourceDeployEnvCard.tsx b/frontend/resourceadm/components/ResourceDeployEnvCard/ResourceDeployEnvCard.tsx
index c87fd4d1866..e014e4b2078 100644
--- a/frontend/resourceadm/components/ResourceDeployEnvCard/ResourceDeployEnvCard.tsx
+++ b/frontend/resourceadm/components/ResourceDeployEnvCard/ResourceDeployEnvCard.tsx
@@ -1,7 +1,6 @@
import React, { useState } from 'react';
import { toast } from 'react-toastify';
import { useTranslation } from 'react-i18next';
-import type { AxiosError } from 'axios';
import { Tag, Paragraph, Spinner, Alert } from '@digdir/design-system-react';
import classes from './ResourceDeployEnvCard.module.css';
import { ArrowRightIcon } from '@studio/icons';
@@ -9,6 +8,7 @@ import { StudioButton } from '@studio/components';
import { usePublishResourceMutation } from '../../hooks/mutations';
import { type Environment } from '../../utils/resourceUtils';
import { useUrlParams } from '../../hooks/useSelectedContext';
+import type { ResourceError } from 'app-shared/types/ResourceAdm';
export type ResourceDeployEnvCardProps = {
isDeployPossible: boolean;
@@ -50,7 +50,7 @@ export const ResourceDeployEnvCard = ({
toast.success(t('resourceadm.resource_published_success', { envName: t(env.label) }));
},
onError: (error: Error) => {
- if ((error as AxiosError).response.status === 403) {
+ if ((error as ResourceError).response?.status === 403) {
setHasNoPublishAccess(true);
}
},
diff --git a/frontend/resourceadm/hooks/mutations/useAddAccessListMemberMutation.ts b/frontend/resourceadm/hooks/mutations/useAddAccessListMemberMutation.ts
index 74c3a93dbb7..b98f2b25f46 100644
--- a/frontend/resourceadm/hooks/mutations/useAddAccessListMemberMutation.ts
+++ b/frontend/resourceadm/hooks/mutations/useAddAccessListMemberMutation.ts
@@ -18,9 +18,12 @@ export const useAddAccessListMemberMutation = (
const { addAccessListMember } = useServicesContext();
return useMutation({
- mutationFn: (orgnr: string) => addAccessListMember(org, listIdentifier, orgnr, env),
+ mutationFn: (orgnrs: string[]) =>
+ addAccessListMember(org, listIdentifier, env, { data: orgnrs }),
onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: [QueryKey.AccessList, env, listIdentifier] });
+ queryClient.invalidateQueries({
+ queryKey: [QueryKey.AccessListMembers, env, listIdentifier],
+ });
},
});
};
diff --git a/frontend/resourceadm/hooks/mutations/useCreateAccessListMutation.ts b/frontend/resourceadm/hooks/mutations/useCreateAccessListMutation.ts
index a04550ad75b..4ea4ac2d8aa 100644
--- a/frontend/resourceadm/hooks/mutations/useCreateAccessListMutation.ts
+++ b/frontend/resourceadm/hooks/mutations/useCreateAccessListMutation.ts
@@ -17,6 +17,7 @@ export const useCreateAccessListMutation = (org: string, env: string) => {
mutationFn: (payload: Partial) => createAccessList(org, env, payload),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [QueryKey.AccessLists, env] });
+ queryClient.invalidateQueries({ queryKey: [QueryKey.ResourceAccessLists, env] });
},
});
};
diff --git a/frontend/resourceadm/hooks/mutations/useDeleteAccessListMutation.ts b/frontend/resourceadm/hooks/mutations/useDeleteAccessListMutation.ts
index 8f90a7849c0..f0193f313a5 100644
--- a/frontend/resourceadm/hooks/mutations/useDeleteAccessListMutation.ts
+++ b/frontend/resourceadm/hooks/mutations/useDeleteAccessListMutation.ts
@@ -17,6 +17,7 @@ export const useDeleteAccessListMutation = (org: string, listIdentifier: string,
mutationFn: () => deleteAccessList(org, listIdentifier, env),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [QueryKey.AccessLists, env] });
+ queryClient.invalidateQueries({ queryKey: [QueryKey.ResourceAccessLists, env] });
},
});
};
diff --git a/frontend/resourceadm/hooks/mutations/useRemoveAccessListMemberMutation.ts b/frontend/resourceadm/hooks/mutations/useRemoveAccessListMemberMutation.ts
index d65527c6352..42e6fe4de24 100644
--- a/frontend/resourceadm/hooks/mutations/useRemoveAccessListMemberMutation.ts
+++ b/frontend/resourceadm/hooks/mutations/useRemoveAccessListMemberMutation.ts
@@ -18,9 +18,12 @@ export const useRemoveAccessListMemberMutation = (
const { removeAccessListMember } = useServicesContext();
return useMutation({
- mutationFn: (orgnr: string) => removeAccessListMember(org, listIdentifier, orgnr, env),
+ mutationFn: (orgnrs: string[]) =>
+ removeAccessListMember(org, listIdentifier, env, { data: orgnrs }),
onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: [QueryKey.AccessList, env, listIdentifier] });
+ queryClient.invalidateQueries({
+ queryKey: [QueryKey.AccessListMembers, env, listIdentifier],
+ });
},
});
};
diff --git a/frontend/resourceadm/hooks/queries/useGetAccessListMembersQuery.ts b/frontend/resourceadm/hooks/queries/useGetAccessListMembersQuery.ts
new file mode 100644
index 00000000000..ce0d2ee7d4d
--- /dev/null
+++ b/frontend/resourceadm/hooks/queries/useGetAccessListMembersQuery.ts
@@ -0,0 +1,33 @@
+import type { InfiniteData, UseInfiniteQueryResult } from '@tanstack/react-query';
+import { useInfiniteQuery } from '@tanstack/react-query';
+import { useServicesContext } from 'app-shared/contexts/ServicesContext';
+import { QueryKey } from 'app-shared/types/QueryKey';
+import type { AccessListMember } from 'app-shared/types/ResourceAdm';
+
+/**
+ * Query to get paginated members of access list
+ *
+ * @param org the organisation of the user
+ * @param resourceId the identifier of the resource
+ * @param env the chosen environment
+ *
+ * @returns UseInfiniteQueryResult with a list of access lists members
+ */
+export const useGetAccessListMembersQuery = (
+ org: string,
+ accessListId: string,
+ env: string,
+): UseInfiniteQueryResult> => {
+ const { getAccessListMembers } = useServicesContext();
+ return useInfiniteQuery({
+ queryKey: [QueryKey.AccessListMembers, env, accessListId],
+ queryFn: ({ pageParam }) => getAccessListMembers(org, accessListId, env, pageParam),
+ initialPageParam: '',
+ getNextPageParam: (lastPage) => lastPage.nextPage,
+ enabled: !!org && !!env && !!accessListId,
+ select: (data) => ({
+ ...data,
+ pages: data.pages.flatMap((page) => page.data),
+ }),
+ });
+};
diff --git a/frontend/resourceadm/hooks/queries/useGetAccessListsQuery.ts b/frontend/resourceadm/hooks/queries/useGetAccessListsQuery.ts
index 4d5711dbd78..791a895832a 100644
--- a/frontend/resourceadm/hooks/queries/useGetAccessListsQuery.ts
+++ b/frontend/resourceadm/hooks/queries/useGetAccessListsQuery.ts
@@ -15,13 +15,13 @@ import type { AccessList } from 'app-shared/types/ResourceAdm';
export const useGetAccessListsQuery = (
org: string,
env: string,
-): UseInfiniteQueryResult> => {
+): UseInfiniteQueryResult> => {
const { getAccessLists } = useServicesContext();
return useInfiniteQuery({
queryKey: [QueryKey.AccessLists, env],
queryFn: ({ pageParam }) => getAccessLists(org, env, pageParam),
- initialPageParam: 0,
+ initialPageParam: '',
getNextPageParam: (lastPage) => lastPage.nextPage,
enabled: !!org && !!env,
select: (data) => ({
diff --git a/frontend/resourceadm/hooks/queries/useGetAltinn2LinkServicesQuery.ts b/frontend/resourceadm/hooks/queries/useGetAltinn2LinkServicesQuery.ts
index b908e280ab1..221c33f43e6 100644
--- a/frontend/resourceadm/hooks/queries/useGetAltinn2LinkServicesQuery.ts
+++ b/frontend/resourceadm/hooks/queries/useGetAltinn2LinkServicesQuery.ts
@@ -3,7 +3,7 @@ import { useQuery } from '@tanstack/react-query';
import { useServicesContext } from 'app-shared/contexts/ServicesContext';
import type { Altinn2LinkService } from 'app-shared/types/Altinn2LinkService';
import { QueryKey } from 'app-shared/types/QueryKey';
-import type { AxiosError } from 'axios';
+import type { ResourceError } from 'app-shared/types/ResourceAdm';
/**
* Query to get the list of services from Altinn 2.
@@ -16,10 +16,10 @@ import type { AxiosError } from 'axios';
export const useGetAltinn2LinkServicesQuery = (
org: string,
environment: string,
-): UseQueryResult => {
+): UseQueryResult => {
const { getAltinn2LinkServices } = useServicesContext();
- return useQuery({
+ return useQuery({
queryKey: [QueryKey.Altinn2Services, org, environment],
queryFn: () => getAltinn2LinkServices(org, environment),
});
diff --git a/frontend/resourceadm/hooks/queries/useGetResourceAccessListsQuery.ts b/frontend/resourceadm/hooks/queries/useGetResourceAccessListsQuery.ts
index d3902da8e10..088673be7b3 100644
--- a/frontend/resourceadm/hooks/queries/useGetResourceAccessListsQuery.ts
+++ b/frontend/resourceadm/hooks/queries/useGetResourceAccessListsQuery.ts
@@ -17,13 +17,13 @@ export const useGetResourceAccessListsQuery = (
org: string,
resourceId: string,
env: string,
-): UseInfiniteQueryResult> => {
+): UseInfiniteQueryResult> => {
const { getResourceAccessLists } = useServicesContext();
return useInfiniteQuery({
queryKey: [QueryKey.ResourceAccessLists, env, resourceId],
queryFn: ({ pageParam }) => getResourceAccessLists(org, resourceId, env, pageParam),
- initialPageParam: 0,
+ initialPageParam: '',
getNextPageParam: (lastPage) => lastPage.nextPage,
enabled: !!org && !!env && !!resourceId,
select: (data) => ({
diff --git a/frontend/resourceadm/hooks/queries/usePartiesRegistryQuery.ts b/frontend/resourceadm/hooks/queries/usePartiesRegistryQuery.ts
index e9c43d482fb..d29ed6756e2 100644
--- a/frontend/resourceadm/hooks/queries/usePartiesRegistryQuery.ts
+++ b/frontend/resourceadm/hooks/queries/usePartiesRegistryQuery.ts
@@ -1,13 +1,16 @@
-import { useQuery } from '@tanstack/react-query';
+import { keepPreviousData, useQuery } from '@tanstack/react-query';
import { useServicesContext } from 'app-shared/contexts/ServicesContext';
import { QueryKey } from 'app-shared/types/QueryKey';
-import type { BrregPartySearchResult, BrregSearchResult } from 'app-shared/types/ResourceAdm';
-import type { AxiosError } from 'axios';
+import type {
+ BrregPartySearchResult,
+ BrregSearchResult,
+ ResourceError,
+} from 'app-shared/types/ResourceAdm';
export const usePartiesRegistryQuery = (searchUrl: string) => {
const { getParties } = useServicesContext();
- return useQuery({
+ return useQuery({
queryKey: [QueryKey.PartiesRegistrySearch, searchUrl],
queryFn: () => getParties(searchUrl),
select: (data): BrregSearchResult => {
@@ -24,5 +27,6 @@ export const usePartiesRegistryQuery = (searchUrl: string) => {
};
},
enabled: !!searchUrl,
+ placeholderData: keepPreviousData,
});
};
diff --git a/frontend/resourceadm/hooks/queries/useResourcePolicyPublishStatusQuery.ts b/frontend/resourceadm/hooks/queries/useResourcePolicyPublishStatusQuery.ts
index 5afff26fe28..eb951d75789 100644
--- a/frontend/resourceadm/hooks/queries/useResourcePolicyPublishStatusQuery.ts
+++ b/frontend/resourceadm/hooks/queries/useResourcePolicyPublishStatusQuery.ts
@@ -2,8 +2,7 @@ import type { UseQueryResult } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import { useServicesContext } from 'app-shared/contexts/ServicesContext';
import { QueryKey } from 'app-shared/types/QueryKey';
-import type { ResourceVersionStatus } from 'app-shared/types/ResourceAdm';
-import type { AxiosError } from 'axios';
+import type { ResourceError, ResourceVersionStatus } from 'app-shared/types/ResourceAdm';
/**
* Query to get the status of the versions of a resource.
@@ -18,10 +17,10 @@ export const useResourcePolicyPublishStatusQuery = (
org: string,
repo: string,
id: string,
-): UseQueryResult => {
+): UseQueryResult => {
const { getResourcePublishStatus } = useServicesContext();
- return useQuery({
+ return useQuery({
queryKey: [QueryKey.ResourcePublishStatus, org, repo, id],
queryFn: () => getResourcePublishStatus(org, repo, id),
});
diff --git a/frontend/resourceadm/hooks/queries/useSubPartiesRegistryQuery.ts b/frontend/resourceadm/hooks/queries/useSubPartiesRegistryQuery.ts
index 731689819d7..e76a0211e4b 100644
--- a/frontend/resourceadm/hooks/queries/useSubPartiesRegistryQuery.ts
+++ b/frontend/resourceadm/hooks/queries/useSubPartiesRegistryQuery.ts
@@ -1,13 +1,16 @@
-import { useQuery } from '@tanstack/react-query';
+import { keepPreviousData, useQuery } from '@tanstack/react-query';
import { useServicesContext } from 'app-shared/contexts/ServicesContext';
import { QueryKey } from 'app-shared/types/QueryKey';
-import type { BrregSearchResult, BrregSubPartySearchResult } from 'app-shared/types/ResourceAdm';
-import type { AxiosError } from 'axios';
+import type {
+ BrregSearchResult,
+ BrregSubPartySearchResult,
+ ResourceError,
+} from 'app-shared/types/ResourceAdm';
export const useSubPartiesRegistryQuery = (searchUrl: string) => {
const { getSubParties } = useServicesContext();
- return useQuery({
+ return useQuery({
queryKey: [QueryKey.SubPartiesRegistrySearch, searchUrl],
queryFn: () => getSubParties(searchUrl),
select: (data): BrregSearchResult => {
@@ -24,5 +27,6 @@ export const useSubPartiesRegistryQuery = (searchUrl: string) => {
};
},
enabled: !!searchUrl,
+ placeholderData: keepPreviousData,
});
};
diff --git a/frontend/resourceadm/hooks/queries/useValidatePolicyQuery.ts b/frontend/resourceadm/hooks/queries/useValidatePolicyQuery.ts
index 3c6a32e5099..bc079a34972 100644
--- a/frontend/resourceadm/hooks/queries/useValidatePolicyQuery.ts
+++ b/frontend/resourceadm/hooks/queries/useValidatePolicyQuery.ts
@@ -2,8 +2,7 @@ import type { UseQueryResult } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import { useServicesContext } from 'app-shared/contexts/ServicesContext';
import { QueryKey } from 'app-shared/types/QueryKey';
-import type { Validation } from 'app-shared/types/ResourceAdm';
-import type { AxiosError } from 'axios';
+import type { ResourceError, Validation } from 'app-shared/types/ResourceAdm';
import { ServerCodes } from 'app-shared/enums/ServerCodes';
/**
@@ -19,10 +18,10 @@ export const useValidatePolicyQuery = (
org: string,
repo: string,
id: string,
-): UseQueryResult => {
+): UseQueryResult => {
const { getValidatePolicy } = useServicesContext();
- return useQuery({
+ return useQuery({
queryKey: [QueryKey.ValidatePolicy, org, repo, id],
queryFn: () => getValidatePolicy(org, repo, id),
select: (data) => {
diff --git a/frontend/resourceadm/hooks/queries/useValidateResourceQuery.ts b/frontend/resourceadm/hooks/queries/useValidateResourceQuery.ts
index ecad289fe95..cf098968a85 100644
--- a/frontend/resourceadm/hooks/queries/useValidateResourceQuery.ts
+++ b/frontend/resourceadm/hooks/queries/useValidateResourceQuery.ts
@@ -2,8 +2,7 @@ import type { UseQueryResult } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import { useServicesContext } from 'app-shared/contexts/ServicesContext';
import { QueryKey } from 'app-shared/types/QueryKey';
-import type { Validation } from 'app-shared/types/ResourceAdm';
-import type { AxiosError } from 'axios';
+import type { ResourceError, Validation } from 'app-shared/types/ResourceAdm';
/**
* Query to get the validation status of a resource.
@@ -18,10 +17,10 @@ export const useValidateResourceQuery = (
org: string,
repo: string,
id: string,
-): UseQueryResult => {
+): UseQueryResult => {
const { getValidateResource } = useServicesContext();
- return useQuery({
+ return useQuery({
queryKey: [QueryKey.ValidateResource, org, repo, id],
queryFn: () => getValidateResource(org, repo, id),
select: (data) => ({
diff --git a/frontend/resourceadm/pages/ListAdminPage/ListAdminPage.test.tsx b/frontend/resourceadm/pages/ListAdminPage/ListAdminPage.test.tsx
index bd9ca73bee6..8c098e68820 100644
--- a/frontend/resourceadm/pages/ListAdminPage/ListAdminPage.test.tsx
+++ b/frontend/resourceadm/pages/ListAdminPage/ListAdminPage.test.tsx
@@ -13,7 +13,7 @@ const accessListResults = {
data: [
{ env: 'tt02', identifier: 'listid', name: 'Test-list', description: 'Test-list description' },
],
- nextPage: 1,
+ nextPage: 'http://at22-next-page',
};
const accessListResultsPage2 = {
@@ -25,7 +25,7 @@ const accessListResultsPage2 = {
description: 'Test-list description2',
},
],
- nextPage: null,
+ nextPage: '',
};
const mockedNavigate = jest.fn();
@@ -103,8 +103,20 @@ describe('ListAdminPage', () => {
const user = userEvent.setup();
renderListAdminPage();
- await waitFor(() => screen.findByText(textMock('resourceadm.listadmin_load_more')));
- await user.click(screen.getByText(textMock('resourceadm.listadmin_load_more')));
+ await waitFor(() =>
+ screen.findByText(
+ textMock('resourceadm.listadmin_load_more', {
+ unit: textMock('resourceadm.listadmin_list_unit'),
+ }),
+ ),
+ );
+ await user.click(
+ screen.getByText(
+ textMock('resourceadm.listadmin_load_more', {
+ unit: textMock('resourceadm.listadmin_list_unit'),
+ }),
+ ),
+ );
expect(await screen.findByText('Test-list2')).toBeInTheDocument();
});
diff --git a/frontend/resourceadm/pages/ListAdminPage/ListAdminPage.tsx b/frontend/resourceadm/pages/ListAdminPage/ListAdminPage.tsx
index bea09e091b7..8880c6b0438 100644
--- a/frontend/resourceadm/pages/ListAdminPage/ListAdminPage.tsx
+++ b/frontend/resourceadm/pages/ListAdminPage/ListAdminPage.tsx
@@ -1,7 +1,6 @@
import React, { useRef, useEffect, useCallback } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
-import type { AxiosError } from 'axios';
import { Heading, Link as DigdirLink, ToggleGroup, Button } from '@digdir/design-system-react';
import { StudioSpinner, StudioButton } from '@studio/components';
import { PencilWritingIcon, PlusIcon } from '@studio/icons';
@@ -13,6 +12,7 @@ import { useUrlParams } from '../../hooks/useSelectedContext';
import type { EnvId } from '../../utils/resourceUtils';
import { getAvailableEnvironments, getEnvLabel } from '../../utils/resourceUtils';
import { AccessListErrorMessage } from '../../components/AccessListErrorMessage';
+import type { ResourceError } from 'app-shared/types/ResourceAdm';
export const ListAdminPage = (): React.JSX.Element => {
const { t } = useTranslation();
@@ -75,7 +75,7 @@ export const ListAdminPage = (): React.JSX.Element => {
/>
{listFetchError && (
)}
@@ -122,7 +122,9 @@ export const ListAdminPage = (): React.JSX.Element => {
variant='tertiary'
onClick={() => fetchNextPage()}
>
- {t('resourceadm.listadmin_load_more')}
+ {t('resourceadm.listadmin_load_more', {
+ unit: t('resourceadm.listadmin_list_unit'),
+ })}
)}
diff --git a/frontend/resourceadm/utils/stringUtils/index.ts b/frontend/resourceadm/utils/stringUtils/index.ts
index 75bf35bfce0..a8b3f0dc76f 100644
--- a/frontend/resourceadm/utils/stringUtils/index.ts
+++ b/frontend/resourceadm/utils/stringUtils/index.ts
@@ -1 +1,7 @@
-export { formatIdString, isAppPrefix, isSePrefix, stringNumberToAriaLabel } from './stringUtils';
+export {
+ formatIdString,
+ isAppPrefix,
+ isSePrefix,
+ stringNumberToAriaLabel,
+ isOrgNrString,
+} from './stringUtils';
diff --git a/frontend/resourceadm/utils/stringUtils/stringUtils.ts b/frontend/resourceadm/utils/stringUtils/stringUtils.ts
index 58758509a9c..eb9f7be6f85 100644
--- a/frontend/resourceadm/utils/stringUtils/stringUtils.ts
+++ b/frontend/resourceadm/utils/stringUtils/stringUtils.ts
@@ -26,3 +26,7 @@ export const isSePrefix = (s: string): boolean => {
export const stringNumberToAriaLabel = (s: string): string => {
return s.split('').join(' ');
};
+
+export const isOrgNrString = (s: string): boolean => {
+ return /^\d{9}$/.test(s); // regex for search string is exactly 9 digits
+};
diff --git a/frontend/resourceadm/utils/urlUtils/urlUtils.ts b/frontend/resourceadm/utils/urlUtils/urlUtils.ts
index 407a920d97e..58f90f6499d 100644
--- a/frontend/resourceadm/utils/urlUtils/urlUtils.ts
+++ b/frontend/resourceadm/utils/urlUtils/urlUtils.ts
@@ -1,4 +1,5 @@
import type { NavigationBarPage } from '../../types/NavigationBarPage';
+import { isOrgNrString } from '../stringUtils';
/**
* Returns the path to the dashboard based on the name of the organisation
@@ -45,7 +46,7 @@ export const getAccessListPageUrl = (
export const getPartiesQueryUrl = (search: string, isSubParty?: boolean): string => {
const partyType = isSubParty ? 'underenheter' : 'enheter';
- const isOrgnrSearch = /^\d{9}$/.test(search); // regex for search string is exactly 9 digits
+ const isOrgnrSearch = isOrgNrString(search);
const searchTerm = isOrgnrSearch ? `organisasjonsnummer=${search}` : `navn=${search}`;
- return `https://data.brreg.no/enhetsregisteret/api/${partyType}?${searchTerm}&size=5`;
+ return `https://data.brreg.no/enhetsregisteret/api/${partyType}?${searchTerm}&size=10`;
};
From aca9dda488cf6b25e18fb7ef8761fdac81574a35 Mon Sep 17 00:00:00 2001
From: WilliamThorenfeldt
<133344438+WilliamThorenfeldt@users.noreply.github.com>
Date: Thu, 30 May 2024 18:11:34 +0200
Subject: [PATCH 07/27] Remove prop drilling of app and org in SettingsModal
(#12885)
---
.../app-development/layout/PageHeader.tsx | 4 +-
.../SettingsModal/SettingsModal.test.tsx | 3 --
.../SettingsModal/SettingsModal.tsx | 41 +++----------------
.../components/TabHeader/TabHeader.tsx | 3 --
.../Tabs/AboutTab/AboutTab.test.tsx | 20 ++++-----
.../components/Tabs/AboutTab/AboutTab.tsx | 18 ++------
.../Tabs/AboutTab/InputFields/InputFields.tsx | 8 ----
.../AccessControlTab.test.tsx | 20 +++++----
.../AccessControlTab/AccessControlTab.tsx | 20 ++-------
.../SelectAllowedPartyTypes.test.tsx | 18 +++++---
.../SelectAllowedPartyTypes.tsx | 11 ++---
.../Tabs/PolicyTab/PolicyTab.test.tsx | 20 +++++----
.../components/Tabs/PolicyTab/PolicyTab.tsx | 18 ++------
.../Tabs/SetupTab/SetupTab.test.tsx | 18 ++++----
.../components/Tabs/SetupTab/SetupTab.tsx | 20 ++-------
.../SetupTabContent/SetupTabContent.test.tsx | 11 +++--
.../SetupTabContent/SetupTabContent.tsx | 16 ++------
.../SettingsModalButton.test.tsx | 9 +---
.../SettingsModalButton.tsx | 13 +-----
19 files changed, 91 insertions(+), 200 deletions(-)
diff --git a/frontend/app-development/layout/PageHeader.tsx b/frontend/app-development/layout/PageHeader.tsx
index 0e082047ab7..6bff843e34f 100644
--- a/frontend/app-development/layout/PageHeader.tsx
+++ b/frontend/app-development/layout/PageHeader.tsx
@@ -25,9 +25,7 @@ export const subMenuContent = ({ org, app, hasRepoError }: SubMenuContentProps)
org={org}
app={app}
hasCloneModal
- leftComponent={
- repositoryType !== RepositoryType.DataModels &&
- }
+ leftComponent={repositoryType !== RepositoryType.DataModels &&
}
hasRepoError={hasRepoError}
/>
);
diff --git a/frontend/app-development/layout/SettingsModalButton/SettingsModal/SettingsModal.test.tsx b/frontend/app-development/layout/SettingsModalButton/SettingsModal/SettingsModal.test.tsx
index e20cffd80fe..97436df068e 100644
--- a/frontend/app-development/layout/SettingsModalButton/SettingsModal/SettingsModal.test.tsx
+++ b/frontend/app-development/layout/SettingsModalButton/SettingsModal/SettingsModal.test.tsx
@@ -11,7 +11,6 @@ import { ServicesContextProvider } from 'app-shared/contexts/ServicesContext';
import type { AppConfig } from 'app-shared/types/AppConfig';
import { useAppConfigMutation } from 'app-development/hooks/mutations';
import { MemoryRouter } from 'react-router-dom';
-import { app, org } from '@studio/testing/testids';
jest.mock('../../../hooks/mutations/useAppConfigMutation');
const updateAppConfigMutation = jest.fn();
@@ -33,8 +32,6 @@ describe('SettingsModal', () => {
const defaultProps: SettingsModalProps = {
isOpen: true,
onClose: mockOnClose,
- org,
- app,
};
it('closes the modal when the close button is clicked', async () => {
diff --git a/frontend/app-development/layout/SettingsModalButton/SettingsModal/SettingsModal.tsx b/frontend/app-development/layout/SettingsModalButton/SettingsModal/SettingsModal.tsx
index 00ad5d38afe..3c9874e6f91 100644
--- a/frontend/app-development/layout/SettingsModalButton/SettingsModal/SettingsModal.tsx
+++ b/frontend/app-development/layout/SettingsModalButton/SettingsModal/SettingsModal.tsx
@@ -23,42 +23,19 @@ import { SetupTab } from './components/Tabs/SetupTab';
export type SettingsModalProps = {
isOpen: boolean;
onClose: () => void;
- org: string;
- app: string;
defaultTab?: SettingsModalTab;
};
-/**
- * @component
- * Displays the settings modal
- *
- * @property {boolean}[isOpen] - Flag for if the modal is open
- * @property {function}[onClose] - Function to be executed on close
- *
- * @returns {ReactNode} - The rendered component
- */
-export const SettingsModal = ({
- isOpen,
- onClose,
- org,
- app,
- defaultTab,
-}: SettingsModalProps): ReactNode => {
+export const SettingsModal = ({ isOpen, onClose, defaultTab }: SettingsModalProps): ReactNode => {
const { t } = useTranslation();
const [currentTab, setCurrentTab] = useState
(defaultTab || 'about');
- /**
- * Ids for the navigation tabs
- */
const aboutTabId: SettingsModalTab = 'about';
const setupTabId: SettingsModalTab = 'setup';
const policyTabId: SettingsModalTab = 'policy';
const accessControlTabId: SettingsModalTab = 'access_control';
- /**
- * The tabs to display in the navigation bar
- */
const leftNavigationTabs: LeftNavigationTab[] = [
createNavigationTab(
,
@@ -86,31 +63,23 @@ export const SettingsModal = ({
),
];
- /**
- * Changes the active tab
- * @param tabId
- */
const changeTabTo = (tabId: SettingsModalTab) => {
setCurrentTab(tabId);
};
- /**
- * Displays the currently selected tab and its content
- * @returns
- */
const displayTabs = () => {
switch (currentTab) {
case 'about': {
- return ;
+ return ;
}
case 'setup': {
- return ;
+ return ;
}
case 'policy': {
- return ;
+ return ;
}
case 'access_control': {
- return ;
+ return ;
}
}
};
diff --git a/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/TabHeader/TabHeader.tsx b/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/TabHeader/TabHeader.tsx
index 74a592f766f..c8773b55d7d 100644
--- a/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/TabHeader/TabHeader.tsx
+++ b/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/TabHeader/TabHeader.tsx
@@ -4,9 +4,6 @@ import classes from './TabHeader.module.css';
import { Heading } from '@digdir/design-system-react';
export type TabHeaderProps = {
- /**
- * The text in the header
- */
text: string;
};
diff --git a/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AboutTab/AboutTab.test.tsx b/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AboutTab/AboutTab.test.tsx
index 83629c02787..aecd644552b 100644
--- a/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AboutTab/AboutTab.test.tsx
+++ b/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AboutTab/AboutTab.test.tsx
@@ -1,6 +1,5 @@
import React from 'react';
import { render as rtlRender, screen, waitForElementToBeRemoved } from '@testing-library/react';
-import type { AboutTabProps } from './AboutTab';
import { AboutTab } from './AboutTab';
import { textMock } from '@studio/testing/mocks/i18nMock';
import type { AppConfig } from 'app-shared/types/AppConfig';
@@ -39,10 +38,12 @@ const getAppConfig = jest.fn().mockImplementation(() => Promise.resolve({}));
const getRepoMetadata = jest.fn().mockImplementation(() => Promise.resolve({}));
const getAppMetadata = jest.fn().mockImplementation(() => Promise.resolve({}));
-const defaultProps: AboutTabProps = {
- org,
- app,
-};
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useParams: () => {
+ return { org, app };
+ },
+}));
describe('AboutTab', () => {
afterEach(jest.clearAllMocks);
@@ -72,7 +73,7 @@ describe('AboutTab', () => {
'shows an error message if an error occured on the %s query',
async (queryName) => {
const errorMessage = 'error-message-test';
- render(defaultProps, {
+ render({
[queryName]: () => Promise.reject({ message: errorMessage }),
});
@@ -166,19 +167,18 @@ describe('AboutTab', () => {
});
});
-const resolveAndWaitForSpinnerToDisappear = async (props: Partial = {}) => {
+const resolveAndWaitForSpinnerToDisappear = async () => {
getAppConfig.mockImplementation(() => Promise.resolve(mockAppConfig));
getRepoMetadata.mockImplementation(() => Promise.resolve(mockRepository1));
getAppMetadata.mockImplementation(() => Promise.resolve(mockAppMetadata));
- render(props);
+ render();
await waitForElementToBeRemoved(() =>
screen.queryByTitle(textMock('settings_modal.loading_content')),
);
};
const render = (
- props: Partial = {},
queries: Partial = {},
queryClient: QueryClient = createQueryClientMock(),
) => {
@@ -193,7 +193,7 @@ const render = (
return rtlRender(
-
+
,
);
diff --git a/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AboutTab/AboutTab.tsx b/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AboutTab/AboutTab.tsx
index ed13cb154c5..64356535ca5 100644
--- a/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AboutTab/AboutTab.tsx
+++ b/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AboutTab/AboutTab.tsx
@@ -15,23 +15,11 @@ import { TabDataError } from '../../TabDataError';
import { InputFields } from './InputFields';
import { CreatedFor } from './CreatedFor';
import { TabContent } from '../../TabContent';
+import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams';
-export type AboutTabProps = {
- org: string;
- app: string;
-};
-
-/**
- * @component
- * Displays the tab rendering the config for an app
- *
- * @property {string}[org] - The org
- * @property {string}[app] - The app
- *
- * @returns {ReactNode} - The rendered component
- */
-export const AboutTab = ({ org, app }: AboutTabProps): ReactNode => {
+export const AboutTab = (): ReactNode => {
const { t } = useTranslation();
+ const { org, app } = useStudioEnvironmentParams();
const repositoryType = getRepositoryType(org, app);
diff --git a/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AboutTab/InputFields/InputFields.tsx b/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AboutTab/InputFields/InputFields.tsx
index c48e585297c..4403e0e5985 100644
--- a/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AboutTab/InputFields/InputFields.tsx
+++ b/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AboutTab/InputFields/InputFields.tsx
@@ -8,15 +8,7 @@ import { Textfield } from '@digdir/design-system-react';
type AppConfigForm = Pick;
export type InputFieldsProps = {
- /**
- * The app configuration data
- */
appConfig: AppConfig;
- /**
- * Function to save the updated data
- * @param appConfig the new app config
- * @returns void
- */
onSave: (appConfig: AppConfig) => void;
};
diff --git a/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AccessControlTab/AccessControlTab.test.tsx b/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AccessControlTab/AccessControlTab.test.tsx
index c6f0094290f..f2fdd656671 100644
--- a/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AccessControlTab/AccessControlTab.test.tsx
+++ b/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AccessControlTab/AccessControlTab.test.tsx
@@ -1,6 +1,5 @@
import React from 'react';
import { render as rtlRender, screen, waitForElementToBeRemoved } from '@testing-library/react';
-import type { AccessControlTabProps } from './AccessControlTab';
import { AccessControlTab } from './AccessControlTab';
import { textMock } from '@studio/testing/mocks/i18nMock';
import { createQueryClientMock } from 'app-shared/mocks/queryClientMock';
@@ -10,13 +9,16 @@ import type { QueryClient } from '@tanstack/react-query';
import { mockAppMetadata } from '../../../mocks/applicationMetadataMock';
import userEvent from '@testing-library/user-event';
import { app, org } from '@studio/testing/testids';
+import { MemoryRouter } from 'react-router-dom';
const getAppMetadata = jest.fn().mockImplementation(() => Promise.resolve({}));
-const defaultProps: AccessControlTabProps = {
- org,
- app,
-};
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useParams: () => {
+ return { org, app };
+ },
+}));
describe('AccessControlTab', () => {
afterEach(jest.clearAllMocks);
@@ -88,8 +90,10 @@ const render = (
};
return rtlRender(
-
-
- ,
+
+
+
+
+ ,
);
};
diff --git a/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AccessControlTab/AccessControlTab.tsx b/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AccessControlTab/AccessControlTab.tsx
index d96ab6db594..2cf4f94d2bd 100644
--- a/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AccessControlTab/AccessControlTab.tsx
+++ b/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AccessControlTab/AccessControlTab.tsx
@@ -9,23 +9,11 @@ import { LoadingTabData } from '../../LoadingTabData';
import { TabDataError } from '../../TabDataError';
import { TabContent } from '../../TabContent';
import { SelectAllowedPartyTypes } from './SelectAllowedPartyTypes';
+import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams';
-export type AccessControlTabProps = {
- org: string;
- app: string;
-};
-
-/**
- * @component
- * Displays the tab rendering the access control for an app
- *
- * @property {string}[org] - The org
- * @property {string}[app] - The app
- *
- * @returns {ReactNode} - The rendered component
- */
-export const AccessControlTab = ({ org, app }: AccessControlTabProps): ReactNode => {
+export const AccessControlTab = (): ReactNode => {
const { t } = useTranslation();
+ const { org, app } = useStudioEnvironmentParams();
const {
data: appMetadata,
@@ -52,7 +40,7 @@ export const AccessControlTab = ({ org, app }: AccessControlTabProps): ReactNode
{t('settings_modal.access_control_tab_checkbox_description')}
-
+
>
);
}
diff --git a/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AccessControlTab/SelectAllowedPartyTypes/SelectAllowedPartyTypes.test.tsx b/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AccessControlTab/SelectAllowedPartyTypes/SelectAllowedPartyTypes.test.tsx
index 356e33206c6..7b95536f7c1 100644
--- a/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AccessControlTab/SelectAllowedPartyTypes/SelectAllowedPartyTypes.test.tsx
+++ b/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AccessControlTab/SelectAllowedPartyTypes/SelectAllowedPartyTypes.test.tsx
@@ -12,10 +12,16 @@ import { queriesMock } from 'app-shared/mocks/queriesMock';
import { ServicesContextProvider } from 'app-shared/contexts/ServicesContext';
import { createQueryClientMock } from 'app-shared/mocks/queryClientMock';
import { app, org } from '@studio/testing/testids';
+import { MemoryRouter } from 'react-router-dom';
+
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useParams: () => {
+ return { org, app };
+ },
+}));
const defaultProps: SelectAllowedPartyTypesProps = {
- org,
- app,
appMetadata: mockAppMetadata,
};
@@ -140,8 +146,10 @@ describe('SelectAllowedPartyTypes', () => {
const renderSelectAllowedPartyTypes = (props: Partial = {}) => {
const queryClient: QueryClient = createQueryClientMock();
return rtlRender(
-
-
- ,
+
+
+
+
+ ,
);
};
diff --git a/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AccessControlTab/SelectAllowedPartyTypes/SelectAllowedPartyTypes.tsx b/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AccessControlTab/SelectAllowedPartyTypes/SelectAllowedPartyTypes.tsx
index f5ae291a9cd..9fbcd4f212e 100644
--- a/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AccessControlTab/SelectAllowedPartyTypes/SelectAllowedPartyTypes.tsx
+++ b/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AccessControlTab/SelectAllowedPartyTypes/SelectAllowedPartyTypes.tsx
@@ -6,19 +6,16 @@ import { useTranslation } from 'react-i18next';
import { getPartyTypesAllowedOptions } from '../../../../utils/tabUtils/accessControlTabUtils';
import { useAppMetadataMutation } from 'app-development/hooks/mutations';
import { AccessControlWarningModal } from '../AccessControWarningModal';
+import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams';
export interface SelectAllowedPartyTypesProps {
- org: string;
- app: string;
appMetadata: ApplicationMetadata;
}
-export const SelectAllowedPartyTypes = ({
- org,
- app,
- appMetadata,
-}: SelectAllowedPartyTypesProps) => {
+export const SelectAllowedPartyTypes = ({ appMetadata }: SelectAllowedPartyTypesProps) => {
const { t } = useTranslation();
+ const { org, app } = useStudioEnvironmentParams();
+
const modalRef = useRef(null);
const partyTypesAllowed = appMetadata.partyTypesAllowed;
diff --git a/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/PolicyTab/PolicyTab.test.tsx b/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/PolicyTab/PolicyTab.test.tsx
index bd2cdeb6a57..dfd02963859 100644
--- a/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/PolicyTab/PolicyTab.test.tsx
+++ b/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/PolicyTab/PolicyTab.test.tsx
@@ -1,6 +1,5 @@
import React from 'react';
import { render as rtlRender, screen, waitForElementToBeRemoved } from '@testing-library/react';
-import type { PolicyTabProps } from './PolicyTab';
import { PolicyTab } from './PolicyTab';
import { textMock } from '@studio/testing/mocks/i18nMock';
import { createQueryClientMock } from 'app-shared/mocks/queryClientMock';
@@ -13,6 +12,7 @@ import userEvent from '@testing-library/user-event';
import { useAppPolicyMutation } from 'app-development/hooks/mutations';
import { mockPolicy } from '../../../mocks/policyMock';
import { app, org } from '@studio/testing/testids';
+import { MemoryRouter } from 'react-router-dom';
const mockActions: PolicyAction[] = [
{ actionId: 'a1', actionTitle: 'Action 1', actionDescription: 'The first action' },
@@ -54,10 +54,12 @@ const getAppPolicy = jest.fn().mockImplementation(() => Promise.resolve({}));
const getPolicyActions = jest.fn().mockImplementation(() => Promise.resolve({}));
const getPolicySubjects = jest.fn().mockImplementation(() => Promise.resolve({}));
-const defaultProps: PolicyTabProps = {
- org,
- app,
-};
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useParams: () => {
+ return { org, app };
+ },
+}));
describe('PolicyTab', () => {
afterEach(jest.clearAllMocks);
@@ -151,8 +153,10 @@ const render = (
};
return rtlRender(
-
-
- ,
+
+
+
+
+ ,
);
};
diff --git a/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/PolicyTab/PolicyTab.tsx b/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/PolicyTab/PolicyTab.tsx
index f35d8f74419..b89e9e6c2bd 100644
--- a/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/PolicyTab/PolicyTab.tsx
+++ b/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/PolicyTab/PolicyTab.tsx
@@ -18,23 +18,11 @@ import {
} from 'app-shared/hooks/queries';
import { mergeQueryStatuses } from 'app-shared/utils/tanstackQueryUtils';
import { TabContent } from '../../TabContent';
+import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams';
-export type PolicyTabProps = {
- org: string;
- app: string;
-};
-
-/**
- * @component
- * Displays the tab rendering the polciy for an app
- *
- * @property {string}[org] - The org
- * @property {string}[app] - The app
- *
- * @returns {ReactNode} - The rendered component
- */
-export const PolicyTab = ({ org, app }: PolicyTabProps): ReactNode => {
+export const PolicyTab = (): ReactNode => {
const { t } = useTranslation();
+ const { org, app } = useStudioEnvironmentParams();
const {
status: policyStatus,
diff --git a/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/SetupTab/SetupTab.test.tsx b/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/SetupTab/SetupTab.test.tsx
index 783dad92abe..f82bfec3ec0 100644
--- a/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/SetupTab/SetupTab.test.tsx
+++ b/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/SetupTab/SetupTab.test.tsx
@@ -1,6 +1,5 @@
import React from 'react';
import { render as rtlRender, screen, waitForElementToBeRemoved } from '@testing-library/react';
-import type { SetupTabProps } from './SetupTab';
import { SetupTab } from './SetupTab';
import { textMock } from '@studio/testing/mocks/i18nMock';
import type { ServicesContextProps } from 'app-shared/contexts/ServicesContext';
@@ -10,16 +9,16 @@ import { createQueryClientMock } from 'app-shared/mocks/queryClientMock';
import { MemoryRouter } from 'react-router-dom';
import { queriesMock } from 'app-shared/mocks/queriesMock';
import { mockAppMetadata } from '../../../mocks/applicationMetadataMock';
-
-const mockOrg: string = 'testOrg';
-const mockApp: string = 'testApp';
+import { app, org } from '@studio/testing/testids';
const getAppMetadata = jest.fn().mockImplementation(() => Promise.resolve({}));
-const defaultProps: SetupTabProps = {
- org: mockOrg,
- app: mockApp,
-};
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useParams: () => {
+ return { org, app };
+ },
+}));
describe('SetupTab Component', () => {
afterEach(() => {
@@ -70,7 +69,6 @@ describe('SetupTab Component', () => {
const render = (
queries: Partial = {},
- props: Partial = {},
queryClient: QueryClient = createQueryClientMock(),
) => {
const allQueries: ServicesContextProps = {
@@ -81,7 +79,7 @@ const render = (
return rtlRender(
-
+
,
);
diff --git a/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/SetupTab/SetupTab.tsx b/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/SetupTab/SetupTab.tsx
index 9cba9c0c835..37d97161988 100644
--- a/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/SetupTab/SetupTab.tsx
+++ b/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/SetupTab/SetupTab.tsx
@@ -8,23 +8,11 @@ import { ErrorMessage } from '@digdir/design-system-react';
import { TabHeader } from '../../TabHeader';
import { SetupTabContent } from './SetupTabContent';
import { TabContent } from '../../TabContent';
+import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams';
-export type SetupTabProps = {
- org: string;
- app: string;
-};
-
-/**
- * @component
- * Displays the tab rendering the setup tab for an app
- *
- * @property {string}[org] - The org
- * @property {string}[app] - The app
- *
- * @returns {ReactNode} - The rendered component
- */
-export const SetupTab = ({ org, app }: SetupTabProps): ReactNode => {
+export const SetupTab = (): ReactNode => {
const { t } = useTranslation();
+ const { org, app } = useStudioEnvironmentParams();
const {
status: appMetadataStatus,
@@ -43,7 +31,7 @@ export const SetupTab = ({ org, app }: SetupTabProps): ReactNode => {
);
case 'success':
- return ;
+ return ;
}
};
diff --git a/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/SetupTab/SetupTabContent/SetupTabContent.test.tsx b/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/SetupTab/SetupTabContent/SetupTabContent.test.tsx
index d7844652a84..9463818c60b 100644
--- a/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/SetupTab/SetupTabContent/SetupTabContent.test.tsx
+++ b/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/SetupTab/SetupTabContent/SetupTabContent.test.tsx
@@ -12,9 +12,14 @@ import { queriesMock } from 'app-shared/mocks/queriesMock';
import type { ApplicationMetadata } from 'app-shared/types/ApplicationMetadata';
import { mockAppMetadata } from '../../../../mocks/applicationMetadataMock';
import userEvent from '@testing-library/user-event';
+import { app, org } from '@studio/testing/testids';
-const mockOrg: string = 'testOrg';
-const mockApp: string = 'testApp';
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useParams: () => {
+ return { org, app };
+ },
+}));
jest.mock('../../../../../../../hooks/mutations/useAppMetadataMutation');
const updateAppMetadataMutation = jest.fn();
@@ -27,8 +32,6 @@ mockUpdateAppMetadataMutation.mockReturnValue({
const defaultProps: SetupTabContentProps = {
appMetadata: mockAppMetadata,
- org: mockOrg,
- app: mockApp,
};
describe('SetupTabContent', () => {
diff --git a/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/SetupTab/SetupTabContent/SetupTabContent.tsx b/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/SetupTab/SetupTabContent/SetupTabContent.tsx
index 8733a211792..60580dad971 100644
--- a/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/SetupTab/SetupTabContent/SetupTabContent.tsx
+++ b/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/SetupTab/SetupTabContent/SetupTabContent.tsx
@@ -5,25 +5,15 @@ import type { ApplicationMetadata } from 'app-shared/types/ApplicationMetadata';
import { useTranslation } from 'react-i18next';
import { useAppMetadataMutation } from 'app-development/hooks/mutations';
import { Paragraph, Switch } from '@digdir/design-system-react';
+import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams';
export type SetupTabContentProps = {
appMetadata: ApplicationMetadata;
- org: string;
- app: string;
};
-/**
- * @component
- * The content of the Setup Tab
- *
- * @property {ApplicationMetadata}[appMetadata] - The application metadata
- * @property {string}[org] - The org
- * @property {string}[app] - The app
- *
- * @returns {ReactNode} - The rendered component
- */
-export const SetupTabContent = ({ appMetadata, org, app }: SetupTabContentProps): ReactNode => {
+export const SetupTabContent = ({ appMetadata }: SetupTabContentProps): ReactNode => {
const { t } = useTranslation();
+ const { org, app } = useStudioEnvironmentParams();
const { mutate: updateAppMetadataMutation } = useAppMetadataMutation(org, app);
diff --git a/frontend/app-development/layout/SettingsModalButton/SettingsModalButton.test.tsx b/frontend/app-development/layout/SettingsModalButton/SettingsModalButton.test.tsx
index 44718ad73a4..08cc26da518 100644
--- a/frontend/app-development/layout/SettingsModalButton/SettingsModalButton.test.tsx
+++ b/frontend/app-development/layout/SettingsModalButton/SettingsModalButton.test.tsx
@@ -1,7 +1,6 @@
import React from 'react';
import { render as rtlRender, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
-import type { SettingsModalButtonProps } from './SettingsModalButton';
import { SettingsModalButton } from './SettingsModalButton';
import { textMock } from '@studio/testing/mocks/i18nMock';
import type { QueryClient } from '@tanstack/react-query';
@@ -11,12 +10,6 @@ import { createQueryClientMock } from 'app-shared/mocks/queryClientMock';
import { queriesMock } from 'app-shared/mocks/queriesMock';
import { MemoryRouter } from 'react-router-dom';
import { AppDevelopmentContextProvider } from '../../contexts/AppDevelopmentContext';
-import { app, org } from '@studio/testing/testids';
-
-const defaultProps: SettingsModalButtonProps = {
- org,
- app,
-};
describe('SettingsModal', () => {
const user = userEvent.setup();
@@ -88,7 +81,7 @@ const renderSettingsModalButton = (
-
+
,
diff --git a/frontend/app-development/layout/SettingsModalButton/SettingsModalButton.tsx b/frontend/app-development/layout/SettingsModalButton/SettingsModalButton.tsx
index 7e31b46a7e8..21d1d1579bb 100644
--- a/frontend/app-development/layout/SettingsModalButton/SettingsModalButton.tsx
+++ b/frontend/app-development/layout/SettingsModalButton/SettingsModalButton.tsx
@@ -6,16 +6,7 @@ import { useTranslation } from 'react-i18next';
import { SettingsModal } from './SettingsModal';
import { useSettingsModalContext } from '../../contexts/SettingsModalContext';
-export type SettingsModalButtonProps = {
- org: string;
- app: string;
-};
-
-/**
- * @component
- * Displays a button to open the Settings modal
- */
-export const SettingsModalButton = ({ org, app }: SettingsModalButtonProps): ReactNode => {
+export const SettingsModalButton = (): ReactNode => {
const { t } = useTranslation();
const { settingsModalOpen, setSettingsModalOpen, settingsModalSelectedTab } =
useSettingsModalContext();
@@ -37,8 +28,6 @@ export const SettingsModalButton = ({ org, app }: SettingsModalButtonProps): Rea
setSettingsModalOpen(false)}
- org={org}
- app={app}
defaultTab={settingsModalSelectedTab}
/>
)
From a5d9253df8d8736161aad9068ab57d02d8503310 Mon Sep 17 00:00:00 2001
From: andreastanderen <71079896+standeren@users.noreply.github.com>
Date: Fri, 31 May 2024 13:37:34 +0200
Subject: [PATCH 08/27] Only tryChange appMetadata if connected task from
dataTypeChange is NOT customReceipt (#12890)
---
backend/src/Designer/Constants/General.cs | 5 +++
.../Designer/Controllers/PreviewController.cs | 5 +++
...taTypeChangedApplicationMetadataHandler.cs | 2 +-
.../Services/Implementation/PreviewService.cs | 7 ++--
.../GetAppMetadataModelIdsTests.cs | 2 +-
.../PreviewController/InstancesTests.cs | 4 +--
.../PreviewControllerTestsBase.cs | 4 +--
...pplicationMetadataFileSyncDataTypeTests.cs | 31 ++++++++++++++++
.../LayoutSetsFileSyncDataTypeTests.cs | 35 +++++++++++++++++--
.../App/config/applicationmetadata.json | 13 -------
testdata/App/ui/layout-sets.json | 7 ++++
11 files changed, 90 insertions(+), 25 deletions(-)
diff --git a/backend/src/Designer/Constants/General.cs b/backend/src/Designer/Constants/General.cs
index dc81d71f1b6..7dc07278bc2 100644
--- a/backend/src/Designer/Constants/General.cs
+++ b/backend/src/Designer/Constants/General.cs
@@ -14,5 +14,10 @@ public static class General
/// The name of the cookie used for asp authentication in designer application
///
public const string DesignerCookieName = "AltinnStudioDesigner";
+
+ ///
+ /// The identifying name for a custom receipt at process end in the application
+ ///
+ public const string CustomReceiptId = "CustomReceipt";
}
}
diff --git a/backend/src/Designer/Controllers/PreviewController.cs b/backend/src/Designer/Controllers/PreviewController.cs
index ce8f93184c4..00f125a1d3a 100644
--- a/backend/src/Designer/Controllers/PreviewController.cs
+++ b/backend/src/Designer/Controllers/PreviewController.cs
@@ -1002,6 +1002,11 @@ private static ApplicationMetadata SetMockDataTypeIfMissing(ApplicationMetadata
LayoutSets layoutSetsWithMockedDataTypesIfMissing = AddDataTypesToReturnedLayoutSetsIfMissing(layoutSets);
layoutSetsWithMockedDataTypesIfMissing.Sets.ForEach(set =>
{
+ if (set.Tasks[0] == Constants.General.CustomReceiptId)
+ {
+ return;
+ }
+
if (!applicationMetadata.DataTypes.Any(dataType => dataType.Id == set.DataType))
{
applicationMetadata.DataTypes.Add(new DataType()
diff --git a/backend/src/Designer/EventHandlers/ProcessDataTypeChanged/ProcessDataTypeChangedApplicationMetadataHandler.cs b/backend/src/Designer/EventHandlers/ProcessDataTypeChanged/ProcessDataTypeChangedApplicationMetadataHandler.cs
index 548140bd5a3..674877a02ab 100644
--- a/backend/src/Designer/EventHandlers/ProcessDataTypeChanged/ProcessDataTypeChangedApplicationMetadataHandler.cs
+++ b/backend/src/Designer/EventHandlers/ProcessDataTypeChanged/ProcessDataTypeChangedApplicationMetadataHandler.cs
@@ -34,7 +34,7 @@ await _fileSyncHandlerExecutor.ExecuteWithExceptionHandling(
var applicationMetadata = await repository.GetApplicationMetadata(cancellationToken);
- if (TryChangeDataType(applicationMetadata, notification.NewDataType, notification.ConnectedTaskId))
+ if (notification.ConnectedTaskId != Constants.General.CustomReceiptId && TryChangeDataType(applicationMetadata, notification.NewDataType, notification.ConnectedTaskId))
{
await repository.SaveApplicationMetadata(applicationMetadata);
}
diff --git a/backend/src/Designer/Services/Implementation/PreviewService.cs b/backend/src/Designer/Services/Implementation/PreviewService.cs
index edf7455a7c6..a9a65a76f5c 100644
--- a/backend/src/Designer/Services/Implementation/PreviewService.cs
+++ b/backend/src/Designer/Services/Implementation/PreviewService.cs
@@ -19,7 +19,6 @@ public class PreviewService : IPreviewService
private readonly IAltinnGitRepositoryFactory _altinnGitRepositoryFactory;
public const string MockDataModelIdPrefix = "MockDataModel";
public const string MockDataTaskId = "test-datatask-id";
- private const string CustomReceiptTaskId = "CustomReceipt";
///
/// Constructor
@@ -37,7 +36,7 @@ public async Task GetMockInstance(string org, string app, string devel
AltinnAppGitRepository altinnAppGitRepository = _altinnGitRepositoryFactory.GetAltinnAppGitRepository(org, app, developer);
Application applicationMetadata = await altinnAppGitRepository.GetApplicationMetadata(cancellationToken);
string task = await GetTaskForLayoutSetName(org, app, developer, layoutSetName, cancellationToken);
- bool shouldProcessActAsReceipt = task == CustomReceiptTaskId;
+ bool shouldProcessActAsReceipt = task == Constants.General.CustomReceiptId;
// RegEx for instance guid in app-frontend: [\da-f]{8}-[\da-f]{4}-[1-5][\da-f]{3}-[89ab][\da-f]{3}-[\da-f]{12}
string instanceGuid = "f1e23d45-6789-1bcd-8c34-56789abcdef0";
ProcessState processState = shouldProcessActAsReceipt
@@ -97,7 +96,7 @@ public async Task> GetTasksForAllLayoutSets(string org, string app,
{
foreach (LayoutSetConfig layoutSet in layoutSets.Sets.Where(ls => !tasks.Contains(ls.Tasks[0])))
{
- if (layoutSet.Tasks[0] == CustomReceiptTaskId)
+ if (layoutSet.Tasks[0] == Constants.General.CustomReceiptId)
{
continue;
}
@@ -158,7 +157,7 @@ private async Task> GetDataTypesForInstance(string org, string
private async Task GetDataTypeForCustomReceipt(AltinnAppGitRepository altinnAppGitRepository)
{
LayoutSets layoutSets = await altinnAppGitRepository.GetLayoutSetsFile();
- string dataType = layoutSets?.Sets?.Find(set => set.Tasks[0] == CustomReceiptTaskId)?.DataType;
+ string dataType = layoutSets?.Sets?.Find(set => set.Tasks[0] == Constants.General.CustomReceiptId)?.DataType;
return dataType ?? string.Empty;
}
}
diff --git a/backend/tests/Designer.Tests/Controllers/AppDevelopmentController/GetAppMetadataModelIdsTests.cs b/backend/tests/Designer.Tests/Controllers/AppDevelopmentController/GetAppMetadataModelIdsTests.cs
index 737826b6260..309ada38f73 100644
--- a/backend/tests/Designer.Tests/Controllers/AppDevelopmentController/GetAppMetadataModelIdsTests.cs
+++ b/backend/tests/Designer.Tests/Controllers/AppDevelopmentController/GetAppMetadataModelIdsTests.cs
@@ -34,7 +34,7 @@ public async Task GetAppMetadataModelIds_Should_Return_ModelIdsList(string org,
string responseContent = await response.Content.ReadAsStringAsync();
- responseContent.Should().Be("[\"datamodel\",\"unUsedDatamodel\",\"HvemErHvem_M\",\"receiptDataType\"]");
+ responseContent.Should().Be("[\"datamodel\",\"unUsedDatamodel\",\"HvemErHvem_M\"]");
}
[Theory]
diff --git a/backend/tests/Designer.Tests/Controllers/PreviewController/InstancesTests.cs b/backend/tests/Designer.Tests/Controllers/PreviewController/InstancesTests.cs
index 5aa3d43b056..bbd1977ab98 100644
--- a/backend/tests/Designer.Tests/Controllers/PreviewController/InstancesTests.cs
+++ b/backend/tests/Designer.Tests/Controllers/PreviewController/InstancesTests.cs
@@ -67,7 +67,7 @@ public async Task Post_InstanceForCustomReceipt_Ok()
string dataPathWithData = $"{Org}/{targetRepository}/instances?instanceOwnerPartyId=51001";
using HttpRequestMessage httpRequestMessage = new(HttpMethod.Post, dataPathWithData);
- httpRequestMessage.Headers.Referrer = new Uri($"{MockedReferrerUrl}?org={Org}&app={AppV4}&selectedLayoutSet={CustomReceiptLayoutSetName2}");
+ httpRequestMessage.Headers.Referrer = new Uri($"{MockedReferrerUrl}?org={Org}&app={AppV4}&selectedLayoutSet={CustomReceiptLayoutSetName}");
using HttpResponseMessage response = await HttpClient.SendAsync(httpRequestMessage);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
@@ -75,7 +75,7 @@ public async Task Post_InstanceForCustomReceipt_Ok()
string responseBody = await response.Content.ReadAsStringAsync();
JsonDocument responseDocument = JsonDocument.Parse(responseBody);
Instance instance = JsonConvert.DeserializeObject(responseDocument.RootElement.ToString());
- Assert.Equal(DataTypeForCustomReceipt, instance.Data[0].DataType);
+ Assert.Equal(CustomReceiptDataType, instance.Data[0].DataType);
Assert.Equal("EndEvent_1", instance.Process.EndEvent);
instance.Process.CurrentTask.Should().BeNull();
}
diff --git a/backend/tests/Designer.Tests/Controllers/PreviewController/PreviewControllerTestsBase.cs b/backend/tests/Designer.Tests/Controllers/PreviewController/PreviewControllerTestsBase.cs
index 6a8d46f4f96..5723488a0e7 100644
--- a/backend/tests/Designer.Tests/Controllers/PreviewController/PreviewControllerTestsBase.cs
+++ b/backend/tests/Designer.Tests/Controllers/PreviewController/PreviewControllerTestsBase.cs
@@ -15,8 +15,8 @@ public class PreviewControllerTestsBase : DisagnerEndpointsTestsBase
protected const string Developer = "testUser";
protected const string LayoutSetName = "layoutSet1";
protected const string LayoutSetName2 = "layoutSet2";
- protected const string CustomReceiptLayoutSetName2 = "receipt";
- protected const string DataTypeForCustomReceipt = "receiptDataType";
+ protected const string CustomReceiptLayoutSetName = "receipt";
+ protected const string CustomReceiptDataType = "receiptDataType";
protected const string PartyId = "51001";
protected const string InstanceGuId = "f1e23d45-6789-1bcd-8c34-56789abcdef0";
protected const string AttachmentGuId = "f47ac10b-58cc-4372-a567-0e02b2c3d479";
diff --git a/backend/tests/Designer.Tests/Controllers/ProcessModelingController/FileSync/DataTypeChangeTests/ApplicationMetadataFileSyncDataTypeTests.cs b/backend/tests/Designer.Tests/Controllers/ProcessModelingController/FileSync/DataTypeChangeTests/ApplicationMetadataFileSyncDataTypeTests.cs
index eacd2da767f..4e9d5a2c53c 100644
--- a/backend/tests/Designer.Tests/Controllers/ProcessModelingController/FileSync/DataTypeChangeTests/ApplicationMetadataFileSyncDataTypeTests.cs
+++ b/backend/tests/Designer.Tests/Controllers/ProcessModelingController/FileSync/DataTypeChangeTests/ApplicationMetadataFileSyncDataTypeTests.cs
@@ -82,6 +82,37 @@ public async Task ProcessDataTypeChangedNotify_NewDataTypeForTask5IsMessage_Shou
applicationMetadata.DataTypes.Find(type => type.Id == dataTypeToConnect).TaskId.Should().Be(task); // Data type 'message' is now connected to Task_5
}
+ [Theory]
+ [MemberData(nameof(ProcessDataTypeChangedNotifyTestData))]
+ public async Task ProcessDataTypeChangedNotify_NewDataTypeForCustomReceipt_ShouldNotSyncApplicationMetadata(
+ string org, string app, string developer, string applicationMetadataPath, DataTypeChange dataTypeChange)
+ {
+ string targetRepository = TestDataHelper.GenerateTestRepoName();
+ await CopyRepositoryForTest(org, app, developer, targetRepository);
+ await AddFileToRepo(applicationMetadataPath, "App/config/applicationmetadata.json");
+ string dataTypeToConnect = "message";
+ string task = "CustomReceipt";
+ dataTypeChange.NewDataType = dataTypeToConnect;
+ dataTypeChange.ConnectedTaskId = task;
+
+ string url = VersionPrefix(org, targetRepository);
+
+ string dataTypeChangeString = JsonSerializer.Serialize(dataTypeChange,
+ new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
+ using var httpRequestMessage = new HttpRequestMessage(HttpMethod.Put, url)
+ {
+ Content = new StringContent(dataTypeChangeString, Encoding.UTF8, "application/json")
+ };
+ using var response = await HttpClient.SendAsync(httpRequestMessage);
+ response.StatusCode.Should().Be(HttpStatusCode.Accepted);
+
+ string applicationMetadataFromRepo = TestDataHelper.GetFileFromRepo(org, targetRepository, developer, "App/config/applicationmetadata.json");
+
+ ApplicationMetadata applicationMetadata = JsonSerializer.Deserialize(applicationMetadataFromRepo, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
+
+ applicationMetadata.DataTypes.Find(type => type.Id == dataTypeToConnect).TaskId.Should().NotBe(task); // CustomReceipt has not been added to the dataType
+ }
+
public static IEnumerable
/// An .
/// Id for the added data type
+ /// Id for the task that the data type is connected to
/// A that observes if operation is cancelled.
Task AddDataTypeToApplicationMetadataAsync(AltinnRepoEditingContext altinnRepoEditingContext,
- string dataTypeId, CancellationToken cancellationToken = default);
+ string dataTypeId, string taskId, CancellationToken cancellationToken = default);
///
/// Deletes a simple dataType from applicationMetadata.
diff --git a/backend/tests/Designer.Tests/Controllers/ProcessModelingController/AddDataTypeToApplicationMetadataTests.cs b/backend/tests/Designer.Tests/Controllers/ProcessModelingController/AddDataTypeToApplicationMetadataTests.cs
index 508c72059e0..b02d946942b 100644
--- a/backend/tests/Designer.Tests/Controllers/ProcessModelingController/AddDataTypeToApplicationMetadataTests.cs
+++ b/backend/tests/Designer.Tests/Controllers/ProcessModelingController/AddDataTypeToApplicationMetadataTests.cs
@@ -14,19 +14,19 @@ namespace Designer.Tests.Controllers.ProcessModelingController
{
public class AddDataTypeToApplicationMetadataTests : DisagnerEndpointsTestsBase, IClassFixture>
{
- private static string VersionPrefix(string org, string repository, string dataTypeId) => $"/designer/api/{org}/{repository}/process-modelling/data-type/{dataTypeId}";
+ private static string VersionPrefix(string org, string repository, string dataTypeId, string taskId) => $"/designer/api/{org}/{repository}/process-modelling/data-type/{dataTypeId}?taskId={taskId}";
public AddDataTypeToApplicationMetadataTests(WebApplicationFactory factory) : base(factory)
{
}
[Theory]
- [InlineData("ttd", "empty-app", "testUser", "paymentInformation-1234")]
- public async Task AddDataTypeToApplicationMetadata_ShouldAddDataTypeAndReturnOK(string org, string app, string developer, string dataTypeId)
+ [InlineData("ttd", "empty-app", "testUser", "paymentInformation-1234", "task_1")]
+ public async Task AddDataTypeToApplicationMetadata_ShouldAddDataTypeAndReturnOK(string org, string app, string developer, string dataTypeId, string taskId)
{
string targetRepository = TestDataHelper.GenerateTestRepoName();
await CopyRepositoryForTest(org, app, developer, targetRepository);
- string url = VersionPrefix(org, targetRepository, dataTypeId);
+ string url = VersionPrefix(org, targetRepository, dataTypeId, taskId);
using var request = new HttpRequestMessage(HttpMethod.Post, url);
using var response = await HttpClient.SendAsync(request);
@@ -43,6 +43,7 @@ public async Task AddDataTypeToApplicationMetadata_ShouldAddDataTypeAndReturnOK(
AllowedContentTypes = new List() { "application/json" },
MaxCount = 1,
MinCount = 0,
+ TaskId = taskId,
EnablePdfCreation = false,
EnableFileScan = false,
ValidationErrorOnPendingFileScan = false,
@@ -52,15 +53,16 @@ public async Task AddDataTypeToApplicationMetadata_ShouldAddDataTypeAndReturnOK(
appMetadata.DataTypes.Count.Should().Be(2);
appMetadata.DataTypes.Find(dataType => dataType.Id == dataTypeId).Should().BeEquivalentTo(expectedDataType);
+ appMetadata.DataTypes.Find(dataType => dataType.Id == dataTypeId).TaskId.Should().Be(taskId);
}
[Theory]
- [InlineData("ttd", "empty-app", "testUser", "ref-data-as-pdf")]
- public async Task AddDataTypeToApplicationMetadataWhenExists_ShouldNotAddDataTypeAndReturnOK(string org, string app, string developer, string dataTypeId)
+ [InlineData("ttd", "empty-app", "testUser", "ref-data-as-pdf", "task_1")]
+ public async Task AddDataTypeToApplicationMetadataWhenExists_ShouldNotAddDataTypeAndReturnOK(string org, string app, string developer, string dataTypeId, string taskId)
{
string targetRepository = TestDataHelper.GenerateTestRepoName();
await CopyRepositoryForTest(org, app, developer, targetRepository);
- string url = VersionPrefix(org, targetRepository, dataTypeId);
+ string url = VersionPrefix(org, targetRepository, dataTypeId, taskId);
using var request = new HttpRequestMessage(HttpMethod.Post, url);
using var response = await HttpClient.SendAsync(request);
diff --git a/frontend/app-development/hooks/mutations/useAddDataTypeToAppMetadata.test.ts b/frontend/app-development/hooks/mutations/useAddDataTypeToAppMetadata.test.ts
index 04ef3607723..e6f2cf4b125 100644
--- a/frontend/app-development/hooks/mutations/useAddDataTypeToAppMetadata.test.ts
+++ b/frontend/app-development/hooks/mutations/useAddDataTypeToAppMetadata.test.ts
@@ -5,6 +5,7 @@ import { useAddDataTypeToAppMetadata } from './useAddDataTypeToAppMetadata';
import { app, org } from '@studio/testing/testids';
const dataTypeId = 'paymentInformation-1234';
+const taskId = 'task_1';
describe('useAddDataTypeToAppMetadata', () => {
it('Calls addDataTypeToAppMetadata with correct arguments and payload', async () => {
@@ -13,10 +14,11 @@ describe('useAddDataTypeToAppMetadata', () => {
).renderHookResult.result;
await addDataTypeToAppMetadata.current.mutateAsync({
dataTypeId,
+ taskId,
});
await waitFor(() => expect(addDataTypeToAppMetadata.current.isSuccess).toBe(true));
expect(queriesMock.addDataTypeToAppMetadata).toHaveBeenCalledTimes(1);
- expect(queriesMock.addDataTypeToAppMetadata).toHaveBeenCalledWith(org, app, dataTypeId);
+ expect(queriesMock.addDataTypeToAppMetadata).toHaveBeenCalledWith(org, app, dataTypeId, taskId);
});
});
diff --git a/frontend/app-development/hooks/mutations/useAddDataTypeToAppMetadata.ts b/frontend/app-development/hooks/mutations/useAddDataTypeToAppMetadata.ts
index c04e3253416..15ffabc8011 100644
--- a/frontend/app-development/hooks/mutations/useAddDataTypeToAppMetadata.ts
+++ b/frontend/app-development/hooks/mutations/useAddDataTypeToAppMetadata.ts
@@ -3,13 +3,14 @@ import { useServicesContext } from 'app-shared/contexts/ServicesContext';
type AddDataTypeToAppMetadataMutation = {
dataTypeId: string;
+ taskId: string;
};
export const useAddDataTypeToAppMetadata = (org: string, app: string) => {
const { addDataTypeToAppMetadata } = useServicesContext();
return useMutation({
- mutationFn: ({ dataTypeId }: AddDataTypeToAppMetadataMutation) =>
- addDataTypeToAppMetadata(org, app, dataTypeId),
+ mutationFn: ({ dataTypeId, taskId }: AddDataTypeToAppMetadataMutation) =>
+ addDataTypeToAppMetadata(org, app, dataTypeId, taskId),
});
};
diff --git a/frontend/packages/process-editor/src/contexts/BpmnApiContext.tsx b/frontend/packages/process-editor/src/contexts/BpmnApiContext.tsx
index 2bd670a1c4f..ffb27aa892d 100644
--- a/frontend/packages/process-editor/src/contexts/BpmnApiContext.tsx
+++ b/frontend/packages/process-editor/src/contexts/BpmnApiContext.tsx
@@ -20,7 +20,7 @@ export type BpmnApiContextProps = {
deleteLayoutSet: (data: { layoutSetIdToUpdate: string }) => void;
mutateLayoutSetId: (data: { layoutSetIdToUpdate: string; newLayoutSetId: string }) => void;
mutateDataType: (dataTypeChange: DataTypeChange, options?: QueryOptions) => void;
- addDataTypeToAppMetadata: (data: { dataTypeId: string }) => void;
+ addDataTypeToAppMetadata: (data: { dataTypeId: string; taskId: string }) => void;
deleteDataTypeFromAppMetadata: (data: { dataTypeId: string }) => void;
saveBpmn: (bpmnXml: string, metaData?: MetaDataForm) => void;
diff --git a/frontend/packages/process-editor/src/utils/ProcessTaskManager/AddProcessTaskManager.test.ts b/frontend/packages/process-editor/src/utils/ProcessTaskManager/AddProcessTaskManager.test.ts
index bea3fb59bd3..33abd80c6cb 100644
--- a/frontend/packages/process-editor/src/utils/ProcessTaskManager/AddProcessTaskManager.test.ts
+++ b/frontend/packages/process-editor/src/utils/ProcessTaskManager/AddProcessTaskManager.test.ts
@@ -69,6 +69,7 @@ describe('AddProcessTaskManager', () => {
expect(addDataTypeToAppMetadata).toHaveBeenCalledWith({
dataTypeId: 'paymentInformation',
+ taskId: 'testId',
});
});
@@ -85,6 +86,7 @@ describe('AddProcessTaskManager', () => {
expect(addDataTypeToAppMetadata).toHaveBeenCalledWith({
dataTypeId: 'signingInformation',
+ taskId: 'testId',
});
});
diff --git a/frontend/packages/process-editor/src/utils/ProcessTaskManager/AddProcessTaskManager.ts b/frontend/packages/process-editor/src/utils/ProcessTaskManager/AddProcessTaskManager.ts
index 6dc899b4956..2aeac737cf3 100644
--- a/frontend/packages/process-editor/src/utils/ProcessTaskManager/AddProcessTaskManager.ts
+++ b/frontend/packages/process-editor/src/utils/ProcessTaskManager/AddProcessTaskManager.ts
@@ -61,6 +61,7 @@ export class AddProcessTaskManager {
this.addDataTypeToAppMetadata({
dataTypeId,
+ taskId: this.bpmnDetails.id,
});
}
@@ -77,6 +78,7 @@ export class AddProcessTaskManager {
this.addDataTypeToAppMetadata({
dataTypeId,
+ taskId: this.bpmnDetails.id,
});
}
diff --git a/frontend/packages/shared/src/api/mutations.ts b/frontend/packages/shared/src/api/mutations.ts
index 8bdbe0f8900..5ec6e7e9cca 100644
--- a/frontend/packages/shared/src/api/mutations.ts
+++ b/frontend/packages/shared/src/api/mutations.ts
@@ -127,7 +127,7 @@ export const updateResource = (org: string, repo: string, payload: Resource) =>
// ProcessEditor
-export const addDataTypeToAppMetadata = (org: string, app: string, dataTypeId: string) => post(processEditorDataTypePath(org, app, dataTypeId));
+export const addDataTypeToAppMetadata = (org: string, app: string, dataTypeId: string, taskId: string) => post(processEditorDataTypePath(org, app, dataTypeId, taskId));
export const deleteDataTypeFromAppMetadata = (org: string, app: string, dataTypeId: string) => del(processEditorDataTypePath(org, app, dataTypeId));
export const updateBpmnXml = (org: string, app: string, form: any) =>
diff --git a/frontend/packages/shared/src/api/paths.js b/frontend/packages/shared/src/api/paths.js
index 9d0744538d8..d319f19dea4 100644
--- a/frontend/packages/shared/src/api/paths.js
+++ b/frontend/packages/shared/src/api/paths.js
@@ -151,4 +151,4 @@ export const processEditorPath = (org, app) => `${basePath}/${org}/${app}/proces
export const processEditorWebSocketHub = () => '/sync-hub';
export const processEditorPathPut = (org, app) => `${basePath}/${org}/${app}/process-modelling/process-definition-latest`;
export const processEditorDataTypeChangePath = (org, app) => `${basePath}/${org}/${app}/process-modelling/data-type`;
-export const processEditorDataTypePath = (org, app, dataTypeId) => `${basePath}/${org}/${app}/process-modelling/data-type/${dataTypeId}`;
+export const processEditorDataTypePath = (org, app, dataTypeId, taskId) => `${basePath}/${org}/${app}/process-modelling/data-type/${dataTypeId}?${s({ taskId })}`;
From 02ee823f4dbacdf47862eefd4bfd949ea4714dcf Mon Sep 17 00:00:00 2001
From: JamalAlabdullah <90609090+JamalAlabdullah@users.noreply.github.com>
Date: Mon, 3 Jun 2024 08:49:14 +0200
Subject: [PATCH 10/27] Bug/12043 uploading a data model with invalid name
shows wrong error messages (#12886)
* Added validation to prevent upload invalid datamodel nem
---
frontend/language/src/nb.json | 1 +
.../src/components/FileSelector.test.tsx | 18 ++++++++++++++++++
.../shared/src/components/FileSelector.tsx | 7 +++++++
3 files changed, 26 insertions(+)
diff --git a/frontend/language/src/nb.json b/frontend/language/src/nb.json
index c70d3d267ac..fad4bb447f2 100644
--- a/frontend/language/src/nb.json
+++ b/frontend/language/src/nb.json
@@ -49,6 +49,7 @@
"app_data_modelling.properties_information": "Ingen egenskaper vises for øyeblikket fordi ingen elementer er valgt. Velg et element for å konfigurere og vise egenskapene til det valgte elementet.",
"app_data_modelling.select_xsd": "Velg en XSD",
"app_data_modelling.upload_xsd": "Last opp",
+ "app_data_modelling.upload_xsd_invalid_error": "Datamodellnavnet er ugyldig, Navnet kan ha store og små bokstaver fra det norske alfabetet, i tillegg til tall, understrek, punktum og bindestrek.",
"app_data_modelling.uploading_xsd": "Laster opp XSD...",
"app_deployment.btn_deploy_new_version": "Publiser ny versjon",
"app_deployment.choose_version": "Velg versjon som skal publiseres",
diff --git a/frontend/packages/shared/src/components/FileSelector.test.tsx b/frontend/packages/shared/src/components/FileSelector.test.tsx
index e0ab46cc301..d53bbe83668 100644
--- a/frontend/packages/shared/src/components/FileSelector.test.tsx
+++ b/frontend/packages/shared/src/components/FileSelector.test.tsx
@@ -6,6 +6,13 @@ import { FileSelector } from './FileSelector';
import { textMock } from '@studio/testing/mocks/i18nMock';
import { Button } from '@digdir/design-system-react';
import { fileSelectorInputId } from '@studio/testing/testids';
+import { toast } from 'react-toastify';
+
+jest.mock('react-toastify', () => ({
+ toast: {
+ error: jest.fn(),
+ },
+}));
const user = userEvent.setup();
@@ -76,4 +83,15 @@ describe('FileSelector', () => {
await user.click(button);
expect(fileInput.onclick).toHaveBeenCalled();
});
+
+ it('Should show a toast error when an invalid file name is uploaded', async () => {
+ const invalidFileName = '123_invalid_name"%#$&';
+ const file = new File(['datamodell'], invalidFileName);
+ renderFileSelector();
+ const fileInput = screen.getByTestId(fileSelectorInputId);
+ await user.upload(fileInput, file);
+ expect(toast.error).toHaveBeenCalledWith(
+ textMock('app_data_modelling.upload_xsd_invalid_error'),
+ );
+ });
});
diff --git a/frontend/packages/shared/src/components/FileSelector.tsx b/frontend/packages/shared/src/components/FileSelector.tsx
index 8e6123ccc56..ac88c322836 100644
--- a/frontend/packages/shared/src/components/FileSelector.tsx
+++ b/frontend/packages/shared/src/components/FileSelector.tsx
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
import { StudioButton } from '@studio/components';
import { UploadIcon } from '@studio/icons';
import { fileSelectorInputId } from '@studio/testing/testids';
+import { toast } from 'react-toastify';
export interface IFileSelectorProps {
accept?: string;
@@ -40,6 +41,12 @@ export const FileSelector = ({
const handleSubmit = (event?: React.FormEvent) => {
event?.preventDefault();
const file = fileInput?.current?.files?.item(0);
+ if (!file.name.match(/^[a-zA-Z][a-zA-Z0-9_.\-æÆøØåÅ ]*$/)) {
+ toast.error(t('app_data_modelling.upload_xsd_invalid_error'));
+ fileInput.current.value = '';
+ return;
+ }
+
if (file) {
const formData = new FormData();
formData.append(formFileName, file);
From 231ff431043cca70f86ba9259a689c3d3dff61ea Mon Sep 17 00:00:00 2001
From: andreastanderen <71079896+standeren@users.noreply.github.com>
Date: Mon, 3 Jun 2024 10:08:18 +0200
Subject: [PATCH 11/27] Delete dataType connection in LayoutSets based on
dataTypeId directly when deleting a data model (#12900)
---
.../Implementation/SchemaModelService.cs | 10 +++----
.../DeleteDatamodelTests.cs | 29 ++++++++++++++++++-
.../PreviewControllerTestsBase.cs | 2 +-
.../App/ui/layout-sets.json | 2 +-
4 files changed, 35 insertions(+), 8 deletions(-)
diff --git a/backend/src/Designer/Services/Implementation/SchemaModelService.cs b/backend/src/Designer/Services/Implementation/SchemaModelService.cs
index 959b3cb350b..944408dc8e0 100644
--- a/backend/src/Designer/Services/Implementation/SchemaModelService.cs
+++ b/backend/src/Designer/Services/Implementation/SchemaModelService.cs
@@ -412,18 +412,18 @@ private static async Task DeleteDatatypeFromApplicationMetadataAndLayoutSets(Alt
if (applicationMetadata.DataTypes != null)
{
- DataType removeForm = applicationMetadata.DataTypes.Find(m => m.Id == id);
+ DataType dataTypeToDelete = applicationMetadata.DataTypes.Find(m => m.Id == id);
if (altinnAppGitRepository.AppUsesLayoutSets())
{
var layoutSets = await altinnAppGitRepository.GetLayoutSetsFile();
- var layoutSet = layoutSets.Sets.Find(set => set.Tasks[0] == removeForm.TaskId);
- if (layoutSet is not null)
+ List layoutSetsWithDeletedDataType = layoutSets.Sets.FindAll(set => set.DataType == dataTypeToDelete.Id);
+ foreach (LayoutSetConfig layoutSet in layoutSetsWithDeletedDataType)
{
layoutSet.DataType = null;
- await altinnAppGitRepository.SaveLayoutSets(layoutSets);
}
+ await altinnAppGitRepository.SaveLayoutSets(layoutSets);
}
- applicationMetadata.DataTypes.Remove(removeForm);
+ applicationMetadata.DataTypes.Remove(dataTypeToDelete);
await altinnAppGitRepository.SaveApplicationMetadata(applicationMetadata);
}
}
diff --git a/backend/tests/Designer.Tests/Controllers/DataModelsController/DeleteDatamodelTests.cs b/backend/tests/Designer.Tests/Controllers/DataModelsController/DeleteDatamodelTests.cs
index 229081bf8c5..29f30386db0 100644
--- a/backend/tests/Designer.Tests/Controllers/DataModelsController/DeleteDatamodelTests.cs
+++ b/backend/tests/Designer.Tests/Controllers/DataModelsController/DeleteDatamodelTests.cs
@@ -1,10 +1,13 @@
using System.Net;
using System.Net.Http;
+using System.Text.Json;
using System.Threading.Tasks;
+using Altinn.App.Core.Models;
using Altinn.Studio.Designer.Configuration;
using Altinn.Studio.Designer.Services.Interfaces;
using Designer.Tests.Controllers.ApiTests;
using Designer.Tests.Mocks;
+using Designer.Tests.Utils;
using FluentAssertions;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
@@ -33,7 +36,7 @@ protected override void ConfigureTestServices(IServiceCollection services)
[Theory]
[InlineData("ttd", "ttd-datamodels", "/App/models/41111.schema.json")]
- public async Task Delete_Datamodel_Ok(string org, string repo, string modelPath)
+ public async Task Delete_Datamodel_FromDataModelRepo_Ok(string org, string repo, string modelPath)
{
string dataPathWithData = $"{VersionPrefix(org, repo)}/datamodel?modelPath={modelPath}";
@@ -42,4 +45,28 @@ public async Task Delete_Datamodel_Ok(string org, string repo, string modelPath)
using HttpResponseMessage response = await HttpClient.SendAsync(httpRequestMessage);
response.StatusCode.Should().Be(HttpStatusCode.NoContent);
}
+
+ [Theory]
+ [InlineData("ttd", "app-with-layoutsets", "testUser", "/App/models/datamodel.schema.json")]
+ public async Task Delete_Datamodel_FromAppWhereDataModelHasMultipleLayoutSetConnections_Ok(string org, string repo, string developer, string modelPath)
+ {
+ string targetRepository = TestDataHelper.GenerateTestRepoName();
+ await CopyRepositoryForTest(org, repo, developer, targetRepository);
+ string dataPathWithData = $"{VersionPrefix(org, targetRepository)}/datamodel?modelPath={modelPath}";
+ string applicationMetadataFromRepoBefore = TestDataHelper.GetFileFromRepo(org, targetRepository, developer, "App/config/applicationmetadata.json");
+ string layoutSetsFromRepoBefore = TestDataHelper.GetFileFromRepo(org, targetRepository, developer, "App/ui/layout-sets.json");
+ applicationMetadataFromRepoBefore.Should().Contain("datamodel");
+ LayoutSets layoutSets = JsonSerializer.Deserialize(layoutSetsFromRepoBefore, new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
+ layoutSets.Sets.FindAll(set => set.DataType == "datamodel").Count.Should().Be(2);
+
+ using HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Delete, dataPathWithData);
+
+ using HttpResponseMessage response = await HttpClient.SendAsync(httpRequestMessage);
+ response.StatusCode.Should().Be(HttpStatusCode.NoContent);
+
+ string applicationMetadataFromRepoAfter = TestDataHelper.GetFileFromRepo(org, targetRepository, developer, "App/config/applicationmetadata.json");
+ string layoutSetsFromRepoAfter = TestDataHelper.GetFileFromRepo(org, targetRepository, developer, "App/ui/layout-sets.json");
+ applicationMetadataFromRepoAfter.Should().NotContain("datamodel");
+ layoutSetsFromRepoAfter.Should().NotContain("datamodel");
+ }
}
diff --git a/backend/tests/Designer.Tests/Controllers/PreviewController/PreviewControllerTestsBase.cs b/backend/tests/Designer.Tests/Controllers/PreviewController/PreviewControllerTestsBase.cs
index 5723488a0e7..5d755d7e1cc 100644
--- a/backend/tests/Designer.Tests/Controllers/PreviewController/PreviewControllerTestsBase.cs
+++ b/backend/tests/Designer.Tests/Controllers/PreviewController/PreviewControllerTestsBase.cs
@@ -16,7 +16,7 @@ public class PreviewControllerTestsBase : DisagnerEndpointsTestsBase
protected const string LayoutSetName = "layoutSet1";
protected const string LayoutSetName2 = "layoutSet2";
protected const string CustomReceiptLayoutSetName = "receipt";
- protected const string CustomReceiptDataType = "receiptDataType";
+ protected const string CustomReceiptDataType = "datamodel";
protected const string PartyId = "51001";
protected const string InstanceGuId = "f1e23d45-6789-1bcd-8c34-56789abcdef0";
protected const string AttachmentGuId = "f47ac10b-58cc-4372-a567-0e02b2c3d479";
diff --git a/backend/tests/Designer.Tests/_TestData/Repositories/testUser/ttd/app-with-layoutsets/App/ui/layout-sets.json b/backend/tests/Designer.Tests/_TestData/Repositories/testUser/ttd/app-with-layoutsets/App/ui/layout-sets.json
index af0dd127378..5d4eb2977d6 100644
--- a/backend/tests/Designer.Tests/_TestData/Repositories/testUser/ttd/app-with-layoutsets/App/ui/layout-sets.json
+++ b/backend/tests/Designer.Tests/_TestData/Repositories/testUser/ttd/app-with-layoutsets/App/ui/layout-sets.json
@@ -24,7 +24,7 @@
},
{
"id": "receipt",
- "dataType": "receiptDataType",
+ "dataType": "datamodel",
"tasks": [
"CustomReceipt"
]
From ed6c0ff75e4c74f6fb596f0da517df9220844e11 Mon Sep 17 00:00:00 2001
From: David Ovrelid <46874830+framitdavid@users.noreply.github.com>
Date: Mon, 3 Jun 2024 11:52:23 +0200
Subject: [PATCH 12/27] removed unused file (#12901)
---
.../wwwroot/designer/js/jsoneditor.js | 8405 -----------------
1 file changed, 8405 deletions(-)
delete mode 100644 backend/src/Designer/wwwroot/designer/js/jsoneditor.js
diff --git a/backend/src/Designer/wwwroot/designer/js/jsoneditor.js b/backend/src/Designer/wwwroot/designer/js/jsoneditor.js
deleted file mode 100644
index 08cb963843a..00000000000
--- a/backend/src/Designer/wwwroot/designer/js/jsoneditor.js
+++ /dev/null
@@ -1,8405 +0,0 @@
-/*! JSON Editor v0.7.28 - JSON Schema -> HTML Editor
- * By Jeremy Dorn - https://github.com/jdorn/json-editor/
- * Released under the MIT license
- *
- * Date: 2016-08-07
- */
-
-/**
- * See README.md for requirements and usage info
- */
-
-(function () {
- /*jshint loopfunc: true */
- /* Simple JavaScript Inheritance
- * By John Resig http://ejohn.org/
- * MIT Licensed.
- */
- // Inspired by base2 and Prototype
- var Class;
- (function () {
- var initializing = false,
- fnTest = /xyz/.test(function () {
- window.postMessage('xyz');
- })
- ? /\b_super\b/
- : /.*/;
-
- // The base Class implementation (does nothing)
- Class = function () {};
-
- // Create a new Class that inherits from this class
- Class.extend = function extend(prop) {
- var _super = this.prototype;
-
- // Instantiate a base class (but only create the instance,
- // don't run the init constructor)
- initializing = true;
- var prototype = new this();
- initializing = false;
-
- // Copy the properties over onto the new prototype
- for (var name in prop) {
- // Check if we're overwriting an existing function
- prototype[name] =
- typeof prop[name] == 'function' &&
- typeof _super[name] == 'function' &&
- fnTest.test(prop[name])
- ? (function (name, fn) {
- return function () {
- var tmp = this._super;
-
- // Add a new ._super() method that is the same method
- // but on the super-class
- this._super = _super[name];
-
- // The method only need to be bound temporarily, so we
- // remove it when we're done executing
- var ret = fn.apply(this, arguments);
- this._super = tmp;
-
- return ret;
- };
- })(name, prop[name])
- : prop[name];
- }
-
- // The dummy class constructor
- function Class() {
- // All construction is actually done in the init method
- if (!initializing && this.init) this.init.apply(this, arguments);
- }
-
- // Populate our constructed prototype object
- Class.prototype = prototype;
-
- // Enforce the constructor to be what we expect
- Class.prototype.constructor = Class;
-
- // And make this class extendable
- Class.extend = extend;
-
- return Class;
- };
-
- return Class;
- })();
-
- // CustomEvent constructor polyfill
- // From MDN
- (function () {
- function CustomEvent(event, params) {
- params = params || { bubbles: false, cancelable: false, detail: undefined };
- var evt = document.createEvent('CustomEvent');
- evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
- return evt;
- }
-
- CustomEvent.prototype = window.Event.prototype;
-
- window.CustomEvent = CustomEvent;
- })();
-
- // requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel
- // MIT license
- (function () {
- var lastTime = 0;
- var vendors = ['ms', 'moz', 'webkit', 'o'];
- for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
- window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
- window.cancelAnimationFrame =
- window[vendors[x] + 'CancelAnimationFrame'] ||
- window[vendors[x] + 'CancelRequestAnimationFrame'];
- }
-
- if (!window.requestAnimationFrame)
- window.requestAnimationFrame = function (callback, element) {
- var currTime = new Date().getTime();
- var timeToCall = Math.max(0, 16 - (currTime - lastTime));
- var id = window.setTimeout(function () {
- callback(currTime + timeToCall);
- }, timeToCall);
- lastTime = currTime + timeToCall;
- return id;
- };
-
- if (!window.cancelAnimationFrame)
- window.cancelAnimationFrame = function (id) {
- clearTimeout(id);
- };
- })();
-
- // Array.isArray polyfill
- // From MDN
- (function () {
- if (!Array.isArray) {
- Array.isArray = function (arg) {
- return Object.prototype.toString.call(arg) === '[object Array]';
- };
- }
- })();
- /**
- * Taken from jQuery 2.1.3
- *
- * @param obj
- * @returns {boolean}
- */
- var $isplainobject = function (obj) {
- // Not plain objects:
- // - Any object or value whose internal [[Class]] property is not "[object Object]"
- // - DOM nodes
- // - window
- if (typeof obj !== 'object' || obj.nodeType || (obj !== null && obj === obj.window)) {
- return false;
- }
-
- if (
- obj.constructor &&
- !Object.prototype.hasOwnProperty.call(obj.constructor.prototype, 'isPrototypeOf')
- ) {
- return false;
- }
-
- // If the function hasn't returned already, we're confident that
- // |obj| is a plain object, created by {} or constructed with new Object
- return true;
- };
-
- var $extend = function (destination) {
- var source, i, property;
- for (i = 1; i < arguments.length; i++) {
- source = arguments[i];
- for (property in source) {
- if (!source.hasOwnProperty(property)) continue;
- if (source[property] && $isplainobject(source[property])) {
- if (!destination.hasOwnProperty(property)) destination[property] = {};
- $extend(destination[property], source[property]);
- } else {
- destination[property] = source[property];
- }
- }
- }
- return destination;
- };
-
- var $each = function (obj, callback) {
- if (!obj || typeof obj !== 'object') return;
- var i;
- if (
- Array.isArray(obj) ||
- (typeof obj.length === 'number' && obj.length > 0 && obj.length - 1 in obj)
- ) {
- for (i = 0; i < obj.length; i++) {
- if (callback(i, obj[i]) === false) return;
- }
- } else {
- if (Object.keys) {
- var keys = Object.keys(obj);
- for (i = 0; i < keys.length; i++) {
- if (callback(keys[i], obj[keys[i]]) === false) return;
- }
- } else {
- for (i in obj) {
- if (!obj.hasOwnProperty(i)) continue;
- if (callback(i, obj[i]) === false) return;
- }
- }
- }
- };
-
- var $trigger = function (el, event) {
- var e = document.createEvent('HTMLEvents');
- e.initEvent(event, true, true);
- el.dispatchEvent(e);
- };
- var $triggerc = function (el, event) {
- var e = new CustomEvent(event, {
- bubbles: true,
- cancelable: true,
- });
-
- el.dispatchEvent(e);
- };
-
- var JSONEditor = function (element, options) {
- if (!(element instanceof Element)) {
- throw new Error('element should be an instance of Element');
- }
- options = $extend({}, JSONEditor.defaults.options, options || {});
- this.element = element;
- this.options = options;
- this.init();
- };
- JSONEditor.prototype = {
- // necessary since we remove the ctor property by doing a literal assignment. Without this
- // the $isplainobject function will think that this is a plain object.
- constructor: JSONEditor,
- init: function () {
- var self = this;
-
- this.ready = false;
-
- var theme_class =
- JSONEditor.defaults.themes[this.options.theme || JSONEditor.defaults.theme];
- if (!theme_class)
- throw 'Unknown theme ' + (this.options.theme || JSONEditor.defaults.theme);
-
- this.schema = this.options.schema;
- this.theme = new theme_class();
- this.template = this.options.template;
- this.refs = this.options.refs || {};
- this.uuid = 0;
- this.__data = {};
-
- var icon_class =
- JSONEditor.defaults.iconlibs[this.options.iconlib || JSONEditor.defaults.iconlib];
- if (icon_class) this.iconlib = new icon_class();
-
- this.root_container = this.theme.getContainer();
- this.element.appendChild(this.root_container);
-
- this.translate = this.options.translate || JSONEditor.defaults.translate;
-
- // Fetch all external refs via ajax
- this._loadExternalRefs(this.schema, function () {
- self._getDefinitions(self.schema);
-
- // Validator options
- var validator_options = {};
- if (self.options.custom_validators) {
- validator_options.custom_validators = self.options.custom_validators;
- }
- self.validator = new JSONEditor.Validator(self, null, validator_options);
-
- // Create the root editor
- var editor_class = self.getEditorClass(self.schema);
- self.root = self.createEditor(editor_class, {
- jsoneditor: self,
- schema: self.schema,
- required: true,
- container: self.root_container,
- });
-
- self.root.preBuild();
- self.root.build();
- self.root.postBuild();
-
- // Starting data
- if (self.options.startval) self.root.setValue(self.options.startval);
-
- self.validation_results = self.validator.validate(self.root.getValue());
- self.root.showValidationErrors(self.validation_results);
- self.ready = true;
-
- // Fire ready event asynchronously
- window.requestAnimationFrame(function () {
- if (!self.ready) return;
- self.validation_results = self.validator.validate(self.root.getValue());
- self.root.showValidationErrors(self.validation_results);
- self.trigger('ready');
- self.trigger('change');
- });
- });
- },
- getValue: function () {
- if (!this.ready)
- throw "JSON Editor not ready yet. Listen for 'ready' event before getting the value";
-
- return this.root.getValue();
- },
- setValue: function (value) {
- if (!this.ready)
- throw "JSON Editor not ready yet. Listen for 'ready' event before setting the value";
-
- this.root.setValue(value);
- return this;
- },
- validate: function (value) {
- if (!this.ready)
- throw "JSON Editor not ready yet. Listen for 'ready' event before validating";
-
- // Custom value
- if (arguments.length === 1) {
- return this.validator.validate(value);
- }
- // Current value (use cached result)
- else {
- return this.validation_results;
- }
- },
- destroy: function () {
- if (this.destroyed) return;
- if (!this.ready) return;
-
- this.schema = null;
- this.options = null;
- this.root.destroy();
- this.root = null;
- this.root_container = null;
- this.validator = null;
- this.validation_results = null;
- this.theme = null;
- this.iconlib = null;
- this.template = null;
- this.__data = null;
- this.ready = false;
- this.element.innerHTML = '';
-
- this.destroyed = true;
- },
- on: function (event, callback) {
- this.callbacks = this.callbacks || {};
- this.callbacks[event] = this.callbacks[event] || [];
- this.callbacks[event].push(callback);
-
- return this;
- },
- off: function (event, callback) {
- // Specific callback
- if (event && callback) {
- this.callbacks = this.callbacks || {};
- this.callbacks[event] = this.callbacks[event] || [];
- var newcallbacks = [];
- for (var i = 0; i < this.callbacks[event].length; i++) {
- if (this.callbacks[event][i] === callback) continue;
- newcallbacks.push(this.callbacks[event][i]);
- }
- this.callbacks[event] = newcallbacks;
- }
- // All callbacks for a specific event
- else if (event) {
- this.callbacks = this.callbacks || {};
- this.callbacks[event] = [];
- }
- // All callbacks for all events
- else {
- this.callbacks = {};
- }
-
- return this;
- },
- trigger: function (event) {
- if (this.callbacks && this.callbacks[event] && this.callbacks[event].length) {
- for (var i = 0; i < this.callbacks[event].length; i++) {
- this.callbacks[event][i]();
- }
- }
-
- return this;
- },
- setOption: function (option, value) {
- if (option === 'show_errors') {
- this.options.show_errors = value;
- this.onChange();
- }
- // Only the `show_errors` option is supported for now
- else {
- throw (
- 'Option ' +
- option +
- ' must be set during instantiation and cannot be changed later'
- );
- }
-
- return this;
- },
- getEditorClass: function (schema) {
- var classname;
-
- schema = this.expandSchema(schema);
-
- $each(JSONEditor.defaults.resolvers, function (i, resolver) {
- var tmp = resolver(schema);
- if (tmp) {
- if (JSONEditor.defaults.editors[tmp]) {
- classname = tmp;
- return false;
- }
- }
- });
-
- if (!classname) throw 'Unknown editor for schema ' + JSON.stringify(schema);
- if (!JSONEditor.defaults.editors[classname]) throw 'Unknown editor ' + classname;
-
- return JSONEditor.defaults.editors[classname];
- },
- createEditor: function (editor_class, options) {
- options = $extend({}, editor_class.options || {}, options);
- return new editor_class(options);
- },
- onChange: function () {
- if (!this.ready) return;
-
- if (this.firing_change) return;
- this.firing_change = true;
-
- var self = this;
-
- window.requestAnimationFrame(function () {
- self.firing_change = false;
- if (!self.ready) return;
-
- // Validate and cache results
- self.validation_results = self.validator.validate(self.root.getValue());
-
- if (self.options.show_errors !== 'never') {
- self.root.showValidationErrors(self.validation_results);
- } else {
- self.root.showValidationErrors([]);
- }
-
- // Fire change event
- self.trigger('change');
- });
-
- return this;
- },
- compileTemplate: function (template, name) {
- name = name || JSONEditor.defaults.template;
-
- var engine;
-
- // Specifying a preset engine
- if (typeof name === 'string') {
- if (!JSONEditor.defaults.templates[name]) throw 'Unknown template engine ' + name;
- engine = JSONEditor.defaults.templates[name]();
-
- if (!engine) throw 'Template engine ' + name + ' missing required library.';
- }
- // Specifying a custom engine
- else {
- engine = name;
- }
-
- if (!engine) throw 'No template engine set';
- if (!engine.compile) throw 'Invalid template engine set';
-
- return engine.compile(template);
- },
- _data: function (el, key, value) {
- // Setting data
- if (arguments.length === 3) {
- var uuid;
- if (el.hasAttribute('data-jsoneditor-' + key)) {
- uuid = el.getAttribute('data-jsoneditor-' + key);
- } else {
- uuid = this.uuid++;
- el.setAttribute('data-jsoneditor-' + key, uuid);
- }
-
- this.__data[uuid] = value;
- }
- // Getting data
- else {
- // No data stored
- if (!el.hasAttribute('data-jsoneditor-' + key)) return null;
-
- return this.__data[el.getAttribute('data-jsoneditor-' + key)];
- }
- },
- registerEditor: function (editor) {
- this.editors = this.editors || {};
- this.editors[editor.path] = editor;
- return this;
- },
- unregisterEditor: function (editor) {
- this.editors = this.editors || {};
- this.editors[editor.path] = null;
- return this;
- },
- getEditor: function (path) {
- if (!this.editors) return;
- return this.editors[path];
- },
- watch: function (path, callback) {
- this.watchlist = this.watchlist || {};
- this.watchlist[path] = this.watchlist[path] || [];
- this.watchlist[path].push(callback);
-
- return this;
- },
- unwatch: function (path, callback) {
- if (!this.watchlist || !this.watchlist[path]) return this;
- // If removing all callbacks for a path
- if (!callback) {
- this.watchlist[path] = null;
- return this;
- }
-
- var newlist = [];
- for (var i = 0; i < this.watchlist[path].length; i++) {
- if (this.watchlist[path][i] === callback) continue;
- else newlist.push(this.watchlist[path][i]);
- }
- this.watchlist[path] = newlist.length ? newlist : null;
- return this;
- },
- notifyWatchers: function (path) {
- if (!this.watchlist || !this.watchlist[path]) return this;
- for (var i = 0; i < this.watchlist[path].length; i++) {
- this.watchlist[path][i]();
- }
- },
- isEnabled: function () {
- return !this.root || this.root.isEnabled();
- },
- enable: function () {
- this.root.enable();
- },
- disable: function () {
- this.root.disable();
- },
- _getDefinitions: function (schema, path) {
- path = path || '#/definitions/';
- if (schema.definitions) {
- for (var i in schema.definitions) {
- if (!schema.definitions.hasOwnProperty(i)) continue;
- this.refs[path + i] = schema.definitions[i];
- if (schema.definitions[i].definitions) {
- this._getDefinitions(schema.definitions[i], path + i + '/definitions/');
- }
- }
- }
- },
- _getExternalRefs: function (schema) {
- var refs = {};
- var merge_refs = function (newrefs) {
- for (var i in newrefs) {
- if (newrefs.hasOwnProperty(i)) {
- refs[i] = true;
- }
- }
- };
-
- if (
- schema.$ref &&
- typeof schema.$ref !== 'object' &&
- schema.$ref.substr(0, 1) !== '#' &&
- !this.refs[schema.$ref]
- ) {
- refs[schema.$ref] = true;
- }
-
- for (var i in schema) {
- if (!schema.hasOwnProperty(i)) continue;
- if (schema[i] && typeof schema[i] === 'object' && Array.isArray(schema[i])) {
- for (var j = 0; j < schema[i].length; j++) {
- if (typeof schema[i][j] === 'object') {
- merge_refs(this._getExternalRefs(schema[i][j]));
- }
- }
- } else if (schema[i] && typeof schema[i] === 'object') {
- merge_refs(this._getExternalRefs(schema[i]));
- }
- }
-
- return refs;
- },
- _loadExternalRefs: function (schema, callback) {
- var self = this;
- var refs = this._getExternalRefs(schema);
-
- var done = 0,
- waiting = 0,
- callback_fired = false;
-
- $each(refs, function (url) {
- if (self.refs[url]) return;
- if (!self.options.ajax)
- throw 'Must set ajax option to true to load external ref ' + url;
- self.refs[url] = 'loading';
- waiting++;
-
- var r = new XMLHttpRequest();
- r.open('GET', url, true);
- r.onreadystatechange = function () {
- if (r.readyState != 4) return;
- // Request succeeded
- if (r.status === 200) {
- var response;
- try {
- response = JSON.parse(r.responseText);
- } catch (e) {
- window.console.log(e);
- throw 'Failed to parse external ref ' + url;
- }
- if (!response || typeof response !== 'object')
- throw 'External ref does not contain a valid schema - ' + url;
-
- self.refs[url] = response;
- self._loadExternalRefs(response, function () {
- done++;
- if (done >= waiting && !callback_fired) {
- callback_fired = true;
- callback();
- }
- });
- }
- // Request failed
- else {
- window.console.log(r);
- throw 'Failed to fetch ref via ajax- ' + url;
- }
- };
- r.send();
- });
-
- if (!waiting) {
- callback();
- }
- },
- expandRefs: function (schema) {
- schema = $extend({}, schema);
-
- while (schema.$ref) {
- var ref = schema.$ref;
- delete schema.$ref;
-
- if (!this.refs[ref]) ref = decodeURIComponent(ref);
-
- schema = this.extendSchemas(schema, this.refs[ref]);
- }
- return schema;
- },
- expandSchema: function (schema) {
- var self = this;
- var extended = $extend({}, schema);
- var i;
-
- // Version 3 `type`
- if (typeof schema.type === 'object') {
- // Array of types
- if (Array.isArray(schema.type)) {
- $each(schema.type, function (key, value) {
- // Schema
- if (typeof value === 'object') {
- schema.type[key] = self.expandSchema(value);
- }
- });
- }
- // Schema
- else {
- schema.type = self.expandSchema(schema.type);
- }
- }
- // Version 3 `disallow`
- if (typeof schema.disallow === 'object') {
- // Array of types
- if (Array.isArray(schema.disallow)) {
- $each(schema.disallow, function (key, value) {
- // Schema
- if (typeof value === 'object') {
- schema.disallow[key] = self.expandSchema(value);
- }
- });
- }
- // Schema
- else {
- schema.disallow = self.expandSchema(schema.disallow);
- }
- }
- // Version 4 `anyOf`
- if (schema.anyOf) {
- $each(schema.anyOf, function (key, value) {
- schema.anyOf[key] = self.expandSchema(value);
- });
- }
- // Version 4 `dependencies` (schema dependencies)
- if (schema.dependencies) {
- $each(schema.dependencies, function (key, value) {
- if (typeof value === 'object' && !Array.isArray(value)) {
- schema.dependencies[key] = self.expandSchema(value);
- }
- });
- }
- // Version 4 `not`
- if (schema.not) {
- schema.not = this.expandSchema(schema.not);
- }
-
- // allOf schemas should be merged into the parent
- if (schema.allOf) {
- for (i = 0; i < schema.allOf.length; i++) {
- extended = this.extendSchemas(extended, this.expandSchema(schema.allOf[i]));
- }
- delete extended.allOf;
- }
- // extends schemas should be merged into parent
- if (schema['extends']) {
- // If extends is a schema
- if (!Array.isArray(schema['extends'])) {
- extended = this.extendSchemas(extended, this.expandSchema(schema['extends']));
- }
- // If extends is an array of schemas
- else {
- for (i = 0; i < schema['extends'].length; i++) {
- extended = this.extendSchemas(
- extended,
- this.expandSchema(schema['extends'][i])
- );
- }
- }
- delete extended['extends'];
- }
- // parent should be merged into oneOf schemas
- if (schema.oneOf) {
- var tmp = $extend({}, extended);
- delete tmp.oneOf;
- for (i = 0; i < schema.oneOf.length; i++) {
- extended.oneOf[i] = this.extendSchemas(this.expandSchema(schema.oneOf[i]), tmp);
- }
- }
-
- return this.expandRefs(extended);
- },
- extendSchemas: function (obj1, obj2) {
- obj1 = $extend({}, obj1);
- obj2 = $extend({}, obj2);
-
- var self = this;
- var extended = {};
- $each(obj1, function (prop, val) {
- // If this key is also defined in obj2, merge them
- if (typeof obj2[prop] !== 'undefined') {
- // Required and defaultProperties arrays should be unioned together
- if (
- (prop === 'required' || prop === 'defaultProperties') &&
- typeof val === 'object' &&
- Array.isArray(val)
- ) {
- // Union arrays and unique
- extended[prop] = val.concat(obj2[prop]).reduce(function (p, c) {
- if (p.indexOf(c) < 0) p.push(c);
- return p;
- }, []);
- }
- // Type should be intersected and is either an array or string
- else if (prop === 'type' && (typeof val === 'string' || Array.isArray(val))) {
- // Make sure we're dealing with arrays
- if (typeof val === 'string') val = [val];
- if (typeof obj2.type === 'string') obj2.type = [obj2.type];
-
- // If type is only defined in the first schema, keep it
- if (!obj2.type || !obj2.type.length) {
- extended.type = val;
- }
- // If type is defined in both schemas, do an intersect
- else {
- extended.type = val.filter(function (n) {
- return obj2.type.indexOf(n) !== -1;
- });
- }
-
- // If there's only 1 type and it's a primitive, use a string instead of array
- if (extended.type.length === 1 && typeof extended.type[0] === 'string') {
- extended.type = extended.type[0];
- }
- // Remove the type property if it's empty
- else if (extended.type.length === 0) {
- delete extended.type;
- }
- }
- // All other arrays should be intersected (enum, etc.)
- else if (typeof val === 'object' && Array.isArray(val)) {
- extended[prop] = val.filter(function (n) {
- return obj2[prop].indexOf(n) !== -1;
- });
- }
- // Objects should be recursively merged
- else if (typeof val === 'object' && val !== null) {
- extended[prop] = self.extendSchemas(val, obj2[prop]);
- }
- // Otherwise, use the first value
- else {
- extended[prop] = val;
- }
- }
- // Otherwise, just use the one in obj1
- else {
- extended[prop] = val;
- }
- });
- // Properties in obj2 that aren't in obj1
- $each(obj2, function (prop, val) {
- if (typeof obj1[prop] === 'undefined') {
- extended[prop] = val;
- }
- });
-
- return extended;
- },
- };
-
- JSONEditor.defaults = {
- themes: {},
- templates: {},
- iconlibs: {},
- editors: {},
- languages: {},
- resolvers: [],
- custom_validators: [],
- };
-
- JSONEditor.Validator = Class.extend({
- init: function (jsoneditor, schema, options) {
- this.jsoneditor = jsoneditor;
- this.schema = schema || this.jsoneditor.schema;
- this.options = options || {};
- this.translate = this.jsoneditor.translate || JSONEditor.defaults.translate;
- },
- validate: function (value) {
- return this._validateSchema(this.schema, value);
- },
- _validateSchema: function (schema, value, path) {
- var self = this;
- var errors = [];
- var valid, i, j;
- var stringified = JSON.stringify(value);
-
- path = path || 'root';
-
- // Work on a copy of the schema
- schema = $extend({}, this.jsoneditor.expandRefs(schema));
-
- /*
- * Type Agnostic Validation
- */
-
- // Version 3 `required`
- if (schema.required && schema.required === true) {
- if (typeof value === 'undefined') {
- errors.push({
- path: path,
- property: 'required',
- message: this.translate('error_notset'),
- });
-
- // Can't do any more validation at this point
- return errors;
- }
- }
- // Value not defined
- else if (typeof value === 'undefined') {
- // If required_by_default is set, all fields are required
- if (this.jsoneditor.options.required_by_default) {
- errors.push({
- path: path,
- property: 'required',
- message: this.translate('error_notset'),
- });
- }
- // Not required, no further validation needed
- else {
- return errors;
- }
- }
-
- // `enum`
- if (schema['enum']) {
- valid = false;
- for (i = 0; i < schema['enum'].length; i++) {
- if (stringified === JSON.stringify(schema['enum'][i])) valid = true;
- }
- if (!valid) {
- errors.push({
- path: path,
- property: 'enum',
- message: this.translate('error_enum'),
- });
- }
- }
-
- // `extends` (version 3)
- if (schema['extends']) {
- for (i = 0; i < schema['extends'].length; i++) {
- errors = errors.concat(this._validateSchema(schema['extends'][i], value, path));
- }
- }
-
- // `allOf`
- if (schema.allOf) {
- for (i = 0; i < schema.allOf.length; i++) {
- errors = errors.concat(this._validateSchema(schema.allOf[i], value, path));
- }
- }
-
- // `anyOf`
- if (schema.anyOf) {
- valid = false;
- for (i = 0; i < schema.anyOf.length; i++) {
- if (!this._validateSchema(schema.anyOf[i], value, path).length) {
- valid = true;
- break;
- }
- }
- if (!valid) {
- errors.push({
- path: path,
- property: 'anyOf',
- message: this.translate('error_anyOf'),
- });
- }
- }
-
- // `oneOf`
- if (schema.oneOf) {
- valid = 0;
- var oneof_errors = [];
- for (i = 0; i < schema.oneOf.length; i++) {
- // Set the error paths to be path.oneOf[i].rest.of.path
- var tmp = this._validateSchema(schema.oneOf[i], value, path);
- if (!tmp.length) {
- valid++;
- }
-
- for (j = 0; j < tmp.length; j++) {
- tmp[j].path = path + '.oneOf[' + i + ']' + tmp[j].path.substr(path.length);
- }
- oneof_errors = oneof_errors.concat(tmp);
- }
- if (valid !== 1) {
- errors.push({
- path: path,
- property: 'oneOf',
- message: this.translate('error_oneOf', [valid]),
- });
- errors = errors.concat(oneof_errors);
- }
- }
-
- // `not`
- if (schema.not) {
- if (!this._validateSchema(schema.not, value, path).length) {
- errors.push({
- path: path,
- property: 'not',
- message: this.translate('error_not'),
- });
- }
- }
-
- // `type` (both Version 3 and Version 4 support)
- if (schema.type) {
- // Union type
- if (Array.isArray(schema.type)) {
- valid = false;
- for (i = 0; i < schema.type.length; i++) {
- if (this._checkType(schema.type[i], value)) {
- valid = true;
- break;
- }
- }
- if (!valid) {
- errors.push({
- path: path,
- property: 'type',
- message: this.translate('error_type_union'),
- });
- }
- }
- // Simple type
- else {
- if (!this._checkType(schema.type, value)) {
- errors.push({
- path: path,
- property: 'type',
- message: this.translate('error_type', [schema.type]),
- });
- }
- }
- }
-
- // `disallow` (version 3)
- if (schema.disallow) {
- // Union type
- if (Array.isArray(schema.disallow)) {
- valid = true;
- for (i = 0; i < schema.disallow.length; i++) {
- if (this._checkType(schema.disallow[i], value)) {
- valid = false;
- break;
- }
- }
- if (!valid) {
- errors.push({
- path: path,
- property: 'disallow',
- message: this.translate('error_disallow_union'),
- });
- }
- }
- // Simple type
- else {
- if (this._checkType(schema.disallow, value)) {
- errors.push({
- path: path,
- property: 'disallow',
- message: this.translate('error_disallow', [schema.disallow]),
- });
- }
- }
- }
-
- /*
- * Type Specific Validation
- */
-
- // Number Specific Validation
- if (typeof value === 'number') {
- // `multipleOf` and `divisibleBy`
- if (schema.multipleOf || schema.divisibleBy) {
- var divisor = schema.multipleOf || schema.divisibleBy;
- // Vanilla JS, prone to floating point rounding errors (e.g. 1.14 / .01 == 113.99999)
- valid = value / divisor === Math.floor(value / divisor);
-
- // Use math.js is available
- if (window.math) {
- valid = window.math
- .mod(window.math.bignumber(value), window.math.bignumber(divisor))
- .equals(0);
- }
- // Use decimal.js is available
- else if (window.Decimal) {
- valid = new window.Decimal(value)
- .mod(new window.Decimal(divisor))
- .equals(0);
- }
-
- if (!valid) {
- errors.push({
- path: path,
- property: schema.multipleOf ? 'multipleOf' : 'divisibleBy',
- message: this.translate('error_multipleOf', [divisor]),
- });
- }
- }
-
- // `maximum`
- if (schema.hasOwnProperty('maximum')) {
- // Vanilla JS, prone to floating point rounding errors (e.g. .999999999999999 == 1)
- valid = schema.exclusiveMaximum
- ? value < schema.maximum
- : value <= schema.maximum;
-
- // Use math.js is available
- if (window.math) {
- valid = window.math[schema.exclusiveMaximum ? 'smaller' : 'smallerEq'](
- window.math.bignumber(value),
- window.math.bignumber(schema.maximum)
- );
- }
- // Use Decimal.js if available
- else if (window.Decimal) {
- valid = new window.Decimal(value)[schema.exclusiveMaximum ? 'lt' : 'lte'](
- new window.Decimal(schema.maximum)
- );
- }
-
- if (!valid) {
- errors.push({
- path: path,
- property: 'maximum',
- message: this.translate(
- schema.exclusiveMaximum
- ? 'error_maximum_excl'
- : 'error_maximum_incl',
- [schema.maximum]
- ),
- });
- }
- }
-
- // `minimum`
- if (schema.hasOwnProperty('minimum')) {
- // Vanilla JS, prone to floating point rounding errors (e.g. .999999999999999 == 1)
- valid = schema.exclusiveMinimum
- ? value > schema.minimum
- : value >= schema.minimum;
-
- // Use math.js is available
- if (window.math) {
- valid = window.math[schema.exclusiveMinimum ? 'larger' : 'largerEq'](
- window.math.bignumber(value),
- window.math.bignumber(schema.minimum)
- );
- }
- // Use Decimal.js if available
- else if (window.Decimal) {
- valid = new window.Decimal(value)[schema.exclusiveMinimum ? 'gt' : 'gte'](
- new window.Decimal(schema.minimum)
- );
- }
-
- if (!valid) {
- errors.push({
- path: path,
- property: 'minimum',
- message: this.translate(
- schema.exclusiveMinimum
- ? 'error_minimum_excl'
- : 'error_minimum_incl',
- [schema.minimum]
- ),
- });
- }
- }
- }
- // String specific validation
- else if (typeof value === 'string') {
- // `maxLength`
- if (schema.maxLength) {
- if ((value + '').length > schema.maxLength) {
- errors.push({
- path: path,
- property: 'maxLength',
- message: this.translate('error_maxLength', [schema.maxLength]),
- });
- }
- }
-
- // `minLength`
- if (schema.minLength) {
- if ((value + '').length < schema.minLength) {
- errors.push({
- path: path,
- property: 'minLength',
- message: this.translate(
- schema.minLength === 1 ? 'error_notempty' : 'error_minLength',
- [schema.minLength]
- ),
- });
- }
- }
-
- // `pattern`
- if (schema.pattern) {
- if (!new RegExp(schema.pattern).test(value)) {
- errors.push({
- path: path,
- property: 'pattern',
- message: this.translate('error_pattern', [schema.pattern]),
- });
- }
- }
- }
- // Array specific validation
- else if (typeof value === 'object' && value !== null && Array.isArray(value)) {
- // `items` and `additionalItems`
- if (schema.items) {
- // `items` is an array
- if (Array.isArray(schema.items)) {
- for (i = 0; i < value.length; i++) {
- // If this item has a specific schema tied to it
- // Validate against it
- if (schema.items[i]) {
- errors = errors.concat(
- this._validateSchema(schema.items[i], value[i], path + '.' + i)
- );
- }
- // If all additional items are allowed
- else if (schema.additionalItems === true) {
- break;
- }
- // If additional items is a schema
- // TODO: Incompatibility between version 3 and 4 of the spec
- else if (schema.additionalItems) {
- errors = errors.concat(
- this._validateSchema(
- schema.additionalItems,
- value[i],
- path + '.' + i
- )
- );
- }
- // If no additional items are allowed
- else if (schema.additionalItems === false) {
- errors.push({
- path: path,
- property: 'additionalItems',
- message: this.translate('error_additionalItems'),
- });
- break;
- }
- // Default for `additionalItems` is an empty schema
- else {
- break;
- }
- }
- }
- // `items` is a schema
- else {
- // Each item in the array must validate against the schema
- for (i = 0; i < value.length; i++) {
- errors = errors.concat(
- this._validateSchema(schema.items, value[i], path + '.' + i)
- );
- }
- }
- }
-
- // `maxItems`
- if (schema.maxItems) {
- if (value.length > schema.maxItems) {
- errors.push({
- path: path,
- property: 'maxItems',
- message: this.translate('error_maxItems', [schema.maxItems]),
- });
- }
- }
-
- // `minItems`
- if (schema.minItems) {
- if (value.length < schema.minItems) {
- errors.push({
- path: path,
- property: 'minItems',
- message: this.translate('error_minItems', [schema.minItems]),
- });
- }
- }
-
- // `uniqueItems`
- if (schema.uniqueItems) {
- var seen = {};
- for (i = 0; i < value.length; i++) {
- valid = JSON.stringify(value[i]);
- if (seen[valid]) {
- errors.push({
- path: path,
- property: 'uniqueItems',
- message: this.translate('error_uniqueItems'),
- });
- break;
- }
- seen[valid] = true;
- }
- }
- }
- // Object specific validation
- else if (typeof value === 'object' && value !== null) {
- // `maxProperties`
- if (schema.maxProperties) {
- valid = 0;
- for (i in value) {
- if (!value.hasOwnProperty(i)) continue;
- valid++;
- }
- if (valid > schema.maxProperties) {
- errors.push({
- path: path,
- property: 'maxProperties',
- message: this.translate('error_maxProperties', [schema.maxProperties]),
- });
- }
- }
-
- // `minProperties`
- if (schema.minProperties) {
- valid = 0;
- for (i in value) {
- if (!value.hasOwnProperty(i)) continue;
- valid++;
- }
- if (valid < schema.minProperties) {
- errors.push({
- path: path,
- property: 'minProperties',
- message: this.translate('error_minProperties', [schema.minProperties]),
- });
- }
- }
-
- // Version 4 `required`
- if (schema.required && Array.isArray(schema.required)) {
- for (i = 0; i < schema.required.length; i++) {
- if (typeof value[schema.required[i]] === 'undefined') {
- errors.push({
- path: path,
- property: 'required',
- message: this.translate('error_required', [schema.required[i]]),
- });
- }
- }
- }
-
- // `properties`
- var validated_properties = {};
- if (schema.properties) {
- for (i in schema.properties) {
- if (!schema.properties.hasOwnProperty(i)) continue;
- validated_properties[i] = true;
- errors = errors.concat(
- this._validateSchema(schema.properties[i], value[i], path + '.' + i)
- );
- }
- }
-
- // `patternProperties`
- if (schema.patternProperties) {
- for (i in schema.patternProperties) {
- if (!schema.patternProperties.hasOwnProperty(i)) continue;
-
- var regex = new RegExp(i);
-
- // Check which properties match
- for (j in value) {
- if (!value.hasOwnProperty(j)) continue;
- if (regex.test(j)) {
- validated_properties[j] = true;
- errors = errors.concat(
- this._validateSchema(
- schema.patternProperties[i],
- value[j],
- path + '.' + j
- )
- );
- }
- }
- }
- }
-
- // The no_additional_properties option currently doesn't work with extended schemas that use oneOf or anyOf
- if (
- typeof schema.additionalProperties === 'undefined' &&
- this.jsoneditor.options.no_additional_properties &&
- !schema.oneOf &&
- !schema.anyOf
- ) {
- schema.additionalProperties = false;
- }
-
- // `additionalProperties`
- if (typeof schema.additionalProperties !== 'undefined') {
- for (i in value) {
- if (!value.hasOwnProperty(i)) continue;
- if (!validated_properties[i]) {
- // No extra properties allowed
- if (!schema.additionalProperties) {
- errors.push({
- path: path,
- property: 'additionalProperties',
- message: this.translate('error_additional_properties', [i]),
- });
- break;
- }
- // Allowed
- else if (schema.additionalProperties === true) {
- break;
- }
- // Must match schema
- // TODO: incompatibility between version 3 and 4 of the spec
- else {
- errors = errors.concat(
- this._validateSchema(
- schema.additionalProperties,
- value[i],
- path + '.' + i
- )
- );
- }
- }
- }
- }
-
- // `dependencies`
- if (schema.dependencies) {
- for (i in schema.dependencies) {
- if (!schema.dependencies.hasOwnProperty(i)) continue;
-
- // Doesn't need to meet the dependency
- if (typeof value[i] === 'undefined') continue;
-
- // Property dependency
- if (Array.isArray(schema.dependencies[i])) {
- for (j = 0; j < schema.dependencies[i].length; j++) {
- if (typeof value[schema.dependencies[i][j]] === 'undefined') {
- errors.push({
- path: path,
- property: 'dependencies',
- message: this.translate('error_dependency', [
- schema.dependencies[i][j],
- ]),
- });
- }
- }
- }
- // Schema dependency
- else {
- errors = errors.concat(
- this._validateSchema(schema.dependencies[i], value, path)
- );
- }
- }
- }
- }
-
- // Custom type validation (global)
- $each(JSONEditor.defaults.custom_validators, function (i, validator) {
- errors = errors.concat(validator.call(self, schema, value, path));
- });
- // Custom type validation (instance specific)
- if (this.options.custom_validators) {
- $each(this.options.custom_validators, function (i, validator) {
- errors = errors.concat(validator.call(self, schema, value, path));
- });
- }
-
- return errors;
- },
- _checkType: function (type, value) {
- // Simple types
- if (typeof type === 'string') {
- if (type === 'string') return typeof value === 'string';
- else if (type === 'number') return typeof value === 'number';
- else if (type === 'integer')
- return typeof value === 'number' && value === Math.floor(value);
- else if (type === 'boolean') return typeof value === 'boolean';
- else if (type === 'array') return Array.isArray(value);
- else if (type === 'object')
- return value !== null && !Array.isArray(value) && typeof value === 'object';
- else if (type === 'null') return value === null;
- else return true;
- }
- // Schema
- else {
- return !this._validateSchema(type, value).length;
- }
- },
- });
-
- /**
- * All editors should extend from this class
- */
- JSONEditor.AbstractEditor = Class.extend({
- onChildEditorChange: function (editor) {
- this.onChange(true);
- },
- notify: function () {
- this.jsoneditor.notifyWatchers(this.path);
- },
- change: function () {
- if (this.parent) this.parent.onChildEditorChange(this);
- else this.jsoneditor.onChange();
- },
- onChange: function (bubble) {
- this.notify();
- if (this.watch_listener) this.watch_listener();
- if (bubble) this.change();
- },
- register: function () {
- this.jsoneditor.registerEditor(this);
- this.onChange();
- },
- unregister: function () {
- if (!this.jsoneditor) return;
- this.jsoneditor.unregisterEditor(this);
- },
- getNumColumns: function () {
- return 12;
- },
- init: function (options) {
- this.jsoneditor = options.jsoneditor;
-
- this.theme = this.jsoneditor.theme;
- this.template_engine = this.jsoneditor.template;
- this.iconlib = this.jsoneditor.iconlib;
-
- this.translate = this.jsoneditor.translate || JSONEditor.defaults.translate;
-
- this.original_schema = options.schema;
- this.schema = this.jsoneditor.expandSchema(this.original_schema);
-
- this.options = $extend({}, this.options || {}, options.schema.options || {}, options);
-
- if (!options.path && !this.schema.id) this.schema.id = 'root';
- this.path = options.path || 'root';
- this.formname = options.formname || this.path.replace(/\.([^.]+)/g, '[$1]');
- if (this.jsoneditor.options.form_name_root)
- this.formname = this.formname.replace(
- /^root\[/,
- this.jsoneditor.options.form_name_root + '['
- );
- this.key = this.path.split('.').pop();
- this.parent = options.parent;
-
- this.link_watchers = [];
-
- if (options.container) this.setContainer(options.container);
- },
- setContainer: function (container) {
- this.container = container;
- if (this.schema.id) this.container.setAttribute('data-schemaid', this.schema.id);
- if (this.schema.type && typeof this.schema.type === 'string')
- this.container.setAttribute('data-schematype', this.schema.type);
- this.container.setAttribute('data-schemapath', this.path);
- },
-
- preBuild: function () {},
- build: function () {},
- postBuild: function () {
- this.setupWatchListeners();
- this.addLinks();
- this.setValue(this.getDefault(), true);
- this.updateHeaderText();
- this.register();
- this.onWatchedFieldChange();
- },
-
- setupWatchListeners: function () {
- var self = this;
-
- // Watched fields
- this.watched = {};
- if (this.schema.vars) this.schema.watch = this.schema.vars;
- this.watched_values = {};
- this.watch_listener = function () {
- if (self.refreshWatchedFieldValues()) {
- self.onWatchedFieldChange();
- }
- };
-
- this.register();
- if (this.schema.hasOwnProperty('watch')) {
- var path, path_parts, first, root, adjusted_path;
-
- for (var name in this.schema.watch) {
- if (!this.schema.watch.hasOwnProperty(name)) continue;
- path = this.schema.watch[name];
-
- if (Array.isArray(path)) {
- if (path.length < 2) continue;
- path_parts = [path[0]].concat(path[1].split('.'));
- } else {
- path_parts = path.split('.');
- if (
- !self.theme.closest(
- self.container,
- '[data-schemaid="' + path_parts[0] + '"]'
- )
- )
- path_parts.unshift('#');
- }
- first = path_parts.shift();
-
- if (first === '#') first = self.jsoneditor.schema.id || 'root';
-
- // Find the root node for this template variable
- root = self.theme.closest(self.container, '[data-schemaid="' + first + '"]');
- if (!root) throw 'Could not find ancestor node with id ' + first;
-
- // Keep track of the root node and path for use when rendering the template
- adjusted_path =
- root.getAttribute('data-schemapath') + '.' + path_parts.join('.');
-
- self.jsoneditor.watch(adjusted_path, self.watch_listener);
-
- self.watched[name] = adjusted_path;
- }
- }
-
- // Dynamic header
- if (this.schema.headerTemplate) {
- this.header_template = this.jsoneditor.compileTemplate(
- this.schema.headerTemplate,
- this.template_engine
- );
- }
- },
-
- addLinks: function () {
- // Add links
- if (!this.no_link_holder) {
- this.link_holder = this.theme.getLinksHolder();
- this.container.appendChild(this.link_holder);
- if (this.schema.links) {
- for (var i = 0; i < this.schema.links.length; i++) {
- this.addLink(this.getLink(this.schema.links[i]));
- }
- }
- }
- },
-
- getButton: function (text, icon, title) {
- var btnClass = 'json-editor-btn-' + icon;
- if (!this.iconlib) icon = null;
- else icon = this.iconlib.getIcon(icon);
-
- if (!icon && title) {
- text = title;
- title = null;
- }
-
- var btn = this.theme.getButton(text, icon, title);
- btn.className += ' ' + btnClass + ' ';
- return btn;
- },
- setButtonText: function (button, text, icon, title) {
- if (!this.iconlib) icon = null;
- else icon = this.iconlib.getIcon(icon);
-
- if (!icon && title) {
- text = title;
- title = null;
- }
-
- return this.theme.setButtonText(button, text, icon, title);
- },
- addLink: function (link) {
- if (this.link_holder) this.link_holder.appendChild(link);
- },
- getLink: function (data) {
- var holder, link;
-
- // Get mime type of the link
- var mime = data.mediaType || 'application/javascript';
- var type = mime.split('/')[0];
-
- // Template to generate the link href
- var href = this.jsoneditor.compileTemplate(data.href, this.template_engine);
-
- // Template to generate the link's download attribute
- var download = null;
- if (data.download) download = data.download;
-
- if (download && download !== true) {
- download = this.jsoneditor.compileTemplate(download, this.template_engine);
- }
-
- // Image links
- if (type === 'image') {
- holder = this.theme.getBlockLinkHolder();
- link = document.createElement('a');
- link.setAttribute('target', '_blank');
- var image = document.createElement('img');
-
- this.theme.createImageLink(holder, link, image);
-
- // When a watched field changes, update the url
- this.link_watchers.push(function (vars) {
- var url = href(vars);
- link.setAttribute('href', url);
- link.setAttribute('title', data.rel || url);
- image.setAttribute('src', url);
- });
- }
- // Audio/Video links
- else if (['audio', 'video'].indexOf(type) >= 0) {
- holder = this.theme.getBlockLinkHolder();
-
- link = this.theme.getBlockLink();
- link.setAttribute('target', '_blank');
-
- var media = document.createElement(type);
- media.setAttribute('controls', 'controls');
-
- this.theme.createMediaLink(holder, link, media);
-
- // When a watched field changes, update the url
- this.link_watchers.push(function (vars) {
- var url = href(vars);
- link.setAttribute('href', url);
- link.textContent = data.rel || url;
- media.setAttribute('src', url);
- });
- }
- // Text links
- else {
- link = holder = this.theme.getBlockLink();
- holder.setAttribute('target', '_blank');
- holder.textContent = data.rel;
-
- // When a watched field changes, update the url
- this.link_watchers.push(function (vars) {
- var url = href(vars);
- holder.setAttribute('href', url);
- holder.textContent = data.rel || url;
- });
- }
-
- if (download && link) {
- if (download === true) {
- link.setAttribute('download', '');
- } else {
- this.link_watchers.push(function (vars) {
- link.setAttribute('download', download(vars));
- });
- }
- }
-
- if (data.class) link.className = link.className + ' ' + data.class;
-
- return holder;
- },
- refreshWatchedFieldValues: function () {
- if (!this.watched_values) return;
- var watched = {};
- var changed = false;
- var self = this;
-
- if (this.watched) {
- var val, editor;
- for (var name in this.watched) {
- if (!this.watched.hasOwnProperty(name)) continue;
- editor = self.jsoneditor.getEditor(this.watched[name]);
- val = editor ? editor.getValue() : null;
- if (self.watched_values[name] !== val) changed = true;
- watched[name] = val;
- }
- }
-
- watched.self = this.getValue();
- if (this.watched_values.self !== watched.self) changed = true;
-
- this.watched_values = watched;
-
- return changed;
- },
- getWatchedFieldValues: function () {
- return this.watched_values;
- },
- updateHeaderText: function () {
- if (this.header) {
- // If the header has children, only update the text node's value
- if (this.header.children.length) {
- for (var i = 0; i < this.header.childNodes.length; i++) {
- if (this.header.childNodes[i].nodeType === 3) {
- this.header.childNodes[i].nodeValue = this.getHeaderText();
- break;
- }
- }
- }
- // Otherwise, just update the entire node
- else {
- this.header.textContent = this.getHeaderText();
- }
- }
- },
- getHeaderText: function (title_only) {
- if (this.header_text) return this.header_text;
- else if (title_only) return this.schema.title;
- else return this.getTitle();
- },
- onWatchedFieldChange: function () {
- var vars;
- if (this.header_template) {
- vars = $extend(this.getWatchedFieldValues(), {
- key: this.key,
- i: this.key,
- i0: this.key * 1,
- i1: this.key * 1 + 1,
- title: this.getTitle(),
- });
- var header_text = this.header_template(vars);
-
- if (header_text !== this.header_text) {
- this.header_text = header_text;
- this.updateHeaderText();
- this.notify();
- //this.fireChangeHeaderEvent();
- }
- }
- if (this.link_watchers.length) {
- vars = this.getWatchedFieldValues();
- for (var i = 0; i < this.link_watchers.length; i++) {
- this.link_watchers[i](vars);
- }
- }
- },
- setValue: function (value) {
- this.value = value;
- },
- getValue: function () {
- return this.value;
- },
- refreshValue: function () {},
- getChildEditors: function () {
- return false;
- },
- destroy: function () {
- var self = this;
- this.unregister(this);
- $each(this.watched, function (name, adjusted_path) {
- self.jsoneditor.unwatch(adjusted_path, self.watch_listener);
- });
- this.watched = null;
- this.watched_values = null;
- this.watch_listener = null;
- this.header_text = null;
- this.header_template = null;
- this.value = null;
- if (this.container && this.container.parentNode)
- this.container.parentNode.removeChild(this.container);
- this.container = null;
- this.jsoneditor = null;
- this.schema = null;
- this.path = null;
- this.key = null;
- this.parent = null;
- },
- getDefault: function () {
- if (this.schema['default']) return this.schema['default'];
- if (this.schema['enum']) return this.schema['enum'][0];
-
- var type = this.schema.type || this.schema.oneOf;
- if (type && Array.isArray(type)) type = type[0];
- if (type && typeof type === 'object') type = type.type;
- if (type && Array.isArray(type)) type = type[0];
-
- if (typeof type === 'string') {
- if (type === 'number') return 0.0;
- if (type === 'boolean') return false;
- if (type === 'integer') return 0;
- if (type === 'string') return '';
- if (type === 'object') return {};
- if (type === 'array') return [];
- }
-
- return null;
- },
- getTitle: function () {
- return this.schema.title || this.key;
- },
- enable: function () {
- this.disabled = false;
- },
- disable: function () {
- this.disabled = true;
- },
- isEnabled: function () {
- return !this.disabled;
- },
- isRequired: function () {
- if (typeof this.schema.required === 'boolean') return this.schema.required;
- else if (
- this.parent &&
- this.parent.schema &&
- Array.isArray(this.parent.schema.required)
- )
- return this.parent.schema.required.indexOf(this.key) > -1;
- else if (this.jsoneditor.options.required_by_default) return true;
- else return false;
- },
- getDisplayText: function (arr) {
- var disp = [];
- var used = {};
-
- // Determine how many times each attribute name is used.
- // This helps us pick the most distinct display text for the schemas.
- $each(arr, function (i, el) {
- if (el.title) {
- used[el.title] = used[el.title] || 0;
- used[el.title]++;
- }
- if (el.description) {
- used[el.description] = used[el.description] || 0;
- used[el.description]++;
- }
- if (el.format) {
- used[el.format] = used[el.format] || 0;
- used[el.format]++;
- }
- if (el.type) {
- used[el.type] = used[el.type] || 0;
- used[el.type]++;
- }
- });
-
- // Determine display text for each element of the array
- $each(arr, function (i, el) {
- var name;
-
- // If it's a simple string
- if (typeof el === 'string') name = el;
- // Object
- else if (el.title && used[el.title] <= 1) name = el.title;
- else if (el.format && used[el.format] <= 1) name = el.format;
- else if (el.type && used[el.type] <= 1) name = el.type;
- else if (el.description && used[el.description] <= 1) name = el.descripton;
- else if (el.title) name = el.title;
- else if (el.format) name = el.format;
- else if (el.type) name = el.type;
- else if (el.description) name = el.description;
- else if (JSON.stringify(el).length < 50) name = JSON.stringify(el);
- else name = 'type';
-
- disp.push(name);
- });
-
- // Replace identical display text with "text 1", "text 2", etc.
- var inc = {};
- $each(disp, function (i, name) {
- inc[name] = inc[name] || 0;
- inc[name]++;
-
- if (used[name] > 1) disp[i] = name + ' ' + inc[name];
- });
-
- return disp;
- },
- getOption: function (key) {
- try {
- throw 'getOption is deprecated';
- } catch (e) {
- window.console.error(e);
- }
-
- return this.options[key];
- },
- showValidationErrors: function (errors) {},
- });
-
- JSONEditor.defaults.editors['null'] = JSONEditor.AbstractEditor.extend({
- getValue: function () {
- return null;
- },
- setValue: function () {
- this.onChange();
- },
- getNumColumns: function () {
- return 2;
- },
- });
-
- JSONEditor.defaults.editors.string = JSONEditor.AbstractEditor.extend({
- register: function () {
- this._super();
- if (!this.input) return;
- this.input.setAttribute('name', this.formname);
- },
- unregister: function () {
- this._super();
- if (!this.input) return;
- this.input.removeAttribute('name');
- },
- setValue: function (value, initial, from_template) {
- var self = this;
-
- if (this.template && !from_template) {
- return;
- }
-
- if (value === null || typeof value === 'undefined') value = '';
- else if (typeof value === 'object') value = JSON.stringify(value);
- else if (typeof value !== 'string') value = '' + value;
-
- if (value === this.serialized) return;
-
- // Sanitize value before setting it
- var sanitized = this.sanitize(value);
-
- if (this.input.value === sanitized) {
- return;
- }
-
- this.input.value = sanitized;
-
- // If using SCEditor, update the WYSIWYG
- if (this.sceditor_instance) {
- this.sceditor_instance.val(sanitized);
- } else if (this.epiceditor) {
- this.epiceditor.importFile(null, sanitized);
- } else if (this.ace_editor) {
- this.ace_editor.setValue(sanitized);
- }
-
- var changed = from_template || this.getValue() !== value;
-
- this.refreshValue();
-
- if (initial) this.is_dirty = false;
- else if (this.jsoneditor.options.show_errors === 'change') this.is_dirty = true;
-
- if (this.adjust_height) this.adjust_height(this.input);
-
- // Bubble this setValue to parents if the value changed
- this.onChange(changed);
- },
- getNumColumns: function () {
- var min = Math.ceil(
- Math.max(
- this.getTitle().length,
- this.schema.maxLength || 0,
- this.schema.minLength || 0
- ) / 5
- );
- var num;
-
- if (this.input_type === 'textarea') num = 6;
- else if (['text', 'email'].indexOf(this.input_type) >= 0) num = 4;
- else num = 2;
-
- return Math.min(12, Math.max(min, num));
- },
- build: function () {
- var self = this,
- i;
- if (!this.options.compact)
- this.header = this.label = this.theme.getFormInputLabel(this.getTitle());
- if (this.schema.description)
- this.description = this.theme.getFormInputDescription(this.schema.description);
-
- this.format = this.schema.format;
- if (!this.format && this.schema.media && this.schema.media.type) {
- this.format = this.schema.media.type.replace(
- /(^(application|text)\/(x-)?(script\.)?)|(-source$)/g,
- ''
- );
- }
- if (!this.format && this.options.default_format) {
- this.format = this.options.default_format;
- }
- if (this.options.format) {
- this.format = this.options.format;
- }
-
- // Specific format
- if (this.format) {
- // Text Area
- if (this.format === 'textarea') {
- this.input_type = 'textarea';
- this.input = this.theme.getTextareaInput();
- }
- // Range Input
- else if (this.format === 'range') {
- this.input_type = 'range';
- var min = this.schema.minimum || 0;
- var max = this.schema.maximum || Math.max(100, min + 1);
- var step = 1;
- if (this.schema.multipleOf) {
- if (min % this.schema.multipleOf)
- min = Math.ceil(min / this.schema.multipleOf) * this.schema.multipleOf;
- if (max % this.schema.multipleOf)
- max = Math.floor(max / this.schema.multipleOf) * this.schema.multipleOf;
- step = this.schema.multipleOf;
- }
-
- this.input = this.theme.getRangeInput(min, max, step);
- }
- // Source Code
- else if (
- [
- 'actionscript',
- 'batchfile',
- 'bbcode',
- 'c',
- 'c++',
- 'cpp',
- 'coffee',
- 'csharp',
- 'css',
- 'dart',
- 'django',
- 'ejs',
- 'erlang',
- 'golang',
- 'groovy',
- 'handlebars',
- 'haskell',
- 'haxe',
- 'html',
- 'ini',
- 'jade',
- 'java',
- 'javascript',
- 'json',
- 'less',
- 'lisp',
- 'lua',
- 'makefile',
- 'markdown',
- 'matlab',
- 'mysql',
- 'objectivec',
- 'pascal',
- 'perl',
- 'pgsql',
- 'php',
- 'python',
- 'r',
- 'ruby',
- 'sass',
- 'scala',
- 'scss',
- 'smarty',
- 'sql',
- 'stylus',
- 'svg',
- 'twig',
- 'vbscript',
- 'xml',
- 'yaml',
- ].indexOf(this.format) >= 0
- ) {
- this.input_type = this.format;
- this.source_code = true;
-
- this.input = this.theme.getTextareaInput();
- }
- // HTML5 Input type
- else {
- this.input_type = this.format;
- this.input = this.theme.getFormInputField(this.input_type);
- }
- }
- // Normal text input
- else {
- this.input_type = 'text';
- this.input = this.theme.getFormInputField(this.input_type);
- }
-
- // minLength, maxLength, and pattern
- if (typeof this.schema.maxLength !== 'undefined')
- this.input.setAttribute('maxlength', this.schema.maxLength);
- if (typeof this.schema.pattern !== 'undefined')
- this.input.setAttribute('pattern', this.schema.pattern);
- else if (typeof this.schema.minLength !== 'undefined')
- this.input.setAttribute('pattern', '.{' + this.schema.minLength + ',}');
-
- if (this.options.compact) {
- this.container.className += ' compact';
- } else {
- if (this.options.input_width) this.input.style.width = this.options.input_width;
- }
-
- if (this.schema.readOnly || this.schema.readonly || this.schema.template) {
- this.always_disabled = true;
- this.input.disabled = true;
- }
-
- this.input.addEventListener('change', function (e) {
- e.preventDefault();
- e.stopPropagation();
-
- // Don't allow changing if this field is a template
- if (self.schema.template) {
- this.value = self.value;
- return;
- }
-
- var val = this.value;
-
- // sanitize value
- var sanitized = self.sanitize(val);
- if (val !== sanitized) {
- this.value = sanitized;
- }
-
- self.is_dirty = true;
-
- self.refreshValue();
- self.onChange(true);
- });
-
- if (this.options.input_height) this.input.style.height = this.options.input_height;
- if (this.options.expand_height) {
- this.adjust_height = function (el) {
- if (!el) return;
- var i,
- ch = el.offsetHeight;
- // Input too short
- if (el.offsetHeight < el.scrollHeight) {
- i = 0;
- while (el.offsetHeight < el.scrollHeight + 3) {
- if (i > 100) break;
- i++;
- ch++;
- el.style.height = ch + 'px';
- }
- } else {
- i = 0;
- while (el.offsetHeight >= el.scrollHeight + 3) {
- if (i > 100) break;
- i++;
- ch--;
- el.style.height = ch + 'px';
- }
- el.style.height = ch + 1 + 'px';
- }
- };
-
- this.input.addEventListener('keyup', function (e) {
- self.adjust_height(this);
- });
- this.input.addEventListener('change', function (e) {
- self.adjust_height(this);
- });
- this.adjust_height();
- }
-
- if (this.format) this.input.setAttribute('data-schemaformat', this.format);
-
- this.control = this.theme.getFormControl(this.label, this.input, this.description);
- this.container.appendChild(this.control);
-
- // Any special formatting that needs to happen after the input is added to the dom
- window.requestAnimationFrame(function () {
- // Skip in case the input is only a temporary editor,
- // otherwise, in the case of an ace_editor creation,
- // it will generate an error trying to append it to the missing parentNode
- if (self.input.parentNode) self.afterInputReady();
- if (self.adjust_height) self.adjust_height(self.input);
- });
-
- // Compile and store the template
- if (this.schema.template) {
- this.template = this.jsoneditor.compileTemplate(
- this.schema.template,
- this.template_engine
- );
- this.refreshValue();
- } else {
- this.refreshValue();
- }
- },
- enable: function () {
- if (!this.always_disabled) {
- this.input.disabled = false;
- // TODO: WYSIWYG and Markdown editors
- }
- this._super();
- },
- disable: function () {
- this.input.disabled = true;
- // TODO: WYSIWYG and Markdown editors
- this._super();
- },
- afterInputReady: function () {
- var self = this,
- options;
-
- // Code editor
- if (this.source_code) {
- // WYSIWYG html and bbcode editor
- if (
- this.options.wysiwyg &&
- ['html', 'bbcode'].indexOf(this.input_type) >= 0 &&
- window.jQuery &&
- window.jQuery.fn &&
- window.jQuery.fn.sceditor
- ) {
- options = $extend(
- {},
- {
- plugins: self.input_type === 'html' ? 'xhtml' : 'bbcode',
- emoticonsEnabled: false,
- width: '100%',
- height: 300,
- },
- JSONEditor.plugins.sceditor,
- self.options.sceditor_options || {}
- );
-
- window.jQuery(self.input).sceditor(options);
-
- self.sceditor_instance = window.jQuery(self.input).sceditor('instance');
-
- self.sceditor_instance.blur(function () {
- // Get editor's value
- var val = window.jQuery('' + self.sceditor_instance.val() + '
');
- // Remove sceditor spans/divs
- window
- .jQuery(
- '#sceditor-start-marker,#sceditor-end-marker,.sceditor-nlf',
- val
- )
- .remove();
- // Set the value and update
- self.input.value = val.html();
- self.value = self.input.value;
- self.is_dirty = true;
- self.onChange(true);
- });
- }
- // EpicEditor for markdown (if it's loaded)
- else if (this.input_type === 'markdown' && window.EpicEditor) {
- this.epiceditor_container = document.createElement('div');
- this.input.parentNode.insertBefore(this.epiceditor_container, this.input);
- this.input.style.display = 'none';
-
- options = $extend({}, JSONEditor.plugins.epiceditor, {
- container: this.epiceditor_container,
- clientSideStorage: false,
- });
-
- this.epiceditor = new window.EpicEditor(options).load();
-
- this.epiceditor.importFile(null, this.getValue());
-
- this.epiceditor.on('update', function () {
- var val = self.epiceditor.exportFile();
- self.input.value = val;
- self.value = val;
- self.is_dirty = true;
- self.onChange(true);
- });
- }
- // ACE editor for everything else
- else if (window.ace) {
- var mode = this.input_type;
- // aliases for c/cpp
- if (mode === 'cpp' || mode === 'c++' || mode === 'c') {
- mode = 'c_cpp';
- }
-
- this.ace_container = document.createElement('div');
- this.ace_container.style.width = '100%';
- this.ace_container.style.position = 'relative';
- this.ace_container.style.height = '400px';
- this.input.parentNode.insertBefore(this.ace_container, this.input);
- this.input.style.display = 'none';
- this.ace_editor = window.ace.edit(this.ace_container);
-
- this.ace_editor.setValue(this.getValue());
-
- // The theme
- if (JSONEditor.plugins.ace.theme)
- this.ace_editor.setTheme('ace/theme/' + JSONEditor.plugins.ace.theme);
- // The mode
- mode = window.ace.require('ace/mode/' + mode);
- if (mode) this.ace_editor.getSession().setMode(new mode.Mode());
-
- // Listen for changes
- this.ace_editor.on('change', function () {
- var val = self.ace_editor.getValue();
- self.input.value = val;
- self.refreshValue();
- self.is_dirty = true;
- self.onChange(true);
- });
- }
- }
-
- self.theme.afterInputReady(self.input);
- },
- refreshValue: function () {
- this.value = this.input.value;
- if (typeof this.value !== 'string') this.value = '';
- this.serialized = this.value;
- },
- destroy: function () {
- // If using SCEditor, destroy the editor instance
- if (this.sceditor_instance) {
- this.sceditor_instance.destroy();
- } else if (this.epiceditor) {
- this.epiceditor.unload();
- } else if (this.ace_editor) {
- this.ace_editor.destroy();
- }
-
- this.template = null;
- if (this.input && this.input.parentNode) this.input.parentNode.removeChild(this.input);
- if (this.label && this.label.parentNode) this.label.parentNode.removeChild(this.label);
- if (this.description && this.description.parentNode)
- this.description.parentNode.removeChild(this.description);
-
- this._super();
- },
- /**
- * This is overridden in derivative editors
- */
- sanitize: function (value) {
- return value;
- },
- /**
- * Re-calculates the value if needed
- */
- onWatchedFieldChange: function () {
- var self = this,
- vars,
- j;
-
- // If this editor needs to be rendered by a macro template
- if (this.template) {
- vars = this.getWatchedFieldValues();
- this.setValue(this.template(vars), false, true);
- }
-
- this._super();
- },
- showValidationErrors: function (errors) {
- var self = this;
-
- if (this.jsoneditor.options.show_errors === 'always') {
- } else if (
- !this.is_dirty &&
- this.previous_error_setting === this.jsoneditor.options.show_errors
- )
- return;
-
- this.previous_error_setting = this.jsoneditor.options.show_errors;
-
- var messages = [];
- $each(errors, function (i, error) {
- if (error.path === self.path) {
- messages.push(error.message);
- }
- });
-
- if (messages.length) {
- this.theme.addInputError(this.input, messages.join('. ') + '.');
- } else {
- this.theme.removeInputError(this.input);
- }
- },
- });
-
- JSONEditor.defaults.editors.number = JSONEditor.defaults.editors.string.extend({
- sanitize: function (value) {
- return (value + '').replace(/[^0-9\.\-eE]/g, '');
- },
- getNumColumns: function () {
- return 2;
- },
- getValue: function () {
- return this.value * 1;
- },
- });
-
- JSONEditor.defaults.editors.integer = JSONEditor.defaults.editors.number.extend({
- sanitize: function (value) {
- value = value + '';
- return value.replace(/[^0-9\-]/g, '');
- },
- getNumColumns: function () {
- return 2;
- },
- });
-
- JSONEditor.defaults.editors.object = JSONEditor.AbstractEditor.extend({
- getDefault: function () {
- return $extend({}, this.schema['default'] || {});
- },
- getChildEditors: function () {
- return this.editors;
- },
- register: function () {
- this._super();
- if (this.editors) {
- for (var i in this.editors) {
- if (!this.editors.hasOwnProperty(i)) continue;
- this.editors[i].register();
- }
- }
- },
- unregister: function () {
- this._super();
- if (this.editors) {
- for (var i in this.editors) {
- if (!this.editors.hasOwnProperty(i)) continue;
- this.editors[i].unregister();
- }
- }
- },
- getNumColumns: function () {
- return Math.max(Math.min(12, this.maxwidth), 3);
- },
- enable: function () {
- if (this.editjson_button) this.editjson_button.disabled = false;
- if (this.addproperty_button) this.addproperty_button.disabled = false;
-
- this._super();
- if (this.editors) {
- for (var i in this.editors) {
- if (!this.editors.hasOwnProperty(i)) continue;
- this.editors[i].enable();
- }
- }
- },
- disable: function () {
- if (this.editjson_button) this.editjson_button.disabled = true;
- if (this.addproperty_button) this.addproperty_button.disabled = true;
- this.hideEditJSON();
-
- this._super();
- if (this.editors) {
- for (var i in this.editors) {
- if (!this.editors.hasOwnProperty(i)) continue;
- this.editors[i].disable();
- }
- }
- },
- layoutEditors: function () {
- var self = this,
- i,
- j;
-
- if (!this.row_container) return;
-
- // Sort editors by propertyOrder
- this.property_order = Object.keys(this.editors);
- this.property_order = this.property_order.sort(function (a, b) {
- var ordera = self.editors[a].schema.propertyOrder;
- var orderb = self.editors[b].schema.propertyOrder;
- if (typeof ordera !== 'number') ordera = 1000;
- if (typeof orderb !== 'number') orderb = 1000;
-
- return ordera - orderb;
- });
-
- var container;
-
- if (this.format === 'grid') {
- var rows = [];
- $each(this.property_order, function (j, key) {
- var editor = self.editors[key];
- if (editor.property_removed) return;
- var found = false;
- var width = editor.options.hidden
- ? 0
- : editor.options.grid_columns || editor.getNumColumns();
- var height = editor.options.hidden ? 0 : editor.container.offsetHeight;
- // See if the editor will fit in any of the existing rows first
- for (var i = 0; i < rows.length; i++) {
- // If the editor will fit in the row horizontally
- if (rows[i].width + width <= 12) {
- // If the editor is close to the other elements in height
- // i.e. Don't put a really tall editor in an otherwise short row or vice versa
- if (
- !height ||
- (rows[i].minh * 0.5 < height && rows[i].maxh * 2 > height)
- ) {
- found = i;
- }
- }
- }
-
- // If there isn't a spot in any of the existing rows, start a new row
- if (found === false) {
- rows.push({
- width: 0,
- minh: 999999,
- maxh: 0,
- editors: [],
- });
- found = rows.length - 1;
- }
-
- rows[found].editors.push({
- key: key,
- //editor: editor,
- width: width,
- height: height,
- });
- rows[found].width += width;
- rows[found].minh = Math.min(rows[found].minh, height);
- rows[found].maxh = Math.max(rows[found].maxh, height);
- });
-
- // Make almost full rows width 12
- // Do this by increasing all editors' sizes proprotionately
- // Any left over space goes to the biggest editor
- // Don't touch rows with a width of 6 or less
- for (i = 0; i < rows.length; i++) {
- if (rows[i].width < 12) {
- var biggest = false;
- var new_width = 0;
- for (j = 0; j < rows[i].editors.length; j++) {
- if (biggest === false) biggest = j;
- else if (rows[i].editors[j].width > rows[i].editors[biggest].width)
- biggest = j;
- rows[i].editors[j].width *= 12 / rows[i].width;
- rows[i].editors[j].width = Math.floor(rows[i].editors[j].width);
- new_width += rows[i].editors[j].width;
- }
- if (new_width < 12) rows[i].editors[biggest].width += 12 - new_width;
- rows[i].width = 12;
- }
- }
-
- // layout hasn't changed
- if (this.layout === JSON.stringify(rows)) return false;
- this.layout = JSON.stringify(rows);
-
- // Layout the form
- container = document.createElement('div');
- for (i = 0; i < rows.length; i++) {
- var row = this.theme.getGridRow();
- container.appendChild(row);
- for (j = 0; j < rows[i].editors.length; j++) {
- var key = rows[i].editors[j].key;
- var editor = this.editors[key];
-
- if (editor.options.hidden) editor.container.style.display = 'none';
- else
- this.theme.setGridColumnSize(
- editor.container,
- rows[i].editors[j].width
- );
- row.appendChild(editor.container);
- }
- }
- }
- // Normal layout
- else {
- container = document.createElement('div');
- $each(this.property_order, function (i, key) {
- var editor = self.editors[key];
- if (editor.property_removed) return;
- var row = self.theme.getGridRow();
- container.appendChild(row);
-
- if (editor.options.hidden) editor.container.style.display = 'none';
- else self.theme.setGridColumnSize(editor.container, 12);
- row.appendChild(editor.container);
- });
- }
- this.row_container.innerHTML = '';
- this.row_container.appendChild(container);
- },
- getPropertySchema: function (key) {
- // Schema declared directly in properties
- var schema = this.schema.properties[key] || {};
- schema = $extend({}, schema);
- var matched = this.schema.properties[key] ? true : false;
-
- // Any matching patternProperties should be merged in
- if (this.schema.patternProperties) {
- for (var i in this.schema.patternProperties) {
- if (!this.schema.patternProperties.hasOwnProperty(i)) continue;
- var regex = new RegExp(i);
- if (regex.test(key)) {
- schema.allOf = schema.allOf || [];
- schema.allOf.push(this.schema.patternProperties[i]);
- matched = true;
- }
- }
- }
-
- // Hasn't matched other rules, use additionalProperties schema
- if (
- !matched &&
- this.schema.additionalProperties &&
- typeof this.schema.additionalProperties === 'object'
- ) {
- schema = $extend({}, this.schema.additionalProperties);
- }
-
- return schema;
- },
- preBuild: function () {
- this._super();
-
- this.editors = {};
- this.cached_editors = {};
- var self = this;
-
- this.format =
- this.options.layout ||
- this.options.object_layout ||
- this.schema.format ||
- this.jsoneditor.options.object_layout ||
- 'normal';
-
- this.schema.properties = this.schema.properties || {};
-
- this.minwidth = 0;
- this.maxwidth = 0;
-
- // If the object should be rendered as a table row
- if (this.options.table_row) {
- $each(this.schema.properties, function (key, schema) {
- var editor = self.jsoneditor.getEditorClass(schema);
- self.editors[key] = self.jsoneditor.createEditor(editor, {
- jsoneditor: self.jsoneditor,
- schema: schema,
- path: self.path + '.' + key,
- parent: self,
- compact: true,
- required: true,
- });
- self.editors[key].preBuild();
-
- var width = self.editors[key].options.hidden
- ? 0
- : self.editors[key].options.grid_columns ||
- self.editors[key].getNumColumns();
-
- self.minwidth += width;
- self.maxwidth += width;
- });
- this.no_link_holder = true;
- }
- // If the object should be rendered as a table
- else if (this.options.table) {
- // TODO: table display format
- throw 'Not supported yet';
- }
- // If the object should be rendered as a div
- else {
- if (!this.schema.defaultProperties) {
- if (
- this.jsoneditor.options.display_required_only ||
- this.options.display_required_only
- ) {
- this.schema.defaultProperties = [];
- $each(this.schema.properties, function (k, s) {
- if (self.isRequired({ key: k, schema: s })) {
- self.schema.defaultProperties.push(k);
- }
- });
- } else {
- self.schema.defaultProperties = Object.keys(self.schema.properties);
- }
- }
-
- // Increase the grid width to account for padding
- self.maxwidth += 1;
-
- $each(this.schema.defaultProperties, function (i, key) {
- self.addObjectProperty(key, true);
-
- if (self.editors[key]) {
- self.minwidth = Math.max(
- self.minwidth,
- self.editors[key].options.grid_columns ||
- self.editors[key].getNumColumns()
- );
- self.maxwidth +=
- self.editors[key].options.grid_columns ||
- self.editors[key].getNumColumns();
- }
- });
- }
-
- // Sort editors by propertyOrder
- this.property_order = Object.keys(this.editors);
- this.property_order = this.property_order.sort(function (a, b) {
- var ordera = self.editors[a].schema.propertyOrder;
- var orderb = self.editors[b].schema.propertyOrder;
- if (typeof ordera !== 'number') ordera = 1000;
- if (typeof orderb !== 'number') orderb = 1000;
-
- return ordera - orderb;
- });
- },
- build: function () {
- var self = this;
-
- // If the object should be rendered as a table row
- if (this.options.table_row) {
- this.editor_holder = this.container;
- $each(this.editors, function (key, editor) {
- var holder = self.theme.getTableCell();
- self.editor_holder.appendChild(holder);
-
- editor.setContainer(holder);
- editor.build();
- editor.postBuild();
-
- if (self.editors[key].options.hidden) {
- holder.style.display = 'none';
- }
- if (self.editors[key].options.input_width) {
- holder.style.width = self.editors[key].options.input_width;
- }
- });
- }
- // If the object should be rendered as a table
- else if (this.options.table) {
- // TODO: table display format
- throw 'Not supported yet';
- }
- // If the object should be rendered as a div
- else {
- this.header = document.createElement('span');
- this.header.textContent = this.getTitle();
- this.title = this.theme.getHeader(this.header);
- this.container.appendChild(this.title);
- this.container.style.position = 'relative';
-
- // Edit JSON modal
- this.editjson_holder = this.theme.getModal();
- this.editjson_textarea = this.theme.getTextareaInput();
- this.editjson_textarea.style.height = '170px';
- this.editjson_textarea.style.width = '300px';
- this.editjson_textarea.style.display = 'block';
- this.editjson_save = this.getButton('Save', 'save', 'Save');
- this.editjson_save.addEventListener('click', function (e) {
- e.preventDefault();
- e.stopPropagation();
- self.saveJSON();
- });
- this.editjson_cancel = this.getButton('Cancel', 'cancel', 'Cancel');
- this.editjson_cancel.addEventListener('click', function (e) {
- e.preventDefault();
- e.stopPropagation();
- self.hideEditJSON();
- });
- this.editjson_holder.appendChild(this.editjson_textarea);
- this.editjson_holder.appendChild(this.editjson_save);
- this.editjson_holder.appendChild(this.editjson_cancel);
-
- // Manage Properties modal
- this.addproperty_holder = this.theme.getModal();
- this.addproperty_list = document.createElement('div');
- this.addproperty_list.style.width = '295px';
- this.addproperty_list.style.maxHeight = '160px';
- this.addproperty_list.style.padding = '5px 0';
- this.addproperty_list.style.overflowY = 'auto';
- this.addproperty_list.style.overflowX = 'hidden';
- this.addproperty_list.style.paddingLeft = '5px';
- this.addproperty_list.setAttribute('class', 'property-selector');
- this.addproperty_add = this.getButton('add', 'add', 'add');
- this.addproperty_input = this.theme.getFormInputField('text');
- this.addproperty_input.setAttribute('placeholder', 'Property name...');
- this.addproperty_input.style.width = '220px';
- this.addproperty_input.style.marginBottom = '0';
- this.addproperty_input.style.display = 'inline-block';
- this.addproperty_add.addEventListener('click', function (e) {
- e.preventDefault();
- e.stopPropagation();
- if (self.addproperty_input.value) {
- if (self.editors[self.addproperty_input.value]) {
- window.alert('there is already a property with that name');
- return;
- }
-
- self.addObjectProperty(self.addproperty_input.value);
- if (self.editors[self.addproperty_input.value]) {
- self.editors[self.addproperty_input.value].disable();
- }
- self.onChange(true);
- }
- });
- this.addproperty_holder.appendChild(this.addproperty_list);
- this.addproperty_holder.appendChild(this.addproperty_input);
- this.addproperty_holder.appendChild(this.addproperty_add);
- var spacer = document.createElement('div');
- spacer.style.clear = 'both';
- this.addproperty_holder.appendChild(spacer);
-
- // Description
- if (this.schema.description) {
- this.description = this.theme.getDescription(this.schema.description);
- this.container.appendChild(this.description);
- }
-
- // Validation error placeholder area
- this.error_holder = document.createElement('div');
- this.container.appendChild(this.error_holder);
-
- // Container for child editor area
- this.editor_holder = this.theme.getIndentedPanel();
- this.container.appendChild(this.editor_holder);
-
- // Container for rows of child editors
- this.row_container = this.theme.getGridContainer();
- this.editor_holder.appendChild(this.row_container);
-
- $each(this.editors, function (key, editor) {
- var holder = self.theme.getGridColumn();
- self.row_container.appendChild(holder);
-
- editor.setContainer(holder);
- editor.build();
- editor.postBuild();
- });
-
- // Control buttons
- this.title_controls = this.theme.getHeaderButtonHolder();
- this.editjson_controls = this.theme.getHeaderButtonHolder();
- this.addproperty_controls = this.theme.getHeaderButtonHolder();
- this.title.appendChild(this.title_controls);
- this.title.appendChild(this.editjson_controls);
- this.title.appendChild(this.addproperty_controls);
-
- // Show/Hide button
- this.collapsed = false;
- this.toggle_button = this.getButton(
- '',
- 'collapse',
- this.translate('button_collapse')
- );
- this.title_controls.appendChild(this.toggle_button);
- this.toggle_button.addEventListener('click', function (e) {
- e.preventDefault();
- e.stopPropagation();
- if (self.collapsed) {
- self.editor_holder.style.display = '';
- self.collapsed = false;
- self.setButtonText(
- self.toggle_button,
- '',
- 'collapse',
- self.translate('button_collapse')
- );
- } else {
- self.editor_holder.style.display = 'none';
- self.collapsed = true;
- self.setButtonText(
- self.toggle_button,
- '',
- 'expand',
- self.translate('button_expand')
- );
- }
- });
-
- // If it should start collapsed
- if (this.options.collapsed) {
- $trigger(this.toggle_button, 'click');
- }
-
- // Collapse button disabled
- if (
- this.schema.options &&
- typeof this.schema.options.disable_collapse !== 'undefined'
- ) {
- if (this.schema.options.disable_collapse)
- this.toggle_button.style.display = 'none';
- } else if (this.jsoneditor.options.disable_collapse) {
- this.toggle_button.style.display = 'none';
- }
-
- // Edit JSON Button
- this.editjson_button = this.getButton('JSON', 'edit', 'Edit JSON');
- this.editjson_button.addEventListener('click', function (e) {
- e.preventDefault();
- e.stopPropagation();
- self.toggleEditJSON();
- });
- this.editjson_controls.appendChild(this.editjson_button);
- this.editjson_controls.appendChild(this.editjson_holder);
-
- // Edit JSON Buttton disabled
- if (
- this.schema.options &&
- typeof this.schema.options.disable_edit_json !== 'undefined'
- ) {
- if (this.schema.options.disable_edit_json)
- this.editjson_button.style.display = 'none';
- } else if (this.jsoneditor.options.disable_edit_json) {
- this.editjson_button.style.display = 'none';
- }
-
- // Object Properties Button
- this.addproperty_button = this.getButton('Properties', 'edit', 'Object Properties');
- this.addproperty_button.addEventListener('click', function (e) {
- e.preventDefault();
- e.stopPropagation();
- self.toggleAddProperty();
- });
- this.addproperty_controls.appendChild(this.addproperty_button);
- this.addproperty_controls.appendChild(this.addproperty_holder);
- this.refreshAddProperties();
- }
-
- // Fix table cell ordering
- if (this.options.table_row) {
- this.editor_holder = this.container;
- $each(this.property_order, function (i, key) {
- self.editor_holder.appendChild(self.editors[key].container);
- });
- }
- // Layout object editors in grid if needed
- else {
- // Initial layout
- this.layoutEditors();
- // Do it again now that we know the approximate heights of elements
- this.layoutEditors();
- }
- },
- showEditJSON: function () {
- if (!this.editjson_holder) return;
- this.hideAddProperty();
-
- // Position the form directly beneath the button
- // TODO: edge detection
- this.editjson_holder.style.left = this.editjson_button.offsetLeft + 'px';
- this.editjson_holder.style.top =
- this.editjson_button.offsetTop + this.editjson_button.offsetHeight + 'px';
-
- // Start the textarea with the current value
- this.editjson_textarea.value = JSON.stringify(this.getValue(), null, 2);
-
- // Disable the rest of the form while editing JSON
- this.disable();
-
- this.editjson_holder.style.display = '';
- this.editjson_button.disabled = false;
- this.editing_json = true;
- },
- hideEditJSON: function () {
- if (!this.editjson_holder) return;
- if (!this.editing_json) return;
-
- this.editjson_holder.style.display = 'none';
- this.enable();
- this.editing_json = false;
- },
- saveJSON: function () {
- if (!this.editjson_holder) return;
-
- try {
- var json = JSON.parse(this.editjson_textarea.value);
- this.setValue(json);
- this.hideEditJSON();
- } catch (e) {
- window.alert('invalid JSON');
- throw e;
- }
- },
- toggleEditJSON: function () {
- if (this.editing_json) this.hideEditJSON();
- else this.showEditJSON();
- },
- insertPropertyControlUsingPropertyOrder: function (property, control, container) {
- var propertyOrder;
- if (this.schema.properties[property])
- propertyOrder = this.schema.properties[property].propertyOrder;
- if (typeof propertyOrder !== 'number') propertyOrder = 1000;
- control.propertyOrder = propertyOrder;
-
- for (var i = 0; i < container.childNodes.length; i++) {
- var child = container.childNodes[i];
- if (control.propertyOrder < child.propertyOrder) {
- this.addproperty_list.insertBefore(control, child);
- control = null;
- break;
- }
- }
- if (control) {
- this.addproperty_list.appendChild(control);
- }
- },
- addPropertyCheckbox: function (key) {
- var self = this;
- var checkbox, label, labelText, control;
-
- checkbox = self.theme.getCheckbox();
- checkbox.style.width = 'auto';
-
- if (this.schema.properties[key] && this.schema.properties[key].title)
- labelText = this.schema.properties[key].title;
- else labelText = key;
-
- label = self.theme.getCheckboxLabel(labelText);
-
- control = self.theme.getFormControl(label, checkbox);
- control.style.paddingBottom =
- control.style.marginBottom =
- control.style.paddingTop =
- control.style.marginTop =
- 0;
- control.style.height = 'auto';
- //control.style.overflowY = 'hidden';
-
- this.insertPropertyControlUsingPropertyOrder(key, control, this.addproperty_list);
-
- checkbox.checked = key in this.editors;
- checkbox.addEventListener('change', function () {
- if (checkbox.checked) {
- self.addObjectProperty(key);
- } else {
- self.removeObjectProperty(key);
- }
- self.onChange(true);
- });
- self.addproperty_checkboxes[key] = checkbox;
-
- return checkbox;
- },
- showAddProperty: function () {
- if (!this.addproperty_holder) return;
- this.hideEditJSON();
-
- // Position the form directly beneath the button
- // TODO: edge detection
- this.addproperty_holder.style.left = this.addproperty_button.offsetLeft + 'px';
- this.addproperty_holder.style.top =
- this.addproperty_button.offsetTop + this.addproperty_button.offsetHeight + 'px';
-
- // Disable the rest of the form while editing JSON
- this.disable();
-
- this.adding_property = true;
- this.addproperty_button.disabled = false;
- this.addproperty_holder.style.display = '';
- this.refreshAddProperties();
- },
- hideAddProperty: function () {
- if (!this.addproperty_holder) return;
- if (!this.adding_property) return;
-
- this.addproperty_holder.style.display = 'none';
- this.enable();
-
- this.adding_property = false;
- },
- toggleAddProperty: function () {
- if (this.adding_property) this.hideAddProperty();
- else this.showAddProperty();
- },
- removeObjectProperty: function (property) {
- if (this.editors[property]) {
- this.editors[property].unregister();
- delete this.editors[property];
-
- this.refreshValue();
- this.layoutEditors();
- }
- },
- addObjectProperty: function (name, prebuild_only) {
- var self = this;
-
- // Property is already added
- if (this.editors[name]) return;
-
- // Property was added before and is cached
- if (this.cached_editors[name]) {
- this.editors[name] = this.cached_editors[name];
- if (prebuild_only) return;
- this.editors[name].register();
- }
- // New property
- else {
- if (
- !this.canHaveAdditionalProperties() &&
- (!this.schema.properties || !this.schema.properties[name])
- ) {
- return;
- }
-
- var schema = self.getPropertySchema(name);
-
- // Add the property
- var editor = self.jsoneditor.getEditorClass(schema);
-
- self.editors[name] = self.jsoneditor.createEditor(editor, {
- jsoneditor: self.jsoneditor,
- schema: schema,
- path: self.path + '.' + name,
- parent: self,
- });
- self.editors[name].preBuild();
-
- if (!prebuild_only) {
- var holder = self.theme.getChildEditorHolder();
- self.editor_holder.appendChild(holder);
- self.editors[name].setContainer(holder);
- self.editors[name].build();
- self.editors[name].postBuild();
- }
-
- self.cached_editors[name] = self.editors[name];
- }
-
- // If we're only prebuilding the editors, don't refresh values
- if (!prebuild_only) {
- self.refreshValue();
- self.layoutEditors();
- }
- },
- onChildEditorChange: function (editor) {
- this.refreshValue();
- this._super(editor);
- },
- canHaveAdditionalProperties: function () {
- if (typeof this.schema.additionalProperties === 'boolean') {
- return this.schema.additionalProperties;
- }
- return !this.jsoneditor.options.no_additional_properties;
- },
- destroy: function () {
- $each(this.cached_editors, function (i, el) {
- el.destroy();
- });
- if (this.editor_holder) this.editor_holder.innerHTML = '';
- if (this.title && this.title.parentNode) this.title.parentNode.removeChild(this.title);
- if (this.error_holder && this.error_holder.parentNode)
- this.error_holder.parentNode.removeChild(this.error_holder);
-
- this.editors = null;
- this.cached_editors = null;
- if (this.editor_holder && this.editor_holder.parentNode)
- this.editor_holder.parentNode.removeChild(this.editor_holder);
- this.editor_holder = null;
-
- this._super();
- },
- getValue: function () {
- var result = this._super();
- if (
- this.jsoneditor.options.remove_empty_properties ||
- this.options.remove_empty_properties
- ) {
- for (var i in result) {
- if (result.hasOwnProperty(i)) {
- if (!result[i]) delete result[i];
- }
- }
- }
- return result;
- },
- refreshValue: function () {
- this.value = {};
- var self = this;
-
- for (var i in this.editors) {
- if (!this.editors.hasOwnProperty(i)) continue;
- this.value[i] = this.editors[i].getValue();
- }
-
- if (this.adding_property) this.refreshAddProperties();
- },
- refreshAddProperties: function () {
- if (
- this.options.disable_properties ||
- (this.options.disable_properties !== false &&
- this.jsoneditor.options.disable_properties)
- ) {
- this.addproperty_controls.style.display = 'none';
- return;
- }
-
- var can_add = false,
- can_remove = false,
- num_props = 0,
- i,
- show_modal = false;
-
- // Get number of editors
- for (i in this.editors) {
- if (!this.editors.hasOwnProperty(i)) continue;
- num_props++;
- }
-
- // Determine if we can add back removed properties
- can_add =
- this.canHaveAdditionalProperties() &&
- !(
- typeof this.schema.maxProperties !== 'undefined' &&
- num_props >= this.schema.maxProperties
- );
-
- if (this.addproperty_checkboxes) {
- this.addproperty_list.innerHTML = '';
- }
- this.addproperty_checkboxes = {};
-
- // Check for which editors can't be removed or added back
- for (i in this.cached_editors) {
- if (!this.cached_editors.hasOwnProperty(i)) continue;
-
- this.addPropertyCheckbox(i);
-
- if (this.isRequired(this.cached_editors[i]) && i in this.editors) {
- this.addproperty_checkboxes[i].disabled = true;
- }
-
- if (
- typeof this.schema.minProperties !== 'undefined' &&
- num_props <= this.schema.minProperties
- ) {
- this.addproperty_checkboxes[i].disabled =
- this.addproperty_checkboxes[i].checked;
- if (!this.addproperty_checkboxes[i].checked) show_modal = true;
- } else if (!(i in this.editors)) {
- if (!can_add && !this.schema.properties.hasOwnProperty(i)) {
- this.addproperty_checkboxes[i].disabled = true;
- } else {
- this.addproperty_checkboxes[i].disabled = false;
- show_modal = true;
- }
- } else {
- show_modal = true;
- can_remove = true;
- }
- }
-
- if (this.canHaveAdditionalProperties()) {
- show_modal = true;
- }
-
- // Additional addproperty checkboxes not tied to a current editor
- for (i in this.schema.properties) {
- if (!this.schema.properties.hasOwnProperty(i)) continue;
- if (this.cached_editors[i]) continue;
- show_modal = true;
- this.addPropertyCheckbox(i);
- }
-
- // If no editors can be added or removed, hide the modal button
- if (!show_modal) {
- this.hideAddProperty();
- this.addproperty_controls.style.display = 'none';
- }
- // If additional properties are disabled
- else if (!this.canHaveAdditionalProperties()) {
- this.addproperty_add.style.display = 'none';
- this.addproperty_input.style.display = 'none';
- }
- // If no new properties can be added
- else if (!can_add) {
- this.addproperty_add.disabled = true;
- }
- // If new properties can be added
- else {
- this.addproperty_add.disabled = false;
- }
- },
- isRequired: function (editor) {
- if (typeof editor.schema.required === 'boolean') return editor.schema.required;
- else if (Array.isArray(this.schema.required))
- return this.schema.required.indexOf(editor.key) > -1;
- else if (this.jsoneditor.options.required_by_default) return true;
- else return false;
- },
- setValue: function (value, initial) {
- var self = this;
- value = value || {};
-
- if (typeof value !== 'object' || Array.isArray(value)) value = {};
-
- // First, set the values for all of the defined properties
- $each(this.cached_editors, function (i, editor) {
- // Value explicitly set
- if (typeof value[i] !== 'undefined') {
- self.addObjectProperty(i);
- editor.setValue(value[i], initial);
- }
- // Otherwise, remove value unless this is the initial set or it's required
- else if (!initial && !self.isRequired(editor)) {
- self.removeObjectProperty(i);
- }
- // Otherwise, set the value to the default
- else {
- editor.setValue(editor.getDefault(), initial);
- }
- });
-
- $each(value, function (i, val) {
- if (!self.cached_editors[i]) {
- self.addObjectProperty(i);
- if (self.editors[i]) self.editors[i].setValue(val, initial);
- }
- });
-
- this.refreshValue();
- this.layoutEditors();
- this.onChange();
- },
- showValidationErrors: function (errors) {
- var self = this;
-
- // Get all the errors that pertain to this editor
- var my_errors = [];
- var other_errors = [];
- $each(errors, function (i, error) {
- if (error.path === self.path) {
- my_errors.push(error);
- } else {
- other_errors.push(error);
- }
- });
-
- // Show errors for this editor
- if (this.error_holder) {
- if (my_errors.length) {
- var message = [];
- this.error_holder.innerHTML = '';
- this.error_holder.style.display = '';
- $each(my_errors, function (i, error) {
- self.error_holder.appendChild(self.theme.getErrorMessage(error.message));
- });
- }
- // Hide error area
- else {
- this.error_holder.style.display = 'none';
- }
- }
-
- // Show error for the table row if this is inside a table
- if (this.options.table_row) {
- if (my_errors.length) {
- this.theme.addTableRowError(this.container);
- } else {
- this.theme.removeTableRowError(this.container);
- }
- }
-
- // Show errors for child editors
- $each(this.editors, function (i, editor) {
- editor.showValidationErrors(other_errors);
- });
- },
- });
-
- JSONEditor.defaults.editors.array = JSONEditor.AbstractEditor.extend({
- getDefault: function () {
- return this.schema['default'] || [];
- },
- register: function () {
- this._super();
- if (this.rows) {
- for (var i = 0; i < this.rows.length; i++) {
- this.rows[i].register();
- }
- }
- },
- unregister: function () {
- this._super();
- if (this.rows) {
- for (var i = 0; i < this.rows.length; i++) {
- this.rows[i].unregister();
- }
- }
- },
- getNumColumns: function () {
- var info = this.getItemInfo(0);
- // Tabs require extra horizontal space
- if (this.tabs_holder) {
- return Math.max(Math.min(12, info.width + 2), 4);
- } else {
- return info.width;
- }
- },
- enable: function () {
- if (this.add_row_button) this.add_row_button.disabled = false;
- if (this.remove_all_rows_button) this.remove_all_rows_button.disabled = false;
- if (this.delete_last_row_button) this.delete_last_row_button.disabled = false;
-
- if (this.rows) {
- for (var i = 0; i < this.rows.length; i++) {
- this.rows[i].enable();
-
- if (this.rows[i].moveup_button) this.rows[i].moveup_button.disabled = false;
- if (this.rows[i].movedown_button) this.rows[i].movedown_button.disabled = false;
- if (this.rows[i].delete_button) this.rows[i].delete_button.disabled = false;
- }
- }
- this._super();
- },
- disable: function () {
- if (this.add_row_button) this.add_row_button.disabled = true;
- if (this.remove_all_rows_button) this.remove_all_rows_button.disabled = true;
- if (this.delete_last_row_button) this.delete_last_row_button.disabled = true;
-
- if (this.rows) {
- for (var i = 0; i < this.rows.length; i++) {
- this.rows[i].disable();
-
- if (this.rows[i].moveup_button) this.rows[i].moveup_button.disabled = true;
- if (this.rows[i].movedown_button) this.rows[i].movedown_button.disabled = true;
- if (this.rows[i].delete_button) this.rows[i].delete_button.disabled = true;
- }
- }
- this._super();
- },
- preBuild: function () {
- this._super();
-
- this.rows = [];
- this.row_cache = [];
-
- this.hide_delete_buttons =
- this.options.disable_array_delete || this.jsoneditor.options.disable_array_delete;
- this.hide_delete_all_rows_buttons =
- this.hide_delete_buttons ||
- this.options.disable_array_delete_all_rows ||
- this.jsoneditor.options.disable_array_delete_all_rows;
- this.hide_delete_last_row_buttons =
- this.hide_delete_buttons ||
- this.options.disable_array_delete_last_row ||
- this.jsoneditor.options.disable_array_delete_last_row;
- this.hide_move_buttons =
- this.options.disable_array_reorder || this.jsoneditor.options.disable_array_reorder;
- this.hide_add_button =
- this.options.disable_array_add || this.jsoneditor.options.disable_array_add;
- },
- build: function () {
- var self = this;
-
- if (!this.options.compact) {
- this.header = document.createElement('span');
- this.header.textContent = this.getTitle();
- this.title = this.theme.getHeader(this.header);
- this.container.appendChild(this.title);
- this.title_controls = this.theme.getHeaderButtonHolder();
- this.title.appendChild(this.title_controls);
- if (this.schema.description) {
- this.description = this.theme.getDescription(this.schema.description);
- this.container.appendChild(this.description);
- }
- this.error_holder = document.createElement('div');
- this.container.appendChild(this.error_holder);
-
- if (this.schema.format === 'tabs') {
- this.controls = this.theme.getHeaderButtonHolder();
- this.title.appendChild(this.controls);
- this.tabs_holder = this.theme.getTabHolder();
- this.container.appendChild(this.tabs_holder);
- this.row_holder = this.theme.getTabContentHolder(this.tabs_holder);
-
- this.active_tab = null;
- } else {
- this.panel = this.theme.getIndentedPanel();
- this.container.appendChild(this.panel);
- this.row_holder = document.createElement('div');
- this.panel.appendChild(this.row_holder);
- this.controls = this.theme.getButtonHolder();
- this.panel.appendChild(this.controls);
- }
- } else {
- this.panel = this.theme.getIndentedPanel();
- this.container.appendChild(this.panel);
- this.controls = this.theme.getButtonHolder();
- this.panel.appendChild(this.controls);
- this.row_holder = document.createElement('div');
- this.panel.appendChild(this.row_holder);
- }
-
- // Add controls
- this.addControls();
- },
- onChildEditorChange: function (editor) {
- this.refreshValue();
- this.refreshTabs(true);
- this._super(editor);
- },
- getItemTitle: function () {
- if (!this.item_title) {
- if (this.schema.items && !Array.isArray(this.schema.items)) {
- var tmp = this.jsoneditor.expandRefs(this.schema.items);
- this.item_title = tmp.title || 'item';
- } else {
- this.item_title = 'item';
- }
- }
- return this.item_title;
- },
- getItemSchema: function (i) {
- if (Array.isArray(this.schema.items)) {
- if (i >= this.schema.items.length) {
- if (this.schema.additionalItems === true) {
- return {};
- } else if (this.schema.additionalItems) {
- return $extend({}, this.schema.additionalItems);
- }
- } else {
- return $extend({}, this.schema.items[i]);
- }
- } else if (this.schema.items) {
- return $extend({}, this.schema.items);
- } else {
- return {};
- }
- },
- getItemInfo: function (i) {
- var schema = this.getItemSchema(i);
-
- // Check if it's cached
- this.item_info = this.item_info || {};
- var stringified = JSON.stringify(schema);
- if (typeof this.item_info[stringified] !== 'undefined')
- return this.item_info[stringified];
-
- // Get the schema for this item
- schema = this.jsoneditor.expandRefs(schema);
-
- this.item_info[stringified] = {
- title: schema.title || 'item',
- default: schema['default'],
- width: 12,
- child_editors: schema.properties || schema.items,
- };
-
- return this.item_info[stringified];
- },
- getElementEditor: function (i) {
- var item_info = this.getItemInfo(i);
- var schema = this.getItemSchema(i);
- schema = this.jsoneditor.expandRefs(schema);
- schema.title = item_info.title + ' ' + (i + 1);
-
- var editor = this.jsoneditor.getEditorClass(schema);
-
- var holder;
- if (this.tabs_holder) {
- holder = this.theme.getTabContent();
- } else if (item_info.child_editors) {
- holder = this.theme.getChildEditorHolder();
- } else {
- holder = this.theme.getIndentedPanel();
- }
-
- this.row_holder.appendChild(holder);
-
- var ret = this.jsoneditor.createEditor(editor, {
- jsoneditor: this.jsoneditor,
- schema: schema,
- container: holder,
- path: this.path + '.' + i,
- parent: this,
- required: true,
- });
- ret.preBuild();
- ret.build();
- ret.postBuild();
-
- if (!ret.title_controls) {
- ret.array_controls = this.theme.getButtonHolder();
- holder.appendChild(ret.array_controls);
- }
-
- return ret;
- },
- destroy: function () {
- this.empty(true);
- if (this.title && this.title.parentNode) this.title.parentNode.removeChild(this.title);
- if (this.description && this.description.parentNode)
- this.description.parentNode.removeChild(this.description);
- if (this.row_holder && this.row_holder.parentNode)
- this.row_holder.parentNode.removeChild(this.row_holder);
- if (this.controls && this.controls.parentNode)
- this.controls.parentNode.removeChild(this.controls);
- if (this.panel && this.panel.parentNode) this.panel.parentNode.removeChild(this.panel);
-
- this.rows =
- this.row_cache =
- this.title =
- this.description =
- this.row_holder =
- this.panel =
- this.controls =
- null;
-
- this._super();
- },
- empty: function (hard) {
- if (!this.rows) return;
- var self = this;
- $each(this.rows, function (i, row) {
- if (hard) {
- if (row.tab && row.tab.parentNode) row.tab.parentNode.removeChild(row.tab);
- self.destroyRow(row, true);
- self.row_cache[i] = null;
- }
- self.rows[i] = null;
- });
- self.rows = [];
- if (hard) self.row_cache = [];
- },
- destroyRow: function (row, hard) {
- var holder = row.container;
- if (hard) {
- row.destroy();
- if (holder.parentNode) holder.parentNode.removeChild(holder);
- if (row.tab && row.tab.parentNode) row.tab.parentNode.removeChild(row.tab);
- } else {
- if (row.tab) row.tab.style.display = 'none';
- holder.style.display = 'none';
- row.unregister();
- }
- },
- getMax: function () {
- if (Array.isArray(this.schema.items) && this.schema.additionalItems === false) {
- return Math.min(this.schema.items.length, this.schema.maxItems || Infinity);
- } else {
- return this.schema.maxItems || Infinity;
- }
- },
- refreshTabs: function (refresh_headers) {
- var self = this;
- $each(this.rows, function (i, row) {
- if (!row.tab) return;
-
- if (refresh_headers) {
- row.tab_text.textContent = row.getHeaderText();
- } else {
- if (row.tab === self.active_tab) {
- self.theme.markTabActive(row.tab);
- row.container.style.display = '';
- } else {
- self.theme.markTabInactive(row.tab);
- row.container.style.display = 'none';
- }
- }
- });
- },
- setValue: function (value, initial) {
- // Update the array's value, adding/removing rows when necessary
- value = value || [];
-
- if (!Array.isArray(value)) value = [value];
-
- var serialized = JSON.stringify(value);
- if (serialized === this.serialized) return;
-
- // Make sure value has between minItems and maxItems items in it
- if (this.schema.minItems) {
- while (value.length < this.schema.minItems) {
- value.push(this.getItemInfo(value.length)['default']);
- }
- }
- if (this.getMax() && value.length > this.getMax()) {
- value = value.slice(0, this.getMax());
- }
-
- var self = this;
- $each(value, function (i, val) {
- if (self.rows[i]) {
- // TODO: don't set the row's value if it hasn't changed
- self.rows[i].setValue(val, initial);
- } else if (self.row_cache[i]) {
- self.rows[i] = self.row_cache[i];
- self.rows[i].setValue(val, initial);
- self.rows[i].container.style.display = '';
- if (self.rows[i].tab) self.rows[i].tab.style.display = '';
- self.rows[i].register();
- } else {
- self.addRow(val, initial);
- }
- });
-
- for (var j = value.length; j < self.rows.length; j++) {
- self.destroyRow(self.rows[j]);
- self.rows[j] = null;
- }
- self.rows = self.rows.slice(0, value.length);
-
- // Set the active tab
- var new_active_tab = null;
- $each(self.rows, function (i, row) {
- if (row.tab === self.active_tab) {
- new_active_tab = row.tab;
- return false;
- }
- });
- if (!new_active_tab && self.rows.length) new_active_tab = self.rows[0].tab;
-
- self.active_tab = new_active_tab;
-
- self.refreshValue(initial);
- self.refreshTabs(true);
- self.refreshTabs();
-
- self.onChange();
-
- // TODO: sortable
- },
- refreshValue: function (force) {
- var self = this;
- var oldi = this.value ? this.value.length : 0;
- this.value = [];
-
- $each(this.rows, function (i, editor) {
- // Get the value for this editor
- self.value[i] = editor.getValue();
- });
-
- if (oldi !== this.value.length || force) {
- // If we currently have minItems items in the array
- var minItems = this.schema.minItems && this.schema.minItems >= this.rows.length;
-
- $each(this.rows, function (i, editor) {
- // Hide the move down button for the last row
- if (editor.movedown_button) {
- if (i === self.rows.length - 1) {
- editor.movedown_button.style.display = 'none';
- } else {
- editor.movedown_button.style.display = '';
- }
- }
-
- // Hide the delete button if we have minItems items
- if (editor.delete_button) {
- if (minItems) {
- editor.delete_button.style.display = 'none';
- } else {
- editor.delete_button.style.display = '';
- }
- }
-
- // Get the value for this editor
- self.value[i] = editor.getValue();
- });
-
- var controls_needed = false;
-
- if (!this.value.length) {
- this.delete_last_row_button.style.display = 'none';
- this.remove_all_rows_button.style.display = 'none';
- } else if (this.value.length === 1) {
- this.remove_all_rows_button.style.display = 'none';
-
- // If there are minItems items in the array, or configured to hide the delete_last_row button, hide the delete button beneath the rows
- if (minItems || this.hide_delete_last_row_buttons) {
- this.delete_last_row_button.style.display = 'none';
- } else {
- this.delete_last_row_button.style.display = '';
- controls_needed = true;
- }
- } else {
- if (minItems || this.hide_delete_last_row_buttons) {
- this.delete_last_row_button.style.display = 'none';
- } else {
- this.delete_last_row_button.style.display = '';
- controls_needed = true;
- }
-
- if (minItems || this.hide_delete_all_rows_buttons) {
- this.remove_all_rows_button.style.display = 'none';
- } else {
- this.remove_all_rows_button.style.display = '';
- controls_needed = true;
- }
- }
-
- // If there are maxItems in the array, hide the add button beneath the rows
- if ((this.getMax() && this.getMax() <= this.rows.length) || this.hide_add_button) {
- this.add_row_button.style.display = 'none';
- } else {
- this.add_row_button.style.display = '';
- controls_needed = true;
- }
-
- if (!this.collapsed && controls_needed) {
- this.controls.style.display = 'inline-block';
- } else {
- this.controls.style.display = 'none';
- }
- }
- },
- addRow: function (value, initial) {
- var self = this;
- var i = this.rows.length;
-
- self.rows[i] = this.getElementEditor(i);
- self.row_cache[i] = self.rows[i];
-
- if (self.tabs_holder) {
- self.rows[i].tab_text = document.createElement('span');
- self.rows[i].tab_text.textContent = self.rows[i].getHeaderText();
- self.rows[i].tab = self.theme.getTab(self.rows[i].tab_text);
- self.rows[i].tab.addEventListener('click', function (e) {
- self.active_tab = self.rows[i].tab;
- self.refreshTabs();
- e.preventDefault();
- e.stopPropagation();
- });
-
- self.theme.addTab(self.tabs_holder, self.rows[i].tab);
- }
-
- var controls_holder = self.rows[i].title_controls || self.rows[i].array_controls;
-
- // Buttons to delete row, move row up, and move row down
- if (!self.hide_delete_buttons) {
- self.rows[i].delete_button = this.getButton(
- self.getItemTitle(),
- 'delete',
- this.translate('button_delete_row_title', [self.getItemTitle()])
- );
- self.rows[i].delete_button.className += ' delete';
- self.rows[i].delete_button.setAttribute('data-i', i);
- self.rows[i].delete_button.addEventListener('click', function (e) {
- e.preventDefault();
- e.stopPropagation();
- var i = this.getAttribute('data-i') * 1;
-
- var value = self.getValue();
-
- var newval = [];
- var new_active_tab = null;
- $each(value, function (j, row) {
- if (j === i) {
- // If the one we're deleting is the active tab
- if (self.rows[j].tab === self.active_tab) {
- // Make the next tab active if there is one
- // Note: the next tab is going to be the current tab after deletion
- if (self.rows[j + 1]) new_active_tab = self.rows[j].tab;
- // Otherwise, make the previous tab active if there is one
- else if (j) new_active_tab = self.rows[j - 1].tab;
- }
-
- return; // If this is the one we're deleting
- }
- newval.push(row);
- });
- self.setValue(newval);
- if (new_active_tab) {
- self.active_tab = new_active_tab;
- self.refreshTabs();
- }
-
- self.onChange(true);
- });
-
- if (controls_holder) {
- controls_holder.appendChild(self.rows[i].delete_button);
- }
- }
-
- if (i && !self.hide_move_buttons) {
- self.rows[i].moveup_button = this.getButton(
- '',
- 'moveup',
- this.translate('button_move_up_title')
- );
- self.rows[i].moveup_button.className += ' moveup';
- self.rows[i].moveup_button.setAttribute('data-i', i);
- self.rows[i].moveup_button.addEventListener('click', function (e) {
- e.preventDefault();
- e.stopPropagation();
- var i = this.getAttribute('data-i') * 1;
-
- if (i <= 0) return;
- var rows = self.getValue();
- var tmp = rows[i - 1];
- rows[i - 1] = rows[i];
- rows[i] = tmp;
-
- self.setValue(rows);
- self.active_tab = self.rows[i - 1].tab;
- self.refreshTabs();
-
- self.onChange(true);
- });
-
- if (controls_holder) {
- controls_holder.appendChild(self.rows[i].moveup_button);
- }
- }
-
- if (!self.hide_move_buttons) {
- self.rows[i].movedown_button = this.getButton(
- '',
- 'movedown',
- this.translate('button_move_down_title')
- );
- self.rows[i].movedown_button.className += ' movedown';
- self.rows[i].movedown_button.setAttribute('data-i', i);
- self.rows[i].movedown_button.addEventListener('click', function (e) {
- e.preventDefault();
- e.stopPropagation();
- var i = this.getAttribute('data-i') * 1;
-
- var rows = self.getValue();
- if (i >= rows.length - 1) return;
- var tmp = rows[i + 1];
- rows[i + 1] = rows[i];
- rows[i] = tmp;
-
- self.setValue(rows);
- self.active_tab = self.rows[i + 1].tab;
- self.refreshTabs();
- self.onChange(true);
- });
-
- if (controls_holder) {
- controls_holder.appendChild(self.rows[i].movedown_button);
- }
- }
-
- if (value) self.rows[i].setValue(value, initial);
- self.refreshTabs();
- },
- addControls: function () {
- var self = this;
-
- this.collapsed = false;
- this.toggle_button = this.getButton('', 'collapse', this.translate('button_collapse'));
- this.title_controls.appendChild(this.toggle_button);
- var row_holder_display = self.row_holder.style.display;
- var controls_display = self.controls.style.display;
- this.toggle_button.addEventListener('click', function (e) {
- e.preventDefault();
- e.stopPropagation();
- if (self.collapsed) {
- self.collapsed = false;
- if (self.panel) self.panel.style.display = '';
- self.row_holder.style.display = row_holder_display;
- if (self.tabs_holder) self.tabs_holder.style.display = '';
- self.controls.style.display = controls_display;
- self.setButtonText(this, '', 'collapse', self.translate('button_collapse'));
- } else {
- self.collapsed = true;
- self.row_holder.style.display = 'none';
- if (self.tabs_holder) self.tabs_holder.style.display = 'none';
- self.controls.style.display = 'none';
- if (self.panel) self.panel.style.display = 'none';
- self.setButtonText(this, '', 'expand', self.translate('button_expand'));
- }
- });
-
- // If it should start collapsed
- if (this.options.collapsed) {
- $trigger(this.toggle_button, 'click');
- }
-
- // Collapse button disabled
- if (
- this.schema.options &&
- typeof this.schema.options.disable_collapse !== 'undefined'
- ) {
- if (this.schema.options.disable_collapse) this.toggle_button.style.display = 'none';
- } else if (this.jsoneditor.options.disable_collapse) {
- this.toggle_button.style.display = 'none';
- }
-
- // Add "new row" and "delete last" buttons below editor
- this.add_row_button = this.getButton(
- this.getItemTitle(),
- 'add',
- this.translate('button_add_row_title', [this.getItemTitle()])
- );
-
- this.add_row_button.addEventListener('click', function (e) {
- e.preventDefault();
- e.stopPropagation();
- var i = self.rows.length;
- if (self.row_cache[i]) {
- self.rows[i] = self.row_cache[i];
- self.rows[i].setValue(self.rows[i].getDefault(), true);
- self.rows[i].container.style.display = '';
- if (self.rows[i].tab) self.rows[i].tab.style.display = '';
- self.rows[i].register();
- } else {
- self.addRow();
- }
- self.active_tab = self.rows[i].tab;
- self.refreshTabs();
- self.refreshValue();
- self.onChange(true);
- });
- self.controls.appendChild(this.add_row_button);
-
- this.delete_last_row_button = this.getButton(
- this.translate('button_delete_last', [this.getItemTitle()]),
- 'delete',
- this.translate('button_delete_last_title', [this.getItemTitle()])
- );
- this.delete_last_row_button.addEventListener('click', function (e) {
- e.preventDefault();
- e.stopPropagation();
- var rows = self.getValue();
-
- var new_active_tab = null;
- if (self.rows.length > 1 && self.rows[self.rows.length - 1].tab === self.active_tab)
- new_active_tab = self.rows[self.rows.length - 2].tab;
-
- rows.pop();
- self.setValue(rows);
- if (new_active_tab) {
- self.active_tab = new_active_tab;
- self.refreshTabs();
- }
- self.onChange(true);
- });
- self.controls.appendChild(this.delete_last_row_button);
-
- this.remove_all_rows_button = this.getButton(
- this.translate('button_delete_all'),
- 'delete',
- this.translate('button_delete_all_title')
- );
- this.remove_all_rows_button.addEventListener('click', function (e) {
- e.preventDefault();
- e.stopPropagation();
- self.setValue([]);
- self.onChange(true);
- });
- self.controls.appendChild(this.remove_all_rows_button);
-
- if (self.tabs) {
- this.add_row_button.style.width = '100%';
- this.add_row_button.style.textAlign = 'left';
- this.add_row_button.style.marginBottom = '3px';
-
- this.delete_last_row_button.style.width = '100%';
- this.delete_last_row_button.style.textAlign = 'left';
- this.delete_last_row_button.style.marginBottom = '3px';
-
- this.remove_all_rows_button.style.width = '100%';
- this.remove_all_rows_button.style.textAlign = 'left';
- this.remove_all_rows_button.style.marginBottom = '3px';
- }
- },
- showValidationErrors: function (errors) {
- var self = this;
-
- // Get all the errors that pertain to this editor
- var my_errors = [];
- var other_errors = [];
- $each(errors, function (i, error) {
- if (error.path === self.path) {
- my_errors.push(error);
- } else {
- other_errors.push(error);
- }
- });
-
- // Show errors for this editor
- if (this.error_holder) {
- if (my_errors.length) {
- var message = [];
- this.error_holder.innerHTML = '';
- this.error_holder.style.display = '';
- $each(my_errors, function (i, error) {
- self.error_holder.appendChild(self.theme.getErrorMessage(error.message));
- });
- }
- // Hide error area
- else {
- this.error_holder.style.display = 'none';
- }
- }
-
- // Show errors for child editors
- $each(this.rows, function (i, row) {
- row.showValidationErrors(other_errors);
- });
- },
- });
-
- JSONEditor.defaults.editors.table = JSONEditor.defaults.editors.array.extend({
- register: function () {
- this._super();
- if (this.rows) {
- for (var i = 0; i < this.rows.length; i++) {
- this.rows[i].register();
- }
- }
- },
- unregister: function () {
- this._super();
- if (this.rows) {
- for (var i = 0; i < this.rows.length; i++) {
- this.rows[i].unregister();
- }
- }
- },
- getNumColumns: function () {
- return Math.max(Math.min(12, this.width), 3);
- },
- preBuild: function () {
- var item_schema = this.jsoneditor.expandRefs(this.schema.items || {});
-
- this.item_title = item_schema.title || 'row';
- this.item_default = item_schema['default'] || null;
- this.item_has_child_editors = item_schema.properties || item_schema.items;
- this.width = 12;
- this._super();
- },
- build: function () {
- var self = this;
- this.table = this.theme.getTable();
- this.container.appendChild(this.table);
- this.thead = this.theme.getTableHead();
- this.table.appendChild(this.thead);
- this.header_row = this.theme.getTableRow();
- this.thead.appendChild(this.header_row);
- this.row_holder = this.theme.getTableBody();
- this.table.appendChild(this.row_holder);
-
- // Determine the default value of array element
- var tmp = this.getElementEditor(0, true);
- this.item_default = tmp.getDefault();
- this.width = tmp.getNumColumns() + 2;
-
- if (!this.options.compact) {
- this.title = this.theme.getHeader(this.getTitle());
- this.container.appendChild(this.title);
- this.title_controls = this.theme.getHeaderButtonHolder();
- this.title.appendChild(this.title_controls);
- if (this.schema.description) {
- this.description = this.theme.getDescription(this.schema.description);
- this.container.appendChild(this.description);
- }
- this.panel = this.theme.getIndentedPanel();
- this.container.appendChild(this.panel);
- this.error_holder = document.createElement('div');
- this.panel.appendChild(this.error_holder);
- } else {
- this.panel = document.createElement('div');
- this.container.appendChild(this.panel);
- }
-
- this.panel.appendChild(this.table);
- this.controls = this.theme.getButtonHolder();
- this.panel.appendChild(this.controls);
-
- if (this.item_has_child_editors) {
- var ce = tmp.getChildEditors();
- var order = tmp.property_order || Object.keys(ce);
- for (var i = 0; i < order.length; i++) {
- var th = self.theme.getTableHeaderCell(ce[order[i]].getTitle());
- if (ce[order[i]].options.hidden) th.style.display = 'none';
- self.header_row.appendChild(th);
- }
- } else {
- self.header_row.appendChild(self.theme.getTableHeaderCell(this.item_title));
- }
-
- tmp.destroy();
- this.row_holder.innerHTML = '';
-
- // Row Controls column
- this.controls_header_cell = self.theme.getTableHeaderCell(' ');
- self.header_row.appendChild(this.controls_header_cell);
-
- // Add controls
- this.addControls();
- },
- onChildEditorChange: function (editor) {
- this.refreshValue();
- this._super();
- },
- getItemDefault: function () {
- return $extend({}, { default: this.item_default })['default'];
- },
- getItemTitle: function () {
- return this.item_title;
- },
- getElementEditor: function (i, ignore) {
- var schema_copy = $extend({}, this.schema.items);
- var editor = this.jsoneditor.getEditorClass(schema_copy, this.jsoneditor);
- var row = this.row_holder.appendChild(this.theme.getTableRow());
- var holder = row;
- if (!this.item_has_child_editors) {
- holder = this.theme.getTableCell();
- row.appendChild(holder);
- }
-
- var ret = this.jsoneditor.createEditor(editor, {
- jsoneditor: this.jsoneditor,
- schema: schema_copy,
- container: holder,
- path: this.path + '.' + i,
- parent: this,
- compact: true,
- table_row: true,
- });
-
- ret.preBuild();
- if (!ignore) {
- ret.build();
- ret.postBuild();
-
- ret.controls_cell = row.appendChild(this.theme.getTableCell());
- ret.row = row;
- ret.table_controls = this.theme.getButtonHolder();
- ret.controls_cell.appendChild(ret.table_controls);
- ret.table_controls.style.margin = 0;
- ret.table_controls.style.padding = 0;
- }
-
- return ret;
- },
- destroy: function () {
- this.innerHTML = '';
- if (this.title && this.title.parentNode) this.title.parentNode.removeChild(this.title);
- if (this.description && this.description.parentNode)
- this.description.parentNode.removeChild(this.description);
- if (this.row_holder && this.row_holder.parentNode)
- this.row_holder.parentNode.removeChild(this.row_holder);
- if (this.table && this.table.parentNode) this.table.parentNode.removeChild(this.table);
- if (this.panel && this.panel.parentNode) this.panel.parentNode.removeChild(this.panel);
-
- this.rows =
- this.title =
- this.description =
- this.row_holder =
- this.table =
- this.panel =
- null;
-
- this._super();
- },
- setValue: function (value, initial) {
- // Update the array's value, adding/removing rows when necessary
- value = value || [];
-
- // Make sure value has between minItems and maxItems items in it
- if (this.schema.minItems) {
- while (value.length < this.schema.minItems) {
- value.push(this.getItemDefault());
- }
- }
- if (this.schema.maxItems && value.length > this.schema.maxItems) {
- value = value.slice(0, this.schema.maxItems);
- }
-
- var serialized = JSON.stringify(value);
- if (serialized === this.serialized) return;
-
- var numrows_changed = false;
-
- var self = this;
- $each(value, function (i, val) {
- if (self.rows[i]) {
- // TODO: don't set the row's value if it hasn't changed
- self.rows[i].setValue(val);
- } else {
- self.addRow(val);
- numrows_changed = true;
- }
- });
-
- for (var j = value.length; j < self.rows.length; j++) {
- var holder = self.rows[j].container;
- if (!self.item_has_child_editors) {
- self.rows[j].row.parentNode.removeChild(self.rows[j].row);
- }
- self.rows[j].destroy();
- if (holder.parentNode) holder.parentNode.removeChild(holder);
- self.rows[j] = null;
- numrows_changed = true;
- }
- self.rows = self.rows.slice(0, value.length);
-
- self.refreshValue();
- if (numrows_changed || initial) self.refreshRowButtons();
-
- self.onChange();
-
- // TODO: sortable
- },
- refreshRowButtons: function () {
- var self = this;
-
- // If we currently have minItems items in the array
- var minItems = this.schema.minItems && this.schema.minItems >= this.rows.length;
-
- var need_row_buttons = false;
- $each(this.rows, function (i, editor) {
- // Hide the move down button for the last row
- if (editor.movedown_button) {
- if (i === self.rows.length - 1) {
- editor.movedown_button.style.display = 'none';
- } else {
- need_row_buttons = true;
- editor.movedown_button.style.display = '';
- }
- }
-
- // Hide the delete button if we have minItems items
- if (editor.delete_button) {
- if (minItems) {
- editor.delete_button.style.display = 'none';
- } else {
- need_row_buttons = true;
- editor.delete_button.style.display = '';
- }
- }
-
- if (editor.moveup_button) {
- need_row_buttons = true;
- }
- });
-
- // Show/hide controls column in table
- $each(this.rows, function (i, editor) {
- if (need_row_buttons) {
- editor.controls_cell.style.display = '';
- } else {
- editor.controls_cell.style.display = 'none';
- }
- });
- if (need_row_buttons) {
- this.controls_header_cell.style.display = '';
- } else {
- this.controls_header_cell.style.display = 'none';
- }
-
- var controls_needed = false;
-
- if (!this.value.length) {
- this.delete_last_row_button.style.display = 'none';
- this.remove_all_rows_button.style.display = 'none';
- this.table.style.display = 'none';
- } else if (this.value.length === 1) {
- this.table.style.display = '';
- this.remove_all_rows_button.style.display = 'none';
-
- // If there are minItems items in the array, or configured to hide the delete_last_row button, hide the delete button beneath the rows
- if (minItems || this.hide_delete_last_row_buttons) {
- this.delete_last_row_button.style.display = 'none';
- } else {
- this.delete_last_row_button.style.display = '';
- controls_needed = true;
- }
- } else {
- this.table.style.display = '';
-
- if (minItems || this.hide_delete_last_row_buttons) {
- this.delete_last_row_button.style.display = 'none';
- } else {
- this.delete_last_row_button.style.display = '';
- controls_needed = true;
- }
-
- if (minItems || this.hide_delete_all_rows_buttons) {
- this.remove_all_rows_button.style.display = 'none';
- } else {
- this.remove_all_rows_button.style.display = '';
- controls_needed = true;
- }
- }
-
- // If there are maxItems in the array, hide the add button beneath the rows
- if (
- (this.schema.maxItems && this.schema.maxItems <= this.rows.length) ||
- this.hide_add_button
- ) {
- this.add_row_button.style.display = 'none';
- } else {
- this.add_row_button.style.display = '';
- controls_needed = true;
- }
-
- if (!controls_needed) {
- this.controls.style.display = 'none';
- } else {
- this.controls.style.display = '';
- }
- },
- refreshValue: function () {
- var self = this;
- this.value = [];
-
- $each(this.rows, function (i, editor) {
- // Get the value for this editor
- self.value[i] = editor.getValue();
- });
- this.serialized = JSON.stringify(this.value);
- },
- addRow: function (value) {
- var self = this;
- var i = this.rows.length;
-
- self.rows[i] = this.getElementEditor(i);
-
- var controls_holder = self.rows[i].table_controls;
-
- // Buttons to delete row, move row up, and move row down
- if (!this.hide_delete_buttons) {
- self.rows[i].delete_button = this.getButton(
- '',
- 'delete',
- this.translate('button_delete_row_title_short')
- );
- self.rows[i].delete_button.className += ' delete';
- self.rows[i].delete_button.setAttribute('data-i', i);
- self.rows[i].delete_button.addEventListener('click', function (e) {
- e.preventDefault();
- e.stopPropagation();
- var i = this.getAttribute('data-i') * 1;
-
- var value = self.getValue();
-
- var newval = [];
- $each(value, function (j, row) {
- if (j === i) return; // If this is the one we're deleting
- newval.push(row);
- });
- self.setValue(newval);
- self.onChange(true);
- });
- controls_holder.appendChild(self.rows[i].delete_button);
- }
-
- if (i && !this.hide_move_buttons) {
- self.rows[i].moveup_button = this.getButton(
- '',
- 'moveup',
- this.translate('button_move_up_title')
- );
- self.rows[i].moveup_button.className += ' moveup';
- self.rows[i].moveup_button.setAttribute('data-i', i);
- self.rows[i].moveup_button.addEventListener('click', function (e) {
- e.preventDefault();
- e.stopPropagation();
- var i = this.getAttribute('data-i') * 1;
-
- if (i <= 0) return;
- var rows = self.getValue();
- var tmp = rows[i - 1];
- rows[i - 1] = rows[i];
- rows[i] = tmp;
-
- self.setValue(rows);
- self.onChange(true);
- });
- controls_holder.appendChild(self.rows[i].moveup_button);
- }
-
- if (!this.hide_move_buttons) {
- self.rows[i].movedown_button = this.getButton(
- '',
- 'movedown',
- this.translate('button_move_down_title')
- );
- self.rows[i].movedown_button.className += ' movedown';
- self.rows[i].movedown_button.setAttribute('data-i', i);
- self.rows[i].movedown_button.addEventListener('click', function (e) {
- e.preventDefault();
- e.stopPropagation();
- var i = this.getAttribute('data-i') * 1;
- var rows = self.getValue();
- if (i >= rows.length - 1) return;
- var tmp = rows[i + 1];
- rows[i + 1] = rows[i];
- rows[i] = tmp;
-
- self.setValue(rows);
- self.onChange(true);
- });
- controls_holder.appendChild(self.rows[i].movedown_button);
- }
-
- if (value) self.rows[i].setValue(value);
- },
- addControls: function () {
- var self = this;
-
- this.collapsed = false;
- this.toggle_button = this.getButton('', 'collapse', this.translate('button_collapse'));
- if (this.title_controls) {
- this.title_controls.appendChild(this.toggle_button);
- this.toggle_button.addEventListener('click', function (e) {
- e.preventDefault();
- e.stopPropagation();
-
- if (self.collapsed) {
- self.collapsed = false;
- self.panel.style.display = '';
- self.setButtonText(this, '', 'collapse', self.translate('button_collapse'));
- } else {
- self.collapsed = true;
- self.panel.style.display = 'none';
- self.setButtonText(this, '', 'expand', self.translate('button_expand'));
- }
- });
-
- // If it should start collapsed
- if (this.options.collapsed) {
- $trigger(this.toggle_button, 'click');
- }
-
- // Collapse button disabled
- if (
- this.schema.options &&
- typeof this.schema.options.disable_collapse !== 'undefined'
- ) {
- if (this.schema.options.disable_collapse)
- this.toggle_button.style.display = 'none';
- } else if (this.jsoneditor.options.disable_collapse) {
- this.toggle_button.style.display = 'none';
- }
- }
-
- // Add "new row" and "delete last" buttons below editor
- this.add_row_button = this.getButton(
- this.getItemTitle(),
- 'add',
- this.translate('button_add_row_title', [this.getItemTitle()])
- );
- this.add_row_button.addEventListener('click', function (e) {
- e.preventDefault();
- e.stopPropagation();
-
- self.addRow();
- self.refreshValue();
- self.refreshRowButtons();
- self.onChange(true);
- });
- self.controls.appendChild(this.add_row_button);
-
- this.delete_last_row_button = this.getButton(
- this.translate('button_delete_last', [this.getItemTitle()]),
- 'delete',
- this.translate('button_delete_last_title', [this.getItemTitle()])
- );
- this.delete_last_row_button.addEventListener('click', function (e) {
- e.preventDefault();
- e.stopPropagation();
-
- var rows = self.getValue();
- rows.pop();
- self.setValue(rows);
- self.onChange(true);
- });
- self.controls.appendChild(this.delete_last_row_button);
-
- this.remove_all_rows_button = this.getButton(
- this.translate('button_delete_all'),
- 'delete',
- this.translate('button_delete_all_title')
- );
- this.remove_all_rows_button.addEventListener('click', function (e) {
- e.preventDefault();
- e.stopPropagation();
-
- self.setValue([]);
- self.onChange(true);
- });
- self.controls.appendChild(this.remove_all_rows_button);
- },
- });
-
- // Multiple Editor (for when `type` is an array)
- JSONEditor.defaults.editors.multiple = JSONEditor.AbstractEditor.extend({
- register: function () {
- if (this.editors) {
- for (var i = 0; i < this.editors.length; i++) {
- if (!this.editors[i]) continue;
- this.editors[i].unregister();
- }
- if (this.editors[this.type]) this.editors[this.type].register();
- }
- this._super();
- },
- unregister: function () {
- this._super();
- if (this.editors) {
- for (var i = 0; i < this.editors.length; i++) {
- if (!this.editors[i]) continue;
- this.editors[i].unregister();
- }
- }
- },
- getNumColumns: function () {
- if (!this.editors[this.type]) return 4;
- return Math.max(this.editors[this.type].getNumColumns(), 4);
- },
- enable: function () {
- if (this.editors) {
- for (var i = 0; i < this.editors.length; i++) {
- if (!this.editors[i]) continue;
- this.editors[i].enable();
- }
- }
- this.switcher.disabled = false;
- this._super();
- },
- disable: function () {
- if (this.editors) {
- for (var i = 0; i < this.editors.length; i++) {
- if (!this.editors[i]) continue;
- this.editors[i].disable();
- }
- }
- this.switcher.disabled = true;
- this._super();
- },
- switchEditor: function (i) {
- var self = this;
-
- if (!this.editors[i]) {
- this.buildChildEditor(i);
- }
-
- var current_value = self.getValue();
-
- self.type = i;
-
- self.register();
-
- $each(self.editors, function (type, editor) {
- if (!editor) return;
- if (self.type === type) {
- if (self.keep_values) editor.setValue(current_value, true);
- editor.container.style.display = '';
- } else editor.container.style.display = 'none';
- });
- self.refreshValue();
- self.refreshHeaderText();
- },
- buildChildEditor: function (i) {
- var self = this;
- var type = this.types[i];
- var holder = self.theme.getChildEditorHolder();
- self.editor_holder.appendChild(holder);
-
- var schema;
-
- if (typeof type === 'string') {
- schema = $extend({}, self.schema);
- schema.type = type;
- } else {
- schema = $extend({}, self.schema, type);
- schema = self.jsoneditor.expandRefs(schema);
-
- // If we need to merge `required` arrays
- if (
- type.required &&
- Array.isArray(type.required) &&
- self.schema.required &&
- Array.isArray(self.schema.required)
- ) {
- schema.required = self.schema.required.concat(type.required);
- }
- }
-
- var editor = self.jsoneditor.getEditorClass(schema);
-
- self.editors[i] = self.jsoneditor.createEditor(editor, {
- jsoneditor: self.jsoneditor,
- schema: schema,
- container: holder,
- path: self.path,
- parent: self,
- required: true,
- });
- self.editors[i].preBuild();
- self.editors[i].build();
- self.editors[i].postBuild();
-
- if (self.editors[i].header) self.editors[i].header.style.display = 'none';
-
- self.editors[i].option = self.switcher_options[i];
-
- holder.addEventListener('change_header_text', function () {
- self.refreshHeaderText();
- });
-
- if (i !== self.type) holder.style.display = 'none';
- },
- preBuild: function () {
- var self = this;
-
- this.types = [];
- this.type = 0;
- this.editors = [];
- this.validators = [];
-
- this.keep_values = true;
- if (typeof this.jsoneditor.options.keep_oneof_values !== 'undefined')
- this.keep_values = this.jsoneditor.options.keep_oneof_values;
- if (typeof this.options.keep_oneof_values !== 'undefined')
- this.keep_values = this.options.keep_oneof_values;
-
- if (this.schema.oneOf) {
- this.oneOf = true;
- this.types = this.schema.oneOf;
- delete this.schema.oneOf;
- } else if (this.schema.anyOf) {
- this.anyOf = true;
- this.types = this.schema.anyOf;
- delete this.schema.anyOf;
- } else {
- if (!this.schema.type || this.schema.type === 'any') {
- this.types = [
- 'string',
- 'number',
- 'integer',
- 'boolean',
- 'object',
- 'array',
- 'null',
- ];
-
- // If any of these primitive types are disallowed
- if (this.schema.disallow) {
- var disallow = this.schema.disallow;
- if (typeof disallow !== 'object' || !Array.isArray(disallow)) {
- disallow = [disallow];
- }
- var allowed_types = [];
- $each(this.types, function (i, type) {
- if (disallow.indexOf(type) === -1) allowed_types.push(type);
- });
- this.types = allowed_types;
- }
- } else if (Array.isArray(this.schema.type)) {
- this.types = this.schema.type;
- } else {
- this.types = [this.schema.type];
- }
- delete this.schema.type;
- }
-
- this.display_text = this.getDisplayText(this.types);
- },
- build: function () {
- var self = this;
- var container = this.container;
-
- this.header = this.label = this.theme.getFormInputLabel(this.getTitle());
- this.container.appendChild(this.header);
-
- this.switcher = this.theme.getSwitcher(this.display_text);
- container.appendChild(this.switcher);
- this.switcher.addEventListener('change', function (e) {
- e.preventDefault();
- e.stopPropagation();
-
- self.switchEditor(self.display_text.indexOf(this.value));
- self.onChange(true);
- });
-
- this.editor_holder = document.createElement('div');
- container.appendChild(this.editor_holder);
-
- var validator_options = {};
- if (self.jsoneditor.options.custom_validators) {
- validator_options.custom_validators = self.jsoneditor.options.custom_validators;
- }
-
- this.switcher_options = this.theme.getSwitcherOptions(this.switcher);
- $each(this.types, function (i, type) {
- self.editors[i] = false;
-
- var schema;
-
- if (typeof type === 'string') {
- schema = $extend({}, self.schema);
- schema.type = type;
- } else {
- schema = $extend({}, self.schema, type);
-
- // If we need to merge `required` arrays
- if (
- type.required &&
- Array.isArray(type.required) &&
- self.schema.required &&
- Array.isArray(self.schema.required)
- ) {
- schema.required = self.schema.required.concat(type.required);
- }
- }
-
- self.validators[i] = new JSONEditor.Validator(
- self.jsoneditor,
- schema,
- validator_options
- );
- });
-
- this.switchEditor(0);
- },
- onChildEditorChange: function (editor) {
- if (this.editors[this.type]) {
- this.refreshValue();
- this.refreshHeaderText();
- }
-
- this._super();
- },
- refreshHeaderText: function () {
- var display_text = this.getDisplayText(this.types);
- $each(this.switcher_options, function (i, option) {
- option.textContent = display_text[i];
- });
- },
- refreshValue: function () {
- this.value = this.editors[this.type].getValue();
- },
- setValue: function (val, initial) {
- // Determine type by getting the first one that validates
- var self = this;
- $each(this.validators, function (i, validator) {
- if (!validator.validate(val).length) {
- self.type = i;
- self.switcher.value = self.display_text[i];
- return false;
- }
- });
-
- this.switchEditor(this.type);
-
- this.editors[this.type].setValue(val, initial);
-
- this.refreshValue();
- self.onChange();
- },
- destroy: function () {
- $each(this.editors, function (type, editor) {
- if (editor) editor.destroy();
- });
- if (this.editor_holder && this.editor_holder.parentNode)
- this.editor_holder.parentNode.removeChild(this.editor_holder);
- if (this.switcher && this.switcher.parentNode)
- this.switcher.parentNode.removeChild(this.switcher);
- this._super();
- },
- showValidationErrors: function (errors) {
- var self = this;
-
- // oneOf and anyOf error paths need to remove the oneOf[i] part before passing to child editors
- if (this.oneOf || this.anyOf) {
- var check_part = this.oneOf ? 'oneOf' : 'anyOf';
- $each(this.editors, function (i, editor) {
- if (!editor) return;
- var check = self.path + '.' + check_part + '[' + i + ']';
- var new_errors = [];
- $each(errors, function (j, error) {
- if (error.path.substr(0, check.length) === check) {
- var new_error = $extend({}, error);
- new_error.path = self.path + new_error.path.substr(check.length);
- new_errors.push(new_error);
- }
- });
-
- editor.showValidationErrors(new_errors);
- });
- } else {
- $each(this.editors, function (type, editor) {
- if (!editor) return;
- editor.showValidationErrors(errors);
- });
- }
- },
- });
-
- // Enum Editor (used for objects and arrays with enumerated values)
- JSONEditor.defaults.editors['enum'] = JSONEditor.AbstractEditor.extend({
- getNumColumns: function () {
- return 4;
- },
- build: function () {
- var container = this.container;
- this.title = this.header = this.label = this.theme.getFormInputLabel(this.getTitle());
- this.container.appendChild(this.title);
-
- this.options.enum_titles = this.options.enum_titles || [];
-
- this['enum'] = this.schema['enum'];
- this.selected = 0;
- this.select_options = [];
- this.html_values = [];
-
- var self = this;
- for (var i = 0; i < this['enum'].length; i++) {
- this.select_options[i] = this.options.enum_titles[i] || 'Value ' + (i + 1);
- this.html_values[i] = this.getHTML(this['enum'][i]);
- }
-
- // Switcher
- this.switcher = this.theme.getSwitcher(this.select_options);
- this.container.appendChild(this.switcher);
-
- // Display area
- this.display_area = this.theme.getIndentedPanel();
- this.container.appendChild(this.display_area);
-
- if (this.options.hide_display) this.display_area.style.display = 'none';
-
- this.switcher.addEventListener('change', function () {
- self.selected = self.select_options.indexOf(this.value);
- self.value = self['enum'][self.selected];
- self.refreshValue();
- self.onChange(true);
- });
- this.value = this['enum'][0];
- this.refreshValue();
-
- if (this['enum'].length === 1) this.switcher.style.display = 'none';
- },
- refreshValue: function () {
- var self = this;
- self.selected = -1;
- var stringified = JSON.stringify(this.value);
- $each(this['enum'], function (i, el) {
- if (stringified === JSON.stringify(el)) {
- self.selected = i;
- return false;
- }
- });
-
- if (self.selected < 0) {
- self.setValue(self['enum'][0]);
- return;
- }
-
- this.switcher.value = this.select_options[this.selected];
- this.display_area.innerHTML = this.html_values[this.selected];
- },
- enable: function () {
- if (!this.always_disabled) this.switcher.disabled = false;
- this._super();
- },
- disable: function () {
- this.switcher.disabled = true;
- this._super();
- },
- getHTML: function (el) {
- var self = this;
-
- if (el === null) {
- return 'null';
- }
- // Array or Object
- else if (typeof el === 'object') {
- // TODO: use theme
- var ret = '';
-
- $each(el, function (i, child) {
- var html = self.getHTML(child);
-
- // Add the keys to object children
- if (!Array.isArray(el)) {
- // TODO: use theme
- html = '' + i + ': ' + html + '
';
- }
-
- // TODO: use theme
- ret += '' + html + '';
- });
-
- if (Array.isArray(el)) ret = '' + ret + '
';
- else
- ret =
- "';
-
- return ret;
- }
- // Boolean
- else if (typeof el === 'boolean') {
- return el ? 'true' : 'false';
- }
- // String
- else if (typeof el === 'string') {
- return el.replace(/&/g, '&').replace(//g, '>');
- }
- // Number
- else {
- return el;
- }
- },
- setValue: function (val) {
- if (this.value !== val) {
- this.value = val;
- this.refreshValue();
- this.onChange();
- }
- },
- destroy: function () {
- if (this.display_area && this.display_area.parentNode)
- this.display_area.parentNode.removeChild(this.display_area);
- if (this.title && this.title.parentNode) this.title.parentNode.removeChild(this.title);
- if (this.switcher && this.switcher.parentNode)
- this.switcher.parentNode.removeChild(this.switcher);
-
- this._super();
- },
- });
-
- JSONEditor.defaults.editors.select = JSONEditor.AbstractEditor.extend({
- setValue: function (value, initial) {
- value = this.typecast(value || '');
-
- // Sanitize value before setting it
- var sanitized = value;
- if (this.enum_values.indexOf(sanitized) < 0) {
- sanitized = this.enum_values[0];
- }
-
- if (this.value === sanitized) {
- return;
- }
-
- this.input.value = this.enum_options[this.enum_values.indexOf(sanitized)];
- if (this.select2) this.select2.select2('val', this.input.value);
- this.value = sanitized;
- this.onChange();
- },
- register: function () {
- this._super();
- if (!this.input) return;
- this.input.setAttribute('name', this.formname);
- },
- unregister: function () {
- this._super();
- if (!this.input) return;
- this.input.removeAttribute('name');
- },
- getNumColumns: function () {
- if (!this.enum_options) return 3;
- var longest_text = this.getTitle().length;
- for (var i = 0; i < this.enum_options.length; i++) {
- longest_text = Math.max(longest_text, this.enum_options[i].length + 4);
- }
- return Math.min(12, Math.max(longest_text / 7, 2));
- },
- typecast: function (value) {
- if (this.schema.type === 'boolean') {
- return !!value;
- } else if (this.schema.type === 'number') {
- return 1 * value;
- } else if (this.schema.type === 'integer') {
- return Math.floor(value * 1);
- } else {
- return '' + value;
- }
- },
- getValue: function () {
- return this.value;
- },
- preBuild: function () {
- var self = this;
- this.input_type = 'select';
- this.enum_options = [];
- this.enum_values = [];
- this.enum_display = [];
- var i;
-
- // Enum options enumerated
- if (this.schema['enum']) {
- var display = (this.schema.options && this.schema.options.enum_titles) || [];
-
- $each(this.schema['enum'], function (i, option) {
- self.enum_options[i] = '' + option;
- self.enum_display[i] = '' + (display[i] || option);
- self.enum_values[i] = self.typecast(option);
- });
-
- if (!this.isRequired()) {
- self.enum_display.unshift(' ');
- self.enum_options.unshift('undefined');
- self.enum_values.unshift(undefined);
- }
- }
- // Boolean
- else if (this.schema.type === 'boolean') {
- self.enum_display = (this.schema.options && this.schema.options.enum_titles) || [
- 'true',
- 'false',
- ];
- self.enum_options = ['1', ''];
- self.enum_values = [true, false];
-
- if (!this.isRequired()) {
- self.enum_display.unshift(' ');
- self.enum_options.unshift('undefined');
- self.enum_values.unshift(undefined);
- }
- }
- // Dynamic Enum
- else if (this.schema.enumSource) {
- this.enumSource = [];
- this.enum_display = [];
- this.enum_options = [];
- this.enum_values = [];
-
- // Shortcut declaration for using a single array
- if (!Array.isArray(this.schema.enumSource)) {
- if (this.schema.enumValue) {
- this.enumSource = [
- {
- source: this.schema.enumSource,
- value: this.schema.enumValue,
- },
- ];
- } else {
- this.enumSource = [
- {
- source: this.schema.enumSource,
- },
- ];
- }
- } else {
- for (i = 0; i < this.schema.enumSource.length; i++) {
- // Shorthand for watched variable
- if (typeof this.schema.enumSource[i] === 'string') {
- this.enumSource[i] = {
- source: this.schema.enumSource[i],
- };
- }
- // Make a copy of the schema
- else if (!Array.isArray(this.schema.enumSource[i])) {
- this.enumSource[i] = $extend({}, this.schema.enumSource[i]);
- } else {
- this.enumSource[i] = this.schema.enumSource[i];
- }
- }
- }
-
- // Now, enumSource is an array of sources
- // Walk through this array and fix up the values
- for (i = 0; i < this.enumSource.length; i++) {
- if (this.enumSource[i].value) {
- this.enumSource[i].value = this.jsoneditor.compileTemplate(
- this.enumSource[i].value,
- this.template_engine
- );
- }
- if (this.enumSource[i].title) {
- this.enumSource[i].title = this.jsoneditor.compileTemplate(
- this.enumSource[i].title,
- this.template_engine
- );
- }
- if (this.enumSource[i].filter) {
- this.enumSource[i].filter = this.jsoneditor.compileTemplate(
- this.enumSource[i].filter,
- this.template_engine
- );
- }
- }
- }
- // Other, not supported
- else {
- throw "'select' editor requires the enum property to be set.";
- }
- },
- build: function () {
- var self = this;
- if (!this.options.compact)
- this.header = this.label = this.theme.getFormInputLabel(this.getTitle());
- if (this.schema.description)
- this.description = this.theme.getFormInputDescription(this.schema.description);
-
- if (this.options.compact) this.container.className += ' compact';
-
- this.input = this.theme.getSelectInput(this.enum_options);
- this.theme.setSelectOptions(this.input, this.enum_options, this.enum_display);
-
- if (this.schema.readOnly || this.schema.readonly) {
- this.always_disabled = true;
- this.input.disabled = true;
- }
-
- this.input.addEventListener('change', function (e) {
- e.preventDefault();
- e.stopPropagation();
- self.onInputChange();
- });
-
- this.control = this.theme.getFormControl(this.label, this.input, this.description);
- this.container.appendChild(this.control);
-
- this.value = this.enum_values[0];
- },
- onInputChange: function () {
- var val = this.input.value;
-
- var new_val;
- // Invalid option, use first option instead
- if (this.enum_options.indexOf(val) === -1) {
- new_val = this.enum_values[0];
- } else {
- new_val = this.enum_values[this.enum_options.indexOf(val)];
- }
-
- // If valid hasn't changed
- if (new_val === this.value) return;
-
- // Store new value and propogate change event
- this.value = new_val;
- this.onChange(true);
- },
- setupSelect2: function () {
- // If the Select2 library is loaded use it when we have lots of items
- if (
- window.jQuery &&
- window.jQuery.fn &&
- window.jQuery.fn.select2 &&
- (this.enum_options.length > 2 || (this.enum_options.length && this.enumSource))
- ) {
- var options = $extend({}, JSONEditor.plugins.select2);
- if (this.schema.options && this.schema.options.select2_options)
- options = $extend(options, this.schema.options.select2_options);
- this.select2 = window.jQuery(this.input).select2(options);
- var self = this;
- this.select2.on('select2-blur', function () {
- self.input.value = self.select2.select2('val');
- self.onInputChange();
- });
- this.select2.on('change', function () {
- self.input.value = self.select2.select2('val');
- self.onInputChange();
- });
- } else {
- this.select2 = null;
- }
- },
- postBuild: function () {
- this._super();
- this.theme.afterInputReady(this.input);
- this.setupSelect2();
- },
- onWatchedFieldChange: function () {
- var self = this,
- vars,
- j;
-
- // If this editor uses a dynamic select box
- if (this.enumSource) {
- vars = this.getWatchedFieldValues();
- var select_options = [];
- var select_titles = [];
-
- for (var i = 0; i < this.enumSource.length; i++) {
- // Constant values
- if (Array.isArray(this.enumSource[i])) {
- select_options = select_options.concat(this.enumSource[i]);
- select_titles = select_titles.concat(this.enumSource[i]);
- } else {
- var items = [];
- // Static list of items
- if (Array.isArray(this.enumSource[i].source)) {
- items = this.enumSource[i].source;
- // A watched field
- } else {
- items = vars[this.enumSource[i].source];
- }
-
- if (items) {
- // Only use a predefined part of the array
- if (this.enumSource[i].slice) {
- items = Array.prototype.slice.apply(
- items,
- this.enumSource[i].slice
- );
- }
- // Filter the items
- if (this.enumSource[i].filter) {
- var new_items = [];
- for (j = 0; j < items.length; j++) {
- if (
- this.enumSource[i].filter({
- i: j,
- item: items[j],
- watched: vars,
- })
- )
- new_items.push(items[j]);
- }
- items = new_items;
- }
-
- var item_titles = [];
- var item_values = [];
- for (j = 0; j < items.length; j++) {
- var item = items[j];
-
- // Rendered value
- if (this.enumSource[i].value) {
- item_values[j] = this.enumSource[i].value({
- i: j,
- item: item,
- });
- }
- // Use value directly
- else {
- item_values[j] = items[j];
- }
-
- // Rendered title
- if (this.enumSource[i].title) {
- item_titles[j] = this.enumSource[i].title({
- i: j,
- item: item,
- });
- }
- // Use value as the title also
- else {
- item_titles[j] = item_values[j];
- }
- }
-
- // TODO: sort
-
- select_options = select_options.concat(item_values);
- select_titles = select_titles.concat(item_titles);
- }
- }
- }
-
- var prev_value = this.value;
-
- this.theme.setSelectOptions(this.input, select_options, select_titles);
- this.enum_options = select_options;
- this.enum_display = select_titles;
- this.enum_values = select_options;
-
- if (this.select2) {
- this.select2.select2('destroy');
- }
-
- // If the previous value is still in the new select options, stick with it
- if (select_options.indexOf(prev_value) !== -1) {
- this.input.value = prev_value;
- this.value = prev_value;
- }
- // Otherwise, set the value to the first select option
- else {
- this.input.value = select_options[0];
- this.value = select_options[0] || '';
- if (this.parent) this.parent.onChildEditorChange(this);
- else this.jsoneditor.onChange();
- this.jsoneditor.notifyWatchers(this.path);
- }
-
- this.setupSelect2();
- }
-
- this._super();
- },
- enable: function () {
- if (!this.always_disabled) {
- this.input.disabled = false;
- if (this.select2) this.select2.select2('enable', true);
- }
- this._super();
- },
- disable: function () {
- this.input.disabled = true;
- if (this.select2) this.select2.select2('enable', false);
- this._super();
- },
- destroy: function () {
- if (this.label && this.label.parentNode) this.label.parentNode.removeChild(this.label);
- if (this.description && this.description.parentNode)
- this.description.parentNode.removeChild(this.description);
- if (this.input && this.input.parentNode) this.input.parentNode.removeChild(this.input);
- if (this.select2) {
- this.select2.select2('destroy');
- this.select2 = null;
- }
-
- this._super();
- },
- });
-
- JSONEditor.defaults.editors.selectize = JSONEditor.AbstractEditor.extend({
- setValue: function (value, initial) {
- value = this.typecast(value || '');
-
- // Sanitize value before setting it
- var sanitized = value;
- if (this.enum_values.indexOf(sanitized) < 0) {
- sanitized = this.enum_values[0];
- }
-
- if (this.value === sanitized) {
- return;
- }
-
- this.input.value = this.enum_options[this.enum_values.indexOf(sanitized)];
-
- if (this.selectize) {
- this.selectize[0].selectize.addItem(sanitized);
- }
-
- this.value = sanitized;
- this.onChange();
- },
- register: function () {
- this._super();
- if (!this.input) return;
- this.input.setAttribute('name', this.formname);
- },
- unregister: function () {
- this._super();
- if (!this.input) return;
- this.input.removeAttribute('name');
- },
- getNumColumns: function () {
- if (!this.enum_options) return 3;
- var longest_text = this.getTitle().length;
- for (var i = 0; i < this.enum_options.length; i++) {
- longest_text = Math.max(longest_text, this.enum_options[i].length + 4);
- }
- return Math.min(12, Math.max(longest_text / 7, 2));
- },
- typecast: function (value) {
- if (this.schema.type === 'boolean') {
- return !!value;
- } else if (this.schema.type === 'number') {
- return 1 * value;
- } else if (this.schema.type === 'integer') {
- return Math.floor(value * 1);
- } else {
- return '' + value;
- }
- },
- getValue: function () {
- return this.value;
- },
- preBuild: function () {
- var self = this;
- this.input_type = 'select';
- this.enum_options = [];
- this.enum_values = [];
- this.enum_display = [];
- var i;
-
- // Enum options enumerated
- if (this.schema.enum) {
- var display = (this.schema.options && this.schema.options.enum_titles) || [];
-
- $each(this.schema.enum, function (i, option) {
- self.enum_options[i] = '' + option;
- self.enum_display[i] = '' + (display[i] || option);
- self.enum_values[i] = self.typecast(option);
- });
- }
- // Boolean
- else if (this.schema.type === 'boolean') {
- self.enum_display = (this.schema.options && this.schema.options.enum_titles) || [
- 'true',
- 'false',
- ];
- self.enum_options = ['1', '0'];
- self.enum_values = [true, false];
- }
- // Dynamic Enum
- else if (this.schema.enumSource) {
- this.enumSource = [];
- this.enum_display = [];
- this.enum_options = [];
- this.enum_values = [];
-
- // Shortcut declaration for using a single array
- if (!Array.isArray(this.schema.enumSource)) {
- if (this.schema.enumValue) {
- this.enumSource = [
- {
- source: this.schema.enumSource,
- value: this.schema.enumValue,
- },
- ];
- } else {
- this.enumSource = [
- {
- source: this.schema.enumSource,
- },
- ];
- }
- } else {
- for (i = 0; i < this.schema.enumSource.length; i++) {
- // Shorthand for watched variable
- if (typeof this.schema.enumSource[i] === 'string') {
- this.enumSource[i] = {
- source: this.schema.enumSource[i],
- };
- }
- // Make a copy of the schema
- else if (!Array.isArray(this.schema.enumSource[i])) {
- this.enumSource[i] = $extend({}, this.schema.enumSource[i]);
- } else {
- this.enumSource[i] = this.schema.enumSource[i];
- }
- }
- }
-
- // Now, enumSource is an array of sources
- // Walk through this array and fix up the values
- for (i = 0; i < this.enumSource.length; i++) {
- if (this.enumSource[i].value) {
- this.enumSource[i].value = this.jsoneditor.compileTemplate(
- this.enumSource[i].value,
- this.template_engine
- );
- }
- if (this.enumSource[i].title) {
- this.enumSource[i].title = this.jsoneditor.compileTemplate(
- this.enumSource[i].title,
- this.template_engine
- );
- }
- if (this.enumSource[i].filter) {
- this.enumSource[i].filter = this.jsoneditor.compileTemplate(
- this.enumSource[i].filter,
- this.template_engine
- );
- }
- }
- }
- // Other, not supported
- else {
- throw "'select' editor requires the enum property to be set.";
- }
- },
- build: function () {
- var self = this;
- if (!this.options.compact)
- this.header = this.label = this.theme.getFormInputLabel(this.getTitle());
- if (this.schema.description)
- this.description = this.theme.getFormInputDescription(this.schema.description);
-
- if (this.options.compact) this.container.className += ' compact';
-
- this.input = this.theme.getSelectInput(this.enum_options);
- this.theme.setSelectOptions(this.input, this.enum_options, this.enum_display);
-
- if (this.schema.readOnly || this.schema.readonly) {
- this.always_disabled = true;
- this.input.disabled = true;
- }
-
- this.input.addEventListener('change', function (e) {
- e.preventDefault();
- e.stopPropagation();
- self.onInputChange();
- });
-
- this.control = this.theme.getFormControl(this.label, this.input, this.description);
- this.container.appendChild(this.control);
-
- this.value = this.enum_values[0];
- },
- onInputChange: function () {
- var val = this.input.value;
-
- var sanitized = val;
- if (this.enum_options.indexOf(val) === -1) {
- sanitized = this.enum_options[0];
- }
-
- this.value = this.enum_values[this.enum_options.indexOf(val)];
- this.onChange(true);
- },
- setupSelectize: function () {
- // If the Selectize library is loaded use it when we have lots of items
- var self = this;
- if (
- window.jQuery &&
- window.jQuery.fn &&
- window.jQuery.fn.selectize &&
- (this.enum_options.length >= 2 || (this.enum_options.length && this.enumSource))
- ) {
- var options = $extend({}, JSONEditor.plugins.selectize);
- if (this.schema.options && this.schema.options.selectize_options)
- options = $extend(options, this.schema.options.selectize_options);
- this.selectize = window.jQuery(this.input).selectize(
- $extend(options, {
- create: true,
- onChange: function () {
- self.onInputChange();
- },
- })
- );
- } else {
- this.selectize = null;
- }
- },
- postBuild: function () {
- this._super();
- this.theme.afterInputReady(this.input);
- this.setupSelectize();
- },
- onWatchedFieldChange: function () {
- var self = this,
- vars,
- j;
-
- // If this editor uses a dynamic select box
- if (this.enumSource) {
- vars = this.getWatchedFieldValues();
- var select_options = [];
- var select_titles = [];
-
- for (var i = 0; i < this.enumSource.length; i++) {
- // Constant values
- if (Array.isArray(this.enumSource[i])) {
- select_options = select_options.concat(this.enumSource[i]);
- select_titles = select_titles.concat(this.enumSource[i]);
- }
- // A watched field
- else if (vars[this.enumSource[i].source]) {
- var items = vars[this.enumSource[i].source];
-
- // Only use a predefined part of the array
- if (this.enumSource[i].slice) {
- items = Array.prototype.slice.apply(items, this.enumSource[i].slice);
- }
- // Filter the items
- if (this.enumSource[i].filter) {
- var new_items = [];
- for (j = 0; j < items.length; j++) {
- if (this.enumSource[i].filter({ i: j, item: items[j] }))
- new_items.push(items[j]);
- }
- items = new_items;
- }
-
- var item_titles = [];
- var item_values = [];
- for (j = 0; j < items.length; j++) {
- var item = items[j];
-
- // Rendered value
- if (this.enumSource[i].value) {
- item_values[j] = this.enumSource[i].value({
- i: j,
- item: item,
- });
- }
- // Use value directly
- else {
- item_values[j] = items[j];
- }
-
- // Rendered title
- if (this.enumSource[i].title) {
- item_titles[j] = this.enumSource[i].title({
- i: j,
- item: item,
- });
- }
- // Use value as the title also
- else {
- item_titles[j] = item_values[j];
- }
- }
-
- // TODO: sort
-
- select_options = select_options.concat(item_values);
- select_titles = select_titles.concat(item_titles);
- }
- }
-
- var prev_value = this.value;
-
- this.theme.setSelectOptions(this.input, select_options, select_titles);
- this.enum_options = select_options;
- this.enum_display = select_titles;
- this.enum_values = select_options;
-
- // If the previous value is still in the new select options, stick with it
- if (select_options.indexOf(prev_value) !== -1) {
- this.input.value = prev_value;
- this.value = prev_value;
- }
-
- // Otherwise, set the value to the first select option
- else {
- this.input.value = select_options[0];
- this.value = select_options[0] || '';
- if (this.parent) this.parent.onChildEditorChange(this);
- else this.jsoneditor.onChange();
- this.jsoneditor.notifyWatchers(this.path);
- }
-
- if (this.selectize) {
- // Update the Selectize options
- this.updateSelectizeOptions(select_options);
- } else {
- this.setupSelectize();
- }
-
- this._super();
- }
- },
- updateSelectizeOptions: function (select_options) {
- var selectized = this.selectize[0].selectize,
- self = this;
-
- selectized.off();
- selectized.clearOptions();
- for (var n in select_options) {
- selectized.addOption({ value: select_options[n], text: select_options[n] });
- }
- selectized.addItem(this.value);
- selectized.on('change', function () {
- self.onInputChange();
- });
- },
- enable: function () {
- if (!this.always_disabled) {
- this.input.disabled = false;
- if (this.selectize) {
- this.selectize[0].selectize.unlock();
- }
- }
- this._super();
- },
- disable: function () {
- this.input.disabled = true;
- if (this.selectize) {
- this.selectize[0].selectize.lock();
- }
- this._super();
- },
- destroy: function () {
- if (this.label && this.label.parentNode) this.label.parentNode.removeChild(this.label);
- if (this.description && this.description.parentNode)
- this.description.parentNode.removeChild(this.description);
- if (this.input && this.input.parentNode) this.input.parentNode.removeChild(this.input);
- if (this.selectize) {
- this.selectize[0].selectize.destroy();
- this.selectize = null;
- }
- this._super();
- },
- });
-
- JSONEditor.defaults.editors.multiselect = JSONEditor.AbstractEditor.extend({
- preBuild: function () {
- this._super();
- var i;
-
- this.select_options = {};
- this.select_values = {};
-
- var items_schema = this.jsoneditor.expandRefs(this.schema.items || {});
-
- var e = items_schema['enum'] || [];
- var t = items_schema.options ? items_schema.options.enum_titles || [] : [];
- this.option_keys = [];
- this.option_titles = [];
- for (i = 0; i < e.length; i++) {
- // If the sanitized value is different from the enum value, don't include it
- if (this.sanitize(e[i]) !== e[i]) continue;
-
- this.option_keys.push(e[i] + '');
- this.option_titles.push((t[i] || e[i]) + '');
- this.select_values[e[i] + ''] = e[i];
- }
- },
- build: function () {
- var self = this,
- i;
- if (!this.options.compact)
- this.header = this.label = this.theme.getFormInputLabel(this.getTitle());
- if (this.schema.description)
- this.description = this.theme.getFormInputDescription(this.schema.description);
-
- if (
- (!this.schema.format && this.option_keys.length < 8) ||
- this.schema.format === 'checkbox'
- ) {
- this.input_type = 'checkboxes';
-
- this.inputs = {};
- this.controls = {};
- for (i = 0; i < this.option_keys.length; i++) {
- this.inputs[this.option_keys[i]] = this.theme.getCheckbox();
- this.select_options[this.option_keys[i]] = this.inputs[this.option_keys[i]];
- var label = this.theme.getCheckboxLabel(this.option_titles[i]);
- this.controls[this.option_keys[i]] = this.theme.getFormControl(
- label,
- this.inputs[this.option_keys[i]]
- );
- }
-
- this.control = this.theme.getMultiCheckboxHolder(
- this.controls,
- this.label,
- this.description
- );
- } else {
- this.input_type = 'select';
- this.input = this.theme.getSelectInput(this.option_keys);
- this.theme.setSelectOptions(this.input, this.option_keys, this.option_titles);
- this.input.multiple = true;
- this.input.size = Math.min(10, this.option_keys.length);
-
- for (i = 0; i < this.option_keys.length; i++) {
- this.select_options[this.option_keys[i]] = this.input.children[i];
- }
-
- if (this.schema.readOnly || this.schema.readonly) {
- this.always_disabled = true;
- this.input.disabled = true;
- }
-
- this.control = this.theme.getFormControl(this.label, this.input, this.description);
- }
-
- this.container.appendChild(this.control);
- this.control.addEventListener('change', function (e) {
- e.preventDefault();
- e.stopPropagation();
-
- var new_value = [];
- for (i = 0; i < self.option_keys.length; i++) {
- if (
- self.select_options[self.option_keys[i]].selected ||
- self.select_options[self.option_keys[i]].checked
- )
- new_value.push(self.select_values[self.option_keys[i]]);
- }
-
- self.updateValue(new_value);
- self.onChange(true);
- });
- },
- setValue: function (value, initial) {
- var i;
- value = value || [];
- if (typeof value !== 'object') value = [value];
- else if (!Array.isArray(value)) value = [];
-
- // Make sure we are dealing with an array of strings so we can check for strict equality
- for (i = 0; i < value.length; i++) {
- if (typeof value[i] !== 'string') value[i] += '';
- }
-
- // Update selected status of options
- for (i in this.select_options) {
- if (!this.select_options.hasOwnProperty(i)) continue;
-
- this.select_options[i][this.input_type === 'select' ? 'selected' : 'checked'] =
- value.indexOf(i) !== -1;
- }
-
- this.updateValue(value);
- this.onChange();
- },
- setupSelect2: function () {
- if (window.jQuery && window.jQuery.fn && window.jQuery.fn.select2) {
- var options = window.jQuery.extend({}, JSONEditor.plugins.select2);
- if (this.schema.options && this.schema.options.select2_options)
- options = $extend(options, this.schema.options.select2_options);
- this.select2 = window.jQuery(this.input).select2(options);
- var self = this;
- this.select2.on('select2-blur', function () {
- var val = self.select2.select2('val');
- self.value = val;
- self.onChange(true);
- });
- } else {
- this.select2 = null;
- }
- },
- onInputChange: function () {
- this.value = this.input.value;
- this.onChange(true);
- },
- postBuild: function () {
- this._super();
- this.setupSelect2();
- },
- register: function () {
- this._super();
- if (!this.input) return;
- this.input.setAttribute('name', this.formname);
- },
- unregister: function () {
- this._super();
- if (!this.input) return;
- this.input.removeAttribute('name');
- },
- getNumColumns: function () {
- var longest_text = this.getTitle().length;
- for (var i in this.select_values) {
- if (!this.select_values.hasOwnProperty(i)) continue;
- longest_text = Math.max(longest_text, (this.select_values[i] + '').length + 4);
- }
-
- return Math.min(12, Math.max(longest_text / 7, 2));
- },
- updateValue: function (value) {
- var changed = false;
- var new_value = [];
- for (var i = 0; i < value.length; i++) {
- if (!this.select_options[value[i] + '']) {
- changed = true;
- continue;
- }
- var sanitized = this.sanitize(this.select_values[value[i]]);
- new_value.push(sanitized);
- if (sanitized !== value[i]) changed = true;
- }
- this.value = new_value;
- if (this.select2) this.select2.select2('val', this.value);
- return changed;
- },
- sanitize: function (value) {
- if (this.schema.items.type === 'number') {
- return 1 * value;
- } else if (this.schema.items.type === 'integer') {
- return Math.floor(value * 1);
- } else {
- return '' + value;
- }
- },
- enable: function () {
- if (!this.always_disabled) {
- if (this.input) {
- this.input.disabled = false;
- } else if (this.inputs) {
- for (var i in this.inputs) {
- if (!this.inputs.hasOwnProperty(i)) continue;
- this.inputs[i].disabled = false;
- }
- }
- if (this.select2) this.select2.select2('enable', true);
- }
- this._super();
- },
- disable: function () {
- if (this.input) {
- this.input.disabled = true;
- } else if (this.inputs) {
- for (var i in this.inputs) {
- if (!this.inputs.hasOwnProperty(i)) continue;
- this.inputs[i].disabled = true;
- }
- }
- if (this.select2) this.select2.select2('enable', false);
- this._super();
- },
- destroy: function () {
- if (this.select2) {
- this.select2.select2('destroy');
- this.select2 = null;
- }
- this._super();
- },
- });
-
- JSONEditor.defaults.editors.base64 = JSONEditor.AbstractEditor.extend({
- getNumColumns: function () {
- return 4;
- },
- build: function () {
- var self = this;
- this.title = this.header = this.label = this.theme.getFormInputLabel(this.getTitle());
-
- // Input that holds the base64 string
- this.input = this.theme.getFormInputField('hidden');
- this.container.appendChild(this.input);
-
- // Don't show uploader if this is readonly
- if (!this.schema.readOnly && !this.schema.readonly) {
- if (!window.FileReader) throw 'FileReader required for base64 editor';
-
- // File uploader
- this.uploader = this.theme.getFormInputField('file');
-
- this.uploader.addEventListener('change', function (e) {
- e.preventDefault();
- e.stopPropagation();
-
- if (this.files && this.files.length) {
- var fr = new FileReader();
- fr.onload = function (evt) {
- self.value = evt.target.result;
- self.refreshPreview();
- self.onChange(true);
- fr = null;
- };
- fr.readAsDataURL(this.files[0]);
- }
- });
- }
-
- this.preview = this.theme.getFormInputDescription(this.schema.description);
- this.container.appendChild(this.preview);
-
- this.control = this.theme.getFormControl(
- this.label,
- this.uploader || this.input,
- this.preview
- );
- this.container.appendChild(this.control);
- },
- refreshPreview: function () {
- if (this.last_preview === this.value) return;
- this.last_preview = this.value;
-
- this.preview.innerHTML = '';
-
- if (!this.value) return;
-
- var mime = this.value.match(/^data:([^;,]+)[;,]/);
- if (mime) mime = mime[1];
-
- if (!mime) {
- this.preview.innerHTML = 'Invalid data URI';
- } else {
- this.preview.innerHTML =
- 'Type: ' +
- mime +
- ', Size: ' +
- Math.floor(
- (this.value.length - this.value.split(',')[0].length - 1) / 1.33333
- ) +
- ' bytes';
- if (mime.substr(0, 5) === 'image') {
- this.preview.innerHTML += '
';
- var img = document.createElement('img');
- img.style.maxWidth = '100%';
- img.style.maxHeight = '100px';
- img.src = this.value;
- this.preview.appendChild(img);
- }
- }
- },
- enable: function () {
- if (this.uploader) this.uploader.disabled = false;
- this._super();
- },
- disable: function () {
- if (this.uploader) this.uploader.disabled = true;
- this._super();
- },
- setValue: function (val) {
- if (this.value !== val) {
- this.value = val;
- this.input.value = this.value;
- this.refreshPreview();
- this.onChange();
- }
- },
- destroy: function () {
- if (this.preview && this.preview.parentNode)
- this.preview.parentNode.removeChild(this.preview);
- if (this.title && this.title.parentNode) this.title.parentNode.removeChild(this.title);
- if (this.input && this.input.parentNode) this.input.parentNode.removeChild(this.input);
- if (this.uploader && this.uploader.parentNode)
- this.uploader.parentNode.removeChild(this.uploader);
-
- this._super();
- },
- });
-
- JSONEditor.defaults.editors.upload = JSONEditor.AbstractEditor.extend({
- getNumColumns: function () {
- return 4;
- },
- build: function () {
- var self = this;
- this.title = this.header = this.label = this.theme.getFormInputLabel(this.getTitle());
-
- // Input that holds the base64 string
- this.input = this.theme.getFormInputField('hidden');
- this.container.appendChild(this.input);
-
- // Don't show uploader if this is readonly
- if (!this.schema.readOnly && !this.schema.readonly) {
- if (!this.jsoneditor.options.upload)
- throw 'Upload handler required for upload editor';
-
- // File uploader
- this.uploader = this.theme.getFormInputField('file');
-
- this.uploader.addEventListener('change', function (e) {
- e.preventDefault();
- e.stopPropagation();
-
- if (this.files && this.files.length) {
- var fr = new FileReader();
- fr.onload = function (evt) {
- self.preview_value = evt.target.result;
- self.refreshPreview();
- self.onChange(true);
- fr = null;
- };
- fr.readAsDataURL(this.files[0]);
- }
- });
- }
-
- var description = this.schema.description;
- if (!description) description = '';
-
- this.preview = this.theme.getFormInputDescription(description);
- this.container.appendChild(this.preview);
-
- this.control = this.theme.getFormControl(
- this.label,
- this.uploader || this.input,
- this.preview
- );
- this.container.appendChild(this.control);
- },
- refreshPreview: function () {
- if (this.last_preview === this.preview_value) return;
- this.last_preview = this.preview_value;
-
- this.preview.innerHTML = '';
-
- if (!this.preview_value) return;
-
- var self = this;
-
- var mime = this.preview_value.match(/^data:([^;,]+)[;,]/);
- if (mime) mime = mime[1];
- if (!mime) mime = 'unknown';
-
- var file = this.uploader.files[0];
-
- this.preview.innerHTML =
- 'Type: ' +
- mime +
- ', Size: ' +
- file.size +
- ' bytes';
- if (mime.substr(0, 5) === 'image') {
- this.preview.innerHTML += '
';
- var img = document.createElement('img');
- img.style.maxWidth = '100%';
- img.style.maxHeight = '100px';
- img.src = this.preview_value;
- this.preview.appendChild(img);
- }
-
- this.preview.innerHTML += '
';
- var uploadButton = this.getButton('Upload', 'upload', 'Upload');
- this.preview.appendChild(uploadButton);
- uploadButton.addEventListener('click', function (event) {
- event.preventDefault();
-
- uploadButton.setAttribute('disabled', 'disabled');
- self.theme.removeInputError(self.uploader);
-
- if (self.theme.getProgressBar) {
- self.progressBar = self.theme.getProgressBar();
- self.preview.appendChild(self.progressBar);
- }
-
- self.jsoneditor.options.upload(self.path, file, {
- success: function (url) {
- self.setValue(url);
-
- if (self.parent) self.parent.onChildEditorChange(self);
- else self.jsoneditor.onChange();
-
- if (self.progressBar) self.preview.removeChild(self.progressBar);
- uploadButton.removeAttribute('disabled');
- },
- failure: function (error) {
- self.theme.addInputError(self.uploader, error);
- if (self.progressBar) self.preview.removeChild(self.progressBar);
- uploadButton.removeAttribute('disabled');
- },
- updateProgress: function (progress) {
- if (self.progressBar) {
- if (progress) self.theme.updateProgressBar(self.progressBar, progress);
- else self.theme.updateProgressBarUnknown(self.progressBar);
- }
- },
- });
- });
- },
- enable: function () {
- if (this.uploader) this.uploader.disabled = false;
- this._super();
- },
- disable: function () {
- if (this.uploader) this.uploader.disabled = true;
- this._super();
- },
- setValue: function (val) {
- if (this.value !== val) {
- this.value = val;
- this.input.value = this.value;
- this.onChange();
- }
- },
- destroy: function () {
- if (this.preview && this.preview.parentNode)
- this.preview.parentNode.removeChild(this.preview);
- if (this.title && this.title.parentNode) this.title.parentNode.removeChild(this.title);
- if (this.input && this.input.parentNode) this.input.parentNode.removeChild(this.input);
- if (this.uploader && this.uploader.parentNode)
- this.uploader.parentNode.removeChild(this.uploader);
-
- this._super();
- },
- });
-
- JSONEditor.defaults.editors.checkbox = JSONEditor.AbstractEditor.extend({
- setValue: function (value, initial) {
- this.value = !!value;
- this.input.checked = this.value;
- this.onChange();
- },
- register: function () {
- this._super();
- if (!this.input) return;
- this.input.setAttribute('name', this.formname);
- },
- unregister: function () {
- this._super();
- if (!this.input) return;
- this.input.removeAttribute('name');
- },
- getNumColumns: function () {
- return Math.min(12, Math.max(this.getTitle().length / 7, 2));
- },
- build: function () {
- var self = this;
- if (!this.options.compact) {
- this.label = this.header = this.theme.getCheckboxLabel(this.getTitle());
- }
- if (this.schema.description)
- this.description = this.theme.getFormInputDescription(this.schema.description);
- if (this.options.compact) this.container.className += ' compact';
-
- this.input = this.theme.getCheckbox();
- this.control = this.theme.getFormControl(this.label, this.input, this.description);
-
- if (this.schema.readOnly || this.schema.readonly) {
- this.always_disabled = true;
- this.input.disabled = true;
- }
-
- this.input.addEventListener('change', function (e) {
- e.preventDefault();
- e.stopPropagation();
- self.value = this.checked;
- self.onChange(true);
- });
-
- this.container.appendChild(this.control);
- },
- enable: function () {
- if (!this.always_disabled) {
- this.input.disabled = false;
- }
- this._super();
- },
- disable: function () {
- this.input.disabled = true;
- this._super();
- },
- destroy: function () {
- if (this.label && this.label.parentNode) this.label.parentNode.removeChild(this.label);
- if (this.description && this.description.parentNode)
- this.description.parentNode.removeChild(this.description);
- if (this.input && this.input.parentNode) this.input.parentNode.removeChild(this.input);
- this._super();
- },
- });
-
- JSONEditor.defaults.editors.arraySelectize = JSONEditor.AbstractEditor.extend({
- build: function () {
- this.title = this.theme.getFormInputLabel(this.getTitle());
-
- this.title_controls = this.theme.getHeaderButtonHolder();
- this.title.appendChild(this.title_controls);
- this.error_holder = document.createElement('div');
-
- if (this.schema.description) {
- this.description = this.theme.getDescription(this.schema.description);
- }
-
- this.input = document.createElement('select');
- this.input.setAttribute('multiple', 'multiple');
-
- var group = this.theme.getFormControl(this.title, this.input, this.description);
-
- this.container.appendChild(group);
- this.container.appendChild(this.error_holder);
-
- window.jQuery(this.input).selectize({
- delimiter: false,
- createOnBlur: true,
- create: true,
- });
- },
- postBuild: function () {
- var self = this;
- this.input.selectize.on('change', function (event) {
- self.refreshValue();
- self.onChange(true);
- });
- },
- destroy: function () {
- this.empty(true);
- if (this.title && this.title.parentNode) this.title.parentNode.removeChild(this.title);
- if (this.description && this.description.parentNode)
- this.description.parentNode.removeChild(this.description);
- if (this.input && this.input.parentNode) this.input.parentNode.removeChild(this.input);
-
- this._super();
- },
- empty: function (hard) {},
- setValue: function (value, initial) {
- var self = this;
- // Update the array's value, adding/removing rows when necessary
- value = value || [];
- if (!Array.isArray(value)) value = [value];
-
- this.input.selectize.clearOptions();
- this.input.selectize.clear(true);
-
- value.forEach(function (item) {
- self.input.selectize.addOption({ text: item, value: item });
- });
- this.input.selectize.setValue(value);
-
- this.refreshValue(initial);
- },
- refreshValue: function (force) {
- this.value = this.input.selectize.getValue();
- },
- showValidationErrors: function (errors) {
- var self = this;
-
- // Get all the errors that pertain to this editor
- var my_errors = [];
- var other_errors = [];
- $each(errors, function (i, error) {
- if (error.path === self.path) {
- my_errors.push(error);
- } else {
- other_errors.push(error);
- }
- });
-
- // Show errors for this editor
- if (this.error_holder) {
- if (my_errors.length) {
- var message = [];
- this.error_holder.innerHTML = '';
- this.error_holder.style.display = '';
- $each(my_errors, function (i, error) {
- self.error_holder.appendChild(self.theme.getErrorMessage(error.message));
- });
- }
- // Hide error area
- else {
- this.error_holder.style.display = 'none';
- }
- }
- },
- });
-
- var matchKey = (function () {
- var elem = document.documentElement;
-
- if (elem.matches) return 'matches';
- else if (elem.webkitMatchesSelector) return 'webkitMatchesSelector';
- else if (elem.mozMatchesSelector) return 'mozMatchesSelector';
- else if (elem.msMatchesSelector) return 'msMatchesSelector';
- else if (elem.oMatchesSelector) return 'oMatchesSelector';
- })();
-
- JSONEditor.AbstractTheme = Class.extend({
- getContainer: function () {
- return document.createElement('div');
- },
- getFloatRightLinkHolder: function () {
- var el = document.createElement('div');
- el.style = el.style || {};
- el.style.cssFloat = 'right';
- el.style.marginLeft = '10px';
- return el;
- },
- getModal: function () {
- var el = document.createElement('div');
- el.style.backgroundColor = 'white';
- el.style.border = '1px solid black';
- el.style.boxShadow = '3px 3px black';
- el.style.position = 'absolute';
- el.style.zIndex = '10';
- el.style.display = 'none';
- return el;
- },
- getGridContainer: function () {
- var el = document.createElement('div');
- return el;
- },
- getGridRow: function () {
- var el = document.createElement('div');
- el.className = 'row';
- return el;
- },
- getGridColumn: function () {
- var el = document.createElement('div');
- return el;
- },
- setGridColumnSize: function (el, size) {},
- getLink: function (text) {
- var el = document.createElement('a');
- el.setAttribute('href', '#');
- el.appendChild(document.createTextNode(text));
- return el;
- },
- disableHeader: function (header) {
- header.style.color = '#ccc';
- },
- disableLabel: function (label) {
- label.style.color = '#ccc';
- },
- enableHeader: function (header) {
- header.style.color = '';
- },
- enableLabel: function (label) {
- label.style.color = '';
- },
- getFormInputLabel: function (text) {
- var el = document.createElement('label');
- el.appendChild(document.createTextNode(text));
- return el;
- },
- getCheckboxLabel: function (text) {
- var el = this.getFormInputLabel(text);
- el.style.fontWeight = 'normal';
- return el;
- },
- getHeader: function (text) {
- var el = document.createElement('h3');
- if (typeof text === 'string') {
- el.textContent = text;
- } else {
- el.appendChild(text);
- }
-
- return el;
- },
- getCheckbox: function () {
- var el = this.getFormInputField('checkbox');
- el.style.display = 'inline-block';
- el.style.width = 'auto';
- return el;
- },
- getMultiCheckboxHolder: function (controls, label, description) {
- var el = document.createElement('div');
-
- if (label) {
- label.style.display = 'block';
- el.appendChild(label);
- }
-
- for (var i in controls) {
- if (!controls.hasOwnProperty(i)) continue;
- controls[i].style.display = 'inline-block';
- controls[i].style.marginRight = '20px';
- el.appendChild(controls[i]);
- }
-
- if (description) el.appendChild(description);
-
- return el;
- },
- getSelectInput: function (options) {
- var select = document.createElement('select');
- if (options) this.setSelectOptions(select, options);
- return select;
- },
- getSwitcher: function (options) {
- var switcher = this.getSelectInput(options);
- switcher.style.backgroundColor = 'transparent';
- switcher.style.display = 'inline-block';
- switcher.style.fontStyle = 'italic';
- switcher.style.fontWeight = 'normal';
- switcher.style.height = 'auto';
- switcher.style.marginBottom = 0;
- switcher.style.marginLeft = '5px';
- switcher.style.padding = '0 0 0 3px';
- switcher.style.width = 'auto';
- return switcher;
- },
- getSwitcherOptions: function (switcher) {
- return switcher.getElementsByTagName('option');
- },
- setSwitcherOptions: function (switcher, options, titles) {
- this.setSelectOptions(switcher, options, titles);
- },
- setSelectOptions: function (select, options, titles) {
- titles = titles || [];
- select.innerHTML = '';
- for (var i = 0; i < options.length; i++) {
- var option = document.createElement('option');
- option.setAttribute('value', options[i]);
- option.textContent = titles[i] || options[i];
- select.appendChild(option);
- }
- },
- getTextareaInput: function () {
- var el = document.createElement('textarea');
- el.style = el.style || {};
- el.style.width = '100%';
- el.style.height = '300px';
- el.style.boxSizing = 'border-box';
- return el;
- },
- getRangeInput: function (min, max, step) {
- var el = this.getFormInputField('range');
- el.setAttribute('min', min);
- el.setAttribute('max', max);
- el.setAttribute('step', step);
- return el;
- },
- getFormInputField: function (type) {
- var el = document.createElement('input');
- el.setAttribute('type', type);
- return el;
- },
- afterInputReady: function (input) {},
- getFormControl: function (label, input, description) {
- var el = document.createElement('div');
- el.className = 'form-control';
- if (label) el.appendChild(label);
- if (input.type === 'checkbox') {
- label.insertBefore(input, label.firstChild);
- } else {
- el.appendChild(input);
- }
-
- if (description) el.appendChild(description);
- return el;
- },
- getIndentedPanel: function () {
- var el = document.createElement('div');
- el.style = el.style || {};
- el.style.paddingLeft = '10px';
- el.style.marginLeft = '10px';
- el.style.borderLeft = '1px solid #ccc';
- return el;
- },
- getChildEditorHolder: function () {
- return document.createElement('div');
- },
- getDescription: function (text) {
- var el = document.createElement('p');
- el.innerHTML = text;
- return el;
- },
- getCheckboxDescription: function (text) {
- return this.getDescription(text);
- },
- getFormInputDescription: function (text) {
- return this.getDescription(text);
- },
- getHeaderButtonHolder: function () {
- return this.getButtonHolder();
- },
- getButtonHolder: function () {
- return document.createElement('div');
- },
- getButton: function (text, icon, title) {
- var el = document.createElement('button');
- el.type = 'button';
- this.setButtonText(el, text, icon, title);
- return el;
- },
- setButtonText: function (button, text, icon, title) {
- button.innerHTML = '';
- if (icon) {
- button.appendChild(icon);
- button.innerHTML += ' ';
- }
- button.appendChild(document.createTextNode(text));
- if (title) button.setAttribute('title', title);
- },
- getTable: function () {
- return document.createElement('table');
- },
- getTableRow: function () {
- return document.createElement('tr');
- },
- getTableHead: function () {
- return document.createElement('thead');
- },
- getTableBody: function () {
- return document.createElement('tbody');
- },
- getTableHeaderCell: function (text) {
- var el = document.createElement('th');
- el.textContent = text;
- return el;
- },
- getTableCell: function () {
- var el = document.createElement('td');
- return el;
- },
- getErrorMessage: function (text) {
- var el = document.createElement('p');
- el.style = el.style || {};
- el.style.color = 'red';
- el.appendChild(document.createTextNode(text));
- return el;
- },
- addInputError: function (input, text) {},
- removeInputError: function (input) {},
- addTableRowError: function (row) {},
- removeTableRowError: function (row) {},
- getTabHolder: function () {
- var el = document.createElement('div');
- el.innerHTML =
- "";
- return el;
- },
- applyStyles: function (el, styles) {
- el.style = el.style || {};
- for (var i in styles) {
- if (!styles.hasOwnProperty(i)) continue;
- el.style[i] = styles[i];
- }
- },
- closest: function (elem, selector) {
- while (elem && elem !== document) {
- if (elem[matchKey]) {
- if (elem[matchKey](selector)) {
- return elem;
- } else {
- elem = elem.parentNode;
- }
- } else {
- return false;
- }
- }
- return false;
- },
- getTab: function (span) {
- var el = document.createElement('div');
- el.appendChild(span);
- el.style = el.style || {};
- this.applyStyles(el, {
- border: '1px solid #ccc',
- borderWidth: '1px 0 1px 1px',
- textAlign: 'center',
- lineHeight: '30px',
- borderRadius: '5px',
- borderBottomRightRadius: 0,
- borderTopRightRadius: 0,
- fontWeight: 'bold',
- cursor: 'pointer',
- });
- return el;
- },
- getTabContentHolder: function (tab_holder) {
- return tab_holder.children[1];
- },
- getTabContent: function () {
- return this.getIndentedPanel();
- },
- markTabActive: function (tab) {
- this.applyStyles(tab, {
- opacity: 1,
- background: 'white',
- });
- },
- markTabInactive: function (tab) {
- this.applyStyles(tab, {
- opacity: 0.5,
- background: '',
- });
- },
- addTab: function (holder, tab) {
- holder.children[0].appendChild(tab);
- },
- getBlockLink: function () {
- var link = document.createElement('a');
- link.style.display = 'block';
- return link;
- },
- getBlockLinkHolder: function () {
- var el = document.createElement('div');
- return el;
- },
- getLinksHolder: function () {
- var el = document.createElement('div');
- return el;
- },
- createMediaLink: function (holder, link, media) {
- holder.appendChild(link);
- media.style.width = '100%';
- holder.appendChild(media);
- },
- createImageLink: function (holder, link, image) {
- holder.appendChild(link);
- link.appendChild(image);
- },
- });
-
- JSONEditor.defaults.themes.bootstrap2 = JSONEditor.AbstractTheme.extend({
- getRangeInput: function (min, max, step) {
- // TODO: use bootstrap slider
- return this._super(min, max, step);
- },
- getGridContainer: function () {
- var el = document.createElement('div');
- el.className = 'container-fluid';
- return el;
- },
- getGridRow: function () {
- var el = document.createElement('div');
- el.className = 'row-fluid';
- return el;
- },
- getFormInputLabel: function (text) {
- var el = this._super(text);
- el.style.display = 'inline-block';
- el.style.fontWeight = 'bold';
- return el;
- },
- setGridColumnSize: function (el, size) {
- el.className = 'span' + size;
- },
- getSelectInput: function (options) {
- var input = this._super(options);
- input.style.width = 'auto';
- input.style.maxWidth = '98%';
- return input;
- },
- getFormInputField: function (type) {
- var el = this._super(type);
- el.style.width = '98%';
- return el;
- },
- afterInputReady: function (input) {
- if (input.controlgroup) return;
- input.controlgroup = this.closest(input, '.control-group');
- input.controls = this.closest(input, '.controls');
- if (this.closest(input, '.compact')) {
- input.controlgroup.className = input.controlgroup.className
- .replace(/control-group/g, '')
- .replace(/[ ]{2,}/g, ' ');
- input.controls.className = input.controlgroup.className
- .replace(/controls/g, '')
- .replace(/[ ]{2,}/g, ' ');
- input.style.marginBottom = 0;
- }
-
- // TODO: use bootstrap slider
- },
- getIndentedPanel: function () {
- var el = document.createElement('div');
- el.className = 'well well-small';
- el.style.paddingBottom = 0;
- return el;
- },
- getFormInputDescription: function (text) {
- var el = document.createElement('p');
- el.className = 'help-inline';
- el.textContent = text;
- return el;
- },
- getFormControl: function (label, input, description) {
- var ret = document.createElement('div');
- ret.className = 'control-group';
-
- var controls = document.createElement('div');
- controls.className = 'controls';
-
- if (label && input.getAttribute('type') === 'checkbox') {
- ret.appendChild(controls);
- label.className += ' checkbox';
- label.appendChild(input);
- controls.appendChild(label);
- controls.style.height = '30px';
- } else {
- if (label) {
- label.className += ' control-label';
- ret.appendChild(label);
- }
- controls.appendChild(input);
- ret.appendChild(controls);
- }
-
- if (description) controls.appendChild(description);
-
- return ret;
- },
- getHeaderButtonHolder: function () {
- var el = this.getButtonHolder();
- el.style.marginLeft = '10px';
- return el;
- },
- getButtonHolder: function () {
- var el = document.createElement('div');
- el.className = 'btn-group';
- return el;
- },
- getButton: function (text, icon, title) {
- var el = this._super(text, icon, title);
- el.className += ' btn btn-default';
- return el;
- },
- getTable: function () {
- var el = document.createElement('table');
- el.className = 'table table-bordered';
- el.style.width = 'auto';
- el.style.maxWidth = 'none';
- return el;
- },
- addInputError: function (input, text) {
- if (!input.controlgroup || !input.controls) return;
- input.controlgroup.className += ' error';
- if (!input.errmsg) {
- input.errmsg = document.createElement('p');
- input.errmsg.className = 'help-block errormsg';
- input.controls.appendChild(input.errmsg);
- } else {
- input.errmsg.style.display = '';
- }
-
- input.errmsg.textContent = text;
- },
- removeInputError: function (input) {
- if (!input.errmsg) return;
- input.errmsg.style.display = 'none';
- input.controlgroup.className = input.controlgroup.className.replace(/\s?error/g, '');
- },
- getTabHolder: function () {
- var el = document.createElement('div');
- el.className = 'tabbable tabs-left';
- el.innerHTML =
- "";
- return el;
- },
- getTab: function (text) {
- var el = document.createElement('li');
- var a = document.createElement('a');
- a.setAttribute('href', '#');
- a.appendChild(text);
- el.appendChild(a);
- return el;
- },
- getTabContentHolder: function (tab_holder) {
- return tab_holder.children[1];
- },
- getTabContent: function () {
- var el = document.createElement('div');
- el.className = 'tab-pane active';
- return el;
- },
- markTabActive: function (tab) {
- tab.className += ' active';
- },
- markTabInactive: function (tab) {
- tab.className = tab.className.replace(/\s?active/g, '');
- },
- addTab: function (holder, tab) {
- holder.children[0].appendChild(tab);
- },
- getProgressBar: function () {
- var container = document.createElement('div');
- container.className = 'progress';
-
- var bar = document.createElement('div');
- bar.className = 'bar';
- bar.style.width = '0%';
- container.appendChild(bar);
-
- return container;
- },
- updateProgressBar: function (progressBar, progress) {
- if (!progressBar) return;
-
- progressBar.firstChild.style.width = progress + '%';
- },
- updateProgressBarUnknown: function (progressBar) {
- if (!progressBar) return;
-
- progressBar.className = 'progress progress-striped active';
- progressBar.firstChild.style.width = '100%';
- },
- });
-
- JSONEditor.defaults.themes.bootstrap3 = JSONEditor.AbstractTheme.extend({
- getSelectInput: function (options) {
- var el = this._super(options);
- el.className += 'form-control';
- //el.style.width = 'auto';
- return el;
- },
- setGridColumnSize: function (el, size) {
- el.className = 'col-md-' + size;
- },
- afterInputReady: function (input) {
- if (input.controlgroup) return;
- input.controlgroup = this.closest(input, '.form-group');
- if (this.closest(input, '.compact')) {
- input.controlgroup.style.marginBottom = 0;
- }
-
- // TODO: use bootstrap slider
- },
- getTextareaInput: function () {
- var el = document.createElement('textarea');
- el.className = 'form-control';
- return el;
- },
- getRangeInput: function (min, max, step) {
- // TODO: use better slider
- return this._super(min, max, step);
- },
- getFormInputField: function (type) {
- var el = this._super(type);
- if (type !== 'checkbox') {
- el.className += 'form-control';
- }
- return el;
- },
- getFormControl: function (label, input, description) {
- var group = document.createElement('div');
-
- if (label && input.type === 'checkbox') {
- group.className += ' checkbox';
- label.appendChild(input);
- label.style.fontSize = '14px';
- group.style.marginTop = '0';
- group.appendChild(label);
- input.style.position = 'relative';
- input.style.cssFloat = 'left';
- } else {
- group.className += ' form-group';
- if (label) {
- label.className += ' control-label';
- group.appendChild(label);
- }
- group.appendChild(input);
- }
-
- if (description) group.appendChild(description);
-
- return group;
- },
- getIndentedPanel: function () {
- var el = document.createElement('div');
- el.className = 'well well-sm';
- el.style.paddingBottom = 0;
- return el;
- },
- getFormInputDescription: function (text) {
- var el = document.createElement('p');
- el.className = 'help-block';
- el.innerHTML = text;
- return el;
- },
- getHeaderButtonHolder: function () {
- var el = this.getButtonHolder();
- el.style.marginLeft = '10px';
- return el;
- },
- getButtonHolder: function () {
- var el = document.createElement('div');
- el.className = 'btn-group';
- return el;
- },
- getButton: function (text, icon, title) {
- var el = this._super(text, icon, title);
- el.className += 'btn btn-default';
- return el;
- },
- getTable: function () {
- var el = document.createElement('table');
- el.className = 'table table-bordered';
- el.style.width = 'auto';
- el.style.maxWidth = 'none';
- return el;
- },
-
- addInputError: function (input, text) {
- if (!input.controlgroup) return;
- input.controlgroup.className += ' has-error';
- if (!input.errmsg) {
- input.errmsg = document.createElement('p');
- input.errmsg.className = 'help-block errormsg';
- input.controlgroup.appendChild(input.errmsg);
- } else {
- input.errmsg.style.display = '';
- }
-
- input.errmsg.textContent = text;
- },
- removeInputError: function (input) {
- if (!input.errmsg) return;
- input.errmsg.style.display = 'none';
- input.controlgroup.className = input.controlgroup.className.replace(
- /\s?has-error/g,
- ''
- );
- },
- getTabHolder: function () {
- var el = document.createElement('div');
- el.innerHTML =
- "";
- el.className = 'rows';
- return el;
- },
- getTab: function (text) {
- var el = document.createElement('a');
- el.className = 'list-group-item';
- el.setAttribute('href', '#');
- el.appendChild(text);
- return el;
- },
- markTabActive: function (tab) {
- tab.className += ' active';
- },
- markTabInactive: function (tab) {
- tab.className = tab.className.replace(/\s?active/g, '');
- },
- getProgressBar: function () {
- var min = 0,
- max = 100,
- start = 0;
-
- var container = document.createElement('div');
- container.className = 'progress';
-
- var bar = document.createElement('div');
- bar.className = 'progress-bar';
- bar.setAttribute('role', 'progressbar');
- bar.setAttribute('aria-valuenow', start);
- bar.setAttribute('aria-valuemin', min);
- bar.setAttribute('aria-valuenax', max);
- bar.innerHTML = start + '%';
- container.appendChild(bar);
-
- return container;
- },
- updateProgressBar: function (progressBar, progress) {
- if (!progressBar) return;
-
- var bar = progressBar.firstChild;
- var percentage = progress + '%';
- bar.setAttribute('aria-valuenow', progress);
- bar.style.width = percentage;
- bar.innerHTML = percentage;
- },
- updateProgressBarUnknown: function (progressBar) {
- if (!progressBar) return;
-
- var bar = progressBar.firstChild;
- progressBar.className = 'progress progress-striped active';
- bar.removeAttribute('aria-valuenow');
- bar.style.width = '100%';
- bar.innerHTML = '';
- },
- });
-
- // Base Foundation theme
- JSONEditor.defaults.themes.foundation = JSONEditor.AbstractTheme.extend({
- getChildEditorHolder: function () {
- var el = document.createElement('div');
- el.style.marginBottom = '15px';
- return el;
- },
- getSelectInput: function (options) {
- var el = this._super(options);
- el.style.minWidth = 'none';
- el.style.padding = '5px';
- el.style.marginTop = '3px';
- return el;
- },
- getSwitcher: function (options) {
- var el = this._super(options);
- el.style.paddingRight = '8px';
- return el;
- },
- afterInputReady: function (input) {
- if (this.closest(input, '.compact')) {
- input.style.marginBottom = 0;
- }
- input.group = this.closest(input, '.form-control');
- },
- getFormInputLabel: function (text) {
- var el = this._super(text);
- el.style.display = 'inline-block';
- return el;
- },
- getFormInputField: function (type) {
- var el = this._super(type);
- el.style.width = '100%';
- el.style.marginBottom = type === 'checkbox' ? '0' : '12px';
- return el;
- },
- getFormInputDescription: function (text) {
- var el = document.createElement('p');
- el.textContent = text;
- el.style.marginTop = '-10px';
- el.style.fontStyle = 'italic';
- return el;
- },
- getIndentedPanel: function () {
- var el = document.createElement('div');
- el.className = 'panel';
- el.style.paddingBottom = 0;
- return el;
- },
- getHeaderButtonHolder: function () {
- var el = this.getButtonHolder();
- el.style.display = 'inline-block';
- el.style.marginLeft = '10px';
- el.style.verticalAlign = 'middle';
- return el;
- },
- getButtonHolder: function () {
- var el = document.createElement('div');
- el.className = 'button-group';
- return el;
- },
- getButton: function (text, icon, title) {
- var el = this._super(text, icon, title);
- el.className += ' small button';
- return el;
- },
- addInputError: function (input, text) {
- if (!input.group) return;
- input.group.className += ' error';
-
- if (!input.errmsg) {
- input.insertAdjacentHTML('afterend', '');
- input.errmsg = input.parentNode.getElementsByClassName('error')[0];
- } else {
- input.errmsg.style.display = '';
- }
-
- input.errmsg.textContent = text;
- },
- removeInputError: function (input) {
- if (!input.errmsg) return;
- input.group.className = input.group.className.replace(/ error/g, '');
- input.errmsg.style.display = 'none';
- },
- getProgressBar: function () {
- var progressBar = document.createElement('div');
- progressBar.className = 'progress';
-
- var meter = document.createElement('span');
- meter.className = 'meter';
- meter.style.width = '0%';
- progressBar.appendChild(meter);
- return progressBar;
- },
- updateProgressBar: function (progressBar, progress) {
- if (!progressBar) return;
- progressBar.firstChild.style.width = progress + '%';
- },
- updateProgressBarUnknown: function (progressBar) {
- if (!progressBar) return;
- progressBar.firstChild.style.width = '100%';
- },
- });
-
- // Foundation 3 Specific Theme
- JSONEditor.defaults.themes.foundation3 = JSONEditor.defaults.themes.foundation.extend({
- getHeaderButtonHolder: function () {
- var el = this._super();
- el.style.fontSize = '.6em';
- return el;
- },
- getFormInputLabel: function (text) {
- var el = this._super(text);
- el.style.fontWeight = 'bold';
- return el;
- },
- getTabHolder: function () {
- var el = document.createElement('div');
- el.className = 'row';
- el.innerHTML =
- "
";
- return el;
- },
- setGridColumnSize: function (el, size) {
- var sizes = [
- 'zero',
- 'one',
- 'two',
- 'three',
- 'four',
- 'five',
- 'six',
- 'seven',
- 'eight',
- 'nine',
- 'ten',
- 'eleven',
- 'twelve',
- ];
- el.className = 'columns ' + sizes[size];
- },
- getTab: function (text) {
- var el = document.createElement('dd');
- var a = document.createElement('a');
- a.setAttribute('href', '#');
- a.appendChild(text);
- el.appendChild(a);
- return el;
- },
- getTabContentHolder: function (tab_holder) {
- return tab_holder.children[1];
- },
- getTabContent: function () {
- var el = document.createElement('div');
- el.className = 'content active';
- el.style.paddingLeft = '5px';
- return el;
- },
- markTabActive: function (tab) {
- tab.className += ' active';
- },
- markTabInactive: function (tab) {
- tab.className = tab.className.replace(/\s*active/g, '');
- },
- addTab: function (holder, tab) {
- holder.children[0].appendChild(tab);
- },
- });
-
- // Foundation 4 Specific Theme
- JSONEditor.defaults.themes.foundation4 = JSONEditor.defaults.themes.foundation.extend({
- getHeaderButtonHolder: function () {
- var el = this._super();
- el.style.fontSize = '.6em';
- return el;
- },
- setGridColumnSize: function (el, size) {
- el.className = 'columns large-' + size;
- },
- getFormInputDescription: function (text) {
- var el = this._super(text);
- el.style.fontSize = '.8rem';
- return el;
- },
- getFormInputLabel: function (text) {
- var el = this._super(text);
- el.style.fontWeight = 'bold';
- return el;
- },
- });
-
- // Foundation 5 Specific Theme
- JSONEditor.defaults.themes.foundation5 = JSONEditor.defaults.themes.foundation.extend({
- getFormInputDescription: function (text) {
- var el = this._super(text);
- el.style.fontSize = '.8rem';
- return el;
- },
- setGridColumnSize: function (el, size) {
- el.className = 'columns medium-' + size;
- },
- getButton: function (text, icon, title) {
- var el = this._super(text, icon, title);
- el.className = el.className.replace(/\s*small/g, '') + ' tiny';
- return el;
- },
- getTabHolder: function () {
- var el = document.createElement('div');
- el.innerHTML =
- "
";
- return el;
- },
- getTab: function (text) {
- var el = document.createElement('dd');
- var a = document.createElement('a');
- a.setAttribute('href', '#');
- a.appendChild(text);
- el.appendChild(a);
- return el;
- },
- getTabContentHolder: function (tab_holder) {
- return tab_holder.children[1];
- },
- getTabContent: function () {
- var el = document.createElement('div');
- el.className = 'content active';
- el.style.paddingLeft = '5px';
- return el;
- },
- markTabActive: function (tab) {
- tab.className += ' active';
- },
- markTabInactive: function (tab) {
- tab.className = tab.className.replace(/\s*active/g, '');
- },
- addTab: function (holder, tab) {
- holder.children[0].appendChild(tab);
- },
- });
-
- JSONEditor.defaults.themes.foundation6 = JSONEditor.defaults.themes.foundation5.extend({
- getIndentedPanel: function () {
- var el = document.createElement('div');
- el.className = 'callout secondary';
- return el;
- },
- getButtonHolder: function () {
- var el = document.createElement('div');
- el.className = 'button-group tiny';
- el.style.marginBottom = 0;
- return el;
- },
- getFormInputLabel: function (text) {
- var el = this._super(text);
- el.style.display = 'block';
- return el;
- },
- getFormControl: function (label, input, description) {
- var el = document.createElement('div');
- el.className = 'form-control';
- if (label) el.appendChild(label);
- if (input.type === 'checkbox') {
- label.insertBefore(input, label.firstChild);
- } else if (label) {
- label.appendChild(input);
- } else {
- el.appendChild(input);
- }
-
- if (description) label.appendChild(description);
- return el;
- },
- addInputError: function (input, text) {
- if (!input.group) return;
- input.group.className += ' error';
-
- if (!input.errmsg) {
- var errorEl = document.createElement('span');
- errorEl.className = 'form-error is-visible';
- input.group.getElementsByTagName('label')[0].appendChild(errorEl);
-
- input.className = input.className + ' is-invalid-input';
-
- input.errmsg = errorEl;
- } else {
- input.errmsg.style.display = '';
- input.className = '';
- }
-
- input.errmsg.textContent = text;
- },
- removeInputError: function (input) {
- if (!input.errmsg) return;
- input.className = input.className.replace(/ is-invalid-input/g, '');
- if (input.errmsg.parentNode) {
- input.errmsg.parentNode.removeChild(input.errmsg);
- }
- },
- });
-
- JSONEditor.defaults.themes.html = JSONEditor.AbstractTheme.extend({
- getFormInputLabel: function (text) {
- var el = this._super(text);
- el.style.display = 'block';
- el.style.marginBottom = '3px';
- el.style.fontWeight = 'bold';
- return el;
- },
- getFormInputDescription: function (text) {
- var el = this._super(text);
- el.style.fontSize = '.8em';
- el.style.margin = 0;
- el.style.display = 'inline-block';
- el.style.fontStyle = 'italic';
- return el;
- },
- getIndentedPanel: function () {
- var el = this._super();
- el.style.border = '1px solid #ddd';
- el.style.padding = '5px';
- el.style.margin = '5px';
- el.style.borderRadius = '3px';
- return el;
- },
- getChildEditorHolder: function () {
- var el = this._super();
- el.style.marginBottom = '8px';
- return el;
- },
- getHeaderButtonHolder: function () {
- var el = this.getButtonHolder();
- el.style.display = 'inline-block';
- el.style.marginLeft = '10px';
- el.style.fontSize = '.8em';
- el.style.verticalAlign = 'middle';
- return el;
- },
- getTable: function () {
- var el = this._super();
- el.style.borderBottom = '1px solid #ccc';
- el.style.marginBottom = '5px';
- return el;
- },
- addInputError: function (input, text) {
- input.style.borderColor = 'red';
-
- if (!input.errmsg) {
- var group = this.closest(input, '.form-control');
- input.errmsg = document.createElement('div');
- input.errmsg.setAttribute('class', 'errmsg');
- input.errmsg.style = input.errmsg.style || {};
- input.errmsg.style.color = 'red';
- group.appendChild(input.errmsg);
- } else {
- input.errmsg.style.display = 'block';
- }
-
- input.errmsg.innerHTML = '';
- input.errmsg.appendChild(document.createTextNode(text));
- },
- removeInputError: function (input) {
- input.style.borderColor = '';
- if (input.errmsg) input.errmsg.style.display = 'none';
- },
- getProgressBar: function () {
- var max = 100,
- start = 0;
-
- var progressBar = document.createElement('progress');
- progressBar.setAttribute('max', max);
- progressBar.setAttribute('value', start);
- return progressBar;
- },
- updateProgressBar: function (progressBar, progress) {
- if (!progressBar) return;
- progressBar.setAttribute('value', progress);
- },
- updateProgressBarUnknown: function (progressBar) {
- if (!progressBar) return;
- progressBar.removeAttribute('value');
- },
- });
-
- JSONEditor.defaults.themes.jqueryui = JSONEditor.AbstractTheme.extend({
- getTable: function () {
- var el = this._super();
- el.setAttribute('cellpadding', 5);
- el.setAttribute('cellspacing', 0);
- return el;
- },
- getTableHeaderCell: function (text) {
- var el = this._super(text);
- el.className = 'ui-state-active';
- el.style.fontWeight = 'bold';
- return el;
- },
- getTableCell: function () {
- var el = this._super();
- el.className = 'ui-widget-content';
- return el;
- },
- getHeaderButtonHolder: function () {
- var el = this.getButtonHolder();
- el.style.marginLeft = '10px';
- el.style.fontSize = '.6em';
- el.style.display = 'inline-block';
- return el;
- },
- getFormInputDescription: function (text) {
- var el = this.getDescription(text);
- el.style.marginLeft = '10px';
- el.style.display = 'inline-block';
- return el;
- },
- getFormControl: function (label, input, description) {
- var el = this._super(label, input, description);
- if (input.type === 'checkbox') {
- el.style.lineHeight = '25px';
-
- el.style.padding = '3px 0';
- } else {
- el.style.padding = '4px 0 8px 0';
- }
- return el;
- },
- getDescription: function (text) {
- var el = document.createElement('span');
- el.style.fontSize = '.8em';
- el.style.fontStyle = 'italic';
- el.textContent = text;
- return el;
- },
- getButtonHolder: function () {
- var el = document.createElement('div');
- el.className = 'ui-buttonset';
- el.style.fontSize = '.7em';
- return el;
- },
- getFormInputLabel: function (text) {
- var el = document.createElement('label');
- el.style.fontWeight = 'bold';
- el.style.display = 'block';
- el.textContent = text;
- return el;
- },
- getButton: function (text, icon, title) {
- var button = document.createElement('button');
- button.className = 'ui-button ui-widget ui-state-default ui-corner-all';
-
- // Icon only
- if (icon && !text) {
- button.className += ' ui-button-icon-only';
- icon.className += ' ui-button-icon-primary ui-icon-primary';
- button.appendChild(icon);
- }
- // Icon and Text
- else if (icon) {
- button.className += ' ui-button-text-icon-primary';
- icon.className += ' ui-button-icon-primary ui-icon-primary';
- button.appendChild(icon);
- }
- // Text only
- else {
- button.className += ' ui-button-text-only';
- }
-
- var el = document.createElement('span');
- el.className = 'ui-button-text';
- el.textContent = text || title || '.';
- button.appendChild(el);
-
- button.setAttribute('title', title);
-
- return button;
- },
- setButtonText: function (button, text, icon, title) {
- button.innerHTML = '';
- button.className = 'ui-button ui-widget ui-state-default ui-corner-all';
-
- // Icon only
- if (icon && !text) {
- button.className += ' ui-button-icon-only';
- icon.className += ' ui-button-icon-primary ui-icon-primary';
- button.appendChild(icon);
- }
- // Icon and Text
- else if (icon) {
- button.className += ' ui-button-text-icon-primary';
- icon.className += ' ui-button-icon-primary ui-icon-primary';
- button.appendChild(icon);
- }
- // Text only
- else {
- button.className += ' ui-button-text-only';
- }
-
- var el = document.createElement('span');
- el.className = 'ui-button-text';
- el.textContent = text || title || '.';
- button.appendChild(el);
-
- button.setAttribute('title', title);
- },
- getIndentedPanel: function () {
- var el = document.createElement('div');
- el.className = 'ui-widget-content ui-corner-all';
- el.style.padding = '1em 1.4em';
- el.style.marginBottom = '20px';
- return el;
- },
- afterInputReady: function (input) {
- if (input.controls) return;
- input.controls = this.closest(input, '.form-control');
- },
- addInputError: function (input, text) {
- if (!input.controls) return;
- if (!input.errmsg) {
- input.errmsg = document.createElement('div');
- input.errmsg.className = 'ui-state-error';
- input.controls.appendChild(input.errmsg);
- } else {
- input.errmsg.style.display = '';
- }
-
- input.errmsg.textContent = text;
- },
- removeInputError: function (input) {
- if (!input.errmsg) return;
- input.errmsg.style.display = 'none';
- },
- markTabActive: function (tab) {
- tab.className = tab.className.replace(/\s*ui-widget-header/g, '') + ' ui-state-active';
- },
- markTabInactive: function (tab) {
- tab.className = tab.className.replace(/\s*ui-state-active/g, '') + ' ui-widget-header';
- },
- });
-
- JSONEditor.defaults.themes.barebones = JSONEditor.AbstractTheme.extend({
- getFormInputLabel: function (text) {
- var el = this._super(text);
- return el;
- },
- getFormInputDescription: function (text) {
- var el = this._super(text);
- return el;
- },
- getIndentedPanel: function () {
- var el = this._super();
- return el;
- },
- getChildEditorHolder: function () {
- var el = this._super();
- return el;
- },
- getHeaderButtonHolder: function () {
- var el = this.getButtonHolder();
- return el;
- },
- getTable: function () {
- var el = this._super();
- return el;
- },
- addInputError: function (input, text) {
- if (!input.errmsg) {
- var group = this.closest(input, '.form-control');
- input.errmsg = document.createElement('div');
- input.errmsg.setAttribute('class', 'errmsg');
- group.appendChild(input.errmsg);
- } else {
- input.errmsg.style.display = 'block';
- }
-
- input.errmsg.innerHTML = '';
- input.errmsg.appendChild(document.createTextNode(text));
- },
- removeInputError: function (input) {
- input.style.borderColor = '';
- if (input.errmsg) input.errmsg.style.display = 'none';
- },
- getProgressBar: function () {
- var max = 100,
- start = 0;
-
- var progressBar = document.createElement('progress');
- progressBar.setAttribute('max', max);
- progressBar.setAttribute('value', start);
- return progressBar;
- },
- updateProgressBar: function (progressBar, progress) {
- if (!progressBar) return;
- progressBar.setAttribute('value', progress);
- },
- updateProgressBarUnknown: function (progressBar) {
- if (!progressBar) return;
- progressBar.removeAttribute('value');
- },
- });
-
- JSONEditor.AbstractIconLib = Class.extend({
- mapping: {
- collapse: '',
- expand: '',
- delete: '',
- edit: '',
- add: '',
- cancel: '',
- save: '',
- moveup: '',
- movedown: '',
- },
- icon_prefix: '',
- getIconClass: function (key) {
- if (this.mapping[key]) return this.icon_prefix + this.mapping[key];
- else return null;
- },
- getIcon: function (key) {
- var iconclass = this.getIconClass(key);
-
- if (!iconclass) return null;
-
- var i = document.createElement('i');
- i.className = iconclass;
- return i;
- },
- });
-
- JSONEditor.defaults.iconlibs.bootstrap2 = JSONEditor.AbstractIconLib.extend({
- mapping: {
- collapse: 'chevron-down',
- expand: 'chevron-up',
- delete: 'trash',
- edit: 'pencil',
- add: 'plus',
- cancel: 'ban-circle',
- save: 'ok',
- moveup: 'arrow-up',
- movedown: 'arrow-down',
- },
- icon_prefix: 'icon-',
- });
-
- JSONEditor.defaults.iconlibs.bootstrap3 = JSONEditor.AbstractIconLib.extend({
- mapping: {
- collapse: 'chevron-down',
- expand: 'chevron-right',
- delete: 'remove',
- edit: 'pencil',
- add: 'plus',
- cancel: 'floppy-remove',
- save: 'floppy-saved',
- moveup: 'arrow-up',
- movedown: 'arrow-down',
- },
- icon_prefix: 'glyphicon glyphicon-',
- });
-
- JSONEditor.defaults.iconlibs.fontawesome3 = JSONEditor.AbstractIconLib.extend({
- mapping: {
- collapse: 'chevron-down',
- expand: 'chevron-right',
- delete: 'remove',
- edit: 'pencil',
- add: 'plus',
- cancel: 'ban-circle',
- save: 'save',
- moveup: 'arrow-up',
- movedown: 'arrow-down',
- },
- icon_prefix: 'icon-',
- });
-
- JSONEditor.defaults.iconlibs.fontawesome4 = JSONEditor.AbstractIconLib.extend({
- mapping: {
- collapse: 'caret-square-o-down',
- expand: 'caret-square-o-right',
- delete: 'times',
- edit: 'pencil',
- add: 'plus',
- cancel: 'ban',
- save: 'save',
- moveup: 'arrow-up',
- movedown: 'arrow-down',
- },
- icon_prefix: 'fa fa-',
- });
-
- JSONEditor.defaults.iconlibs.foundation2 = JSONEditor.AbstractIconLib.extend({
- mapping: {
- collapse: 'minus',
- expand: 'plus',
- delete: 'remove',
- edit: 'edit',
- add: 'add-doc',
- cancel: 'error',
- save: 'checkmark',
- moveup: 'up-arrow',
- movedown: 'down-arrow',
- },
- icon_prefix: 'foundicon-',
- });
-
- JSONEditor.defaults.iconlibs.foundation3 = JSONEditor.AbstractIconLib.extend({
- mapping: {
- collapse: 'minus',
- expand: 'plus',
- delete: 'x',
- edit: 'pencil',
- add: 'page-add',
- cancel: 'x-circle',
- save: 'save',
- moveup: 'arrow-up',
- movedown: 'arrow-down',
- },
- icon_prefix: 'fi-',
- });
-
- JSONEditor.defaults.iconlibs.jqueryui = JSONEditor.AbstractIconLib.extend({
- mapping: {
- collapse: 'triangle-1-s',
- expand: 'triangle-1-e',
- delete: 'trash',
- edit: 'pencil',
- add: 'plusthick',
- cancel: 'closethick',
- save: 'disk',
- moveup: 'arrowthick-1-n',
- movedown: 'arrowthick-1-s',
- },
- icon_prefix: 'ui-icon ui-icon-',
- });
-
- JSONEditor.defaults.templates['default'] = function () {
- return {
- compile: function (template) {
- var matches = template.match(/{{\s*([a-zA-Z0-9\-_ \.]+)\s*}}/g);
- var l = matches && matches.length;
-
- // Shortcut if the template contains no variables
- if (!l)
- return function () {
- return template;
- };
-
- // Pre-compute the search/replace functions
- // This drastically speeds up template execution
- var replacements = [];
- var get_replacement = function (i) {
- var p = matches[i].replace(/[{}]+/g, '').trim().split('.');
- var n = p.length;
- var func;
-
- if (n > 1) {
- var cur;
- func = function (vars) {
- cur = vars;
- for (i = 0; i < n; i++) {
- cur = cur[p[i]];
- if (!cur) break;
- }
- return cur;
- };
- } else {
- p = p[0];
- func = function (vars) {
- return vars[p];
- };
- }
-
- replacements.push({
- s: matches[i],
- r: func,
- });
- };
- for (var i = 0; i < l; i++) {
- get_replacement(i);
- }
-
- // The compiled function
- return function (vars) {
- var ret = template + '';
- var r;
- for (i = 0; i < l; i++) {
- r = replacements[i];
- ret = ret.replace(r.s, r.r(vars));
- }
- return ret;
- };
- },
- };
- };
-
- JSONEditor.defaults.templates.ejs = function () {
- if (!window.EJS) return false;
-
- return {
- compile: function (template) {
- var compiled = new window.EJS({
- text: template,
- });
-
- return function (context) {
- return compiled.render(context);
- };
- },
- };
- };
-
- JSONEditor.defaults.templates.handlebars = function () {
- return window.Handlebars;
- };
-
- JSONEditor.defaults.templates.hogan = function () {
- if (!window.Hogan) return false;
-
- return {
- compile: function (template) {
- var compiled = window.Hogan.compile(template);
- return function (context) {
- return compiled.render(context);
- };
- },
- };
- };
-
- JSONEditor.defaults.templates.markup = function () {
- if (!window.Mark || !window.Mark.up) return false;
-
- return {
- compile: function (template) {
- return function (context) {
- return window.Mark.up(template, context);
- };
- },
- };
- };
-
- JSONEditor.defaults.templates.mustache = function () {
- if (!window.Mustache) return false;
-
- return {
- compile: function (template) {
- return function (view) {
- return window.Mustache.render(template, view);
- };
- },
- };
- };
-
- JSONEditor.defaults.templates.swig = function () {
- return window.swig;
- };
-
- JSONEditor.defaults.templates.underscore = function () {
- if (!window._) return false;
-
- return {
- compile: function (template) {
- return function (context) {
- return window._.template(template, context);
- };
- },
- };
- };
-
- // Set the default theme
- JSONEditor.defaults.theme = 'html';
-
- // Set the default template engine
- JSONEditor.defaults.template = 'default';
-
- // Default options when initializing JSON Editor
- JSONEditor.defaults.options = {};
-
- // String translate function
- JSONEditor.defaults.translate = function (key, variables) {
- var lang = JSONEditor.defaults.languages[JSONEditor.defaults.language];
- if (!lang) throw 'Unknown language ' + JSONEditor.defaults.language;
-
- var string =
- lang[key] || JSONEditor.defaults.languages[JSONEditor.defaults.default_language][key];
-
- if (typeof string === 'undefined') throw 'Unknown translate string ' + key;
-
- if (variables) {
- for (var i = 0; i < variables.length; i++) {
- string = string.replace(new RegExp('\\{\\{' + i + '}}', 'g'), variables[i]);
- }
- }
-
- return string;
- };
-
- // Translation strings and default languages
- JSONEditor.defaults.default_language = 'en';
- JSONEditor.defaults.language = JSONEditor.defaults.default_language;
- JSONEditor.defaults.languages.en = {
- /**
- * When a property is not set
- */
- error_notset: 'Property must be set',
- /**
- * When a string must not be empty
- */
- error_notempty: 'Value required',
- /**
- * When a value is not one of the enumerated values
- */
- error_enum: 'Value must be one of the enumerated values',
- /**
- * When a value doesn't validate any schema of a 'anyOf' combination
- */
- error_anyOf: 'Value must validate against at least one of the provided schemas',
- /**
- * When a value doesn't validate
- * @variables This key takes one variable: The number of schemas the value does not validate
- */
- error_oneOf:
- 'Value must validate against exactly one of the provided schemas. It currently validates against {{0}} of the schemas.',
- /**
- * When a value does not validate a 'not' schema
- */
- error_not: 'Value must not validate against the provided schema',
- /**
- * When a value does not match any of the provided types
- */
- error_type_union: 'Value must be one of the provided types',
- /**
- * When a value does not match the given type
- * @variables This key takes one variable: The type the value should be of
- */
- error_type: 'Value must be of type {{0}}',
- /**
- * When the value validates one of the disallowed types
- */
- error_disallow_union: 'Value must not be one of the provided disallowed types',
- /**
- * When the value validates a disallowed type
- * @variables This key takes one variable: The type the value should not be of
- */
- error_disallow: 'Value must not be of type {{0}}',
- /**
- * When a value is not a multiple of or divisible by a given number
- * @variables This key takes one variable: The number mentioned above
- */
- error_multipleOf: 'Value must be a multiple of {{0}}',
- /**
- * When a value is greater than it's supposed to be (exclusive)
- * @variables This key takes one variable: The maximum
- */
- error_maximum_excl: 'Value must be less than {{0}}',
- /**
- * When a value is greater than it's supposed to be (inclusive
- * @variables This key takes one variable: The maximum
- */
- error_maximum_incl: 'Value must be at most {{0}}',
- /**
- * When a value is lesser than it's supposed to be (exclusive)
- * @variables This key takes one variable: The minimum
- */
- error_minimum_excl: 'Value must be greater than {{0}}',
- /**
- * When a value is lesser than it's supposed to be (inclusive)
- * @variables This key takes one variable: The minimum
- */
- error_minimum_incl: 'Value must be at least {{0}}',
- /**
- * When a value have too many characters
- * @variables This key takes one variable: The maximum character count
- */
- error_maxLength: 'Value must be at most {{0}} characters long',
- /**
- * When a value does not have enough characters
- * @variables This key takes one variable: The minimum character count
- */
- error_minLength: 'Value must be at least {{0}} characters long',
- /**
- * When a value does not match a given pattern
- */
- error_pattern: 'Value must match the pattern {{0}}',
- /**
- * When an array has additional items whereas it is not supposed to
- */
- error_additionalItems: 'No additional items allowed in this array',
- /**
- * When there are to many items in an array
- * @variables This key takes one variable: The maximum item count
- */
- error_maxItems: 'Value must have at most {{0}} items',
- /**
- * When there are not enough items in an array
- * @variables This key takes one variable: The minimum item count
- */
- error_minItems: 'Value must have at least {{0}} items',
- /**
- * When an array is supposed to have unique items but has duplicates
- */
- error_uniqueItems: 'Array must have unique items',
- /**
- * When there are too many properties in an object
- * @variables This key takes one variable: The maximum property count
- */
- error_maxProperties: 'Object must have at most {{0}} properties',
- /**
- * When there are not enough properties in an object
- * @variables This key takes one variable: The minimum property count
- */
- error_minProperties: 'Object must have at least {{0}} properties',
- /**
- * When a required property is not defined
- * @variables This key takes one variable: The name of the missing property
- */
- error_required: "Object is missing the required property '{{0}}'",
- /**
- * When there is an additional property is set whereas there should be none
- * @variables This key takes one variable: The name of the additional property
- */
- error_additional_properties: 'No additional properties allowed, but property {{0}} is set',
- /**
- * When a dependency is not resolved
- * @variables This key takes one variable: The name of the missing property for the dependency
- */
- error_dependency: 'Must have property {{0}}',
- /**
- * Text on Delete All buttons
- */
- button_delete_all: 'All',
- /**
- * Title on Delete All buttons
- */
- button_delete_all_title: 'Delete All',
- /**
- * Text on Delete Last buttons
- * @variable This key takes one variable: The title of object to delete
- */
- button_delete_last: 'Last {{0}}',
- /**
- * Title on Delete Last buttons
- * @variable This key takes one variable: The title of object to delete
- */
- button_delete_last_title: 'Delete Last {{0}}',
- /**
- * Title on Add Row buttons
- * @variable This key takes one variable: The title of object to add
- */
- button_add_row_title: 'Add {{0}}',
- /**
- * Title on Move Down buttons
- */
- button_move_down_title: 'Move down',
- /**
- * Title on Move Up buttons
- */
- button_move_up_title: 'Move up',
- /**
- * Title on Delete Row buttons
- * @variable This key takes one variable: The title of object to delete
- */
- button_delete_row_title: 'Delete {{0}}',
- /**
- * Title on Delete Row buttons, short version (no parameter with the object title)
- */
- button_delete_row_title_short: 'Delete',
- /**
- * Title on Collapse buttons
- */
- button_collapse: 'Collapse',
- /**
- * Title on Expand buttons
- */
- button_expand: 'Expand',
- };
-
- // Miscellaneous Plugin Settings
- JSONEditor.plugins = {
- ace: {
- theme: '',
- },
- epiceditor: {},
- sceditor: {},
- select2: {},
- selectize: {},
- };
-
- // Default per-editor options
- $each(JSONEditor.defaults.editors, function (i, editor) {
- JSONEditor.defaults.editors[i].options = editor.options || {};
- });
-
- // Set the default resolvers
- // Use "multiple" as a fall back for everything
- JSONEditor.defaults.resolvers.unshift(function (schema) {
- if (typeof schema.type !== 'string') return 'multiple';
- });
- // If the type is not set but properties are defined, we can infer the type is actually object
- JSONEditor.defaults.resolvers.unshift(function (schema) {
- // If the schema is a simple type
- if (!schema.type && schema.properties) return 'object';
- });
- // If the type is set and it's a basic type, use the primitive editor
- JSONEditor.defaults.resolvers.unshift(function (schema) {
- // If the schema is a simple type
- if (typeof schema.type === 'string') return schema.type;
- });
- // Boolean editors
- JSONEditor.defaults.resolvers.unshift(function (schema) {
- if (schema.type === 'boolean') {
- // If explicitly set to 'checkbox', use that
- if (schema.format === 'checkbox' || (schema.options && schema.options.checkbox)) {
- return 'checkbox';
- }
- // Otherwise, default to select menu
- return JSONEditor.plugins.selectize.enable ? 'selectize' : 'select';
- }
- });
- // Use the multiple editor for schemas where the `type` is set to "any"
- JSONEditor.defaults.resolvers.unshift(function (schema) {
- // If the schema can be of any type
- if (schema.type === 'any') return 'multiple';
- });
- // Editor for base64 encoded files
- JSONEditor.defaults.resolvers.unshift(function (schema) {
- // If the schema can be of any type
- if (schema.type === 'string' && schema.media && schema.media.binaryEncoding === 'base64') {
- return 'base64';
- }
- });
- // Editor for uploading files
- JSONEditor.defaults.resolvers.unshift(function (schema) {
- if (
- schema.type === 'string' &&
- schema.format === 'url' &&
- schema.options &&
- schema.options.upload === true
- ) {
- if (window.FileReader) return 'upload';
- }
- });
- // Use the table editor for arrays with the format set to `table`
- JSONEditor.defaults.resolvers.unshift(function (schema) {
- // Type `array` with format set to `table`
- if (schema.type == 'array' && schema.format == 'table') {
- return 'table';
- }
- });
- // Use the `select` editor for dynamic enumSource enums
- JSONEditor.defaults.resolvers.unshift(function (schema) {
- if (schema.enumSource) return JSONEditor.plugins.selectize.enable ? 'selectize' : 'select';
- });
- // Use the `enum` or `select` editors for schemas with enumerated properties
- JSONEditor.defaults.resolvers.unshift(function (schema) {
- if (schema['enum']) {
- if (schema.type === 'array' || schema.type === 'object') {
- return 'enum';
- } else if (
- schema.type === 'number' ||
- schema.type === 'integer' ||
- schema.type === 'string'
- ) {
- return JSONEditor.plugins.selectize.enable ? 'selectize' : 'select';
- }
- }
- });
- // Specialized editors for arrays of strings
- JSONEditor.defaults.resolvers.unshift(function (schema) {
- if (
- schema.type === 'array' &&
- schema.items &&
- !Array.isArray(schema.items) &&
- schema.uniqueItems &&
- ['string', 'number', 'integer'].indexOf(schema.items.type) >= 0
- ) {
- // For enumerated strings, number, or integers
- if (schema.items.enum) {
- return 'multiselect';
- }
- // For non-enumerated strings (tag editor)
- else if (JSONEditor.plugins.selectize.enable && schema.items.type === 'string') {
- return 'arraySelectize';
- }
- }
- });
- // Use the multiple editor for schemas with `oneOf` set
- JSONEditor.defaults.resolvers.unshift(function (schema) {
- // If this schema uses `oneOf` or `anyOf`
- if (schema.oneOf || schema.anyOf) return 'multiple';
- });
-
- /**
- * This is a small wrapper for using JSON Editor like a typical jQuery plugin.
- */
- (function () {
- if (window.jQuery || window.Zepto) {
- var $ = window.jQuery || window.Zepto;
- $.jsoneditor = JSONEditor.defaults;
-
- $.fn.jsoneditor = function (options) {
- var self = this;
- var editor = this.data('jsoneditor');
- if (options === 'value') {
- if (!editor)
- throw 'Must initialize jsoneditor before getting/setting the value';
-
- // Set value
- if (arguments.length > 1) {
- editor.setValue(arguments[1]);
- }
- // Get value
- else {
- return editor.getValue();
- }
- } else if (options === 'validate') {
- if (!editor) throw 'Must initialize jsoneditor before validating';
-
- // Validate a specific value
- if (arguments.length > 1) {
- return editor.validate(arguments[1]);
- }
- // Validate current value
- else {
- return editor.validate();
- }
- } else if (options === 'destroy') {
- if (editor) {
- editor.destroy();
- this.data('jsoneditor', null);
- }
- } else {
- // Destroy first
- if (editor) {
- editor.destroy();
- }
-
- // Create editor
- editor = new JSONEditor(this.get(0), options);
- this.data('jsoneditor', editor);
-
- // Setup event listeners
- editor.on('change', function () {
- self.trigger('change');
- });
- editor.on('ready', function () {
- self.trigger('ready');
- });
- }
-
- return this;
- };
- }
- })();
-
- window.JSONEditor = JSONEditor;
-})();
-
-//# sourceMappingURL=jsoneditor.js.map
From ab44a2a2d1d2b8b99978711adb8ae6bcb882e1a4 Mon Sep 17 00:00:00 2001
From: Mirko Sekulic
Date: Mon, 3 Jun 2024 12:49:05 +0200
Subject: [PATCH 13/27] Security issues fix (#12903)
* local variable disposal
* more local variable disposal
---
.../GiteaAPIWrapper/GiteaAPIWrapper.cs | 16 ++++++++--------
.../Implementation/ResourceRegistryService.cs | 2 +-
.../Implementation/SchemaModelService.cs | 2 +-
.../AltinnAuthenticationClient.cs | 4 ++--
.../AltinnAuthorizationPolicyClient.cs | 2 +-
.../AltinnStorageAppMetadataClient.cs | 2 +-
.../AltinnStorageTextResourceClient.cs | 2 +-
7 files changed, 15 insertions(+), 15 deletions(-)
diff --git a/backend/src/Designer/Services/Implementation/GiteaAPIWrapper/GiteaAPIWrapper.cs b/backend/src/Designer/Services/Implementation/GiteaAPIWrapper/GiteaAPIWrapper.cs
index 061c9738cd0..6d9852d88a4 100644
--- a/backend/src/Designer/Services/Implementation/GiteaAPIWrapper/GiteaAPIWrapper.cs
+++ b/backend/src/Designer/Services/Implementation/GiteaAPIWrapper/GiteaAPIWrapper.cs
@@ -172,8 +172,8 @@ public async Task> GetTeams()
///
public async Task PutStarred(string org, string repository)
{
- HttpRequestMessage request = new(HttpMethod.Put, $"user/starred/{org}/{repository}");
- HttpResponseMessage response = await _httpClient.SendAsync(request);
+ using HttpRequestMessage request = new(HttpMethod.Put, $"user/starred/{org}/{repository}");
+ using HttpResponseMessage response = await _httpClient.SendAsync(request);
return response.StatusCode == HttpStatusCode.NoContent;
}
@@ -181,8 +181,8 @@ public async Task PutStarred(string org, string repository)
///
public async Task DeleteStarred(string org, string repository)
{
- HttpRequestMessage request = new(HttpMethod.Delete, $"user/starred/{org}/{repository}");
- HttpResponseMessage response = await _httpClient.SendAsync(request);
+ using HttpRequestMessage request = new(HttpMethod.Delete, $"user/starred/{org}/{repository}");
+ using HttpResponseMessage response = await _httpClient.SendAsync(request);
return response.StatusCode == HttpStatusCode.NoContent;
}
@@ -436,7 +436,7 @@ public async Task CreateBranch(string org, string repository, string bra
private async Task PostBranch(string org, string repository, string branchName)
{
string content = $"{{\"new_branch_name\":\"{branchName}\"}}";
- HttpRequestMessage message = new(HttpMethod.Post, $"repos/{org}/{repository}/branches");
+ using HttpRequestMessage message = new(HttpMethod.Post, $"repos/{org}/{repository}/branches");
message.Content = new StringContent(content, Encoding.UTF8, "application/json");
return await _httpClient.SendAsync(message);
@@ -573,7 +573,7 @@ public async Task> GetDirectoryAsync(string org, string a
public async Task CreatePullRequest(string org, string repository, CreatePullRequestOption createPullRequestOption)
{
string content = JsonSerializer.Serialize(createPullRequestOption);
- HttpResponseMessage response = await _httpClient.PostAsync($"repos/{org}/{repository}/pulls", new StringContent(content, Encoding.UTF8, "application/json"));
+ using HttpResponseMessage response = await _httpClient.PostAsync($"repos/{org}/{repository}/pulls", new StringContent(content, Encoding.UTF8, "application/json"));
return response.IsSuccessStatusCode;
}
@@ -648,8 +648,8 @@ private async Task DeleteAllAppKeys(List appKeys, string csrf)
formValues.Add(new KeyValuePair("_csrf", csrf));
formValues.Add(new KeyValuePair("id", key));
- FormUrlEncodedContent content = new(formValues);
- HttpResponseMessage response = await client.PostAsync(giteaUrl, content);
+ using FormUrlEncodedContent content = new(formValues);
+ using HttpResponseMessage response = await client.PostAsync(giteaUrl, content);
if (!response.StatusCode.Equals(HttpStatusCode.OK))
{
break;
diff --git a/backend/src/Designer/Services/Implementation/ResourceRegistryService.cs b/backend/src/Designer/Services/Implementation/ResourceRegistryService.cs
index d39cc809eaf..71c67c901d5 100644
--- a/backend/src/Designer/Services/Implementation/ResourceRegistryService.cs
+++ b/backend/src/Designer/Services/Implementation/ResourceRegistryService.cs
@@ -111,7 +111,7 @@ public async Task PublishServiceResource(ServiceResource serviceRe
if (policyPath != null)
{
- MultipartFormDataContent content = new MultipartFormDataContent();
+ using MultipartFormDataContent content = new MultipartFormDataContent();
if (ResourceAdminHelper.ValidFilePath(policyPath))
{
diff --git a/backend/src/Designer/Services/Implementation/SchemaModelService.cs b/backend/src/Designer/Services/Implementation/SchemaModelService.cs
index 944408dc8e0..cb7f5ca8f91 100644
--- a/backend/src/Designer/Services/Implementation/SchemaModelService.cs
+++ b/backend/src/Designer/Services/Implementation/SchemaModelService.cs
@@ -173,7 +173,7 @@ public async Task BuildSchemaFromXsd(AltinnRepoEditingContext altinnRepo
cancellationToken.ThrowIfCancellationRequested();
var altinnAppGitRepository = _altinnGitRepositoryFactory.GetAltinnAppGitRepository(altinnRepoEditingContext.Org, altinnRepoEditingContext.Repo, altinnRepoEditingContext.Developer);
- MemoryStream xsdMemoryStream = new MemoryStream();
+ using MemoryStream xsdMemoryStream = new MemoryStream();
xsdStream.CopyTo(xsdMemoryStream);
string jsonContent;
AltinnRepositoryType altinnRepositoryType = await altinnAppGitRepository.GetRepositoryType();
diff --git a/backend/src/Designer/TypedHttpClients/AltinnAuthentication/AltinnAuthenticationClient.cs b/backend/src/Designer/TypedHttpClients/AltinnAuthentication/AltinnAuthenticationClient.cs
index ecbf97121e5..3409144ad0c 100644
--- a/backend/src/Designer/TypedHttpClients/AltinnAuthentication/AltinnAuthenticationClient.cs
+++ b/backend/src/Designer/TypedHttpClients/AltinnAuthentication/AltinnAuthenticationClient.cs
@@ -54,12 +54,12 @@ public async Task ConvertTokenAsync(string token, Uri uri)
* Have to create a HttpRequestMessage instead of using helper extension methods like _httpClient.PostAsync(...)
* because the endpoint is built up in that way
*/
- HttpRequestMessage message = new HttpRequestMessage
+ using HttpRequestMessage message = new HttpRequestMessage
{
RequestUri = new Uri($"{uri.Scheme}://{uri.Host}:{uri.Port}/{_platformSettings.ApiAuthenticationConvertUri}")
};
- HttpResponseMessage response = await _httpClient.SendAsync(message);
+ using HttpResponseMessage response = await _httpClient.SendAsync(message);
_logger.LogInformation($"//AltinnAuthenticationClient // ConvertTokenAsync // Response: {response}");
var outputToken = await response.Content.ReadAsAsync();
diff --git a/backend/src/Designer/TypedHttpClients/AltinnAuthorization/AltinnAuthorizationPolicyClient.cs b/backend/src/Designer/TypedHttpClients/AltinnAuthorization/AltinnAuthorizationPolicyClient.cs
index a13d2a75d64..0007cec5d23 100644
--- a/backend/src/Designer/TypedHttpClients/AltinnAuthorization/AltinnAuthorizationPolicyClient.cs
+++ b/backend/src/Designer/TypedHttpClients/AltinnAuthorization/AltinnAuthorizationPolicyClient.cs
@@ -44,7 +44,7 @@ public async Task SavePolicy(string org, string app, string policyFile, string e
* because the base address can change on each request and after HttpClient gets initial base address,
* it is not advised (and not allowed) to change base address.
*/
- HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, uri)
+ using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, uri)
{
Content = new StringContent(policyFile, Encoding.UTF8, "application/xml"),
};
diff --git a/backend/src/Designer/TypedHttpClients/AltinnStorage/AltinnStorageAppMetadataClient.cs b/backend/src/Designer/TypedHttpClients/AltinnStorage/AltinnStorageAppMetadataClient.cs
index a94b59a8219..ff271d2ce8d 100644
--- a/backend/src/Designer/TypedHttpClients/AltinnStorage/AltinnStorageAppMetadataClient.cs
+++ b/backend/src/Designer/TypedHttpClients/AltinnStorage/AltinnStorageAppMetadataClient.cs
@@ -58,7 +58,7 @@ public async Task UpsertApplicationMetadata(
* because the base address can change on each request and after HttpClient gets initial base address,
* it is not advised (and not allowed) to change base address.
*/
- HttpRequestMessage request = new(HttpMethod.Post, uri)
+ using HttpRequestMessage request = new(HttpMethod.Post, uri)
{
Content = new StringContent(stringContent, Encoding.UTF8, "application/json"),
};
diff --git a/backend/src/Designer/TypedHttpClients/AltinnStorage/AltinnStorageTextResourceClient.cs b/backend/src/Designer/TypedHttpClients/AltinnStorage/AltinnStorageTextResourceClient.cs
index e648f16d013..e8444eda136 100644
--- a/backend/src/Designer/TypedHttpClients/AltinnStorage/AltinnStorageTextResourceClient.cs
+++ b/backend/src/Designer/TypedHttpClients/AltinnStorage/AltinnStorageTextResourceClient.cs
@@ -38,7 +38,7 @@ public async Task Upsert(string org, string app, TextResource textResource, stri
Uri uri = await CreatePostUri(envName, org, app);
HttpClientHelper.AddSubscriptionKeys(_httpClient, uri, _platformSettings);
string stringContent = JsonSerializer.Serialize(textResource);
- HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, uri)
+ using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, uri)
{
Content = new StringContent(stringContent, Encoding.UTF8, "application/json"),
};
From fea8a6bdb7f383ca1450776b94ee610cdbcc6aa8 Mon Sep 17 00:00:00 2001
From: WilliamThorenfeldt
<133344438+WilliamThorenfeldt@users.noreply.github.com>
Date: Mon, 3 Jun 2024 14:36:30 +0200
Subject: [PATCH 14/27] Implementing StudioPopover (#12888)
---
.../StudioPopover/StudioPopover.mdx | 14 +++++
.../StudioPopover/StudioPopover.stories.tsx | 59 +++++++++++++++++++
.../StudioPopover/StudioPopover.test.tsx | 36 +++++++++++
.../StudioPopover/StudioPopover.tsx | 33 +++++++++++
.../src/components/StudioPopover/index.ts | 1 +
.../studio-components/src/components/index.ts | 1 +
6 files changed, 144 insertions(+)
create mode 100644 frontend/libs/studio-components/src/components/StudioPopover/StudioPopover.mdx
create mode 100644 frontend/libs/studio-components/src/components/StudioPopover/StudioPopover.stories.tsx
create mode 100644 frontend/libs/studio-components/src/components/StudioPopover/StudioPopover.test.tsx
create mode 100644 frontend/libs/studio-components/src/components/StudioPopover/StudioPopover.tsx
create mode 100644 frontend/libs/studio-components/src/components/StudioPopover/index.ts
diff --git a/frontend/libs/studio-components/src/components/StudioPopover/StudioPopover.mdx b/frontend/libs/studio-components/src/components/StudioPopover/StudioPopover.mdx
new file mode 100644
index 00000000000..a18a2bf442f
--- /dev/null
+++ b/frontend/libs/studio-components/src/components/StudioPopover/StudioPopover.mdx
@@ -0,0 +1,14 @@
+import { Canvas, Meta } from '@storybook/blocks';
+import { Heading, Paragraph } from '@digdir/design-system-react';
+import * as StudioPopoverStories from './StudioPopover.stories';
+
+
+
+
+ StudioPopover
+
+
+ StudioPopover is an extension of the digdir-designsystemet `Popover` component.
+
+
+
diff --git a/frontend/libs/studio-components/src/components/StudioPopover/StudioPopover.stories.tsx b/frontend/libs/studio-components/src/components/StudioPopover/StudioPopover.stories.tsx
new file mode 100644
index 00000000000..5d8b1ce1937
--- /dev/null
+++ b/frontend/libs/studio-components/src/components/StudioPopover/StudioPopover.stories.tsx
@@ -0,0 +1,59 @@
+import React from 'react';
+import type { Meta, StoryFn } from '@storybook/react';
+import { StudioPopover } from './StudioPopover';
+
+const studioPopoverPlacementOptions: string[] = [
+ 'top',
+ 'right',
+ 'bottom',
+ 'left',
+ 'top-start',
+ 'top-end',
+ 'right-start',
+ 'right-end',
+ 'bottom-start',
+ 'bottom-end',
+ 'left-start',
+ 'left-end',
+];
+const studioPopoverVariantOptions: string[] = ['default', 'danger', 'info', 'warning'];
+const studioPopoverSizeOptions: string[] = ['small', 'medium', 'large'];
+
+type Story = StoryFn;
+
+const meta: Meta = {
+ title: 'Studio/StudioPopover',
+ component: StudioPopover,
+ argTypes: {
+ placement: {
+ control: 'select',
+ options: studioPopoverPlacementOptions,
+ },
+ variant: {
+ control: 'radio',
+ options: studioPopoverVariantOptions,
+ },
+ size: {
+ control: 'select',
+ options: studioPopoverSizeOptions,
+ },
+ },
+};
+
+export const Preview: Story = (args): React.ReactElement => {
+ return (
+
+ My trigger!
+ StudioPopover content
+
+ );
+};
+
+Preview.args = {
+ placement: 'top',
+ variant: 'default',
+ size: 'medium',
+ onOpenChange: () => {},
+};
+
+export default meta;
diff --git a/frontend/libs/studio-components/src/components/StudioPopover/StudioPopover.test.tsx b/frontend/libs/studio-components/src/components/StudioPopover/StudioPopover.test.tsx
new file mode 100644
index 00000000000..c22ea254a03
--- /dev/null
+++ b/frontend/libs/studio-components/src/components/StudioPopover/StudioPopover.test.tsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import { StudioPopover, type StudioPopoverProps } from './StudioPopover';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+
+const triggerText: string = 'My trigger';
+const contentText: string = 'Content';
+
+describe('StudioPopover', () => {
+ it('hides the content initially', () => {
+ renderStudioPopover();
+
+ const content = screen.queryByText(contentText);
+ expect(content).not.toBeInTheDocument();
+ });
+
+ it('renders the popover content when the trigger is clicked', async () => {
+ const user = userEvent.setup();
+ renderStudioPopover();
+ expect(screen.queryByText(contentText)).not.toBeInTheDocument();
+
+ const button = screen.getByRole('button', { name: triggerText });
+ await user.click(button);
+
+ expect(screen.getByText(contentText)).toBeInTheDocument();
+ });
+});
+
+const renderStudioPopover = (studioPopoverProps: Partial = {}) => {
+ return render(
+
+ {triggerText}
+ {contentText}
+ ,
+ );
+};
diff --git a/frontend/libs/studio-components/src/components/StudioPopover/StudioPopover.tsx b/frontend/libs/studio-components/src/components/StudioPopover/StudioPopover.tsx
new file mode 100644
index 00000000000..ce01fce8af8
--- /dev/null
+++ b/frontend/libs/studio-components/src/components/StudioPopover/StudioPopover.tsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import {
+ type PopoverProps,
+ Popover,
+ type PopoverTriggerProps,
+ type PopoverContentProps,
+} from '@digdir/design-system-react';
+
+const StudioPopoverTrigger = ({ ...rest }: PopoverTriggerProps): React.ReactElement => {
+ return ;
+};
+
+const StudioPopoverContent = ({ ...rest }: PopoverContentProps): React.ReactElement => {
+ return ;
+};
+
+export type StudioPopoverProps = PopoverProps;
+
+const StudioPopoverRoot = ({ ...rest }: StudioPopoverProps): React.ReactElement => {
+ return ;
+};
+
+type StudioPopoverComponent = typeof StudioPopoverRoot & {
+ Trigger: typeof StudioPopoverTrigger;
+ Content: typeof StudioPopoverContent;
+};
+
+const StudioPopover = StudioPopoverRoot as StudioPopoverComponent;
+
+StudioPopover.Trigger = StudioPopoverTrigger;
+StudioPopover.Content = StudioPopoverContent;
+
+export { StudioPopover, StudioPopoverTrigger, StudioPopoverContent };
diff --git a/frontend/libs/studio-components/src/components/StudioPopover/index.ts b/frontend/libs/studio-components/src/components/StudioPopover/index.ts
new file mode 100644
index 00000000000..606d5a42b60
--- /dev/null
+++ b/frontend/libs/studio-components/src/components/StudioPopover/index.ts
@@ -0,0 +1 @@
+export { StudioPopover } from './StudioPopover';
diff --git a/frontend/libs/studio-components/src/components/index.ts b/frontend/libs/studio-components/src/components/index.ts
index a34601bddcf..833e68168f1 100644
--- a/frontend/libs/studio-components/src/components/index.ts
+++ b/frontend/libs/studio-components/src/components/index.ts
@@ -16,6 +16,7 @@ export * from './StudioModal';
export * from './StudioNativeSelect';
export * from './StudioNotFoundPage';
export * from './StudioPageSpinner';
+export * from './StudioPopover';
export * from './StudioProperty';
export * from './StudioSectionHeader';
export * from './StudioSpinner';
From c91bfd7355699520479869699c33e1700888e98b Mon Sep 17 00:00:00 2001
From: WilliamThorenfeldt
<133344438+WilliamThorenfeldt@users.noreply.github.com>
Date: Mon, 3 Jun 2024 15:41:11 +0200
Subject: [PATCH 15/27] 12600 process receipt fixes (#12884)
* Fixing UI issues#
* fixing logic for custom receipt
* refactor code
* fixing typecheck
* fixing some comments
* trying to fix issue
* trying to fix issue
* Delete dataType connection in LayoutSets based on dataTypeId directly when deleting a data model
* Fixing tests
* Fixing tests
* Remove comment
* PR feedback
---------
Co-authored-by: andreastanderen
---
.../Implementation/SchemaModelService.cs | 4 +-
.../features/processEditor/ProcessEditor.tsx | 7 +++
frontend/language/src/nb.json | 2 +-
.../process-editor/src/ProcessEditor.test.tsx | 1 +
.../process-editor/src/ProcessEditor.tsx | 3 +
.../ConfigContent/ConfigContent.tsx | 10 +---
.../CreateCustomReceiptForm.test.tsx | 55 +++++++------------
.../CreateCustomReceiptForm.tsx | 21 +++----
.../SelectCustomReceiptDataModelId.test.tsx | 43 +++++++--------
.../SelectCustomReceiptDataModelId.tsx | 49 ++++++++---------
.../CustomReceipt/CustomReceipt.module.css | 5 ++
.../CustomReceipt/CustomReceipt.test.tsx | 9 ++-
.../CustomReceipt/CustomReceipt.tsx | 12 ++--
.../SelectDataType/SelectDataType.tsx | 2 +
.../src/contexts/BpmnApiContext.tsx | 40 +-------------
.../configPanelUtils/configPanelUtils.test.ts | 21 -------
.../configPanelUtils/configPanelUtils.ts | 10 ----
.../src/utils/configPanelUtils/index.ts | 1 -
.../test/mocks/bpmnContextMock.ts | 1 +
.../queries/useAppMetadataModelIdsQuery.ts | 8 ++-
20 files changed, 113 insertions(+), 191 deletions(-)
diff --git a/backend/src/Designer/Services/Implementation/SchemaModelService.cs b/backend/src/Designer/Services/Implementation/SchemaModelService.cs
index cb7f5ca8f91..574bcaa4da2 100644
--- a/backend/src/Designer/Services/Implementation/SchemaModelService.cs
+++ b/backend/src/Designer/Services/Implementation/SchemaModelService.cs
@@ -416,8 +416,8 @@ private static async Task DeleteDatatypeFromApplicationMetadataAndLayoutSets(Alt
if (altinnAppGitRepository.AppUsesLayoutSets())
{
var layoutSets = await altinnAppGitRepository.GetLayoutSetsFile();
- List layoutSetsWithDeletedDataType = layoutSets.Sets.FindAll(set => set.DataType == dataTypeToDelete.Id);
- foreach (LayoutSetConfig layoutSet in layoutSetsWithDeletedDataType)
+ List layoutSetsWithDataTypeToDelete = layoutSets.Sets.FindAll(set => set.DataType == id);
+ foreach (LayoutSetConfig layoutSet in layoutSetsWithDataTypeToDelete)
{
layoutSet.DataType = null;
}
diff --git a/frontend/app-development/features/processEditor/ProcessEditor.tsx b/frontend/app-development/features/processEditor/ProcessEditor.tsx
index 96e4329d79b..d67b5f0e528 100644
--- a/frontend/app-development/features/processEditor/ProcessEditor.tsx
+++ b/frontend/app-development/features/processEditor/ProcessEditor.tsx
@@ -65,6 +65,11 @@ export const ProcessEditor = (): React.ReactElement => {
const { data: availableDataModelIds, isPending: availableDataModelIdsPending } =
useAppMetadataModelIdsQuery(org, app);
+ const { data: allDataModelIds, isPending: allDataModelIdsPending } = useAppMetadataModelIdsQuery(
+ org,
+ app,
+ false,
+ );
const { data: layoutSets } = useLayoutSetsQuery(org, app);
const pendingApiOperations: boolean =
@@ -74,6 +79,7 @@ export const ProcessEditor = (): React.ReactElement => {
deleteLayoutSetPending ||
updateDataTypePending ||
availableDataModelIdsPending ||
+ allDataModelIdsPending ||
isPendingCurrentPolicy;
const { onWSMessageReceived } = useWebSocket({
@@ -139,6 +145,7 @@ export const ProcessEditor = (): React.ReactElement => {
return (
{
const layoutSet = layoutSets?.sets.find((set) => set.tasks.includes(bpmnDetails.id));
const existingDataTypeForTask = layoutSet?.dataType;
- const dataModelIds = getDataModelOptions(availableDataModelIds, existingDataTypeForTask);
-
const taskHasConnectedLayoutSet = layoutSets?.sets?.some((set) => set.tasks[0] == bpmnDetails.id);
return (
@@ -53,7 +47,7 @@ export const ConfigContent = (): React.ReactElement => {
{taskHasConnectedLayoutSet && (
)}
diff --git a/frontend/packages/process-editor/src/components/ConfigPanel/ConfigEndEvent/CustomReceiptContent/CreateCustomReceiptForm/CreateCustomReceiptForm.test.tsx b/frontend/packages/process-editor/src/components/ConfigPanel/ConfigEndEvent/CustomReceiptContent/CreateCustomReceiptForm/CreateCustomReceiptForm.test.tsx
index 8112407a874..5c20c780dfb 100644
--- a/frontend/packages/process-editor/src/components/ConfigPanel/ConfigEndEvent/CustomReceiptContent/CreateCustomReceiptForm/CreateCustomReceiptForm.test.tsx
+++ b/frontend/packages/process-editor/src/components/ConfigPanel/ConfigEndEvent/CustomReceiptContent/CreateCustomReceiptForm/CreateCustomReceiptForm.test.tsx
@@ -17,10 +17,9 @@ import { queryOptionMock } from 'app-shared/mocks/queryOptionMock';
import { PROTECTED_TASK_NAME_CUSTOM_RECEIPT } from 'app-shared/constants';
const mockAddLayoutSet = jest.fn().mockImplementation(queryOptionMock);
-const mockMutateDataType = jest.fn().mockImplementation(queryOptionMock);
const mockOnCloseForm = jest.fn();
-const mockAvailableDataModelIds: string[] = ['model1', 'model2'];
+const mockAllDataModelIds: string[] = ['model1', 'model2'];
const defaultProps: CreateCustomReceiptFormProps = {
onCloseForm: mockOnCloseForm,
@@ -28,9 +27,8 @@ const defaultProps: CreateCustomReceiptFormProps = {
const defaultBpmnApiContextProps: BpmnApiContextProps = {
...mockBpmnApiContextValue,
- availableDataModelIds: mockAvailableDataModelIds,
+ allDataModelIds: mockAllDataModelIds,
addLayoutSet: mockAddLayoutSet,
- mutateDataType: mockMutateDataType,
};
describe('CreateCustomReceiptForm', () => {
@@ -46,13 +44,14 @@ describe('CreateCustomReceiptForm', () => {
const newId: string = 'newLayoutSetId';
await user.type(layoutSetInput, newId);
- const selectElement = screen.getByLabelText(
- textMock('process_editor.configuration_panel_custom_receipt_select_data_model_label'),
- );
- await user.click(selectElement);
+ const combobox = screen.getByRole('combobox', {
+ name: textMock('process_editor.configuration_panel_custom_receipt_select_data_model_label'),
+ });
+ await user.click(combobox);
- const optionElement = screen.getByRole('option', { name: mockAvailableDataModelIds[0] });
- await user.selectOptions(selectElement, optionElement);
+ const optionElement = screen.getByRole('option', { name: mockAllDataModelIds[0] });
+ await user.click(optionElement);
+ await user.keyboard('{Escape}');
const createButton = screen.getByRole('button', {
name: textMock('process_editor.configuration_panel_custom_receipt_create_button'),
@@ -65,6 +64,7 @@ describe('CreateCustomReceiptForm', () => {
{
layoutSetConfig: {
id: newId,
+ dataType: mockAllDataModelIds[0],
tasks: [PROTECTED_TASK_NAME_CUSTOM_RECEIPT],
},
layoutSetIdToUpdate: newId,
@@ -75,19 +75,6 @@ describe('CreateCustomReceiptForm', () => {
),
);
- await waitFor(() => expect(mockMutateDataType).toHaveBeenCalledTimes(1));
- await waitFor(() =>
- expect(mockMutateDataType).toHaveBeenCalledWith(
- {
- connectedTaskId: PROTECTED_TASK_NAME_CUSTOM_RECEIPT,
- newDataType: mockAvailableDataModelIds[0],
- },
- {
- onSuccess: expect.any(Function),
- },
- ),
- );
-
expect(mockOnCloseForm).toHaveBeenCalled();
});
@@ -95,13 +82,14 @@ describe('CreateCustomReceiptForm', () => {
const user = userEvent.setup();
renderCreateCustomReceiptForm();
- const selectElement = screen.getByLabelText(
- textMock('process_editor.configuration_panel_custom_receipt_select_data_model_label'),
- );
- await user.click(selectElement);
+ const combobox = screen.getByRole('combobox', {
+ name: textMock('process_editor.configuration_panel_custom_receipt_select_data_model_label'),
+ });
+ await user.click(combobox);
- const optionElement = screen.getByRole('option', { name: mockAvailableDataModelIds[0] });
- await user.selectOptions(selectElement, optionElement);
+ const optionElement = screen.getByRole('option', { name: mockAllDataModelIds[0] });
+ await user.click(optionElement);
+ await user.keyboard('{Escape}');
const createButton = screen.getByRole('button', {
name: textMock('process_editor.configuration_panel_custom_receipt_create_button'),
@@ -190,18 +178,15 @@ describe('CreateCustomReceiptForm', () => {
it('displays error when there are no value present for data model id', async () => {
const user = userEvent.setup();
- renderCreateCustomReceiptForm({ bpmnApiContextProps: mockBpmnApiContextValue });
+ renderCreateCustomReceiptForm({
+ bpmnApiContextProps: { allDataModelIds: mockAllDataModelIds },
+ });
const layoutSetInput = screen.getByLabelText(
textMock('process_editor.configuration_panel_custom_receipt_textfield_label'),
);
await user.type(layoutSetInput, 'newLayoutSetId');
- const selectElement = screen.getByLabelText(
- textMock('process_editor.configuration_panel_custom_receipt_select_data_model_label'),
- );
- await user.click(selectElement);
-
const createButton = screen.getByRole('button', {
name: textMock('process_editor.configuration_panel_custom_receipt_create_button'),
});
diff --git a/frontend/packages/process-editor/src/components/ConfigPanel/ConfigEndEvent/CustomReceiptContent/CreateCustomReceiptForm/CreateCustomReceiptForm.tsx b/frontend/packages/process-editor/src/components/ConfigPanel/ConfigEndEvent/CustomReceiptContent/CreateCustomReceiptForm/CreateCustomReceiptForm.tsx
index d7e794c92a7..7c0020a6aca 100644
--- a/frontend/packages/process-editor/src/components/ConfigPanel/ConfigEndEvent/CustomReceiptContent/CreateCustomReceiptForm/CreateCustomReceiptForm.tsx
+++ b/frontend/packages/process-editor/src/components/ConfigPanel/ConfigEndEvent/CustomReceiptContent/CreateCustomReceiptForm/CreateCustomReceiptForm.tsx
@@ -4,7 +4,6 @@ import { useTranslation } from 'react-i18next';
import { StudioButton, StudioTextfield } from '@studio/components';
import { useBpmnApiContext } from '../../../../../contexts/BpmnApiContext';
import { type CustomReceiptType } from '../../../../../types/CustomReceiptType';
-import { type DataTypeChange } from 'app-shared/types/api/DataTypeChange';
import { PROTECTED_TASK_NAME_CUSTOM_RECEIPT } from 'app-shared/constants';
import { type LayoutSetConfig } from 'app-shared/types/api/LayoutSetsResponse';
import { SelectCustomReceiptDataModelId } from './SelectCustomReceiptDataModelId';
@@ -18,9 +17,11 @@ export const CreateCustomReceiptForm = ({
onCloseForm,
}: CreateCustomReceiptFormProps): React.ReactElement => {
const { t } = useTranslation();
- const { layoutSets, existingCustomReceiptLayoutSetId, addLayoutSet, mutateDataType } =
+ const { allDataModelIds, layoutSets, existingCustomReceiptLayoutSetId, addLayoutSet } =
useBpmnApiContext();
+ const allDataModelIdsEmpty: boolean = allDataModelIds.length === 0;
+
const [layoutSetError, setLayoutSetError] = useState(null);
const [dataModelError, setDataModelError] = useState(null);
@@ -46,6 +47,7 @@ export const CreateCustomReceiptForm = ({
const updateErrors = (customReceiptForm: CustomReceiptType) => {
const { layoutSetId, dataModelId } = customReceiptForm;
setLayoutSetError(!layoutSetId ? t('validation_errors.required') : null);
+
setDataModelError(
!dataModelId
? t('process_editor.configuration_panel_custom_receipt_create_data_model_error')
@@ -56,6 +58,7 @@ export const CreateCustomReceiptForm = ({
const createNewCustomReceipt = (customReceipt: CustomReceiptType) => {
const customReceiptLayoutSetConfig: LayoutSetConfig = {
id: customReceipt.layoutSetId,
+ dataType: customReceipt.dataModelId,
tasks: [PROTECTED_TASK_NAME_CUSTOM_RECEIPT],
};
addLayoutSet(
@@ -64,21 +67,11 @@ export const CreateCustomReceiptForm = ({
layoutSetConfig: customReceiptLayoutSetConfig,
},
{
- onSuccess: () => saveDataModel(customReceipt.dataModelId),
+ onSuccess: onCloseForm,
},
);
};
- const saveDataModel = (dataModelId: string) => {
- const dataTypeChange: DataTypeChange = {
- newDataType: dataModelId,
- connectedTaskId: PROTECTED_TASK_NAME_CUSTOM_RECEIPT,
- };
- mutateDataType(dataTypeChange, {
- onSuccess: onCloseForm,
- });
- };
-
const handleValidateLayoutSetId = (event: React.ChangeEvent) => {
const validationResult = getLayoutSetIdValidationErrorKey(
layoutSets,
@@ -103,7 +96,7 @@ export const CreateCustomReceiptForm = ({
onChange={() => setDataModelError(null)}
/>
-
+
{t('process_editor.configuration_panel_custom_receipt_create_button')}
diff --git a/frontend/packages/process-editor/src/components/ConfigPanel/ConfigEndEvent/CustomReceiptContent/CreateCustomReceiptForm/SelectCustomReceiptDataModelId/SelectCustomReceiptDataModelId.test.tsx b/frontend/packages/process-editor/src/components/ConfigPanel/ConfigEndEvent/CustomReceiptContent/CreateCustomReceiptForm/SelectCustomReceiptDataModelId/SelectCustomReceiptDataModelId.test.tsx
index a463b0dd67c..c5e2e49fbfe 100644
--- a/frontend/packages/process-editor/src/components/ConfigPanel/ConfigEndEvent/CustomReceiptContent/CreateCustomReceiptForm/SelectCustomReceiptDataModelId/SelectCustomReceiptDataModelId.test.tsx
+++ b/frontend/packages/process-editor/src/components/ConfigPanel/ConfigEndEvent/CustomReceiptContent/CreateCustomReceiptForm/SelectCustomReceiptDataModelId/SelectCustomReceiptDataModelId.test.tsx
@@ -19,7 +19,7 @@ import {
const mockError: string = 'Error';
const mockOnChange = jest.fn();
-const mockAvailableDataModelIds: string[] = ['model1', 'model2'];
+const mockAllDataModelIds: string[] = ['model1', 'model2'];
const defaultProps: SelectCustomReceiptDataModelIdProps = {
error: mockError,
@@ -32,38 +32,37 @@ describe('SelectCustomReceiptDataModelId', () => {
it('calls onChange function when an option is selected', async () => {
const user = userEvent.setup();
renderSelectCustomReceiptDataModelId({
- availableDataModelIds: mockAvailableDataModelIds,
+ allDataModelIds: mockAllDataModelIds,
});
- const selectElement = screen.getByLabelText(
- textMock('process_editor.configuration_panel_custom_receipt_select_data_model_label'),
- );
- await user.click(selectElement);
+ const combobox = screen.getByRole('combobox', {
+ name: textMock('process_editor.configuration_panel_custom_receipt_select_data_model_label'),
+ });
+ await user.click(combobox);
- const optionElement = screen.getByRole('option', { name: mockAvailableDataModelIds[0] });
- await user.selectOptions(selectElement, optionElement);
+ const optionElement = screen.getByRole('option', { name: mockAllDataModelIds[0] });
+ await user.click(optionElement);
expect(mockOnChange).toHaveBeenCalledTimes(1);
});
- it('displays the description when there are no available data model ids', () => {
- renderSelectCustomReceiptDataModelId();
-
- const description = screen.getByText(
- textMock('process_editor.configuration_panel_custom_receipt_select_data_model_description'),
- );
- expect(description).toBeInTheDocument();
- });
-
- it('hides the description when there are available data model ids', () => {
+ it('should display a combobox without value and an empty combobox element informing that data models are missing when clicking "add data model" when there are no data models', async () => {
+ const user = userEvent.setup();
renderSelectCustomReceiptDataModelId({
- availableDataModelIds: mockAvailableDataModelIds,
+ allDataModelIds: [],
});
- const description = screen.queryByText(
- textMock('process_editor.configuration_panel_custom_receipt_select_data_model_description'),
+ const combobox = screen.getByRole('combobox', {
+ name: textMock('process_editor.configuration_panel_custom_receipt_select_data_model_label'),
+ });
+
+ await user.click(combobox);
+ expect(combobox).not.toHaveValue();
+
+ const noAvailableModelsOption = screen.getByText(
+ textMock('process_editor.configuration_panel_no_data_model_to_select'),
);
- expect(description).not.toBeInTheDocument();
+ expect(noAvailableModelsOption).toBeInTheDocument();
});
});
diff --git a/frontend/packages/process-editor/src/components/ConfigPanel/ConfigEndEvent/CustomReceiptContent/CreateCustomReceiptForm/SelectCustomReceiptDataModelId/SelectCustomReceiptDataModelId.tsx b/frontend/packages/process-editor/src/components/ConfigPanel/ConfigEndEvent/CustomReceiptContent/CreateCustomReceiptForm/SelectCustomReceiptDataModelId/SelectCustomReceiptDataModelId.tsx
index 376d8b56604..ddb31bc950e 100644
--- a/frontend/packages/process-editor/src/components/ConfigPanel/ConfigEndEvent/CustomReceiptContent/CreateCustomReceiptForm/SelectCustomReceiptDataModelId/SelectCustomReceiptDataModelId.tsx
+++ b/frontend/packages/process-editor/src/components/ConfigPanel/ConfigEndEvent/CustomReceiptContent/CreateCustomReceiptForm/SelectCustomReceiptDataModelId/SelectCustomReceiptDataModelId.tsx
@@ -1,11 +1,8 @@
-import React from 'react';
-import { StudioNativeSelect } from '@studio/components';
+import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useBpmnApiContext } from '../../../../../../contexts/BpmnApiContext';
-import {
- getDataTypeFromLayoutSetsWithExistingId,
- getDataModelOptions,
-} from '../../../../../../utils/configPanelUtils';
+import { getDataTypeFromLayoutSetsWithExistingId } from '../../../../../../utils/configPanelUtils';
+import { Combobox } from '@digdir/design-system-react';
export type SelectCustomReceiptDataModelIdProps = {
error: string;
@@ -17,41 +14,39 @@ export const SelectCustomReceiptDataModelId = ({
onChange,
}: SelectCustomReceiptDataModelIdProps): React.ReactElement => {
const { t } = useTranslation();
- const { layoutSets, existingCustomReceiptLayoutSetId, availableDataModelIds } =
- useBpmnApiContext();
+ const { layoutSets, existingCustomReceiptLayoutSetId, allDataModelIds } = useBpmnApiContext();
const existingDataModelId: string = getDataTypeFromLayoutSetsWithExistingId(
layoutSets,
existingCustomReceiptLayoutSetId,
);
- const dataModelOptions = getDataModelOptions(availableDataModelIds, existingDataModelId);
-
- const availableDataModelIdsEmpty: boolean = dataModelOptions.length === 0;
+ const [value, setValue] = useState(existingDataModelId ? [existingDataModelId] : []);
return (
-
-
- {dataModelOptions.map((id: string) => (
-
+
+ {t('process_editor.configuration_panel_no_data_model_to_select')}
+
+ {allDataModelIds.map((option) => (
+ {
+ setValue([option]);
+ onChange();
+ }}
+ >
+ {option}
+
))}
-
+
);
};
diff --git a/frontend/packages/process-editor/src/components/ConfigPanel/ConfigEndEvent/CustomReceiptContent/CustomReceipt/CustomReceipt.module.css b/frontend/packages/process-editor/src/components/ConfigPanel/ConfigEndEvent/CustomReceiptContent/CustomReceipt/CustomReceipt.module.css
index 5c2a48c6771..bd3509dd32e 100644
--- a/frontend/packages/process-editor/src/components/ConfigPanel/ConfigEndEvent/CustomReceiptContent/CustomReceipt/CustomReceipt.module.css
+++ b/frontend/packages/process-editor/src/components/ConfigPanel/ConfigEndEvent/CustomReceiptContent/CustomReceipt/CustomReceipt.module.css
@@ -31,3 +31,8 @@
gap: var(--fds-spacing-2);
padding-inline: var(--custom-receipt-spacing);
}
+
+.textfield {
+ padding-inline: var(--custom-receipt-spacing);
+ padding-block: var(--fds-spacing-1);
+}
diff --git a/frontend/packages/process-editor/src/components/ConfigPanel/ConfigEndEvent/CustomReceiptContent/CustomReceipt/CustomReceipt.test.tsx b/frontend/packages/process-editor/src/components/ConfigPanel/ConfigEndEvent/CustomReceiptContent/CustomReceipt/CustomReceipt.test.tsx
index c8028f21814..c916dc25893 100644
--- a/frontend/packages/process-editor/src/components/ConfigPanel/ConfigEndEvent/CustomReceiptContent/CustomReceipt/CustomReceipt.test.tsx
+++ b/frontend/packages/process-editor/src/components/ConfigPanel/ConfigEndEvent/CustomReceiptContent/CustomReceipt/CustomReceipt.test.tsx
@@ -33,12 +33,15 @@ const layoutSetIdTextKeys: Record = {
[existingLayoutSetName]: 'process_editor.configuration_panel_layout_set_id_not_unique',
};
-const mockAvailableDataModelIds: string[] = [mockBpmnApiContextValue.layoutSets.sets[1].dataType];
+const mockAllDataModelIds: string[] = [
+ mockBpmnApiContextValue.layoutSets.sets[0].dataType,
+ mockBpmnApiContextValue.layoutSets.sets[1].dataType,
+];
const defaultBpmnContextProps: BpmnApiContextProps = {
...mockBpmnApiContextValue,
existingCustomReceiptLayoutSetId: existingCustomReceiptLayoutSetId,
- availableDataModelIds: mockAvailableDataModelIds,
+ allDataModelIds: mockAllDataModelIds,
};
describe('CustomReceipt', () => {
@@ -82,7 +85,7 @@ describe('CustomReceipt', () => {
name: textMock('process_editor.configuration_panel_set_data_model'),
});
await user.click(combobox);
- const newOption: string = mockAvailableDataModelIds[0];
+ const newOption: string = mockAllDataModelIds[1];
const option = screen.getByRole('option', { name: newOption });
await user.click(option);
diff --git a/frontend/packages/process-editor/src/components/ConfigPanel/ConfigEndEvent/CustomReceiptContent/CustomReceipt/CustomReceipt.tsx b/frontend/packages/process-editor/src/components/ConfigPanel/ConfigEndEvent/CustomReceiptContent/CustomReceipt/CustomReceipt.tsx
index 68265b9c443..5de39ebee93 100644
--- a/frontend/packages/process-editor/src/components/ConfigPanel/ConfigEndEvent/CustomReceiptContent/CustomReceipt/CustomReceipt.tsx
+++ b/frontend/packages/process-editor/src/components/ConfigPanel/ConfigEndEvent/CustomReceiptContent/CustomReceipt/CustomReceipt.tsx
@@ -4,10 +4,7 @@ import { StudioDeleteButton, StudioToggleableTextfield } from '@studio/component
import { KeyVerticalIcon } from '@studio/icons';
import { Paragraph } from '@digdir/design-system-react';
import { useBpmnApiContext } from '../../../../../contexts/BpmnApiContext';
-import {
- getDataTypeFromLayoutSetsWithExistingId,
- getDataModelOptions,
-} from '../../../../../utils/configPanelUtils';
+import { getDataTypeFromLayoutSetsWithExistingId } from '../../../../../utils/configPanelUtils';
import { RedirectToCreatePageButton } from '../RedirectToCreatePageButton';
import { useTranslation } from 'react-i18next';
import { EditDataType } from '../../../EditDataType';
@@ -18,7 +15,7 @@ export const CustomReceipt = (): React.ReactElement => {
const { t } = useTranslation();
const {
layoutSets,
- availableDataModelIds,
+ allDataModelIds,
existingCustomReceiptLayoutSetId,
deleteLayoutSet,
mutateLayoutSetId,
@@ -44,8 +41,6 @@ export const CustomReceipt = (): React.ReactElement => {
});
};
- const dataModelOptions = getDataModelOptions(availableDataModelIds, existingDataModelId);
-
const handleValidation = (newLayoutSetId: string): string => {
const validationResult = getLayoutSetIdValidationErrorKey(
layoutSets,
@@ -61,6 +56,7 @@ export const CustomReceipt = (): React.ReactElement => {
,
label: t('process_editor.configuration_panel_custom_receipt_textfield_label'),
value: existingCustomReceiptLayoutSetId,
@@ -83,7 +79,7 @@ export const CustomReceipt = (): React.ReactElement => {
/>
diff --git a/frontend/packages/process-editor/src/components/ConfigPanel/EditDataType/SelectDataType/SelectDataType.tsx b/frontend/packages/process-editor/src/components/ConfigPanel/EditDataType/SelectDataType/SelectDataType.tsx
index 6b048359385..0ef46d6332c 100644
--- a/frontend/packages/process-editor/src/components/ConfigPanel/EditDataType/SelectDataType/SelectDataType.tsx
+++ b/frontend/packages/process-editor/src/components/ConfigPanel/EditDataType/SelectDataType/SelectDataType.tsx
@@ -23,7 +23,9 @@ export const SelectDataType = ({
}: SelectDataTypeProps): React.ReactElement => {
const { t } = useTranslation();
const { mutateDataType } = useBpmnApiContext();
+
const currentValue = existingDataType ? [existingDataType] : [];
+
const handleChangeDataModel = (newDataModelIds?: string[]) => {
const newDataModelId = newDataModelIds ? newDataModelIds[0] : undefined;
if (newDataModelId !== existingDataType) {
diff --git a/frontend/packages/process-editor/src/contexts/BpmnApiContext.tsx b/frontend/packages/process-editor/src/contexts/BpmnApiContext.tsx
index ffb27aa892d..d2a43f37d33 100644
--- a/frontend/packages/process-editor/src/contexts/BpmnApiContext.tsx
+++ b/frontend/packages/process-editor/src/contexts/BpmnApiContext.tsx
@@ -10,6 +10,7 @@ type QueryOptions = {
export type BpmnApiContextProps = {
availableDataModelIds: string[];
+ allDataModelIds: string[];
layoutSets: LayoutSets;
pendingApiOperations: boolean;
existingCustomReceiptLayoutSetId: string | undefined;
@@ -22,7 +23,6 @@ export type BpmnApiContextProps = {
mutateDataType: (dataTypeChange: DataTypeChange, options?: QueryOptions) => void;
addDataTypeToAppMetadata: (data: { dataTypeId: string; taskId: string }) => void;
deleteDataTypeFromAppMetadata: (data: { dataTypeId: string }) => void;
-
saveBpmn: (bpmnXml: string, metaData?: MetaDataForm) => void;
openPolicyEditor: () => void;
onProcessTaskAdd: (taskMetadata: OnProcessTaskEvent) => void;
@@ -37,43 +37,9 @@ export type BpmnApiContextProviderProps = {
export const BpmnApiContextProvider = ({
children,
- availableDataModelIds,
- layoutSets,
- pendingApiOperations,
- existingCustomReceiptLayoutSetId,
- addLayoutSet,
- deleteLayoutSet,
- mutateLayoutSetId,
- mutateDataType,
- addDataTypeToAppMetadata,
- deleteDataTypeFromAppMetadata,
- saveBpmn,
- openPolicyEditor,
- onProcessTaskRemove,
- onProcessTaskAdd,
+ ...rest
}: Partial) => {
- return (
-
- {children}
-
- );
+ return {children};
};
export const useBpmnApiContext = (): Partial => {
diff --git a/frontend/packages/process-editor/src/utils/configPanelUtils/configPanelUtils.test.ts b/frontend/packages/process-editor/src/utils/configPanelUtils/configPanelUtils.test.ts
index 05206cd9d2a..4d1c750e635 100644
--- a/frontend/packages/process-editor/src/utils/configPanelUtils/configPanelUtils.test.ts
+++ b/frontend/packages/process-editor/src/utils/configPanelUtils/configPanelUtils.test.ts
@@ -2,7 +2,6 @@ import {
getConfigTitleHelpTextKey,
getConfigTitleKey,
getDataTypeFromLayoutSetsWithExistingId,
- getDataModelOptions,
} from './configPanelUtils';
describe('configPanelUtils', () => {
@@ -77,24 +76,4 @@ describe('configPanelUtils', () => {
expect(existingDataModelId).toBeUndefined();
});
});
-
- describe('getDataModelOptions', () => {
- it('should return availableIds with existingId appended when existingId is provided', () => {
- const availableIds = ['id1', 'id2', 'id3'];
- const existingId = 'existingId';
-
- const result = getDataModelOptions(availableIds, existingId);
-
- expect(result).toEqual([...availableIds, existingId]);
- });
-
- it('should return availableIds unchanged when existingId is not provided', () => {
- const availableIds = ['id1', 'id2', 'id3'];
- const existingId = '';
-
- const result = getDataModelOptions(availableIds, existingId);
-
- expect(result).toEqual(availableIds);
- });
- });
});
diff --git a/frontend/packages/process-editor/src/utils/configPanelUtils/configPanelUtils.ts b/frontend/packages/process-editor/src/utils/configPanelUtils/configPanelUtils.ts
index 15000793fbd..6f02b26c209 100644
--- a/frontend/packages/process-editor/src/utils/configPanelUtils/configPanelUtils.ts
+++ b/frontend/packages/process-editor/src/utils/configPanelUtils/configPanelUtils.ts
@@ -60,13 +60,3 @@ export const getDataTypeFromLayoutSetsWithExistingId = (
): string | undefined => {
return layoutSets.sets.find((layoutSet) => layoutSet.id === existingId)?.dataType;
};
-
-/**
- * Gets the data model id options based on the available ids and the existing id
- * @param availableIds the list of available ids
- * @param existingId the existing id
- * @returns a list data model options
- */
-export const getDataModelOptions = (availableIds: string[], existingId: string): string[] => {
- return existingId ? [...availableIds, existingId] : availableIds;
-};
diff --git a/frontend/packages/process-editor/src/utils/configPanelUtils/index.ts b/frontend/packages/process-editor/src/utils/configPanelUtils/index.ts
index 142c7bdfb36..05e6d0a6bb4 100644
--- a/frontend/packages/process-editor/src/utils/configPanelUtils/index.ts
+++ b/frontend/packages/process-editor/src/utils/configPanelUtils/index.ts
@@ -3,5 +3,4 @@ export {
getConfigTitleHelpTextKey,
checkForInvalidCharacters,
getDataTypeFromLayoutSetsWithExistingId,
- getDataModelOptions,
} from './configPanelUtils';
diff --git a/frontend/packages/process-editor/test/mocks/bpmnContextMock.ts b/frontend/packages/process-editor/test/mocks/bpmnContextMock.ts
index 6e8f8ba42ed..6390bfa5556 100644
--- a/frontend/packages/process-editor/test/mocks/bpmnContextMock.ts
+++ b/frontend/packages/process-editor/test/mocks/bpmnContextMock.ts
@@ -37,6 +37,7 @@ export const mockBpmnApiContextValue: BpmnApiContextProps = {
pendingApiOperations: false,
existingCustomReceiptLayoutSetId: undefined,
availableDataModelIds: [],
+ allDataModelIds: [],
addLayoutSet: jest.fn(),
deleteLayoutSet: jest.fn(),
mutateLayoutSetId: jest.fn(),
diff --git a/frontend/packages/shared/src/hooks/queries/useAppMetadataModelIdsQuery.ts b/frontend/packages/shared/src/hooks/queries/useAppMetadataModelIdsQuery.ts
index a7c511af90d..4662924198a 100644
--- a/frontend/packages/shared/src/hooks/queries/useAppMetadataModelIdsQuery.ts
+++ b/frontend/packages/shared/src/hooks/queries/useAppMetadataModelIdsQuery.ts
@@ -3,10 +3,14 @@ import type { UseQueryResult } from '@tanstack/react-query';
import { useServicesContext } from 'app-shared/contexts/ServicesContext';
import { QueryKey } from 'app-shared/types/QueryKey';
-export const useAppMetadataModelIdsQuery = (org: string, app: string): UseQueryResult => {
+export const useAppMetadataModelIdsQuery = (
+ org: string,
+ app: string,
+ onlyUnReferenced: boolean = true,
+): UseQueryResult => {
const { getAppMetadataModelIds } = useServicesContext();
return useQuery({
queryKey: [QueryKey.AppMetadataModelIds, org, app],
- queryFn: () => getAppMetadataModelIds(org, app, true),
+ queryFn: () => getAppMetadataModelIds(org, app, onlyUnReferenced),
});
};
From 1d7f1059901ae7711c5f43239e4cf82d81ae9dd8 Mon Sep 17 00:00:00 2001
From: WilliamThorenfeldt
<133344438+WilliamThorenfeldt@users.noreply.github.com>
Date: Mon, 3 Jun 2024 20:26:02 +0200
Subject: [PATCH 16/27] Replace m UI popover with studio popover in
mergeconflict warning (#12907)
* adding new component for showing the popover
* Implementing StudioPopover
* Adding tests
* delete unused file
* adding missing test
---
.../simpleMerge/DownloadRepoModal.tsx | 57 --------
.../DownloadRepoPopoverContent.module.css | 12 ++
.../DownloadRepoPopoverContent.test.tsx | 58 ++++++++
.../DownloadRepoPopoverContent.tsx | 40 ++++++
.../DownloadRepoPopoverContent/index.ts | 1 +
.../MergeConflictWarning.module.css | 1 +
.../simpleMerge/MergeConflictWarning.test.tsx | 2 +-
.../simpleMerge/MergeConflictWarning.tsx | 83 ++++++------
.../RemoveChangesPopoverContent.module.css | 9 ++
.../RemoveChangesPopoverContent.test.tsx | 110 +++++++++++++++
.../RemoveChangesPopoverContent.tsx | 94 +++++++++++++
.../RemoveChangesPopoverContent/index.ts | 1 +
.../features/simpleMerge/RepoModal.module.css | 17 ---
.../simpleMerge/ResetRepoModal.test.tsx | 118 -----------------
.../features/simpleMerge/ResetRepoModal.tsx | 125 ------------------
.../app-development/layout/PageLayout.tsx | 2 +-
16 files changed, 365 insertions(+), 365 deletions(-)
delete mode 100644 frontend/app-development/features/simpleMerge/DownloadRepoModal.tsx
create mode 100644 frontend/app-development/features/simpleMerge/DownloadRepoPopoverContent/DownloadRepoPopoverContent.module.css
create mode 100644 frontend/app-development/features/simpleMerge/DownloadRepoPopoverContent/DownloadRepoPopoverContent.test.tsx
create mode 100644 frontend/app-development/features/simpleMerge/DownloadRepoPopoverContent/DownloadRepoPopoverContent.tsx
create mode 100644 frontend/app-development/features/simpleMerge/DownloadRepoPopoverContent/index.ts
create mode 100644 frontend/app-development/features/simpleMerge/RemoveChangesPopoverContent/RemoveChangesPopoverContent.module.css
create mode 100644 frontend/app-development/features/simpleMerge/RemoveChangesPopoverContent/RemoveChangesPopoverContent.test.tsx
create mode 100644 frontend/app-development/features/simpleMerge/RemoveChangesPopoverContent/RemoveChangesPopoverContent.tsx
create mode 100644 frontend/app-development/features/simpleMerge/RemoveChangesPopoverContent/index.ts
delete mode 100644 frontend/app-development/features/simpleMerge/RepoModal.module.css
delete mode 100644 frontend/app-development/features/simpleMerge/ResetRepoModal.test.tsx
delete mode 100644 frontend/app-development/features/simpleMerge/ResetRepoModal.tsx
diff --git a/frontend/app-development/features/simpleMerge/DownloadRepoModal.tsx b/frontend/app-development/features/simpleMerge/DownloadRepoModal.tsx
deleted file mode 100644
index e42e3947890..00000000000
--- a/frontend/app-development/features/simpleMerge/DownloadRepoModal.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import React from 'react';
-import { Popover } from '@mui/material';
-import { StudioButton } from '@studio/components';
-import classes from './RepoModal.module.css';
-import { repoDownloadPath } from 'app-shared/api/paths';
-import { Trans, useTranslation } from 'react-i18next';
-
-interface IDownloadRepoModalProps {
- anchorRef: React.MutableRefObject;
- onClose: any;
- open: boolean;
- org: string;
- app: string;
-}
-
-export function DownloadRepoModal(props: IDownloadRepoModalProps) {
- const { t } = useTranslation();
- return (
-
- );
-}
diff --git a/frontend/app-development/features/simpleMerge/DownloadRepoPopoverContent/DownloadRepoPopoverContent.module.css b/frontend/app-development/features/simpleMerge/DownloadRepoPopoverContent/DownloadRepoPopoverContent.module.css
new file mode 100644
index 00000000000..8c42c53322d
--- /dev/null
+++ b/frontend/app-development/features/simpleMerge/DownloadRepoPopoverContent/DownloadRepoPopoverContent.module.css
@@ -0,0 +1,12 @@
+.wrapper {
+ padding: var(--fds-spacing-3);
+}
+
+.buttonContainer {
+ display: flex;
+ justify-content: flex-end;
+}
+
+.link {
+ padding-bottom: var(--fds-spacing-3);
+}
diff --git a/frontend/app-development/features/simpleMerge/DownloadRepoPopoverContent/DownloadRepoPopoverContent.test.tsx b/frontend/app-development/features/simpleMerge/DownloadRepoPopoverContent/DownloadRepoPopoverContent.test.tsx
new file mode 100644
index 00000000000..77373a0ec32
--- /dev/null
+++ b/frontend/app-development/features/simpleMerge/DownloadRepoPopoverContent/DownloadRepoPopoverContent.test.tsx
@@ -0,0 +1,58 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import {
+ DownloadRepoPopoverContent,
+ type DownloadRepoPopoverContentProps,
+} from './DownloadRepoPopoverContent';
+import { MemoryRouter } from 'react-router-dom';
+import { app, org } from '@studio/testing/testids';
+import { textMock } from '@studio/testing/mocks/i18nMock';
+import { repoDownloadPath } from 'app-shared/api/paths';
+
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useParams: () => {
+ return { org, app };
+ },
+}));
+
+const mockOnClose = jest.fn();
+
+const defaultProps: DownloadRepoPopoverContentProps = {
+ onClose: mockOnClose,
+};
+
+describe('DownloadRepoPopoverContent', () => {
+ afterEach(jest.clearAllMocks);
+
+ it('renders links with correct hrefs', () => {
+ renderDownloadRepoPopoverContent();
+
+ expect(
+ screen.getByRole('link', { name: textMock('overview.download_repo_changes') }),
+ ).toHaveAttribute('href', repoDownloadPath(org, app));
+
+ expect(
+ screen.getByRole('link', { name: textMock('overview.download_repo_full') }),
+ ).toHaveAttribute('href', repoDownloadPath(org, app, true));
+ });
+
+ it('calls onClose function when cancel button is clicked', async () => {
+ const user = userEvent.setup();
+ renderDownloadRepoPopoverContent();
+
+ const cancelButton = screen.getByRole('button', { name: textMock('general.cancel') });
+ await user.click(cancelButton);
+
+ expect(mockOnClose).toHaveBeenCalled();
+ });
+});
+
+const renderDownloadRepoPopoverContent = () => {
+ return render(
+
+
+ ,
+ );
+};
diff --git a/frontend/app-development/features/simpleMerge/DownloadRepoPopoverContent/DownloadRepoPopoverContent.tsx b/frontend/app-development/features/simpleMerge/DownloadRepoPopoverContent/DownloadRepoPopoverContent.tsx
new file mode 100644
index 00000000000..6ae3c3ea185
--- /dev/null
+++ b/frontend/app-development/features/simpleMerge/DownloadRepoPopoverContent/DownloadRepoPopoverContent.tsx
@@ -0,0 +1,40 @@
+import React from 'react';
+import classes from './DownloadRepoPopoverContent.module.css';
+import { Trans, useTranslation } from 'react-i18next';
+import { StudioButton } from '@studio/components';
+import { repoDownloadPath } from 'app-shared/api/paths';
+import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams';
+import { Heading, Link, Paragraph } from '@digdir/design-system-react';
+
+export type DownloadRepoPopoverContentProps = {
+ onClose: () => void;
+};
+
+export const DownloadRepoPopoverContent = ({
+ onClose,
+}: DownloadRepoPopoverContentProps): React.ReactElement => {
+ const { t } = useTranslation();
+ const { org, app } = useStudioEnvironmentParams();
+
+ return (
+
+
+ {t('overview.download_repo_heading')}
+
+
+
+
+
+ {t('overview.download_repo_changes')}
+
+
+ {t('overview.download_repo_full')}
+
+
+
+ {t('general.cancel')}
+
+
+
+ );
+};
diff --git a/frontend/app-development/features/simpleMerge/DownloadRepoPopoverContent/index.ts b/frontend/app-development/features/simpleMerge/DownloadRepoPopoverContent/index.ts
new file mode 100644
index 00000000000..d4a4e20586b
--- /dev/null
+++ b/frontend/app-development/features/simpleMerge/DownloadRepoPopoverContent/index.ts
@@ -0,0 +1 @@
+export { DownloadRepoPopoverContent } from './DownloadRepoPopoverContent';
diff --git a/frontend/app-development/features/simpleMerge/MergeConflictWarning.module.css b/frontend/app-development/features/simpleMerge/MergeConflictWarning.module.css
index 480b89cbf57..3462086a2bf 100644
--- a/frontend/app-development/features/simpleMerge/MergeConflictWarning.module.css
+++ b/frontend/app-development/features/simpleMerge/MergeConflictWarning.module.css
@@ -6,6 +6,7 @@
border: 1px solid #c9c9c9;
border-radius: 3px;
}
+
.buttonContainer {
justify-content: flex-end;
}
diff --git a/frontend/app-development/features/simpleMerge/MergeConflictWarning.test.tsx b/frontend/app-development/features/simpleMerge/MergeConflictWarning.test.tsx
index 35d3c33cba9..c7c7872fe43 100644
--- a/frontend/app-development/features/simpleMerge/MergeConflictWarning.test.tsx
+++ b/frontend/app-development/features/simpleMerge/MergeConflictWarning.test.tsx
@@ -6,7 +6,7 @@ import { MergeConflictWarning } from './MergeConflictWarning';
describe('MergeConflictWarning', () => {
it('should render merge conflict warning container', () => {
- renderWithProviders(, {
+ renderWithProviders(, {
startUrl: `${APP_DEVELOPMENT_BASENAME}/test-org/test-app`,
});
const container = screen.getByRole('dialog');
diff --git a/frontend/app-development/features/simpleMerge/MergeConflictWarning.tsx b/frontend/app-development/features/simpleMerge/MergeConflictWarning.tsx
index bd3249027da..90821ff6ae4 100644
--- a/frontend/app-development/features/simpleMerge/MergeConflictWarning.tsx
+++ b/frontend/app-development/features/simpleMerge/MergeConflictWarning.tsx
@@ -1,58 +1,49 @@
-import React, { useRef, useState } from 'react';
+import React, { useState } from 'react';
import classes from './MergeConflictWarning.module.css';
-import { Download } from '@navikt/ds-icons';
import { ButtonContainer } from 'app-shared/primitives';
-import { DownloadRepoModal } from './DownloadRepoModal';
-import { ResetRepoModal } from './ResetRepoModal';
import { useTranslation } from 'react-i18next';
-import { StudioButton } from '@studio/components';
+import { StudioPopover } from '@studio/components';
+import { RemoveChangesPopoverContent } from './RemoveChangesPopoverContent';
+import { Heading, Paragraph } from '@digdir/design-system-react';
+import { DownloadRepoPopoverContent } from './DownloadRepoPopoverContent';
-interface MergeConflictWarningProps {
- org: string;
- app: string;
-}
+export const MergeConflictWarning = () => {
+ const { t } = useTranslation();
-export const MergeConflictWarning = ({ org, app }: MergeConflictWarningProps) => {
- const [resetRepoModalOpen, setResetRepoModalOpen] = useState(false);
+ const [resetRepoPopoverOpen, setResetRepoPopoverOpen] = useState(false);
const [downloadModalOpen, setDownloadModalOpen] = useState(false);
- const { t } = useTranslation();
- const toggleDownloadModal = () => setDownloadModalOpen(!downloadModalOpen);
- const toggleResetModal = () => setResetRepoModalOpen(!resetRepoModalOpen);
- const downloadModalAnchor = useRef();
- const resetRepoModalAnchor = useRef();
+
+ const toggleDownloadModal = () => setDownloadModalOpen((currentValue: boolean) => !currentValue);
+ const toggleResetModal = () => setResetRepoPopoverOpen((currentValue: boolean) => !currentValue);
+
return (
-
{t('merge_conflict.headline')}
-
{t('merge_conflict.body1')}
-
{t('merge_conflict.body2')}
-
}
- iconPlacement='right'
- onClick={toggleDownloadModal}
- ref={downloadModalAnchor}
- size='small'
- >
- {t('merge_conflict.download_zip_file')}
-
-
+
+ {t('merge_conflict.headline')}
+
+
+ {t('merge_conflict.body1')}
+
+
+ {t('merge_conflict.body2')}
+
+
+
+ {t('merge_conflict.download_zip_file')}
+
+
+
+
+
-
- {t('merge_conflict.remove_my_changes')}
-
-
+
+
+ {t('merge_conflict.remove_my_changes')}
+
+
+
+
+
);
diff --git a/frontend/app-development/features/simpleMerge/RemoveChangesPopoverContent/RemoveChangesPopoverContent.module.css b/frontend/app-development/features/simpleMerge/RemoveChangesPopoverContent/RemoveChangesPopoverContent.module.css
new file mode 100644
index 00000000000..287acf81edb
--- /dev/null
+++ b/frontend/app-development/features/simpleMerge/RemoveChangesPopoverContent/RemoveChangesPopoverContent.module.css
@@ -0,0 +1,9 @@
+.wrapper {
+ padding: var(--fds-spacing-3);
+}
+
+.buttonContainer {
+ display: flex;
+ gap: var(--fds-spacing-3);
+ margin-top: var(--fds-spacing-5);
+}
diff --git a/frontend/app-development/features/simpleMerge/RemoveChangesPopoverContent/RemoveChangesPopoverContent.test.tsx b/frontend/app-development/features/simpleMerge/RemoveChangesPopoverContent/RemoveChangesPopoverContent.test.tsx
new file mode 100644
index 00000000000..dcd0a95ee3c
--- /dev/null
+++ b/frontend/app-development/features/simpleMerge/RemoveChangesPopoverContent/RemoveChangesPopoverContent.test.tsx
@@ -0,0 +1,110 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import {
+ RemoveChangesPopoverContent,
+ type RemoveChangesPopoverContentProps,
+} from './RemoveChangesPopoverContent';
+import { MemoryRouter } from 'react-router-dom';
+import { app, org } from '@studio/testing/testids';
+import { textMock } from '@studio/testing/mocks/i18nMock';
+import type { QueryClient } from '@tanstack/react-query';
+import {
+ ServicesContextProvider,
+ type ServicesContextProps,
+} from 'app-shared/contexts/ServicesContext';
+import { createQueryClientMock } from 'app-shared/mocks/queryClientMock';
+
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useParams: () => {
+ return { org, app };
+ },
+}));
+const resetRepoChanges = jest.fn().mockImplementation(() => Promise.resolve({}));
+
+const mockOnClose = jest.fn();
+
+const defaultProps: RemoveChangesPopoverContentProps = {
+ onClose: mockOnClose,
+};
+
+describe('DownloadRepoPopoverContent', () => {
+ afterEach(jest.clearAllMocks);
+
+ it('enables the confirm button when the correct app name is typed', async () => {
+ const user = userEvent.setup();
+ renderRemoveChangesPopoverContent();
+
+ const input = screen.getByLabelText(textMock('overview.reset_repo_confirm_repo_name'));
+ const confirmButton = screen.getByRole('button', {
+ name: textMock('overview.reset_repo_button'),
+ });
+
+ expect(confirmButton).toBeDisabled();
+
+ await user.type(input, app);
+
+ expect(confirmButton).toBeEnabled();
+ });
+
+ it('calls onResetWrapper and displays success toast when the confirm button is clicked', async () => {
+ const user = userEvent.setup();
+ renderRemoveChangesPopoverContent();
+
+ const input = screen.getByLabelText(textMock('overview.reset_repo_confirm_repo_name'));
+ const confirmButton = screen.getByRole('button', {
+ name: textMock('overview.reset_repo_button'),
+ });
+
+ await user.type(input, app);
+ await user.click(confirmButton);
+
+ expect(resetRepoChanges).toHaveBeenCalledTimes(1);
+
+ const toastSuccessText = await screen.findByText(textMock('overview.reset_repo_completed'));
+ expect(toastSuccessText).toBeInTheDocument();
+
+ expect(mockOnClose).toHaveBeenCalledTimes(1);
+ });
+
+ it('calls onClose function when cancel button is clicked', async () => {
+ const user = userEvent.setup();
+ renderRemoveChangesPopoverContent();
+
+ const cancelButton = screen.getByRole('button', { name: textMock('general.cancel') });
+ await user.click(cancelButton);
+
+ expect(mockOnClose).toHaveBeenCalledTimes(1);
+ });
+
+ it('calls onClose function when Enter is pressed', async () => {
+ const user = userEvent.setup();
+ renderRemoveChangesPopoverContent();
+
+ const input = screen.getByLabelText(textMock('overview.reset_repo_confirm_repo_name'));
+ await user.type(input, app);
+ await user.keyboard('{Enter}');
+
+ expect(resetRepoChanges).toHaveBeenCalledTimes(1);
+ expect(mockOnClose).toHaveBeenCalledTimes(1);
+ });
+});
+
+const renderRemoveChangesPopoverContent = (
+ queries: Partial = {},
+ queryClient: QueryClient = createQueryClientMock(),
+) => {
+ const allQueries: ServicesContextProps = {
+ resetRepoChanges,
+ ...queries,
+ };
+
+ return render(
+
+
+
+
+ ,
+ );
+};
diff --git a/frontend/app-development/features/simpleMerge/RemoveChangesPopoverContent/RemoveChangesPopoverContent.tsx b/frontend/app-development/features/simpleMerge/RemoveChangesPopoverContent/RemoveChangesPopoverContent.tsx
new file mode 100644
index 00000000000..0e05b072dc2
--- /dev/null
+++ b/frontend/app-development/features/simpleMerge/RemoveChangesPopoverContent/RemoveChangesPopoverContent.tsx
@@ -0,0 +1,94 @@
+import React, { useState } from 'react';
+import classes from './RemoveChangesPopoverContent.module.css';
+import { Heading, Paragraph } from '@digdir/design-system-react';
+import { StudioTextfield, StudioButton, StudioSpinner } from '@studio/components';
+import { useTranslation, Trans } from 'react-i18next';
+import { useQueryClient } from '@tanstack/react-query';
+import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams';
+import { useResetRepositoryMutation } from '../../../hooks/mutations/useResetRepositoryMutation';
+import { toast } from 'react-toastify';
+
+export type RemoveChangesPopoverContentProps = {
+ onClose: () => void;
+};
+
+export const RemoveChangesPopoverContent = ({
+ onClose,
+}: RemoveChangesPopoverContentProps): React.ReactElement => {
+ const { t } = useTranslation();
+ const { org, app } = useStudioEnvironmentParams();
+
+ const queryClient = useQueryClient();
+ const repoResetMutation = useResetRepositoryMutation(org, app);
+
+ const [canDelete, setCanDelete] = useState(false);
+
+ const handleOnKeypressEnter = (event: any) => {
+ if (event.key === 'Enter' && canDelete) {
+ onResetWrapper();
+ }
+ };
+
+ const onResetWrapper = () => {
+ setCanDelete(false);
+ repoResetMutation.mutate(undefined, {
+ onSuccess: () => {
+ toast.success(t('overview.reset_repo_completed'));
+ queryClient.removeQueries();
+ onCloseWrapper();
+ },
+ });
+ };
+
+ const onCloseWrapper = () => {
+ repoResetMutation.reset();
+ onClose();
+ };
+
+ const handleChange = (event: React.ChangeEvent) => {
+ const name: string = event.target.value;
+ setCanDelete(name === app);
+ };
+
+ return (
+
+
+ {t('overview.reset_repo_confirm_heading')}
+
+
+ }}
+ />
+
+
+ {repoResetMutation.isPending && (
+
+ )}
+ {!repoResetMutation.isPending && (
+
+
+ {t('overview.reset_repo_button')}
+
+
+ {t('general.cancel')}
+
+
+ )}
+
+ );
+};
diff --git a/frontend/app-development/features/simpleMerge/RemoveChangesPopoverContent/index.ts b/frontend/app-development/features/simpleMerge/RemoveChangesPopoverContent/index.ts
new file mode 100644
index 00000000000..57715fb9c97
--- /dev/null
+++ b/frontend/app-development/features/simpleMerge/RemoveChangesPopoverContent/index.ts
@@ -0,0 +1 @@
+export { RemoveChangesPopoverContent } from './RemoveChangesPopoverContent';
diff --git a/frontend/app-development/features/simpleMerge/RepoModal.module.css b/frontend/app-development/features/simpleMerge/RepoModal.module.css
deleted file mode 100644
index 7d7d5dd1821..00000000000
--- a/frontend/app-development/features/simpleMerge/RepoModal.module.css
+++ /dev/null
@@ -1,17 +0,0 @@
-.modalContainer {
- padding: 30px;
- width: 471px;
- display: flex;
- gap: 18px;
- flex-direction: column;
- box-sizing: border-box;
-}
-.modalContainer p,
-.modalContainer h2 {
- margin: 0;
- padding: 0;
-}
-.buttonContainer {
- display: flex;
- gap: 24px;
-}
diff --git a/frontend/app-development/features/simpleMerge/ResetRepoModal.test.tsx b/frontend/app-development/features/simpleMerge/ResetRepoModal.test.tsx
deleted file mode 100644
index add4dbb78e5..00000000000
--- a/frontend/app-development/features/simpleMerge/ResetRepoModal.test.tsx
+++ /dev/null
@@ -1,118 +0,0 @@
-import React from 'react';
-import type { IResetRepoModalProps } from './ResetRepoModal';
-import { ResetRepoModal } from './ResetRepoModal';
-import { screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import { renderWithMockStore } from 'app-development/test/mocks';
-
-import { mockUseTranslation } from '@studio/testing/mocks/i18nMock';
-import type { ServicesContextProps } from 'app-shared/contexts/ServicesContext';
-import { resetRepoContainerId } from '@studio/testing/testids';
-
-const user = userEvent.setup();
-
-const resetModalHeading = 'Reset repository';
-const resetModalConfirmInfo = 'Are you sure you want to reset the test repository?';
-const resetModalConfirmRepoName = 'Type repository name to confirm';
-const resetModalButton = 'Reset repository';
-const resetModalCancel = 'Cancel';
-
-const texts = {
- 'overview.reset_repo_confirm_heading': resetModalHeading,
- 'overview.reset_repo_confirm_info': resetModalConfirmInfo,
- 'overview.reset_repo_confirm_repo_name': resetModalConfirmRepoName,
- 'overview.reset_repo_button': resetModalButton,
- 'general.cancel': resetModalCancel,
-};
-
-jest.mock('react-i18next', () => ({
- useTranslation: () => ({
- ...mockUseTranslation(texts),
- i18n: {
- exists: (key: string) => texts[key] !== undefined,
- },
- }),
- Trans: ({ i18nKey }: { i18nKey: any }) => texts[i18nKey],
-}));
-
-describe('ResetRepoModal', () => {
- let mockAnchorEl: any;
- const mockRepoName = 'TestRepo';
- const mockFunc = jest.fn();
-
- beforeEach(() => {
- mockAnchorEl = {
- // eslint-disable-next-line testing-library/no-node-access
- current: document.querySelector('body'),
- };
- });
-
- const render = (
- props?: Partial,
- queries?: Partial,
- ) => {
- const defaultProps = {
- anchorRef: mockAnchorEl,
- handleClickResetRepo: mockFunc,
- onClose: mockFunc,
- open: true,
- repositoryName: mockRepoName,
- org: 'testOrg',
- };
- return renderWithMockStore({}, queries)();
- };
-
- it('renders the component', () => {
- render();
- const resetRepoContainer = screen.getByTestId(resetRepoContainerId);
- expect(resetRepoContainer).toBeDefined();
- });
-
- it('renders the reset my changes button as disabled when repo name is not entered', () => {
- render();
- const resetRepoButton = screen.getByRole('button', {
- name: resetModalButton,
- });
- expect(resetRepoButton).toBeDisabled();
- });
-
- it('renders the reset my changes button as disabled when incorrect repo name is entered', async () => {
- render();
- const repoNameInput = screen.getByLabelText(resetModalConfirmRepoName);
- await user.type(repoNameInput, 'notTheRepoName');
- const resetRepoButton = screen.getByRole('button', {
- name: resetModalButton,
- });
- expect(resetRepoButton).toBeDisabled();
- });
-
- it('enables the reset my changes button when repo name is entered', async () => {
- render();
- const repoNameInput = screen.getByLabelText(resetModalConfirmRepoName);
- await user.type(repoNameInput, mockRepoName);
- const resetRepoButton = screen.getByRole('button', {
- name: resetModalButton,
- });
- expect(resetRepoButton).toBeEnabled();
- });
-
- it('calls the reset mutation when reset button is clicked', async () => {
- const resetRepoChanges = jest.fn();
- const mockQueries: Partial = {
- resetRepoChanges,
- };
- render({}, mockQueries);
- const repoNameInput = screen.getByLabelText(resetModalConfirmRepoName);
- await user.type(repoNameInput, mockRepoName);
- await user.click(screen.getByRole('button', { name: resetModalButton }));
- expect(resetRepoChanges).toHaveBeenCalled();
- });
-
- it('renders the success message after reset is completed', async () => {
- render();
- const repoNameInput = screen.getByLabelText(resetModalConfirmRepoName);
- await user.type(repoNameInput, mockRepoName);
- await user.click(screen.getByRole('button', { name: resetModalButton }));
- expect(await screen.findByText('overview.reset_repo_completed')).toBeInTheDocument();
- });
-});
diff --git a/frontend/app-development/features/simpleMerge/ResetRepoModal.tsx b/frontend/app-development/features/simpleMerge/ResetRepoModal.tsx
deleted file mode 100644
index 1fa6f4420da..00000000000
--- a/frontend/app-development/features/simpleMerge/ResetRepoModal.tsx
+++ /dev/null
@@ -1,125 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import classes from './RepoModal.module.css';
-import { StudioButton, StudioSpinner } from '@studio/components';
-import { Textfield } from '@digdir/design-system-react';
-import { Popover } from '@mui/material';
-import { useTranslation, Trans } from 'react-i18next';
-import { useResetRepositoryMutation } from 'app-development/hooks/mutations/useResetRepositoryMutation';
-import { resetRepoContainerId } from '@studio/testing/testids';
-import { toast } from 'react-toastify';
-
-import { useQueryClient } from '@tanstack/react-query';
-
-export interface IResetRepoModalProps {
- anchorRef: React.MutableRefObject;
- onClose: any;
- open: boolean;
- repositoryName: string;
- org: string;
-}
-
-export function ResetRepoModal(props: IResetRepoModalProps) {
- const [canDelete, setCanDelete] = useState(false);
- const [deleteRepoName, setDeleteRepoName] = useState('');
-
- useEffect(() => {
- if (deleteRepoName === props.repositoryName) {
- setCanDelete(true);
- } else {
- setCanDelete(false);
- }
- }, [deleteRepoName, props.repositoryName]);
-
- const onDeleteRepoNameChange = (event: any) => setDeleteRepoName(event.target.value);
-
- const repoResetMutation = useResetRepositoryMutation(props.org, props.repositoryName);
- const queryClient = useQueryClient();
- const onResetWrapper = () => {
- setCanDelete(false);
- repoResetMutation.mutate(undefined, {
- onSuccess: () => {
- toast.success(t('overview.reset_repo_completed'));
- queryClient.removeQueries();
- onCloseWrapper();
- },
- });
- };
-
- const handleOnKeypressEnter = (event: any) => {
- if (event.key === 'Enter' && canDelete) {
- onResetWrapper();
- }
- };
-
- const onCloseWrapper = () => {
- setDeleteRepoName('');
- repoResetMutation.reset();
- props.onClose();
- };
- const { t } = useTranslation();
- return (
-
-
-
-
{t('overview.reset_repo_confirm_heading')}
-
- }}
- />
-
-
-
- {repoResetMutation.isPending && (
-
- )}
- {!repoResetMutation.isPending && (
-
-
- {t('overview.reset_repo_button')}
-
-
- {t('general.cancel')}
-
-
- )}
-
-
-
- );
-}
diff --git a/frontend/app-development/layout/PageLayout.tsx b/frontend/app-development/layout/PageLayout.tsx
index f68a6cc04c7..4160142c311 100644
--- a/frontend/app-development/layout/PageLayout.tsx
+++ b/frontend/app-development/layout/PageLayout.tsx
@@ -44,7 +44,7 @@ export const PageLayout = (): React.ReactNode => {
return ;
}
if (repoStatus?.hasMergeConflict) {
- return ;
+ return ;
}
return ;
};
From 552497250160435d3791b1fdd1a825034133c03a Mon Sep 17 00:00:00 2001
From: WilliamThorenfeldt
<133344438+WilliamThorenfeldt@users.noreply.github.com>
Date: Mon, 3 Jun 2024 21:08:01 +0200
Subject: [PATCH 17/27] Implementing a way to show newly changed app name in
preview of the app (#12847)
* Implementing a way to show newly changed app name in preview of the app
* adding missing props
* fixing comment
* fixing final tests
* Refactor preview context to its own file
* fixing broken test
* refactor code from feedback
* Adding test
---
.../contexts/AppDevelopmentContext.tsx | 3 +-
.../PreviewContext/PreviewContext.test.tsx | 63 ++++++++++++++++
.../PreviewContext/PreviewContext.tsx | 45 ++++++++++++
.../contexts/PreviewContext/index.ts | 6 ++
.../SettingsModalContext.test.tsx | 0
.../SettingsModalContext.tsx | 2 +-
.../contexts/SettingsModalContext/index.ts | 5 ++
.../SettingsModal/SettingsModal.test.tsx | 21 ++++--
.../Tabs/AboutTab/AboutTab.test.tsx | 72 ++++++++++++++-----
.../components/Tabs/AboutTab/AboutTab.tsx | 9 ++-
.../app-development/router/routes.test.tsx | 8 ++-
frontend/app-development/router/routes.tsx | 10 ++-
.../ux-editor/src/AppContext.test.tsx | 2 +-
.../packages/ux-editor/src/AppContext.tsx | 14 +++-
.../packages/ux-editor/src/SubApp.test.tsx | 2 +-
frontend/packages/ux-editor/src/SubApp.tsx | 9 ++-
.../src/components/Preview/Preview.tsx | 6 ++
.../src/hooks/useChecksum.ts/index.ts | 1 +
.../hooks/useChecksum.ts/useChecksum.test.ts | 31 ++++++++
.../src/hooks/useChecksum.ts/useChecksum.ts | 13 ++++
.../ux-editor/src/testing/appContextMock.ts | 2 +
21 files changed, 288 insertions(+), 36 deletions(-)
create mode 100644 frontend/app-development/contexts/PreviewContext/PreviewContext.test.tsx
create mode 100644 frontend/app-development/contexts/PreviewContext/PreviewContext.tsx
create mode 100644 frontend/app-development/contexts/PreviewContext/index.ts
rename frontend/app-development/contexts/{ => SettingsModalContext}/SettingsModalContext.test.tsx (100%)
rename frontend/app-development/contexts/{ => SettingsModalContext}/SettingsModalContext.tsx (95%)
create mode 100644 frontend/app-development/contexts/SettingsModalContext/index.ts
create mode 100644 frontend/packages/ux-editor/src/hooks/useChecksum.ts/index.ts
create mode 100644 frontend/packages/ux-editor/src/hooks/useChecksum.ts/useChecksum.test.ts
create mode 100644 frontend/packages/ux-editor/src/hooks/useChecksum.ts/useChecksum.ts
diff --git a/frontend/app-development/contexts/AppDevelopmentContext.tsx b/frontend/app-development/contexts/AppDevelopmentContext.tsx
index 337aaf70a30..1e5e6ecc72f 100644
--- a/frontend/app-development/contexts/AppDevelopmentContext.tsx
+++ b/frontend/app-development/contexts/AppDevelopmentContext.tsx
@@ -1,4 +1,5 @@
import { SettingsModalContextProvider } from './SettingsModalContext';
+import { PreviewContextProvider } from './PreviewContext';
import { combineComponents } from '../utils/context/combineComponents';
/**
@@ -6,7 +7,7 @@ import { combineComponents } from '../utils/context/combineComponents';
* Beware of the order of the providers, as they will be combined in the order they are added to the array.
* The last provider in the array will be the innermost provider.
*/
-const providers = [SettingsModalContextProvider];
+const providers = [SettingsModalContextProvider, PreviewContextProvider];
/** Combine all context providers for app-development. */
export const AppDevelopmentContextProvider = combineComponents(...providers);
diff --git a/frontend/app-development/contexts/PreviewContext/PreviewContext.test.tsx b/frontend/app-development/contexts/PreviewContext/PreviewContext.test.tsx
new file mode 100644
index 00000000000..90fb778dcf7
--- /dev/null
+++ b/frontend/app-development/contexts/PreviewContext/PreviewContext.test.tsx
@@ -0,0 +1,63 @@
+import React from 'react';
+import { render, renderHook, screen, waitFor, act } from '@testing-library/react';
+import { PreviewContextProvider, usePreviewContext } from './PreviewContext';
+
+describe('PreviewContext', () => {
+ it('should render children', () => {
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByRole('button', { name: 'My button' })).toBeInTheDocument();
+ });
+
+ it('should provide a usePreviewContext hook', () => {
+ const TestComponent = () => {
+ const {} = usePreviewContext();
+ return ;
+ };
+
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByTestId('context')).toHaveTextContent('');
+ });
+
+ it('should throw an error when usePreviewContext is used outside of a PreviewContextProvider', () => {
+ // Mock console error to check if it has been called
+ const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
+ const TestComponent = () => {
+ usePreviewContext();
+ return Test
;
+ };
+
+ expect(() => render()).toThrow(
+ 'usePreviewContext must be used within a PreviewContextProvider',
+ );
+ expect(consoleError).toHaveBeenCalled();
+ });
+
+ it('should toggle the shouldReloadPreview between true and false when doReload and hasReloaded is invoked', async () => {
+ const { result } = renderHook(() => usePreviewContext(), {
+ wrapper: PreviewContextProvider,
+ });
+
+ const { shouldReloadPreview, doReloadPreview, previewHasLoaded } = result.current;
+ expect(shouldReloadPreview).toBe(false);
+
+ act(doReloadPreview);
+ await waitFor(() => {
+ expect(result.current.shouldReloadPreview).toBe(true);
+ });
+
+ act(previewHasLoaded);
+ await waitFor(() => {
+ expect(result.current.shouldReloadPreview).toBe(false);
+ });
+ });
+});
diff --git a/frontend/app-development/contexts/PreviewContext/PreviewContext.tsx b/frontend/app-development/contexts/PreviewContext/PreviewContext.tsx
new file mode 100644
index 00000000000..1ff69c2f088
--- /dev/null
+++ b/frontend/app-development/contexts/PreviewContext/PreviewContext.tsx
@@ -0,0 +1,45 @@
+import React, { createContext, useContext, useState } from 'react';
+
+export type PreviewContextProps = {
+ shouldReloadPreview: boolean;
+ doReloadPreview: () => void;
+ previewHasLoaded: () => void;
+};
+
+export const PreviewContext = createContext>(undefined);
+
+export type PreviewContextProviderProps = {
+ children: React.ReactNode;
+};
+
+export const PreviewContextProvider = ({ children }: Partial) => {
+ const [shouldReloadPreview, setShouldReloadPreview] = useState(false);
+
+ const doReloadPreview = () => {
+ setShouldReloadPreview(true);
+ };
+
+ const previewHasLoaded = () => {
+ setShouldReloadPreview(false);
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const usePreviewContext = (): Partial => {
+ const context = useContext(PreviewContext);
+ if (context === undefined) {
+ throw new Error('usePreviewContext must be used within a PreviewContextProvider');
+ }
+ return context;
+};
diff --git a/frontend/app-development/contexts/PreviewContext/index.ts b/frontend/app-development/contexts/PreviewContext/index.ts
new file mode 100644
index 00000000000..6aad2f97d2f
--- /dev/null
+++ b/frontend/app-development/contexts/PreviewContext/index.ts
@@ -0,0 +1,6 @@
+export {
+ PreviewContext,
+ type PreviewContextProps,
+ usePreviewContext,
+ PreviewContextProvider,
+} from './PreviewContext';
diff --git a/frontend/app-development/contexts/SettingsModalContext.test.tsx b/frontend/app-development/contexts/SettingsModalContext/SettingsModalContext.test.tsx
similarity index 100%
rename from frontend/app-development/contexts/SettingsModalContext.test.tsx
rename to frontend/app-development/contexts/SettingsModalContext/SettingsModalContext.test.tsx
diff --git a/frontend/app-development/contexts/SettingsModalContext.tsx b/frontend/app-development/contexts/SettingsModalContext/SettingsModalContext.tsx
similarity index 95%
rename from frontend/app-development/contexts/SettingsModalContext.tsx
rename to frontend/app-development/contexts/SettingsModalContext/SettingsModalContext.tsx
index 50269104b66..894095ca17f 100644
--- a/frontend/app-development/contexts/SettingsModalContext.tsx
+++ b/frontend/app-development/contexts/SettingsModalContext/SettingsModalContext.tsx
@@ -1,4 +1,4 @@
-import type { SettingsModalTab } from '../types/SettingsModalTab';
+import type { SettingsModalTab } from '../../types/SettingsModalTab';
import React, { createContext, useContext, useState } from 'react';
export type SettingsModalContextProps = {
diff --git a/frontend/app-development/contexts/SettingsModalContext/index.ts b/frontend/app-development/contexts/SettingsModalContext/index.ts
new file mode 100644
index 00000000000..98189b19c0c
--- /dev/null
+++ b/frontend/app-development/contexts/SettingsModalContext/index.ts
@@ -0,0 +1,5 @@
+export {
+ SettingsModalContext,
+ SettingsModalContextProvider,
+ useSettingsModalContext,
+} from './SettingsModalContext';
diff --git a/frontend/app-development/layout/SettingsModalButton/SettingsModal/SettingsModal.test.tsx b/frontend/app-development/layout/SettingsModalButton/SettingsModal/SettingsModal.test.tsx
index 97436df068e..3d142cafd87 100644
--- a/frontend/app-development/layout/SettingsModalButton/SettingsModal/SettingsModal.test.tsx
+++ b/frontend/app-development/layout/SettingsModalButton/SettingsModal/SettingsModal.test.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { render as rtlRender, screen, waitForElementToBeRemoved } from '@testing-library/react';
+import { render, screen, waitForElementToBeRemoved } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import type { SettingsModalProps } from './SettingsModal';
import { SettingsModal } from './SettingsModal';
@@ -9,9 +9,12 @@ import type { QueryClient, UseMutationResult } from '@tanstack/react-query';
import type { ServicesContextProps } from 'app-shared/contexts/ServicesContext';
import { ServicesContextProvider } from 'app-shared/contexts/ServicesContext';
import type { AppConfig } from 'app-shared/types/AppConfig';
-import { useAppConfigMutation } from 'app-development/hooks/mutations';
+import { useAppConfigMutation } from '../../../hooks/mutations';
import { MemoryRouter } from 'react-router-dom';
+import { SettingsModalContextProvider } from '../../../contexts/SettingsModalContext';
+import { PreviewContextProvider } from '../../../contexts/PreviewContext';
+
jest.mock('../../../hooks/mutations/useAppConfigMutation');
const updateAppConfigMutation = jest.fn();
const mockUpdateAppConfigMutation = useAppConfigMutation as jest.MockedFunction<
@@ -35,7 +38,7 @@ describe('SettingsModal', () => {
};
it('closes the modal when the close button is clicked', async () => {
- render(defaultProps);
+ renderSettingsModal(defaultProps);
const closeButton = screen.getByRole('button', {
name: textMock('settings_modal.close_button_label'),
@@ -188,7 +191,7 @@ describe('SettingsModal', () => {
* to be removed from the screen
*/
const resolveAndWaitForSpinnerToDisappear = async () => {
- render(defaultProps);
+ renderSettingsModal(defaultProps);
await waitForElementToBeRemoved(() =>
screen.queryByTitle(textMock('settings_modal.loading_content')),
@@ -196,15 +199,19 @@ describe('SettingsModal', () => {
};
});
-const render = (
+const renderSettingsModal = (
props: SettingsModalProps,
queries: Partial = {},
queryClient: QueryClient = createQueryClientMock(),
) => {
- return rtlRender(
+ return render(
-
+
+
+
+
+
,
);
diff --git a/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AboutTab/AboutTab.test.tsx b/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AboutTab/AboutTab.test.tsx
index aecd644552b..a905f76a520 100644
--- a/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AboutTab/AboutTab.test.tsx
+++ b/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AboutTab/AboutTab.test.tsx
@@ -1,11 +1,11 @@
import React from 'react';
-import { render as rtlRender, screen, waitForElementToBeRemoved } from '@testing-library/react';
+import { render, screen, waitForElementToBeRemoved } from '@testing-library/react';
import { AboutTab } from './AboutTab';
import { textMock } from '@studio/testing/mocks/i18nMock';
import type { AppConfig } from 'app-shared/types/AppConfig';
import userEvent from '@testing-library/user-event';
import { useAppConfigMutation } from 'app-development/hooks/mutations';
-import type { QueryClient, UseMutationResult } from '@tanstack/react-query';
+import type { UseMutationResult } from '@tanstack/react-query';
import type { ServicesContextProps } from 'app-shared/contexts/ServicesContext';
import { ServicesContextProvider } from 'app-shared/contexts/ServicesContext';
import { createQueryClientMock } from 'app-shared/mocks/queryClientMock';
@@ -16,6 +16,11 @@ import { formatDateToDateAndTimeString } from 'app-development/utils/dateUtils';
import { MemoryRouter } from 'react-router-dom';
import type { ApplicationMetadata } from 'app-shared/types/ApplicationMetadata';
import { app, org } from '@studio/testing/testids';
+import { SettingsModalContextProvider } from '../../../../../../contexts/SettingsModalContext';
+import {
+ PreviewContext,
+ type PreviewContextProps,
+} from '../../../../../../contexts/PreviewContext';
const mockNewText: string = 'test';
@@ -25,6 +30,12 @@ const mockAppMetadata: ApplicationMetadata = {
createdBy: 'Test Testesen',
};
+const defaultPreviewContextProps: PreviewContextProps = {
+ shouldReloadPreview: false,
+ doReloadPreview: jest.fn(),
+ previewHasLoaded: jest.fn(),
+};
+
jest.mock('../../../../../../hooks/mutations/useAppConfigMutation');
const updateAppConfigMutation = jest.fn();
const mockUpdateAppConfigMutation = useAppConfigMutation as jest.MockedFunction<
@@ -49,23 +60,23 @@ describe('AboutTab', () => {
afterEach(jest.clearAllMocks);
it('initially displays the spinner when loading data', () => {
- render();
+ renderAboutTab();
expect(screen.getByTitle(textMock('settings_modal.loading_content'))).toBeInTheDocument();
});
it('fetches appConfig on mount', () => {
- render();
+ renderAboutTab();
expect(getAppConfig).toHaveBeenCalledTimes(1);
});
it('fetches repoMetaData on mount', () => {
- render();
+ renderAboutTab();
expect(getRepoMetadata).toHaveBeenCalledTimes(1);
});
it('fetches applicationMetadata on mount', () => {
- render();
+ renderAboutTab();
expect(getAppMetadata).toHaveBeenCalledTimes(1);
});
@@ -73,8 +84,10 @@ describe('AboutTab', () => {
'shows an error message if an error occured on the %s query',
async (queryName) => {
const errorMessage = 'error-message-test';
- render({
- [queryName]: () => Promise.reject({ message: errorMessage }),
+ renderAboutTab({
+ queries: {
+ [queryName]: () => Promise.reject({ message: errorMessage }),
+ },
});
await waitForElementToBeRemoved(() =>
@@ -139,7 +152,7 @@ describe('AboutTab', () => {
it('displays owners login name when full name is not set', async () => {
getRepoMetadata.mockImplementation(() => Promise.resolve(mockRepository2));
- render();
+ renderAboutTab();
await waitForElementToBeRemoved(() =>
screen.queryByTitle(textMock('settings_modal.loading_content')),
);
@@ -165,23 +178,40 @@ describe('AboutTab', () => {
expect(screen.getByText(mockAppMetadata.createdBy)).toBeInTheDocument();
});
+
+ it('calls "doReloadPreview" when saving the app config', async () => {
+ const user = userEvent.setup();
+
+ const doReloadPreview = jest.fn();
+ await resolveAndWaitForSpinnerToDisappear({ previewContextProps: { doReloadPreview } });
+
+ const serviceName = screen.getByLabelText(textMock('settings_modal.about_tab_name_label'));
+ await user.type(serviceName, mockNewText);
+ await user.tab();
+
+ expect(doReloadPreview).toHaveBeenCalledTimes(1);
+ });
});
-const resolveAndWaitForSpinnerToDisappear = async () => {
+type Props = {
+ queries: Partial;
+ previewContextProps: Partial;
+};
+
+const resolveAndWaitForSpinnerToDisappear = async (props: Partial = {}) => {
getAppConfig.mockImplementation(() => Promise.resolve(mockAppConfig));
getRepoMetadata.mockImplementation(() => Promise.resolve(mockRepository1));
getAppMetadata.mockImplementation(() => Promise.resolve(mockAppMetadata));
- render();
+ renderAboutTab(props);
await waitForElementToBeRemoved(() =>
screen.queryByTitle(textMock('settings_modal.loading_content')),
);
};
-const render = (
- queries: Partial = {},
- queryClient: QueryClient = createQueryClientMock(),
-) => {
+const renderAboutTab = (props: Partial = {}) => {
+ const { queries, previewContextProps } = props;
+
const allQueries: ServicesContextProps = {
...queriesMock,
getAppConfig,
@@ -190,10 +220,16 @@ const render = (
...queries,
};
- return rtlRender(
+ return render(
-
-
+
+
+
+
+
+
,
);
diff --git a/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AboutTab/AboutTab.tsx b/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AboutTab/AboutTab.tsx
index 64356535ca5..12f4b0f59ba 100644
--- a/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AboutTab/AboutTab.tsx
+++ b/frontend/app-development/layout/SettingsModalButton/SettingsModal/components/Tabs/AboutTab/AboutTab.tsx
@@ -1,4 +1,3 @@
-import type { ReactNode } from 'react';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { TabHeader } from '../../TabHeader';
@@ -15,14 +14,17 @@ import { TabDataError } from '../../TabDataError';
import { InputFields } from './InputFields';
import { CreatedFor } from './CreatedFor';
import { TabContent } from '../../TabContent';
+import { usePreviewContext } from '../../../../../../contexts/PreviewContext';
import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams';
-export const AboutTab = (): ReactNode => {
+export const AboutTab = (): React.ReactElement => {
const { t } = useTranslation();
const { org, app } = useStudioEnvironmentParams();
const repositoryType = getRepositoryType(org, app);
+ const { doReloadPreview } = usePreviewContext();
+
const {
status: appConfigStatus,
data: appConfigData,
@@ -42,6 +44,9 @@ export const AboutTab = (): ReactNode => {
const { mutate: updateAppConfigMutation } = useAppConfigMutation(org, app);
const handleSaveAppConfig = (appConfig: AppConfig) => {
+ if (appConfigData.serviceName !== appConfig.serviceName) {
+ doReloadPreview();
+ }
updateAppConfigMutation(appConfig);
};
diff --git a/frontend/app-development/router/routes.test.tsx b/frontend/app-development/router/routes.test.tsx
index 0f68113c105..9a139f72947 100644
--- a/frontend/app-development/router/routes.test.tsx
+++ b/frontend/app-development/router/routes.test.tsx
@@ -8,6 +8,8 @@ import type { AppVersion } from 'app-shared/types/AppVersion';
import { ServicesContextProvider } from 'app-shared/contexts/ServicesContext';
import { queriesMock } from 'app-shared/mocks/queriesMock';
import { app, org } from '@studio/testing/testids';
+import { SettingsModalContextProvider } from '../contexts/SettingsModalContext';
+import { PreviewContextProvider } from '../contexts/PreviewContext';
// Mocks:
jest.mock('@altinn/ux-editor-v3/SubApp', () => ({
@@ -52,7 +54,11 @@ const renderSubapp = (path: RoutePaths, frontendVersion: string = null) => {
queryClient.setQueryData([QueryKey.AppVersion, org, app], appVersion);
return render(
-
+
+
+
+
+
,
);
};
diff --git a/frontend/app-development/router/routes.tsx b/frontend/app-development/router/routes.tsx
index 6088c6ccd20..178a28e7c74 100644
--- a/frontend/app-development/router/routes.tsx
+++ b/frontend/app-development/router/routes.tsx
@@ -10,6 +10,7 @@ import type { AppVersion } from 'app-shared/types/AppVersion';
import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams';
import { useAppVersionQuery } from 'app-shared/hooks/queries';
import React from 'react';
+import { usePreviewContext } from '../contexts/PreviewContext';
interface IRouteProps {
headerTextKey?: string;
@@ -37,8 +38,15 @@ const isLatestFrontendVersion = (version: AppVersion): boolean =>
const UiEditor = () => {
const { org, app } = useStudioEnvironmentParams();
const { data: version } = useAppVersionQuery(org, app);
+ const { shouldReloadPreview, previewHasLoaded } = usePreviewContext();
+
if (!version) return null;
- return isLatestFrontendVersion(version) ? : ;
+
+ return isLatestFrontendVersion(version) ? (
+
+ ) : (
+
+ );
};
export const routerRoutes: RouterRoute[] = [
diff --git a/frontend/packages/ux-editor/src/AppContext.test.tsx b/frontend/packages/ux-editor/src/AppContext.test.tsx
index 5e4468e08a9..1d10c920ae1 100644
--- a/frontend/packages/ux-editor/src/AppContext.test.tsx
+++ b/frontend/packages/ux-editor/src/AppContext.test.tsx
@@ -71,7 +71,7 @@ const renderAppContext = (children: (appContext: AppContextProps) => React.React
...render(
-
+
{(appContext: AppContextProps) => children(appContext)}
diff --git a/frontend/packages/ux-editor/src/AppContext.tsx b/frontend/packages/ux-editor/src/AppContext.tsx
index 007b4787ef2..740b586c279 100644
--- a/frontend/packages/ux-editor/src/AppContext.tsx
+++ b/frontend/packages/ux-editor/src/AppContext.tsx
@@ -16,15 +16,23 @@ export interface AppContextProps {
refetchLayouts: (layoutSetName: string, resetQueries?: boolean) => Promise;
refetchLayoutSettings: (layoutSetName: string, resetQueries?: boolean) => Promise;
refetchTexts: (language: string, resetQueries?: boolean) => Promise;
+ shouldReloadPreview: boolean;
+ previewHasLoaded: () => void;
}
export const AppContext = createContext(null);
type AppContextProviderProps = {
children: React.ReactNode;
+ shouldReloadPreview: boolean;
+ previewHasLoaded: () => void;
};
-export const AppContextProvider = ({ children }: AppContextProviderProps): React.JSX.Element => {
+export const AppContextProvider = ({
+ children,
+ shouldReloadPreview,
+ previewHasLoaded,
+}: AppContextProviderProps): React.JSX.Element => {
const previewIframeRef = useRef(null);
const { selectedFormLayoutSetName, setSelectedFormLayoutSetName } =
useSelectedFormLayoutSetName();
@@ -77,6 +85,8 @@ export const AppContextProvider = ({ children }: AppContextProviderProps): React
refetchLayouts,
refetchLayoutSettings,
refetchTexts,
+ shouldReloadPreview,
+ previewHasLoaded,
}),
[
selectedFormLayoutSetName,
@@ -86,6 +96,8 @@ export const AppContextProvider = ({ children }: AppContextProviderProps): React
refetchLayouts,
refetchLayoutSettings,
refetchTexts,
+ shouldReloadPreview,
+ previewHasLoaded,
],
);
diff --git a/frontend/packages/ux-editor/src/SubApp.test.tsx b/frontend/packages/ux-editor/src/SubApp.test.tsx
index c16a30879c9..a9804d5774b 100644
--- a/frontend/packages/ux-editor/src/SubApp.test.tsx
+++ b/frontend/packages/ux-editor/src/SubApp.test.tsx
@@ -18,7 +18,7 @@ jest.mock('./App', () => ({
describe('SubApp', () => {
it('Renders the app within the AppContext provider', () => {
- render();
+ render();
const provider = screen.getByTestId(providerTestId);
expect(provider).toBeInTheDocument();
expect(within(provider).getByTestId(appTestId)).toBeInTheDocument();
diff --git a/frontend/packages/ux-editor/src/SubApp.tsx b/frontend/packages/ux-editor/src/SubApp.tsx
index b5552672c09..39b99fe3594 100644
--- a/frontend/packages/ux-editor/src/SubApp.tsx
+++ b/frontend/packages/ux-editor/src/SubApp.tsx
@@ -3,9 +3,14 @@ import { App } from './App';
import './styles/index.css';
import { AppContextProvider } from './AppContext';
-export const SubApp = () => {
+type SubAppProps = {
+ shouldReloadPreview: boolean;
+ previewHasLoaded: () => void;
+};
+
+export const SubApp = (props: SubAppProps) => {
return (
-
+
);
diff --git a/frontend/packages/ux-editor/src/components/Preview/Preview.tsx b/frontend/packages/ux-editor/src/components/Preview/Preview.tsx
index cf4df33dcb7..cf8571029f6 100644
--- a/frontend/packages/ux-editor/src/components/Preview/Preview.tsx
+++ b/frontend/packages/ux-editor/src/components/Preview/Preview.tsx
@@ -4,6 +4,7 @@ import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmen
import cn from 'classnames';
import { useTranslation } from 'react-i18next';
import { useAppContext, useSelectedTaskId } from '../../hooks';
+import { useChecksum } from '../../hooks/useChecksum.ts';
import { previewPage } from 'app-shared/api/paths';
import { Paragraph } from '@digdir/design-system-react';
import { StudioButton, StudioCenter } from '@studio/components';
@@ -64,6 +65,9 @@ const PreviewFrame = () => {
const taskId = useSelectedTaskId(selectedFormLayoutSetName);
const { t } = useTranslation();
+ const { shouldReloadPreview, previewHasLoaded } = useAppContext();
+ const checksum = useChecksum(shouldReloadPreview);
+
useEffect(() => {
return () => {
previewIframeRef.current = null;
@@ -76,10 +80,12 @@ const PreviewFrame = () => {
diff --git a/frontend/packages/ux-editor/src/hooks/useChecksum.ts/index.ts b/frontend/packages/ux-editor/src/hooks/useChecksum.ts/index.ts
new file mode 100644
index 00000000000..ebf1248fe9e
--- /dev/null
+++ b/frontend/packages/ux-editor/src/hooks/useChecksum.ts/index.ts
@@ -0,0 +1 @@
+export { useChecksum } from './useChecksum';
diff --git a/frontend/packages/ux-editor/src/hooks/useChecksum.ts/useChecksum.test.ts b/frontend/packages/ux-editor/src/hooks/useChecksum.ts/useChecksum.test.ts
new file mode 100644
index 00000000000..0e3ae624371
--- /dev/null
+++ b/frontend/packages/ux-editor/src/hooks/useChecksum.ts/useChecksum.test.ts
@@ -0,0 +1,31 @@
+import { useChecksum } from './useChecksum';
+import { renderHook } from '@testing-library/react';
+
+describe('useChecksum', () => {
+ it('should increment checksum when shouldCreateNewChecksum is true', async () => {
+ const { result, rerender } = renderHook(
+ ({ shouldCreateNewChecksum }) => useChecksum(shouldCreateNewChecksum),
+ {
+ initialProps: { shouldCreateNewChecksum: false },
+ },
+ );
+ expect(result.current).toBe(0);
+
+ rerender({ shouldCreateNewChecksum: true });
+ expect(result.current).toBe(1);
+ });
+
+ it('should not change checksum when shouldCreateNewChecksum is false', () => {
+ const { result, rerender } = renderHook(
+ ({ shouldCreateNewChecksum }) => useChecksum(shouldCreateNewChecksum),
+ {
+ initialProps: { shouldCreateNewChecksum: false },
+ },
+ );
+
+ expect(result.current).toBe(0);
+
+ rerender({ shouldCreateNewChecksum: false });
+ expect(result.current).toBe(0);
+ });
+});
diff --git a/frontend/packages/ux-editor/src/hooks/useChecksum.ts/useChecksum.ts b/frontend/packages/ux-editor/src/hooks/useChecksum.ts/useChecksum.ts
new file mode 100644
index 00000000000..10c28c0e613
--- /dev/null
+++ b/frontend/packages/ux-editor/src/hooks/useChecksum.ts/useChecksum.ts
@@ -0,0 +1,13 @@
+import { useEffect, useState } from 'react';
+
+export const useChecksum = (shouldCreateNewChecksum: boolean): number => {
+ const [checksum, setChecksum] = useState
(0);
+
+ useEffect(() => {
+ if (shouldCreateNewChecksum) {
+ setChecksum((v) => v + 1);
+ }
+ }, [shouldCreateNewChecksum]);
+
+ return checksum;
+};
diff --git a/frontend/packages/ux-editor/src/testing/appContextMock.ts b/frontend/packages/ux-editor/src/testing/appContextMock.ts
index 918b4d3fa69..33c74f1fd8d 100644
--- a/frontend/packages/ux-editor/src/testing/appContextMock.ts
+++ b/frontend/packages/ux-editor/src/testing/appContextMock.ts
@@ -16,4 +16,6 @@ export const appContextMock: AppContextProps = {
refetchLayouts: jest.fn(),
refetchLayoutSettings: jest.fn(),
refetchTexts: jest.fn(),
+ shouldReloadPreview: false,
+ previewHasLoaded: jest.fn(),
};
From 971ba4cd29ba8c5d05586c4fa88ac591f034b357 Mon Sep 17 00:00:00 2001
From: Lars <74791975+lassopicasso@users.noreply.github.com>
Date: Tue, 4 Jun 2024 07:16:05 +0200
Subject: [PATCH 18/27] add key to configContent (#12899)
---
.../process-editor/src/components/ConfigPanel/ConfigPanel.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/packages/process-editor/src/components/ConfigPanel/ConfigPanel.tsx b/frontend/packages/process-editor/src/components/ConfigPanel/ConfigPanel.tsx
index 6e355b8575c..66720700f14 100644
--- a/frontend/packages/process-editor/src/components/ConfigPanel/ConfigPanel.tsx
+++ b/frontend/packages/process-editor/src/components/ConfigPanel/ConfigPanel.tsx
@@ -45,7 +45,7 @@ const ConfigPanelContent = (): React.ReactElement => {
const elementIsTask = bpmnDetails.type === BpmnTypeEnum.Task;
if (elementIsTask) {
- return ;
+ return ;
}
return (
From be60dd12e990cf5c7f3041d0b2e85969d9616220 Mon Sep 17 00:00:00 2001
From: David Ovrelid <46874830+framitdavid@users.noreply.github.com>
Date: Tue, 4 Jun 2024 07:28:10 +0200
Subject: [PATCH 19/27] should rerender the diagram when local-changes is
deleted (#12911)
---
.../process-editor/src/components/Canvas/Canvas.tsx | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/frontend/packages/process-editor/src/components/Canvas/Canvas.tsx b/frontend/packages/process-editor/src/components/Canvas/Canvas.tsx
index b851ba92693..f78777025eb 100644
--- a/frontend/packages/process-editor/src/components/Canvas/Canvas.tsx
+++ b/frontend/packages/process-editor/src/components/Canvas/Canvas.tsx
@@ -11,12 +11,14 @@ import { BPMNEditor } from './BPMNEditor';
import { VersionHelpText } from './VersionHelpText';
export const Canvas = (): React.ReactElement => {
- const { isEditAllowed } = useBpmnContext();
+ const { isEditAllowed, bpmnXml } = useBpmnContext();
return (
<>
{!isEditAllowed && }
- {isEditAllowed ? : }
+
+ {isEditAllowed ? : }
+
>
);
};
From 63502e83246c180f778759bd25834ff5dc8733b8 Mon Sep 17 00:00:00 2001
From: David Ovrelid <46874830+framitdavid@users.noreply.github.com>
Date: Tue, 4 Jun 2024 07:28:31 +0200
Subject: [PATCH 20/27] resized the "get started" box (#12910)
* resize the get started box
---
.../components/Documentation.module.css | 18 +++++++-----------
.../overview/components/Documentation.tsx | 4 ++--
2 files changed, 9 insertions(+), 13 deletions(-)
diff --git a/frontend/app-development/features/overview/components/Documentation.module.css b/frontend/app-development/features/overview/components/Documentation.module.css
index 41a3ac0ce83..a574297a503 100644
--- a/frontend/app-development/features/overview/components/Documentation.module.css
+++ b/frontend/app-development/features/overview/components/Documentation.module.css
@@ -5,15 +5,11 @@
position: relative;
}
-.heading {
- margin-bottom: var(--fds-spacing-2);
-}
-
.link {
display: flex;
align-items: flex-start;
- margin-top: var(--fds-spacing-4);
- line-height: var(--fds-sizing-6);
+ margin-top: var(--fds-spacing-2);
+ line-height: var(--fds-sizing-5);
}
.linkIcon {
@@ -22,7 +18,7 @@
@media (min-width: 1200px) {
.documentation {
- min-height: 280px;
+ min-height: 104px;
}
.content {
@@ -34,16 +30,16 @@
.documentation::before {
background-image: url('/designer/img/Altinn-studio-3.svg');
background-position: center center;
- background-size: 150px auto;
+ background-size: 64px auto;
background-repeat: no-repeat;
content: ' ';
display: block;
position: absolute;
- bottom: var(--fds-spacing-4);
+ bottom: 0;
right: var(--fds-spacing-4);
- width: 150px;
- height: 150px;
+ width: 64px;
+ height: 64px;
opacity: 0.3;
transform: rotateY(180deg);
diff --git a/frontend/app-development/features/overview/components/Documentation.tsx b/frontend/app-development/features/overview/components/Documentation.tsx
index 331b88f4986..99a35d7c3a5 100644
--- a/frontend/app-development/features/overview/components/Documentation.tsx
+++ b/frontend/app-development/features/overview/components/Documentation.tsx
@@ -4,11 +4,11 @@ import { Heading, Link } from '@digdir/design-system-react';
import { ExternalLinkIcon } from '@studio/icons';
import { useTranslation } from 'react-i18next';
-export const Documentation = () => {
+export const Documentation = (): React.ReactElement => {
const { t } = useTranslation();
return (
-
+
{t('overview.documentation.title')}
Date: Tue, 4 Jun 2024 09:03:06 +0200
Subject: [PATCH 21/27] Fix the link to the datamodel page for datamodel repos
(#12909)
* Fix link to data-model
* Fix urlUtils tests
---
frontend/dashboard/utils/urlUtils/urlUtils.test.ts | 2 +-
frontend/dashboard/utils/urlUtils/urlUtils.ts | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/frontend/dashboard/utils/urlUtils/urlUtils.test.ts b/frontend/dashboard/utils/urlUtils/urlUtils.test.ts
index b6d8812623a..24294d01539 100644
--- a/frontend/dashboard/utils/urlUtils/urlUtils.test.ts
+++ b/frontend/dashboard/utils/urlUtils/urlUtils.test.ts
@@ -20,7 +20,7 @@ describe('urlUtils', () => {
repo: 'org-name-datamodels',
});
- expect(result).toBe(`${APP_DEVELOPMENT_BASENAME}/org-name/org-name-datamodels/datamodel`);
+ expect(result).toBe(`${APP_DEVELOPMENT_BASENAME}/org-name/org-name-datamodels/data-model`);
});
it('should not return url to dataModelling when repo name does not match "-dataModels"', () => {
diff --git a/frontend/dashboard/utils/urlUtils/urlUtils.ts b/frontend/dashboard/utils/urlUtils/urlUtils.ts
index 64da8107a02..1058905aca5 100644
--- a/frontend/dashboard/utils/urlUtils/urlUtils.ts
+++ b/frontend/dashboard/utils/urlUtils/urlUtils.ts
@@ -13,7 +13,7 @@ type GetRepoUrl = {
export const getRepoEditUrl = ({ org, repo }: GetRepoUrl): string => {
if (getRepositoryType(org, repo) === RepositoryType.DataModels) {
- return `${APP_DEVELOPMENT_BASENAME}/${org}/${repo}/datamodel`;
+ return `${APP_DEVELOPMENT_BASENAME}/${org}/${repo}/data-model`;
}
return `${APP_DEVELOPMENT_BASENAME}/${org}/${repo}`;
From c1c872e5149bd20dbf6f4fdcf8b03c0e020fd7dd Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Tue, 4 Jun 2024 09:54:48 +0200
Subject: [PATCH 22/27] Update nuget non-major dependencies (#12895)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
---
backend/packagegroups/NuGet.props | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/backend/packagegroups/NuGet.props b/backend/packagegroups/NuGet.props
index a6923db26e8..370b94c41fa 100644
--- a/backend/packagegroups/NuGet.props
+++ b/backend/packagegroups/NuGet.props
@@ -2,9 +2,9 @@
-
-
-
+
+
+
@@ -19,7 +19,7 @@
-
+
@@ -29,7 +29,7 @@
-
+
@@ -40,12 +40,12 @@
-
+
-
-
+
+
@@ -54,7 +54,7 @@
-
+
From 0981c610b74b45d32f079555771f3bf286df5cc5 Mon Sep 17 00:00:00 2001
From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com>
Date: Tue, 4 Jun 2024 10:56:24 +0200
Subject: [PATCH 23/27] Update npm non-major dependencies (#12897)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: WilliamThorenfeldt <133344438+WilliamThorenfeldt@users.noreply.github.com>
---
development/azure-devops-mock/package.json | 2 +-
frontend/app-development/package.json | 2 +-
frontend/libs/studio-components/package.json | 6 +-
frontend/packages/schema-editor/package.json | 2 +-
frontend/packages/shared/package.json | 2 +-
frontend/packages/ux-editor-v3/package.json | 2 +-
frontend/packages/ux-editor/package.json | 2 +-
frontend/scripts/package.json | 6 +-
frontend/scripts/yarn.lock | 219 +--
frontend/testing/cypress/package.json | 2 +-
package.json | 16 +-
yarn.lock | 1554 ++++++++++--------
12 files changed, 983 insertions(+), 832 deletions(-)
diff --git a/development/azure-devops-mock/package.json b/development/azure-devops-mock/package.json
index 41aa56558c0..f9422daf1b5 100644
--- a/development/azure-devops-mock/package.json
+++ b/development/azure-devops-mock/package.json
@@ -8,7 +8,7 @@
"cors": "2.8.5",
"express": "4.19.2",
"morgan": "1.10.0",
- "nodemon": "3.1.0",
+ "nodemon": "3.1.2",
"p-queue": "8.0.1"
},
"license": "MIT",
diff --git a/frontend/app-development/package.json b/frontend/app-development/package.json
index d266cdebf6a..188c192c1c5 100644
--- a/frontend/app-development/package.json
+++ b/frontend/app-development/package.json
@@ -9,7 +9,7 @@
"not op_mini all"
],
"dependencies": {
- "@mui/material": "5.15.18",
+ "@mui/material": "5.15.19",
"@reduxjs/toolkit": "1.9.7",
"@studio/icons": "workspace:^",
"@studio/pure-functions": "workspace:^",
diff --git a/frontend/libs/studio-components/package.json b/frontend/libs/studio-components/package.json
index ff25ff7c143..6cbc8f748bb 100644
--- a/frontend/libs/studio-components/package.json
+++ b/frontend/libs/studio-components/package.json
@@ -11,18 +11,18 @@
},
"dependencies": {
"@studio/icons": "^0.1.0",
- "ajv": "8.13.0",
+ "ajv": "8.14.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"uuid": "9.0.1"
},
"devDependencies": {
- "@chromatic-com/storybook": "1.4.0",
+ "@chromatic-com/storybook": "1.5.0",
"@storybook/addon-essentials": "^8.0.4",
"@storybook/addon-interactions": "^8.0.4",
"@storybook/addon-links": "^8.0.4",
"@storybook/addon-onboarding": "^8.0.4",
- "@storybook/addon-webpack5-compiler-swc": "1.0.2",
+ "@storybook/addon-webpack5-compiler-swc": "1.0.3",
"@storybook/blocks": "^8.0.4",
"@storybook/react": "^8.0.4",
"@storybook/react-webpack5": "^8.0.4",
diff --git a/frontend/packages/schema-editor/package.json b/frontend/packages/schema-editor/package.json
index 34192f25fdf..e1155e8217f 100644
--- a/frontend/packages/schema-editor/package.json
+++ b/frontend/packages/schema-editor/package.json
@@ -9,7 +9,7 @@
"jest": "29.7.0"
},
"peerDependencies": {
- "@mui/material": "5.15.18",
+ "@mui/material": "5.15.19",
"axios": "1.7.2",
"classnames": "2.5.1",
"react": "18.3.1",
diff --git a/frontend/packages/shared/package.json b/frontend/packages/shared/package.json
index 43fac0daedb..2b534754907 100644
--- a/frontend/packages/shared/package.json
+++ b/frontend/packages/shared/package.json
@@ -3,7 +3,7 @@
"version": "0.1.0",
"author": "Altinn",
"dependencies": {
- "@mui/material": "5.15.18",
+ "@mui/material": "5.15.19",
"@reduxjs/toolkit": "1.9.7",
"axios": "1.7.2",
"classnames": "2.5.1",
diff --git a/frontend/packages/ux-editor-v3/package.json b/frontend/packages/ux-editor-v3/package.json
index 5f1e497d98a..5e05eafd028 100644
--- a/frontend/packages/ux-editor-v3/package.json
+++ b/frontend/packages/ux-editor-v3/package.json
@@ -4,7 +4,7 @@
"version": "1.0.1",
"author": "Altinn",
"dependencies": {
- "@mui/material": "5.15.18",
+ "@mui/material": "5.15.19",
"@reduxjs/toolkit": "1.9.7",
"@studio/icons": "workspace:^",
"axios": "1.7.2",
diff --git a/frontend/packages/ux-editor/package.json b/frontend/packages/ux-editor/package.json
index 5f50b49f912..ac2be39679f 100644
--- a/frontend/packages/ux-editor/package.json
+++ b/frontend/packages/ux-editor/package.json
@@ -4,7 +4,7 @@
"version": "1.0.1",
"author": "Altinn",
"dependencies": {
- "@mui/material": "5.15.18",
+ "@mui/material": "5.15.19",
"@studio/icons": "workspace:^",
"axios": "1.7.2",
"classnames": "2.5.1",
diff --git a/frontend/scripts/package.json b/frontend/scripts/package.json
index c575adcccca..3c020c9f2f5 100644
--- a/frontend/scripts/package.json
+++ b/frontend/scripts/package.json
@@ -4,15 +4,15 @@
"axios": "1.7.2"
},
"devDependencies": {
- "@typescript-eslint/eslint-plugin": "7.10.0",
- "@typescript-eslint/parser": "7.10.0",
+ "@typescript-eslint/eslint-plugin": "7.11.0",
+ "@typescript-eslint/parser": "7.11.0",
"eslint": "8.57.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-prettier": "5.1.3",
"glob": "10.4.1",
"husky": "9.0.11",
- "lint-staged": "15.2.4",
+ "lint-staged": "15.2.5",
"prettier": "^3.0.3",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
diff --git a/frontend/scripts/yarn.lock b/frontend/scripts/yarn.lock
index 1893190fcbc..80319a98cf6 100644
--- a/frontend/scripts/yarn.lock
+++ b/frontend/scripts/yarn.lock
@@ -202,15 +202,15 @@ __metadata:
languageName: node
linkType: hard
-"@typescript-eslint/eslint-plugin@npm:7.10.0":
- version: 7.10.0
- resolution: "@typescript-eslint/eslint-plugin@npm:7.10.0"
+"@typescript-eslint/eslint-plugin@npm:7.11.0":
+ version: 7.11.0
+ resolution: "@typescript-eslint/eslint-plugin@npm:7.11.0"
dependencies:
"@eslint-community/regexpp": "npm:^4.10.0"
- "@typescript-eslint/scope-manager": "npm:7.10.0"
- "@typescript-eslint/type-utils": "npm:7.10.0"
- "@typescript-eslint/utils": "npm:7.10.0"
- "@typescript-eslint/visitor-keys": "npm:7.10.0"
+ "@typescript-eslint/scope-manager": "npm:7.11.0"
+ "@typescript-eslint/type-utils": "npm:7.11.0"
+ "@typescript-eslint/utils": "npm:7.11.0"
+ "@typescript-eslint/visitor-keys": "npm:7.11.0"
graphemer: "npm:^1.4.0"
ignore: "npm:^5.3.1"
natural-compare: "npm:^1.4.0"
@@ -221,44 +221,44 @@ __metadata:
peerDependenciesMeta:
typescript:
optional: true
- checksum: 10/dfe505cdf718dd29e8637b902e4c544c6b7d246d2051fd1936090423eb3dadfe2bd757de51e565e6fd80e74cf1918e191c26fee6df515100484ec3efd9b8d111
+ checksum: 10/be95ed0bbd5b34c47239677ea39d531bcd8a18717a67d70a297bed5b0050b256159856bb9c1e894ac550d011c24bb5b4abf8056c5d70d0d5895f0cc1accd14ea
languageName: node
linkType: hard
-"@typescript-eslint/parser@npm:7.10.0":
- version: 7.10.0
- resolution: "@typescript-eslint/parser@npm:7.10.0"
+"@typescript-eslint/parser@npm:7.11.0":
+ version: 7.11.0
+ resolution: "@typescript-eslint/parser@npm:7.11.0"
dependencies:
- "@typescript-eslint/scope-manager": "npm:7.10.0"
- "@typescript-eslint/types": "npm:7.10.0"
- "@typescript-eslint/typescript-estree": "npm:7.10.0"
- "@typescript-eslint/visitor-keys": "npm:7.10.0"
+ "@typescript-eslint/scope-manager": "npm:7.11.0"
+ "@typescript-eslint/types": "npm:7.11.0"
+ "@typescript-eslint/typescript-estree": "npm:7.11.0"
+ "@typescript-eslint/visitor-keys": "npm:7.11.0"
debug: "npm:^4.3.4"
peerDependencies:
eslint: ^8.56.0
peerDependenciesMeta:
typescript:
optional: true
- checksum: 10/1fa71049b2debf2f7f5366fb433e3d4c8e1591c2061a15fa8797d14623a2b6984340a59e7717acc013ce8c6a2ed32c5c0e811fe948b5936d41c2a5a09b61d130
+ checksum: 10/0a32417aec62d7de04427323ab3fc8159f9f02429b24f739d8748e8b54fc65b0e3dbae8e4779c4b795f0d8e5f98a4d83a43b37ea0f50ebda51546cdcecf73caa
languageName: node
linkType: hard
-"@typescript-eslint/scope-manager@npm:7.10.0":
- version: 7.10.0
- resolution: "@typescript-eslint/scope-manager@npm:7.10.0"
+"@typescript-eslint/scope-manager@npm:7.11.0":
+ version: 7.11.0
+ resolution: "@typescript-eslint/scope-manager@npm:7.11.0"
dependencies:
- "@typescript-eslint/types": "npm:7.10.0"
- "@typescript-eslint/visitor-keys": "npm:7.10.0"
- checksum: 10/838a7a9573577d830b2f65801ce045abe6fad08ac7e04bac4cc9b2e5b7cbac07e645de9c79b9485f4cc361fe25da5319025aa0336fad618023fff62e4e980638
+ "@typescript-eslint/types": "npm:7.11.0"
+ "@typescript-eslint/visitor-keys": "npm:7.11.0"
+ checksum: 10/79eff310405c6657ff092641e3ad51c6698c6708b915ecef945ebdd1737bd48e1458c5575836619f42dec06143ec0e3a826f3e551af590d297367da3d08f329e
languageName: node
linkType: hard
-"@typescript-eslint/type-utils@npm:7.10.0":
- version: 7.10.0
- resolution: "@typescript-eslint/type-utils@npm:7.10.0"
+"@typescript-eslint/type-utils@npm:7.11.0":
+ version: 7.11.0
+ resolution: "@typescript-eslint/type-utils@npm:7.11.0"
dependencies:
- "@typescript-eslint/typescript-estree": "npm:7.10.0"
- "@typescript-eslint/utils": "npm:7.10.0"
+ "@typescript-eslint/typescript-estree": "npm:7.11.0"
+ "@typescript-eslint/utils": "npm:7.11.0"
debug: "npm:^4.3.4"
ts-api-utils: "npm:^1.3.0"
peerDependencies:
@@ -266,23 +266,23 @@ __metadata:
peerDependenciesMeta:
typescript:
optional: true
- checksum: 10/e62db9ffbfbccce60258108f7ed025005e04df18da897ff1b30049e3c10a47150e94c2fb5ac0ab9711ebb60517521213dcccbea6d08125107a87a67088a79042
+ checksum: 10/ab6ebeff68a60fc40d0ace88e03d6b4242b8f8fe2fa300db161780d58777b57f69fa077cd482e1b673316559459bd20b8cc89a7f9f30e644bfed8293f77f0e4b
languageName: node
linkType: hard
-"@typescript-eslint/types@npm:7.10.0":
- version: 7.10.0
- resolution: "@typescript-eslint/types@npm:7.10.0"
- checksum: 10/76075a7b87ddfff8e7e4aebf3d225e67bf79ead12a7709999d4d5c31611d9c0813ca69a9298f320efb018fe493ce3763c964a0e670a4c953d8eff000f10672c0
+"@typescript-eslint/types@npm:7.11.0":
+ version: 7.11.0
+ resolution: "@typescript-eslint/types@npm:7.11.0"
+ checksum: 10/c6a0b47ef43649a59c9d51edfc61e367b55e519376209806b1c98385a8385b529e852c7a57e081fb15ef6a5dc0fc8e90bd5a508399f5ac2137f4d462e89cdc30
languageName: node
linkType: hard
-"@typescript-eslint/typescript-estree@npm:7.10.0":
- version: 7.10.0
- resolution: "@typescript-eslint/typescript-estree@npm:7.10.0"
+"@typescript-eslint/typescript-estree@npm:7.11.0":
+ version: 7.11.0
+ resolution: "@typescript-eslint/typescript-estree@npm:7.11.0"
dependencies:
- "@typescript-eslint/types": "npm:7.10.0"
- "@typescript-eslint/visitor-keys": "npm:7.10.0"
+ "@typescript-eslint/types": "npm:7.11.0"
+ "@typescript-eslint/visitor-keys": "npm:7.11.0"
debug: "npm:^4.3.4"
globby: "npm:^11.1.0"
is-glob: "npm:^4.0.3"
@@ -292,31 +292,31 @@ __metadata:
peerDependenciesMeta:
typescript:
optional: true
- checksum: 10/d11d0c45749c9bd4a187b6dfdf5600e36ba8c87667cd2020d9158667c47c32ec0bcb1ef3b7eee5577b667def5f7f33d8131092a0f221b3d3e8105078800f923f
+ checksum: 10/b98b101e42d3b91003510a5c5a83f4350b6c1cf699bf2e409717660579ffa71682bc280c4f40166265c03f9546ed4faedc3723e143f1ab0ed7f5990cc3dff0ae
languageName: node
linkType: hard
-"@typescript-eslint/utils@npm:7.10.0":
- version: 7.10.0
- resolution: "@typescript-eslint/utils@npm:7.10.0"
+"@typescript-eslint/utils@npm:7.11.0":
+ version: 7.11.0
+ resolution: "@typescript-eslint/utils@npm:7.11.0"
dependencies:
"@eslint-community/eslint-utils": "npm:^4.4.0"
- "@typescript-eslint/scope-manager": "npm:7.10.0"
- "@typescript-eslint/types": "npm:7.10.0"
- "@typescript-eslint/typescript-estree": "npm:7.10.0"
+ "@typescript-eslint/scope-manager": "npm:7.11.0"
+ "@typescript-eslint/types": "npm:7.11.0"
+ "@typescript-eslint/typescript-estree": "npm:7.11.0"
peerDependencies:
eslint: ^8.56.0
- checksum: 10/62327b585295f9c3aa2508aefac639d562b6f7f270a229aa3a2af8dbd055f4a4d230a8facae75a8a53bb8222b0041162072d259add56b541f8bdfda8da36ea5f
+ checksum: 10/fbef14e166a70ccc4527c0731e0338acefa28218d1a018aa3f5b6b1ad9d75c56278d5f20bda97cf77da13e0a67c4f3e579c5b2f1c2e24d676960927921b55851
languageName: node
linkType: hard
-"@typescript-eslint/visitor-keys@npm:7.10.0":
- version: 7.10.0
- resolution: "@typescript-eslint/visitor-keys@npm:7.10.0"
+"@typescript-eslint/visitor-keys@npm:7.11.0":
+ version: 7.11.0
+ resolution: "@typescript-eslint/visitor-keys@npm:7.11.0"
dependencies:
- "@typescript-eslint/types": "npm:7.10.0"
+ "@typescript-eslint/types": "npm:7.11.0"
eslint-visitor-keys: "npm:^3.4.3"
- checksum: 10/44b555a075bdff38e3e13c454ceaac50aa2546635e81f907d1ea84822c8887487d1d6bb4ff690f627da9585dc19ad07e228847c162c30bb06c46fb119899d8cc
+ checksum: 10/1f2cf1214638e9e78e052393c9e24295196ec4781b05951659a3997e33f8699a760ea3705c17d770e10eda2067435199e0136ab09e5fac63869e22f2da184d89
languageName: node
linkType: hard
@@ -368,8 +368,8 @@ __metadata:
version: 0.0.0-use.local
resolution: "altinn-studio-internal-stats@workspace:."
dependencies:
- "@typescript-eslint/eslint-plugin": "npm:7.10.0"
- "@typescript-eslint/parser": "npm:7.10.0"
+ "@typescript-eslint/eslint-plugin": "npm:7.11.0"
+ "@typescript-eslint/parser": "npm:7.11.0"
axios: "npm:1.7.2"
eslint: "npm:8.57.0"
eslint-config-prettier: "npm:9.1.0"
@@ -377,7 +377,7 @@ __metadata:
eslint-plugin-prettier: "npm:5.1.3"
glob: "npm:10.4.1"
husky: "npm:9.0.11"
- lint-staged: "npm:15.2.4"
+ lint-staged: "npm:15.2.5"
prettier: "npm:^3.0.3"
ts-node: "npm:^10.9.1"
tsconfig-paths: "npm:^4.2.0"
@@ -607,13 +607,6 @@ __metadata:
languageName: node
linkType: hard
-"chalk@npm:5.3.0":
- version: 5.3.0
- resolution: "chalk@npm:5.3.0"
- checksum: 10/6373caaab21bd64c405bfc4bd9672b145647fc9482657b5ea1d549b3b2765054e9d3d928870cdf764fb4aad67555f5061538ff247b8310f110c5c888d92397ea
- languageName: node
- linkType: hard
-
"chalk@npm:^4.0.0":
version: 4.1.2
resolution: "chalk@npm:4.1.2"
@@ -624,6 +617,13 @@ __metadata:
languageName: node
linkType: hard
+"chalk@npm:~5.3.0":
+ version: 5.3.0
+ resolution: "chalk@npm:5.3.0"
+ checksum: 10/6373caaab21bd64c405bfc4bd9672b145647fc9482657b5ea1d549b3b2765054e9d3d928870cdf764fb4aad67555f5061538ff247b8310f110c5c888d92397ea
+ languageName: node
+ linkType: hard
+
"cli-cursor@npm:^4.0.0":
version: 4.0.0
resolution: "cli-cursor@npm:4.0.0"
@@ -675,7 +675,7 @@ __metadata:
languageName: node
linkType: hard
-"commander@npm:12.1.0":
+"commander@npm:~12.1.0":
version: 12.1.0
resolution: "commander@npm:12.1.0"
checksum: 10/cdaeb672d979816853a4eed7f1310a9319e8b976172485c2a6b437ed0db0a389a44cfb222bfbde772781efa9f215bdd1b936f80d6b249485b465c6cb906e1f93
@@ -707,7 +707,16 @@ __metadata:
languageName: node
linkType: hard
-"debug@npm:4.3.4, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4":
+"debug@npm:^3.2.7":
+ version: 3.2.7
+ resolution: "debug@npm:3.2.7"
+ dependencies:
+ ms: "npm:^2.1.1"
+ checksum: 10/d86fd7be2b85462297ea16f1934dc219335e802f629ca9a69b63ed8ed041dda492389bb2ee039217c02e5b54792b1c51aa96ae954cf28634d363a2360c7a1639
+ languageName: node
+ linkType: hard
+
+"debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4":
version: 4.3.4
resolution: "debug@npm:4.3.4"
dependencies:
@@ -719,12 +728,15 @@ __metadata:
languageName: node
linkType: hard
-"debug@npm:^3.2.7":
- version: 3.2.7
- resolution: "debug@npm:3.2.7"
+"debug@npm:~4.3.4":
+ version: 4.3.5
+ resolution: "debug@npm:4.3.5"
dependencies:
- ms: "npm:^2.1.1"
- checksum: 10/d86fd7be2b85462297ea16f1934dc219335e802f629ca9a69b63ed8ed041dda492389bb2ee039217c02e5b54792b1c51aa96ae954cf28634d363a2360c7a1639
+ ms: "npm:2.1.2"
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+ checksum: 10/cb6eab424c410e07813ca1392888589972ce9a32b8829c6508f5e1f25f3c3e70a76731610ae55b4bbe58d1a2fffa1424b30e97fa8d394e49cd2656a9643aedd2
languageName: node
linkType: hard
@@ -1107,7 +1119,7 @@ __metadata:
languageName: node
linkType: hard
-"execa@npm:8.0.1":
+"execa@npm:~8.0.1":
version: 8.0.1
resolution: "execa@npm:8.0.1"
dependencies:
@@ -1847,34 +1859,34 @@ __metadata:
languageName: node
linkType: hard
-"lilconfig@npm:3.1.1":
+"lilconfig@npm:~3.1.1":
version: 3.1.1
resolution: "lilconfig@npm:3.1.1"
checksum: 10/c80fbf98ae7d1daf435e16a83fe3c63743b9d92804cac6dc53ee081c7c265663645c3162d8a0d04ff1874f9c07df145519743317dee67843234c6ed279300f83
languageName: node
linkType: hard
-"lint-staged@npm:15.2.4":
- version: 15.2.4
- resolution: "lint-staged@npm:15.2.4"
+"lint-staged@npm:15.2.5":
+ version: 15.2.5
+ resolution: "lint-staged@npm:15.2.5"
dependencies:
- chalk: "npm:5.3.0"
- commander: "npm:12.1.0"
- debug: "npm:4.3.4"
- execa: "npm:8.0.1"
- lilconfig: "npm:3.1.1"
- listr2: "npm:8.2.1"
- micromatch: "npm:4.0.6"
- pidtree: "npm:0.6.0"
- string-argv: "npm:0.3.2"
- yaml: "npm:2.4.2"
+ chalk: "npm:~5.3.0"
+ commander: "npm:~12.1.0"
+ debug: "npm:~4.3.4"
+ execa: "npm:~8.0.1"
+ lilconfig: "npm:~3.1.1"
+ listr2: "npm:~8.2.1"
+ micromatch: "npm:~4.0.7"
+ pidtree: "npm:~0.6.0"
+ string-argv: "npm:~0.3.2"
+ yaml: "npm:~2.4.2"
bin:
lint-staged: bin/lint-staged.js
- checksum: 10/98df6520e71d395917208bf953fd2e39f58bd47eebd3889bd44739017643d02221c4e7068e3f5a8e2d0cd32c233a846227c9bb29ec7e02cbcc7b76650e5ec4d6
+ checksum: 10/2cb8e14e532a4de0a338da44dc5e22f94581390b988ba3d345d1132d592d9ce50be50846c9a9d25eaffaf3f1f634e0a056598f6abb705269ac21d0fd3bad3a45
languageName: node
linkType: hard
-"listr2@npm:8.2.1":
+"listr2@npm:~8.2.1":
version: 8.2.1
resolution: "listr2@npm:8.2.1"
dependencies:
@@ -1954,16 +1966,6 @@ __metadata:
languageName: node
linkType: hard
-"micromatch@npm:4.0.6":
- version: 4.0.6
- resolution: "micromatch@npm:4.0.6"
- dependencies:
- braces: "npm:^3.0.3"
- picomatch: "npm:^4.0.2"
- checksum: 10/ed95dc8d00dbe3795a0daf0f19f411714f4b39d888824f7b460f4bdbd8952b06628c64704c05b319aca27e2418628a6286b5595890ce9d0c53e5ae962435c83f
- languageName: node
- linkType: hard
-
"micromatch@npm:^4.0.4":
version: 4.0.5
resolution: "micromatch@npm:4.0.5"
@@ -1974,6 +1976,16 @@ __metadata:
languageName: node
linkType: hard
+"micromatch@npm:~4.0.7":
+ version: 4.0.7
+ resolution: "micromatch@npm:4.0.7"
+ dependencies:
+ braces: "npm:^3.0.3"
+ picomatch: "npm:^2.3.1"
+ checksum: 10/a11ed1cb67dcbbe9a5fc02c4062cf8bb0157d73bf86956003af8dcfdf9b287f9e15ec0f6d6925ff6b8b5b496202335e497b01de4d95ef6cf06411bc5e5c474a0
+ languageName: node
+ linkType: hard
+
"mime-db@npm:1.52.0":
version: 1.52.0
resolution: "mime-db@npm:1.52.0"
@@ -2260,14 +2272,7 @@ __metadata:
languageName: node
linkType: hard
-"picomatch@npm:^4.0.2":
- version: 4.0.2
- resolution: "picomatch@npm:4.0.2"
- checksum: 10/ce617b8da36797d09c0baacb96ca8a44460452c89362d7cb8f70ca46b4158ba8bc3606912de7c818eb4a939f7f9015cef3c766ec8a0c6bfc725fdc078e39c717
- languageName: node
- linkType: hard
-
-"pidtree@npm:0.6.0":
+"pidtree@npm:~0.6.0":
version: 0.6.0
resolution: "pidtree@npm:0.6.0"
bin:
@@ -2544,7 +2549,7 @@ __metadata:
languageName: node
linkType: hard
-"string-argv@npm:0.3.2":
+"string-argv@npm:~0.3.2":
version: 0.3.2
resolution: "string-argv@npm:0.3.2"
checksum: 10/f9d3addf887026b4b5f997a271149e93bf71efc8692e7dc0816e8807f960b18bcb9787b45beedf0f97ff459575ee389af3f189d8b649834cac602f2e857e75af
@@ -2977,12 +2982,12 @@ __metadata:
languageName: node
linkType: hard
-"yaml@npm:2.4.2":
- version: 2.4.2
- resolution: "yaml@npm:2.4.2"
+"yaml@npm:~2.4.2":
+ version: 2.4.3
+ resolution: "yaml@npm:2.4.3"
bin:
yaml: bin.mjs
- checksum: 10/6eafbcd68dead734035f6f72af21bd820c29214caf7d8e40c595671a3c908535cef8092b9660a1c055c5833aa148aa640e0c5fa4adb5af2dacd6d28296ccd81c
+ checksum: 10/a618d3b968e3fb601cf7266db6e250e5cdd3b81853039a59108145202d5055b47c2d23a8e1ab661f8ba3ba095dcf6b4bb55cea2c14b97a418e5b059d27f8814e
languageName: node
linkType: hard
diff --git a/frontend/testing/cypress/package.json b/frontend/testing/cypress/package.json
index 0d70806baee..328696cc46a 100644
--- a/frontend/testing/cypress/package.json
+++ b/frontend/testing/cypress/package.json
@@ -4,7 +4,7 @@
"version": "1.0.0",
"devDependencies": {
"@faker-js/faker": "8.4.1",
- "@testing-library/cypress": "10.0.1",
+ "@testing-library/cypress": "10.0.2",
"axe-core": "4.9.1",
"cypress": "13.10.0",
"cypress-axe": "1.5.0",
diff --git a/package.json b/package.json
index d3a1622a0cd..5785a4f4e90 100644
--- a/package.json
+++ b/package.json
@@ -8,9 +8,9 @@
"@microsoft/applicationinsights-react-js": "17.2.0",
"@microsoft/applicationinsights-web": "3.2.1",
"@microsoft/signalr": "8.0.0",
- "@tanstack/react-query": "5.37.1",
- "@tanstack/react-query-devtools": "5.37.1",
- "ajv": "8.13.0",
+ "@tanstack/react-query": "5.40.0",
+ "@tanstack/react-query-devtools": "5.40.0",
+ "ajv": "8.14.0",
"ajv-formats": "3.0.1",
"react-error-boundary": "4.0.13",
"react-i18next": "14.1.2",
@@ -23,7 +23,7 @@
"@redux-saga/is": "1.1.3",
"@redux-saga/symbols": "1.1.3",
"@svgr/webpack": "8.1.0",
- "@swc/core": "1.5.7",
+ "@swc/core": "1.5.24",
"@swc/jest": "0.2.36",
"@testing-library/dom": "10.1.0",
"@testing-library/jest-dom": "6.4.5",
@@ -36,8 +36,8 @@
"@types/react-dom": "18.3.0",
"@types/react-redux": "7.1.33",
"@types/redux-mock-store": "1.0.6",
- "@typescript-eslint/eslint-plugin": "7.10.0",
- "@typescript-eslint/parser": "7.10.0",
+ "@typescript-eslint/eslint-plugin": "7.11.0",
+ "@typescript-eslint/parser": "7.11.0",
"clean-webpack-plugin": "4.0.0",
"cross-env": "7.0.3",
"css-loader": "6.11.0",
@@ -47,7 +47,7 @@
"eslint-plugin-import": "2.29.1",
"eslint-plugin-jsx-a11y": "6.8.0",
"eslint-plugin-prettier": "5.1.3",
- "eslint-plugin-react": "7.34.1",
+ "eslint-plugin-react": "7.34.2",
"eslint-plugin-react-hooks": "4.6.2",
"eslint-plugin-testing-library": "6.2.2",
"glob": "10.4.1",
@@ -57,7 +57,7 @@
"jest-environment-jsdom": "29.7.0",
"jest-fail-on-console": "3.3.0",
"jest-junit": "16.0.0",
- "lint-staged": "15.2.4",
+ "lint-staged": "15.2.5",
"mini-css-extract-plugin": "2.9.0",
"prettier": "3.2.5",
"react": "18.3.1",
diff --git a/yarn.lock b/yarn.lock
index a0e51d613bd..d515e746129 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -97,7 +97,7 @@ __metadata:
jest: "npm:29.7.0"
jsonpointer: "npm:5.0.1"
peerDependencies:
- "@mui/material": 5.15.18
+ "@mui/material": 5.15.19
axios: 1.7.2
classnames: 2.5.1
react: 18.3.1
@@ -133,7 +133,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "@altinn/ux-editor-v3@workspace:frontend/packages/ux-editor-v3"
dependencies:
- "@mui/material": "npm:5.15.18"
+ "@mui/material": "npm:5.15.19"
"@redux-devtools/extension": "npm:3.3.0"
"@reduxjs/toolkit": "npm:1.9.7"
"@studio/icons": "workspace:^"
@@ -160,7 +160,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "@altinn/ux-editor@workspace:frontend/packages/ux-editor"
dependencies:
- "@mui/material": "npm:5.15.18"
+ "@mui/material": "npm:5.15.19"
"@studio/icons": "workspace:^"
axios: "npm:1.7.2"
classnames: "npm:2.5.1"
@@ -3336,16 +3336,16 @@ __metadata:
languageName: node
linkType: hard
-"@chromatic-com/storybook@npm:1.4.0":
- version: 1.4.0
- resolution: "@chromatic-com/storybook@npm:1.4.0"
+"@chromatic-com/storybook@npm:1.5.0":
+ version: 1.5.0
+ resolution: "@chromatic-com/storybook@npm:1.5.0"
dependencies:
- chromatic: "npm:^11.3.2"
+ chromatic: "npm:^11.4.0"
filesize: "npm:^10.0.12"
jsonfile: "npm:^6.1.0"
react-confetti: "npm:^6.1.0"
strip-ansi: "npm:^7.1.0"
- checksum: 10/296d11641dca1ac94293705746c70a292d5258b9befad99613ea99e64149f2b8ee188ba25937000b4aeceaa6ac7c6ea5db65196357b90c0fad6da5afd3d8b98b
+ checksum: 10/06ca51df331062300e77b80e970a2ff1df47da03d1414ed211413c2e493656d9f13c141834157386d557550a1e2ff887f46f7d0519bb25d77ddd353825b8c256
languageName: node
linkType: hard
@@ -4896,20 +4896,20 @@ __metadata:
languageName: node
linkType: hard
-"@mui/core-downloads-tracker@npm:^5.15.18":
- version: 5.15.18
- resolution: "@mui/core-downloads-tracker@npm:5.15.18"
- checksum: 10/d6f5d5bc70b7ace16b1fa86adf5f5bc175840fd21d69f230b8b2348fab4863940df030ace0c6b76beb57bd8bfa6a460788cbd86be90772c13684d87d409d8d32
+"@mui/core-downloads-tracker@npm:^5.15.19":
+ version: 5.15.19
+ resolution: "@mui/core-downloads-tracker@npm:5.15.19"
+ checksum: 10/32dd442d72a4cf4abea0e5c0a325707c3f8aba16b7b40ed674da2c068ed10d686f1941240e527407d685e00ed12931c331d99265e1ed570630c856ffbe291c23
languageName: node
linkType: hard
-"@mui/material@npm:5.15.18":
- version: 5.15.18
- resolution: "@mui/material@npm:5.15.18"
+"@mui/material@npm:5.15.19":
+ version: 5.15.19
+ resolution: "@mui/material@npm:5.15.19"
dependencies:
"@babel/runtime": "npm:^7.23.9"
"@mui/base": "npm:5.0.0-beta.40"
- "@mui/core-downloads-tracker": "npm:^5.15.18"
+ "@mui/core-downloads-tracker": "npm:^5.15.19"
"@mui/system": "npm:^5.15.15"
"@mui/types": "npm:^7.2.14"
"@mui/utils": "npm:^5.15.14"
@@ -4932,7 +4932,7 @@ __metadata:
optional: true
"@types/react":
optional: true
- checksum: 10/5c52261090eb91e9ba6d3e7d791b8b8be9c90af0e27f8fcace24b9788952bd03ab18c9736760fe72edf17e9d67fcc32c71f9b132c7d534919a120d59c84f568c
+ checksum: 10/92618aaefc85b4d4a6012dba48fe4b4936db45c1afd3436b148c81d8b8d0a001c57fd654bd101f94077979f1cf4e3ad5a7e5dd24a6f1b666f3d2e23d75a63f84
languageName: node
linkType: hard
@@ -5960,60 +5960,60 @@ __metadata:
languageName: node
linkType: hard
-"@storybook/addon-actions@npm:8.1.3":
- version: 8.1.3
- resolution: "@storybook/addon-actions@npm:8.1.3"
+"@storybook/addon-actions@npm:8.1.5":
+ version: 8.1.5
+ resolution: "@storybook/addon-actions@npm:8.1.5"
dependencies:
- "@storybook/core-events": "npm:8.1.3"
+ "@storybook/core-events": "npm:8.1.5"
"@storybook/global": "npm:^5.0.0"
"@types/uuid": "npm:^9.0.1"
dequal: "npm:^2.0.2"
polished: "npm:^4.2.2"
uuid: "npm:^9.0.0"
- checksum: 10/553dabea313e1a377b9ab9905d9427f9f2acc80ebfbe87b4ccb97407781c58bb5094e352db8594cbd2d3fad78ed4196dc7cab4cd9c4e3761d0d8e0bc9e46eefa
+ checksum: 10/982bcf831ac1cc59d2771cb29a0970481a0ce8e70267ce4192733a8f0b81b2c0bc9e2b7d37b6227d1d5dc852a6869db1162f2c51db7cad9d2472bc69d86059ac
languageName: node
linkType: hard
-"@storybook/addon-backgrounds@npm:8.1.3":
- version: 8.1.3
- resolution: "@storybook/addon-backgrounds@npm:8.1.3"
+"@storybook/addon-backgrounds@npm:8.1.5":
+ version: 8.1.5
+ resolution: "@storybook/addon-backgrounds@npm:8.1.5"
dependencies:
"@storybook/global": "npm:^5.0.0"
memoizerific: "npm:^1.11.3"
ts-dedent: "npm:^2.0.0"
- checksum: 10/f5e88afbdac4ad3dd065093c1c872cf9ae15580e98236bd9e71e3eb5574771a77fdd398c6d9455c333c56ea03a443b897b7c28eee80050d9f08ac9de7c550843
+ checksum: 10/5e8ee61d735b7c4083306eb7e9ab30a3f8ee4b9be1faa4068a16d3001b2a7decca02bb64a39c6cf156d35625447fb8fe3e47c6fd360d1d6c46424a67d84512f3
languageName: node
linkType: hard
-"@storybook/addon-controls@npm:8.1.3":
- version: 8.1.3
- resolution: "@storybook/addon-controls@npm:8.1.3"
+"@storybook/addon-controls@npm:8.1.5":
+ version: 8.1.5
+ resolution: "@storybook/addon-controls@npm:8.1.5"
dependencies:
- "@storybook/blocks": "npm:8.1.3"
+ "@storybook/blocks": "npm:8.1.5"
dequal: "npm:^2.0.2"
lodash: "npm:^4.17.21"
ts-dedent: "npm:^2.0.0"
- checksum: 10/9afad63df40509f5e0c6cb25a8c98165adfe79fbbaefc21cdaafffd3f192d21c5dd59d567d0e7f1713907e7c519ecc03385622bbad69c8ca0fb8bb367597d64c
+ checksum: 10/4e69e053a07c387e0f2ef4b3e5afef58088c729431939855d25b78cba3536b7b102baeaa360f0db11bf042599cb25963aeff06caa2803c0a7b93a3c85cd88c69
languageName: node
linkType: hard
-"@storybook/addon-docs@npm:8.1.3":
- version: 8.1.3
- resolution: "@storybook/addon-docs@npm:8.1.3"
+"@storybook/addon-docs@npm:8.1.5":
+ version: 8.1.5
+ resolution: "@storybook/addon-docs@npm:8.1.5"
dependencies:
"@babel/core": "npm:^7.24.4"
"@mdx-js/react": "npm:^3.0.0"
- "@storybook/blocks": "npm:8.1.3"
- "@storybook/client-logger": "npm:8.1.3"
- "@storybook/components": "npm:8.1.3"
- "@storybook/csf-plugin": "npm:8.1.3"
- "@storybook/csf-tools": "npm:8.1.3"
+ "@storybook/blocks": "npm:8.1.5"
+ "@storybook/client-logger": "npm:8.1.5"
+ "@storybook/components": "npm:8.1.5"
+ "@storybook/csf-plugin": "npm:8.1.5"
+ "@storybook/csf-tools": "npm:8.1.5"
"@storybook/global": "npm:^5.0.0"
- "@storybook/node-logger": "npm:8.1.3"
- "@storybook/preview-api": "npm:8.1.3"
- "@storybook/react-dom-shim": "npm:8.1.3"
- "@storybook/theming": "npm:8.1.3"
- "@storybook/types": "npm:8.1.3"
+ "@storybook/node-logger": "npm:8.1.5"
+ "@storybook/preview-api": "npm:8.1.5"
+ "@storybook/react-dom-shim": "npm:8.1.5"
+ "@storybook/theming": "npm:8.1.5"
+ "@storybook/types": "npm:8.1.5"
"@types/react": "npm:^16.8.0 || ^17.0.0 || ^18.0.0"
fs-extra: "npm:^11.1.0"
react: "npm:^16.8.0 || ^17.0.0 || ^18.0.0"
@@ -6021,58 +6021,58 @@ __metadata:
rehype-external-links: "npm:^3.0.0"
rehype-slug: "npm:^6.0.0"
ts-dedent: "npm:^2.0.0"
- checksum: 10/76af531db044401c3cbfe61320699540a3cd6f6a101fa9aa79862ed54d48bf1a4431c209cf7675f2dcec7585fb3ed3dab372997598416c8338c3b5ea9af3c058
+ checksum: 10/1df8def08e1b470c0f1e0dd0ae029167771146411ee9f75a2aa76ac5c256aaa25c465d9f5fe6148d85c4e8c1a00d69372d136222e5742f311f4439f99223e373
languageName: node
linkType: hard
"@storybook/addon-essentials@npm:^8.0.4":
- version: 8.1.3
- resolution: "@storybook/addon-essentials@npm:8.1.3"
- dependencies:
- "@storybook/addon-actions": "npm:8.1.3"
- "@storybook/addon-backgrounds": "npm:8.1.3"
- "@storybook/addon-controls": "npm:8.1.3"
- "@storybook/addon-docs": "npm:8.1.3"
- "@storybook/addon-highlight": "npm:8.1.3"
- "@storybook/addon-measure": "npm:8.1.3"
- "@storybook/addon-outline": "npm:8.1.3"
- "@storybook/addon-toolbars": "npm:8.1.3"
- "@storybook/addon-viewport": "npm:8.1.3"
- "@storybook/core-common": "npm:8.1.3"
- "@storybook/manager-api": "npm:8.1.3"
- "@storybook/node-logger": "npm:8.1.3"
- "@storybook/preview-api": "npm:8.1.3"
+ version: 8.1.5
+ resolution: "@storybook/addon-essentials@npm:8.1.5"
+ dependencies:
+ "@storybook/addon-actions": "npm:8.1.5"
+ "@storybook/addon-backgrounds": "npm:8.1.5"
+ "@storybook/addon-controls": "npm:8.1.5"
+ "@storybook/addon-docs": "npm:8.1.5"
+ "@storybook/addon-highlight": "npm:8.1.5"
+ "@storybook/addon-measure": "npm:8.1.5"
+ "@storybook/addon-outline": "npm:8.1.5"
+ "@storybook/addon-toolbars": "npm:8.1.5"
+ "@storybook/addon-viewport": "npm:8.1.5"
+ "@storybook/core-common": "npm:8.1.5"
+ "@storybook/manager-api": "npm:8.1.5"
+ "@storybook/node-logger": "npm:8.1.5"
+ "@storybook/preview-api": "npm:8.1.5"
ts-dedent: "npm:^2.0.0"
- checksum: 10/62ae2cb466c0d365500921c08c78f00f002f015851e2cc6d4a7ca3cc44339319a35a40f7155482ca5fa832aecaf5eb1bf209e0a79024be3465e98280cefe947b
+ checksum: 10/ed41297cc76f80629c36ea0ba2614a9307ca7e84b1046db128bbbbaaf07cd540aa67f61060a4470b4de3a675f9659bfb090cfc738b7b43f89d2a038c17f38181
languageName: node
linkType: hard
-"@storybook/addon-highlight@npm:8.1.3":
- version: 8.1.3
- resolution: "@storybook/addon-highlight@npm:8.1.3"
+"@storybook/addon-highlight@npm:8.1.5":
+ version: 8.1.5
+ resolution: "@storybook/addon-highlight@npm:8.1.5"
dependencies:
"@storybook/global": "npm:^5.0.0"
- checksum: 10/d20705aa5c8a8b876e5393faf740dafe4069a12a5ac35fa43240ef73e1e3410d08f5d06bedc3da08ae4ca3a3bd7d64e0baf299df460addff004947acd4043d63
+ checksum: 10/ac5846a12ef3e5f830e51f2dbddd5e946d03dcac72594659b369e155b50df7cfcb2554d38e60944994d7a85f5e7fb5c00a1d2b3d0196b0779d83d3adf048cbcc
languageName: node
linkType: hard
"@storybook/addon-interactions@npm:^8.0.4":
- version: 8.1.3
- resolution: "@storybook/addon-interactions@npm:8.1.3"
+ version: 8.1.5
+ resolution: "@storybook/addon-interactions@npm:8.1.5"
dependencies:
"@storybook/global": "npm:^5.0.0"
- "@storybook/instrumenter": "npm:8.1.3"
- "@storybook/test": "npm:8.1.3"
- "@storybook/types": "npm:8.1.3"
+ "@storybook/instrumenter": "npm:8.1.5"
+ "@storybook/test": "npm:8.1.5"
+ "@storybook/types": "npm:8.1.5"
polished: "npm:^4.2.2"
ts-dedent: "npm:^2.2.0"
- checksum: 10/3f9a0b5cbf4250b1f20aa16fc4e39aab58b6f5a2c2d16e758e5e59e6fd3fe9e2f27494c5d8ba63048d1e5a8d39b3e8a4ab285a73bd333fa29cd620526c7d805e
+ checksum: 10/38bedd694affff7e7d83f7eff2c9f0032a2bbdf862338e690b2ff31494456bcf7bf02e5cd8c4fa5ef98a6bef6aa9adfde501af1bbc3732d17a45b4923ac6e9d9
languageName: node
linkType: hard
"@storybook/addon-links@npm:^8.0.4":
- version: 8.1.3
- resolution: "@storybook/addon-links@npm:8.1.3"
+ version: 8.1.5
+ resolution: "@storybook/addon-links@npm:8.1.5"
dependencies:
"@storybook/csf": "npm:^0.1.7"
"@storybook/global": "npm:^5.0.0"
@@ -6082,81 +6082,81 @@ __metadata:
peerDependenciesMeta:
react:
optional: true
- checksum: 10/062016c865f163a82fccec97d132ac6fd4ce6d11e738b1b990226fb553664af5ccf372fd25340b3fb2b87fb2ba2363df8f7b1b6971772e2b78b40b9f7aaee1d7
+ checksum: 10/47c08bfd6e540697d9f97d22e771a7b9755095d664f560e5656be6f4b014502eaf3f6bd7bbc2b9fa1f56bb7f6513594082181ecd01de972d47e3d7d1b9ba680b
languageName: node
linkType: hard
-"@storybook/addon-measure@npm:8.1.3":
- version: 8.1.3
- resolution: "@storybook/addon-measure@npm:8.1.3"
+"@storybook/addon-measure@npm:8.1.5":
+ version: 8.1.5
+ resolution: "@storybook/addon-measure@npm:8.1.5"
dependencies:
"@storybook/global": "npm:^5.0.0"
tiny-invariant: "npm:^1.3.1"
- checksum: 10/7ab6dd70fae5f4e657f09003ea0ecbf5869440b97bf7c45ce0f2a1729732b245119457273a6027ae334342345d2b5b91db19a9c59cf08109d857b26476df30af
+ checksum: 10/2ab5fe69743944dd0f13324814ce6f329a4bf1dae4a93b85d02665c3b83f3a24b5c33cbdb51ec581dc5a9edc51832fe1cc9bc0b463dacb292c48398943963bfb
languageName: node
linkType: hard
"@storybook/addon-onboarding@npm:^8.0.4":
- version: 8.1.3
- resolution: "@storybook/addon-onboarding@npm:8.1.3"
+ version: 8.1.5
+ resolution: "@storybook/addon-onboarding@npm:8.1.5"
dependencies:
react-confetti: "npm:^6.1.0"
- checksum: 10/11f8bfc593c5ff5c5360a10335ea70e451ba0104b629ebb2d919dedcdf7f691968c83c09118c5b9d437d52c22cb5c0af841f24228cd3a310e310341be0621743
+ checksum: 10/e33aa18a2d0e27b40a003b28ea55c353d02870043acba1314f820613565b3a8e2763979bb2f2d5ac266a7746d92982737cf2ef391536a050e62da1f5cdd8beb5
languageName: node
linkType: hard
-"@storybook/addon-outline@npm:8.1.3":
- version: 8.1.3
- resolution: "@storybook/addon-outline@npm:8.1.3"
+"@storybook/addon-outline@npm:8.1.5":
+ version: 8.1.5
+ resolution: "@storybook/addon-outline@npm:8.1.5"
dependencies:
"@storybook/global": "npm:^5.0.0"
ts-dedent: "npm:^2.0.0"
- checksum: 10/d1bd9a45ce7effce2ac515f6899371f875673c10a33577674bc9e1be8d0e8dae1f35d5f6466fd20302e933aac1a93daa788b8468d5b0ce60a23c415b804f6abc
+ checksum: 10/a676f3816b4524a688d2d5196fbf07335042deb116cc5f093886f95a468f75374ae387c05f75356f6408df36a4471e3d1eefa477c5b6184e31f4b3df85f31266
languageName: node
linkType: hard
-"@storybook/addon-toolbars@npm:8.1.3":
- version: 8.1.3
- resolution: "@storybook/addon-toolbars@npm:8.1.3"
- checksum: 10/1c41b75866461046033df2512733703c67abff9e1f2882cda50678c211e3791347cbb62f9b6c9ee6d74ade6c6d82bc37abcd704656f08ade982e267591c834ed
+"@storybook/addon-toolbars@npm:8.1.5":
+ version: 8.1.5
+ resolution: "@storybook/addon-toolbars@npm:8.1.5"
+ checksum: 10/18eb96df77b8250833dbfe7c5ff4eb0610ac823370b6dc8c520e3a83c8ac753aaec740eab7018d996ae0d1cd416fc3e603fdfe38a2e1444d6edc1488f797c5d9
languageName: node
linkType: hard
-"@storybook/addon-viewport@npm:8.1.3":
- version: 8.1.3
- resolution: "@storybook/addon-viewport@npm:8.1.3"
+"@storybook/addon-viewport@npm:8.1.5":
+ version: 8.1.5
+ resolution: "@storybook/addon-viewport@npm:8.1.5"
dependencies:
memoizerific: "npm:^1.11.3"
- checksum: 10/92a3b9a0c828cd0de39b6762df4884fbba246768d67ff2da84d0a4458789f2da8f230be073ee0f52e1e77fda0c59d53b57a5b726f3b2bc9feb97b2e6d7e7b4b3
+ checksum: 10/ca78400d65c8e3ec35fc37e918db13337123c5588d498e312b9f2394cb73541dcbe5e29807fad54dd593051979d394ec1e65502fb8d17872889c4458aa9b899c
languageName: node
linkType: hard
-"@storybook/addon-webpack5-compiler-swc@npm:1.0.2":
- version: 1.0.2
- resolution: "@storybook/addon-webpack5-compiler-swc@npm:1.0.2"
+"@storybook/addon-webpack5-compiler-swc@npm:1.0.3":
+ version: 1.0.3
+ resolution: "@storybook/addon-webpack5-compiler-swc@npm:1.0.3"
dependencies:
- "@swc/core": "npm:^1.3.102"
+ "@swc/core": "npm:1.5.7"
swc-loader: "npm:^0.2.3"
- checksum: 10/890b321888b844326b72cdb98b6627dcefe2fa3c0314816dd31b93fbc7a6ec6a3e3db7fb44505bef1155994874f2c47d1d75fd4c22a29a233d110852577ccc62
+ checksum: 10/21d5fbf6021a1cb1c7a0c6b0d5d17a475d4afd29610fa1c43e2b21a015cee4446b9711c991812eca17e5ff78c3e9d402762d93584e5fecf2de303f261fb0db59
languageName: node
linkType: hard
-"@storybook/blocks@npm:8.1.3, @storybook/blocks@npm:^8.0.4":
- version: 8.1.3
- resolution: "@storybook/blocks@npm:8.1.3"
+"@storybook/blocks@npm:8.1.5, @storybook/blocks@npm:^8.0.4":
+ version: 8.1.5
+ resolution: "@storybook/blocks@npm:8.1.5"
dependencies:
- "@storybook/channels": "npm:8.1.3"
- "@storybook/client-logger": "npm:8.1.3"
- "@storybook/components": "npm:8.1.3"
- "@storybook/core-events": "npm:8.1.3"
+ "@storybook/channels": "npm:8.1.5"
+ "@storybook/client-logger": "npm:8.1.5"
+ "@storybook/components": "npm:8.1.5"
+ "@storybook/core-events": "npm:8.1.5"
"@storybook/csf": "npm:^0.1.7"
- "@storybook/docs-tools": "npm:8.1.3"
+ "@storybook/docs-tools": "npm:8.1.5"
"@storybook/global": "npm:^5.0.0"
"@storybook/icons": "npm:^1.2.5"
- "@storybook/manager-api": "npm:8.1.3"
- "@storybook/preview-api": "npm:8.1.3"
- "@storybook/theming": "npm:8.1.3"
- "@storybook/types": "npm:8.1.3"
+ "@storybook/manager-api": "npm:8.1.5"
+ "@storybook/preview-api": "npm:8.1.5"
+ "@storybook/theming": "npm:8.1.5"
+ "@storybook/types": "npm:8.1.5"
"@types/lodash": "npm:^4.14.167"
color-convert: "npm:^2.0.1"
dequal: "npm:^2.0.2"
@@ -6177,18 +6177,18 @@ __metadata:
optional: true
react-dom:
optional: true
- checksum: 10/981082993b2640c688e45cdf246072f4848b2fd1452a6558376f550194dce2a4a3092d9006cb6ff3b3601bd892a82de61e696fd3bc7cab01d03f012d37d2570d
+ checksum: 10/4895ae61eb8e89db620c07c7a06c8e52864f5c81dfce49b1f121b7dffe4d58381fe79e29d6637cdf26840b748c2533acc9d378d4b2538abbfac2be861599af30
languageName: node
linkType: hard
-"@storybook/builder-manager@npm:8.1.3":
- version: 8.1.3
- resolution: "@storybook/builder-manager@npm:8.1.3"
+"@storybook/builder-manager@npm:8.1.5":
+ version: 8.1.5
+ resolution: "@storybook/builder-manager@npm:8.1.5"
dependencies:
"@fal-works/esbuild-plugin-global-externals": "npm:^2.1.2"
- "@storybook/core-common": "npm:8.1.3"
- "@storybook/manager": "npm:8.1.3"
- "@storybook/node-logger": "npm:8.1.3"
+ "@storybook/core-common": "npm:8.1.5"
+ "@storybook/manager": "npm:8.1.5"
+ "@storybook/node-logger": "npm:8.1.5"
"@types/ejs": "npm:^3.1.1"
"@yarnpkg/esbuild-plugin-pnp": "npm:^3.0.0-rc.10"
browser-assert: "npm:^1.2.1"
@@ -6199,22 +6199,22 @@ __metadata:
fs-extra: "npm:^11.1.0"
process: "npm:^0.11.10"
util: "npm:^0.12.4"
- checksum: 10/b1f39c8b2b98dd3ae910ebd77d70d01ce269edcb5c09fe6a59fea5fbeae5252d3025383119a6b14f71762a85aa5dc8dac5570f1066d124df7f424550a969ae64
+ checksum: 10/dd064a27f9ae11ce825ae777a36c033ef610b336f82e2e03d80b569b6fd5930cd2259dfe87f1abdcd3fc46a02bb111fab36a16fa28973d8e1e89dbaf62411938
languageName: node
linkType: hard
-"@storybook/builder-webpack5@npm:8.1.3":
- version: 8.1.3
- resolution: "@storybook/builder-webpack5@npm:8.1.3"
- dependencies:
- "@storybook/channels": "npm:8.1.3"
- "@storybook/client-logger": "npm:8.1.3"
- "@storybook/core-common": "npm:8.1.3"
- "@storybook/core-events": "npm:8.1.3"
- "@storybook/core-webpack": "npm:8.1.3"
- "@storybook/node-logger": "npm:8.1.3"
- "@storybook/preview": "npm:8.1.3"
- "@storybook/preview-api": "npm:8.1.3"
+"@storybook/builder-webpack5@npm:8.1.5":
+ version: 8.1.5
+ resolution: "@storybook/builder-webpack5@npm:8.1.5"
+ dependencies:
+ "@storybook/channels": "npm:8.1.5"
+ "@storybook/client-logger": "npm:8.1.5"
+ "@storybook/core-common": "npm:8.1.5"
+ "@storybook/core-events": "npm:8.1.5"
+ "@storybook/core-webpack": "npm:8.1.5"
+ "@storybook/node-logger": "npm:8.1.5"
+ "@storybook/preview": "npm:8.1.5"
+ "@storybook/preview-api": "npm:8.1.5"
"@types/node": "npm:^18.0.0"
"@types/semver": "npm:^7.3.4"
browser-assert: "npm:^1.2.1"
@@ -6244,38 +6244,38 @@ __metadata:
peerDependenciesMeta:
typescript:
optional: true
- checksum: 10/6a62d7740be0f3fe3b5c6f79e7d8ed4be0d40cd43d297ed9564beaeabd3697e84da4b48e050fc7a1cf8180c1263e9527b7bd38c646eb83c68666cbea02f28348
+ checksum: 10/3198355c85df8bbc92fc19feda9234c1b538e092d34d501d66c96ec9e95b5091bce6a4651d0971f60a76ab0d7972014f3161a67d0f2550196adfe366f73b050f
languageName: node
linkType: hard
-"@storybook/channels@npm:8.1.3":
- version: 8.1.3
- resolution: "@storybook/channels@npm:8.1.3"
+"@storybook/channels@npm:8.1.5":
+ version: 8.1.5
+ resolution: "@storybook/channels@npm:8.1.5"
dependencies:
- "@storybook/client-logger": "npm:8.1.3"
- "@storybook/core-events": "npm:8.1.3"
+ "@storybook/client-logger": "npm:8.1.5"
+ "@storybook/core-events": "npm:8.1.5"
"@storybook/global": "npm:^5.0.0"
telejson: "npm:^7.2.0"
tiny-invariant: "npm:^1.3.1"
- checksum: 10/c7e0c7765b355499aadfe8b74e9723a38d48e0f1d83dd54baa89c1cc6ec3ccc024c55bfd4d5f9d83d3b37c5e6639823da57f32096f96395d7150ed76cc420b95
+ checksum: 10/a179641ab5dc914f1ca303b4a0812233bb9bc8983543a314250152ece60d9a8a6eec6c6d3d2a29edd4cca1eede84b76714dd9a1c71f5e5dae95f4e2d4cc935d0
languageName: node
linkType: hard
-"@storybook/cli@npm:8.1.3":
- version: 8.1.3
- resolution: "@storybook/cli@npm:8.1.3"
+"@storybook/cli@npm:8.1.5":
+ version: 8.1.5
+ resolution: "@storybook/cli@npm:8.1.5"
dependencies:
"@babel/core": "npm:^7.24.4"
"@babel/types": "npm:^7.24.0"
"@ndelangen/get-tarball": "npm:^3.0.7"
- "@storybook/codemod": "npm:8.1.3"
- "@storybook/core-common": "npm:8.1.3"
- "@storybook/core-events": "npm:8.1.3"
- "@storybook/core-server": "npm:8.1.3"
- "@storybook/csf-tools": "npm:8.1.3"
- "@storybook/node-logger": "npm:8.1.3"
- "@storybook/telemetry": "npm:8.1.3"
- "@storybook/types": "npm:8.1.3"
+ "@storybook/codemod": "npm:8.1.5"
+ "@storybook/core-common": "npm:8.1.5"
+ "@storybook/core-events": "npm:8.1.5"
+ "@storybook/core-server": "npm:8.1.5"
+ "@storybook/csf-tools": "npm:8.1.5"
+ "@storybook/node-logger": "npm:8.1.5"
+ "@storybook/telemetry": "npm:8.1.5"
+ "@storybook/types": "npm:8.1.5"
"@types/semver": "npm:^7.3.4"
"@yarnpkg/fslib": "npm:2.10.3"
"@yarnpkg/libzip": "npm:2.3.0"
@@ -6298,36 +6298,36 @@ __metadata:
read-pkg-up: "npm:^7.0.1"
semver: "npm:^7.3.7"
strip-json-comments: "npm:^3.0.1"
- tempy: "npm:^1.0.1"
+ tempy: "npm:^3.1.0"
tiny-invariant: "npm:^1.3.1"
ts-dedent: "npm:^2.0.0"
bin:
getstorybook: ./bin/index.js
sb: ./bin/index.js
- checksum: 10/5cfdb1846583ef8091991cd3b7ffe13a21861b79dd7aed5c3fc7f412c477727e11896bb05bb93b3bbbef7f01fcd8efb7e03477008e5e782bdd90642c0e2f7d2f
+ checksum: 10/9169976677323f46e6f91641036d45b41f99c70398d6e55665f50c7fe483e057368ea40f5b45fe8be34b3ec5a691d219451bcb02db1a71ecb5e5923d0c6bebd8
languageName: node
linkType: hard
-"@storybook/client-logger@npm:8.1.3":
- version: 8.1.3
- resolution: "@storybook/client-logger@npm:8.1.3"
+"@storybook/client-logger@npm:8.1.5":
+ version: 8.1.5
+ resolution: "@storybook/client-logger@npm:8.1.5"
dependencies:
"@storybook/global": "npm:^5.0.0"
- checksum: 10/c7735d2ef39c07b89faade51fe26a56816eadc61e9b4ab952c087dbbd7e7ef3acfd49d437460d8503cb02d17613a4734267c5a56ccc6f9f2a1b2330b8f5459e5
+ checksum: 10/4d3d35941f7bcf65524d01dcb3416af77863412fee24d095825cf0cff4fa7a91b1d5ef74e6011f881b3ceacbc336f1091defb9f3e66e1e825a9d9de9a47e0dfe
languageName: node
linkType: hard
-"@storybook/codemod@npm:8.1.3":
- version: 8.1.3
- resolution: "@storybook/codemod@npm:8.1.3"
+"@storybook/codemod@npm:8.1.5":
+ version: 8.1.5
+ resolution: "@storybook/codemod@npm:8.1.5"
dependencies:
"@babel/core": "npm:^7.24.4"
"@babel/preset-env": "npm:^7.24.4"
"@babel/types": "npm:^7.24.0"
"@storybook/csf": "npm:^0.1.7"
- "@storybook/csf-tools": "npm:8.1.3"
- "@storybook/node-logger": "npm:8.1.3"
- "@storybook/types": "npm:8.1.3"
+ "@storybook/csf-tools": "npm:8.1.5"
+ "@storybook/node-logger": "npm:8.1.5"
+ "@storybook/types": "npm:8.1.5"
"@types/cross-spawn": "npm:^6.0.2"
cross-spawn: "npm:^7.0.3"
globby: "npm:^14.0.1"
@@ -6336,39 +6336,39 @@ __metadata:
prettier: "npm:^3.1.1"
recast: "npm:^0.23.5"
tiny-invariant: "npm:^1.3.1"
- checksum: 10/eeaf00b01e7acfad0403f2a6d1f955a065a0ad3dfcc836a620ef4322b23df4739bc3bd2318e40331e0ac2ea21f793441e08850c312372302eadd139a8adbf39c
+ checksum: 10/b4eb1aece94148ddff9e2d72c6044ae04406f1d8fd6a61ccff9fb51408368683927e0ee85c2c832fd50da5515072c24c2187028194166d9c74eb1ddd33b02d2b
languageName: node
linkType: hard
-"@storybook/components@npm:8.1.3":
- version: 8.1.3
- resolution: "@storybook/components@npm:8.1.3"
+"@storybook/components@npm:8.1.5":
+ version: 8.1.5
+ resolution: "@storybook/components@npm:8.1.5"
dependencies:
"@radix-ui/react-dialog": "npm:^1.0.5"
"@radix-ui/react-slot": "npm:^1.0.2"
- "@storybook/client-logger": "npm:8.1.3"
+ "@storybook/client-logger": "npm:8.1.5"
"@storybook/csf": "npm:^0.1.7"
"@storybook/global": "npm:^5.0.0"
"@storybook/icons": "npm:^1.2.5"
- "@storybook/theming": "npm:8.1.3"
- "@storybook/types": "npm:8.1.3"
+ "@storybook/theming": "npm:8.1.5"
+ "@storybook/types": "npm:8.1.5"
memoizerific: "npm:^1.11.3"
util-deprecate: "npm:^1.0.2"
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
- checksum: 10/b53b257e43519c8952caf0241ea0adb7a50addcb5b00f92fa01362f7562a27a524b5ab17b702013e610c869612bac8608c1e230f0d33b7cfeb6278d20f0f3896
+ checksum: 10/a5a4dd490b87e142fd7f362d8694baa6d9828713ed25d3476a3f233f294cf66011400045381c6a793e1b98de21e772a95409072e8bc9bb87be2cc7f867b39b86
languageName: node
linkType: hard
-"@storybook/core-common@npm:8.1.3":
- version: 8.1.3
- resolution: "@storybook/core-common@npm:8.1.3"
+"@storybook/core-common@npm:8.1.5":
+ version: 8.1.5
+ resolution: "@storybook/core-common@npm:8.1.5"
dependencies:
- "@storybook/core-events": "npm:8.1.3"
- "@storybook/csf-tools": "npm:8.1.3"
- "@storybook/node-logger": "npm:8.1.3"
- "@storybook/types": "npm:8.1.3"
+ "@storybook/core-events": "npm:8.1.5"
+ "@storybook/csf-tools": "npm:8.1.5"
+ "@storybook/node-logger": "npm:8.1.5"
+ "@storybook/types": "npm:8.1.5"
"@yarnpkg/fslib": "npm:2.10.3"
"@yarnpkg/libzip": "npm:2.3.0"
chalk: "npm:^4.1.0"
@@ -6390,7 +6390,7 @@ __metadata:
pretty-hrtime: "npm:^1.0.3"
resolve-from: "npm:^5.0.0"
semver: "npm:^7.3.7"
- tempy: "npm:^1.0.1"
+ tempy: "npm:^3.1.0"
tiny-invariant: "npm:^1.3.1"
ts-dedent: "npm:^2.0.0"
util: "npm:^0.12.4"
@@ -6399,42 +6399,42 @@ __metadata:
peerDependenciesMeta:
prettier:
optional: true
- checksum: 10/29cd5c88715b5967d59a06836dfc751a921d4d5075c9bcd99fc2f9972f77130e08a5f671224da1bc93433dc10813fbdeaa44294575ce8da4e45750bbf9ffbc11
+ checksum: 10/458c8f6c697101a4853a713e45e75fe7e91fc8d294ed762f5179e4ea04d278a6fde53d53dcbf9b7259e133a677a05b3c349f266ee3641e007358d78beff1fb9a
languageName: node
linkType: hard
-"@storybook/core-events@npm:8.1.3":
- version: 8.1.3
- resolution: "@storybook/core-events@npm:8.1.3"
+"@storybook/core-events@npm:8.1.5":
+ version: 8.1.5
+ resolution: "@storybook/core-events@npm:8.1.5"
dependencies:
"@storybook/csf": "npm:^0.1.7"
ts-dedent: "npm:^2.0.0"
- checksum: 10/dc4cf5a7d0f36fff14ce7e9630e44f826ea748700753cd53be64a7d7b0d3e99b227224eb72267a92baf533823579bd51a9b6999926736e0067b437ca1d0452ed
+ checksum: 10/628f5b31b25361ca0ebdf530230f355d822251339e8656c8aed92e9a3f71de6f606216f7fc92da27d76af9ddf67ff1b97467c32710867ba49b228f2233a87d1b
languageName: node
linkType: hard
-"@storybook/core-server@npm:8.1.3":
- version: 8.1.3
- resolution: "@storybook/core-server@npm:8.1.3"
+"@storybook/core-server@npm:8.1.5":
+ version: 8.1.5
+ resolution: "@storybook/core-server@npm:8.1.5"
dependencies:
"@aw-web-design/x-default-browser": "npm:1.4.126"
"@babel/core": "npm:^7.24.4"
"@babel/parser": "npm:^7.24.4"
"@discoveryjs/json-ext": "npm:^0.5.3"
- "@storybook/builder-manager": "npm:8.1.3"
- "@storybook/channels": "npm:8.1.3"
- "@storybook/core-common": "npm:8.1.3"
- "@storybook/core-events": "npm:8.1.3"
+ "@storybook/builder-manager": "npm:8.1.5"
+ "@storybook/channels": "npm:8.1.5"
+ "@storybook/core-common": "npm:8.1.5"
+ "@storybook/core-events": "npm:8.1.5"
"@storybook/csf": "npm:^0.1.7"
- "@storybook/csf-tools": "npm:8.1.3"
+ "@storybook/csf-tools": "npm:8.1.5"
"@storybook/docs-mdx": "npm:3.1.0-next.0"
"@storybook/global": "npm:^5.0.0"
- "@storybook/manager": "npm:8.1.3"
- "@storybook/manager-api": "npm:8.1.3"
- "@storybook/node-logger": "npm:8.1.3"
- "@storybook/preview-api": "npm:8.1.3"
- "@storybook/telemetry": "npm:8.1.3"
- "@storybook/types": "npm:8.1.3"
+ "@storybook/manager": "npm:8.1.5"
+ "@storybook/manager-api": "npm:8.1.5"
+ "@storybook/node-logger": "npm:8.1.5"
+ "@storybook/preview-api": "npm:8.1.5"
+ "@storybook/telemetry": "npm:8.1.5"
+ "@storybook/types": "npm:8.1.5"
"@types/detect-port": "npm:^1.3.0"
"@types/diff": "npm:^5.0.9"
"@types/node": "npm:^18.0.0"
@@ -6463,47 +6463,47 @@ __metadata:
util-deprecate: "npm:^1.0.2"
watchpack: "npm:^2.2.0"
ws: "npm:^8.2.3"
- checksum: 10/d4ebc1724815005fc538c54d0bdce7279fe7c983ef40c9963e6ee842dd80c33385d7ba9324e6c04d720d2d556ddec96ea717a9b724066d6dac1298d77f6290b3
+ checksum: 10/1a13d99d2f4d6b82ccf18e014e67e0d1b62621392949cc0d4088922c3f4c4ecea01967bda9b4c2b1d93f25544eb378280d36840c4c050c33e630cf2420b9d579
languageName: node
linkType: hard
-"@storybook/core-webpack@npm:8.1.3":
- version: 8.1.3
- resolution: "@storybook/core-webpack@npm:8.1.3"
+"@storybook/core-webpack@npm:8.1.5":
+ version: 8.1.5
+ resolution: "@storybook/core-webpack@npm:8.1.5"
dependencies:
- "@storybook/core-common": "npm:8.1.3"
- "@storybook/node-logger": "npm:8.1.3"
- "@storybook/types": "npm:8.1.3"
+ "@storybook/core-common": "npm:8.1.5"
+ "@storybook/node-logger": "npm:8.1.5"
+ "@storybook/types": "npm:8.1.5"
"@types/node": "npm:^18.0.0"
ts-dedent: "npm:^2.0.0"
- checksum: 10/cbe9cf6b45269916dd217ef821e3aab22035e7c1a44f6478c86776ec8bdfd6ace4be7c7170ba354548127cefca8b3e227cf743c3a546c636213cc967ccd068b1
+ checksum: 10/0ef76df3ed5ea59ef8487e380481f3310de1faefba65c5fd4f511edc27faea34e9982448eb59fff6a04b19c14728974b2cebae36475f7324670392c9b9963b50
languageName: node
linkType: hard
-"@storybook/csf-plugin@npm:8.1.3":
- version: 8.1.3
- resolution: "@storybook/csf-plugin@npm:8.1.3"
+"@storybook/csf-plugin@npm:8.1.5":
+ version: 8.1.5
+ resolution: "@storybook/csf-plugin@npm:8.1.5"
dependencies:
- "@storybook/csf-tools": "npm:8.1.3"
+ "@storybook/csf-tools": "npm:8.1.5"
unplugin: "npm:^1.3.1"
- checksum: 10/d987a38225594e231aa5ef45355611ff9bd96a9f1b24cf2044f09e9100b3e6b895fb420d6a226b4e36fa3cf3baee82071fd04fbef01d53a9dab50e3dccac1025
+ checksum: 10/96b51c1e38eaa41e296cfeb1e73ab1933c793146ba7db26e8f31a3c2ad828de6498503ccb49402f7595f444e9eda02c00ee6b663be052486eb36d668548077d2
languageName: node
linkType: hard
-"@storybook/csf-tools@npm:8.1.3":
- version: 8.1.3
- resolution: "@storybook/csf-tools@npm:8.1.3"
+"@storybook/csf-tools@npm:8.1.5":
+ version: 8.1.5
+ resolution: "@storybook/csf-tools@npm:8.1.5"
dependencies:
"@babel/generator": "npm:^7.24.4"
"@babel/parser": "npm:^7.24.4"
"@babel/traverse": "npm:^7.24.1"
"@babel/types": "npm:^7.24.0"
"@storybook/csf": "npm:^0.1.7"
- "@storybook/types": "npm:8.1.3"
+ "@storybook/types": "npm:8.1.5"
fs-extra: "npm:^11.1.0"
recast: "npm:^0.23.5"
ts-dedent: "npm:^2.0.0"
- checksum: 10/5d385ea82cc698e6a51600c6ed7826e00925e9c8d116aefe4fd7d269096099801eb02aed303de3316c2f957d3110f39214208be24ea6566151735b4a188abc0f
+ checksum: 10/5ed787d32077783fe3e3b612664257076861b7972bacef36e8f26b92a49b476fb9845f63ec50de38d530429886e2bebfe79a0488fbc05b5a33634b438644d552
languageName: node
linkType: hard
@@ -6532,19 +6532,19 @@ __metadata:
languageName: node
linkType: hard
-"@storybook/docs-tools@npm:8.1.3":
- version: 8.1.3
- resolution: "@storybook/docs-tools@npm:8.1.3"
+"@storybook/docs-tools@npm:8.1.5":
+ version: 8.1.5
+ resolution: "@storybook/docs-tools@npm:8.1.5"
dependencies:
- "@storybook/core-common": "npm:8.1.3"
- "@storybook/core-events": "npm:8.1.3"
- "@storybook/preview-api": "npm:8.1.3"
- "@storybook/types": "npm:8.1.3"
+ "@storybook/core-common": "npm:8.1.5"
+ "@storybook/core-events": "npm:8.1.5"
+ "@storybook/preview-api": "npm:8.1.5"
+ "@storybook/types": "npm:8.1.5"
"@types/doctrine": "npm:^0.0.3"
assert: "npm:^2.1.0"
doctrine: "npm:^3.0.0"
lodash: "npm:^4.17.21"
- checksum: 10/3eac6ddd48db53c9f8759cfcc9dd01958bb396ea194c9d4705a7a2016e6ff19996e71148cca4691ea64de5452ba5b64eba86d811dd38d805988ce1d63a216865
+ checksum: 10/1249df8daebe3bc9d660e789c48ea2385c38d291d72e1b99d31487ae466723b2e5523553c7b104831e8db2fe523115b05f64ed120f3845f36a737315f7f8d838
languageName: node
linkType: hard
@@ -6565,66 +6565,66 @@ __metadata:
languageName: node
linkType: hard
-"@storybook/instrumenter@npm:8.1.3":
- version: 8.1.3
- resolution: "@storybook/instrumenter@npm:8.1.3"
+"@storybook/instrumenter@npm:8.1.5":
+ version: 8.1.5
+ resolution: "@storybook/instrumenter@npm:8.1.5"
dependencies:
- "@storybook/channels": "npm:8.1.3"
- "@storybook/client-logger": "npm:8.1.3"
- "@storybook/core-events": "npm:8.1.3"
+ "@storybook/channels": "npm:8.1.5"
+ "@storybook/client-logger": "npm:8.1.5"
+ "@storybook/core-events": "npm:8.1.5"
"@storybook/global": "npm:^5.0.0"
- "@storybook/preview-api": "npm:8.1.3"
+ "@storybook/preview-api": "npm:8.1.5"
"@vitest/utils": "npm:^1.3.1"
util: "npm:^0.12.4"
- checksum: 10/dfca08300b7b98f99ee4f1d9fbb41206446c2cef911a3f7155b66310147af1e70cb0d96e8b7f35e9658e150d0611116541cf35996b36a4def777ea097f7e3d6f
+ checksum: 10/4c803321f677cbf21e5befeb1a69ccf17b2dbe8e32804defccddd812cdfb8f01c7c91a02ea03405d6c1bce63ac53162a4892d6433b9cc05e8c000066b8ad1717
languageName: node
linkType: hard
-"@storybook/manager-api@npm:8.1.3":
- version: 8.1.3
- resolution: "@storybook/manager-api@npm:8.1.3"
+"@storybook/manager-api@npm:8.1.5":
+ version: 8.1.5
+ resolution: "@storybook/manager-api@npm:8.1.5"
dependencies:
- "@storybook/channels": "npm:8.1.3"
- "@storybook/client-logger": "npm:8.1.3"
- "@storybook/core-events": "npm:8.1.3"
+ "@storybook/channels": "npm:8.1.5"
+ "@storybook/client-logger": "npm:8.1.5"
+ "@storybook/core-events": "npm:8.1.5"
"@storybook/csf": "npm:^0.1.7"
"@storybook/global": "npm:^5.0.0"
"@storybook/icons": "npm:^1.2.5"
- "@storybook/router": "npm:8.1.3"
- "@storybook/theming": "npm:8.1.3"
- "@storybook/types": "npm:8.1.3"
+ "@storybook/router": "npm:8.1.5"
+ "@storybook/theming": "npm:8.1.5"
+ "@storybook/types": "npm:8.1.5"
dequal: "npm:^2.0.2"
lodash: "npm:^4.17.21"
memoizerific: "npm:^1.11.3"
store2: "npm:^2.14.2"
telejson: "npm:^7.2.0"
ts-dedent: "npm:^2.0.0"
- checksum: 10/efad33bd2390588fb07afd7940f5d0d98d381f843199bcc83180aec44cde5f2187c81ddf7b0e72775faf6ae88947335d7190444696374f50182210818caa37b6
+ checksum: 10/31044a5e22984f3522f80530e7f8b48a3f6596910a59275d3dde8014f4b7124f5b65b3c7dcd66a056c9d80b94208a5b9075a11c1a604b388f0fae0d1eb381a3f
languageName: node
linkType: hard
-"@storybook/manager@npm:8.1.3":
- version: 8.1.3
- resolution: "@storybook/manager@npm:8.1.3"
- checksum: 10/fe6b01d6e2a301410d481779fc4d5932c544867ae74cc3c09df4b1c6b6826062ca0eefdf0021278097ccf1e079ecee20ae0728d65352ed2ec569b9d0727e3351
+"@storybook/manager@npm:8.1.5":
+ version: 8.1.5
+ resolution: "@storybook/manager@npm:8.1.5"
+ checksum: 10/4b8360a4cd94e6f022d703452f1ef3774c83380aca285f6206c1171550587b107c4cbfc41c93b38cc354ac4cff7a363f49967b16a0fdcff814a476f2ca719366
languageName: node
linkType: hard
-"@storybook/node-logger@npm:8.1.3":
- version: 8.1.3
- resolution: "@storybook/node-logger@npm:8.1.3"
- checksum: 10/b3e372f7c56c5a8e60144ca8fd1755a964ceaf33031357649ea7da09a23756864df44f4ad081562ba81f034ac1861e4deb5bd129197e42e03f4a5d2037937cee
+"@storybook/node-logger@npm:8.1.5":
+ version: 8.1.5
+ resolution: "@storybook/node-logger@npm:8.1.5"
+ checksum: 10/3f34a03afe83c45ac441b06d3f7b2bedaaa5c0a77febabc055716a82755a5d86811f9d3fef84a2f52118075cc34c9ff8a80984580c509432e78b0259a15fe212
languageName: node
linkType: hard
-"@storybook/preset-react-webpack@npm:8.1.3":
- version: 8.1.3
- resolution: "@storybook/preset-react-webpack@npm:8.1.3"
+"@storybook/preset-react-webpack@npm:8.1.5":
+ version: 8.1.5
+ resolution: "@storybook/preset-react-webpack@npm:8.1.5"
dependencies:
- "@storybook/core-webpack": "npm:8.1.3"
- "@storybook/docs-tools": "npm:8.1.3"
- "@storybook/node-logger": "npm:8.1.3"
- "@storybook/react": "npm:8.1.3"
+ "@storybook/core-webpack": "npm:8.1.5"
+ "@storybook/docs-tools": "npm:8.1.5"
+ "@storybook/node-logger": "npm:8.1.5"
+ "@storybook/react": "npm:8.1.5"
"@storybook/react-docgen-typescript-plugin": "npm:1.0.6--canary.9.0c3f3b7.0"
"@types/node": "npm:^18.0.0"
"@types/semver": "npm:^7.3.4"
@@ -6642,20 +6642,20 @@ __metadata:
peerDependenciesMeta:
typescript:
optional: true
- checksum: 10/e2ae92801b8a2c0d128b41e765926f67106d1ac137badefe62c8a3e5a574693f0296c719194ef77230e3653efb963ffe4681a903111c53a4bc094582de905d59
+ checksum: 10/40df8d28966e056bb844a5c6daa4e1169ccd543e04acccef1952e666e359e147608f241001bb1ad017f265e1006ba2de87aafb97c13c022d592749a7e2932320
languageName: node
linkType: hard
-"@storybook/preview-api@npm:8.1.3":
- version: 8.1.3
- resolution: "@storybook/preview-api@npm:8.1.3"
+"@storybook/preview-api@npm:8.1.5":
+ version: 8.1.5
+ resolution: "@storybook/preview-api@npm:8.1.5"
dependencies:
- "@storybook/channels": "npm:8.1.3"
- "@storybook/client-logger": "npm:8.1.3"
- "@storybook/core-events": "npm:8.1.3"
+ "@storybook/channels": "npm:8.1.5"
+ "@storybook/client-logger": "npm:8.1.5"
+ "@storybook/core-events": "npm:8.1.5"
"@storybook/csf": "npm:^0.1.7"
"@storybook/global": "npm:^5.0.0"
- "@storybook/types": "npm:8.1.3"
+ "@storybook/types": "npm:8.1.5"
"@types/qs": "npm:^6.9.5"
dequal: "npm:^2.0.2"
lodash: "npm:^4.17.21"
@@ -6664,14 +6664,14 @@ __metadata:
tiny-invariant: "npm:^1.3.1"
ts-dedent: "npm:^2.0.0"
util-deprecate: "npm:^1.0.2"
- checksum: 10/5b760236cfee5fe268f9d997cdbcb7bcb7119aea583c515a1990e08573b112c8c6c979959837ae89e988bfda9767cd2c7f7b408fe780ba92ca47e52e7e7f053f
+ checksum: 10/03a30b37bc4e0077711c849fad11c4ec555d9c3d418d8c9a2c8ccbf1c3023670e26d46f49d14c0179aeb8fd37a7d470930bb24070a66620fb9ba97d6ff8d9cd2
languageName: node
linkType: hard
-"@storybook/preview@npm:8.1.3":
- version: 8.1.3
- resolution: "@storybook/preview@npm:8.1.3"
- checksum: 10/84abd5e08369bb0236cc66c9300edc38bf1ce370adeced6bb90da0ea8aa58c8c05f987c1612acd16a46577c5d8b4aec0b3856931288d225a5b43979100f77245
+"@storybook/preview@npm:8.1.5":
+ version: 8.1.5
+ resolution: "@storybook/preview@npm:8.1.5"
+ checksum: 10/f7bb249c38e78368652b2a2561b6df25a2b11b3f12a1fa4b89bcfbd62ea1f7c4276084c330789367af91459f6e743c9ff99b1370c4a07d0bd8b9dffa69dfaaca
languageName: node
linkType: hard
@@ -6693,24 +6693,24 @@ __metadata:
languageName: node
linkType: hard
-"@storybook/react-dom-shim@npm:8.1.3":
- version: 8.1.3
- resolution: "@storybook/react-dom-shim@npm:8.1.3"
+"@storybook/react-dom-shim@npm:8.1.5":
+ version: 8.1.5
+ resolution: "@storybook/react-dom-shim@npm:8.1.5"
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
- checksum: 10/493df9b4d3f5241007c2d268e684002d499e5f6532538f40c4348fc5272b8f2732e2a94a4ccedc8f28c715e38f0acb55edcda6c540a855f344225e48159ca1e8
+ checksum: 10/51a3572a3ab8898a5d8e667131bd7bc3c6bb0882129a42cbf1fe396ce0c7e4e11e2affd390343c8def3f87fe53160e9ca4a8aca181f1817f33bf65f8382666d4
languageName: node
linkType: hard
"@storybook/react-webpack5@npm:^8.0.4":
- version: 8.1.3
- resolution: "@storybook/react-webpack5@npm:8.1.3"
+ version: 8.1.5
+ resolution: "@storybook/react-webpack5@npm:8.1.5"
dependencies:
- "@storybook/builder-webpack5": "npm:8.1.3"
- "@storybook/preset-react-webpack": "npm:8.1.3"
- "@storybook/react": "npm:8.1.3"
- "@storybook/types": "npm:8.1.3"
+ "@storybook/builder-webpack5": "npm:8.1.5"
+ "@storybook/preset-react-webpack": "npm:8.1.5"
+ "@storybook/react": "npm:8.1.5"
+ "@storybook/types": "npm:8.1.5"
"@types/node": "npm:^18.0.0"
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
@@ -6719,20 +6719,20 @@ __metadata:
peerDependenciesMeta:
typescript:
optional: true
- checksum: 10/65e46143403c58ac1326d0ec5b89831ccc738176fcf01a1f2d61c8b5be5ab66c27f5d2564bf30b9b956f3416f41ba2209f03b7778d17d66375354532f3b46812
+ checksum: 10/90188a6b3e1eacf653de14354c4cebdc93f6f24d2b163c04df8208f5434fe54ec85963224c76b60b8c64731b28add62a17ad0821e536eab2da0bd7da26517a81
languageName: node
linkType: hard
-"@storybook/react@npm:8.1.3, @storybook/react@npm:^8.0.4":
- version: 8.1.3
- resolution: "@storybook/react@npm:8.1.3"
+"@storybook/react@npm:8.1.5, @storybook/react@npm:^8.0.4":
+ version: 8.1.5
+ resolution: "@storybook/react@npm:8.1.5"
dependencies:
- "@storybook/client-logger": "npm:8.1.3"
- "@storybook/docs-tools": "npm:8.1.3"
+ "@storybook/client-logger": "npm:8.1.5"
+ "@storybook/docs-tools": "npm:8.1.5"
"@storybook/global": "npm:^5.0.0"
- "@storybook/preview-api": "npm:8.1.3"
- "@storybook/react-dom-shim": "npm:8.1.3"
- "@storybook/types": "npm:8.1.3"
+ "@storybook/preview-api": "npm:8.1.5"
+ "@storybook/react-dom-shim": "npm:8.1.5"
+ "@storybook/types": "npm:8.1.5"
"@types/escodegen": "npm:^0.0.6"
"@types/estree": "npm:^0.0.51"
"@types/node": "npm:^18.0.0"
@@ -6755,61 +6755,61 @@ __metadata:
peerDependenciesMeta:
typescript:
optional: true
- checksum: 10/8646b3afe337b2544e23a75ea8772578ac839e3d4de29bb401acc9792ea39c464c896db261bb776ad2e2bf141dd7fbdf893c975698872b9c756fc662dec1148a
+ checksum: 10/dbc762fad9573410eba665ba82598419078d000a8b780ada7c9f0f7cc3b8c01db79c23a0f30b93b5dd1f09945d4af9405d513254421efaa587c78491e329b9df
languageName: node
linkType: hard
-"@storybook/router@npm:8.1.3":
- version: 8.1.3
- resolution: "@storybook/router@npm:8.1.3"
+"@storybook/router@npm:8.1.5":
+ version: 8.1.5
+ resolution: "@storybook/router@npm:8.1.5"
dependencies:
- "@storybook/client-logger": "npm:8.1.3"
+ "@storybook/client-logger": "npm:8.1.5"
memoizerific: "npm:^1.11.3"
qs: "npm:^6.10.0"
- checksum: 10/820946bea27ba2d55596b6c5b68ddeedf98224a4a06c3e41271a1240da9bfbc4d1d548f8d3fd0bbfa92206f065b6749a2af788bea2d666c362466c869fa0ba5a
+ checksum: 10/5f9cf87f2c0a5a0bf117a2dd165b764b05f925f885ebdcadb3b52330b13e493a8289113d072cfc0ab2f3e12096dfb8d2c160fd7c053c6df70d7d1ced85d52d7d
languageName: node
linkType: hard
-"@storybook/telemetry@npm:8.1.3":
- version: 8.1.3
- resolution: "@storybook/telemetry@npm:8.1.3"
+"@storybook/telemetry@npm:8.1.5":
+ version: 8.1.5
+ resolution: "@storybook/telemetry@npm:8.1.5"
dependencies:
- "@storybook/client-logger": "npm:8.1.3"
- "@storybook/core-common": "npm:8.1.3"
- "@storybook/csf-tools": "npm:8.1.3"
+ "@storybook/client-logger": "npm:8.1.5"
+ "@storybook/core-common": "npm:8.1.5"
+ "@storybook/csf-tools": "npm:8.1.5"
chalk: "npm:^4.1.0"
detect-package-manager: "npm:^2.0.1"
fetch-retry: "npm:^5.0.2"
fs-extra: "npm:^11.1.0"
read-pkg-up: "npm:^7.0.1"
- checksum: 10/3611f1f26489824d26e473f36d8d1271a3b68e86c4275f861d252c4df51ecc68ca1c9203a76636dd88d5873bdaeb5a6377dbe0da40a8717f81cf334d784f8d10
+ checksum: 10/eb89526b3d5e79e3ee1da5012ad40a6c3254f77264f4a765c3e00c3eaaaaff35c17732c8e29ace0a2ed7eba6b9a760774838cb25f7b9779591ab6738ace6750d
languageName: node
linkType: hard
-"@storybook/test@npm:8.1.3, @storybook/test@npm:^8.0.4":
- version: 8.1.3
- resolution: "@storybook/test@npm:8.1.3"
+"@storybook/test@npm:8.1.5, @storybook/test@npm:^8.0.4":
+ version: 8.1.5
+ resolution: "@storybook/test@npm:8.1.5"
dependencies:
- "@storybook/client-logger": "npm:8.1.3"
- "@storybook/core-events": "npm:8.1.3"
- "@storybook/instrumenter": "npm:8.1.3"
- "@storybook/preview-api": "npm:8.1.3"
+ "@storybook/client-logger": "npm:8.1.5"
+ "@storybook/core-events": "npm:8.1.5"
+ "@storybook/instrumenter": "npm:8.1.5"
+ "@storybook/preview-api": "npm:8.1.5"
"@testing-library/dom": "npm:^9.3.4"
"@testing-library/jest-dom": "npm:^6.4.2"
"@testing-library/user-event": "npm:^14.5.2"
"@vitest/expect": "npm:1.3.1"
"@vitest/spy": "npm:^1.3.1"
util: "npm:^0.12.4"
- checksum: 10/740a8bfd3667509b8c86e83781ed145714a7a27cf732f0dbb860456aaad4f4a996bf3db66f33cc82a9e901960bf1a8bb8fdb7e8eef548d7a6aab20042406c8c7
+ checksum: 10/1a3c262ff410c5305613fb02ae5fecf574adc6670f63d12643108a56de944a76455d642d4ccf96d81cd51deaf0bfc37b9e3dffbe9d3634dcce89dd31f56828b4
languageName: node
linkType: hard
-"@storybook/theming@npm:8.1.3":
- version: 8.1.3
- resolution: "@storybook/theming@npm:8.1.3"
+"@storybook/theming@npm:8.1.5":
+ version: 8.1.5
+ resolution: "@storybook/theming@npm:8.1.5"
dependencies:
"@emotion/use-insertion-effect-with-fallbacks": "npm:^1.0.1"
- "@storybook/client-logger": "npm:8.1.3"
+ "@storybook/client-logger": "npm:8.1.5"
"@storybook/global": "npm:^5.0.0"
memoizerific: "npm:^1.11.3"
peerDependencies:
@@ -6820,18 +6820,18 @@ __metadata:
optional: true
react-dom:
optional: true
- checksum: 10/a69fb8aaba73fe671a1c81234aef79f25a9146ccc6721b73238264978efac54a13a5cb4e697606222fd4000c6e70676ef2d12f5d5dee76f4e202df0286e5ec59
+ checksum: 10/fa91537d839c6f3b5ed386df5a1d93a06cc34571821408f71e668f5e8e1eb446cf35bb69dc0a006aa83c2742239ce87cf72ca899c9f6ae0797c40b45c059d8a7
languageName: node
linkType: hard
-"@storybook/types@npm:8.1.3":
- version: 8.1.3
- resolution: "@storybook/types@npm:8.1.3"
+"@storybook/types@npm:8.1.5":
+ version: 8.1.5
+ resolution: "@storybook/types@npm:8.1.5"
dependencies:
- "@storybook/channels": "npm:8.1.3"
+ "@storybook/channels": "npm:8.1.5"
"@types/express": "npm:^4.7.0"
file-system-cache: "npm:2.3.0"
- checksum: 10/856dc930023a758e57a1512a9cfabf321f382fbfd78bcb27466a53850f857ed3f43351ca1a3003fbcf194dfe6296488e0dc75c6bfa80b3884e56517bfc4c8119
+ checksum: 10/136018379def5feb1b63eb3dc283eadec00a765f0359c3508fbe7e499060015d4d2cb2608bac7f43a780a5ddfcffde6f4f7ae59b4f37931ca7c781c135393036
languageName: node
linkType: hard
@@ -6839,12 +6839,12 @@ __metadata:
version: 0.0.0-use.local
resolution: "@studio/components@workspace:frontend/libs/studio-components"
dependencies:
- "@chromatic-com/storybook": "npm:1.4.0"
+ "@chromatic-com/storybook": "npm:1.5.0"
"@storybook/addon-essentials": "npm:^8.0.4"
"@storybook/addon-interactions": "npm:^8.0.4"
"@storybook/addon-links": "npm:^8.0.4"
"@storybook/addon-onboarding": "npm:^8.0.4"
- "@storybook/addon-webpack5-compiler-swc": "npm:1.0.2"
+ "@storybook/addon-webpack5-compiler-swc": "npm:1.0.3"
"@storybook/blocks": "npm:^8.0.4"
"@storybook/react": "npm:^8.0.4"
"@storybook/react-webpack5": "npm:^8.0.4"
@@ -6853,7 +6853,7 @@ __metadata:
"@testing-library/jest-dom": "npm:6.4.5"
"@testing-library/react": "npm:15.0.7"
"@types/jest": "npm:^29.5.5"
- ajv: "npm:8.13.0"
+ ajv: "npm:8.14.0"
eslint: "npm:8.57.0"
eslint-plugin-storybook: "npm:^0.8.0"
jest: "npm:^29.7.0"
@@ -7053,9 +7053,9 @@ __metadata:
languageName: node
linkType: hard
-"@swc/core-darwin-arm64@npm:1.4.8":
- version: 1.4.8
- resolution: "@swc/core-darwin-arm64@npm:1.4.8"
+"@swc/core-darwin-arm64@npm:1.5.24":
+ version: 1.5.24
+ resolution: "@swc/core-darwin-arm64@npm:1.5.24"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
@@ -7067,9 +7067,9 @@ __metadata:
languageName: node
linkType: hard
-"@swc/core-darwin-x64@npm:1.4.8":
- version: 1.4.8
- resolution: "@swc/core-darwin-x64@npm:1.4.8"
+"@swc/core-darwin-x64@npm:1.5.24":
+ version: 1.5.24
+ resolution: "@swc/core-darwin-x64@npm:1.5.24"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
@@ -7081,9 +7081,9 @@ __metadata:
languageName: node
linkType: hard
-"@swc/core-linux-arm-gnueabihf@npm:1.4.8":
- version: 1.4.8
- resolution: "@swc/core-linux-arm-gnueabihf@npm:1.4.8"
+"@swc/core-linux-arm-gnueabihf@npm:1.5.24":
+ version: 1.5.24
+ resolution: "@swc/core-linux-arm-gnueabihf@npm:1.5.24"
conditions: os=linux & cpu=arm
languageName: node
linkType: hard
@@ -7095,9 +7095,9 @@ __metadata:
languageName: node
linkType: hard
-"@swc/core-linux-arm64-gnu@npm:1.4.8":
- version: 1.4.8
- resolution: "@swc/core-linux-arm64-gnu@npm:1.4.8"
+"@swc/core-linux-arm64-gnu@npm:1.5.24":
+ version: 1.5.24
+ resolution: "@swc/core-linux-arm64-gnu@npm:1.5.24"
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard
@@ -7109,9 +7109,9 @@ __metadata:
languageName: node
linkType: hard
-"@swc/core-linux-arm64-musl@npm:1.4.8":
- version: 1.4.8
- resolution: "@swc/core-linux-arm64-musl@npm:1.4.8"
+"@swc/core-linux-arm64-musl@npm:1.5.24":
+ version: 1.5.24
+ resolution: "@swc/core-linux-arm64-musl@npm:1.5.24"
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard
@@ -7123,9 +7123,9 @@ __metadata:
languageName: node
linkType: hard
-"@swc/core-linux-x64-gnu@npm:1.4.8":
- version: 1.4.8
- resolution: "@swc/core-linux-x64-gnu@npm:1.4.8"
+"@swc/core-linux-x64-gnu@npm:1.5.24":
+ version: 1.5.24
+ resolution: "@swc/core-linux-x64-gnu@npm:1.5.24"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
@@ -7137,9 +7137,9 @@ __metadata:
languageName: node
linkType: hard
-"@swc/core-linux-x64-musl@npm:1.4.8":
- version: 1.4.8
- resolution: "@swc/core-linux-x64-musl@npm:1.4.8"
+"@swc/core-linux-x64-musl@npm:1.5.24":
+ version: 1.5.24
+ resolution: "@swc/core-linux-x64-musl@npm:1.5.24"
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
@@ -7151,9 +7151,9 @@ __metadata:
languageName: node
linkType: hard
-"@swc/core-win32-arm64-msvc@npm:1.4.8":
- version: 1.4.8
- resolution: "@swc/core-win32-arm64-msvc@npm:1.4.8"
+"@swc/core-win32-arm64-msvc@npm:1.5.24":
+ version: 1.5.24
+ resolution: "@swc/core-win32-arm64-msvc@npm:1.5.24"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
@@ -7165,9 +7165,9 @@ __metadata:
languageName: node
linkType: hard
-"@swc/core-win32-ia32-msvc@npm:1.4.8":
- version: 1.4.8
- resolution: "@swc/core-win32-ia32-msvc@npm:1.4.8"
+"@swc/core-win32-ia32-msvc@npm:1.5.24":
+ version: 1.5.24
+ resolution: "@swc/core-win32-ia32-msvc@npm:1.5.24"
conditions: os=win32 & cpu=ia32
languageName: node
linkType: hard
@@ -7179,9 +7179,9 @@ __metadata:
languageName: node
linkType: hard
-"@swc/core-win32-x64-msvc@npm:1.4.8":
- version: 1.4.8
- resolution: "@swc/core-win32-x64-msvc@npm:1.4.8"
+"@swc/core-win32-x64-msvc@npm:1.5.24":
+ version: 1.5.24
+ resolution: "@swc/core-win32-x64-msvc@npm:1.5.24"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
@@ -7193,24 +7193,24 @@ __metadata:
languageName: node
linkType: hard
-"@swc/core@npm:1.5.7":
- version: 1.5.7
- resolution: "@swc/core@npm:1.5.7"
+"@swc/core@npm:1.5.24":
+ version: 1.5.24
+ resolution: "@swc/core@npm:1.5.24"
dependencies:
- "@swc/core-darwin-arm64": "npm:1.5.7"
- "@swc/core-darwin-x64": "npm:1.5.7"
- "@swc/core-linux-arm-gnueabihf": "npm:1.5.7"
- "@swc/core-linux-arm64-gnu": "npm:1.5.7"
- "@swc/core-linux-arm64-musl": "npm:1.5.7"
- "@swc/core-linux-x64-gnu": "npm:1.5.7"
- "@swc/core-linux-x64-musl": "npm:1.5.7"
- "@swc/core-win32-arm64-msvc": "npm:1.5.7"
- "@swc/core-win32-ia32-msvc": "npm:1.5.7"
- "@swc/core-win32-x64-msvc": "npm:1.5.7"
- "@swc/counter": "npm:^0.1.2"
- "@swc/types": "npm:0.1.7"
+ "@swc/core-darwin-arm64": "npm:1.5.24"
+ "@swc/core-darwin-x64": "npm:1.5.24"
+ "@swc/core-linux-arm-gnueabihf": "npm:1.5.24"
+ "@swc/core-linux-arm64-gnu": "npm:1.5.24"
+ "@swc/core-linux-arm64-musl": "npm:1.5.24"
+ "@swc/core-linux-x64-gnu": "npm:1.5.24"
+ "@swc/core-linux-x64-musl": "npm:1.5.24"
+ "@swc/core-win32-arm64-msvc": "npm:1.5.24"
+ "@swc/core-win32-ia32-msvc": "npm:1.5.24"
+ "@swc/core-win32-x64-msvc": "npm:1.5.24"
+ "@swc/counter": "npm:^0.1.3"
+ "@swc/types": "npm:^0.1.7"
peerDependencies:
- "@swc/helpers": ^0.5.0
+ "@swc/helpers": "*"
dependenciesMeta:
"@swc/core-darwin-arm64":
optional: true
@@ -7235,26 +7235,26 @@ __metadata:
peerDependenciesMeta:
"@swc/helpers":
optional: true
- checksum: 10/83e03908db40f2133c3624a83d4550336d7a56e64af7d42fd959c746b8da950a253f3c6d9eaa3467e10abeda024aa6b039a987adc839326f969e1d26625f14ef
+ checksum: 10/02be0fac3cd524226e844edb18028b32fb8b0d1664b8a1d2df283a3df841a7496ae7cc9559868842b8418a23921e5f530173de1164dfc5c1fcdf9005648c1591
languageName: node
linkType: hard
-"@swc/core@npm:^1.3.102":
- version: 1.4.8
- resolution: "@swc/core@npm:1.4.8"
+"@swc/core@npm:1.5.7":
+ version: 1.5.7
+ resolution: "@swc/core@npm:1.5.7"
dependencies:
- "@swc/core-darwin-arm64": "npm:1.4.8"
- "@swc/core-darwin-x64": "npm:1.4.8"
- "@swc/core-linux-arm-gnueabihf": "npm:1.4.8"
- "@swc/core-linux-arm64-gnu": "npm:1.4.8"
- "@swc/core-linux-arm64-musl": "npm:1.4.8"
- "@swc/core-linux-x64-gnu": "npm:1.4.8"
- "@swc/core-linux-x64-musl": "npm:1.4.8"
- "@swc/core-win32-arm64-msvc": "npm:1.4.8"
- "@swc/core-win32-ia32-msvc": "npm:1.4.8"
- "@swc/core-win32-x64-msvc": "npm:1.4.8"
+ "@swc/core-darwin-arm64": "npm:1.5.7"
+ "@swc/core-darwin-x64": "npm:1.5.7"
+ "@swc/core-linux-arm-gnueabihf": "npm:1.5.7"
+ "@swc/core-linux-arm64-gnu": "npm:1.5.7"
+ "@swc/core-linux-arm64-musl": "npm:1.5.7"
+ "@swc/core-linux-x64-gnu": "npm:1.5.7"
+ "@swc/core-linux-x64-musl": "npm:1.5.7"
+ "@swc/core-win32-arm64-msvc": "npm:1.5.7"
+ "@swc/core-win32-ia32-msvc": "npm:1.5.7"
+ "@swc/core-win32-x64-msvc": "npm:1.5.7"
"@swc/counter": "npm:^0.1.2"
- "@swc/types": "npm:^0.1.5"
+ "@swc/types": "npm:0.1.7"
peerDependencies:
"@swc/helpers": ^0.5.0
dependenciesMeta:
@@ -7281,7 +7281,7 @@ __metadata:
peerDependenciesMeta:
"@swc/helpers":
optional: true
- checksum: 10/817b674130bc43345e7d8e7fbcf56658d4a655d9544c646475063c7849529c0a6c236a15f3217e7b0b2b99050bda189d3ff26e225b80b838a87b954b97abcb2a
+ checksum: 10/83e03908db40f2133c3624a83d4550336d7a56e64af7d42fd959c746b8da950a253f3c6d9eaa3467e10abeda024aa6b039a987adc839326f969e1d26625f14ef
languageName: node
linkType: hard
@@ -7305,7 +7305,7 @@ __metadata:
languageName: node
linkType: hard
-"@swc/types@npm:0.1.7":
+"@swc/types@npm:0.1.7, @swc/types@npm:^0.1.7":
version: 0.1.7
resolution: "@swc/types@npm:0.1.7"
dependencies:
@@ -7314,17 +7314,10 @@ __metadata:
languageName: node
linkType: hard
-"@swc/types@npm:^0.1.5":
- version: 0.1.5
- resolution: "@swc/types@npm:0.1.5"
- checksum: 10/5f4de8c60d2623bed607c7fa1e0cee4ffc682af28d5ffe88dc9ed9903a1c2088ccc39f684689d6bb314595c9fbb560beaec773d633be515fb856ffc81d738822
- languageName: node
- linkType: hard
-
-"@tanstack/query-core@npm:5.36.1":
- version: 5.36.1
- resolution: "@tanstack/query-core@npm:5.36.1"
- checksum: 10/32f5e34d044016517f3afba3a31efa686bca70275be773ec8df0ee320ecf573867ff4f996bedc107012568d586f19ac51cbbebb45808d6475f25486c47363ed9
+"@tanstack/query-core@npm:5.40.0":
+ version: 5.40.0
+ resolution: "@tanstack/query-core@npm:5.40.0"
+ checksum: 10/d7f022295aa392c2f8903f56b87039b925e4bfe26c5e8efdc482f2615d830f02f1b49ffda35838ff682d4546de87817f445db260f225ed1caf8c24eb8c197cf9
languageName: node
linkType: hard
@@ -7335,26 +7328,26 @@ __metadata:
languageName: node
linkType: hard
-"@tanstack/react-query-devtools@npm:5.37.1":
- version: 5.37.1
- resolution: "@tanstack/react-query-devtools@npm:5.37.1"
+"@tanstack/react-query-devtools@npm:5.40.0":
+ version: 5.40.0
+ resolution: "@tanstack/react-query-devtools@npm:5.40.0"
dependencies:
"@tanstack/query-devtools": "npm:5.37.1"
peerDependencies:
- "@tanstack/react-query": ^5.37.1
- react: ^18.0.0
- checksum: 10/7fd8ac801671c004c69ff289fbcf923938eeb43dea19abbe3c3cee7531b56bc1bf56a651b76138f9c00444e50a8f3bc535f5fe8504d2379f4d043557c15bdf35
+ "@tanstack/react-query": ^5.40.0
+ react: ^18 || ^19
+ checksum: 10/adfc8ba7da208bb25fa1c7321460afc0926b16c22a8081e49b3a2e5dee55cc41be6f3e3205c6db21a8e82a48ec1ef8bcc1c731d382825f43c7cd8b057301eccb
languageName: node
linkType: hard
-"@tanstack/react-query@npm:5.37.1":
- version: 5.37.1
- resolution: "@tanstack/react-query@npm:5.37.1"
+"@tanstack/react-query@npm:5.40.0":
+ version: 5.40.0
+ resolution: "@tanstack/react-query@npm:5.40.0"
dependencies:
- "@tanstack/query-core": "npm:5.36.1"
+ "@tanstack/query-core": "npm:5.40.0"
peerDependencies:
react: ^18.0.0
- checksum: 10/f7f11f0702382c7e8fc0bee7c24d11836b4c43eb5feea10282a9054afd4be554945db2887d30c2ded8c6f11018eeba93266a319b23de5f19c0fe50a445d39141
+ checksum: 10/4deeaa529a9596b9188018ba76365c6e65889371d0846f080e2094ee551b4e03c7968f2e34a4a71320920588859809e1f4ab566e75f5b7a0c5e59a847de17353
languageName: node
linkType: hard
@@ -7377,15 +7370,15 @@ __metadata:
languageName: node
linkType: hard
-"@testing-library/cypress@npm:10.0.1":
- version: 10.0.1
- resolution: "@testing-library/cypress@npm:10.0.1"
+"@testing-library/cypress@npm:10.0.2":
+ version: 10.0.2
+ resolution: "@testing-library/cypress@npm:10.0.2"
dependencies:
"@babel/runtime": "npm:^7.14.6"
- "@testing-library/dom": "npm:^9.0.0"
+ "@testing-library/dom": "npm:^10.1.0"
peerDependencies:
cypress: ^12.0.0 || ^13.0.0
- checksum: 10/ce1715f998dd26819d527d3265b316ed694a992c78faa15fa5877621bf2e277754598ac7733062d6ce01876c21945e054139250ce978b9daf08e59d4e389d475
+ checksum: 10/8eaa8c38808350b2adfd75a8b51d28bd862bfa7249188e070ef6022338c6b90f7dc3a9db1f58509bd16e21a17f8409498e12afec852a2807e47f86a170baa0c2
languageName: node
linkType: hard
@@ -7999,20 +7992,20 @@ __metadata:
linkType: hard
"@types/node@npm:*, @types/node@npm:^20.10.5":
- version: 20.12.12
- resolution: "@types/node@npm:20.12.12"
+ version: 20.14.1
+ resolution: "@types/node@npm:20.14.1"
dependencies:
undici-types: "npm:~5.26.4"
- checksum: 10/e3945da0a3017bdc1f88f15bdfb823f526b2a717bd58d4640082d6eb0bd2794b5c99bfb914b9e9324ec116dce36066990353ed1c777e8a7b0641f772575793c4
+ checksum: 10/1417d9768dccca9491fb5fc41ef0ee9044cef4c86eef413608dd983c30af19d5f0930b356791dde3aec238a6a47bbea42c47b69929b5fe822cf09cd543f6c52f
languageName: node
linkType: hard
"@types/node@npm:^18.0.0":
- version: 18.19.33
- resolution: "@types/node@npm:18.19.33"
+ version: 18.19.34
+ resolution: "@types/node@npm:18.19.34"
dependencies:
undici-types: "npm:~5.26.4"
- checksum: 10/e5816356e3bcf1af272587d6a95c172199532a86bdb379e4d314a10605463908b36316af51ff6d3c19d9f1965e14a6f62c6a5cbab876aafffe71e1211512084a
+ checksum: 10/5c8daed0c672e824c36d31312377fc4dddf3e91006fadad719527bb2bd710d4207193c1c7034da9d2d6cbc03da89d3693c86207f751540c18a7d38802040fbd9
languageName: node
linkType: hard
@@ -8346,15 +8339,15 @@ __metadata:
languageName: node
linkType: hard
-"@typescript-eslint/eslint-plugin@npm:7.10.0":
- version: 7.10.0
- resolution: "@typescript-eslint/eslint-plugin@npm:7.10.0"
+"@typescript-eslint/eslint-plugin@npm:7.11.0":
+ version: 7.11.0
+ resolution: "@typescript-eslint/eslint-plugin@npm:7.11.0"
dependencies:
"@eslint-community/regexpp": "npm:^4.10.0"
- "@typescript-eslint/scope-manager": "npm:7.10.0"
- "@typescript-eslint/type-utils": "npm:7.10.0"
- "@typescript-eslint/utils": "npm:7.10.0"
- "@typescript-eslint/visitor-keys": "npm:7.10.0"
+ "@typescript-eslint/scope-manager": "npm:7.11.0"
+ "@typescript-eslint/type-utils": "npm:7.11.0"
+ "@typescript-eslint/utils": "npm:7.11.0"
+ "@typescript-eslint/visitor-keys": "npm:7.11.0"
graphemer: "npm:^1.4.0"
ignore: "npm:^5.3.1"
natural-compare: "npm:^1.4.0"
@@ -8365,25 +8358,25 @@ __metadata:
peerDependenciesMeta:
typescript:
optional: true
- checksum: 10/dfe505cdf718dd29e8637b902e4c544c6b7d246d2051fd1936090423eb3dadfe2bd757de51e565e6fd80e74cf1918e191c26fee6df515100484ec3efd9b8d111
+ checksum: 10/be95ed0bbd5b34c47239677ea39d531bcd8a18717a67d70a297bed5b0050b256159856bb9c1e894ac550d011c24bb5b4abf8056c5d70d0d5895f0cc1accd14ea
languageName: node
linkType: hard
-"@typescript-eslint/parser@npm:7.10.0":
- version: 7.10.0
- resolution: "@typescript-eslint/parser@npm:7.10.0"
+"@typescript-eslint/parser@npm:7.11.0":
+ version: 7.11.0
+ resolution: "@typescript-eslint/parser@npm:7.11.0"
dependencies:
- "@typescript-eslint/scope-manager": "npm:7.10.0"
- "@typescript-eslint/types": "npm:7.10.0"
- "@typescript-eslint/typescript-estree": "npm:7.10.0"
- "@typescript-eslint/visitor-keys": "npm:7.10.0"
+ "@typescript-eslint/scope-manager": "npm:7.11.0"
+ "@typescript-eslint/types": "npm:7.11.0"
+ "@typescript-eslint/typescript-estree": "npm:7.11.0"
+ "@typescript-eslint/visitor-keys": "npm:7.11.0"
debug: "npm:^4.3.4"
peerDependencies:
eslint: ^8.56.0
peerDependenciesMeta:
typescript:
optional: true
- checksum: 10/1fa71049b2debf2f7f5366fb433e3d4c8e1591c2061a15fa8797d14623a2b6984340a59e7717acc013ce8c6a2ed32c5c0e811fe948b5936d41c2a5a09b61d130
+ checksum: 10/0a32417aec62d7de04427323ab3fc8159f9f02429b24f739d8748e8b54fc65b0e3dbae8e4779c4b795f0d8e5f98a4d83a43b37ea0f50ebda51546cdcecf73caa
languageName: node
linkType: hard
@@ -8407,22 +8400,22 @@ __metadata:
languageName: node
linkType: hard
-"@typescript-eslint/scope-manager@npm:7.10.0":
- version: 7.10.0
- resolution: "@typescript-eslint/scope-manager@npm:7.10.0"
+"@typescript-eslint/scope-manager@npm:7.11.0":
+ version: 7.11.0
+ resolution: "@typescript-eslint/scope-manager@npm:7.11.0"
dependencies:
- "@typescript-eslint/types": "npm:7.10.0"
- "@typescript-eslint/visitor-keys": "npm:7.10.0"
- checksum: 10/838a7a9573577d830b2f65801ce045abe6fad08ac7e04bac4cc9b2e5b7cbac07e645de9c79b9485f4cc361fe25da5319025aa0336fad618023fff62e4e980638
+ "@typescript-eslint/types": "npm:7.11.0"
+ "@typescript-eslint/visitor-keys": "npm:7.11.0"
+ checksum: 10/79eff310405c6657ff092641e3ad51c6698c6708b915ecef945ebdd1737bd48e1458c5575836619f42dec06143ec0e3a826f3e551af590d297367da3d08f329e
languageName: node
linkType: hard
-"@typescript-eslint/type-utils@npm:7.10.0":
- version: 7.10.0
- resolution: "@typescript-eslint/type-utils@npm:7.10.0"
+"@typescript-eslint/type-utils@npm:7.11.0":
+ version: 7.11.0
+ resolution: "@typescript-eslint/type-utils@npm:7.11.0"
dependencies:
- "@typescript-eslint/typescript-estree": "npm:7.10.0"
- "@typescript-eslint/utils": "npm:7.10.0"
+ "@typescript-eslint/typescript-estree": "npm:7.11.0"
+ "@typescript-eslint/utils": "npm:7.11.0"
debug: "npm:^4.3.4"
ts-api-utils: "npm:^1.3.0"
peerDependencies:
@@ -8430,7 +8423,7 @@ __metadata:
peerDependenciesMeta:
typescript:
optional: true
- checksum: 10/e62db9ffbfbccce60258108f7ed025005e04df18da897ff1b30049e3c10a47150e94c2fb5ac0ab9711ebb60517521213dcccbea6d08125107a87a67088a79042
+ checksum: 10/ab6ebeff68a60fc40d0ace88e03d6b4242b8f8fe2fa300db161780d58777b57f69fa077cd482e1b673316559459bd20b8cc89a7f9f30e644bfed8293f77f0e4b
languageName: node
linkType: hard
@@ -8448,10 +8441,10 @@ __metadata:
languageName: node
linkType: hard
-"@typescript-eslint/types@npm:7.10.0":
- version: 7.10.0
- resolution: "@typescript-eslint/types@npm:7.10.0"
- checksum: 10/76075a7b87ddfff8e7e4aebf3d225e67bf79ead12a7709999d4d5c31611d9c0813ca69a9298f320efb018fe493ce3763c964a0e670a4c953d8eff000f10672c0
+"@typescript-eslint/types@npm:7.11.0":
+ version: 7.11.0
+ resolution: "@typescript-eslint/types@npm:7.11.0"
+ checksum: 10/c6a0b47ef43649a59c9d51edfc61e367b55e519376209806b1c98385a8385b529e852c7a57e081fb15ef6a5dc0fc8e90bd5a508399f5ac2137f4d462e89cdc30
languageName: node
linkType: hard
@@ -8491,12 +8484,12 @@ __metadata:
languageName: node
linkType: hard
-"@typescript-eslint/typescript-estree@npm:7.10.0":
- version: 7.10.0
- resolution: "@typescript-eslint/typescript-estree@npm:7.10.0"
+"@typescript-eslint/typescript-estree@npm:7.11.0":
+ version: 7.11.0
+ resolution: "@typescript-eslint/typescript-estree@npm:7.11.0"
dependencies:
- "@typescript-eslint/types": "npm:7.10.0"
- "@typescript-eslint/visitor-keys": "npm:7.10.0"
+ "@typescript-eslint/types": "npm:7.11.0"
+ "@typescript-eslint/visitor-keys": "npm:7.11.0"
debug: "npm:^4.3.4"
globby: "npm:^11.1.0"
is-glob: "npm:^4.0.3"
@@ -8506,21 +8499,21 @@ __metadata:
peerDependenciesMeta:
typescript:
optional: true
- checksum: 10/d11d0c45749c9bd4a187b6dfdf5600e36ba8c87667cd2020d9158667c47c32ec0bcb1ef3b7eee5577b667def5f7f33d8131092a0f221b3d3e8105078800f923f
+ checksum: 10/b98b101e42d3b91003510a5c5a83f4350b6c1cf699bf2e409717660579ffa71682bc280c4f40166265c03f9546ed4faedc3723e143f1ab0ed7f5990cc3dff0ae
languageName: node
linkType: hard
-"@typescript-eslint/utils@npm:7.10.0":
- version: 7.10.0
- resolution: "@typescript-eslint/utils@npm:7.10.0"
+"@typescript-eslint/utils@npm:7.11.0":
+ version: 7.11.0
+ resolution: "@typescript-eslint/utils@npm:7.11.0"
dependencies:
"@eslint-community/eslint-utils": "npm:^4.4.0"
- "@typescript-eslint/scope-manager": "npm:7.10.0"
- "@typescript-eslint/types": "npm:7.10.0"
- "@typescript-eslint/typescript-estree": "npm:7.10.0"
+ "@typescript-eslint/scope-manager": "npm:7.11.0"
+ "@typescript-eslint/types": "npm:7.11.0"
+ "@typescript-eslint/typescript-estree": "npm:7.11.0"
peerDependencies:
eslint: ^8.56.0
- checksum: 10/62327b585295f9c3aa2508aefac639d562b6f7f270a229aa3a2af8dbd055f4a4d230a8facae75a8a53bb8222b0041162072d259add56b541f8bdfda8da36ea5f
+ checksum: 10/fbef14e166a70ccc4527c0731e0338acefa28218d1a018aa3f5b6b1ad9d75c56278d5f20bda97cf77da13e0a67c4f3e579c5b2f1c2e24d676960927921b55851
languageName: node
linkType: hard
@@ -8580,13 +8573,13 @@ __metadata:
languageName: node
linkType: hard
-"@typescript-eslint/visitor-keys@npm:7.10.0":
- version: 7.10.0
- resolution: "@typescript-eslint/visitor-keys@npm:7.10.0"
+"@typescript-eslint/visitor-keys@npm:7.11.0":
+ version: 7.11.0
+ resolution: "@typescript-eslint/visitor-keys@npm:7.11.0"
dependencies:
- "@typescript-eslint/types": "npm:7.10.0"
+ "@typescript-eslint/types": "npm:7.11.0"
eslint-visitor-keys: "npm:^3.4.3"
- checksum: 10/44b555a075bdff38e3e13c454ceaac50aa2546635e81f907d1ea84822c8887487d1d6bb4ff690f627da9585dc19ad07e228847c162c30bb06c46fb119899d8cc
+ checksum: 10/1f2cf1214638e9e78e052393c9e24295196ec4781b05951659a3997e33f8699a760ea3705c17d770e10eda2067435199e0136ab09e5fac63869e22f2da184d89
languageName: node
linkType: hard
@@ -9089,15 +9082,15 @@ __metadata:
languageName: node
linkType: hard
-"ajv@npm:8.13.0":
- version: 8.13.0
- resolution: "ajv@npm:8.13.0"
+"ajv@npm:8.14.0":
+ version: 8.14.0
+ resolution: "ajv@npm:8.14.0"
dependencies:
fast-deep-equal: "npm:^3.1.3"
json-schema-traverse: "npm:^1.0.0"
require-from-string: "npm:^2.0.2"
uri-js: "npm:^4.4.1"
- checksum: 10/4ada268c9a6e44be87fd295df0f0a91267a7bae8dbc8a67a2d5799c3cb459232839c99d18b035597bb6e3ffe88af6979f7daece854f590a81ebbbc2dfa80002c
+ checksum: 10/b6430527c2e1bf3d20dce4cca2979b5cc69db15751ac00105e269e04d7b09c2e20364070257cafacfa676171a8bf9c84c1cd9def97267a20cd15c64daa486151
languageName: node
linkType: hard
@@ -9151,10 +9144,10 @@ __metadata:
"@redux-saga/is": "npm:1.1.3"
"@redux-saga/symbols": "npm:1.1.3"
"@svgr/webpack": "npm:8.1.0"
- "@swc/core": "npm:1.5.7"
+ "@swc/core": "npm:1.5.24"
"@swc/jest": "npm:0.2.36"
- "@tanstack/react-query": "npm:5.37.1"
- "@tanstack/react-query-devtools": "npm:5.37.1"
+ "@tanstack/react-query": "npm:5.40.0"
+ "@tanstack/react-query-devtools": "npm:5.40.0"
"@testing-library/dom": "npm:10.1.0"
"@testing-library/jest-dom": "npm:6.4.5"
"@testing-library/react": "npm:15.0.7"
@@ -9166,9 +9159,9 @@ __metadata:
"@types/react-dom": "npm:18.3.0"
"@types/react-redux": "npm:7.1.33"
"@types/redux-mock-store": "npm:1.0.6"
- "@typescript-eslint/eslint-plugin": "npm:7.10.0"
- "@typescript-eslint/parser": "npm:7.10.0"
- ajv: "npm:8.13.0"
+ "@typescript-eslint/eslint-plugin": "npm:7.11.0"
+ "@typescript-eslint/parser": "npm:7.11.0"
+ ajv: "npm:8.14.0"
ajv-formats: "npm:3.0.1"
clean-webpack-plugin: "npm:4.0.0"
cross-env: "npm:7.0.3"
@@ -9179,7 +9172,7 @@ __metadata:
eslint-plugin-import: "npm:2.29.1"
eslint-plugin-jsx-a11y: "npm:6.8.0"
eslint-plugin-prettier: "npm:5.1.3"
- eslint-plugin-react: "npm:7.34.1"
+ eslint-plugin-react: "npm:7.34.2"
eslint-plugin-react-hooks: "npm:4.6.2"
eslint-plugin-testing-library: "npm:6.2.2"
glob: "npm:10.4.1"
@@ -9189,7 +9182,7 @@ __metadata:
jest-environment-jsdom: "npm:29.7.0"
jest-fail-on-console: "npm:3.3.0"
jest-junit: "npm:16.0.0"
- lint-staged: "npm:15.2.4"
+ lint-staged: "npm:15.2.5"
mini-css-extract-plugin: "npm:2.9.0"
prettier: "npm:3.2.5"
react: "npm:18.3.1"
@@ -9311,7 +9304,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "app-development@workspace:frontend/app-development"
dependencies:
- "@mui/material": "npm:5.15.18"
+ "@mui/material": "npm:5.15.19"
"@reduxjs/toolkit": "npm:1.9.7"
"@studio/icons": "workspace:^"
"@studio/pure-functions": "workspace:^"
@@ -9365,7 +9358,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "app-shared@workspace:frontend/packages/shared"
dependencies:
- "@mui/material": "npm:5.15.18"
+ "@mui/material": "npm:5.15.19"
"@reduxjs/toolkit": "npm:1.9.7"
"@types/react": "npm:18.3.3"
axios: "npm:1.7.2"
@@ -9529,6 +9522,20 @@ __metadata:
languageName: node
linkType: hard
+"array-includes@npm:^3.1.8":
+ version: 3.1.8
+ resolution: "array-includes@npm:3.1.8"
+ dependencies:
+ call-bind: "npm:^1.0.7"
+ define-properties: "npm:^1.2.1"
+ es-abstract: "npm:^1.23.2"
+ es-object-atoms: "npm:^1.0.0"
+ get-intrinsic: "npm:^1.2.4"
+ is-string: "npm:^1.0.7"
+ checksum: 10/290b206c9451f181fb2b1f79a3bf1c0b66bb259791290ffbada760c79b284eef6f5ae2aeb4bcff450ebc9690edd25732c4c73a3c2b340fcc0f4563aed83bf488
+ languageName: node
+ linkType: hard
+
"array-union@npm:^1.0.1":
version: 1.0.2
resolution: "array-union@npm:1.0.2"
@@ -9552,16 +9559,17 @@ __metadata:
languageName: node
linkType: hard
-"array.prototype.findlast@npm:^1.2.4":
- version: 1.2.4
- resolution: "array.prototype.findlast@npm:1.2.4"
+"array.prototype.findlast@npm:^1.2.5":
+ version: 1.2.5
+ resolution: "array.prototype.findlast@npm:1.2.5"
dependencies:
- call-bind: "npm:^1.0.5"
+ call-bind: "npm:^1.0.7"
define-properties: "npm:^1.2.1"
- es-abstract: "npm:^1.22.3"
+ es-abstract: "npm:^1.23.2"
es-errors: "npm:^1.3.0"
+ es-object-atoms: "npm:^1.0.0"
es-shim-unscopables: "npm:^1.0.2"
- checksum: 10/1711e48058cabbad24cb694fa3721b760e56004758142c439880a19b9b206e3584b94bbad41e5f68e0da8785db1d09250061a46769baa90a0d2e09c05987c82d
+ checksum: 10/7dffcc665aa965718ad6de7e17ac50df0c5e38798c0a5bf9340cf24feb8594df6ec6f3fcbe714c1577728a1b18b5704b15669474b27bceeca91ef06ce2a23c31
languageName: node
linkType: hard
@@ -9837,7 +9845,7 @@ __metadata:
cors: "npm:2.8.5"
express: "npm:4.19.2"
morgan: "npm:1.10.0"
- nodemon: "npm:3.1.0"
+ nodemon: "npm:3.1.2"
p-queue: "npm:8.0.1"
languageName: unknown
linkType: soft
@@ -10493,13 +10501,6 @@ __metadata:
languageName: node
linkType: hard
-"chalk@npm:5.3.0":
- version: 5.3.0
- resolution: "chalk@npm:5.3.0"
- checksum: 10/6373caaab21bd64c405bfc4bd9672b145647fc9482657b5ea1d549b3b2765054e9d3d928870cdf764fb4aad67555f5061538ff247b8310f110c5c888d92397ea
- languageName: node
- linkType: hard
-
"chalk@npm:^2.0.0, chalk@npm:^2.4.2":
version: 2.4.2
resolution: "chalk@npm:2.4.2"
@@ -10531,6 +10532,13 @@ __metadata:
languageName: node
linkType: hard
+"chalk@npm:~5.3.0":
+ version: 5.3.0
+ resolution: "chalk@npm:5.3.0"
+ checksum: 10/6373caaab21bd64c405bfc4bd9672b145647fc9482657b5ea1d549b3b2765054e9d3d928870cdf764fb4aad67555f5061538ff247b8310f110c5c888d92397ea
+ languageName: node
+ linkType: hard
+
"char-regex@npm:^1.0.2":
version: 1.0.2
resolution: "char-regex@npm:1.0.2"
@@ -10606,9 +10614,9 @@ __metadata:
languageName: node
linkType: hard
-"chromatic@npm:^11.3.2":
- version: 11.4.0
- resolution: "chromatic@npm:11.4.0"
+"chromatic@npm:^11.4.0":
+ version: 11.5.1
+ resolution: "chromatic@npm:11.5.1"
peerDependencies:
"@chromatic-com/cypress": ^0.*.* || ^1.0.0
"@chromatic-com/playwright": ^0.*.* || ^1.0.0
@@ -10621,7 +10629,7 @@ __metadata:
chroma: dist/bin.js
chromatic: dist/bin.js
chromatic-cli: dist/bin.js
- checksum: 10/9f4c89cacf5351e334958f0e2be64e462b31372c3ee1d9468600d7de555f9b7aea3227e0edb5a730d77b84f118048223b3bc674dac65d92e91fabc99b621ecaa
+ checksum: 10/092406c78e3971b16221170f29581ae1d9ea4aa3c014e945aafeb8663cb44ebed97fbb8b36aedf93b0a336d4eca69440061ba8b9eb133f77a46059fe8452a691
languageName: node
linkType: hard
@@ -10889,13 +10897,6 @@ __metadata:
languageName: node
linkType: hard
-"commander@npm:12.1.0":
- version: 12.1.0
- resolution: "commander@npm:12.1.0"
- checksum: 10/cdaeb672d979816853a4eed7f1310a9319e8b976172485c2a6b437ed0db0a389a44cfb222bfbde772781efa9f215bdd1b936f80d6b249485b465c6cb906e1f93
- languageName: node
- linkType: hard
-
"commander@npm:^10.0.1":
version: 10.0.1
resolution: "commander@npm:10.0.1"
@@ -10931,6 +10932,13 @@ __metadata:
languageName: node
linkType: hard
+"commander@npm:~12.1.0":
+ version: 12.1.0
+ resolution: "commander@npm:12.1.0"
+ checksum: 10/cdaeb672d979816853a4eed7f1310a9319e8b976172485c2a6b437ed0db0a389a44cfb222bfbde772781efa9f215bdd1b936f80d6b249485b465c6cb906e1f93
+ languageName: node
+ linkType: hard
+
"common-tags@npm:^1.8.0":
version: 1.8.2
resolution: "common-tags@npm:1.8.2"
@@ -11194,10 +11202,12 @@ __metadata:
languageName: node
linkType: hard
-"crypto-random-string@npm:^2.0.0":
- version: 2.0.0
- resolution: "crypto-random-string@npm:2.0.0"
- checksum: 10/0283879f55e7c16fdceacc181f87a0a65c53bc16ffe1d58b9d19a6277adcd71900d02bb2c4843dd55e78c51e30e89b0fec618a7f170ebcc95b33182c28f05fd6
+"crypto-random-string@npm:^4.0.0":
+ version: 4.0.0
+ resolution: "crypto-random-string@npm:4.0.0"
+ dependencies:
+ type-fest: "npm:^1.0.1"
+ checksum: 10/cd5d7ae13803de53680aaed4c732f67209af5988cbeec5f6b29082020347c2d8849ca921b2008be7d6bd1d9d198c3c3697e7441d6d0d3da1bf51e9e4d2032149
languageName: node
linkType: hard
@@ -11513,7 +11523,7 @@ __metadata:
resolution: "cypress-studio@workspace:frontend/testing/cypress"
dependencies:
"@faker-js/faker": "npm:8.4.1"
- "@testing-library/cypress": "npm:10.0.1"
+ "@testing-library/cypress": "npm:10.0.2"
axe-core: "npm:4.9.1"
cypress: "npm:13.10.0"
cypress-axe: "npm:1.5.0"
@@ -11630,7 +11640,7 @@ __metadata:
languageName: node
linkType: hard
-"data-view-byte-length@npm:^1.0.0":
+"data-view-byte-length@npm:^1.0.0, data-view-byte-length@npm:^1.0.1":
version: 1.0.1
resolution: "data-view-byte-length@npm:1.0.1"
dependencies:
@@ -11668,7 +11678,7 @@ __metadata:
languageName: node
linkType: hard
-"debug@npm:4, debug@npm:4.3.4, debug@npm:^4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4":
+"debug@npm:4, debug@npm:^4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4":
version: 4.3.4
resolution: "debug@npm:4.3.4"
dependencies:
@@ -11689,6 +11699,18 @@ __metadata:
languageName: node
linkType: hard
+"debug@npm:~4.3.4":
+ version: 4.3.5
+ resolution: "debug@npm:4.3.5"
+ dependencies:
+ ms: "npm:2.1.2"
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+ checksum: 10/cb6eab424c410e07813ca1392888589972ce9a32b8829c6508f5e1f25f3c3e70a76731610ae55b4bbe58d1a2fffa1424b30e97fa8d394e49cd2656a9643aedd2
+ languageName: node
+ linkType: hard
+
"decimal.js@npm:^10.4.2":
version: 10.4.3
resolution: "decimal.js@npm:10.4.3"
@@ -11894,22 +11916,6 @@ __metadata:
languageName: node
linkType: hard
-"del@npm:^6.0.0":
- version: 6.1.1
- resolution: "del@npm:6.1.1"
- dependencies:
- globby: "npm:^11.0.1"
- graceful-fs: "npm:^4.2.4"
- is-glob: "npm:^4.0.1"
- is-path-cwd: "npm:^2.2.0"
- is-path-inside: "npm:^3.0.2"
- p-map: "npm:^4.0.0"
- rimraf: "npm:^3.0.2"
- slash: "npm:^3.0.0"
- checksum: 10/563288b73b8b19a7261c47fd21a330eeab6e2acd7c6208c49790dfd369127120dd7836cdf0c1eca216b77c94782a81507eac6b4734252d3bef2795cb366996b6
- languageName: node
- linkType: hard
-
"delayed-stream@npm:~1.0.0":
version: 1.0.0
resolution: "delayed-stream@npm:1.0.0"
@@ -12578,7 +12584,7 @@ __metadata:
languageName: node
linkType: hard
-"es-abstract@npm:^1.22.3, es-abstract@npm:^1.22.4":
+"es-abstract@npm:^1.22.3":
version: 1.23.0
resolution: "es-abstract@npm:1.23.0"
dependencies:
@@ -12631,6 +12637,60 @@ __metadata:
languageName: node
linkType: hard
+"es-abstract@npm:^1.23.0, es-abstract@npm:^1.23.2, es-abstract@npm:^1.23.3":
+ version: 1.23.3
+ resolution: "es-abstract@npm:1.23.3"
+ dependencies:
+ array-buffer-byte-length: "npm:^1.0.1"
+ arraybuffer.prototype.slice: "npm:^1.0.3"
+ available-typed-arrays: "npm:^1.0.7"
+ call-bind: "npm:^1.0.7"
+ data-view-buffer: "npm:^1.0.1"
+ data-view-byte-length: "npm:^1.0.1"
+ data-view-byte-offset: "npm:^1.0.0"
+ es-define-property: "npm:^1.0.0"
+ es-errors: "npm:^1.3.0"
+ es-object-atoms: "npm:^1.0.0"
+ es-set-tostringtag: "npm:^2.0.3"
+ es-to-primitive: "npm:^1.2.1"
+ function.prototype.name: "npm:^1.1.6"
+ get-intrinsic: "npm:^1.2.4"
+ get-symbol-description: "npm:^1.0.2"
+ globalthis: "npm:^1.0.3"
+ gopd: "npm:^1.0.1"
+ has-property-descriptors: "npm:^1.0.2"
+ has-proto: "npm:^1.0.3"
+ has-symbols: "npm:^1.0.3"
+ hasown: "npm:^2.0.2"
+ internal-slot: "npm:^1.0.7"
+ is-array-buffer: "npm:^3.0.4"
+ is-callable: "npm:^1.2.7"
+ is-data-view: "npm:^1.0.1"
+ is-negative-zero: "npm:^2.0.3"
+ is-regex: "npm:^1.1.4"
+ is-shared-array-buffer: "npm:^1.0.3"
+ is-string: "npm:^1.0.7"
+ is-typed-array: "npm:^1.1.13"
+ is-weakref: "npm:^1.0.2"
+ object-inspect: "npm:^1.13.1"
+ object-keys: "npm:^1.1.1"
+ object.assign: "npm:^4.1.5"
+ regexp.prototype.flags: "npm:^1.5.2"
+ safe-array-concat: "npm:^1.1.2"
+ safe-regex-test: "npm:^1.0.3"
+ string.prototype.trim: "npm:^1.2.9"
+ string.prototype.trimend: "npm:^1.0.8"
+ string.prototype.trimstart: "npm:^1.0.8"
+ typed-array-buffer: "npm:^1.0.2"
+ typed-array-byte-length: "npm:^1.0.1"
+ typed-array-byte-offset: "npm:^1.0.2"
+ typed-array-length: "npm:^1.0.6"
+ unbox-primitive: "npm:^1.0.2"
+ which-typed-array: "npm:^1.1.15"
+ checksum: 10/2da795a6a1ac5fc2c452799a409acc2e3692e06dc6440440b076908617188899caa562154d77263e3053bcd9389a07baa978ab10ac3b46acc399bd0c77be04cb
+ languageName: node
+ linkType: hard
+
"es-define-property@npm:^1.0.0":
version: 1.0.0
resolution: "es-define-property@npm:1.0.0"
@@ -12686,26 +12746,25 @@ __metadata:
languageName: node
linkType: hard
-"es-iterator-helpers@npm:^1.0.17":
- version: 1.0.17
- resolution: "es-iterator-helpers@npm:1.0.17"
+"es-iterator-helpers@npm:^1.0.19":
+ version: 1.0.19
+ resolution: "es-iterator-helpers@npm:1.0.19"
dependencies:
- asynciterator.prototype: "npm:^1.0.0"
call-bind: "npm:^1.0.7"
define-properties: "npm:^1.2.1"
- es-abstract: "npm:^1.22.4"
+ es-abstract: "npm:^1.23.3"
es-errors: "npm:^1.3.0"
- es-set-tostringtag: "npm:^2.0.2"
+ es-set-tostringtag: "npm:^2.0.3"
function-bind: "npm:^1.1.2"
get-intrinsic: "npm:^1.2.4"
globalthis: "npm:^1.0.3"
has-property-descriptors: "npm:^1.0.2"
- has-proto: "npm:^1.0.1"
+ has-proto: "npm:^1.0.3"
has-symbols: "npm:^1.0.3"
internal-slot: "npm:^1.0.7"
iterator.prototype: "npm:^1.1.2"
- safe-array-concat: "npm:^1.1.0"
- checksum: 10/42c6eb65368d34b556dac1cc8d34ba753eb526bc7d4594be3b77799440be78d31fddfd60717af2d9ce6d021de8346e7a573141d789821e38836e60441f93ccfd
+ safe-array-concat: "npm:^1.1.2"
+ checksum: 10/980a8081cf6798fe17fcea193b0448d784d72d76aca7240b10813207c67e3dc0d8a23992263870c4fc291da5a946935b0c56dec4fa1a9de8fee0165e4fa1fc58
languageName: node
linkType: hard
@@ -12723,6 +12782,15 @@ __metadata:
languageName: node
linkType: hard
+"es-object-atoms@npm:^1.0.0":
+ version: 1.0.0
+ resolution: "es-object-atoms@npm:1.0.0"
+ dependencies:
+ es-errors: "npm:^1.3.0"
+ checksum: 10/f8910cf477e53c0615f685c5c96210591841850871b81924fcf256bfbaa68c254457d994a4308c60d15b20805e7f61ce6abc669375e01a5349391a8c1767584f
+ languageName: node
+ linkType: hard
+
"es-set-tostringtag@npm:^2.0.1":
version: 2.0.1
resolution: "es-set-tostringtag@npm:2.0.1"
@@ -12734,7 +12802,7 @@ __metadata:
languageName: node
linkType: hard
-"es-set-tostringtag@npm:^2.0.2, es-set-tostringtag@npm:^2.0.3":
+"es-set-tostringtag@npm:^2.0.3":
version: 2.0.3
resolution: "es-set-tostringtag@npm:2.0.3"
dependencies:
@@ -13071,31 +13139,31 @@ __metadata:
languageName: node
linkType: hard
-"eslint-plugin-react@npm:7.34.1":
- version: 7.34.1
- resolution: "eslint-plugin-react@npm:7.34.1"
+"eslint-plugin-react@npm:7.34.2":
+ version: 7.34.2
+ resolution: "eslint-plugin-react@npm:7.34.2"
dependencies:
- array-includes: "npm:^3.1.7"
- array.prototype.findlast: "npm:^1.2.4"
+ array-includes: "npm:^3.1.8"
+ array.prototype.findlast: "npm:^1.2.5"
array.prototype.flatmap: "npm:^1.3.2"
array.prototype.toreversed: "npm:^1.1.2"
array.prototype.tosorted: "npm:^1.1.3"
doctrine: "npm:^2.1.0"
- es-iterator-helpers: "npm:^1.0.17"
+ es-iterator-helpers: "npm:^1.0.19"
estraverse: "npm:^5.3.0"
jsx-ast-utils: "npm:^2.4.1 || ^3.0.0"
minimatch: "npm:^3.1.2"
- object.entries: "npm:^1.1.7"
- object.fromentries: "npm:^2.0.7"
- object.hasown: "npm:^1.1.3"
- object.values: "npm:^1.1.7"
+ object.entries: "npm:^1.1.8"
+ object.fromentries: "npm:^2.0.8"
+ object.hasown: "npm:^1.1.4"
+ object.values: "npm:^1.2.0"
prop-types: "npm:^15.8.1"
resolve: "npm:^2.0.0-next.5"
semver: "npm:^6.3.1"
- string.prototype.matchall: "npm:^4.0.10"
+ string.prototype.matchall: "npm:^4.0.11"
peerDependencies:
eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8
- checksum: 10/ee059971065ea7e73ab5d8728774235c7dbf7a5e9f937c3b47e97f8fa9a5a96ab511d2ae6d5ec76a7e705ca666673d454f1e75a94033720819d041827f50f9c8
+ checksum: 10/6efccc29ad09a45fe1764089199e87b69b63a40152dd40cbbece639c6be4cda4306b58b15ba9b449f639705eb9a08ddc51b58308077c0394537769c455f976b7
languageName: node
linkType: hard
@@ -13366,23 +13434,6 @@ __metadata:
languageName: node
linkType: hard
-"execa@npm:8.0.1, execa@npm:^8.0.1":
- version: 8.0.1
- resolution: "execa@npm:8.0.1"
- dependencies:
- cross-spawn: "npm:^7.0.3"
- get-stream: "npm:^8.0.1"
- human-signals: "npm:^5.0.0"
- is-stream: "npm:^3.0.0"
- merge-stream: "npm:^2.0.0"
- npm-run-path: "npm:^5.1.0"
- onetime: "npm:^6.0.0"
- signal-exit: "npm:^4.1.0"
- strip-final-newline: "npm:^3.0.0"
- checksum: 10/d2ab5fe1e2bb92b9788864d0713f1fce9a07c4594e272c0c97bc18c90569897ab262e4ea58d27a694d288227a2e24f16f5e2575b44224ad9983b799dc7f1098d
- languageName: node
- linkType: hard
-
"execa@npm:^5.0.0, execa@npm:^5.1.1":
version: 5.1.1
resolution: "execa@npm:5.1.1"
@@ -13400,6 +13451,23 @@ __metadata:
languageName: node
linkType: hard
+"execa@npm:^8.0.1, execa@npm:~8.0.1":
+ version: 8.0.1
+ resolution: "execa@npm:8.0.1"
+ dependencies:
+ cross-spawn: "npm:^7.0.3"
+ get-stream: "npm:^8.0.1"
+ human-signals: "npm:^5.0.0"
+ is-stream: "npm:^3.0.0"
+ merge-stream: "npm:^2.0.0"
+ npm-run-path: "npm:^5.1.0"
+ onetime: "npm:^6.0.0"
+ signal-exit: "npm:^4.1.0"
+ strip-final-newline: "npm:^3.0.0"
+ checksum: 10/d2ab5fe1e2bb92b9788864d0713f1fce9a07c4594e272c0c97bc18c90569897ab262e4ea58d27a694d288227a2e24f16f5e2575b44224ad9983b799dc7f1098d
+ languageName: node
+ linkType: hard
+
"executable@npm:^4.1.1":
version: 4.1.1
resolution: "executable@npm:4.1.1"
@@ -14445,7 +14513,7 @@ __metadata:
languageName: node
linkType: hard
-"globby@npm:^11.0.1, globby@npm:^11.1.0":
+"globby@npm:^11.1.0":
version: 11.1.0
resolution: "globby@npm:11.1.0"
dependencies:
@@ -14667,7 +14735,7 @@ __metadata:
languageName: node
linkType: hard
-"hasown@npm:^2.0.1":
+"hasown@npm:^2.0.1, hasown@npm:^2.0.2":
version: 2.0.2
resolution: "hasown@npm:2.0.2"
dependencies:
@@ -15593,7 +15661,7 @@ __metadata:
languageName: node
linkType: hard
-"is-path-cwd@npm:^2.0.0, is-path-cwd@npm:^2.2.0":
+"is-path-cwd@npm:^2.0.0":
version: 2.2.0
resolution: "is-path-cwd@npm:2.2.0"
checksum: 10/46a840921bb8cc0dc7b5b423a14220e7db338072a4495743a8230533ce78812dc152548c86f4b828411fe98c5451959f07cf841c6a19f611e46600bd699e8048
@@ -16907,13 +16975,6 @@ __metadata:
languageName: node
linkType: hard
-"lilconfig@npm:3.1.1, lilconfig@npm:^3.1.1":
- version: 3.1.1
- resolution: "lilconfig@npm:3.1.1"
- checksum: 10/c80fbf98ae7d1daf435e16a83fe3c63743b9d92804cac6dc53ee081c7c265663645c3162d8a0d04ff1874f9c07df145519743317dee67843234c6ed279300f83
- languageName: node
- linkType: hard
-
"lilconfig@npm:^2.0.5":
version: 2.1.0
resolution: "lilconfig@npm:2.1.0"
@@ -16921,6 +16982,13 @@ __metadata:
languageName: node
linkType: hard
+"lilconfig@npm:^3.1.1, lilconfig@npm:~3.1.1":
+ version: 3.1.1
+ resolution: "lilconfig@npm:3.1.1"
+ checksum: 10/c80fbf98ae7d1daf435e16a83fe3c63743b9d92804cac6dc53ee081c7c265663645c3162d8a0d04ff1874f9c07df145519743317dee67843234c6ed279300f83
+ languageName: node
+ linkType: hard
+
"lines-and-columns@npm:^1.1.6":
version: 1.2.4
resolution: "lines-and-columns@npm:1.2.4"
@@ -16928,37 +16996,23 @@ __metadata:
languageName: node
linkType: hard
-"lint-staged@npm:15.2.4":
- version: 15.2.4
- resolution: "lint-staged@npm:15.2.4"
+"lint-staged@npm:15.2.5":
+ version: 15.2.5
+ resolution: "lint-staged@npm:15.2.5"
dependencies:
- chalk: "npm:5.3.0"
- commander: "npm:12.1.0"
- debug: "npm:4.3.4"
- execa: "npm:8.0.1"
- lilconfig: "npm:3.1.1"
- listr2: "npm:8.2.1"
- micromatch: "npm:4.0.6"
- pidtree: "npm:0.6.0"
- string-argv: "npm:0.3.2"
- yaml: "npm:2.4.2"
+ chalk: "npm:~5.3.0"
+ commander: "npm:~12.1.0"
+ debug: "npm:~4.3.4"
+ execa: "npm:~8.0.1"
+ lilconfig: "npm:~3.1.1"
+ listr2: "npm:~8.2.1"
+ micromatch: "npm:~4.0.7"
+ pidtree: "npm:~0.6.0"
+ string-argv: "npm:~0.3.2"
+ yaml: "npm:~2.4.2"
bin:
lint-staged: bin/lint-staged.js
- checksum: 10/98df6520e71d395917208bf953fd2e39f58bd47eebd3889bd44739017643d02221c4e7068e3f5a8e2d0cd32c233a846227c9bb29ec7e02cbcc7b76650e5ec4d6
- languageName: node
- linkType: hard
-
-"listr2@npm:8.2.1":
- version: 8.2.1
- resolution: "listr2@npm:8.2.1"
- dependencies:
- cli-truncate: "npm:^4.0.0"
- colorette: "npm:^2.0.20"
- eventemitter3: "npm:^5.0.1"
- log-update: "npm:^6.0.0"
- rfdc: "npm:^1.3.1"
- wrap-ansi: "npm:^9.0.0"
- checksum: 10/1d33348682fee7af49c91d508f970fb58897fbc722a17ae6b9d4d904c909e105ead8c123652238bc99462b1e323c56e6e62877a03d788ca32fe706cfec283789
+ checksum: 10/2cb8e14e532a4de0a338da44dc5e22f94581390b988ba3d345d1132d592d9ce50be50846c9a9d25eaffaf3f1f634e0a056598f6abb705269ac21d0fd3bad3a45
languageName: node
linkType: hard
@@ -16983,6 +17037,20 @@ __metadata:
languageName: node
linkType: hard
+"listr2@npm:~8.2.1":
+ version: 8.2.1
+ resolution: "listr2@npm:8.2.1"
+ dependencies:
+ cli-truncate: "npm:^4.0.0"
+ colorette: "npm:^2.0.20"
+ eventemitter3: "npm:^5.0.1"
+ log-update: "npm:^6.0.0"
+ rfdc: "npm:^1.3.1"
+ wrap-ansi: "npm:^9.0.0"
+ checksum: 10/1d33348682fee7af49c91d508f970fb58897fbc722a17ae6b9d4d904c909e105ead8c123652238bc99462b1e323c56e6e62877a03d788ca32fe706cfec283789
+ languageName: node
+ linkType: hard
+
"loader-runner@npm:^4.2.0":
version: 4.3.0
resolution: "loader-runner@npm:4.3.0"
@@ -17388,16 +17456,6 @@ __metadata:
languageName: node
linkType: hard
-"micromatch@npm:4.0.6":
- version: 4.0.6
- resolution: "micromatch@npm:4.0.6"
- dependencies:
- braces: "npm:^3.0.3"
- picomatch: "npm:^4.0.2"
- checksum: 10/ed95dc8d00dbe3795a0daf0f19f411714f4b39d888824f7b460f4bdbd8952b06628c64704c05b319aca27e2418628a6286b5595890ce9d0c53e5ae962435c83f
- languageName: node
- linkType: hard
-
"micromatch@npm:^4.0.0, micromatch@npm:^4.0.2, micromatch@npm:^4.0.4":
version: 4.0.5
resolution: "micromatch@npm:4.0.5"
@@ -17408,6 +17466,16 @@ __metadata:
languageName: node
linkType: hard
+"micromatch@npm:~4.0.7":
+ version: 4.0.7
+ resolution: "micromatch@npm:4.0.7"
+ dependencies:
+ braces: "npm:^3.0.3"
+ picomatch: "npm:^2.3.1"
+ checksum: 10/a11ed1cb67dcbbe9a5fc02c4062cf8bb0157d73bf86956003af8dcfdf9b287f9e15ec0f6d6925ff6b8b5b496202335e497b01de4d95ef6cf06411bc5e5c474a0
+ languageName: node
+ linkType: hard
+
"mime-db@npm:1.52.0, mime-db@npm:>= 1.43.0 < 2":
version: 1.52.0
resolution: "mime-db@npm:1.52.0"
@@ -17919,9 +17987,9 @@ __metadata:
languageName: node
linkType: hard
-"nodemon@npm:3.1.0":
- version: 3.1.0
- resolution: "nodemon@npm:3.1.0"
+"nodemon@npm:3.1.2":
+ version: 3.1.2
+ resolution: "nodemon@npm:3.1.2"
dependencies:
chokidar: "npm:^3.5.2"
debug: "npm:^4"
@@ -17935,7 +18003,7 @@ __metadata:
undefsafe: "npm:^2.0.5"
bin:
nodemon: bin/nodemon.js
- checksum: 10/a8757f3eda5e11fbe0e50ef47177d5e86cf8a22e99723373100d37d5f25fb758280419c02d286210d242d0675adf5ef0d61052948f10c8318d656761d3dfa2b1
+ checksum: 10/e5a87e42c874f9504fa5740c65692978c1f8444e9250598f167a3d55a90d3189a91790d35b4a2a0991b05d574f2679978449c6190b698a5c6e3f0343b5dfcac0
languageName: node
linkType: hard
@@ -18121,6 +18189,17 @@ __metadata:
languageName: node
linkType: hard
+"object.entries@npm:^1.1.8":
+ version: 1.1.8
+ resolution: "object.entries@npm:1.1.8"
+ dependencies:
+ call-bind: "npm:^1.0.7"
+ define-properties: "npm:^1.2.1"
+ es-object-atoms: "npm:^1.0.0"
+ checksum: 10/2301918fbd1ee697cf6ff7cd94f060c738c0a7d92b22fd24c7c250e9b593642c9707ad2c44d339303c1439c5967d8964251cdfc855f7f6ec55db2dd79e8dc2a7
+ languageName: node
+ linkType: hard
+
"object.fromentries@npm:^2.0.7":
version: 2.0.7
resolution: "object.fromentries@npm:2.0.7"
@@ -18132,6 +18211,18 @@ __metadata:
languageName: node
linkType: hard
+"object.fromentries@npm:^2.0.8":
+ version: 2.0.8
+ resolution: "object.fromentries@npm:2.0.8"
+ dependencies:
+ call-bind: "npm:^1.0.7"
+ define-properties: "npm:^1.2.1"
+ es-abstract: "npm:^1.23.2"
+ es-object-atoms: "npm:^1.0.0"
+ checksum: 10/5b2e80f7af1778b885e3d06aeb335dcc86965e39464671adb7167ab06ac3b0f5dd2e637a90d8ebd7426d69c6f135a4753ba3dd7d0fe2a7030cf718dcb910fd92
+ languageName: node
+ linkType: hard
+
"object.groupby@npm:^1.0.1":
version: 1.0.1
resolution: "object.groupby@npm:1.0.1"
@@ -18144,13 +18235,14 @@ __metadata:
languageName: node
linkType: hard
-"object.hasown@npm:^1.1.3":
- version: 1.1.3
- resolution: "object.hasown@npm:1.1.3"
+"object.hasown@npm:^1.1.4":
+ version: 1.1.4
+ resolution: "object.hasown@npm:1.1.4"
dependencies:
- define-properties: "npm:^1.2.0"
- es-abstract: "npm:^1.22.1"
- checksum: 10/735679729c25a4e0d3713adf5df9861d862f0453e87ada4d991b75cd4225365dec61a08435e1127f42c9cc1adfc8e952fa5dca75364ebda6539dadf4721dc9c4
+ define-properties: "npm:^1.2.1"
+ es-abstract: "npm:^1.23.2"
+ es-object-atoms: "npm:^1.0.0"
+ checksum: 10/797385577b3ef3c0d19333e03ed34bc7987978ae1ee1245069c9922e17d1128265187f729dc610260d03f8d418af26fcd7919b423793bf0af9099d9f08367d69
languageName: node
linkType: hard
@@ -18176,6 +18268,17 @@ __metadata:
languageName: node
linkType: hard
+"object.values@npm:^1.2.0":
+ version: 1.2.0
+ resolution: "object.values@npm:1.2.0"
+ dependencies:
+ call-bind: "npm:^1.0.7"
+ define-properties: "npm:^1.2.1"
+ es-object-atoms: "npm:^1.0.0"
+ checksum: 10/db2e498019c354428c5dd30d02980d920ac365b155fce4dcf63eb9433f98ccf0f72624309e182ce7cc227c95e45d474e1d483418e60de2293dd23fa3ebe34903
+ languageName: node
+ linkType: hard
+
"objectorarray@npm:^1.0.5":
version: 1.0.5
resolution: "objectorarray@npm:1.0.5"
@@ -18658,14 +18761,7 @@ __metadata:
languageName: node
linkType: hard
-"picomatch@npm:^4.0.2":
- version: 4.0.2
- resolution: "picomatch@npm:4.0.2"
- checksum: 10/ce617b8da36797d09c0baacb96ca8a44460452c89362d7cb8f70ca46b4158ba8bc3606912de7c818eb4a939f7f9015cef3c766ec8a0c6bfc725fdc078e39c717
- languageName: node
- linkType: hard
-
-"pidtree@npm:0.6.0":
+"pidtree@npm:~0.6.0":
version: 0.6.0
resolution: "pidtree@npm:0.6.0"
bin:
@@ -20722,7 +20818,7 @@ __metadata:
languageName: node
linkType: hard
-"safe-array-concat@npm:^1.1.0":
+"safe-array-concat@npm:^1.1.0, safe-array-concat@npm:^1.1.2":
version: 1.1.2
resolution: "safe-array-concat@npm:1.1.2"
dependencies:
@@ -21073,18 +21169,6 @@ __metadata:
languageName: node
linkType: hard
-"set-function-name@npm:^2.0.0":
- version: 2.0.2
- resolution: "set-function-name@npm:2.0.2"
- dependencies:
- define-data-property: "npm:^1.1.4"
- es-errors: "npm:^1.3.0"
- functions-have-names: "npm:^1.2.3"
- has-property-descriptors: "npm:^1.0.2"
- checksum: 10/c7614154a53ebf8c0428a6c40a3b0b47dac30587c1a19703d1b75f003803f73cdfa6a93474a9ba678fa565ef5fbddc2fae79bca03b7d22ab5fd5163dbe571a74
- languageName: node
- linkType: hard
-
"set-function-name@npm:^2.0.1":
version: 2.0.1
resolution: "set-function-name@npm:2.0.1"
@@ -21096,6 +21180,18 @@ __metadata:
languageName: node
linkType: hard
+"set-function-name@npm:^2.0.2":
+ version: 2.0.2
+ resolution: "set-function-name@npm:2.0.2"
+ dependencies:
+ define-data-property: "npm:^1.1.4"
+ es-errors: "npm:^1.3.0"
+ functions-have-names: "npm:^1.2.3"
+ has-property-descriptors: "npm:^1.0.2"
+ checksum: 10/c7614154a53ebf8c0428a6c40a3b0b47dac30587c1a19703d1b75f003803f73cdfa6a93474a9ba678fa565ef5fbddc2fae79bca03b7d22ab5fd5163dbe571a74
+ languageName: node
+ linkType: hard
+
"set-harmonic-interval@npm:^1.0.1":
version: 1.0.1
resolution: "set-harmonic-interval@npm:1.0.1"
@@ -21586,14 +21682,14 @@ __metadata:
linkType: hard
"storybook@npm:^8.0.4":
- version: 8.1.3
- resolution: "storybook@npm:8.1.3"
+ version: 8.1.5
+ resolution: "storybook@npm:8.1.5"
dependencies:
- "@storybook/cli": "npm:8.1.3"
+ "@storybook/cli": "npm:8.1.5"
bin:
sb: ./index.js
storybook: ./index.js
- checksum: 10/05b143b311a97587bc8da41e18de2070697877f1133c5389d281598a70ab0caf4148d90f138674114ecc9f2f9b47322c542da35cc3c57508f71f4efbd3e9ae0c
+ checksum: 10/92e02edb8e38213bd249ea5b7de1e7790ef0ca196fa835edb143d9dce4d2b3414f080ed19bc584261ae9a818f4225011a69c527052a1c2de4e8671b2994e3b4f
languageName: node
linkType: hard
@@ -21604,7 +21700,7 @@ __metadata:
languageName: node
linkType: hard
-"string-argv@npm:0.3.2":
+"string-argv@npm:~0.3.2":
version: 0.3.2
resolution: "string-argv@npm:0.3.2"
checksum: 10/f9d3addf887026b4b5f997a271149e93bf71efc8692e7dc0816e8807f960b18bcb9787b45beedf0f97ff459575ee389af3f189d8b649834cac602f2e857e75af
@@ -21654,20 +21750,23 @@ __metadata:
languageName: node
linkType: hard
-"string.prototype.matchall@npm:^4.0.10":
- version: 4.0.10
- resolution: "string.prototype.matchall@npm:4.0.10"
+"string.prototype.matchall@npm:^4.0.11":
+ version: 4.0.11
+ resolution: "string.prototype.matchall@npm:4.0.11"
dependencies:
- call-bind: "npm:^1.0.2"
- define-properties: "npm:^1.2.0"
- es-abstract: "npm:^1.22.1"
- get-intrinsic: "npm:^1.2.1"
+ call-bind: "npm:^1.0.7"
+ define-properties: "npm:^1.2.1"
+ es-abstract: "npm:^1.23.2"
+ es-errors: "npm:^1.3.0"
+ es-object-atoms: "npm:^1.0.0"
+ get-intrinsic: "npm:^1.2.4"
+ gopd: "npm:^1.0.1"
has-symbols: "npm:^1.0.3"
- internal-slot: "npm:^1.0.5"
- regexp.prototype.flags: "npm:^1.5.0"
- set-function-name: "npm:^2.0.0"
- side-channel: "npm:^1.0.4"
- checksum: 10/0f7a1a7f91790cd45f804039a16bc6389c8f4f25903e648caa3eea080b019a5c7b0cac2ca83976646140c2332b159042140bf389f23675609d869dd52450cddc
+ internal-slot: "npm:^1.0.7"
+ regexp.prototype.flags: "npm:^1.5.2"
+ set-function-name: "npm:^2.0.2"
+ side-channel: "npm:^1.0.6"
+ checksum: 10/a902ff4500f909f2a08e55cc5ab1ffbbc905f603b36837674370ee3921058edd0392147e15891910db62a2f31ace2adaf065eaa3bc6e9810bdbc8ca48e05a7b5
languageName: node
linkType: hard
@@ -21693,6 +21792,18 @@ __metadata:
languageName: node
linkType: hard
+"string.prototype.trim@npm:^1.2.9":
+ version: 1.2.9
+ resolution: "string.prototype.trim@npm:1.2.9"
+ dependencies:
+ call-bind: "npm:^1.0.7"
+ define-properties: "npm:^1.2.1"
+ es-abstract: "npm:^1.23.0"
+ es-object-atoms: "npm:^1.0.0"
+ checksum: 10/b2170903de6a2fb5a49bb8850052144e04b67329d49f1343cdc6a87cb24fb4e4b8ad00d3e273a399b8a3d8c32c89775d93a8f43cb42fbff303f25382079fb58a
+ languageName: node
+ linkType: hard
+
"string.prototype.trimend@npm:^1.0.6":
version: 1.0.6
resolution: "string.prototype.trimend@npm:1.0.6"
@@ -21715,6 +21826,17 @@ __metadata:
languageName: node
linkType: hard
+"string.prototype.trimend@npm:^1.0.8":
+ version: 1.0.8
+ resolution: "string.prototype.trimend@npm:1.0.8"
+ dependencies:
+ call-bind: "npm:^1.0.7"
+ define-properties: "npm:^1.2.1"
+ es-object-atoms: "npm:^1.0.0"
+ checksum: 10/c2e862ae724f95771da9ea17c27559d4eeced9208b9c20f69bbfcd1b9bc92375adf8af63a103194dba17c4cc4a5cb08842d929f415ff9d89c062d44689c8761b
+ languageName: node
+ linkType: hard
+
"string.prototype.trimstart@npm:^1.0.6":
version: 1.0.6
resolution: "string.prototype.trimstart@npm:1.0.6"
@@ -21737,6 +21859,17 @@ __metadata:
languageName: node
linkType: hard
+"string.prototype.trimstart@npm:^1.0.8":
+ version: 1.0.8
+ resolution: "string.prototype.trimstart@npm:1.0.8"
+ dependencies:
+ call-bind: "npm:^1.0.7"
+ define-properties: "npm:^1.2.1"
+ es-object-atoms: "npm:^1.0.0"
+ checksum: 10/160167dfbd68e6f7cb9f51a16074eebfce1571656fc31d40c3738ca9e30e35496f2c046fe57b6ad49f65f238a152be8c86fd9a2dd58682b5eba39dad995b3674
+ languageName: node
+ linkType: hard
+
"string_decoder@npm:^1.1.1":
version: 1.3.0
resolution: "string_decoder@npm:1.3.0"
@@ -22097,10 +22230,10 @@ __metadata:
languageName: node
linkType: hard
-"temp-dir@npm:^2.0.0":
- version: 2.0.0
- resolution: "temp-dir@npm:2.0.0"
- checksum: 10/cc4f0404bf8d6ae1a166e0e64f3f409b423f4d1274d8c02814a59a5529f07db6cd070a749664141b992b2c1af337fa9bb451a460a43bb9bcddc49f235d3115aa
+"temp-dir@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "temp-dir@npm:3.0.0"
+ checksum: 10/577211e995d1d584dd60f1469351d45e8a5b4524e4a9e42d3bdd12cfde1d0bb8f5898311bef24e02aaafb69514c1feb58c7b4c33dcec7129da3b0861a4ca935b
languageName: node
linkType: hard
@@ -22113,16 +22246,15 @@ __metadata:
languageName: node
linkType: hard
-"tempy@npm:^1.0.1":
- version: 1.0.1
- resolution: "tempy@npm:1.0.1"
+"tempy@npm:^3.1.0":
+ version: 3.1.0
+ resolution: "tempy@npm:3.1.0"
dependencies:
- del: "npm:^6.0.0"
- is-stream: "npm:^2.0.0"
- temp-dir: "npm:^2.0.0"
- type-fest: "npm:^0.16.0"
- unique-string: "npm:^2.0.0"
- checksum: 10/e3a3857cd102db84c484b8e878203b496f0e927025b7c60dd118c0c9a0962f4589321c6b3093185d529576af5c58be65d755e72c2a6ad009ff340ab8cbbe4d33
+ is-stream: "npm:^3.0.0"
+ temp-dir: "npm:^3.0.0"
+ type-fest: "npm:^2.12.2"
+ unique-string: "npm:^3.0.0"
+ checksum: 10/f5540bc24dcd9d41ab0b31e9eed73c3ef825080f1c8b1e854e4b73059155c889f72f5f7c15e8cd462d59aa10c9726e423c81d6a365d614b538c6cc78a1209cc6
languageName: node
linkType: hard
@@ -22390,8 +22522,8 @@ __metadata:
linkType: hard
"ts-jest@npm:^29.1.1":
- version: 29.1.3
- resolution: "ts-jest@npm:29.1.3"
+ version: 29.1.4
+ resolution: "ts-jest@npm:29.1.4"
dependencies:
bs-logger: "npm:0.x"
fast-json-stable-stringify: "npm:2.x"
@@ -22421,7 +22553,7 @@ __metadata:
optional: true
bin:
ts-jest: cli.js
- checksum: 10/cc1f608bb5859e112ffb8a6d84ddb5c20954b7ec8c89a8c7f95e373368d8946b5843594fe7779078eec2b7e825962848f1a1ba7a44c71b8a08ed4e75d3a3f8d8
+ checksum: 10/3103c0e2f9937ae6bb51918105883565bb2d11cae1121ae20aedd1c4374f843341463a4a1986e02a958d119be0d3a9b996d761bc4aac85152a29385e609fed3c
languageName: node
linkType: hard
@@ -22592,13 +22724,6 @@ __metadata:
languageName: node
linkType: hard
-"type-fest@npm:^0.16.0":
- version: 0.16.0
- resolution: "type-fest@npm:0.16.0"
- checksum: 10/fd8c47ccb90e9fe7bae8bfc0e116e200e096120200c1ab1737bf0bc9334b344dd4925f876ed698174ffd58cd179bb56a55467be96aedc22d5d72748eac428bc8
- languageName: node
- linkType: hard
-
"type-fest@npm:^0.20.2":
version: 0.20.2
resolution: "type-fest@npm:0.20.2"
@@ -22627,7 +22752,14 @@ __metadata:
languageName: node
linkType: hard
-"type-fest@npm:^2.19.0, type-fest@npm:~2.19":
+"type-fest@npm:^1.0.1":
+ version: 1.4.0
+ resolution: "type-fest@npm:1.4.0"
+ checksum: 10/89875c247564601c2650bacad5ff80b859007fbdb6c9e43713ae3ffa3f584552eea60f33711dd762e16496a1ab4debd409822627be14097d9a17e39c49db591a
+ languageName: node
+ linkType: hard
+
+"type-fest@npm:^2.12.2, type-fest@npm:^2.19.0, type-fest@npm:~2.19":
version: 2.19.0
resolution: "type-fest@npm:2.19.0"
checksum: 10/7bf9e8fdf34f92c8bb364c0af14ca875fac7e0183f2985498b77be129dc1b3b1ad0a6b3281580f19e48c6105c037fb966ad9934520c69c6434d17fd0af4eed78
@@ -22750,6 +22882,20 @@ __metadata:
languageName: node
linkType: hard
+"typed-array-length@npm:^1.0.6":
+ version: 1.0.6
+ resolution: "typed-array-length@npm:1.0.6"
+ dependencies:
+ call-bind: "npm:^1.0.7"
+ for-each: "npm:^0.3.3"
+ gopd: "npm:^1.0.1"
+ has-proto: "npm:^1.0.3"
+ is-typed-array: "npm:^1.1.13"
+ possible-typed-array-names: "npm:^1.0.0"
+ checksum: 10/05e96cf4ff836743ebfc593d86133b8c30e83172cb5d16c56814d7bacfed57ce97e87ada9c4b2156d9aaa59f75cdef01c25bd9081c7826e0b869afbefc3e8c39
+ languageName: node
+ linkType: hard
+
"typescript-compare@npm:^0.0.2":
version: 0.0.2
resolution: "typescript-compare@npm:0.0.2"
@@ -22919,12 +23065,12 @@ __metadata:
languageName: node
linkType: hard
-"unique-string@npm:^2.0.0":
- version: 2.0.0
- resolution: "unique-string@npm:2.0.0"
+"unique-string@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "unique-string@npm:3.0.0"
dependencies:
- crypto-random-string: "npm:^2.0.0"
- checksum: 10/107cae65b0b618296c2c663b8e52e4d1df129e9af04ab38d53b4f2189e96da93f599c85f4589b7ffaf1a11c9327cbb8a34f04c71b8d4950d3e385c2da2a93828
+ crypto-random-string: "npm:^4.0.0"
+ checksum: 10/1a1e2e7d02eab1bb10f720475da735e1990c8a5ff34edd1a3b6bc31590cb4210b7a1233d779360cc622ce11c211e43afa1628dd658f35d3e6a89964b622940df
languageName: node
linkType: hard
@@ -23595,7 +23741,7 @@ __metadata:
languageName: node
linkType: hard
-"which-typed-array@npm:^1.1.14, which-typed-array@npm:^1.1.2":
+"which-typed-array@npm:^1.1.14, which-typed-array@npm:^1.1.15, which-typed-array@npm:^1.1.2":
version: 1.1.15
resolution: "which-typed-array@npm:1.1.15"
dependencies:
@@ -23844,15 +23990,6 @@ __metadata:
languageName: node
linkType: hard
-"yaml@npm:2.4.2":
- version: 2.4.2
- resolution: "yaml@npm:2.4.2"
- bin:
- yaml: bin.mjs
- checksum: 10/6eafbcd68dead734035f6f72af21bd820c29214caf7d8e40c595671a3c908535cef8092b9660a1c055c5833aa148aa640e0c5fa4adb5af2dacd6d28296ccd81c
- languageName: node
- linkType: hard
-
"yaml@npm:^1.10.0, yaml@npm:^1.10.2":
version: 1.10.2
resolution: "yaml@npm:1.10.2"
@@ -23860,6 +23997,15 @@ __metadata:
languageName: node
linkType: hard
+"yaml@npm:~2.4.2":
+ version: 2.4.3
+ resolution: "yaml@npm:2.4.3"
+ bin:
+ yaml: bin.mjs
+ checksum: 10/a618d3b968e3fb601cf7266db6e250e5cdd3b81853039a59108145202d5055b47c2d23a8e1ab661f8ba3ba095dcf6b4bb55cea2c14b97a418e5b059d27f8814e
+ languageName: node
+ linkType: hard
+
"yargs-parser@npm:^21.0.1, yargs-parser@npm:^21.1.1":
version: 21.1.1
resolution: "yargs-parser@npm:21.1.1"
From 6258b3b3df3b660417930b72ff54a0c17d654625 Mon Sep 17 00:00:00 2001
From: andreastanderen <71079896+standeren@users.noreply.github.com>
Date: Tue, 4 Jun 2024 13:14:47 +0200
Subject: [PATCH 24/27] 12807 the app developer should be able to add
paymentDetails component to the app (#12879)
* Update and run script to get component schemas from v4 without specific minor-version
* Add paymentDetails as an advanced component
* Add texts etc for updated component schemas
* Fix tests
* Fix PR comments
---
frontend/language/src/nb.json | 23 +++++-
.../src/react/icons/PaymentDetailsIcon.tsx | 36 +++++++++
.../studio-icons/src/react/icons/index.ts | 1 +
.../src/types/ComponentSpecificConfig.ts | 1 +
.../shared/src/types/ComponentType.ts | 1 +
.../components/config/FormComponentConfig.tsx | 2 +-
.../PageConfigPanel.module.css | 3 +
.../PageConfigPanel/PageConfigPanel.tsx | 3 +-
.../config/FormComponentConfig.test.tsx | 58 +++++++++++++-
.../components/config/FormComponentConfig.tsx | 6 +-
.../config/editModal/EditNumberValue.tsx | 3 +-
.../ux-editor/src/data/formItemConfig.ts | 8 ++
.../useComponentPropertyDescription.test.ts | 37 +++++++++
.../hooks/useComponentPropertyDescription.ts | 14 ++++
.../src/testing/componentSchemaMocks.ts | 2 +
.../json/component/Address.schema.v1.json | 24 +++++-
.../json/component/Button.schema.v1.json | 7 +-
.../json/component/Checkboxes.schema.v1.json | 1 +
.../json/component/Dropdown.schema.v1.json | 1 +
.../json/component/Group.schema.v1.json | 6 ++
.../json/component/Input.schema.v1.json | 2 +-
.../json/component/LikertItem.schema.v1.json | 1 +
.../component/MultipleSelect.schema.v1.json | 1 +
.../json/component/Payment.schema.v1.json | 10 +--
.../component/PaymentDetails.schema.v1.json | 77 +++++++++++++++++++
.../component/RadioButtons.schema.v1.json | 1 +
.../component/RepeatingGroup.schema.v1.json | 41 ++++++++++
.../json/component/Summary.schema.v1.json | 13 +++-
frontend/scripts/componentSchemas/version.ts | 4 +-
29 files changed, 359 insertions(+), 28 deletions(-)
create mode 100644 frontend/libs/studio-icons/src/react/icons/PaymentDetailsIcon.tsx
create mode 100644 frontend/packages/ux-editor/src/components/Properties/PageConfigPanel/PageConfigPanel.module.css
create mode 100644 frontend/packages/ux-editor/src/hooks/useComponentPropertyDescription.test.ts
create mode 100644 frontend/packages/ux-editor/src/hooks/useComponentPropertyDescription.ts
create mode 100644 frontend/packages/ux-editor/src/testing/schemas/json/component/PaymentDetails.schema.v1.json
diff --git a/frontend/language/src/nb.json b/frontend/language/src/nb.json
index 9f91a1453ec..27254d0babd 100644
--- a/frontend/language/src/nb.json
+++ b/frontend/language/src/nb.json
@@ -1606,9 +1606,11 @@
"ux_editor.component_other_properties_title": "Andre innstillinger",
"ux_editor.component_properties.action": "Aksjon",
"ux_editor.component_properties.actions": "Aksjoner",
+ "ux_editor.component_properties.addButton": "'Legg til'-knapp",
"ux_editor.component_properties.alertOnChange": "Bruker skal få advarsel når de gjør endring",
"ux_editor.component_properties.alertOnDelete": "Bruker skal få advarsel når de sletter",
"ux_editor.component_properties.align": "Plassering*",
+ "ux_editor.component_properties.alwaysShowAddButton": "Vis alltid 'Legg til'-knapp",
"ux_editor.component_properties.attribution": "Opphav",
"ux_editor.component_properties.autocomplete": "HTML autofullfør",
"ux_editor.component_properties.autocomplete_default": "Standard",
@@ -1624,9 +1626,11 @@
"ux_editor.component_properties.dataListId": "Id på dataliste",
"ux_editor.component_properties.dataTypeIds": "Liste med ID på datatyper som skal vises",
"ux_editor.component_properties.dateSent": "Dato instansen ble sendt inn",
+ "ux_editor.component_properties.deleteButton": "'Slett'-knapp",
"ux_editor.component_properties.display": "Visning",
"ux_editor.component_properties.displayMode": "Visningsmodus (enkel eller liste)",
"ux_editor.component_properties.edit": "Redigeringsvalg",
+ "ux_editor.component_properties.editButton": "'Endre'-knapp",
"ux_editor.component_properties.elements": "Elementer som skal vises (når ingen er satt vises alle)",
"ux_editor.component_properties.excludedChildren": "Komponenter som skal ekskluderes fra gruppens oppsummering",
"ux_editor.component_properties.filter": "Filter",
@@ -1639,7 +1643,7 @@
"ux_editor.component_properties.hidden": "Feltet skal skjules",
"ux_editor.component_properties.hiddenRow": "Definer hvilke rader som skal skjules",
"ux_editor.component_properties.hideBottomBorder": "Skjul skillelinje under komponenten",
- "ux_editor.component_properties.hideChangeButton": "Skjul \"Endre\"-knapp",
+ "ux_editor.component_properties.hideChangeButton": "Skjul 'Endre'-knapp",
"ux_editor.component_properties.id": "ID",
"ux_editor.component_properties.image": "Bildeinnstillinger (image)",
"ux_editor.component_properties.includePDF": "Inkluder PDF",
@@ -1658,6 +1662,8 @@
"ux_editor.component_properties.minDate": "Tidligste dato",
"ux_editor.component_properties.minNumberOfAttachments": "Minimum antall vedlegg",
"ux_editor.component_properties.mode": "Modus",
+ "ux_editor.component_properties.multiPage": "Fordel gruppen over flere sider",
+ "ux_editor.component_properties.nextButton": "Vis 'Neste'-knapp",
"ux_editor.component_properties.openInNewTab": "Lenken skal åpnes i ny fane",
"ux_editor.component_properties.optionalIndicator": "Vis valgfri-indikator på ledetekst",
"ux_editor.component_properties.options": "Alternativer",
@@ -1675,7 +1681,10 @@
"ux_editor.component_properties.rows": "Rader",
"ux_editor.component_properties.rowsAfter": "Rader etter",
"ux_editor.component_properties.rowsBefore": "Rader før",
+ "ux_editor.component_properties.rowsPerPage": "Rader per side",
"ux_editor.component_properties.sandbox": "Sandbox",
+ "ux_editor.component_properties.saveAndNextButton": "'Lagre og neste'-knapp",
+ "ux_editor.component_properties.saveButton": "'Lagre'-knapp",
"ux_editor.component_properties.saveWhileTyping": "Overstyr tidsintervall for lagring når bruker skriver",
"ux_editor.component_properties.secure": "Bruk sikret versjon av API for å hente valg (secure)",
"ux_editor.component_properties.select_all_attachments": "Alle vedlegg",
@@ -1693,6 +1702,7 @@
"ux_editor.component_properties.sortableColumns": "Kolonner som kan sorteres",
"ux_editor.component_properties.source": "Kilde (source)",
"ux_editor.component_properties.src": "Kilde*",
+ "ux_editor.component_properties.stickyHeader": "Fest tittelraden",
"ux_editor.component_properties.style": "Stil",
"ux_editor.component_properties.subdomains": "Subdomener (kommaseparert)",
"ux_editor.component_properties.tableColumns": "Innstillinger for kolonner",
@@ -1713,6 +1723,8 @@
"ux_editor.component_properties.variant": "Variant",
"ux_editor.component_properties.width": "Bredde*",
"ux_editor.component_properties.zoom": "Zoom",
+ "ux_editor.component_properties_description.pageBreak": "Valgfri sideskift før eller etter komponenten i PDF",
+ "ux_editor.component_properties_description.pagination": "Pagineringsvalg for repeterende gruppe",
"ux_editor.component_title.Accordion": "Accordion",
"ux_editor.component_title.AccordionGroup": "Accordiongruppe",
"ux_editor.component_title.ActionButton": "Aksjonsknapp",
@@ -1751,6 +1763,7 @@
"ux_editor.component_title.Panel": "Informativ melding",
"ux_editor.component_title.Paragraph": "Paragraf",
"ux_editor.component_title.Payment": "Betaling",
+ "ux_editor.component_title.PaymentDetails": "Betalingsdetaljer",
"ux_editor.component_title.PrintButton": "Utskriftsknapp",
"ux_editor.component_title.RadioButtons": "Radioknapper",
"ux_editor.component_title.RepeatingGroup": "Repeterende gruppe",
@@ -1867,6 +1880,7 @@
"ux_editor.modal_properties_data_model_label.careOf": "C/O",
"ux_editor.modal_properties_data_model_label.group": "Repeterende gruppe",
"ux_editor.modal_properties_data_model_label.houseNumber": "Gatenummer",
+ "ux_editor.modal_properties_data_model_label.label": "Visningsverdi",
"ux_editor.modal_properties_data_model_label.list": "Flere vedlegg",
"ux_editor.modal_properties_data_model_label.metadata": "Metadata",
"ux_editor.modal_properties_data_model_label.postPlace": "Poststed",
@@ -1933,6 +1947,7 @@
"ux_editor.modal_properties_textResourceBindings_back_add": "Legg til tekst på tilbake-knapp",
"ux_editor.modal_properties_textResourceBindings_body": "Tekstinnhold",
"ux_editor.modal_properties_textResourceBindings_body_add": "Legg til tekstinnhold",
+ "ux_editor.modal_properties_textResourceBindings_careOfTitle": "Tittel for C/O",
"ux_editor.modal_properties_textResourceBindings_description": "Beskrivelse",
"ux_editor.modal_properties_textResourceBindings_description_add": "Legg til beskrivelse",
"ux_editor.modal_properties_textResourceBindings_edit_button_close": "'Rediger'-knapp (lukket gruppe)",
@@ -1941,6 +1956,7 @@
"ux_editor.modal_properties_textResourceBindings_edit_button_open_add": "Legg til tekst for 'Rediger'-knapp (åpen gruppe)",
"ux_editor.modal_properties_textResourceBindings_help": "Hjelpetekst",
"ux_editor.modal_properties_textResourceBindings_help_add": "Legg til hjelpetekst",
+ "ux_editor.modal_properties_textResourceBindings_houseNumberTitle": "Tittel for gatenummer",
"ux_editor.modal_properties_textResourceBindings_leftColumnHeader": "Overskrift venstre kolonne",
"ux_editor.modal_properties_textResourceBindings_leftColumnHeader_add": "Legg til tekst for overskrift i venstre kolonne",
"ux_editor.modal_properties_textResourceBindings_next": "Tekst på neste-knapp",
@@ -1948,6 +1964,9 @@
"ux_editor.modal_properties_textResourceBindings_page_id": "Side-ID",
"ux_editor.modal_properties_textResourceBindings_page_name": "Visningsnavn for side",
"ux_editor.modal_properties_textResourceBindings_page_name_add": "Legg til visningsnavn for side",
+ "ux_editor.modal_properties_textResourceBindings_pagination_back_button": "Tilbakeknapp ved paginering",
+ "ux_editor.modal_properties_textResourceBindings_pagination_next_button": "Nesteknapp ved paginering",
+ "ux_editor.modal_properties_textResourceBindings_postPlaceTitle": "Tittel for poststed",
"ux_editor.modal_properties_textResourceBindings_questionDescriptions": "Beskrivelser av spørsmål",
"ux_editor.modal_properties_textResourceBindings_questionDescriptions_add": "Legg til tekst for beskrivelser av spørsmål",
"ux_editor.modal_properties_textResourceBindings_questionHelpTexts": "Hjelpetekster for spørsmål",
@@ -1956,6 +1975,7 @@
"ux_editor.modal_properties_textResourceBindings_questions_add": "Legg til tekst for spørsmål",
"ux_editor.modal_properties_textResourceBindings_requiredValidation": "Valideringsmelding for påkrevde felt",
"ux_editor.modal_properties_textResourceBindings_requiredValidation_add": "Legg til valideringsmelding for påkrevde felt",
+ "ux_editor.modal_properties_textResourceBindings_returnToSummaryButtonTitle": "Tittel for 'Tilbake til oppsummering'-knapp",
"ux_editor.modal_properties_textResourceBindings_save_and_next_button": "'Lagre og neste'-knapp",
"ux_editor.modal_properties_textResourceBindings_save_and_next_button_add": "Legg til tekst for 'Lagre og neste'-knapp",
"ux_editor.modal_properties_textResourceBindings_save_button": "'Lagre'-knapp",
@@ -1974,6 +1994,7 @@
"ux_editor.modal_properties_textResourceBindings_target_add": "Legg til URL for lenke",
"ux_editor.modal_properties_textResourceBindings_title": "Ledetekst",
"ux_editor.modal_properties_textResourceBindings_title_add": "Legg til ledetekst",
+ "ux_editor.modal_properties_textResourceBindings_zipCodeTitle": "Tittel for postnummer",
"ux_editor.modal_properties_trigger_validation_label": "Skal feltet trigge en validering?",
"ux_editor.modal_properties_valid_file_endings": "Gyldige filtyper",
"ux_editor.modal_properties_valid_file_endings_all": "Alle filtyper",
diff --git a/frontend/libs/studio-icons/src/react/icons/PaymentDetailsIcon.tsx b/frontend/libs/studio-icons/src/react/icons/PaymentDetailsIcon.tsx
new file mode 100644
index 00000000000..8ea8e55f3a0
--- /dev/null
+++ b/frontend/libs/studio-icons/src/react/icons/PaymentDetailsIcon.tsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import { SvgTemplate } from './SvgTemplate';
+import type { IconProps } from '../types';
+
+export const PaymentDetailsIcon = (props: IconProps): React.ReactElement => {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/frontend/libs/studio-icons/src/react/icons/index.ts b/frontend/libs/studio-icons/src/react/icons/index.ts
index e5814fe8bbc..e3469c218d0 100644
--- a/frontend/libs/studio-icons/src/react/icons/index.ts
+++ b/frontend/libs/studio-icons/src/react/icons/index.ts
@@ -16,6 +16,7 @@ export { NavBarIcon } from './NavBarIcon';
export { NumberIcon } from './NumberIcon';
export { ObjectIcon } from './ObjectIcon';
export { TextIcon } from './TextIcon';
+export { PaymentDetailsIcon } from './PaymentDetailsIcon';
export { PaymentTaskIcon } from './PaymentTaskIcon';
export { PropertyIcon } from './PropertyIcon';
export { RadioButtonIcon } from './RadioButtonIcon';
diff --git a/frontend/packages/shared/src/types/ComponentSpecificConfig.ts b/frontend/packages/shared/src/types/ComponentSpecificConfig.ts
index 27b04a250b3..8523ab6304f 100644
--- a/frontend/packages/shared/src/types/ComponentSpecificConfig.ts
+++ b/frontend/packages/shared/src/types/ComponentSpecificConfig.ts
@@ -321,6 +321,7 @@ export type ComponentSpecificConfig = {
};
[ComponentType.Paragraph]: {};
[ComponentType.Payment]: SummarizableComponentProps;
+ [ComponentType.PaymentDetails]: {};
[ComponentType.PrintButton]: {};
[ComponentType.RadioButtons]: FormComponentProps &
SummarizableComponentProps &
diff --git a/frontend/packages/shared/src/types/ComponentType.ts b/frontend/packages/shared/src/types/ComponentType.ts
index c34a583128e..1bcf039a935 100644
--- a/frontend/packages/shared/src/types/ComponentType.ts
+++ b/frontend/packages/shared/src/types/ComponentType.ts
@@ -33,6 +33,7 @@ export enum ComponentType {
Panel = 'Panel',
Paragraph = 'Paragraph',
Payment = 'Payment',
+ PaymentDetails = 'PaymentDetails',
PrintButton = 'PrintButton',
RadioButtons = 'RadioButtons',
RepeatingGroup = 'RepeatingGroup',
diff --git a/frontend/packages/ux-editor-v3/src/components/config/FormComponentConfig.tsx b/frontend/packages/ux-editor-v3/src/components/config/FormComponentConfig.tsx
index 819c882e743..de7993bd786 100644
--- a/frontend/packages/ux-editor-v3/src/components/config/FormComponentConfig.tsx
+++ b/frontend/packages/ux-editor-v3/src/components/config/FormComponentConfig.tsx
@@ -118,7 +118,7 @@ export const FormComponentConfig = ({
)}
{!hideUnsupported && (
- {'Andre innstillinger'}
+ {t('ux_editor.component_other_properties_title')}
)}
{options && optionsId && (
diff --git a/frontend/packages/ux-editor/src/components/Properties/PageConfigPanel/PageConfigPanel.module.css b/frontend/packages/ux-editor/src/components/Properties/PageConfigPanel/PageConfigPanel.module.css
new file mode 100644
index 00000000000..bbe191ed129
--- /dev/null
+++ b/frontend/packages/ux-editor/src/components/Properties/PageConfigPanel/PageConfigPanel.module.css
@@ -0,0 +1,3 @@
+.text {
+ padding: var(--fds-spacing-5) 0;
+}
diff --git a/frontend/packages/ux-editor/src/components/Properties/PageConfigPanel/PageConfigPanel.tsx b/frontend/packages/ux-editor/src/components/Properties/PageConfigPanel/PageConfigPanel.tsx
index d093ce90252..c2636ef4853 100644
--- a/frontend/packages/ux-editor/src/components/Properties/PageConfigPanel/PageConfigPanel.tsx
+++ b/frontend/packages/ux-editor/src/components/Properties/PageConfigPanel/PageConfigPanel.tsx
@@ -11,6 +11,7 @@ import { textResourceByLanguageAndIdSelector } from '../../../selectors/textReso
import type { ITextResource } from 'app-shared/types/global';
import { duplicatedIdsExistsInLayout } from '../../../utils/formLayoutUtils';
import { PageConfigWarning } from './PageConfigWarning';
+import classes from './PageConfigPanel.module.css';
export const PageConfigPanel = () => {
const { selectedFormLayoutName } = useAppContext();
@@ -54,7 +55,7 @@ export const PageConfigPanel = () => {
{t('right_menu.text')}
-
+
{}}
label={t('ux_editor.modal_properties_textResourceBindings_page_name')}
diff --git a/frontend/packages/ux-editor/src/components/config/FormComponentConfig.test.tsx b/frontend/packages/ux-editor/src/components/config/FormComponentConfig.test.tsx
index 37c74101f70..cd826609be2 100644
--- a/frontend/packages/ux-editor/src/components/config/FormComponentConfig.test.tsx
+++ b/frontend/packages/ux-editor/src/components/config/FormComponentConfig.test.tsx
@@ -7,6 +7,25 @@ import InputSchema from '../../testing/schemas/json/component/Input.schema.v1.js
import type { ServicesContextProps } from 'app-shared/contexts/ServicesContext';
import { screen } from '@testing-library/react';
import { textMock } from '@studio/testing/mocks/i18nMock';
+import type { KeyValuePairs } from 'app-shared/types/KeyValuePairs';
+
+const somePropertyName = 'somePropertyName';
+const customTextMockToHandleUndefined = (
+ keys: string | string[],
+ variables?: KeyValuePairs,
+) => {
+ const key = Array.isArray(keys) ? keys[0] : keys;
+ if (key === `ux_editor.component_properties_description.${somePropertyName}`) return key;
+ return variables
+ ? '[mockedText(' + key + ', ' + JSON.stringify(variables) + ')]'
+ : '[mockedText(' + key + ')]';
+};
+
+jest.mock('react-i18next', () => ({
+ useTranslation: () => ({
+ t: customTextMockToHandleUndefined,
+ }),
+}));
describe('FormComponentConfig', () => {
it('should render expected components', async () => {
@@ -103,7 +122,7 @@ describe('FormComponentConfig', () => {
},
});
expect(
- screen.queryByText(textMock(`ux_editor.component_properties.grid`)),
+ screen.queryByText(textMock('ux_editor.component_properties.grid')),
).not.toBeInTheDocument();
});
@@ -116,10 +135,41 @@ describe('FormComponentConfig', () => {
},
});
expect(
- screen.queryByText(textMock(`ux_editor.component_properties.grid`)),
+ screen.queryByText(textMock('ux_editor.component_properties.grid')),
).not.toBeInTheDocument();
});
+ it('should show description text for objects if key is defined', () => {
+ render({
+ props: {
+ schema: InputSchema,
+ },
+ });
+ expect(
+ screen.getByText(textMock('ux_editor.component_properties_description.pageBreak')),
+ ).toBeInTheDocument();
+ });
+
+ it('should show description from schema for objects if key is not defined', () => {
+ const descriptionFromSchema = 'Some description for some object property';
+ render({
+ props: {
+ schema: {
+ ...InputSchema,
+ properties: {
+ ...InputSchema.properties,
+ somePropertyName: {
+ type: 'object',
+ properties: {},
+ description: descriptionFromSchema,
+ },
+ },
+ },
+ },
+ });
+ expect(screen.getByText(descriptionFromSchema)).toBeInTheDocument();
+ });
+
it('should not render property if it is unsupported', () => {
render({
props: {
@@ -169,11 +219,11 @@ describe('FormComponentConfig', () => {
});
expect(
screen.getByRole('combobox', {
- name: textMock(`ux_editor.component_properties.supportedArrayProperty`),
+ name: textMock('ux_editor.component_properties.supportedArrayProperty'),
}),
).toBeInTheDocument();
expect(
- screen.queryByLabelText(textMock(`ux_editor.component_properties.unsupportedArrayProperty`)),
+ screen.queryByLabelText(textMock('ux_editor.component_properties.unsupportedArrayProperty')),
).not.toBeInTheDocument();
});
diff --git a/frontend/packages/ux-editor/src/components/config/FormComponentConfig.tsx b/frontend/packages/ux-editor/src/components/config/FormComponentConfig.tsx
index c715d48abf0..41c11e378ae 100644
--- a/frontend/packages/ux-editor/src/components/config/FormComponentConfig.tsx
+++ b/frontend/packages/ux-editor/src/components/config/FormComponentConfig.tsx
@@ -13,6 +13,7 @@ import {
import { EditGrid } from './editModal/EditGrid';
import type { FormItem } from '../../types/FormItem';
import type { UpdateFormMutateOptions } from '../../containers/FormItemContext';
+import { useComponentPropertyDescription } from '../../hooks/useComponentPropertyDescription';
export interface IEditFormComponentProps {
editFormId: string;
@@ -34,6 +35,7 @@ export const FormComponentConfig = ({
}: FormComponentConfigProps) => {
const t = useText();
const componentPropertyLabel = useComponentPropertyLabel();
+ const componentPropertyDescription = useComponentPropertyDescription();
if (!schema?.properties) return null;
@@ -199,7 +201,9 @@ export const FormComponentConfig = ({
{componentPropertyLabel(propertyKey)}
{properties[propertyKey]?.description && (
-
{properties[propertyKey].description}
+
+ {componentPropertyDescription(propertyKey) ?? properties[propertyKey].description}
+
)}
= FilterKeysOfType;
diff --git a/frontend/packages/ux-editor/src/data/formItemConfig.ts b/frontend/packages/ux-editor/src/data/formItemConfig.ts
index 0f053516e8b..dceddc68f8b 100644
--- a/frontend/packages/ux-editor/src/data/formItemConfig.ts
+++ b/frontend/packages/ux-editor/src/data/formItemConfig.ts
@@ -21,6 +21,7 @@ import {
NavBarIcon,
PaperclipIcon,
TextIcon,
+ PaymentDetailsIcon,
PinIcon,
PresentationIcon,
RadioButtonIcon,
@@ -382,6 +383,12 @@ export const formItemConfigs: FormItemConfigs = {
defaultProperties: {},
icon: WalletIcon,
},
+ [ComponentType.PaymentDetails]: {
+ name: ComponentType.PaymentDetails,
+ itemType: LayoutItemType.Component,
+ defaultProperties: {},
+ icon: PaymentDetailsIcon,
+ },
[ComponentType.PrintButton]: {
name: ComponentType.PrintButton,
itemType: LayoutItemType.Component,
@@ -446,6 +453,7 @@ export const advancedItems: FormItemConfigs[ComponentType][] = [
formItemConfigs[ComponentType.List],
formItemConfigs[ComponentType.Custom],
formItemConfigs[ComponentType.RepeatingGroup],
+ formItemConfigs[ComponentType.PaymentDetails],
];
export const schemaComponents: FormItemConfigs[ComponentType][] = [
diff --git a/frontend/packages/ux-editor/src/hooks/useComponentPropertyDescription.test.ts b/frontend/packages/ux-editor/src/hooks/useComponentPropertyDescription.test.ts
new file mode 100644
index 00000000000..5ea47a25159
--- /dev/null
+++ b/frontend/packages/ux-editor/src/hooks/useComponentPropertyDescription.test.ts
@@ -0,0 +1,37 @@
+import { renderHook } from '@testing-library/react';
+import { useComponentPropertyDescription } from './useComponentPropertyDescription';
+import { textMock } from '@studio/testing/mocks/i18nMock';
+import type { KeyValuePairs } from 'app-shared/types/KeyValuePairs';
+
+const somePropertyName = 'somePropertyName';
+const customTextMockToHandleUndefined = (
+ keys: string | string[],
+ variables?: KeyValuePairs,
+) => {
+ const key = Array.isArray(keys) ? keys[0] : keys;
+ if (key === `ux_editor.component_properties_description.${somePropertyName}`) return key;
+ return variables
+ ? '[mockedText(' + key + ', ' + JSON.stringify(variables) + ')]'
+ : '[mockedText(' + key + ')]';
+};
+
+jest.mock('react-i18next', () => ({
+ useTranslation: () => ({
+ t: customTextMockToHandleUndefined,
+ }),
+}));
+
+describe('useComponentPropertyDescription', () => {
+ it('Returns a function that returns the description', () => {
+ const result = renderHook(() => useComponentPropertyDescription()).result.current;
+ const propertyDescription = result('testDescription');
+ expect(propertyDescription).toEqual(
+ textMock('ux_editor.component_properties_description.testDescription'),
+ );
+ });
+ it('Returns a function that returns undefined if there was no text key for the description', () => {
+ const result = renderHook(() => useComponentPropertyDescription()).result.current;
+ const propertyDescription = result(somePropertyName);
+ expect(propertyDescription).toEqual(undefined);
+ });
+});
diff --git a/frontend/packages/ux-editor/src/hooks/useComponentPropertyDescription.ts b/frontend/packages/ux-editor/src/hooks/useComponentPropertyDescription.ts
new file mode 100644
index 00000000000..39bc3a4cbdc
--- /dev/null
+++ b/frontend/packages/ux-editor/src/hooks/useComponentPropertyDescription.ts
@@ -0,0 +1,14 @@
+import { useTranslation } from 'react-i18next';
+import { useCallback } from 'react';
+
+export const useComponentPropertyDescription = () => {
+ const { t } = useTranslation();
+ return useCallback(
+ (propertyKey: string) => {
+ const translationKey: string = `ux_editor.component_properties_description.${propertyKey}`;
+ const translation = t(translationKey);
+ return translation === translationKey ? undefined : translation;
+ },
+ [t],
+ );
+};
diff --git a/frontend/packages/ux-editor/src/testing/componentSchemaMocks.ts b/frontend/packages/ux-editor/src/testing/componentSchemaMocks.ts
index fe42f186751..93d345b342c 100644
--- a/frontend/packages/ux-editor/src/testing/componentSchemaMocks.ts
+++ b/frontend/packages/ux-editor/src/testing/componentSchemaMocks.ts
@@ -31,6 +31,7 @@ import NavigationBarSchema from './schemas/json/component/NavigationBar.schema.v
import NavigationButtonsSchema from './schemas/json/component/NavigationButtons.schema.v1.json';
import PanelSchema from './schemas/json/component/Panel.schema.v1.json';
import ParagraphSchema from './schemas/json/component/Paragraph.schema.v1.json';
+import PaymentDetailsSchema from './schemas/json/component/PaymentDetails.schema.v1.json';
import PaymentSchema from './schemas/json/component/Payment.schema.v1.json';
import PrintButtonSchema from './schemas/json/component/PrintButton.schema.v1.json';
import RadioButtonsSchema from './schemas/json/component/RadioButtons.schema.v1.json';
@@ -75,6 +76,7 @@ export const componentSchemaMocks: Record = {
[ComponentType.Panel]: PanelSchema,
[ComponentType.Paragraph]: ParagraphSchema,
[ComponentType.Payment]: PaymentSchema,
+ [ComponentType.PaymentDetails]: PaymentDetailsSchema,
[ComponentType.PrintButton]: PrintButtonSchema,
[ComponentType.RadioButtons]: RadioButtonsSchema,
[ComponentType.RepeatingGroup]: RepeatingGroupSchema,
diff --git a/frontend/packages/ux-editor/src/testing/schemas/json/component/Address.schema.v1.json b/frontend/packages/ux-editor/src/testing/schemas/json/component/Address.schema.v1.json
index f044bad4956..a7c2e3da09e 100644
--- a/frontend/packages/ux-editor/src/testing/schemas/json/component/Address.schema.v1.json
+++ b/frontend/packages/ux-editor/src/testing/schemas/json/component/Address.schema.v1.json
@@ -98,8 +98,8 @@
"textResourceBindings": {
"properties": {
"title": {
- "title": "Title from Summary",
- "description": "Title of the component (currently only used when referenced from a Summary component)",
+ "title": "Title",
+ "description": "Title of the component",
"$ref": "expression.schema.v1.json#/definitions/string"
},
"tableTitle": {
@@ -126,6 +126,26 @@
"title": "Accessible summary title",
"description": "Title used for aria-label on the edit button in the summary view (overrides the default and summary title)",
"$ref": "expression.schema.v1.json#/definitions/string"
+ },
+ "careOfTitle": {
+ "title": "Care Of Title",
+ "description": "Title for care-of",
+ "$ref": "expression.schema.v1.json#/definitions/string"
+ },
+ "zipCodeTitle": {
+ "title": "Zip Code Title",
+ "description": "Title for the zip code",
+ "$ref": "expression.schema.v1.json#/definitions/string"
+ },
+ "postPlaceTitle": {
+ "title": "Post place Title",
+ "description": "Title for post place",
+ "$ref": "expression.schema.v1.json#/definitions/string"
+ },
+ "houseNumberTitle": {
+ "title": "House number Title",
+ "description": "Title for house number",
+ "$ref": "expression.schema.v1.json#/definitions/string"
}
}
},
diff --git a/frontend/packages/ux-editor/src/testing/schemas/json/component/Button.schema.v1.json b/frontend/packages/ux-editor/src/testing/schemas/json/component/Button.schema.v1.json
index db443f287f6..da68744a360 100644
--- a/frontend/packages/ux-editor/src/testing/schemas/json/component/Button.schema.v1.json
+++ b/frontend/packages/ux-editor/src/testing/schemas/json/component/Button.schema.v1.json
@@ -63,12 +63,7 @@
"title": "Mode",
"description": "The mode of the button",
"default": "submit",
- "enum": ["submit", "save", "go-to-task", "instantiate"],
- "type": "string"
- },
- "taskId": {
- "title": "Task ID",
- "description": "The ID of the task to go to (only used when mode is \"go-to-task\")",
+ "enum": ["submit", "save", "instantiate"],
"type": "string"
},
"mapping": {
diff --git a/frontend/packages/ux-editor/src/testing/schemas/json/component/Checkboxes.schema.v1.json b/frontend/packages/ux-editor/src/testing/schemas/json/component/Checkboxes.schema.v1.json
index 1b42a5d2716..ebe7009320b 100644
--- a/frontend/packages/ux-editor/src/testing/schemas/json/component/Checkboxes.schema.v1.json
+++ b/frontend/packages/ux-editor/src/testing/schemas/json/component/Checkboxes.schema.v1.json
@@ -244,6 +244,7 @@
"type": "object",
"properties": {
"simpleBinding": { "type": "string" },
+ "label": { "type": "string" },
"metadata": {
"description": "Describes the location where metadata for the option based component should be stored in the datamodel.",
"type": "string"
diff --git a/frontend/packages/ux-editor/src/testing/schemas/json/component/Dropdown.schema.v1.json b/frontend/packages/ux-editor/src/testing/schemas/json/component/Dropdown.schema.v1.json
index eccd8e5c282..6112433c186 100644
--- a/frontend/packages/ux-editor/src/testing/schemas/json/component/Dropdown.schema.v1.json
+++ b/frontend/packages/ux-editor/src/testing/schemas/json/component/Dropdown.schema.v1.json
@@ -244,6 +244,7 @@
"type": "object",
"properties": {
"simpleBinding": { "type": "string" },
+ "label": { "type": "string" },
"metadata": {
"description": "Describes the location where metadata for the option based component should be stored in the datamodel.",
"type": "string"
diff --git a/frontend/packages/ux-editor/src/testing/schemas/json/component/Group.schema.v1.json b/frontend/packages/ux-editor/src/testing/schemas/json/component/Group.schema.v1.json
index efaca1d2a16..ad8dcbb8037 100644
--- a/frontend/packages/ux-editor/src/testing/schemas/json/component/Group.schema.v1.json
+++ b/frontend/packages/ux-editor/src/testing/schemas/json/component/Group.schema.v1.json
@@ -89,6 +89,12 @@
"description": "Array of component IDs that should be displayed in the group",
"type": "array",
"items": { "type": "string" }
+ },
+ "headingLevel": {
+ "title": "Heading level",
+ "description": "The heading level of the group title.",
+ "enum": [2, 3, 4, 5, 6],
+ "type": "string"
}
},
"required": ["id", "type", "children"],
diff --git a/frontend/packages/ux-editor/src/testing/schemas/json/component/Input.schema.v1.json b/frontend/packages/ux-editor/src/testing/schemas/json/component/Input.schema.v1.json
index 45b0aa78a64..f004d94851a 100644
--- a/frontend/packages/ux-editor/src/testing/schemas/json/component/Input.schema.v1.json
+++ b/frontend/packages/ux-editor/src/testing/schemas/json/component/Input.schema.v1.json
@@ -170,8 +170,8 @@
"properties": {
"currency": {
"title": "Language-sensitive currency formatting",
- "description": "Enables currency to be language sensitive based on selected app language. Note: parts that already exist in number property are not overridden by this prop.",
"type": "string",
+ "description": "Enables currency to be language sensitive based on selected app language. Note: parts that already exist in number property are not overridden by this prop.",
"enum": [
"AED",
"AFN",
diff --git a/frontend/packages/ux-editor/src/testing/schemas/json/component/LikertItem.schema.v1.json b/frontend/packages/ux-editor/src/testing/schemas/json/component/LikertItem.schema.v1.json
index 874eb7e94db..d6b3490dadc 100644
--- a/frontend/packages/ux-editor/src/testing/schemas/json/component/LikertItem.schema.v1.json
+++ b/frontend/packages/ux-editor/src/testing/schemas/json/component/LikertItem.schema.v1.json
@@ -232,6 +232,7 @@
"type": "object",
"properties": {
"simpleBinding": { "type": "string" },
+ "label": { "type": "string" },
"metadata": {
"description": "Describes the location where metadata for the option based component should be stored in the datamodel.",
"type": "string"
diff --git a/frontend/packages/ux-editor/src/testing/schemas/json/component/MultipleSelect.schema.v1.json b/frontend/packages/ux-editor/src/testing/schemas/json/component/MultipleSelect.schema.v1.json
index 4c66ce2e2f0..82e6ee24a56 100644
--- a/frontend/packages/ux-editor/src/testing/schemas/json/component/MultipleSelect.schema.v1.json
+++ b/frontend/packages/ux-editor/src/testing/schemas/json/component/MultipleSelect.schema.v1.json
@@ -244,6 +244,7 @@
"type": "object",
"properties": {
"simpleBinding": { "type": "string" },
+ "label": { "type": "string" },
"metadata": {
"description": "Describes the location where metadata for the option based component should be stored in the datamodel.",
"type": "string"
diff --git a/frontend/packages/ux-editor/src/testing/schemas/json/component/Payment.schema.v1.json b/frontend/packages/ux-editor/src/testing/schemas/json/component/Payment.schema.v1.json
index 3cd01ec59d4..d3acc1aa1a2 100644
--- a/frontend/packages/ux-editor/src/testing/schemas/json/component/Payment.schema.v1.json
+++ b/frontend/packages/ux-editor/src/testing/schemas/json/component/Payment.schema.v1.json
@@ -1,5 +1,5 @@
{
- "$id": "https://altinncdn.no/schemas/json/component/Header.schema.v1.json",
+ "$id": "https://altinncdn.no/schemas/json/component/Payment.schema.v1.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"id": {
@@ -59,12 +59,12 @@
"properties": {
"title": {
"title": "Title",
- "description": "The text to display in the header",
+ "description": "The title of the paragraph",
"$ref": "expression.schema.v1.json#/definitions/string"
},
- "help": {
- "title": "Help text",
- "description": "The text to display in the help tooltip/popup",
+ "description": {
+ "title": "Description",
+ "description": "Description, optionally shown below the title",
"$ref": "expression.schema.v1.json#/definitions/string"
}
},
diff --git a/frontend/packages/ux-editor/src/testing/schemas/json/component/PaymentDetails.schema.v1.json b/frontend/packages/ux-editor/src/testing/schemas/json/component/PaymentDetails.schema.v1.json
new file mode 100644
index 00000000000..0ad2bdb54a8
--- /dev/null
+++ b/frontend/packages/ux-editor/src/testing/schemas/json/component/PaymentDetails.schema.v1.json
@@ -0,0 +1,77 @@
+{
+ "$id": "https://altinncdn.no/schemas/json/component/PaymentDetails.schema.v1.json",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "properties": {
+ "id": {
+ "title": "ID",
+ "description": "The component ID. Must be unique within all layouts/pages in a layout-set. Cannot end with .",
+ "type": "string",
+ "pattern": "^[0-9a-zA-Z][0-9a-zA-Z-]*(-?[a-zA-Z]+|[a-zA-Z][0-9]+|-[0-9]{6,})$"
+ },
+ "hidden": {
+ "title": "Hidden",
+ "description": "Boolean value or expression indicating if the component should be hidden. Defaults to false.",
+ "default": false,
+ "$ref": "expression.schema.v1.json#/definitions/boolean"
+ },
+ "grid": {
+ "properties": {
+ "xs": { "$ref": "#/definitions/IGridSize" },
+ "sm": { "$ref": "#/definitions/IGridSize" },
+ "md": { "$ref": "#/definitions/IGridSize" },
+ "lg": { "$ref": "#/definitions/IGridSize" },
+ "xl": { "$ref": "#/definitions/IGridSize" },
+ "labelGrid": { "$ref": "#/definitions/IGridStyling" },
+ "innerGrid": { "$ref": "#/definitions/IGridStyling" }
+ }
+ },
+ "pageBreak": {
+ "title": "Page break",
+ "description": "Optionally insert page-break before/after component when rendered in PDF",
+ "type": "object",
+ "properties": {
+ "breakBefore": {
+ "title": "Page break before",
+ "description": "PDF only: Value or expression indicating whether a page break should be added before the component. Can be either: 'auto' (default), 'always', or 'avoid'.",
+ "examples": ["auto", "always", "avoid"],
+ "default": "auto",
+ "$ref": "expression.schema.v1.json#/definitions/string"
+ },
+ "breakAfter": {
+ "title": "Page break after",
+ "description": "PDF only: Value or expression indicating whether a page break should be added after the component. Can be either: 'auto' (default), 'always', or 'avoid'.",
+ "examples": ["auto", "always", "avoid"],
+ "default": "auto",
+ "$ref": "expression.schema.v1.json#/definitions/string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "type": { "const": "PaymentDetails" },
+ "textResourceBindings": {
+ "type": "object",
+ "properties": {
+ "title": {
+ "title": "Title",
+ "description": "The title of the paragraph",
+ "$ref": "expression.schema.v1.json#/definitions/string"
+ },
+ "description": {
+ "title": "Description",
+ "description": "Description, optionally shown below the title",
+ "$ref": "expression.schema.v1.json#/definitions/string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "mapping": {
+ "title": "Mapping",
+ "description": "A mapping of key-value pairs (usually used for mapping a path in the data model to a query string parameter).",
+ "type": "object",
+ "properties": {},
+ "additionalProperties": { "type": "string" }
+ }
+ },
+ "required": ["id", "type"],
+ "title": "PaymentDetails component schema"
+}
diff --git a/frontend/packages/ux-editor/src/testing/schemas/json/component/RadioButtons.schema.v1.json b/frontend/packages/ux-editor/src/testing/schemas/json/component/RadioButtons.schema.v1.json
index 86acb101d06..9cdf560b12f 100644
--- a/frontend/packages/ux-editor/src/testing/schemas/json/component/RadioButtons.schema.v1.json
+++ b/frontend/packages/ux-editor/src/testing/schemas/json/component/RadioButtons.schema.v1.json
@@ -244,6 +244,7 @@
"type": "object",
"properties": {
"simpleBinding": { "type": "string" },
+ "label": { "type": "string" },
"metadata": {
"description": "Describes the location where metadata for the option based component should be stored in the datamodel.",
"type": "string"
diff --git a/frontend/packages/ux-editor/src/testing/schemas/json/component/RepeatingGroup.schema.v1.json b/frontend/packages/ux-editor/src/testing/schemas/json/component/RepeatingGroup.schema.v1.json
index e8a9bc23ca5..c55b12ea421 100644
--- a/frontend/packages/ux-editor/src/testing/schemas/json/component/RepeatingGroup.schema.v1.json
+++ b/frontend/packages/ux-editor/src/testing/schemas/json/component/RepeatingGroup.schema.v1.json
@@ -105,6 +105,16 @@
"title": "Edit button (open) (for repeating groups)",
"description": "The text for the \"Edit\" button when the repeating group item is not in edit mode (i.e. the user can open the edit mode)",
"$ref": "expression.schema.v1.json#/definitions/string"
+ },
+ "pagination_next_button": {
+ "title": "Next button in pagination",
+ "description": "The text for the \"Next\" button in pagination",
+ "$ref": "expression.schema.v1.json#/definitions/string"
+ },
+ "pagination_back_button": {
+ "title": "Back button in pagination",
+ "description": "The text for the \"Back\" button in pagination",
+ "$ref": "expression.schema.v1.json#/definitions/string"
}
}
},
@@ -127,6 +137,23 @@
"type": "array",
"items": { "type": "string" }
},
+ "showValidations": {
+ "title": "Validation types",
+ "description": "List of validation types to show",
+ "type": "array",
+ "items": {
+ "enum": [
+ "Schema",
+ "Component",
+ "Expression",
+ "CustomBackend",
+ "Required",
+ "AllExceptRequired",
+ "All"
+ ],
+ "type": "string"
+ }
+ },
"validateOnSaveRow": {
"title": "Validation types",
"description": "List of validation types to show",
@@ -211,6 +238,14 @@
},
"additionalProperties": false
},
+ "pagination": {
+ "title": "Pagination options",
+ "description": "Pagination options for the repeating group rows.",
+ "type": "object",
+ "properties": { "rowsPerPage": { "type": "integer", "minimum": 1 } },
+ "required": ["rowsPerPage"],
+ "additionalProperties": false
+ },
"maxCount": {
"title": "Max number of rows",
"description": "Maximum number of rows that can be added.",
@@ -240,6 +275,12 @@
"default": false,
"$ref": "expression.schema.v1.json#/definitions/boolean"
},
+ "stickyHeader": {
+ "title": "Sticky header",
+ "description": "If set to true, the header of the repeating group will be sticky",
+ "default": false,
+ "type": "boolean"
+ },
"rowsBefore": {
"title": "Rows in Grid or Grid-like component",
"description": "The list of rows in this grid",
diff --git a/frontend/packages/ux-editor/src/testing/schemas/json/component/Summary.schema.v1.json b/frontend/packages/ux-editor/src/testing/schemas/json/component/Summary.schema.v1.json
index c9b916905c9..72736df6135 100644
--- a/frontend/packages/ux-editor/src/testing/schemas/json/component/Summary.schema.v1.json
+++ b/frontend/packages/ux-editor/src/testing/schemas/json/component/Summary.schema.v1.json
@@ -55,8 +55,6 @@
},
"type": { "const": "Summary" },
"textResourceBindings": {
- "title": "TRBSummarizable",
- "type": "object",
"properties": {
"summaryTitle": {
"title": "Summary title",
@@ -67,6 +65,11 @@
"title": "Accessible summary title",
"description": "Title used for aria-label on the edit button in the summary view (overrides the default and summary title)",
"$ref": "expression.schema.v1.json#/definitions/string"
+ },
+ "returnToSummaryButtonTitle": {
+ "title": "ReturnToSummaryButtonTitle",
+ "description": "Used to specify the text on the NavigationButtons component that should be used after clicking \"Change\" on the summary component",
+ "$ref": "expression.schema.v1.json#/definitions/string"
}
}
},
@@ -115,6 +118,12 @@
"description": "Set to true to hide the blue dashed border below the summary component. False by default.",
"default": false,
"type": "boolean"
+ },
+ "nextButton": {
+ "title": "Display the next button",
+ "description": "Set to to true display a \"next\" button as well as the return to summary button",
+ "default": false,
+ "type": "boolean"
}
},
"additionalProperties": false
diff --git a/frontend/scripts/componentSchemas/version.ts b/frontend/scripts/componentSchemas/version.ts
index 7c6afd50e88..dc10b28c7cf 100644
--- a/frontend/scripts/componentSchemas/version.ts
+++ b/frontend/scripts/componentSchemas/version.ts
@@ -6,9 +6,9 @@ export const versionSettings = {
},
v4: {
layoutSchemaUrl:
- 'https://altinncdn.no/toolkits/altinn-app-frontend/4.0.0-rc2/schemas/json/layout/layout.schema.v1.json',
+ 'https://altinncdn.no/toolkits/altinn-app-frontend/4/schemas/json/layout/layout.schema.v1.json',
expressionSchemaUrl:
- 'https://altinncdn.no/toolkits/altinn-app-frontend/4.0.0-rc2/schemas/json/layout/expression.schema.v1.json',
+ 'https://altinncdn.no/toolkits/altinn-app-frontend/4/schemas/json/layout/expression.schema.v1.json',
componentSchemaPath: '../../packages/ux-editor/src/testing/schemas/json/component',
},
};
From 06facb5f040aefe93389bfb7a53b02471c8e5c52 Mon Sep 17 00:00:00 2001
From: andreastanderen <71079896+standeren@users.noreply.github.com>
Date: Thu, 6 Jun 2024 09:13:06 +0200
Subject: [PATCH 25/27] 10469 should fetch actual footer layout if it exists
(#12843)
* Add endpoint in preview to get footer
* Add tests
* Fix PR comments
* Simplify Footer definition class
---
.../Designer/Controllers/PreviewController.cs | 16 +++++-
.../GitRepository/AltinnAppGitRepository.cs | 15 ++++++
backend/src/Designer/Models/FooterFile.cs | 46 ++++++++++++++++
.../ApplicationMetadataTests.cs | 8 +--
.../ApplicationSettingsTests.cs | 2 +-
.../PreviewController/DatamodelTests.cs | 4 +-
.../GetApplicationLanguagesTests.cs | 2 +-
.../PreviewController/GetFooterTests.cs | 52 +++++++++++++++++++
.../PreviewController/GetFormDataTests.cs | 6 +--
.../PreviewController/GetFormLayoutsTests.cs | 4 +-
.../PreviewController/GetImageTests.cs | 12 ++---
.../PreviewController/GetOptionsTests.cs | 2 +-
.../GetRuleConfigurationTests.cs | 2 +-
.../PreviewController/LanguageTests.cs | 2 +-
.../PreviewControllerTestsBase.cs | 3 +-
.../PreviewController/TextResourcesTests.cs | 2 +-
.../app-with-layoutsets/App/ui/footer.json | 25 +++++++++
17 files changed, 177 insertions(+), 26 deletions(-)
create mode 100644 backend/src/Designer/Models/FooterFile.cs
create mode 100644 backend/tests/Designer.Tests/Controllers/PreviewController/GetFooterTests.cs
create mode 100644 backend/tests/Designer.Tests/_TestData/Repositories/testUser/ttd/app-with-layoutsets/App/ui/footer.json
diff --git a/backend/src/Designer/Controllers/PreviewController.cs b/backend/src/Designer/Controllers/PreviewController.cs
index 00f125a1d3a..25e13de512c 100644
--- a/backend/src/Designer/Controllers/PreviewController.cs
+++ b/backend/src/Designer/Controllers/PreviewController.cs
@@ -933,12 +933,24 @@ public IActionResult UpdateAttachmentWithTag(string org, string app, [FromQuery]
///
/// Unique identifier of the organisation responsible for the app.
/// Application identifier which is unique within an organisation.
+ /// A that observes if operation is cancelled.
/// Empty response
[HttpGet]
[Route("api/v1/footer")]
- public IActionResult Footer(string org, string app)
+ public async Task> Footer(string org, string app, CancellationToken cancellationToken)
{
- return Ok();
+ try
+ {
+ string developer = AuthenticationHelper.GetDeveloperUserName(_httpContextAccessor.HttpContext);
+ AltinnAppGitRepository altinnAppGitRepository =
+ _altinnGitRepositoryFactory.GetAltinnAppGitRepository(org, app, developer);
+ FooterFile footerFile = await altinnAppGitRepository.GetFooter(cancellationToken);
+ return Ok(footerFile);
+ }
+ catch (FileNotFoundException)
+ {
+ return Ok();
+ }
}
///
diff --git a/backend/src/Designer/Infrastructure/GitRepository/AltinnAppGitRepository.cs b/backend/src/Designer/Infrastructure/GitRepository/AltinnAppGitRepository.cs
index fa52a336c6f..5f06370e4bd 100644
--- a/backend/src/Designer/Infrastructure/GitRepository/AltinnAppGitRepository.cs
+++ b/backend/src/Designer/Infrastructure/GitRepository/AltinnAppGitRepository.cs
@@ -44,6 +44,7 @@ public class AltinnAppGitRepository : AltinnGitRepository
private const string LayoutSettingsFilename = "Settings.json";
private const string AppMetadataFilename = "applicationmetadata.json";
private const string LayoutSetsFilename = "layout-sets.json";
+ private const string FooterFilename = "footer.json";
private const string RuleHandlerFilename = "RuleHandler.js";
private const string RuleConfigurationFilename = "RuleConfiguration.json";
private const string ProcessDefinitionFilename = "process.bpmn";
@@ -598,6 +599,15 @@ public async Task SaveLayoutSets(LayoutSets layoutSets)
}
}
+ public async Task GetFooter(CancellationToken cancellationToken = default)
+ {
+ string footerFilePath = GetPathToFooterFile();
+ cancellationToken.ThrowIfCancellationRequested();
+ string fileContent = await ReadTextByRelativePathAsync(footerFilePath, cancellationToken);
+ FooterFile footerFile = JsonSerializer.Deserialize(fileContent, JsonOptions);
+ return footerFile;
+ }
+
///
/// Saves the RuleHandler.js for a specific layout set
///
@@ -838,6 +848,11 @@ private static string GetPathToLayoutSetsFile()
return Path.Combine(LayoutsFolderName, LayoutSetsFilename);
}
+ private static string GetPathToFooterFile()
+ {
+ return Path.Combine(LayoutsFolderName, FooterFilename);
+ }
+
private static string GetPathToRuleHandler(string layoutSetName)
{
return layoutSetName.IsNullOrEmpty() ?
diff --git a/backend/src/Designer/Models/FooterFile.cs b/backend/src/Designer/Models/FooterFile.cs
new file mode 100644
index 00000000000..a8ab1330ac2
--- /dev/null
+++ b/backend/src/Designer/Models/FooterFile.cs
@@ -0,0 +1,46 @@
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+using JetBrains.Annotations;
+
+namespace Altinn.Studio.Designer.Models;
+public class FooterFile
+{
+ [JsonPropertyName("$schema")]
+ public string Schema { get; set; }
+
+ [JsonPropertyName("footer")]
+ public List Footer { get; set; }
+}
+
+public class FooterLayout
+{
+ [JsonPropertyName("type")]
+ [JsonConverter(typeof(JsonStringEnumConverter))]
+ public ComponentType Type { get; set; }
+
+ [JsonPropertyName("title")]
+ public string Title { get; set; }
+
+ [JsonPropertyName("target")]
+ [CanBeNull]
+ public string Target { get; set; }
+
+ [JsonPropertyName("icon")]
+ [JsonConverter(typeof(JsonStringEnumConverter))]
+ public IconType? Icon { get; set; }
+}
+
+public enum ComponentType
+{
+ Email,
+ Link,
+ Phone,
+ Text
+}
+
+public enum IconType
+{
+ Information,
+ Email,
+ Phone
+}
diff --git a/backend/tests/Designer.Tests/Controllers/PreviewController/ApplicationMetadataTests.cs b/backend/tests/Designer.Tests/Controllers/PreviewController/ApplicationMetadataTests.cs
index b23e88c88cf..3b67c523711 100644
--- a/backend/tests/Designer.Tests/Controllers/PreviewController/ApplicationMetadataTests.cs
+++ b/backend/tests/Designer.Tests/Controllers/PreviewController/ApplicationMetadataTests.cs
@@ -39,12 +39,12 @@ protected override void ConfigureTestServices(IServiceCollection services)
[Fact]
public async Task Get_ApplicationMetadata_Ok()
{
- string expectedApplicationMetadataString = TestDataHelper.GetFileFromRepo(Org, AppV3, Developer, "App/config/applicationmetadata.json");
+ string expectedApplicationMetadataString = TestDataHelper.GetFileFromRepo(Org, PreviewApp, Developer, "App/config/applicationmetadata.json");
_appDevelopmentServiceMock
.Setup(rs => rs.GetAppLibVersion(It.IsAny()))
.Returns(NuGet.Versioning.NuGetVersion.Parse("1.0.0"));
- string dataPathWithData = $"{Org}/{AppV3}/api/v1/applicationmetadata";
+ string dataPathWithData = $"{Org}/{PreviewApp}/api/v1/applicationmetadata";
using HttpRequestMessage httpRequestMessage = new(HttpMethod.Get, dataPathWithData);
using HttpResponseMessage response = await HttpClient.SendAsync(httpRequestMessage);
@@ -60,12 +60,12 @@ public async Task Get_ApplicationMetadata_Ok()
[Fact]
public async Task Get_ApplicationMetadata_With_V8_Altinn_Nuget_Version_Ok()
{
- string expectedApplicationMetadataString = TestDataHelper.GetFileFromRepo(Org, AppV3, Developer, "App/config/applicationmetadata.json");
+ string expectedApplicationMetadataString = TestDataHelper.GetFileFromRepo(Org, PreviewApp, Developer, "App/config/applicationmetadata.json");
_appDevelopmentServiceMock
.Setup(rs => rs.GetAppLibVersion(It.IsAny()))
.Returns(NuGet.Versioning.NuGetVersion.Parse("8.0.0"));
- string dataPathWithData = $"{Org}/{AppV3}/api/v1/applicationmetadata";
+ string dataPathWithData = $"{Org}/{PreviewApp}/api/v1/applicationmetadata";
using HttpRequestMessage httpRequestMessage = new(HttpMethod.Get, dataPathWithData);
using HttpResponseMessage response = await HttpClient.SendAsync(httpRequestMessage);
diff --git a/backend/tests/Designer.Tests/Controllers/PreviewController/ApplicationSettingsTests.cs b/backend/tests/Designer.Tests/Controllers/PreviewController/ApplicationSettingsTests.cs
index 2415c588319..e21321543d9 100644
--- a/backend/tests/Designer.Tests/Controllers/PreviewController/ApplicationSettingsTests.cs
+++ b/backend/tests/Designer.Tests/Controllers/PreviewController/ApplicationSettingsTests.cs
@@ -19,7 +19,7 @@ public ApplicationSettingsTests(WebApplicationFactory factory) : base(f
[Fact]
public async Task Get_ApplicationSettings_Ok()
{
- string dataPathWithData = $"{Org}/{AppV3}/api/v1/applicationsettings";
+ string dataPathWithData = $"{Org}/{PreviewApp}/api/v1/applicationsettings";
using HttpRequestMessage httpRequestMessage = new(HttpMethod.Get, dataPathWithData);
using HttpResponseMessage response = await HttpClient.SendAsync(httpRequestMessage);
diff --git a/backend/tests/Designer.Tests/Controllers/PreviewController/DatamodelTests.cs b/backend/tests/Designer.Tests/Controllers/PreviewController/DatamodelTests.cs
index 6cd3b32d1e3..51620b90ff5 100644
--- a/backend/tests/Designer.Tests/Controllers/PreviewController/DatamodelTests.cs
+++ b/backend/tests/Designer.Tests/Controllers/PreviewController/DatamodelTests.cs
@@ -20,9 +20,9 @@ public DatamodelTests(WebApplicationFactory factory) : base(factory)
[Fact]
public async Task Get_Datamodel_Ok()
{
- string expectedDatamodel = TestDataHelper.GetFileFromRepo(Org, AppV3, Developer, "App/models/custom-dm-name.schema.json");
+ string expectedDatamodel = TestDataHelper.GetFileFromRepo(Org, PreviewApp, Developer, "App/models/custom-dm-name.schema.json");
- string dataPathWithData = $"{Org}/{AppV3}/api/jsonschema/custom-dm-name";
+ string dataPathWithData = $"{Org}/{PreviewApp}/api/jsonschema/custom-dm-name";
using HttpRequestMessage httpRequestMessage = new(HttpMethod.Get, dataPathWithData);
using HttpResponseMessage response = await HttpClient.SendAsync(httpRequestMessage);
diff --git a/backend/tests/Designer.Tests/Controllers/PreviewController/GetApplicationLanguagesTests.cs b/backend/tests/Designer.Tests/Controllers/PreviewController/GetApplicationLanguagesTests.cs
index 7bf641f87be..eabee827f9f 100644
--- a/backend/tests/Designer.Tests/Controllers/PreviewController/GetApplicationLanguagesTests.cs
+++ b/backend/tests/Designer.Tests/Controllers/PreviewController/GetApplicationLanguagesTests.cs
@@ -16,7 +16,7 @@ public GetApplicationLanguagesTests(WebApplicationFactory factory) : ba
[Fact]
public async Task Get_ApplicationLanguages_Ok()
{
- string dataPathWithData = $"{Org}/{AppV3}/api/v1/applicationlanguages";
+ string dataPathWithData = $"{Org}/{PreviewApp}/api/v1/applicationlanguages";
using HttpRequestMessage httpRequestMessage = new(HttpMethod.Get, dataPathWithData);
using HttpResponseMessage response = await HttpClient.SendAsync(httpRequestMessage);
diff --git a/backend/tests/Designer.Tests/Controllers/PreviewController/GetFooterTests.cs b/backend/tests/Designer.Tests/Controllers/PreviewController/GetFooterTests.cs
new file mode 100644
index 00000000000..ee391e0371f
--- /dev/null
+++ b/backend/tests/Designer.Tests/Controllers/PreviewController/GetFooterTests.cs
@@ -0,0 +1,52 @@
+using System.IO;
+using System.Net;
+using System.Net.Http;
+using System.Text.Json;
+using System.Threading.Tasks;
+using Altinn.Studio.Designer.Models;
+using Designer.Tests.Utils;
+using FluentAssertions;
+using Microsoft.AspNetCore.Mvc.Testing;
+using SharedResources.Tests;
+using Xunit;
+
+namespace Designer.Tests.Controllers.PreviewController
+{
+ public class GetFooterTests : PreviewControllerTestsBase, IClassFixture>
+ {
+ public GetFooterTests(WebApplicationFactory factory) : base(factory)
+ {
+ }
+
+ [Fact]
+ public async Task Get_Footer_Exists_Ok()
+ {
+ string expectedFooter = TestDataHelper.GetFileFromRepo(Org, AppV4, Developer, "App/ui/footer.json");
+ FooterFile actualFooterFile = JsonSerializer.Deserialize(expectedFooter);
+
+ string dataPathWithData = $"{Org}/{AppV4}/api/v1/footer";
+ using HttpRequestMessage httpRequestMessage = new(HttpMethod.Get, dataPathWithData);
+
+ using HttpResponseMessage response = await HttpClient.SendAsync(httpRequestMessage);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+
+ string responseString = await response.Content.ReadAsStringAsync();
+ FooterFile responseFooterFile = JsonSerializer.Deserialize(responseString);
+
+ responseFooterFile.Footer.Should().BeEquivalentTo(actualFooterFile.Footer);
+ }
+
+ [Fact]
+ public async Task Get_Footer_Non_Existing_Ok()
+ {
+ string dataPathWithData = $"{Org}/{AppV3}/api/v1/footer";
+ using HttpRequestMessage httpRequestMessage = new(HttpMethod.Get, dataPathWithData);
+
+ using HttpResponseMessage response = await HttpClient.SendAsync(httpRequestMessage);
+ Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+
+ string responseString = await response.Content.ReadAsStringAsync();
+ responseString.Should().BeEmpty();
+ }
+ }
+}
diff --git a/backend/tests/Designer.Tests/Controllers/PreviewController/GetFormDataTests.cs b/backend/tests/Designer.Tests/Controllers/PreviewController/GetFormDataTests.cs
index a35463f70bb..eae40b51eb8 100644
--- a/backend/tests/Designer.Tests/Controllers/PreviewController/GetFormDataTests.cs
+++ b/backend/tests/Designer.Tests/Controllers/PreviewController/GetFormDataTests.cs
@@ -20,11 +20,11 @@ public GetFormDataTests(WebApplicationFactory factory) : base(factory)
[Fact]
public async Task Get_FormData_Ok()
{
- string expectedFormData = TestDataHelper.GetFileFromRepo(Org, AppV3, Developer, "App/models/custom-dm-name.schema.json");
+ string expectedFormData = TestDataHelper.GetFileFromRepo(Org, PreviewApp, Developer, "App/models/custom-dm-name.schema.json");
- string dataPathWithData = $"{Org}/{AppV3}/instances/{PartyId}/{InstanceGuId}/data/test-datatask-id";
+ string dataPathWithData = $"{Org}/{PreviewApp}/instances/{PartyId}/{InstanceGuId}/data/test-datatask-id";
using HttpRequestMessage httpRequestMessage = new(HttpMethod.Get, dataPathWithData);
- httpRequestMessage.Headers.Referrer = new Uri($"{MockedReferrerUrl}?org={Org}&app={AppV3}&selectedLayoutSet=");
+ httpRequestMessage.Headers.Referrer = new Uri($"{MockedReferrerUrl}?org={Org}&app={PreviewApp}&selectedLayoutSet=");
using HttpResponseMessage response = await HttpClient.SendAsync(httpRequestMessage);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
diff --git a/backend/tests/Designer.Tests/Controllers/PreviewController/GetFormLayoutsTests.cs b/backend/tests/Designer.Tests/Controllers/PreviewController/GetFormLayoutsTests.cs
index fdeed0c7f2c..d5644f87e52 100644
--- a/backend/tests/Designer.Tests/Controllers/PreviewController/GetFormLayoutsTests.cs
+++ b/backend/tests/Designer.Tests/Controllers/PreviewController/GetFormLayoutsTests.cs
@@ -19,10 +19,10 @@ public GetFormLayoutsTests(WebApplicationFactory factory) : base(factor
[Fact]
public async Task Get_FormLayouts_Ok()
{
- string expectedFormLayout = TestDataHelper.GetFileFromRepo(Org, AppV3, Developer, "App/ui/layouts/layout.json");
+ string expectedFormLayout = TestDataHelper.GetFileFromRepo(Org, PreviewApp, Developer, "App/ui/layouts/layout.json");
string expectedFormLayouts = @"{""layout"": " + expectedFormLayout + "}";
- string dataPathWithData = $"{Org}/{AppV3}/api/resource/FormLayout.json";
+ string dataPathWithData = $"{Org}/{PreviewApp}/api/resource/FormLayout.json";
using HttpRequestMessage httpRequestMessage = new(HttpMethod.Get, dataPathWithData);
using HttpResponseMessage response = await HttpClient.SendAsync(httpRequestMessage);
diff --git a/backend/tests/Designer.Tests/Controllers/PreviewController/GetImageTests.cs b/backend/tests/Designer.Tests/Controllers/PreviewController/GetImageTests.cs
index fb031d8be7f..afcc096ffcc 100644
--- a/backend/tests/Designer.Tests/Controllers/PreviewController/GetImageTests.cs
+++ b/backend/tests/Designer.Tests/Controllers/PreviewController/GetImageTests.cs
@@ -20,9 +20,9 @@ public GetImageTests(WebApplicationFactory factory) : base(factory)
[Fact]
public async Task Get_Image_From_Wwww_Root_Folder_Ok()
{
- byte[] expectedImageData = TestDataHelper.GetFileAsByteArrayFromRepo(Org, AppV3, Developer, "App/wwwroot/AltinnD-logo.svg");
+ byte[] expectedImageData = TestDataHelper.GetFileAsByteArrayFromRepo(Org, PreviewApp, Developer, "App/wwwroot/AltinnD-logo.svg");
- string dataPathWithData = $"{Org}/{AppV3}/AltinnD-logo.svg";
+ string dataPathWithData = $"{Org}/{PreviewApp}/AltinnD-logo.svg";
using HttpResponseMessage response = await HttpClient.GetAsync(dataPathWithData);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
@@ -35,9 +35,9 @@ public async Task Get_Image_From_Wwww_Root_Folder_Ok()
[Fact]
public async Task Get_Image_From_Sub_Folder_Ok()
{
- byte[] expectedImageData = TestDataHelper.GetFileAsByteArrayFromRepo(Org, AppV3, Developer, "App/wwwroot/images/AltinnD-logo.svg");
+ byte[] expectedImageData = TestDataHelper.GetFileAsByteArrayFromRepo(Org, PreviewApp, Developer, "App/wwwroot/images/AltinnD-logo.svg");
- string dataPathWithData = $"{Org}/{AppV3}/images/AltinnD-logo.svg";
+ string dataPathWithData = $"{Org}/{PreviewApp}/images/AltinnD-logo.svg";
using HttpResponseMessage response = await HttpClient.GetAsync(dataPathWithData);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
@@ -50,9 +50,9 @@ public async Task Get_Image_From_Sub_Folder_Ok()
[Fact]
public async Task Get_Image_From_Sub_Sub_Folder_Ok()
{
- byte[] expectedImageData = TestDataHelper.GetFileAsByteArrayFromRepo(Org, AppV3, Developer, "App/wwwroot/images/subImagesFolder/AltinnD-logo.svg");
+ byte[] expectedImageData = TestDataHelper.GetFileAsByteArrayFromRepo(Org, PreviewApp, Developer, "App/wwwroot/images/subImagesFolder/AltinnD-logo.svg");
- string dataPathWithData = $"{Org}/{AppV3}/images/subImagesFolder/AltinnD-logo.svg";
+ string dataPathWithData = $"{Org}/{PreviewApp}/images/subImagesFolder/AltinnD-logo.svg";
using HttpResponseMessage response = await HttpClient.GetAsync(dataPathWithData);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
diff --git a/backend/tests/Designer.Tests/Controllers/PreviewController/GetOptionsTests.cs b/backend/tests/Designer.Tests/Controllers/PreviewController/GetOptionsTests.cs
index e17ffabc197..22c3bb7d419 100644
--- a/backend/tests/Designer.Tests/Controllers/PreviewController/GetOptionsTests.cs
+++ b/backend/tests/Designer.Tests/Controllers/PreviewController/GetOptionsTests.cs
@@ -17,7 +17,7 @@ public GetOptionsTests(WebApplicationFactory factory) : base(factory)
[Fact]
public async Task Get_Options_when_options_exists_Ok()
{
- string dataPathWithData = $"{Org}/{AppV3}/api/options/test-options";
+ string dataPathWithData = $"{Org}/{PreviewApp}/api/options/test-options";
using HttpRequestMessage httpRequestMessage = new(HttpMethod.Get, dataPathWithData);
using HttpResponseMessage response = await HttpClient.SendAsync(httpRequestMessage);
diff --git a/backend/tests/Designer.Tests/Controllers/PreviewController/GetRuleConfigurationTests.cs b/backend/tests/Designer.Tests/Controllers/PreviewController/GetRuleConfigurationTests.cs
index 2165826fba6..71ce47d70c7 100644
--- a/backend/tests/Designer.Tests/Controllers/PreviewController/GetRuleConfigurationTests.cs
+++ b/backend/tests/Designer.Tests/Controllers/PreviewController/GetRuleConfigurationTests.cs
@@ -35,7 +35,7 @@ public async Task Get_RuleConfiguration_Ok()
[Fact]
public async Task Get_RuleConfiguration_NoContent()
{
- string dataPathWithData = $"{Org}/{AppV3}/api/resource/RuleConfiguration.json";
+ string dataPathWithData = $"{Org}/{PreviewApp}/api/resource/RuleConfiguration.json";
using HttpRequestMessage httpRequestMessage = new(HttpMethod.Get, dataPathWithData);
using HttpResponseMessage response = await HttpClient.SendAsync(httpRequestMessage);
diff --git a/backend/tests/Designer.Tests/Controllers/PreviewController/LanguageTests.cs b/backend/tests/Designer.Tests/Controllers/PreviewController/LanguageTests.cs
index 5d05376467d..3ed7ebb0839 100644
--- a/backend/tests/Designer.Tests/Controllers/PreviewController/LanguageTests.cs
+++ b/backend/tests/Designer.Tests/Controllers/PreviewController/LanguageTests.cs
@@ -19,7 +19,7 @@ public LanguageTests(WebApplicationFactory factory) : base(factory)
[Fact]
public async Task Get_Text_Ok()
{
- string dataPathWithData = $"{Org}/{AppV3}/api/v1/texts/nb";
+ string dataPathWithData = $"{Org}/{PreviewApp}/api/v1/texts/nb";
using HttpResponseMessage response = await HttpClient.GetAsync(dataPathWithData);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
diff --git a/backend/tests/Designer.Tests/Controllers/PreviewController/PreviewControllerTestsBase.cs b/backend/tests/Designer.Tests/Controllers/PreviewController/PreviewControllerTestsBase.cs
index 5d755d7e1cc..593d0e73c98 100644
--- a/backend/tests/Designer.Tests/Controllers/PreviewController/PreviewControllerTestsBase.cs
+++ b/backend/tests/Designer.Tests/Controllers/PreviewController/PreviewControllerTestsBase.cs
@@ -10,8 +10,9 @@ public class PreviewControllerTestsBase : DisagnerEndpointsTestsBase
where TTestClass : class
{
protected const string Org = "ttd";
- protected const string AppV3 = "preview-app";
+ protected const string AppV3 = "app-without-layoutsets";
protected const string AppV4 = "app-with-layoutsets";
+ protected const string PreviewApp = "preview-app";
protected const string Developer = "testUser";
protected const string LayoutSetName = "layoutSet1";
protected const string LayoutSetName2 = "layoutSet2";
diff --git a/backend/tests/Designer.Tests/Controllers/PreviewController/TextResourcesTests.cs b/backend/tests/Designer.Tests/Controllers/PreviewController/TextResourcesTests.cs
index cf5bad81621..c7a0e29eb85 100644
--- a/backend/tests/Designer.Tests/Controllers/PreviewController/TextResourcesTests.cs
+++ b/backend/tests/Designer.Tests/Controllers/PreviewController/TextResourcesTests.cs
@@ -19,7 +19,7 @@ public TextResourcesTests(WebApplicationFactory factory) : base(factory
[Fact]
public async Task Get_TextResources_Ok()
{
- string dataPathWithData = $"{Org}/{AppV3}/api/v1/textresources";
+ string dataPathWithData = $"{Org}/{PreviewApp}/api/v1/textresources";
using HttpRequestMessage httpRequestMessage = new(HttpMethod.Get, dataPathWithData);
using HttpResponseMessage response = await HttpClient.SendAsync(httpRequestMessage);
diff --git a/backend/tests/Designer.Tests/_TestData/Repositories/testUser/ttd/app-with-layoutsets/App/ui/footer.json b/backend/tests/Designer.Tests/_TestData/Repositories/testUser/ttd/app-with-layoutsets/App/ui/footer.json
new file mode 100644
index 00000000000..749a351af48
--- /dev/null
+++ b/backend/tests/Designer.Tests/_TestData/Repositories/testUser/ttd/app-with-layoutsets/App/ui/footer.json
@@ -0,0 +1,25 @@
+{
+ "$schema": "https://altinncdn.no/toolkits/altinn-app-frontend/4/schemas/json/layout/footer.schema.v1.json",
+ "footer": [
+ {
+ "type": "Text",
+ "title": "**Frontend Test**
*Testdepartementet*"
+ },
+ {
+ "type": "Link",
+ "icon": "information",
+ "title": "general.accessibility",
+ "target": "general.accessibility_url"
+ },
+ {
+ "type": "Email",
+ "title": "hjelp@etaten.no",
+ "target": "hjelp@etaten.no"
+ },
+ {
+ "type": "Phone",
+ "title": "+47 987 65 432",
+ "target": "+4798765432"
+ }
+ ]
+}
\ No newline at end of file
From 5d7be67dc2461b57c9c225feefe60c3744365b24 Mon Sep 17 00:00:00 2001
From: David Ovrelid <46874830+framitdavid@users.noreply.github.com>
Date: Thu, 6 Jun 2024 10:35:39 +0200
Subject: [PATCH 26/27] refactor(ProcessEditor): Decouple integration with BFF
when adding tasks in the process editor. (#12894)
refactor(ProcessEditor): Decouple integration with BFF when adding tasks in the process editor
---
.../features/processEditor/ProcessEditor.tsx | 7 +-
.../handlers/OnProcessTaskAddHandler.test.ts | 147 +++++++++++++++---
.../handlers/OnProcessTaskAddHandler.ts | 78 +++++++++-
.../OnProcessTaskRemoveHandler.test.ts | 135 +++++++++++++---
.../handlers/OnProcessTaskRemoveHandler.ts | 85 ++++++++++
.../mutations/useAddLayoutSetMutation.ts | 25 +--
.../process-editor/src/ProcessEditor.test.tsx | 2 -
.../process-editor/src/ProcessEditor.tsx | 6 -
.../RedirectToCreatePageButton.test.tsx | 2 +-
.../src/contexts/BpmnApiContext.tsx | 12 +-
.../src/hooks/useBpmnEditor.test.tsx | 24 +--
.../process-editor/src/hooks/useBpmnEditor.ts | 54 ++-----
.../process-editor/src/types/OnProcessTask.ts | 5 +-
.../types.ts => types/TaskEvent.ts} | 2 +-
.../AddProcessTaskManager.test.ts | 109 -------------
.../AddProcessTaskManager.ts | 99 ------------
.../RemoveProcessTaskManager.test.ts | 118 --------------
.../RemoveProcessTaskManager.ts | 104 -------------
.../src/utils/ProcessTaskManager/index.ts | 3 -
.../src/utils/hookUtils/hookUtils.test.ts | 6 +-
.../src/utils/hookUtils/hookUtils.ts | 4 +-
.../test/mocks/bpmnContextMock.ts | 2 -
frontend/testing/testids.js | 4 +-
23 files changed, 468 insertions(+), 565 deletions(-)
rename frontend/packages/process-editor/src/{utils/ProcessTaskManager/types.ts => types/TaskEvent.ts} (57%)
delete mode 100644 frontend/packages/process-editor/src/utils/ProcessTaskManager/AddProcessTaskManager.test.ts
delete mode 100644 frontend/packages/process-editor/src/utils/ProcessTaskManager/AddProcessTaskManager.ts
delete mode 100644 frontend/packages/process-editor/src/utils/ProcessTaskManager/RemoveProcessTaskManager.test.ts
delete mode 100644 frontend/packages/process-editor/src/utils/ProcessTaskManager/RemoveProcessTaskManager.ts
delete mode 100644 frontend/packages/process-editor/src/utils/ProcessTaskManager/index.ts
diff --git a/frontend/app-development/features/processEditor/ProcessEditor.tsx b/frontend/app-development/features/processEditor/ProcessEditor.tsx
index d67b5f0e528..fc87f7ff780 100644
--- a/frontend/app-development/features/processEditor/ProcessEditor.tsx
+++ b/frontend/app-development/features/processEditor/ProcessEditor.tsx
@@ -122,7 +122,9 @@ export const ProcessEditor = (): React.ReactElement => {
org,
app,
currentPolicy,
+ addLayoutSet,
mutateApplicationPolicy,
+ addDataTypeToAppMetadata,
);
onProcessTaskAddHandler.handleOnProcessTaskAdd(taskMetadata);
};
@@ -132,7 +134,10 @@ export const ProcessEditor = (): React.ReactElement => {
org,
app,
currentPolicy,
+ layoutSets,
mutateApplicationPolicy,
+ deleteDataTypeFromAppMetadata,
+ deleteLayoutSet,
);
onProcessTaskRemoveHandler.handleOnProcessTaskRemove(taskMetadata);
};
@@ -155,8 +160,6 @@ export const ProcessEditor = (): React.ReactElement => {
appLibVersion={appLibData.backendVersion}
bpmnXml={hasBpmnQueryError ? null : bpmnXml}
mutateDataType={mutateDataType}
- addDataTypeToAppMetadata={addDataTypeToAppMetadata}
- deleteDataTypeFromAppMetadata={deleteDataTypeFromAppMetadata}
saveBpmn={saveBpmnXml}
openPolicyEditor={() => {
setSettingsModalSelectedTab('policy');
diff --git a/frontend/app-development/features/processEditor/handlers/OnProcessTaskAddHandler.test.ts b/frontend/app-development/features/processEditor/handlers/OnProcessTaskAddHandler.test.ts
index 7bdb416e853..53e05202c71 100644
--- a/frontend/app-development/features/processEditor/handlers/OnProcessTaskAddHandler.test.ts
+++ b/frontend/app-development/features/processEditor/handlers/OnProcessTaskAddHandler.test.ts
@@ -1,25 +1,81 @@
import type { Policy } from 'app-shared/types/Policy';
import type { OnProcessTaskEvent } from '@altinn/process-editor/types/OnProcessTask';
-import type { TaskEvent } from '@altinn/process-editor/utils/ProcessTaskManager';
import { OnProcessTaskAddHandler } from './OnProcessTaskAddHandler';
+import { BpmnTypeEnum } from '@altinn/process-editor/enum/BpmnTypeEnum';
+import type { TaskEvent } from '@altinn/process-editor/types/TaskEvent';
+import type { BpmnTaskType } from '@altinn/process-editor/types/BpmnTaskType';
+import { app, org } from '@studio/testing/testids';
+
+const currentPolicyMock: Policy = {
+ requiredAuthenticationLevelOrg: '3',
+ requiredAuthenticationLevelEndUser: '3',
+ rules: [],
+};
+const addLayoutSetMock = jest.fn();
+const mutateApplicationPolicyMock = jest.fn();
+const addDataTypeToAppMetadataMock = jest.fn();
+
+const createOnProcessTaskHandler = () =>
+ new OnProcessTaskAddHandler(
+ org,
+ app,
+ currentPolicyMock,
+ addLayoutSetMock,
+ mutateApplicationPolicyMock,
+ addDataTypeToAppMetadataMock,
+ );
+
+const createTaskEvent = (extensionConfig?: object): TaskEvent =>
+ ({
+ element: {
+ id: 'testId',
+ businessObject: {
+ id: 'testEventId',
+ $type: BpmnTypeEnum.Task,
+ extensionElements: extensionConfig ? { values: [extensionConfig] } : undefined,
+ },
+ },
+ }) as TaskEvent;
describe('OnProcessTaskAddHandler', () => {
- it('should add default payment policy to current policy when task type is payment', () => {
- const org = 'testOrg';
- const app = 'testApp';
- const currentPolicy: Policy = {
- requiredAuthenticationLevelOrg: '3',
- requiredAuthenticationLevelEndUser: '3',
- rules: [],
- };
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
- const mutateApplicationPolicy = jest.fn();
+ it('should add layoutSet when data-task is added', () => {
+ const onProcessTaskAddHandler = createOnProcessTaskHandler();
+
+ onProcessTaskAddHandler.handleOnProcessTaskAdd({
+ taskEvent: createTaskEvent(),
+ taskType: 'data',
+ });
+
+ expect(addLayoutSetMock).toHaveBeenCalledWith({
+ layoutSetConfig: { id: 'testId', tasks: ['testId'] },
+ layoutSetIdToUpdate: 'testId',
+ });
+ expect(addLayoutSetMock).toHaveBeenCalledTimes(1);
+ expect(addDataTypeToAppMetadataMock).not.toHaveBeenCalled();
+ expect(mutateApplicationPolicyMock).not.toHaveBeenCalled();
+ });
+
+ it('should add layoutSet, dataType and default policy when payment task is added', () => {
const taskMetadata: OnProcessTaskEvent = {
taskType: 'payment',
taskEvent: {
element: {
id: 'testElementId',
- businessObject: {},
+ businessObject: {
+ id: 'testEventId',
+ $type: BpmnTypeEnum.Task,
+ extensionElements: {
+ values: [
+ {
+ paymentConfig: { paymentDataType: 'paymentInformation' },
+ },
+ ],
+ },
+ },
},
} as TaskEvent,
};
@@ -41,14 +97,69 @@ describe('OnProcessTaskAddHandler', () => {
],
};
- const onProcessTaskAddHandler = new OnProcessTaskAddHandler(
- org,
- app,
- currentPolicy,
- mutateApplicationPolicy,
- );
+ const onProcessTaskAddHandler = createOnProcessTaskHandler();
+ onProcessTaskAddHandler.handleOnProcessTaskAdd(taskMetadata);
+
+ expect(addLayoutSetMock).toHaveBeenCalledWith({
+ layoutSetConfig: {
+ id: 'testElementId',
+ tasks: ['testElementId'],
+ },
+ layoutSetIdToUpdate: 'testElementId',
+ });
+ expect(addDataTypeToAppMetadataMock).toHaveBeenCalledWith({
+ dataTypeId: 'paymentInformation',
+ taskId: 'testElementId',
+ });
+ expect(mutateApplicationPolicyMock).toHaveBeenCalledWith(expectedResponse);
+ });
+
+ it('should add datatype when signing task is added', () => {
+ const onProcessTaskAddHandler = createOnProcessTaskHandler();
+
+ const taskMetadata: OnProcessTaskEvent = {
+ taskType: 'signing',
+ taskEvent: {
+ element: {
+ id: 'testElementId',
+ businessObject: {
+ id: 'testEventId',
+ $type: BpmnTypeEnum.Task,
+ extensionElements: {
+ values: [
+ {
+ signatureConfig: { signatureDataType: 'signingInformation' },
+ },
+ ],
+ },
+ },
+ },
+ } as TaskEvent,
+ };
onProcessTaskAddHandler.handleOnProcessTaskAdd(taskMetadata);
- expect(mutateApplicationPolicy).toHaveBeenCalledWith(expectedResponse);
+
+ expect(addDataTypeToAppMetadataMock).toHaveBeenCalledWith({
+ dataTypeId: 'signingInformation',
+ taskId: 'testElementId',
+ });
+ expect(addLayoutSetMock).not.toHaveBeenCalled();
+ expect(mutateApplicationPolicyMock).not.toHaveBeenCalled();
});
+
+ it.each(['confirmation', 'feedback'])(
+ 'should not add layoutSet, dataType or default policy when task type is %s',
+ (task) => {
+ const onProcessTaskAddHandler = createOnProcessTaskHandler();
+
+ onProcessTaskAddHandler.handleOnProcessTaskAdd({
+ taskEvent: createTaskEvent(),
+ taskType: task as BpmnTaskType,
+ });
+
+ expect(addLayoutSetMock).not.toHaveBeenCalled();
+ expect(addDataTypeToAppMetadataMock).not.toHaveBeenCalled();
+ expect(mutateApplicationPolicyMock).not.toHaveBeenCalled();
+ },
+ );
});
diff --git a/frontend/app-development/features/processEditor/handlers/OnProcessTaskAddHandler.ts b/frontend/app-development/features/processEditor/handlers/OnProcessTaskAddHandler.ts
index 91115313861..d8d9b02dc7b 100644
--- a/frontend/app-development/features/processEditor/handlers/OnProcessTaskAddHandler.ts
+++ b/frontend/app-development/features/processEditor/handlers/OnProcessTaskAddHandler.ts
@@ -1,13 +1,23 @@
-import type { OnProcessTaskEvent } from '@altinn/process-editor/types/OnProcessTask';
import { PaymentPolicyBuilder } from '../../../utils/policy';
+import type { OnProcessTaskEvent } from '@altinn/process-editor/types/OnProcessTask';
import type { Policy } from 'app-shared/types/Policy';
+import { getDataTypeIdFromBusinessObject } from '@altinn/process-editor/utils/hookUtils/hookUtils';
+import type {
+ AddLayoutSetMutation,
+ AddLayoutSetMutationPayload,
+} from '../../../hooks/mutations/useAddLayoutSetMutation';
export class OnProcessTaskAddHandler {
constructor(
private readonly org: string,
private readonly app: string,
private readonly currentPolicy: Policy,
+ private readonly addLayoutSet: AddLayoutSetMutation,
private readonly mutateApplicationPolicy: (policy: Policy) => void,
+ private readonly addDataTypeToAppMetadata: (data: {
+ dataTypeId: string;
+ taskId: string;
+ }) => void,
) {}
/**
@@ -15,12 +25,46 @@ export class OnProcessTaskAddHandler {
* @param taskMetadata
*/
public handleOnProcessTaskAdd(taskMetadata: OnProcessTaskEvent): void {
+ if (taskMetadata.taskType === 'data') {
+ this.handleDataTaskAdd(taskMetadata);
+ }
+
if (taskMetadata.taskType === 'payment') {
this.handlePaymentTaskAdd(taskMetadata);
}
+
+ if (taskMetadata.taskType === 'signing') {
+ this.handleSigningTaskAdd(taskMetadata);
+ }
+ }
+
+ /**
+ * Adds a layout set to the added data task
+ * @param taskMetadata
+ * @private
+ */
+ private handleDataTaskAdd(taskMetadata: OnProcessTaskEvent): void {
+ this.addLayoutSet(this.createLayoutSetConfig(taskMetadata.taskEvent));
}
+ /**
+ * Adds a dataType, layoutSet and default policy to the added payment task
+ * @param taskMetadata
+ * @private
+ */
private handlePaymentTaskAdd(taskMetadata: OnProcessTaskEvent): void {
+ this.addLayoutSet(this.createLayoutSetConfig(taskMetadata.taskEvent));
+
+ const dataTypeId = getDataTypeIdFromBusinessObject(
+ taskMetadata.taskType,
+ taskMetadata.taskEvent.element.businessObject,
+ );
+
+ this.addDataTypeToAppMetadata({
+ dataTypeId,
+ taskId: taskMetadata.taskEvent.element.id,
+ });
+
const paymentPolicyBuilder = new PaymentPolicyBuilder(this.org, this.app);
const defaultPaymentPolicy = paymentPolicyBuilder.getDefaultPaymentPolicy(
taskMetadata.taskEvent.element.id,
@@ -32,4 +76,36 @@ export class OnProcessTaskAddHandler {
rules: [...this.currentPolicy.rules, ...defaultPaymentPolicy.rules],
});
}
+
+ /**
+ * Adds a dataType to the added signing task
+ * @param taskMetadata
+ * @private
+ */
+ private handleSigningTaskAdd(taskMetadata: OnProcessTaskEvent): void {
+ const dataTypeId = getDataTypeIdFromBusinessObject(
+ taskMetadata.taskType,
+ taskMetadata.taskEvent.element.businessObject,
+ );
+
+ this.addDataTypeToAppMetadata({
+ dataTypeId,
+ taskId: taskMetadata.taskEvent.element.id,
+ });
+ }
+
+ /**
+ * Creates the layout set config for the task
+ * @returns {{layoutSetIdToUpdate: string, layoutSetConfig: LayoutSetConfig}}
+ * @private
+ */
+ private createLayoutSetConfig(
+ taskEvent: OnProcessTaskEvent['taskEvent'],
+ ): AddLayoutSetMutationPayload {
+ const elementId = taskEvent.element.id;
+ return {
+ layoutSetIdToUpdate: elementId,
+ layoutSetConfig: { id: elementId, tasks: [elementId] },
+ };
+ }
}
diff --git a/frontend/app-development/features/processEditor/handlers/OnProcessTaskRemoveHandler.test.ts b/frontend/app-development/features/processEditor/handlers/OnProcessTaskRemoveHandler.test.ts
index 8763dc12f6e..fc6fbb85560 100644
--- a/frontend/app-development/features/processEditor/handlers/OnProcessTaskRemoveHandler.test.ts
+++ b/frontend/app-development/features/processEditor/handlers/OnProcessTaskRemoveHandler.test.ts
@@ -1,13 +1,80 @@
import type { Policy } from 'app-shared/types/Policy';
import type { OnProcessTaskEvent } from '@altinn/process-editor/types/OnProcessTask';
-import type { TaskEvent } from '@altinn/process-editor/utils/ProcessTaskManager';
import { OnProcessTaskRemoveHandler } from './OnProcessTaskRemoveHandler';
+import type { LayoutSets } from 'app-shared/types/api/LayoutSetsResponse';
+import { BpmnTypeEnum } from '@altinn/process-editor/enum/BpmnTypeEnum';
+import type { TaskEvent } from '@altinn/process-editor/types/TaskEvent';
+import type { BpmnBusinessObjectEditor } from '@altinn/process-editor/types/BpmnBusinessObjectEditor';
+import { app, org } from '@studio/testing/testids';
+import type { BpmnTaskType } from '@altinn/process-editor/types/BpmnTaskType';
+
+const currentPolicyMock: Policy = {
+ requiredAuthenticationLevelOrg: '3',
+ requiredAuthenticationLevelEndUser: '3',
+ rules: [],
+};
+const layoutSetsMock = {
+ sets: [],
+};
+
+const mutateApplicationPolicyMock = jest.fn();
+const deleteDataTypeFromAppMetadataMock = jest.fn();
+const deletelayoutSetMock = jest.fn();
+
+const createTaskMetadataMock = (
+ taskType: string,
+ businessObject?: BpmnBusinessObjectEditor,
+): OnProcessTaskEvent => ({
+ taskType: taskType as BpmnTaskType,
+ taskEvent: {
+ element: {
+ id: 'testElementId',
+ businessObject: {
+ ...(businessObject || {}),
+ },
+ },
+ } as TaskEvent,
+});
+
+const createOnRemoveProcessTaskHandler = ({ currentPolicy, layoutSets }: any) => {
+ return new OnProcessTaskRemoveHandler(
+ org,
+ app,
+ currentPolicy || currentPolicyMock,
+ layoutSets || layoutSetsMock,
+ mutateApplicationPolicyMock,
+ deleteDataTypeFromAppMetadataMock,
+ deletelayoutSetMock,
+ );
+};
describe('OnProcessTaskRemoveHandler', () => {
- it('should remove payment policy from current policy when task type is payment', () => {
- const org = 'testOrg';
- const app = 'testApp';
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+ it('should remove layoutSet when data-task is deleted', () => {
+ const layoutSets: LayoutSets = {
+ sets: [{ id: 'testLayoutSetId', dataType: 'data', tasks: ['testElementId'] }],
+ };
+
+ const taskMetadata = createTaskMetadataMock('data', {
+ id: 'testEventId',
+ $type: BpmnTypeEnum.Task,
+ extensionElements: undefined,
+ });
+
+ const onProcessTaskRemoveHandler = createOnRemoveProcessTaskHandler({
+ layoutSets,
+ });
+
+ onProcessTaskRemoveHandler.handleOnProcessTaskRemove(taskMetadata);
+ expect(deletelayoutSetMock).toHaveBeenCalledWith({ layoutSetIdToUpdate: 'testLayoutSetId' });
+ expect(mutateApplicationPolicyMock).not.toHaveBeenCalled();
+ expect(deleteDataTypeFromAppMetadataMock).not.toHaveBeenCalled();
+ });
+
+ it('should remove payment policy from current policy when task type is payment', () => {
const currentPolicy: Policy = {
requiredAuthenticationLevelOrg: '3',
requiredAuthenticationLevelEndUser: '3',
@@ -43,25 +110,53 @@ describe('OnProcessTaskRemoveHandler', () => {
],
};
- const mutateApplicationPolicy = jest.fn();
- const taskMetadata: OnProcessTaskEvent = {
- taskType: 'payment',
- taskEvent: {
- element: {
- id: 'testElementId',
- businessObject: {},
- },
- } as TaskEvent,
+ const onProcessTaskRemoveHandler = createOnRemoveProcessTaskHandler({
+ currentPolicy,
+ });
+ onProcessTaskRemoveHandler.handleOnProcessTaskRemove(createTaskMetadataMock('payment'));
+
+ expect(mutateApplicationPolicyMock).toHaveBeenCalledWith(expectedResponse);
+ expect(mutateApplicationPolicyMock).toHaveBeenCalledTimes(1);
+ expect(deletelayoutSetMock).not.toHaveBeenCalled();
+ });
+
+ it('should delete layoutSet for payment-task if layoutSet exists', () => {
+ const layoutSets: LayoutSets = {
+ sets: [{ id: 'testLayoutSetId', dataType: 'payment', tasks: ['testElementId'] }],
};
- const onProcessTaskRemoveHandler = new OnProcessTaskRemoveHandler(
- org,
- app,
- currentPolicy,
- mutateApplicationPolicy,
- );
+ const taskMetadata = createTaskMetadataMock('payment', {
+ id: 'testEventId',
+ $type: BpmnTypeEnum.Task,
+ extensionElements: undefined,
+ });
+
+ const onProcessTaskRemoveHandler = createOnRemoveProcessTaskHandler({
+ layoutSets,
+ });
+
onProcessTaskRemoveHandler.handleOnProcessTaskRemove(taskMetadata);
+ expect(deletelayoutSetMock).toHaveBeenCalledWith({ layoutSetIdToUpdate: 'testLayoutSetId' });
+ });
+
+ it('should remove datatype from app metadata and delete layoutSet when the signing task is deleted', () => {
+ const layoutSets: LayoutSets = {
+ sets: [{ id: 'testLayoutSetId', dataType: 'signing', tasks: ['testElementId'] }],
+ };
- expect(mutateApplicationPolicy).toHaveBeenCalledWith(expectedResponse);
+ const taskMetadata = createTaskMetadataMock('signing', {
+ id: 'testEventId',
+ $type: BpmnTypeEnum.Task,
+ extensionElements: undefined,
+ });
+
+ const onProcessTaskRemoveHandler = createOnRemoveProcessTaskHandler({
+ layoutSets,
+ });
+
+ onProcessTaskRemoveHandler.handleOnProcessTaskRemove(taskMetadata);
+ expect(deleteDataTypeFromAppMetadataMock).toHaveBeenCalled();
+ expect(deletelayoutSetMock).toHaveBeenCalledWith({ layoutSetIdToUpdate: 'testLayoutSetId' });
+ expect(mutateApplicationPolicyMock).not.toHaveBeenCalled();
});
});
diff --git a/frontend/app-development/features/processEditor/handlers/OnProcessTaskRemoveHandler.ts b/frontend/app-development/features/processEditor/handlers/OnProcessTaskRemoveHandler.ts
index 2a63a6f2ff7..05f4d32578e 100644
--- a/frontend/app-development/features/processEditor/handlers/OnProcessTaskRemoveHandler.ts
+++ b/frontend/app-development/features/processEditor/handlers/OnProcessTaskRemoveHandler.ts
@@ -1,13 +1,21 @@
import type { Policy } from 'app-shared/types/Policy';
import type { OnProcessTaskEvent } from '@altinn/process-editor/types/OnProcessTask';
import { PaymentPolicyBuilder } from '../../../utils/policy';
+import {
+ getDataTypeIdFromBusinessObject,
+ getLayoutSetIdFromTaskId,
+} from '@altinn/process-editor/utils/hookUtils/hookUtils';
+import type { LayoutSets } from 'app-shared/types/api/LayoutSetsResponse';
export class OnProcessTaskRemoveHandler {
constructor(
private readonly org: string,
private readonly app: string,
private readonly currentPolicy: Policy,
+ private readonly layoutSets: LayoutSets,
private readonly mutateApplicationPolicy: (policy: Policy) => void,
+ private readonly deleteDataTypeFromAppMetadata: (data: { dataTypeId: string }) => void,
+ private readonly deleteLayoutSet: (data: { layoutSetIdToUpdate: string }) => void,
) {}
/**
@@ -15,12 +23,51 @@ export class OnProcessTaskRemoveHandler {
* @param taskMetadata
*/
public handleOnProcessTaskRemove(taskMetadata: OnProcessTaskEvent): void {
+ if (taskMetadata.taskType === 'data') {
+ this.handleDataTaskRemove(taskMetadata);
+ }
+
if (taskMetadata.taskType === 'payment') {
this.handlePaymentTaskRemove(taskMetadata);
}
+
+ if (taskMetadata.taskType === 'signing') {
+ this.handleSigningTaskRemove(taskMetadata);
+ }
+ }
+
+ /**
+ * Deletes the layoutSet from the deleted data task
+ * @private
+ */
+ private handleDataTaskRemove(taskMetadata: OnProcessTaskEvent): void {
+ const layoutSetId = getLayoutSetIdFromTaskId(
+ taskMetadata.taskEvent.element.id,
+ this.layoutSets,
+ );
+
+ if (layoutSetId) {
+ this.deleteLayoutSet({
+ layoutSetIdToUpdate: layoutSetId,
+ });
+ }
}
+ /**
+ * Deletes the dataType, layoutSet and policy for the deleted payment task
+ * @param taskMetadata
+ * @private
+ */
private handlePaymentTaskRemove(taskMetadata: OnProcessTaskEvent): void {
+ const dataTypeId = getDataTypeIdFromBusinessObject(
+ taskMetadata.taskType,
+ taskMetadata.taskEvent.element.businessObject,
+ );
+
+ this.deleteDataTypeFromAppMetadata({
+ dataTypeId,
+ });
+
const paymentPolicyBuilder = new PaymentPolicyBuilder(this.org, this.app);
const currentPaymentRuleId = paymentPolicyBuilder.getPolicyRuleId(
taskMetadata.taskEvent.element.id,
@@ -33,5 +80,43 @@ export class OnProcessTaskRemoveHandler {
};
this.mutateApplicationPolicy(updatedPolicy);
+
+ const layoutSetId = getLayoutSetIdFromTaskId(
+ taskMetadata.taskEvent.element.id,
+ this.layoutSets,
+ );
+
+ if (layoutSetId) {
+ this.deleteLayoutSet({
+ layoutSetIdToUpdate: layoutSetId,
+ });
+ }
+ }
+
+ /**
+ * Deletes layoutSet and dataType from the deleted signing task
+ * @param taskMetadata
+ * @private
+ */
+ private handleSigningTaskRemove(taskMetadata: OnProcessTaskEvent): void {
+ const dataTypeId = getDataTypeIdFromBusinessObject(
+ taskMetadata.taskType,
+ taskMetadata.taskEvent.element.businessObject,
+ );
+
+ this.deleteDataTypeFromAppMetadata({
+ dataTypeId,
+ });
+
+ const layoutSetId = getLayoutSetIdFromTaskId(
+ taskMetadata.taskEvent.element.id,
+ this.layoutSets,
+ );
+
+ if (layoutSetId) {
+ this.deleteLayoutSet({
+ layoutSetIdToUpdate: layoutSetId,
+ });
+ }
}
}
diff --git a/frontend/app-development/hooks/mutations/useAddLayoutSetMutation.ts b/frontend/app-development/hooks/mutations/useAddLayoutSetMutation.ts
index 9fa216a66d7..093d82eba13 100644
--- a/frontend/app-development/hooks/mutations/useAddLayoutSetMutation.ts
+++ b/frontend/app-development/hooks/mutations/useAddLayoutSetMutation.ts
@@ -1,9 +1,22 @@
-import { useMutation, useQueryClient } from '@tanstack/react-query';
+import { type UseMutateFunction, useMutation, useQueryClient } from '@tanstack/react-query';
import { useServicesContext } from 'app-shared/contexts/ServicesContext';
import { QueryKey } from 'app-shared/types/QueryKey';
import type { LayoutSetConfig, LayoutSets } from 'app-shared/types/api/LayoutSetsResponse';
import { useLocalStorage } from 'app-shared/hooks/useLocalStorage';
-import type { LayoutSetsResponse } from 'app-shared/types/api/AddLayoutSetResponse';
+import type {
+ AddLayoutSetResponse,
+ LayoutSetsResponse,
+} from 'app-shared/types/api/AddLayoutSetResponse';
+
+export type AddLayoutSetMutationPayload = {
+ layoutSetIdToUpdate: string;
+ layoutSetConfig: LayoutSetConfig;
+};
+export type AddLayoutSetMutation = UseMutateFunction<
+ AddLayoutSetResponse,
+ Error,
+ AddLayoutSetMutationPayload
+>;
const isLayoutSets = (obj: LayoutSetsResponse): obj is LayoutSets => {
if (obj === undefined || !(obj instanceof Object)) return false;
@@ -16,13 +29,7 @@ export const useAddLayoutSetMutation = (org: string, app: string) => {
const [_, setSelectedLayoutSet] = useLocalStorage('layoutSet/' + app, null);
return useMutation({
- mutationFn: ({
- layoutSetIdToUpdate,
- layoutSetConfig,
- }: {
- layoutSetIdToUpdate: string;
- layoutSetConfig: LayoutSetConfig;
- }) =>
+ mutationFn: ({ layoutSetIdToUpdate, layoutSetConfig }: AddLayoutSetMutationPayload) =>
addLayoutSet(org, app, layoutSetIdToUpdate, layoutSetConfig).then((layoutSets) => ({
layoutSets,
layoutSetConfig,
diff --git a/frontend/packages/process-editor/src/ProcessEditor.test.tsx b/frontend/packages/process-editor/src/ProcessEditor.test.tsx
index 7d8911e544e..1e182480440 100644
--- a/frontend/packages/process-editor/src/ProcessEditor.test.tsx
+++ b/frontend/packages/process-editor/src/ProcessEditor.test.tsx
@@ -23,8 +23,6 @@ const defaultProps: ProcessEditorProps = {
deleteLayoutSet: jest.fn(),
mutateLayoutSetId: jest.fn(),
mutateDataType: jest.fn(),
- addDataTypeToAppMetadata: jest.fn(),
- deleteDataTypeFromAppMetadata: jest.fn(),
openPolicyEditor: jest.fn(),
onProcessTaskRemove: jest.fn(),
onProcessTaskAdd: jest.fn(),
diff --git a/frontend/packages/process-editor/src/ProcessEditor.tsx b/frontend/packages/process-editor/src/ProcessEditor.tsx
index 01cd825c4e5..da4a36af845 100644
--- a/frontend/packages/process-editor/src/ProcessEditor.tsx
+++ b/frontend/packages/process-editor/src/ProcessEditor.tsx
@@ -25,8 +25,6 @@ export type ProcessEditorProps = {
deleteLayoutSet: BpmnApiContextProps['deleteLayoutSet'];
mutateLayoutSetId: BpmnApiContextProps['mutateLayoutSetId'];
mutateDataType: BpmnApiContextProps['mutateDataType'];
- addDataTypeToAppMetadata: BpmnApiContextProps['addDataTypeToAppMetadata'];
- deleteDataTypeFromAppMetadata: BpmnApiContextProps['deleteDataTypeFromAppMetadata'];
saveBpmn: (bpmnXml: string, metaData?: MetaDataForm) => void;
openPolicyEditor: BpmnApiContextProps['openPolicyEditor'];
onProcessTaskAdd: BpmnApiContextProps['onProcessTaskAdd'];
@@ -45,8 +43,6 @@ export const ProcessEditor = ({
deleteLayoutSet,
mutateLayoutSetId,
mutateDataType,
- addDataTypeToAppMetadata,
- deleteDataTypeFromAppMetadata,
saveBpmn,
openPolicyEditor,
onProcessTaskAdd,
@@ -74,8 +70,6 @@ export const ProcessEditor = ({
deleteLayoutSet={deleteLayoutSet}
mutateLayoutSetId={mutateLayoutSetId}
mutateDataType={mutateDataType}
- addDataTypeToAppMetadata={addDataTypeToAppMetadata}
- deleteDataTypeFromAppMetadata={deleteDataTypeFromAppMetadata}
saveBpmn={saveBpmn}
openPolicyEditor={openPolicyEditor}
onProcessTaskAdd={onProcessTaskAdd}
diff --git a/frontend/packages/process-editor/src/components/ConfigPanel/ConfigEndEvent/CustomReceiptContent/RedirectToCreatePageButton/RedirectToCreatePageButton.test.tsx b/frontend/packages/process-editor/src/components/ConfigPanel/ConfigEndEvent/CustomReceiptContent/RedirectToCreatePageButton/RedirectToCreatePageButton.test.tsx
index ddbecfe6da1..1699cd3103c 100644
--- a/frontend/packages/process-editor/src/components/ConfigPanel/ConfigEndEvent/CustomReceiptContent/RedirectToCreatePageButton/RedirectToCreatePageButton.test.tsx
+++ b/frontend/packages/process-editor/src/components/ConfigPanel/ConfigEndEvent/CustomReceiptContent/RedirectToCreatePageButton/RedirectToCreatePageButton.test.tsx
@@ -19,7 +19,7 @@ describe('RedirectToCreatePageButton', () => {
const navigationButton = screen.getByRole('link', {
name: textMock('process_editor.configuration_panel_custom_receipt_navigate_to_lage_button'),
});
- expect(navigationButton).toHaveAttribute('href', '/editor/org/app/ui-editor');
+ expect(navigationButton).toHaveAttribute('href', '/editor/testOrg/testApp/ui-editor');
});
});
diff --git a/frontend/packages/process-editor/src/contexts/BpmnApiContext.tsx b/frontend/packages/process-editor/src/contexts/BpmnApiContext.tsx
index d2a43f37d33..9f2a621afba 100644
--- a/frontend/packages/process-editor/src/contexts/BpmnApiContext.tsx
+++ b/frontend/packages/process-editor/src/contexts/BpmnApiContext.tsx
@@ -21,8 +21,6 @@ export type BpmnApiContextProps = {
deleteLayoutSet: (data: { layoutSetIdToUpdate: string }) => void;
mutateLayoutSetId: (data: { layoutSetIdToUpdate: string; newLayoutSetId: string }) => void;
mutateDataType: (dataTypeChange: DataTypeChange, options?: QueryOptions) => void;
- addDataTypeToAppMetadata: (data: { dataTypeId: string; taskId: string }) => void;
- deleteDataTypeFromAppMetadata: (data: { dataTypeId: string }) => void;
saveBpmn: (bpmnXml: string, metaData?: MetaDataForm) => void;
openPolicyEditor: () => void;
onProcessTaskAdd: (taskMetadata: OnProcessTaskEvent) => void;
@@ -39,7 +37,15 @@ export const BpmnApiContextProvider = ({
children,
...rest
}: Partial) => {
- return {children};
+ return (
+
+ {children}
+
+ );
};
export const useBpmnApiContext = (): Partial => {
diff --git a/frontend/packages/process-editor/src/hooks/useBpmnEditor.test.tsx b/frontend/packages/process-editor/src/hooks/useBpmnEditor.test.tsx
index f65d13097d8..89935df6da8 100644
--- a/frontend/packages/process-editor/src/hooks/useBpmnEditor.test.tsx
+++ b/frontend/packages/process-editor/src/hooks/useBpmnEditor.test.tsx
@@ -10,7 +10,6 @@ import type { BpmnTaskType } from '../types/BpmnTaskType';
import type { LayoutSets } from 'app-shared/types/api/LayoutSetsResponse';
import { getMockBpmnElementForTask, mockBpmnDetails } from '../../test/mocks/bpmnDetailsMock';
import { mockModelerRef } from '../../test/mocks/bpmnModelerMock';
-import { AddProcessTaskManager, RemoveProcessTaskManager } from '../utils/ProcessTaskManager';
const layoutSetId = 'someLayoutSetId';
const layoutSetsMock: LayoutSets = {
@@ -50,8 +49,6 @@ class BpmnModelerMockImpl {
}
}
-jest.mock('../utils/ProcessTaskManager');
-
jest.mock('../utils/hookUtils', () => ({
getBpmnEditorDetailsFromBusinessObject: jest.fn().mockReturnValue({}),
}));
@@ -77,6 +74,9 @@ jest.mock('./useBpmnModeler', () => ({
}));
const setBpmnDetailsMock = jest.fn();
+const onProcessTaskAddMock = jest.fn();
+const onProcessTaskRemoveMock = jest.fn();
+
const overrideUseBpmnContext = () => {
(useBpmnContext as jest.Mock).mockReturnValue({
getUpdatedXml: jest.fn(),
@@ -103,9 +103,9 @@ const wrapper = ({ children }) => (
{children}
@@ -130,23 +130,13 @@ describe('useBpmnEditor', () => {
it('should handle "shape.add" event', async () => {
renderUseBpmnEditor(false, 'shape.add');
- const handleTaskAddMock = jest.fn();
- (AddProcessTaskManager as jest.Mock).mockImplementation(() => ({
- handleTaskAdd: () => handleTaskAddMock(),
- }));
-
- await waitFor(() => expect(handleTaskAddMock).toHaveBeenCalledTimes(1));
+ await waitFor(() => expect(onProcessTaskAddMock).toHaveBeenCalledTimes(1));
});
it('should handle "shape.remove" event', async () => {
renderUseBpmnEditor(false, 'shape.remove');
- const handleTaskRemoveMock = jest.fn();
- (RemoveProcessTaskManager as jest.Mock).mockImplementation(() => ({
- handleTaskRemove: () => handleTaskRemoveMock(),
- }));
-
- await waitFor(() => expect(handleTaskRemoveMock).toHaveBeenCalledTimes(1));
+ await waitFor(() => expect(onProcessTaskRemoveMock).toHaveBeenCalledTimes(1));
});
it('should call setBpmnDetails when "element.click" event is triggered on eventBus', () => {
diff --git a/frontend/packages/process-editor/src/hooks/useBpmnEditor.ts b/frontend/packages/process-editor/src/hooks/useBpmnEditor.ts
index b158d7088d3..e6390e72bda 100644
--- a/frontend/packages/process-editor/src/hooks/useBpmnEditor.ts
+++ b/frontend/packages/process-editor/src/hooks/useBpmnEditor.ts
@@ -5,11 +5,7 @@ import { useBpmnModeler } from './useBpmnModeler';
import { getBpmnEditorDetailsFromBusinessObject } from '../utils/hookUtils';
import { useBpmnConfigPanelFormContext } from '../contexts/BpmnConfigPanelContext';
import { useBpmnApiContext } from '../contexts/BpmnApiContext';
-import {
- AddProcessTaskManager,
- RemoveProcessTaskManager,
- type TaskEvent,
-} from '../utils/ProcessTaskManager';
+import type { TaskEvent } from '../types/TaskEvent';
// Wrapper around bpmn-js to Reactify it
@@ -24,22 +20,7 @@ export const useBpmnEditor = (): UseBpmnViewerResult => {
const { metaDataFormRef, resetForm } = useBpmnConfigPanelFormContext();
const { getModeler, destroyModeler } = useBpmnModeler();
- const {
- addLayoutSet,
- deleteLayoutSet,
- addDataTypeToAppMetadata,
- deleteDataTypeFromAppMetadata,
- saveBpmn,
- layoutSets,
- onProcessTaskAdd,
- onProcessTaskRemove,
- } = useBpmnApiContext();
-
- // Needs to update the layoutSetsRef.current when layoutSets changes to avoid staled data in the event listeners
- const layoutSetsRef = useRef(layoutSets);
- useEffect(() => {
- layoutSetsRef.current = layoutSets;
- }, [layoutSets]);
+ const { saveBpmn, onProcessTaskAdd, onProcessTaskRemove } = useBpmnApiContext();
const handleCommandStackChanged = async () => {
saveBpmn(await getUpdatedXml(), metaDataFormRef.current || null);
@@ -48,28 +29,19 @@ export const useBpmnEditor = (): UseBpmnViewerResult => {
const handleShapeAdd = (taskEvent: TaskEvent): void => {
const bpmnDetails = getBpmnEditorDetailsFromBusinessObject(taskEvent?.element?.businessObject);
- const addProcessTaskManager = new AddProcessTaskManager(
- addLayoutSet,
- addDataTypeToAppMetadata,
- bpmnDetails,
- onProcessTaskAdd,
- );
-
- addProcessTaskManager.handleTaskAdd(taskEvent);
+ onProcessTaskAdd({
+ taskEvent,
+ taskType: bpmnDetails.taskType,
+ });
updateBpmnDetailsByTaskEvent(taskEvent);
};
const handleShapeRemove = (taskEvent: TaskEvent): void => {
const bpmnDetails = getBpmnEditorDetailsFromBusinessObject(taskEvent?.element?.businessObject);
- const removeProcessTaskManager = new RemoveProcessTaskManager(
- layoutSetsRef.current,
- deleteLayoutSet,
- deleteDataTypeFromAppMetadata,
- bpmnDetails,
- onProcessTaskRemove,
- );
-
- removeProcessTaskManager.handleTaskRemove(taskEvent);
+ onProcessTaskRemove({
+ taskEvent,
+ taskType: bpmnDetails.taskType,
+ });
setBpmnDetails(null);
};
@@ -95,13 +67,13 @@ export const useBpmnEditor = (): UseBpmnViewerResult => {
};
const initializeBpmnChanges = () => {
- modelerRef.current.on('commandStack.changed', async () => {
+ modelerRef.current.on('commandStack.changed', async (): Promise => {
await handleCommandStackChanged();
});
- modelerRef.current.on('shape.add', (taskEvent: TaskEvent) => {
+ modelerRef.current.on('shape.add', (taskEvent: TaskEvent): void => {
handleShapeAdd(taskEvent);
});
- modelerRef.current.on('shape.remove', (taskEvent: TaskEvent) => {
+ modelerRef.current.on('shape.remove', (taskEvent: TaskEvent): void => {
handleShapeRemove(taskEvent);
});
};
diff --git a/frontend/packages/process-editor/src/types/OnProcessTask.ts b/frontend/packages/process-editor/src/types/OnProcessTask.ts
index 50da2236fc3..8ad0591956c 100644
--- a/frontend/packages/process-editor/src/types/OnProcessTask.ts
+++ b/frontend/packages/process-editor/src/types/OnProcessTask.ts
@@ -1,6 +1,7 @@
-import type { TaskEvent } from '@altinn/process-editor/utils/ProcessTaskManager';
+import type { BpmnTaskType } from '../types/BpmnTaskType';
+import type { TaskEvent } from '../types/TaskEvent';
export type OnProcessTaskEvent = {
taskEvent?: TaskEvent;
- taskType: string;
+ taskType: BpmnTaskType;
};
diff --git a/frontend/packages/process-editor/src/utils/ProcessTaskManager/types.ts b/frontend/packages/process-editor/src/types/TaskEvent.ts
similarity index 57%
rename from frontend/packages/process-editor/src/utils/ProcessTaskManager/types.ts
rename to frontend/packages/process-editor/src/types/TaskEvent.ts
index af39dc64186..8304756068b 100644
--- a/frontend/packages/process-editor/src/utils/ProcessTaskManager/types.ts
+++ b/frontend/packages/process-editor/src/types/TaskEvent.ts
@@ -1,4 +1,4 @@
-import { type BpmnBusinessObjectEditor } from '../../types/BpmnBusinessObjectEditor';
+import { type BpmnBusinessObjectEditor } from './BpmnBusinessObjectEditor';
export type TaskEvent = Event & {
element: {
diff --git a/frontend/packages/process-editor/src/utils/ProcessTaskManager/AddProcessTaskManager.test.ts b/frontend/packages/process-editor/src/utils/ProcessTaskManager/AddProcessTaskManager.test.ts
deleted file mode 100644
index 33abd80c6cb..00000000000
--- a/frontend/packages/process-editor/src/utils/ProcessTaskManager/AddProcessTaskManager.test.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import { AddProcessTaskManager } from './AddProcessTaskManager';
-import { BpmnTypeEnum } from '../../enum/BpmnTypeEnum';
-import { type TaskEvent } from '../ProcessTaskManager/types';
-import { type BpmnDetails } from '../../types/BpmnDetails';
-import { type BpmnTaskType } from '../../types/BpmnTaskType';
-
-describe('AddProcessTaskManager', () => {
- const createBpmnDetails = (id: string, name: string, taskType: BpmnTaskType): BpmnDetails => ({
- id,
- name,
- taskType,
- type: BpmnTypeEnum.Task,
- });
-
- const createTaskEvent = (taskType: string, extensionConfig?: object): TaskEvent =>
- ({
- element: {
- businessObject: {
- id: 'testEventId',
- $type: BpmnTypeEnum.Task,
- extensionElements: extensionConfig ? { values: [extensionConfig] } : undefined,
- },
- },
- }) as TaskEvent;
-
- const addLayoutSet = jest.fn();
- const addDataTypeToAppMetadata = jest.fn();
- const onProcessTaskAdd = jest.fn();
-
- beforeEach(() => {
- jest.resetAllMocks();
- });
-
- const createAddProcessTaskManager = (bpmnDetails: BpmnDetails) =>
- new AddProcessTaskManager(
- addLayoutSet,
- addDataTypeToAppMetadata,
- bpmnDetails,
- onProcessTaskAdd,
- );
-
- it('should add layoutSet when data-task is added', () => {
- const bpmnDetails = createBpmnDetails('testId', 'dataTask', 'data');
- const addProcessTaskManager = createAddProcessTaskManager(bpmnDetails);
-
- addProcessTaskManager.handleTaskAdd(createTaskEvent('data'));
-
- expect(addLayoutSet).toHaveBeenCalledWith({
- layoutSetConfig: { id: 'testId', tasks: ['testId'] },
- layoutSetIdToUpdate: 'testId',
- });
- });
-
- it('should add layoutSet and dataType when PaymentTask is added', () => {
- const bpmnDetails = createBpmnDetails('testId', 'paymentTask', 'payment');
- const addProcessTaskManager = createAddProcessTaskManager(bpmnDetails);
-
- addProcessTaskManager.handleTaskAdd(
- createTaskEvent('payment', {
- taskType: 'payment',
- paymentConfig: { paymentDataType: 'paymentInformation' },
- }),
- );
-
- expect(addLayoutSet).toHaveBeenCalledWith({
- layoutSetConfig: { id: 'testId', tasks: ['testId'] },
- layoutSetIdToUpdate: 'testId',
- });
-
- expect(addDataTypeToAppMetadata).toHaveBeenCalledWith({
- dataTypeId: 'paymentInformation',
- taskId: 'testId',
- });
- });
-
- it('should add layoutSet and datatype when signing task is added', () => {
- const bpmnDetails = createBpmnDetails('testId', 'signingTask', 'signing');
- const addProcessTaskManager = createAddProcessTaskManager(bpmnDetails);
-
- addProcessTaskManager.handleTaskAdd(
- createTaskEvent('signing', {
- taskType: 'signing',
- signatureConfig: { signatureDataType: 'signingInformation' },
- }),
- );
-
- expect(addDataTypeToAppMetadata).toHaveBeenCalledWith({
- dataTypeId: 'signingInformation',
- taskId: 'testId',
- });
- });
-
- it('should inform the consumer of the package that a task has been added with the taskEvent and taskType', () => {
- const bpmnDetails = createBpmnDetails('testId', 'signingTask', 'signing');
- const addProcessTaskManager = createAddProcessTaskManager(bpmnDetails);
-
- const taskEvent = createTaskEvent('signing', {
- taskType: 'signing',
- signatureConfig: { signatureDataType: 'signingInformation' },
- });
-
- addProcessTaskManager.handleTaskAdd(taskEvent);
-
- expect(onProcessTaskAdd).toHaveBeenCalledWith({
- taskEvent,
- taskType: 'signing',
- });
- });
-});
diff --git a/frontend/packages/process-editor/src/utils/ProcessTaskManager/AddProcessTaskManager.ts b/frontend/packages/process-editor/src/utils/ProcessTaskManager/AddProcessTaskManager.ts
deleted file mode 100644
index 2aeac737cf3..00000000000
--- a/frontend/packages/process-editor/src/utils/ProcessTaskManager/AddProcessTaskManager.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-import { type LayoutSetConfig } from 'app-shared/types/api/LayoutSetsResponse';
-import { getDataTypeIdFromBusinessObject } from '../../utils/hookUtils/hookUtils';
-import { type BpmnApiContextProps } from '../../contexts/BpmnApiContext';
-import { type BpmnDetails } from '../../types/BpmnDetails';
-import { type TaskEvent } from './types';
-
-export class AddProcessTaskManager {
- constructor(
- private readonly addLayoutSet: BpmnApiContextProps['addLayoutSet'],
- private readonly addDataTypeToAppMetadata: BpmnApiContextProps['addDataTypeToAppMetadata'],
- private readonly bpmnDetails: BpmnDetails,
- private readonly onProcessTaskAdd: BpmnApiContextProps['onProcessTaskAdd'],
- ) {}
-
- /**
- * Handles the task add event and delegates the handling to the specific task type
- * @param taskEvent
- */
- public handleTaskAdd(taskEvent: TaskEvent): void {
- if (this.bpmnDetails.taskType === 'data') {
- this.handleDataTaskAdd();
- }
-
- if (this.bpmnDetails.taskType === 'payment') {
- this.handlePaymentTaskAdd(taskEvent);
- }
-
- if (this.bpmnDetails.taskType === 'signing') {
- this.handleSigningTaskAdd(taskEvent);
- }
-
- // Informs the consumer of this package that a task with the given taskEvent and taskType has been added
- this.onProcessTaskAdd({
- taskEvent,
- taskType: this.bpmnDetails.taskType,
- });
- }
-
- /**
- * Adds a layout set to the added data task
- * @private
- */
- private handleDataTaskAdd(): void {
- this.addLayoutSet(this.createLayoutSetConfig());
- }
-
- /**
- * Adds a dataType and layoutSet to the added payment task
- * @param taskEvent
- * @private
- */
- private handlePaymentTaskAdd(taskEvent: TaskEvent): void {
- // Add layout set to the task
- this.addLayoutSet(this.createLayoutSetConfig());
-
- // Add dataType
- const dataTypeId = getDataTypeIdFromBusinessObject(
- this.bpmnDetails.taskType,
- taskEvent.element.businessObject,
- );
-
- this.addDataTypeToAppMetadata({
- dataTypeId,
- taskId: this.bpmnDetails.id,
- });
- }
-
- /**
- * Adds a dataType and layout set to the added signing task
- * @param taskEvent
- * @private
- */
- private handleSigningTaskAdd(taskEvent: TaskEvent): void {
- const dataTypeId = getDataTypeIdFromBusinessObject(
- this.bpmnDetails.taskType,
- taskEvent.element.businessObject,
- );
-
- this.addDataTypeToAppMetadata({
- dataTypeId,
- taskId: this.bpmnDetails.id,
- });
- }
-
- /**
- * Creates the layout set config for the task
- * @returns {{layoutSetIdToUpdate: string, layoutSetConfig: LayoutSetConfig}}
- * @private
- */
- private createLayoutSetConfig(): {
- layoutSetIdToUpdate: string;
- layoutSetConfig: LayoutSetConfig;
- } {
- return {
- layoutSetIdToUpdate: this.bpmnDetails.id,
- layoutSetConfig: { id: this.bpmnDetails.id, tasks: [this.bpmnDetails.id] },
- };
- }
-}
diff --git a/frontend/packages/process-editor/src/utils/ProcessTaskManager/RemoveProcessTaskManager.test.ts b/frontend/packages/process-editor/src/utils/ProcessTaskManager/RemoveProcessTaskManager.test.ts
deleted file mode 100644
index 58ae7f90cb9..00000000000
--- a/frontend/packages/process-editor/src/utils/ProcessTaskManager/RemoveProcessTaskManager.test.ts
+++ /dev/null
@@ -1,118 +0,0 @@
-import { RemoveProcessTaskManager } from './RemoveProcessTaskManager';
-import { type LayoutSets } from 'app-shared/types/api/LayoutSetsResponse';
-import { type BpmnTaskType } from '../../types/BpmnTaskType';
-import { type BpmnDetails } from '../../types/BpmnDetails';
-import { BpmnTypeEnum } from '../../enum/BpmnTypeEnum';
-import { type TaskEvent } from '../ProcessTaskManager/types';
-
-describe('RemoveProcessTaskManager', () => {
- beforeEach(() => {
- jest.resetAllMocks();
- });
- const createBpmnDetails = (id: string, name: string, taskType: BpmnTaskType): BpmnDetails => ({
- id,
- name,
- taskType,
- type: BpmnTypeEnum.Task,
- });
-
- const createTaskEvent = (taskType: string, extensionConfig?: object): TaskEvent =>
- ({
- element: {
- businessObject: {
- id: 'testEventId',
- $type: taskType,
- extensionElements: extensionConfig ? { values: [extensionConfig] } : undefined,
- },
- },
- }) as TaskEvent;
-
- it('should remove layoutSet when data-task is deleted', () => {
- const layoutSets: LayoutSets = {
- sets: [{ id: 'testLayoutSetId', dataType: 'data', tasks: ['testTask'] }],
- };
-
- const deleteLayoutSet = jest.fn();
- const bpmnDetails = createBpmnDetails('testTask', 'testTask', 'data');
-
- const removeProcessTaskManager = new RemoveProcessTaskManager(
- layoutSets,
- deleteLayoutSet,
- jest.fn(),
- bpmnDetails,
- jest.fn(),
- );
-
- removeProcessTaskManager.handleTaskRemove({} as TaskEvent);
- expect(deleteLayoutSet).toHaveBeenCalledWith({ layoutSetIdToUpdate: 'testLayoutSetId' });
- });
-
- it('should remove datatype from app metadata and delete layoutSet when the signing Task is deleted', () => {
- const layoutSets: LayoutSets = {
- sets: [{ id: 'testLayoutSetId', dataType: 'signing', tasks: ['testTask'] }],
- };
-
- const deleteLayoutSet = jest.fn();
- const deleteDataTypeFromAppMetadata = jest.fn();
- const bpmnDetails = createBpmnDetails('testTask', 'testTask', 'signing');
-
- const removeProcessTaskManager = new RemoveProcessTaskManager(
- layoutSets,
- deleteLayoutSet,
- deleteDataTypeFromAppMetadata,
- bpmnDetails,
- jest.fn(),
- );
-
- removeProcessTaskManager.handleTaskRemove(createTaskEvent('signing'));
- expect(deleteDataTypeFromAppMetadata).toHaveBeenCalled();
- expect(deleteLayoutSet).toHaveBeenCalledWith({ layoutSetIdToUpdate: 'testLayoutSetId' });
- });
-
- it('should remove datatype and layoutSet when the payment Task is deleted', () => {
- const layoutSets: LayoutSets = {
- sets: [{ id: 'testLayoutSetId', dataType: 'payment', tasks: ['testTask'] }],
- };
-
- const deleteLayoutSet = jest.fn();
- const deleteDataTypeFromAppMetadata = jest.fn();
- const bpmnDetails = createBpmnDetails('testTask', 'testTask', 'payment');
-
- const removeProcessTaskManager = new RemoveProcessTaskManager(
- layoutSets,
- deleteLayoutSet,
- deleteDataTypeFromAppMetadata,
- bpmnDetails,
- jest.fn(),
- );
-
- removeProcessTaskManager.handleTaskRemove(createTaskEvent('payment'));
- expect(deleteDataTypeFromAppMetadata).toHaveBeenCalled();
- expect(deleteLayoutSet).toHaveBeenCalledWith({ layoutSetIdToUpdate: 'testLayoutSetId' });
- });
-
- it('should inform the consumer of the package that a task has been removed with the taskEvent and taskType', () => {
- const layoutSets: LayoutSets = {
- sets: [{ id: 'testLayoutSetId', dataType: 'payment', tasks: ['testTask'] }],
- };
-
- const deleteLayoutSet = jest.fn();
- const deleteDataTypeFromAppMetadata = jest.fn();
- const onProcessTaskRemove = jest.fn();
- const bpmnDetails = createBpmnDetails('testTask', 'testTask', 'payment');
-
- const removeProcessTaskManager = new RemoveProcessTaskManager(
- layoutSets,
- deleteLayoutSet,
- deleteDataTypeFromAppMetadata,
- bpmnDetails,
- onProcessTaskRemove,
- );
-
- removeProcessTaskManager.handleTaskRemove(createTaskEvent('payment'));
- expect(onProcessTaskRemove).toHaveBeenCalledWith({
- taskEvent: createTaskEvent('payment'),
- taskType: 'payment',
- });
- });
-});
diff --git a/frontend/packages/process-editor/src/utils/ProcessTaskManager/RemoveProcessTaskManager.ts b/frontend/packages/process-editor/src/utils/ProcessTaskManager/RemoveProcessTaskManager.ts
deleted file mode 100644
index 3df18321ae5..00000000000
--- a/frontend/packages/process-editor/src/utils/ProcessTaskManager/RemoveProcessTaskManager.ts
+++ /dev/null
@@ -1,104 +0,0 @@
-import { type BpmnApiContextProps } from '../../contexts/BpmnApiContext';
-import { type BpmnDetails } from '../../types/BpmnDetails';
-import { type TaskEvent } from './types';
-import {
- getDataTypeIdFromBusinessObject,
- getLayoutSetIdFromTaskId,
-} from '../../utils/hookUtils/hookUtils';
-
-export class RemoveProcessTaskManager {
- constructor(
- private readonly layoutSets: BpmnApiContextProps['layoutSets'],
- private readonly deleteLayoutSet: BpmnApiContextProps['deleteLayoutSet'],
- private readonly deleteDataTypeFromAppMetadata: BpmnApiContextProps['deleteDataTypeFromAppMetadata'],
- private readonly bpmnDetails: BpmnDetails,
- private readonly onProcessTaskRemove: BpmnApiContextProps['onProcessTaskRemove'],
- ) {}
-
- /**
- * Handles the task remove event and delegates the handling to the specific task type
- * @param taskEvent
- */
- public handleTaskRemove(taskEvent: TaskEvent): void {
- if (this.bpmnDetails.taskType === 'data') {
- this.handleDataTaskRemove();
- }
-
- if (this.bpmnDetails.taskType === 'payment') {
- this.handlePaymentTaskRemove(taskEvent);
- }
-
- if (this.bpmnDetails.taskType === 'signing') {
- this.handleSigningTaskRemove(taskEvent);
- }
-
- // Informs the consumer of this package that a task with the provided taskEvent and taskType has been removed
- this.onProcessTaskRemove({
- taskEvent,
- taskType: this.bpmnDetails.taskType,
- });
- }
-
- /**
- * Deletes the layout set from the deleted data task
- * @private
- */
- private handleDataTaskRemove(): void {
- const layoutSetId = getLayoutSetIdFromTaskId(this.bpmnDetails, this.layoutSets);
-
- if (layoutSetId) {
- this.deleteLayoutSet({
- layoutSetIdToUpdate: layoutSetId,
- });
- }
- }
-
- /**
- * Deletes the dataType and layout set from the deleted payment task
- * @param taskEvent
- * @private
- */
- private handlePaymentTaskRemove(taskEvent: TaskEvent): void {
- // Delete dataType
- const dataTypeId = getDataTypeIdFromBusinessObject(
- this.bpmnDetails.taskType,
- taskEvent.element.businessObject,
- );
-
- this.deleteDataTypeFromAppMetadata({
- dataTypeId,
- });
-
- // Delete layout set
- const layoutSetId = getLayoutSetIdFromTaskId(this.bpmnDetails, this.layoutSets);
- if (layoutSetId) {
- this.deleteLayoutSet({
- layoutSetIdToUpdate: layoutSetId,
- });
- }
- }
-
- /**
- * Deletes the dataType from the deleted signing task
- * @param taskEvent
- * @private
- */
- private handleSigningTaskRemove(taskEvent: TaskEvent): void {
- const dataTypeId = getDataTypeIdFromBusinessObject(
- this.bpmnDetails.taskType,
- taskEvent.element.businessObject,
- );
-
- this.deleteDataTypeFromAppMetadata({
- dataTypeId,
- });
-
- // Delete layout set
- const layoutSetId = getLayoutSetIdFromTaskId(this.bpmnDetails, this.layoutSets);
- if (layoutSetId) {
- this.deleteLayoutSet({
- layoutSetIdToUpdate: layoutSetId,
- });
- }
- }
-}
diff --git a/frontend/packages/process-editor/src/utils/ProcessTaskManager/index.ts b/frontend/packages/process-editor/src/utils/ProcessTaskManager/index.ts
deleted file mode 100644
index 058d60aef5b..00000000000
--- a/frontend/packages/process-editor/src/utils/ProcessTaskManager/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export { AddProcessTaskManager } from './AddProcessTaskManager';
-export { RemoveProcessTaskManager } from './RemoveProcessTaskManager';
-export type { TaskEvent } from './types';
diff --git a/frontend/packages/process-editor/src/utils/hookUtils/hookUtils.test.ts b/frontend/packages/process-editor/src/utils/hookUtils/hookUtils.test.ts
index 4a27b873823..a46a6873fa3 100644
--- a/frontend/packages/process-editor/src/utils/hookUtils/hookUtils.test.ts
+++ b/frontend/packages/process-editor/src/utils/hookUtils/hookUtils.test.ts
@@ -121,18 +121,18 @@ describe('hookUtils', () => {
describe('getLayoutSetIdFromTaskId', () => {
it('should return the layout set id corresponding to the task id', () => {
- const result = getLayoutSetIdFromTaskId({ ...mockBpmnDetails, id: 'task1' }, layoutSets);
+ const result = getLayoutSetIdFromTaskId('task1', layoutSets);
expect(result).toBe('layoutSet1');
});
it('should return undefined if task id does not exist in any layout set', () => {
- const result = getLayoutSetIdFromTaskId(mockBpmnDetails, layoutSets);
+ const result = getLayoutSetIdFromTaskId(mockBpmnDetails.id, layoutSets);
expect(result).toBeUndefined();
});
it('should return undefined if layout sets are empty', () => {
const layoutSets = { sets: [] };
- const result = getLayoutSetIdFromTaskId(mockBpmnDetails, layoutSets);
+ const result = getLayoutSetIdFromTaskId(mockBpmnDetails.id, layoutSets);
expect(result).toBeUndefined();
});
});
diff --git a/frontend/packages/process-editor/src/utils/hookUtils/hookUtils.ts b/frontend/packages/process-editor/src/utils/hookUtils/hookUtils.ts
index 5900912ed05..a36f5e2fbab 100644
--- a/frontend/packages/process-editor/src/utils/hookUtils/hookUtils.ts
+++ b/frontend/packages/process-editor/src/utils/hookUtils/hookUtils.ts
@@ -65,7 +65,7 @@ export const getDataTypeIdFromBusinessObject = (
return businessObject?.extensionElements?.values[0][configNode][dataTypeName];
};
-export const getLayoutSetIdFromTaskId = (bpmnDetails: BpmnDetails, layoutSets: LayoutSets) => {
- const layoutSet = layoutSets.sets.find((set) => set.tasks[0] === bpmnDetails.id);
+export const getLayoutSetIdFromTaskId = (elementId: string, layoutSets: LayoutSets) => {
+ const layoutSet = layoutSets.sets.find((set) => set.tasks[0] === elementId);
return layoutSet?.id;
};
diff --git a/frontend/packages/process-editor/test/mocks/bpmnContextMock.ts b/frontend/packages/process-editor/test/mocks/bpmnContextMock.ts
index 6390bfa5556..97c3d7fe5dd 100644
--- a/frontend/packages/process-editor/test/mocks/bpmnContextMock.ts
+++ b/frontend/packages/process-editor/test/mocks/bpmnContextMock.ts
@@ -42,8 +42,6 @@ export const mockBpmnApiContextValue: BpmnApiContextProps = {
deleteLayoutSet: jest.fn(),
mutateLayoutSetId: jest.fn(),
mutateDataType: jest.fn(),
- addDataTypeToAppMetadata: jest.fn(),
- deleteDataTypeFromAppMetadata: jest.fn(),
saveBpmn: jest.fn(),
openPolicyEditor: jest.fn(),
onProcessTaskRemove: jest.fn(),
diff --git a/frontend/testing/testids.js b/frontend/testing/testids.js
index d30cafbaa5b..26b3047fb7a 100644
--- a/frontend/testing/testids.js
+++ b/frontend/testing/testids.js
@@ -11,5 +11,5 @@ export const typeItemId = (pointer) => `type-item-${pointer}`;
export const userMenuItemId = 'user-menu-item';
export const pageAccordionContentId = (pageName) => `page-accordion-content-${pageName}`;
export const profileButtonId = 'profileButton';
-export const org = 'org';
-export const app = 'app';
+export const org = 'testOrg';
+export const app = 'testApp';
From 7ff8ad8ebf85734ecf78cec2bd84971cfff0c9a0 Mon Sep 17 00:00:00 2001
From: Nina Kylstad
Date: Thu, 6 Jun 2024 14:02:23 +0200
Subject: [PATCH 27/27] added serialization value to icon enum properties
(#12928)
---
backend/src/Designer/Models/FooterFile.cs | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/backend/src/Designer/Models/FooterFile.cs b/backend/src/Designer/Models/FooterFile.cs
index a8ab1330ac2..a5e099529aa 100644
--- a/backend/src/Designer/Models/FooterFile.cs
+++ b/backend/src/Designer/Models/FooterFile.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using System.Runtime.Serialization;
using System.Text.Json.Serialization;
using JetBrains.Annotations;
@@ -40,7 +41,10 @@ public enum ComponentType
public enum IconType
{
+ [EnumMember(Value = "information")]
Information,
+ [EnumMember(Value = "email")]
Email,
+ [EnumMember(Value = "phone")]
Phone
}