From 112d005535e93daec85b0ab79a805f72216fccac Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Mon, 15 Apr 2024 12:12:02 +0200 Subject: [PATCH 01/17] Start updating types, add DISCLOSURE field --- frontend/src/js/external-forms/config-types.ts | 11 ++++++++++- frontend/src/js/external-forms/form/Field.tsx | 13 +++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/frontend/src/js/external-forms/config-types.ts b/frontend/src/js/external-forms/config-types.ts index c10b8f37ba..48a06a5fc1 100644 --- a/frontend/src/js/external-forms/config-types.ts +++ b/frontend/src/js/external-forms/config-types.ts @@ -12,7 +12,7 @@ interface TranslatableString { export type Forms = Form[]; -export type FormField = Field | Tabs | Group; +export type FormField = Field | Tabs | Group | Disclosure; export type NonFormField = Headline | Description; export type GeneralField = FormField | NonFormField; @@ -36,6 +36,15 @@ export interface Group { fields: GeneralField[]; } +export interface Disclosure { + type: "DISCLOSURE"; + name: string; + creatable?: boolean; + defaultOpen?: boolean; + label: TranslatableString; + fields: GeneralField[]; +} + export interface Tabs { name: string; // Sent to backend API type: "TABS"; diff --git a/frontend/src/js/external-forms/form/Field.tsx b/frontend/src/js/external-forms/form/Field.tsx index 6e41cecf0c..13dcd099cd 100644 --- a/frontend/src/js/external-forms/form/Field.tsx +++ b/frontend/src/js/external-forms/form/Field.tsx @@ -388,6 +388,19 @@ const Field = ({ }} ); + case "DISCLOSURE": + return ( +
+ + {field.label[locale]} + + {field.fields.map((f, i) => { + const key = getFieldKey(formType, f, i); + + return ; + })} +
+ ); case "GROUP": return ( <> From 2f9c91b09d64730075b1a56c4f9079cf52b48d5b Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Mon, 15 Apr 2024 15:31:43 +0200 Subject: [PATCH 02/17] Extract out all the fields --- frontend/mock-api/forms/export-form.json | 32 ++ frontend/mock-api/mockApi.ts | 12 +- .../src/js/external-forms/config-types.ts | 16 +- .../js/external-forms/form/ConnectedField.tsx | 112 ++++ frontend/src/js/external-forms/form/Field.tsx | 542 +++--------------- .../form/fields/CheckboxField.tsx | 33 ++ .../form/fields/ConceptListField.tsx | 115 ++++ .../form/fields/DatasetSelectField.tsx | 41 ++ .../form/fields/DateRangeField.tsx | 36 ++ .../form/fields/DisclosureField.tsx | 68 +++ .../external-forms/form/fields/GroupField.tsx | 43 ++ .../form/fields/HeadlineField.tsx | 24 + .../form/fields/NumberField.tsx | 41 ++ .../form/fields/ResultGroupField.tsx | 34 ++ .../form/fields/SelectField.tsx | 37 ++ .../form/fields/StringField.tsx | 36 ++ .../external-forms/form/fields/TabsField.tsx | 75 +++ .../form/fields/TextAreaField.tsx | 37 ++ 18 files changed, 844 insertions(+), 490 deletions(-) create mode 100644 frontend/src/js/external-forms/form/ConnectedField.tsx create mode 100644 frontend/src/js/external-forms/form/fields/CheckboxField.tsx create mode 100644 frontend/src/js/external-forms/form/fields/ConceptListField.tsx create mode 100644 frontend/src/js/external-forms/form/fields/DatasetSelectField.tsx create mode 100644 frontend/src/js/external-forms/form/fields/DateRangeField.tsx create mode 100644 frontend/src/js/external-forms/form/fields/DisclosureField.tsx create mode 100644 frontend/src/js/external-forms/form/fields/GroupField.tsx create mode 100644 frontend/src/js/external-forms/form/fields/HeadlineField.tsx create mode 100644 frontend/src/js/external-forms/form/fields/NumberField.tsx create mode 100644 frontend/src/js/external-forms/form/fields/ResultGroupField.tsx create mode 100644 frontend/src/js/external-forms/form/fields/SelectField.tsx create mode 100644 frontend/src/js/external-forms/form/fields/StringField.tsx create mode 100644 frontend/src/js/external-forms/form/fields/TabsField.tsx create mode 100644 frontend/src/js/external-forms/form/fields/TextAreaField.tsx diff --git a/frontend/mock-api/forms/export-form.json b/frontend/mock-api/forms/export-form.json index 789479e699..24f0027633 100644 --- a/frontend/mock-api/forms/export-form.json +++ b/frontend/mock-api/forms/export-form.json @@ -60,6 +60,38 @@ }, "validations": ["NOT_EMPTY"] }, + { + "name": "theDisclosure", + "type": "DISCLOSURE", + "label": { + "de": "Datenschutz" + }, + "creatable": true, + "fields": [ + { + "type": "HEADLINE", + "label": { + "de": "Datenschutztext" + } + }, + { + "type": "CHECKBOX", + "name": "disclosureCheckbox", + "label": { + "de": "Ich habe den Datenschutztext gelesen und bin einverstanden." + } + }, + { + "type": "NUMBER", + "name": "timesIlike", + "label": { + "de": "Gefällt mir SO sehr" + }, + "defaultValue": 1, + "min": 1 + } + ] + }, { "name": "timeMode", "type": "TABS", diff --git a/frontend/mock-api/mockApi.ts b/frontend/mock-api/mockApi.ts index 81f6ff56b8..48ffa61c90 100644 --- a/frontend/mock-api/mockApi.ts +++ b/frontend/mock-api/mockApi.ts @@ -21,9 +21,9 @@ const chance = new Chance(); // Taken from: // http://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array function shuffleArray(array: T[]) { - for (var i = array.length - 1; i > 0; i--) { - var j = Math.floor(Math.random() * (i + 1)); - var temp = array[i]; + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + const temp = array[i]; array[i] = array[j]; array[j] = temp; } @@ -244,7 +244,7 @@ export default function mockApi(app: Application) { setTimeout(() => { const ids: unknown[] = []; - const possibleTagsWithProbabilities = [ + const possibleTagsWithProbabilities: [string, number][] = [ ["research", 0.3], ["fun", 0.02], ["test", 0.02], @@ -257,7 +257,7 @@ export default function mockApi(app: Application) { ["Another very long long tagname, 2020", 0.001], ]; - for (var i = 24700; i < 25700; i++) { + for (let i = 24700; i < 25700; i++) { const notExecuted = Math.random() < 0.1; ids.push({ @@ -548,7 +548,7 @@ export default function mockApi(app: Application) { "interesting", ]; - for (var i = 84600; i < 85600; i++) { + for (let i = 84600; i < 85600; i++) { configs.push({ id: i, label: "Saved Config", diff --git a/frontend/src/js/external-forms/config-types.ts b/frontend/src/js/external-forms/config-types.ts index 48a06a5fc1..880161939d 100644 --- a/frontend/src/js/external-forms/config-types.ts +++ b/frontend/src/js/external-forms/config-types.ts @@ -99,7 +99,7 @@ export interface Headline { /* ------------------------------ */ -interface Description { +export interface Description { type: "DESCRIPTION"; label: TranslatableString; } @@ -114,7 +114,7 @@ export type CheckboxField = CommonField & { /* ------------------------------ */ type StringFieldValidation = NOT_EMPTY_VALIDATION; -type StringField = CommonField & { +export type StringField = CommonField & { type: "STRING"; placeholder?: TranslatableString; defaultValue?: string; // Default: "" @@ -128,7 +128,7 @@ type StringField = CommonField & { /* ------------------------------ */ type TextareaFieldValidation = NOT_EMPTY_VALIDATION; -type TextareaField = CommonField & { +export type TextareaField = CommonField & { type: "TEXTAREA"; placeholder?: TranslatableString; defaultValue?: string; // Default: "" @@ -143,7 +143,7 @@ type TextareaField = CommonField & { type NumberFieldValidation = | NOT_EMPTY_VALIDATION | GREATER_THAN_ZERO_VALIDATION; -type NumberField = CommonField & { +export type NumberField = CommonField & { type: "NUMBER"; defaultValue?: number; // Default: null placeholder?: TranslatableString; @@ -162,13 +162,13 @@ type SelectOption = { value: SelectValue; }; type SelectFieldValidation = NOT_EMPTY_VALIDATION; -type SelectField = CommonField & { +export type SelectField = CommonField & { type: "SELECT"; options: SelectOption[]; defaultValue?: SelectValue; validations?: SelectFieldValidation[]; }; -type DatasetSelectField = CommonField & { +export type DatasetSelectField = CommonField & { type: "DATASET_SELECT"; validations?: SelectFieldValidation[]; }; @@ -186,7 +186,7 @@ type DatasetSelectField = CommonField & { /* ------------------------------ */ type DateRangeFieldValidation = NOT_EMPTY_VALIDATION; -type DateRangeField = CommonField & { +export type DateRangeField = CommonField & { type: "DATE_RANGE"; validations?: DateRangeFieldValidation[]; }; @@ -194,7 +194,7 @@ type DateRangeField = CommonField & { /* ------------------------------ */ type ResultGroupFieldValidation = NOT_EMPTY_VALIDATION; -type ResultGroupField = CommonField & { +export type ResultGroupField = CommonField & { type: "RESULT_GROUP"; dropzoneLabel: TranslatableString; validations?: ResultGroupFieldValidation[]; diff --git a/frontend/src/js/external-forms/form/ConnectedField.tsx b/frontend/src/js/external-forms/form/ConnectedField.tsx new file mode 100644 index 0000000000..c8bb84c5de --- /dev/null +++ b/frontend/src/js/external-forms/form/ConnectedField.tsx @@ -0,0 +1,112 @@ +import styled from "@emotion/styled"; +import { ReactNode } from "react"; +import { Control, ControllerRenderProps, useController } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import { exists } from "../../common/helpers/exists"; +import { Field, Tabs } from "../config-types"; +import { getErrorForField } from "../validators"; +import { DynamicFormValues } from "./Form"; + +// TODO: REFINE COLORS +// const useColorByField = (fieldType: FormField["type"]) => { +// const theme = useTheme(); + +// const COLOR_BY_FIELD_TYPE: Record = useMemo( +// () => ({ +// STRING: theme.col.palette[8], +// DATE_RANGE: theme.col.palette[0], +// NUMBER: theme.col.palette[1], +// CONCEPT_LIST: theme.col.palette[2], +// SELECT: theme.col.palette[3], +// DATASET_SELECT: theme.col.palette[4], +// CHECKBOX: theme.col.palette[7], +// RESULT_GROUP: theme.col.palette[5], +// TABS: theme.col.palette[9], +// }), +// [theme], +// ); + +// return COLOR_BY_FIELD_TYPE[fieldType]; +// }; + +type Props = T & { + children: (props: ControllerRenderProps) => ReactNode; + control: Control; + formField: Field | Tabs; + defaultValue?: unknown; + noContainer?: boolean; + noLabel?: boolean; +}; +const FieldContainer = styled("div")<{ + noLabel?: boolean; + hasError?: boolean; + red?: boolean; +}>` + display: flex; + flex-direction: column; + gap: 5px; + padding: ${({ noLabel }) => (noLabel ? "7px 10px" : "2px 10px 7px")}; + background-color: white; + border-radius: ${({ theme }) => theme.borderRadius}; + border: 1px solid + ${({ theme, hasError, red }) => + hasError + ? red + ? theme.col.red + : theme.col.blueGrayDark + : theme.col.grayLight}; +`; + +const ErrorContainer = styled("div")<{ red?: boolean }>` + color: ${({ theme, red }) => (red ? theme.col.red : theme.col.blueGrayDark)}; + font-weight: 700; + font-size: ${({ theme }) => theme.font.sm}; +`; + +export const setValueConfig = { + shouldValidate: true, + shouldDirty: true, + shouldTouch: true, +}; + +export const ConnectedField = ({ + children, + control, + formField, + defaultValue, + noContainer, + noLabel, + ...props +}: Props) => { + const { t } = useTranslation(); + const { field, fieldState } = useController({ + name: formField.name, + defaultValue, + control, + rules: { + validate: (value) => getErrorForField(t, formField, value) || true, + }, + shouldUnregister: false, + }); + + // TODO: REFINE COLORS + // const color = useColorByField(formField.type); + + const requiredMsg = t("externalForms.formValidation.isRequired"); + const isRedError = fieldState.error?.message !== requiredMsg; + + return noContainer ? ( +
{children({ ...field, ...props })}
+ ) : ( + + {children({ ...field, ...props })} + + {fieldState.error?.message} + + + ); +}; diff --git a/frontend/src/js/external-forms/form/Field.tsx b/frontend/src/js/external-forms/form/Field.tsx index 13dcd099cd..8e0cf57680 100644 --- a/frontend/src/js/external-forms/form/Field.tsx +++ b/frontend/src/js/external-forms/form/Field.tsx @@ -1,170 +1,27 @@ -import styled from "@emotion/styled"; -import { memo, ReactNode } from "react"; -import { - Control, - ControllerRenderProps, - useController, - UseFormRegister, - UseFormSetValue, -} from "react-hook-form"; -import { useTranslation } from "react-i18next"; +import { memo } from "react"; +import { Control, UseFormRegister, UseFormSetValue } from "react-hook-form"; import type { SelectOptionT } from "../../api/types"; -import type { DateStringMinMax } from "../../common/helpers/dateHelper"; -import { exists } from "../../common/helpers/exists"; import { useDatasetId } from "../../dataset/selectors"; import type { Language } from "../../localization/useActiveLang"; -import { nodeIsInvalid } from "../../model/node"; -import type { DragItemQuery } from "../../standard-query-editor/types"; -import InputCheckbox from "../../ui-components/InputCheckbox"; -import InputDateRange from "../../ui-components/InputDateRange"; -import InputPlain from "../../ui-components/InputPlain/InputPlain"; -import InputSelect from "../../ui-components/InputSelect/InputSelect"; -import { InputTextarea } from "../../ui-components/InputTextarea/InputTextarea"; -import ToggleButton from "../../ui-components/ToggleButton"; -import type { Field as FieldT, GeneralField, Tabs } from "../config-types"; +import type { GeneralField } from "../config-types"; import { Description } from "../form-components/Description"; -import { - getHeadlineFieldAs, - Headline, - HeadlineIndex, -} from "../form-components/Headline"; -import FormConceptGroup from "../form-concept-group/FormConceptGroup"; -import type { FormConceptGroupT } from "../form-concept-group/formConceptGroupState"; -import FormQueryDropzone from "../form-query-dropzone/FormQueryDropzone"; -import FormTabNavigation from "../form-tab-navigation/FormTabNavigation"; -import { getFieldKey, getInitialValue, isFormField } from "../helper"; -import { getErrorForField } from "../validators"; +import { getInitialValue, isFormField } from "../helper"; import type { DynamicFormValues } from "./Form"; - -// TODO: REFINE COLORS -// const useColorByField = (fieldType: FormField["type"]) => { -// const theme = useTheme(); - -// const COLOR_BY_FIELD_TYPE: Record = useMemo( -// () => ({ -// STRING: theme.col.palette[8], -// DATE_RANGE: theme.col.palette[0], -// NUMBER: theme.col.palette[1], -// CONCEPT_LIST: theme.col.palette[2], -// SELECT: theme.col.palette[3], -// DATASET_SELECT: theme.col.palette[4], -// CHECKBOX: theme.col.palette[7], -// RESULT_GROUP: theme.col.palette[5], -// TABS: theme.col.palette[9], -// }), -// [theme], -// ); - -// return COLOR_BY_FIELD_TYPE[fieldType]; -// }; - -type Props = T & { - children: (props: ControllerRenderProps) => ReactNode; - control: Control; - formField: FieldT | Tabs; - defaultValue?: unknown; - noContainer?: boolean; - noLabel?: boolean; -}; -const FieldContainer = styled("div")<{ - noLabel?: boolean; - hasError?: boolean; - red?: boolean; -}>` - display: flex; - flex-direction: column; - gap: 5px; - padding: ${({ noLabel }) => (noLabel ? "7px 10px" : "2px 10px 7px")}; - background-color: white; - border-radius: ${({ theme }) => theme.borderRadius}; - border: 1px solid - ${({ theme, hasError, red }) => - hasError - ? red - ? theme.col.red - : theme.col.blueGrayDark - : theme.col.grayLight}; -`; - -const ErrorContainer = styled("div")<{ red?: boolean }>` - color: ${({ theme, red }) => (red ? theme.col.red : theme.col.blueGrayDark)}; - font-weight: 700; - font-size: ${({ theme }) => theme.font.sm}; -`; - -const ConnectedField = ({ - children, - control, - formField, - defaultValue, - noContainer, - noLabel, - ...props -}: Props) => { - const { t } = useTranslation(); - const { field, fieldState } = useController({ - name: formField.name, - defaultValue, - control, - rules: { - validate: (value) => getErrorForField(t, formField, value) || true, - }, - shouldUnregister: false, - }); - - // TODO: REFINE COLORS - // const color = useColorByField(formField.type); - - const requiredMsg = t("externalForms.formValidation.isRequired"); - const isRedError = fieldState.error?.message !== requiredMsg; - - return noContainer ? ( -
{children({ ...field, ...props })}
- ) : ( - - {children({ ...field, ...props })} - - {fieldState.error?.message} - - - ); -}; - -const SxToggleButton = styled(ToggleButton)` - margin-bottom: 5px; -`; - -const Spacer = styled("div")` - height: 14px; -`; - -const Group = styled("div")` - display: flex; - flex-direction: row; - flex-wrap: wrap; -`; - -const NestedFields = styled("div")` - display: flex; - flex-direction: column; - gap: 7px; - padding: 12px 10px 12px; - background-color: ${({ theme }) => theme.col.bg}; - border: 1px solid ${({ theme }) => theme.col.gray}; - border-radius: ${({ theme }) => theme.borderRadius}; -`; - -const setValueConfig = { - shouldValidate: true, - shouldDirty: true, - shouldTouch: true, -}; +import { CheckboxField } from "./fields/CheckboxField"; +import { ConceptListField } from "./fields/ConceptListField"; +import { DatasetSelectField } from "./fields/DatasetSelectField"; +import { DateRangeField } from "./fields/DateRangeField"; +import { DisclosureField } from "./fields/DisclosureField"; +import { GroupField } from "./fields/GroupField"; +import { HeadlineField } from "./fields/HeadlineField"; +import { NumberField } from "./fields/NumberField"; +import { ResultGroupField } from "./fields/ResultGroupField"; +import { SelectField } from "./fields/SelectField"; +import { StringField } from "./fields/StringField"; +import { TabsField } from "./fields/TabsField"; +import { TextAreaField } from "./fields/TextAreaField"; const Field = ({ field, @@ -180,9 +37,7 @@ const Field = ({ control: Control; }) => { const datasetId = useDatasetId(); - const { formType, h1Index, locale, availableDatasets, setValue, control } = - commonProps; - const { t } = useTranslation(); + const { locale, availableDatasets } = commonProps; const defaultValue = isFormField(field) && field.type !== "GROUP" @@ -195,12 +50,7 @@ const Field = ({ switch (field.type) { case "HEADLINE": - return ( - - {exists(h1Index) && {h1Index + 1}} - {field.label[locale]} - - ); + return ; case "DESCRIPTION": return ( - {({ ref, ...fieldProps }) => ( - setValue(field.name, value, setValueConfig)} - tooltip={field.tooltip ? field.tooltip[locale] : undefined} - /> - )} - + commonProps={commonProps} + /> ); case "TEXTAREA": return ( - - {({ ref, ...fieldProps }) => ( - { - setValue(field.name, value, setValueConfig); - }} - tooltip={field.tooltip ? field.tooltip[locale] : undefined} - /> - )} - + commonProps={commonProps} + /> ); case "NUMBER": return ( - - {({ ref, ...fieldProps }) => ( - setValue(field.name, value, setValueConfig)} - inputProps={{ - step: field.step || "1", - pattern: field.pattern, - min: field.min, - max: field.max, - }} - tooltip={field.tooltip ? field.tooltip[locale] : undefined} - /> - )} - + commonProps={commonProps} + /> ); case "DATE_RANGE": return ( - - {({ ref, ...fieldProps }) => { - return ( - - setValue(field.name, value, setValueConfig) - } - /> - ); - }} - + commonProps={commonProps} + /> ); case "RESULT_GROUP": return ( - - {({ ref, ...fieldProps }) => ( - setValue(field.name, value, setValueConfig)} - /> - )} - + commonProps={commonProps} + /> ); case "CHECKBOX": return ( - - {({ ref, ...fieldProps }) => ( - setValue(field.name, value, setValueConfig)} - label={field.label[locale] || ""} - infoTooltip={field.tooltip ? field.tooltip[locale] : undefined} - /> - )} - + commonProps={commonProps} + /> ); case "SELECT": return ( - - {({ ref, ...fieldProps }) => ( - ({ - label: option.label[locale] || "", - value: option.value, - }))} - tooltip={field.tooltip ? field.tooltip[locale] : undefined} - value={fieldProps.value as SelectOptionT | null} - onChange={(value) => setValue(field.name, value, setValueConfig)} - /> - )} - + commonProps={commonProps} + /> ); case "DATASET_SELECT": return ( - 0 - ? availableDatasets.find((opt) => opt.value === datasetId) || - availableDatasets[0] - : null - } - > - {({ ref, ...fieldProps }) => { - return ( - - setValue(field.name, value, setValueConfig) - } - /> - ); - }} - + ); case "DISCLOSURE": return ( -
- - {field.label[locale]} - - {field.fields.map((f, i) => { - const key = getFieldKey(formType, f, i); - - return ; - })} -
+ ); case "GROUP": - return ( - <> - {field.label && {field.label[locale]}} - {field.description && ( - {field.description[locale]} - )} - - {field.fields.map((f, i) => { - const key = getFieldKey(formType, f, i); - - return ; - })} - - - ); + return ; case "TABS": return ( - - {({ ref, ...fieldProps }) => { - const tabToShow = field.tabs.find( - (tab) => tab.name === fieldProps.value, - ); - - return ( - <> - - setValue(field.name, tab, setValueConfig) - } - options={field.tabs.map((tab) => ({ - label: () => tab.title[locale] || "", - value: tab.name, - tooltip: tab.tooltip ? tab.tooltip[locale] : undefined, - }))} - /> - {tabToShow && tabToShow.fields.length > 0 ? ( - - {tabToShow.fields.map((f, i) => { - const key = getFieldKey(formType, f, i); - - return ; - })} - - ) : ( - - )} - - ); - }} - + /> ); case "CONCEPT_LIST": return ( - - {({ ref, ...fieldProps }) => ( - setValue(field.name, value, setValueConfig)} - label={field.label[locale] || ""} - tooltip={field.tooltip ? field.tooltip[locale] : undefined} - conceptDropzoneText={ - field.conceptDropzoneLabel - ? field.conceptDropzoneLabel[locale] || "" - : t("externalForms.default.conceptDropzoneLabel") - } - attributeDropzoneText={ - field.conceptColumnDropzoneLabel - ? field.conceptColumnDropzoneLabel[locale] || "" - : t("externalForms.default.conceptDropzoneLabel") - } - formType={formType} - disallowMultipleColumns={!field.isTwoDimensional} - isSingle={field.isSingle} - blocklistedTables={field.blocklistedConnectors} - allowlistedTables={field.allowlistedConnectors} - blocklistedSelects={field.blocklistedSelects} - allowlistedSelects={field.allowlistedSelects} - defaults={field.defaults} - isValidConcept={(item) => - !nodeIsInvalid( - item, - field.blocklistedConceptIds, - field.allowlistedConceptIds, - ) - } - // What follows is VERY custom - // Concept Group supports rendering a prefix field - // That's specifically required by one of the forms: "PSM Form" - // So the following looks like it wants to be generic, - // but it's really implemented for one field - newValue={ - field.rowPrefixField - ? { - concepts: [], - connector: "OR", - [field.rowPrefixField.name]: - field.rowPrefixField.defaultValue, - } - : { concepts: [], connector: "OR" } - } - rowPrefixFieldname={field.rowPrefixField?.name} - renderRowPrefix={ - exists(field.rowPrefixField) - ? ({ value: fieldValue, onChange, row, i }) => ( - ({ - label: option.label[locale] || "", - value: option.value, - }), - )} - value={ - /* Because we're essentially adding an extra dynamic field to FormConceptGroupT - with the key `field.rowPrefixField.name` */ - (row as unknown as Record)[ - field.rowPrefixField!.name - ] - } - onChange={(value) => - onChange([ - ...fieldValue.slice(0, i), - { - ...fieldValue[i], - [field.rowPrefixField!.name]: value, - }, - ...fieldValue.slice(i + 1), - ]) - } - /> - ) - : undefined - } - /> - )} - + /> ); default: return null; diff --git a/frontend/src/js/external-forms/form/fields/CheckboxField.tsx b/frontend/src/js/external-forms/form/fields/CheckboxField.tsx new file mode 100644 index 0000000000..e5498a168d --- /dev/null +++ b/frontend/src/js/external-forms/form/fields/CheckboxField.tsx @@ -0,0 +1,33 @@ +import { ComponentProps } from "react"; +import InputCheckbox from "../../../ui-components/InputCheckbox"; +import { CheckboxField as CheckboxFieldT } from "../../config-types"; +import { ConnectedField, setValueConfig } from "../ConnectedField"; +import Field from "../Field"; + +export const CheckboxField = ({ + field, + defaultValue, + commonProps: { control, locale, setValue }, +}: { + field: CheckboxFieldT; + defaultValue: unknown; + commonProps: Omit, "field">; +}) => { + return ( + + {({ ref, ...fieldProps }) => ( + setValue(field.name, value, setValueConfig)} + label={field.label[locale] || ""} + infoTooltip={field.tooltip ? field.tooltip[locale] : undefined} + /> + )} + + ); +}; diff --git a/frontend/src/js/external-forms/form/fields/ConceptListField.tsx b/frontend/src/js/external-forms/form/fields/ConceptListField.tsx new file mode 100644 index 0000000000..52b2e68688 --- /dev/null +++ b/frontend/src/js/external-forms/form/fields/ConceptListField.tsx @@ -0,0 +1,115 @@ +import styled from "@emotion/styled"; +import { ComponentProps } from "react"; +import { useTranslation } from "react-i18next"; +import { exists } from "../../../common/helpers/exists"; +import { nodeIsInvalid } from "../../../model/node"; +import ToggleButton from "../../../ui-components/ToggleButton"; +import { ConceptListField as ConceptListFieldT } from "../../config-types"; +import FormConceptGroup from "../../form-concept-group/FormConceptGroup"; +import { FormConceptGroupT } from "../../form-concept-group/formConceptGroupState"; +import { ConnectedField, setValueConfig } from "../ConnectedField"; +import Field from "../Field"; + +const SxToggleButton = styled(ToggleButton)` + margin-bottom: 5px; +`; + +export const ConceptListField = ({ + field, + defaultValue, + commonProps: { formType, control, locale, setValue }, +}: { + field: ConceptListFieldT; + defaultValue: unknown; + commonProps: Omit, "field">; +}) => { + const { t } = useTranslation(); + + return ( + + {({ ref, ...fieldProps }) => ( + setValue(field.name, value, setValueConfig)} + label={field.label[locale] || ""} + tooltip={field.tooltip ? field.tooltip[locale] : undefined} + conceptDropzoneText={ + field.conceptDropzoneLabel + ? field.conceptDropzoneLabel[locale] || "" + : t("externalForms.default.conceptDropzoneLabel") + } + attributeDropzoneText={ + field.conceptColumnDropzoneLabel + ? field.conceptColumnDropzoneLabel[locale] || "" + : t("externalForms.default.conceptDropzoneLabel") + } + formType={formType} + disallowMultipleColumns={!field.isTwoDimensional} + isSingle={field.isSingle} + blocklistedTables={field.blocklistedConnectors} + allowlistedTables={field.allowlistedConnectors} + blocklistedSelects={field.blocklistedSelects} + allowlistedSelects={field.allowlistedSelects} + defaults={field.defaults} + isValidConcept={(item) => + !nodeIsInvalid( + item, + field.blocklistedConceptIds, + field.allowlistedConceptIds, + ) + } + // What follows is VERY custom + // Concept Group supports rendering a prefix field + // That's specifically required by one of the forms: "PSM Form" + // So the following looks like it wants to be generic, + // but it's really implemented for one field + newValue={ + field.rowPrefixField + ? { + concepts: [], + connector: "OR", + [field.rowPrefixField.name]: + field.rowPrefixField.defaultValue, + } + : { concepts: [], connector: "OR" } + } + rowPrefixFieldname={field.rowPrefixField?.name} + renderRowPrefix={ + exists(field.rowPrefixField) + ? ({ value: fieldValue, onChange, row, i }) => ( + ({ + label: option.label[locale] || "", + value: option.value, + }))} + value={ + /* Because we're essentially adding an extra dynamic field to FormConceptGroupT + with the key `field.rowPrefixField.name` */ + (row as unknown as Record)[ + field.rowPrefixField!.name + ] + } + onChange={(value) => + onChange([ + ...fieldValue.slice(0, i), + { + ...fieldValue[i], + [field.rowPrefixField!.name]: value, + }, + ...fieldValue.slice(i + 1), + ]) + } + /> + ) + : undefined + } + /> + )} + + ); +}; diff --git a/frontend/src/js/external-forms/form/fields/DatasetSelectField.tsx b/frontend/src/js/external-forms/form/fields/DatasetSelectField.tsx new file mode 100644 index 0000000000..7e33fd99f3 --- /dev/null +++ b/frontend/src/js/external-forms/form/fields/DatasetSelectField.tsx @@ -0,0 +1,41 @@ +import { ComponentProps } from "react"; +import { SelectOptionT } from "../../../api/types"; +import InputSelect from "../../../ui-components/InputSelect/InputSelect"; +import { DatasetSelectField as DatasetSelectFieldT } from "../../config-types"; +import { ConnectedField, setValueConfig } from "../ConnectedField"; +import Field from "../Field"; + +export const DatasetSelectField = ({ + field, + datasetId, + commonProps: { control, locale, setValue, availableDatasets }, +}: { + field: DatasetSelectFieldT; + datasetId: string | null; + commonProps: Omit, "field">; +}) => { + return ( + 0 + ? availableDatasets.find((opt) => opt.value === datasetId) || + availableDatasets[0] + : null + } + > + {({ ref, ...fieldProps }) => { + return ( + setValue(field.name, value, setValueConfig)} + /> + ); + }} + + ); +}; diff --git a/frontend/src/js/external-forms/form/fields/DateRangeField.tsx b/frontend/src/js/external-forms/form/fields/DateRangeField.tsx new file mode 100644 index 0000000000..1a44745465 --- /dev/null +++ b/frontend/src/js/external-forms/form/fields/DateRangeField.tsx @@ -0,0 +1,36 @@ +import { ComponentProps } from "react"; +import { DateStringMinMax } from "../../../common/helpers/dateHelper"; +import InputDateRange from "../../../ui-components/InputDateRange"; +import { DateRangeField as DateRangeFieldT } from "../../config-types"; +import { ConnectedField, setValueConfig } from "../ConnectedField"; +import Field from "../Field"; + +export const DateRangeField = ({ + field, + defaultValue, + commonProps: { control, locale, setValue }, +}: { + field: DateRangeFieldT; + defaultValue: unknown; + commonProps: Omit, "field">; +}) => { + return ( + + {({ ref, ...fieldProps }) => { + return ( + setValue(field.name, value, setValueConfig)} + /> + ); + }} + + ); +}; diff --git a/frontend/src/js/external-forms/form/fields/DisclosureField.tsx b/frontend/src/js/external-forms/form/fields/DisclosureField.tsx new file mode 100644 index 0000000000..39a3b5db43 --- /dev/null +++ b/frontend/src/js/external-forms/form/fields/DisclosureField.tsx @@ -0,0 +1,68 @@ +import styled from "@emotion/styled"; +import { + faChevronDown, + faChevronRight, +} from "@fortawesome/free-solid-svg-icons"; +import { ComponentProps, useState } from "react"; +import FaIcon from "../../../icon/FaIcon"; +import { Disclosure } from "../../config-types"; +import { getFieldKey } from "../../helper"; +import Field from "../Field"; + +const Details = styled("details")` + overflow: hidden; + border: 1px solid ${({ theme }) => theme.col.gray}; + border-radius: ${({ theme }) => theme.borderRadius}; +`; + +const Summary = styled("summary")` + cursor: pointer; + display: flex; + align-items: center; + gap: 2px; + padding: 12px 12px 12px; + background-color: white; + font-size: ${({ theme }) => theme.font.sm}; + font-weight: 400; +`; + +const DisclosureNestedFields = styled("div")` + display: flex; + flex-direction: column; + gap: 7px; + background-color: ${({ theme }) => theme.col.bg}; + border-top: 1px solid ${({ theme }) => theme.col.gray}; + padding: 12px 10px 12px; +`; + +export const DisclosureField = ({ + field, + commonProps, +}: { + field: Disclosure; + commonProps: Omit, "field">; +}) => { + const [isOpen, setOpen] = useState(false); + + if (field.fields.length === 0) return null; + + const { formType, locale } = commonProps; + + return ( +
setOpen(!isOpen)}> + + + + + {field.label[locale]} + + + {field.fields.map((f, i) => { + const key = getFieldKey(formType, f, i); + + return ; + })} + +
+ ); +}; diff --git a/frontend/src/js/external-forms/form/fields/GroupField.tsx b/frontend/src/js/external-forms/form/fields/GroupField.tsx new file mode 100644 index 0000000000..dce9b94622 --- /dev/null +++ b/frontend/src/js/external-forms/form/fields/GroupField.tsx @@ -0,0 +1,43 @@ +import styled from "@emotion/styled"; +import { ComponentProps } from "react"; +import { Group } from "../../config-types"; +import { Description } from "../../form-components/Description"; +import { Headline } from "../../form-components/Headline"; +import { getFieldKey } from "../../helper"; +import Field from "../Field"; + +const GroupContainer = styled("div")` + display: flex; + flex-direction: row; + flex-wrap: wrap; +`; + +export const GroupField = ({ + field, + commonProps, +}: { + field: Group; + commonProps: Omit, "field">; +}) => { + return ( + <> + {field.label && {field.label[commonProps.locale]}} + {field.description && ( + {field.description[commonProps.locale]} + )} + + {field.fields.map((f, i) => { + const key = getFieldKey(commonProps.formType, f, i); + + return ; + })} + + + ); +}; diff --git a/frontend/src/js/external-forms/form/fields/HeadlineField.tsx b/frontend/src/js/external-forms/form/fields/HeadlineField.tsx new file mode 100644 index 0000000000..2423c746c3 --- /dev/null +++ b/frontend/src/js/external-forms/form/fields/HeadlineField.tsx @@ -0,0 +1,24 @@ +import { ComponentProps } from "react"; +import { exists } from "../../../common/helpers/exists"; +import { Headline } from "../../config-types"; +import { + getHeadlineFieldAs, + Headline as HeadlineComponent, + HeadlineIndex, +} from "../../form-components/Headline"; +import Field from "../Field"; + +export const HeadlineField = ({ + field, + commonProps: { h1Index, locale }, +}: { + field: Headline; + commonProps: Omit, "field">; +}) => { + return ( + + {exists(h1Index) && {h1Index + 1}} + {field.label[locale]} + + ); +}; diff --git a/frontend/src/js/external-forms/form/fields/NumberField.tsx b/frontend/src/js/external-forms/form/fields/NumberField.tsx new file mode 100644 index 0000000000..2efc0edeb5 --- /dev/null +++ b/frontend/src/js/external-forms/form/fields/NumberField.tsx @@ -0,0 +1,41 @@ +import { ComponentProps } from "react"; +import InputPlain from "../../../ui-components/InputPlain/InputPlain"; +import { NumberField as NumberFieldT } from "../../config-types"; +import { ConnectedField, setValueConfig } from "../ConnectedField"; +import Field from "../Field"; + +export const NumberField = ({ + field, + defaultValue, + commonProps: { control, locale, setValue }, +}: { + field: NumberFieldT; + defaultValue: unknown; + commonProps: Omit, "field">; +}) => { + return ( + + {({ ref, ...fieldProps }) => ( + setValue(field.name, value, setValueConfig)} + inputProps={{ + step: field.step || "1", + pattern: field.pattern, + min: field.min, + max: field.max, + }} + tooltip={field.tooltip ? field.tooltip[locale] : undefined} + /> + )} + + ); +}; diff --git a/frontend/src/js/external-forms/form/fields/ResultGroupField.tsx b/frontend/src/js/external-forms/form/fields/ResultGroupField.tsx new file mode 100644 index 0000000000..acd3859022 --- /dev/null +++ b/frontend/src/js/external-forms/form/fields/ResultGroupField.tsx @@ -0,0 +1,34 @@ +import { ComponentProps } from "react"; +import { DragItemQuery } from "../../../standard-query-editor/types"; +import { ResultGroupField as ResultGroupFieldT } from "../../config-types"; +import FormQueryDropzone from "../../form-query-dropzone/FormQueryDropzone"; +import { ConnectedField, setValueConfig } from "../ConnectedField"; +import Field from "../Field"; + +export const ResultGroupField = ({ + field, + defaultValue, + commonProps: { control, locale, setValue }, +}: { + field: ResultGroupFieldT; + defaultValue: unknown; + commonProps: Omit, "field">; +}) => { + return ( + + {({ ref, ...fieldProps }) => ( + setValue(field.name, value, setValueConfig)} + /> + )} + + ); +}; diff --git a/frontend/src/js/external-forms/form/fields/SelectField.tsx b/frontend/src/js/external-forms/form/fields/SelectField.tsx new file mode 100644 index 0000000000..21001c1bcb --- /dev/null +++ b/frontend/src/js/external-forms/form/fields/SelectField.tsx @@ -0,0 +1,37 @@ +import { ComponentProps } from "react"; +import { SelectOptionT } from "../../../api/types"; +import InputSelect from "../../../ui-components/InputSelect/InputSelect"; +import { SelectField as SelectFieldT } from "../../config-types"; +import { ConnectedField, setValueConfig } from "../ConnectedField"; +import Field from "../Field"; + +export const SelectField = ({ + field, + defaultValue, + commonProps: { control, locale, setValue }, +}: { + field: SelectFieldT; + defaultValue: unknown; + commonProps: Omit, "field">; +}) => { + return ( + + {({ ref, ...fieldProps }) => ( + ({ + label: option.label[locale] || "", + value: option.value, + }))} + tooltip={field.tooltip ? field.tooltip[locale] : undefined} + value={fieldProps.value as SelectOptionT | null} + onChange={(value) => setValue(field.name, value, setValueConfig)} + /> + )} + + ); +}; diff --git a/frontend/src/js/external-forms/form/fields/StringField.tsx b/frontend/src/js/external-forms/form/fields/StringField.tsx new file mode 100644 index 0000000000..0886911c90 --- /dev/null +++ b/frontend/src/js/external-forms/form/fields/StringField.tsx @@ -0,0 +1,36 @@ +import { ComponentProps } from "react"; +import InputPlain from "../../../ui-components/InputPlain/InputPlain"; +import type { StringField as StringFieldT } from "../../config-types"; +import { ConnectedField, setValueConfig } from "../ConnectedField"; +import Field from "../Field"; + +export const StringField = ({ + field, + defaultValue, + commonProps: { locale, control, setValue }, +}: { + field: StringFieldT; + defaultValue: unknown; + commonProps: Omit, "field">; +}) => { + return ( + + {({ ref, ...fieldProps }) => ( + setValue(field.name, value, setValueConfig)} + tooltip={field.tooltip ? field.tooltip[locale] : undefined} + /> + )} + + ); +}; diff --git a/frontend/src/js/external-forms/form/fields/TabsField.tsx b/frontend/src/js/external-forms/form/fields/TabsField.tsx new file mode 100644 index 0000000000..48b6d64002 --- /dev/null +++ b/frontend/src/js/external-forms/form/fields/TabsField.tsx @@ -0,0 +1,75 @@ +import styled from "@emotion/styled"; +import { ComponentProps } from "react"; +import { Tabs } from "../../config-types"; +import FormTabNavigation from "../../form-tab-navigation/FormTabNavigation"; +import { getFieldKey } from "../../helper"; +import { ConnectedField, setValueConfig } from "../ConnectedField"; +import Field from "../Field"; + +const Spacer = styled("div")` + height: 14px; +`; + +const NestedFields = styled("div")` + display: flex; + flex-direction: column; + gap: 7px; + background-color: ${({ theme }) => theme.col.bg}; + padding: 12px 10px 12px; + border: 1px solid ${({ theme }) => theme.col.gray}; + border-radius: ${({ theme }) => theme.borderRadius}; +`; + +export const TabsField = ({ + field, + commonProps, + defaultValue, +}: { + field: Tabs; + commonProps: Omit, "field">; + defaultValue: unknown; +}) => { + return ( + + {({ ref, ...fieldProps }) => { + const tabToShow = field.tabs.find( + (tab) => tab.name === fieldProps.value, + ); + + return ( + <> + + commonProps.setValue(field.name, tab, setValueConfig) + } + options={field.tabs.map((tab) => ({ + label: () => tab.title[commonProps.locale] || "", + value: tab.name, + tooltip: tab.tooltip + ? tab.tooltip[commonProps.locale] + : undefined, + }))} + /> + {tabToShow && tabToShow.fields.length > 0 ? ( + + {tabToShow.fields.map((f, i) => { + const key = getFieldKey(commonProps.formType, f, i); + + return ; + })} + + ) : ( + + )} + + ); + }} + + ); +}; diff --git a/frontend/src/js/external-forms/form/fields/TextAreaField.tsx b/frontend/src/js/external-forms/form/fields/TextAreaField.tsx new file mode 100644 index 0000000000..aabdcce739 --- /dev/null +++ b/frontend/src/js/external-forms/form/fields/TextAreaField.tsx @@ -0,0 +1,37 @@ +import { ComponentProps } from "react"; +import { InputTextarea } from "../../../ui-components/InputTextarea/InputTextarea"; +import { TextareaField } from "../../config-types"; +import { ConnectedField, setValueConfig } from "../ConnectedField"; +import Field from "../Field"; + +export const TextAreaField = ({ + field, + defaultValue, + commonProps: { control, locale, setValue }, +}: { + field: TextareaField; + defaultValue: unknown; + commonProps: Omit, "field">; +}) => { + return ( + + {({ ref, ...fieldProps }) => ( + { + setValue(field.name, value, setValueConfig); + }} + tooltip={field.tooltip ? field.tooltip[locale] : undefined} + /> + )} + + ); +}; From 607213367b859e28115067fe9a79959e56aac0c8 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Mon, 15 Apr 2024 16:03:17 +0200 Subject: [PATCH 03/17] Allow for a tooltip --- frontend/mock-api/forms/export-form.json | 3 +++ frontend/src/js/external-forms/config-types.ts | 3 ++- frontend/src/js/external-forms/form/fields/DisclosureField.tsx | 3 +++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/mock-api/forms/export-form.json b/frontend/mock-api/forms/export-form.json index 24f0027633..49538be7f1 100644 --- a/frontend/mock-api/forms/export-form.json +++ b/frontend/mock-api/forms/export-form.json @@ -67,6 +67,9 @@ "de": "Datenschutz" }, "creatable": true, + "tooltip": { + "de": "Der Datenschutz ist ein wichtiges Thema, deshalb musst du hier zustimmen." + }, "fields": [ { "type": "HEADLINE", diff --git a/frontend/src/js/external-forms/config-types.ts b/frontend/src/js/external-forms/config-types.ts index 880161939d..2c8fedb30d 100644 --- a/frontend/src/js/external-forms/config-types.ts +++ b/frontend/src/js/external-forms/config-types.ts @@ -38,10 +38,11 @@ export interface Group { export interface Disclosure { type: "DISCLOSURE"; - name: string; creatable?: boolean; defaultOpen?: boolean; + name: string; label: TranslatableString; + tooltip?: TranslatableString; fields: GeneralField[]; } diff --git a/frontend/src/js/external-forms/form/fields/DisclosureField.tsx b/frontend/src/js/external-forms/form/fields/DisclosureField.tsx index 39a3b5db43..527b977abd 100644 --- a/frontend/src/js/external-forms/form/fields/DisclosureField.tsx +++ b/frontend/src/js/external-forms/form/fields/DisclosureField.tsx @@ -4,7 +4,9 @@ import { faChevronRight, } from "@fortawesome/free-solid-svg-icons"; import { ComponentProps, useState } from "react"; +import { exists } from "../../../common/helpers/exists"; import FaIcon from "../../../icon/FaIcon"; +import InfoTooltip from "../../../tooltip/InfoTooltip"; import { Disclosure } from "../../config-types"; import { getFieldKey } from "../../helper"; import Field from "../Field"; @@ -55,6 +57,7 @@ export const DisclosureField = ({ {field.label[locale]} + {exists(field.tooltip) && } {field.fields.map((f, i) => { From d7d8ceed9ad21bfd5fa38b81368f237896bd7552 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Mon, 15 Apr 2024 23:56:48 +0200 Subject: [PATCH 04/17] Add tailwind --- frontend/README.md | 5 +- frontend/package-lock.json | 559 ++++++++++++++++-- frontend/package.json | 4 + frontend/postcss.config.js | 6 + frontend/src/index.css | 3 + frontend/src/js/button/BasicButton.tsx | 2 +- frontend/src/js/external-forms/form/Form.tsx | 11 +- frontend/src/js/header/LogoutButton.tsx | 7 +- frontend/src/js/index.tsx | 1 + frontend/src/js/pane/TabNavigation.tsx | 46 +- .../EmptyQueryEditorDropzone.tsx | 7 +- frontend/src/js/tooltip/TooltipHeader.tsx | 28 +- frontend/src/js/ui-components/BaseInput.tsx | 1 + .../src/js/ui-components/InputCheckbox.tsx | 31 +- .../InputSelect/InputSelectComponents.tsx | 2 + frontend/tailwind.config.js | 28 + 16 files changed, 596 insertions(+), 145 deletions(-) create mode 100644 frontend/postcss.config.js create mode 100644 frontend/src/index.css create mode 100644 frontend/tailwind.config.js diff --git a/frontend/README.md b/frontend/README.md index d5c6c9b582..11b2cfded8 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -123,7 +123,10 @@ Depending on the use-case, we're still calling the same concepts differently som ### Styles -- Emotion is used for theming and styles. The plan is to migrate (back) to styled-components or to another css-in-js solution, because emotion's "styled" is less TypeScript compatible in some edge cases like generic component props (see usage of Dropzone). +- Currently, we're mostly using Emotion for theming and styles. +- The plan is to slowly migrate to [Tailwind CSS](https://tailwindcss.com/) and [tailwind-styled-components](https://github.com/MathiasGilson/Tailwind-Styled-Component). +- New styles should be written using tailwind. +- Reasoning: Theming with Emotion is verbose, Emotion's "styled" is less TypeScript compatible in some edge cases like generic component props (see usage of Dropzone). But the main reason for migrating to tailwind, of course, is that tailwind means a lot less boilerplate code. It also allows for more consistent styling and offers a great dev UX. ### State diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 350710442a..ed2f6481f0 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -27,6 +27,7 @@ "@typescript-eslint/parser": "^6.10.0", "@vitejs/plugin-react": "^4.1.1", "apache-arrow": "^13.0.0", + "autoprefixer": "^10.4.19", "axios": "^1.6.0", "chance": "^1.1.11", "chart.js": "^4.4.0", @@ -43,6 +44,7 @@ "lodash.difference": "^4.5.0", "mustache": "^4.2.0", "nodemon": "^3.0.1", + "postcss": "^8.4.38", "prettier-plugin-organize-imports": "^3.2.3", "rc-table": "^7.35.2", "react": "^18.2.0", @@ -71,6 +73,7 @@ "remark-flexible-markers": "^1.0.3", "remark-gfm": "^3.0.1", "resize-observer-polyfill": "^1.5.1", + "tailwindcss": "^3.4.3", "typesafe-actions": "^5.1.0", "vite": "^4.5.0" }, @@ -111,6 +114,7 @@ "papaparse": "^5.4.1", "prettier": "^3.0.3", "storybook": "7.5.3", + "tailwind-styled-components": "^2.2.0", "terser": "^5.24.0", "ts-jest": "^29.1.1", "ts-node": "^10.5.0", @@ -148,6 +152,17 @@ "node": ">=0.10.0" } }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@ampproject/remapping": { "version": "2.2.1", "license": "Apache-2.0", @@ -2135,7 +2150,7 @@ }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "0.3.9" @@ -2146,7 +2161,7 @@ }, "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { "version": "0.3.9", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", @@ -2474,7 +2489,6 @@ }, "node_modules/@isaacs/cliui": { "version": "8.0.2", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -3056,7 +3070,6 @@ }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", - "dev": true, "license": "MIT", "optional": true, "engines": { @@ -5321,22 +5334,22 @@ }, "node_modules/@tsconfig/node10": { "version": "1.0.9", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@tsconfig/node12": { "version": "1.0.11", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@tsconfig/node14": { "version": "1.0.3", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@tsconfig/node16": { "version": "1.0.4", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/aria-query": { @@ -6271,7 +6284,7 @@ }, "node_modules/acorn-walk": { "version": "8.3.0", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -6370,6 +6383,11 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" + }, "node_modules/anymatch": { "version": "3.1.3", "license": "ISC", @@ -6413,7 +6431,7 @@ }, "node_modules/arg": { "version": "4.1.3", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/argparse": { @@ -6507,6 +6525,42 @@ "version": "0.4.0", "license": "MIT" }, + "node_modules/autoprefixer": { + "version": "10.4.19", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", + "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-lite": "^1.0.30001599", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.5", "dev": true, @@ -6860,7 +6914,6 @@ }, "node_modules/brace-expansion": { "version": "2.0.1", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -6889,7 +6942,9 @@ } }, "node_modules/browserslist": { - "version": "4.22.1", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", "funding": [ { "type": "opencollective", @@ -6904,11 +6959,10 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001541", - "electron-to-chromium": "^1.4.535", - "node-releases": "^2.0.13", + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", "update-browserslist-db": "^1.0.13" }, "bin": { @@ -7012,8 +7066,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "engines": { + "node": ">= 6" + } + }, "node_modules/caniuse-lite": { - "version": "1.0.30001561", + "version": "1.0.30001610", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001610.tgz", + "integrity": "sha512-QFutAY4NgaelojVMjY63o6XlZyORPaLfyMnsl3HgnWdJUcX6K0oaJymHjH8PT5Gk7sTm8rvC/c5COUQKXqmOMA==", "funding": [ { "type": "opencollective", @@ -7027,8 +7091,7 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ], - "license": "CC-BY-4.0" + ] }, "node_modules/ccount": { "version": "2.0.1", @@ -7609,7 +7672,7 @@ }, "node_modules/create-require": { "version": "1.1.1", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/cross-spawn": { @@ -7632,6 +7695,17 @@ "node": ">=8" } }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/cssom": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", @@ -7942,9 +8016,14 @@ "detect-port": "bin/detect-port.js" } }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" + }, "node_modules/diff": { "version": "4.0.2", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -7969,6 +8048,11 @@ "node": ">=8" } }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" + }, "node_modules/dnd-core": { "version": "16.0.1", "license": "MIT", @@ -8066,7 +8150,6 @@ }, "node_modules/eastasianwidth": { "version": "0.2.0", - "dev": true, "license": "MIT" }, "node_modules/ee-first": { @@ -8088,8 +8171,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.576", - "license": "ISC" + "version": "1.4.736", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.736.tgz", + "integrity": "sha512-Rer6wc3ynLelKNM4lOCg7/zPQj8tPOCB2hzD32PX9wd3hgRRi9MxEbmkFCokzcEhRVMiOVLjnL9ig9cefJ+6+Q==" }, "node_modules/emittery": { "version": "0.13.1", @@ -8105,7 +8189,6 @@ }, "node_modules/emoji-regex": { "version": "8.0.0", - "dev": true, "license": "MIT" }, "node_modules/encodeurl": { @@ -8905,7 +8988,6 @@ }, "node_modules/foreground-child": { "version": "3.1.1", - "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.0", @@ -8920,7 +9002,6 @@ }, "node_modules/foreground-child/node_modules/signal-exit": { "version": "4.1.0", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -8948,6 +9029,18 @@ "node": ">= 0.6" } }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/fresh": { "version": "0.5.2", "license": "MIT", @@ -9802,7 +9895,6 @@ }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -10180,7 +10272,6 @@ }, "node_modules/jackspeak": { "version": "2.3.6", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -10811,6 +10902,14 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jiti": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", + "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", + "bin": { + "jiti": "bin/jiti.js" + } + }, "node_modules/js-sha256": { "version": "0.9.0", "license": "MIT" @@ -11049,6 +11148,14 @@ "node": ">= 0.8.0" } }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "engines": { + "node": ">=10" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "license": "MIT" @@ -11184,7 +11291,7 @@ }, "node_modules/make-error": { "version": "1.3.6", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/makeerror": { @@ -12162,6 +12269,16 @@ "mustache": "bin/mustache" } }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.7", "funding": [ @@ -12254,8 +12371,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.13", - "license": "MIT" + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" }, "node_modules/nodemon": { "version": "3.0.1", @@ -12326,6 +12444,14 @@ "node": ">=0.10.0" } }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/npm-run-path": { "version": "4.0.1", "dev": true, @@ -12350,6 +12476,14 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.1", "license": "MIT", @@ -12640,11 +12774,11 @@ "license": "MIT" }, "node_modules/path-scurry": { - "version": "1.10.1", - "dev": true, - "license": "BlueOak-1.0.0", + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", + "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", "dependencies": { - "lru-cache": "^9.1.1 || ^10.0.0", + "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { @@ -12655,16 +12789,15 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.0.1", - "dev": true, - "license": "ISC", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", "engines": { "node": "14 || >=16.14" } }, "node_modules/path-scurry/node_modules/minipass": { "version": "7.0.4", - "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -12725,7 +12858,6 @@ }, "node_modules/pirates": { "version": "4.0.6", - "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -12802,7 +12934,9 @@ } }, "node_modules/postcss": { - "version": "8.4.31", + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", "funding": [ { "type": "opencollective", @@ -12817,16 +12951,140 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { - "nanoid": "^3.3.6", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" + "source-map-js": "^1.2.0" }, "engines": { "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", + "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/postcss-load-config/node_modules/yaml": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz", + "integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/postcss-nested": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "dependencies": { + "postcss-selector-parser": "^6.0.11" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.16", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", + "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + }, "node_modules/prelude-ls": { "version": "1.2.1", "license": "MIT", @@ -13746,6 +14004,22 @@ "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/read-cache/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/read-pkg": { "version": "5.2.0", "dev": true, @@ -14487,8 +14761,9 @@ } }, "node_modules/source-map-js": { - "version": "1.0.2", - "license": "BSD-3-Clause", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", "engines": { "node": ">=0.10.0" } @@ -14651,7 +14926,6 @@ }, "node_modules/string-width": { "version": "5.1.2", - "dev": true, "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -14668,7 +14942,6 @@ "node_modules/string-width-cjs": { "name": "string-width", "version": "4.2.3", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -14683,7 +14956,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -14693,12 +14965,10 @@ }, "node_modules/string-width/node_modules/emoji-regex": { "version": "9.2.2", - "dev": true, "license": "MIT" }, "node_modules/strip-ansi": { "version": "7.0.1", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -14713,7 +14983,6 @@ "node_modules/strip-ansi-cjs": { "name": "strip-ansi", "version": "6.0.1", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -14724,7 +14993,6 @@ }, "node_modules/strip-ansi/node_modules/ansi-regex": { "version": "6.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -14785,6 +15053,78 @@ "version": "4.2.0", "license": "MIT" }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "10.3.12", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", + "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.3.6", + "minimatch": "^9.0.1", + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/minipass": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", + "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/supports-color": { "version": "5.5.0", "license": "MIT", @@ -14859,6 +15199,81 @@ "node": ">=12.17" } }, + "node_modules/tailwind-merge": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-1.14.0.tgz", + "integrity": "sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwind-styled-components": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tailwind-styled-components/-/tailwind-styled-components-2.2.0.tgz", + "integrity": "sha512-Ogemwk0p69aU8WE/ooJZHjqstdJgT5R6HGU6TFz2uSnveSEtvW+C6aWOjGCvCr5H/bREv0IbbQ4yODknRrLBRQ==", + "dev": true, + "dependencies": { + "tailwind-merge": "^1.3.0" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz", + "integrity": "sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.0", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" + }, + "node_modules/tailwindcss/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/tar": { "version": "6.2.0", "dev": true, @@ -15069,6 +15484,25 @@ "version": "0.2.0", "license": "MIT" }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/through2": { "version": "2.0.5", "dev": true, @@ -15204,6 +15638,11 @@ "node": ">=6.10" } }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" + }, "node_modules/ts-jest": { "version": "29.1.1", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz", @@ -15249,7 +15688,7 @@ }, "node_modules/ts-node": { "version": "10.9.1", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "^0.8.0", @@ -15680,7 +16119,6 @@ }, "node_modules/util-deprecate": { "version": "1.0.2", - "dev": true, "license": "MIT" }, "node_modules/utils-merge": { @@ -15734,7 +16172,7 @@ }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/v8-to-istanbul": { @@ -16071,7 +16509,6 @@ }, "node_modules/wrap-ansi": { "version": "8.1.0", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -16088,7 +16525,6 @@ "node_modules/wrap-ansi-cjs": { "name": "wrap-ansi", "version": "7.0.0", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -16106,7 +16542,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -16120,7 +16555,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -16130,7 +16564,6 @@ }, "node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "6.2.1", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -16280,7 +16713,7 @@ }, "node_modules/yn": { "version": "3.1.1", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6" diff --git a/frontend/package.json b/frontend/package.json index 122fed6e08..ada0f1fd4b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -41,6 +41,7 @@ "@typescript-eslint/parser": "^6.10.0", "@vitejs/plugin-react": "^4.1.1", "apache-arrow": "^13.0.0", + "autoprefixer": "^10.4.19", "axios": "^1.6.0", "chance": "^1.1.11", "chart.js": "^4.4.0", @@ -57,6 +58,7 @@ "lodash.difference": "^4.5.0", "mustache": "^4.2.0", "nodemon": "^3.0.1", + "postcss": "^8.4.38", "prettier-plugin-organize-imports": "^3.2.3", "rc-table": "^7.35.2", "react": "^18.2.0", @@ -85,6 +87,7 @@ "remark-flexible-markers": "^1.0.3", "remark-gfm": "^3.0.1", "resize-observer-polyfill": "^1.5.1", + "tailwindcss": "^3.4.3", "typesafe-actions": "^5.1.0", "vite": "^4.5.0" }, @@ -125,6 +128,7 @@ "papaparse": "^5.4.1", "prettier": "^3.0.3", "storybook": "7.5.3", + "tailwind-styled-components": "^2.2.0", "terser": "^5.24.0", "ts-jest": "^29.1.1", "ts-node": "^10.5.0", diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000000..2e7af2b7f1 --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000000..bd6213e1df --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/frontend/src/js/button/BasicButton.tsx b/frontend/src/js/button/BasicButton.tsx index 95f660ca71..798827d2c9 100644 --- a/frontend/src/js/button/BasicButton.tsx +++ b/frontend/src/js/button/BasicButton.tsx @@ -22,7 +22,7 @@ const Button = styled("button")` : tiny ? "4px 6px" : small - ? "6px 10px" + ? "6px 8px" : large ? "12px 18px" : "8px 15px"}; diff --git a/frontend/src/js/external-forms/form/Form.tsx b/frontend/src/js/external-forms/form/Form.tsx index 05d8a16c30..43d3fdfdaf 100644 --- a/frontend/src/js/external-forms/form/Form.tsx +++ b/frontend/src/js/external-forms/form/Form.tsx @@ -10,13 +10,6 @@ import { getFieldKey, getH1Index } from "../helper"; import Field from "./Field"; -const FormContent = styled("div")` - width: 100%; - display: flex; - flex-direction: column; - gap: 7px; -`; - const SxFormHeader = styled(FormHeader)` margin: 5px 0 15px; `; @@ -35,7 +28,7 @@ const Form = memo(({ config, datasetOptions, methods }: Props) => { const activeLang = useActiveLang(); return ( - +
{config.description && config.description[activeLang] && ( { /> ); })} - +
); }); diff --git a/frontend/src/js/header/LogoutButton.tsx b/frontend/src/js/header/LogoutButton.tsx index fdd2ea1882..320649592a 100644 --- a/frontend/src/js/header/LogoutButton.tsx +++ b/frontend/src/js/header/LogoutButton.tsx @@ -1,4 +1,3 @@ -import styled from "@emotion/styled"; import { faSignOutAlt } from "@fortawesome/free-solid-svg-icons"; import { useKeycloak } from "@react-keycloak-fork/web"; import { FC } from "react"; @@ -11,10 +10,6 @@ import { clearIndexedDBCache } from "../common/helpers/indexedDBCache"; import { isIDPEnabled } from "../environment"; import WithTooltip from "../tooltip/WithTooltip"; -const SxIconButton = styled(IconButton)` - padding: 6px 6px; -`; - interface PropsT { className?: string; } @@ -46,7 +41,7 @@ const LogoutButton: FC = ({ className }) => { return ( - + ); }; diff --git a/frontend/src/js/index.tsx b/frontend/src/js/index.tsx index 3481f30045..75f069b29c 100644 --- a/frontend/src/js/index.tsx +++ b/frontend/src/js/index.tsx @@ -3,6 +3,7 @@ import { createRoot } from "react-dom/client"; import { Store } from "redux"; import "../fonts.css"; +import "../index.css"; import AppRoot from "./AppRoot"; import GlobalStyles from "./GlobalStyles"; diff --git a/frontend/src/js/pane/TabNavigation.tsx b/frontend/src/js/pane/TabNavigation.tsx index 879f3e1705..6d2535a254 100644 --- a/frontend/src/js/pane/TabNavigation.tsx +++ b/frontend/src/js/pane/TabNavigation.tsx @@ -2,6 +2,7 @@ import styled from "@emotion/styled"; import { faSpinner } from "@fortawesome/free-solid-svg-icons"; import { FC } from "react"; +import tw from "tailwind-styled-components"; import FaIcon from "../icon/FaIcon"; import { HoverNavigatable } from "../small-tab-navigation/HoverNavigatable"; import WithTooltip from "../tooltip/WithTooltip"; @@ -14,33 +15,28 @@ const Root = styled("div")` align-items: flex-start; `; -const Headline = styled("h2")<{ active: boolean }>` - font-size: ${({ theme }) => theme.font.sm}; - margin-bottom: 0; - margin-top: 6px; - padding: 0 12px; - letter-spacing: 1px; - line-height: 30px; - text-transform: uppercase; - flex-shrink: 0; +const Headline = tw("h2")<{ active: boolean }>` + text-sm + mb-0 + mt-[6px] + mr-[5px] + px-3 + font-bold + leading-[30px] + uppercase + flex-shrink-0 + transition-colors + cursor-pointer + tracking-wider - transition: - color ${({ theme }) => theme.transitionTime}, - border-bottom ${({ theme }) => theme.transitionTime}; - cursor: pointer; - margin-right: 5px; - color: ${({ theme, active }) => - active ? theme.col.blueGrayDark : theme.col.gray}; - border-bottom: 3px solid - ${({ theme, active }) => (active ? theme.col.blueGrayDark : "transparent")}; + border-b-[3px] + ${({ active }) => + active ? "text-primary-500" : "text-gray-500 hover:text-black"}; + ${({ active }) => + active + ? "border-primary-500" + : "border-transparent hover:border-primary-200"}; - &:hover { - color: ${({ theme, active }) => - active ? theme.col.blueGrayDark : theme.col.black}; - border-bottom: 3px solid - ${({ theme, active }) => - active ? theme.col.blueGrayDark : theme.col.grayLight}; - } `; const SxWithTooltip = styled(WithTooltip)` diff --git a/frontend/src/js/standard-query-editor/EmptyQueryEditorDropzone.tsx b/frontend/src/js/standard-query-editor/EmptyQueryEditorDropzone.tsx index 63badc3a3e..681565b07a 100644 --- a/frontend/src/js/standard-query-editor/EmptyQueryEditorDropzone.tsx +++ b/frontend/src/js/standard-query-editor/EmptyQueryEditorDropzone.tsx @@ -30,9 +30,6 @@ const ArrowRight = styled(FaIcon)` grid-area: arrow; `; const Headline = styled("h2")` - margin: 0; - font-size: ${({ theme }) => theme.font.huge}; - line-height: 1.3; grid-area: headline; `; const Grid = styled("div")` @@ -66,7 +63,9 @@ export const EmptyQueryEditorDropzone = memo(() => { return ( - {t("dropzone.explanation")} + + {t("dropzone.explanation")} +

{t("dropzone.dropIntoThisArea")}

diff --git a/frontend/src/js/tooltip/TooltipHeader.tsx b/frontend/src/js/tooltip/TooltipHeader.tsx index 54881738b1..e907e75a56 100644 --- a/frontend/src/js/tooltip/TooltipHeader.tsx +++ b/frontend/src/js/tooltip/TooltipHeader.tsx @@ -6,22 +6,22 @@ import { useDispatch } from "react-redux"; import IconButton from "../button/IconButton"; +import tw from "tailwind-styled-components"; import { toggleDisplayTooltip } from "./actions"; -const Header = styled("h2")` - background-color: white; - height: 40px; - flex-shrink: 0; - display: flex; - align-items: center; - border-bottom: 1px solid ${({ theme }) => theme.col.grayLight}; - margin: 0; - padding: 0 20px; - font-size: ${({ theme }) => theme.font.sm}; - letter-spacing: 1px; - line-height: 38px; - text-transform: uppercase; - color: ${({ theme }) => theme.col.blueGrayDark}; +const Header = tw("h2")` + bg-white + h-[40px] + flex-shrink-0 + flex items-center + px-5 + pt-1 + text-sm + tracking-[1px] + uppercase + text-primary-500 + border-b border-gray-100 + font-bold `; const StyledIconButton = styled(IconButton)` diff --git a/frontend/src/js/ui-components/BaseInput.tsx b/frontend/src/js/ui-components/BaseInput.tsx index f732d83c15..9fadc5dccc 100644 --- a/frontend/src/js/ui-components/BaseInput.tsx +++ b/frontend/src/js/ui-components/BaseInput.tsx @@ -30,6 +30,7 @@ const Input = styled("input")<{ large?: boolean; disabled?: boolean }>` large ? "10px 30px 10px 14px" : "6px 30px 6px 10px"}; font-size: ${({ theme, large }) => (large ? theme.font.lg : theme.font.sm)}; border-radius: ${({ theme }) => theme.borderRadius}; + font-weight: 400; `; const SignalIcon = styled(FaIcon)` diff --git a/frontend/src/js/ui-components/InputCheckbox.tsx b/frontend/src/js/ui-components/InputCheckbox.tsx index 01e5d0e9f0..9c5fbd75d4 100644 --- a/frontend/src/js/ui-components/InputCheckbox.tsx +++ b/frontend/src/js/ui-components/InputCheckbox.tsx @@ -1,6 +1,8 @@ import styled from "@emotion/styled"; +import { faCheck } from "@fortawesome/free-solid-svg-icons"; import { exists } from "../common/helpers/exists"; +import FaIcon from "../icon/FaIcon"; import InfoTooltip from "../tooltip/InfoTooltip"; import WithTooltip from "../tooltip/WithTooltip"; @@ -29,27 +31,6 @@ const Container = styled("div")<{ $disabled?: boolean }>` opacity: ${({ $disabled }) => ($disabled ? 0.5 : 1)}; `; -const Checkmark = styled("div")` - position: absolute; - top: 0; - left: 0; - height: 20px; - width: 20px; - background-color: ${({ theme }) => theme.col.blueGrayDark}; - - &:after { - content: ""; - position: absolute; - left: 6px; - top: 2px; - width: 5px; - height: 10px; - border: solid white; - border-width: 0 3px 3px 0; - transform: rotate(45deg); - } -`; - const InputCheckbox = ({ label, className, @@ -77,7 +58,13 @@ const InputCheckbox = ({ $disabled={disabled} > - {!!value && } + + {!!value && ( +
+ +
+ )} +
{exists(infoTooltip) && } diff --git a/frontend/src/js/ui-components/InputSelect/InputSelectComponents.tsx b/frontend/src/js/ui-components/InputSelect/InputSelectComponents.tsx index 567894245c..a55e37b2ff 100644 --- a/frontend/src/js/ui-components/InputSelect/InputSelectComponents.tsx +++ b/frontend/src/js/ui-components/InputSelect/InputSelectComponents.tsx @@ -71,6 +71,8 @@ export const Input = styled("input")` outline: none; flex-grow: 1; width: 0; /* to fix default width */ + font-weight: 400; + font-size: ${({ theme }) => theme.font.sm}; ${({ disabled }) => disabled && css` diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 0000000000..3e41bb156a --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,28 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], + theme: { + extend: { + colors: { + primary: { + 50: "#dadedb", + 100: "#ccd6d0", + 200: "#98b099", + 500: "#1f5f30", + }, + gray: { + 50: "#eee", + 100: "#dadada", + 400: "#aaa", + 500: "#888", + 800: "#222", + }, + bg: { + 50: "#fafafa", + 100: "#f4f6f5,", + }, + }, + }, + }, + plugins: [], +}; From 5e18a2522511b8f05cbf56127d1645bb7fdcf384 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Mon, 15 Apr 2024 23:58:27 +0200 Subject: [PATCH 05/17] Make it a dynamic disclosure list --- frontend/mock-api/forms/export-form.json | 2 +- .../src/js/external-forms/config-types.ts | 5 +- frontend/src/js/external-forms/form/Field.tsx | 26 +-- .../form/fields/DisclosureField.tsx | 71 -------- .../form/fields/DisclosureListField.tsx | 156 ++++++++++++++++++ frontend/src/js/external-forms/helper.ts | 22 ++- 6 files changed, 193 insertions(+), 89 deletions(-) delete mode 100644 frontend/src/js/external-forms/form/fields/DisclosureField.tsx create mode 100644 frontend/src/js/external-forms/form/fields/DisclosureListField.tsx diff --git a/frontend/mock-api/forms/export-form.json b/frontend/mock-api/forms/export-form.json index 49538be7f1..c1437549d9 100644 --- a/frontend/mock-api/forms/export-form.json +++ b/frontend/mock-api/forms/export-form.json @@ -62,7 +62,7 @@ }, { "name": "theDisclosure", - "type": "DISCLOSURE", + "type": "DISCLOSURE_LIST", "label": { "de": "Datenschutz" }, diff --git a/frontend/src/js/external-forms/config-types.ts b/frontend/src/js/external-forms/config-types.ts index 2c8fedb30d..9747a177f8 100644 --- a/frontend/src/js/external-forms/config-types.ts +++ b/frontend/src/js/external-forms/config-types.ts @@ -12,8 +12,9 @@ interface TranslatableString { export type Forms = Form[]; -export type FormField = Field | Tabs | Group | Disclosure; export type NonFormField = Headline | Description; +export type FormField = Field | Tabs | Group | Disclosure; +export type FormFieldWithValue = Exclude; export type GeneralField = FormField | NonFormField; @@ -37,7 +38,7 @@ export interface Group { } export interface Disclosure { - type: "DISCLOSURE"; + type: "DISCLOSURE_LIST"; creatable?: boolean; defaultOpen?: boolean; name: string; diff --git a/frontend/src/js/external-forms/form/Field.tsx b/frontend/src/js/external-forms/form/Field.tsx index 8e0cf57680..75a9be1a59 100644 --- a/frontend/src/js/external-forms/form/Field.tsx +++ b/frontend/src/js/external-forms/form/Field.tsx @@ -6,14 +6,14 @@ import { useDatasetId } from "../../dataset/selectors"; import type { Language } from "../../localization/useActiveLang"; import type { GeneralField } from "../config-types"; import { Description } from "../form-components/Description"; -import { getInitialValue, isFormField } from "../helper"; +import { getInitialValue, isFormFieldWithValue } from "../helper"; import type { DynamicFormValues } from "./Form"; import { CheckboxField } from "./fields/CheckboxField"; import { ConceptListField } from "./fields/ConceptListField"; import { DatasetSelectField } from "./fields/DatasetSelectField"; import { DateRangeField } from "./fields/DateRangeField"; -import { DisclosureField } from "./fields/DisclosureField"; +import { DisclosureListField } from "./fields/DisclosureListField"; import { GroupField } from "./fields/GroupField"; import { HeadlineField } from "./fields/HeadlineField"; import { NumberField } from "./fields/NumberField"; @@ -39,14 +39,13 @@ const Field = ({ const datasetId = useDatasetId(); const { locale, availableDatasets } = commonProps; - const defaultValue = - isFormField(field) && field.type !== "GROUP" - ? getInitialValue(field, { - availableDatasets, - activeLang: locale, - datasetId, - }) - : null; + const defaultValue = isFormFieldWithValue(field) + ? getInitialValue(field, { + availableDatasets, + activeLang: locale, + datasetId, + }) + : null; switch (field.type) { case "HEADLINE": @@ -121,12 +120,13 @@ const Field = ({ datasetId={datasetId} /> ); - case "DISCLOSURE": + case "DISCLOSURE_LIST": return ( - ); case "GROUP": diff --git a/frontend/src/js/external-forms/form/fields/DisclosureField.tsx b/frontend/src/js/external-forms/form/fields/DisclosureField.tsx deleted file mode 100644 index 527b977abd..0000000000 --- a/frontend/src/js/external-forms/form/fields/DisclosureField.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import styled from "@emotion/styled"; -import { - faChevronDown, - faChevronRight, -} from "@fortawesome/free-solid-svg-icons"; -import { ComponentProps, useState } from "react"; -import { exists } from "../../../common/helpers/exists"; -import FaIcon from "../../../icon/FaIcon"; -import InfoTooltip from "../../../tooltip/InfoTooltip"; -import { Disclosure } from "../../config-types"; -import { getFieldKey } from "../../helper"; -import Field from "../Field"; - -const Details = styled("details")` - overflow: hidden; - border: 1px solid ${({ theme }) => theme.col.gray}; - border-radius: ${({ theme }) => theme.borderRadius}; -`; - -const Summary = styled("summary")` - cursor: pointer; - display: flex; - align-items: center; - gap: 2px; - padding: 12px 12px 12px; - background-color: white; - font-size: ${({ theme }) => theme.font.sm}; - font-weight: 400; -`; - -const DisclosureNestedFields = styled("div")` - display: flex; - flex-direction: column; - gap: 7px; - background-color: ${({ theme }) => theme.col.bg}; - border-top: 1px solid ${({ theme }) => theme.col.gray}; - padding: 12px 10px 12px; -`; - -export const DisclosureField = ({ - field, - commonProps, -}: { - field: Disclosure; - commonProps: Omit, "field">; -}) => { - const [isOpen, setOpen] = useState(false); - - if (field.fields.length === 0) return null; - - const { formType, locale } = commonProps; - - return ( -
setOpen(!isOpen)}> - - - - - {field.label[locale]} - {exists(field.tooltip) && } - - - {field.fields.map((f, i) => { - const key = getFieldKey(formType, f, i); - - return ; - })} - -
- ); -}; diff --git a/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx b/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx new file mode 100644 index 0000000000..f29774ab0b --- /dev/null +++ b/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx @@ -0,0 +1,156 @@ +import { + faAdd, + faChevronDown, + faChevronRight, + faTimes, +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { ComponentProps, useState } from "react"; +import { useFieldArray } from "react-hook-form"; +import tw from "tailwind-styled-components"; +import IconButton from "../../../button/IconButton"; +import { TransparentButton } from "../../../button/TransparentButton"; +import { exists } from "../../../common/helpers/exists"; +import FaIcon from "../../../icon/FaIcon"; +import InfoTooltip from "../../../tooltip/InfoTooltip"; +import { Disclosure } from "../../config-types"; +import { + getFieldKey, + getInitialValue, + isFormFieldWithValue, +} from "../../helper"; +import Field from "../Field"; + +const Summary = tw("summary")` + relative + cursor-pointer + flex + items-center + justify-between + gap-3 + py-3 + pl-3 + pr-10 + bg-white + text-sm + font-normal +`; + +const DisclosureField = ({ + field, + index, + remove, + canRemove, + commonProps, +}: { + field: Disclosure; + index: number; + remove: (index: number) => void; + canRemove?: boolean; + commonProps: Omit, "field">; +}) => { + const [isOpen, setOpen] = useState(false); + + if (field.fields.length === 0) return null; + + const { formType, locale } = commonProps; + + return ( +
setOpen(!isOpen)} + > + +
+ + + + {field.label[locale]} + {exists(field.tooltip) && ( + + )} +
+ {field.creatable && canRemove && ( + remove(index)} + /> + )} +
+
+ {field.fields.map((f, i) => { + const key = getFieldKey(formType, f, i); + const childField = isFormFieldWithValue(f) + ? { ...f, name: `${field.name}[${index}].${f.name}` } + : f; + + console.log(childField.name); + + return ; + })} +
+
+ ); +}; + +export const DisclosureListField = ({ + field, + defaultValue, + commonProps, + datasetId, +}: { + field: Disclosure; + defaultValue: unknown; + commonProps: Omit, "field">; + datasetId: string | null; +}) => { + const { fields, append, remove } = useFieldArray({ + control: commonProps.control, + name: field.name, + }); + console.log(field, defaultValue); + console.log(fields); + + if (field.fields.length === 0) return null; + + const { locale } = commonProps; + + return ( +
+ {fields.map((fd, index) => ( + 1} + commonProps={commonProps} + /> + ))} + {field.creatable && ( + + append( + Object.fromEntries( + field.fields.filter(isFormFieldWithValue).map((f) => [ + f.name, + getInitialValue(f, { + activeLang: locale, + availableDatasets: commonProps.availableDatasets, + datasetId, + }), + ]), + ), + ) + } + > + + + )} +
+ ); +}; diff --git a/frontend/src/js/external-forms/helper.ts b/frontend/src/js/external-forms/helper.ts index c1ff843e84..15f8b8403d 100644 --- a/frontend/src/js/external-forms/helper.ts +++ b/frontend/src/js/external-forms/helper.ts @@ -1,7 +1,11 @@ import type { DatasetT, SelectOptionT } from "../api/types"; import type { Language } from "../localization/useActiveLang"; -import type { FormField, GeneralField, Group } from "./config-types"; +import type { + FormField, + FormFieldWithValue, + GeneralField, +} from "./config-types"; const nonFormFieldTypes = new Set(["HEADLINE", "DESCRIPTION"]); @@ -46,6 +50,12 @@ export const isFormField = (field: GeneralField): field is FormField => { return !nonFormFieldTypes.has(field.type); }; +export const isFormFieldWithValue = ( + field: GeneralField, +): field is FormFieldWithValue => { + return isFormField(field) && field.type !== "GROUP"; +}; + export function collectAllFormFields(fields: GeneralField[]): FormField[] { return fields.filter(isFormField).flatMap((field) => { if (field.type === "GROUP") { @@ -62,7 +72,7 @@ export function collectAllFormFields(fields: GeneralField[]): FormField[] { } export function getInitialValue( - field: Exclude, + field: FormFieldWithValue, context: { availableDatasets: SelectOptionT[]; activeLang: Language; @@ -106,6 +116,14 @@ export function getInitialValue( min: null, max: null, }; + case "DISCLOSURE_LIST": + return [ + Object.fromEntries( + field.fields + .filter(isFormFieldWithValue) + .map((f) => [f.name, getInitialValue(f, context)]), + ), + ]; default: return field.defaultValue || undefined; } From fc6800fc15eb2fde9413a634e39fe6ea02d47188 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Tue, 16 Apr 2024 10:13:01 +0200 Subject: [PATCH 06/17] Add transformQueryToApi --- .../external-forms/form/fields/DisclosureListField.tsx | 3 +-- frontend/src/js/external-forms/transformQueryToApi.ts | 9 +++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx b/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx index f29774ab0b..8d05abb28c 100644 --- a/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx +++ b/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx @@ -86,8 +86,6 @@ const DisclosureField = ({ ? { ...f, name: `${field.name}[${index}].${f.name}` } : f; - console.log(childField.name); - return ; })} @@ -108,6 +106,7 @@ export const DisclosureListField = ({ }) => { const { fields, append, remove } = useFieldArray({ control: commonProps.control, + // @ts-expect-error TODO: figure out how to deal with a dynamic name name: field.name, }); console.log(field, defaultValue); diff --git a/frontend/src/js/external-forms/transformQueryToApi.ts b/frontend/src/js/external-forms/transformQueryToApi.ts index 237c5e5803..2c5e9256f4 100644 --- a/frontend/src/js/external-forms/transformQueryToApi.ts +++ b/frontend/src/js/external-forms/transformQueryToApi.ts @@ -109,6 +109,15 @@ function transformFieldToApiEntries( }, ], ]; + case "DISCLOSURE_LIST": + return [ + [ + rawFieldname, + (formValue as DynamicFormValues[]).map((v) => + transformFieldsToApi(fieldConfig.fields, v), + ), + ], + ]; } } From c196d0b0bdb6574280346aacaf730788de27a857 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Tue, 16 Apr 2024 10:17:51 +0200 Subject: [PATCH 07/17] Format --- frontend/postcss.config.js | 2 +- frontend/src/index.css | 2 +- frontend/tsconfig.json | 23 ++++------------------- 3 files changed, 6 insertions(+), 21 deletions(-) diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js index 2e7af2b7f1..2aa7205d4b 100644 --- a/frontend/postcss.config.js +++ b/frontend/postcss.config.js @@ -3,4 +3,4 @@ export default { tailwindcss: {}, autoprefixer: {}, }, -} +}; diff --git a/frontend/src/index.css b/frontend/src/index.css index bd6213e1df..b5c61c9567 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,3 +1,3 @@ @tailwind base; @tailwind components; -@tailwind utilities; \ No newline at end of file +@tailwind utilities; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 8d376b9839..88804f38bb 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -18,13 +18,7 @@ "module": "ESNext", "target": "es6", "allowJs": true, - "lib": [ - "es5", - "es6", - "dom", - "dom.iterable", - "es2019" - ], + "lib": ["es5", "es6", "dom", "dom.iterable", "es2019"], "forceConsistentCasingInFileNames": true, "noUnusedLocals": true, "noUnusedParameters": true, @@ -32,15 +26,6 @@ "noFallthroughCasesInSwitch": true, "strictBindCallApply": true }, - "include": [ - "src", - "node_modules/**/*/*.d.ts" - ], - "exclude": [ - "node_modules", - "public", - ".cache", - ".idea", - "src/ignored/*" - ] -} \ No newline at end of file + "include": ["src", "node_modules/**/*/*.d.ts"], + "exclude": ["node_modules", "public", ".cache", ".idea", "src/ignored/*"] +} From d0f33972177e6b0c448e6bd532b3fde65d81be83 Mon Sep 17 00:00:00 2001 From: Max Thonagel <12283268+thoniTUB@users.noreply.github.com> Date: Wed, 17 Apr 2024 08:34:47 +0200 Subject: [PATCH 08/17] fixes validation of initial users/roles, adds default for overview scope Signed-off-by: Max Thonagel <12283268+thoniTUB@users.noreply.github.com> --- .../auth/DefaultAuthorizationConfig.java | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/backend/src/main/java/com/bakdata/conquery/models/config/auth/DefaultAuthorizationConfig.java b/backend/src/main/java/com/bakdata/conquery/models/config/auth/DefaultAuthorizationConfig.java index 299b402789..0ba2a1f284 100644 --- a/backend/src/main/java/com/bakdata/conquery/models/config/auth/DefaultAuthorizationConfig.java +++ b/backend/src/main/java/com/bakdata/conquery/models/config/auth/DefaultAuthorizationConfig.java @@ -1,5 +1,6 @@ package com.bakdata.conquery.models.config.auth; +import java.util.Collections; import java.util.List; import java.util.Set; @@ -7,12 +8,15 @@ import com.bakdata.conquery.apiv1.auth.ProtoUser; import com.bakdata.conquery.io.cps.CPSType; import com.bakdata.conquery.models.auth.permissions.AdminPermission; +import com.bakdata.conquery.models.auth.permissions.DatasetPermission; +import com.bakdata.conquery.models.auth.permissions.SuperPermission; +import io.dropwizard.validation.ValidationMethod; import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; -import lombok.Getter; +import lombok.Data; @CPSType(base = AuthorizationConfig.class, id = "DEFAULT") -@Getter +@Data public class DefaultAuthorizationConfig implements AuthorizationConfig { @Valid @@ -21,10 +25,14 @@ public class DefaultAuthorizationConfig implements AuthorizationConfig { .permissions(Set.of(AdminPermission.DOMAIN)) .build()); - @NotEmpty @Valid - private List initialUsers; + private List initialUsers = Collections.emptyList(); @NotEmpty - private List overviewScope; + private List overviewScope = List.of(DatasetPermission.DOMAIN, AdminPermission.DOMAIN, SuperPermission.DOMAIN); + + @ValidationMethod(message = "No initial entities defined. Access will not be possible") + public boolean isInitialAccessPossible() { + return !(initialRoles.isEmpty() && initialUsers.isEmpty()); + } } From fa00eab6f8b84ceada2a5b3d0b795d451a7bf1c4 Mon Sep 17 00:00:00 2001 From: Max Thonagel <12283268+thoniTUB@users.noreply.github.com> Date: Wed, 17 Apr 2024 08:58:07 +0200 Subject: [PATCH 09/17] adds missing filter test Signed-off-by: Max Thonagel <12283268+thoniTUB@users.noreply.github.com> --- .../conquery/service/FilterSearchTest.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/backend/src/test/java/com/bakdata/conquery/service/FilterSearchTest.java b/backend/src/test/java/com/bakdata/conquery/service/FilterSearchTest.java index 825c13df7c..952afc520a 100644 --- a/backend/src/test/java/com/bakdata/conquery/service/FilterSearchTest.java +++ b/backend/src/test/java/com/bakdata/conquery/service/FilterSearchTest.java @@ -67,4 +67,40 @@ public void totals() { assertThat(search.getTotal(filter)).isEqualTo(5); } + + @Test + public void totalsEmptyFiler() { + final IndexConfig indexConfig = new IndexConfig(); + FilterSearch search = new FilterSearch(indexConfig); + + // Column Searchable + SelectFilter filter = new SingleSelectFilter(); + ConceptTreeConnector connector = new ConceptTreeConnector(); + TreeConcept concept = new TreeConcept(); + Column column = new Column(); + Table table = new Table(); + Dataset dataset = new Dataset("test_dataset"); + + table.setName("test_table"); + table.setDataset(dataset); + concept.setDataset(dataset); + concept.setName("test_concept"); + concept.setConnectors(List.of(connector)); + connector.setName("test_connector"); + connector.setFilters(List.of(filter)); + connector.setConcept(concept); + column.setTable(table); + column.setName("test_column"); + column.setSearchDisabled(true); + filter.setColumn(column); + filter.setConnector(connector); + + // Register + filter.getSearchReferences().forEach(searchable -> { + search.addSearches(Map.of(searchable, searchable.createTrieSearch(indexConfig))); + }); + search.shrinkSearch(column); + + assertThat(search.getTotal(filter)).isEqualTo(0); + } } From 4818107724e008063a680698229398db8f99554b Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Mon, 22 Apr 2024 11:40:22 +0200 Subject: [PATCH 10/17] Fix applying defaultValue --- .../src/js/external-forms/FormContainer.tsx | 9 ++++--- .../form/fields/DisclosureListField.tsx | 26 ++++++++++++++----- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/frontend/src/js/external-forms/FormContainer.tsx b/frontend/src/js/external-forms/FormContainer.tsx index 848bef92e7..7ce584e83e 100644 --- a/frontend/src/js/external-forms/FormContainer.tsx +++ b/frontend/src/js/external-forms/FormContainer.tsx @@ -13,11 +13,12 @@ const Root = styled("div")` -webkit-overflow-scrolling: touch; `; -type Props = Omit, "config"> & { +const FormContainer = ({ + config, + ...props +}: Omit, "config"> & { config: FormType | null; -}; - -const FormContainer = ({ config, ...props }: Props) => { +}) => { return ( {exists(config) && ( diff --git a/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx b/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx index 8d05abb28c..eadd1ef16f 100644 --- a/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx +++ b/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx @@ -5,7 +5,7 @@ import { faTimes, } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { ComponentProps, useState } from "react"; +import { ComponentProps, useEffect, useState } from "react"; import { useFieldArray } from "react-hook-form"; import tw from "tailwind-styled-components"; import IconButton from "../../../button/IconButton"; @@ -104,13 +104,27 @@ export const DisclosureListField = ({ commonProps: Omit, "field">; datasetId: string | null; }) => { - const { fields, append, remove } = useFieldArray({ - control: commonProps.control, - // @ts-expect-error TODO: figure out how to deal with a dynamic name + const { fields, append, remove, replace } = useFieldArray({ + // gets control through context name: field.name, }); - console.log(field, defaultValue); - console.log(fields); + + useEffect( + function applyDefaultValue() { + if ( + fields.length === 0 && + exists(defaultValue) && + (defaultValue as unknown[]).length > 0 + ) { + // TODO: Actually, the defaultValue SHOULD get picked up by + // the useFieldArray hook's name and the defaultValues passed + // to useForm above. But somehow, it doesn't. So we have to + // manually apply the default value here. + replace(defaultValue); + } + }, + [fields.length, replace, defaultValue], + ); if (field.fields.length === 0) return null; From 48f9ffb41790451cbd530949614029791acd918f Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Mon, 22 Apr 2024 11:55:41 +0200 Subject: [PATCH 11/17] Allow a create new label --- frontend/src/js/external-forms/config-types.ts | 1 + .../src/js/external-forms/form/fields/DisclosureListField.tsx | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/js/external-forms/config-types.ts b/frontend/src/js/external-forms/config-types.ts index 9747a177f8..6c854261b6 100644 --- a/frontend/src/js/external-forms/config-types.ts +++ b/frontend/src/js/external-forms/config-types.ts @@ -43,6 +43,7 @@ export interface Disclosure { defaultOpen?: boolean; name: string; label: TranslatableString; + createNewLabel?: TranslatableString; tooltip?: TranslatableString; fields: GeneralField[]; } diff --git a/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx b/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx index eadd1ef16f..19910ab7ea 100644 --- a/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx +++ b/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx @@ -144,7 +144,7 @@ export const DisclosureListField = ({ ))} {field.creatable && ( append( @@ -162,6 +162,7 @@ export const DisclosureListField = ({ } > + {field.createNewLabel ? field.createNewLabel[locale] : undefined} )} From ddd02a7a744b91ce8e4748f614aa591fe20cbf9a Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Mon, 22 Apr 2024 14:48:15 +0200 Subject: [PATCH 12/17] Add react types, run npm audit fix --- frontend/package-lock.json | 120 ++++++++++++++++--------------------- frontend/package.json | 11 ++-- frontend/tsconfig.json | 23 +++++-- 3 files changed, 77 insertions(+), 77 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ed2f6481f0..8e86b137b3 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -20,11 +20,6 @@ "@paralleldrive/cuid2": "^2.2.2", "@react-keycloak-fork/web": "^4.0.3", "@tippyjs/react": "^4.2.6", - "@types/file-saver": "^2.0.6", - "@types/react-highlight-words": "^0.16.6", - "@types/react-window": "^1.8.7", - "@typescript-eslint/eslint-plugin": "^6.10.0", - "@typescript-eslint/parser": "^6.10.0", "@vitejs/plugin-react": "^4.1.1", "apache-arrow": "^13.0.0", "autoprefixer": "^10.4.19", @@ -94,16 +89,22 @@ "@types/compression": "^1.7.4", "@types/cors": "^2.8.15", "@types/express": "^4.17.20", + "@types/file-saver": "^2.0.6", "@types/i18next": "^13.0.0", "@types/jest": "^29.5.7", "@types/mustache": "^4.2.4", "@types/node": "^18.15.3", "@types/papaparse": "^5.3.10", + "@types/react": "^18.2.79", "@types/react-datepicker": "^4.19.1", "@types/react-dom": "^18.2.14", + "@types/react-highlight-words": "^0.16.6", "@types/react-list": "^0.8.9", "@types/react-router-dom": "^5.3.3", + "@types/react-window": "^1.8.7", "@types/redux": "^3.6.0", + "@typescript-eslint/eslint-plugin": "^6.10.0", + "@typescript-eslint/parser": "^6.10.0", "body-parser": "^1.20.2", "cors": "^2.8.5", "eslint": "^8.53.0", @@ -5573,6 +5574,7 @@ }, "node_modules/@types/file-saver": { "version": "2.0.6", + "dev": true, "license": "MIT" }, "node_modules/@types/find-cache-dir": { @@ -5712,6 +5714,7 @@ }, "node_modules/@types/json-schema": { "version": "7.0.14", + "dev": true, "license": "MIT" }, "node_modules/@types/lodash": { @@ -5830,11 +5833,11 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "18.2.36", - "license": "MIT", + "version": "18.2.79", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.79.tgz", + "integrity": "sha512-RwGAGXPl9kSXwdNTafkOEuFrTBD5SA2B3iEB96xi8+xu5ddUa/cpvyVCSNn+asgLCTHkb5ZxN8gbuibYJi4s1w==", "dependencies": { "@types/prop-types": "*", - "@types/scheduler": "*", "csstype": "^3.0.2" } }, @@ -5859,6 +5862,7 @@ }, "node_modules/@types/react-highlight-words": { "version": "0.16.6", + "dev": true, "license": "MIT", "dependencies": { "@types/react": "*" @@ -5893,6 +5897,7 @@ }, "node_modules/@types/react-window": { "version": "1.8.7", + "dev": true, "license": "MIT", "dependencies": { "@types/react": "*" @@ -5911,12 +5916,9 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/scheduler": { - "version": "0.16.5", - "license": "MIT" - }, "node_modules/@types/semver": { "version": "7.5.4", + "dev": true, "license": "MIT" }, "node_modules/@types/send": { @@ -5994,6 +5996,7 @@ }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.10.0", + "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.5.1", @@ -6027,6 +6030,7 @@ }, "node_modules/@typescript-eslint/parser": { "version": "6.10.0", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "@typescript-eslint/scope-manager": "6.10.0", @@ -6053,6 +6057,7 @@ }, "node_modules/@typescript-eslint/scope-manager": { "version": "6.10.0", + "dev": true, "license": "MIT", "dependencies": { "@typescript-eslint/types": "6.10.0", @@ -6068,6 +6073,7 @@ }, "node_modules/@typescript-eslint/type-utils": { "version": "6.10.0", + "dev": true, "license": "MIT", "dependencies": { "@typescript-eslint/typescript-estree": "6.10.0", @@ -6093,6 +6099,7 @@ }, "node_modules/@typescript-eslint/types": { "version": "6.10.0", + "dev": true, "license": "MIT", "engines": { "node": "^16.0.0 || >=18.0.0" @@ -6104,6 +6111,7 @@ }, "node_modules/@typescript-eslint/typescript-estree": { "version": "6.10.0", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "@typescript-eslint/types": "6.10.0", @@ -6129,6 +6137,7 @@ }, "node_modules/@typescript-eslint/utils": { "version": "6.10.0", + "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", @@ -6152,6 +6161,7 @@ }, "node_modules/@typescript-eslint/visitor-keys": { "version": "6.10.0", + "dev": true, "license": "MIT", "dependencies": { "@typescript-eslint/types": "6.10.0", @@ -6483,6 +6493,7 @@ }, "node_modules/array-union": { "version": "2.1.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6867,7 +6878,6 @@ }, "node_modules/body-parser": { "version": "1.20.2", - "dev": true, "license": "MIT", "dependencies": { "bytes": "3.1.2", @@ -6890,7 +6900,6 @@ }, "node_modules/body-parser/node_modules/debug": { "version": "2.6.9", - "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -6898,7 +6907,6 @@ }, "node_modules/body-parser/node_modules/ms": { "version": "2.0.0", - "dev": true, "license": "MIT" }, "node_modules/bplist-parser": { @@ -7596,8 +7604,9 @@ "license": "MIT" }, "node_modules/cookie": { - "version": "0.5.0", - "license": "MIT", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "engines": { "node": ">= 0.6" } @@ -8040,6 +8049,7 @@ }, "node_modules/dir-glob": { "version": "3.0.1", + "dev": true, "license": "MIT", "dependencies": { "path-type": "^4.0.0" @@ -8587,15 +8597,16 @@ } }, "node_modules/express": { - "version": "4.18.2", - "license": "MIT", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -8636,51 +8647,18 @@ "express": "4 || 5 || ^5.0.0-beta.1" } }, - "node_modules/express/node_modules/body-parser": { - "version": "1.20.1", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, "node_modules/express/node_modules/debug": { "version": "2.6.9", - "license": "MIT", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dependencies": { "ms": "2.0.0" } }, "node_modules/express/node_modules/ms": { "version": "2.0.0", - "license": "MIT" - }, - "node_modules/express/node_modules/raw-body": { - "version": "2.5.1", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/express/node_modules/safe-buffer": { "version": "5.2.1", @@ -8961,14 +8939,15 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.3", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", "url": "https://github.com/sponsors/RubenVerborgh" } ], - "license": "MIT", "engines": { "node": ">=4.0" }, @@ -9303,6 +9282,7 @@ }, "node_modules/globby": { "version": "11.1.0", + "dev": true, "license": "MIT", "dependencies": { "array-union": "^2.1.0", @@ -9724,9 +9704,10 @@ } }, "node_modules/ip": { - "version": "2.0.0", - "dev": true, - "license": "MIT" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", + "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==", + "dev": true }, "node_modules/ipaddr.js": { "version": "1.9.1", @@ -13413,7 +13394,6 @@ }, "node_modules/raw-body": { "version": "2.5.2", - "dev": true, "license": "MIT", "dependencies": { "bytes": "3.1.2", @@ -14747,6 +14727,7 @@ }, "node_modules/slash": { "version": "3.0.0", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -15275,9 +15256,10 @@ } }, "node_modules/tar": { - "version": "6.2.0", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "dev": true, - "license": "ISC", "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -15622,6 +15604,7 @@ }, "node_modules/ts-api-utils": { "version": "1.0.3", + "dev": true, "license": "MIT", "engines": { "node": ">=16.13.0" @@ -16232,8 +16215,9 @@ } }, "node_modules/vite": { - "version": "4.5.0", - "license": "MIT", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", + "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", "dependencies": { "esbuild": "^0.18.10", "postcss": "^8.4.27", diff --git a/frontend/package.json b/frontend/package.json index ada0f1fd4b..0d165804c0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -34,11 +34,6 @@ "@paralleldrive/cuid2": "^2.2.2", "@react-keycloak-fork/web": "^4.0.3", "@tippyjs/react": "^4.2.6", - "@types/file-saver": "^2.0.6", - "@types/react-highlight-words": "^0.16.6", - "@types/react-window": "^1.8.7", - "@typescript-eslint/eslint-plugin": "^6.10.0", - "@typescript-eslint/parser": "^6.10.0", "@vitejs/plugin-react": "^4.1.1", "apache-arrow": "^13.0.0", "autoprefixer": "^10.4.19", @@ -108,16 +103,22 @@ "@types/compression": "^1.7.4", "@types/cors": "^2.8.15", "@types/express": "^4.17.20", + "@types/file-saver": "^2.0.6", "@types/i18next": "^13.0.0", "@types/jest": "^29.5.7", "@types/mustache": "^4.2.4", "@types/node": "^18.15.3", "@types/papaparse": "^5.3.10", + "@types/react": "^18.2.79", "@types/react-datepicker": "^4.19.1", "@types/react-dom": "^18.2.14", + "@types/react-highlight-words": "^0.16.6", "@types/react-list": "^0.8.9", "@types/react-router-dom": "^5.3.3", + "@types/react-window": "^1.8.7", "@types/redux": "^3.6.0", + "@typescript-eslint/eslint-plugin": "^6.10.0", + "@typescript-eslint/parser": "^6.10.0", "body-parser": "^1.20.2", "cors": "^2.8.5", "eslint": "^8.53.0", diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 88804f38bb..8d376b9839 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -18,7 +18,13 @@ "module": "ESNext", "target": "es6", "allowJs": true, - "lib": ["es5", "es6", "dom", "dom.iterable", "es2019"], + "lib": [ + "es5", + "es6", + "dom", + "dom.iterable", + "es2019" + ], "forceConsistentCasingInFileNames": true, "noUnusedLocals": true, "noUnusedParameters": true, @@ -26,6 +32,15 @@ "noFallthroughCasesInSwitch": true, "strictBindCallApply": true }, - "include": ["src", "node_modules/**/*/*.d.ts"], - "exclude": ["node_modules", "public", ".cache", ".idea", "src/ignored/*"] -} + "include": [ + "src", + "node_modules/**/*/*.d.ts" + ], + "exclude": [ + "node_modules", + "public", + ".cache", + ".idea", + "src/ignored/*" + ] +} \ No newline at end of file From 3c90b872c00cf9213e9c0ca55b9f8e874e07a379 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Mon, 22 Apr 2024 16:19:16 +0200 Subject: [PATCH 13/17] Allow only one open at a time --- frontend/.eslintrc.json | 1 + .../src/js/external-forms/config-types.ts | 1 + .../form/fields/DisclosureListField.tsx | 34 +++++++++++++++++-- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json index 6fbf6bf00a..f4a523e6df 100644 --- a/frontend/.eslintrc.json +++ b/frontend/.eslintrc.json @@ -16,6 +16,7 @@ ], "parser": "@typescript-eslint/parser", "rules": { + "no-debugger": "warn", "eqeqeq": "error", "jsx-a11y/accessible-emoji": "off", "no-case-declarations": "off", diff --git a/frontend/src/js/external-forms/config-types.ts b/frontend/src/js/external-forms/config-types.ts index 6c854261b6..88f91adb29 100644 --- a/frontend/src/js/external-forms/config-types.ts +++ b/frontend/src/js/external-forms/config-types.ts @@ -41,6 +41,7 @@ export interface Disclosure { type: "DISCLOSURE_LIST"; creatable?: boolean; defaultOpen?: boolean; + onlyOneOpenAtATime?: boolean; name: string; label: TranslatableString; createNewLabel?: TranslatableString; diff --git a/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx b/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx index 19910ab7ea..9d8b8406b8 100644 --- a/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx +++ b/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx @@ -39,18 +39,20 @@ const Summary = tw("summary")` const DisclosureField = ({ field, index, + isOpen, + toggleOpen, remove, canRemove, commonProps, }: { field: Disclosure; index: number; + isOpen: boolean; + toggleOpen: () => void; remove: (index: number) => void; canRemove?: boolean; commonProps: Omit, "field">; }) => { - const [isOpen, setOpen] = useState(false); - if (field.fields.length === 0) return null; const { formType, locale } = commonProps; @@ -59,7 +61,15 @@ const DisclosureField = ({
setOpen(!isOpen)} + onToggle={(e) => { + if ( + (isOpen && e.currentTarget.open) || + (!isOpen && !e.currentTarget.open) + ) + return; + + toggleOpen(); + }} >
@@ -93,6 +103,20 @@ const DisclosureField = ({ ); }; +const useOpenState = () => { + const [isOpen, setIsOpen] = useState>({}); + + const toggleOpen = (id: string) => { + setIsOpen((prev) => ({ + // not spreading ...prev here – we're closing all other fields + // when opening a new one + [id]: !prev[id], + })); + }; + + return { isOpen, toggleOpen }; +}; + export const DisclosureListField = ({ field, defaultValue, @@ -109,6 +133,8 @@ export const DisclosureListField = ({ name: field.name, }); + const { isOpen, toggleOpen } = useOpenState(); + useEffect( function applyDefaultValue() { if ( @@ -138,6 +164,8 @@ export const DisclosureListField = ({ field={field} index={index} remove={remove} + isOpen={isOpen[fd.id]} + toggleOpen={() => toggleOpen(fd.id)} canRemove={fields.length > 1} commonProps={commonProps} /> From c22fde28f7b00214877fa55f536d7fcc56d21137 Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Mon, 22 Apr 2024 16:32:35 +0200 Subject: [PATCH 14/17] Fully support one open at a time --- .../src/js/external-forms/config-types.ts | 26 +++++++-------- .../form/fields/DisclosureListField.tsx | 32 +++++++++++++------ 2 files changed, 35 insertions(+), 23 deletions(-) diff --git a/frontend/src/js/external-forms/config-types.ts b/frontend/src/js/external-forms/config-types.ts index 88f91adb29..6f9cc80d95 100644 --- a/frontend/src/js/external-forms/config-types.ts +++ b/frontend/src/js/external-forms/config-types.ts @@ -13,7 +13,7 @@ interface TranslatableString { export type Forms = Form[]; export type NonFormField = Headline | Description; -export type FormField = Field | Tabs | Group | Disclosure; +export type FormField = Field | Tabs | Group; export type FormFieldWithValue = Exclude; export type GeneralField = FormField | NonFormField; @@ -37,18 +37,6 @@ export interface Group { fields: GeneralField[]; } -export interface Disclosure { - type: "DISCLOSURE_LIST"; - creatable?: boolean; - defaultOpen?: boolean; - onlyOneOpenAtATime?: boolean; - name: string; - label: TranslatableString; - createNewLabel?: TranslatableString; - tooltip?: TranslatableString; - fields: GeneralField[]; -} - export interface Tabs { name: string; // Sent to backend API type: "TABS"; @@ -73,6 +61,7 @@ type GREATER_THAN_ZERO_VALIDATION = "GREATER_THAN_ZERO"; /* FIELDS AND THEIR VALIDATIONS */ /* ------------------------------ */ export type Field = + | DisclosureListField | CheckboxField | StringField | TextareaField @@ -110,6 +99,17 @@ export interface Description { /* ------------------------------ */ +export interface DisclosureListField extends CommonField { + type: "DISCLOSURE_LIST"; + creatable?: boolean; + defaultOpen?: boolean; + createNewLabel?: TranslatableString; + fields: GeneralField[]; + onlyOneOpenAtATime?: boolean; +} + +/* ------------------------------ */ + export type CheckboxField = CommonField & { type: "CHECKBOX"; defaultValue?: boolean; // Default: False diff --git a/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx b/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx index 9d8b8406b8..e2ade6508e 100644 --- a/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx +++ b/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx @@ -13,7 +13,7 @@ import { TransparentButton } from "../../../button/TransparentButton"; import { exists } from "../../../common/helpers/exists"; import FaIcon from "../../../icon/FaIcon"; import InfoTooltip from "../../../tooltip/InfoTooltip"; -import { Disclosure } from "../../config-types"; +import { DisclosureListField as DisclosureListFieldT } from "../../config-types"; import { getFieldKey, getInitialValue, @@ -45,7 +45,7 @@ const DisclosureField = ({ canRemove, commonProps, }: { - field: Disclosure; + field: DisclosureListFieldT; index: number; isOpen: boolean; toggleOpen: () => void; @@ -65,8 +65,10 @@ const DisclosureField = ({ if ( (isOpen && e.currentTarget.open) || (!isOpen && !e.currentTarget.open) - ) + ) { + // Without this, we're getting open/close flickering return; + } toggleOpen(); }} @@ -103,13 +105,20 @@ const DisclosureField = ({ ); }; -const useOpenState = () => { - const [isOpen, setIsOpen] = useState>({}); +const useOpenState = ({ + defaultOpen, + onlyOneOpenAtATime = false, +}: { + defaultOpen?: string; + onlyOneOpenAtATime?: boolean; +}) => { + const [isOpen, setIsOpen] = useState>( + defaultOpen ? { [defaultOpen]: true } : {}, + ); const toggleOpen = (id: string) => { setIsOpen((prev) => ({ - // not spreading ...prev here – we're closing all other fields - // when opening a new one + ...(onlyOneOpenAtATime ? {} : prev), [id]: !prev[id], })); }; @@ -123,17 +132,20 @@ export const DisclosureListField = ({ commonProps, datasetId, }: { - field: Disclosure; + field: DisclosureListFieldT; defaultValue: unknown; commonProps: Omit, "field">; datasetId: string | null; }) => { const { fields, append, remove, replace } = useFieldArray({ - // gets control through context + // gets `control` through context name: field.name, }); - const { isOpen, toggleOpen } = useOpenState(); + const { isOpen, toggleOpen } = useOpenState({ + onlyOneOpenAtATime: field.onlyOneOpenAtATime, + defaultOpen: field.defaultOpen ? fields[0]?.id : undefined, + }); useEffect( function applyDefaultValue() { From 003ca44625a429088e997e99d02fdce233aa5c9f Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Mon, 22 Apr 2024 16:37:59 +0200 Subject: [PATCH 15/17] Fix validators --- frontend/src/js/external-forms/validators.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/src/js/external-forms/validators.ts b/frontend/src/js/external-forms/validators.ts index a4fcd4e953..6c471c918e 100644 --- a/frontend/src/js/external-forms/validators.ts +++ b/frontend/src/js/external-forms/validators.ts @@ -7,6 +7,7 @@ import { isValidSelect } from "../model/select"; import { CheckboxField, ConceptListField, + DisclosureListField, Field, FormField, } from "./config-types"; @@ -147,9 +148,12 @@ function getConfigurableValidations(fieldType: string) { const isFieldWithValidations = ( field: FormField, -): field is Exclude => { +): field is Exclude => { return ( - field.type !== "TABS" && field.type !== "GROUP" && field.type !== "CHECKBOX" + field.type !== "TABS" && + field.type !== "GROUP" && + field.type !== "CHECKBOX" && + field.type !== "DISCLOSURE_LIST" ); }; From 2878584bfca2c9bf5cd5efbdd43bd67f729666ad Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Mon, 22 Apr 2024 16:41:07 +0200 Subject: [PATCH 16/17] And add to default validations --- frontend/src/js/external-forms/validators.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/js/external-forms/validators.ts b/frontend/src/js/external-forms/validators.ts index 6c471c918e..5d97ac7605 100644 --- a/frontend/src/js/external-forms/validators.ts +++ b/frontend/src/js/external-forms/validators.ts @@ -126,6 +126,7 @@ const DEFAULT_VALIDATION_BY_TYPE: Record< // MULTI_SELECT: null, // @ts-ignore TODO: Refactor using generics to try and tie the `field` to its `value` DATE_RANGE: validateDateRange, + DISCLOSURE_LIST: null, }; function getNotEmptyValidation(fieldType: string) { From 65dc5fdaf17fc40f977986f2da683df7b63e6e1b Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Mon, 22 Apr 2024 18:30:50 +0200 Subject: [PATCH 17/17] Re-trigger validation of default values after applying them to disclosure list field --- frontend/src/js/external-forms/form/Field.tsx | 8 +++++++- frontend/src/js/external-forms/form/Form.tsx | 1 + .../js/external-forms/form/fields/DisclosureListField.tsx | 3 ++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/frontend/src/js/external-forms/form/Field.tsx b/frontend/src/js/external-forms/form/Field.tsx index 75a9be1a59..c304f56e33 100644 --- a/frontend/src/js/external-forms/form/Field.tsx +++ b/frontend/src/js/external-forms/form/Field.tsx @@ -1,5 +1,10 @@ import { memo } from "react"; -import { Control, UseFormRegister, UseFormSetValue } from "react-hook-form"; +import { + Control, + UseFormRegister, + UseFormSetValue, + UseFormTrigger, +} from "react-hook-form"; import type { SelectOptionT } from "../../api/types"; import { useDatasetId } from "../../dataset/selectors"; @@ -35,6 +40,7 @@ const Field = ({ register: UseFormRegister; setValue: UseFormSetValue; control: Control; + trigger: UseFormTrigger; }) => { const datasetId = useDatasetId(); const { locale, availableDatasets } = commonProps; diff --git a/frontend/src/js/external-forms/form/Form.tsx b/frontend/src/js/external-forms/form/Form.tsx index 43d3fdfdaf..22cdaad5fe 100644 --- a/frontend/src/js/external-forms/form/Form.tsx +++ b/frontend/src/js/external-forms/form/Form.tsx @@ -46,6 +46,7 @@ const Form = memo(({ config, datasetOptions, methods }: Props) => { h1Index={h1Index} register={methods.register} control={methods.control} + trigger={methods.trigger} field={field} setValue={methods.setValue} availableDatasets={datasetOptions} diff --git a/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx b/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx index e2ade6508e..d8b60729da 100644 --- a/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx +++ b/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx @@ -159,9 +159,10 @@ export const DisclosureListField = ({ // to useForm above. But somehow, it doesn't. So we have to // manually apply the default value here. replace(defaultValue); + setTimeout(() => commonProps.trigger(), 100); } }, - [fields.length, replace, defaultValue], + [fields.length, replace, defaultValue, commonProps], ); if (field.fields.length === 0) return null;