From 07cecda529ff4a9457133318e32ff661782c66ed Mon Sep 17 00:00:00 2001 From: Vitalii Kiiko Date: Tue, 21 Jan 2025 11:08:37 +0100 Subject: [PATCH] - saving progress; --- package.json | 3 + src/assets/scss/components/appForm.scss | 2 +- src/assets/scss/components/formBuilder.scss | 21 ++ src/assets/scss/components/misc.scss | 4 + .../components/DefaultCell/index.js | 25 ++ .../components/LastRowCell/index.js | 21 ++ .../components/NumericFormulaCell/index.js | 34 ++ .../CriteriaTable/RenderTable/index.js | 86 +++++ .../components/CriteriaTable/index.js | 132 +++++++ .../FormField/components/NumberInput/index.js | 3 + src/components/FormField/index.js | 2 + src/helpers/getTokens.js | 11 + src/helpers/renderWithDataVars.js | 23 ++ src/pages/BandoApplication/index.js | 70 +++- src/pages/BandoFlowEdit/index.js | 13 +- .../components/BuilderElement/index.js | 2 +- .../components/ElementSetting/index.js | 25 +- .../components/ElementSettingChips/index.js | 5 +- .../index.js | 326 ++++++++++++++++++ .../ElementSettingTableColumns/index.js | 18 +- .../BuilderElementSettings/index.js | 53 ++- src/pages/BandoFormsPreview/index.js | 72 +++- src/tempData.js | 26 +- 23 files changed, 915 insertions(+), 62 deletions(-) create mode 100644 src/components/FormField/components/CriteriaTable/RenderTable/components/DefaultCell/index.js create mode 100644 src/components/FormField/components/CriteriaTable/RenderTable/components/LastRowCell/index.js create mode 100644 src/components/FormField/components/CriteriaTable/RenderTable/components/NumericFormulaCell/index.js create mode 100644 src/components/FormField/components/CriteriaTable/RenderTable/index.js create mode 100644 src/components/FormField/components/CriteriaTable/index.js create mode 100644 src/helpers/getTokens.js create mode 100644 src/helpers/renderWithDataVars.js create mode 100644 src/pages/BandoFormsEdit/components/BuilderElementSettings/components/ElementSettingCriteriaTableColumns/index.js diff --git a/package.json b/package.json index ccd2660..90df6a6 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "copy-to-clipboard": "3.3.3", "deep-object-diff": "1.1.9", "dompurify": "3.2.2", + "expression-language": "^1.2.0", "fast-deep-equal": "3.1.3", "hotkeys-js": "3.13.7", "html-react-parser": "5.1.18", @@ -25,6 +26,8 @@ "klona": "2.0.6", "leader-line-new": "1.1.9", "luxon": "3.5.0", + "mathjs": "^14.0.1", + "mustache": "^4.2.0", "object-path-immutable": "4.1.2", "primeicons": "7.0.0", "primereact": "10.8.5", diff --git a/src/assets/scss/components/appForm.scss b/src/assets/scss/components/appForm.scss index bb532bd..405d28e 100644 --- a/src/assets/scss/components/appForm.scss +++ b/src/assets/scss/components/appForm.scss @@ -57,7 +57,7 @@ background-color: #e3e3e3; } - &.table { + &.table, &.criteria_table { div.addNewTableRow { width: 100%; text-align: center; diff --git a/src/assets/scss/components/formBuilder.scss b/src/assets/scss/components/formBuilder.scss index 9382f37..178cd4a 100644 --- a/src/assets/scss/components/formBuilder.scss +++ b/src/assets/scss/components/formBuilder.scss @@ -181,6 +181,27 @@ gap: 0.5rem; } +.formElementSettings__fieldDescription { + padding: 15px; + background-color: #ffe0c5; + border: 1px solid #e6a973; + + p { + margin: 0; + color: #c68e5e; + font-size: 15px; + line-height: 1.5; + + code { + font-size: 14px; + padding: 3px 6px; + border: 1px solid #e1b48b; + background-color: #fbeadb; + border-radius: 3px; + } + } +} + .formElementSettings__tabs { width: 100%; diff --git a/src/assets/scss/components/misc.scss b/src/assets/scss/components/misc.scss index 9f59541..309e5ff 100644 --- a/src/assets/scss/components/misc.scss +++ b/src/assets/scss/components/misc.scss @@ -163,6 +163,10 @@ } } +.p-inputnumber-input[readonly] { + background-color: #e1e1e1; +} + .p-inputgroup.flex-1 { align-items: center; } diff --git a/src/components/FormField/components/CriteriaTable/RenderTable/components/DefaultCell/index.js b/src/components/FormField/components/CriteriaTable/RenderTable/components/DefaultCell/index.js new file mode 100644 index 0000000..6351f92 --- /dev/null +++ b/src/components/FormField/components/CriteriaTable/RenderTable/components/DefaultCell/index.js @@ -0,0 +1,25 @@ +const DefaultCell = ({ getValue, row: { index }, column: { id }, table }) => { + const initialValue = getValue(); + const disabled = table.options.meta?.disabled; + + const onBlur = (e) => { + table.options.meta?.updateData(index, id, e.target.value); + }; + + const onFocus = (e) => { + e.target.select(); + } + + return ( + table.options.meta?.updateData(index, id, e.target.value)} + onBlur={onBlur} + onFocus={onFocus} + className="w-full px-2 py-1 border rounded" + /> + ); +}; + +export default DefaultCell; \ No newline at end of file diff --git a/src/components/FormField/components/CriteriaTable/RenderTable/components/LastRowCell/index.js b/src/components/FormField/components/CriteriaTable/RenderTable/components/LastRowCell/index.js new file mode 100644 index 0000000..3a35e77 --- /dev/null +++ b/src/components/FormField/components/CriteriaTable/RenderTable/components/LastRowCell/index.js @@ -0,0 +1,21 @@ +import { head, isEmpty, isNil, sum } from 'ramda'; + +const LastRowCell = ({columnId, lastRows, columnMeta = {}, getColumnDataFn}) => { + const cellData = head(lastRows.filter(o => !isNil(o[columnId]))); + let cellValue = cellData[columnId]; + + if (columnMeta.enableFormula) { + const getAllRowsValues = getColumnDataFn(columnId) + .map(v => isEmpty(v) || isNil(v) ? 0 : v); + + if (cellValue === 'sum') { + cellValue = sum(getAllRowsValues); + } else { + cellValue = 0; + } + } + + return {cellValue}; +}; + +export default LastRowCell; \ No newline at end of file diff --git a/src/components/FormField/components/CriteriaTable/RenderTable/components/NumericFormulaCell/index.js b/src/components/FormField/components/CriteriaTable/RenderTable/components/NumericFormulaCell/index.js new file mode 100644 index 0000000..58a6f25 --- /dev/null +++ b/src/components/FormField/components/CriteriaTable/RenderTable/components/NumericFormulaCell/index.js @@ -0,0 +1,34 @@ +const NumericFormulaCell = ({ getValue, row: { index }, column: { id }, table }) => { + const initialValue = getValue(); + const disabled = table.options.meta?.disabled; + + const onBlur = (e) => { + const numValue = e.target.value === 0 ? null : Number(e.target.value); + table.options.meta?.updateData(index, id, numValue); + }; + + const onFocus = (e) => { + e.target.select(); + } + + const onChange = (e) => { + if (e.target.value === 0 || !isNaN(e.target.value)) { + table.options.meta?.updateData(index, id, e.target.value); + } + }; + + return ( + + ); +}; + +export default NumericFormulaCell; \ No newline at end of file diff --git a/src/components/FormField/components/CriteriaTable/RenderTable/index.js b/src/components/FormField/components/CriteriaTable/RenderTable/index.js new file mode 100644 index 0000000..604a453 --- /dev/null +++ b/src/components/FormField/components/CriteriaTable/RenderTable/index.js @@ -0,0 +1,86 @@ +import React from 'react'; +import { flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table'; +import { wrap } from 'object-path-immutable'; +import { isEmpty } from 'ramda'; + +// components +import DefaultCell from './components/DefaultCell'; +import LastRowCell from './components/LastRowCell'; + +const RenderTable = ({ data, columns, lastRow, setRowsFn, disabled }) => { + const table = useReactTable({ + data, + columns, + defaultColumn: { + cell: DefaultCell + }, + getCoreRowModel: getCoreRowModel(), + meta: { + disabled, + updateData: (rowIndex, columnId, value) => { + const newRowsData = wrap(data).set([rowIndex, columnId], value).value(); + setRowsFn(newRowsData); + }, + } + }); + + const getColumnData = (columnId) => { + const rows = table.getRowModel().rows; + return rows.map(row => row.getValue(columnId)); + }; + + return ( + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + ); + })} + + ))} + + + {table.getRowModel().rows.map((row) => { + return ( + + {row.getVisibleCells().map((cell) => { + return ( + + ); + })} + + ); + })} + {!isEmpty(lastRow) + ? + {columns.map((o) => )} + + : null} + +
+ {header.isPlaceholder ? null : ( + <> + {flexRender( + header.column.columnDef.header, + header.getContext() + )} + + )} +
+ {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} +
+ ) +} + +export default RenderTable \ No newline at end of file diff --git a/src/components/FormField/components/CriteriaTable/index.js b/src/components/FormField/components/CriteriaTable/index.js new file mode 100644 index 0000000..e1d65f2 --- /dev/null +++ b/src/components/FormField/components/CriteriaTable/index.js @@ -0,0 +1,132 @@ +import React, { useEffect, useState, useCallback } from 'react'; +import { classNames } from 'primereact/utils'; +import { pathOr, isEmpty } from 'ramda'; +import { wrap } from 'object-path-immutable'; +import equal from 'fast-deep-equal'; +import { klona } from 'klona'; + +// tools +import { nonEmptyTables } from '../../../../helpers/validators'; + +//components +import RenderTable from './RenderTable'; +import NumericFormulaCell from './RenderTable/components/NumericFormulaCell'; + +const Table = ({ + fieldName, + setDataFn, + label, + register, + errors, + disabled = false, + config = {}, + defaultValue = [], + tableColumns = [] + }) => { + const [columnsCfg, setColumnsCfg] = useState([]); + const [rowsCfg, setRowsCfg] = useState([]); + const [columns, setColumns] = useState([]); + const [lastRow, setLastRow] = useState([]); + const [rows, setRows] = useState(null); + + const updateRows = useCallback((data) => { + setRows(data); + setDataFn(fieldName, data, { shouldValidate: true }); + }, [rows, defaultValue]); + + const properConfig = (config) => { + let newConfig = klona(config); + if (config.validate && config.validate.nonEmptyTables) { + newConfig = wrap(newConfig) + .set(['validate', 'nonEmptyTables'], (v) => nonEmptyTables(v, tableColumns)) + .value(); + } + + return newConfig; + } + + useEffect(() => { + let newColumns = columnsCfg.map((o) => { + const item = { + accessorKey: o.name, + header: () => o.label, + footer: (props) => props.column.id, + meta: { + predefined: o.predefined, + enableFormula: o.enableFormula, + fieldtype: o.fieldtype + } + } + + if (o.predefined) { + item.cell = (info) => info.getValue(); + } else if (o.enableFormula || o.fieldtype === 'numeric') { + item.cell = NumericFormulaCell; + } + + return item; + }); + + setColumns(newColumns); + }, [columnsCfg, disabled]); + + useEffect(() => { + setRows(rowsCfg); + }, [rowsCfg]); + + useEffect(() => { + const stateFieldData = pathOr([], ['stateFieldData'], tableColumns); + const obj = stateFieldData + .reduce((acc, cur) => { + acc[cur.name] = '' + return acc; + }, {}); + let rowsData = pathOr([obj], ['rowsData'], tableColumns); + rowsData = isEmpty(rowsData) ? [obj] : rowsData; + setColumnsCfg(stateFieldData); + setRowsCfg(rowsData); + + let lastRowData = stateFieldData.reduce((acc, cur) => { + const value = cur.enableFormula ? cur.lastRowFormula : (cur.lastRowText ? cur.lastRowText : ''); + acc.push({ [cur.name]: value }); + return acc; + }, []); + + const lastRowValues = lastRowData + .map(o => { + const values = Object.values(o); + return values[0]; + }) + .filter(v => !isEmpty(v)); + + setLastRow(!isEmpty(lastRowValues) ? lastRowData : []); + }, [tableColumns]); + + useEffect(() => { + if (!equal(rows, defaultValue)) { + setRows(defaultValue); + } + }, [defaultValue]); + + useEffect(() => { + setRows(defaultValue); + register(fieldName, properConfig(config)); + }, []); + + console.log('rows', rows, lastRow, tableColumns) + return ( + <> + + {rows ? : null} + ) +} + +export default Table; \ No newline at end of file diff --git a/src/components/FormField/components/NumberInput/index.js b/src/components/FormField/components/NumberInput/index.js index 19fc28c..0fec4ce 100644 --- a/src/components/FormField/components/NumberInput/index.js +++ b/src/components/FormField/components/NumberInput/index.js @@ -21,6 +21,7 @@ const NumberInput = ({ min, max, disabled = false, + readOnly = false, useGrouping = true }) => { const minAttr = config.min ? config.min : min; @@ -33,11 +34,13 @@ const NumberInput = ({ render={({ field, fieldState }) => ( field.onChange(e.value)} min={minAttr} max={maxAttr} locale={locale} + showButtons useGrouping={useGrouping} maxFractionDigits={!isNaN(parseInt(maxFractionDigits)) ? parseInt(maxFractionDigits) : 0} minFractionDigits={!isNaN(parseInt(minFractionDigits)) ? parseInt(minFractionDigits) : 0} diff --git a/src/components/FormField/index.js b/src/components/FormField/index.js index c1559a7..6a60658 100644 --- a/src/components/FormField/index.js +++ b/src/components/FormField/index.js @@ -17,6 +17,7 @@ import Checkboxes from './components/Checkboxes'; import Fileupload from './components/Fileupload'; import Table from './components/Table'; import PasswordField from './components/PasswordField'; +import CriteriaTable from './components/CriteriaTable'; const FormField = (props) => { const fields = { @@ -33,6 +34,7 @@ const FormField = (props) => { wysiwyg: Wysiwyg, checkboxes: Checkboxes, table: Table, + criteria_table: CriteriaTable, password: PasswordField } const Comp = !isNil(fields[props.type]) ? fields[props.type] : null; diff --git a/src/helpers/getTokens.js b/src/helpers/getTokens.js new file mode 100644 index 0000000..81d40c3 --- /dev/null +++ b/src/helpers/getTokens.js @@ -0,0 +1,11 @@ +import { tokenize } from 'expression-language'; + +const getTokens = expr => tokenize(expr).tokens + .filter((o, i, arr) => o.type === 'name' + && (typeof arr[i+1] === 'undefined' + || (typeof arr[i+1] !== 'undefined' && arr[i+1].value !== '(')) + ) + .map(o => o.value) + .filter((v, i, arr) => arr.indexOf(v) === i); + +export default getTokens; \ No newline at end of file diff --git a/src/helpers/renderWithDataVars.js b/src/helpers/renderWithDataVars.js new file mode 100644 index 0000000..ffeb4dc --- /dev/null +++ b/src/helpers/renderWithDataVars.js @@ -0,0 +1,23 @@ +import mustache from 'mustache'; +import { replace } from 'ramda'; + +/** + * Use mustachejs to parse content and replace variables with their values + * Each variable (everything in brackets) is expression for EE + * + * @param {string} content + * @param {object} context + * + * @returns {string} + */ +const renderWithDataVars = (content = '', context = {}) => { + try { + let newContent = replace(/{/g, '{{&', content); + newContent = replace(/}/g, '}}', newContent); + return mustache.render(newContent, context); + } catch { + return content; + } +}; + +export default renderWithDataVars; diff --git a/src/pages/BandoApplication/index.js b/src/pages/BandoApplication/index.js index 0e5846d..4204e44 100644 --- a/src/pages/BandoApplication/index.js +++ b/src/pages/BandoApplication/index.js @@ -1,10 +1,13 @@ import React, { useState, useEffect, useRef, useMemo } from 'react'; import { __, sprintf } from '@wordpress/i18n'; import { useParams } from 'react-router-dom'; -import { head, is, pluck, isEmpty, pathOr } from 'ramda'; +import { head, is, pluck, isEmpty, pathOr, isNil } from 'ramda'; import { useForm } from 'react-hook-form'; import 'quill/dist/quill.core.css'; import { wrap } from 'object-path-immutable'; +import { evaluate } from 'mathjs'; +import equal from 'fast-deep-equal'; +import { klona } from 'klona'; // store import { storeSet, storeGet, useStore } from '../../store'; @@ -26,6 +29,10 @@ import { import renderHtmlContent from '../../helpers/renderHtmlContent'; import set404FromErrorResponse from '../../helpers/set404FromErrorResponse'; import getFormatedFileSizeText from '../../helpers/getFormatedFileSizeText'; +import renderWithDataVars from '../../helpers/renderWithDataVars'; +import getTokens from '../../helpers/getTokens'; +import formatDateString from '../../helpers/formatDateString'; +import isDateTimeInPast from '../../helpers/isDateTimeInPast'; // components import { Skeleton } from 'primereact/skeleton'; @@ -39,8 +46,6 @@ import { Dialog } from 'primereact/dialog'; import FileuploadApplicationSignedPdf from '../../components/FileuploadApplicationSignedPdf'; import { defaultMaxFileSize } from '../../configData'; -import formatDateString from '../../helpers/formatDateString'; -import isDateTimeInPast from '../../helpers/isDateTimeInPast'; const BandoApplication = () => { const chosenCompanyId = useStore().main.chosenCompanyId(); @@ -48,6 +53,8 @@ const BandoApplication = () => { const [isExpired, setIsExpired] = useState(false); const [formData, setFormData] = useState([]); const [formInitialData, setFormInitialData] = useState(null); + const [fieldsWithVars, setFieldsWithVars] = useState({}); + const [fieldsWithFormula, setFieldsWithFormula] = useState({}); const [bandoTitle, setBandoTitle] = useState(''); const [bandoId, setBandoId] = useState(0); const [formId, setFormId] = useState(''); @@ -67,6 +74,7 @@ const BandoApplication = () => { trigger, register, getValues, + watch, reset } = useForm({ defaultValues: useMemo(() => { @@ -89,6 +97,7 @@ const BandoApplication = () => { } const activeStepIndex = activeStep - 1; const values = getValues(); + const formValues = watch(); const onValidate = () => { const applId = getApplicationId(); @@ -342,6 +351,8 @@ const BandoApplication = () => { dynamicData = wrap(dynamicData).set(['custom', 'userFullName'], userFullName).value(); const legalRepresentantName = company.isLegalRepresentant ? userFullName : ''; dynamicData = wrap(dynamicData).set(['custom', 'legalRepresentant'], legalRepresentantName).value(); + let allvars = {}; + let allformulas = {}; if (data.data.applicationFormResponse.content) { // eslint-disable-next-line array-callback-return @@ -349,7 +360,15 @@ const BandoApplication = () => { if (o.dynamicData && !isEmpty(o.dynamicData)) { formDataInitial[o.id] = pathOr('', o.dynamicData.split('.'), dynamicData); } - }) + const variable = head(o.settings.filter(o => o.name === 'variable')); + if (variable && !isEmpty(variable.value)) { + allvars[o.id] = variable.value[0]; + } + const formula = head(o.settings.filter(o => o.name === 'formula')); + if (formula && !isEmpty(formula.value)) { + allformulas[o.id] = formula.value; + } + }); } if (data.data.applicationFormResponse.formFields) { @@ -366,6 +385,8 @@ const BandoApplication = () => { } reset(); + setFieldsWithVars(allvars); + setFieldsWithFormula(allformulas); setFormInitialData(formDataInitial); } storeSet.main.unsetAsyncRequest(); @@ -501,10 +522,45 @@ const BandoApplication = () => { // TODO hardcoded for now const signedDocMime = bandoId === 10 ? ['.p7m,application/pkcs7-mime,application/x-pkcs7-mime', '.pdf,application/pdf'] - : ['.p7m,application/pkcs7-mime,application/x-pkcs7-mime'] + : ['.p7m,application/pkcs7-mime,application/x-pkcs7-mime']; const signedDocValidationString = bandoId === 10 ? ['.p7m', '.pdf'] - : ['.p7m'] + : ['.p7m']; + + useEffect(() => { + if (!isEmpty(fieldsWithVars) && !isEmpty(fieldsWithFormula)) { + const updatedFormValues = klona(formValues); + let context = {}; + + // eslint-disable-next-line array-callback-return + Object.keys(updatedFormValues).map(fieldId => { + if (!isNil(fieldsWithFormula[fieldId])) { + const formula = fieldsWithFormula[fieldId]; + context = getTokens(formula) + .filter(v => !['false', 'null', 'true'].includes(v)) + .reduce((acc, cur) => { + acc[cur] = isNil(context[cur]) ? 0 : context[cur]; + return acc; + }, {}); + const mathFormula = renderWithDataVars(formula, context); + try { + updatedFormValues[fieldId] = evaluate(mathFormula); + } catch (e) { + console.log('Error in math formula: "', mathFormula, '"', e.message); + updatedFormValues[fieldId] = 0; + } + } + if (!isNil(fieldsWithVars[fieldId])) { + context[fieldsWithVars[fieldId]] = updatedFormValues[fieldId] + } + }); + + if (!isEmpty(updatedFormValues) && !equal(updatedFormValues, formValues)) { + reset(updatedFormValues); + } + + } + }, [formValues]); useEffect(() => { if ('SUBMIT' === applicationStatus) { @@ -595,6 +651,7 @@ const BandoApplication = () => { const tableColumns = head(o.settings.filter(o => o.name === 'table_columns')); const step = head(o.settings.filter(o => o.name === 'step')); const mime = head(o.settings.filter(o => o.name === 'mime')); + const formula = head(o.settings.filter(o => o.name === 'formula')); let mimeValue = ''; if (mime) { @@ -630,6 +687,7 @@ const BandoApplication = () => { : { const [forms, setForms] = useState([]); const [formOptions, setFormOptions] = useState([]); const [chosenMainFieldOptions, setChosenMainFieldOptions] = useState([]); - //const [chosenMainField, setChosenMainField] = useState(''); const [mainFieldSuboptions, setMainFieldSubOptions] = useState([]); const [bandoStatus, setBandoStatus] = useState(''); const [isFlowAllowed, setIsFlowAllowed] = useState(true); @@ -162,18 +161,18 @@ const BandoFlowEdit = () => { const shoudDisableSaving = useCallback(() => { const nonEmptyFlowItems = flowStructure.flowData.filter(o => isEmpty(o.chosenField)).filter(o => !isEmpty(o.chosenValue)); - /*if (flowForms.length > 2) { - console.log('disable BTN:', nonEmptyFlowItems.length !== flowForms.length - 2, isEmpty(flowEdges), 'PUBLISH' === bandoStatus, - isEmpty(initialForm), isEmpty(finalForm)); + /*if (forms.length > 2) { + console.log('disable BTN:', nonEmptyFlowItems.length !== forms.length - 2, isEmpty(flowStructure.flowEdges), 'PUBLISH' === bandoStatus, + isEmpty(flowStructure.initialForm), isEmpty(flowStructure.finalForm)); } else { - console.log('disable BTN:', nonEmptyFlowItems.length !== 1, isEmpty(flowEdges), 'PUBLISH' === bandoStatus, - isEmpty(initialForm), isEmpty(finalForm)); + console.log('disable BTN (2 forms):', isEmpty(flowStructure.flowEdges), 'PUBLISH' === bandoStatus, + isEmpty(flowStructure.initialForm), isEmpty(flowStructure.finalForm)); }*/ return forms.length > 2 ? nonEmptyFlowItems.length !== forms.length - 2 || isEmpty(flowStructure.flowEdges) || 'PUBLISH' === bandoStatus || isEmpty(flowStructure.initialForm) || isEmpty(flowStructure.finalForm) - : nonEmptyFlowItems.length !== 1 || isEmpty(flowStructure.flowEdges) || 'PUBLISH' === bandoStatus + : isEmpty(flowStructure.flowEdges) || 'PUBLISH' === bandoStatus || isEmpty(flowStructure.initialForm) || isEmpty(flowStructure.finalForm); }, [flowStructure, forms]); diff --git a/src/pages/BandoFormsEdit/components/BuilderElement/index.js b/src/pages/BandoFormsEdit/components/BuilderElement/index.js index 9f5e0d0..71c0324 100644 --- a/src/pages/BandoFormsEdit/components/BuilderElement/index.js +++ b/src/pages/BandoFormsEdit/components/BuilderElement/index.js @@ -124,7 +124,7 @@ const BuilderElement = ({ id, name, label, index, bandoStatus }) => {
- {name === 'numberinput' + {['numberinput', 'criteria_table'].includes(name) ? : null} {name === 'numberinput' ? : null} diff --git a/src/pages/BandoFormsEdit/components/BuilderElementSettings/components/ElementSetting/index.js b/src/pages/BandoFormsEdit/components/BuilderElementSettings/components/ElementSetting/index.js index 8a5d0c8..cb50706 100644 --- a/src/pages/BandoFormsEdit/components/BuilderElementSettings/components/ElementSetting/index.js +++ b/src/pages/BandoFormsEdit/components/BuilderElementSettings/components/ElementSetting/index.js @@ -2,19 +2,23 @@ import React from 'react'; import { __ } from '@wordpress/i18n'; import { is } from 'ramda'; +// tools +import renderHtmlContent from '../../../../../../helpers/renderHtmlContent'; + // components import ElementSettingRepeater from '../ElementSettingRepeater'; import { InputText } from 'primereact/inputtext'; import { MultiSelect } from 'primereact/multiselect'; import { Editor } from 'primereact/editor'; - -import { mimeTypes } from '../../../../../../configData'; import ElementSettingTableColumns from '../ElementSettingTableColumns'; import { InputSwitch } from 'primereact/inputswitch'; import ElementSettingChips from '../ElementSettingChips'; +import ElementSettingCriteriaTableColumns from '../ElementSettingCriteriaTableColumns'; + +import { mimeTypes } from '../../../../../../configData'; + const ElementSetting = ({ setting, changeFn, updateDataFn, bandoStatus }) => { - const settingLabels = { label: __('Label', 'gepafin'), placeholder: __('Segnaposto', 'gepafin'), @@ -25,10 +29,15 @@ const ElementSetting = ({ setting, changeFn, updateDataFn, bandoStatus }) => { mime: __('Tipo di file', 'gepafin'), text: __('Testo formattato', 'gepafin'), table_columns: '', + criteria_table_columns: '', variable: __('Variable (only letters and "_")', 'gepafin'), formula: __('Auto calculation formula', 'gepafin') } + const settingDescription = { + formula: __('Create formula using previously declared variables. Use these math operators: +, -, *, /. Example of formula: {entrate}+{assicurazione}.', 'gepafin') + } + const renderHeader = () => { return ( @@ -84,6 +93,12 @@ const ElementSetting = ({ setting, changeFn, updateDataFn, bandoStatus }) => { name={setting.name} bandoStatus={bandoStatus} setDataFn={updateDataFn}/> + } else if (setting.name === 'criteria_table_columns') { + return } else if (['isRequestedAmount', 'isDelegation'].includes(setting.name)) { return { return
{getProperField(setting)} + {settingDescription[setting.name] + ?
+

{renderHtmlContent(settingDescription[setting.name])}

+
: null}
} diff --git a/src/pages/BandoFormsEdit/components/BuilderElementSettings/components/ElementSettingChips/index.js b/src/pages/BandoFormsEdit/components/BuilderElementSettings/components/ElementSettingChips/index.js index 9b44a7e..846dcc5 100644 --- a/src/pages/BandoFormsEdit/components/BuilderElementSettings/components/ElementSettingChips/index.js +++ b/src/pages/BandoFormsEdit/components/BuilderElementSettings/components/ElementSettingChips/index.js @@ -2,8 +2,9 @@ import React, { useState } from 'react'; // components import { Chips } from 'primereact/chips'; +import { isEmpty } from 'ramda'; -const ElementSettingChips = ({ restrictedValues = [], changeFn, value }) => { +const ElementSettingChips = ({ restrictedValues = [], changeFn, value = [] }) => { const [lastTyped, setLastTyped] = useState([]) const isValidValue = (newVal) => { @@ -12,7 +13,7 @@ const ElementSettingChips = ({ restrictedValues = [], changeFn, value }) => { }; const handleAdd = (e) => { - const newValue = e.value[e.value.length - 1]; + const newValue = isEmpty(e.value) ? '' : e.value[e.value.length - 1]; if (restrictedValues.includes(newValue)) { changeFn([]); diff --git a/src/pages/BandoFormsEdit/components/BuilderElementSettings/components/ElementSettingCriteriaTableColumns/index.js b/src/pages/BandoFormsEdit/components/BuilderElementSettings/components/ElementSettingCriteriaTableColumns/index.js new file mode 100644 index 0000000..0053d76 --- /dev/null +++ b/src/pages/BandoFormsEdit/components/BuilderElementSettings/components/ElementSettingCriteriaTableColumns/index.js @@ -0,0 +1,326 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { __, sprintf } from '@wordpress/i18n'; +import { wrap } from 'object-path-immutable'; +import { isEmpty, pathOr } from 'ramda'; + +// components +import { InputText } from 'primereact/inputtext'; +import { Button } from 'primereact/button'; +import { InputSwitch } from 'primereact/inputswitch'; + +// tools +import uniqid from '../../../../../../helpers/uniqid'; +import { Dropdown } from 'primereact/dropdown'; +import { Accordion, AccordionTab } from 'primereact/accordion'; + +const ElementSettingCriteriaTableColumns = ({ + value, + name, + setDataFn, + bandoStatus + }) => { + const [stateFieldData, setStateFieldData] = useState([]); + const [rowsData, setRowsData] = useState([]); + + const removeItem = (index) => { + let newData = stateFieldData + .toSpliced(index, 1) + newData = newData.map((o, i) => { + return i === newData.length - 1 + ? {...o, fieldtype: 'numeric', predefined: false, enableFormula: true} + : {...o, fieldtype: 'text', predefined: true, enableFormula: false} + }); + setStateFieldData(newData); + } + + const addNewItem = () => { + setStateFieldData([ + ...stateFieldData.map(o => ({...o, fieldtype: 'text', predefined: true, enableFormula: false})), + { name: uniqid('o'), label: '', fieldtype: 'numeric', predefined: false, enableFormula: true } + ]); + } + + const addNewRow = () => { + const obj = stateFieldData + .filter(o => o.predefined) + .reduce((acc, cur) => { + acc[cur.name] = '' + return acc; + }, {}); + setRowsData([...rowsData, obj]); + } + + const removeRow = (index) => { + const newRowsData = wrap(rowsData).del([index]).value(); + setRowsData(newRowsData); + } + + const onInputChange = (e, index) => { + const { value } = e.target; + const newData = stateFieldData.map((o, i) => { + if (i === index) { + o.label = value; + } + return o; + }) + setStateFieldData(newData); + } + + const onLastRowInputChange = (e, index) => { + const { value } = e.target; + const newData = stateFieldData.map((o, i) => { + if (i === index) { + o.lastRowText = value; + } + return o; + }) + setStateFieldData(newData); + } + + const onTypeChange = (value, index) => { + const newData = stateFieldData.map((o, i) => { + if (i === index) { + o.fieldtype = value; + } + return o; + }) + setStateFieldData(newData); + } + + const onLastRowFormulaChange = (value, index) => { + const newData = stateFieldData.map((o, i) => { + if (i === index) { + o.lastRowFormula = value; + } + return o; + }) + setStateFieldData(newData); + } + + const onSubInputChange = (e, name, index) => { + const { value } = e.target; + const newRowsData = wrap(rowsData).set([index, name], value).value(); + setRowsData(newRowsData); + } + + const setChecked = (value, index) => { + let name = ''; + const newData = stateFieldData.map((o, i) => { + if (i === index) { + o.predefined = value; + name = o.name; + } + return o; + }); + + let newRowsData = []; + + if (value === false) { + newRowsData = rowsData.map(o => wrap(o).set([name], '').value()); + } else { + newRowsData = rowsData.map(o => wrap(o).set([name], '').value()); + } + + setRowsData(newRowsData); + setStateFieldData(newData); + } + + const setColFormulaChecked = (index) => { + const newData = stateFieldData.map((o, i) => { + if (i === index) { + const newVal = o.enableFormula !== true; + o.enableFormula = newVal; + if (newVal) { + o.lastRowFormula = 'sum'; + o.fieldtype = 'numeric'; + delete o.lastRowText; + } else { + delete o.lastRowFormula; + delete o.fieldtype; + o.lastRowText = ''; + } + } + return o; + }); + setStateFieldData(newData); + } + + const handleClearLastRowData = useCallback(() => { + const newData = stateFieldData.map((o) => { + delete o.lastRowFormula; + o.lastRowText = ''; + delete o.enableFormula; + + return o; + }); + + setStateFieldData(newData); + }, [stateFieldData]); + + const properFields = (item, i) => { + return <> +
+ onInputChange(e, i)}/> +
+
+ onTypeChange(e.value, i)} + options={[ + { value: 'text', label: __('Testo', 'gepafin') }, + { value: 'numeric', label: __('Numerico', 'gepafin') } + ]}/> +
+
+ +
+
+ setChecked(e.value, i)}/> +
+
+
+ + } + + const properSubField = (item, i, name) => { + return <> +
+ onSubInputChange(e, name, i)}/> +
+
+
+ + } + + const properFieldsLastRow = useCallback((item, i) => { + return <> +
+ {sprintf(__('Colonna %d'), i + 1)} +
+ {item.enableFormula + ?
+ onLastRowFormulaChange(e.value, i)} + options={[ + { value: 'sum', label: __('Somma automatica', 'gepafin') } + ]}/> +
+ :
+ onLastRowInputChange(e, i)}/> +
} + + }, [stateFieldData]); + + const lastRow =
+
+
+ {__('Definisci ultima righa', 'gepafin')} +
+
+ {stateFieldData.map((o, i) =>
+ {properFieldsLastRow(o, i)} +
)} +
; + + useEffect(() => { + const stateFieldData = pathOr([], ['stateFieldData'], value); + setStateFieldData(stateFieldData); + const rowsData = pathOr([], ['rowsData'], value); + setRowsData(rowsData); + }, []); + + useEffect(() => { + setDataFn(name, { + stateFieldData, + rowsData + }); + }, [stateFieldData, rowsData]); + + return ( + <> +
+ {stateFieldData.length > 0 + ?
+
{__('Colonne', 'gepafin')}
+
{__('Tipo', 'gepafin')}
+
{__('Calcola', 'gepafin')}
+
{__('Predefinito?', 'gepafin')}
+
+
: null} + {stateFieldData.map((o, i) =>
+ {properFields(o, i)} +
)} +
+ {stateFieldData + .filter(o => o.predefined).length > 0 + ?
+ + {stateFieldData + //.filter(o => o.predefined) + .map((o, i) => + o.predefined + ? +
+ {rowsData.map((c, k) => { + return
+ {properSubField(c, k, o.name)} +
+ })} +
+
: null)} +
+
+ : null} + {lastRow} + + ) +} + +export default ElementSettingCriteriaTableColumns; \ No newline at end of file diff --git a/src/pages/BandoFormsEdit/components/BuilderElementSettings/components/ElementSettingTableColumns/index.js b/src/pages/BandoFormsEdit/components/BuilderElementSettings/components/ElementSettingTableColumns/index.js index 1806e6d..30eb834 100644 --- a/src/pages/BandoFormsEdit/components/BuilderElementSettings/components/ElementSettingTableColumns/index.js +++ b/src/pages/BandoFormsEdit/components/BuilderElementSettings/components/ElementSettingTableColumns/index.js @@ -158,7 +158,7 @@ const ElementSettingTableColumns = ({
onTypeChange(e.value, i)} options={[ @@ -168,6 +168,7 @@ const ElementSettingTableColumns = ({
@@ -212,6 +214,7 @@ const ElementSettingTableColumns = ({ {item.enableFormula ?
onLastRowFormulaChange(e.value, i)} options={[ @@ -232,6 +235,7 @@ const ElementSettingTableColumns = ({ {__('Definisci ultima righa', 'gepafin')}
{stateFieldData .filter(o => o.predefined).length > 0 diff --git a/src/pages/BandoFormsEdit/components/BuilderElementSettings/index.js b/src/pages/BandoFormsEdit/components/BuilderElementSettings/index.js index 0c1eed9..e95efd6 100644 --- a/src/pages/BandoFormsEdit/components/BuilderElementSettings/index.js +++ b/src/pages/BandoFormsEdit/components/BuilderElementSettings/index.js @@ -110,22 +110,6 @@ const BuilderElementSettings = ({ closeSettingsFn, bandoStatus }) => { return dynamicDataOptions[type] ?? []; } - /*const searchDynamicTags = (event) => { - const type = activeElementData.name; - const available = dynamicDataOptions[type]; - let filtered; - - if (!event.query.trim().length) { - filtered = [...available]; - } else { - filtered = available.filter((tag) => { - return tag.label.toLowerCase().startsWith(event.query.toLowerCase()); - }); - } - - setFilteredDynamicDataOptions(filtered); - }*/ - useEffect(() => { const chosen = head(elements.filter(o => o.id === activeElement)); const elementItems = storeGet.main.elementItems(); @@ -163,11 +147,11 @@ const BuilderElementSettings = ({ closeSettingsFn, bandoStatus }) => { ? settings .filter(o => !['variable', 'formula'].includes(o.name)) .map((o) => ) + key={o.name} + setting={o} + bandoStatus={bandoStatus} + changeFn={onChange} + updateDataFn={onUpdateOptions}/>) : null} {!isNil(dynamicDataOptions[activeElementData.name]) ?
@@ -250,18 +234,21 @@ const BuilderElementSettings = ({ closeSettingsFn, bandoStatus }) => { placeholder={__('Scegli', 'gepafin')}/>
- - {settings - ? settings - .filter(o => ['variable', 'formula'].includes(o.name)) - .map((o) => ) - : null} - + {settings + && settings + .filter(o => ['variable', 'formula'].includes(o.name)).length > 0 + ? + {settings + ? settings + .filter(o => ['variable', 'formula'].includes(o.name)) + .map((o) => ) + : null} + : null}