From e59f7a85cbe69026d33be0584ba971c622dd3d6a Mon Sep 17 00:00:00 2001 From: David Manthey Date: Mon, 12 Jun 2023 09:08:42 -0400 Subject: [PATCH] Changes to make it easier to inherit the metadata widget --- CHANGELOG.md | 1 + docs/girder_config_options.rst | 43 +++++++ .../web_client/views/metadataWidget.js | 107 ++++++++++++++---- 3 files changed, 132 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46b06d932..390b7a9bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Added an internal field to report populated tile levels in some sources ([#1197](../../pull/1197), [#1199](../../pull/1199)) - Allow specifying an empty style dict ([#1200](../../pull/1200)) - Allow rounding histogram bin edges and reducing bin counts ([#1201](../../pull/1201)) +- Allow configuring which metadata can be added to items ([#1202](../../pull/1202)) ### Changes - Change how extensions and fallback priorities interact ([#1192](../../pull/1192)) diff --git a/docs/girder_config_options.rst b/docs/girder_config_options.rst index b89d27697..5cf0b659d 100644 --- a/docs/girder_config_options.rst +++ b/docs/girder_config_options.rst @@ -45,6 +45,9 @@ The yaml file has the following structure: .large_image_config.yaml ~~~~~~~~~~~~~~~~~~~~~~~~ +Items Lists +........... + This is used to specify how items appear in item lists. There are two settings, one for folders in the main Girder UI and one for folders in dialogs (such as when browsing in the file dialog). :: @@ -128,6 +131,46 @@ This is used to specify how items appear in item lists. There are two settings, If there are no large images in a folder, none of the image columns will appear. +Item Metadata +............. + +By default, item metadata can contain any keys and values. These can be given better titles and restricted in their data types. + +:: + + --- + # If present, offer to add these specific keys and restrict their datatypes + itemMetadata: + - + # value is the key name within the metadata + value: stain + # title is the displayed titles + title: Stain + # description is used as both a tooltip and as placeholder text + description: Staining method + # if required is true, the delete button does not appear + required: true + # If a regex is specified, the value must match + # regex: '^(Eosin|H&E|Other)$' + # If an enum is specified, the value is set via a dropdown select box + enum: + - Eosin + - H&E + - Other + # If a default is specified, when the value is created, it will show + # this value in the control + default: H&E + - + value: rating + # type can be "number", "integer", or "text" (default) + type: number + # minimum and maximum are inclusive + minimum: 0 + maximum: 10 + # Exclusive values can be specified instead + # exclusiveMinimum: 0 + # exclusiveMaximum: 10 + Editing Configuration Files --------------------------- diff --git a/girder/girder_large_image/web_client/views/metadataWidget.js b/girder/girder_large_image/web_client/views/metadataWidget.js index b5cc71633..cbf49f89d 100644 --- a/girder/girder_large_image/web_client/views/metadataWidget.js +++ b/girder/girder_large_image/web_client/views/metadataWidget.js @@ -84,8 +84,8 @@ var MetadatumWidget = View.extend({ var newMode = this.parentView.modes[to]; if (_.has(newMode, 'validation') && - _.has(newMode.validation, 'from') && - _.has(newMode.validation.from, from)) { + _.has(newMode.validation, 'from') && + _.has(newMode.validation.from, from)) { var validate = newMode.validation.from[from][0]; var msg = newMode.validation.from[from][1]; @@ -106,7 +106,7 @@ var MetadatumWidget = View.extend({ var fromEditorMode = (existingEditor instanceof JsonMetadatumEditWidget) ? 'json' : 'simple'; var newValue = (overrides || {}).value || existingEditor.$el.attr('g-value'); if (!this._validate(fromEditorMode, newEditorMode, newValue)) { - return; + return false; } var row = existingEditor.$el; @@ -246,7 +246,7 @@ var MetadatumEditWidget = View.extend({ confirmCallback: () => { this.item.removeMetadata(this.key, function () { metadataList.remove(); - // TODO: trigger an event? + this.parentView.parentView.trigger('li-metadata-widget-update', {}); }, null, { field: this.fieldName, path: this.apiPath @@ -328,7 +328,8 @@ var MetadatumEditWidget = View.extend({ } else { this.parentView.mode = 'simple'; } - // TODO: trigger an event + // event to re-render metadata panel header when metadata is edited + this.parentView.parentView.trigger('li-metadata-widget-update', {}); this.parentView.render(); this.newDatum = false; @@ -354,8 +355,7 @@ var MetadatumEditWidget = View.extend({ return false; } getMetadataRecord(this.item, this.fieldName)[tempKey] = tempValue; - // TODO: this.parentView.parentView.render(); - return; + this.parentView.parentView.render(); } this.item.addMetadata(tempKey, tempValue, saveCallback, errorCallback, { field: this.fieldName, @@ -377,7 +377,7 @@ var MetadatumEditWidget = View.extend({ } delete getMetadataRecord(this.item, this.fieldName)[this.key]; getMetadataRecord(this.item, this.fieldName)[tempKey] = tempValue; - // TODO: this.parentView.parentView.render(); + this.parentView.parentView.render(); return; } this.item.editMetadata(tempKey, this.key, tempValue, saveCallback, errorCallback, { @@ -413,7 +413,7 @@ var JsonMetadatumEditWidget = MetadatumEditWidget.extend({ save: function (event) { try { - MetadatumEditWidget.prototype.save.call( + return MetadatumEditWidget.prototype.save.call( this, event, this.editor.get()); } catch (err) { events.trigger('g:alert', { @@ -449,9 +449,12 @@ var JsonMetadatumEditWidget = MetadatumEditWidget.extend({ }); wrap(MetadataWidget, 'initialize', function (initialize, settings) { - const result = initialize.call(this, settings); + try { + initialize.call(this, settings); + } catch (err) { + } this.noSave = settings.noSave; - if (this.item.get('_modelType') === 'item') { + if (this.item && this.item.get('_modelType') === 'item') { largeImageConfig.getConfigFile(this.item.get('folderId')).done((val) => { this._limetadata = (val || {}).itemMetadata; if (this._limetadata) { @@ -461,11 +464,21 @@ wrap(MetadataWidget, 'initialize', function (initialize, settings) { } else { this._limetadata = null; } - return result; }); wrap(MetadataWidget, 'render', function (render) { - var metaDict = this.item.get(this.fieldName) || {}; + let metaDict; + if (this.item.get(this.fieldName)) { + metaDict = this.item.get(this.fieldName) || {}; + } else if (this.item[this.fieldName]) { + metaDict = this.item[this.fieldName] || {}; + } else { + const fieldParts = this.fieldName.split('.'); + metaDict = this.item.get(fieldParts[0]) || {}; + fieldParts.slice(1).forEach((part) => { + metaDict = metaDict[part] || {}; + }); + } var metaKeys = Object.keys(metaDict); metaKeys.sort(localeSort); if (this._limetadata) { @@ -485,15 +498,16 @@ wrap(MetadataWidget, 'render', function (render) { return origOrder.indexOf(a) - origOrder.indexOf(b); }); } - - // Metadata header - this.$el.html((this.MetadataWidgetTemplate || MetadataWidgetTemplate)({ + this._sortedMetaKeys = metaKeys; + this._renderedMetaDict = metaDict; + const contents = (this.MetadataWidgetTemplate || MetadataWidgetTemplate)({ item: this.item, title: this.title, accessLevel: this.accessLevel, AccessType: AccessType, limetadata: this._limetadata - })); + }); + this._renderHeader(contents); // Append each metadatum _.each(metaKeys, function (metaKey) { @@ -506,6 +520,7 @@ wrap(MetadataWidget, 'render', function (render) { fieldName: this.fieldName, apiPath: this.apiPath, limetadata: this._limetadata, + noSave: this.noSave, onMetadataEdited: this.onMetadataEdited, onMetadataAdded: this.onMetadataAdded }).render().$el); @@ -515,6 +530,17 @@ wrap(MetadataWidget, 'render', function (render) { }); wrap(MetadataWidget, 'setItem', function (setItem, item) { + if (item !== this.item) { + this._limetadata = null; + if (item && item.get('_modelType') === 'item') { + largeImageConfig.getConfigFile(item.get('folderId')).done((val) => { + this._limetadata = (val || {}).itemMetadata; + if (this._limetadata) { + this.render(); + } + }); + } + } setItem.call(this, item); this.item.on('g:changed', function () { this.render(); @@ -551,6 +577,43 @@ MetadataWidget.prototype.getModeFromValue = function (value, key) { return _.isString(value) ? 'simple' : 'json'; }; +MetadataWidget.prototype.addMetadata = function (evt, mode) { + var EditWidget = this.modes[mode].editor; + var value = (mode === 'json') ? '{}' : ''; + + var widget = new MetadatumWidget({ + className: 'g-widget-metadata-row editing', + mode: mode, + key: '', + value: value, + item: this.item, + fieldName: this.fieldName, + noSave: this.noSave, + apiPath: this.apiPath, + accessLevel: this.accessLevel, + parentView: this, + onMetadataEdited: this.onMetadataEdited, + onMetadataAdded: this.onMetadataAdded + }); + widget.$el.appendTo(this.$('.g-widget-metadata-container')); + + new EditWidget({ + item: this.item, + key: '', + value: value, + fieldName: this.fieldName, + noSave: this.noSave, + apiPath: this.apiPath, + accessLevel: this.accessLevel, + newDatum: true, + parentView: widget, + onMetadataEdited: this.onMetadataEdited, + onMetadataAdded: this.onMetadataAdded + }) + .render() + .$el.appendTo(widget.$el); +}; + MetadataWidget.prototype.addMetadataByKey = function (evt) { const key = $(evt.target).attr('metadata-key'); // if this key already exists, just go to editing it @@ -586,6 +649,7 @@ MetadataWidget.prototype.addMetadataByKey = function (evt) { apiPath: this.apiPath, accessLevel: this.accessLevel, newDatum: true, + noSave: this.noSave, parentView: widget, limetadata: this._limetadata, onMetadataEdited: this.onMetadataEdited, @@ -595,9 +659,14 @@ MetadataWidget.prototype.addMetadataByKey = function (evt) { .$el.appendTo(widget.$el); }; -export default { +MetadataWidget.prototype._renderHeader = function (contents) { + this.$el.html(contents); +}; + +export { MetadataWidget, MetadatumWidget, MetadatumEditWidget, - JsonMetadatumEditWidget + JsonMetadatumEditWidget, + liMetadataKeyEntry };