;
-type PageComponent = (props: P) => ReactElement;
+export type PageComponent
= (props: P) => ReactElement;
type PageMap = Map;
@@ -20,21 +25,22 @@ export class RouterRouteMapperImpl implements RouterRouteMapper {
return this._configuredRoutes;
}
- constructor(private pages: PageConfig) {
+ constructor(private pages: PagesConfig) {
this._configuredRoutes = this.getConfiguredRoutes(this.pages);
}
- private getConfiguredRoutes(pages: PageConfig): PageMap {
- const pageMap = new Map ReactElement>();
+ private getConfiguredRoutes(pages: PagesConfig): PageMap {
+ const pageMap = new Map();
- Object.keys(pages).forEach((page) => {
- if (page === 'root') {
- pageMap.set('root', Root);
- }
+ pageMap.set('landingPage', LandingPage);
+ Object.keys(pages).forEach((page: PageName) => {
if (page === 'codeList') {
pageMap.set('codeList', CodeList);
}
+ if (page === 'images') {
+ pageMap.set('images', Images);
+ }
});
return pageMap;
diff --git a/frontend/libs/studio-hooks/package.json b/frontend/libs/studio-hooks/package.json
index f4ff62184e8..46fbd7aad54 100644
--- a/frontend/libs/studio-hooks/package.json
+++ b/frontend/libs/studio-hooks/package.json
@@ -22,5 +22,8 @@
"jest-environment-jsdom": "^29.7.0",
"ts-jest": "^29.1.1",
"typescript": "5.6.2"
+ },
+ "peerDependencies": {
+ "react-router-dom": ">=6.0.0"
}
}
diff --git a/frontend/libs/studio-hooks/src/hooks/useOrgAppScopedStorage.test.tsx b/frontend/libs/studio-hooks/src/hooks/useOrgAppScopedStorage.test.tsx
new file mode 100644
index 00000000000..760240946b5
--- /dev/null
+++ b/frontend/libs/studio-hooks/src/hooks/useOrgAppScopedStorage.test.tsx
@@ -0,0 +1,69 @@
+import { renderHook } from '@testing-library/react';
+import { type UseOrgAppScopedStorage, useOrgAppScopedStorage } from './useOrgAppScopedStorage';
+import { useParams } from 'react-router-dom';
+
+jest.mock('react-router-dom', () => ({
+ useParams: jest.fn(),
+}));
+
+const mockedOrg: string = 'testOrg';
+const mockedApp: string = 'testApp';
+const scopedStorageKey: string = 'testOrg-testApp';
+const storagesToTest: Array = ['localStorage', 'sessionStorage'];
+
+describe('useOrgAppScopedStorage', () => {
+ afterEach(() => {
+ window.localStorage.clear();
+ window.sessionStorage.clear();
+ });
+
+ it.each(storagesToTest)(
+ 'initializes ScopedStorageImpl with correct storage scope, %s',
+ (storage) => {
+ const { result } = renderUseOrgAppScopedStorage({ storage });
+
+ result.current.setItem('key', 'value');
+
+ expect(result.current.setItem).toBeDefined();
+ expect(result.current.getItem).toBeDefined();
+ expect(result.current.removeItem).toBeDefined();
+ expect(window[storage].getItem(scopedStorageKey)).toBe('{"key":"value"}');
+ },
+ );
+
+ it.each(storagesToTest)('should retrieve parsed objects from %s', (storage) => {
+ const { result } = renderUseOrgAppScopedStorage({ storage });
+
+ result.current.setItem('person', { name: 'John', age: 18 });
+
+ expect(result.current.getItem('person')).toEqual({
+ name: 'John',
+ age: 18,
+ });
+ });
+
+ it.each(storagesToTest)('should be possible to remove item from %s', (storage) => {
+ const { result } = renderUseOrgAppScopedStorage({ storage });
+
+ result.current.setItem('key', 'value');
+ result.current.removeItem('key');
+ expect(result.current.getItem('key')).toBeUndefined();
+ });
+
+ it('should use localStorage as default storage', () => {
+ const { result } = renderUseOrgAppScopedStorage({});
+ result.current.setItem('key', 'value');
+
+ expect(window.localStorage.getItem(scopedStorageKey)).toBe('{"key":"value"}');
+ });
+});
+
+const renderUseOrgAppScopedStorage = ({ storage }: UseOrgAppScopedStorage) => {
+ (useParams as jest.Mock).mockReturnValue({ org: mockedOrg, app: mockedApp });
+ const { result } = renderHook(() =>
+ useOrgAppScopedStorage({
+ storage,
+ }),
+ );
+ return { result };
+};
diff --git a/frontend/libs/studio-hooks/src/hooks/useOrgAppScopedStorage.ts b/frontend/libs/studio-hooks/src/hooks/useOrgAppScopedStorage.ts
new file mode 100644
index 00000000000..b9582cdba59
--- /dev/null
+++ b/frontend/libs/studio-hooks/src/hooks/useOrgAppScopedStorage.ts
@@ -0,0 +1,36 @@
+import { useParams } from 'react-router-dom';
+import {
+ type ScopedStorage,
+ type ScopedStorageResult,
+ ScopedStorageImpl,
+} from '@studio/pure-functions';
+
+type OrgAppParams = {
+ org: string;
+ app: string;
+};
+
+const supportedStorageMap: Record = {
+ localStorage: window.localStorage,
+ sessionStorage: window.sessionStorage,
+};
+
+export type UseOrgAppScopedStorage = {
+ storage?: 'localStorage' | 'sessionStorage';
+};
+
+type UseOrgAppScopedStorageResult = ScopedStorageResult;
+export const useOrgAppScopedStorage = ({
+ storage = 'localStorage',
+}: UseOrgAppScopedStorage): UseOrgAppScopedStorageResult => {
+ const { org, app } = useParams();
+
+ const storageKey: string = `${org}-${app}`;
+ const scopedStorage = new ScopedStorageImpl(supportedStorageMap[storage], storageKey);
+
+ return {
+ setItem: scopedStorage.setItem,
+ getItem: scopedStorage.getItem,
+ removeItem: scopedStorage.removeItem,
+ };
+};
diff --git a/frontend/libs/studio-icons/src/react/icons/CodeListsIcon.tsx b/frontend/libs/studio-icons/src/react/icons/CodeListsIcon.tsx
new file mode 100644
index 00000000000..54511bbb09f
--- /dev/null
+++ b/frontend/libs/studio-icons/src/react/icons/CodeListsIcon.tsx
@@ -0,0 +1,15 @@
+import React from 'react';
+import { SvgTemplate } from './SvgTemplate';
+import type { IconProps } from '../types';
+
+export const CodeListsIcon = (props: IconProps): React.ReactElement => (
+
+
+
+);
diff --git a/frontend/libs/studio-icons/src/react/icons/index.ts b/frontend/libs/studio-icons/src/react/icons/index.ts
index 541c847d68b..76234f2d6bb 100644
--- a/frontend/libs/studio-icons/src/react/icons/index.ts
+++ b/frontend/libs/studio-icons/src/react/icons/index.ts
@@ -2,6 +2,7 @@ export { AccordionIcon } from './AccordionIcon';
export { ArrayIcon } from './ArrayIcon';
export { BooleanIcon } from './BooleanIcon';
export { CheckboxIcon } from './CheckboxIcon';
+export { CodeListsIcon } from './CodeListsIcon';
export { CombinationIcon } from './CombinationIcon';
export { ConfirmationTaskIcon } from './ConfirmationTaskIcon';
export { DataTaskIcon } from './DataTaskIcon';
diff --git a/frontend/libs/studio-pure-functions/src/ArrayUtils/ArrayUtils.test.ts b/frontend/libs/studio-pure-functions/src/ArrayUtils/ArrayUtils.test.ts
index 686536b1465..6b2bb066296 100644
--- a/frontend/libs/studio-pure-functions/src/ArrayUtils/ArrayUtils.test.ts
+++ b/frontend/libs/studio-pure-functions/src/ArrayUtils/ArrayUtils.test.ts
@@ -117,4 +117,144 @@ describe('ArrayUtils', () => {
expect(ArrayUtils.prepend(['a', 'b', 'c'], 'd')).toEqual(['d', 'a', 'b', 'c']);
});
});
+
+ describe('replaceLastItem', () => {
+ it('should replace the last item in an array and return the modified array', () => {
+ expect(ArrayUtils.replaceLastItem([1, 2, 3], 99)).toEqual([1, 2, 99]);
+ });
+
+ it('should handle arrays with only one item', () => {
+ expect(ArrayUtils.replaceLastItem([5], 42)).toEqual([42]);
+ });
+
+ it('should return an empty array when called on an empty array', () => {
+ expect(ArrayUtils.replaceLastItem([], 10)).toEqual([]);
+ });
+ });
+
+ describe('areItemsUnique', () => {
+ it('Returns true if all items are unique', () => {
+ expect(ArrayUtils.areItemsUnique([1, 2, 3])).toBe(true);
+ expect(ArrayUtils.areItemsUnique(['a', 'b', 'c'])).toBe(true);
+ expect(ArrayUtils.areItemsUnique(['abc', 'bcd', 'cde'])).toBe(true);
+ expect(ArrayUtils.areItemsUnique([true, false])).toBe(true);
+ expect(ArrayUtils.areItemsUnique([1, 'b', true])).toBe(true);
+ expect(ArrayUtils.areItemsUnique([0, '', false, null, undefined])).toBe(true);
+ });
+
+ it('Returns true if array is empty', () => {
+ expect(ArrayUtils.areItemsUnique([])).toBe(true);
+ });
+
+ it('Returns false if there is at least one duplicated item', () => {
+ expect(ArrayUtils.areItemsUnique([1, 2, 1])).toBe(false);
+ expect(ArrayUtils.areItemsUnique(['a', 'a', 'c'])).toBe(false);
+ expect(ArrayUtils.areItemsUnique(['abc', 'bcd', 'bcd'])).toBe(false);
+ expect(ArrayUtils.areItemsUnique([true, false, true])).toBe(false);
+ expect(ArrayUtils.areItemsUnique([1, 'b', false, 1])).toBe(false);
+ expect(ArrayUtils.areItemsUnique([null, null])).toBe(false);
+ expect(ArrayUtils.areItemsUnique([undefined, undefined])).toBe(false);
+ });
+ });
+
+ describe('swapArrayElements', () => {
+ it('Swaps two elements in an array', () => {
+ const arr: string[] = ['a', 'b', 'c', 'd', 'e', 'f'];
+ expect(ArrayUtils.swapArrayElements(arr, 'a', 'b')).toEqual(['b', 'a', 'c', 'd', 'e', 'f']);
+ });
+ });
+
+ describe('insertArrayElementAtPos', () => {
+ const arr = ['a', 'b', 'c'];
+
+ it('Inserts element at given position', () => {
+ expect(ArrayUtils.insertArrayElementAtPos(arr, 'M', 0)).toEqual(['M', 'a', 'b', 'c']);
+ expect(ArrayUtils.insertArrayElementAtPos(arr, 'M', 1)).toEqual(['a', 'M', 'b', 'c']);
+ expect(ArrayUtils.insertArrayElementAtPos(arr, 'M', 3)).toEqual(['a', 'b', 'c', 'M']);
+ });
+
+ it('Inserts element at the end if the position number is too large', () => {
+ expect(ArrayUtils.insertArrayElementAtPos(arr, 'M', 9)).toEqual(['a', 'b', 'c', 'M']);
+ });
+
+ it('Inserts element at the end if the position number is negative', () => {
+ expect(ArrayUtils.insertArrayElementAtPos(arr, 'M', -1)).toEqual(['a', 'b', 'c', 'M']);
+ });
+ });
+
+ describe('mapByKey', () => {
+ it('Returns an array of values mapped by the given key', () => {
+ const array = [
+ { a: 1, b: 2 },
+ { a: 2, b: 'c' },
+ { a: 3, b: true, c: 'abc' },
+ ];
+ expect(ArrayUtils.mapByKey(array, 'a')).toEqual([1, 2, 3]);
+ });
+ });
+
+ describe('replaceByPredicate', () => {
+ it('Replaces the first item matching the predicate with the given item', () => {
+ const array = ['test1', 'test2', 'test3'];
+ const predicate = (item: string) => item === 'test2';
+ const replaceWith = 'test4';
+ expect(ArrayUtils.replaceByPredicate(array, predicate, replaceWith)).toEqual([
+ 'test1',
+ 'test4',
+ 'test3',
+ ]);
+ });
+ });
+
+ describe('rplaceItemsByValue', () => {
+ it('Replaces all items matching the given value with the given replacement', () => {
+ const array = ['a', 'b', 'c'];
+ expect(ArrayUtils.replaceItemsByValue(array, 'b', 'd')).toEqual(['a', 'd', 'c']);
+ });
+ });
+
+ describe('moveArrayItem', () => {
+ it('Moves the item at the given index to the given position when the new position is BEFORE', () => {
+ const array = ['a', 'b', 'c', 'd', 'e', 'f'];
+ expect(ArrayUtils.moveArrayItem(array, 4, 1)).toEqual(['a', 'e', 'b', 'c', 'd', 'f']);
+ });
+
+ it('Moves the item at the given index to the given position when the new position is after', () => {
+ const array = ['a', 'b', 'c', 'd', 'e', 'f'];
+ expect(ArrayUtils.moveArrayItem(array, 1, 4)).toEqual(['a', 'c', 'd', 'e', 'b', 'f']);
+ });
+
+ it('Keeps the array unchanged if the two indices are the same', () => {
+ const array = ['a', 'b', 'c', 'd', 'e', 'f'];
+ expect(ArrayUtils.moveArrayItem(array, 1, 1)).toEqual(array);
+ });
+ });
+
+ describe('generateUniqueStringWithNumber', () => {
+ it('Returns prefix + 0 when the array is empty', () => {
+ expect(ArrayUtils.generateUniqueStringWithNumber([], 'prefix')).toBe('prefix0');
+ });
+
+ it('Returns prefix + 0 when the array does not contain this value already', () => {
+ const array = ['something', 'something else'];
+ expect(ArrayUtils.generateUniqueStringWithNumber(array, 'prefix')).toBe('prefix0');
+ });
+
+ it('Returns prefix + number based on the existing values', () => {
+ const array = ['prefix0', 'prefix1', 'prefix2'];
+ expect(ArrayUtils.generateUniqueStringWithNumber(array, 'prefix')).toBe('prefix3');
+ });
+
+ it('Returns number only when the prefix is empty', () => {
+ const array = ['0', '1', '2'];
+ expect(ArrayUtils.generateUniqueStringWithNumber(array)).toBe('3');
+ });
+ });
+
+ describe('removeEmptyStrings', () => {
+ it('Removes empty strings from an array', () => {
+ const array = ['0', '1', '', '2', ''];
+ expect(ArrayUtils.removeEmptyStrings(array)).toEqual(['0', '1', '2']);
+ });
+ });
});
diff --git a/frontend/libs/studio-pure-functions/src/ArrayUtils/ArrayUtils.ts b/frontend/libs/studio-pure-functions/src/ArrayUtils/ArrayUtils.ts
index 95104904b89..0056739527d 100644
--- a/frontend/libs/studio-pure-functions/src/ArrayUtils/ArrayUtils.ts
+++ b/frontend/libs/studio-pure-functions/src/ArrayUtils/ArrayUtils.ts
@@ -67,4 +67,119 @@ export class ArrayUtils {
public static prepend(array: T[], item: T): T[] {
return [item, ...array];
}
+
+ /**
+ * Replaces the last item in an array.
+ * @param array The array of interest.
+ * @param replaceWith The item to replace the last item with.
+ * @returns The array with the last item replaced.
+ */
+ static replaceLastItem = (array: T[], replaceWith: T): T[] => {
+ if (array.length === 0) {
+ return array;
+ }
+ array[array.length - 1] = replaceWith;
+ return array;
+ };
+
+ /**
+ * Checks if all items in the given array are unique.
+ * @param array The array of interest.
+ * @returns True if all items in the array are unique and false otherwise.
+ */
+ static areItemsUnique = (array: T[]): boolean => array.length === new Set(array).size;
+
+ /**
+ * Swaps the first values with the given values.
+ * @param array Array to swap items in.
+ * @param itemA First value to swap.
+ * @param itemB Second value to swap.
+ * @returns Array with swapped items.
+ */
+ static swapArrayElements = (array: T[], itemA: T, itemB: T): T[] => {
+ const out = [...array];
+ const indexA = array.indexOf(itemA);
+ const indexB = array.indexOf(itemB);
+ out[indexA] = itemB;
+ out[indexB] = itemA;
+ return out;
+ };
+
+ /**
+ * Inserts an item at a given position in an array.
+ * @param array Array to remove item from.
+ * @param item Item to remove.
+ * @param targetPos Position to remove item from.
+ * @returns Array with item inserted at given position.
+ */
+ static insertArrayElementAtPos = (array: T[], item: T, targetPos: number): T[] => {
+ const out = [...array];
+ if (targetPos >= array.length || targetPos < 0) out.push(item);
+ else out.splice(targetPos, 0, item);
+ return out;
+ };
+
+ /**
+ * Maps an array of objects by a given key.
+ * @param array The array of objects.
+ * @param key The key to map by.
+ * @returns An array of values mapped by the given key.
+ */
+ static mapByKey = (array: T[], key: K): T[K][] =>
+ array.map((item) => item[key]);
+
+ /**
+ * Returns an array of which the items matching the given predicate are replaced with the given item.
+ * @param array The array of interest.
+ * @param predicate The predicate to match items by.
+ * @param replaceWith The item to replace the matching items with.
+ * @returns A shallow copy of the array with the matching items replaced.
+ */
+ static replaceByPredicate = (
+ array: T[],
+ predicate: (item: T) => boolean,
+ replaceWith: T,
+ ): T[] => {
+ const out = [...array];
+ const index = array.findIndex(predicate);
+ if (index > -1) out[index] = replaceWith;
+ return out;
+ };
+
+ /**
+ * Returns an array of which the items matching the given value are replaced with the given item.
+ * @param array The array of interest.
+ * @param value The value to match items by.
+ * @param replaceWith The item to replace the matching items with.
+ */
+ static replaceItemsByValue = (array: T[], value: T, replaceWith: T): T[] =>
+ ArrayUtils.replaceByPredicate(array, (item) => item === value, replaceWith);
+
+ /**
+ * Returns an array where the item at the given index is moved to the given index.
+ * @param array The array of interest.
+ * @param from The index of the item to move.
+ * @param to The index to move the item to.
+ */
+ static moveArrayItem = (array: T[], from: number, to: number): T[] => {
+ const out = [...array];
+ const item = out.splice(from, 1)[0];
+ out.splice(to, 0, item);
+ return out;
+ };
+
+ /** Returns a string that is not already present in the given array by appending a number to the given prefix. */
+ static generateUniqueStringWithNumber = (array: string[], prefix: string = ''): string => {
+ let i = 0;
+ let uniqueString = prefix + i;
+ while (array.includes(uniqueString)) {
+ i++;
+ uniqueString = prefix + i;
+ }
+ return uniqueString;
+ };
+
+ /** Removes empty strings from a string array */
+ static removeEmptyStrings = (array: string[]): string[] =>
+ ArrayUtils.removeItemByValue(array, '');
}
diff --git a/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.test.ts b/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.test.ts
new file mode 100644
index 00000000000..6163672b265
--- /dev/null
+++ b/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.test.ts
@@ -0,0 +1,121 @@
+import { type ScopedStorage, ScopedStorageImpl } from './ScopedStorage';
+
+describe('ScopedStorage', () => {
+ beforeEach(() => {
+ window.localStorage.clear();
+ });
+
+ describe('add new key', () => {
+ it('should create a single scoped key with the provided key-value pair as its value', () => {
+ const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test');
+ scopedStorage.setItem('firstName', 'Random Value');
+ expect(scopedStorage.getItem('firstName')).toBe('Random Value');
+ });
+ });
+
+ describe('get item', () => {
+ it('should return "null" if key does not exist', () => {
+ const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test');
+ expect(scopedStorage.getItem('firstName')).toBeNull();
+ });
+ });
+
+ describe('update existing key', () => {
+ it('should append a new key-value pair to the existing scoped key', () => {
+ const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test');
+ scopedStorage.setItem('firstKey', 'first value');
+ scopedStorage.setItem('secondKey', 'secondValue');
+
+ expect(scopedStorage.getItem('firstKey')).toBe('first value');
+ expect(scopedStorage.getItem('secondKey')).toBe('secondValue');
+ });
+
+ it('should update the value of an existing key-value pair within the scoped key if the value has changed', () => {
+ const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test');
+ scopedStorage.setItem('firstKey', 'first value');
+ scopedStorage.setItem('firstKey', 'first value is updated');
+ expect(scopedStorage.getItem('firstKey')).toBe('first value is updated');
+ });
+ });
+
+ describe('delete values from key', () => {
+ it('should remove a specific key-value pair from the existing scoped key', () => {
+ const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test');
+ scopedStorage.setItem('firstKey', 'first value');
+ expect(scopedStorage.getItem('firstKey')).toBeDefined();
+
+ scopedStorage.removeItem('firstKey');
+ expect(scopedStorage.getItem('firstKey')).toBeUndefined();
+ });
+
+ it('should not remove key if it does not exist', () => {
+ const removeItemMock = jest.fn();
+ const customStorage = {
+ getItem: jest.fn().mockImplementation(() => null),
+ removeItem: removeItemMock,
+ setItem: jest.fn(),
+ };
+
+ const scopedStorage = new ScopedStorageImpl(customStorage, 'unit/test');
+ scopedStorage.removeItem('keyDoesNotExist');
+
+ expect(removeItemMock).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('Storage parsing', () => {
+ const consoleErrorMock = jest.fn();
+ const originalConsoleError = console.error;
+ beforeEach(() => {
+ console.error = consoleErrorMock;
+ });
+
+ afterEach(() => {
+ console.error = originalConsoleError;
+ });
+
+ it('should console.error when parsing the storage fails', () => {
+ window.localStorage.setItem('unit/test', '{"person";{"name":"tester"}}');
+ const scopedStorage = new ScopedStorageImpl(window.localStorage, 'unit/test');
+ expect(scopedStorage.getItem('person')).toBeNull();
+ expect(consoleErrorMock).toHaveBeenCalledWith(
+ expect.stringContaining(
+ 'Failed to parse storage with key unit/test. Ensure that the storage is a valid JSON string. Error: SyntaxError:',
+ ),
+ );
+ });
+ });
+
+ // Verify that Dependency Inversion works as expected
+ describe('when using localStorage', () => {
+ it('should store and retrieve values using localStorage', () => {
+ const scopedStorage = new ScopedStorageImpl(window.sessionStorage, 'local/storage');
+ scopedStorage.setItem('firstNameInSession', 'Random Session Value');
+ expect(scopedStorage.getItem('firstNameInSession')).toBe('Random Session Value');
+ });
+ });
+
+ describe('when using sessionStorage', () => {
+ it('should store and retrieve values using sessionStorage', () => {
+ const scopedStorage = new ScopedStorageImpl(window.sessionStorage, 'session/storage');
+ scopedStorage.setItem('firstNameInSession', 'Random Session Value');
+ expect(scopedStorage.getItem('firstNameInSession')).toBe('Random Session Value');
+ });
+ });
+
+ describe('when using a custom storage implementation', () => {
+ it('should store and retrieve values using the provided custom storage', () => {
+ const setItemMock = jest.fn();
+
+ const customStorage: ScopedStorage = {
+ setItem: setItemMock,
+ getItem: jest.fn(),
+ removeItem: jest.fn(),
+ };
+
+ const scopedStorage = new ScopedStorageImpl(customStorage, 'unit/test');
+ scopedStorage.setItem('testKey', 'testValue');
+ expect(setItemMock).toHaveBeenCalledWith('unit/test', '{"testKey":"testValue"}');
+ });
+ });
+});
diff --git a/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.ts b/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.ts
new file mode 100644
index 00000000000..0fc48637a6f
--- /dev/null
+++ b/frontend/libs/studio-pure-functions/src/ScopedStorage/ScopedStorage.ts
@@ -0,0 +1,80 @@
+type StorageKey = string;
+
+export interface ScopedStorage extends Pick {}
+
+export interface ScopedStorageResult extends ScopedStorage {
+ setItem: (key: string, value: T) => void;
+ getItem: (key: string) => T;
+ removeItem: (key: string) => void;
+}
+
+export class ScopedStorageImpl implements ScopedStorage {
+ private readonly storageKey: StorageKey;
+ private readonly scopedStorage: ScopedStorage;
+
+ constructor(
+ private storage: ScopedStorage,
+ private key: StorageKey,
+ ) {
+ this.storageKey = this.key;
+ this.scopedStorage = this.storage;
+ this.setItem = this.setItem.bind(this);
+ this.getItem = this.getItem.bind(this);
+ this.removeItem = this.removeItem.bind(this);
+ }
+
+ public setItem(key: string, value: T): void {
+ const storageRecords: T = this.getAllRecordsInStorage();
+ this.saveToStorage(
+ JSON.stringify({
+ ...storageRecords,
+ [key]: value,
+ }),
+ );
+ }
+
+ public getItem(key: string) {
+ const records: T = this.getAllRecordsInStorage();
+
+ if (!records) {
+ return null;
+ }
+
+ return records[key] as T;
+ }
+
+ public removeItem(key: string): void {
+ const storageRecords: T | null = this.getAllRecordsInStorage();
+
+ if (!storageRecords) {
+ return;
+ }
+
+ const storageCopy = { ...storageRecords };
+ delete storageCopy[key];
+ this.saveToStorage(JSON.stringify({ ...storageCopy }));
+ }
+
+ private getAllRecordsInStorage(): T | null {
+ return this.parseStorageData(this.scopedStorage.getItem(this.storageKey));
+ }
+
+ private saveToStorage(value: string) {
+ this.storage.setItem(this.storageKey, value);
+ }
+
+ private parseStorageData(storage: string | null): T | null {
+ if (!storage) {
+ return null;
+ }
+
+ try {
+ return JSON.parse(storage) satisfies T;
+ } catch (error) {
+ console.error(
+ `Failed to parse storage with key ${this.storageKey}. Ensure that the storage is a valid JSON string. Error: ${error}`,
+ );
+ return null;
+ }
+ }
+}
diff --git a/frontend/libs/studio-pure-functions/src/ScopedStorage/index.ts b/frontend/libs/studio-pure-functions/src/ScopedStorage/index.ts
new file mode 100644
index 00000000000..0b4b3d5747c
--- /dev/null
+++ b/frontend/libs/studio-pure-functions/src/ScopedStorage/index.ts
@@ -0,0 +1 @@
+export { ScopedStorageImpl, type ScopedStorage, type ScopedStorageResult } from './ScopedStorage';
diff --git a/frontend/libs/studio-pure-functions/src/StringUtils/StringUtils.test.ts b/frontend/libs/studio-pure-functions/src/StringUtils/StringUtils.test.ts
index ec036675abd..73bf38ab0ec 100644
--- a/frontend/libs/studio-pure-functions/src/StringUtils/StringUtils.test.ts
+++ b/frontend/libs/studio-pure-functions/src/StringUtils/StringUtils.test.ts
@@ -52,4 +52,56 @@ describe('StringUtils', () => {
expect(input).toBe('abc/def/ghi');
});
});
+
+ describe('replaceEnd', () => {
+ it('Replaces the given substring with the given replacement at the end of the string', () => {
+ expect(StringUtils.replaceEnd('abc/def/ghi', 'ghi', 'xyz')).toBe('abc/def/xyz');
+ });
+
+ it('Does not replace the given substring other places than at the end', () => {
+ expect(StringUtils.replaceEnd('abcdefghi', 'abc', 'xyz')).toBe('abcdefghi');
+ expect(StringUtils.replaceEnd('abcdefghi', 'def', 'xyz')).toBe('abcdefghi');
+ expect(StringUtils.replaceEnd('abcdefghidef', 'def', 'xyz')).toBe('abcdefghixyz');
+ });
+ });
+
+ describe('replaceStart', () => {
+ it('Replaces the given substring with the given replacement at the start of the string', () => {
+ expect(StringUtils.replaceStart('abc/def/ghi', 'abc', 'xyz')).toBe('xyz/def/ghi');
+ });
+
+ it('Does not replace the given substring other places than at the start', () => {
+ expect(StringUtils.replaceStart('abcdefghi', 'ghi', 'xyz')).toBe('abcdefghi');
+ expect(StringUtils.replaceStart('abcdefghi', 'def', 'xyz')).toBe('abcdefghi');
+ expect(StringUtils.replaceStart('defabcdefghi', 'def', 'xyz')).toBe('xyzabcdefghi');
+ });
+ });
+
+ describe('substringBeforeLast', () => {
+ it('Returns substring before last occurrence of separator', () => {
+ expect(StringUtils.substringBeforeLast('abc/def/ghi', '/')).toBe('abc/def');
+ });
+
+ it('Returns whole string if separator is not found', () => {
+ expect(StringUtils.substringBeforeLast('abc', '/')).toBe('abc');
+ });
+
+ it('Returns whole string if there are no characters before the last separator', () => {
+ expect(StringUtils.substringBeforeLast('/abc', '/')).toBe('');
+ });
+ });
+
+ describe('substringAfterLast', () => {
+ it('Returns substring after last occurrence of separator', () => {
+ expect(StringUtils.substringAfterLast('abc/def/ghi', '/')).toBe('ghi');
+ });
+
+ it('Returns whole string if separator is not found', () => {
+ expect(StringUtils.substringAfterLast('abc', '/')).toBe('abc');
+ });
+
+ it('Returns empty string if there are no characters after the last separator', () => {
+ expect(StringUtils.substringAfterLast('abc/def/', '/')).toBe('');
+ });
+ });
});
diff --git a/frontend/libs/studio-pure-functions/src/StringUtils/StringUtils.ts b/frontend/libs/studio-pure-functions/src/StringUtils/StringUtils.ts
index f38304fb861..b5a3927feb4 100644
--- a/frontend/libs/studio-pure-functions/src/StringUtils/StringUtils.ts
+++ b/frontend/libs/studio-pure-functions/src/StringUtils/StringUtils.ts
@@ -1,3 +1,5 @@
+import { ArrayUtils } from '../ArrayUtils';
+
export class StringUtils {
/**
* Removes any of the given substrings from the start of the string.
@@ -32,4 +34,48 @@ export class StringUtils {
}
return str;
};
+
+ /**
+ * Replaces the given substring with the given replacement at the end of the string.
+ * If the substring does not appear at the end of the string, the string is returned unchanged.
+ * @param str The string to search in.
+ * @param substring The substring to search for.
+ * @param replacement The replacement to replace the substring with.
+ * @returns The string with the substring replaced at the end.
+ */
+ static replaceEnd = (str: string, substring: string, replacement: string): string =>
+ str.replace(new RegExp(substring + '$'), replacement);
+
+ /**
+ * Replaces the given substring with the given replacement at the start of the string.
+ * If the substring does not appear at the start of the string, the string is returned unchanged.
+ * @param str The string to search in.
+ * @param substring The substring to search for.
+ * @param replacement The replacement to replace the substring with.
+ * @returns The string with the substring replaced at the start.
+ */
+ static replaceStart = (str: string, substring: string, replacement: string): string => {
+ if (str.startsWith(substring)) {
+ return replacement + str.slice(substring.length);
+ }
+ return str;
+ };
+
+ /**
+ * Returns substring before last occurrence of separator.
+ * @param str The string to search in.
+ * @param separator The separator to search for.
+ * @returns The substring before the last occurrence of the given separator.
+ */
+ static substringBeforeLast = (str: string, separator: string): string =>
+ str.includes(separator) ? str.substring(0, str.lastIndexOf(separator)) : str;
+
+ /**
+ * Returns substring after last occurrence of separator.
+ * @param str The string to search in.
+ * @param separator The separator to search for.
+ * @returns The substring after the last occurrence of the given separator.
+ */
+ static substringAfterLast = (str: string, separator: string): string =>
+ ArrayUtils.last(str.split(separator)) || '';
}
diff --git a/frontend/libs/studio-pure-functions/src/index.ts b/frontend/libs/studio-pure-functions/src/index.ts
index fb31ab06c5e..7e38f84007c 100644
--- a/frontend/libs/studio-pure-functions/src/index.ts
+++ b/frontend/libs/studio-pure-functions/src/index.ts
@@ -3,5 +3,6 @@ export * from './BlobDownloader';
export * from './DateUtils';
export * from './NumberUtils';
export * from './ObjectUtils';
+export * from './ScopedStorage';
export * from './StringUtils';
export * from './types';
diff --git a/frontend/packages/process-editor/src/components/ConfigPanel/ConfigSequenceFlow/ConfigSequenceFlow.tsx b/frontend/packages/process-editor/src/components/ConfigPanel/ConfigSequenceFlow/ConfigSequenceFlow.tsx
index e2343dab2dd..4d888b84aa9 100644
--- a/frontend/packages/process-editor/src/components/ConfigPanel/ConfigSequenceFlow/ConfigSequenceFlow.tsx
+++ b/frontend/packages/process-editor/src/components/ConfigPanel/ConfigSequenceFlow/ConfigSequenceFlow.tsx
@@ -10,7 +10,7 @@ import { PlusIcon } from '@studio/icons';
import { useBpmnContext } from '../../../contexts/BpmnContext';
import { Paragraph } from '@digdir/designsystemet-react';
import { BpmnExpressionModeler } from '../../../utils/bpmnModeler/BpmnExpressionModeler';
-import { useExpressionTexts } from 'app-shared/components/Expression/useExpressionTexts';
+import { useExpressionTexts } from 'app-shared/hooks/useExpressionTexts';
import { useTranslation } from 'react-i18next';
import classes from './ConfigSequenceFlow.module.css';
import { ConfigIcon } from '../../../components/ConfigPanel/ConfigContent/ConfigIcon';
diff --git a/frontend/packages/process-editor/src/hooks/useBpmnEditor.ts b/frontend/packages/process-editor/src/hooks/useBpmnEditor.ts
index 0d8b2a84690..cf6c51b7ea0 100644
--- a/frontend/packages/process-editor/src/hooks/useBpmnEditor.ts
+++ b/frontend/packages/process-editor/src/hooks/useBpmnEditor.ts
@@ -36,7 +36,11 @@ export const useBpmnEditor = (): UseBpmnViewerResult => {
taskEvent,
taskType: bpmnDetails.taskType,
});
- if (bpmnDetails.taskType === 'data' || bpmnDetails.taskType === 'payment')
+ if (
+ bpmnDetails.taskType === 'data' ||
+ bpmnDetails.taskType === 'payment' ||
+ bpmnDetails.taskType === 'signing'
+ )
addAction(bpmnDetails.id);
};
diff --git a/frontend/packages/schema-editor/src/components/SchemaInspector/ItemRestrictions/EnumList/EnumList.tsx b/frontend/packages/schema-editor/src/components/SchemaInspector/ItemRestrictions/EnumList/EnumList.tsx
index c202eccd2e3..ae27c271845 100644
--- a/frontend/packages/schema-editor/src/components/SchemaInspector/ItemRestrictions/EnumList/EnumList.tsx
+++ b/frontend/packages/schema-editor/src/components/SchemaInspector/ItemRestrictions/EnumList/EnumList.tsx
@@ -8,7 +8,6 @@ import { useTranslation } from 'react-i18next';
import { PlusIcon } from '@studio/icons';
import { findDuplicateValues } from './utils';
import { useSchemaEditorAppContext } from '@altinn/schema-editor/hooks/useSchemaEditorAppContext';
-import { removeEmptyStrings } from 'app-shared/utils/arrayUtils';
import { StudioButton } from '@studio/components';
export type EnumListProps = {
@@ -44,7 +43,7 @@ export const EnumList = ({ schemaNode }: EnumListProps): JSX.Element => {
const duplicates: string[] = findDuplicateValues(newEnumList);
if (duplicates === null) {
- const newNode = { ...schemaNode, enum: removeEmptyStrings(newEnumList) };
+ const newNode = { ...schemaNode, enum: ArrayUtils.removeEmptyStrings(newEnumList) };
save(schemaModel.updateNode(newNode.schemaPointer, newNode));
}
diff --git a/frontend/packages/schema-editor/src/components/SchemaInspector/ItemRestrictions/EnumList/utils.ts b/frontend/packages/schema-editor/src/components/SchemaInspector/ItemRestrictions/EnumList/utils.ts
index c5e92bb0219..752bda76999 100644
--- a/frontend/packages/schema-editor/src/components/SchemaInspector/ItemRestrictions/EnumList/utils.ts
+++ b/frontend/packages/schema-editor/src/components/SchemaInspector/ItemRestrictions/EnumList/utils.ts
@@ -1,9 +1,9 @@
-import { areItemsUnique, removeEmptyStrings } from 'app-shared/utils/arrayUtils';
+import { ArrayUtils } from '@studio/pure-functions';
export const findDuplicateValues = (array: string[]): string[] | null => {
- const arrayWithoutEmptyStrings: string[] = removeEmptyStrings(array);
+ const arrayWithoutEmptyStrings: string[] = ArrayUtils.removeEmptyStrings(array);
- if (areItemsUnique(arrayWithoutEmptyStrings)) return null;
+ if (ArrayUtils.areItemsUnique(arrayWithoutEmptyStrings)) return null;
return findDuplicates(arrayWithoutEmptyStrings);
};
diff --git a/frontend/packages/schema-model/src/lib/SchemaModel/SchemaModel.ts b/frontend/packages/schema-model/src/lib/SchemaModel/SchemaModel.ts
index 85263007fe8..06484fd850c 100644
--- a/frontend/packages/schema-model/src/lib/SchemaModel/SchemaModel.ts
+++ b/frontend/packages/schema-model/src/lib/SchemaModel/SchemaModel.ts
@@ -16,16 +16,9 @@ import {
isNodeValidParent,
isReference,
} from '../utils';
-import {
- generateUniqueStringWithNumber,
- insertArrayElementAtPos,
- moveArrayItem,
- replaceItemsByValue,
-} from 'app-shared/utils/arrayUtils';
import { ROOT_POINTER, UNIQUE_POINTER_PREFIX } from '../constants';
import type { ReferenceNode } from '../../types/ReferenceNode';
import { ObjectUtils, ArrayUtils, StringUtils } from '@studio/pure-functions';
-import { replaceStart } from 'app-shared/utils/stringUtils';
import {
createDefinitionPointer,
createPropertyPointer,
@@ -199,7 +192,7 @@ export class SchemaModel extends SchemaModelBase {
private addChildPointer = (target: NodePosition, newPointer: string): void => {
const parent = this.getNodeBySchemaPointer(target.parentPointer) as FieldNode | CombinationNode;
if (!isNodeValidParent(parent)) throw new Error('Invalid parent node.');
- parent.children = insertArrayElementAtPos(parent.children, newPointer, target.index);
+ parent.children = ArrayUtils.insertArrayElementAtPos(parent.children, newPointer, target.index);
};
public addFieldType = (name: string): FieldNode => {
@@ -249,7 +242,7 @@ export class SchemaModel extends SchemaModelBase {
toIndex: number,
): UiSchemaNode => {
const finalIndex = ArrayUtils.getValidIndex(parent.children, toIndex);
- parent.children = moveArrayItem(parent.children, fromIndex, finalIndex);
+ parent.children = ArrayUtils.moveArrayItem(parent.children, fromIndex, finalIndex);
this.synchronizeCombinationChildPointers(parent);
return this.getNodeBySchemaPointer(parent.children[toIndex]);
};
@@ -303,7 +296,7 @@ export class SchemaModel extends SchemaModelBase {
): UiSchemaNode => {
const currentIndex = this.getIndexOfChildNode(schemaPointer);
const finalIndex = ArrayUtils.getValidIndex(parent.children, newIndex);
- parent.children = moveArrayItem(parent.children, currentIndex, finalIndex);
+ parent.children = ArrayUtils.moveArrayItem(parent.children, currentIndex, finalIndex);
return this.getNodeBySchemaPointer(parent.children[finalIndex]);
};
@@ -359,7 +352,7 @@ export class SchemaModel extends SchemaModelBase {
private changePointerInParent(oldPointer: string, newPointer: string): void {
const parentNode = this.getParentNode(oldPointer);
if (parentNode) {
- const children = replaceItemsByValue(parentNode.children, oldPointer, newPointer);
+ const children = ArrayUtils.replaceItemsByValue(parentNode.children, oldPointer, newPointer);
this.updateNodeData(parentNode.schemaPointer, { ...parentNode, children });
}
}
@@ -375,7 +368,7 @@ export class SchemaModel extends SchemaModelBase {
const node = this.getNodeBySchemaPointer(newPointer); // Expects the node map to be updated
if (isFieldOrCombination(node) && node.children) {
const makeNewPointer = (schemaPointer: string) =>
- replaceStart(schemaPointer, oldPointer, newPointer);
+ StringUtils.replaceStart(schemaPointer, oldPointer, newPointer);
node.children.forEach((childPointer) => {
const newPointer = makeNewPointer(childPointer);
this.changePointer(childPointer, newPointer);
@@ -426,14 +419,14 @@ export class SchemaModel extends SchemaModelBase {
const node = this.getNodeBySchemaPointer(schemaPointer);
const childPointers = isFieldOrCombination(node) ? node.children : [];
const childNames = childPointers.map(extractNameFromPointer);
- return generateUniqueStringWithNumber(childNames, namePrefix);
+ return ArrayUtils.generateUniqueStringWithNumber(childNames, namePrefix);
}
public generateUniqueDefinitionName(namePrefix: string = ''): string {
const definitions = this.getDefinitions();
const definitionPointers = definitions.map((node) => node.schemaPointer);
const definitionNames = definitionPointers.map(extractNameFromPointer);
- return generateUniqueStringWithNumber(definitionNames, namePrefix);
+ return ArrayUtils.generateUniqueStringWithNumber(definitionNames, namePrefix);
}
public changeCombinationType(
diff --git a/frontend/packages/schema-model/src/lib/mappers/getPointers.ts b/frontend/packages/schema-model/src/lib/mappers/getPointers.ts
index 59b7ef25d1f..3121797a80b 100644
--- a/frontend/packages/schema-model/src/lib/mappers/getPointers.ts
+++ b/frontend/packages/schema-model/src/lib/mappers/getPointers.ts
@@ -1,5 +1,5 @@
import type { UiSchemaNodes } from '../../types';
-import { mapByKey } from 'app-shared/utils/arrayUtils';
+import { ArrayUtils } from '@studio/pure-functions';
/**
* Returns all pointers from uiSchema.
@@ -7,4 +7,4 @@ import { mapByKey } from 'app-shared/utils/arrayUtils';
* @returns An array of pointers.
*/
export const getPointers = (uiSchema: UiSchemaNodes): string[] =>
- mapByKey(uiSchema, 'schemaPointer');
+ ArrayUtils.mapByKey(uiSchema, 'schemaPointer');
diff --git a/frontend/packages/schema-model/src/lib/mutations/ui-schema-reducers.test.ts b/frontend/packages/schema-model/src/lib/mutations/ui-schema-reducers.test.ts
index d8b7eb144b1..d115e07bfa2 100644
--- a/frontend/packages/schema-model/src/lib/mutations/ui-schema-reducers.test.ts
+++ b/frontend/packages/schema-model/src/lib/mutations/ui-schema-reducers.test.ts
@@ -47,13 +47,13 @@ import { expect } from '@jest/globals';
import { CombinationKind, FieldType, Keyword, ObjectKind, StrRestrictionKey } from '../../types';
import { ROOT_POINTER } from '../constants';
import { getPointers } from '../mappers/getPointers';
-import { substringAfterLast, substringBeforeLast } from 'app-shared/utils/stringUtils';
import type { KeyValuePairs } from 'app-shared/types/KeyValuePairs';
import { validateTestUiSchema } from '../../../test/validateTestUiSchema';
import { SchemaModel } from '../SchemaModel';
import type { FieldNode } from '../../types/FieldNode';
import type { ReferenceNode } from '../../types/ReferenceNode';
import type { CombinationNode } from '../../types/CombinationNode';
+import { StringUtils } from '@studio/pure-functions';
describe('ui-schema-reducers', () => {
let result: SchemaModel;
@@ -73,7 +73,7 @@ describe('ui-schema-reducers', () => {
it('Converts a property to a root level definition', () => {
const { schemaPointer } = stringNodeMock;
result = promoteProperty(createNewModelMock(), schemaPointer);
- const expectedPointer = `${ROOT_POINTER}/$defs/${substringAfterLast(schemaPointer, '/')}`;
+ const expectedPointer = `${ROOT_POINTER}/$defs/${StringUtils.substringAfterLast(schemaPointer, '/')}`;
expect(getPointers(result.asArray())).toContain(expectedPointer);
expect(result.getNodeBySchemaPointer(expectedPointer)).toMatchObject({
fieldType: stringNodeMock.fieldType,
@@ -223,7 +223,7 @@ describe('ui-schema-reducers', () => {
const name = 'new name';
const callback = jest.fn();
const args: SetPropertyNameArgs = { path: schemaPointer, name, callback };
- const expectedPointer = substringBeforeLast(schemaPointer, '/') + '/' + name;
+ const expectedPointer = StringUtils.substringBeforeLast(schemaPointer, '/') + '/' + name;
it('Sets the name of the given property', () => {
result = setPropertyName(createNewModelMock(), args);
diff --git a/frontend/packages/schema-model/src/lib/mutations/ui-schema-reducers.ts b/frontend/packages/schema-model/src/lib/mutations/ui-schema-reducers.ts
index bb824511f99..bc16412edfa 100644
--- a/frontend/packages/schema-model/src/lib/mutations/ui-schema-reducers.ts
+++ b/frontend/packages/schema-model/src/lib/mutations/ui-schema-reducers.ts
@@ -4,7 +4,7 @@ import { isField, isReference, splitPointerInBaseAndName } from '../utils';
import { convertPropToType } from './convert-node';
import type { KeyValuePairs } from 'app-shared/types/KeyValuePairs';
import { castRestrictionType } from '../restrictions';
-import { swapArrayElements } from 'app-shared/utils/arrayUtils';
+import { ArrayUtils } from '@studio/pure-functions';
import { changeNameInPointer } from '../pointerUtils';
export const promoteProperty: UiSchemaReducer = (uiSchema, path) => {
@@ -206,6 +206,7 @@ export const changeChildrenOrder: UiSchemaReducer = (
if (baseA !== baseB) return uiSchema;
const newSchema = uiSchema.deepClone();
const parentNode = newSchema.getParentNode(pointerA);
- if (parentNode) parentNode.children = swapArrayElements(parentNode.children, pointerA, pointerB);
+ if (parentNode)
+ parentNode.children = ArrayUtils.swapArrayElements(parentNode.children, pointerA, pointerB);
return newSchema;
};
diff --git a/frontend/packages/schema-model/test/validateTestUiSchema.ts b/frontend/packages/schema-model/test/validateTestUiSchema.ts
index 12fa4077962..2010155b44a 100644
--- a/frontend/packages/schema-model/test/validateTestUiSchema.ts
+++ b/frontend/packages/schema-model/test/validateTestUiSchema.ts
@@ -1,7 +1,6 @@
import type { UiSchemaNodes } from '../src';
import { FieldType, ObjectKind, ROOT_POINTER } from '../src';
import { getPointers } from '../src/lib/mappers/getPointers';
-import { areItemsUnique, mapByKey } from 'app-shared/utils/arrayUtils';
import {
isField,
isFieldOrCombination,
@@ -20,7 +19,7 @@ export const hasRootNode = (uiSchema: UiSchemaNodes) =>
/** Verifies that all pointers are unique */
export const pointersAreUnique = (uiSchema: UiSchemaNodes) =>
- expect(areItemsUnique(getPointers(uiSchema))).toBe(true);
+ expect(ArrayUtils.areItemsUnique(getPointers(uiSchema))).toBe(true);
/** Verifies that all pointers referenced to as children exist */
export const allPointersExist = (uiSchema: UiSchemaNodes) => {
@@ -33,7 +32,10 @@ export const allPointersExist = (uiSchema: UiSchemaNodes) => {
/** Verifies that all nodes except the root node have a parent */
export const nodesHaveParent = (uiSchema: UiSchemaNodes) => {
- const allChildPointers = mapByKey(uiSchema.filter(isFieldOrCombination), 'children').flat();
+ const allChildPointers = ArrayUtils.mapByKey(
+ uiSchema.filter(isFieldOrCombination),
+ 'children',
+ ).flat();
ArrayUtils.removeItemByValue(getPointers(uiSchema), ROOT_POINTER).forEach((schemaPointer) => {
expect(allChildPointers).toContain(schemaPointer);
});
diff --git a/frontend/packages/shared/src/components/FileSelector.test.tsx b/frontend/packages/shared/src/components/FileSelector.test.tsx
deleted file mode 100644
index c66a9077aeb..00000000000
--- a/frontend/packages/shared/src/components/FileSelector.test.tsx
+++ /dev/null
@@ -1,97 +0,0 @@
-import React from 'react';
-import { render, screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import type { IFileSelectorProps } from './FileSelector';
-import { FileSelector } from './FileSelector';
-import { textMock } from '@studio/testing/mocks/i18nMock';
-import { Button } from '@digdir/designsystemet-react';
-import { fileSelectorInputId } from '@studio/testing/testids';
-import { toast } from 'react-toastify';
-
-jest.mock('react-toastify', () => ({
- toast: {
- error: jest.fn(),
- },
-}));
-
-const user = userEvent.setup();
-
-const renderFileSelector = (props: Partial = {}) => {
- const allProps: IFileSelectorProps = {
- submitHandler: jest.fn(),
- busy: false,
- formFileName: '',
- ...props,
- };
-
- render( );
-};
-
-const customButtonText = 'Lorem ipsum';
-const testCustomButtonRenderer = (onClick: React.MouseEventHandler) => (
- {customButtonText}
-);
-
-describe('FileSelector', () => {
- it('should not call submitHandler when no files are selected', async () => {
- const handleSubmit = jest.fn();
- renderFileSelector({ submitHandler: handleSubmit });
-
- const fileInput = screen.getByTestId(fileSelectorInputId);
- await user.upload(fileInput, null);
-
- expect(handleSubmit).not.toHaveBeenCalled();
- });
-
- it('should call submitHandler when a file is selected', async () => {
- const file = new File(['hello'], 'hello.png', { type: 'image/png' });
- const handleSubmit = jest.fn();
- renderFileSelector({ submitHandler: handleSubmit });
-
- const fileInput = screen.getByTestId(fileSelectorInputId);
- await user.upload(fileInput, file);
-
- expect(handleSubmit).toHaveBeenCalledWith(expect.any(FormData), 'hello.png');
- });
-
- it('Should show text on the button by default', async () => {
- renderFileSelector();
- expect(
- screen.getByRole('button', { name: textMock('app_data_modelling.upload_xsd') }),
- ).toBeInTheDocument();
- });
-
- it('Should show custom button', async () => {
- renderFileSelector({ submitButtonRenderer: testCustomButtonRenderer });
- expect(screen.getByRole('button', { name: customButtonText })).toBeInTheDocument();
- });
-
- it('Should call file input onClick handler when the default upload button is clicked', async () => {
- renderFileSelector();
- const button = screen.getByRole('button', { name: textMock('app_data_modelling.upload_xsd') });
- const fileInput = screen.getByTestId(fileSelectorInputId);
- fileInput.onclick = jest.fn();
- await user.click(button);
- expect(fileInput.onclick).toHaveBeenCalled();
- });
-
- it('Should call file input onClick handler when the custom upload button is clicked', async () => {
- renderFileSelector({ submitButtonRenderer: testCustomButtonRenderer });
- const button = screen.getByRole('button', { name: customButtonText });
- const fileInput = screen.getByTestId(fileSelectorInputId);
- fileInput.onclick = jest.fn();
- 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('schema_editor.invalid_datamodel_upload_filename'),
- );
- });
-});
diff --git a/frontend/packages/shared/src/components/FileSelector.tsx b/frontend/packages/shared/src/components/FileSelector.tsx
deleted file mode 100644
index a9a86af00f2..00000000000
--- a/frontend/packages/shared/src/components/FileSelector.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import React from 'react';
-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';
-import classes from './FileSelector.module.css';
-
-export interface IFileSelectorProps {
- accept?: string;
- busy: boolean;
- disabled?: boolean;
- formFileName: string;
- submitButtonRenderer?: (fileInputClickHandler: (event: any) => void) => JSX.Element;
- submitHandler: (file: FormData, fileName: string) => void;
-}
-
-export const FileSelector = ({
- accept = undefined,
- busy,
- disabled,
- formFileName,
- submitButtonRenderer,
- submitHandler,
-}: IFileSelectorProps) => {
- const { t } = useTranslation();
- const defaultSubmitButtonRenderer = (fileInputClickHandler: (event: any) => void) => (
- }
- onClick={fileInputClickHandler}
- disabled={disabled}
- variant='tertiary'
- >
- {t('app_data_modelling.upload_xsd')}
-
- );
-
- const fileInput = React.useRef(null);
-
- 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('schema_editor.invalid_datamodel_upload_filename'));
- fileInput.current.value = '';
- return;
- }
-
- if (file) {
- const formData = new FormData();
- formData.append(formFileName, file);
- submitHandler(formData, file.name);
- }
- };
-
- const handleInputChange = () => {
- const file = fileInput?.current?.files?.item(0);
- if (file) handleSubmit();
- };
-
- return (
-
- );
-};
diff --git a/frontend/packages/shared/src/components/atoms/AltinnContentIcon.tsx b/frontend/packages/shared/src/components/atoms/AltinnContentIcon.tsx
deleted file mode 100644
index 0f8e5c06b5c..00000000000
--- a/frontend/packages/shared/src/components/atoms/AltinnContentIcon.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import React from 'react';
-
-export function AltinnContentIcon() {
- return (
- <>
-
-
-
-
-
-
-
-
-
-
-
- >
- );
-}
-
-export default AltinnContentIcon;
diff --git a/frontend/packages/shared/src/components/molecules/AltinnContentLoader.tsx b/frontend/packages/shared/src/components/molecules/AltinnContentLoader.tsx
index d8a98675fc5..d80ec5e8bbd 100644
--- a/frontend/packages/shared/src/components/molecules/AltinnContentLoader.tsx
+++ b/frontend/packages/shared/src/components/molecules/AltinnContentLoader.tsx
@@ -1,23 +1,24 @@
-import React from 'react';
+import React, { type ReactNode } from 'react';
import type { IContentLoaderProps } from 'react-content-loader';
import ContentLoader from 'react-content-loader';
-import AltinnContentIconComponent from '../atoms/AltinnContentIcon';
export type IAltinnContentLoaderProps = {
/** The height of the loader, defaults to 200 */
height?: number;
/** The width of the loader, defaults to 400 */
width?: number;
+ children: ReactNode;
} & IContentLoaderProps;
-export const AltinnContentLoader = (props: React.PropsWithChildren) => {
+export const AltinnContentLoader = ({
+ height,
+ width,
+ children,
+ ...rest
+}: IAltinnContentLoaderProps) => {
return (
-
- {props.children ? props.children : }
+
+ {children}
);
};
diff --git a/frontend/packages/shared/src/constants.js b/frontend/packages/shared/src/constants.js
index a4dea619350..193e01a5748 100644
--- a/frontend/packages/shared/src/constants.js
+++ b/frontend/packages/shared/src/constants.js
@@ -3,6 +3,7 @@ export const APP_DEVELOPMENT_BASENAME = '/editor';
export const DASHBOARD_BASENAME = '/dashboard';
export const DASHBOARD_ROOT_ROUTE = '/';
export const RESOURCEADM_BASENAME = '/resourceadm';
+export const STUDIO_LIBRARY_BASENAME = '/library';
export const PREVIEW_BASENAME = '/preview';
export const STUDIO_ROOT_BASENAME = '/';
export const DEFAULT_LANGUAGE = 'nb';
diff --git a/frontend/packages/shared/src/hooks/useConfirmationDialogOnPageLeave.test.tsx b/frontend/packages/shared/src/hooks/useConfirmationDialogOnPageLeave.test.tsx
deleted file mode 100644
index cd92fdbe68e..00000000000
--- a/frontend/packages/shared/src/hooks/useConfirmationDialogOnPageLeave.test.tsx
+++ /dev/null
@@ -1,134 +0,0 @@
-import React from 'react';
-import { render as rtlRender, waitFor } from '@testing-library/react';
-import { useConfirmationDialogOnPageLeave } from './useConfirmationDialogOnPageLeave';
-import { RouterProvider, createMemoryRouter, useBeforeUnload } from 'react-router-dom';
-
-jest.mock('react-router-dom', () => ({
- ...jest.requireActual('react-router-dom'),
- useBeforeUnload: jest.fn(),
-}));
-
-const confirmationMessage = 'test';
-
-const Component = ({ showConfirmationDialog }: { showConfirmationDialog: boolean }) => {
- useConfirmationDialogOnPageLeave(showConfirmationDialog, confirmationMessage);
- return null;
-};
-
-const render = (showConfirmationDialog: boolean) => {
- const router = createMemoryRouter([
- {
- path: '/',
- element: ,
- },
- {
- path: '/test',
- element: null,
- },
- ]);
-
- const { rerender } = rtlRender( );
- return {
- rerender,
- router,
- };
-};
-
-describe('useConfirmationDialogOnPageLeave', () => {
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
- it('should call useBeforeUnload with the expected arguments', () => {
- const showConfirmationDialog = true;
- render(showConfirmationDialog);
-
- expect(useBeforeUnload).toHaveBeenCalledWith(expect.any(Function), {
- capture: true,
- });
- });
-
- it('should prevent navigation if showConfirmationDialog is true', () => {
- const event = {
- type: 'beforeunload',
- returnValue: confirmationMessage,
- } as BeforeUnloadEvent;
- event.preventDefault = jest.fn();
-
- const showConfirmationDialog = true;
- render(showConfirmationDialog);
-
- const callbackFn = (useBeforeUnload as jest.MockedFunction).mock
- .calls[0][0];
- callbackFn(event);
-
- expect(event.preventDefault).toHaveBeenCalled();
- expect(event.returnValue).toBe(confirmationMessage);
- });
-
- it('should not prevent navigation if showConfirmationDialog is false', () => {
- const event = {
- type: 'beforeunload',
- returnValue: '',
- } as BeforeUnloadEvent;
- event.preventDefault = jest.fn();
-
- const showConfirmationDialog = false;
- render(showConfirmationDialog);
-
- const callbackFn = (useBeforeUnload as jest.MockedFunction).mock
- .calls[0][0];
- callbackFn(event);
-
- expect(event.preventDefault).not.toHaveBeenCalled();
- expect(event.returnValue).toBe('');
- });
-
- it('doesnt show confirmation dialog when there are no unsaved changes', async () => {
- window.confirm = jest.fn();
-
- const showConfirmationDialog = false;
- const { router } = render(showConfirmationDialog);
-
- await waitFor(() => router.navigate('/test'));
-
- expect(window.confirm).toHaveBeenCalledTimes(0);
- expect(router.state.location.pathname).toBe('/test');
- });
-
- it('show confirmation dialog when there are unsaved changes', async () => {
- window.confirm = jest.fn();
-
- const showConfirmationDialog = true;
- const { router } = render(showConfirmationDialog);
-
- await waitFor(() => router.navigate('/test'));
-
- expect(window.confirm).toHaveBeenCalledTimes(1);
- expect(router.state.location.pathname).toBe('/');
- });
-
- it('cancel redirection when clicking cancel', async () => {
- window.confirm = jest.fn(() => false);
-
- const showConfirmationDialog = true;
- const { router } = render(showConfirmationDialog);
-
- await waitFor(() => router.navigate('/test'));
-
- expect(window.confirm).toHaveBeenCalledTimes(1);
- expect(router.state.location.pathname).toBe('/');
- });
-
- it('redirect when clicking OK', async () => {
- window.confirm = jest.fn(() => true);
-
- const showConfirmationDialog = true;
- const { router } = render(showConfirmationDialog);
-
- await waitFor(() => router.navigate('/test'));
-
- expect(window.confirm).toHaveBeenCalledTimes(1);
- expect(router.state.location.pathname).toBe('/test');
- });
-});
diff --git a/frontend/packages/shared/src/hooks/useConfirmationDialogOnPageLeave.ts b/frontend/packages/shared/src/hooks/useConfirmationDialogOnPageLeave.ts
deleted file mode 100644
index 24b7a993d0c..00000000000
--- a/frontend/packages/shared/src/hooks/useConfirmationDialogOnPageLeave.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { useCallback, useEffect } from 'react';
-import { useBeforeUnload, useBlocker } from 'react-router-dom';
-
-export const useConfirmationDialogOnPageLeave = (
- showConfirmationDialog: boolean,
- confirmationMessage: string,
-) => {
- useBeforeUnload(
- useCallback(
- (event: BeforeUnloadEvent) => {
- if (showConfirmationDialog) {
- event.preventDefault();
- event.returnValue = confirmationMessage;
- }
- },
- [showConfirmationDialog, confirmationMessage],
- ),
- { capture: true },
- );
-
- const blocker = useBlocker(({ currentLocation, nextLocation }) => {
- return showConfirmationDialog && currentLocation.pathname !== nextLocation.pathname;
- });
-
- useEffect(() => {
- if (blocker.state === 'blocked') {
- if (window.confirm(confirmationMessage)) {
- blocker.proceed();
- } else {
- blocker.reset();
- }
- }
- }, [blocker, confirmationMessage]);
-};
diff --git a/frontend/packages/shared/src/components/Expression/useExpressionTexts.ts b/frontend/packages/shared/src/hooks/useExpressionTexts.ts
similarity index 100%
rename from frontend/packages/shared/src/components/Expression/useExpressionTexts.ts
rename to frontend/packages/shared/src/hooks/useExpressionTexts.ts
diff --git a/frontend/packages/shared/src/mocks/apiErrorMocks.ts b/frontend/packages/shared/src/mocks/apiErrorMocks.ts
deleted file mode 100644
index 6825624341e..00000000000
--- a/frontend/packages/shared/src/mocks/apiErrorMocks.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import type { AxiosResponse } from 'axios';
-import { AxiosError } from 'axios';
-
-export const createApiErrorMock = (status?: number): AxiosError => {
- const error = new AxiosError();
- error.response = {
- status,
- } as AxiosResponse;
- return error;
-};
diff --git a/frontend/packages/shared/src/types/ComponentSpecificConfig.ts b/frontend/packages/shared/src/types/ComponentSpecificConfig.ts
index a5f68ead45f..d36b87df446 100644
--- a/frontend/packages/shared/src/types/ComponentSpecificConfig.ts
+++ b/frontend/packages/shared/src/types/ComponentSpecificConfig.ts
@@ -376,7 +376,7 @@ export type ComponentSpecificConfig = {
rowsAfter?: GridRow[];
labelSettings?: LabelSettings;
};
- [ComponentType.SubForm]: FormComponentProps;
+ [ComponentType.Subform]: FormComponentProps;
[ComponentType.Summary]: SummarizableComponentProps & {
componentRef: string;
largeGroup?: boolean;
diff --git a/frontend/packages/shared/src/types/ComponentType.ts b/frontend/packages/shared/src/types/ComponentType.ts
index 959c9b30513..7fa28585a34 100644
--- a/frontend/packages/shared/src/types/ComponentType.ts
+++ b/frontend/packages/shared/src/types/ComponentType.ts
@@ -37,7 +37,7 @@ export enum ComponentType {
PrintButton = 'PrintButton',
RadioButtons = 'RadioButtons',
RepeatingGroup = 'RepeatingGroup',
- SubForm = 'SubForm',
+ Subform = 'Subform',
Summary = 'Summary',
Summary2 = 'Summary2',
TextArea = 'TextArea',
diff --git a/frontend/packages/shared/src/types/api/LayoutSetPayload.ts b/frontend/packages/shared/src/types/api/LayoutSetPayload.ts
index 70dad189389..5fb9a7d6164 100644
--- a/frontend/packages/shared/src/types/api/LayoutSetPayload.ts
+++ b/frontend/packages/shared/src/types/api/LayoutSetPayload.ts
@@ -5,7 +5,7 @@ export interface LayoutSetPayload {
layoutSetConfig: LayoutSetConfig;
}
-type SubFormConfig = {
+type SubformConfig = {
type: 'subform';
};
@@ -16,4 +16,4 @@ type RegularLayoutSetConfig = {
export type LayoutSetConfig = {
id: string;
dataType?: string;
-} & (SubFormConfig | RegularLayoutSetConfig);
+} & (SubformConfig | RegularLayoutSetConfig);
diff --git a/frontend/packages/shared/src/utils/arrayUtils.test.ts b/frontend/packages/shared/src/utils/arrayUtils.test.ts
deleted file mode 100644
index f67891a41c4..00000000000
--- a/frontend/packages/shared/src/utils/arrayUtils.test.ts
+++ /dev/null
@@ -1,139 +0,0 @@
-import {
- areItemsUnique,
- generateUniqueStringWithNumber,
- insertArrayElementAtPos,
- mapByKey,
- moveArrayItem,
- removeEmptyStrings,
- replaceByPredicate,
- replaceItemsByValue,
- swapArrayElements,
-} from './arrayUtils';
-
-describe('arrayUtils', () => {
- describe('areItemsUnique', () => {
- it('Returns true if all items are unique', () => {
- expect(areItemsUnique([1, 2, 3])).toBe(true);
- expect(areItemsUnique(['a', 'b', 'c'])).toBe(true);
- expect(areItemsUnique(['abc', 'bcd', 'cde'])).toBe(true);
- expect(areItemsUnique([true, false])).toBe(true);
- expect(areItemsUnique([1, 'b', true])).toBe(true);
- expect(areItemsUnique([0, '', false, null, undefined])).toBe(true);
- });
-
- it('Returns true if array is empty', () => {
- expect(areItemsUnique([])).toBe(true);
- });
-
- it('Returns false if there is at least one duplicated item', () => {
- expect(areItemsUnique([1, 2, 1])).toBe(false);
- expect(areItemsUnique(['a', 'a', 'c'])).toBe(false);
- expect(areItemsUnique(['abc', 'bcd', 'bcd'])).toBe(false);
- expect(areItemsUnique([true, false, true])).toBe(false);
- expect(areItemsUnique([1, 'b', false, 1])).toBe(false);
- expect(areItemsUnique([null, null])).toBe(false);
- expect(areItemsUnique([undefined, undefined])).toBe(false);
- });
- });
-
- describe('insertArrayElementAtPos', () => {
- const arr = ['a', 'b', 'c'];
-
- it('Inserts element at given position', () => {
- expect(insertArrayElementAtPos(arr, 'M', 0)).toEqual(['M', 'a', 'b', 'c']);
- expect(insertArrayElementAtPos(arr, 'M', 1)).toEqual(['a', 'M', 'b', 'c']);
- expect(insertArrayElementAtPos(arr, 'M', 3)).toEqual(['a', 'b', 'c', 'M']);
- });
-
- it('Inserts element at the end if the position number is too large', () => {
- expect(insertArrayElementAtPos(arr, 'M', 9)).toEqual(['a', 'b', 'c', 'M']);
- });
-
- it('Inserts element at the end if the position number is negative', () => {
- expect(insertArrayElementAtPos(arr, 'M', -1)).toEqual(['a', 'b', 'c', 'M']);
- });
- });
-
- describe('swapArrayElements', () => {
- it('Swaps two elements in an array', () => {
- const arr: string[] = ['a', 'b', 'c', 'd', 'e', 'f'];
- expect(swapArrayElements(arr, 'a', 'b')).toEqual(['b', 'a', 'c', 'd', 'e', 'f']);
- });
- });
-
- describe('mapByKey', () => {
- it('Returns an array of values mapped by the given key', () => {
- const array = [
- { a: 1, b: 2 },
- { a: 2, b: 'c' },
- { a: 3, b: true, c: 'abc' },
- ];
- expect(mapByKey(array, 'a')).toEqual([1, 2, 3]);
- });
- });
-
- describe('rplaceItemsByValue', () => {
- it('Replaces all items matching the given value with the given replacement', () => {
- const array = ['a', 'b', 'c'];
- expect(replaceItemsByValue(array, 'b', 'd')).toEqual(['a', 'd', 'c']);
- });
- });
-
- describe('replaceByPredicate', () => {
- it('Replaces the first item matching the predicate with the given item', () => {
- const array = ['test1', 'test2', 'test3'];
- const predicate = (item: string) => item === 'test2';
- const replaceWith = 'test4';
- expect(replaceByPredicate(array, predicate, replaceWith)).toEqual([
- 'test1',
- 'test4',
- 'test3',
- ]);
- });
- });
-
- describe('moveArrayItem', () => {
- it('Moves the item at the given index to the given position when the new position is BEFORE', () => {
- const array = ['a', 'b', 'c', 'd', 'e', 'f'];
- expect(moveArrayItem(array, 4, 1)).toEqual(['a', 'e', 'b', 'c', 'd', 'f']);
- });
-
- it('Moves the item at the given index to the given position when the new position is after', () => {
- const array = ['a', 'b', 'c', 'd', 'e', 'f'];
- expect(moveArrayItem(array, 1, 4)).toEqual(['a', 'c', 'd', 'e', 'b', 'f']);
- });
-
- it('Keeps the array unchanged if the two indices are the same', () => {
- const array = ['a', 'b', 'c', 'd', 'e', 'f'];
- expect(moveArrayItem(array, 1, 1)).toEqual(array);
- });
- });
-
- describe('generateUniqueStringWithNumber', () => {
- it('Returns prefix + 0 when the array is empty', () => {
- expect(generateUniqueStringWithNumber([], 'prefix')).toBe('prefix0');
- });
-
- it('Returns prefix + 0 when the array does not contain this value already', () => {
- const array = ['something', 'something else'];
- expect(generateUniqueStringWithNumber(array, 'prefix')).toBe('prefix0');
- });
-
- it('Returns prefix + number based on the existing values', () => {
- const array = ['prefix0', 'prefix1', 'prefix2'];
- expect(generateUniqueStringWithNumber(array, 'prefix')).toBe('prefix3');
- });
-
- it('Returns number only when the prefix is empty', () => {
- const array = ['0', '1', '2'];
- expect(generateUniqueStringWithNumber(array)).toBe('3');
- });
- });
-
- describe('removeEmptyStrings', () => {
- it('Removes empty strings from an array', () => {
- const array = ['0', '1', '', '2', ''];
- expect(removeEmptyStrings(array)).toEqual(['0', '1', '2']);
- });
- });
-});
diff --git a/frontend/packages/shared/src/utils/arrayUtils.ts b/frontend/packages/shared/src/utils/arrayUtils.ts
deleted file mode 100644
index 92f490c4650..00000000000
--- a/frontend/packages/shared/src/utils/arrayUtils.ts
+++ /dev/null
@@ -1,113 +0,0 @@
-import { ArrayUtils } from '@studio/pure-functions';
-
-/**
- * Replaces the last item in an array.
- * @param array The array of interest.
- * @param replaceWith The item to replace the last item with.
- * @returns The array with the last item replaced.
- */
-export const replaceLastItem = (array: T[], replaceWith: T): T[] => {
- array[array.length - 1] = replaceWith;
- return array;
-};
-
-/**
- * Checks if all items in the given array are unique.
- * @param array The array of interest.
- * @returns True if all items in the array are unique and false otherwise.
- */
-export const areItemsUnique = (array: T[]): boolean => array.length === new Set(array).size;
-
-/**
- * Swaps the first values with the given values.
- * @param array Array to swap items in.
- * @param itemA First value to swap.
- * @param itemB Second value to swap.
- * @returns Array with swapped items.
- */
-export const swapArrayElements = (array: T[], itemA: T, itemB: T): T[] => {
- const out = [...array];
- const indexA = array.indexOf(itemA);
- const indexB = array.indexOf(itemB);
- out[indexA] = itemB;
- out[indexB] = itemA;
- return out;
-};
-
-/**
- * Inserts an item at a given position in an array.
- * @param array Array to remove item from.
- * @param item Item to remove.
- * @param targetPos Position to remove item from.
- * @returns Array with item inserted at given position.
- */
-export const insertArrayElementAtPos = (array: T[], item: T, targetPos: number): T[] => {
- const out = [...array];
- if (targetPos >= array.length || targetPos < 0) out.push(item);
- else out.splice(targetPos, 0, item);
- return out;
-};
-
-/**
- * Maps an array of objects by a given key.
- * @param array The array of objects.
- * @param key The key to map by.
- * @returns An array of values mapped by the given key.
- */
-export const mapByKey = (array: T[], key: K): T[K][] =>
- array.map((item) => item[key]);
-
-/**
- * Returns an array of which the items matching the given value are replaced with the given item.
- * @param array The array of interest.
- * @param value The value to match items by.
- * @param replaceWith The item to replace the matching items with.
- */
-export const replaceItemsByValue = (array: T[], value: T, replaceWith: T): T[] =>
- replaceByPredicate(array, (item) => item === value, replaceWith);
-
-/**
- * Returns an array of which the items matching the given predicate are replaced with the given item.
- * @param array The array of interest.
- * @param predicate The predicate to match items by.
- * @param replaceWith The item to replace the matching items with.
- * @returns A shallow copy of the array with the matching items replaced.
- */
-export const replaceByPredicate = (
- array: T[],
- predicate: (item: T) => boolean,
- replaceWith: T,
-): T[] => {
- const out = [...array];
- const index = array.findIndex(predicate);
- if (index > -1) out[index] = replaceWith;
- return out;
-};
-
-/**
- * Returns an array where the item at the given index is moved to the given index.
- * @param array The array of interest.
- * @param from The index of the item to move.
- * @param to The index to move the item to.
- */
-export const moveArrayItem = (array: T[], from: number, to: number): T[] => {
- const out = [...array];
- const item = out.splice(from, 1)[0];
- out.splice(to, 0, item);
- return out;
-};
-
-/** Returns a string that is not already present in the given array by appending a number to the given prefix. */
-export const generateUniqueStringWithNumber = (array: string[], prefix: string = ''): string => {
- let i = 0;
- let uniqueString = prefix + i;
- while (array.includes(uniqueString)) {
- i++;
- uniqueString = prefix + i;
- }
- return uniqueString;
-};
-
-/** Removes empty strings from a string array */
-export const removeEmptyStrings = (array: string[]): string[] =>
- ArrayUtils.removeItemByValue(array, '');
diff --git a/frontend/packages/shared/src/utils/featureToggleUtils.ts b/frontend/packages/shared/src/utils/featureToggleUtils.ts
index 8f3dc9010f2..a610516af17 100644
--- a/frontend/packages/shared/src/utils/featureToggleUtils.ts
+++ b/frontend/packages/shared/src/utils/featureToggleUtils.ts
@@ -10,6 +10,7 @@ export type SupportedFeatureFlags =
| 'resourceMigration'
| 'multipleDataModelsPerTask'
| 'exportForm'
+ | 'addComponentModal'
| 'subform'
| 'summary2';
diff --git a/frontend/packages/shared/src/utils/stringUtils.test.ts b/frontend/packages/shared/src/utils/stringUtils.test.ts
deleted file mode 100644
index 65a3603972d..00000000000
--- a/frontend/packages/shared/src/utils/stringUtils.test.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-import {
- replaceEnd,
- replaceStart,
- substringAfterLast,
- substringBeforeLast,
-} from 'app-shared/utils/stringUtils';
-
-describe('stringUtils', () => {
- describe('substringAfterLast', () => {
- it('Returns substring after last occurrence of separator', () => {
- expect(substringAfterLast('abc/def/ghi', '/')).toBe('ghi');
- });
-
- it('Returns whole string if separator is not found', () => {
- expect(substringAfterLast('abc', '/')).toBe('abc');
- });
-
- it('Returns empty string if there are no characters after the last separator', () => {
- expect(substringAfterLast('abc/def/', '/')).toBe('');
- });
- });
-
- describe('substringBeforeLast', () => {
- it('Returns substring before last occurrence of separator', () => {
- expect(substringBeforeLast('abc/def/ghi', '/')).toBe('abc/def');
- });
-
- it('Returns whole string if separator is not found', () => {
- expect(substringBeforeLast('abc', '/')).toBe('abc');
- });
-
- it('Returns whole string if there are no characters before the last separator', () => {
- expect(substringBeforeLast('/abc', '/')).toBe('');
- });
- });
-
- describe('replaceStart', () => {
- it('Replaces the given substring with the given replacement at the start of the string', () => {
- expect(replaceStart('abc/def/ghi', 'abc', 'xyz')).toBe('xyz/def/ghi');
- });
-
- it('Does not replace the given substring other places than at the start', () => {
- expect(replaceStart('abcdefghi', 'ghi', 'xyz')).toBe('abcdefghi');
- expect(replaceStart('abcdefghi', 'def', 'xyz')).toBe('abcdefghi');
- expect(replaceStart('defabcdefghi', 'def', 'xyz')).toBe('xyzabcdefghi');
- });
- });
-
- describe('replaceEnd', () => {
- it('Replaces the given substring with the given replacement at the end of the string', () => {
- expect(replaceEnd('abc/def/ghi', 'ghi', 'xyz')).toBe('abc/def/xyz');
- });
-
- it('Does not replace the given substring other places than at the end', () => {
- expect(replaceEnd('abcdefghi', 'abc', 'xyz')).toBe('abcdefghi');
- expect(replaceEnd('abcdefghi', 'def', 'xyz')).toBe('abcdefghi');
- expect(replaceEnd('abcdefghidef', 'def', 'xyz')).toBe('abcdefghixyz');
- });
- });
-});
diff --git a/frontend/packages/shared/src/utils/stringUtils.ts b/frontend/packages/shared/src/utils/stringUtils.ts
deleted file mode 100644
index 5cd182b7cb9..00000000000
--- a/frontend/packages/shared/src/utils/stringUtils.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import { ArrayUtils } from '@studio/pure-functions';
-
-/**
- * Returns substring after last occurrence of separator.
- * @param str The string to search in.
- * @param separator The separator to search for.
- * @returns The substring after the last occurrence of the given separator.
- */
-export const substringAfterLast = (str: string, separator: string): string =>
- ArrayUtils.last(str.split(separator)) || '';
-
-/**
- * Returns substring before last occurrence of separator.
- * @param str The string to search in.
- * @param separator The separator to search for.
- * @returns The substring before the last occurrence of the given separator.
- */
-export const substringBeforeLast = (str: string, separator: string): string =>
- str.includes(separator) ? str.substring(0, str.lastIndexOf(separator)) : str;
-
-/**
- * Replaces the given substring with the given replacement at the start of the string.
- * If the substring does not appear at the start of the string, the string is returned unchanged.
- * @param str The string to search in.
- * @param substring The substring to search for.
- * @param replacement The replacement to replace the substring with.
- * @returns The string with the substring replaced at the start.
- */
-export const replaceStart = (str: string, substring: string, replacement: string): string => {
- if (str.startsWith(substring)) {
- return replacement + str.slice(substring.length);
- }
- return str;
-};
-
-/**
- * Replaces the given substring with the given replacement at the end of the string.
- * If the substring does not appear at the end of the string, the string is returned unchanged.
- * @param str The string to search in.
- * @param substring The substring to search for.
- * @param replacement The replacement to replace the substring with.
- * @returns The string with the substring replaced at the end.
- */
-export const replaceEnd = (str: string, substring: string, replacement: string): string =>
- str.replace(new RegExp(substring + '$'), replacement);
diff --git a/frontend/packages/ux-editor-v3/package.json b/frontend/packages/ux-editor-v3/package.json
index aec88e29933..84f6c8627da 100644
--- a/frontend/packages/ux-editor-v3/package.json
+++ b/frontend/packages/ux-editor-v3/package.json
@@ -11,7 +11,6 @@
"react-dnd": "16.0.1",
"react-dnd-html5-backend": "16.0.1",
"react-dom": "18.3.1",
- "react-modal": "3.16.1",
"react-redux": "9.1.2",
"redux": "5.0.1",
"typescript": "5.6.2",
diff --git a/frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutoComplete.tsx b/frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutoComplete.tsx
index 897713794d3..6fc0dd4d643 100644
--- a/frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutoComplete.tsx
+++ b/frontend/packages/ux-editor-v3/src/components/config/editModal/EditAutoComplete.tsx
@@ -2,9 +2,9 @@ import type { ChangeEvent } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import type { IGenericEditComponent } from '../componentConfig';
import { stringToArray, arrayToString } from '../../../utils/stringUtils';
-import { replaceLastItem } from 'app-shared/utils/arrayUtils';
import { FormField } from '../../FormField';
import { StudioButton, StudioPopover, StudioTextfield } from '@studio/components';
+import { ArrayUtils } from '@studio/pure-functions';
const getLastWord = (value: string) => value.split(' ').pop();
const stdAutocompleteOpts = [
@@ -79,7 +79,7 @@ export const EditAutoComplete = ({ component, handleComponentChange }: IGenericE
const buildNewText = (word: string): string => {
const wordParts = stringToArray(autocompleteText, ' ');
- const newWordParts = replaceLastItem(wordParts, word);
+ const newWordParts = ArrayUtils.replaceLastItem(wordParts, word);
return arrayToString(newWordParts);
};
diff --git a/frontend/packages/ux-editor-v3/src/components/config/editModal/EditDataModelBindings.tsx b/frontend/packages/ux-editor-v3/src/components/config/editModal/EditDataModelBindings.tsx
index 94069b70a3e..61d0b11c481 100644
--- a/frontend/packages/ux-editor-v3/src/components/config/editModal/EditDataModelBindings.tsx
+++ b/frontend/packages/ux-editor-v3/src/components/config/editModal/EditDataModelBindings.tsx
@@ -9,7 +9,7 @@ import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmen
import { LinkIcon } from '@studio/icons';
import { StudioButton } from '@studio/components';
import classes from './EditDataModelBindings.module.css';
-import { InputActionWrapper } from 'app-shared/components/InputActionWrapper';
+import { InputActionWrapper } from './InputActionWrapper';
import { useAppContext } from '../../../hooks/useAppContext';
export interface EditDataModelBindingsProps extends IGenericEditComponent {
diff --git a/frontend/packages/shared/src/components/InputActionWrapper/InputActionWrapper.module.css b/frontend/packages/ux-editor-v3/src/components/config/editModal/InputActionWrapper/InputActionWrapper.module.css
similarity index 100%
rename from frontend/packages/shared/src/components/InputActionWrapper/InputActionWrapper.module.css
rename to frontend/packages/ux-editor-v3/src/components/config/editModal/InputActionWrapper/InputActionWrapper.module.css
diff --git a/frontend/packages/shared/src/components/InputActionWrapper/InputActionWrapper.test.tsx b/frontend/packages/ux-editor-v3/src/components/config/editModal/InputActionWrapper/InputActionWrapper.test.tsx
similarity index 100%
rename from frontend/packages/shared/src/components/InputActionWrapper/InputActionWrapper.test.tsx
rename to frontend/packages/ux-editor-v3/src/components/config/editModal/InputActionWrapper/InputActionWrapper.test.tsx
diff --git a/frontend/packages/shared/src/components/InputActionWrapper/InputActionWrapper.tsx b/frontend/packages/ux-editor-v3/src/components/config/editModal/InputActionWrapper/InputActionWrapper.tsx
similarity index 100%
rename from frontend/packages/shared/src/components/InputActionWrapper/InputActionWrapper.tsx
rename to frontend/packages/ux-editor-v3/src/components/config/editModal/InputActionWrapper/InputActionWrapper.tsx
diff --git a/frontend/packages/shared/src/components/InputActionWrapper/index.ts b/frontend/packages/ux-editor-v3/src/components/config/editModal/InputActionWrapper/index.ts
similarity index 100%
rename from frontend/packages/shared/src/components/InputActionWrapper/index.ts
rename to frontend/packages/ux-editor-v3/src/components/config/editModal/InputActionWrapper/index.ts
diff --git a/frontend/packages/ux-editor-v3/src/hooks/useValidateComponent.ts b/frontend/packages/ux-editor-v3/src/hooks/useValidateComponent.ts
index 627551f3c3e..48ee1aa6dc7 100644
--- a/frontend/packages/ux-editor-v3/src/hooks/useValidateComponent.ts
+++ b/frontend/packages/ux-editor-v3/src/hooks/useValidateComponent.ts
@@ -1,4 +1,4 @@
-import { areItemsUnique } from 'app-shared/utils/arrayUtils';
+import { ArrayUtils } from '@studio/pure-functions';
import { ComponentTypeV3 } from 'app-shared/types/ComponentTypeV3';
import type {
FormCheckboxesComponent,
@@ -36,7 +36,7 @@ const validateOptionGroup = (
isValid: false,
error: ErrorCode.NoOptions,
};
- } else if (!areItemsUnique(component.options.map((option) => option.value))) {
+ } else if (!ArrayUtils.areItemsUnique(component.options.map((option) => option.value))) {
return {
isValid: false,
error: ErrorCode.DuplicateValues,
diff --git a/frontend/packages/ux-editor-v3/src/utils/formLayoutUtils.ts b/frontend/packages/ux-editor-v3/src/utils/formLayoutUtils.ts
index 9515d411613..cbd8ec08f11 100644
--- a/frontend/packages/ux-editor-v3/src/utils/formLayoutUtils.ts
+++ b/frontend/packages/ux-editor-v3/src/utils/formLayoutUtils.ts
@@ -7,7 +7,6 @@ import type {
IToolbarElement,
} from '../types/global';
import { BASE_CONTAINER_ID, MAX_NESTED_GROUP_LEVEL } from 'app-shared/constants';
-import { insertArrayElementAtPos } from 'app-shared/utils/arrayUtils';
import { ArrayUtils, ObjectUtils } from '@studio/pure-functions';
import { ComponentTypeV3 } from 'app-shared/types/ComponentTypeV3';
import type { FormComponent } from '../types/FormComponent';
@@ -301,7 +300,7 @@ export const moveLayoutItem = (
newLayout.order[oldContainerId],
id,
);
- newLayout.order[newContainerId] = insertArrayElementAtPos(
+ newLayout.order[newContainerId] = ArrayUtils.insertArrayElementAtPos(
newLayout.order[newContainerId],
id,
newPosition,
diff --git a/frontend/packages/ux-editor/package.json b/frontend/packages/ux-editor/package.json
index 0456d883558..6ad917fc51e 100644
--- a/frontend/packages/ux-editor/package.json
+++ b/frontend/packages/ux-editor/package.json
@@ -10,7 +10,6 @@
"react-dnd": "16.0.1",
"react-dnd-html5-backend": "16.0.1",
"react-dom": "18.3.1",
- "react-modal": "3.16.1",
"typescript": "5.6.2",
"uuid": "10.0.0"
},
diff --git a/frontend/packages/ux-editor/src/classes/Subformutils.test.ts b/frontend/packages/ux-editor/src/classes/SubformUtils.test.ts
similarity index 62%
rename from frontend/packages/ux-editor/src/classes/Subformutils.test.ts
rename to frontend/packages/ux-editor/src/classes/SubformUtils.test.ts
index a9e33d4dbca..83e83b8580b 100644
--- a/frontend/packages/ux-editor/src/classes/Subformutils.test.ts
+++ b/frontend/packages/ux-editor/src/classes/SubformUtils.test.ts
@@ -1,26 +1,26 @@
-import { SubFormUtilsImpl } from './SubFormUtils';
+import { SubformUtilsImpl } from './SubformUtils';
import type { LayoutSetConfig } from 'app-shared/types/api/LayoutSetsResponse';
-describe('SubFormUtilsImpl', () => {
+describe('SubformUtilsImpl', () => {
describe('hasSubform', () => {
it('should return false for hasSubforms when there are no subform layout sets', () => {
const layoutSets: Array = [{ id: '1' }];
- const subFormUtils = new SubFormUtilsImpl(layoutSets);
- expect(subFormUtils.hasSubforms).toBe(false);
+ const subformUtils = new SubformUtilsImpl(layoutSets);
+ expect(subformUtils.hasSubforms).toBe(false);
});
it('should return true for hasSubforms when there are subform layout sets', () => {
const layoutSets: Array = [{ id: '1', type: 'subform' }];
- const subFormUtils = new SubFormUtilsImpl(layoutSets);
- expect(subFormUtils.hasSubforms).toBe(true);
+ const subformUtils = new SubformUtilsImpl(layoutSets);
+ expect(subformUtils.hasSubforms).toBe(true);
});
});
describe('subformLayoutSetsIds', () => {
it('should return an empty array for subformLayoutSetsIds when there are no subform layout sets', () => {
const layoutSets: Array = [{ id: '1' }];
- const subFormUtils = new SubFormUtilsImpl(layoutSets);
- expect(subFormUtils.subformLayoutSetsIds).toEqual([]);
+ const subformUtils = new SubformUtilsImpl(layoutSets);
+ expect(subformUtils.subformLayoutSetsIds).toEqual([]);
});
it('should return the correct subform layout set IDs', () => {
@@ -29,8 +29,8 @@ describe('SubFormUtilsImpl', () => {
{ id: '2' },
{ id: '3', type: 'subform' },
];
- const subFormUtils = new SubFormUtilsImpl(layoutSets);
- expect(subFormUtils.subformLayoutSetsIds).toEqual(['1', '3']);
+ const subformUtils = new SubformUtilsImpl(layoutSets);
+ expect(subformUtils.subformLayoutSetsIds).toEqual(['1', '3']);
});
});
});
diff --git a/frontend/packages/ux-editor/src/classes/SubFormUtils.ts b/frontend/packages/ux-editor/src/classes/SubformUtils.ts
similarity index 63%
rename from frontend/packages/ux-editor/src/classes/SubFormUtils.ts
rename to frontend/packages/ux-editor/src/classes/SubformUtils.ts
index 1cda8bd7507..ebd270e2825 100644
--- a/frontend/packages/ux-editor/src/classes/SubFormUtils.ts
+++ b/frontend/packages/ux-editor/src/classes/SubformUtils.ts
@@ -1,15 +1,15 @@
import type { LayoutSetConfig } from 'app-shared/types/api/LayoutSetsResponse';
-type SubFormLayoutSet = LayoutSetConfig & {
+type SubformLayoutSet = LayoutSetConfig & {
type: 'subform';
};
-interface SubFormUtils {
+interface SubformUtils {
hasSubforms: boolean;
subformLayoutSetsIds: Array;
}
-export class SubFormUtilsImpl implements SubFormUtils {
+export class SubformUtilsImpl implements SubformUtils {
constructor(private readonly layoutSets: Array) {}
public get hasSubforms(): boolean {
@@ -17,12 +17,12 @@ export class SubFormUtilsImpl implements SubFormUtils {
}
public get subformLayoutSetsIds(): Array {
- return this.getSubformLayoutSets.map((set: SubFormLayoutSet) => set.id);
+ return this.getSubformLayoutSets.map((set: SubformLayoutSet) => set.id);
}
- private get getSubformLayoutSets(): Array {
+ private get getSubformLayoutSets(): Array {
return (this.layoutSets || []).filter(
(set) => set.type === 'subform',
- ) as Array;
+ ) as Array;
}
}
diff --git a/frontend/packages/ux-editor/src/components/Elements/LayoutSetsContainer.tsx b/frontend/packages/ux-editor/src/components/Elements/LayoutSetsContainer.tsx
index ded81d7fc39..d9faf1b9839 100644
--- a/frontend/packages/ux-editor/src/components/Elements/LayoutSetsContainer.tsx
+++ b/frontend/packages/ux-editor/src/components/Elements/LayoutSetsContainer.tsx
@@ -5,7 +5,7 @@ import { useText, useAppContext } from '../../hooks';
import classes from './LayoutSetsContainer.module.css';
import { ExportForm } from './ExportForm';
import { shouldDisplayFeature } from 'app-shared/utils/featureToggleUtils';
-import { SubFormWrapper } from './SubForm/SubFormWrapper';
+import { SubformWrapper } from './Subform/SubformWrapper';
import { StudioCombobox } from '@studio/components';
export function LayoutSetsContainer() {
@@ -59,9 +59,9 @@ export function LayoutSetsContainer() {
{shouldDisplayFeature('exportForm') && }
{shouldDisplayFeature('subform') && (
-
)}
diff --git a/frontend/packages/ux-editor/src/components/Elements/SubForm/SubFormWrapper.tsx b/frontend/packages/ux-editor/src/components/Elements/SubForm/SubFormWrapper.tsx
deleted file mode 100644
index a94beb2b1a3..00000000000
--- a/frontend/packages/ux-editor/src/components/Elements/SubForm/SubFormWrapper.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import type { LayoutSets } from 'app-shared/types/api/LayoutSetsResponse';
-import React from 'react';
-import { CreateSubFormWrapper } from './CreateSubFormWrapper';
-import { DeleteSubFormWrapper } from './DeleteSubFormWrapper';
-
-type SubFormWrapperProps = {
- layoutSets: LayoutSets;
- onSubFormCreated: (layoutSetName: string) => void;
- selectedLayoutSet: string;
-};
-
-export const SubFormWrapper = ({
- layoutSets,
- onSubFormCreated,
- selectedLayoutSet,
-}: SubFormWrapperProps): React.ReactElement => {
- return (
-
-
-
-
- );
-};
diff --git a/frontend/packages/ux-editor/src/components/Elements/SubForm/CreateSubFormWrapper.module.css b/frontend/packages/ux-editor/src/components/Elements/Subform/CreateSubformWrapper.module.css
similarity index 100%
rename from frontend/packages/ux-editor/src/components/Elements/SubForm/CreateSubFormWrapper.module.css
rename to frontend/packages/ux-editor/src/components/Elements/Subform/CreateSubformWrapper.module.css
diff --git a/frontend/packages/ux-editor/src/components/Elements/SubForm/CreateSubFormWrapper.test.tsx b/frontend/packages/ux-editor/src/components/Elements/Subform/CreateSubformWrapper.test.tsx
similarity index 60%
rename from frontend/packages/ux-editor/src/components/Elements/SubForm/CreateSubFormWrapper.test.tsx
rename to frontend/packages/ux-editor/src/components/Elements/Subform/CreateSubformWrapper.test.tsx
index 6d9e4f93d1c..79568f610c4 100644
--- a/frontend/packages/ux-editor/src/components/Elements/SubForm/CreateSubFormWrapper.test.tsx
+++ b/frontend/packages/ux-editor/src/components/Elements/Subform/CreateSubformWrapper.test.tsx
@@ -2,55 +2,55 @@ import React from 'react';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { renderWithProviders } from '../../../testing/mocks';
-import { CreateSubFormWrapper } from './CreateSubFormWrapper';
+import { CreateSubformWrapper } from './CreateSubformWrapper';
import { textMock } from '@studio/testing/mocks/i18nMock';
import { app, org } from '@studio/testing/testids';
import { layoutSetsMock, layoutSet1NameMock } from '@altinn/ux-editor/testing/layoutSetsMock';
import type { ServicesContextProps } from 'app-shared/contexts/ServicesContext';
-const subFormName = 'underskjema';
+const subformName = 'underskjema';
-describe('CreateSubFormWrapper', () => {
+describe('CreateSubformWrapper', () => {
it('should open dialog when clicking "create subform" button', async () => {
const user = userEvent.setup();
- renderCreateSubFormWrapper();
+ renderCreateSubformWrapper();
- const createSubFormButton = screen.getByRole('button', {
+ const createSubformButton = screen.getByRole('button', {
name: textMock('ux_editor.create.subform'),
});
- await user.click(createSubFormButton);
+ await user.click(createSubformButton);
expect(await screen.findByRole('dialog')).toBeInTheDocument();
});
- it('should call onSubFormCreated when subform is created', async () => {
+ it('should call onSubformCreated when subform is created', async () => {
const user = userEvent.setup();
- const onSubFormCreated = jest.fn();
- renderCreateSubFormWrapper(onSubFormCreated);
+ const onSubformCreated = jest.fn();
+ renderCreateSubformWrapper(onSubformCreated);
- const createSubFormButton = screen.getByRole('button', {
+ const createSubformButton = screen.getByRole('button', {
name: textMock('ux_editor.create.subform'),
});
- await user.click(createSubFormButton);
+ await user.click(createSubformButton);
const input = screen.getByRole('textbox');
- await user.type(input, subFormName);
+ await user.type(input, subformName);
const confirmButton = screen.getByRole('button', {
name: textMock('ux_editor.create.subform.confirm_button'),
});
await user.click(confirmButton);
- expect(onSubFormCreated).toHaveBeenCalledWith(subFormName);
+ expect(onSubformCreated).toHaveBeenCalledWith(subformName);
});
it('should disable confirm button when name already exist', async () => {
const user = userEvent.setup();
- renderCreateSubFormWrapper();
+ renderCreateSubformWrapper();
- const createSubFormButton = screen.getByRole('button', {
+ const createSubformButton = screen.getByRole('button', {
name: textMock('ux_editor.create.subform'),
});
- await user.click(createSubFormButton);
+ await user.click(createSubformButton);
const input = screen.getByRole('textbox');
await user.type(input, layoutSet1NameMock);
@@ -62,36 +62,36 @@ describe('CreateSubFormWrapper', () => {
});
it('should add subform when name is valid', async () => {
- const onSubFormCreatedMock = jest.fn();
+ const onSubformCreatedMock = jest.fn();
const user = userEvent.setup();
const addLayoutSet = jest.fn();
- renderCreateSubFormWrapper(onSubFormCreatedMock, { addLayoutSet });
+ renderCreateSubformWrapper(onSubformCreatedMock, { addLayoutSet });
- const createSubFormButton = screen.getByRole('button', {
+ const createSubformButton = screen.getByRole('button', {
name: textMock('ux_editor.create.subform'),
});
- await user.click(createSubFormButton);
+ await user.click(createSubformButton);
const input = screen.getByRole('textbox');
- await user.type(input, subFormName);
+ await user.type(input, subformName);
const confirmButton = screen.getByRole('button', {
name: textMock('ux_editor.create.subform.confirm_button'),
});
await user.click(confirmButton);
- expect(addLayoutSet).toHaveBeenCalledWith(org, app, subFormName, {
- layoutSetConfig: { id: subFormName, type: 'subform' },
+ expect(addLayoutSet).toHaveBeenCalledWith(org, app, subformName, {
+ layoutSetConfig: { id: subformName, type: 'subform' },
});
});
});
-const renderCreateSubFormWrapper = (
- onSubFormCreated?: jest.Mock,
+const renderCreateSubformWrapper = (
+ onSubformCreated?: jest.Mock,
queries: Partial = {},
) => {
return renderWithProviders(
- ,
+ ,
{ queries },
);
};
diff --git a/frontend/packages/ux-editor/src/components/Elements/SubForm/CreateSubFormWrapper.tsx b/frontend/packages/ux-editor/src/components/Elements/Subform/CreateSubformWrapper.tsx
similarity index 74%
rename from frontend/packages/ux-editor/src/components/Elements/SubForm/CreateSubFormWrapper.tsx
rename to frontend/packages/ux-editor/src/components/Elements/Subform/CreateSubformWrapper.tsx
index 75f7d2b12a9..0ce957979c2 100644
--- a/frontend/packages/ux-editor/src/components/Elements/SubForm/CreateSubFormWrapper.tsx
+++ b/frontend/packages/ux-editor/src/components/Elements/Subform/CreateSubformWrapper.tsx
@@ -6,19 +6,19 @@ import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmen
import { useTranslation } from 'react-i18next';
import { useValidateLayoutSetName } from 'app-shared/hooks/useValidateLayoutSetName';
import type { LayoutSets } from 'app-shared/types/api/LayoutSetsResponse';
-import classes from './CreateSubFormWrapper.module.css';
+import classes from './CreateSubformWrapper.module.css';
-type CreateSubFormWrapperProps = {
+type CreateSubformWrapperProps = {
layoutSets: LayoutSets;
- onSubFormCreated: (layoutSetName: string) => void;
+ onSubformCreated: (layoutSetName: string) => void;
};
-export const CreateSubFormWrapper = ({
+export const CreateSubformWrapper = ({
layoutSets,
- onSubFormCreated,
-}: CreateSubFormWrapperProps) => {
+ onSubformCreated,
+}: CreateSubformWrapperProps) => {
const [createNewOpen, setCreateNewOpen] = useState(false);
- const [newSubFormName, setNewSubFormName] = useState('');
+ const [newSubformName, setNewSubformName] = useState('');
const [nameError, setNameError] = useState('');
const { t } = useTranslation();
const { validateLayoutSetName } = useValidateLayoutSetName();
@@ -29,19 +29,19 @@ export const CreateSubFormWrapper = ({
setCreateNewOpen(false);
addLayoutSet({
- layoutSetIdToUpdate: newSubFormName,
+ layoutSetIdToUpdate: newSubformName,
layoutSetConfig: {
- id: newSubFormName,
+ id: newSubformName,
type: 'subform',
},
});
- onSubFormCreated(newSubFormName);
+ onSubformCreated(newSubformName);
};
- const onNameChange = (subFormName: string) => {
- const subFormNameValidation = validateLayoutSetName(subFormName, layoutSets);
- setNameError(subFormNameValidation);
- setNewSubFormName(subFormName);
+ const onNameChange = (subformName: string) => {
+ const subformNameValidation = validateLayoutSetName(subformName, layoutSets);
+ setNameError(subformNameValidation);
+ setNewSubformName(subformName);
};
return (
@@ -59,7 +59,7 @@ export const CreateSubFormWrapper = ({
onNameChange(e.target.value)}
error={nameError}
/>
@@ -67,7 +67,7 @@ export const CreateSubFormWrapper = ({
className={classes.confirmCreateButton}
variant='secondary'
onClick={onCreateConfirmClick}
- disabled={!newSubFormName || !!nameError}
+ disabled={!newSubformName || !!nameError}
>
{t('ux_editor.create.subform.confirm_button')}
diff --git a/frontend/packages/ux-editor/src/components/Elements/SubForm/DeleteSubFormWrapper.test.tsx b/frontend/packages/ux-editor/src/components/Elements/Subform/DeleteSubformWrapper.test.tsx
similarity index 65%
rename from frontend/packages/ux-editor/src/components/Elements/SubForm/DeleteSubFormWrapper.test.tsx
rename to frontend/packages/ux-editor/src/components/Elements/Subform/DeleteSubformWrapper.test.tsx
index 7a753ea8fe7..3fc14c032af 100644
--- a/frontend/packages/ux-editor/src/components/Elements/SubForm/DeleteSubFormWrapper.test.tsx
+++ b/frontend/packages/ux-editor/src/components/Elements/Subform/DeleteSubformWrapper.test.tsx
@@ -1,46 +1,46 @@
import React from 'react';
-import { DeleteSubFormWrapper } from './DeleteSubFormWrapper';
+import { DeleteSubformWrapper } from './DeleteSubformWrapper';
import userEvent from '@testing-library/user-event';
import { screen } from '@testing-library/react';
import type { ServicesContextProps } from 'app-shared/contexts/ServicesContext';
import {
layoutSetsMock,
layoutSet1NameMock,
- layoutSet3SubFormNameMock,
+ layoutSet3SubformNameMock,
} from '../../../testing/layoutSetsMock';
import { renderWithProviders } from '../../../testing/mocks';
import { textMock } from '@studio/testing/mocks/i18nMock';
import { app, org } from '@studio/testing/testids';
-describe('DeleteSubFormWrapper', () => {
+describe('DeleteSubformWrapper', () => {
it('should disable delete button when selected layoutset is not a subform', () => {
- renderDeleteSubFormWrapper(layoutSet1NameMock);
+ renderDeleteSubformWrapper(layoutSet1NameMock);
- const deleteSubFormButton = screen.getByRole('button', {
+ const deleteSubformButton = screen.getByRole('button', {
name: textMock('ux_editor.delete.subform'),
});
- expect(deleteSubFormButton).toBeDisabled();
+ expect(deleteSubformButton).toBeDisabled();
});
it('should enable delete button when selected layoutset is a subform', () => {
- renderDeleteSubFormWrapper(layoutSet3SubFormNameMock);
+ renderDeleteSubformWrapper(layoutSet3SubformNameMock);
- const deleteSubFormButton = screen.getByRole('button', {
+ const deleteSubformButton = screen.getByRole('button', {
name: textMock('ux_editor.delete.subform'),
});
- expect(deleteSubFormButton).toBeEnabled();
+ expect(deleteSubformButton).toBeEnabled();
});
it('should not call deleteLayoutSet when delete button is clicked but not confirmed', async () => {
jest.spyOn(window, 'confirm').mockImplementation(() => false);
const deleteLayoutSet = jest.fn();
const user = userEvent.setup();
- renderDeleteSubFormWrapper(layoutSet3SubFormNameMock, { deleteLayoutSet });
+ renderDeleteSubformWrapper(layoutSet3SubformNameMock, { deleteLayoutSet });
- const deleteSubFormButton = screen.getByRole('button', {
+ const deleteSubformButton = screen.getByRole('button', {
name: textMock('ux_editor.delete.subform'),
});
- await user.click(deleteSubFormButton);
+ await user.click(deleteSubformButton);
expect(deleteLayoutSet).not.toHaveBeenCalled();
});
@@ -49,24 +49,24 @@ describe('DeleteSubFormWrapper', () => {
jest.spyOn(window, 'confirm').mockImplementation(() => true);
const deleteLayoutSet = jest.fn();
const user = userEvent.setup();
- renderDeleteSubFormWrapper(layoutSet3SubFormNameMock, { deleteLayoutSet });
+ renderDeleteSubformWrapper(layoutSet3SubformNameMock, { deleteLayoutSet });
- const deleteSubFormButton = screen.getByRole('button', {
+ const deleteSubformButton = screen.getByRole('button', {
name: textMock('ux_editor.delete.subform'),
});
- await user.click(deleteSubFormButton);
+ await user.click(deleteSubformButton);
expect(deleteLayoutSet).toHaveBeenCalled();
- expect(deleteLayoutSet).toHaveBeenCalledWith(org, app, layoutSet3SubFormNameMock);
+ expect(deleteLayoutSet).toHaveBeenCalledWith(org, app, layoutSet3SubformNameMock);
});
});
-const renderDeleteSubFormWrapper = (
+const renderDeleteSubformWrapper = (
selectedLayoutSet: string,
queries: Partial = {},
) => {
return renderWithProviders(
- ,
+ ,
{ queries },
);
};
diff --git a/frontend/packages/ux-editor/src/components/Elements/SubForm/DeleteSubFormWrapper.tsx b/frontend/packages/ux-editor/src/components/Elements/Subform/DeleteSubformWrapper.tsx
similarity index 78%
rename from frontend/packages/ux-editor/src/components/Elements/SubForm/DeleteSubFormWrapper.tsx
rename to frontend/packages/ux-editor/src/components/Elements/Subform/DeleteSubformWrapper.tsx
index de1c315903a..3f771ed3c88 100644
--- a/frontend/packages/ux-editor/src/components/Elements/SubForm/DeleteSubFormWrapper.tsx
+++ b/frontend/packages/ux-editor/src/components/Elements/Subform/DeleteSubformWrapper.tsx
@@ -4,32 +4,32 @@ import { useDeleteLayoutSetMutation } from 'app-development/hooks/mutations/useD
import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams';
import type { LayoutSets } from 'app-shared/types/api/LayoutSetsResponse';
import { useTranslation } from 'react-i18next';
-import { SubFormUtils } from './SubFormUtils';
+import { SubformUtils } from './SubformUtils';
-type DeleteSubFormWrapperProps = {
+type DeleteSubformWrapperProps = {
layoutSets: LayoutSets;
selectedLayoutSet: string;
};
-export const DeleteSubFormWrapper = ({
+export const DeleteSubformWrapper = ({
layoutSets,
selectedLayoutSet,
-}: DeleteSubFormWrapperProps): React.ReactElement => {
+}: DeleteSubformWrapperProps): React.ReactElement => {
const { org, app } = useStudioEnvironmentParams();
const { mutate: deleteLayoutSet } = useDeleteLayoutSetMutation(org, app);
const { t } = useTranslation();
- const onDeleteSubForm = () => {
+ const onDeleteSubform = () => {
deleteLayoutSet({ layoutSetIdToUpdate: selectedLayoutSet });
};
const isRegularLayoutSet = !Boolean(
- SubFormUtils.findSubFormById(layoutSets.sets, selectedLayoutSet),
+ SubformUtils.findSubformById(layoutSets.sets, selectedLayoutSet),
);
return (
{
- describe('findSubFormById', () => {
+describe('SubformUtils', () => {
+ describe('findSubformById', () => {
const layoutSets: Array = [{ id: '1' }, { id: '2', type: 'subform' }, { id: '3' }];
it('should return the layout set when it is a subform', () => {
- const result = SubFormUtils.findSubFormById(layoutSets, '2');
+ const result = SubformUtils.findSubformById(layoutSets, '2');
expect(result).toEqual({ id: '2', type: 'subform' });
});
it('should return null when the layout set is not a subform', () => {
- const result = SubFormUtils.findSubFormById(layoutSets, '1');
+ const result = SubformUtils.findSubformById(layoutSets, '1');
expect(result).toBeNull();
});
it('should return null when the layout set is not found', () => {
- const result = SubFormUtils.findSubFormById(layoutSets, 'non-existent-id');
+ const result = SubformUtils.findSubformById(layoutSets, 'non-existent-id');
expect(result).toBeNull();
});
});
diff --git a/frontend/packages/ux-editor/src/components/Elements/SubForm/SubFormUtils.ts b/frontend/packages/ux-editor/src/components/Elements/Subform/SubformUtils.ts
similarity index 67%
rename from frontend/packages/ux-editor/src/components/Elements/SubForm/SubFormUtils.ts
rename to frontend/packages/ux-editor/src/components/Elements/Subform/SubformUtils.ts
index bc2670805f8..c7af57e8a90 100644
--- a/frontend/packages/ux-editor/src/components/Elements/SubForm/SubFormUtils.ts
+++ b/frontend/packages/ux-editor/src/components/Elements/Subform/SubformUtils.ts
@@ -2,8 +2,8 @@ import type { LayoutSet } from 'app-shared/types/api/LayoutSetsResponse';
const SUBFORM_IDENTIFIER = 'subform';
-export class SubFormUtils {
- public static findSubFormById(
+export class SubformUtils {
+ public static findSubformById(
layoutSets: Array,
layoutSetId: string,
): LayoutSet | null {
@@ -11,10 +11,10 @@ export class SubFormUtils {
if (!foundLayoutSet) return null;
- return SubFormUtils.isLayoutSetSubForm(foundLayoutSet) ? foundLayoutSet : null;
+ return SubformUtils.isLayoutSetSubform(foundLayoutSet) ? foundLayoutSet : null;
}
- private static isLayoutSetSubForm(layoutSet: LayoutSet): boolean {
+ private static isLayoutSetSubform(layoutSet: LayoutSet): boolean {
return layoutSet.type === SUBFORM_IDENTIFIER;
}
}
diff --git a/frontend/packages/ux-editor/src/components/Elements/Subform/SubformWrapper.tsx b/frontend/packages/ux-editor/src/components/Elements/Subform/SubformWrapper.tsx
new file mode 100644
index 00000000000..1654affec3a
--- /dev/null
+++ b/frontend/packages/ux-editor/src/components/Elements/Subform/SubformWrapper.tsx
@@ -0,0 +1,23 @@
+import type { LayoutSets } from 'app-shared/types/api/LayoutSetsResponse';
+import React from 'react';
+import { CreateSubformWrapper } from './CreateSubformWrapper';
+import { DeleteSubformWrapper } from './DeleteSubformWrapper';
+
+type SubformWrapperProps = {
+ layoutSets: LayoutSets;
+ onSubformCreated: (layoutSetName: string) => void;
+ selectedLayoutSet: string;
+};
+
+export const SubformWrapper = ({
+ layoutSets,
+ onSubformCreated,
+ selectedLayoutSet,
+}: SubformWrapperProps): React.ReactElement => {
+ return (
+
+
+
+
+ );
+};
diff --git a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/index.ts b/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/index.ts
deleted file mode 100644
index 4c9bc5c96da..00000000000
--- a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { EditSubFormTableColumns } from './EditSubFormTableColumns';
diff --git a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/ColumnElement/ColumnElement.module.css b/frontend/packages/ux-editor/src/components/Properties/EditSubformTableColumns/ColumnElement/ColumnElement.module.css
similarity index 100%
rename from frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/ColumnElement/ColumnElement.module.css
rename to frontend/packages/ux-editor/src/components/Properties/EditSubformTableColumns/ColumnElement/ColumnElement.module.css
diff --git a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/ColumnElement/ColumnElement.test.tsx b/frontend/packages/ux-editor/src/components/Properties/EditSubformTableColumns/ColumnElement/ColumnElement.test.tsx
similarity index 100%
rename from frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/ColumnElement/ColumnElement.test.tsx
rename to frontend/packages/ux-editor/src/components/Properties/EditSubformTableColumns/ColumnElement/ColumnElement.test.tsx
diff --git a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/ColumnElement/ColumnElement.tsx b/frontend/packages/ux-editor/src/components/Properties/EditSubformTableColumns/ColumnElement/ColumnElement.tsx
similarity index 100%
rename from frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/ColumnElement/ColumnElement.tsx
rename to frontend/packages/ux-editor/src/components/Properties/EditSubformTableColumns/ColumnElement/ColumnElement.tsx
diff --git a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/ColumnElement/index.ts b/frontend/packages/ux-editor/src/components/Properties/EditSubformTableColumns/ColumnElement/index.ts
similarity index 100%
rename from frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/ColumnElement/index.ts
rename to frontend/packages/ux-editor/src/components/Properties/EditSubformTableColumns/ColumnElement/index.ts
diff --git a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/EditSubFormTableColumns.module.css b/frontend/packages/ux-editor/src/components/Properties/EditSubformTableColumns/EditSubformTableColumns.module.css
similarity index 100%
rename from frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/EditSubFormTableColumns.module.css
rename to frontend/packages/ux-editor/src/components/Properties/EditSubformTableColumns/EditSubformTableColumns.module.css
diff --git a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/EditSubFormTableColumns.test.tsx b/frontend/packages/ux-editor/src/components/Properties/EditSubformTableColumns/EditSubformTableColumns.test.tsx
similarity index 84%
rename from frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/EditSubFormTableColumns.test.tsx
rename to frontend/packages/ux-editor/src/components/Properties/EditSubformTableColumns/EditSubformTableColumns.test.tsx
index 59928aaf55f..63729c5cb74 100644
--- a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/EditSubFormTableColumns.test.tsx
+++ b/frontend/packages/ux-editor/src/components/Properties/EditSubformTableColumns/EditSubformTableColumns.test.tsx
@@ -1,9 +1,9 @@
import React from 'react';
import { screen } from '@testing-library/react';
import {
- EditSubFormTableColumns,
- type EditSubFormTableColumnsProps,
-} from './EditSubFormTableColumns';
+ EditSubformTableColumns,
+ type EditSubformTableColumnsProps,
+} from './EditSubformTableColumns';
import { textMock } from '@studio/testing/mocks/i18nMock';
import { renderWithProviders } from 'dashboard/testing/mocks';
import { createQueryClientMock } from 'app-shared/mocks/queryClientMock';
@@ -12,14 +12,14 @@ import userEvent from '@testing-library/user-event';
import { ComponentType } from 'app-shared/types/ComponentType';
import { componentMocks } from '@altinn/ux-editor/testing/componentMocks';
-const subFormComponentMock = componentMocks[ComponentType.SubForm];
+const subformComponentMock = componentMocks[ComponentType.Subform];
-const defaultProps: EditSubFormTableColumnsProps = {
- component: subFormComponentMock,
+const defaultProps: EditSubformTableColumnsProps = {
+ component: subformComponentMock,
handleComponentChange: jest.fn(),
};
-describe('EditSubFormTableColumns', () => {
+describe('EditSubformTableColumns', () => {
afterEach(() => {
jest.clearAllMocks();
});
@@ -28,8 +28,8 @@ describe('EditSubFormTableColumns', () => {
const handleComponentChangeMock = jest.fn();
const user = userEvent.setup();
- renderEditSubFormTableColumns({
- component: { ...subFormComponentMock, tableColumns: undefined },
+ renderEditSubformTableColumns({
+ component: { ...subformComponentMock, tableColumns: undefined },
handleComponentChange: handleComponentChangeMock,
});
@@ -48,7 +48,7 @@ describe('EditSubFormTableColumns', () => {
const handleComponentChangeMock = jest.fn();
const user = userEvent.setup();
- renderEditSubFormTableColumns({
+ renderEditSubformTableColumns({
handleComponentChange: handleComponentChangeMock,
});
@@ -67,12 +67,12 @@ describe('EditSubFormTableColumns', () => {
const handleComponentChangeMock = jest.fn();
const user = userEvent.setup();
- renderEditSubFormTableColumns({
+ renderEditSubformTableColumns({
handleComponentChange: handleComponentChangeMock,
});
const headerInputbutton = screen.getByRole('button', {
- name: `${textMock('ux_editor.properties_panel.subform_table_columns.header_content_label')}: ${subFormComponentMock.tableColumns[0].headerContent}`,
+ name: `${textMock('ux_editor.properties_panel.subform_table_columns.header_content_label')}: ${subformComponentMock.tableColumns[0].headerContent}`,
});
await user.click(headerInputbutton);
@@ -95,7 +95,7 @@ describe('EditSubFormTableColumns', () => {
const handleComponentChangeMock = jest.fn();
const user = userEvent.setup();
- renderEditSubFormTableColumns({
+ renderEditSubformTableColumns({
handleComponentChange: handleComponentChangeMock,
});
@@ -113,9 +113,9 @@ describe('EditSubFormTableColumns', () => {
});
});
-const renderEditSubFormTableColumns = (props: Partial = {}) => {
+const renderEditSubformTableColumns = (props: Partial = {}) => {
const queryClient = createQueryClientMock();
- return renderWithProviders( , {
+ return renderWithProviders( , {
...queriesMock,
queryClient,
});
diff --git a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/EditSubFormTableColumns.tsx b/frontend/packages/ux-editor/src/components/Properties/EditSubformTableColumns/EditSubformTableColumns.tsx
similarity index 90%
rename from frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/EditSubFormTableColumns.tsx
rename to frontend/packages/ux-editor/src/components/Properties/EditSubformTableColumns/EditSubformTableColumns.tsx
index 1e6f6cd718c..bfbcb7e4eba 100644
--- a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/EditSubFormTableColumns.tsx
+++ b/frontend/packages/ux-editor/src/components/Properties/EditSubformTableColumns/EditSubformTableColumns.tsx
@@ -1,5 +1,5 @@
import React, { type ReactElement } from 'react';
-import classes from './EditSubFormTableColumns.module.css';
+import classes from './EditSubformTableColumns.module.css';
import { StudioButton, StudioHeading } from '@studio/components';
import { useTranslation } from 'react-i18next';
import { type IGenericEditComponent } from '../../config/componentConfig';
@@ -9,12 +9,12 @@ import { filterOutTableColumn, updateComponentWithSubform } from './utils';
import { useUniqueKeys } from '@studio/hooks';
import { ColumnElement } from './ColumnElement';
-export type EditSubFormTableColumnsProps = IGenericEditComponent;
+export type EditSubformTableColumnsProps = IGenericEditComponent;
-export const EditSubFormTableColumns = ({
+export const EditSubformTableColumns = ({
component,
handleComponentChange,
-}: EditSubFormTableColumnsProps): ReactElement => {
+}: EditSubformTableColumnsProps): ReactElement => {
const { t } = useTranslation();
const tableColumns: TableColumn[] = component?.tableColumns ?? [];
diff --git a/frontend/packages/ux-editor/src/components/Properties/EditSubformTableColumns/index.ts b/frontend/packages/ux-editor/src/components/Properties/EditSubformTableColumns/index.ts
new file mode 100644
index 00000000000..cef4ebcc7d6
--- /dev/null
+++ b/frontend/packages/ux-editor/src/components/Properties/EditSubformTableColumns/index.ts
@@ -0,0 +1 @@
+export { EditSubformTableColumns } from './EditSubformTableColumns';
diff --git a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/types/TableColumn.ts b/frontend/packages/ux-editor/src/components/Properties/EditSubformTableColumns/types/TableColumn.ts
similarity index 100%
rename from frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/types/TableColumn.ts
rename to frontend/packages/ux-editor/src/components/Properties/EditSubformTableColumns/types/TableColumn.ts
diff --git a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/utils/editSubFormTableColumnsUtils.test.ts b/frontend/packages/ux-editor/src/components/Properties/EditSubformTableColumns/utils/editSubformTableColumnsUtils.test.ts
similarity index 85%
rename from frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/utils/editSubFormTableColumnsUtils.test.ts
rename to frontend/packages/ux-editor/src/components/Properties/EditSubformTableColumns/utils/editSubformTableColumnsUtils.test.ts
index 11ef2891b3e..95211b1a7c5 100644
--- a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/utils/editSubFormTableColumnsUtils.test.ts
+++ b/frontend/packages/ux-editor/src/components/Properties/EditSubformTableColumns/utils/editSubformTableColumnsUtils.test.ts
@@ -1,4 +1,4 @@
-import { updateComponentWithSubform, filterOutTableColumn } from './editSubFormTableColumnsUtils';
+import { updateComponentWithSubform, filterOutTableColumn } from './editSubformTableColumnsUtils';
import { type FormItem } from '@altinn/ux-editor/types/FormItem';
import { ComponentType } from 'app-shared/types/ComponentType';
import { type TableColumn } from '../types/TableColumn';
@@ -18,25 +18,25 @@ const mockTableColumn3: TableColumn = {
cellContent: { query: 'query 3', default: 'default 3' },
};
-const subFormComponentMock = componentMocks[ComponentType.SubForm];
+const subformComponentMock = componentMocks[ComponentType.Subform];
-describe('editSubFormTableColumnsUtils', () => {
+describe('editSubformTableColumnsUtils', () => {
describe('updateComponentWithSubform', () => {
it('should add table columns to the component', () => {
const tableColumnsToAdd = [mockTableColumn2, mockTableColumn3];
- const updatedComponent = updateComponentWithSubform(subFormComponentMock, tableColumnsToAdd);
+ const updatedComponent = updateComponentWithSubform(subformComponentMock, tableColumnsToAdd);
expect(updatedComponent.tableColumns).toEqual([
- subFormComponentMock.tableColumns[0],
+ subformComponentMock.tableColumns[0],
mockTableColumn2,
mockTableColumn3,
]);
});
it('should handle case where the component has no initial tableColumns', () => {
- const componentWithoutColumns: FormItem = {
- ...subFormComponentMock,
+ const componentWithoutColumns: FormItem = {
+ ...subformComponentMock,
tableColumns: undefined,
};
@@ -51,9 +51,9 @@ describe('editSubFormTableColumnsUtils', () => {
});
it('should return the same component if tableColumnsToAdd is an empty array', () => {
- const updatedComponent = updateComponentWithSubform(subFormComponentMock, []);
+ const updatedComponent = updateComponentWithSubform(subformComponentMock, []);
- expect(updatedComponent.tableColumns).toEqual([subFormComponentMock.tableColumns[0]]);
+ expect(updatedComponent.tableColumns).toEqual([subformComponentMock.tableColumns[0]]);
});
});
diff --git a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/utils/editSubFormTableColumnsUtils.ts b/frontend/packages/ux-editor/src/components/Properties/EditSubformTableColumns/utils/editSubformTableColumnsUtils.ts
similarity index 87%
rename from frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/utils/editSubFormTableColumnsUtils.ts
rename to frontend/packages/ux-editor/src/components/Properties/EditSubformTableColumns/utils/editSubformTableColumnsUtils.ts
index fe0f6144b1c..fa642a74b09 100644
--- a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/utils/editSubFormTableColumnsUtils.ts
+++ b/frontend/packages/ux-editor/src/components/Properties/EditSubformTableColumns/utils/editSubformTableColumnsUtils.ts
@@ -3,9 +3,9 @@ import { type ComponentType } from 'app-shared/types/ComponentType';
import { type TableColumn } from '../types/TableColumn';
export const updateComponentWithSubform = (
- component: FormItem,
+ component: FormItem,
tableColumnsToAdd: TableColumn[],
-): FormItem => {
+): FormItem => {
return {
...component,
tableColumns: [...(component?.tableColumns ?? []), ...tableColumnsToAdd],
diff --git a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/utils/index.ts b/frontend/packages/ux-editor/src/components/Properties/EditSubformTableColumns/utils/index.ts
similarity index 64%
rename from frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/utils/index.ts
rename to frontend/packages/ux-editor/src/components/Properties/EditSubformTableColumns/utils/index.ts
index 25f7e5966eb..b05d8bfbd76 100644
--- a/frontend/packages/ux-editor/src/components/Properties/EditSubFormTableColumns/utils/index.ts
+++ b/frontend/packages/ux-editor/src/components/Properties/EditSubformTableColumns/utils/index.ts
@@ -1 +1 @@
-export { updateComponentWithSubform, filterOutTableColumn } from './editSubFormTableColumnsUtils';
+export { updateComponentWithSubform, filterOutTableColumn } from './editSubformTableColumnsUtils';
diff --git a/frontend/packages/ux-editor/src/components/Properties/Properties.test.tsx b/frontend/packages/ux-editor/src/components/Properties/Properties.test.tsx
index 17d526e4de0..b86d7839142 100644
--- a/frontend/packages/ux-editor/src/components/Properties/Properties.test.tsx
+++ b/frontend/packages/ux-editor/src/components/Properties/Properties.test.tsx
@@ -256,8 +256,8 @@ describe('Properties', () => {
it('render properties accordions for a subform component when it is linked to a subform layoutSet', () => {
editFormComponentSpy.mockReturnValue( );
renderProperties({
- formItem: { ...componentMocks[ComponentType.SubForm], layoutSet: layoutSetName },
- formItemId: componentMocks[ComponentType.SubForm].id,
+ formItem: { ...componentMocks[ComponentType.Subform], layoutSet: layoutSetName },
+ formItemId: componentMocks[ComponentType.Subform].id,
});
expect(screen.getByText(textMock('right_menu.text'))).toBeInTheDocument();
expect(screen.getByText(textMock('right_menu.data_model_bindings'))).toBeInTheDocument();
diff --git a/frontend/packages/ux-editor/src/components/Properties/Properties.tsx b/frontend/packages/ux-editor/src/components/Properties/Properties.tsx
index ff03e5b6a69..1009b572dfa 100644
--- a/frontend/packages/ux-editor/src/components/Properties/Properties.tsx
+++ b/frontend/packages/ux-editor/src/components/Properties/Properties.tsx
@@ -32,7 +32,7 @@ export const Properties = () => {
}
};
- const isNotSubformOrHasLayoutSet = formItem.type !== 'SubForm' || !!formItem.layoutSet;
+ const isNotSubformOrHasLayoutSet = formItem.type !== 'Subform' || !!formItem.layoutSet;
return (
diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformLayoutSet/CreateNewSubformLayoutSet.module.css b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformLayoutSet/CreateNewSubformLayoutSet.module.css
new file mode 100644
index 00000000000..7def1fa7574
--- /dev/null
+++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformLayoutSet/CreateNewSubformLayoutSet.module.css
@@ -0,0 +1,10 @@
+.savelayoutSetButton {
+ display: flex;
+ align-self: flex-start;
+ border: 2px solid var(--success-color);
+ color: var(--success-color);
+}
+
+.headerIcon {
+ font-size: large;
+}
diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformLayoutSet/CreateNewSubformLayoutSet.test.tsx b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformLayoutSet/CreateNewSubformLayoutSet.test.tsx
new file mode 100644
index 00000000000..970ce181232
--- /dev/null
+++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformLayoutSet/CreateNewSubformLayoutSet.test.tsx
@@ -0,0 +1,67 @@
+import React from 'react';
+import { renderWithProviders } from '../../../../../../testing/mocks';
+import { CreateNewSubformLayoutSet } from './CreateNewSubformLayoutSet';
+import type { ComponentType } from 'app-shared/types/ComponentType';
+import { textMock } from '@studio/testing/mocks/i18nMock';
+import { screen, waitFor } from '@testing-library/react';
+import { createQueryClientMock } from 'app-shared/mocks/queryClientMock';
+import { app, org } from '@studio/testing/testids';
+import { QueryKey } from 'app-shared/types/QueryKey';
+import { layoutSets } from 'app-shared/mocks/mocks';
+import type { LayoutSets } from 'app-shared/types/api/LayoutSetsResponse';
+import userEvent from '@testing-library/user-event';
+import type { FormComponent } from '../../../../../../types/FormComponent';
+import { AppContext } from '../../../../../../AppContext';
+import { appContextMock } from '../../../../../../testing/appContextMock';
+
+const onSubFormCreatedMock = jest.fn();
+
+describe('CreateNewSubformLayoutSet ', () => {
+ afterEach(jest.clearAllMocks);
+
+ it('displays the card with label and input field', () => {
+ renderCreateNewSubformLayoutSet();
+ const card = screen.getByRole('textbox', {
+ name: textMock('ux_editor.component_properties.subform.created_layout_set_name'),
+ });
+
+ expect(card).toBeInTheDocument();
+ });
+
+ it('displays the input field', () => {
+ renderCreateNewSubformLayoutSet();
+ const input = screen.getByRole('textbox');
+ expect(input).toBeInTheDocument();
+ });
+
+ it('displays the save button', () => {
+ renderCreateNewSubformLayoutSet();
+ const saveButton = screen.getByRole('button', { name: textMock('general.close') });
+ expect(saveButton).toBeInTheDocument();
+ });
+
+ it('calls onSubFormCreated when save button is clicked', async () => {
+ const user = userEvent.setup();
+ renderCreateNewSubformLayoutSet();
+ const input = screen.getByRole('textbox');
+ await user.type(input, 'NewSubForm');
+ const saveButton = screen.getByRole('button', { name: textMock('general.close') });
+ await user.click(saveButton);
+ await waitFor(() => expect(onSubFormCreatedMock).toHaveBeenCalledTimes(1));
+ expect(onSubFormCreatedMock).toHaveBeenCalledWith('NewSubForm');
+ });
+});
+
+const renderCreateNewSubformLayoutSet = (
+ layoutSetsMock: LayoutSets = layoutSets,
+ componentProps: Partial
> = {},
+) => {
+ const queryClient = createQueryClientMock();
+ queryClient.setQueryData([QueryKey.LayoutSets, org, app], layoutSetsMock);
+ return renderWithProviders(
+
+
+ ,
+ { queryClient },
+ );
+};
diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformLayoutSet/CreateNewSubformLayoutSet.tsx b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformLayoutSet/CreateNewSubformLayoutSet.tsx
new file mode 100644
index 00000000000..ef9b52de926
--- /dev/null
+++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformLayoutSet/CreateNewSubformLayoutSet.tsx
@@ -0,0 +1,61 @@
+import React, { useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { StudioButton, StudioCard, StudioTextfield } from '@studio/components';
+import { ClipboardIcon, CheckmarkIcon } from '@studio/icons';
+import { useAddLayoutSetMutation } from 'app-development/hooks/mutations/useAddLayoutSetMutation';
+import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams';
+import classes from './CreateNewSubformLayoutSet.module.css';
+
+type CreateNewSubformLayoutSetProps = {
+ onSubFormCreated: (layoutSetName: string) => void;
+};
+
+export const CreateNewSubformLayoutSet = ({
+ onSubFormCreated,
+}: CreateNewSubformLayoutSetProps): React.ReactElement => {
+ const { t } = useTranslation();
+ const [newSubForm, setNewSubForm] = useState('');
+ const { org, app } = useStudioEnvironmentParams();
+ const { mutate: addLayoutSet } = useAddLayoutSetMutation(org, app);
+
+ const createNewSubform = () => {
+ if (!newSubForm) return;
+ addLayoutSet({
+ layoutSetIdToUpdate: newSubForm,
+ layoutSetConfig: {
+ id: newSubForm,
+ type: 'subform',
+ },
+ });
+ onSubFormCreated(newSubForm);
+ setNewSubForm('');
+ };
+
+ function handleChange(e: React.ChangeEvent) {
+ setNewSubForm(e.target.value);
+ }
+
+ return (
+
+
+
+
+
+
+ }
+ onClick={createNewSubform}
+ title={t('general.close')}
+ variant='tertiary'
+ color='success'
+ />
+
+
+ );
+};
diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformLayoutSet/index.ts b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformLayoutSet/index.ts
new file mode 100644
index 00000000000..39c8808d341
--- /dev/null
+++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/CreateNewSubformLayoutSet/index.ts
@@ -0,0 +1 @@
+export { CreateNewSubformLayoutSet } from './CreateNewSubformLayoutSet';
diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/DefinedLayoutSet/DefinedLayoutSet.tsx b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/DefinedLayoutSet/DefinedLayoutSet.tsx
index 1b78a5ff4a0..3dedfbec09e 100644
--- a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/DefinedLayoutSet/DefinedLayoutSet.tsx
+++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/DefinedLayoutSet/DefinedLayoutSet.tsx
@@ -5,31 +5,31 @@ import { useTranslation } from 'react-i18next';
import classes from './DefinedLayoutSet.module.css';
type DefinedLayoutSetProps = {
- existingLayoutSetForSubForm: string;
+ existingLayoutSetForSubform: string;
onClick: () => void;
};
export const DefinedLayoutSet = ({
- existingLayoutSetForSubForm,
+ existingLayoutSetForSubform,
onClick,
}: DefinedLayoutSetProps) => {
const { t } = useTranslation();
const value = (
- {existingLayoutSetForSubForm}
+ {existingLayoutSetForSubform}
);
return (
diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/EditLayoutSet.module.css b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/EditLayoutSet.module.css
new file mode 100644
index 00000000000..cec24eef80a
--- /dev/null
+++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/EditLayoutSet.module.css
@@ -0,0 +1,4 @@
+.button {
+ padding-left: 0;
+ border-radius: var(--fds-sizing-1);
+}
diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/EditLayoutSet.tsx b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/EditLayoutSet.tsx
index eff8e7bcd9c..6968f45919a 100644
--- a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/EditLayoutSet.tsx
+++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/EditLayoutSet.tsx
@@ -2,53 +2,74 @@ import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { DefinedLayoutSet } from './DefinedLayoutSet/DefinedLayoutSet';
import { SelectLayoutSet } from './SelectLayoutSet/SelectLayoutSet';
-import { StudioRecommendedNextAction } from '@studio/components';
+import { StudioParagraph, StudioProperty, StudioRecommendedNextAction } from '@studio/components';
+import { PlusIcon } from '@studio/icons';
+import classes from './EditLayoutSet.module.css';
+import { CreateNewSubformLayoutSet } from './CreateNewSubformLayoutSet';
type EditLayoutSetProps = {
existingLayoutSetForSubform: string;
onUpdateLayoutSet: (layoutSetId: string) => void;
+ onSubFormCreated: (layoutSetName: string) => void;
};
export const EditLayoutSet = ({
existingLayoutSetForSubform,
onUpdateLayoutSet,
+ onSubFormCreated,
}: EditLayoutSetProps): React.ReactElement => {
const { t } = useTranslation();
const [isLayoutSetSelectorVisible, setIsLayoutSetSelectorVisible] = useState(false);
+ const [showCreateSubform, setShowCreateSubform] = useState(false);
+
+ function handleClick() {
+ setShowCreateSubform(true);
+ }
if (isLayoutSetSelectorVisible) {
return (
);
}
-
const layoutSetIsUndefined = !existingLayoutSetForSubform;
if (layoutSetIsUndefined) {
return (
-
-
-
+ <>
+
+
+ {t('ux_editor.component_properties.subform.create_layout_set_description')}
+
+
+ }
+ onClick={handleClick}
+ />
+
+ {showCreateSubform && }
+ >
);
}
return (
setIsLayoutSetSelectorVisible(true)}
/>
);
diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/SelectLayoutSet/SelectLayoutSet.tsx b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/SelectLayoutSet/SelectLayoutSet.tsx
index d0eff68e556..e4a5af2a737 100644
--- a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/SelectLayoutSet/SelectLayoutSet.tsx
+++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSet/SelectLayoutSet/SelectLayoutSet.tsx
@@ -5,18 +5,18 @@ import classes from './SelectLayoutSet.module.css';
import { EditLayoutSetButtons } from './EditLayoutSetButtons/EditLayoutSetButtons';
import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams';
import { useLayoutSetsQuery } from 'app-shared/hooks/queries/useLayoutSetsQuery';
-import { SubFormUtilsImpl } from '../../../../../../classes/SubFormUtils';
+import { SubformUtilsImpl } from '../../../../../../classes/SubformUtils';
import cn from 'classnames';
type SelectLayoutSetProps = {
- existingLayoutSetForSubForm: string;
+ existingLayoutSetForSubform: string;
onUpdateLayoutSet: (layoutSetId: string) => void;
onSetLayoutSetSelectorVisible: (visible: boolean) => void;
showButtons?: boolean;
};
export const SelectLayoutSet = ({
- existingLayoutSetForSubForm,
+ existingLayoutSetForSubform,
onUpdateLayoutSet,
onSetLayoutSetSelectorVisible,
showButtons,
@@ -24,7 +24,7 @@ export const SelectLayoutSet = ({
const { t } = useTranslation();
const { org, app } = useStudioEnvironmentParams();
const { data: layoutSets } = useLayoutSetsQuery(org, app);
- const subFormUtils = new SubFormUtilsImpl(layoutSets.sets);
+ const subformUtils = new SubformUtilsImpl(layoutSets.sets);
const addLinkToLayoutSet = (layoutSetId: string): void => {
onUpdateLayoutSet(layoutSetId);
@@ -53,7 +53,7 @@ export const SelectLayoutSet = ({
return (
{t('ux_editor.component_properties.subform.choose_layout_set')}
- {subFormUtils.subformLayoutSetsIds.map((option) => (
+ {subformUtils.subformLayoutSetsIds.map((option) => (
{option}
diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSetForSubform.test.tsx b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSetForSubform.test.tsx
index 0140a74acfc..9be5df6e5d6 100644
--- a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSetForSubform.test.tsx
+++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSetForSubform.test.tsx
@@ -18,20 +18,20 @@ import { appContextMock } from '../../../../testing/appContextMock';
const handleComponentChangeMock = jest.fn();
const setSelectedFormLayoutSetMock = jest.fn();
-describe('EditLayoutSetForSubForm', () => {
+describe('EditLayoutSetForSubform', () => {
afterEach(jest.clearAllMocks);
it('displays "no existing subform layout sets" message if no subform layout set exist', () => {
- renderEditLayoutSetForSubForm();
- const noExistingSubFormForLayoutSet = screen.getByText(
+ renderEditLayoutSetForSubform();
+ const noExistingSubformForLayoutSet = screen.getByText(
textMock('ux_editor.component_properties.subform.no_layout_sets_acting_as_subform'),
);
- expect(noExistingSubFormForLayoutSet).toBeInTheDocument();
+ expect(noExistingSubformForLayoutSet).toBeInTheDocument();
});
it('displays the headers for recommendNextAction if subform layout sets exists', () => {
const subformLayoutSetId = 'subformLayoutSetId';
- renderEditLayoutSetForSubForm({ sets: [{ id: subformLayoutSetId, type: 'subform' }] });
+ renderEditLayoutSetForSubform({ sets: [{ id: subformLayoutSetId, type: 'subform' }] });
const setLayoutSetButton = screen.getByRole('heading', {
name: textMock('ux_editor.component_properties.subform.choose_layout_set_header'),
});
@@ -40,16 +40,39 @@ describe('EditLayoutSetForSubForm', () => {
it('displays the description for recommendNextAction if subform layout sets exists', () => {
const subformLayoutSetId = 'subformLayoutSetId';
- renderEditLayoutSetForSubForm({ sets: [{ id: subformLayoutSetId, type: 'subform' }] });
+ renderEditLayoutSetForSubform({ sets: [{ id: subformLayoutSetId, type: 'subform' }] });
const setLayoutSetButton = screen.getByText(
textMock('ux_editor.component_properties.subform.choose_layout_set_description'),
);
expect(setLayoutSetButton).toBeInTheDocument();
});
+ it('displays a button(Opprett et nytt skjema) to set a layout set for the subform', async () => {
+ const subformLayoutSetId = 'subformLayoutSetId';
+ renderEditLayoutSetForSubform({ sets: [{ id: subformLayoutSetId, type: 'subform' }] });
+ const createNewLayoutSetButton = screen.getByRole('button', {
+ name: textMock('ux_editor.component_properties.subform.create_layout_set_button'),
+ });
+ expect(createNewLayoutSetButton).toBeInTheDocument();
+ });
+
+ it('renders CreateNewLayoutSet component when clicking the create new layout set button', async () => {
+ const user = userEvent.setup();
+ const subformLayoutSetId = 'subformLayoutSetId';
+ renderEditLayoutSetForSubform({ sets: [{ id: subformLayoutSetId, type: 'subform' }] });
+ const createNewLayoutSetButton = screen.getByRole('button', {
+ name: textMock('ux_editor.component_properties.subform.create_layout_set_button'),
+ });
+ await user.click(createNewLayoutSetButton);
+ const createNewLayoutSetComponent = screen.getByRole('textbox', {
+ name: textMock('ux_editor.component_properties.subform.created_layout_set_name'),
+ });
+ expect(createNewLayoutSetComponent).toBeInTheDocument();
+ });
+
it('displays a select to choose a layout set for the subform', async () => {
const subformLayoutSetId = 'subformLayoutSetId';
- renderEditLayoutSetForSubForm({ sets: [{ id: subformLayoutSetId, type: 'subform' }] });
+ renderEditLayoutSetForSubform({ sets: [{ id: subformLayoutSetId, type: 'subform' }] });
const selectLayoutSet = getSelectForLayoutSet();
const options = within(selectLayoutSet).getAllByRole('option');
expect(options).toHaveLength(2);
@@ -62,7 +85,7 @@ describe('EditLayoutSetForSubForm', () => {
it('calls handleComponentChange when setting a layout set for the subform', async () => {
const user = userEvent.setup();
const subformLayoutSetId = 'subformLayoutSetId';
- renderEditLayoutSetForSubForm({ sets: [{ id: subformLayoutSetId, type: 'subform' }] });
+ renderEditLayoutSetForSubform({ sets: [{ id: subformLayoutSetId, type: 'subform' }] });
const selectLayoutSet = getSelectForLayoutSet();
await user.selectOptions(selectLayoutSet, subformLayoutSetId);
expect(handleComponentChangeMock).toHaveBeenCalledTimes(1);
@@ -76,7 +99,7 @@ describe('EditLayoutSetForSubForm', () => {
it('should display the selected layout set in document after the user choose it', async () => {
const user = userEvent.setup();
const subformLayoutSetId = 'subformLayoutSetId';
- renderEditLayoutSetForSubForm({ sets: [{ id: subformLayoutSetId, type: 'subform' }] });
+ renderEditLayoutSetForSubform({ sets: [{ id: subformLayoutSetId, type: 'subform' }] });
const selectLayoutSet = getSelectForLayoutSet();
await user.selectOptions(selectLayoutSet, subformLayoutSetId);
expect(screen.getByText(subformLayoutSetId)).toBeInTheDocument();
@@ -85,7 +108,7 @@ describe('EditLayoutSetForSubForm', () => {
it('should display the select again with its buttons when the user clicks on the seleced layoutset', async () => {
const user = userEvent.setup();
const subformLayoutSetId = 'subformLayoutSetId';
- renderEditLayoutSetForSubForm(
+ renderEditLayoutSetForSubform(
{ sets: [{ id: subformLayoutSetId, type: 'subform' }] },
{ layoutSet: subformLayoutSetId },
);
@@ -99,7 +122,7 @@ describe('EditLayoutSetForSubForm', () => {
it('calls handleComponentChange with no layout set for component if selecting the empty option', async () => {
const user = userEvent.setup();
const subformLayoutSetId = 'subformLayoutSetId';
- renderEditLayoutSetForSubForm({ sets: [{ id: subformLayoutSetId, type: 'subform' }] });
+ renderEditLayoutSetForSubform({ sets: [{ id: subformLayoutSetId, type: 'subform' }] });
const selectLayoutSet = getSelectForLayoutSet();
const emptyOptionText = textMock('ux_editor.component_properties.subform.choose_layout_set');
await user.selectOptions(selectLayoutSet, emptyOptionText);
@@ -111,10 +134,30 @@ describe('EditLayoutSetForSubForm', () => {
);
});
+ it('calls handleComponentChange after creating a new layout set and clicking Lukk button', async () => {
+ const user = userEvent.setup();
+ const subformLayoutSetId = 'subformLayoutSetId';
+ renderEditLayoutSetForSubform({ sets: [{ id: subformLayoutSetId, type: 'subform' }] });
+ const createNewLayoutSetButton = screen.getByRole('button', {
+ name: textMock('ux_editor.component_properties.subform.create_layout_set_button'),
+ });
+ await user.click(createNewLayoutSetButton);
+ const input = screen.getByRole('textbox');
+ await user.type(input, 'NewSubForm');
+ const saveButton = screen.getByRole('button', { name: textMock('general.close') });
+ await user.click(saveButton);
+ expect(handleComponentChangeMock).toHaveBeenCalledTimes(1);
+ expect(handleComponentChangeMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ layoutSet: 'NewSubForm',
+ }),
+ );
+ });
+
it('closes the view mode when clicking close button after selecting a layout set', async () => {
const user = userEvent.setup();
const subformLayoutSetId = 'subformLayoutSetId';
- renderEditLayoutSetForSubForm(
+ renderEditLayoutSetForSubform(
{ sets: [{ id: subformLayoutSetId, type: 'subform' }] },
{ layoutSet: subformLayoutSetId },
);
@@ -132,7 +175,7 @@ describe('EditLayoutSetForSubForm', () => {
it('calls handleComponentChange with no layout set for component when clicking delete button', async () => {
const user = userEvent.setup();
const subformLayoutSetId = 'subformLayoutSetId';
- renderEditLayoutSetForSubForm(
+ renderEditLayoutSetForSubform(
{ sets: [{ id: subformLayoutSetId, type: 'subform' }] },
{ layoutSet: subformLayoutSetId },
);
@@ -151,7 +194,7 @@ describe('EditLayoutSetForSubForm', () => {
it('displays a button with the existing layout set for the subform if set', () => {
const subformLayoutSetId = 'subformLayoutSetId';
- renderEditLayoutSetForSubForm(
+ renderEditLayoutSetForSubform(
{ sets: [{ id: subformLayoutSetId, type: 'subform' }] },
{ layoutSet: subformLayoutSetId },
);
@@ -166,7 +209,7 @@ describe('EditLayoutSetForSubForm', () => {
it('opens view mode when a layout set for the subform is set', async () => {
const user = userEvent.setup();
const subformLayoutSetId = 'subformLayoutSetId';
- renderEditLayoutSetForSubForm(
+ renderEditLayoutSetForSubform(
{ sets: [{ id: subformLayoutSetId, type: 'subform' }] },
{ layoutSet: subformLayoutSetId },
);
@@ -186,9 +229,9 @@ const getSelectForLayoutSet = () =>
name: textMock('ux_editor.component_properties.subform.choose_layout_set_label'),
});
-const renderEditLayoutSetForSubForm = (
+const renderEditLayoutSetForSubform = (
layoutSetsMock: LayoutSets = layoutSets,
- componentProps: Partial> = {},
+ componentProps: Partial> = {},
) => {
const queryClient = createQueryClientMock();
queryClient.setQueryData([QueryKey.LayoutSets, org, app], layoutSetsMock);
@@ -197,7 +240,7 @@ const renderEditLayoutSetForSubForm = (
value={{ ...appContextMock, setSelectedFormLayoutSetName: setSelectedFormLayoutSetMock }}
>
,
diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSetForSubform.tsx b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSetForSubform.tsx
index e158d7ffaf4..db9441e69b3 100644
--- a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSetForSubform.tsx
+++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/EditLayoutSetForSubform/EditLayoutSetForSubform.tsx
@@ -2,10 +2,11 @@ import React from 'react';
import { EditLayoutSet } from './EditLayoutSet';
import { NoSubformLayoutsExist } from './NoSubformLayoutsExist';
import type { ComponentType } from 'app-shared/types/ComponentType';
-import { SubFormUtilsImpl } from '../../../../classes/SubFormUtils';
+import { SubformUtilsImpl } from '../../../../classes/SubformUtils';
import { useLayoutSetsQuery } from 'app-shared/hooks/queries/useLayoutSetsQuery';
import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams';
import type { IGenericEditComponent } from '../../../../components/config/componentConfig';
+import { useAppContext } from '../../../../hooks';
export const EditLayoutSetForSubform = ({
handleComponentChange,
@@ -13,10 +14,11 @@ export const EditLayoutSetForSubform = ({
}: IGenericEditComponent): React.ReactElement => {
const { org, app } = useStudioEnvironmentParams();
const { data: layoutSets } = useLayoutSetsQuery(org, app);
+ const { setSelectedFormLayoutSetName } = useAppContext();
- const subFormUtils = new SubFormUtilsImpl(layoutSets.sets);
+ const subformUtils = new SubformUtilsImpl(layoutSets.sets);
- if (!subFormUtils.hasSubforms) {
+ if (!subformUtils.hasSubforms) {
return ;
}
@@ -25,10 +27,16 @@ export const EditLayoutSetForSubform = ({
handleComponentChange(updatedComponent);
};
+ function handleCreatedSubForm(layoutSetName: string) {
+ setSelectedFormLayoutSetName(layoutSetName);
+ handleUpdatedLayoutSet(layoutSetName);
+ }
+
return (
);
};
diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/PropertiesHeader.test.tsx b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/PropertiesHeader.test.tsx
index 20babe7477d..de05ced479d 100644
--- a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/PropertiesHeader.test.tsx
+++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/PropertiesHeader.test.tsx
@@ -105,7 +105,7 @@ describe('PropertiesHeader', () => {
renderPropertiesHeader({
formItem: {
...component1Mock,
- type: ComponentType.SubForm,
+ type: ComponentType.Subform,
layoutSet: layoutSetName,
id: subformLayoutSetId,
},
@@ -122,7 +122,7 @@ describe('PropertiesHeader', () => {
renderPropertiesHeader({
formItem: {
...component1Mock,
- type: ComponentType.SubForm,
+ type: ComponentType.Subform,
},
});
expect(
@@ -134,7 +134,7 @@ describe('PropertiesHeader', () => {
renderPropertiesHeader({
formItem: {
...component1Mock,
- type: ComponentType.SubForm,
+ type: ComponentType.Subform,
},
});
expect(screen.queryByText(textMock('right_menu.text'))).not.toBeInTheDocument();
diff --git a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/PropertiesHeader.tsx b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/PropertiesHeader.tsx
index 8452125a566..60887f9fb4e 100644
--- a/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/PropertiesHeader.tsx
+++ b/frontend/packages/ux-editor/src/components/Properties/PropertiesHeader/PropertiesHeader.tsx
@@ -42,7 +42,7 @@ export const PropertiesHeader = ({
/>
- {formItem.type === ComponentType.SubForm && (
+ {formItem.type === ComponentType.Subform && (
{
props: {
...props,
formItem: {
- ...componentMocks[ComponentType.SubForm],
+ ...componentMocks[ComponentType.Subform],
},
},
});
@@ -317,14 +317,14 @@ describe('TextTab', () => {
expect(addColumnButton).toBeInTheDocument();
});
- it('should call handleUpdate when handleComponentChange is triggered from EditSubFormTableColumns', async () => {
+ it('should call handleUpdate when handleComponentChange is triggered from EditSubformTableColumns', async () => {
const user = userEvent.setup();
render({
props: {
...props,
formItem: {
- ...componentMocks[ComponentType.SubForm],
+ ...componentMocks[ComponentType.Subform],
},
},
});
diff --git a/frontend/packages/ux-editor/src/components/Properties/Text.tsx b/frontend/packages/ux-editor/src/components/Properties/Text.tsx
index 523ad896a84..3b3d9a87368 100644
--- a/frontend/packages/ux-editor/src/components/Properties/Text.tsx
+++ b/frontend/packages/ux-editor/src/components/Properties/Text.tsx
@@ -12,7 +12,7 @@ import type { ComponentSpecificConfig } from 'app-shared/types/ComponentSpecific
import { useAppContext } from '../../hooks';
import { EditImage } from '../config/editModal/EditImage';
import classes from './Text.module.css';
-import { EditSubFormTableColumns } from './EditSubFormTableColumns';
+import { EditSubformTableColumns } from './EditSubformTableColumns';
import { type FormContainer } from '@altinn/ux-editor/types/FormContainer';
export const Text = () => {
@@ -79,8 +79,8 @@ export const Text = () => {
>
)}
- {form.type === ComponentType.SubForm && (
-
+ {form.type === ComponentType.Subform && (
+
)}
>
);
diff --git a/frontend/packages/shared/src/components/Expression/Expression.tsx b/frontend/packages/ux-editor/src/components/config/ExpressionContent/Expression/Expression.tsx
similarity index 83%
rename from frontend/packages/shared/src/components/Expression/Expression.tsx
rename to frontend/packages/ux-editor/src/components/config/ExpressionContent/Expression/Expression.tsx
index 54945d45dc0..4fa95ab2895 100644
--- a/frontend/packages/shared/src/components/Expression/Expression.tsx
+++ b/frontend/packages/ux-editor/src/components/config/ExpressionContent/Expression/Expression.tsx
@@ -1,7 +1,7 @@
import React from 'react';
import type { StudioExpressionProps } from '@studio/components';
import { StudioExpression } from '@studio/components';
-import { useExpressionTexts } from './useExpressionTexts';
+import { useExpressionTexts } from 'app-shared/hooks/useExpressionTexts';
export type ExpressionProps = Omit;
diff --git a/frontend/packages/shared/src/components/Expression/index.ts b/frontend/packages/ux-editor/src/components/config/ExpressionContent/Expression/index.ts
similarity index 100%
rename from frontend/packages/shared/src/components/Expression/index.ts
rename to frontend/packages/ux-editor/src/components/config/ExpressionContent/Expression/index.ts
diff --git a/frontend/packages/ux-editor/src/components/config/ExpressionContent/ExpressionContent.tsx b/frontend/packages/ux-editor/src/components/config/ExpressionContent/ExpressionContent.tsx
index 5c1e240ea14..4d0eef78fd8 100644
--- a/frontend/packages/ux-editor/src/components/config/ExpressionContent/ExpressionContent.tsx
+++ b/frontend/packages/ux-editor/src/components/config/ExpressionContent/ExpressionContent.tsx
@@ -8,7 +8,7 @@ import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmen
import { useDataModelMetadataQuery } from '../../../hooks/queries/useDataModelMetadataQuery';
import { Paragraph } from '@digdir/designsystemet-react';
import classes from './ExpressionContent.module.css';
-import { Expression as ExpressionWithTexts } from 'app-shared/components/Expression';
+import { Expression as ExpressionWithTexts } from './Expression';
import { useText, useAppContext } from '../../../hooks';
export interface ExpressionContentProps {
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 e97a4e206f8..eb817551ba3 100644
--- a/frontend/packages/ux-editor/src/components/config/FormComponentConfig.test.tsx
+++ b/frontend/packages/ux-editor/src/components/config/FormComponentConfig.test.tsx
@@ -62,7 +62,7 @@ describe('FormComponentConfig', () => {
id: 'subform-unit-test-id',
layoutSet: 'subform-unit-test-layout-set',
itemType: 'COMPONENT',
- type: ComponentType.SubForm,
+ type: ComponentType.Subform,
},
schema: {
properties: {
diff --git a/frontend/packages/ux-editor/src/containers/DesignView/AddItemModal/AddItemContent.module.css b/frontend/packages/ux-editor/src/containers/DesignView/AddItemModal/AddItemContent.module.css
new file mode 100644
index 00000000000..1b78dd45868
--- /dev/null
+++ b/frontend/packages/ux-editor/src/containers/DesignView/AddItemModal/AddItemContent.module.css
@@ -0,0 +1,39 @@
+.allComponentsWrapper {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ height: 100%;
+ flex: 3;
+ padding: 20px;
+ overflow-y: scroll;
+}
+
+.componentButton {
+ height: 50px;
+ width: 180px;
+ margin: 8px;
+}
+
+.componentCategory {
+ padding-top: 12px;
+}
+
+.componentHelpText {
+ width: 100%;
+}
+
+.componentsInfoWrapper {
+ flex: 1;
+ background-color: var(--fds-semantic-surface-info-subtle);
+ padding: 20px;
+ height: 600px;
+ position: sticky;
+}
+
+.root {
+ display: flex;
+ flex-direction: row;
+ overflow-y: hidden;
+ height: 100%;
+ /* min-width: 70vw; */
+}
diff --git a/frontend/packages/ux-editor/src/containers/DesignView/AddItemModal/AddItemContent.tsx b/frontend/packages/ux-editor/src/containers/DesignView/AddItemModal/AddItemContent.tsx
new file mode 100644
index 00000000000..def0d76a6f0
--- /dev/null
+++ b/frontend/packages/ux-editor/src/containers/DesignView/AddItemModal/AddItemContent.tsx
@@ -0,0 +1,60 @@
+import React from 'react';
+import type { ComponentType } from 'app-shared/types/ComponentType';
+import type { KeyValuePairs } from 'app-shared/types/KeyValuePairs';
+import type { IToolbarElement } from '../../../types/global';
+import classes from './AddItemContent.module.css';
+import { ItemCategory } from './ItemCategory';
+import type { AddedItem } from './types';
+import { ItemInfo } from './ItemInfo';
+import { useFormLayouts } from '../../../hooks';
+import { generateComponentId } from '../../../utils/generateId';
+import { StudioParagraph } from '@studio/components';
+
+export type AddItemContentProps = {
+ item: AddedItem | null;
+ setItem: (item: AddedItem | null) => void;
+ onAddItem: (addedItem: AddedItem) => void;
+ onCancel: () => void;
+ availableComponents: KeyValuePairs;
+};
+
+export const AddItemContent = ({
+ item,
+ setItem,
+ onAddItem,
+ onCancel,
+ availableComponents,
+}: AddItemContentProps) => {
+ const layouts = useFormLayouts();
+
+ return (
+
+
+
+ Klikk på en komponent for å se mer informasjon om den.
+
+ {Object.keys(availableComponents).map((key) => {
+ return (
+ generateComponentId(type, layouts)}
+ />
+ );
+ })}
+
+
+ generateComponentId(type, layouts)}
+ item={item}
+ setItem={setItem}
+ />
+
+
+ );
+};
diff --git a/frontend/packages/ux-editor/src/containers/DesignView/AddItemModal/AddItemModal.tsx b/frontend/packages/ux-editor/src/containers/DesignView/AddItemModal/AddItemModal.tsx
new file mode 100644
index 00000000000..489a663d582
--- /dev/null
+++ b/frontend/packages/ux-editor/src/containers/DesignView/AddItemModal/AddItemModal.tsx
@@ -0,0 +1,103 @@
+import React, { useCallback, useRef } from 'react';
+import {
+ addItemOfType,
+ getAvailableChildComponentsForContainer,
+ getItem,
+} from '../../../utils/formLayoutUtils';
+import { useAddItemToLayoutMutation } from '../../../hooks/mutations/useAddItemToLayoutMutation';
+import { useFormItemContext } from '../../FormItemContext';
+import { useAppContext } from '../../../hooks';
+import { useStudioEnvironmentParams } from 'app-shared/hooks/useStudioEnvironmentParams';
+import type { IInternalLayout } from '../../../types/global';
+import type { ComponentType } from 'app-shared/types/ComponentType';
+import { StudioButton, StudioModal } from '@studio/components';
+import type { AddedItem } from './types';
+import { BASE_CONTAINER_ID } from 'app-shared/constants';
+import { AddItemContent } from './AddItemContent';
+import { PlusCircleIcon } from '@studio/icons';
+import { usePreviewContext } from 'app-development/contexts/PreviewContext';
+
+export type AddItemProps = {
+ containerId: string;
+ layout: IInternalLayout;
+};
+
+export const AddItemModal = ({ containerId, layout }: AddItemProps) => {
+ const [selectedItem, setSelectedItem] = React.useState(null);
+
+ const { doReloadPreview } = usePreviewContext();
+ const handleCloseModal = () => {
+ setSelectedItem(null);
+ modalRef.current?.close();
+ };
+ const { handleEdit } = useFormItemContext();
+
+ const { org, app } = useStudioEnvironmentParams();
+ const { selectedFormLayoutSetName } = useAppContext();
+
+ const { mutate: addItemToLayout } = useAddItemToLayoutMutation(
+ org,
+ app,
+ selectedFormLayoutSetName,
+ );
+
+ const modalRef = useRef(null);
+
+ const addItem = (type: ComponentType, parentId: string, index: number, newId: string) => {
+ const updatedLayout = addItemOfType(layout, type, newId, parentId, index);
+
+ addItemToLayout(
+ { componentType: type, newId, parentId, index },
+ {
+ onSuccess: () => {
+ doReloadPreview();
+ },
+ },
+ );
+ handleEdit(getItem(updatedLayout, newId));
+ };
+
+ const onAddComponent = (addedItem: AddedItem) => {
+ addItem(
+ addedItem.componentType,
+ containerId,
+ layout.order[containerId].length,
+ addedItem.componentId,
+ );
+ handleCloseModal();
+ };
+
+ const handleOpenModal = useCallback(() => {
+ modalRef.current?.showModal();
+ }, []);
+
+ return (
+
+
+ }
+ onClick={handleOpenModal}
+ variant='tertiary'
+ fullWidth
+ >
+ Legg til komponent
+
+
+
+
+
+
+ );
+};
diff --git a/frontend/packages/ux-editor/src/containers/DesignView/AddItemModal/ItemCategory/ItemCategory.module.css b/frontend/packages/ux-editor/src/containers/DesignView/AddItemModal/ItemCategory/ItemCategory.module.css
new file mode 100644
index 00000000000..0685e31c04e
--- /dev/null
+++ b/frontend/packages/ux-editor/src/containers/DesignView/AddItemModal/ItemCategory/ItemCategory.module.css
@@ -0,0 +1,20 @@
+.componentButton {
+ margin: 8px;
+ justify-content: start;
+ width: 270px;
+}
+
+.componentsWrapper {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ flex-wrap: wrap;
+ justify-content: start;
+ align-items: start;
+}
+
+.itemCategory {
+ margin-bottom: 12px;
+ margin-right: 12px;
+ max-width: calc(50% - 12px);
+}
diff --git a/frontend/packages/ux-editor/src/containers/DesignView/AddItemModal/ItemCategory/ItemCategory.tsx b/frontend/packages/ux-editor/src/containers/DesignView/AddItemModal/ItemCategory/ItemCategory.tsx
new file mode 100644
index 00000000000..f2662d95c1c
--- /dev/null
+++ b/frontend/packages/ux-editor/src/containers/DesignView/AddItemModal/ItemCategory/ItemCategory.tsx
@@ -0,0 +1,72 @@
+import React from 'react';
+import { StudioButton, StudioCard, StudioHeading } from '@studio/components';
+import classes from './ItemCategory.module.css';
+import { useTranslation } from 'react-i18next';
+import type { IToolbarElement } from '../../../../types/global';
+import type { AddedItem } from '../types';
+import type { ComponentType } from 'app-shared/types/ComponentType';
+import { getComponentTitleByComponentType } from '../../../../utils/language';
+
+export type ItemCategoryProps = {
+ items: IToolbarElement[];
+ category: string;
+ selectedItemType: ComponentType;
+ setAddedItem(addedItem: AddedItem): void;
+ generateComponentId: (type: string) => string;
+};
+
+export const ItemCategory = ({
+ items,
+ category,
+ selectedItemType,
+ setAddedItem,
+ generateComponentId,
+}: ItemCategoryProps) => {
+ const { t } = useTranslation();
+
+ return (
+
+
+ {t(`ux_editor.component_category.${category}`)}
+
+
+ {items.map((item: IToolbarElement) => (
+ {
+ setAddedItem({
+ componentType: item.type,
+ componentId: generateComponentId(item.type),
+ });
+ }}
+ />
+ ))}
+
+
+ );
+};
+
+type ComponentButtonProps = {
+ tooltipContent: string;
+ selected: boolean;
+ icon: React.ComponentType;
+ onClick: () => void;
+};
+function ComponentButton({ tooltipContent, selected, icon, onClick }: ComponentButtonProps) {
+ return (
+
+ {tooltipContent}
+
+ );
+}
diff --git a/frontend/packages/ux-editor/src/containers/DesignView/AddItemModal/ItemCategory/index.ts b/frontend/packages/ux-editor/src/containers/DesignView/AddItemModal/ItemCategory/index.ts
new file mode 100644
index 00000000000..60d2c7c51f9
--- /dev/null
+++ b/frontend/packages/ux-editor/src/containers/DesignView/AddItemModal/ItemCategory/index.ts
@@ -0,0 +1 @@
+export { ItemCategory } from './ItemCategory';
diff --git a/frontend/packages/ux-editor/src/containers/DesignView/AddItemModal/ItemInfo/ItemInfo.module.css b/frontend/packages/ux-editor/src/containers/DesignView/AddItemModal/ItemInfo/ItemInfo.module.css
new file mode 100644
index 00000000000..9fc6bf7f4b7
--- /dev/null
+++ b/frontend/packages/ux-editor/src/containers/DesignView/AddItemModal/ItemInfo/ItemInfo.module.css
@@ -0,0 +1,38 @@
+.allComponentsWrapper {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ flex: 3;
+ padding: 20px;
+}
+
+.componentButton {
+ height: 50px;
+ width: 180px;
+ margin: 8px;
+}
+
+.componentCategory {
+ padding-top: 12px;
+}
+
+.componentHelpText {
+ width: 100%;
+}
+
+.componentsInfoWrapper {
+ flex: 1;
+ background-color: var(--fds-semantic-surface-info-subtle);
+ padding: 20px;
+}
+
+.componentsWrapper {
+ display: flex;
+ flex-direction: row;
+ height: 100%;
+ flex-wrap: wrap;
+}
+
+.root {
+ min-width: 360px;
+}
diff --git a/frontend/packages/ux-editor/src/containers/DesignView/AddItemModal/ItemInfo/ItemInfo.tsx b/frontend/packages/ux-editor/src/containers/DesignView/AddItemModal/ItemInfo/ItemInfo.tsx
new file mode 100644
index 00000000000..525282495d7
--- /dev/null
+++ b/frontend/packages/ux-editor/src/containers/DesignView/AddItemModal/ItemInfo/ItemInfo.tsx
@@ -0,0 +1,74 @@
+import React from 'react';
+import {
+ StudioHeading,
+ StudioIconTextfield,
+ StudioParagraph,
+ StudioRecommendedNextAction,
+} from '@studio/components';
+import {
+ getComponentHelperTextByComponentType,
+ getComponentTitleByComponentType,
+} from '../../../../utils/language';
+import type { AddedItem } from '../types';
+import { useTranslation } from 'react-i18next';
+import { PencilIcon } from '@studio/icons';
+import classes from './ItemInfo.module.css';
+
+export type ItemInfoProps = {
+ item: AddedItem | null;
+ onAddItem: (item: AddedItem) => void;
+ onCancel: () => void;
+ setItem: (item: AddedItem | null) => void;
+ generateComponentId: (type: string) => string;
+};
+
+export const ItemInfo = ({
+ item,
+ onAddItem,
+ onCancel,
+ setItem,
+ generateComponentId,
+}: ItemInfoProps) => {
+ const { t } = useTranslation();
+ return (
+
+
+ {!item && t('ux_editor.component_add_item.info_heading')}
+ {item && getComponentTitleByComponentType(item.componentType, t)}
+
+ {!item &&
{t('ux_editor.component_add_item.info_no_component_selected')}
}
+ {item && (
+
+
+ {getComponentHelperTextByComponentType(item.componentType, t)}
+
+
+ )}
+ {item && (
+
{
+ onAddItem(item);
+ setItem(null);
+ }}
+ onSkip={() => {
+ onCancel();
+ setItem(null);
+ }}
+ saveButtonText='Legg til'
+ skipButtonText='Avbryt'
+ title={`Legg til ${getComponentTitleByComponentType(item.componentType, t)}`}
+ description='Vi lager automatisk en unik ID for komponenten. Du kan endre den her til noe du selv ønsker, eller la den være som den er. Du kan også endre denne id-en senere.'
+ >
+ }
+ label={t('Komponent ID')}
+ value={item.componentId}
+ onChange={(event: any) => {
+ setItem({ ...item, componentId: event.target.value });
+ }}
+ />
+
+ )}
+
+ );
+};
diff --git a/frontend/packages/ux-editor/src/containers/DesignView/AddItemModal/ItemInfo/index.ts b/frontend/packages/ux-editor/src/containers/DesignView/AddItemModal/ItemInfo/index.ts
new file mode 100644
index 00000000000..4bfa8e07920
--- /dev/null
+++ b/frontend/packages/ux-editor/src/containers/DesignView/AddItemModal/ItemInfo/index.ts
@@ -0,0 +1 @@
+export { ItemInfo } from './ItemInfo';
diff --git a/frontend/packages/ux-editor/src/containers/DesignView/AddItemModal/README.md b/frontend/packages/ux-editor/src/containers/DesignView/AddItemModal/README.md
new file mode 100644
index 00000000000..f5dfd802abc
--- /dev/null
+++ b/frontend/packages/ux-editor/src/containers/DesignView/AddItemModal/README.md
@@ -0,0 +1,11 @@
+## POC for modal to add component - used for live user testing
+
+We implemented a POC of an alternative way of adding components. All files in this enclosing folder are part of the POC.
+Please note that not all components/code in this folder is implemented according to our development guidelines, on account
+of it being a POC. If we decide to use these files going forward after the user test, some refactoring and cleanup should be
+done.
+
+If we at some point after the user test decide not to go forward with the concept, or to implement it in a different way, we can
+delete this folder.
+
+The component `AddItemModal.tsx` is used in the file `FormLayout.tsx`.
diff --git a/frontend/packages/ux-editor/src/containers/DesignView/AddItemModal/index.ts b/frontend/packages/ux-editor/src/containers/DesignView/AddItemModal/index.ts
new file mode 100644
index 00000000000..f54ca4f5db0
--- /dev/null
+++ b/frontend/packages/ux-editor/src/containers/DesignView/AddItemModal/index.ts
@@ -0,0 +1 @@
+export { AddItemModal } from './AddItemModal';
diff --git a/frontend/packages/ux-editor/src/containers/DesignView/AddItemModal/types.ts b/frontend/packages/ux-editor/src/containers/DesignView/AddItemModal/types.ts
new file mode 100644
index 00000000000..217a75e3db5
--- /dev/null
+++ b/frontend/packages/ux-editor/src/containers/DesignView/AddItemModal/types.ts
@@ -0,0 +1,6 @@
+import type { ComponentType } from 'app-shared/types/ComponentType';
+
+export type AddedItem = {
+ componentType: ComponentType;
+ componentId: string;
+};
diff --git a/frontend/packages/ux-editor/src/containers/DesignView/FormLayout.tsx b/frontend/packages/ux-editor/src/containers/DesignView/FormLayout.tsx
index eede7362302..95ad44c25e6 100644
--- a/frontend/packages/ux-editor/src/containers/DesignView/FormLayout.tsx
+++ b/frontend/packages/ux-editor/src/containers/DesignView/FormLayout.tsx
@@ -5,6 +5,9 @@ import { hasMultiPageGroup } from '../../utils/formLayoutUtils';
import { useTranslation } from 'react-i18next';
import { Alert, Paragraph } from '@digdir/designsystemet-react';
import { FormLayoutWarning } from './FormLayoutWarning';
+import { BASE_CONTAINER_ID } from 'app-shared/constants';
+import { AddItemModal } from './AddItemModal/AddItemModal';
+import { shouldDisplayFeature } from 'app-shared/utils/featureToggleUtils';
export interface FormLayoutProps {
layout: IInternalLayout;
@@ -20,6 +23,10 @@ export const FormLayout = ({ layout, isInvalid, duplicateComponents }: FormLayou
<>
{hasMultiPageGroup(layout) && }
+ {/** The following check and component are added as part of a live user test behind a feature flag. Can be removed if we decide not to use after user test. */}
+ {shouldDisplayFeature('addComponentModal') && (
+
+ )}
>
);
};
diff --git a/frontend/packages/ux-editor/src/containers/FormDesigner.tsx b/frontend/packages/ux-editor/src/containers/FormDesigner.tsx
index e1cbebd5b3c..28d7cd8e269 100644
--- a/frontend/packages/ux-editor/src/containers/FormDesigner.tsx
+++ b/frontend/packages/ux-editor/src/containers/FormDesigner.tsx
@@ -31,6 +31,7 @@ import { useAddItemToLayoutMutation } from '../hooks/mutations/useAddItemToLayou
import { useFormLayoutMutation } from '../hooks/mutations/useFormLayoutMutation';
import { Preview } from '../components/Preview';
import { DragAndDropTree } from 'app-shared/components/DragAndDropTree';
+import { shouldDisplayFeature } from 'app-shared/utils/featureToggleUtils';
export const FormDesigner = (): JSX.Element => {
const { org, app } = useStudioEnvironmentParams();
@@ -159,19 +160,27 @@ export const FormDesigner = (): JSX.Element => {
orientation='horizontal'
localStorageContext={`form-designer-main:${user.id}:${org}`}
>
+ {/**
+ * The following check is done for a live user test behind feature flag. It can be removed if this is not something
+ * that is going to be used in the future.
+ */}
+ {!shouldDisplayFeature('addComponentModal') && (
+
+ setElementsCollapsed(!elementsCollapsed)}
+ />
+
+ )}
- setElementsCollapsed(!elementsCollapsed)}
- />
-
-
{
component: formItemConfigs[ComponentType.Payment],
},
{
- component: formItemConfigs[ComponentType.SubForm],
+ component: formItemConfigs[ComponentType.Subform],
},
])(
'should return false for unsupported subform component: $component.name',
diff --git a/frontend/packages/ux-editor/src/data/FilterUtils.ts b/frontend/packages/ux-editor/src/data/FilterUtils.ts
index 1bb49048431..e68022c4184 100644
--- a/frontend/packages/ux-editor/src/data/FilterUtils.ts
+++ b/frontend/packages/ux-editor/src/data/FilterUtils.ts
@@ -17,7 +17,7 @@ export class FilterUtils {
formItemConfigs[ComponentType.FileUploadWithTag],
formItemConfigs[ComponentType.InstantiationButton],
formItemConfigs[ComponentType.Payment],
- formItemConfigs[ComponentType.SubForm],
+ formItemConfigs[ComponentType.Subform],
];
return !unsupportedSubformComponents.includes(component);
};
diff --git a/frontend/packages/ux-editor/src/data/formItemConfig.test.ts b/frontend/packages/ux-editor/src/data/formItemConfig.test.ts
index cf71a7b3f9b..cc3481cc92a 100644
--- a/frontend/packages/ux-editor/src/data/formItemConfig.test.ts
+++ b/frontend/packages/ux-editor/src/data/formItemConfig.test.ts
@@ -14,9 +14,9 @@ describe('formItemConfig', () => {
confOnScreenComponents,
];
const allAvailableComponents = allAvailableLists.flat();
- const excludedComponents = [ComponentType.Payment, ComponentType.SubForm, ComponentType.Summary2];
+ const excludedComponents = [ComponentType.Payment, ComponentType.Subform, ComponentType.Summary2];
- /** Test that all components, except Payment, SubForm and Summary2 (since behind featureFlag), are available in one of the visible lists */
+ /** Test that all components, except Payment, Subform and Summary2 (since behind featureFlag), are available in one of the visible lists */
it.each(
Object.values(ComponentType).filter(
(componentType) => !excludedComponents.includes(componentType),
@@ -29,8 +29,8 @@ describe('formItemConfig', () => {
expect(allAvailableComponents.map(({ name }) => name)).not.toContain(ComponentType.Payment);
});
- test('that subForm component is not available in the visible lists', () => {
- expect(allAvailableComponents.map(({ name }) => name)).not.toContain(ComponentType.SubForm);
+ test('that subform component is not available in the visible lists', () => {
+ expect(allAvailableComponents.map(({ name }) => name)).not.toContain(ComponentType.Subform);
});
test('that Summary2 component is not available in the visible lists', () => {
diff --git a/frontend/packages/ux-editor/src/data/formItemConfig.ts b/frontend/packages/ux-editor/src/data/formItemConfig.ts
index 6950422ef02..124a85b58d3 100644
--- a/frontend/packages/ux-editor/src/data/formItemConfig.ts
+++ b/frontend/packages/ux-editor/src/data/formItemConfig.ts
@@ -37,6 +37,7 @@ import {
import type { ContainerComponentType } from '../types/ContainerComponent';
import { LayoutItemType } from '../types/global';
import type { ComponentSpecificConfig } from 'app-shared/types/ComponentSpecificConfig';
+import type { KeyValuePairs } from 'app-shared/types/KeyValuePairs';
import { shouldDisplayFeature } from 'app-shared/utils/featureToggleUtils';
import { FilterUtils } from './FilterUtils';
@@ -457,11 +458,11 @@ export const formItemConfigs: FormItemConfigs = {
icon: RepeatingGroupIcon,
validChildTypes: Object.values(ComponentType),
},
- [ComponentType.SubForm]: {
- name: ComponentType.SubForm,
+ [ComponentType.Subform]: {
+ name: ComponentType.Subform,
itemType: LayoutItemType.Component,
defaultProperties: {},
- propertyPath: 'definitions/subForm',
+ propertyPath: 'definitions/subform',
icon: ClipboardIcon,
},
[ComponentType.Summary]: {
@@ -513,7 +514,7 @@ export const advancedItems: FormItemConfigs[ComponentType][] = [
formItemConfigs[ComponentType.Custom],
formItemConfigs[ComponentType.RepeatingGroup],
formItemConfigs[ComponentType.PaymentDetails],
- shouldDisplayFeature('subform') && formItemConfigs[ComponentType.SubForm],
+ shouldDisplayFeature('subform') && formItemConfigs[ComponentType.Subform],
].filter(FilterUtils.filterOutDisabledFeatureItems);
export const schemaComponents: FormItemConfigs[ComponentType][] = [
@@ -561,6 +562,56 @@ export const paymentLayoutComponents: FormItemConfigs[ComponentType][] = [
...confOnScreenComponents,
];
+export type ComponentCategory =
+ | 'form'
+ | 'select'
+ | 'button'
+ | 'text'
+ | 'info'
+ | 'container'
+ | 'attachment'
+ | 'advanced';
+
+export const allComponents: KeyValuePairs = {
+ form: [ComponentType.Input, ComponentType.TextArea, ComponentType.Datepicker],
+ select: [
+ ComponentType.Checkboxes,
+ ComponentType.RadioButtons,
+ ComponentType.Dropdown,
+ ComponentType.MultipleSelect,
+ ComponentType.Likert,
+ ],
+ text: [ComponentType.Header, ComponentType.Paragraph, ComponentType.Panel, ComponentType.Alert],
+ info: [
+ ComponentType.InstanceInformation,
+ ComponentType.Image,
+ ComponentType.Link,
+ ComponentType.IFrame,
+ ComponentType.Summary,
+ ],
+ button: [
+ ComponentType.Button,
+ ComponentType.CustomButton,
+ ComponentType.NavigationButtons,
+ ComponentType.PrintButton,
+ ComponentType.InstantiationButton,
+ ComponentType.ActionButton,
+ ],
+ attachment: [
+ ComponentType.AttachmentList,
+ ComponentType.FileUpload,
+ ComponentType.FileUploadWithTag,
+ ],
+ container: [
+ ComponentType.Group,
+ ComponentType.Grid,
+ ComponentType.Accordion,
+ ComponentType.AccordionGroup,
+ ComponentType.List,
+ ComponentType.RepeatingGroup,
+ ],
+ advanced: [ComponentType.Address, ComponentType.Map, ComponentType.Custom],
+};
export const subformLayoutComponents: Array = [
...schemaComponents,
...textComponents,
diff --git a/frontend/packages/ux-editor/src/hooks/useValidateComponent.ts b/frontend/packages/ux-editor/src/hooks/useValidateComponent.ts
index 2951134c6f3..7846a18be71 100644
--- a/frontend/packages/ux-editor/src/hooks/useValidateComponent.ts
+++ b/frontend/packages/ux-editor/src/hooks/useValidateComponent.ts
@@ -1,4 +1,4 @@
-import { areItemsUnique } from 'app-shared/utils/arrayUtils';
+import { ArrayUtils } from '@studio/pure-functions';
import { ComponentType } from 'app-shared/types/ComponentType';
import type {
FormCheckboxesComponent,
@@ -36,7 +36,7 @@ const validateOptionGroup = (
isValid: false,
error: ErrorCode.NoOptions,
};
- } else if (!areItemsUnique(component.options.map((option) => option.value))) {
+ } else if (!ArrayUtils.areItemsUnique(component.options.map((option) => option.value))) {
return {
isValid: false,
error: ErrorCode.DuplicateValues,
diff --git a/frontend/packages/ux-editor/src/testing/componentMocks.ts b/frontend/packages/ux-editor/src/testing/componentMocks.ts
index f997e3cbf5b..886371032d6 100644
--- a/frontend/packages/ux-editor/src/testing/componentMocks.ts
+++ b/frontend/packages/ux-editor/src/testing/componentMocks.ts
@@ -83,8 +83,8 @@ const textareaComponent: FormComponent = {
...commonProps(ComponentType.TextArea),
dataModelBindings: { simpleBinding: '' },
};
-const subFormComponent: FormComponent = {
- ...commonProps(ComponentType.SubForm),
+const subformComponent: FormComponent = {
+ ...commonProps(ComponentType.Subform),
tableColumns: [
{
headerContent: 'header content',
@@ -208,7 +208,7 @@ export const componentMocks = {
[ComponentType.Paragraph]: paragraphComponent,
[ComponentType.RadioButtons]: radiosComponent,
[ComponentType.RepeatingGroup]: repeatingGroupContainer,
- [ComponentType.SubForm]: subFormComponent,
+ [ComponentType.Subform]: subformComponent,
[ComponentType.TextArea]: textareaComponent,
[ComponentType.Custom]: thirdPartyComponent,
[ComponentType.Summary2]: summary2Component,
diff --git a/frontend/packages/ux-editor/src/testing/componentSchemaMocks.ts b/frontend/packages/ux-editor/src/testing/componentSchemaMocks.ts
index 23e11d345a3..3fd45f481e1 100644
--- a/frontend/packages/ux-editor/src/testing/componentSchemaMocks.ts
+++ b/frontend/packages/ux-editor/src/testing/componentSchemaMocks.ts
@@ -36,7 +36,7 @@ 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';
import RepeatingGroupSchema from './schemas/json/component/RepeatingGroup.schema.v1.json';
-import SubFormSchema from './schemas/json/component/Subform.schema.v1.json';
+import SubformSchema from './schemas/json/component/Subform.schema.v1.json';
import SummarySchema from './schemas/json/component/Summary.schema.v1.json';
import Summary2Schema from './schemas/json/component/Summary2.schema.v1.json';
import TextAreaSchema from './schemas/json/component/TextArea.schema.v1.json';
@@ -82,7 +82,7 @@ export const componentSchemaMocks: Record = {
[ComponentType.PrintButton]: PrintButtonSchema,
[ComponentType.RadioButtons]: RadioButtonsSchema,
[ComponentType.RepeatingGroup]: RepeatingGroupSchema,
- [ComponentType.SubForm]: SubFormSchema,
+ [ComponentType.Subform]: SubformSchema,
[ComponentType.Summary]: SummarySchema,
[ComponentType.Summary2]: Summary2Schema,
[ComponentType.TextArea]: TextAreaSchema,
diff --git a/frontend/packages/ux-editor/src/testing/layoutSetsMock.ts b/frontend/packages/ux-editor/src/testing/layoutSetsMock.ts
index f6476fce917..03176a71552 100644
--- a/frontend/packages/ux-editor/src/testing/layoutSetsMock.ts
+++ b/frontend/packages/ux-editor/src/testing/layoutSetsMock.ts
@@ -3,7 +3,7 @@ import type { LayoutSets } from 'app-shared/types/api/LayoutSetsResponse';
export const dataModelNameMock = 'test-data-model';
export const layoutSet1NameMock = 'test-layout-set';
export const layoutSet2NameMock = 'test-layout-set-2';
-export const layoutSet3SubFormNameMock = 'test-layout-set-3';
+export const layoutSet3SubformNameMock = 'test-layout-set-3';
export const layoutSetsMock: LayoutSets = {
sets: [
@@ -18,7 +18,7 @@ export const layoutSetsMock: LayoutSets = {
tasks: ['Task_2'],
},
{
- id: layoutSet3SubFormNameMock,
+ id: layoutSet3SubformNameMock,
dataType: 'data-model-3',
type: 'subform',
},
diff --git a/frontend/packages/ux-editor/src/testing/schemas/json/component/component-list.json b/frontend/packages/ux-editor/src/testing/schemas/json/component/component-list.json
index e0f81003320..28bccd341ea 100644
--- a/frontend/packages/ux-editor/src/testing/schemas/json/component/component-list.json
+++ b/frontend/packages/ux-editor/src/testing/schemas/json/component/component-list.json
@@ -30,7 +30,7 @@
"Paragraph",
"PrintButton",
"RadioButtons",
- "SubForm",
+ "Subform",
"Summary",
"TextArea"
]
diff --git a/frontend/packages/ux-editor/src/utils/formLayoutUtils.ts b/frontend/packages/ux-editor/src/utils/formLayoutUtils.ts
index 22635afe259..024494f261d 100644
--- a/frontend/packages/ux-editor/src/utils/formLayoutUtils.ts
+++ b/frontend/packages/ux-editor/src/utils/formLayoutUtils.ts
@@ -5,19 +5,19 @@ import type {
IToolbarElement,
} from '../types/global';
import { BASE_CONTAINER_ID, MAX_NESTED_GROUP_LEVEL } from 'app-shared/constants';
-import { insertArrayElementAtPos, areItemsUnique } from 'app-shared/utils/arrayUtils';
import { ArrayUtils, ObjectUtils } from '@studio/pure-functions';
import { ComponentType, type CustomComponentType } from 'app-shared/types/ComponentType';
import type { FormComponent } from '../types/FormComponent';
import { generateFormItem } from './component';
import type { FormItemConfigs } from '../data/formItemConfig';
-import { formItemConfigs } from '../data/formItemConfig';
+import { formItemConfigs, allComponents } from '../data/formItemConfig';
import type { FormContainer } from '../types/FormContainer';
import type { FormItem } from '../types/FormItem';
import * as formItemUtils from './formItemUtils';
import type { ContainerComponentType } from '../types/ContainerComponent';
import { flattenObjectValues } from 'app-shared/utils/objectUtils';
import type { FormLayoutPage } from '../types/FormLayoutPage';
+import type { KeyValuePairs } from 'app-shared/types/KeyValuePairs';
export const mapComponentToToolbarElement = (
c: FormItemConfigs[T],
@@ -291,7 +291,7 @@ export const moveLayoutItem = (
newLayout.order[oldContainerId],
id,
);
- newLayout.order[newContainerId] = insertArrayElementAtPos(
+ newLayout.order[newContainerId] = ArrayUtils.insertArrayElementAtPos(
newLayout.order[newContainerId],
id,
newPosition,
@@ -434,7 +434,7 @@ export const idExistsInLayout = (id: string, layout: IInternalLayout): boolean =
export const duplicatedIdsExistsInLayout = (layout: IInternalLayout): boolean => {
if (!layout?.order) return false;
const idsInLayout = flattenObjectValues(layout.order);
- return !areItemsUnique(idsInLayout);
+ return !ArrayUtils.areItemsUnique(idsInLayout);
};
/**
@@ -491,6 +491,26 @@ export const getDuplicatedIds = (layout: IInternalLayout): string[] => {
export const getAllFormItemIds = (layout: IInternalLayout): string[] =>
flattenObjectValues(layout.order);
+/**
+ * Gets all available componenent types to add for a given container
+ * @param layout
+ * @param containerId
+ * @returns
+ */
+export const getAvailableChildComponentsForContainer = (
+ layout: IInternalLayout,
+ containerId: string,
+): KeyValuePairs => {
+ if (containerId !== BASE_CONTAINER_ID) return {};
+ const allComponentLists: KeyValuePairs = {};
+ Object.keys(allComponents).forEach((key) => {
+ allComponentLists[key] = allComponents[key].map((element: ComponentType) =>
+ mapComponentToToolbarElement(formItemConfigs[element]),
+ );
+ });
+ return allComponentLists;
+};
+
/**
* Get all components in the given layout
* @param layout The layout
diff --git a/frontend/testing/playwright/tests/process-editor/process-editor.spec.ts b/frontend/testing/playwright/tests/process-editor/process-editor.spec.ts
index 7e141402a01..ce653356886 100644
--- a/frontend/testing/playwright/tests/process-editor/process-editor.spec.ts
+++ b/frontend/testing/playwright/tests/process-editor/process-editor.spec.ts
@@ -215,6 +215,7 @@ const addNewSigningTaskToProcessEditor = async (page: Page): Promise =>
extraMovingDistanceX,
extraMovingDistanceY,
);
+ await processEditorPage.skipRecommendedTask();
await processEditorPage.waitForTaskToBeVisibleInConfigPanel(signingTask);
return signingTask;
diff --git a/frontend/testing/testids.js b/frontend/testing/testids.js
index 978d7565865..46621c9da3a 100644
--- a/frontend/testing/testids.js
+++ b/frontend/testing/testids.js
@@ -3,7 +3,6 @@ export const dataModellingContainerId = 'data-modelling-container';
export const deleteButtonId = (key) => `delete-button-${key}`;
export const draggableToolbarItemId = 'draggableToolbarItem';
export const droppableListId = 'droppableList';
-export const fileSelectorInputId = 'file-selector-input';
export const orgMenuItemId = (orgUserName) =>
orgUserName ? `menu-org-${orgUserName}` : 'menu-org-no-org-user-name';
export const resetRepoContainerId = 'reset-repo-container';
diff --git a/src/Altinn.Platform/Altinn.Platform.PDF/pom.xml b/src/Altinn.Platform/Altinn.Platform.PDF/pom.xml
index a4750b3d329..53eb93205fe 100644
--- a/src/Altinn.Platform/Altinn.Platform.PDF/pom.xml
+++ b/src/Altinn.Platform/Altinn.Platform.PDF/pom.xml
@@ -32,7 +32,7 @@
org.springframework.boot
spring-boot-starter-parent
- 3.3.4
+ 3.3.5
diff --git a/yarn.lock b/yarn.lock
index cb5956b6b22..44a03132739 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -128,7 +128,6 @@ __metadata:
react-dnd: "npm:16.0.1"
react-dnd-html5-backend: "npm:16.0.1"
react-dom: "npm:18.3.1"
- react-modal: "npm:3.16.1"
react-redux: "npm:9.1.2"
redux: "npm:5.0.1"
redux-mock-store: "npm:1.5.4"
@@ -150,7 +149,6 @@ __metadata:
react-dnd: "npm:16.0.1"
react-dnd-html5-backend: "npm:16.0.1"
react-dom: "npm:18.3.1"
- react-modal: "npm:3.16.1"
typescript: "npm:5.6.2"
uuid: "npm:10.0.0"
peerDependencies:
@@ -4368,7 +4366,7 @@ __metadata:
languageName: unknown
linkType: soft
-"@studio/content-library@workspace:frontend/libs/studio-content-library":
+"@studio/content-library@workspace:^, @studio/content-library@workspace:frontend/libs/studio-content-library":
version: 0.0.0-use.local
resolution: "@studio/content-library@workspace:frontend/libs/studio-content-library"
dependencies:
@@ -4392,6 +4390,8 @@ __metadata:
ts-jest: "npm:^29.1.1"
typescript: "npm:5.6.2"
uuid: "npm:10.0.0"
+ peerDependencies:
+ react-router-dom: ">=6.0.0"
languageName: unknown
linkType: soft
@@ -6562,6 +6562,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "app-development@workspace:frontend/app-development"
dependencies:
+ "@studio/content-library": "workspace:^"
"@studio/hooks": "workspace:^"
"@studio/icons": "workspace:^"
"@studio/pure-functions": "workspace:^"
@@ -10229,13 +10230,6 @@ __metadata:
languageName: node
linkType: hard
-"exenv@npm:^1.2.0":
- version: 1.2.2
- resolution: "exenv@npm:1.2.2"
- checksum: 10/6840185e421394bcb143debb866d31d19c3e4a4bca87d2f319d68d61afff353b3c678f2eb389e3b98ab9aecbec19f6bebbdc4193984378af0a3366c498a7efc8
- languageName: node
- linkType: hard
-
"exit@npm:^0.1.2":
version: 0.1.2
resolution: "exit@npm:0.1.2"
@@ -15912,28 +15906,6 @@ __metadata:
languageName: node
linkType: hard
-"react-lifecycles-compat@npm:^3.0.0":
- version: 3.0.4
- resolution: "react-lifecycles-compat@npm:3.0.4"
- checksum: 10/c66b9c98c15cd6b0d0a4402df5f665e8cc7562fb7033c34508865bea51fd7b623f7139b5b7e708515d3cd665f264a6a9403e1fa7e6d61a05759066f5e9f07783
- languageName: node
- linkType: hard
-
-"react-modal@npm:3.16.1":
- version: 3.16.1
- resolution: "react-modal@npm:3.16.1"
- dependencies:
- exenv: "npm:^1.2.0"
- prop-types: "npm:^15.7.2"
- react-lifecycles-compat: "npm:^3.0.0"
- warning: "npm:^4.0.3"
- peerDependencies:
- react: ^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18
- react-dom: ^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18
- checksum: 10/79787ed2754f65168fccefcef50b509fa1cbc2b44907f92dcfd78ea6f9702668c70604f192a4bb45badb664902fb100179d6d191e478310be94e656271963905
- languageName: node
- linkType: hard
-
"react-number-format@npm:5.2.2":
version: 5.2.2
resolution: "react-number-format@npm:5.2.2"
@@ -18833,15 +18805,6 @@ __metadata:
languageName: node
linkType: hard
-"warning@npm:^4.0.3":
- version: 4.0.3
- resolution: "warning@npm:4.0.3"
- dependencies:
- loose-envify: "npm:^1.0.0"
- checksum: 10/e7842aff036e2e07ce7a6cc3225e707775b969fe3d0577ad64bd24660e3a9ce3017f0b8c22a136566dcd3a151f37b8ed1ccee103b3bd82bd8a571bf80b247bc4
- languageName: node
- linkType: hard
-
"watchpack@npm:^2.4.1":
version: 2.4.1
resolution: "watchpack@npm:2.4.1"