diff --git a/res/strings/renderer.de.yml b/res/strings/renderer.de.yml index c153a6b9e..b7c55ffd6 100644 --- a/res/strings/renderer.de.yml +++ b/res/strings/renderer.de.yml @@ -29,6 +29,8 @@ de: other {# Objekte}} selection: Ausschnitt + popup: + placeholder: Neues Feld … tags: tab: Tags add: > @@ -227,6 +229,9 @@ de: saturation: label: Sättigung value: "{value, number}" + sharpen: + label: Schärfe + value: "{value, number}" notepad: placeholder: Notizen hier eintragen … completions: diff --git a/res/strings/renderer.en.yml b/res/strings/renderer.en.yml index 28cff05af..8669de5a2 100644 --- a/res/strings/renderer.en.yml +++ b/res/strings/renderer.en.yml @@ -97,7 +97,11 @@ en: en: English fr: Français ja: 日本語 - template: Default Template + templates: + label: Default templates + item: Item + photo: Photo + selection: Selection debug: Enable Developer Mode localtime: Assume photo metadata use local timezone dup: @@ -166,6 +170,7 @@ en: item: created: Date Added modified: Modified + template: Template photo: file: File size: Size @@ -222,7 +227,7 @@ en: types: "https://tropy.org/v1/tropy#Item": Item Template "https://tropy.org/v1/tropy#Photo": Photo Template - "https://tropy.org/v1/tropy#Selection": Photo Selection Template + "https://tropy.org/v1/tropy#Selection": Selection Template field: property: Property label: Label @@ -467,6 +472,8 @@ en: tag: save: Failed to save tag. create: Failed to tag item. Tags must be unique per item. + template: + change: Failed to change item template. list: load: Failed to load lists. create: Failed to create list. List names must be unique. @@ -478,6 +485,12 @@ en: consolidate: Failed to consolidate photos. create: Failed to import photo. save: Failed to save photo. + template: + change: Failed to change photo template. + selection: + create: Failed to create photo selection. + template: + change: Failed to change selection template. ontology: import: Failed to import vocabulary. vocab: diff --git a/src/actions/item.js b/src/actions/item.js index f6737c314..9f0b7504b 100644 --- a/src/actions/item.js +++ b/src/actions/item.js @@ -112,14 +112,6 @@ module.exports = { } }, - save({ id, property, value }, meta) { - return { - type: ITEM.SAVE, - payload: { id: array(id), property, value }, - meta: { cmd: 'project', history: 'add', ...meta } - } - }, - update(payload, meta) { return { type: ITEM.UPDATE, @@ -251,5 +243,23 @@ module.exports = { meta: { ...meta } } } + }, + + template: { + change({ id, template }, meta) { + return { + type: ITEM.TEMPLATE.CHANGE, + payload: { + id: array(id), + property: 'template', + value: template + }, + meta: { + cmd: 'project', + history: 'add', + ...meta + } + } + } } } diff --git a/src/actions/photo.js b/src/actions/photo.js index 12a3674c0..3cbd42478 100644 --- a/src/actions/photo.js +++ b/src/actions/photo.js @@ -162,5 +162,23 @@ module.exports = { meta: { ...meta } } } + }, + + template: { + change({ id, template }, meta) { + return { + type: PHOTO.TEMPLATE.CHANGE, + payload: { + id: array(id), + property: 'template', + value: template + }, + meta: { + cmd: 'project', + history: 'add', + ...meta + } + } + } } } diff --git a/src/actions/selection.js b/src/actions/selection.js index edc8cfa3b..b6fef1152 100644 --- a/src/actions/selection.js +++ b/src/actions/selection.js @@ -1,6 +1,7 @@ 'use strict' const { SELECTION } = require('../constants') +const { array } = require('../common/util') module.exports = { create(payload, meta) { @@ -80,4 +81,32 @@ module.exports = { } }, + bulk: { + update(payload, meta = {}) { + return { + type: SELECTION.BULK.UPDATE, + payload, + meta + } + } + }, + + + template: { + change({ id, template }, meta) { + return { + type: SELECTION.TEMPLATE.CHANGE, + payload: { + id: array(id), + property: 'template', + value: template + }, + meta: { + cmd: 'project', + history: 'add', + ...meta + } + } + } + } } diff --git a/src/commands/item.js b/src/commands/item.js index 5f3c6a4dc..c160e85a9 100644 --- a/src/commands/item.js +++ b/src/commands/item.js @@ -7,6 +7,7 @@ const { DuplicateError } = require('../common/error') const { all, call, put, select, cps } = require('redux-saga/effects') const { Command } = require('./command') const { ImportCommand } = require('./import') +const { SaveCommand } = require('./subject') const { prompt, open, fail, save } = require('../dialog') const { Image } = require('../image') const act = require('../actions') @@ -16,7 +17,6 @@ const { darwin } = require('../common/os') const { ITEM, DC } = require('../constants') const { MODE } = require('../constants/project') const { keys } = Object -const { isArray } = Array const { writeFile: write } = require('fs') const { win } = require('../window') const { groupedByTemplate } = require('../export') @@ -230,78 +230,9 @@ class Restore extends Command { } } -class Save extends Command { - static get ACTION() { return ITEM.SAVE } - - *exec() { - const { db } = this.options - const { payload, meta } = this.action - - if (!isArray(payload)) { - const { id: ids, property, value } = this.action.payload - - assert.equal(property, 'template') - - const state = yield select(({ items, ontology, metadata }) => ({ - items, templates: ontology.template, metadata - })) - - const props = { [property]: value, modified: new Date(meta.now) } - const template = state.templates[value] - - assert(template != null, 'unknown template') - - const data = getTemplateValues(template) - const datakeys = data != null ? keys(data) : [] - const hasData = datakeys.length > 0 - - const original = ids.map(id => [ - id, - get(state.items, [id, property]), - hasData ? pick(state.metadata[id], datakeys, {}, true) : null - ]) - - yield call(db.transaction, async tx => { - await mod.item.update(tx, ids, props, meta.now) - - if (hasData) { - await mod.metadata.update(tx, { id: ids, data }) - } - }) - - yield put(act.item.bulk.update([ids, props])) - - if (hasData) { - yield put(act.metadata.update({ ids, data })) - } - - this.undo = { ...this.action, payload: original } - - } else { - - let hasData = false - let changed = { props: {}, data: {} } - - yield call(db.transaction, async tx => { - for (let [id, template, data] of payload) { - changed.props[id] = { template, modified: new Date(meta.now) } - await mod.item.update(tx, [id], changed.props[id], meta.now) - - if (data) { - hasData = true - changed.data[id] = data - await mod.metadata.update(tx, { id, data }) - } - } - }) - - yield put(act.item.bulk.update(changed.props)) - - if (hasData) { - yield put(act.metadata.merge(changed.data)) - } - } - } +class TemplateChange extends SaveCommand { + static get ACTION() { return ITEM.TEMPLATE.CHANGE } + get type() { return 'item' } } class Merge extends Command { @@ -614,7 +545,7 @@ module.exports = { Merge, Split, Restore, - Save, + TemplateChange, Preview, AddTag, RemoveTag, diff --git a/src/commands/photo.js b/src/commands/photo.js index 2bab6ee2b..586f8e5c6 100644 --- a/src/commands/photo.js +++ b/src/commands/photo.js @@ -4,6 +4,7 @@ const assert = require('assert') const { all, call, put, select } = require('redux-saga/effects') const { Command } = require('./command') const { ImportCommand } = require('./import') +const { SaveCommand } = require('./subject') const { fail, open } = require('../dialog') const mod = require('../models') const act = require('../actions') @@ -404,6 +405,12 @@ class Restore extends Command { } } +class TemplateChange extends SaveCommand { + static get ACTION() { return PHOTO.TEMPLATE.CHANGE } + get type() { return 'photo' } +} + + module.exports = { Consolidate, Create, @@ -413,5 +420,6 @@ module.exports = { Move, Order, Restore, - Save + Save, + TemplateChange } diff --git a/src/commands/selection.js b/src/commands/selection.js index c19c296c7..8e0b90d3e 100644 --- a/src/commands/selection.js +++ b/src/commands/selection.js @@ -3,6 +3,7 @@ const { call, put, select } = require('redux-saga/effects') const { Command } = require('./command') const { ImportCommand } = require('./import') +const { SaveCommand } = require('./subject') const { Image } = require('../image') const mod = require('../models') const act = require('../actions') @@ -10,6 +11,11 @@ const { SELECTION } = require('../constants') const { pick, splice } = require('../common/util') const { keys } = Object +const { + getSelectionTemplate, + getTemplateValues +} = require('../selectors') + class Create extends ImportCommand { static get ACTION() { return SELECTION.CREATE } @@ -18,21 +24,31 @@ class Create extends ImportCommand { let { db } = this.options let { payload, meta } = this.action - let photo = yield select(state => state.photos[payload.photo]) + let [photo, template] = yield select(state => ([ + state.photos[payload.photo], + getSelectionTemplate(state) + ])) + let image = yield call(Image.open, photo.path, photo.page) let idx = (meta.idx != null) ? meta.idx : [photo.selections.length] - let selection = yield call(db.transaction, tx => - mod.selection.create(tx, null, payload)) + let data = getTemplateValues(template) - let data = { selections: [selection.id] } + let selection = yield call(db.transaction, tx => + mod.selection.create(tx, { + data, + template: template.id, + ...payload + })) yield* this.createThumbnails(selection.id, image, { selection }) - yield put(act.photo.selections.add({ id: photo.id, ...data }, { idx })) + let common = { selections: [selection.id] } - this.undo = act.selection.delete({ photo: photo.id, ...data }, { idx }) - this.redo = act.selection.restore({ photo: photo.id, ...data }, { idx }) + yield put(act.photo.selections.add({ id: photo.id, ...common }, { idx })) + + this.undo = act.selection.delete({ photo: photo.id, ...common }, { idx }) + this.redo = act.selection.restore({ photo: photo.id, ...common }, { idx }) return selection } @@ -140,6 +156,12 @@ class Save extends Command { } } +class TemplateChange extends SaveCommand { + static get ACTION() { return SELECTION.TEMPLATE.CHANGE } + get type() { return 'selection' } +} + + module.exports = { Create, @@ -147,5 +169,6 @@ module.exports = { Load, Order, Restore, - Save + Save, + TemplateChange } diff --git a/src/commands/subject.js b/src/commands/subject.js new file mode 100644 index 000000000..83685452f --- /dev/null +++ b/src/commands/subject.js @@ -0,0 +1,120 @@ +'use strict' + +const assert = require('assert') +const { empty, get, pick } = require('../common/util') +const { Command } = require('./command') +const { call, put, select } = require('redux-saga/effects') +const { getTemplateDefaultValues } = require('../selectors') +const mod = require('../models') +const act = require('../actions') + + +class SaveCommand extends Command { + get isUndo() { + return Array.isArray(this.action.payload) + } + + get type() { + return 'item' + } + + *getOriginals(ids, props) { + let [subjects, metadata] = yield select(state => [ + state[`${this.type}s`], + state.metadata + ]) + + return ids.map(id => [ + id, + get(subjects, [id, 'template']), + props.length ? pick(metadata[id], props, {}, true) : null + ]) + } + + *restore(originals) { + let { db } = this.options + let { meta } = this.action + let { type } = this + let changes = { [type]: {} } + let tmp = {} + + yield call(db.transaction, async tx => { + for (let [id, template, data] of originals) { + changes[type][id] = { template, modified: new Date(meta.now) } + + await mod.subject.update(tx, { + id, + template, + timestamp: meta.now + }) + + if (data != null) { + tmp[id] = data + await mod.metadata.update(tx, { id, data }) + } + } + }) + + if (!empty(tmp)) changes.data = tmp + + return changes + } + + *exec() { + let { db } = this.options + let { payload, meta } = this.action + let { type } = this + + if (this.isUndo) { + let changes = yield this.restore(payload) + yield put(act[type].bulk.update(changes[type])) + + if (changes.data) { + yield put(act.metadata.merge(changes.data)) + } + + } else { + let { id, property, value: template } = payload + + // Currently the only valid subject save is a template change! + assert.equal(property, 'template') + + let data = yield select(state => + getTemplateDefaultValues(state, { template })) + + let props = Object.keys(data) + let originals = yield this.getOriginals(id, props) + + + yield call(db.transaction, async tx => { + await mod.subject.update(tx, { + id, + template, + timestamp: meta.now + }) + + if (props.length) { + await mod.metadata.update(tx, { id, data }) + } + }) + + yield put(act[type].bulk.update([id, { + template, + modified: new Date(meta.now) + }])) + + if (props.length) { + yield put(act.metadata.update({ id, data })) + } + + this.undo = { + ...this.action, + payload: originals + } + } + } +} + +module.exports = { + SaveCommand +} diff --git a/src/common/ontology.js b/src/common/ontology.js index 0be0ab82c..77ec56b07 100644 --- a/src/common/ontology.js +++ b/src/common/ontology.js @@ -2,15 +2,46 @@ const { join, basename, extname } = require('path') const { createReadStream: stream } = require('fs') -const { any, empty, get, pick, titlecase } = require('./util') +const { any, empty, get, identify, pick, titlecase } = require('./util') const { Resource } = require('./res') const N3 = require('n3') -const { RDF, RDFS, DC, TERMS, SKOS, OWL, VANN } = require('../constants') +const { RDF, RDFS, DC, TERMS, SKOS, OWL, VANN, TYPE } = require('../constants') const { TEMPLATE } = require('../constants/ontology') const { readFileAsync: read, writeFileAsync: write } = require('fs') class Template { + static defaults = { + type: TYPE.ITEM, + name: '', + creator: '', + description: '', + created: null, + isProtected: false, + fields: [] + } + + static identify() { + return `https://tropy.org/v1/templates/id#${identify()}` + } + + static make(template = Template.defaults) { + return { + ...template, + id: template.id || Template.identify(), + fields: [...template.fields] + } + } + + static copy(template, mapField = Field.copy) { + return { + ...pick(template, Template.keys), + created: null, + isProtected: false, + fields: template.fields.map(mapField) + } + } + static async open(path) { return JSON.parse(await read(path)) } @@ -23,26 +54,52 @@ class Template { return { '@context': TEMPLATE.CONTEXT, '@id': data.id, - '@type': TEMPLATE.TYPE, + '@type': TYPE.TEMPLATE, 'type': data.type, 'name': data.name, 'version': data.version, 'domain': data.domain, 'creator': data.creator, 'description': data.description, - 'field': data.fields.map(field => pick(field, [ - 'property', - 'label', - 'datatype', - 'hint', - 'isRequired', - 'isConstant', - 'value' - ])) + 'field': data.fields.map(Field.copy) + } + } + + static keys = Object.keys(Template.defaults) +} + +class Field { + static defaults = { + datatype: TYPE.TEXT, + hint: '', + isConstant: false, + isRequired: false, + label: '', + property: '', + value: '' + } + + static identify() { + return Field.counter-- + } + + static make(field = Field.defaults) { + return { + id: field.id || Field.identify(), + ...field } } + + static copy(field) { + return pick(field, Field.keys) + } + + static counter = -1 + static keys = Object.keys(Field.defaults) } +Template.Field = Field + class Ontology extends Resource { static get base() { diff --git a/src/components/icons.js b/src/components/icons.js index 06a8ed6ca..49d846e78 100644 --- a/src/components/icons.js +++ b/src/components/icons.js @@ -327,6 +327,14 @@ i('Import', ( )) +i('ItemSmall', ( + + + + + +)) + i('Lift', ( diff --git a/src/components/metadata/panel.js b/src/components/metadata/panel.js index a1c5ed118..88f2e8aae 100644 --- a/src/components/metadata/panel.js +++ b/src/components/metadata/panel.js @@ -4,7 +4,6 @@ const React = require('react') const { connect } = require('react-redux') const { MetadataList } = require('./list') const { MetadataSection } = require('./section') -const { TemplateSelect } = require('../template/select') const { PopupSelect } = require('../resource/popup') const { PhotoInfo } = require('../photo/info') const { ItemInfo } = require('../item/info') @@ -18,14 +17,14 @@ const { arrayOf, bool, func, object, shape } = require('prop-types') const { getActiveSelection, + getAllTemplatesByType, getItemFields, - getItemTemplates, getPhotoFields, getPropertyList, getSelectedItemTemplate, getSelectedItems, - getSelectionFields, - getSelectedPhoto + getSelectedPhoto, + getSelectionFields } = require('../../selectors') @@ -112,12 +111,29 @@ class MetadataPanel extends React.PureComponent { this.prev(1) } - handleTemplateChange = (template, hasChanged) => { + handleItemTemplateChange = (template, hasChanged) => { if (hasChanged || this.isBulk) { - this.props.onItemSave({ + this.props.onTemplateChange('item', { id: this.props.items.map(it => it.id), - property: 'template', - value: template.id + template: template.id + }) + } + } + + handlePhotoTemplateChange = (template, hasChanged) => { + if (hasChanged) { + this.props.onTemplateChange('photo', { + id: [this.props.photo.id], + template: template.id + }) + } + } + + handleSelectionTemplateChange = (template, hasChanged) => { + if (hasChanged) { + this.props.onTemplateChange('selection', { + id: [this.props.selection.id], + template: template.id }) } } @@ -170,15 +186,13 @@ class MetadataPanel extends React.PureComponent { return !this.isEmpty && ( - + isDisabled={this.props.isDisabled} + isMixed={this.props.template.mixed} + template={this.props.template.id} + templates={this.props.templates.item} + title="panel.metadata.item" + onTemplateChange={this.handleItemTemplateChange} + onContextMenu={this.handleItemContextMenu}> + isDisabled={this.props.isDisabled} + template={this.props.photo.template} + templates={this.props.templates.photo} + title="panel.metadata.photo" + onTemplateChange={this.handlePhotoTemplateChange} + onContextMenu={this.handlePhotoContextMenu}> + isDisabled={this.props.isDisabled} + template={this.props.selection.template} + templates={this.props.templates.selection} + title="panel.metadata.selection" + onTemplateChange={this.handleSelectionTemplateChange} + onContextMenu={this.handleSelectionContextMenu}> ({ - onItemSave(...args) { - dispatch(actions.item.save(...args)) - }, - onMetadataAdd(...args) { dispatch(actions.metadata.add(...args)) }, onMetadataDelete(...args) { dispatch(actions.metadata.delete(...args)) + }, + + onTemplateChange(type, ...args) { + dispatch(actions[type].template.change(...args)) } }), null, { forwardRef: true } )(MetadataPanel) diff --git a/src/components/metadata/section.js b/src/components/metadata/section.js index 98d3b7f5b..2e01ea5ad 100644 --- a/src/components/metadata/section.js +++ b/src/components/metadata/section.js @@ -1,29 +1,52 @@ 'use strict' const React = require('react') -const { bool, func, node, number, string } = require('prop-types') const { FormattedMessage } = require('react-intl') +const { TemplateSelect } = require('../template/select') +const { noop } = require('../../common/util') const cx = require('classnames') +const { array, bool, func, node, number, string } = require('prop-types') - -const MetadataSection = - ({ children, count, onContextMenu, separator, title }) => ( -
-
- +const MetadataSection = (props) => { + let hasTemplates = !!props.templates + return ( +
+
+
- {children} + {hasTemplates && + } + {props.children}
) +} MetadataSection.propTypes = { children: node.isRequired, + isDisabled: bool, + isMixed: bool, count: number, onContextMenu: func, - separator: bool, + onTemplateChange: func.isRequired, + template: string, + templates: array, title: string.isRequired } +MetadataSection.propTypes = { + onTemplateChange: noop +} + module.exports = { MetadataSection } diff --git a/src/components/prefs/app.js b/src/components/prefs/app.js index f3eaea6c2..a64a137e2 100644 --- a/src/components/prefs/app.js +++ b/src/components/prefs/app.js @@ -1,12 +1,15 @@ 'use strict' const React = require('react') -const { PureComponent } = React -const { array, arrayOf, bool, func, shape, string } = require('prop-types') const { TemplateSelect } = require('../template/select') const { ipcRenderer: ipc } = require('electron') const { ESPER, ITEM } = require('../../constants') const { darwin } = require('../../common/os') +const { IconItemSmall, IconPhoto, IconSelection } = require('../icons') + +const { + array, arrayOf, bool, func, object, shape, string +} = require('prop-types') const { FormElement, @@ -17,7 +20,7 @@ const { } = require('../form') -class AppPrefs extends PureComponent { +class AppPrefs extends React.PureComponent { handleDebugChange() { ipc.send('cmd', 'app:toggle-debug-flag') } @@ -34,21 +37,50 @@ class AppPrefs extends PureComponent { this.props.onSettingsUpdate({ localtime }) } - handleTemplateChange = (template) => { - this.props.onSettingsUpdate({ template: template.id }) + handleItemTemplateChange = (template) => { + this.handleTemplateChange('item', template) + } + + handlePhotoTemplateChange = (template) => { + this.handleTemplateChange('photo', template) + } + + handleSelectionTemplateChange = (template) => { + this.handleTemplateChange('selection', template) + } + + handleTemplateChange(type, template) { + this.props.onSettingsUpdate({ + templates: { [type]: template.id } + }) } render() { return (
- + } isRequired - options={this.props.templates} - value={this.props.settings.template} + options={this.props.templates.item} + value={this.props.settings.templates.item} tabIndex={0} - onChange={this.handleTemplateChange}/> + onChange={this.handleItemTemplateChange}/> + } + isRequired + options={this.props.templates.photo} + value={this.props.settings.templates.photo} + tabIndex={0} + onChange={this.handlePhotoTemplateChange}/> + } + isRequired + options={this.props.templates.selection} + value={this.props.settings.templates.selection} + tabIndex={0} + onChange={this.handleSelectionTemplateChange}/>
@@ -126,7 +125,7 @@ class PrefsContainer extends PureComponent { static propTypes = { edit: object.isRequired, isFrameless: bool, - itemTemplates: array.isRequired, + templates: object.isRequired, pane: string.isRequired, settings: object.isRequired, vocab: array.isRequired, @@ -154,7 +153,7 @@ module.exports = { PrefsContainer: connect( state => ({ edit: state.edit, - itemTemplates: getItemTemplates(state), + templates: getAllTemplatesByType(state), keymap: state.keymap, pane: state.prefs.pane, project: state.project, diff --git a/src/components/select.js b/src/components/select.js index 538a9eebe..42c91e656 100644 --- a/src/components/select.js +++ b/src/components/select.js @@ -115,6 +115,7 @@ class Select extends React.Component { 'can-clear': !this.props.hideClearButton && this.state.canClearValue, 'disabled': this.state.isDisabled, 'focus': this.state.hasFocus, + 'has-icon': this.props.icon != null, 'invalid': this.state.isInvalid, 'multi': this.state.isMulti, 'open': this.state.isOpen, @@ -345,7 +346,8 @@ class Select extends React.Component { return this.state.isOpen && ( + {this.props.icon} {this.renderContent()} {this.renderInput()} {this.renderClearButton()} @@ -389,6 +392,7 @@ class Select extends React.Component { canClearByBackspace: bool, className: string, hideClearButton: bool, + icon: node, id: string, isDisabled: bool, isInputHidden: bool, diff --git a/src/components/template/editor.js b/src/components/template/editor.js index aee636c79..0ca1fea60 100644 --- a/src/components/template/editor.js +++ b/src/components/template/editor.js @@ -1,16 +1,16 @@ 'use strict' const React = require('react') -const { PureComponent } = React const { connect } = require('react-redux') const { TemplateFieldList } = require('./field-list') const { TemplateToolbar } = require('./toolbar') const { FormattedMessage } = require('react-intl') const { FormField, FormGroup, FormSelect } = require('../form') -const { identify, omit, pick } = require('../../common/util') +const { Template } = require('../../common/ontology') const { arrayOf, func, shape, string } = require('prop-types') const actions = require('../../actions') const { TYPE } = require('../../constants') +const { insert, move, remove } = require('../../common/util') const { getDatatypeList, @@ -18,33 +18,16 @@ const { getPropertyList } = require('../../selectors') -const TEMPLATE = { - name: '', - type: TYPE.ITEM, - creator: '', - description: '', - created: null, - isProtected: false, - fields: [] -} - -const defaultId = () => - `https://tropy.org/v1/templates/id#${identify()}` - -const makeTemplate = (template) => ({ - id: (template || TEMPLATE).id || defaultId(), ...(template || TEMPLATE) -}) +class TemplateEditor extends React.PureComponent { + state = Template.make() -class TemplateEditor extends PureComponent { - constructor(props) { - super(props) - this.state = makeTemplate() - } - - componentWillReceiveProps({ templates }) { - if (this.state.id != null && this.props.templates !== templates) { - this.setState(makeTemplate(templates.find(t => t.id === this.state.id))) + componentDidUpdate(props) { + if (props.templates !== this.props.templates) { + let template = this.props.templates.find(t => t.id === this.state.id) + if (template != null) { + this.handleTemplateChange(template) + } } } @@ -57,22 +40,10 @@ class TemplateEditor extends PureComponent { } handleTemplateCopy = () => { - this.setState(makeTemplate({ - ...TEMPLATE, - name: this.state.name, - type: this.state.type, - creator: this.state.creator, - description: this.state.description, - fields: this.state.fields.map((f, idx) => ({ + this.setState(Template.make({ + ...Template.copy(this.state, (field, idx) => ({ id: -(idx + 1), - ...pick(f, [ - 'property', - 'label', - 'datatype', - 'isRequired', - 'hint', - 'constant' - ]) + ...Template.Field.copy(field) })) })) } @@ -80,31 +51,25 @@ class TemplateEditor extends PureComponent { handleTemplateDelete = () => { if (this.state.id) { this.props.onDelete([this.state.id]) - this.setState(makeTemplate()) + this.setState(Template.make()) } } handleTemplateCreate = () => { - const { id, name, type, creator, description, fields } = this.state - + let { id } = this.state this.props.onCreate({ - [id]: { - id, - name, - type, - creator, - description, - fields: fields.map(field => omit(field, ['id'])) - } + [id]: { id, ...Template.copy(this.state) } }) } - handleTemplateClear = () => { - this.handleTemplateChange() - } - handleTemplateChange = (template) => { - this.setState(makeTemplate(template)) + let state = Template.make(template || undefined) + + if (state.created && !state.isProtected && state.fields.length === 0) { + state.fields.push(Template.Field.make()) + } + + this.setState(state) } handleTemplateUpdate = (template) => { @@ -118,9 +83,69 @@ class TemplateEditor extends PureComponent { } } - render() { - const { isPristine } = this + handleFieldSave = (id, data, idx) => { + if (id < 0) { + this.props.onFieldAdd({ + id: this.state.id, + field: { datatype: TYPE.TEXT, ...data } + }, { idx }) + } else { + this.props.onFieldSave({ + id: this.state.id, + field: { id, ...data } + }) + } + } + + handleFieldInsert = (field, at) => { + this.setState({ + fields: insert(this.state.fields, at, Template.Field.make()) + }) + } + handleFieldRemove = (field) => { + if (field.id < 0) { + this.setState({ + fields: remove(this.state.fields, field) + }) + } else { + this.props.onFieldRemove({ + id: this.state.id, + field: field.id + }) + } + } + + handleSortStart = () => { + this.__fields = this.state.fields + } + + handleSort = (field) => { + this.__fields = null + + if (field.id > 0) { + this.props.onFieldOrder({ + id: this.state.id, + fields: this.state.fields + .map(f => f.id) + .filter(id => id > 0) + }) + } + } + + handleSortPreview = (from, to, offset) => { + this.setState({ + fields: move(this.state.fields, from, to, offset) + }) + } + + handleSortReset = () => { + this.setState({ fields: this.__fields }) + this.__fields = null + } + + render() { + let { isPristine } = this return (
@@ -131,7 +156,6 @@ class TemplateEditor extends PureComponent { isProtected={this.state.isProtected} isPristine={this.isPristine} onChange={this.handleTemplateChange} - onClear={this.handleTemplateClear} onCopy={this.handleTemplateCopy} onDelete={this.handleTemplateDelete} onExport={this.props.onExport} @@ -203,10 +227,13 @@ class TemplateEditor extends PureComponent { datatypes={this.props.datatypes} properties={this.props.properties} isDisabled={this.state.isProtected || isPristine} - onFieldAdd={this.props.onFieldAdd} - onFieldOrder={this.props.onFieldOrder} - onFieldRemove={this.props.onFieldRemove} - onFieldSave={this.props.onFieldSave}/> + onInsert={this.handleFieldInsert} + onRemove={this.handleFieldRemove} + onSave={this.handleFieldSave} + onSort={this.handleSort} + onSortPreview={this.handleSortPreview} + onSortReset={this.handleSortReset} + onSortStart={this.handleSortStart}/>
) @@ -236,10 +263,11 @@ class TemplateEditor extends PureComponent { } static defaultProps = { - types: [TYPE.ITEM, TYPE.PHOTO] + types: [TYPE.ITEM, TYPE.PHOTO, TYPE.SELECTION] } } + module.exports = { TemplateEditor: connect( state => ({ diff --git a/src/components/template/field-list.js b/src/components/template/field-list.js index f2ab5ef6f..96d87695d 100644 --- a/src/components/template/field-list.js +++ b/src/components/template/field-list.js @@ -1,154 +1,32 @@ 'use strict' const React = require('react') -const { PureComponent } = React const { TemplateField } = require('./field') -const { insert, move, remove } = require('../../common/util') -const { arrayOf, bool, func, object, shape, string } = require('prop-types') -const { TEXT } = require('../../constants/type') - - -let tmpId = -1 - -const newField = () => ({ - id: tmpId--, - value: '', - hint: '', - property: '', - datatype: TEXT, - isConstant: false, - isRequired: false -}) - -const updateFields = (props) => ( - props.isDisabled || !props.template ? - props.fields : - props.fields.length > 0 ? - [...props.fields] : - [newField()] -) - - -class TemplateFieldList extends PureComponent { - constructor(props) { - super(props) - - this.state = { - fields: updateFields(props) - } - } - - componentWillReceiveProps(props) { - if (props.fields !== this.props.fields) { - this.setState({ - fields: updateFields(props) - }) - } - } - - handleFieldSave = (id, data, idx) => { - if (id < 0) { - this.props.onFieldAdd({ - id: this.props.template, - field: { datatype: TEXT, ...data } - }, { idx }) - } else { - this.props.onFieldSave({ - id: this.props.template, - field: { id, ...data } - }) - } - } - - handleFieldInsert = (field, at) => { - this.setState({ - fields: insert(this.state.fields, at, newField()) - }) - } - - handleFieldRemove = (field) => { - if (field.id < 0) { - this.setState({ - fields: remove(this.state.fields, field) - }) - } else { - this.props.onFieldRemove({ - id: this.props.template, - field: field.id - }) - } - } - - handleSortStart = () => { - this.originalFields = this.state.fields - } - - handleSort = (field) => { - this.originalFields = null - - if (field.id > 0) { - this.props.onFieldOrder({ - id: this.props.template, - fields: this.state.fields - .map(f => f.id) - .filter(id => id > 0) - }) - } - } - - handleSortPreview = (from, to, offset) => { - this.setState({ - fields: move(this.state.fields, from, to, offset) - }) - } - - handleSortReset = () => { - this.setState({ fields: this.originalFields }) - this.originalFields = null - } - - render() { - if (this.props.template == null) return - const isSingle = this.state.fields.length === 1 - - return ( -
    - {this.state.fields.map((field, idx) => - )} -
- ) - } +const { arrayOf, number, shape, string } = require('prop-types') + +const TemplateFieldList = ({ fields, template, ...props }) => { + if (template == null) return + let isSingle = fields.length === 1 + + return ( +
    + {fields.map((field, idx) => + )} +
+ ) +} - static propTypes = { - isDisabled: bool, - template: string.isRequired, - fields: arrayOf(object).isRequired, - datatypes: arrayOf(shape({ - id: string.isRequired - })).isRequired, - properties: arrayOf(shape({ - id: string.isRequired - })).isRequired, - onFieldAdd: func.isRequired, - onFieldOrder: func.isRequired, - onFieldRemove: func.isRequired, - onFieldSave: func.isRequired, - } +TemplateFieldList.propTypes = { + fields: arrayOf(shape({ + id: number.isRequired + })).isRequired, + template: string.isRequired, } module.exports = { diff --git a/src/components/template/select.js b/src/components/template/select.js index 598f3cec1..c060b2e40 100644 --- a/src/components/template/select.js +++ b/src/components/template/select.js @@ -1,14 +1,13 @@ 'use strict' const React = require('react') -const { PureComponent } = React const { Select } = require('../select') const { FormattedMessage } = require('react-intl') const collate = require('../../collate') -const { bool, array, func, number, string } = require('prop-types') +const { bool, array, func, node, number, string } = require('prop-types') const cx = require('classnames') -class TemplateSelect extends PureComponent { +class TemplateSelect extends React.PureComponent { get placeholder() { return this.props.placeholder != null && @@ -24,6 +23,7 @@ class TemplateSelect extends PureComponent { render() { let { isMixed, ...props } = this.props + return (