From c06c51362632593233ceffb48f874c690eb92393 Mon Sep 17 00:00:00 2001 From: AlasDiablo <25723276+AlasDiablo@users.noreply.github.com> Date: Tue, 2 Jul 2024 14:52:00 +0200 Subject: [PATCH 1/4] feat(format): add a poc for a routine builder --- .../fields/sourceValue/SourceValueRoutine.js | 156 +++++++++++++++--- 1 file changed, 130 insertions(+), 26 deletions(-) diff --git a/src/app/js/fields/sourceValue/SourceValueRoutine.js b/src/app/js/fields/sourceValue/SourceValueRoutine.js index 4dfa3b1f4..453e0c6d3 100644 --- a/src/app/js/fields/sourceValue/SourceValueRoutine.js +++ b/src/app/js/fields/sourceValue/SourceValueRoutine.js @@ -1,19 +1,60 @@ import React from 'react'; -import compose from 'recompose/compose'; +import { compose } from 'recompose'; import ListAltIcon from '@mui/icons-material/ListAlt'; import PropTypes from 'prop-types'; import RoutineCatalog from '../wizard/RoutineCatalog'; import translate from 'redux-polyglot/translate'; import { polyglot as polyglotPropTypes } from '../../propTypes'; import { Box, Button, TextField } from '@mui/material'; +import { fromFields } from '../../sharedSelectors'; +import { loadField } from '../index'; +import { connect } from 'react-redux'; +import { getFieldForSpecificScope } from '../../../../common/scope'; +import SearchAutocomplete from '../../admin/Search/SearchAutocomplete'; const SourceValueRoutine = ({ + fields, updateDefaultValueTransformers, value, p: polyglot, }) => { const [openRoutineCatalog, setOpenRoutineCatalog] = React.useState(false); const [valueInput, setValueInput] = React.useState(value || ''); + const [routine, setRoutine] = React.useState(''); + const [routineArgs, setRoutineArgs] = React.useState([]); + const [routineFields, setRoutineFields] = React.useState([]); + const [first, setFirst] = React.useState(true); + + const fieldsResource = React.useMemo( + () => getFieldForSpecificScope(fields, 'collection'), + [fields], + ); + + React.useEffect(() => { + if (typeof value === 'string') { + setRoutine(value.split('/').slice(0, 4).join('/')); + const args = value.split('/').slice(4); + setRoutineArgs(args); + setRoutineFields( + fieldsResource.filter((field) => { + return args.includes(field.name); + }), + ); + } + }, [value]); + + React.useEffect(() => { + if (!first) { + handleChange({ + target: { + value: [routine, ...routineArgs].join('/'), + }, + }); + } else { + setFirst(false); + } + }, [routine, routineArgs]); + const handleChange = (event) => { setValueInput(event.target.value); const transformers = [ @@ -32,40 +73,103 @@ const SourceValueRoutine = ({ updateDefaultValueTransformers(transformers); }; + const handleRoutineFieldsChange = (event, newValue) => { + setRoutineFields(newValue); + setRoutineArgs(newValue.map((field) => field.name)); + }; + return ( - - - - + + + + + + + + + setOpenRoutineCatalog(false)} + onChange={handleChange} + currentValue={value} + /> + + + + + + + + - setOpenRoutineCatalog(false)} - onChange={handleChange} - currentValue={value} - /> ); }; +const mapStateToProps = (state) => { + return { + // sort by label asc + fields: fromFields + .getFields(state) + .sort((a, b) => a.label.localeCompare(b.label)), + }; +}; + +const mapDispatchToProps = { + loadField, +}; + SourceValueRoutine.propTypes = { + fields: PropTypes.arrayOf(PropTypes.object).isRequired, p: polyglotPropTypes.isRequired, updateDefaultValueTransformers: PropTypes.func.isRequired, value: PropTypes.string, }; -export default compose(translate)(SourceValueRoutine); +export default compose( + translate, + connect(mapStateToProps, mapDispatchToProps), +)(SourceValueRoutine); From 609953fd368067b9a7fb97426c7758fba581b1cd Mon Sep 17 00:00:00 2001 From: AlasDiablo <25723276+AlasDiablo@users.noreply.github.com> Date: Wed, 3 Jul 2024 10:23:58 +0200 Subject: [PATCH 2/4] feat(format): add a routine selector, remove last field --- src/app/custom/translations.tsv | 1 + src/app/js/fields/FieldRepresentation.js | 2 +- .../fields/sourceValue/SourceValueRoutine.js | 67 +++---- .../wizard/RoutineCatalogAutocomplete.js | 175 ++++++++++++++++++ 4 files changed, 200 insertions(+), 45 deletions(-) create mode 100644 src/app/js/fields/wizard/RoutineCatalogAutocomplete.js diff --git a/src/app/custom/translations.tsv b/src/app/custom/translations.tsv index 4c3f8bfee..4c8983141 100644 --- a/src/app/custom/translations.tsv +++ b/src/app/custom/translations.tsv @@ -1098,3 +1098,4 @@ "ejs_variable_list" "Data from a routine is displayed using an HTML template based on EJS syntax. You can use these variables to access data and utils:" "L’affichage des données en provenance d’une routine est réalisé à l’aide d’un template HTML utilisant la syntaxe EJS. Vous pouvez utiliser ces variables pour accéder aux données et aux utilitaires :" "ejs_data" "Variable containing the routine data" "Variable contenant les données de la routine" "ejs_lodash" "Variable containing the Lodash function" "Variable contenant les fonctions de Lodash" +"routine_args" "Routine fields" "Champs de la routine" diff --git a/src/app/js/fields/FieldRepresentation.js b/src/app/js/fields/FieldRepresentation.js index 2a2973180..eb25f602e 100644 --- a/src/app/js/fields/FieldRepresentation.js +++ b/src/app/js/fields/FieldRepresentation.js @@ -67,7 +67,7 @@ function FieldRepresentation({ field, shortMode = false, p: polyglot }) { } FieldRepresentation.propTypes = { - field: PropTypes.isRequired, + field: PropTypes.object.isRequired, shortMode: PropTypes.bool, p: polyglotPropTypes.isRequired, }; diff --git a/src/app/js/fields/sourceValue/SourceValueRoutine.js b/src/app/js/fields/sourceValue/SourceValueRoutine.js index 453e0c6d3..eea0bcb3d 100644 --- a/src/app/js/fields/sourceValue/SourceValueRoutine.js +++ b/src/app/js/fields/sourceValue/SourceValueRoutine.js @@ -11,6 +11,7 @@ import { loadField } from '../index'; import { connect } from 'react-redux'; import { getFieldForSpecificScope } from '../../../../common/scope'; import SearchAutocomplete from '../../admin/Search/SearchAutocomplete'; +import RoutineCatalogAutocomplete from '../wizard/RoutineCatalogAutocomplete'; const SourceValueRoutine = ({ fields, @@ -19,7 +20,6 @@ const SourceValueRoutine = ({ p: polyglot, }) => { const [openRoutineCatalog, setOpenRoutineCatalog] = React.useState(false); - const [valueInput, setValueInput] = React.useState(value || ''); const [routine, setRoutine] = React.useState(''); const [routineArgs, setRoutineArgs] = React.useState([]); const [routineFields, setRoutineFields] = React.useState([]); @@ -45,32 +45,29 @@ const SourceValueRoutine = ({ React.useEffect(() => { if (!first) { - handleChange({ - target: { - value: [routine, ...routineArgs].join('/'), + const finalRoutine = [routine, ...routineArgs].join('/'); + const transformers = [ + { + operation: 'ROUTINE', + args: [ + { + name: 'value', + type: 'string', + value: finalRoutine, + }, + ], }, - }); + ]; + updateDefaultValueTransformers(transformers); } else { setFirst(false); } }, [routine, routineArgs]); - const handleChange = (event) => { - setValueInput(event.target.value); - const transformers = [ - { - operation: 'ROUTINE', - args: [ - { - name: 'value', - type: 'string', - value: event.target.value, - }, - ], - }, - ]; - - updateDefaultValueTransformers(transformers); + const handleRoutineChange = (event) => { + setRoutine(event.target.value); + setRoutineFields([]); + setRoutineArgs([]); }; const handleRoutineFieldsChange = (event, newValue) => { @@ -86,13 +83,10 @@ const SourceValueRoutine = ({ display="flex" alignItems="center" > - @@ -109,7 +103,7 @@ const SourceValueRoutine = ({ setOpenRoutineCatalog(false)} - onChange={handleChange} + onChange={handleRoutineChange} currentValue={value} /> @@ -122,7 +116,7 @@ const SourceValueRoutine = ({ > - - - - ); }; diff --git a/src/app/js/fields/wizard/RoutineCatalogAutocomplete.js b/src/app/js/fields/wizard/RoutineCatalogAutocomplete.js new file mode 100644 index 000000000..048f09508 --- /dev/null +++ b/src/app/js/fields/wizard/RoutineCatalogAutocomplete.js @@ -0,0 +1,175 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import PropTypes from 'prop-types'; +import translate from 'redux-polyglot/translate'; +import compose from 'recompose/compose'; +import { polyglot as polyglotPropTypes } from '../../propTypes'; + +import { Typography, Box, Link, Tooltip } from '@mui/material'; + +import TextField from '@mui/material/TextField'; +import Autocomplete from '@mui/material/Autocomplete'; +import { styled, lighten, darken } from '@mui/system'; + +import SettingsEthernetIcon from '@mui/icons-material/SettingsEthernet'; +import ThumbUpIcon from '@mui/icons-material/ThumbUp'; + +import routines from '../../../custom/routines/routines-catalog.json'; +import routinesPrecomputed from '../../../custom/routines/routines-precomputed-catalog.json'; + +const GroupHeader = styled('div')(({ theme }) => ({ + position: 'sticky', + top: '-8px', + padding: '4px 10px', + color: theme.palette.primary.main, + backgroundColor: + theme.palette.mode === 'light' + ? lighten(theme.palette.primary.light, 0.85) + : darken(theme.palette.primary.main, 0.8), +})); + +const GroupItems = styled('ul')({ + padding: 0, +}); + +const RoutineOption = ({ key, option, polyglot, ...props }) => { + return ( + + + + + {option.title} + + + + + + {polyglot.t(`${option.id}_description`)} + + + + + {option.recommendedWith && ( + + + + + {option.recommendedWith.toString()} + + + + )} + {option.doc && ( + + e.stopPropagation()} + > + + + + )} + + + + ); +}; + +RoutineOption.propTypes = { + key: PropTypes.string.isRequired, + option: PropTypes.shape({ + title: PropTypes.string.isRequired, + id: PropTypes.string.isRequired, + doc: PropTypes.string, + url: PropTypes.string, + recommendedWith: PropTypes.array, + }).isRequired, + polyglot: polyglotPropTypes.isRequired, +}; + +const RoutineCatalog = ({ + p: polyglot, + label, + onChange, + currentValue, + precomputed = false, +}) => { + const [value, setValue] = useState(null); + + /** + * @type {Array<{id: string, title: string, url: string, doc: string, recommendedWith: string[]}>} + */ + const catalog = useMemo(() => { + let routineCatalog = precomputed ? routinesPrecomputed : routines; + const formatedRoutineCatalog = routineCatalog.map((routine) => { + const title = polyglot.t(`${routine.id}_title`); + const firstLetter = title[0].toUpperCase(); + const formatedFirstLetter = /[0-9]/.test(firstLetter) + ? '0-9' + : firstLetter; + return { + ...routine, + title: polyglot.t(`${routine.id}_title`), + firstLetter: formatedFirstLetter, + }; + }); + return formatedRoutineCatalog.sort( + (a, b) => -b.firstLetter.localeCompare(a.firstLetter), + ); + }, [precomputed]); + + useEffect(() => { + setValue(catalog.find((routine) => routine.url.includes(currentValue))); + }, [currentValue]); + + const handleChange = (event, newValue) => { + setValue(newValue); + onChange({ + target: { + value: newValue.url, + }, + }); + }; + + return ( + option.firstLetter} + getOptionLabel={(option) => option.title} + renderOption={(props, option) => ( + + )} + renderInput={(params) => } + renderGroup={(params) => ( +
  • + {params.group} + {params.children} +
  • + )} + /> + ); +}; + +RoutineCatalog.propTypes = { + label: PropTypes.string.isRequired, + p: polyglotPropTypes.isRequired, + onChange: PropTypes.func.isRequired, + currentValue: PropTypes.string, + precomputed: PropTypes.bool, +}; + +export default compose(translate)(RoutineCatalog); From 0b7b6b4c5a3c619d57335fa7da4e8f48bcfc5405 Mon Sep 17 00:00:00 2001 From: AlasDiablo <25723276+AlasDiablo@users.noreply.github.com> Date: Wed, 3 Jul 2024 14:11:42 +0200 Subject: [PATCH 3/4] feat(format): add routine selector in precomputed data --- .../sourceValue/SourceValuePrecomputed.js | 13 +++++++----- .../fields/sourceValue/SourceValueRoutine.js | 4 ++-- .../wizard/RoutineCatalogAutocomplete.js | 21 +++++++++++++++---- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/app/js/fields/sourceValue/SourceValuePrecomputed.js b/src/app/js/fields/sourceValue/SourceValuePrecomputed.js index bf93b53ec..fcb95a18f 100644 --- a/src/app/js/fields/sourceValue/SourceValuePrecomputed.js +++ b/src/app/js/fields/sourceValue/SourceValuePrecomputed.js @@ -9,6 +9,7 @@ import { fromPrecomputed } from '../../admin/selectors'; import { polyglot as polyglotPropTypes } from '../../propTypes'; import { Autocomplete, Box, Button, TextField } from '@mui/material'; import { toast } from 'react-toastify'; +import RoutineCatalogAutocomplete from '../wizard/RoutineCatalogAutocomplete'; const SourceValuePrecomputed = ({ precomputedData, @@ -97,14 +98,15 @@ const SourceValuePrecomputed = ({ )} onChange={handleChangePrecomputed} /> + - + + setOpenRoutineCatalog(false)} diff --git a/src/app/js/fields/sourceValue/SourceValueRoutine.js b/src/app/js/fields/sourceValue/SourceValueRoutine.js index eea0bcb3d..cbb3f8fec 100644 --- a/src/app/js/fields/sourceValue/SourceValueRoutine.js +++ b/src/app/js/fields/sourceValue/SourceValueRoutine.js @@ -5,7 +5,7 @@ import PropTypes from 'prop-types'; import RoutineCatalog from '../wizard/RoutineCatalog'; import translate from 'redux-polyglot/translate'; import { polyglot as polyglotPropTypes } from '../../propTypes'; -import { Box, Button, TextField } from '@mui/material'; +import { Box, Button } from '@mui/material'; import { fromFields } from '../../sharedSelectors'; import { loadField } from '../index'; import { connect } from 'react-redux'; @@ -86,7 +86,7 @@ const SourceValueRoutine = ({ diff --git a/src/app/js/fields/wizard/RoutineCatalogAutocomplete.js b/src/app/js/fields/wizard/RoutineCatalogAutocomplete.js index 048f09508..6cf6dc8f5 100644 --- a/src/app/js/fields/wizard/RoutineCatalogAutocomplete.js +++ b/src/app/js/fields/wizard/RoutineCatalogAutocomplete.js @@ -63,7 +63,7 @@ const RoutineOption = ({ key, option, polyglot, ...props }) => { - {option.recommendedWith.toString()} + {option.recommendedWith.join(', ')} @@ -130,8 +130,15 @@ const RoutineCatalog = ({ }, [precomputed]); useEffect(() => { - setValue(catalog.find((routine) => routine.url.includes(currentValue))); - }, [currentValue]); + setValue( + catalog.find( + (routine) => + typeof currentValue === 'string' && + currentValue.startsWith('/') && + routine.url.includes(currentValue), + ), + ); + }, [currentValue, catalog]); const handleChange = (event, newValue) => { setValue(newValue); @@ -144,8 +151,14 @@ const RoutineCatalog = ({ return ( { + if (!option1 || !option2) { + return false; + } + return option1.id === option2.id && option1.url === option2.url; + }} fullWidth options={catalog} groupBy={(option) => option.firstLetter} From 5541ec57239c5bf3bcc12bd77926cae3b5e1f221 Mon Sep 17 00:00:00 2001 From: AlasDiablo <25723276+AlasDiablo@users.noreply.github.com> Date: Thu, 4 Jul 2024 07:21:00 +0200 Subject: [PATCH 4/4] fix(format): apply minor fix --- .../js/fields/wizard/RoutineCatalogAutocomplete.js | 12 +++++++----- src/app/js/propTypes.js | 1 + 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/app/js/fields/wizard/RoutineCatalogAutocomplete.js b/src/app/js/fields/wizard/RoutineCatalogAutocomplete.js index 6cf6dc8f5..c5dc87afa 100644 --- a/src/app/js/fields/wizard/RoutineCatalogAutocomplete.js +++ b/src/app/js/fields/wizard/RoutineCatalogAutocomplete.js @@ -124,9 +124,11 @@ const RoutineCatalog = ({ firstLetter: formatedFirstLetter, }; }); - return formatedRoutineCatalog.sort( - (a, b) => -b.firstLetter.localeCompare(a.firstLetter), - ); + const sorter = new Intl.Collator(polyglot.currentLocale, { + numeric: true, + ignorePunctuation: true, + }); + return formatedRoutineCatalog.sort(sorter.compare); }, [precomputed]); useEffect(() => { @@ -136,7 +138,7 @@ const RoutineCatalog = ({ typeof currentValue === 'string' && currentValue.startsWith('/') && routine.url.includes(currentValue), - ), + ) ?? null, ); }, [currentValue, catalog]); @@ -151,7 +153,7 @@ const RoutineCatalog = ({ return ( { if (!option1 || !option2) { diff --git a/src/app/js/propTypes.js b/src/app/js/propTypes.js index 2d0c4b489..4cde985ba 100644 --- a/src/app/js/propTypes.js +++ b/src/app/js/propTypes.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { PROPOSED, VALIDATED, REJECTED } from '../../common/propositionStatus'; export const polyglot = PropTypes.shape({ + currentLocale: PropTypes.string.isRequired, t: PropTypes.func.isRequired, });