Skip to content

Commit

Permalink
IIIF #5 - Adding modal and API end point for uploading multiple resou…
Browse files Browse the repository at this point in the history
…rces
  • Loading branch information
dleadbetter committed Jul 11, 2022
1 parent b0d3ac5 commit b7e2a05
Show file tree
Hide file tree
Showing 9 changed files with 171 additions and 6 deletions.
3 changes: 3 additions & 0 deletions app/controllers/api/resources_controller.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
class Api::ResourcesController < Api::BaseController
# Includes
include Api::Uploadable

# Search attributes
search_attributes :name

Expand Down
32 changes: 32 additions & 0 deletions client/src/components/FileUpload.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// @flow

import React, { type ComponentType } from 'react';
import { Button, Form, Item } from 'semantic-ui-react';
import { withTranslation } from 'react-i18next';

const FileUpload: ComponentType<any> = withTranslation()((props) => (
<Item
className='file-upload'
>
<Item.Image
src={props.item.content_url}
/>
<Item.Content>
<Form.Input
error={props.isError('name')}
label={props.t('FileUpload.labels.name')}
onChange={props.onTextInputChange.bind(this, 'name')}
required={props.isRequired('name')}
value={props.item.name}
/>
<Button
basic
color='red'
icon='trash'
onClick={props.onDelete}
/>
</Item.Content>
</Item>
));

export default FileUpload;
5 changes: 5 additions & 0 deletions client/src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@
"header": "Success!"
}
},
"sort": {
"created": "Date created",
"name": "Name",
"updated": "Date updated"
},
"tabs": {
"details": "Details",
"organizations": "Organizations",
Expand Down
10 changes: 10 additions & 0 deletions client/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,13 @@ code {
.item-list > .items > .item .ui.button {
margin-top: 0.5em;
}

.ui.items > .file-upload.item > .image > img {
width: 150px;
height: 150px;
object-fit: cover;
}

.file-upload-modal > .toaster.ui.message {
width: 60%;
}
57 changes: 54 additions & 3 deletions client/src/pages/Resources.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,32 @@
// @flow

import { ItemList, LazyImage } from '@performant-software/semantic-components';
import React, { type ComponentType } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { ItemList, FileUploadModal, LazyImage } from '@performant-software/semantic-components';
import React, { useCallback, type ComponentType } from 'react';
import { useLocation, useNavigate, useParams } from 'react-router-dom';
import FileUpload from '../components/FileUpload';
import ResourcesService from '../services/Resources';
import { useTranslation } from 'react-i18next';

const Resources: ComponentType<any> = () => {
const location = useLocation();
const navigate = useNavigate();
const { projectId } = useParams();
const { t } = useTranslation();

/**
* Uploads the passed resources and navigates to the current URL to refresh the state.
*
* @type {function(*): Promise<R>|Promise<R|unknown>|Promise<*>|*}
*/
const onUpload = useCallback((resources) => (
ResourcesService
.upload(resources)
.then(() => navigate(location.pathname, {
state: {
saved: true
}
}))
), []);

return (
<ItemList
Expand All @@ -21,12 +40,44 @@ const Resources: ComponentType<any> = () => {
location: 'top',
onClick: () => navigate('new')
}}
buttons={[{
render: () => (
<FileUploadModal
button={t('Common.buttons.upload')}
itemComponent={FileUpload}
onAddFile={(file) => ({
name: file.name,
project_id: projectId,
content: file,
content_url: URL.createObjectURL(file)
})}
onSave={onUpload}
required={[]}
/>
)
}]}
collectionName='resources'
onLoad={(params) => ResourcesService.fetchAll({ ...params, project_id: projectId })}
onDelete={(resource) => ResourcesService.delete(resource)}
perPageOptions={[10, 25, 50, 100]}
renderHeader={(resource) => resource.name}
renderImage={(resource) => <LazyImage src={resource.content_url} />}
renderMeta={() => ''}
renderDescription={() => <div>Test</div>}
saved={location.state && location.state.saved}
sort={[{
key: 'name',
value: 'resources.name',
text: t('Common.sort.name')
}, {
key: 'created_at',
value: 'resources.created_at',
text: t('Common.sort.created')
}, {
key: 'updated_at',
value: 'resources.updated_at',
text: t('Common.sort.updated')
}]}
/>
);
};
Expand Down
17 changes: 17 additions & 0 deletions client/src/services/Resources.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { BaseService } from '@performant-software/shared-components';
import Resource from '../transforms/Resource';
import type { Resource as ResourceType } from '../types/Resource';

/**
* Class responsible for handling all resource API requests.
Expand All @@ -24,6 +25,22 @@ class Resources extends BaseService {
getTransform(): any {
return Resource;
}

/**
* Uploads the passed array of resources.
*
* @param resources
*
* @returns {*}
*/
upload(resources: Array<ResourceType>): Promise<any> {
const url = `${this.getBaseUrl()}/upload`;

const transform = this.getTransform();
const payload = transform.toUploadPayload(resources);

return this.getAxios().post(url, payload, this.getConfig());
}
}

const ResourcesService: Resources = new Resources();
Expand Down
27 changes: 25 additions & 2 deletions client/src/transforms/Resource.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// @flow

import { Attachments, FormDataTransform } from '@performant-software/shared-components';
import _ from 'underscore';
import type { Resource as ResourceType } from '../types/Resource';
import String from '../utils/String';

/**
* Class for handling transforming resource for PUT/POST requests.
Expand Down Expand Up @@ -30,18 +32,39 @@ class Resource extends FormDataTransform {
}

/**
* Returns the passed resource record as JSON for PUT/POST requests.
* Returns the passed resource record as FormData for PUT/POST requests.
*
* @param resource
*
* @returns {*}
* @returns {FormData}
*/
toPayload(resource: ResourceType): FormData {
const formData = super.toPayload(resource);
Attachments.toPayload(formData, this.getParameterName(), resource, 'content');

return formData;
}

/**
* Returns the passed array of resources as FormData for multi-record upload.
*
* @param resources
*
* @returns {FormData}
*/
toUploadPayload(resources: Array<ResourceType>): FormData {
const formData = new FormData();

_.each(resources, (resource, index) => {
_.each(this.getPayloadKeys(), (key) => {
formData.append(`resources[${index}][${key}]`, String.toString(resource[key]));
});

Attachments.toPayload(formData, `resources[${index}]`, resource, 'content');
});

return formData;
}
}

const ResourceTransform: Resource = new Resource();
Expand Down
22 changes: 22 additions & 0 deletions client/src/utils/String.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// @flow

import _ from 'underscore';

/**
* Converts the passed value to a string for FormData.
*
* @param value
*
* @returns {*|string}
*/
const toString: any = (value: any) => {
if (_.isNumber(value) || _.isBoolean(value)) {
return value;
}

return value || '';
};

export default {
toString
};
4 changes: 3 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
namespace :api do
resources :organizations
resources :projects
resources :resources
resources :resources do
post :upload, on: :collection
end
resources :users

# Authentication
Expand Down

0 comments on commit b7e2a05

Please sign in to comment.