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