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