Skip to content

Commit

Permalink
[SDESK-7029] fix(ui): Show validation for CustomVocabulary fields (#1845
Browse files Browse the repository at this point in the history
)

* [SDESK-7029] fix(ui): Show validation for CustomVocabulary fields

* improve(style): Use sass variable for input invalid background

* Remove gettext from cv.display_name

* Check schema.vocabularies.length before filtering by it
  • Loading branch information
MarkLark86 authored Aug 24, 2023
1 parent 7eb2fbc commit 2508e44
Show file tree
Hide file tree
Showing 10 changed files with 118 additions and 88 deletions.
2 changes: 2 additions & 0 deletions client/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {editor} from './editor';
import {contentProfiles} from './contentProfiles';
import {locks} from './locks';
import {assignments} from './assignments';
import {vocabularies} from './vocabularies';

export const planningApis: Omit<IPlanningAPI, 'redux' | '$location'> = {
events,
Expand All @@ -25,4 +26,5 @@ export const planningApis: Omit<IPlanningAPI, 'redux' | '$location'> = {
editor,
contentProfiles,
locks,
vocabularies,
};
13 changes: 13 additions & 0 deletions client/api/vocabularies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {IVocabulary} from 'superdesk-api';
import {IPlanningAPI, IPlanningState} from '../interfaces';
import {planningApi} from '../superdeskApi';

function getCustomVocabularies(): Array<IVocabulary> {
const state: IPlanningState = planningApi.redux.store.getState();

return state.customVocabularies;
}

export const vocabularies: IPlanningAPI['vocabularies'] = {
getCustomVocabularies,
};
58 changes: 0 additions & 58 deletions client/components/CustomVocabulariesFields.tsx

This file was deleted.

4 changes: 4 additions & 0 deletions client/components/UI/Form/SelectMetaTermsInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ export class SelectMetaTermsInput extends React.Component {
{...props}
withButton={true}
readOnly={readOnly}
invalid={this.props.invalid === true}
required={this.props.required}
className={classNames(
'dropdown-terms',
'select__meta-terms',
Expand Down Expand Up @@ -200,10 +202,12 @@ SelectMetaTermsInput.propTypes = {
onPopupClose: PropTypes.func,
maxLength: PropTypes.number,
language: PropTypes.string,
invalid: PropTypes.bool,
};

SelectMetaTermsInput.defaultProps = {
required: false,
invalid: false,
labelKey: 'name',
valueKey: 'qcode',
searchKey: 'name',
Expand Down
10 changes: 9 additions & 1 deletion client/components/UI/style.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
@import '~superdesk-ui-framework/app/styles/_variables.scss';
@import '~superdesk-ui-framework/app/styles/_mixins.scss';
@import '~superdesk-ui-framework/app/styles/variables/_colors.scss';

time.Datetime {
padding-left: 0;
Expand Down Expand Up @@ -31,7 +32,14 @@ time.Datetime {

.select__meta-terms {
.sd-line-input__input {
background-color: var(--color-input-bg) !important;
background-color: var(--color-input-bg);
}

&.sd-line-input--invalid {
.sd-line-input__input {
background-color: rgba(228, 27, 37, 0.075);
border-color: $red !important;
}
}
}

Expand Down
64 changes: 39 additions & 25 deletions client/components/fields/editor/CustomVocabularies.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import * as React from 'react';
import {connect} from 'react-redux';

import {IVocabulary} from 'superdesk-api';
import {IEditorFieldProps, IEditorProfile} from '../../../interfaces';
import CustomVocabulariesFields from '../../CustomVocabulariesFields';
import {superdeskApi} from '../../../superdeskApi';
import {IEditorFieldProps, IProfileSchemaTypeList} from '../../../interfaces';
import {SelectMetaTermsInput, Row} from '../../UI/Form';
import {getUserInterfaceLanguageFromCV} from '../../../utils/users';

interface IProps extends IEditorFieldProps {
schema?: IProfileSchemaTypeList;
vocabularies: Array<IVocabulary>;
profile: IEditorProfile;
popupContainer?(): HTMLElement;
onPopupOpen?(): void;
onPopupClose?(): void;
Expand All @@ -20,29 +21,42 @@ const mapStateToProps = (state) => ({

class CustomVocabulariesComponent extends React.PureComponent<IProps> {
render() {
const language = this.props.language ?? getUserInterfaceLanguageFromCV();
const {gettext} = superdeskApi.localization;
const customVocabularies = this.props.vocabularies.filter((cv) => (
(this.props.schema.vocabularies ?? []).includes(cv._id)
));

return (
<CustomVocabulariesFields
testId={this.props.testId}
customVocabularies={this.props.vocabularies.filter((cv) => (
(this.props.schema.vocabularies ?? []).includes(cv._id)
))}
fieldProps={{
item: this.props.item,
diff: this.props.item,
readOnly: this.props.disabled,
onChange: this.props.onChange,
errors: this.props.errors,
}}
language={language}
popupProps={{
onPopupOpen: this.props.onPopupOpen,
onPopupClose: this.props.onPopupClose,
}}
popupContainer={this.props.popupContainer}
/>
);
return customVocabularies.map((cv) => {
const cvFieldName = `custom_vocabularies.${cv._id}`;
const error = this.props.showErrors ? this.props.errors?.[cvFieldName] : undefined;
const parentField = cv.schema_field || 'subject';

return (
<Row
key={cv._id}
id={`form-row-${cvFieldName}`}
data-test-id={this.props.testId?.length ? `${this.props.testId}.${cv._id}` : cv._id}
>
<SelectMetaTermsInput
options={cv.items.map((item) => Object.assign({scheme: cv._id}, item))}
value={this.props.item[parentField]}
label={gettext(cv.display_name)}
readOnly={this.props.disabled}
onChange={this.props.onChange}
required={this.props.required || this.props.schema.required}
field={parentField}
scheme={cv._id}
popupContainer={this.props.popupContainer}
onPopupOpen={this.props.onPopupOpen}
onPopupClose={this.props.onPopupClose}
language={this.props.language}
invalid={!!error}
message={error}
noMargin={true}
/>
</Row>
);
});
}
}

Expand Down
2 changes: 1 addition & 1 deletion client/components/fields/editor/base/vocabulary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export class EditorFieldVocabulary extends React.PureComponent<IEditorFieldVocab
defaultValue={defaultValue}
value={value}
message={error}
invalid={error?.length > 0 && this.props.invalid}
invalid={this.props.invalid ?? (error?.length > 0 && this.props.showErrors)}
valueKey={this.props.valueKey ?? 'qcode'}
labelKey={this.props.labelKey ?? 'name'}
searchKey={this.props.searchKey ?? 'name'}
Expand Down
6 changes: 6 additions & 0 deletions client/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
IDesk,
IContentProfile,
IArticle,
IVocabulary,
IVocabularyItem,
RICH_FORMATTING_OPTION,
} from 'superdesk-api';
Expand Down Expand Up @@ -981,6 +982,7 @@ export interface IProfileSchemaTypeList extends IBaseProfileSchemaType<'list'> {
read_only?: boolean;
schema?: {[key: string]: any};
mandatory_in_list?: {[key: string]: any};
vocabularies?: Array<IVocabulary['_id']>;
}

export interface IProfileSchemaTypeInteger extends IBaseProfileSchemaType<'integer'> {}
Expand Down Expand Up @@ -1651,6 +1653,7 @@ export interface IPlanningState {
editorOpened?: boolean;
readOnly?: boolean;
planningHistoryItems: Array<any>;
customVocabularies: Array<IVocabulary>;
}

export interface IFeaturedPlanningState {
Expand Down Expand Up @@ -2143,6 +2146,9 @@ export interface IPlanningAPI {
plannings: Array<IPlanningItem>;
}>;
}
vocabularies: {
getCustomVocabularies(): Array<IVocabulary>;
};
search<T>(args: ISearchAPIParams): Promise<IRestApiResponse<T>>;
ui: {
list: {
Expand Down
6 changes: 3 additions & 3 deletions client/validators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {gettext} from '../utils';

import {default as eventValidators} from './events';
import {default as planningValidators} from './planning';
import {formProfile} from './profile';
import {formProfile, formProfileCustomVocabularies} from './profile';
import {validateAssignment} from './assignments';

export {eventValidators, formProfile, validateAssignment};
Expand Down Expand Up @@ -133,7 +133,7 @@ export const validators = {
recurring_rules: [eventValidators.validateRecurringRules],
place: [formProfile],
reference: [formProfile],
custom_vocabularies: [formProfile],
custom_vocabularies: [formProfileCustomVocabularies],
related_plannings: [formProfile],
},
planning: {
Expand All @@ -148,7 +148,7 @@ export const validators = {
subject: [formProfile],
urgency: [formProfile],
coverages: [planningValidators.validateCoverages],
custom_vocabularies: [formProfile],
custom_vocabularies: [formProfileCustomVocabularies],
place: [formProfile],
name: [formProfile],
},
Expand Down
41 changes: 41 additions & 0 deletions client/validators/profile.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import {get, isEmpty} from 'lodash';

import {IVocabularyItem, ISubject} from 'superdesk-api';
import {IEditorProfile, IProfileSchemaTypeList, IEventOrPlanningItem} from '../interfaces';
import {planningApi} from '../superdeskApi';

import {gettext} from '../utils';

export const formProfile = ({field, value, profile, errors, messages, diff}) => {
Expand Down Expand Up @@ -85,3 +90,39 @@ export const formProfile = ({field, value, profile, errors, messages, diff}) =>
delete errors[field];
}
};

interface IValidateCustomCVArgs {
field: string;
value: undefined; // Value is `undefined`, as there is no field called `custom_vocabularies`
profile: IEditorProfile;
errors: {[field: string]: string};
messages: Array<string>;
diff: Partial<IEventOrPlanningItem>;
}

export function formProfileCustomVocabularies({field, value, profile, errors, messages, diff}: IValidateCustomCVArgs) {
const schema = profile.schema?.[field];

if (schema == null || schema.type !== 'list' || profile.editor[field]?.enabled !== true) {
return;
} else if (schema.required !== true || (schema.vocabularies || []).length === 0) {
return;
}

planningApi.vocabularies.getCustomVocabularies()
.filter((cv) => schema.vocabularies.includes(cv._id))
.forEach((cv) => {
const values = ((diff[cv.schema_field || 'subject'] || []) as Array<ISubject>).filter((item) => (
item.scheme === cv._id
));
const fieldName = `custom_vocabularies.${cv._id}`;
const fieldLabel = cv.display_name.toUpperCase();

if (!values.length) {
errors[fieldName] = gettext('This field is required');
messages.push(gettext('{{ name }} is a required field', {name: fieldLabel}));
} else {
delete errors[fieldName];
}
});
}

0 comments on commit 2508e44

Please sign in to comment.