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