Skip to content

Commit

Permalink
IIIF #5 - Adding client and server side validations for resources
Browse files Browse the repository at this point in the history
  • Loading branch information
dleadbetter committed Jul 18, 2022
1 parent 0ce2522 commit 25296ab
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 4 deletions.
20 changes: 20 additions & 0 deletions app/models/resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ class Resource < ApplicationRecord
has_one_attached :content
has_one_attached :content_converted

# Validations
validate :validate_metadata

# Overload attachable methods
alias_method :attachable_content_url, :content_url
alias_method :attachable_content_base_url, :content_base_url
Expand Down Expand Up @@ -90,4 +93,21 @@ def create_manifest
def set_uuid
self.uuid = SecureRandom.uuid
end

def validate_metadata
items = JSON.parse(project.metadata || '[]')
return if items.nil? || items.empty?

values = JSON.parse(self.metadata || '{}')

items.each do |item|
required = item['required'].to_s.to_bool
name = item['name']
value = values[name]

if required && (value.nil? || value.empty?)
errors.add("metadata[#{name}]", I18n.t('errors.resource.metadata.required', name: name))
end
end
end
end
14 changes: 14 additions & 0 deletions client/src/components/ResourceMetadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,20 @@ type Item = {
};

type Props = {
isError: (key: string) => boolean,
items: Array<Item>,
onChange: (item: any) => void,
value: any
};

const ResourceMetadata: ComponentType<any> = (props: Props) => {
/**
* Returns true if there is a validation error for the passed item.
*
* @type {function(*): *}
*/
const isError = useCallback((item) => props.isError(`metadata[${item.name}]`), [props.isError]);

/**
* Changes the value for the passed item.
*
Expand All @@ -42,6 +50,7 @@ const ResourceMetadata: ComponentType<any> = (props: Props) => {
if (item.type === Metadata.Types.string) {
rendered = (
<Form.Input
error={isError(item)}
label={item.name}
required={item.required}
onChange={(e, { value }) => onChange(item, value)}
Expand All @@ -53,6 +62,7 @@ const ResourceMetadata: ComponentType<any> = (props: Props) => {
if (item.type === Metadata.Types.number) {
rendered = (
<Form.Input
error={isError(item)}
label={item.name}
required={item.required}
onChange={(e, { value }) => onChange(item, value)}
Expand All @@ -65,6 +75,7 @@ const ResourceMetadata: ComponentType<any> = (props: Props) => {
if (item.type === Metadata.Types.dropdown) {
rendered = (
<Form.Dropdown
error={isError(item)}
label={item.name}
multiple={item.multiple}
required={item.required}
Expand All @@ -80,6 +91,7 @@ const ResourceMetadata: ComponentType<any> = (props: Props) => {
if (item.type === Metadata.Types.text) {
rendered = (
<Form.TextArea
error={isError(item)}
label={item.name}
required={item.required}
onChange={(e, { value }) => onChange(item, value)}
Expand All @@ -91,6 +103,7 @@ const ResourceMetadata: ComponentType<any> = (props: Props) => {
if (item.type === Metadata.Types.date) {
rendered = (
<Form.Input
error={isError(item)}
label={item.name}
required={item.required}
>
Expand All @@ -106,6 +119,7 @@ const ResourceMetadata: ComponentType<any> = (props: Props) => {
rendered = (
<Form.Checkbox
checked={props.value && props.value[item.name]}
error={isError(item)}
label={item.name}
onChange={(e, { checked }) => onChange(item, checked)}
/>
Expand Down
3 changes: 3 additions & 0 deletions client/src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@
}
},
"Resource": {
"errors": {
"required": "{{name}} is required"
},
"labels": {
"content": "Content"
}
Expand Down
43 changes: 39 additions & 4 deletions client/src/pages/Resource.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// @flow

import { FileInputButton, LazyImage } from '@performant-software/semantic-components';
import { Object as ObjectUtils } from '@performant-software/shared-components';
import CloverIIIF from '@samvera/clover-iiif';
import React, {
useEffect,
Expand All @@ -17,6 +18,7 @@ import {
Modal
} from 'semantic-ui-react';
import _ from 'underscore';
import i18n from '../i18n/i18n';
import ProjectsService from '../services/Projects';
import ResourceMetadata from '../components/ResourceMetadata';
import ResourcesService from '../services/Resources';
Expand All @@ -41,16 +43,23 @@ const ResourceForm = withTranslation()((props) => {
), [props.item.manifest]);

/**
* Sets the project ID on the state.
* Loads the related project record.
*/
useEffect(() => {
props.onSetState({ project_id: projectId });

ProjectsService
.fetchOne(projectId)
.then(({ data }) => setProject(data.project));
}, [projectId]);

/**
* Sets the project ID on the state.
*/
useEffect(() => {
if (project) {
props.onSetState({ project, project_id: project.id });
}
}, [project]);

return (
<SimpleEditPage
{...props}
Expand Down Expand Up @@ -123,6 +132,7 @@ const ResourceForm = withTranslation()((props) => {
/>
{ project && (
<ResourceMetadata
isError={props.isError}
items={JSON.parse(project.metadata)}
onChange={(obj) => props.onTextInputChange('metadata', null, { value: JSON.stringify(obj) })}
value={props.item.metadata && JSON.parse(props.item.metadata)}
Expand Down Expand Up @@ -150,6 +160,30 @@ const ResourceForm = withTranslation()((props) => {
);
});

const ValidateResource = (resource) => {
const errors = {};

if (resource) {
const { project } = resource;

// Validate required metadata
if (project && project.metadata) {
const items = JSON.parse(project.metadata);
const values = JSON.parse(resource.metadata || '{}');

_.each(items, (item) => {
const { name, required } = item;

if (required && ObjectUtils.isEmpty(values[name])) {
_.extend(errors, { [`metadata[${name}]`]: i18n.t('Resource.errors.required', { name }) });
}
});
}
}

return errors;
};

const Resource: ComponentType<any> = withEditPage(ResourceForm, {
id: 'resourceId',
onInitialize: (
Expand All @@ -162,7 +196,8 @@ const Resource: ComponentType<any> = withEditPage(ResourceForm, {
.save(resource)
.then(({ data }) => data.resource)
),
required: ['name']
required: ['name'],
validate: ValidateResource
});

export default Resource;
3 changes: 3 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,7 @@ en:
options_duplicate: "Dropdowns cannot contain duplicate options"
options_empty: "Dropdown options cannot be empty."
type_empty: "Type cannot be empty"
resource:
metadata:
required: "%{name} is required."
unauthorized: "You do not have access to this record."

0 comments on commit 25296ab

Please sign in to comment.