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/mock-api/forms/export-form.json b/frontend/mock-api/forms/export-form.json index 789479e699..c1437549d9 100644 --- a/frontend/mock-api/forms/export-form.json +++ b/frontend/mock-api/forms/export-form.json @@ -60,6 +60,41 @@ }, "validations": ["NOT_EMPTY"] }, + { + "name": "theDisclosure", + "type": "DISCLOSURE_LIST", + "label": { + "de": "Datenschutz" + }, + "creatable": true, + "tooltip": { + "de": "Der Datenschutz ist ein wichtiges Thema, deshalb musst du hier zustimmen." + }, + "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/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..2aa7205d4b --- /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..b5c61c9567 --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; 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/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/config-types.ts b/frontend/src/js/external-forms/config-types.ts index c10b8f37ba..6c854261b6 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; export type NonFormField = Headline | Description; +export type FormField = Field | Tabs | Group | Disclosure; +export type FormFieldWithValue = Exclude; export type GeneralField = FormField | NonFormField; @@ -36,6 +37,17 @@ export interface Group { fields: GeneralField[]; } +export interface Disclosure { + type: "DISCLOSURE_LIST"; + creatable?: boolean; + defaultOpen?: boolean; + name: string; + label: TranslatableString; + createNewLabel?: TranslatableString; + tooltip?: TranslatableString; + fields: GeneralField[]; +} + export interface Tabs { name: string; // Sent to backend API type: "TABS"; @@ -90,7 +102,7 @@ export interface Headline { /* ------------------------------ */ -interface Description { +export interface Description { type: "DESCRIPTION"; label: TranslatableString; } @@ -105,7 +117,7 @@ export type CheckboxField = CommonField & { /* ------------------------------ */ type StringFieldValidation = NOT_EMPTY_VALIDATION; -type StringField = CommonField & { +export type StringField = CommonField & { type: "STRING"; placeholder?: TranslatableString; defaultValue?: string; // Default: "" @@ -119,7 +131,7 @@ type StringField = CommonField & { /* ------------------------------ */ type TextareaFieldValidation = NOT_EMPTY_VALIDATION; -type TextareaField = CommonField & { +export type TextareaField = CommonField & { type: "TEXTAREA"; placeholder?: TranslatableString; defaultValue?: string; // Default: "" @@ -134,7 +146,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; @@ -153,13 +165,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[]; }; @@ -177,7 +189,7 @@ type DatasetSelectField = CommonField & { /* ------------------------------ */ type DateRangeFieldValidation = NOT_EMPTY_VALIDATION; -type DateRangeField = CommonField & { +export type DateRangeField = CommonField & { type: "DATE_RANGE"; validations?: DateRangeFieldValidation[]; }; @@ -185,7 +197,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 6e41cecf0c..75a9be1a59 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, isFormFieldWithValue } 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 { DisclosureListField } from "./fields/DisclosureListField"; +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,27 +37,19 @@ 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" - ? getInitialValue(field, { - availableDatasets, - activeLang: locale, - datasetId, - }) - : null; + const defaultValue = isFormFieldWithValue(field) + ? getInitialValue(field, { + availableDatasets, + activeLang: locale, + datasetId, + }) + : null; 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 "GROUP": + case "DISCLOSURE_LIST": return ( - <> - {field.label && {field.label[locale]}} - {field.description && ( - {field.description[locale]} - )} - - {field.fields.map((f, i) => { - const key = getFieldKey(formType, f, i); - - return ; - })} - - + ); + case "GROUP": + 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/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/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/DisclosureListField.tsx b/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx new file mode 100644 index 0000000000..19910ab7ea --- /dev/null +++ b/frontend/src/js/external-forms/form/fields/DisclosureListField.tsx @@ -0,0 +1,170 @@ +import { + faAdd, + faChevronDown, + faChevronRight, + faTimes, +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { ComponentProps, useEffect, 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; + + return ; + })} +
+
+ ); +}; + +export const DisclosureListField = ({ + field, + defaultValue, + commonProps, + datasetId, +}: { + field: Disclosure; + defaultValue: unknown; + commonProps: Omit, "field">; + datasetId: string | null; +}) => { + const { fields, append, remove, replace } = useFieldArray({ + // gets control through context + name: field.name, + }); + + 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; + + 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, + }), + ]), + ), + ) + } + > + + {field.createNewLabel ? field.createNewLabel[locale] : undefined} + + )} +
+ ); +}; 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} + /> + )} + + ); +}; 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; } 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), + ), + ], + ]; } } 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: [], +}; 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/*"] +}