Checklist management without reordering
This commit is contained in:
parent
dc2b6dac8b
commit
d0f410323e
12 changed files with 1100 additions and 46 deletions
|
|
@ -1,10 +1,12 @@
|
|||
import 'preact/debug'
|
||||
import 'preact/devtools'
|
||||
import { signal } from 'preact/signals'
|
||||
|
||||
import { h, Component, render, createRef } from 'preact'
|
||||
import htm from 'htm'
|
||||
import { Session } from 'session'
|
||||
import { Node, NodeUI } from 'node'
|
||||
import { Websocket } from 'ws'
|
||||
import { signal } from 'preact/signals'
|
||||
const html = htm.bind(h)
|
||||
|
||||
class App extends Component {
|
||||
|
|
|
|||
|
|
@ -12,13 +12,47 @@ export class ChecklistGroup {
|
|||
constructor(data) {//{{{
|
||||
Object.keys(data).forEach(key => {
|
||||
if (key == 'Items')
|
||||
this.items = data[key].map(itemData =>
|
||||
new ChecklistItem(itemData)
|
||||
this.items = data[key].map(itemData => {
|
||||
let item = new ChecklistItem(itemData)
|
||||
item.checklistGroup = this
|
||||
return item
|
||||
}
|
||||
).sort(ChecklistItem.sort)
|
||||
else
|
||||
this[key] = data[key]
|
||||
})
|
||||
}//}}}
|
||||
addItem(label, okCallback) {//{{{
|
||||
window._app.current.request('/node/checklist_group/item_add', {
|
||||
ChecklistGroupID: this.ID,
|
||||
Label: label,
|
||||
})
|
||||
.then(json => {
|
||||
let item = new ChecklistItem(json.Item)
|
||||
item.checklistGroup = this
|
||||
this.items.push(item)
|
||||
okCallback()
|
||||
})
|
||||
.catch(window._app.current.responseError)
|
||||
return
|
||||
}//}}}
|
||||
updateLabel(newLabel, okCallback, errCallback) {//{{{
|
||||
window._app.current.request('/node/checklist_group/label', {
|
||||
ChecklistGroupID: this.ID,
|
||||
Label: newLabel,
|
||||
})
|
||||
.then(okCallback)
|
||||
.catch(errCallback)
|
||||
}//}}}
|
||||
delete(okCallback, errCallback) {//{{{
|
||||
window._app.current.request('/node/checklist_group/delete', {
|
||||
ChecklistGroupID: this.ID,
|
||||
})
|
||||
.then(() => {
|
||||
okCallback()
|
||||
})
|
||||
.catch(errCallback)
|
||||
}//}}}
|
||||
}
|
||||
|
||||
export class ChecklistItem {
|
||||
|
|
@ -32,34 +66,193 @@ export class ChecklistItem {
|
|||
this[key] = data[key]
|
||||
})
|
||||
}//}}}
|
||||
updateState(newState, okCallback, errCallback) {//{{{
|
||||
window._app.current.request('/node/checklist_item/state', {
|
||||
ChecklistItemID: this.ID,
|
||||
State: newState,
|
||||
})
|
||||
.then(okCallback)
|
||||
.catch(errCallback)
|
||||
}//}}}
|
||||
updateLabel(newLabel, okCallback, errCallback) {//{{{
|
||||
window._app.current.request('/node/checklist_item/label', {
|
||||
ChecklistItemID: this.ID,
|
||||
Label: newLabel,
|
||||
})
|
||||
.then(okCallback)
|
||||
.catch(errCallback)
|
||||
}//}}}
|
||||
delete(okCallback, errCallback) {//{{{
|
||||
window._app.current.request('/node/checklist_item/delete', {
|
||||
ChecklistItemID: this.ID,
|
||||
})
|
||||
.then(() => {
|
||||
this.checklistGroup.items = this.checklistGroup.items.filter(item => item.ID != this.ID)
|
||||
okCallback()
|
||||
})
|
||||
.catch(errCallback)
|
||||
}//}}}
|
||||
}
|
||||
|
||||
export class Checklist extends Component {
|
||||
render({ groups }) {//{{{
|
||||
constructor() {//{{{
|
||||
super()
|
||||
this.edit = signal(true)
|
||||
this.dragItemSource = null
|
||||
this.dragItemTarget = null
|
||||
this.state = {
|
||||
confirmDeletion: true,
|
||||
}
|
||||
}//}}}
|
||||
render({ groups }, { confirmDeletion }) {//{{{
|
||||
if (groups.length == 0)
|
||||
return
|
||||
|
||||
groups.sort(ChecklistGroup.sort)
|
||||
if (typeof groups.sort != 'function')
|
||||
groups = []
|
||||
|
||||
let groupElements = groups.map(group => html`<${ChecklistGroupElement} group=${group} />`)
|
||||
groups.sort(ChecklistGroup.sort)
|
||||
let groupElements = groups.map(group => html`<${ChecklistGroupElement} key="group-${group.ID}" ui=${this} group=${group} />`)
|
||||
|
||||
let edit = 'edit-list-gray.svg'
|
||||
let confirmDeletionEl = ''
|
||||
if (this.edit.value) {
|
||||
edit = 'edit-list.svg'
|
||||
confirmDeletionEl = html`
|
||||
<div>
|
||||
<input type="checkbox" id="confirm-checklist-delete" checked=${confirmDeletion} onchange=${() => this.setState({ confirmDeletion: !confirmDeletion })} />
|
||||
<label for="confirm-checklist-delete">Confirm checklist deletion</label>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
let addGroup = ()=>{
|
||||
if (this.edit.value)
|
||||
return html`<img src="/images/${_VERSION}/add-gray.svg" onclick=${()=>this.addGroup()} />`
|
||||
}
|
||||
|
||||
return html`
|
||||
<div id="checklist">
|
||||
<h1>Checklist</h1>
|
||||
<div class="header">
|
||||
<h1>Checklist</h1>
|
||||
<img src="/images/${_VERSION}/${edit}" onclick=${() => this.toggleEdit()} />
|
||||
<${addGroup} />
|
||||
</div>
|
||||
${confirmDeletionEl}
|
||||
${groupElements}
|
||||
</div>
|
||||
`
|
||||
}//}}}
|
||||
|
||||
toggleEdit() {//{{{
|
||||
this.edit.value = !this.edit.value
|
||||
}//}}}
|
||||
addGroup() {//{{{
|
||||
let label = prompt("Create a new group")
|
||||
if (label === null)
|
||||
return
|
||||
label = label.trim()
|
||||
if (label == '')
|
||||
return
|
||||
|
||||
window._app.current.request('/node/checklist_group/add', {
|
||||
NodeID: window._app.current.nodeUI.current.node.value.ID,
|
||||
Label: label,
|
||||
})
|
||||
.then(json => {
|
||||
let group = new ChecklistGroup(json.Group)
|
||||
this.props.groups.push(group)
|
||||
this.forceUpdate()
|
||||
})
|
||||
.catch(window._app.current.responseError)
|
||||
return
|
||||
}//}}}
|
||||
dragTarget(target) {//{{{
|
||||
if (this.dragItemTarget)
|
||||
this.dragItemTarget.setDragTarget(false)
|
||||
this.dragItemTarget = target
|
||||
target.setDragTarget(true)
|
||||
}//}}}
|
||||
dragReset() {//{{{
|
||||
if (this.dragItemTarget) {
|
||||
this.dragItemTarget.setDragTarget(false)
|
||||
this.dragItemTarget = null
|
||||
}
|
||||
}//}}}
|
||||
}
|
||||
|
||||
class ChecklistGroupElement extends Component {
|
||||
render({ group }) {//{{{
|
||||
let items = group.items.map(item => html`<${ChecklistItemElement} item=${item} />`)
|
||||
constructor() {//{{{
|
||||
super()
|
||||
this.label = createRef()
|
||||
}//}}}
|
||||
render({ ui, group }) {//{{{
|
||||
let items = ({ ui, group }) =>
|
||||
group.items.map(item => html`<${ChecklistItemElement} key="item-${item.ID}" ui=${ui} group=${this} item=${item} />`)
|
||||
|
||||
let label = ()=>html`<div class="label" style="cursor: pointer" ref=${this.label} onclick=${()=>this.editLabel()}>${group.Label}</div>`
|
||||
|
||||
return html`
|
||||
<div class="checklist-group">${group.Label}</div>
|
||||
${items}
|
||||
<div class="checklist-group-container">
|
||||
<div class="checklist-group ${ui.edit.value ? 'edit' : ''}">
|
||||
<div class="reorder" style="cursor: grab">☰</div>
|
||||
<img src="/images/${_VERSION}/trashcan.svg" onclick=${() => this.delete()} />
|
||||
<${label} />
|
||||
<img src="/images/${_VERSION}/add-gray.svg" onclick=${() => this.addItem()} />
|
||||
</div>
|
||||
<${items} ui=${ui} group=${group} />
|
||||
</div>
|
||||
`
|
||||
}//}}}
|
||||
addItem() {//{{{
|
||||
let label = prompt("Create a new item")
|
||||
if (label === null)
|
||||
return
|
||||
label = label.trim()
|
||||
if (label == '')
|
||||
return
|
||||
|
||||
this.props.group.addItem(label, () => {
|
||||
this.forceUpdate()
|
||||
})
|
||||
}//}}}
|
||||
editLabel() {//{{{
|
||||
let label = prompt('Edit label', this.props.group.Label)
|
||||
if (label === null)
|
||||
return
|
||||
|
||||
label = label.trim()
|
||||
if (label == '') {
|
||||
alert(`A label can't be empty.`)
|
||||
return
|
||||
}
|
||||
|
||||
this.label.current.classList.remove('error')
|
||||
this.props.group.updateLabel(label, () => {
|
||||
this.props.group.Label = label
|
||||
this.label.current.innerHTML = label
|
||||
this.label.current.classList.add('ok')
|
||||
this.forceUpdate()
|
||||
setTimeout(() => this.label.current.classList.remove('ok'), 500)
|
||||
}, () => {
|
||||
this.label.current.classList.add('error')
|
||||
})
|
||||
|
||||
}//}}}
|
||||
delete() {//{{{
|
||||
if (this.props.ui.state.confirmDeletion) {
|
||||
if (!confirm(`Delete '${this.props.group.Label}'?`))
|
||||
return
|
||||
}
|
||||
|
||||
this.props.group.delete(() => {
|
||||
this.props.ui.props.groups = this.props.ui.props.groups.filter(g => g.ID != this.props.group.ID)
|
||||
this.props.ui.forceUpdate()
|
||||
}, err => {
|
||||
console.log(err)
|
||||
console.log('error')
|
||||
})
|
||||
}//}}}
|
||||
}
|
||||
|
||||
class ChecklistItemElement extends Component {
|
||||
|
|
@ -67,31 +260,100 @@ class ChecklistItemElement extends Component {
|
|||
super(props)
|
||||
this.state = {
|
||||
checked: props.item.Checked,
|
||||
dragTarget: false,
|
||||
}
|
||||
this.checkbox = createRef()
|
||||
this.label = createRef()
|
||||
}//}}}
|
||||
render({ item }, { checked }) {//{{{
|
||||
render({ ui, item }, { checked, dragTarget }) {//{{{
|
||||
let checkbox = ()=>{
|
||||
if (ui.edit.value)
|
||||
return html`<label ref=${this.label} onclick=${() => this.editLabel()} style="cursor: pointer">${item.Label}</label>`
|
||||
else
|
||||
return html`
|
||||
<input type="checkbox" ref=${this.checkbox} key="checkbox-${item.ID}" id="checkbox-${item.ID}" checked=${checked} onchange=${evt => this.update(evt.target.checked)} />
|
||||
<label ref=${this.label} for="checkbox-${item.ID}">${item.Label}</label>
|
||||
`
|
||||
}
|
||||
return html`
|
||||
<div class="checklist-item ${checked ? 'checked' : ''}">
|
||||
<input type="checkbox" ref=${this.checkbox} key="checkbox-${item.ID}" id="checkbox-${item.ID}" checked=${checked} onchange=${evt => this.update(evt.target.checked)} />
|
||||
<label for="checkbox-${item.ID}">${item.Label}</label>
|
||||
<div class="checklist-item ${checked ? 'checked' : ''} ${ui.edit.value ? 'edit' : ''} ${dragTarget ? 'drag-target' : ''}" draggable=true>
|
||||
<div class="reorder" style="user-select: none;">☰</div>
|
||||
<img src="/images/${_VERSION}/trashcan.svg" onclick=${() => this.delete()} />
|
||||
<${checkbox} />
|
||||
</div>
|
||||
`
|
||||
}//}}}
|
||||
componentDidMount() {//{{{
|
||||
this.base.addEventListener('dragstart', ()=>this.dragStart())
|
||||
this.base.addEventListener('dragend', ()=>this.dragEnd())
|
||||
this.base.addEventListener('dragenter', evt=>this.dragEnter(evt))
|
||||
}//}}}
|
||||
|
||||
update(checked) {//{{{
|
||||
this.setState({ checked })
|
||||
this.checkbox.current.classList.remove('error')
|
||||
window._app.current.request('/node/checklist_item/state', {
|
||||
ChecklistItemID: this.props.item.ID,
|
||||
State: checked,
|
||||
this.props.item.updateState(checked, () => {
|
||||
this.checkbox.current.classList.add('ok')
|
||||
setTimeout(() => this.checkbox.current.classList.remove('ok'), 500)
|
||||
}, () => {
|
||||
this.checkbox.current.classList.add('error')
|
||||
})
|
||||
.then(res => {
|
||||
this.checkbox.current.classList.add('ok')
|
||||
setTimeout(()=>this.checkbox.current.classList.remove('ok'), 500)
|
||||
}//}}}
|
||||
editLabel() {//{{{
|
||||
let label = prompt('Edit label', this.props.item.Label)
|
||||
if (label === null)
|
||||
return
|
||||
|
||||
})
|
||||
.catch(()=>{
|
||||
this.checkbox.current.classList.add('error')
|
||||
})
|
||||
label = label.trim()
|
||||
if (label == '') {
|
||||
alert(`A label can't be empty.`)
|
||||
return
|
||||
}
|
||||
|
||||
this.label.current.classList.remove('error')
|
||||
this.props.item.updateLabel(label, () => {
|
||||
this.props.item.Label = label
|
||||
this.label.current.innerHTML = label
|
||||
this.label.current.classList.add('ok')
|
||||
setTimeout(() => this.label.current.classList.remove('ok'), 500)
|
||||
}, () => {
|
||||
this.label.current.classList.add('error')
|
||||
})
|
||||
|
||||
}//}}}
|
||||
delete() {//{{{
|
||||
if (this.props.ui.state.confirmDeletion) {
|
||||
if (!confirm(`Delete '${this.props.item.Label}'?`))
|
||||
return
|
||||
}
|
||||
|
||||
this.props.item.delete(() => {
|
||||
this.props.group.forceUpdate()
|
||||
}, err => {
|
||||
console.log(err)
|
||||
console.log('error')
|
||||
})
|
||||
}//}}}
|
||||
|
||||
setDragTarget(state) {//{{{
|
||||
this.setState({ dragTarget: state })
|
||||
}//}}}
|
||||
dragStart() {//{{{
|
||||
// Shouldn't be needed, but in case the previous drag was bungled up, we reset.
|
||||
this.props.ui.dragReset()
|
||||
this.props.ui.dragItemSource = this
|
||||
}//}}}
|
||||
dragEnter(evt) {//{{{
|
||||
evt.preventDefault()
|
||||
this.props.ui.dragTarget(this)
|
||||
}//}}}
|
||||
dragEnd() {//{{{
|
||||
console.log(
|
||||
this.props.ui.dragItemSource.props.item.Label,
|
||||
this.props.ui.dragItemTarget.props.item.Label,
|
||||
)
|
||||
this.props.ui.dragReset()
|
||||
}//}}}
|
||||
}
|
||||
|
||||
// vim: foldmethod=marker
|
||||
|
|
|
|||
|
|
@ -146,6 +146,10 @@ export class NodeUI extends Component {
|
|||
return
|
||||
|
||||
switch (evt.key.toUpperCase()) {
|
||||
case 'C':
|
||||
this.showPage('node')
|
||||
break
|
||||
|
||||
case 'E':
|
||||
this.showPage('keys')
|
||||
break
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue