From 470a8284ff3cb4fd4c0b953b0c29c72b93fa6073 Mon Sep 17 00:00:00 2001 From: Sylvain Date: Mon, 11 Apr 2022 13:19:07 +0200 Subject: [PATCH] (ui) sso data mapping - by type --- app/frontend/application.js.erb | 3 +- .../src/javascript/components/README.md | 16 ++++++ .../boolean-mapping-form.tsx | 28 +++++++++++ .../data-mapping-form.tsx | 45 ++++++++++++----- .../date-mapping-form.tsx | 49 +++++++++++++++++++ .../integer-mapping-form.tsx | 46 +++++++++++++++++ .../string-mapping-form.tsx | 45 +++++++++++++++++ .../type-mapping-modal.tsx | 34 ++++++++++--- .../src/javascript/components/form/README.md | 8 +++ .../javascript/components/form/form-input.tsx | 2 +- .../components/form/form-multi-select.tsx | 2 +- .../components/form/form-select.tsx | 2 +- .../models/authentication-provider.ts | 2 +- app/frontend/src/stylesheets/application.scss | 1 + .../array-mapping-form.scss | 23 +++++++++ .../data-mapping-form.scss | 21 -------- config/locales/app.shared.en.yml | 12 +++++ 17 files changed, 295 insertions(+), 44 deletions(-) create mode 100644 app/frontend/src/javascript/components/README.md create mode 100644 app/frontend/src/javascript/components/authentication-provider/boolean-mapping-form.tsx create mode 100644 app/frontend/src/javascript/components/authentication-provider/date-mapping-form.tsx create mode 100644 app/frontend/src/javascript/components/authentication-provider/integer-mapping-form.tsx create mode 100644 app/frontend/src/javascript/components/authentication-provider/string-mapping-form.tsx create mode 100644 app/frontend/src/javascript/components/form/README.md create mode 100644 app/frontend/src/stylesheets/modules/authentication-provider/array-mapping-form.scss diff --git a/app/frontend/application.js.erb b/app/frontend/application.js.erb index f5b628cb1..7e466adb1 100644 --- a/app/frontend/application.js.erb +++ b/app/frontend/application.js.erb @@ -85,7 +85,8 @@ require('src/javascript/plugins.js.erb'); function importAll (r) { r.keys().forEach(r); } -importAll(require.context('src/javascript/components/', true, /.*/)); +// we do not include markdown files (*.md) +importAll(require.context('src/javascript/components/', true, /^.+\.(?!md).+/)); importAll(require.context('src/javascript/controllers/', true, /.*/)); importAll(require.context('src/javascript/services/', true, /.*/)); importAll(require.context('src/javascript/directives/', true, /.*/)); diff --git a/app/frontend/src/javascript/components/README.md b/app/frontend/src/javascript/components/README.md new file mode 100644 index 000000000..3611c989c --- /dev/null +++ b/app/frontend/src/javascript/components/README.md @@ -0,0 +1,16 @@ +# components + +This directory is holding the components built with [React](https://reactjs.org/). + +During the migration phase, these components may be included in [the legacy angularJS app](../../templates) using [react2angular](https://github.com/coatue-oss/react2angular). + +These components must be written using the following conventions: +- The component name must be in CamelCase. +- The component must be exported as a named export (no `export default`). +- A component `FooBar` must have a `className="foo-bar"` attribute on its top-level element. +- The stylesheet associated with the component must be located in `app/frontend/src/stylesheets/modules/same-directory-structure/foo-bar.scss`. +- All methods in the component must be commented with a comment block. +- Other constants and variables must be commented with an inline block. +- Depending on if we want to use the `` wrapper or not, we can export the component directly or wrap it in a `` wrapper. +- When a component is used in angularJS, the wrapped is required. The component must be named like `const Foo` (no export if not used in React) and must have a `const FooWrapper` at the end of its file, which wraps the component in a ``. + diff --git a/app/frontend/src/javascript/components/authentication-provider/boolean-mapping-form.tsx b/app/frontend/src/javascript/components/authentication-provider/boolean-mapping-form.tsx new file mode 100644 index 000000000..deb8f03d9 --- /dev/null +++ b/app/frontend/src/javascript/components/authentication-provider/boolean-mapping-form.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { UseFormRegister } from 'react-hook-form'; +import { FieldValues } from 'react-hook-form/dist/types/fields'; +import { useTranslation } from 'react-i18next'; +import { FormInput } from '../form/form-input'; + +export interface BooleanMappingFormProps { + register: UseFormRegister, + fieldMappingId: number, +} + +export const BooleanMappingForm = ({ register, fieldMappingId }: BooleanMappingFormProps) => { + const { t } = useTranslation('shared'); + + return ( +
+

{t('app.shared.authentication.mappings')}

