Skip to content

Commit

Permalink
IIIF #78 - Adding attachment info the Resource edit page
Browse files Browse the repository at this point in the history
  • Loading branch information
dleadbetter committed Jan 14, 2025
1 parent df0f24b commit dcde885
Show file tree
Hide file tree
Showing 10 changed files with 249 additions and 7 deletions.
13 changes: 13 additions & 0 deletions app/serializers/resources_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,20 @@ class ResourcesSerializer < BaseSerializer

show_attributes(:manifest_url) { |resource| manifest_url(resource) }

show_attributes(:content_info) { |resource| content_info(resource.content) }
show_attributes(:content_converted_info) { |resource| content_info(resource.content_converted) }

def self.manifest_url(resource)
"#{ENV['HOSTNAME']}/public/resources/#{resource.uuid}/manifest"
end

def self.content_info(attachment)
return unless attachment.attached?

{
key: attachment.key,
byte_size: attachment.byte_size,
content_type: attachment.content_type
}
end
end
80 changes: 80 additions & 0 deletions client/src/components/AttachmentDetails.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// @flow

import cx from 'classnames';
import React, { type ComponentType } from 'react';
import { useTranslation } from 'react-i18next';
import { Icon, Image, List } from 'semantic-ui-react';
import FileUtils from '../utils/File';
import styles from './AttachmentDetails.module.css';

type Props = {
attachment?: {
byte_size: number,
content_type: string,
key: string,
}
};

const AttachmentDetails: ComponentType<any> = (props: Props) => {
const { t } = useTranslation();

return (
<div
className={styles.attachmentDetails}
>
{ !props.attachment && (
<span>
{ t('AttachmentDetails.messages.noContent') }
</span>
)}
{ props.attachment && (
<List
className={cx(styles.ui, styles.list, styles.horizontal)}
horizontal
>
<List.Item
content={props.attachment?.key}
header={t('AttachmentDetails.labels.key')}
image={(
<Image>
<Icon
circular
name='aws'
size='large'
/>
</Image>
)}
/>
<List.Item
content={FileUtils.getFileSize(props.attachment?.byte_size)}
header={t('AttachmentDetails.labels.fileSize')}
image={(
<Image>
<Icon
circular
name='file alternate'
size='large'
/>
</Image>
)}
/>
<List.Item
content={props.attachment?.content_type}
header={t('AttachmentDetails.labels.type')}
image={(
<Image>
<Icon
circular
name='th large'
size='large'
/>
</Image>
)}
/>
</List>
)}
</div>
);
};

export default AttachmentDetails;
9 changes: 9 additions & 0 deletions client/src/components/AttachmentDetails.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.attachmentDetails {
margin-top: 1.5em;
}

.attachmentDetails > .ui.list.horizontal {
display: flex;
justify-content: space-between;
width: 100%;
}
31 changes: 31 additions & 0 deletions client/src/components/AttachmentStatus.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// @flow

import React, { type ComponentType } from 'react';
import { Icon } from 'semantic-ui-react';

type Props = {
className?: string,
value: boolean
};

const AttachmentStatus: ComponentType<any> = (props: Props) => {
if (props.value) {
return (
<Icon
className={props.className}
color='green'
name='check circle'
/>
);
}

return (
<Icon
className={props.className}
color='red'
name='times circle'
/>
);
};

export default AttachmentStatus;
3 changes: 2 additions & 1 deletion client/src/components/SimpleEditPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { useLocation, useNavigate } from 'react-router-dom';
import type { Translateable } from '../types/Translateable';

