├── .dockerignore ├── .env ├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── build.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── .vscode ├── settings.json └── snippets-nhn.code-snippets ├── Dockerfile ├── LICENSE ├── Pipelines └── Skjemabyggeren-pipeline.yml ├── README.md ├── decs.d.ts ├── functions ├── authorization-code.js ├── end-session.js ├── get-token.js └── util │ └── client-context.js ├── netlify.toml ├── package-lock.json ├── package.json ├── public ├── _redirects ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.tsx ├── components │ ├── Accordion │ │ ├── Accordion.css │ │ └── Accordion.tsx │ ├── AdvancedQuestionOptions │ │ ├── AdvancedQuestionOptions.css │ │ ├── AdvancedQuestionOptions.tsx │ │ ├── CalculatedExpression │ │ │ └── CalculatedExpression.tsx │ │ ├── Code │ │ │ └── Codes.tsx │ │ ├── CopyFrom │ │ │ └── CopyFrom.tsx │ │ ├── FhirPathSelect │ │ │ └── FhirPathSelect.tsx │ │ ├── Guidance │ │ │ ├── GuidanceAction.tsx │ │ │ └── GuidanceParam.tsx │ │ ├── HyperlinkTargetElementToggle.tsx │ │ ├── Initial │ │ │ ├── Initial.tsx │ │ │ ├── InitialInputTypeBoolean.tsx │ │ │ ├── InitialInputTypeChoice.tsx │ │ │ ├── InitialInputTypeDecimal.tsx │ │ │ ├── InitialInputTypeInteger.tsx │ │ │ └── InitialInputTypeString.tsx │ │ └── View │ │ │ └── view.tsx │ ├── AnchorMenu │ │ ├── AnchorMenu.css │ │ ├── AnchorMenu.tsx │ │ └── ItemButtons │ │ │ ├── ItemButtons.css │ │ │ └── ItemButtons.tsx │ ├── AnswerOption │ │ ├── AnswerOption.css │ │ ├── AnswerOption.tsx │ │ ├── DraggableAnswerOptions.tsx │ │ └── testi.json │ ├── Btn │ │ ├── Btn.css │ │ └── Btn.tsx │ ├── CheckboxBtn │ │ ├── CheckboxBtn.css │ │ ├── CheckboxBtn.tsx │ │ └── InfoCheckbox.tsx │ ├── DatePicker │ │ ├── DatePicker.css │ │ ├── DatePicker.tsx │ │ └── DateTimePicker.tsx │ ├── Drawer │ │ ├── Drawer.css │ │ ├── Drawer.tsx │ │ └── FormDetailsDrawer │ │ │ └── FormDetailsDrawer.tsx │ ├── EnableWhen │ │ ├── EnableBehavior.tsx │ │ ├── EnableWhen.css │ │ ├── EnableWhen.tsx │ │ ├── EnableWhenAnswerTypes.tsx │ │ ├── EnableWhenInfoBox.tsx │ │ ├── EnableWhenOperator.tsx │ │ └── Infobox.tsx │ ├── FormField │ │ ├── FormField.tsx │ │ └── UriField.tsx │ ├── IconBtn │ │ ├── IconBtn.css │ │ └── IconBtn.tsx │ ├── ImportValueSet │ │ ├── ImportValueSet.css │ │ └── ImportValueSet.tsx │ ├── InputField │ │ └── inputField.tsx │ ├── JSONView │ │ └── JSONView.tsx │ ├── Languages │ │ ├── LanguageAccordion.tsx │ │ ├── Languages.css │ │ ├── Languages.tsx │ │ └── Translation │ │ │ ├── TranslateContainedValueSets.tsx │ │ │ ├── TranslateItemRow.tsx │ │ │ ├── TranslateMetaData.tsx │ │ │ ├── TranslateMetaDataRow.tsx │ │ │ ├── TranslateOptionRow.tsx │ │ │ ├── TranslateSettings.tsx │ │ │ ├── TranslateSidebar.tsx │ │ │ ├── TranslationModal.css │ │ │ └── TranslationModal.tsx │ ├── MarkdownEditor │ │ ├── MarkdownEditor.css │ │ ├── MarkdownEditor.tsx │ │ └── useDebounce.tsx │ ├── Metadata │ │ ├── MetaSecurityEditor.tsx │ │ ├── MetadataEditor.tsx │ │ └── QuestionnaireSettings.tsx │ ├── Modal │ │ ├── Modal.css │ │ └── Modal.tsx │ ├── Navbar │ │ ├── Navbar.css │ │ └── Navbar.tsx │ ├── PredefinedValueSetModal │ │ ├── PredefinedValueSetModal.css │ │ └── PredefinedValueSetModal.tsx │ ├── Question │ │ ├── Question.css │ │ ├── Question.tsx │ │ ├── QuestionType │ │ │ ├── Choice.tsx │ │ │ ├── ChoiceTypeSelect.tsx │ │ │ ├── DateType.tsx │ │ │ ├── Infotext.tsx │ │ │ ├── OptionReference.css │ │ │ ├── OptionReference.tsx │ │ │ └── PredefinedValueSets.tsx │ │ ├── UnitType │ │ │ └── UnitTypeSelector.tsx │ │ └── ValidationAnswerTypes │ │ │ ├── FhirPathDateValidation.tsx │ │ │ ├── ValidationAnswerTypeAttachment.tsx │ │ │ ├── ValidationAnswerTypeDate.tsx │ │ │ ├── ValidationAnswerTypeNumber.tsx │ │ │ ├── ValidationAnswerTypeString.tsx │ │ │ └── ValidationAnswerTypes.tsx │ ├── QuestionDrawer │ │ ├── QuestionDrawer.css │ │ └── QuestionDrawer.tsx │ ├── RadioBtn │ │ ├── RadioBtn.css │ │ └── RadioBtn.tsx │ ├── Refero │ │ ├── FormFillerPreview.tsx │ │ ├── FormFillerSidebar.tsx │ │ └── styles │ │ │ ├── fieldset.scss │ │ │ ├── formFillerPreview.css │ │ │ ├── formFillerSidebar.css │ │ │ ├── helpbutton.scss │ │ │ ├── navigator.scss │ │ │ └── refero.scss │ ├── Select │ │ ├── GroupedSelect.tsx │ │ ├── Select.css │ │ └── Select.tsx │ ├── Sidebar │ │ └── Sidebar.tsx │ ├── Spinner │ │ ├── Spinner.css │ │ ├── Spinner.tsx │ │ ├── SpinnerBox.css │ │ └── SpinnerBox.tsx │ ├── SwitchBtn │ │ ├── SwitchBtn.css │ │ └── SwitchBtn.tsx │ ├── Typeahead │ │ ├── Typeahead.css │ │ └── Typeahead.tsx │ └── ValidationErrorsModal │ │ └── validationErrorsModal.tsx ├── contexts │ └── UserContext.tsx ├── helpers │ ├── CreateUUID.ts │ ├── EnrichmentSet.ts │ ├── FhirToTreeStateMapper.ts │ ├── LanguageHelper.ts │ ├── MetadataHelper.test.ts │ ├── MetadataHelper.ts │ ├── QuestionHelper.ts │ ├── answerOptionHelper.ts │ ├── codeHelper.ts │ ├── emptyPropertyReplacer.ts │ ├── enableWhenValidConditional.ts │ ├── enumHelper.ts │ ├── exportTranslations.ts │ ├── extensionHelper.ts │ ├── fhirPathDateValidation.ts │ ├── formatHelper.ts │ ├── generateQuestionnaire.test.ts │ ├── generateQuestionnaire.ts │ ├── globalVisibilityHelper.ts │ ├── i18n.ts │ ├── initPredefinedValueSet.ts │ ├── itemControl.ts │ ├── orphanValidation.ts │ ├── questionTypeFeatures.ts │ ├── treeHelper.ts │ ├── uriHelper.ts │ └── valueSetHelper.ts ├── hooks │ ├── useItemNavigation.ts │ ├── useKeyPress.ts │ └── useOutsideClick.ts ├── images │ ├── icons │ │ ├── add-outline.svg │ │ ├── alert-circle-outline.svg │ │ ├── arrow-undo-outline.svg │ │ ├── calendar-outline.svg │ │ ├── chevron-down-outline.svg │ │ ├── close-outline.svg │ │ ├── copy-outline.svg │ │ ├── ellipsis-horizontal-outline.svg │ │ ├── folder-outline.svg │ │ ├── help-circle-outline.svg │ │ ├── information-circle-outline.svg │ │ ├── reorder-three-outline.svg │ │ ├── search-outline.svg │ │ ├── time-outline.svg │ │ └── trash-outline.svg │ └── loader.gif ├── index.css ├── index.tsx ├── locales │ ├── fr-FR │ │ └── translation.json │ ├── nb-NO │ │ └── translation.json │ ├── referoResources.tsx │ └── referoSidebarResources.tsx ├── react-app-env.d.ts ├── router │ ├── history.ts │ └── index.tsx ├── serviceWorker.ts ├── setupTests.ts ├── store │ └── treeStore │ │ ├── indexedDbHelper.ts │ │ ├── treeActions.ts │ │ └── treeStore.tsx ├── types │ ├── IQuestionnaireMetadataType.ts │ ├── IQuestionnareItemType.ts │ ├── LanguageTypes.ts │ ├── OptionTypes.ts │ ├── dev.d.ts │ ├── fhir.ts │ ├── hyperlinkTargetType.ts │ └── scoringFormulas.ts ├── utils │ ├── answerOptionExtensionUtils.ts │ ├── itemSearchUtils.ts │ └── scoringUtils.ts └── views │ ├── FormBuilder.css │ ├── FormBuilder.tsx │ ├── FrontPage.css │ └── FrontPage.tsx ├── stylelint.config.js └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .env 4 | .git 5 | .gitignore 6 | .github 7 | .vscode 8 | .functions 9 | Pipelines -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | GENERATE_SOURCEMAP=false 2 | #BROWSER="none" 3 | CLIENT_ID="61e577d9-4158-4849-af11-0f01c374634d" 4 | OPENID_ISSUER="https://helseid-sts.test.nhn.no" 5 | REACT_APP_URL="http://localhost:3000" 6 | CLAVIS="" 7 | CINCINNO="GULT-SKILT" -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | src/serviceWorker.ts 4 | src/react-app-env.d.ts 5 | src/setupTests.ts -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', // Specifies the ESLint parser 3 | parserOptions: { 4 | ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features 5 | sourceType: 'module', // Allows for the use of imports 6 | ecmaFeatures: { 7 | jsx: true, // Allows for the parsing of JSX 8 | }, 9 | }, 10 | settings: { 11 | react: { 12 | version: 'detect', // Tells eslint-plugin-react to automatically detect the version of React to use 13 | }, 14 | }, 15 | extends: [ 16 | 'plugin:react/recommended', // Uses the recommended rules from @eslint-plugin-react 17 | 'plugin:react-hooks/recommended', 18 | 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from @typescript-eslint/eslint-plugin 19 | 'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier 20 | 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. 21 | // Make sure this is always the last configuration in the extends array. 22 | ], 23 | rules: { 24 | // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs 25 | // e.g. "@typescript-eslint/explicit-function-return-type": "off", 26 | '@typescript-eslint/no-empty-interface': 'off', 27 | 'prettier/prettier': 'warn', 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: triage-needed 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: triage-needed 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: '14' 19 | - run: npm ci 20 | - run: npm run build --if-present 21 | # - run: npm test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | .idea 21 | .eslintcache 22 | examples/form-filler/node_modules 23 | examples/form-filler/wwwroot 24 | 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | debug.log 29 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Generated file - do not reformat 2 | src/types/fhir.ts -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: true, 3 | endOfLine:'auto', 4 | trailingComma: 'all', 5 | singleQuote: true, 6 | tabWidth: 4, 7 | printWidth: 120, 8 | }; -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules\\typescript\\lib", 3 | "eslint.enable": true, 4 | "eslint.alwaysShowStatus": true, 5 | "eslint.lintTask.enable": true, 6 | "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"], 7 | "editor.formatOnSave": true, 8 | "prettier.requireConfig": true, 9 | "editor.defaultFormatter": "esbenp.prettier-vscode" 10 | } -------------------------------------------------------------------------------- /.vscode/snippets-nhn.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | // Place your structor workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and 3 | // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope 4 | // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is 5 | // used to trigger the snippet and the body will be expanded and inserted. Possible variables are: 6 | // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. 7 | // Placeholders with the same ids are connected. 8 | // Example: 9 | // "Print to console": { 10 | // "scope": "javascript,typescript", 11 | // "prefix": "log", 12 | // "body": [ 13 | // "console.log('$1');", 14 | // "$2" 15 | // ], 16 | // "description": "Log output to console" 17 | // } 18 | "React Component Typescript": { 19 | "scope": "typescript, typescriptreact", 20 | "prefix": "component TS", 21 | "body": [ 22 | "import React, { useState } from 'react';", 23 | "", 24 | "type $2Props = {", 25 | "type?: any;", 26 | "};", 27 | "", 28 | "const $1 = ({ type }: $2Props): JSX.Element => {", 29 | "const [startDate, setStartDate] = useState();", 30 | "", 31 | "return (", 32 | "<>", 33 | "", 34 | ");", 35 | "};", 36 | "", 37 | "export default $1;", 38 | ], 39 | "description": "Component React Typescript" 40 | }, 41 | "React UseState": { 42 | "scope": "typescript, typescriptreact", 43 | "prefix": "useState TS", 44 | "body": [ 45 | "const [$1, set$1] = useState();", 46 | ], 47 | "description": "Component React UseState" 48 | } 49 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Mulit-stage build 2 | # 1. Build image (temporary) 3 | # 2. Copy output from temporary image to runnable image 4 | # 5 | # Build with: 6 | # docker build -t helsenorge/skjemabygger . 7 | # 8 | # Run with (example): 9 | # docker run -p 8090:80 --rm -it --name helsenorge-skjemabygger helsenorge/skjemabygger 10 | 11 | ### 1. Build image (temporary) ### 12 | FROM node:14-bullseye-slim as build 13 | 14 | # Set the working directory to /src 15 | WORKDIR /src 16 | 17 | # Copy the package.json and package-lock.json files to the container 18 | COPY package*.json ./ 19 | 20 | # Install dependencies 21 | RUN npm install 22 | 23 | # Copy the rest of the application code to the container 24 | COPY . . 25 | RUN npm run build --if-present 26 | 27 | ### 2 Runnable image ### 28 | FROM nginx:1.21.0-alpine 29 | 30 | COPY --from=build /src/build /usr/share/nginx/html 31 | EXPOSE 80 32 | CMD ["nginx", "-g", "daemon off;"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Norsk Helsenett SF 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Pipelines/Skjemabyggeren-pipeline.yml: -------------------------------------------------------------------------------- 1 | resources: 2 | repositories: 3 | - repository: templates # internt navn på repo, brukes i referanser 4 | type: git # type repo (les mer i dokumentasjon) 5 | name: HN-AzureDevopsPipelines # Navn på repo som inneholder templates 6 | 7 | trigger: 8 | branches: 9 | include: 10 | - master 11 | - release/* 12 | - hotfix/* 13 | - feature/* 14 | pool: 15 | name: Helsenorge_LinuxScaleSetAgents 16 | 17 | variables: 18 | - group: Release-versjonering # henter ut variabelsett der versjonsnummer for releaser er satt¨ 19 | - template: Templates/Variables/versioning-variables.yml@templates # Setter versjonerings variabel basert på branch. Bruker verdier fra Release-versjonering variabelsett 20 | - name: ENABLE_STYLELINT_PLUGIN 21 | value: true 22 | 23 | stages: 24 | # Stage - Build versioning 25 | - template: Templates/Versioning/pipeline-version-stage.yml@templates 26 | parameters: 27 | build_version: ${{ variables.build_version }} # variable hentet fra variabeltemplate Templates/Variables/versioning.yml@templates 28 | 29 | # Stage - Containers 30 | - stage: Containers 31 | dependsOn: ['Versioning'] 32 | displayName: Build and Push containers 33 | jobs: 34 | - template: Templates/Docker/build-and-push-containers-job.yml@templates 35 | parameters: 36 | containers: 37 | - containerRepository: helsenorge/skjemabyggeren 38 | dockerFile: Dockerfile 39 | tag: ${{ variables.build_version }} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Structor - FHIR form builder 2 | 3 | Structor form builder is an open source tool for building FHIR Questionnaire forms. A live demo could be found at [formdesigner.helsenorgelab.no/](https://formdesigner.helsenorgelab.no/). 4 | 5 | ## FHIR Questionnaires 6 | 7 | The FHIR specification defines [Questionnaires](https://www.hl7.org/fhir/questionnaire.html): 8 | 9 | > A structured set of questions intended to guide the collection of answers from end-users. Questionnaires provide detailed control over order, presentation, phraseology and grouping to allow coherent, consistent data collection. 10 | 11 | ## Quickstart 12 | 13 | Either open the demo at [formdesigner.helsenorgelab.no/](https://formdesigner.helsenorgelab.no/) or clone this repo, install Typescript, run `npm install` and run `npm start`. 14 | 15 | ## Netlify functions 16 | 17 | Run `npm install -g netlify-cli` before running npm run functions :) 18 | 19 | ## Docker 20 | See [Dockerfile](Dockerfile) for info. -------------------------------------------------------------------------------- /decs.d.ts: -------------------------------------------------------------------------------- 1 | declare module '@nosferatu500/react-sortable-tree'; 2 | -------------------------------------------------------------------------------- /functions/authorization-code.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const { generators } = require('openid-client'); 3 | const clientContext = require('./util/client-context'); 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 6 | exports.handler = async (event, context) => { 7 | const code_verifier = generators.codeVerifier(); 8 | const code_challenge = generators.codeChallenge(code_verifier); 9 | 10 | const client = await clientContext.createClient(); 11 | 12 | let authUrl = client.authorizationUrl({ 13 | redirect_uri: `${process.env.REACT_APP_URL}/code`, 14 | scope: 'openid profile helseid://scopes/identity/pid helseid://scopes/identity/security_level', 15 | response_type: 'code', 16 | code_challenge, 17 | code_challenge_method: 'S256', 18 | }); 19 | 20 | return { statusCode: 200, body: JSON.stringify({ code_verifier, auth_url: authUrl }) }; 21 | }; 22 | -------------------------------------------------------------------------------- /functions/end-session.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const clientContext = require('./util/client-context'); 3 | const cookie = require('cookie'); 4 | const CryptoJS = require('crypto-js'); 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 7 | exports.handler = async (event, context) => { 8 | const cookies = cookie.parse(event.headers.cookie); 9 | 10 | if (!cookies || !cookies.auth_cookie) { 11 | return { statusCode: 400, body: 'missing cookie..' }; 12 | } 13 | 14 | const bytes = CryptoJS.AES.decrypt(cookies.auth_cookie, process.env.CINCINNO); 15 | const access_token = bytes.toString(CryptoJS.enc.Utf8); 16 | 17 | const client = await clientContext.createClient(); 18 | 19 | const redirectUri = client.endSessionUrl({ 20 | id_token_hint: access_token, 21 | post_logout_redirect_uri: `${process.env.REACT_APP_URL}/`, 22 | }); 23 | 24 | const clearAuthCookie = cookie.serialize('auth_cookie', '', { 25 | secure: true, 26 | httpOnly: true, 27 | path: '/', 28 | maxAge: 0, 29 | }); 30 | 31 | return { 32 | statusCode: 200, 33 | body: JSON.stringify({ url: redirectUri }), 34 | headers: { 35 | 'Set-Cookie': clearAuthCookie, 36 | 'Cache-Control': 'no-cache', 37 | 'Content-Type': 'text/html', 38 | }, 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /functions/get-token.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const axios = require('axios'); 3 | const { SignJWT } = require('jose/jwt/sign'); 4 | const { parseJwk } = require('jose/jwk/parse'); 5 | const clientContext = require('./util/client-context'); 6 | const qs = require('qs'); 7 | const cookie = require('cookie'); 8 | const CryptoJS = require('crypto-js'); 9 | 10 | function createCookie(token) { 11 | const hour = 3600000; 12 | const eightHours = 1 * 8 * hour; 13 | const ciphertext = CryptoJS.AES.encrypt(token, process.env.CINCINNO).toString(); 14 | return cookie.serialize('auth_cookie', ciphertext, { 15 | secure: true, 16 | httpOnly: true, 17 | path: '/', 18 | maxAge: eightHours, 19 | }); 20 | } 21 | 22 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 23 | exports.handler = async (event, context) => { 24 | const client = await clientContext.createClient(); 25 | 26 | const code = event.queryStringParameters.code; 27 | const verifyCode = event.queryStringParameters.code_verifier; 28 | 29 | if (!code || !verifyCode) { 30 | return { 31 | statusCode: 400, 32 | body: JSON.stringify({ 33 | error: 'Missing code or verifycode.', 34 | }), 35 | }; 36 | } 37 | 38 | const privateKey = await parseJwk(JSON.parse(process.env.CLAVIS), 'PS256'); 39 | 40 | const clientAssertionjwt = await new SignJWT({ 41 | sub: process.env.CLIENT_ID, 42 | client_id: process.env.CLIENT_ID, 43 | }) 44 | .setProtectedHeader({ alg: 'PS256' }) 45 | .setIssuedAt() 46 | .setAudience(`${process.env.OPENID_ISSUER}/connect/token`) 47 | .setIssuer(process.env.CLIENT_ID || 'SET-CLIENT-ID') 48 | .setExpirationTime('60s') 49 | .sign(privateKey); 50 | 51 | const body = { 52 | grant_type: 'authorization_code', 53 | code: code, 54 | redirect_uri: `${process.env.REACT_APP_URL}/code`, 55 | client_id: process.env.CLIENT_ID, 56 | code_verifier: verifyCode, 57 | scope: 'openid profile helseid://scopes/identity/pid helseid://scopes/identity/security_level', 58 | client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer', 59 | client_assertion: clientAssertionjwt, 60 | }; 61 | 62 | const options = { 63 | url: `${process.env.OPENID_ISSUER}/connect/token`, 64 | method: 'POST', 65 | headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 66 | data: qs.stringify(body), 67 | }; 68 | 69 | try { 70 | const tokenSetRespons = await axios(options); 71 | const { access_token } = tokenSetRespons.data; 72 | const userInfo = await client.userinfo(access_token); 73 | const accessCookie = createCookie(access_token); 74 | return { 75 | statusCode: 200, 76 | body: JSON.stringify(userInfo), 77 | headers: { 78 | 'Set-Cookie': accessCookie, 79 | 'Cache-Control': 'no-cache', 80 | 'Content-Type': 'text/html', 81 | }, 82 | }; 83 | } catch (error) { 84 | return { statusCode: 400, body: JSON.stringify({ message: 'User error', error }) }; 85 | } 86 | }; 87 | -------------------------------------------------------------------------------- /functions/util/client-context.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const { Issuer } = require('openid-client'); 3 | 4 | const createClient = async () => { 5 | const ehelseIssuer = await Issuer.discover(`${process.env.OPENID_ISSUER}/.well-known/openid-configuration`); 6 | 7 | return new ehelseIssuer.Client({ 8 | client_id: process.env.CLIENT_ID, 9 | redirect_uris: [`${process.env.REACT_APP_URL}/code`], 10 | response_types: ['code'], 11 | }); 12 | }; 13 | 14 | exports.createClient = createClient; 15 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | functions = "functions/" -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "formbuilder", 3 | "version": "1.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "@ckeditor/ckeditor5-react": "^3.0.0", 7 | "@helsenorge/autosuggest": "^28.2.1", 8 | "@helsenorge/ckeditor5-build-markdown": "^24.0.0", 9 | "@helsenorge/core-utils": "^28.2.1", 10 | "@helsenorge/date-time": "^28.2.1", 11 | "@helsenorge/designsystem-react": "^4.3.0", 12 | "@helsenorge/file-upload": "^28.2.1", 13 | "@helsenorge/form": "^28.2.1", 14 | "@helsenorge/refero": "^13.2.2", 15 | "@helsenorge/toolkit": "^21.3.0-prerelease", 16 | "@nosferatu500/react-sortable-tree": "^3.0.3", 17 | "axios": "^0.21.4", 18 | "crypto-js": "^4.1.1", 19 | "date-fns": "^2.30.0", 20 | "i18next": "^19.6.3", 21 | "idb": "^6.0.0", 22 | "immer": "^8.0.1", 23 | "jose": "^3.11.3", 24 | "openid-client": "^4.7.2", 25 | "qs": "^6.11.2", 26 | "react": "^17.0.2", 27 | "react-beautiful-dnd": "^13.1.1", 28 | "react-datepicker": "^3.5.0", 29 | "react-dnd-html5-backend": "^14.0.1", 30 | "react-dom": "^17.0.2", 31 | "react-i18next": "^11.7.0", 32 | "react-redux": "^7.1.0", 33 | "react-router-dom": "^5.2.0", 34 | "react-scripts": "^4.0.3", 35 | "redux": "^4.2.1", 36 | "redux-thunk": "^2.4.2", 37 | "remove-markdown": "^0.5.0" 38 | }, 39 | "scripts": { 40 | "start": "react-scripts start", 41 | "build": "react-scripts build", 42 | "test": "react-scripts test", 43 | "lint": "eslint ./src/**/*.{js,ts,tsx} --quiet --fix", 44 | "eject": "react-scripts eject", 45 | "functions": "netlify dev" 46 | }, 47 | "eslintConfig": { 48 | "extends": [ 49 | "react-app", 50 | "react-app/jest" 51 | ], 52 | "rules": { 53 | "eqeqeq": "warn", 54 | "strict": "off" 55 | } 56 | }, 57 | "browserslist": [ 58 | ">0.2%", 59 | "not dead", 60 | "not op_mini all" 61 | ], 62 | "proxy": "http://localhost:8888", 63 | "devDependencies": { 64 | "@testing-library/jest-dom": "^5.11.9", 65 | "@testing-library/react": "^11.2.3", 66 | "@testing-library/user-event": "^12.6.1", 67 | "@types/jest": "^26.0.20", 68 | "@types/node": "^14.14.22", 69 | "@types/react": "^17.0.0", 70 | "@types/react-beautiful-dnd": "^13.1.4", 71 | "@types/react-datepicker": "^3.1.2", 72 | "@types/react-dom": "^17.0.0", 73 | "@types/react-redux": "^7.1.26", 74 | "@types/react-router-dom": "^5.3.3", 75 | "@types/react-sortable-tree": "^0.3.17", 76 | "@typescript-eslint/eslint-plugin": "^4.14.0", 77 | "@typescript-eslint/parser": "^4.14.0", 78 | "chokidar": "^3.5.3", 79 | "eslint": "^7.18.0", 80 | "eslint-config-prettier": "^7.2.0", 81 | "eslint-plugin-prettier": "^3.3.1", 82 | "eslint-plugin-react": "^7.33.2", 83 | "eslint-plugin-react-hooks": "^4.6.0", 84 | "prettier": "^2.2.1", 85 | "typescript": "^4.8.3", 86 | "sass": "^1.66.1" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | # Netlify redirects 2 | /* / 200 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helsenorge/structor/b426682a55b38e6c0d8d65f0046a472875e43e46/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Skjemabygger 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helsenorge/structor/b426682a55b38e6c0d8d65f0046a472875e43e46/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/helsenorge/structor/b426682a55b38e6c0d8d65f0046a472875e43e46/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Skjemabygger", 3 | "name": "Skjemabygger", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter as Router } from 'react-router-dom'; 3 | import Routes from '../src/router/index'; 4 | 5 | function App(): JSX.Element { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } 12 | 13 | export default App; 14 | -------------------------------------------------------------------------------- /src/components/Accordion/Accordion.css: -------------------------------------------------------------------------------- 1 | .accordion { 2 | background: transparent; 3 | color: #000; 4 | cursor: pointer; 5 | font-size: 20px; 6 | width: 100%; 7 | text-align: left; 8 | border: none; 9 | outline: none; 10 | transition: 0.4s; 11 | border-top: 8px solid #ebf8ff; 12 | } 13 | 14 | .accordion p { 15 | font-size: 16px; 16 | } 17 | 18 | .accordion:after { 19 | content: '+'; /* Unicode character for "plus" sign (+) */ 20 | float: right; 21 | margin-left: 5px; 22 | font-size: 24px; 23 | } 24 | 25 | .accordion.active:after { 26 | transform: rotate(45deg); 27 | } 28 | 29 | .active, 30 | .accordion:hover { 31 | background-color: #ccc; 32 | } 33 | 34 | .panel { 35 | padding: 0; 36 | background-color: white; 37 | transition: max-height 0.2s ease-in-out; 38 | white-space: pre-wrap; 39 | } 40 | 41 | .panel.active { 42 | display: block; 43 | } 44 | -------------------------------------------------------------------------------- /src/components/Accordion/Accordion.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, MouseEvent } from 'react'; 2 | import './Accordion.css'; 3 | 4 | type AccordionProps = { 5 | title: string; 6 | children: React.ReactNode; 7 | }; 8 | 9 | const Accordion = (props: AccordionProps): JSX.Element => { 10 | const [open, setOpen] = useState(false); 11 | 12 | const handleClick = (event: MouseEvent) => { 13 | event.preventDefault(); 14 | setOpen(!open); 15 | }; 16 | 17 | return ( 18 | <> 19 | 22 |
23 | {open &&
{props.children}
} 24 |
25 | 26 | ); 27 | }; 28 | 29 | export default Accordion; 30 | -------------------------------------------------------------------------------- /src/components/AdvancedQuestionOptions/AdvancedQuestionOptions.css: -------------------------------------------------------------------------------- 1 | .msg-error button { 2 | background: none; 3 | border: none; 4 | color: #4299e1; 5 | cursor: pointer; 6 | font-weight: 700; 7 | } -------------------------------------------------------------------------------- /src/components/AdvancedQuestionOptions/CalculatedExpression/CalculatedExpression.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useTranslation } from 'react-i18next'; 3 | import { Extension, QuestionnaireItem } from '../../../types/fhir'; 4 | import { IExtentionType } from '../../../types/IQuestionnareItemType'; 5 | import FormField from '../../FormField/FormField'; 6 | 7 | type CalculatedExpressionProps = { 8 | item: QuestionnaireItem; 9 | disabled?: boolean; 10 | updateExtension: (extension: Extension) => void; 11 | removeExtension: (extensionType: IExtentionType) => void; 12 | }; 13 | 14 | const CalculatedExpression = (props: CalculatedExpressionProps): JSX.Element => { 15 | const { t } = useTranslation(); 16 | const handleBlur = (event: React.FocusEvent) => { 17 | if (!event.target.value) { 18 | props.removeExtension(IExtentionType.calculatedExpression); 19 | } else { 20 | const ceExtension: Extension = { 21 | url: IExtentionType.calculatedExpression, 22 | valueString: event.target.value, 23 | }; 24 | props.updateExtension(ceExtension); 25 | } 26 | }; 27 | 28 | const calculatedExpression = 29 | props.item.extension?.find((ext) => ext.url === IExtentionType.calculatedExpression)?.valueString || ''; 30 | return ( 31 | 32 |