12479: Allow the user to edit the currently accepted value on a
[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: vnode.attrs.placeholder,
13             value: vnode.attrs.value,
14             onchange: function() {
15                 console.log(this.value)
16                 if (this.value != '') {
17                     vnode.attrs.value(this.value)
18                 }
19             },
20         }, vnode.attrs.value)
21     },
22     oncreate: function(vnode) {
23         if (vnode.attrs.setFocus) {
24             vnode.dom.focus()
25         }
26     }
27 }
28
29 window.SelectOrAutocomplete = {
30     onFocus: function(vnode) {
31         // Allow the user to edit an already entered value by removing it
32         // and filling the input field with the same text
33         activeSelect = vnode.state.selectized[0].selectize
34         value = activeSelect.getValue()
35         if (value.length > 0) {
36             activeSelect.clear(silent = true)
37             activeSelect.setTextboxValue(value)
38         }
39     },
40     view: function(vnode) {
41         return m("input", {
42             style: {
43                 width: '100%'
44             },
45             type: 'text',
46             value: vnode.attrs.value
47         }, vnode.attrs.value)
48     },
49     oncreate: function(vnode) {
50         vnode.state.selectized = $(vnode.dom).selectize({
51             labelField: 'value',
52             valueField: 'value',
53             searchField: 'value',
54             sortField: 'value',
55             persist: false,
56             hideSelected: true,
57             openOnFocus: false,
58             createOnBlur: true,
59             maxItems: 1,
60             placeholder: vnode.attrs.placeholder,
61             create: vnode.attrs.create ? function(input) {
62                 return {value: input}
63             } : false,
64             items: [vnode.attrs.value()],
65             options: vnode.attrs.options.map(function(option) {
66                 return {value: option}
67             }),
68             onChange: function(val) {
69                 if (val != '') {
70                     vnode.attrs.value(val)
71                 }
72             },
73             onFocus: function() {
74                 vnode.state.onFocus(vnode)
75             }
76         })
77         if (vnode.attrs.setFocus) {
78             vnode.state.selectized[0].selectize.focus()
79         }
80     }
81 }
82
83 window.TagEditorRow = {
84     view: function(vnode) {
85         var nameOpts = Object.keys(vnode.attrs.vocabulary().tags)
86         var valueOpts = []
87         var inputComponent = SelectOrAutocomplete
88         if (nameOpts.length === 0) {
89             // If there's not vocabulary defined, switch to a simple input field
90             inputComponent = SimpleInput
91         } else {
92             // Name options list
93             if (vnode.attrs.name() != '' && !(vnode.attrs.name() in vnode.attrs.vocabulary().tags)) {
94                 nameOpts.push(vnode.attrs.name())
95             }
96             // Value options list
97             if (vnode.attrs.name() in vnode.attrs.vocabulary().tags &&
98                 'values' in vnode.attrs.vocabulary().tags[vnode.attrs.name()]) {
99                     valueOpts = vnode.attrs.vocabulary().tags[vnode.attrs.name()].values
100             }
101             if (vnode.attrs.value() != '') {
102                 valueOpts.push(vnode.attrs.value())
103             }
104         }
105         return m("tr", [
106             // Erase tag
107             m("td", [
108                 vnode.attrs.editMode &&
109                 m('div.text-center', m('a.btn.btn-default.btn-sm', {
110                     style: {
111                         align: 'center'
112                     },
113                     onclick: function(e) { vnode.attrs.removeTag() }
114                 }, m('i.fa.fa-fw.fa-trash-o')))
115             ]),
116             // Tag name
117             m("td", [
118                 vnode.attrs.editMode ?
119                 m("div", {key: 'name-'+vnode.attrs.name()},[
120                     m(inputComponent, {
121                         options: nameOpts,
122                         value: vnode.attrs.name,
123                         // Allow any tag name unless "strict" is set to true.
124                         create: !vnode.attrs.vocabulary().strict,
125                         placeholder: 'new tag',
126                         // Focus on tag name field when adding a new tag that's
127                         // not the first one.
128                         setFocus: vnode.attrs.name() === ''
129                     })
130                 ])
131                 : vnode.attrs.name
132             ]),
133             // Tag value
134             m("td", [
135                 vnode.attrs.editMode ?
136                 m("div", {key: 'value-'+vnode.attrs.name()}, [
137                     m(inputComponent, {
138                         options: valueOpts,
139                         value: vnode.attrs.value,
140                         placeholder: 'new value',
141                         // Allow any value on tags not listed on the vocabulary.
142                         // Allow any value on tags without values, or the ones
143                         // that aren't explicitly declared to be strict.
144                         create: !(vnode.attrs.name() in vnode.attrs.vocabulary().tags)
145                             || !vnode.attrs.vocabulary().tags[vnode.attrs.name()].values
146                             || vnode.attrs.vocabulary().tags[vnode.attrs.name()].values.length === 0
147                             || !vnode.attrs.vocabulary().tags[vnode.attrs.name()].strict,
148                         // Focus on tag value field when new tag name is set
149                         setFocus: vnode.attrs.name() !== '' && vnode.attrs.value() === ''
150                     })
151                 ])
152                 : vnode.attrs.value
153             ])
154         ])
155     }
156 }
157
158 window.TagEditorTable = {
159     view: function(vnode) {
160         return m("table.table.table-condensed.table-justforlayout", [
161             m("colgroup", [
162                 m("col", {width:"5%"}),
163                 m("col", {width:"25%"}),
164                 m("col", {width:"70%"}),
165             ]),
166             m("thead", [
167                 m("tr", [
168                     m("th"),
169                     m("th", "Key"),
170                     m("th", "Value"),
171                 ])
172             ]),
173             m("tbody", [
174                 vnode.attrs.tags.length > 0
175                 ? vnode.attrs.tags.map(function(tag, idx) {
176                     return m(TagEditorRow, {
177                         key: idx,
178                         removeTag: function() {
179                             vnode.attrs.tags.splice(idx, 1)
180                             vnode.attrs.dirty(true)
181                         },
182                         editMode: vnode.attrs.editMode,
183                         name: tag.name,
184                         value: tag.value,
185                         vocabulary: vnode.attrs.vocabulary
186                     })
187                 })
188                 : m("tr", m("td[colspan=3]", m("center","loading tags...")))
189             ]),
190         ])
191     }
192 }
193
194 window.TagEditorApp = {
195     appendTag: function(vnode, name, value) {
196         var tag = {name: m.stream(name), value: m.stream(value)}
197         vnode.state.tags.push(tag)
198         // Set dirty flag when any of name/value changes to non empty string
199         tag.name.map(function(v) {
200             if (v !== '') {
201                 vnode.state.dirty(true)
202             }
203         })
204         tag.value.map(function(v) {
205             if (v !== '') {
206                 vnode.state.dirty(true)
207             }
208         })
209         tag.name.map(m.redraw)
210     },
211     oninit: function(vnode) {
212         vnode.state.sessionDB = new SessionDB()
213         // Get vocabulary
214         vnode.state.vocabulary = m.stream({"strict":false, "tags":{}})
215         m.request('/vocabulary.json').then(vnode.state.vocabulary)
216         vnode.state.editMode = vnode.attrs.targetEditable
217         vnode.state.tags = []
218         vnode.state.dirty = m.stream(false)
219         vnode.state.dirty.map(m.redraw)
220         vnode.state.objPath = '/arvados/v1/'+vnode.attrs.targetController+'/'+vnode.attrs.targetUuid
221         // Get tags
222         vnode.state.sessionDB.request(
223             vnode.state.sessionDB.loadLocal(),
224             '/arvados/v1/'+vnode.attrs.targetController,
225             {
226                 data: {
227                     filters: JSON.stringify([['uuid', '=', vnode.attrs.targetUuid]]),
228                     select: JSON.stringify(['properties'])
229                 },
230             }).then(function(obj) {
231                 if (obj.items.length == 1) {
232                     o = obj.items[0]
233                     Object.keys(o.properties).forEach(function(k) {
234                         vnode.state.appendTag(vnode, k, o.properties[k])
235                     })
236                     // Data synced with server, so dirty state should be false
237                     vnode.state.dirty(false)
238                     // Add new tag row when the last one is completed
239                     vnode.state.dirty.map(function() {
240                         if (!vnode.state.editMode) { return }
241                         lastTag = vnode.state.tags.slice(-1).pop()
242                         if (lastTag === undefined || (lastTag.name() !== '' && lastTag.value() !== '')) {
243                             vnode.state.appendTag(vnode, '', '')
244                         }
245                     })
246                 }
247             }
248         )
249     },
250     view: function(vnode) {
251         return [
252             vnode.state.editMode &&
253             m("div.pull-left", [
254                 m("a.btn.btn-primary.btn-sm"+(vnode.state.dirty() ? '' : '.disabled'), {
255                     style: {
256                         margin: '10px 0px'
257                     },
258                     onclick: function(e) {
259                         var tags = {}
260                         vnode.state.tags.forEach(function(t) {
261                             if (t.name() != '' && t.value() != '') {
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                         })
274                     }
275                 }, vnode.state.dirty() ? ' Save changes ' : ' Saved ')
276             ]),
277             // Tags table
278             m(TagEditorTable, {
279                 editMode: vnode.state.editMode,
280                 tags: vnode.state.tags,
281                 vocabulary: vnode.state.vocabulary,
282                 dirty: vnode.state.dirty
283             })
284         ]
285     },
286 }