-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
IIIF #5 - Updating to allow defining custom metadata on projects; All…
…owing setting custom metadata on resources
- Loading branch information
1 parent
1adb564
commit e5ee76a
Showing
15 changed files
with
449 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
class ProjectsSerializer < BaseSerializer | ||
index_attributes :id, :uid, :name, :description, :avatar_thumbnail_url, :organization_id, organization: OrganizationsSerializer | ||
show_attributes :id, :uid, :name, :description, :api_key, :avatar_url, :avatar_preview_url, :organization_id, organization: OrganizationsSerializer | ||
show_attributes :id, :uid, :name, :description, :api_key, :avatar_url, :avatar_preview_url, :metadata, :organization_id, organization: OrganizationsSerializer | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
// @flow | ||
|
||
import React, { useCallback, type ComponentType } from 'react'; | ||
import { useTranslation } from 'react-i18next'; | ||
import { Button, Form, List } from 'semantic-ui-react'; | ||
import _ from 'underscore'; | ||
import Metadata from '../constants/Metadata'; | ||
import MetadataOptions from './MetadataOptions'; | ||
|
||
const MetadataList: ComponentType<any> = (props) => { | ||
const { t } = useTranslation(); | ||
|
||
/** | ||
* Adds a new item to the list. | ||
* | ||
* @type {(function(): void)|*} | ||
*/ | ||
const onAddItem = useCallback(() => { | ||
props.onChange([...props.items, {}]); | ||
}, [props.items]); | ||
|
||
/** | ||
* Removes the item at the passed index from the list. | ||
* | ||
* @type {(function(*): void)|*} | ||
*/ | ||
const onRemoveItem = useCallback((findIndex) => { | ||
props.onChange(_.reject(props.items, (item, index) => index === findIndex)); | ||
}, [props.items]); | ||
|
||
/** | ||
* Updates the passed attribute of the item at the passed index. | ||
* | ||
* @type {(function(number, string, ?Event, {value: *}): void)|*} | ||
*/ | ||
const onUpdateItem = useCallback((findIndex: number, attribute: string, e: ?Event, { value }) => { | ||
props.onChange(_.map(props.items, (item, index) => ( | ||
index !== findIndex ? item : ({ ...item, [attribute]: value }) | ||
))); | ||
}, [props.items]); | ||
|
||
return ( | ||
<Form> | ||
<Button | ||
basic | ||
content={t('Common.buttons.add')} | ||
icon='plus' | ||
onClick={onAddItem.bind(this)} | ||
type='button' | ||
/> | ||
<List | ||
divided | ||
relaxed='very' | ||
> | ||
{ _.map(props.items, (item, index) => ( | ||
<List.Item> | ||
<Form.Group | ||
style={{ | ||
alignItems: 'center' | ||
}} | ||
> | ||
<Form.Input | ||
onChange={onUpdateItem.bind(this, index, 'name')} | ||
placeholder={t('MetadataList.labels.name')} | ||
value={item.name} | ||
width={7} | ||
/> | ||
<Form.Dropdown | ||
clearable | ||
onChange={onUpdateItem.bind(this, index, 'type')} | ||
options={Metadata.getOptions()} | ||
placeholder={t('MetadataList.labels.type')} | ||
value={item.type} | ||
selectOnBlur={false} | ||
selection | ||
width={6} | ||
/> | ||
<Form.Checkbox | ||
checked={item.required} | ||
label={t('MetadataList.labels.required')} | ||
onChange={(e, { checked }) => onUpdateItem(index, 'required', e, { value: checked })} | ||
width={2} | ||
/> | ||
<Form.Button | ||
color='red' | ||
icon='trash' | ||
onClick={onRemoveItem.bind(this, index)} | ||
type='button' | ||
width={1} | ||
/> | ||
</Form.Group> | ||
{ item.type === 'dropdown' && ( | ||
<Form.Group | ||
style={{ | ||
alignItems: 'center' | ||
}} | ||
> | ||
<Form.Checkbox | ||
checked={item.multiple} | ||
label={t('MetadataList.labels.multiple')} | ||
onChange={(e, { checked }) => onUpdateItem(index, 'multiple', e, { value: checked })} | ||
/> | ||
<Form.Field> | ||
<MetadataOptions | ||
options={item.options} | ||
onChange={(options) => onUpdateItem(index, 'options', null, { value: options })} | ||
/> | ||
</Form.Field> | ||
</Form.Group> | ||
)} | ||
</List.Item> | ||
))} | ||
</List> | ||
</Form> | ||
); | ||
}; | ||
|
||
export default MetadataList; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
// @flow | ||
|
||
import React, { | ||
useCallback, | ||
useEffect, | ||
useState, | ||
type ComponentType | ||
} from 'react'; | ||
import { Button, Input, Label } from 'semantic-ui-react'; | ||
import _ from 'underscore'; | ||
|
||
type Props = { | ||
options: Array<string>, | ||
onChange: (options: Array<string>) => void | ||
}; | ||
|
||
const MetadataOptions: ComponentType<any> = (props: Props) => { | ||
const [options, setOptions] = useState(_.map(props.options, (option) => ({ value: option }))); | ||
|
||
/** | ||
* Adds a new option to the list. | ||
* | ||
* @type {(function(): void)|*} | ||
*/ | ||
const onAddOption = useCallback(() => { | ||
setOptions((prevOptions) => [...prevOptions, { new: true }]); | ||
}, []); | ||
|
||
/** | ||
* Deletes the option at the passed index from the list. | ||
* | ||
* @type {(function(*): void)|*} | ||
*/ | ||
const onDeleteOption = useCallback((findIndex) => { | ||
setOptions((prevOptions) => _.filter(prevOptions, (option, index) => index !== findIndex)); | ||
}, []); | ||
|
||
/** | ||
* Removes the "new" indicator from the option at the passed index. | ||
* | ||
* @type {(function(*): void)|*} | ||
*/ | ||
const onSaveOption = useCallback((findIndex) => { | ||
setOptions((prevOptions) => _.map( | ||
prevOptions, | ||
(option, index) => (findIndex !== index ? option : ({ ...option, new: false })) | ||
)); | ||
}, [options]); | ||
|
||
/** | ||
* Updates the value of the option at the passed index. | ||
* | ||
* @type {(function(*, *, {value: *}): void)|*} | ||
*/ | ||
const onUpdateOption = useCallback((findIndex, e, { value }) => { | ||
setOptions((prevOptions) => _.map( | ||
prevOptions, | ||
(option, index) => (index !== findIndex ? option : ({ ...option, value })) | ||
)); | ||
}, []); | ||
|
||
/** | ||
* Calls the onChange prop when the list of options changes. | ||
*/ | ||
useEffect(() => { | ||
const savedOptions = _.filter(options, (option) => !option.new); | ||
props.onChange(_.pluck(savedOptions, 'value')); | ||
}, [options]); | ||
|
||
return ( | ||
<div> | ||
<Button | ||
basic | ||
icon='plus' | ||
onClick={onAddOption} | ||
type='button' | ||
/> | ||
{ _.map(options, (option, index) => ( | ||
<> | ||
{ option.new && ( | ||
<Label> | ||
<Input | ||
onChange={onUpdateOption.bind(this, index)} | ||
value={option.value} | ||
style={{ | ||
width: 'unset' | ||
}} | ||
/> | ||
<Button | ||
basic | ||
color='green' | ||
compact | ||
icon='checkmark' | ||
onClick={onSaveOption.bind(this, index)} | ||
type='button' | ||
/> | ||
</Label> | ||
)} | ||
{ !option.new && ( | ||
<Label | ||
content={option.value} | ||
onRemove={onDeleteOption.bind(this, index)} | ||
/> | ||
)} | ||
</> | ||
))} | ||
</div> | ||
); | ||
}; | ||
|
||
export default MetadataOptions; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
// @flow | ||
|
||
import { DatePicker } from '@performant-software/semantic-components'; | ||
import React, { useCallback, type ComponentType } from 'react'; | ||
import { Form } from 'semantic-ui-react'; | ||
import _ from 'underscore'; | ||
import Metadata from '../constants/Metadata'; | ||
|
||
type Item = { | ||
multiple?: boolean, | ||
name: string, | ||
options?: Array<string>, | ||
required?: boolean, | ||
type: string, | ||
value: any | ||
}; | ||
|
||
type Props = { | ||
items: Array<Item>, | ||
onChange: (item: any) => void, | ||
value: any | ||
}; | ||
|
||
const ResourceMetadata: ComponentType<any> = (props: Props) => { | ||
/** | ||
* Changes the value for the passed item. | ||
* | ||
* @type {(function(*, *): void)|*} | ||
*/ | ||
const onChange = useCallback((item, value) => { | ||
props.onChange({ ...props.value, [item.name]: value }); | ||
}, [props.onChange, props.value]); | ||
|
||
/** | ||
* Renders the passed item. | ||
* | ||
* @type {function(*): *} | ||
*/ | ||
const renderItem = useCallback((item) => { | ||
let rendered; | ||
|
||
if (item.type === Metadata.Types.string) { | ||
rendered = ( | ||
<Form.Input | ||
label={item.name} | ||
required={item.required} | ||
onChange={(e, { value }) => onChange(item, value)} | ||
value={props.value && props.value[item.name]} | ||
/> | ||
); | ||
} | ||
|
||
if (item.type === Metadata.Types.number) { | ||
rendered = ( | ||
<Form.Input | ||
label={item.name} | ||
required={item.required} | ||
onChange={(e, { value }) => onChange(item, value)} | ||
value={props.value && props.value[item.name]} | ||
type='number' | ||
/> | ||
); | ||
} | ||
|
||
if (item.type === Metadata.Types.dropdown) { | ||
rendered = ( | ||
<Form.Dropdown | ||
label={item.name} | ||
multiple={item.multiple} | ||
required={item.required} | ||
options={_.map(item.options, (option) => ({ key: option, value: option, text: option }))} | ||
onChange={(e, { value }) => onChange(item, value)} | ||
selectOnBlur={false} | ||
selection | ||
value={props.value && props.value[item.name]} | ||
/> | ||
); | ||
} | ||
|
||
if (item.type === Metadata.Types.text) { | ||
rendered = ( | ||
<Form.TextArea | ||
label={item.name} | ||
required={item.required} | ||
onChange={(e, { value }) => onChange(item, value)} | ||
value={props.value && props.value[item.name]} | ||
/> | ||
); | ||
} | ||
|
||
if (item.type === Metadata.Types.date) { | ||
rendered = ( | ||
<Form.Input | ||
label={item.name} | ||
required={item.required} | ||
> | ||
<DatePicker | ||
onChange={(date) => onChange(item, date && date.toString())} | ||
value={props.value && props.value[item.name] && new Date(props.value[item.name])} | ||
/> | ||
</Form.Input> | ||
); | ||
} | ||
|
||
if (item.type === Metadata.Types.checkbox) { | ||
rendered = ( | ||
<Form.Checkbox | ||
checked={props.value && props.value[item.name]} | ||
label={item.name} | ||
onChange={(e, { checked }) => onChange(item, checked)} | ||
/> | ||
); | ||
} | ||
|
||
return rendered; | ||
}, [props.value]); | ||
|
||
return ( | ||
<> | ||
{ _.map(props.items, renderItem.bind(this)) } | ||
</> | ||
); | ||
}; | ||
|
||
export default ResourceMetadata; |
Oops, something went wrong.