1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
6 view: function(vnode) {
7 return m('input.form-control', {
12 placeholder: 'Add ' + vnode.attrs.placeholder,
13 value: vnode.attrs.value,
14 onchange: function() {
15 if (this.value != '') {
16 vnode.attrs.value(this.value)
23 window.SelectOrAutocomplete = {
24 view: function(vnode) {
25 return m('input.form-control', {
30 value: vnode.attrs.value,
31 placeholder: (vnode.attrs.create ? 'Add or select ': 'Select ') + vnode.attrs.placeholder,
34 oncreate: function(vnode) {
35 vnode.state.awesomplete = new Awesomplete(vnode.dom, {
36 list: vnode.attrs.options,
42 vnode.state.create = vnode.attrs.create
43 vnode.state.options = vnode.attrs.options
44 // Option is selected from the list.
45 $(vnode.dom).on('awesomplete-selectcomplete', function(event) {
46 vnode.attrs.value(this.value)
48 $(vnode.dom).on('change', function(event) {
49 if (!vnode.state.create && !(this.value in vnode.state.options)) {
50 this.value = vnode.attrs.value()
52 if (vnode.attrs.value() !== this.value) {
53 vnode.attrs.value(this.value)
57 $(vnode.dom).on('focusin', function(event) {
58 if (this.value === '') {
59 vnode.state.awesomplete.evaluate()
60 vnode.state.awesomplete.open()
64 onupdate: function(vnode) {
65 vnode.state.awesomplete.list = vnode.attrs.options
66 vnode.state.create = vnode.attrs.create
67 vnode.state.options = vnode.attrs.options
71 window.TagEditorRow = {
72 view: function(vnode) {
73 var nameOpts = Object.keys(vnode.attrs.vocabulary().tags)
75 var inputComponent = SelectOrAutocomplete
76 if (nameOpts.length === 0) {
77 // If there's not vocabulary defined, switch to a simple input field
78 inputComponent = SimpleInput
81 if (vnode.attrs.name() != '' && !(vnode.attrs.name() in vnode.attrs.vocabulary().tags)) {
82 nameOpts.push(vnode.attrs.name())
85 if (vnode.attrs.name() in vnode.attrs.vocabulary().tags &&
86 'values' in vnode.attrs.vocabulary().tags[vnode.attrs.name()]) {
87 valueOpts = vnode.attrs.vocabulary().tags[vnode.attrs.name()].values
93 vnode.attrs.editMode &&
94 m('div.text-center', m('a.btn.btn-default.btn-sm', {
98 onclick: function(e) { vnode.attrs.removeTag() }
99 }, m('i.fa.fa-fw.fa-trash-o')))
103 vnode.attrs.editMode ?
104 m('div', {key: 'key'}, [
107 value: vnode.attrs.name,
108 // Allow any tag name unless 'strict' is set to true.
109 create: !vnode.attrs.vocabulary().strict,
117 vnode.attrs.editMode ?
118 m('div', {key: 'value'}, [
121 value: vnode.attrs.value,
122 placeholder: 'value',
123 // Allow any value on tags not listed on the vocabulary.
124 // Allow any value on tags without values, or the ones
125 // that aren't explicitly declared to be strict.
126 create: !(vnode.attrs.name() in vnode.attrs.vocabulary().tags)
127 || !vnode.attrs.vocabulary().tags[vnode.attrs.name()].values
128 || vnode.attrs.vocabulary().tags[vnode.attrs.name()].values.length === 0
129 || !vnode.attrs.vocabulary().tags[vnode.attrs.name()].strict,
138 window.TagEditorTable = {
139 view: function(vnode) {
140 return m('table.table.table-condensed.table-justforlayout', [
142 m('col', {width:'5%'}),
143 m('col', {width:'25%'}),
144 m('col', {width:'70%'}),
154 vnode.attrs.tags.length > 0
155 ? vnode.attrs.tags.map(function(tag, idx) {
156 return m(TagEditorRow, {
158 removeTag: function() {
159 vnode.attrs.tags.splice(idx, 1)
160 vnode.attrs.dirty(true)
162 editMode: vnode.attrs.editMode,
165 vocabulary: vnode.attrs.vocabulary
168 : m('tr', m('td[colspan=3]', m('center', 'Loading tags...')))
176 window.TagEditorApp = {
177 appendTag: function(vnode, name, value) {
178 var tag = {name: m.stream(name), value: m.stream(value), rowKey: uniqueID++}
179 vnode.state.tags.push(tag)
180 // Set dirty flag when any of name/value changes to non empty string
181 tag.name.map(function() { vnode.state.dirty(true) })
182 tag.value.map(function() { vnode.state.dirty(true) })
183 tag.name.map(m.redraw)
185 fixTag: function(vnode, tagName) {
186 // Recover tag if deleted, recover its value if modified
187 savedTagValue = vnode.state.saved_tags[tagName]
188 if (savedTagValue === undefined) {
192 vnode.state.tags.forEach(function(tag) {
193 if (tag.name == tagName) {
194 tag.value = vnode.state.saved_tags[tagName]
199 vnode.state.tags.pop() // Remove the last empty row
200 vnode.state.appendTag(vnode, tagName, savedTagValue)
203 oninit: function(vnode) {
204 vnode.state.sessionDB = new SessionDB()
206 vnode.state.vocabulary = m.stream({'strict':false, 'tags':{}})
207 var vocabularyTimestamp = parseInt(Date.now() / 300000) // Bust cache every 5 minutes
208 m.request('/vocabulary.json?v=' + vocabularyTimestamp).then(vnode.state.vocabulary)
209 vnode.state.editMode = vnode.attrs.targetEditable
210 vnode.state.tags = []
211 vnode.state.saved_tags = {}
212 vnode.state.dirty = m.stream(false)
213 vnode.state.dirty.map(m.redraw)
214 vnode.state.error = m.stream('')
215 vnode.state.objPath = 'arvados/v1/' + vnode.attrs.targetController + '/' + vnode.attrs.targetUuid
217 vnode.state.sessionDB.request(
218 vnode.state.sessionDB.loadLocal(),
219 'arvados/v1/' + vnode.attrs.targetController,
222 filters: JSON.stringify([['uuid', '=', vnode.attrs.targetUuid]]),
223 select: JSON.stringify(['properties'])
225 }).then(function(obj) {
226 if (obj.items.length == 1) {
228 Object.keys(o.properties).forEach(function(k) {
229 vnode.state.appendTag(vnode, k, o.properties[k])
231 if (vnode.state.editMode) {
232 vnode.state.appendTag(vnode, '', '')
234 // Data synced with server, so dirty state should be false
235 vnode.state.dirty(false)
236 vnode.state.saved_tags = o.properties
237 // Add new tag row when the last one is completed
238 vnode.state.dirty.map(function() {
239 if (!vnode.state.editMode) { return }
240 lastTag = vnode.state.tags.slice(-1).pop()
241 if (lastTag === undefined || (lastTag.name() !== '' || lastTag.value() !== '')) {
242 vnode.state.appendTag(vnode, '', '')
249 view: function(vnode) {
251 vnode.state.editMode &&
253 m('a.btn.btn-primary.btn-sm' + (vnode.state.dirty() ? '' : '.disabled'), {
257 onclick: function(e) {
259 vnode.state.tags.forEach(function(t) {
260 // Only ignore tags with empty key
261 if (t.name() != '') {
262 tags[t.name()] = t.value()
265 vnode.state.sessionDB.request(
266 vnode.state.sessionDB.loadLocal(),
267 vnode.state.objPath, {
269 data: {properties: JSON.stringify(tags)}
272 vnode.state.dirty(false)
273 vnode.state.error('')
274 vnode.state.saved_tags = tags
275 }).catch(function(err) {
276 if (err.errors !== undefined) {
277 var re = /protected\ property/i
278 var protected_props = []
279 err.errors.forEach(function(error) {
280 if (re.test(error)) {
281 prop = error.split(':')[1].trim()
282 vnode.state.fixTag(vnode, prop)
283 protected_props.push(prop)
286 if (protected_props.length > 0) {
287 errMsg = "Protected properties cannot be updated: " + protected_props.join(', ')
289 errMsg = errors.join(', ')
294 vnode.state.error(errMsg)
297 }, vnode.state.dirty() ? ' Save changes ' : ' Saved '),
303 }, [ vnode.state.error() ])
307 editMode: vnode.state.editMode,
308 tags: vnode.state.tags,
309 vocabulary: vnode.state.vocabulary,
310 dirty: vnode.state.dirty