git ssb

0+

regular / ssb-cms



Tree: 43af34efb145e2705ff168ae3e6f3cc6832314ac

Files: 43af34efb145e2705ff168ae3e6f3cc6832314ac / tree-view.js

10064 bytesRaw
1// mutant
2const h = require('mutant/html-element')
3const MutantMap = require('mutant/map')
4const Dict = require('mutant/dict')
5const Value = require('mutant/value')
6const Struct = require('mutant/struct')
7const MutantArray = require('mutant/array')
8const computed = require('mutant/computed')
9const when = require('mutant/when')
10const send = require('mutant/send')
11const resolve = require('mutant/resolve')
12// --
13//
14const pull = require('pull-stream')
15const ref = require('ssb-ref')
16
17const updates = require('./update-stream')() // no trusted keys
18const {updateObservableMessages} = require('./message-cache')
19const {isDraft} = require('./util')
20const config = require('./cms-config')
21
22module.exports = function(ssb, drafts, root, trusted_keys) {
23
24 let selection = Value()
25 let ready = Value(false)
26
27 function addNode(node) {
28 let content = node.msg().content
29 let value = {
30 content: {
31 root: content.root || node.id,
32 branch: node.id,
33 type: 'node'
34 }
35 }
36 let json = JSON.stringify(value, null, 2)
37 drafts.create(JSON.stringify(value, null, 2), node.id, null, null, (err, key)=>{
38 if (err) throw err
39 })
40 }
41
42 function cloneNode(node) {
43 let content = node.msg().content
44 let json = JSON.stringify(node.msg(), null, 2)
45 drafts.create(json, content.branch, null, null, (err, key)=>{
46 if (err) throw err
47 })
48 }
49
50 function discardDraft(node) {
51 drafts.remove(node.id, (err)=>{
52 if (err) throw err
53 if (selection() === node.id) {
54 document.location.hash = ''
55 }
56 })
57 }
58
59 function selectId(id) {
60 //console.log(this, id)
61 // from https://stackoverflow.com/questions/11451353/how-to-select-the-text-of-a-span-on-click
62 let selection = window.getSelection()
63 let range = document.createRange()
64 this.innerText = id
65 range.selectNodeContents(this)
66 selection.removeAllRanges()
67 selection.addRange(range)
68 }
69
70 function html(node) {
71
72 function _click(handler, args) {
73 return { 'ev-click': function(e) { handler.apply(this, args) }}
74 }
75
76 return h('li', [
77 h('div', {
78 classList: computed([node.open, isDraft(node.id)], (open, draft) => {
79 let l = ['branch']
80 if (open) l.push('open')
81 if (draft) l.push('draft')
82 return l
83 })
84 }, [
85 h('.branch-header', [
86 h('span.triangle', {
87 'ev-click': send(()=>{
88 if (isDraft(node.id)) return
89 node.open.set(!node.open())
90 })
91 }),
92 h('span.msgNode', [ // TODO: do we need this?
93 h('span.type-key', [
94 h('span.type', node.type),
95 h('a', {
96 classList: selection() === node.id ? ['node', 'selected'] : ['node'],
97 href: `#${node.id}`
98 },
99 h('span.name', node.label)
100 )
101 ]),
102 when(node.unsaved, h('span', {title: 'draft'}, '✎')),
103 when(node.forked, h('span', {title: 'conflicting updates, plese merge'}, '⑃')),
104 when(computed([node.msg], v => trusted_keys.includes(v.author)), h('span.trusted', {title: 'Signed-off'})),
105 //when(node.incomplete, h('span', {title: 'incomplete history'}, '⚠')),
106 h('span.buttons', [
107 h('button.id', {
108 'ev-click': function(e) { selectId.apply(this, [node.id]) },
109 'ev-blur': function(e) { this.innerText = 'id' }
110 }, 'id' ),
111 when(node.open, h('button.add', _click(addNode, [node]), 'add' )),
112 when(!isDraft(node.id), h('button.clone', _click(cloneNode, [node]), 'clone' )),
113 when(isDraft(node.id), h('button.discard', _click(discardDraft, [node]), 'discard' ))
114 ])
115 ])
116 ]),
117 when(node.open, h('ul', MutantMap(node.children, html)))
118 ])
119 ])
120 }
121
122 function streamChildren(root, mutantArray, syncedCb) {
123 let drain
124 pull(
125 ssb.cms.branches(root, {live: true, sync: true}),
126 updates({
127 allowUntrusted: true,
128 live: true,
129 sync: true,
130 bufferUntilSync: true
131 }),
132 pull.filter( x=>{
133 if (x.sync) syncedCb(null)
134 return !x.sync
135 }),
136 drain = updateObservableMessages(mutantArray, {
137 makeObservable: makeNode,
138 updateObservable: updateNode
139 })
140 )
141 return drain.abort
142 }
143
144 function updateNode(child, kv) {
145 child.msg.set(kv.value)
146 child.unsaved.set(kv.unsaved)
147 child.forked.set(Object.keys(kv.heads).length > 1)
148 child.incomplete.set(kv.tail !== kv.key || kv.queue.length !== 0)
149 }
150
151 function makeNode(kv) {
152 let dict = Dict(kv.value)
153 let label = computed([dict], x=>x.content && x.content.name)
154 let type = computed([dict], x=>x.content && x.content.type)
155 let node = Struct({
156 msg: dict,
157 label,
158 type,
159 open: false,
160 unsaved: false,
161 loaded: false,
162 forked: false,
163 incomplete: true,
164 children: MutantArray()
165 })
166 node.id = kv.key
167 let abortStream
168 node.open( (isOpen)=> {
169 if (isOpen) {
170 abortStream = streamChildren(node.id, node.children, ()=>{
171 node.loaded.set(true)
172 })
173 } else {
174 abortStream()
175 node.loaded.set(false)
176 node.children.clear()
177 }
178 })
179 return node
180 }
181
182 let roots = MutantArray()
183 let lis = MutantMap(roots, html)
184 let ul = h('ul', lis)
185 let treeView = h('.treeView', ul)
186
187 function ensureVisible(nodePath, cb) {
188
189 function r(children, nodePath, cb) {
190 let child = children.find( x=>x.id === nodePath[0])
191 if (!child) return cb(new Error(`tree node not found at path: $(nodePath.join(' -> '))}`))
192 nodePath.shift()
193 if (!nodePath.length) return cb(null, child)
194
195 if (!child.loaded()) {
196 // we need to wait for the children to arrive
197 let unsubscribe
198 unsubscribe = child.loaded( (isLoaded)=>{
199 if (isLoaded) {
200 unsubscribe()
201 r(child.children, nodePath, cb)
202 }
203 })
204 child.open.set(true)
205 } else r(child.children, nodePath, cb)
206 }
207
208 return r(roots, nodePath, cb)
209 }
210
211
212 function ancestors(msg, result, cb) {
213 let branch = msg.content && msg.content.branch
214 if (branch === config.sbot.cms.root) return cb(null, result)
215 if (branch) {
216 result.unshift(branch)
217 ssb.cms.getMessageOrDraft(branch, (err, msg)=>{
218 if (err) return cb(err)
219 ancestors(msg, result, cb)
220 })
221 } else cb(null, result)
222 }
223
224 selection( (id)=>{
225 if (!id) return
226 // TODO: fastpath for when the TreeNode is already rendered
227 ssb.cms.getMessageOrDraft(id, (err, msg) => {
228 if (err) return console.error(err)
229 let treeNodeId = msg.content && msg.content.revisionRoot || id
230 ancestors(msg, [], (err, nodePath)=>{
231 nodePath.push(treeNodeId)
232 ensureVisible(nodePath, ()=>{
233 treeView.querySelectorAll('.treeView .selected').forEach( el => el.classList.remove('selected') )
234 let el = treeView.querySelector(`a.node[href="#${id}"]`)
235 if (el) el.classList.add('selected')
236 })
237 })
238 })
239 })
240
241 streamChildren(root, roots, (err)=>{
242 if (err) throw err
243 ready.set(true)
244 })
245
246 function findNode(key, children) {
247 let node = children.find( x=>x.id === key )
248 if (node) return node
249 children.find( x=>{
250 return node = findNode(key, x.children)
251 })
252 return node
253 }
254
255 treeView.selection = selection
256 treeView.ready = ready
257 treeView.update = function(key, value) {
258 let node = findNode(key, roots)
259 if (node) node.msg.set(value)
260 }
261 return treeView
262}
263
264module.exports.css = ()=> `
265 .branch.open>ul {
266 margin: 0;
267 padding: 0;
268 //list-style: none;
269 padding-left: 1em;
270 }
271
272 .branch>.branch-header>.triangle::before {
273 content: '▶';
274 color: #555;
275 display: inline-block;
276 width: 1em;
277 font-size: .7em;
278 margin-right: .5em;
279 cursor: zoom-in;
280 }
281
282 .branch.open>.branch-header>.triangle::before {
283 cursor: zoom-out;
284 content: '▼';
285 }
286
287
288
289 ul {
290 list-style: none;
291 }
292 .treeView>ul {
293 padding-left: .5em;
294 }
295 span.key {
296 color: #222;
297 font-weight: bold;
298 margin-right: .2em;
299 }
300 .branch {
301 white-space: nowrap;
302 }
303 .branch-header {
304 display: flex;
305 flex-wrap: nowrap;
306 }
307 .branch-header>span.key {
308 flex-grow: 1;
309 display: inline-flex;
310 flex-wrap: nowrap;
311 }
312
313 .msgNode {
314 flex-grow: 1;
315 display: inline-flex;
316 flex-wrap: nowrap;
317 justify-content: space-between;
318 }
319
320 .branch-header .buttons {
321 flex-grow: 1;
322 display: inline-flex;
323 flex-wrap: nowrap;
324 justify-content: flex-end;
325 }
326
327 .branch>.branch-header button.add {
328 display: none;
329 }
330 .branch.open>.branch-header button.add {
331 display: inline-block;
332 }
333
334 .branch-header {
335 background: #ddd;
336 border-bottom: 1px solid #eee;
337 border-top-left-radius: 8px;
338 padding-left: .3em;
339 }
340 .draft .branch-header {
341 background: #dedfb5;
342 }
343 .branch-header:hover {
344 background: #ccc;
345 }
346
347 .branch-header button {
348 background: transparent;
349 border: none;
350 border-radius: 0;
351 color: #777;
352 padding: 0 .4em;
353 }
354
355 button.id {
356 max-width: 4em;
357 overflow: hidden;
358 }
359
360 .branch-header button:hover {
361 border-top: 1px solid #ccc;
362 color: #eee;
363 border-bottom: 1px solid #aaa;
364 }
365
366 a.node {
367 color: #dde;
368 text-decoration: none;
369 margin-left: .2em;
370 }
371 a.node.draft {
372 color: red;
373 font-style: italic;
374 }
375
376 a.node>span.name {
377 color: #161438;
378 padding: 0px 4px;
379 background: #babace;
380 }
381
382 a.node>span.name:hover {
383 color: #0f0d25;
384 background: #9f9fb1;
385 }
386 .node.selected>span.name,
387 .node.selected>span.name:hover {
388 color: #111110;
389 background: #b39254;
390 }
391
392 a.node>span:hover {
393 background-color: #226;
394 }
395 .node.selected>span {
396 color: black;
397 background: yellow;
398 }
399`
400

Built with git-ssb-web