+ + +
+ ); +}; diff --git a/app/frontend/src/javascript/components/authentication-provider/data-mapping-form.tsx b/app/frontend/src/javascript/components/authentication-provider/data-mapping-form.tsx index 887630768..eb74942f0 100644 --- a/app/frontend/src/javascript/components/authentication-provider/data-mapping-form.tsx +++ b/app/frontend/src/javascript/components/authentication-provider/data-mapping-form.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react'; import { UseFormRegister, useFieldArray, ArrayPath, useWatch, Path } from 'react-hook-form'; import { FieldValues } from 'react-hook-form/dist/types/fields'; import AuthProviderAPI from '../../api/auth-provider'; -import { MappingFields } from '../../models/authentication-provider'; +import { MappingFields, mappingType } from '../../models/authentication-provider'; import { Control } from 'react-hook-form/dist/types/form'; import { FormSelect } from '../form/form-select'; import { FormInput } from '../form/form-input'; @@ -66,7 +66,7 @@ export const DataMappingForm = , index: number): string => { + const getDataType = (formData: Array, index: number): mappingType => { const model = getModel(formData, index); const field = getField(formData, index); if (model && field) { @@ -89,7 +89,7 @@ export const DataMappingForm = +

{t('app.shared.oauth2.define_the_fields_mapping')}

} @@ -97,23 +97,46 @@ export const DataMappingForm = {fields.map((item, index) => ( -
+
- - + +
- - - + + +
- } onClick={toggleTypeMappingModal} /> + } onClick={toggleTypeMappingModal} disabled={getField(output, index) === undefined} /> } onClick={() => remove(index)} className="delete-button" /> - +
))} diff --git a/app/frontend/src/javascript/components/authentication-provider/date-mapping-form.tsx b/app/frontend/src/javascript/components/authentication-provider/date-mapping-form.tsx new file mode 100644 index 000000000..81ca11b44 --- /dev/null +++ b/app/frontend/src/javascript/components/authentication-provider/date-mapping-form.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { FieldValues } from 'react-hook-form/dist/types/fields'; +import { useTranslation } from 'react-i18next'; +import { FormSelect } from '../form/form-select'; +import { Control } from 'react-hook-form/dist/types/form'; + +export interface DateMappingFormProps { + control: Control, + fieldMappingId: number, +} + +export const DateMappingForm = ({ control, fieldMappingId }: DateMappingFormProps) => { + const { t } = useTranslation('shared'); + + // available date formats + const dateFormats = [ + { + label: 'ISO 8601', + value: 'iso8601' + }, + { + label: 'RFC 2822', + value: 'rfc2822' + }, + { + label: 'RFC 3339', + value: 'rfc3339' + }, + { + label: 'Timestamp (s)', + value: 'timestamp-s' + }, + { + label: 'Timestamp (ms)', + value: 'timestamp-ms' + } + ]; + + return ( +
+

{t('app.shared.authentication.input_format')}

+ +
+ ); +}; diff --git a/app/frontend/src/javascript/components/authentication-provider/integer-mapping-form.tsx b/app/frontend/src/javascript/components/authentication-provider/integer-mapping-form.tsx new file mode 100644 index 000000000..5c01f2044 --- /dev/null +++ b/app/frontend/src/javascript/components/authentication-provider/integer-mapping-form.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { ArrayPath, useFieldArray, UseFormRegister } from 'react-hook-form'; +import { Control } from 'react-hook-form/dist/types/form'; +import { FieldValues } from 'react-hook-form/dist/types/fields'; +import { useTranslation } from 'react-i18next'; +import { FabButton } from '../base/fab-button'; +import { FormInput } from '../form/form-input'; + +export interface IntegerMappingFormProps { + register: UseFormRegister, + control: Control, + fieldMappingId: number, +} + +export const IntegerMappingForm = ({ register, control, fieldMappingId }: IntegerMappingFormProps) => { + const { t } = useTranslation('shared'); + + const { fields, append, remove } = useFieldArray({ control, name: 'auth_provider_mappings_attributes_transformation_mapping' as ArrayPath }); + + return ( +
+

{t('app.shared.authentication.mappings')}

+ } + onClick={() => append({})} /> + {fields.map((item, index) => ( +
+
+ + +
+
+ } onClick={() => remove(index)} className="delete-button" /> +
+
+ ))} +
+ ); +}; diff --git a/app/frontend/src/javascript/components/authentication-provider/string-mapping-form.tsx b/app/frontend/src/javascript/components/authentication-provider/string-mapping-form.tsx new file mode 100644 index 000000000..5002188cb --- /dev/null +++ b/app/frontend/src/javascript/components/authentication-provider/string-mapping-form.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { ArrayPath, useFieldArray, UseFormRegister } from 'react-hook-form'; +import { Control } from 'react-hook-form/dist/types/form'; +import { FieldValues } from 'react-hook-form/dist/types/fields'; +import { useTranslation } from 'react-i18next'; +import { FabButton } from '../base/fab-button'; +import { FormInput } from '../form/form-input'; + +export interface StringMappingFormProps { + register: UseFormRegister, + control: Control, + fieldMappingId: number, +} + +export const StringMappingForm = ({ register, control, fieldMappingId }: StringMappingFormProps) => { + const { t } = useTranslation('shared'); + + const { fields, append, remove } = useFieldArray({ control, name: 'auth_provider_mappings_attributes_transformation_mapping' as ArrayPath }); + + return ( +
+

{t('app.shared.authentication.mappings')}

+ } + onClick={() => append({})} /> + {fields.map((item, index) => ( +
+
+ + +
+
+ } onClick={() => remove(index)} className="delete-button" /> +
+
+ ))} +
+ ); +}; diff --git a/app/frontend/src/javascript/components/authentication-provider/type-mapping-modal.tsx b/app/frontend/src/javascript/components/authentication-provider/type-mapping-modal.tsx index 5dd87703a..c1713b843 100644 --- a/app/frontend/src/javascript/components/authentication-provider/type-mapping-modal.tsx +++ b/app/frontend/src/javascript/components/authentication-provider/type-mapping-modal.tsx @@ -1,20 +1,40 @@ import React from 'react'; import { FabModal } from '../base/fab-modal'; +import { useTranslation } from 'react-i18next'; +import { IntegerMappingForm } from './integer-mapping-form'; +import { UseFormRegister } from 'react-hook-form'; +import { Control } from 'react-hook-form/dist/types/form'; +import { FieldValues } from 'react-hook-form/dist/types/fields'; +import { mappingType } from '../../models/authentication-provider'; +import { BooleanMappingForm } from './boolean-mapping-form'; +import { DateMappingForm } from './date-mapping-form'; +import { StringMappingForm } from './string-mapping-form'; -interface TypeMappingModalProps { +interface TypeMappingModalProps { model: string, field: string, - type: string, + type: mappingType, isOpen: boolean, toggleModal: () => void, + register: UseFormRegister, + control: Control, + fieldMappingId: number, } -export const TypeMappingModal: React.FC = ({ model, field, type, isOpen, toggleModal }) => { +export const TypeMappingModal = ({ model, field, type, isOpen, toggleModal, register, control, fieldMappingId }:TypeMappingModalProps) => { + const { t } = useTranslation('shared'); + return ( - -

{model}

-

{field}

-

{type}

+ } + onConfirm={toggleModal}> + {model} > {field} ({t('app.shared.authentication.TYPE_expected', { TYPE: t(`app.shared.authentication.types.${type}`) })}) + {type === 'integer' && } + {type === 'boolean' && } + {type === 'date' && } + {type === 'string' && } ); }; diff --git a/app/frontend/src/javascript/components/form/README.md b/app/frontend/src/javascript/components/form/README.md new file mode 100644 index 000000000..831788909 --- /dev/null +++ b/app/frontend/src/javascript/components/form/README.md @@ -0,0 +1,8 @@ +# components/from + +This directory is holding the inputs components for usage within forms controlled by [React-hook-form](https://react-hook-form.com/). + +All these components must have [props](https://reactjs.org/docs/components-and-props.html) that inherits from [FormComponent](../models/form-component.ts) +or from [FormControlledComponent](../models/form-component.ts). + +Please look at the existing components for examples. diff --git a/app/frontend/src/javascript/components/form/form-input.tsx b/app/frontend/src/javascript/components/form/form-input.tsx index 71aeac8d6..d19923198 100644 --- a/app/frontend/src/javascript/components/form/form-input.tsx +++ b/app/frontend/src/javascript/components/form/form-input.tsx @@ -19,7 +19,7 @@ interface FormInputProps extends InputHTMLAttributes({ id, register, label, tooltip, defaultValue, icon, className, rules, readOnly, disabled, type, addOn, addOnClassName, placeholder, error, step }: FormInputProps) => { // Compose classnames from props const classNames = ` - form-item ${className || ''} + form-input form-item ${className || ''} ${type === 'hidden' ? 'is-hidden' : ''} ${error && error[id] ? 'is-incorrect' : ''} ${rules && rules.required ? 'is-required' : ''} diff --git a/app/frontend/src/javascript/components/form/form-multi-select.tsx b/app/frontend/src/javascript/components/form/form-multi-select.tsx index 427108be1..b94439ef9 100644 --- a/app/frontend/src/javascript/components/form/form-multi-select.tsx +++ b/app/frontend/src/javascript/components/form/form-multi-select.tsx @@ -29,7 +29,7 @@ type selectOption = { value: TOptionValue, label: string }; */ export const FormMultiSelect = ({ id, label, className, control, placeholder, options, valuesDefault, error, rules, disabled, onChange }: FormSelectProps) => { const classNames = ` - form-item ${className || ''} + form-multi-select form-item ${className || ''} ${error && error[id] ? 'is-incorrect' : ''} ${rules && rules.required ? 'is-required' : ''} ${disabled ? 'is-disabled' : ''}`; diff --git a/app/frontend/src/javascript/components/form/form-select.tsx b/app/frontend/src/javascript/components/form/form-select.tsx index d4ba82ff2..1d9865c50 100644 --- a/app/frontend/src/javascript/components/form/form-select.tsx +++ b/app/frontend/src/javascript/components/form/form-select.tsx @@ -28,7 +28,7 @@ type selectOption = { value: TOptionValue, label: string }; */ export const FormSelect = ({ id, label, className, control, placeholder, options, valueDefault, error, rules, disabled, onChange }: FormSelectProps) => { const classNames = ` - form-item ${className || ''} + form-select form-item ${className || ''} ${error && error[id] ? 'is-incorrect' : ''} ${rules && rules.required ? 'is-required' : ''} ${disabled ? 'is-disabled' : ''}`; diff --git a/app/frontend/src/javascript/models/authentication-provider.ts b/app/frontend/src/javascript/models/authentication-provider.ts index e085401a2..56625b32d 100644 --- a/app/frontend/src/javascript/models/authentication-provider.ts +++ b/app/frontend/src/javascript/models/authentication-provider.ts @@ -24,7 +24,7 @@ export interface AuthenticationProviderMapping { false_value: string, mapping: { from: string, - to: number + to: number|string } } } diff --git a/app/frontend/src/stylesheets/application.scss b/app/frontend/src/stylesheets/application.scss index 9c22ed779..ac98a5a0e 100644 --- a/app/frontend/src/stylesheets/application.scss +++ b/app/frontend/src/stylesheets/application.scss @@ -16,6 +16,7 @@ @import "app.plugins"; @import "modules/authentication-provider/data-mapping-form"; +@import "modules/authentication-provider/array-mapping-form"; @import "modules/base/fab-alert"; @import "modules/base/fab-button"; @import "modules/base/fab-input"; diff --git a/app/frontend/src/stylesheets/modules/authentication-provider/array-mapping-form.scss b/app/frontend/src/stylesheets/modules/authentication-provider/array-mapping-form.scss new file mode 100644 index 000000000..63106d495 --- /dev/null +++ b/app/frontend/src/stylesheets/modules/authentication-provider/array-mapping-form.scss @@ -0,0 +1,23 @@ +.array-mapping-form { + .mapping-item { + margin: 1rem; + border-left: 4px solid var(--gray-soft-dark); + border-radius: 4px; + padding-left: 1em; + display: flex; + flex-direction: row; + align-items: center; + + .inputs { + width: 85%; + } + + .actions { + padding: 1em; + .delete-button { + background-color: #cb1117; + color: white; + } + } + } +} diff --git a/app/frontend/src/stylesheets/modules/authentication-provider/data-mapping-form.scss b/app/frontend/src/stylesheets/modules/authentication-provider/data-mapping-form.scss index 3c430fcbc..2d2f766b2 100644 --- a/app/frontend/src/stylesheets/modules/authentication-provider/data-mapping-form.scss +++ b/app/frontend/src/stylesheets/modules/authentication-provider/data-mapping-form.scss @@ -1,25 +1,4 @@ .data-mapping-form { - .data-mapping-item { - margin: 1rem; - border-left: 4px solid var(--gray-soft-dark); - border-radius: 4px; - padding-left: 1em; - display: flex; - flex-direction: row; - align-items: center; - - .inputs { - width: 85%; - } - - .actions { - padding: 1em; - .delete-button { - background-color: #cb1117; - color: white; - } - } - } .local-data, .remote-data{ display: flex; diff --git a/config/locales/app.shared.en.yml b/config/locales/app.shared.en.yml index 7e773d8d3..b2d4759cc 100644 --- a/config/locales/app.shared.en.yml +++ b/config/locales/app.shared.en.yml @@ -257,8 +257,20 @@ en: authentication_type_is_required: "Authentication type is required." data_mapping: "Data mapping" expected_data_type: "Expected data type" + TYPE_expected: "{TYPE} expected" input_format: "Input format" mappings: "Mappings" + mapping_from: "From" + mapping_to: "To" + true_value: "True value" + false_value: "False value" + date_format: "Date format" + types: + integer: "integer" + string: "string" + text: "text" + date: "date" + boolean: "boolean" #edition/creation form of an OAuth2 authentication provider oauth2: common_url: "Server root URL"