Skip to content

Commit

Permalink
SHIBUI-2539: Fixed bug with property values changing on deletion
Browse files Browse the repository at this point in the history
  • Loading branch information
rmathis committed Mar 2, 2023
1 parent f319c0a commit 96be454
Show file tree
Hide file tree
Showing 37 changed files with 400 additions and 292 deletions.
19 changes: 3 additions & 16 deletions ui/src/app/App.router.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ import { Fragment } from "react";
import { NewGroup } from "./admin/container/NewGroup";
import { EditGroup } from "./admin/container/EditGroup";

import { ConfigurationsProvider } from './admin/hoc/ConfigurationsProvider';
import { ConfigurationList } from './admin/container/ConfigurationList';
import { NewConfiguration } from './admin/container/NewConfiguration';
import { EditConfiguration } from './admin/container/EditConfiguration';
Expand Down Expand Up @@ -308,27 +307,15 @@ export const router = createBrowserRouter([
children: [
{
path: `list`,
element: <ConfigurationsProvider>
{(configurations, onDelete) =>
<ConfigurationList configurations={configurations} onDelete={onDelete} />
}
</ConfigurationsProvider>,
element: <ConfigurationList />,
},
{
path: `new`,
element:<ConfigurationsProvider>
{(configurations) =>
<NewConfiguration configurations={configurations} />
}
</ConfigurationsProvider>,
element: <NewConfiguration />,
},
{
path: `:id/edit`,
element: <ConfigurationsProvider>
{(configurations) =>
<EditConfiguration configurations={configurations} />
}
</ConfigurationsProvider>,
element: <EditConfiguration />,
},
{
index: true,
Expand Down
127 changes: 52 additions & 75 deletions ui/src/app/admin/component/ConfigurationForm.js
Original file line number Diff line number Diff line change
@@ -1,46 +1,48 @@
import React from 'react';
import Button from 'react-bootstrap/Button';
import { useFieldArray, useForm } from 'react-hook-form';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSpinner, faSave, faTrash } from '@fortawesome/free-solid-svg-icons';

import Translate from '../../i18n/components/translate';
import PropertySelector from './PropertySelector';

import { useProperties } from '../hoc/PropertiesProvider';

import Form from 'react-bootstrap/Form';
import FloatingLabel from 'react-bootstrap/FloatingLabel';
import { useTranslator } from '../../i18n/hooks';
import { includes, uniqBy } from 'lodash';
import { useGetPropertiesQuery } from '../../store/configurations/PropertyApi';
import { nameUpdated, propertyAdded, propertyRemoved, propertyUpdated, useConfiguration } from '../../store/configurations/ConfigurationSlice';
import { useDispatch } from 'react-redux';
import { PropertyInput } from './PropertyInput';

import { useValidator } from '../../core/hooks/useValidator';

export function ConfigurationForm({ configurations, configuration = {}, loading, onSave, onCancel }) {

const [names, setNames] = React.useState([]);

const { control, register, getValues, watch, formState, handleSubmit } = useForm({
defaultValues: {
...configuration
},
reValidateMode: 'onChange',
mode: 'onChange',
});
const names = React.useMemo(() => configurations.map(p => p.name), [configurations]);
const nameList = React.useMemo(() => names.filter(n => n !== configuration.name), [configuration, names]);
const [touched, setTouched] = React.useState(false);

const { name, ids, entities } = useConfiguration();

const { fields, append, remove } = useFieldArray({
control,
name: "properties",
rules: {
minLength: 1
const dispatch = useDispatch();
const translate = useTranslator();
const { errors } = useValidator({ name }, {
name: {
required: {
value: true,
message: translate(`message.name-required`),
},
custom: {
isValid: (value) => !includes(nameList, value),
message: translate(`message.must-be-unique`),
}
}
});

const { errors, isValid } = formState;

const properties = useProperties();
const { data: properties = [] } = useGetPropertiesQuery();

const addProperties = (props) => {

const selected = getValues('properties');
const selected = Object.values(entities);

const parsed = props.reduce((coll, prop, idx) => {
if (prop.isCategory) {
Expand All @@ -56,39 +58,38 @@ export function ConfigurationForm({ configurations, configuration = {}, loading,

const deduped = uniqBy(filtered, (i) => i.propertyName);

append(deduped);
deduped.forEach((p) => dispatch(propertyAdded(p)));
};

const saveConfig = (formValues) => {
const parsed = formValues.properties.map(p => ({
const saveConfig = ({ name, properties }) => {
const parsed = properties.map(p => ({
propertyName: p.propertyName,
propertyValue: p.propertyValue,
configFile: p.configFile,
category: p.category,
displayType: p.displayType
}));
onSave({
...formValues,
name,
properties: parsed
});
};

const translator = useTranslator();
const remove = React.useCallback((id) => dispatch(propertyRemoved(id)), [dispatch]);

React.useEffect(() => {
setNames(configurations.map(p => p.name));
}, [configurations]);

const onNext = (data) => {};
const updateValue = React.useCallback(({ id, propertyValue }) => {
dispatch(propertyUpdated({ id, changes: { propertyValue } }))
}, [dispatch]);

return (<>
<div className="container-fluid">
<div className="d-flex justify-content-end align-items-center">
<React.Fragment>
<Button variant="info" className="me-2"
type="button"
onClick={() => saveConfig(getValues())}
disabled={ !isValid || fields.length < 1 || loading}
onClick={() => saveConfig({ name, properties: Object.values(entities) })}
disabled={ Object.keys(errors).length > 0 || ids.length < 1 || loading}
aria-label="Save changes to the metadata source. You will return to the dashboard">
<FontAwesomeIcon icon={loading ? faSpinner : faSave} pulse={loading} />&nbsp;
<Translate value="action.save">Save</Translate>
Expand All @@ -103,36 +104,30 @@ export function ConfigurationForm({ configurations, configuration = {}, loading,
</React.Fragment>
</div>
<hr />
<Form onSubmit={handleSubmit(onNext)}>
<Form>
<div className="row">
<div className="col-12 col-lg-5">
<Form.Group className="mb-3" controlId="formName">
<Form.Label><Translate value="label.configuration-name">Name</Translate></Form.Label>
<Form.Control
type="text"
required
isInvalid={errors.name}
placeholder={translator('label.configuration-name-placeholder')}
{...register(`name`, {
required: true,
maxLength: 255,
value: configuration.value || null,
validate: {

unique: v => v.trim() === configuration.name || !includes(names, v)
}
})} />
<Form.Text className={errors.name ? 'text-danger' : 'text-muted'}>
{errors?.name?.type === 'unique' && <Translate value={`message.must-be-unique`} />}
{errors?.name?.type === 'required' && <Translate value={`message.name-required`} />}
</Form.Text>
maxLength={255}
onChange={ (ev) => dispatch(nameUpdated(ev.target.value)) }
value={name}
isInvalid={ touched && errors.name }
onFocus={() => setTouched(true)} />
{touched && errors?.name && <Form.Text className={errors.name ? 'text-danger' : 'text-muted'}>
{ errors?.name }
</Form.Text> }
</Form.Group>
</div>
</div>
<div className="row">
<div className="col-12 col-lg-6 order-2">
<div className="d-flex align-items-end">
<PropertySelector options={properties} properties={fields} onAddProperties={ addProperties } />
<PropertySelector options={properties} properties={Object.values(entities)} onAddProperties={ addProperties } />
</div>
</div>
</div>
Expand All @@ -151,41 +146,23 @@ export function ConfigurationForm({ configurations, configuration = {}, loading,
</tr>
</thead>
<tbody>
{fields.map((p, idx) => (
{ids.map((id, idx) => (
<tr key={idx}>
<td>{ p.propertyName }</td>
<td>{ p.category }</td>
<td>{ p.displayType === 'number' ? 'integer' : p.displayType }</td>
<td>{ entities[id].propertyName }</td>
<td>{ entities[id].category }</td>
<td>{ entities[id].displayType === 'number' ? 'integer' : entities[id].displayType }</td>
<td>
{p.displayType !== 'boolean' ?
<FloatingLabel
controlId={`valueInput-${p.propertyName}`}
label={translator('label.configuration-value')}>
<Form.Control
type={p.displayType === 'number' ? 'number' : 'text'}
placeholder="value"
isInvalid={ errors?.properties && errors.properties[idx] }
{...register(`properties.${idx}.propertyValue`, {
setValueAs: v => (p.displayType === 'number' ? parseInt(v) : v),
maxLength: 255,
})} />
</FloatingLabel>
:
<Form.Check type="switch"
label={ watch(`properties.${idx}.propertyValue`) === true ? translator('value.true') : translator('value.false') }
reverse={'true'} {...register(`properties.${idx}.propertyValue`)}
className="my-3" />
}
<PropertyInput property={entities[id]} onChange={ updateValue } />
</td>
<td className="text-end">
<Button variant="danger" onClick={() => remove(idx)}>
<Button variant="danger" onClick={() => remove(id)}>
<FontAwesomeIcon icon={faTrash} size="lg" />
&nbsp; <Translate value="action.remove">Remove</Translate>
</Button>
</td>
</tr>
))}
{fields.length === 0 &&
{ids.length === 0 &&
<tr>
<td colSpan="5">
<Translate value="message.properties-none">At least one property is required.</Translate>
Expand Down
57 changes: 57 additions & 0 deletions ui/src/app/admin/component/PropertyInput.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React, { Fragment } from 'react';
import FloatingLabel from 'react-bootstrap/FloatingLabel';
import Form from 'react-bootstrap/Form';
import { useValidator } from '../../core/hooks/useValidator';
import { useTranslator } from '../../i18n/hooks';

export function PropertyInput({ property, onChange }) {

const translate = useTranslator();

const value = React.useMemo(() => {
const { propertyValue, displayType } = property;
switch (displayType) {
case 'number':
return parseInt(propertyValue);
case 'boolean':
return propertyValue ? Boolean(propertyValue) : false;
default:
return propertyValue || '';
}
}, [property]);

const { errors } = useValidator({ value }, {
value: {
custom: {
isValid: (value) => !value || value?.length <= 255,
message: translate(`message.must-be-unique`),
}
}
});

return (
<Fragment>
{property.displayType !== 'boolean' ?
<FloatingLabel
controlId={`valueInput-${property.propertyName}`}
label={translate('label.configuration-value')}>
<Form.Control
type={property.displayType === 'number' ? 'number' : 'text'}
placeholder="value"
name={`properties.${property.resourceId}.propertyValue`}
value={ value }
onChange={ (evt) => onChange({ id: property.resourceId, propertyValue: evt.target.value }) }
maxLength={255}
isInvalid={ errors.value } />
</FloatingLabel>
:
<Form.Check type="switch"
label={ value?.toString() === 'true' ? translate('value.true') : translate('value.false') }
reverse={'true'}
checked={ value }
onChange={ (evt) => onChange({ id: property.resourceId, propertyValue: evt.target.checked }) }
className="my-3" />
}
</Fragment>
);
}
3 changes: 2 additions & 1 deletion ui/src/app/admin/component/PropertySelector.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export function PropertySelector ({ properties, options, onAddProperties }) {
}
const cat = {category: item, propertyName: item, isCategory: true};
const catSelected = selected.some(s => s.propertyName === item);
const sorted = orderBy(grouped[item], 'propertyName');
return (
<Fragment key={item}>
{index !== 0 && <Menu.Divider />}
Expand All @@ -32,7 +33,7 @@ export function PropertySelector ({ properties, options, onAddProperties }) {
{item} - Add all
</MenuItem>
</Menu.Header>
{grouped[item].map((i) => {
{sorted.map((i) => {
if (!properties.some((p) => p.propertyName === i.propertyName)) {
index = index + 1;
const item =
Expand Down
5 changes: 4 additions & 1 deletion ui/src/app/admin/container/ConfigurationList.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,18 @@ import { useTranslator } from '../../i18n/hooks';
import useFetch from 'use-http';
import API_BASE_PATH from '../../App.constant';
import { downloadAsZip } from '../../core/utility/download_as';
import { useGetConfigurationsQuery } from '../../store/configurations/ConfigurationApi';

export function ConfigurationList({ configurations, onDelete, loading }) {
export function ConfigurationList({ onDelete, loading }) {

const remove = (id) => {
onDelete(id);
}

const translate = useTranslator();

const { data: configurations } = useGetConfigurationsQuery();

const downloader = useFetch(`${API_BASE_PATH}/shib/property/set`, {
cachePolicy: 'no-cache',
headers: {
Expand Down
Loading

0 comments on commit 96be454

Please sign in to comment.