Fix 2.4.2 upgrade notes formatting refs #19330
[arvados.git] / apps / workbench / app / assets / javascripts / components / edit_tags.js
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 window.SimpleInput = {
6     view: function(vnode) {
7         return m('input.form-control', {
8             style: {
9                 width: '100%',
10             },
11             type: 'text',
12             placeholder: 'Add ' + vnode.attrs.placeholder,
13             value: vnode.attrs.value,
14             onchange: function() {
15                 if (this.value != '') {
16                     vnode.attrs.value(this.value)
17                 }
18             },
19         }, vnode.attrs.value)
20     },
21 }
22
23 window.SelectOrAutocomplete = {
24     view: function(vnode) {
25         return m('input.form-control', {
26             style: {
27                 width: '100%'
28             },
29             type: 'text',
30             value: vnode.attrs.value,
31             placeholder: (vnode.attrs.create ? 'Add or select ': 'Select ') + vnode.attrs.placeholder,
32         }, vnode.attrs.value)
33     },
34     oncreate: function(vnode) {
35         vnode.state.awesomplete = new Awesomplete(vnode.dom, {
36             list: vnode.attrs.options,
37             minChars: 0,
38             maxItems: 1000000,
39             autoFirst: true,
40             sort: false,
41         })
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)
47         })
48         $(vnode.dom).on('change', function(event) {
49             if (!vnode.state.create && !(this.value in vnode.state.options)) {
50                 this.value = vnode.attrs.value()
51             } else {
52                 if (vnode.attrs.value() !== this.value) {
53                     vnode.attrs.value(this.value)
54                 }
55             }
56         })
57         $(vnode.dom).on('focusin', function(event) {
58             if (this.value === '') {
59                 vnode.state.awesomplete.evaluate()
60                 vnode.state.awesomplete.open()
61             }
62         })
63     },
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
68     },
69 }
70
71 window.TagEditorRow = {
72     view: function(vnode) {
73         var nameOpts = Object.keys(vnode.attrs.vocabulary().tags)
74         var valueOpts = []
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
79         } else {
80             // Name options list
81             if (vnode.attrs.name() != '' && !(vnode.attrs.name() in vnode.attrs.vocabulary().tags)) {
82                 nameOpts.push(vnode.attrs.name())
83             }
84             // Value options list
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
88             }
89         }
90         return m('tr', [
91             // Erase tag
92             m('td', [
93                 vnode.attrs.editMode &&
94                 m('div.text-center', m('a.btn.btn-default.btn-sm', {
95                     style: {
96                         align: 'center'
97                     },
98                     onclick: function(e) { vnode.attrs.removeTag() }
99                 }, m('i.fa.fa-fw.fa-trash-o')))
100             ]),
101             // Tag key
102             m('td', [
103                 vnode.attrs.editMode ?
104                 m('div', {key: 'key'}, [
105                     m(inputComponent, {
106                         options: nameOpts,
107                         value: vnode.attrs.name,
108                         // Allow any tag name unless 'strict' is set to true.
109                         create: !vnode.attrs.vocabulary().strict,
110                         placeholder: 'key',
111                     })
112                 ])
113                 : vnode.attrs.name
114             ]),
115             // Tag value
116             m('td', [
117                 vnode.attrs.editMode ?
118                 m('div', {key: 'value'}, [
119                     m(inputComponent, {
120                         options: valueOpts,
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,
130                     })
131                 ])
132                 : vnode.attrs.value
133             ])
134         ])
135     }
136 }
137
138 window.TagEditorTable = {
139     view: function(vnode) {
140         return m('table.table.table-condensed.table-justforlayout', [
141             m('colgroup', [
142                 m('col', {width:'5%'}),
143                 m('col', {width:'25%'}),
144                 m('col', {width:'70%'}),
145             ]),
146             m('thead', [
147                 m('tr', [
148                     m('th'),
149                     m('th', 'Key'),
150                     m('th', 'Value'),
151                 ])
152             ]),
153             m('tbody', [
154                 vnode.attrs.tags.length > 0
155                 ? vnode.attrs.tags.map(function(tag, idx) {
156                     return m(TagEditorRow, {
157                         key: tag.rowKey,
158                         removeTag: function() {
159                             vnode.attrs.tags.splice(idx, 1)
160                             vnode.attrs.dirty(true)
161                         },
162                         editMode: vnode.attrs.editMode,
163                         name: tag.name,
164                         value: tag.value,
165                         vocabulary: vnode.attrs.vocabulary
166                     })
167                 })
168                 : m('tr', m('td[colspan=3]', m('center', 'Loading tags...')))
169             ]),
170         ])
171     }
172 }
173
174 var uniqueID = 1
175
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)
184     },
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) {
189             return
190         }
191         found = false
192         vnode.state.tags.forEach(function(tag) {
193             if (tag.name == tagName) {
194                 tag.value = vnode.state.saved_tags[tagName]
195                 found = true
196             }
197         })
198         if (!found) {
199             vnode.state.tags.pop() // Remove the last empty row
200             vnode.state.appendTag(vnode, tagName, savedTagValue)
201         }
202     },
203     oninit: function(vnode) {
204         vnode.state.sessionDB = new SessionDB()
205         // Get vocabulary
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
216         // Get tags
217         vnode.state.sessionDB.request(
218             vnode.state.sessionDB.loadLocal(),
219             'arvados/v1/' + vnode.attrs.targetController,
220             {
221                 data: {
222                     filters: JSON.stringify([['uuid', '=', vnode.attrs.targetUuid]]),
223                     select: JSON.stringify(['properties'])
224                 },
225             }).then(function(obj) {
226                 if (obj.items.length == 1) {
227                     o = obj.items[0]
228                     Object.keys(o.properties).forEach(function(k) {
229                         vnode.state.appendTag(vnode, k, o.properties[k])
230                     })
231                     if (vnode.state.editMode) {
232                         vnode.state.appendTag(vnode, '', '')
233                     }
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, '', '')
243                         }
244                     })
245                 }
246             }
247         )
248     },
249     view: function(vnode) {
250         return [
251             vnode.state.editMode &&
252             m('div.pull-left', [
253                 m('a.btn.btn-primary.btn-sm' + (vnode.state.dirty() ? '' : '.disabled'), {
254                     style: {
255                         margin: '10px 0px'
256                     },
257                     onclick: function(e) {
258                         var tags = {}
259                         vnode.state.tags.forEach(function(t) {
260                             // Only ignore tags with empty key
261                             if (t.name() != '') {
262                                 tags[t.name()] = t.value()
263                             }
264                         })
265                         vnode.state.sessionDB.request(
266                             vnode.state.sessionDB.loadLocal(),
267                             vnode.state.objPath, {
268                                 method: 'PUT',
269                                 data: {properties: JSON.stringify(tags)}
270                             }
271                         ).then(function(v) {
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)
284                                     }
285                                 })
286                                 if (protected_props.length > 0) {
287                                     errMsg = "Protected properties cannot be updated: " + protected_props.join(', ')
288                                 } else {
289                                     errMsg = errors.join(', ')
290                                 }
291                             } else {
292                                 errMsg = err
293                             }
294                             vnode.state.error(errMsg)
295                         })
296                     }
297                 }, vnode.state.dirty() ? ' Save changes ' : ' Saved '),
298                 m('span', {
299                     style: {
300                         color: '#ff0000',
301                         margin: '0px 10px'
302                     }
303                 }, [ vnode.state.error() ])
304             ]),
305             // Tags table
306             m(TagEditorTable, {
307                 editMode: vnode.state.editMode,
308                 tags: vnode.state.tags,
309                 vocabulary: vnode.state.vocabulary,
310                 dirty: vnode.state.dirty
311             })
312         ]
313     },
314 }