diff --git a/frontend/jest.config.js b/frontend/jest.config.js index 5ab4e7920e3..b39fda1700d 100644 --- a/frontend/jest.config.js +++ b/frontend/jest.config.js @@ -32,7 +32,7 @@ const config = { }, transformIgnorePatterns: [ `node_modules(\\\\|/)(?!${packagesToTransform})`, - 'frontend/packages/ux-editor/src/testing/schemas/', + '\\.schema\\.v1\\.json$', ], reporters: ['default', 'jest-junit'], moduleNameMapper: { diff --git a/frontend/packages/shared/src/components/FormField/FormField.tsx b/frontend/packages/shared/src/components/FormField/FormField.tsx index fe54e4e9b2d..fa054e04a6e 100644 --- a/frontend/packages/shared/src/components/FormField/FormField.tsx +++ b/frontend/packages/shared/src/components/FormField/FormField.tsx @@ -1,11 +1,8 @@ import React, { useCallback, useEffect, useState } from 'react'; import { ErrorMessage, HelpText } from '@digdir/design-system-react'; import classes from './FormField.module.css'; -import { useText } from '../../../../ux-editor/src/hooks'; -import { - validateProperty, - isPropertyRequired, -} from '../../../../ux-editor/src/utils/formValidationUtils'; +import { useTranslation } from 'react-i18next'; +import { validateProperty, isPropertyRequired } from '../../utils/formValidationUtils'; import type { TranslationKey } from 'language/type'; import type { JsonSchema } from 'app-shared/types/JsonSchema'; @@ -53,7 +50,7 @@ export const FormField = ({ customValidationMessages, renderField, }: FormFieldProps): JSX.Element => { - const t = useText(); + const { t } = useTranslation(); const [propertyId, setPropertyId] = useState( schema && propertyPath ? `${schema.$id}#/${propertyPath}` : null, diff --git a/frontend/packages/shared/src/components/InputActionWrapper/InputActionWrapper.tsx b/frontend/packages/shared/src/components/InputActionWrapper/InputActionWrapper.tsx index 6365d7bea02..9d69515840c 100644 --- a/frontend/packages/shared/src/components/InputActionWrapper/InputActionWrapper.tsx +++ b/frontend/packages/shared/src/components/InputActionWrapper/InputActionWrapper.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { CheckmarkIcon, TrashIcon, PencilWritingIcon } from '@studio/icons'; -import { useText } from '../../../../ux-editor/src/hooks'; +import { useTranslation } from 'react-i18next'; import type { ButtonProps } from '@digdir/design-system-react'; import classes from './InputActionWrapper.module.css'; import cn from 'classnames'; @@ -37,7 +37,7 @@ export const InputActionWrapper = ({ onSaveClick, ...rest }: InputActionWrapperProps): JSX.Element => { - const t = useText(); + const { t } = useTranslation(); const defaultActions = actionGroupMap[mode || 'standBy']; const [actions, setActions] = useState(defaultActions); diff --git a/frontend/packages/shared/src/hooks/mutations/useUpsertTextResourcesMutation.test.ts b/frontend/packages/shared/src/hooks/mutations/useUpsertTextResourcesMutation.test.tsx similarity index 59% rename from frontend/packages/shared/src/hooks/mutations/useUpsertTextResourcesMutation.test.ts rename to frontend/packages/shared/src/hooks/mutations/useUpsertTextResourcesMutation.test.tsx index e4af08ed311..433875c21c1 100644 --- a/frontend/packages/shared/src/hooks/mutations/useUpsertTextResourcesMutation.test.ts +++ b/frontend/packages/shared/src/hooks/mutations/useUpsertTextResourcesMutation.test.tsx @@ -1,10 +1,11 @@ import { queriesMock } from 'app-shared/mocks/queriesMock'; -import { renderHookWithMockStore } from '../../../../ux-editor/src/testing/mocks'; -import { waitFor } from '@testing-library/react'; -import { useTextResourcesQuery } from '../../../../../app-development/hooks/queries'; +import { renderHook } from '@testing-library/react'; import type { UpsertTextResourcesMutationArgs } from './useUpsertTextResourcesMutation'; import { useUpsertTextResourcesMutation } from './useUpsertTextResourcesMutation'; import type { ITextResource } from 'app-shared/types/global'; +import { createQueryClientMock } from '../../mocks/queryClientMock'; +import React from 'react'; +import { ServicesContextProvider } from '../../contexts/ServicesContext'; // Test data: const org = 'org'; @@ -17,7 +18,7 @@ const args: UpsertTextResourcesMutationArgs = { language, textResources }; describe('useUpsertTextResourcesMutation', () => { test('Calls upsertTextResources with correct parameters', async () => { - const { result: upsertTextResources } = await renderUpsertTextResourcesMutation(); + const { result: upsertTextResources } = renderUpsertTextResourcesMutation(); await upsertTextResources.current.mutateAsync(args); expect(queriesMock.upsertTextResources).toHaveBeenCalledTimes(1); expect(queriesMock.upsertTextResources).toHaveBeenCalledWith(org, app, language, { @@ -26,10 +27,13 @@ describe('useUpsertTextResourcesMutation', () => { }); }); -const renderUpsertTextResourcesMutation = async () => { - const { result: texts } = renderHookWithMockStore()(() => - useTextResourcesQuery(org, app), - ).renderHookResult; - await waitFor(() => expect(texts.current.isSuccess).toBe(true)); - return renderHookWithMockStore()(() => useUpsertTextResourcesMutation(org, app)).renderHookResult; +const renderUpsertTextResourcesMutation = () => { + const client = createQueryClientMock(); + return renderHook(() => useUpsertTextResourcesMutation(org, app), { + wrapper: ({ children }) => ( + + {children} + + ), + }); }; diff --git a/frontend/packages/shared/src/hooks/queries/useTextResourcesQuery.test.ts b/frontend/packages/shared/src/hooks/queries/useTextResourcesQuery.test.ts deleted file mode 100644 index 1f75ede4724..00000000000 --- a/frontend/packages/shared/src/hooks/queries/useTextResourcesQuery.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { queriesMock } from 'app-shared/mocks/queriesMock'; -import { - renderHookWithMockStore, - textLanguagesMock, -} from '../../../../ux-editor/src/testing/mocks'; -import { useTextResourcesQuery } from './useTextResourcesQuery'; -import { waitFor } from '@testing-library/react'; - -// Test data: -const org = 'org'; -const app = 'app'; - -describe('useTextResourcesQuery', () => { - it('Calls getTextResources for each language', async () => { - const getTextLanguages = jest.fn().mockImplementation(() => Promise.resolve(textLanguagesMock)); - const { result: resourcesResult } = renderHookWithMockStore( - {}, - { - getTextLanguages, - }, - )(() => useTextResourcesQuery(org, app)).renderHookResult; - await waitFor(() => expect(resourcesResult.current.isSuccess).toBe(true)); - expect(getTextLanguages).toHaveBeenCalledTimes(1); - expect(getTextLanguages).toHaveBeenCalledWith(org, app); - expect(queriesMock.getTextResources).toHaveBeenCalledTimes(textLanguagesMock.length); - textLanguagesMock.forEach((language) => { - expect(queriesMock.getTextResources).toHaveBeenCalledWith(org, app, language); - }); - }); -}); diff --git a/frontend/packages/shared/src/hooks/queries/useTextResourcesQuery.test.tsx b/frontend/packages/shared/src/hooks/queries/useTextResourcesQuery.test.tsx new file mode 100644 index 00000000000..db798da2e59 --- /dev/null +++ b/frontend/packages/shared/src/hooks/queries/useTextResourcesQuery.test.tsx @@ -0,0 +1,36 @@ +import { queriesMock } from '../../mocks/queriesMock'; +import { useTextResourcesQuery } from './useTextResourcesQuery'; +import { renderHook, waitFor } from '@testing-library/react'; +import React from 'react'; +import { ServicesContextProvider } from '../../contexts/ServicesContext'; +import { createQueryClientMock } from '../../mocks/queryClientMock'; + +// Test data: +const org = 'org'; +const app = 'app'; +const languagesMock = ['nb', 'nn', 'en']; + +describe('useTextResourcesQuery', () => { + it('Calls getTextResources for each language', async () => { + const getTextLanguages = jest.fn().mockImplementation(() => Promise.resolve(languagesMock)); + const client = createQueryClientMock(); + const { result: resourcesResult } = renderHook(() => useTextResourcesQuery(org, app), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + await waitFor(() => expect(resourcesResult.current.isSuccess).toBe(true)); + expect(getTextLanguages).toHaveBeenCalledTimes(1); + expect(getTextLanguages).toHaveBeenCalledWith(org, app); + expect(queriesMock.getTextResources).toHaveBeenCalledTimes(languagesMock.length); + languagesMock.forEach((language) => { + expect(queriesMock.getTextResources).toHaveBeenCalledWith(org, app, language); + }); + }); +}); diff --git a/frontend/packages/ux-editor/src/utils/formValidationUtils.test.tsx b/frontend/packages/shared/src/utils/formValidationUtils/formValidationUtils.test.tsx similarity index 88% rename from frontend/packages/ux-editor/src/utils/formValidationUtils.test.tsx rename to frontend/packages/shared/src/utils/formValidationUtils/formValidationUtils.test.tsx index edc9b628b42..f933f603896 100644 --- a/frontend/packages/ux-editor/src/utils/formValidationUtils.test.tsx +++ b/frontend/packages/shared/src/utils/formValidationUtils/formValidationUtils.test.tsx @@ -6,11 +6,11 @@ import { validate, validateProperty, } from './formValidationUtils'; -import expressionSchema from '../testing/schemas/json/layout/expression.schema.v1.json'; -import numberFormatSchema from '../testing/schemas/json/layout/number-format.schema.v1.json'; -import layoutSchema from '../testing/schemas/json/layout/layout.schema.v1.json'; -import inputSchema from '../testing/schemas/json/component/Input.schema.v1.json'; -import commonDefsSchema from '../testing/schemas/json/component/common-defs.schema.v1.json'; +import expressionSchema from './test-data/expression.schema.v1.json'; +import numberFormatSchema from './test-data/number-format.schema.v1.json'; +import layoutSchema from './test-data/layout.schema.v1.json'; +import inputSchema from './test-data/Input.schema.v1.json'; +import commonDefsSchema from './test-data/common-defs.schema.v1.json'; describe('formValidationUtils', () => { beforeAll(() => { diff --git a/frontend/packages/ux-editor/src/utils/formValidationUtils.ts b/frontend/packages/shared/src/utils/formValidationUtils/formValidationUtils.ts similarity index 100% rename from frontend/packages/ux-editor/src/utils/formValidationUtils.ts rename to frontend/packages/shared/src/utils/formValidationUtils/formValidationUtils.ts diff --git a/frontend/packages/shared/src/utils/formValidationUtils/index.ts b/frontend/packages/shared/src/utils/formValidationUtils/index.ts new file mode 100644 index 00000000000..adca53fc1cb --- /dev/null +++ b/frontend/packages/shared/src/utils/formValidationUtils/index.ts @@ -0,0 +1 @@ +export * from './formValidationUtils'; diff --git a/frontend/packages/shared/src/utils/formValidationUtils/test-data/Input.schema.v1.json b/frontend/packages/shared/src/utils/formValidationUtils/test-data/Input.schema.v1.json new file mode 100644 index 00000000000..3f010e0ab71 --- /dev/null +++ b/frontend/packages/shared/src/utils/formValidationUtils/test-data/Input.schema.v1.json @@ -0,0 +1,345 @@ +{ + "$id": "https://altinncdn.no/schemas/json/component/Input.schema.v1.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Input component", + "description": "Schema that describes the layout configuration for an input component.", + "type": "object", + "properties": { + "id": { + "$ref": "https://altinncdn.no/schemas/json/component/common-defs.schema.v1.json#/definitions/id" + }, + "type": { + "type": "string", + "title": "Type", + "description": "The component type.", + "const": "Input" + }, + "dataModelBindings": { + "title": "Data model bindings", + "description": "Data model bindings for component", + "type": "object", + "properties": { + "simpleBinding": { + "type": "string", + "title": "Simple binding", + "description": "Data model binding for components connection to a single field in the data model" + } + }, + "required": ["simpleBinding"], + "additionalProperties": false + }, + "required": { + "title": "Required", + "description": "Boolean or expression indicating if the component is required when filling in the form. Defaults to false.", + "default": false, + "$ref": "https://altinncdn.no/schemas/json/layout/expression.schema.v1.json#/definitions/boolean" + }, + "readOnly": { + "title": "Read only", + "description": "Boolean or expression indicating if the component should be presented as read only. Defaults to false.", + "default": false, + "$ref": "https://altinncdn.no/schemas/json/layout/expression.schema.v1.json#/definitions/boolean" + }, + "textResourceBindings": { + "type": "object", + "title": "Text resource bindings", + "description": "Text resource bindings for a component.", + "properties": { + "title": { + "type": "string", + "title": "Label", + "description": "The title/label text for the component" + }, + "description": { + "type": "string", + "title": "Description", + "description": "The description text for the component" + }, + "help": { + "type": "string", + "title": "Help text", + "description": "The help text for the component" + } + }, + "required": ["title"], + "additionalProperties": true + }, + "triggers": { + "$ref": "https://altinncdn.no/schemas/json/component/common-defs.schema.v1.json#/definitions/triggers" + }, + "grid": { + "$ref": "https://altinncdn.no/schemas/json/component/common-defs.schema.v1.json#/definitions/gridSettings" + }, + "labelSettings": { + "$ref": "https://altinncdn.no/schemas/json/component/common-defs.schema.v1.json#/definitions/labelSettings" + }, + "pageBreak": { + "$ref": "https://altinncdn.no/schemas/json/component/common-defs.schema.v1.json#/definitions/pageBreak" + }, + "hidden": { + "title": "Hidden", + "description": "Boolean value or expression indicating if the component should be hidden. Defaults to false.", + "default": false, + "$ref": "https://altinncdn.no/schemas/json/layout/expression.schema.v1.json#/definitions/boolean" + }, + "renderAsSummary": { + "title": "Render as summary", + "description": "Boolean or expression indicating if the component should be rendered as a summary. Defaults to false.", + "default": false, + "$ref": "https://altinncdn.no/schemas/json/layout/expression.schema.v1.json#/definitions/boolean" + }, + "formatting": { + "title": "Input formatting", + "description": "Set of options for formatting input fields.", + "type": "object", + "properties": { + "currency": { + "type": "string", + "title": "Language-sensitive number formatting based on currency", + "description": "Enables currency along with thousand and decimal separators to be language sensitive based on selected app language. They are configured in number property. Note: parts that already exist in number property are not overridden by this prop.", + "enum": [ + "AED", + "AFN", + "ALL", + "AMD", + "ANG", + "AOA", + "ARS", + "AUD", + "AWG", + "AZN", + "BAM", + "BBD", + "BDT", + "BGN", + "BHD", + "BIF", + "BMD", + "BND", + "BOB", + "BOV", + "BRL", + "BSD", + "BTN", + "BWP", + "BYN", + "BZD", + "CAD", + "CDF", + "CHE", + "CHF", + "CHW", + "CLF", + "CLP", + "CNY", + "COP", + "COU", + "CRC", + "CUC", + "CUP", + "CVE", + "CZK", + "DJF", + "DKK", + "DOP", + "DZD", + "EGP", + "ERN", + "ETB", + "EUR", + "FJD", + "FKP", + "GBP", + "GEL", + "GHS", + "GIP", + "GMD", + "GNF", + "GTQ", + "GYD", + "HKD", + "HNL", + "HTG", + "HUF", + "IDR", + "ILS", + "INR", + "IQD", + "IRR", + "ISK", + "JMD", + "JOD", + "JPY", + "KES", + "KGS", + "KHR", + "KMF", + "KPW", + "KRW", + "KWD", + "KYD", + "KZT", + "LAK", + "LBP", + "LKR", + "LRD", + "LSL", + "LYD", + "MAD", + "MDL", + "MGA", + "MKD", + "MMK", + "MNT", + "MOP", + "MRU", + "MUR", + "MVR", + "MWK", + "MXN", + "MXV", + "MYR", + "MZN", + "NAD", + "NGN", + "NIO", + "NOK", + "NPR", + "NZD", + "OMR", + "PAB", + "PEN", + "PGK", + "PHP", + "PKR", + "PLN", + "PYG", + "QAR", + "RON", + "RSD", + "RUB", + "RWF", + "SAR", + "SBD", + "SCR", + "SDG", + "SEK", + "SGD", + "SHP", + "SLE", + "SLL", + "SOS", + "SRD", + "SSP", + "STN", + "SVC", + "SYP", + "SZL", + "THB", + "TJS", + "TMT", + "TND", + "TOP", + "TRY", + "TTD", + "TWD", + "TZS", + "UAH", + "UGX", + "USD", + "USN", + "UYI", + "UYU", + "UYW", + "UZS", + "VED", + "VES", + "VND", + "VUV", + "WST", + "XAF", + "XCD", + "XDR", + "XOF", + "XPF", + "XSU", + "XUA", + "YER", + "ZAR", + "ZMW", + "ZWL" + ] + }, + "unit": { + "title": "Language-sensitive number formatting based on unit", + "type": "string", + "description": "Enables unit along with thousand and decimal separators to be language sensitive based on selected app language. They are configured in number property. Note: parts that already exist in number property are not overridden by this prop.", + "enum": [ + "celsius", + "centimeter", + "day", + "degree", + "foot", + "gram", + "hectare", + "hour", + "inch", + "kilogram", + "kilometer", + "liter", + "meter", + "milliliter", + "millimeter", + "millisecond", + "minute", + "month", + "percent", + "second", + "week", + "year" + ] + }, + "position": { + "type": "string", + "title": "Position of the currency/unit symbol (only when using currency or unit options)", + "description": "Display the unit as prefix or suffix. Default is prefix", + "enum": ["prefix", "suffix"] + }, + "align": { + "type": "string", + "title": "Align input", + "description": "The alignment for Input field (eg. right aligning a series of numbers).", + "enum": ["left", "center", "right"] + } + } + }, + "saveWhileTyping": { + "title": "Automatic saving while typing", + "description": "Boolean or number. True = feature on (default), false = feature off (saves on focus blur), number = timeout in milliseconds (400 by default)", + "default": true, + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "number" + } + ] + }, + "variant": { + "type": "string", + "title": "Input Variant", + "description": "An enum to choose if the inputfield it is a normal textfield or a searchbar", + "enum": ["text", "search"] + }, + "autocomplete": { + "$ref": "https://altinncdn.no/schemas/json/component/common-defs.schema.v1.json#/definitions/autocomplete" + }, + "maxLength": { + "title": "Maximum length", + "description": "Maximum length of input field", + "type": "number" + } + }, + "required": ["id", "type", "dataModelBindings"] +} diff --git a/frontend/packages/shared/src/utils/formValidationUtils/test-data/common-defs.schema.v1.json b/frontend/packages/shared/src/utils/formValidationUtils/test-data/common-defs.schema.v1.json new file mode 100644 index 00000000000..a0569d03990 --- /dev/null +++ b/frontend/packages/shared/src/utils/formValidationUtils/test-data/common-defs.schema.v1.json @@ -0,0 +1,558 @@ +{ + "$id": "https://altinncdn.no/schemas/json/component/common-defs.schema.v1.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Common definitions", + "description": "Schema that contains common definitions used by components", + "type": "object", + "definitions": { + "id": { + "type": "string", + "title": "id", + "pattern": "^[0-9a-zA-Z][0-9a-zA-Z-]*(-?[a-zA-Z]+|[a-zA-Z][0-9]+|-[0-9]{6,})$", + "description": "The component ID. Must be unique within all layouts/pages in a layout-set. Cannot end with ." + }, + "hidden": { + "title": "Hidden", + "description": "Boolean value or expression indicating if the component should be hidden. Defaults to false.", + "default": false, + "$ref": "https://altinncdn.no/schemas/json/layout/expression.schema.v1.json#/definitions/boolean" + }, + "renderAsSummary": { + "title": "Render as summary", + "description": "Boolean or expression indicating if the component should be rendered as a summary. Defaults to false.", + "default": false, + "$ref": "https://altinncdn.no/schemas/json/layout/expression.schema.v1.json#/definitions/boolean" + }, + "required": { + "title": "Required", + "description": "Boolean or expression indicating if the component is required when filling in the form. Defaults to false.", + "default": false, + "$ref": "https://altinncdn.no/schemas/json/layout/expression.schema.v1.json#/definitions/boolean" + }, + "readOnly": { + "title": "Read only", + "description": "Boolean or expression indicating if the component should be presented as read only. Defaults to false.", + "default": false, + "$ref": "https://altinncdn.no/schemas/json/layout/expression.schema.v1.json#/definitions/boolean" + }, + "basicTextResources": { + "type": "object", + "title": "Text resource bindings", + "description": "Text resource bindings for a component.", + "properties": { + "title": { + "type": "string", + "title": "Label", + "description": "The title/label text for the component" + }, + "description": { + "type": "string", + "title": "Description", + "description": "The description text for the component" + }, + "help": { + "type": "string", + "title": "Help text", + "description": "The help text for the component" + }, + "shortName": { + "type": "string", + "title": "Short name", + "description": "The short name for the component (used in validation messages) (optional). If it is not specified, 'title' text is used." + }, + "tableTitle": { + "type": "string", + "title": "Table title", + "description": "The text shown in column title when component is used in repeating group (optional). If it is not specified, 'title' text is used." + } + }, + "required": ["title"], + "additionalProperties": true + }, + "basicDataModelBindings": { + "title": "Data model bindings", + "description": "Data model bindings for component", + "type": "object", + "properties": { + "simpleBinding": { + "type": "string", + "title": "Simple binding", + "description": "Data model binding for components connection to a single field in the data model" + } + }, + "required": ["simpleBinding"], + "additionalProperties": false + }, + "gridProps": { + "properties": { + "xs": { + "$ref": "#/definitions/gridValue", + "title": "xs", + "description": "Grid breakpoint at 0px" + }, + "sm": { + "$ref": "#/definitions/gridValue", + "title": "sm", + "description": "Grid breakpoint at 600px" + }, + "md": { + "$ref": "#/definitions/gridValue", + "title": "md", + "description": "Grid breakpoint at 960px" + }, + "lg": { + "$ref": "#/definitions/gridValue", + "title": "lg", + "description": "Grid breakpoint at 1280px" + }, + "xl": { + "$ref": "#/definitions/gridValue", + "title": "xl", + "description": "Grid breakpoint at 1920px" + } + } + }, + "gridSettings": { + "allOf": [ + { + "$ref": "#/definitions/gridProps" + } + ], + "properties": { + "labelGrid": { + "title": "labelGrid", + "description": "Optional grid for the component label. Used in combination with innerGrid to align labels on the side.", + "examples": [ + { + "xs": 12 + } + ], + "$ref": "#/definitions/gridProps" + }, + "innerGrid": { + "title": "innerGrid", + "description": "Optional grid for inner component content like input field or dropdown. Used to avoid inner content filling the component width.", + "examples": [ + { + "xs": 12 + } + ], + "$ref": "#/definitions/gridProps" + } + } + }, + "gridValue": { + "type": "integer", + "maximum": 12, + "minimum": 1, + "examples": [12] + }, + "labelSettings": { + "type": "object", + "title": "Label settings", + "description": "A collection of settings for how the component label should be rendered.", + "properties": { + "optionalIndicator": { + "type": "boolean", + "title": "Optional indicator", + "description": "Controls whether the text that is indicating that a field is optional should be displayed.", + "default": true + } + } + }, + "pageBreak": { + "type": "object", + "properties": { + "breakBefore": { + "title": "Page break before", + "type": "string", + "description": "PDF only: Value or expression indicating whether a page break should be added before the component. Can be either: 'auto' (default), 'always', or 'avoid'.", + "enum": ["auto", "always", "avoid"], + "default": "auto" + }, + "breakAfter": { + "title": "Page break after", + "type": "string", + "description": "PDF only: Value or expression indicating whether a page break should be added after the component. Can be either: 'auto' (default), 'always', or 'avoid'.", + "enum": ["auto", "always", "avoid"], + "default": "auto" + } + } + }, + "triggers": { + "title": "Triggers", + "description": "An array of actions that should be triggered when data connected to this component changes.", + "type": "array", + "items": { + "type": "string", + "enum": [ + "validation", + "validateRow", + "validatePage", + "validateAllPages", + "calculatePageOrder" + ] + } + }, + "saveWhileTyping": { + "title": "Automatic saving while typing", + "description": "Boolean or number. True = feature on (default), false = feature off (saves on focus blur), number = timeout in milliseconds (400 by default)", + "default": true, + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "number" + } + ] + }, + "autocomplete": { + "title": "HTML attribute: autocomplete", + "description": "The HTML autocomplete attribute lets web developers specify what if any permission the user agent has to provide automated assistance in filling out form field values, as well as guidance to the browser as to the type of information expected in the field.", + "type": "string", + "enum": [ + "on", + "off", + "name", + "honorific-prefix", + "given-name", + "additional-name", + "family-name", + "honorific-suffix", + "nickname", + "email", + "username", + "new-password", + "current-password", + "one-time-code", + "organization-title", + "organization", + "street-address", + "address-line1", + "address-line2", + "address-line3", + "address-level4", + "address-level3", + "address-level2", + "address-level1", + "country", + "country-name", + "postal-code", + "cc-name", + "cc-given-name", + "cc-additional-name", + "cc-family-name", + "cc-number", + "cc-exp", + "cc-exp-month", + "cc-exp-year", + "cc-csc", + "cc-type", + "transaction-currency", + "transaction-amount", + "language", + "bday", + "bday-day", + "bday-month", + "bday-year", + "sex", + "tel", + "tel-country-code", + "tel-national", + "tel-area-code", + "tel-local", + "tel-extension", + "url", + "photo" + ] + }, + "mapping": { + "type": "object", + "title": "Mapping", + "examples": [ + { + "some.source.field": "key1", + "some.other.source": "key2" + } + ], + "additionalProperties": { + "type": "string" + } + }, + "optionsId": { + "type": "string", + "title": "Options ID", + "description": "Reference to connected options by id." + }, + "options": { + "properties": { + "label": { + "type": "string", + "title": "Label", + "description": "The option label. Can be plain text or a text resource binding." + }, + "value": { + "type": "string", + "title": "Value", + "description": "The option value." + } + }, + "required": ["label", "value"] + }, + "gridRow": { + "properties": { + "header": { + "title": "Header?", + "description": "A boolean indicating if the row should be a header row", + "type": "boolean", + "default": false + }, + "readOnly": { + "title": "Read only?", + "description": "A boolean indicating if the row should be a read only row (yellow background)", + "type": "boolean", + "default": false + }, + "cells": { + "title": "Grid row cells", + "description": "An array of cells to be rendered in the row", + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/gridCellText" + }, + { + "$ref": "#/definitions/gridCellComponent" + }, + { + "$ref": "#/definitions/tableColumnOptions" + }, + { + "type": "null", + "title": "Empty cell" + } + ] + } + } + }, + "required": ["cells"] + }, + "gridCellText": { + "properties": { + "text": { + "title": "Text", + "description": "The text or text resource ID to be rendered in the cell", + "type": "string" + } + }, + "$ref": "#/definitions/tableColumnOptions", + "required": ["text"] + }, + "gridCellComponent": { + "properties": { + "component": { + "title": "Component ID", + "description": "The ID of the component to be rendered in the cell", + "type": "string" + } + }, + "$ref": "#/definitions/tableColumnOptions", + "required": ["component"] + }, + "groupEditOptions": { + "properties": { + "mode": { + "title": "Edit mode", + "description": "Mode for how repeating group table is displayed in edit mode", + "type": "string", + "enum": ["hideTable", "likert", "showAll", "showTable", "onlyTable"] + }, + "filter": { + "title": "Filter", + "description": "Conditions for filtering visible items in repeating group", + "type": "array", + "items": { + "$ref": "#/definitions/groupFilterItem" + } + }, + "saveButton": { + "title": "Save button", + "description": "Boolean or expression indicating whether save button should be shown or not for a given row", + "$ref": "https://altinncdn.no/schemas/json/layout/expression.schema.v1.json#/definitions/boolean" + }, + "saveAndNextButton": { + "title": "Save and open next button", + "description": "Boolean or expression indicating whether save and go to next button should be shown or not in addition to save and close button", + "$ref": "https://altinncdn.no/schemas/json/layout/expression.schema.v1.json#/definitions/boolean" + }, + "deleteButton": { + "title": "Delete button", + "description": "Boolean or expression indicating whether delete button should be shown or not for a given row", + "$ref": "https://altinncdn.no/schemas/json/layout/expression.schema.v1.json#/definitions/boolean" + }, + "editButton": { + "title": "Edit button", + "description": "Boolean or expression indicating whether edit button should be shown or not for a given row", + "$ref": "https://altinncdn.no/schemas/json/layout/expression.schema.v1.json#/definitions/boolean" + }, + "multiPage": { + "title": "Multi-page", + "description": "Boolean value indicating if form components in edit mode should be shown over multiple pages/views.", + "type": "boolean" + }, + "addButton": { + "title": "Add button", + "description": "Boolean or expression indicating whether add new button should be shown or not under the table.", + "$ref": "https://altinncdn.no/schemas/json/layout/expression.schema.v1.json#/definitions/boolean" + }, + "alwaysShowAddButton": { + "title": "Show add button on open group", + "description": "Boolean value indicating whether add new button should be shown or not under the table when a group is open.", + "type": "boolean", + "default": false + }, + "openByDefault": { + "title": "Open by default", + "description": "Boolean or string indicating if group should be opened by default. If no items exist: 'first', 'last', and true adds a new item. If items exist already, true does not open anything, but 'first' opens the first item, and 'last' opens the last item in the group.", + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "enum": ["first", "last"] + } + ] + }, + "alertOnDelete": { + "title": "Alert on delete", + "description": "Boolean value indicating if warning popup should be displayed when attempting to delete a row", + "type": "boolean" + } + } + }, + "groupPanelOptions": { + "properties": { + "variant": { + "title": "Panel variant", + "description": "Change the look of the panel.", + "type": "string", + "enum": ["info", "warning", "success"], + "default": "info" + }, + "showIcon": { + "title": "Show icon", + "description": "Boolean value indicating if the icon should be shown.", + "type": "boolean", + "default": true + }, + "iconUrl": { + "title": "Icon url", + "description": "Url of the icon to be shown in panel. Can be relative if hosted by app or full if referencing a cdn or other hosting.", + "type": "string", + "examples": ["fancyIcon.svg", "https://cdn.example.com/fancyIcon.svg"] + }, + "iconAlt": { + "title": "Icon alt", + "description": "Alternative text for the icon. Only applicable if iconUrl is provided. Can be plain text or a text resource reference.", + "type": "string" + }, + "groupReference": { + "title": "Group reference", + "description": "Reference to the group that is being displayed in the panel. Used for referencing another repeating group context.", + "type": "object", + "properties": { + "group": { + "type": "string", + "title": "Group", + "description": "Group reference. Can be either the group id or the group data model binding.", + "examples": ["the-group-id", "some.model.theGroup"] + } + } + } + } + }, + "groupFilterItem": { + "properties": { + "key": { + "title": "Key", + "description": "Key representing field in data model to check.", + "type": "string" + }, + "value": { + "title": "Value", + "description": "Value to check against.", + "type": "string" + } + } + }, + "paginationProperties": { + "type": "object", + "properties": { + "alternatives": { + "type": "array", + "items": { + "type": "number" + }, + "title": "Alternatives", + "description": "List of page sizes the user can choose from. Make sure to test the performance of the largest number of items per page you are allowing." + }, + "default": { + "type": "number", + "title": "Default", + "description": "The pagination size that is set to default." + } + }, + "required": ["alternatives", "default"] + }, + "tableColumnOptions": { + "title": "Column options", + "description": "Column options for specified header.", + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/tableColumnTextOptions" + }, + { + "type": "object", + "properties": { + "width": { + "title": "Width", + "description": "Width of cell in % or 'auto'. Defaults to 'auto'", + "type": "string", + "pattern": "^([0-9]{1,2}%|100%|auto)$" + } + } + } + ] + }, + "tableColumnTextOptions": { + "properties": { + "alignText": { + "title": "Align Text", + "description": "Choose text alignment between 'left', 'center', or 'right' for text in table cells. Defaults to 'left' for text and 'right' for numbers.", + "type": "string", + "enum": ["left", "center", "right"] + }, + "textOverflow": { + "title": "Text Overflow", + "description": "Use this property to controll behaviour when text is too large to be displayed in table cell.", + "properties": { + "lineWrap": { + "title": "Line Wrap", + "description": "Toggle line wrapping on or off. Defaults to true", + "type": "boolean" + }, + "maxHeight": { + "title": "Max Height", + "description": "Determines the number of lines to display in table cell before hiding the rest of the text with an elipsis (...). Defaults to 2.", + "type": "number" + } + } + } + } + } + } +} diff --git a/frontend/packages/shared/src/utils/formValidationUtils/test-data/expression.schema.v1.json b/frontend/packages/shared/src/utils/formValidationUtils/test-data/expression.schema.v1.json new file mode 100644 index 00000000000..3ebd771609b --- /dev/null +++ b/frontend/packages/shared/src/utils/formValidationUtils/test-data/expression.schema.v1.json @@ -0,0 +1,698 @@ +{ + "$id": "https://altinncdn.no/schemas/json/layout/expression.schema.v1.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Expression", + "description": "Multi-purpose expression mini-language used to declare dynamic behaviour in Altinn 3 apps", + "examples": [ + ["equals", ["dataModel", "My.Model.Group.Field"], "string constant"], + ["greaterThanEq", ["component", "my-component-id"], ["dataModel", "My.Model.Other.Field"]], + ["or", ["equals", "foo", "bar"], ["equals", "foo", "baz"]], + [ + "if", + [ + "or", + ["equals", ["component", "my-component"], ""], + ["equals", ["component", "my-component"], null] + ], + "This will be the value if the condition above is true", + "else", + [ + "if", + ["notEquals", ["component", "my-other-component"], "illegal value"], + "This will be the value if the first condition is false, and the second is true", + "else", + "This will be the value if all the conditions above are false" + ] + ], + ["concat", "Are you sure you want to delete ", ["dataModel", "My.Model.Title"], "?"] + ], + "$ref": "#/definitions/any", + "definitions": { + "any": { + "title": "Any expression", + "anyOf": [ + { + "type": "null", + "title": "Null/missing value" + }, + { + "$ref": "#/definitions/strict-string" + }, + { + "$ref": "#/definitions/strict-boolean" + }, + { + "$ref": "#/definitions/strict-number" + }, + { + "$ref": "#/definitions/func-if" + } + ] + }, + "string": { + "title": "Any expression returning string", + "anyOf": [ + { + "type": "null", + "title": "Null/missing value" + }, + { + "$ref": "#/definitions/strict-string" + }, + { + "$ref": "#/definitions/func-if" + }, + { + "$ref": "#/definitions/strict-number", + "description": "Numbers can be cast to strings" + }, + { + "$ref": "#/definitions/strict-boolean", + "description": "Booleans can be cast to strings" + } + ] + }, + "strict-string": { + "title": "Any expression returning string (strict)", + "anyOf": [ + { + "type": "string", + "title": "String constant" + }, + { + "$ref": "#/definitions/func-component" + }, + { + "$ref": "#/definitions/func-dataModel" + }, + { + "$ref": "#/definitions/func-instanceContext" + }, + { + "$ref": "#/definitions/func-frontendSettings" + }, + { + "$ref": "#/definitions/func-concat" + }, + { + "$ref": "#/definitions/func-round" + }, + { + "$ref": "#/definitions/func-text" + }, + { + "$ref": "#/definitions/func-language" + }, + { + "$ref": "#/definitions/func-lowerCase" + }, + { + "$ref": "#/definitions/func-upperCase" + } + ] + }, + "boolean": { + "title": "Any expression returning boolean", + "anyOf": [ + { + "type": "null", + "title": "Null/missing value" + }, + { + "$ref": "#/definitions/strict-boolean" + }, + { + "$ref": "#/definitions/func-if" + }, + { + "$ref": "#/definitions/strict-string", + "description": "Stringy true/false/0/1 can be cast to boolean" + }, + { + "$ref": "#/definitions/strict-number", + "description": "Numeric 0/1 can be cast to boolean" + } + ] + }, + "strict-boolean": { + "title": "Any expression returning boolean (strict)", + "anyOf": [ + { + "type": "boolean", + "title": "Boolean constant" + }, + { + "$ref": "#/definitions/func-equals" + }, + { + "$ref": "#/definitions/func-notEquals" + }, + { + "$ref": "#/definitions/func-greaterThan" + }, + { + "$ref": "#/definitions/func-greaterThanEq" + }, + { + "$ref": "#/definitions/func-lessThan" + }, + { + "$ref": "#/definitions/func-lessThanEq" + }, + { + "$ref": "#/definitions/func-not" + }, + { + "$ref": "#/definitions/func-and" + }, + { + "$ref": "#/definitions/func-or" + }, + { + "$ref": "#/definitions/func-authContext" + }, + { + "$ref": "#/definitions/func-contains" + }, + { + "$ref": "#/definitions/func-notContains" + }, + { + "$ref": "#/definitions/func-endsWith" + }, + { + "$ref": "#/definitions/func-startsWith" + }, + { + "$ref": "#/definitions/func-commaContains" + } + ] + }, + "number": { + "title": "Any expression returning a number", + "anyOf": [ + { + "type": "null", + "title": "Null/missing value" + }, + { + "$ref": "#/definitions/strict-number" + }, + { + "$ref": "#/definitions/func-if" + }, + { + "$ref": "#/definitions/strict-string", + "description": "Numeric strings can be cast to numbers" + } + ] + }, + "strict-number": { + "title": "Any expression returning a number (strict)", + "anyOf": [ + { + "type": "number", + "title": "Numeric constant" + }, + { + "$ref": "#/definitions/func-stringLength" + } + ] + }, + "func-if": { + "title": "If/else conditional expression", + "description": "This function will evaluate and return the result of either branch. If else is not given, null will be returned instead.", + "anyOf": [ + { + "$ref": "#/definitions/func-if-with-else" + }, + { + "$ref": "#/definitions/func-if-without-else" + } + ] + }, + "func-if-without-else": { + "type": "array", + "prefixItems": [ + { + "const": "if" + }, + { + "$ref": "#/definitions/boolean" + }, + { + "$ref": "#/definitions/any" + } + ], + "additionalItems": false + }, + "func-if-with-else": { + "type": "array", + "prefixItems": [ + { + "type": "string", + "const": "if" + }, + { + "$ref": "#/definitions/boolean" + }, + { + "$ref": "#/definitions/any" + }, + { + "type": "string", + "const": "else" + }, + { + "$ref": "#/definitions/any" + } + ], + "additionalItems": false + }, + "func-component": { + "title": "Component value lookup function", + "description": "This function will look up a nearby component and its value (only supports simpleBinding currently). Other components can be siblings, or siblings of parent groups.", + "type": "array", + "prefixItems": [ + { + "type": "string", + "const": "component" + }, + { + "$ref": "#/definitions/string" + } + ], + "additionalItems": false + }, + "func-dataModel": { + "title": "Data model lookup function", + "description": "This function will look up a value in the data model, using the JSON dot notation for referencing the data model structure. Relative positioning inside repeating groups will be resolved automatically if no positional indexes are specified.", + "type": "array", + "prefixItems": [ + { + "type": "string", + "const": "dataModel" + }, + { + "$ref": "#/definitions/string" + } + ], + "additionalItems": false + }, + "func-instanceContext": { + "title": "Instance context lookup function", + "description": "This function can be used to lookup a value from the instance context", + "type": "array", + "prefixItems": [ + { + "type": "string", + "const": "instanceContext" + }, + { + "enum": ["appId", "instanceId", "instanceOwnerPartyId"] + } + ], + "additionalItems": false + }, + "func-authContext": { + "title": "Auth context lookup function", + "description": "This function can be used to check the users permissions on the current process step.", + "type": "array", + "prefixItems": [ + { + "type": "string", + "const": "authContext" + }, + { + "enum": ["read", "write", "instantiate", "confirm", "sign", "reject"] + } + ], + "additionalItems": false + }, + "func-frontendSettings": { + "title": "Frontend settings lookup function", + "description": "This function can be used to lookup a value from frontendSettings (only supports scalar values, no objects or arrays)", + "type": "array", + "prefixItems": [ + { + "type": "string", + "const": "frontendSettings" + }, + { + "$ref": "#/definitions/string" + } + ], + "additionalItems": false + }, + "func-concat": { + "title": "String concatenation function", + "description": "This function will concatenate strings or numbers, producing a final string as a result", + "type": "array", + "prefixItems": [ + { + "type": "string", + "const": "concat" + } + ], + "additionalItems": { + "$ref": "#/definitions/string" + } + }, + "func-equals": { + "title": "Equals function", + "description": "This function compares two values (or expressions) for equality", + "type": "array", + "prefixItems": [ + { + "const": "equals" + }, + { + "$ref": "#/definitions/any" + }, + { + "$ref": "#/definitions/any" + } + ], + "additionalItems": false + }, + "func-notEquals": { + "title": "Not equals function", + "description": "This function compares two values (or expressions) for inequality", + "type": "array", + "prefixItems": [ + { + "type": "string", + "const": "notEquals" + }, + { + "$ref": "#/definitions/any" + }, + { + "$ref": "#/definitions/any" + } + ], + "additionalItems": false + }, + "func-not": { + "title": "Not function", + "description": "This function inverts a boolean, returning true if given false, and vice versa.", + "type": "array", + "prefixItems": [ + { + "type": "string", + "const": "not" + }, + { + "$ref": "#/definitions/boolean" + } + ], + "additionalItems": false + }, + "func-greaterThan": { + "title": "Greater than function", + "description": "This function compares two values (or expressions), returning true if the first argument is greater than the second", + "type": "array", + "prefixItems": [ + { + "type": "string", + "const": "greaterThan" + }, + { + "$ref": "#/definitions/number" + }, + { + "$ref": "#/definitions/number" + } + ], + "additionalItems": false + }, + "func-greaterThanEq": { + "title": "Greater than or equals function", + "description": "This function compares two values (or expressions), returning true if the first argument is greater than or equals the second", + "type": "array", + "prefixItems": [ + { + "type": "string", + "const": "greaterThanEq" + }, + { + "$ref": "#/definitions/number" + }, + { + "$ref": "#/definitions/number" + } + ], + "additionalItems": false + }, + "func-lessThan": { + "title": "Less than function", + "description": "This function compares two values (or expressions), returning true if the first argument is less than the second", + "type": "array", + "prefixItems": [ + { + "type": "string", + "const": "lessThan" + }, + { + "$ref": "#/definitions/number" + }, + { + "$ref": "#/definitions/number" + } + ], + "additionalItems": false + }, + "func-lessThanEq": { + "title": "Less than or equals function", + "description": "This function compares two values (or expressions), returning true if the first argument is less than or equals the second", + "type": "array", + "prefixItems": [ + { + "type": "string", + "const": "lessThanEq" + }, + { + "$ref": "#/definitions/number" + }, + { + "$ref": "#/definitions/number" + } + ], + "additionalItems": false + }, + "func-and": { + "title": "And combinator", + "description": "This function returns true if all the arguments (or expressions) are true", + "type": "array", + "prefixItems": [ + { + "type": "string", + "const": "and" + }, + { + "$ref": "#/definitions/boolean" + } + ], + "additionalItems": { + "$ref": "#/definitions/boolean" + } + }, + "func-or": { + "title": "Or combinator", + "description": "This function returns true if any of the arguments (or expressions) are true", + "type": "array", + "prefixItems": [ + { + "type": "string", + "const": "or" + }, + { + "$ref": "#/definitions/boolean" + } + ], + "additionalItems": { + "$ref": "#/definitions/boolean" + } + }, + "func-round": { + "title": "Round function", + "description": "This function rounds a number to the nearest integer, or to the specified number of decimals", + "type": "array", + "prefixItems": [ + { + "type": "string", + "const": "round" + }, + { + "$ref": "#/definitions/number" + }, + { + "$ref": "#/definitions/number" + } + ], + "additionalItems": false + }, + "func-text": { + "title": "Text function", + "description": "This function retrieves the value of a text resource key, or returns the key if no text resource is found", + "type": "array", + "prefixItems": [ + { + "type": "string", + "const": "text" + }, + { + "$ref": "#/definitions/string" + } + ], + "additionalItems": false + }, + "func-language": { + "title": "Language function", + "description": "This function retrieves the current language (usually 'nb', 'nn' or 'en')", + "type": "array", + "prefixItems": [ + { + "type": "string", + "const": "language" + } + ], + "additionalItems": false + }, + "func-contains": { + "title": "Contains function", + "description": "This function checks if the first string contains the second string", + "type": "array", + "prefixItems": [ + { + "type": "string", + "const": "contains" + }, + { + "$ref": "#/definitions/string" + }, + { + "$ref": "#/definitions/string" + } + ], + "additionalItems": false + }, + "func-notContains": { + "title": "Not contains function", + "description": "This function checks if the first string does not contain the second string", + "type": "array", + "prefixItems": [ + { + "type": "string", + "const": "notContains" + }, + { + "$ref": "#/definitions/string" + }, + { + "$ref": "#/definitions/string" + } + ], + "additionalItems": false + }, + "func-startsWith": { + "title": "Starts with function", + "description": "This function checks if the first string starts with the second string", + "type": "array", + "prefixItems": [ + { + "type": "string", + "const": "startsWith" + }, + { + "$ref": "#/definitions/string" + }, + { + "$ref": "#/definitions/string" + } + ], + "additionalItems": false + }, + "func-endsWith": { + "title": "Ends with function", + "description": "This function checks if the first string ends with the second string", + "type": "array", + "prefixItems": [ + { + "type": "string", + "const": "endsWith" + }, + { + "$ref": "#/definitions/string" + }, + { + "$ref": "#/definitions/string" + } + ], + "additionalItems": false + }, + "func-stringLength": { + "title": "String length function", + "description": "This function returns the length of a string", + "type": "array", + "prefixItems": [ + { + "type": "string", + "const": "stringLength" + }, + { + "$ref": "#/definitions/string" + } + ], + "additionalItems": false + }, + "func-commaContains": { + "title": "Comma contains function", + "description": "This function checks if the first comma-separated string contains the second string", + "type": "array", + "prefixItems": [ + { + "type": "string", + "const": "commaContains" + }, + { + "$ref": "#/definitions/string" + }, + { + "$ref": "#/definitions/string" + } + ], + "additionalItems": false + }, + "func-lowerCase": { + "title": "Lower case function", + "description": "This function converts a string to lower case", + "type": "array", + "prefixItems": [ + { + "type": "string", + "const": "lowerCase" + }, + { + "$ref": "#/definitions/string" + } + ], + "additionalItems": false + }, + "func-upperCase": { + "title": "Upper case function", + "description": "This function converts a string to upper case", + "type": "array", + "prefixItems": [ + { + "type": "string", + "const": "upperCase" + }, + { + "$ref": "#/definitions/string" + } + ], + "additionalItems": false + } + } +} diff --git a/frontend/packages/shared/src/utils/formValidationUtils/test-data/layout.schema.v1.json b/frontend/packages/shared/src/utils/formValidationUtils/test-data/layout.schema.v1.json new file mode 100644 index 00000000000..bcf71fbaadd --- /dev/null +++ b/frontend/packages/shared/src/utils/formValidationUtils/test-data/layout.schema.v1.json @@ -0,0 +1,1943 @@ +{ + "$id": "https://altinncdn.no/schemas/json/layout/layout.schema.v1.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Altinn layout", + "description": "Schema that describes the layout configuration for Altinn applications.", + "type": "object", + "additionalProperties": true, + "properties": { + "data": { + "$ref": "#/definitions/data" + } + }, + "definitions": { + "data": { + "title": "The layout data", + "description": "Contains data describing the layout configuration.", + "type": "object", + "properties": { + "layout": { + "$ref": "#/definitions/layout" + } + } + }, + "layout": { + "title": "The layout", + "description": "Array of components to be presented in the layout.", + "type": "array", + "items": { + "$ref": "#/definitions/component" + } + }, + "component": { + "type": "object", + "properties": { + "id": { + "type": "string", + "title": "id", + "pattern": "^[0-9a-zA-Z][0-9a-zA-Z-]*(-?[a-zA-Z]+|[a-zA-Z][0-9]+|-[0-9]{6,})$", + "description": "The component ID. Must be unique within all layouts/pages in a layout-set. Cannot end with ." + }, + "type": { + "type": "string", + "title": "Type", + "description": "The component type.", + "enum": [ + "ActionButton", + "AddressComponent", + "AttachmentList", + "Button", + "ButtonGroup", + "Checkboxes", + "Custom", + "Datepicker", + "Dropdown", + "FileUpload", + "FileUploadWithTag", + "Grid", + "Group", + "Header", + "Image", + "Input", + "InstanceInformation", + "InstantiationButton", + "IFrame", + "Likert", + "Link", + "List", + "MapComponent", + "MultipleSelect", + "NavigationButtons", + "NavigationBar", + "Panel", + "Paragraph", + "PrintButton", + "RadioButtons", + "Summary", + "TextArea" + ] + }, + "required": { + "title": "Required", + "description": "Boolean or expression indicating if the component is required when filling in the form. Defaults to false.", + "default": false, + "$ref": "expression.schema.v1.json#/definitions/boolean" + }, + "readOnly": { + "title": "Read only", + "description": "Boolean or expression indicating if the component should be presented as read only. Defaults to false.", + "default": false, + "$ref": "expression.schema.v1.json#/definitions/boolean" + }, + "renderAsSummary": { + "title": "Render as summary", + "description": "Boolean or expression indicating if the component should be rendered as a summary. Defaults to false.", + "default": false, + "$ref": "expression.schema.v1.json#/definitions/boolean" + }, + "hidden": { + "title": "Hidden", + "description": "Boolean value or expression indicating if the component should be hidden. Defaults to false.", + "default": false, + "$ref": "expression.schema.v1.json#/definitions/boolean" + }, + "textResourceBindings": { + "type": "object", + "title": "Text resource bindings", + "description": "Text resource bindings for a component.", + "additionalProperties": { + "$ref": "expression.schema.v1.json#/definitions/string" + }, + "examples": [ + { + "title": "some.text.binding", + "help": "some.other.text.binding" + } + ] + }, + "dataModelBindings": { + "type": "object", + "title": "Data model bindings", + "description": "Data model bindings for a component.", + "additionalProperties": { + "type": "string" + }, + "examples": [ + { + "simpleBinding": "some.data.binding" + } + ] + }, + "triggers": { + "$ref": "#/definitions/triggers" + }, + "labelSettings": { + "type": "object", + "title": "Label settings", + "description": "A collection of settings for how the component label should be rendered.", + "properties": { + "optionalIndicator": { + "type": "boolean", + "title": "Optional indicator", + "description": "Controls whether the text that is indicating that a field is optional should be displayed.", + "default": true + } + } + }, + "grid": { + "type": "object", + "title": "Grid", + "description": "Settings for the components grid. Used for controlling horizontal alignment.", + "$ref": "#/definitions/gridSettings", + "examples": [ + { + "xs": 12 + } + ] + }, + "pageBreak": { + "$ref": "#/definitions/pageBreak" + } + }, + "required": ["id", "type"], + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "ActionButton" + } + } + }, + "then": { + "$ref": "#/definitions/actionButtonComponent" + } + }, + { + "if": { + "properties": { + "type": { + "const": "AddressComponent" + } + } + }, + "then": { + "$ref": "#/definitions/addressComponent" + } + }, + { + "if": { + "properties": { + "type": { + "const": "AttachmentList" + } + } + }, + "then": { + "$ref": "#/definitions/attachmentListComponent" + } + }, + { + "if": { + "properties": { + "type": { + "const": "ButtonGroup" + } + } + }, + "then": { + "$ref": "#/definitions/buttonGroupComponent" + } + }, + { + "if": { + "properties": { + "type": { + "const": "Checkboxes" + } + } + }, + "then": { + "$ref": "#/definitions/radioAndCheckboxComponents" + } + }, + { + "if": { + "properties": { + "type": { + "const": "Custom" + } + } + }, + "then": { + "$ref": "#/definitions/customComponent" + } + }, + { + "if": { + "properties": { + "type": { + "const": "Datepicker" + } + } + }, + "then": { + "$ref": "#/definitions/datepickerComponent" + } + }, + { + "if": { + "properties": { + "type": { + "const": "Dropdown" + } + } + }, + "then": { + "$ref": "#/definitions/selectionComponents" + } + }, + { + "if": { + "properties": { + "type": { + "const": "FileUpload" + } + } + }, + "then": { + "$ref": "#/definitions/fileUploadComponent" + } + }, + { + "if": { + "properties": { + "type": { + "const": "FileUploadWithTag" + } + } + }, + "then": { + "$ref": "#/definitions/fileUploadWithTagComponent" + } + }, + { + "if": { + "properties": { + "type": { + "const": "Grid" + } + } + }, + "then": { + "$ref": "#/definitions/gridComponent" + } + }, + { + "if": { + "properties": { + "type": { + "const": "Group" + } + } + }, + "then": { + "$ref": "#/definitions/groupComponent" + } + }, + { + "if": { + "properties": { + "type": { + "const": "Image" + } + } + }, + "then": { + "$ref": "#/definitions/imageComponent" + } + }, + { + "if": { + "properties": { + "type": { + "const": "IFrame" + } + } + }, + "then": { + "$ref": "#/definitions/iframeComponent" + } + }, + { + "if": { + "properties": { + "type": { + "const": "Input" + } + } + }, + "then": { + "$ref": "#/definitions/inputComponent" + } + }, + { + "if": { + "properties": { + "type": { + "const": "TextArea" + } + } + }, + "then": { + "$ref": "#/definitions/textAreaComponent" + } + }, + { + "if": { + "properties": { + "type": { + "const": "InstanceInformation" + } + } + }, + "then": { + "$ref": "#/definitions/instanceInformationComponent" + } + }, + { + "if": { + "properties": { + "type": { + "const": "InstantiationButton" + } + } + }, + "then": { + "$ref": "#/definitions/instantiationButtonComponent" + } + }, + { + "if": { + "properties": { + "type": { + "const": "Likert" + } + } + }, + "then": { + "$ref": "#/definitions/radioAndCheckboxComponents" + } + }, + { + "if": { + "properties": { + "type": { + "const": "Link" + } + } + }, + "then": { + "$ref": "#/definitions/linkComponent" + } + }, + { + "if": { + "properties": { + "type": { + "const": "MultipleSelect" + } + } + }, + "then": { + "$ref": "#/definitions/selectionComponents" + } + }, + { + "if": { + "properties": { + "type": { + "const": "NavigationButtons" + } + } + }, + "then": { + "$ref": "#/definitions/navigationButtonsComponent" + } + }, + { + "if": { + "properties": { + "type": { + "const": "NavigationBar" + } + } + }, + "then": { + "$ref": "#/definitions/navigationBarComponent" + } + }, + { + "if": { + "properties": { + "type": { + "const": "RadioButtons" + } + } + }, + "then": { + "$ref": "#/definitions/radioAndCheckboxComponents" + } + }, + { + "if": { + "properties": { + "type": { + "const": "Summary" + } + } + }, + "then": { + "$ref": "#/definitions/summaryComponent" + } + }, + { + "if": { + "properties": { + "type": { + "const": "Header" + } + } + }, + "then": { + "$ref": "#/definitions/headerComponent" + } + }, + { + "if": { + "properties": { + "type": { + "const": "Panel" + } + } + }, + "then": { + "$ref": "#/definitions/panelComponent" + } + }, + { + "if": { + "properties": { + "type": { + "const": "List" + } + } + }, + "then": { + "$ref": "#/definitions/listComponent" + } + }, + { + "if": { + "properties": { + "type": { + "const": "MapComponent" + } + } + }, + "then": { + "$ref": "#/definitions/mapComponent" + } + } + ] + }, + "headerComponent": { + "properties": { + "size": { + "title": "Header size", + "description": "'L'=

, 'M'=

, 'S'=

", + "type": "string", + "enum": ["L", "M", "S", "h2", "h3", "h4"] + } + }, + "required": ["size"] + }, + "panelComponent": { + "properties": { + "variant": { + "title": "Panel variant", + "description": "Change the look of the panel.", + "type": "string", + "enum": ["info", "warning", "success"], + "default": "info" + }, + "showIcon": { + "title": "Show icon", + "description": "Boolean value indicating if the icon should be shown.", + "type": "boolean", + "default": true + } + } + }, + "fileUploadComponent": { + "properties": { + "maxFileSizeInMB": { + "title": "Maximum file size in MB", + "description": "Sets the maximum file size allowed in megabytes.", + "type": "integer", + "minimum": 0 + }, + "maxNumberOfAttachments": { + "title": "Maximum allowed attachments", + "description": "Sets the maximum number of attachments allowed to upload.", + "type": "integer", + "minimum": 0 + }, + "minNumberOfAttachments": { + "title": "Minimum allowed attachments", + "description": "Sets the minimum number of attachments to upload", + "type": "integer", + "minimum": 0 + }, + "displayMode": { + "title": "Display mode", + "description": "Sets the display mode for the file upload component.", + "type": "string", + "enum": ["simple", "list"] + }, + "hasCustomFileEndings": { + "title": "Has custom file endings", + "description": "Boolean value indicating if the component has valid file endings", + "type": "boolean" + }, + "validFileEndings": { + "title": "Valid file endings", + "description": "A separated string of valid file endings to upload. If not set all endings are accepted.", + "examples": [".csv", ".doc", ".docx", ".gif", ".jpeg", ".pdf", ".txt"], + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } + }, + "required": [ + "displayMode", + "maxFileSizeInMB", + "maxNumberOfAttachments", + "minNumberOfAttachments" + ] + }, + "fileUploadWithTagComponent": { + "allOf": [ + { + "$ref": "#/definitions/fileUploadComponent" + } + ], + "properties": { + "optionsId": { + "type": "string", + "title": "Options ID", + "description": "Reference to connected options by id." + }, + "mapping": { + "$ref": "#/definitions/mapping", + "description": "Optionally used to map options" + } + }, + "required": ["optionsId"] + }, + "datepickerComponent": { + "properties": { + "minDate": { + "type": "string", + "title": "Minimum allowed date", + "description": "Sets the minimum allowed date. Can also use keyword 'today' to disable all past dates dynamically based on the current date. Defaults to 1900-01-01T12:00:00.000Z.", + "default": "1900-01-01T12:00:00.000Z" + }, + "maxDate": { + "type": "string", + "title": "Maximum allowed date", + "description": "Sets the maximum allowed date. Can also use keyword 'today' to disable all future dates dynamically based on the current date. Defaults to 2100-01-01T12:00:00.000Z.", + "default": "2100-01-01T12:00:00.000Z" + }, + "timeStamp": { + "type": "boolean", + "title": "Time stamp", + "description": "Boolean value indicating if the date time should be stored as a timeStamp. Defaults to true.\n If true: 'YYYY-MM-DDThh:mm:ss.sssZ', if false 'YYYY-MM-DD';", + "default": true + }, + "format": { + "type": "string", + "title": "Date format", + "description": "Long date format used when displaying the date to the user. The user date format from the locale will be prioritized over this setting.", + "examples": ["DD/MM/YYYY", "MM/DD/YYYY", "YYYY-MM-DD"], + "default": "DD.MM.YYYY" + } + }, + "required": [] + }, + "navigationButtonsComponent": { + "properties": { + "showBackButton": { + "type": "boolean", + "title": "Show back button", + "description": "Shows two buttons (back/next) instead of just 'next'." + } + } + }, + "navigationBarComponent": { + "properties": { + "compact": { + "type": "boolean", + "title": "Compact navbar menu", + "description": "Change appearance of navbar as compact in desktop view" + } + } + }, + "instanceInformationComponent": { + "properties": { + "elements": { + "title": "Instance information choices", + "description": "The properties to include in the instanceInformation summary", + "type": "object", + "properties": { + "dateSent": { + "title": "Date sent", + "description": "Date when the schema was sent.", + "type": "boolean", + "default": true + }, + "sender": { + "title": "Schema sender", + "description": "The sender of the schema.", + "type": "boolean", + "default": true + }, + "receiver": { + "title": "Schema receiver", + "description": "The receiver of the schema.", + "type": "boolean", + "default": true + }, + "referenceNumber": { + "title": "Schema reference number", + "description": "The reference number of the schema gathered from the instance Guid.", + "type": "boolean", + "default": true + } + } + } + } + }, + "instantiationButtonComponent": { + "properties": { + "mapping": { + "$ref": "#/definitions/mapping", + "description": "Creates a new app instance with data collected from a stateless part of the app." + } + } + }, + "mapComponent": { + "properties": { + "layers": { + "type": "object", + "title": "Layers", + "description": "Map layer", + "required": ["url"], + "properties": { + "url": { + "type": "string", + "title": "Map layer url", + "description": "Url to a map tile. {z}/{x}/{y} will be replaced with tile coordinates, {s} will be replaced with a random subdomain if subdomains are given" + }, + "attribution": { + "type": "string", + "title": "Attribution", + "description": "Ascribing a work or remark to a particular unit for recognition" + }, + "subdomains": { + "type": "array", + "title": "Subdomains", + "description": "List of subdomains. Used for balancing the load on different map tiling servers. A random one will replace {s} in the defined url.", + "items": { + "type": "string" + } + } + } + }, + "centerLocation": { + "type": "object", + "title": "Center location", + "description": "Center location of the map", + "properties": { + "latitude": { + "type": "number", + "title": "latitude", + "description": "Set the latitude coordinate" + }, + "longitude": { + "type": "number", + "title": "longitude", + "description": "Set the longitude coordinate" + } + } + }, + "zoom": { + "type": "number", + "title": "Zoom", + "description": "adjusts the default map-zoom" + } + } + }, + "gridValue": { + "type": "integer", + "maximum": 12, + "minimum": 1, + "examples": [12] + }, + "gridSettings": { + "allOf": [ + { + "$ref": "#/definitions/gridProps" + } + ], + "properties": { + "labelGrid": { + "title": "labelGrid", + "description": "Optional grid for the component label. Used in combination with innerGrid to align labels on the side.", + "examples": [ + { + "xs": 12 + } + ], + "$ref": "#/definitions/gridProps" + }, + "innerGrid": { + "title": "innerGrid", + "description": "Optional grid for inner component content like input field or dropdown. Used to avoid inner content filling the component width.", + "examples": [ + { + "xs": 12 + } + ], + "$ref": "#/definitions/gridProps" + } + } + }, + "gridProps": { + "properties": { + "xs": { + "$ref": "#/definitions/gridValue", + "title": "xs", + "description": "Grid breakpoint at 0px" + }, + "sm": { + "$ref": "#/definitions/gridValue", + "title": "sm", + "description": "Grid breakpoint at 600px" + }, + "md": { + "$ref": "#/definitions/gridValue", + "title": "md", + "description": "Grid breakpoint at 960px" + }, + "lg": { + "$ref": "#/definitions/gridValue", + "title": "lg", + "description": "Grid breakpoint at 1280px" + }, + "xl": { + "$ref": "#/definitions/gridValue", + "title": "xl", + "description": "Grid breakpoint at 1920px" + } + } + }, + "gridComponent": { + "properties": { + "rows": { + "title": "Rows", + "description": "An array of rows to be rendered in the grid.", + "type": "array", + "items": { + "$ref": "#/definitions/gridRow" + } + } + }, + "required": ["rows"] + }, + "gridRow": { + "properties": { + "header": { + "title": "Header?", + "description": "A boolean indicating if the row should be a header row", + "type": "boolean", + "default": false + }, + "readOnly": { + "title": "Read only?", + "description": "A boolean indicating if the row should be a read only row (yellow background)", + "type": "boolean", + "default": false + }, + "cells": { + "title": "Grid row cells", + "description": "An array of cells to be rendered in the row", + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/gridCellText" + }, + { + "$ref": "#/definitions/gridCellComponent" + }, + { + "$ref": "#/definitions/tableColumnOptions" + }, + { + "type": "null", + "title": "Empty cell" + } + ] + } + } + }, + "required": ["cells"] + }, + "gridCellText": { + "properties": { + "text": { + "title": "Text", + "description": "The text or text resource ID to be rendered in the cell", + "type": "string" + } + }, + "$ref": "#/definitions/tableColumnOptions", + "required": ["text"] + }, + "gridCellComponent": { + "properties": { + "component": { + "title": "Component ID", + "description": "The ID of the component to be rendered in the cell", + "type": "string" + } + }, + "$ref": "#/definitions/tableColumnOptions", + "required": ["component"] + }, + "buttonGroupComponent": { + "properties": { + "children": { + "title": "Children", + "description": "An array of the \"id\" of child components belonging to the button group.", + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + "required": ["children"] + }, + "groupComponent": { + "properties": { + "children": { + "title": "Children", + "description": "An array of the \"id\" of child components belonging to the group.", + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "hiddenRow": { + "title": "Hidden row", + "description": "Boolean to decide whether the row should be displayed.", + "$ref": "expression.schema.v1.json#/definitions/boolean" + }, + "edit": { + "title": "Edit", + "description": "Alternatives for edit view of repeating group", + "$ref": "#/definitions/groupEditOptions" + }, + "panel": { + "title": "Panel", + "description": "Alternatives for panel view of repeating group", + "$ref": "#/definitions/groupPanelOptions" + }, + "showGroupingIndicator": { + "title": "Show grouping indicator", + "description": "Boolean to decide whether a vertical line indicating grouping of fields should be visible. Only relevant for non-repeating groups.", + "type": "boolean", + "default": false + }, + "maxCount": { + "type": "integer", + "title": "Maximum count", + "description": "The maximum number of iterations of a group. Only relevant if group is repeating.", + "minimum": 0 + }, + "rowsBefore": { + "title": "Static rows before", + "description": "An array of rows to be rendered before the group table (using Grid component configuration)", + "type": "array", + "items": { + "$ref": "#/definitions/gridRow" + } + }, + "rowsAfter": { + "title": "Static rows after", + "description": "An array of rows to be rendered after the group table (using Grid component configuration)", + "type": "array", + "items": { + "$ref": "#/definitions/gridRow" + } + }, + "tableHeaders": { + "title": "Table Headers", + "description": "An array of the id of child components that should be included as table headers. If not defined all components are shown.", + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + }, + "tableColumns": { + "title": "Table Columns", + "description": "An object containing key-value pairs where the key is a table header and the value is an object containing settings for the headers column", + "type": "object", + "additionalProperties": { + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/tableColumnOptions" + }, + { + "type": "object", + "properties": { + "editInTable": { + "title": "Edit in table", + "description": "Boolean to decide whether the component should be editable in table view", + "default": false, + "type": "boolean" + }, + "showInExpandedEdit": { + "title": "Show in expanded edit", + "description": "Boolean to decide whether the component should be shown in the expanded edit view", + "default": true, + "type": "boolean" + } + } + } + ] + } + } + }, + "required": ["children"] + }, + "groupEditOptions": { + "properties": { + "mode": { + "title": "Edit mode", + "description": "Mode for how repeating group table is displayed in edit mode", + "type": "string", + "enum": ["hideTable", "likert", "showAll", "showTable", "onlyTable"] + }, + "filter": { + "title": "Filter", + "description": "Conditions for filtering visible items in repeating group", + "type": "array", + "items": { + "$ref": "#/definitions/groupFilterItem" + } + }, + "saveButton": { + "title": "Save button", + "description": "Boolean or expression indicating whether save button should be shown or not for a given row", + "$ref": "expression.schema.v1.json#/definitions/boolean" + }, + "saveAndNextButton": { + "title": "Save and open next button", + "description": "Boolean or expression indicating whether save and go to next button should be shown or not in addition to save and close button", + "$ref": "expression.schema.v1.json#/definitions/boolean" + }, + "deleteButton": { + "title": "Delete button", + "description": "Boolean or expression indicating whether delete button should be shown or not for a given row", + "$ref": "expression.schema.v1.json#/definitions/boolean" + }, + "editButton": { + "title": "Edit button", + "description": "Boolean or expression indicating whether edit button should be shown or not for a given row", + "$ref": "expression.schema.v1.json#/definitions/boolean" + }, + "multiPage": { + "title": "Multi-page", + "description": "Boolean value indicating if form components in edit mode should be shown over multiple pages/views.", + "type": "boolean" + }, + "addButton": { + "title": "Add button", + "description": "Boolean or expression indicating whether add new button should be shown or not under the table.", + "$ref": "expression.schema.v1.json#/definitions/boolean" + }, + "alwaysShowAddButton": { + "title": "Show add button on open group", + "description": "Boolean value indicating whether add new button should be shown or not under the table when a group is open.", + "type": "boolean", + "default": false + }, + "openByDefault": { + "title": "Open by default", + "description": "Boolean or string indicating if group should be opened by default. If no items exist: 'first', 'last', and true adds a new item. If items exist already, true does not open anything, but 'first' opens the first item, and 'last' opens the last item in the group.", + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "enum": ["first", "last"] + } + ] + }, + "alertOnDelete": { + "title": "Alert on delete", + "description": "Boolean value indicating if warning popup should be displayed when attempting to delete a row", + "type": "boolean" + } + } + }, + "groupPanelOptions": { + "allOf": [ + { + "$ref": "#/definitions/panelComponent" + } + ], + "properties": { + "iconUrl": { + "title": "Icon url", + "description": "Url of the icon to be shown in panel. Can be relative if hosted by app or full if referencing a cdn or other hosting.", + "type": "string", + "examples": ["fancyIcon.svg", "https://cdn.example.com/fancyIcon.svg"] + }, + "iconAlt": { + "title": "Icon alt", + "description": "Alternative text for the icon. Only applicable if iconUrl is provided. Can be plain text or a text resource reference.", + "type": "string" + }, + "groupReference": { + "title": "Group reference", + "description": "Reference to the group that is being displayed in the panel. Used for referencing another repeating group context.", + "type": "object", + "properties": { + "group": { + "type": "string", + "title": "Group", + "description": "Group reference. Can be either the group id or the group data model binding.", + "examples": ["the-group-id", "some.model.theGroup"] + } + } + } + } + }, + "groupFilterItem": { + "properties": { + "key": { + "title": "Key", + "description": "Key representing field in data model to check.", + "type": "string" + }, + "value": { + "title": "Value", + "description": "Value to check against.", + "type": "string" + } + } + }, + "tableColumnOptions": { + "title": "Column options", + "description": "Column options for specified header.", + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/tableColumnTextOptions" + }, + { + "type": "object", + "properties": { + "width": { + "title": "Width", + "description": "Width of cell in % or 'auto'. Defaults to 'auto'", + "type": "string", + "pattern": "^([0-9]{1,2}%|100%|auto)$" + } + } + } + ] + }, + "tableColumnTextOptions": { + "properties": { + "alignText": { + "title": "Align Text", + "description": "Choose text alignment between 'left', 'center', or 'right' for text in table cells. Defaults to 'left' for text and 'right' for numbers.", + "type": "string", + "enum": ["left", "center", "right"] + }, + "textOverflow": { + "title": "Text Overflow", + "description": "Use this property to controll behaviour when text is too large to be displayed in table cell.", + "properties": { + "lineWrap": { + "title": "Line Wrap", + "description": "Toggle line wrapping on or off. Defaults to true", + "type": "boolean" + }, + "maxHeight": { + "title": "Max Height", + "description": "Determines the number of lines to display in table cell before hiding the rest of the text with an elipsis (...). Defaults to 2.", + "type": "number" + } + } + } + } + }, + "options": { + "properties": { + "label": { + "type": "string", + "title": "Label", + "description": "The option label. Can be plain text or a text resource binding." + }, + "value": { + "type": "string", + "title": "Value", + "description": "The option value." + }, + "description": { + "type": "string", + "title": "Description", + "description": "A description of the option displayed in Radio- and Checkbox groups. Can be plain text or a text resource binding." + }, + "helpText": { + "type": "string", + "title": "Help Text", + "description": "A help text for the option displayed in Radio- and Checkbox groups. Can be plain text or a text resource binding." + } + }, + "required": ["label", "value"] + }, + "triggers": { + "title": "Triggers", + "description": "An array of actions that should be triggered when data connected to this component changes.", + "type": "array", + "items": { + "type": "string", + "enum": [ + "validation", + "validateRow", + "validatePage", + "validateAllPages", + "calculatePageOrder" + ] + } + }, + "selectionComponents": { + "properties": { + "optionsId": { + "type": "string", + "title": "Options ID", + "description": "Reference to connected options by id." + }, + "options": { + "type": "array", + "title": "Options", + "description": "An array of options. Only relevant if no optionsId is set.", + "items": { + "$ref": "#/definitions/options" + } + }, + "preselectedOptionIndex": { + "type": "integer", + "title": "Preselected option index", + "description": "Sets a preselected index.", + "minimum": 0 + }, + "secure": { + "type": "boolean", + "title": "Secure Options", + "description": "Boolean value indicating if the options should be instance aware. Defaults to false. See more on docs: https://docs.altinn.studio/app/development/data/options/" + }, + "source": { + "type": "object", + "title": "Source", + "description": "Object to define a data model source to be used as basis for options. Can not be used if options or optionId is set. See more on docs: https://docs.altinn.studio/app/development/data/options/", + "properties": { + "group": { + "type": "string", + "title": "Group", + "description": "The repeating group to base options on.", + "examples": ["model.some.group"] + }, + "label": { + "type": "string", + "title": "Label", + "description": "Reference to a text resource to be used as the option label.", + "examples": ["some.text.key"] + }, + "value": { + "type": "string", + "title": "Label", + "description": "Field in the group that should be used as value", + "examples": ["model.some.group[{0}].someField"] + }, + "description": { + "type": "string", + "title": "Description", + "description": "A description of the option displayed in Radio- and Checkbox groups. Can be plain text or a text resource binding.", + "examples": ["some.text.key", "My Description"] + }, + "helpText": { + "type": "string", + "title": "Help Text", + "description": "A help text for the option displayed in Radio- and Checkbox groups. Can be plain text or a text resource binding.", + "examples": ["some.text.key", "My Help Text"] + } + }, + "required": ["group", "label", "value"] + }, + "mapping": { + "$ref": "#/definitions/mapping", + "description": "Optionally used to map options" + }, + "autocomplete": { + "$ref": "#/definitions/autocomplete" + } + } + }, + "radioAndCheckboxComponents": { + "allOf": [ + { + "$ref": "#/definitions/selectionComponents" + } + ], + "properties": { + "layout": { + "type": "string", + "enum": ["column", "row", "table"], + "title": "Layout", + "description": "Define the layout style for the options" + } + } + }, + "linkComponent": { + "properties": { + "style": { + "type": "string", + "title": "Style", + "description": "The style of the button", + "enum": ["primary", "secondary", "link"] + }, + "openInNewTab": { + "type": "boolean", + "title": "Open in new tab", + "description": "Boolean value indicating if the link should be opened in a new tab. Defaults to false." + } + }, + "required": ["style"] + }, + "addressComponent": { + "properties": { + "simplified": { + "type": "boolean", + "title": "Simplified", + "description": "Boolean value indicating if the address component should be shown in simple mode.", + "default": false + }, + "saveWhileTyping": { + "$ref": "#/definitions/saveWhileTyping" + } + } + }, + "customComponent": { + "properties": { + "tagName": { + "type": "string", + "title": "Tag name", + "description": "Web component tag name to use" + } + }, + "required": ["tagName"] + }, + "actionButtonComponent": { + "properties": { + "action": { + "type": "string", + "title": "Action", + "description": "The action to be triggered when the button is clicked.", + "examples": ["sign, confirm, reject"] + }, + "buttonStyle": { + "type": "string", + "title": "Button style", + "description": "The style of the button.", + "enum": ["primary", "secondary"] + } + } + }, + "summaryComponent": { + "properties": { + "componentRef": { + "type": "string", + "title": "Component reference", + "description": "String value indicating which layout component (by ID) the summary is for." + }, + "pageRef": { + "type": "string", + "title": "Page reference", + "description": "String value indicating which layout page the referenced component is defined on." + }, + "largeGroup": { + "type": "boolean", + "title": "Large group", + "description": "Boolean value indicating if summary of repeating group should be displayed in large format. Useful for displaying summary with nested groups." + }, + "excludedChildren": { + "type": "array", + "title": "Excluded child components", + "description": "Array of component ids that should not be shown in a repeating group's summary" + }, + "display": { + "type": "object", + "title": "Display properties", + "description": "Optional properties to configure how summary is displayed", + "properties": { + "hideChangeButton": { + "type": "boolean", + "title": "Hide change button", + "description": "Set to true if the change button should be hidden for the summary component. False by default." + }, + "hideBottomBorder": { + "type": "boolean", + "title": "Hide bottom border", + "description": "Set to true to hide the blue dashed border below the summary component. False by default." + }, + "useComponentGrid": { + "type": "boolean", + "title": "Use component grid", + "description": "Set to true to allow summary component to use the grid setup of the referenced component. For group summary, this will apply for all group child components." + } + } + } + } + }, + "attachmentListComponent": { + "properties": { + "dataTypeIds": { + "type": "array", + "items": { + "type": "string" + }, + "title": "Data type IDs", + "description": "List of data type IDs for the attachment list to show.", + "examples": [["SomeDataType", "SomeOtherDataType"]] + }, + "includePDF": { + "type": "boolean", + "title": "Include PDF as attachments", + "description": "Set the flag if the list of attachments should include PDF of answers.", + "default": false + } + } + }, + "imageComponent": { + "properties": { + "image": { + "type": "object", + "title": "Image properties", + "description": "Set of options for image field.", + "properties": { + "src": { + "title": "Image source", + "description": "", + "type": "object", + "properties": { + "nb": { + "type": "string", + "title": "Bokmål" + }, + "nn": { + "type": "string", + "title": "Nynorsk" + }, + "en": { + "type": "string", + "title": "English" + } + }, + "additionalProperties": true + }, + "width": { + "type": "string", + "title": "Image width", + "examples": ["100%"] + }, + "align": { + "type": "string", + "title": "Align image", + "enum": [ + "flex-start", + "center", + "flex-end", + "space-between", + "space-around", + "space-evenly" + ] + } + }, + "required": ["src", "width", "align"] + } + } + }, + "inputComponent": { + "properties": { + "formatting": { + "title": "Input formatting", + "description": "Set of options for formatting input fields.", + "$ref": "#/definitions/inputFormatting" + }, + "saveWhileTyping": { + "$ref": "#/definitions/saveWhileTyping" + }, + "variant": { + "type": "string", + "title": "Input Variant", + "description": "An enum to choose if the inputfield it is a normal textfield or a searchbar", + "enum": ["text", "search"] + }, + "autocomplete": { + "$ref": "#/definitions/autocomplete" + }, + "maxLength": { + "$ref": "#/definitions/maxLength" + } + } + }, + "textAreaComponent": { + "properties": { + "saveWhileTyping": { + "$ref": "#/definitions/saveWhileTyping" + }, + "autocomplete": { + "$ref": "#/definitions/autocomplete" + }, + "maxLength": { + "$ref": "#/definitions/maxLength" + } + } + }, + "saveWhileTyping": { + "title": "Automatic saving while typing", + "description": "Boolean or number. True = feature on (default), false = feature off (saves on focus blur), number = timeout in milliseconds (400 by default)", + "default": true, + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "number" + } + ] + }, + "maxLength": { + "title": "Maximum length", + "description": "Maximum length of input field", + "type": "number" + }, + "inputFormatting": { + "type": "object", + "properties": { + "currency": { + "type": "string", + "title": "Language-sensitive number formatting based on currency", + "description": "Enables currency along with thousand and decimal separators to be language sensitive based on selected app language. They are configured in number property. Note: parts that already exist in number property are not overridden by this prop.", + "enum": [ + "AED", + "AFN", + "ALL", + "AMD", + "ANG", + "AOA", + "ARS", + "AUD", + "AWG", + "AZN", + "BAM", + "BBD", + "BDT", + "BGN", + "BHD", + "BIF", + "BMD", + "BND", + "BOB", + "BOV", + "BRL", + "BSD", + "BTN", + "BWP", + "BYN", + "BZD", + "CAD", + "CDF", + "CHE", + "CHF", + "CHW", + "CLF", + "CLP", + "CNY", + "COP", + "COU", + "CRC", + "CUC", + "CUP", + "CVE", + "CZK", + "DJF", + "DKK", + "DOP", + "DZD", + "EGP", + "ERN", + "ETB", + "EUR", + "FJD", + "FKP", + "GBP", + "GEL", + "GHS", + "GIP", + "GMD", + "GNF", + "GTQ", + "GYD", + "HKD", + "HNL", + "HTG", + "HUF", + "IDR", + "ILS", + "INR", + "IQD", + "IRR", + "ISK", + "JMD", + "JOD", + "JPY", + "KES", + "KGS", + "KHR", + "KMF", + "KPW", + "KRW", + "KWD", + "KYD", + "KZT", + "LAK", + "LBP", + "LKR", + "LRD", + "LSL", + "LYD", + "MAD", + "MDL", + "MGA", + "MKD", + "MMK", + "MNT", + "MOP", + "MRU", + "MUR", + "MVR", + "MWK", + "MXN", + "MXV", + "MYR", + "MZN", + "NAD", + "NGN", + "NIO", + "NOK", + "NPR", + "NZD", + "OMR", + "PAB", + "PEN", + "PGK", + "PHP", + "PKR", + "PLN", + "PYG", + "QAR", + "RON", + "RSD", + "RUB", + "RWF", + "SAR", + "SBD", + "SCR", + "SDG", + "SEK", + "SGD", + "SHP", + "SLE", + "SLL", + "SOS", + "SRD", + "SSP", + "STN", + "SVC", + "SYP", + "SZL", + "THB", + "TJS", + "TMT", + "TND", + "TOP", + "TRY", + "TTD", + "TWD", + "TZS", + "UAH", + "UGX", + "USD", + "USN", + "UYI", + "UYU", + "UYW", + "UZS", + "VED", + "VES", + "VND", + "VUV", + "WST", + "XAF", + "XCD", + "XDR", + "XOF", + "XPF", + "XSU", + "XUA", + "YER", + "ZAR", + "ZMW", + "ZWL" + ] + }, + "unit": { + "title": "Language-sensitive number formatting based on unit", + "type": "string", + "description": "Enables unit along with thousand and decimal separators to be language sensitive based on selected app language. They are configured in number property. Note: parts that already exist in number property are not overridden by this prop.", + "enum": [ + "celsius", + "centimeter", + "day", + "degree", + "foot", + "gram", + "hectare", + "hour", + "inch", + "kilogram", + "kilometer", + "liter", + "meter", + "milliliter", + "millimeter", + "millisecond", + "minute", + "month", + "percent", + "second", + "week", + "year" + ] + }, + "position": { + "type": "string", + "title": "Position of the currency/unit symbol (only when using currency or unit options)", + "description": "Display the unit as prefix or suffix. Default is prefix", + "enum": ["prefix", "suffix"] + }, + "number": { + "$ref": "https://altinncdn.no/schemas/json/component/number-format.schema.v1.json" + }, + "align": { + "type": "string", + "title": "Align input", + "description": "The alignment for Input field (eg. right aligning a series of numbers).", + "enum": ["left", "center", "right"] + } + } + }, + "mapping": { + "type": "object", + "title": "Mapping", + "examples": [ + { + "some.source.field": "key1", + "some.other.source": "key2" + } + ], + "additionalProperties": { + "type": "string" + } + }, + "iframeComponent": { + "type": "object", + "properties": { + "sandbox": { + "type": "object", + "title": "Sandbox", + "description": "Controls the sandbox attribute on the iframe. See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox", + "properties": { + "allowPopups": { + "type": "boolean", + "title": "Allow popups", + "description": "Sets \"allow-popups\" in the sandbox attribute on the iframe. See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox", + "default": false + }, + "allowPopupsToEscapeSandbox": { + "type": "boolean", + "title": "Allow popups to escape sandbox", + "description": "Sets \"allow-popups-to-escape-sandbox\" in the sandbox attribute on the iframe. See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox", + "default": false + } + } + } + } + }, + "listComponent": { + "type": "object", + "properties": { + "tableHeaders": { + "type": "object", + "title": "Table Headers", + "examples": [ + { + "productId": "product.id", + "description": "Beskrivelse av produkt" + } + ], + "description": "An object where the fields in the datalist is mapped to headers. Must correspond to datalist representing a row. Can be added to the resource files to change between languages." + }, + "tableHeadersMobile": { + "type": "array", + "items": { + "type": "string" + }, + "title": "Table Headers Mobile", + "description": "An array of strings representing the columns that is chosen to be shown in the mobile view." + }, + "sortableColumns": { + "type": "array", + "items": { + "type": "string" + }, + "title": "Sortable Columns", + "description": "An array of the columns that is going to be sortable. The column has to be represented by the the headername that is written in tableHeaders." + }, + "pagination": { + "title": "Pagination", + "$ref": "#/definitions/paginationProperties" + }, + "dataListId": { + "type": "string", + "title": "List ID", + "description": "The Id of the list. This id is used to retrive the datalist from the backend." + }, + "secure": { + "type": "boolean", + "title": "Secure ListItems", + "description": "Boolean value indicating if the options should be instance aware. Defaults to false." + }, + "bindingToShowInSummary": { + "type": "string", + "title": "Binding to show in summary", + "description": "The value of this binding will be shown in the summary component for the list. This binding must be one of the specified bindings under dataModelBindings." + }, + "mapping": { + "$ref": "#/definitions/mapping", + "description": "(Optional) Used to map data model paths to query parameters when fetching list data." + } + }, + "required": ["dataListId"] + }, + "paginationProperties": { + "type": "object", + "properties": { + "alternatives": { + "type": "array", + "items": { + "type": "number" + }, + "title": "Alternatives", + "description": "List of page sizes the user can choose from. Make sure to test the performance of the largest number of items per page you are allowing." + }, + "default": { + "type": "number", + "title": "Default", + "description": "The pagination size that is set to default." + } + }, + "required": ["alternatives", "default"] + }, + "pageBreak": { + "type": "object", + "properties": { + "breakBefore": { + "title": "Page break before", + "description": "PDF only: Value or expression indicating whether a page break should be added before the component. Can be either: 'auto' (default), 'always', or 'avoid'.", + "examples": ["auto", "always", "avoid"], + "default": "auto", + "$ref": "expression.schema.v1.json#/definitions/string" + }, + "breakAfter": { + "title": "Page break after", + "description": "PDF only: Value or expression indicating whether a page break should be added after the component. Can be either: 'auto' (default), 'always', or 'avoid'.", + "examples": ["auto", "always", "avoid"], + "default": "auto", + "$ref": "expression.schema.v1.json#/definitions/string" + } + } + }, + "autocomplete": { + "title": "HTML attribute: autocomplete", + "description": "The HTML autocomplete attribute lets web developers specify what if any permission the user agent has to provide automated assistance in filling out form field values, as well as guidance to the browser as to the type of information expected in the field.", + "type": "string", + "enum": [ + "on", + "off", + "name", + "honorific-prefix", + "given-name", + "additional-name", + "family-name", + "honorific-suffix", + "nickname", + "email", + "username", + "new-password", + "current-password", + "one-time-code", + "organization-title", + "organization", + "street-address", + "address-line1", + "address-line2", + "address-line3", + "address-level4", + "address-level3", + "address-level2", + "address-level1", + "country", + "country-name", + "postal-code", + "cc-name", + "cc-given-name", + "cc-additional-name", + "cc-family-name", + "cc-number", + "cc-exp", + "cc-exp-month", + "cc-exp-year", + "cc-csc", + "cc-type", + "transaction-currency", + "transaction-amount", + "language", + "bday", + "bday-day", + "bday-month", + "bday-year", + "sex", + "tel", + "tel-country-code", + "tel-national", + "tel-area-code", + "tel-local", + "tel-extension", + "url", + "photo" + ] + } + } +} diff --git a/frontend/packages/shared/src/utils/formValidationUtils/test-data/number-format.schema.v1.json b/frontend/packages/shared/src/utils/formValidationUtils/test-data/number-format.schema.v1.json new file mode 100644 index 00000000000..608f11b1389 --- /dev/null +++ b/frontend/packages/shared/src/utils/formValidationUtils/test-data/number-format.schema.v1.json @@ -0,0 +1,169 @@ +{ + "$id": "https://altinncdn.no/schemas/json/component/number-format.schema.v1.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Input number formatting", + "description": "Schema that describes the options that can be configured for number formatting on an `input` component, based on react-number-format package. For complete list of available options, see https://s-yadav.github.io/react-number-format/docs/props", + "type": "object", + "additionalProperties": true, + "anyOf": [ + { + "properties": { + "allowedDecimalSeparators": { + "title": "Allowed decimal separators", + "description": "Characters which when pressed result in a decimal separator. When missing, decimalSeparator and '.' are used", + "type": "array", + "items": { + "type": "string", + "maxLength": 1 + }, + "examples": [[",", ".", "/"]] + }, + "allowEmptyFormatting": { + "type": "boolean", + "default": false + }, + "allowLeadingZeros": { + "title": "Allow leading zeros", + "description": "Allow leading zeros at beginning of number", + "type": "boolean", + "default": false + }, + "allowNegative": { + "title": "Allow negative", + "description": "Allow negative numbers (Only when format option is not provided)", + "type": "boolean", + "default": true + }, + "decimalScale": { + "title": "Decimal scale", + "description": "If defined it limits to given decimal scale.", + "type": "number", + "examples": [1, 2, 3] + }, + "decimalSeparator": { + "title": "Decimal separator", + "description": "Support decimal point on a number. Single character string.", + "type": "string", + "maxLength": 1, + "default": "." + }, + "fixedDecimalScale": { + "title": "Fixed decimal scale", + "description": "Used together with decimalScale. If true it adds 0s to match given decimal scale.", + "type": "boolean", + "default": false + }, + "format": { + "type": "boolean", + "default": false + }, + "mask": { + "type": "boolean", + "default": false + }, + "prefix": { + "title": "Prefix", + "description": "Add a prefix before the number", + "type": "string", + "examples": ["$", "kr", "-", "(+47) "] + }, + "suffix": { + "title": "Suffix", + "description": "Add a suffix after the number", + "type": "string", + "examples": ["%", "kr", "kg"] + }, + "thousandSeparator": { + "title": "Thousand separator", + "description": "Add thousand separators on number. Single character string or boolean true (true is default to ,)", + "type": ["string", "boolean"], + "maxLength": 1, + "examples": [true, ",", "."] + } + }, + "if": { + "required": ["fixedDecimalScale"], + "properties": { + "fixedDecimalScale": { + "const": true + } + } + }, + "then": { + "required": ["decimalScale"] + } + }, + { + "properties": { + "allowedDecimalSeparators": { + "type": "boolean", + "default": false + }, + "allowEmptyFormatting": { + "title": "Allow empty formatting", + "description": "Apply formatting to empty inputs", + "type": "boolean", + "default": false + }, + "allowLeadingZeros": { + "type": "boolean", + "default": false + }, + "allowNegative": { + "type": "boolean", + "default": false + }, + "decimalScale": { + "type": "boolean", + "default": false + }, + "decimalSeparator": { + "type": "boolean", + "default": false + }, + "fixedDecimalScale": { + "type": "boolean", + "default": false + }, + "format": { + "title": "Format", + "description": "Format given as hash string, to allow number input in place of hash.", + "type": "string", + "examples": ["### ### ###", "+47 ### ## ###", "##-##-##-##"] + }, + "mask": { + "title": "Mask", + "description": "Mask to show in place of non-entered values", + "type": "string", + "examples": ["_"], + "default": " " + }, + "prefix": { + "type": "boolean", + "default": false + }, + "suffix": { + "type": "boolean", + "default": false + }, + "thousandSeparator": { + "type": "boolean", + "default": false + } + }, + "if": { + "anyOf": [ + { + "required": ["mask"] + }, + { + "required": ["allowEmptyFormatting"] + } + ] + }, + "then": { + "required": ["format"] + } + } + ] +} diff --git a/frontend/packages/text-editor/src/TextEntry.test.tsx b/frontend/packages/text-editor/src/TextEntry.test.tsx index 4c8781c756c..c50141d1a0b 100644 --- a/frontend/packages/text-editor/src/TextEntry.test.tsx +++ b/frontend/packages/text-editor/src/TextEntry.test.tsx @@ -1,8 +1,7 @@ import React from 'react'; -import { act, screen } from '@testing-library/react'; +import { act, screen, render as renderRtl } from '@testing-library/react'; import type { TextEntryProps } from './TextEntry'; import { TextEntry } from './TextEntry'; -import { renderWithMockStore } from '../../ux-editor/src/testing/mocks'; import userEvent from '@testing-library/user-event'; import { textMock } from '../../../testing/mocks/i18nMock'; @@ -66,5 +65,5 @@ const render = async (props: Partial = {}) => { ...props, }; - return renderWithMockStore()(); + return renderRtl(); }; diff --git a/frontend/packages/ux-editor/src/App.tsx b/frontend/packages/ux-editor/src/App.tsx index b8509c81b9f..dbf33f9da21 100644 --- a/frontend/packages/ux-editor/src/App.tsx +++ b/frontend/packages/ux-editor/src/App.tsx @@ -11,7 +11,7 @@ import { useTextResourcesQuery } from 'app-shared/hooks/queries/useTextResources import { useLayoutSetsQuery } from './hooks/queries/useLayoutSetsQuery'; import { useStudioUrlParams } from 'app-shared/hooks/useStudioUrlParams'; import { useAppContext } from './hooks/useAppContext'; -import { FormContextProvider } from '../../ux-editor/src/containers/FormContext'; +import { FormContextProvider } from './containers/FormContext'; import { shouldDisplayFeature } from 'app-shared/utils/featureToggleUtils'; import { UnsupportedVersionMessage } from './components/UnsupportedVersionMessage'; import { useAppVersionQuery } from 'app-shared/hooks/queries/useAppVersionQuery'; diff --git a/frontend/packages/ux-editor/src/hooks/queries/useComponentSchemaQuery.ts b/frontend/packages/ux-editor/src/hooks/queries/useComponentSchemaQuery.ts index a71265b83a7..81501796a6e 100644 --- a/frontend/packages/ux-editor/src/hooks/queries/useComponentSchemaQuery.ts +++ b/frontend/packages/ux-editor/src/hooks/queries/useComponentSchemaQuery.ts @@ -1,7 +1,7 @@ // import { useServicesContext } from 'app-shared/contexts/ServicesContext'; import type { UseQueryResult } from '@tanstack/react-query'; import { useQuery, useQueryClient } from '@tanstack/react-query'; -import { addSchemas, dereferenceSchema } from '../../utils/formValidationUtils'; +import { addSchemas, dereferenceSchema } from 'app-shared/utils/formValidationUtils'; import { componentSchemaMocks } from '../../testing/componentSchemaMocks'; import { QueryKey } from 'app-shared/types/QueryKey'; diff --git a/frontend/packages/ux-editor/src/hooks/queries/useLayoutSchemaQuery.ts b/frontend/packages/ux-editor/src/hooks/queries/useLayoutSchemaQuery.ts index 3c730553faa..1b3812a8044 100644 --- a/frontend/packages/ux-editor/src/hooks/queries/useLayoutSchemaQuery.ts +++ b/frontend/packages/ux-editor/src/hooks/queries/useLayoutSchemaQuery.ts @@ -1,7 +1,7 @@ // import { useServicesContext } from 'app-shared/contexts/ServicesContext'; import type { UseQueryResult } from '@tanstack/react-query'; import { useQueries, useQueryClient } from '@tanstack/react-query'; -import { addSchemas } from '../../utils/formValidationUtils'; +import { addSchemas } from 'app-shared/utils/formValidationUtils'; import expressionSchema from '../../testing/schemas/json/layout/expression.schema.v1.json'; import numberFormatSchema from '../../testing/schemas/json/layout/number-format.schema.v1.json';