type Props = EditContainerProps & Translateable & {
className?: string,
onTabClick?: (tab: string) => void,
stickyMenu?: boolean
};
Expand Down Expand Up @@ -138,7 +139,7 @@ const SimpleEditPage: ComponentType<any> = (props: Props) => {

return (
<Grid
className={styles.simpleEditPage}
className={cx(styles.simpleEditPage, props.className)}
>
<Grid.Row>
<Grid.Column>
Expand Down
13 changes: 13 additions & 0 deletions client/src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@
"success": "You've successfully authenticated"
}
},
"AttachmentDetails": {
"labels": {
"fileSize": "File size",
"key": "Key",
"type": "Type"
},
"messages": {
"noContent": "No attached content."
}
},
"Common": {
"buttons": {
"add": "Add",
Expand Down Expand Up @@ -108,7 +118,10 @@
"exif": "View Info"
},
"labels": {
"attachments": "Attachments",
"content": "Content",
"convertedImage": "Converted PTIF",
"sourceImage": "Source Image",
"uuid": "Unique identifier"
}
},
Expand Down
75 changes: 70 additions & 5 deletions client/src/pages/Resource.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,46 @@ import React, {
} from 'react';
import { withTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
import { Button, Form } from 'semantic-ui-react';
import {
Button,
Form,
Header,
Menu,
Segment
} from 'semantic-ui-react';
import ProjectsService from '../services/Projects';
import ReadOnlyField from '../components/ReadOnlyField';
import ResourceExifModal from '../components/ResourceExifModal';
import ResourcesService from '../services/Resources';
import SimpleEditPage from '../components/SimpleEditPage';
import styles from './Resource.module.css';
import withEditPage from '../hooks/EditPage';
import AttachmentStatus from '../components/AttachmentStatus';
import cx from 'classnames';
import AttachmentDetails from '../components/AttachmentDetails';

const Tabs = {
content: 'content',
content_converted: 'content_converted'
};

const ResourceForm = withTranslation()((props) => {
const [tab, setTab] = useState(Tabs.content);
const [info, setInfo] = useState(false);
const [project, setProject] = useState();

const { projectId } = useParams();

/**
* Memo-izes the current attachment info.
*
* @type {AttachmentInfo}
*/
const attachment = useMemo(() => (tab === Tabs.content
? props.item.content_info
: props.item.content_converted_info
), [tab, props.item]);

/**
* Converts the EXIF data to JSON.
*
Expand Down Expand Up @@ -63,12 +89,17 @@ const ResourceForm = withTranslation()((props) => {

return (
<SimpleEditPage
className={styles.resource}
{...props}
>
<SimpleEditPage.Tab
key='details'
name={props.t('Common.tabs.details')}
>
<ReadOnlyField
label={props.t('Resource.labels.uuid')}
value={props.item.uuid}
/>
<Form.Input
label={props.t('Resource.labels.content')}
>
Expand Down Expand Up @@ -103,10 +134,6 @@ const ResourceForm = withTranslation()((props) => {
required={props.isRequired('name')}
value={props.item.name}
/>
<ReadOnlyField
label={props.t('Resource.labels.uuid')}
value={props.item.uuid}
/>
<UserDefinedFieldsForm
data={props.item.user_defined}
defineableId={projectId}
Expand All @@ -122,6 +149,44 @@ const ResourceForm = withTranslation()((props) => {
onClose={() => setInfo(false)}
/>
)}
<Header
content={props.t('Resource.labels.attachments')}
/>
<Segment
padded
>
<Menu
className={cx(styles.ui, styles.menu)}
pointing
secondary
>
<Menu.Item
name={Tabs.content}
active={tab === Tabs.content}
onClick={() => setTab(Tabs.content)}
>
{ props.t('Resource.labels.sourceImage') }
<AttachmentStatus
className={cx(styles.icon, styles.attachmentStatus)}
value={!!props.item.content_info}
/>
</Menu.Item>
<Menu.Item
name={Tabs.content_converted}
active={tab === Tabs.content_converted}
onClick={() => setTab(Tabs.content_converted)}
>
{ props.t('Resource.labels.convertedImage') }
<AttachmentStatus
className={cx(styles.icon, styles.attachmentStatus)}
value={!!props.item.content_converted_info}
/>
</Menu.Item>
</Menu>
<AttachmentDetails
attachment={attachment}
/>
</Segment>
</SimpleEditPage.Tab>
</SimpleEditPage>
);
Expand Down
3 changes: 3 additions & 0 deletions client/src/pages/Resource.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.resource .ui.menu i.icon.attachmentStatus {
margin-left: 0.5em;
}
10 changes: 9 additions & 1 deletion client/src/types/Resource.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

import type { Project } from './Project';

export type AttachmentInfo = {
content_type: string,
byte_size: number,
key: string
};

export type Resource = {
id: number,
name: string,
Expand All @@ -14,5 +20,7 @@ export type Resource = {
manifest: string,
metadata: any,
project_id: number,
project: Project
project: Project,
content_info: AttachmentInfo,
content_converted_info: AttachmentInfo
};
19 changes: 19 additions & 0 deletions client/src/utils/File.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const FILE_SIZE_UNITS = ['B', 'kB', 'MB', 'GB', 'TB'];

/**
* Returns the human-readable file size for the passed bytes.
*
* @param size
*
* @returns {`${number} ${string}`}
*/
const getFileSize = (size: number) => {
const i = size === 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
const fileSize = ((size / 1024 ** i).toFixed(2)) * 1;

return `${fileSize} ${FILE_SIZE_UNITS[i]}`;
};

export default {
getFileSize
};

0 comments on commit dcde885

Please sign in to comment.