- added login page;

- added file upload;
- added faq item edit modal;
This commit is contained in:
Vitalii Kiiko
2024-08-23 16:55:19 +02:00
parent 0a21444ee4
commit 5095ed7365
50 changed files with 1540 additions and 576 deletions

View File

@@ -9,7 +9,7 @@ const Datepicker = ({
label,
control,
errors,
defaultValue,
defaultValue = [],
config = {},
infoText = null,
minDate = null,
@@ -27,7 +27,7 @@ const Datepicker = ({
rules={config}
render={({ field, fieldState }) => (
<Calendar id={field.name}
value={field.value}
value={field.value ?? []}
onChange={(e) => field.onChange(e.value)}
dateFormat="dd/mm/yy"
mask="99/99/9999"

View File

@@ -11,13 +11,15 @@ const DatepickerRange = ({
label,
control,
errors,
defaultValue,
defaultValue = [],
config = {},
infoText = null,
minDate = null,
maxDate = null
}) => {
const datesDefaultValue = !isNil(defaultValue) && !isEmpty(defaultValue) && defaultValue.length ? defaultValue.map(v => new Date(v)) : [];
const datesDefaultValue = !isNil(defaultValue) && defaultValue.length
? defaultValue.map(v => new Date(v))
: [];
return (
<>
<label htmlFor={fieldName} className={classNames({ 'p-error': errors[fieldName] })}>
@@ -31,12 +33,8 @@ const DatepickerRange = ({
render={({ field, fieldState }) => (
<Calendar id={field.name}
value={field.value}
onChange={(e) => {
const formattedValues = e.value.map(d => d ? d.toISOString() : d);
console.log('formattedValues', formattedValues);
field.onChange(formattedValues)
}}
dateFormat="dd/mm/yy"
onChange={field.onChange}
mask="99/99/9999"
showIcon
minDate={minDate}
@@ -44,6 +42,7 @@ const DatepickerRange = ({
selectionMode="range"
readOnlyInput
hideOnRangeSelection
showButtonBar
className={classNames({ 'p-invalid': fieldState.invalid })}/>
)}/>
{infoText ? <small>{infoText}</small> : null}

View File

@@ -1,50 +1,133 @@
import React from 'react';
import React, { useEffect, useState, useRef } from 'react';
import { classNames } from 'primereact/utils';
import { Controller } from 'react-hook-form';
import { __ } from '@wordpress/i18n';
import FileUploadService from '../../../../service/file-upload-service';
import { FileUpload } from 'primereact/fileupload';
import { Tag } from 'primereact/tag';
import { Button } from 'primereact/button';
const Fileupload = ({
fieldName,
setDataFn,
label,
control,
errors,
defaultValue,
register,
defaultValue = [],
config = {},
infoText = null,
accept = 'image/*',
api = '/api/upload',
doctype = 'images',
emptyText = __('Trascina qui il tuo file', 'gepafin'),
chooseLabel = __('Aggiungi immagine', 'gepafin'),
multiple = false
multiple = false
}) => {
const [stateFieldData, setStateFieldData] = useState([]);
const inputRef = useRef();
const customBase64Uploader = (event) => {
const formData = new FormData()
for (const file of event.files) {
formData.append('file', file)
}
/*for (const pair of formData.entries()) {
console.log(pair[0], pair[1]);
}*/
FileUploadService.uploadFile(formData, callback, errorCallback, [['documentType', doctype.toUpperCase()]]);
};
const callback = (data) => {
console.log('data', data);
if (data.status === 'SUCCESS') {
setStateFieldData(data.data);
const files = inputRef.current.getFiles();
inputRef.current.setUploadedFiles(files);
inputRef.current.setFiles([]);
}
}
const errorCallback = (err) => {
console.log('err', err);
}
const itemTemplate = (file) => {
return (
<div className="appForm__fileUploadItem">
<span className="appForm__fileUploadItemName">
{file.name}
</span>
{file.id ? <Tag value={__('Caricato', 'gepafin')} severity="success"></Tag> : null}
{!file.id ? <Tag value={__('In attesa', 'gepafin')} severity="warning"></Tag> : null}
<Button icon="pi pi-times" severity="danger"
aria-label={__('Anulla', 'gepafin')}
onClick={() => onTemplateRemove(file)}/>
</div>
);
};
const onTemplateRemove = (file) => {
if (file.id) {
FileUploadService.deleteFile(
{},
(data) => dCallback(data, file.id),
dErrorCallback,
[['id', file.id]]
);
} else {
const files = inputRef.current.getFiles()
const newFiles = files.filter(o => o.lastModified !== file.lastModified && o.name !== file.name);
inputRef.current.setFiles(newFiles);
}
}
const dCallback = (data, id) => {
if (data.status === 'SUCCESS') {
setStateFieldData(prevState => {
const newFiles = prevState.filter(o => o.id !== id);
inputRef.current.setUploadedFiles(newFiles);
console.log('newFiles', newFiles);
return newFiles;
});
}
}
const dErrorCallback = (err) => {
console.log('err', err);
}
useEffect(() => {
setStateFieldData(defaultValue);
register(fieldName, config)
}, []);
useEffect(() => {
inputRef.current.setUploadedFiles(stateFieldData);
setDataFn(fieldName, [...stateFieldData], { shouldValidate: true });
}, [stateFieldData])
return (
<>
<label htmlFor={fieldName} className={classNames({ 'p-error': errors[fieldName] })}>
{label}{config.required ? '*' : null}
</label>
<Controller
<FileUpload
ref={inputRef}
id={fieldName}
name={fieldName}
control={control}
defaultValue={defaultValue}
rules={config}
render={({ field, fieldState }) => (
<FileUpload
id={field.name}
name={`${field.name}[]`}
url={api}
multiple={multiple}
accept={accept}
maxFileSize={1000000}
emptyTemplate={<p>{emptyText}</p>}
chooseLabel={chooseLabel}
cancelLabel={__('Cancella', 'gepafin')}
uploadLabel={__('Carica', 'gepafin')}
className={classNames({ 'p-invalid': fieldState.invalid })}/>
)}/>
url={'/document/uploadFile'}
multiple={multiple}
accept={accept}
maxFileSize={1000000}
emptyTemplate={<p>{emptyText}</p>}
chooseLabel={chooseLabel}
cancelLabel={__('Cancella', 'gepafin')}
uploadLabel={__('Carica', 'gepafin')}
className={classNames({ 'p-invalid': errors[fieldName] })}
itemTemplate={itemTemplate}
customUpload
uploadHandler={customBase64Uploader}/>
{infoText ? <small>{infoText}</small> : null}
{defaultValue ? <p>Uploaded:</p> : null}
{defaultValue}
</>)
}

View File

@@ -0,0 +1,49 @@
import React from 'react';
import { classNames } from 'primereact/utils';
import { Controller } from 'react-hook-form';
import { isNil, isEmpty } from 'ramda';
// components
import { InputSwitch } from 'primereact/inputswitch';
const Switch = ({
fieldName,
label,
control,
errors,
defaultValue,
config = {},
infoText = null,
onLabel = '',
offLabel = ''
}) => {
const properValue = !isNil(defaultValue) && !isEmpty(defaultValue) && defaultValue !== false;
const input = <Controller
name={fieldName}
control={control}
defaultValue={properValue}
rules={config}
render={({ field, fieldState }) => (
<InputSwitch
checked={field.value}
onChange={(e) => field.onChange(e.value)}
className={classNames({ 'p-invalid': fieldState.invalid })}/>
)}/>
return (
<>
<div className="appForm__row">
<label htmlFor={fieldName} className={classNames({ 'p-error': errors[fieldName] }, 'mr-8')}>
{label}{config.required ? '*' : null}
</label>
<div className="appForm__row">
{offLabel ? <span>{offLabel}</span> : null}
{input}
{onLabel ? <span>{onLabel}</span> : null}
</div>
</div>
{infoText ? <small>{infoText}</small> : null}
</>)
}
export default Switch;

View File

@@ -12,7 +12,9 @@ const TextInput = ({
config = {},
infoText = null,
inputgroup = false,
icon = null
icon = null,
placeholder = '',
inputtype = 'text'
}) => {
const input = <Controller
name={fieldName}
@@ -22,6 +24,8 @@ const TextInput = ({
render={({ field, fieldState }) => (
<InputText id={field.name}
{...field}
type={inputtype}
placeholder={placeholder}
className={classNames({ 'p-invalid': fieldState.invalid })}/>
)}/>
return (

View File

@@ -9,6 +9,7 @@ import Datepicker from './components/Datepicker';
import DatepickerRange from './components/DatepickerRange';
import Fileupload from './components/Fileupload';
import NumberInput from './components/NumberInput';
import Switch from './components/Switch';
const FormField = (props) => {
const fields = {
@@ -17,7 +18,8 @@ const FormField = (props) => {
datepicker: Datepicker,
datepickerrange: DatepickerRange,
fileupload: Fileupload,
numberinput: NumberInput
numberinput: NumberInput,
switch: Switch
}
const Comp = !isNil(fields[props.type]) ? fields[props.type] : null;

View File

@@ -1,6 +1,9 @@
import React, { useRef, useEffect, useState } from 'react';
import { classNames } from 'primereact/utils';
import { __ } from '@wordpress/i18n';
import { isEmpty } from 'ramda';
// components
import { InputText } from 'primereact/inputtext';
import { Button } from 'primereact/button';
import { Menu } from 'primereact/menu';
@@ -14,7 +17,8 @@ const FormFieldRepeater = ({
errors,
register,
label,
infoText
infoText,
config = {}
}) => {
const forMenu = useRef(null);
const [stateFieldData, setStateFieldData] = useState([]);
@@ -23,14 +27,14 @@ const FormFieldRepeater = ({
type: 'existing',
label: __('Esistente', 'gepafin'),
command: (data) => {
setStateFieldData([...stateFieldData, {id: null, value: '', status: data.item.type}]);
setStateFieldData([...stateFieldData, { id: null, value: '', status: data.item.type }]);
}
},
{
type: 'new',
label: __('Nuovo', 'gepafin'),
command: (data) => {
setStateFieldData([...stateFieldData, {id: null, value: '', status: data.item.type}]);
setStateFieldData([...stateFieldData, { id: null, value: '', status: data.item.type }]);
}
}
]
@@ -76,13 +80,13 @@ const FormFieldRepeater = ({
useEffect(() => {
const storeFieldData = data[fieldName] ?? [];
const newData = storeFieldData.map(o => ({...o, status: o.id ? 'existing' : 'new'}))
const newData = storeFieldData.map(o => ({ ...o, status: o.id ? 'existing' : 'new' }));
setStateFieldData(newData);
register(fieldName)
register(fieldName, config);
}, [])
useEffect(() => {
setDataFn(fieldName, [...stateFieldData]);
setDataFn(fieldName, [...stateFieldData], { shouldValidate: true });
}, [stateFieldData])
return (

View File

@@ -18,7 +18,8 @@ const FormFieldRepeaterCriteria = ({
errors,
register,
label,
infoText
infoText,
config = {}
}) => {
const forMenu = useRef(null);
const [stateFieldData, setStateFieldData] = useState([]);
@@ -72,6 +73,7 @@ const FormFieldRepeaterCriteria = ({
const onThresholdChange = (value) => {
setThreshold(value);
setDataFn('threshold', value, { shouldValidate: true });
}
const properField = (item, i) => {
@@ -92,16 +94,19 @@ const FormFieldRepeaterCriteria = ({
const newData = storeFieldData.map(o => ({ ...o, status: o.id ? 'existing' : 'new' }))
setStateFieldData(newData);
setThreshold(data['threshold'])
register(fieldName)
register(fieldName, config)
register('threshold', {
required: __('È obbligatorio', 'gepafin')
})
}, [])
useEffect(() => {
setDataFn(fieldName, [...stateFieldData]);
setDataFn(fieldName, [...stateFieldData], { shouldValidate: true });
}, [stateFieldData])
return (
<div className={classNames(['appForm__field', 'formfieldrepeater'])}>
<label htmlFor={fieldName} className={classNames({ 'p-error': errors[fieldName] })}>
<label htmlFor={fieldName} className={classNames({ 'p-error': errors[fieldName] || errors['threshold'] })}>
{label}
</label>
<div className="appForm__oneCol">

View File

@@ -1,11 +1,16 @@
import React, { useRef, useEffect, useState } from 'react';
import React, { useEffect, useState } from 'react';
import { classNames } from 'primereact/utils';
import { __ } from '@wordpress/i18n';
import { InputText } from 'primereact/inputtext';
import { isEmpty } from 'ramda';
// components
import { Button } from 'primereact/button';
import { Dropdown } from 'primereact/dropdown';
import { Accordion, AccordionTab } from 'primereact/accordion';
import { ToggleButton } from 'primereact/togglebutton';
import { Dialog } from 'primereact/dialog';
import { InputText } from 'primereact/inputtext';
import { InputTextarea } from 'primereact/inputtextarea';
const FormFieldRepeaterFaq = ({
data,
@@ -15,17 +20,24 @@ const FormFieldRepeaterFaq = ({
errors,
register,
label,
infoText
config = {},
setError,
clearErrors
}) => {
const [stateFieldData, setStateFieldData] = useState([]);
const [question, setQuestion] = useState('');
const [answer, setAnswer] = useState('');
const [editDataIndex, setEditDataIndex] = useState(null);
const [isVisibleEditDialog, setIsVisibleEditDialog] = useState(false);
const removeItem = (index) => {
const newData = stateFieldData.toSpliced(index, 1);
setStateFieldData(newData);
}
const selectItem = () => {
setStateFieldData([...stateFieldData, { id: 0, status: 'new', question: '', answer: '', visible: true }]);
const selectItem = (e) => {
const chosen = {...e.value};
setStateFieldData([...stateFieldData, chosen]);
}
const onInputChange = (e, index) => {
@@ -40,7 +52,8 @@ const FormFieldRepeaterFaq = ({
}
const addNewItem = () => {
const newItem = { id: 0, status: 'new', question: '', answer: '', visible: true };
setStateFieldData([...stateFieldData, newItem]);
}
const setChecked = (e, index) => {
@@ -56,7 +69,53 @@ const FormFieldRepeaterFaq = ({
const editItem = (e, index) => {
e.stopPropagation();
console.log('editItem')
setQuestion(stateFieldData[index].question);
setAnswer(stateFieldData[index].answer);
setEditDataIndex(index);
setIsVisibleEditDialog(true);
}
const hideEditDialog = () => {
setIsVisibleEditDialog(false);
setQuestion('');
setAnswer('');
setEditDataIndex(null);
}
const onChangeEditItem = (value, key) => {
if (key === 'question') {
setQuestion(value);
} else {
setAnswer(value)
}
}
const saveEditDialog = () => {
const newData = stateFieldData.map((o, i) => {
if (i === editDataIndex) {
o.question = question;
o.answer = answer;
return o
} else {
return o;
}
});
setStateFieldData(newData);
setIsVisibleEditDialog(false);
setQuestion('');
setAnswer('');
setEditDataIndex(null);
}
const headerEditDialog = () => {
return <span>{__('Aggiungi/modifica FAQ', 'gepafin')}</span>
}
const footerEditDialog = () => {
return <div>
<Button type="button" label={__('Anulla', 'gepafin')} onClick={hideEditDialog} outlined />
<Button type="button" label={__('Salva', 'gepafin')} onClick={saveEditDialog} />
</div>
}
const usedExistingValues = stateFieldData
@@ -67,11 +126,11 @@ const FormFieldRepeaterFaq = ({
const storeFieldData = data[fieldName] ?? [];
const newData = storeFieldData.map(o => ({ ...o, status: o.id ? 'existing' : 'new' }))
setStateFieldData(newData);
register(fieldName)
register(fieldName, config)
}, [])
useEffect(() => {
setDataFn(fieldName, [...stateFieldData]);
setDataFn(fieldName, [...stateFieldData], { shouldValidate: true });
}, [stateFieldData])
return (
@@ -102,7 +161,8 @@ const FormFieldRepeaterFaq = ({
</div>
<div className="appForm__faqTabItem">
<Button icon="pi pi-pencil" severity="success"
aria-label="Edit" onClick={(e) => editItem(e, i)}/>
aria-label="Edit"
onClick={(e) => editItem(e, i)}/>
<Button icon="pi pi-times" severity="danger"
aria-label="Cancel"
onClick={() => removeItem(i)}/>
@@ -115,6 +175,21 @@ const FormFieldRepeaterFaq = ({
</p>
</AccordionTab>)}
</Accordion>
<Dialog
visible={isVisibleEditDialog}
modal header={headerEditDialog}
footer={footerEditDialog}
style={{ maxWidth: '50rem' }}
onHide={hideEditDialog}>
<div className="appForm__field">
<label>{__('Titolo FAQ', 'gepafin')}</label>
<InputText value={question} onChange={(e) => onChangeEditItem(e.target.value, 'question')}/>
</div>
<div className="appForm__field">
<label>{__('Risposta', 'gepafin')}</label>
<InputTextarea value={answer} onChange={(e) => onChangeEditItem(e.target.value, 'answer')} rows={5} cols={30} />
</div>
</Dialog>
</div>
)
}

View File

@@ -1,14 +1,17 @@
import React from 'react';
import {
//Navigate,
Outlet } from 'react-router-dom';
import { Navigate, Outlet } from 'react-router-dom';
// store
import { useStore } from '../../store';
// tools
//import AuthenticationService from '../../service/authentication-service';
import AuthenticationService from '../../service/authentication-service';
const ProtectedRoute = () => {
// we need this to track existance of the token
const token = useStore().main.token();
/*if (!AuthenticationService.wasLoggedIn()) {
if (!AuthenticationService.wasLoggedIn()) {
return (<Navigate to={'/login'} replace/>);
}
@@ -18,10 +21,10 @@ const ProtectedRoute = () => {
if (!AuthenticationService.isLoggedIn()) {
return (<Navigate to={'/login?redirectReason=auth_required'} replace/>);
}*/
}
/*if (window.location.pathname === '/') {
return (<Navigate to={'/dashboard'} replace/>);
return (<Navigate to={'/'} replace/>);
}*/
return <Outlet/>;

View File

@@ -1,13 +1,14 @@
import React, { useRef } from 'react';
import { __ } from '@wordpress/i18n';
// store
import { storeSet } from '../../store';
// components
import { Menu } from 'primereact/menu';
import { Avatar } from 'primereact/avatar';
import { Toast } from 'primereact/toast';
const TopBarProfileMenu = ({ menuLeftRef }) => {
const toast = useRef();
let items = [
{
@@ -36,13 +37,12 @@ const TopBarProfileMenu = ({ menuLeftRef }) => {
label: __('Logout', 'gepafin'),
icon: 'pi pi-sign-out',
command: () => {
console.log('logout')
storeSet.main.doLogout();
}
}
];
return <>
<Toast ref={toast}/>
<Menu model={items} popup ref={menuLeftRef} id="topBar_profileMenu" className="topBar__menuProfile"/>
</>
}

View File

@@ -0,0 +1,34 @@
import { useEffect, useState, useCallback } from 'react';
import { __ } from '@wordpress/i18n';
import equal from 'fast-deep-equal';
// tools
const UnsavedChangesDetector = ({ initialData, getValuesFn }) => {
const [initial] = useState(initialData);
const warnIfUnsavedChanges = useCallback((event) => {
const updatedData = getValuesFn()
const isEqual = equal(initial, updatedData);
if (!isEqual) {
event.returnValue = __('You have unsaved changes. If you proceed, they will be lost.', 'gepafin');
}
return event.returnValue;
}, [initial])
useEffect(() => {
window.addEventListener('beforeunload', (e) => {
warnIfUnsavedChanges(e);
});
return () => {
window.removeEventListener('beforeunload', (e) => {
warnIfUnsavedChanges(e);
});
}
}, []);
}
export default UnsavedChangesDetector;