Initial commit.
commit
8b5e0a0c02
|
@ -0,0 +1,147 @@
|
||||||
|
PACKAGE = 'kanban'
|
||||||
|
VERSION = '0.1'
|
||||||
|
|
||||||
|
PREFIX := /usr/local
|
||||||
|
BINDIR := $(PREFIX)/bin
|
||||||
|
LIBDIR := $(PREFIX)/lib
|
||||||
|
SHAREDIR := $(PREFIX)/share
|
||||||
|
INCLUDEDIR := $(PREFIX)/include
|
||||||
|
MANDIR := $(SHAREDIR)/man
|
||||||
|
|
||||||
|
CC := cc
|
||||||
|
AR := ar
|
||||||
|
RANLIB := ranlib
|
||||||
|
CFLAGS :=
|
||||||
|
LDFLAGS :=
|
||||||
|
|
||||||
|
Q := @
|
||||||
|
|
||||||
|
all: public/main.js public/style.css
|
||||||
|
@:
|
||||||
|
|
||||||
|
public/main.js: public/main.bundle.js public
|
||||||
|
@echo '[01;31m MIN > [01;37mpublic/main.js[00m'
|
||||||
|
$(Q)npx babel --minified public/main.bundle.js -o public/main.js
|
||||||
|
|
||||||
|
|
||||||
|
public/main.bundle.js: client/index.ls public
|
||||||
|
@echo '[01;32m BUN > [01;37mpublic/main.bundle.js[00m'
|
||||||
|
$(Q)npx browserify -t browserify-livescript client/index.ls -o public/main.bundle.js
|
||||||
|
|
||||||
|
|
||||||
|
public/main.js.clean:
|
||||||
|
@echo '[01;37m RM > [01;37mpublic/main.js[00m'
|
||||||
|
$(Q)rm -f public/main.js
|
||||||
|
@echo '[01;37m RM > [01;37mpublic/main.bundle.js[00m'
|
||||||
|
$(Q)rm -f public/main.bundle.js
|
||||||
|
|
||||||
|
|
||||||
|
public/style.css: client/style.sass public
|
||||||
|
@echo '[01;33m CSS > [01;37mpublic/style.css[00m'
|
||||||
|
$(Q)sassc client/style.sass > public/style.css
|
||||||
|
|
||||||
|
|
||||||
|
public/style.css.clean:
|
||||||
|
|
||||||
|
public:
|
||||||
|
$(Q)mkdir -p public
|
||||||
|
$(DESTDIR)$(PREFIX):
|
||||||
|
@echo '[01;35m DIR > [01;37m$(PREFIX)[00m'
|
||||||
|
$(Q)mkdir -p $(DESTDIR)$(PREFIX)
|
||||||
|
$(DESTDIR)$(BINDIR):
|
||||||
|
@echo '[01;35m DIR > [01;37m$(BINDIR)[00m'
|
||||||
|
$(Q)mkdir -p $(DESTDIR)$(BINDIR)
|
||||||
|
$(DESTDIR)$(LIBDIR):
|
||||||
|
@echo '[01;35m DIR > [01;37m$(LIBDIR)[00m'
|
||||||
|
$(Q)mkdir -p $(DESTDIR)$(LIBDIR)
|
||||||
|
$(DESTDIR)$(SHAREDIR):
|
||||||
|
@echo '[01;35m DIR > [01;37m$(SHAREDIR)[00m'
|
||||||
|
$(Q)mkdir -p $(DESTDIR)$(SHAREDIR)
|
||||||
|
$(DESTDIR)$(INCLUDEDIR):
|
||||||
|
@echo '[01;35m DIR > [01;37m$(INCLUDEDIR)[00m'
|
||||||
|
$(Q)mkdir -p $(DESTDIR)$(INCLUDEDIR)
|
||||||
|
$(DESTDIR)$(MANDIR):
|
||||||
|
@echo '[01;35m DIR > [01;37m$(MANDIR)[00m'
|
||||||
|
$(Q)mkdir -p $(DESTDIR)$(MANDIR)
|
||||||
|
install: subdirs.install public/main.js.install public/style.css.install
|
||||||
|
@:
|
||||||
|
|
||||||
|
subdirs.install:
|
||||||
|
|
||||||
|
uninstall: subdirs.uninstall public/main.js.uninstall public/style.css.uninstall
|
||||||
|
@:
|
||||||
|
|
||||||
|
subdirs.uninstall:
|
||||||
|
|
||||||
|
test: all subdirs subdirs.test
|
||||||
|
@:
|
||||||
|
|
||||||
|
subdirs.test:
|
||||||
|
|
||||||
|
clean: public/main.js.clean public/style.css.clean
|
||||||
|
|
||||||
|
distclean: clean
|
||||||
|
|
||||||
|
dist: dist-gz dist-xz dist-bz2
|
||||||
|
$(Q)rm -- $(PACKAGE)-$(VERSION)
|
||||||
|
|
||||||
|
distdir:
|
||||||
|
$(Q)rm -rf -- $(PACKAGE)-$(VERSION)
|
||||||
|
$(Q)ln -s -- . $(PACKAGE)-$(VERSION)
|
||||||
|
|
||||||
|
dist-gz: $(PACKAGE)-$(VERSION).tar.gz
|
||||||
|
$(PACKAGE)-$(VERSION).tar.gz: distdir
|
||||||
|
@echo '[01;33m TAR > [01;37m$(PACKAGE)-$(VERSION).tar.gz[00m'
|
||||||
|
$(Q)tar czf $(PACKAGE)-$(VERSION).tar.gz \
|
||||||
|
$(PACKAGE)-$(VERSION)/client/index.ls \
|
||||||
|
$(PACKAGE)-$(VERSION)/client/style.sass
|
||||||
|
|
||||||
|
dist-xz: $(PACKAGE)-$(VERSION).tar.xz
|
||||||
|
$(PACKAGE)-$(VERSION).tar.xz: distdir
|
||||||
|
@echo '[01;33m TAR > [01;37m$(PACKAGE)-$(VERSION).tar.xz[00m'
|
||||||
|
$(Q)tar cJf $(PACKAGE)-$(VERSION).tar.xz \
|
||||||
|
$(PACKAGE)-$(VERSION)/client/index.ls \
|
||||||
|
$(PACKAGE)-$(VERSION)/client/style.sass
|
||||||
|
|
||||||
|
dist-bz2: $(PACKAGE)-$(VERSION).tar.bz2
|
||||||
|
$(PACKAGE)-$(VERSION).tar.bz2: distdir
|
||||||
|
@echo '[01;33m TAR > [01;37m$(PACKAGE)-$(VERSION).tar.bz2[00m'
|
||||||
|
$(Q)tar cjf $(PACKAGE)-$(VERSION).tar.bz2 \
|
||||||
|
$(PACKAGE)-$(VERSION)/client/index.ls \
|
||||||
|
$(PACKAGE)-$(VERSION)/client/style.sass
|
||||||
|
|
||||||
|
help:
|
||||||
|
@echo '[01;37m :: kanban-0.1[00m'
|
||||||
|
@echo ''
|
||||||
|
@echo '[01;37mGeneric targets:[00m'
|
||||||
|
@echo '[00m - [01;32mhelp [37m Prints this help message.[00m'
|
||||||
|
@echo '[00m - [01;32mall [37m Builds all targets.[00m'
|
||||||
|
@echo '[00m - [01;32mdist [37m Creates tarballs of the files of the project.[00m'
|
||||||
|
@echo '[00m - [01;32minstall [37m Installs the project.[00m'
|
||||||
|
@echo '[00m - [01;32mclean [37m Removes compiled files.[00m'
|
||||||
|
@echo '[00m - [01;32muninstall [37m Deinstalls the project.[00m'
|
||||||
|
@echo ''
|
||||||
|
@echo '[01;37mCLI-modifiable variables:[00m'
|
||||||
|
@echo ' - [01;34mCC [37m ${CC}[00m'
|
||||||
|
@echo ' - [01;34mCFLAGS [37m ${CFLAGS}[00m'
|
||||||
|
@echo ' - [01;34mLDFLAGS [37m ${LDFLAGS}[00m'
|
||||||
|
@echo ' - [01;34mDESTDIR [37m ${DESTDIR}[00m'
|
||||||
|
@echo ' - [01;34mPREFIX [37m ${PREFIX}[00m'
|
||||||
|
@echo ' - [01;34mBINDIR [37m ${BINDIR}[00m'
|
||||||
|
@echo ' - [01;34mLIBDIR [37m ${LIBDIR}[00m'
|
||||||
|
@echo ' - [01;34mSHAREDIR [37m ${SHAREDIR}[00m'
|
||||||
|
@echo ' - [01;34mINCLUDEDIR [37m ${INCLUDEDIR}[00m'
|
||||||
|
@echo ' - [01;34mMANDIR [37m ${MANDIR}[00m'
|
||||||
|
@echo ''
|
||||||
|
@echo '[01;37mProject targets: [00m'
|
||||||
|
@echo ' - [01;33mpublic/main.js[37m livescript[00m'
|
||||||
|
@echo ' - [01;33mpublic/style.css[37m css[00m'
|
||||||
|
@echo ''
|
||||||
|
@echo '[01;37mMakefile options:[00m'
|
||||||
|
@echo ' - gnu: false'
|
||||||
|
@echo ' - colors: true'
|
||||||
|
@echo ''
|
||||||
|
@echo '[01;37mRebuild the Makefile with:[00m'
|
||||||
|
@echo ' zsh ./build.zsh -c'
|
||||||
|
.PHONY: all subdirs clean distclean dist install uninstall help
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
|
||||||
|
h = require 'maquette' .h
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
box: (args, children) ->
|
||||||
|
h \div.box args, children
|
||||||
|
title: (level, args, label) ->
|
||||||
|
if not label
|
||||||
|
label = args
|
||||||
|
args = {}
|
||||||
|
|
||||||
|
h "div.title.is-#{level}", args, [label]
|
||||||
|
label: (args, label) ->
|
||||||
|
if not label
|
||||||
|
label = args
|
||||||
|
args = {}
|
||||||
|
|
||||||
|
h \label.label args, [label]
|
||||||
|
input: (args, children) ->
|
||||||
|
h \input.input args, children
|
||||||
|
|
||||||
|
# FIXME: Use only args and add args.label and args.input?
|
||||||
|
# Or maybe args.name and args.type could be used directly?
|
||||||
|
field: (args, children) ->
|
||||||
|
h \div.field args, children
|
||||||
|
|
||||||
|
modal: (args, content) ->
|
||||||
|
h \div.modal args, [
|
||||||
|
h \div.modal-background args.background
|
||||||
|
h \div.modal-content [args.content]
|
||||||
|
]
|
||||||
|
|
||||||
|
form: (method, url, content) ->
|
||||||
|
h \form.form {
|
||||||
|
action: url
|
||||||
|
method: method
|
||||||
|
}, content
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,485 @@
|
||||||
|
|
||||||
|
maquette = require "maquette"
|
||||||
|
nmd = require "nano-markdown"
|
||||||
|
|
||||||
|
{create-projector, h} = maquette
|
||||||
|
|
||||||
|
projector = create-projector!
|
||||||
|
|
||||||
|
bulma = require "./bulma.ls"
|
||||||
|
|
||||||
|
get-previous = (collection, element) ->
|
||||||
|
var previous
|
||||||
|
|
||||||
|
for item in collection
|
||||||
|
if item == element
|
||||||
|
return previous
|
||||||
|
|
||||||
|
previous = item
|
||||||
|
|
||||||
|
get-next = (collection, element) ->
|
||||||
|
var found-element
|
||||||
|
|
||||||
|
for item in collection
|
||||||
|
if found-element
|
||||||
|
return item
|
||||||
|
|
||||||
|
if item == element
|
||||||
|
found-element := true
|
||||||
|
|
||||||
|
Task = (self, project) ->
|
||||||
|
self.render = ->
|
||||||
|
author = model.users[self.author]
|
||||||
|
if typeof(author) != "object" and author != "request sent"
|
||||||
|
model.users[self.author] = "request sent"
|
||||||
|
# FIXME: This should go directly to authd.
|
||||||
|
socket.send JSON.stringify {
|
||||||
|
type: "get-user"
|
||||||
|
uid: self.author
|
||||||
|
}
|
||||||
|
|
||||||
|
assigned_to = model.users[self.assigned_to]
|
||||||
|
if assigned_to and typeof(assigned_to) != "object" and assigned_to != "request sent"
|
||||||
|
model.users[self.assigned_to] = "request sent"
|
||||||
|
# FIXME: This should go directly to authd.
|
||||||
|
socket.send JSON.stringify {
|
||||||
|
type: "get-user"
|
||||||
|
uid: self.assigned_to
|
||||||
|
}
|
||||||
|
|
||||||
|
is-selected = model.selected == self.id
|
||||||
|
|
||||||
|
h \div.card {
|
||||||
|
key: self.id
|
||||||
|
classes: {
|
||||||
|
"is-selected": is-selected
|
||||||
|
}
|
||||||
|
onclick: ->
|
||||||
|
model.selected := self.id
|
||||||
|
} [
|
||||||
|
h \div.card-content [
|
||||||
|
h \div.media [
|
||||||
|
h \div.media-left [
|
||||||
|
h \img.image.is-48x48.avatar {
|
||||||
|
alt: "user image"
|
||||||
|
src: if typeof(assigned_to) == "object"
|
||||||
|
assigned_to.avatar
|
||||||
|
else
|
||||||
|
"https://bulma.io/images/placeholders/96x96.png"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
h \div.media-content [
|
||||||
|
if model.editing == self.id + ".title"
|
||||||
|
h \input.input {
|
||||||
|
value: self.title
|
||||||
|
onchange: (e) ->
|
||||||
|
model.editing := undefined
|
||||||
|
socket.send JSON.stringify {
|
||||||
|
type: "edit-task"
|
||||||
|
project: project.id
|
||||||
|
task: self.id
|
||||||
|
title: e.target.value
|
||||||
|
}
|
||||||
|
} [ self.title ]
|
||||||
|
else
|
||||||
|
bulma.title 4 self.title
|
||||||
|
|
||||||
|
if typeof(model.users[self.assigned_to]) == "object"
|
||||||
|
user = model.users[self.assigned_to]
|
||||||
|
|
||||||
|
h \div.subtitle.is-6 [
|
||||||
|
"@" + (user.full_name || user.login)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
if is-selected
|
||||||
|
h \div.media-right {key: "edit"} [
|
||||||
|
h \span.small {
|
||||||
|
onclick: ->
|
||||||
|
if model.editing == self.id + ".title"
|
||||||
|
model.editing := undefined
|
||||||
|
else
|
||||||
|
model.editing := self.id + ".title"
|
||||||
|
} [
|
||||||
|
"Edit"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
if is-selected
|
||||||
|
h \div.media-right {key: "delete"} [
|
||||||
|
h \span.small {
|
||||||
|
onclick: ->
|
||||||
|
model.editing := self.id + ".delete"
|
||||||
|
} [
|
||||||
|
"Delete"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
if is-selected
|
||||||
|
h \div.content {
|
||||||
|
after-create: (dom) ->
|
||||||
|
dom.innerHTML = nmd self.description
|
||||||
|
} [
|
||||||
|
if model.editing == self.id + ".description"
|
||||||
|
h \form.form [
|
||||||
|
h \textarea.textarea {
|
||||||
|
value: self.description
|
||||||
|
oninput: (e) ->
|
||||||
|
model.editing-data := e.target.value
|
||||||
|
}
|
||||||
|
h \div.button.is-fullwidth {
|
||||||
|
onclick: ->
|
||||||
|
socket.send JSON.stringify {
|
||||||
|
type: "edit-task"
|
||||||
|
project: project.id
|
||||||
|
task: self.id
|
||||||
|
description: model.editing-data
|
||||||
|
}
|
||||||
|
model.editing-data := undefined
|
||||||
|
model.editing := undefined
|
||||||
|
} [ "Update" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
if is-selected
|
||||||
|
h \span.button.is-small {
|
||||||
|
onclick: ->
|
||||||
|
model.editing := self.id + ".description"
|
||||||
|
} [
|
||||||
|
"edit"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
if is-selected
|
||||||
|
h \div.card-footer {key: "assign"} [
|
||||||
|
if model.editing == self.id + ".assigned_to"
|
||||||
|
h \div.card-footer-item {
|
||||||
|
key: "assign.clicked"
|
||||||
|
} [
|
||||||
|
h \input.input {
|
||||||
|
onchange: (e) ->
|
||||||
|
model.editing := undefined
|
||||||
|
socket.send JSON.stringify {
|
||||||
|
type: "edit-task"
|
||||||
|
project: project.id
|
||||||
|
task: self.id
|
||||||
|
assigned_to: Number e.target.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
else
|
||||||
|
h \div.card-footer-item {
|
||||||
|
key: "assign"
|
||||||
|
onclick: ->
|
||||||
|
model.editing := self.id + ".assigned_to"
|
||||||
|
} [ "Assign" ]
|
||||||
|
]
|
||||||
|
|
||||||
|
if is-selected
|
||||||
|
h \div.card-footer {key: "move"} [
|
||||||
|
h \div.card-footer-item {
|
||||||
|
key: "⇐"
|
||||||
|
onclick: ->
|
||||||
|
socket.send JSON.stringify {
|
||||||
|
type: "edit-task"
|
||||||
|
project: project.id
|
||||||
|
task: self.id
|
||||||
|
column: get-previous project.columns.map((.id)), self.column
|
||||||
|
}
|
||||||
|
} [ "⇐" ]
|
||||||
|
|
||||||
|
if model.editing == self.id + ".delete"
|
||||||
|
h \div.card-footer-item {
|
||||||
|
key: "delete"
|
||||||
|
} [
|
||||||
|
h \div.button.is-danger {
|
||||||
|
onclick: ->
|
||||||
|
socket.send JSON.stringify {
|
||||||
|
type: "delete-task"
|
||||||
|
project: project.id
|
||||||
|
task: self.id
|
||||||
|
}
|
||||||
|
} [ "Delete! For real!" ]
|
||||||
|
]
|
||||||
|
|
||||||
|
h \div.card-footer-item {
|
||||||
|
key: "⇒"
|
||||||
|
onclick: ->
|
||||||
|
socket.send JSON.stringify {
|
||||||
|
type: "edit-task"
|
||||||
|
project: project.id
|
||||||
|
task: self.id
|
||||||
|
column: get-next project.columns.map((.id)), self.column
|
||||||
|
}
|
||||||
|
} [ "⇒" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
self
|
||||||
|
|
||||||
|
Project = (self) ->
|
||||||
|
self.tasks = self.tasks.map (e) -> Task e, self
|
||||||
|
|
||||||
|
self.render-column = (column) ->
|
||||||
|
h \div.column.is-2 {
|
||||||
|
key: column.id
|
||||||
|
} [
|
||||||
|
h \div.card.is-column-header {
|
||||||
|
key: column.id
|
||||||
|
} [
|
||||||
|
h \div.card-header [
|
||||||
|
if model.editing == column.id + ".title"
|
||||||
|
h \input.input {
|
||||||
|
type: "text",
|
||||||
|
value: column.name
|
||||||
|
onchange: (e) ->
|
||||||
|
console.log "onchange??"
|
||||||
|
model.editing := undefined
|
||||||
|
|
||||||
|
socket.send JSON.stringify {
|
||||||
|
type: "edit-column"
|
||||||
|
project: self.id
|
||||||
|
column: column.id
|
||||||
|
name: e.target.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
h \div.card-header-title [
|
||||||
|
bulma.title 3 column.name
|
||||||
|
]
|
||||||
|
|
||||||
|
h \div.card-header-icon {
|
||||||
|
key: "edit"
|
||||||
|
onclick: ->
|
||||||
|
if model.editing == column.id + ".title"
|
||||||
|
model.editing := undefined
|
||||||
|
else
|
||||||
|
model.editing := column.id + ".title"
|
||||||
|
} [
|
||||||
|
"Edit"
|
||||||
|
]
|
||||||
|
|
||||||
|
if self.tasks.filter((.column == column.id)).length == 0
|
||||||
|
h \div.card-header-icon {
|
||||||
|
key: "delete"
|
||||||
|
onclick: ->
|
||||||
|
model.editing := column.id + ".delete"
|
||||||
|
} [
|
||||||
|
"Delete"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
if model.editing == column.id + ".delete"
|
||||||
|
h \div.card-content [
|
||||||
|
h \div.button.is-fullwidth.is-danger {
|
||||||
|
onclick: ->
|
||||||
|
socket.send JSON.stringify {
|
||||||
|
type: "delete-column"
|
||||||
|
project: self.id
|
||||||
|
column: column.id
|
||||||
|
}
|
||||||
|
} [ "Delete me!"]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
for task in self.tasks
|
||||||
|
continue if task.column != column.id
|
||||||
|
|
||||||
|
task.render!
|
||||||
|
|
||||||
|
h \div.button.is-fullwidth {
|
||||||
|
onclick: ->
|
||||||
|
socket.send JSON.stringify {
|
||||||
|
type: "new-task"
|
||||||
|
project: self.id
|
||||||
|
column: column.id
|
||||||
|
title: "General Kenobi…"
|
||||||
|
description: ""
|
||||||
|
}
|
||||||
|
} [ "New task" ]
|
||||||
|
]
|
||||||
|
|
||||||
|
self.render = ->
|
||||||
|
h \div.project {
|
||||||
|
key: self.id
|
||||||
|
} [
|
||||||
|
h \div.hero.is-dark { key: "title" } [
|
||||||
|
h \div.hero-body [
|
||||||
|
# FIXME: Consider using a .level for this.
|
||||||
|
h \div.is-pulled-right {
|
||||||
|
onclick: ->
|
||||||
|
model.editing := self.id + ".name"
|
||||||
|
} [
|
||||||
|
"Edit"
|
||||||
|
]
|
||||||
|
|
||||||
|
if model.editing == self.id + ".name"
|
||||||
|
h \input.input {
|
||||||
|
onchange: (e) ->
|
||||||
|
model.editing := undefined
|
||||||
|
socket.send JSON.stringify {
|
||||||
|
type: "edit-project"
|
||||||
|
project: self.id
|
||||||
|
name: e.target.value
|
||||||
|
}
|
||||||
|
value: self.name
|
||||||
|
}
|
||||||
|
else
|
||||||
|
h \div.title [ self.name ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
h \div.columns [
|
||||||
|
for dom in self.columns.map((column) -> self.render-column(column))
|
||||||
|
dom
|
||||||
|
|
||||||
|
h \div.column.is-2 {
|
||||||
|
key: "new-column"
|
||||||
|
} [
|
||||||
|
h \div.button.is-fullwidth {
|
||||||
|
onclick: ->
|
||||||
|
socket.send JSON.stringify {
|
||||||
|
type: "new-column"
|
||||||
|
project: self.id
|
||||||
|
name: "Hello, there!"
|
||||||
|
}
|
||||||
|
} [ "New Column" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
self
|
||||||
|
|
||||||
|
model = {
|
||||||
|
current-view: "login"
|
||||||
|
editing: undefined
|
||||||
|
selected: undefined
|
||||||
|
users: {}
|
||||||
|
projects: {}
|
||||||
|
projects-list: []
|
||||||
|
}
|
||||||
|
|
||||||
|
socket = new WebSocket "ws://localhost:8888/socket"
|
||||||
|
|
||||||
|
socket.onopen = (event) ->
|
||||||
|
# Nothing to do here ATM.
|
||||||
|
|
||||||
|
socket.onerror = (event) ->
|
||||||
|
model.state = "websocket-error"
|
||||||
|
projector.schedule-render!
|
||||||
|
|
||||||
|
socket.onclase = (event) ->
|
||||||
|
model.state = "websocket-error"
|
||||||
|
model.websocket-error = event.reason
|
||||||
|
projector.schedule-render!
|
||||||
|
|
||||||
|
socket.onmessage = (event) ->
|
||||||
|
console.log event.data
|
||||||
|
|
||||||
|
message = JSON.parse event.data
|
||||||
|
|
||||||
|
switch message.type
|
||||||
|
when "login"
|
||||||
|
model.current-view := "projects-list"
|
||||||
|
when "list-projects"
|
||||||
|
for project in message.projects
|
||||||
|
model.projects[project.id] = Project project
|
||||||
|
model.projects-list := message.projects
|
||||||
|
when "project"
|
||||||
|
model.projects[message.project.id] = Project message.project
|
||||||
|
when "user"
|
||||||
|
model.users[message.user.uid] := message.user
|
||||||
|
else
|
||||||
|
console.log "RECEIVED UNKNOWN MESSAGE TYPE: #{message.type}"
|
||||||
|
|
||||||
|
projector.schedule-render!
|
||||||
|
|
||||||
|
console.log message
|
||||||
|
|
||||||
|
renderer = ->
|
||||||
|
h \div.section [
|
||||||
|
switch model.current-view
|
||||||
|
when "login"
|
||||||
|
h \div.container [
|
||||||
|
h \div.box [
|
||||||
|
h \form [
|
||||||
|
bulma.field [
|
||||||
|
bulma.label "Login"
|
||||||
|
bulma.input {
|
||||||
|
oninput: (e) ->
|
||||||
|
model.login = e.target.value
|
||||||
|
name: \login
|
||||||
|
id: \login-input
|
||||||
|
}
|
||||||
|
]
|
||||||
|
bulma.field [
|
||||||
|
bulma.label "Password"
|
||||||
|
bulma.input {
|
||||||
|
oninput: (e) ->
|
||||||
|
model.password = e.target.value
|
||||||
|
name: \password
|
||||||
|
type: \password
|
||||||
|
id: \password-input
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
h \button.button.is-fullwidth.is-primary {
|
||||||
|
onclick: (e) ->
|
||||||
|
e.prevent-default!
|
||||||
|
|
||||||
|
socket.send JSON.stringify {
|
||||||
|
type: "login",
|
||||||
|
login: model.login
|
||||||
|
password: model.password
|
||||||
|
}
|
||||||
|
} [ "Letsu go!" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
when "project"
|
||||||
|
h \div [
|
||||||
|
h \div.navbar [
|
||||||
|
h \div.navbar-end [
|
||||||
|
h \a.navbar-item {
|
||||||
|
onclick: ->
|
||||||
|
model.viewed-project := undefined
|
||||||
|
model.current-view := "projects-list"
|
||||||
|
} [ "Go back" ]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
if model.projects[model.viewed-project]
|
||||||
|
model.projects[model.viewed-project].render!
|
||||||
|
]
|
||||||
|
when "projects-list"
|
||||||
|
h \div#projects-list [
|
||||||
|
for project in (model.projects-list || [])
|
||||||
|
h \div.box {
|
||||||
|
key: project.id
|
||||||
|
onclick: ->
|
||||||
|
model.current-view := "project"
|
||||||
|
model.viewed-project := project.id
|
||||||
|
socket.send JSON.stringify {
|
||||||
|
type: "project",
|
||||||
|
project: project.id
|
||||||
|
}
|
||||||
|
} [
|
||||||
|
bulma.title 3 project.name
|
||||||
|
]
|
||||||
|
|
||||||
|
h \div.button.is-primary.is-large.is-fullwidth {
|
||||||
|
onclick: ->
|
||||||
|
socket.send JSON.stringify {
|
||||||
|
type: "new-project"
|
||||||
|
name: "Hello, there!"
|
||||||
|
}
|
||||||
|
} [ "New project! (random atm)" ]
|
||||||
|
]
|
||||||
|
else
|
||||||
|
h \div.notification.is-error [
|
||||||
|
"Wait, what? Internal error!"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
document.add-event-listener 'DOMContentLoaded' ->
|
||||||
|
projector.append document.body, renderer
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
@charset "utf-8"
|
||||||
|
|
||||||
|
@import "../node_modules/bulmaswatch/superhero/_variables.scss"
|
||||||
|
|
||||||
|
// Import Bulma core
|
||||||
|
@import "../node_modules/bulma/bulma"
|
||||||
|
|
||||||
|
@import "../node_modules/bulmaswatch/superhero/_overrides.scss"
|
||||||
|
|
||||||
|
.columns
|
||||||
|
overflow-x: scroll
|
||||||
|
|
||||||
|
.avatar
|
||||||
|
border-radius: 4px
|
||||||
|
|
||||||
|
.card.is-selected
|
||||||
|
border-width: 4px
|
||||||
|
|
||||||
|
.is-column-header + .card, .card + .button
|
||||||
|
margin-top: 12px
|
||||||
|
|
||||||
|
.project > .hero
|
||||||
|
margin-bottom: 12px
|
||||||
|
|
||||||
|
.project
|
||||||
|
margin-top: 12px
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Kanban</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<link rel="stylesheet" href="public/style.css"/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="public/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"name": "kanban",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"directories": {
|
||||||
|
"lib": "lib"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"babel": "^6.23.0",
|
||||||
|
"babel-cli": "^6.26.0",
|
||||||
|
"browserify": "^16.2.3",
|
||||||
|
"browserify-livescript": "^0.2.4",
|
||||||
|
"bulma": "^0.7.5",
|
||||||
|
"bulmaswatch": "^0.7.2",
|
||||||
|
"livescript": "^1.6.0",
|
||||||
|
"maquette": "^3.3.4",
|
||||||
|
"nano-markdown": "^1.2.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
|
||||||
|
package=kanban
|
||||||
|
version=0.1
|
||||||
|
|
||||||
|
targets=()
|
||||||
|
|
||||||
|
targets+=(public/main.js)
|
||||||
|
type[public/main.js]=livescript
|
||||||
|
sources[public/main.js]=client/index.ls
|
||||||
|
depends[public/main.js]="$(ls client/*.ls | grep -v /index.ls$ | tr '\n' ' ')"
|
||||||
|
|
||||||
|
targets+=(public/style.css)
|
||||||
|
type[public/style.css]=css
|
||||||
|
sources[public/style.css]=client/style.sass
|
||||||
|
|
||||||
|
#
|
||||||
|
# implementation details below, bruh~
|
||||||
|
# (seriously, don’t read that unless you want to read that)
|
||||||
|
# (I mean it, dude)
|
||||||
|
#
|
||||||
|
|
||||||
|
function LSC {
|
||||||
|
echo "${fg_bold[blue]} LSC > ${fg_bold[white]}$@${reset_color}"
|
||||||
|
}
|
||||||
|
|
||||||
|
function BUN {
|
||||||
|
echo "${fg_bold[green]} BUN > ${fg_bold[white]}$@${reset_color}"
|
||||||
|
}
|
||||||
|
|
||||||
|
function MIN {
|
||||||
|
echo "${fg_bold[red]} MIN > ${fg_bold[white]}$@${reset_color}"
|
||||||
|
}
|
||||||
|
|
||||||
|
function CSS {
|
||||||
|
echo "${fg_bold[yellow]} CSS > ${fg_bold[white]}$@${reset_color}"
|
||||||
|
}
|
||||||
|
|
||||||
|
function livescript.build {
|
||||||
|
write "${target}: ${target%.js}.bundle.js $(dirname $target)"
|
||||||
|
write "\t@echo '$(MIN ${target%.js}.js)'"
|
||||||
|
write "\t${Q}npx babel --minified ${target%.js}.bundle.js -o ${target}"
|
||||||
|
write "\n"
|
||||||
|
|
||||||
|
write "${target%.js}.bundle.js: ${sources[$target]} ${depends[$target]} $(dirname $target)"
|
||||||
|
write "\t@echo '$(BUN ${target%.js}.bundle.js)'"
|
||||||
|
write "\t${Q}npx browserify -t browserify-livescript ${sources[$target]} -o ${target%.js}.bundle.js"
|
||||||
|
write "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
function livescript.clean {
|
||||||
|
write "${target}.clean:"
|
||||||
|
for file in ${target} ${target%.js}.bundle.js; do
|
||||||
|
write "\t@echo '$(RM ${file})'"
|
||||||
|
write "\t${Q}rm -f ${file}"
|
||||||
|
done
|
||||||
|
write "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
function livescript.install {
|
||||||
|
: FIXME
|
||||||
|
}
|
||||||
|
|
||||||
|
function livescript.uninstall {
|
||||||
|
: FIXME
|
||||||
|
}
|
||||||
|
|
||||||
|
function css.build {
|
||||||
|
write "${target}: ${sources[$target]} $(dirname $target)"
|
||||||
|
write "\t@echo '$(CSS ${target})'"
|
||||||
|
write "\t${Q}sassc ${sources[$target]} > ${target}"
|
||||||
|
write "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
function css.clean {
|
||||||
|
script.clean "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
function css.install {
|
||||||
|
: FIXME
|
||||||
|
}
|
||||||
|
|
||||||
|
function css.uninstall {
|
||||||
|
: FIXME
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
name: kanban
|
||||||
|
version: 0.1.0
|
||||||
|
|
||||||
|
# authors:
|
||||||
|
# - name <email@example.com>
|
||||||
|
|
||||||
|
# description: |
|
||||||
|
# Short description of kanban
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
kemal:
|
||||||
|
github: kemalcr/kemal
|
||||||
|
authd:
|
||||||
|
github: Lukc/authd
|
||||||
|
fs:
|
||||||
|
git: https://git.karchnu.fr/JunkOS/fs.cr
|
||||||
|
|
||||||
|
# pg:
|
||||||
|
# github: will/crystal-pg
|
||||||
|
# version: "~> 0.5"
|
||||||
|
|
||||||
|
# development_dependencies:
|
||||||
|
# webmock:
|
||||||
|
# github: manastech/webmock.cr
|
||||||
|
|
||||||
|
# license: MIT
|
|
@ -0,0 +1,406 @@
|
||||||
|
require "kemal"
|
||||||
|
require "uuid"
|
||||||
|
require "file_utils"
|
||||||
|
|
||||||
|
require "authd"
|
||||||
|
|
||||||
|
class Task
|
||||||
|
JSON.mapping({
|
||||||
|
id: String,
|
||||||
|
author: Int32,
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
column: String,
|
||||||
|
assigned_to: Int32?
|
||||||
|
})
|
||||||
|
|
||||||
|
def initialize(@title, @author, @description, @column)
|
||||||
|
@id = UUID.random.to_s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Column
|
||||||
|
JSON.mapping({
|
||||||
|
id: String,
|
||||||
|
name: String
|
||||||
|
})
|
||||||
|
|
||||||
|
def initialize(@name)
|
||||||
|
@id = UUID.random.to_s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Project
|
||||||
|
JSON.mapping({
|
||||||
|
id: String,
|
||||||
|
name: String,
|
||||||
|
columns: Array(Column),
|
||||||
|
tasks: Array(Task),
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: "dark"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
def initialize(@name)
|
||||||
|
@id = UUID.random.to_s
|
||||||
|
|
||||||
|
@columns = [Column.new "To do"]
|
||||||
|
@tasks = [] of Task
|
||||||
|
|
||||||
|
@color = "dark"
|
||||||
|
|
||||||
|
write!
|
||||||
|
end
|
||||||
|
|
||||||
|
def write!
|
||||||
|
Dir.mkdir_p "./projects/#{@id}"
|
||||||
|
|
||||||
|
File.write "./projects/#{@id}/project.json.new", to_json
|
||||||
|
FileUtils.mv "./projects/#{@id}/project.json.new",
|
||||||
|
"./projects/#{@id}/project.json"
|
||||||
|
end
|
||||||
|
|
||||||
|
# FIXME: Transform that exception somehow.
|
||||||
|
# FIXME: Update to not read everything.
|
||||||
|
def self.get_from_id(id)
|
||||||
|
self.all.find(&.id.==(id)).not_nil!
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.all : Array(Project)
|
||||||
|
# FIXME: switch to an index file per project in a storage directory.
|
||||||
|
Dir.children("./projects").compact_map do |filename|
|
||||||
|
begin
|
||||||
|
Project.from_json File.read "./projects/#{filename}/project.json"
|
||||||
|
rescue
|
||||||
|
end
|
||||||
|
end
|
||||||
|
rescue
|
||||||
|
[] of Project
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class Requests::Login
|
||||||
|
JSON.mapping({
|
||||||
|
type: String,
|
||||||
|
login: String,
|
||||||
|
password: String
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
class Requests::Tasks
|
||||||
|
JSON.mapping({
|
||||||
|
type: String,
|
||||||
|
project: String
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
class Requests::NewProject
|
||||||
|
JSON.mapping({
|
||||||
|
type: String,
|
||||||
|
name: String
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
class Requests::EditProject
|
||||||
|
JSON.mapping({
|
||||||
|
type: String,
|
||||||
|
project: String,
|
||||||
|
name: String?,
|
||||||
|
color: String?
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
class Requests::NewColumn
|
||||||
|
JSON.mapping({
|
||||||
|
type: String,
|
||||||
|
project: String,
|
||||||
|
name: String
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
class Requests::NewTask
|
||||||
|
JSON.mapping({
|
||||||
|
type: String,
|
||||||
|
project: String,
|
||||||
|
column: String,
|
||||||
|
title: String,
|
||||||
|
description: String
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
class Requests::EditTask
|
||||||
|
JSON.mapping({
|
||||||
|
type: String,
|
||||||
|
project: String,
|
||||||
|
task: String,
|
||||||
|
column: String?,
|
||||||
|
title: String?,
|
||||||
|
description: String?,
|
||||||
|
assigned_to: Int32?
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
class Requests::DeleteTask
|
||||||
|
JSON.mapping({
|
||||||
|
type: String,
|
||||||
|
project: String,
|
||||||
|
task: String
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
class Requests::EditColumn
|
||||||
|
JSON.mapping({
|
||||||
|
type: String,
|
||||||
|
project: String,
|
||||||
|
column: String,
|
||||||
|
name: String?
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
class Requests::DeleteColumn
|
||||||
|
JSON.mapping({
|
||||||
|
type: String,
|
||||||
|
project: String,
|
||||||
|
column: String
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
class Requests::GetUser
|
||||||
|
JSON.mapping({
|
||||||
|
type: String,
|
||||||
|
uid: Int32
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
authd = AuthD::Client.new
|
||||||
|
|
||||||
|
Kemal.config.extra_options do |parser|
|
||||||
|
parser.on "-k file", "--jwt-key file", "Provides the JWT key for authd." do |file|
|
||||||
|
authd.key = File.read(file).chomp
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
sockets = [] of HTTP::WebSocket
|
||||||
|
users = {} of HTTP::WebSocket => AuthD::User
|
||||||
|
|
||||||
|
ws "/socket" do |socket|
|
||||||
|
socket.on_close do
|
||||||
|
users.delete socket
|
||||||
|
sockets.delete socket
|
||||||
|
end
|
||||||
|
|
||||||
|
socket.on_message do |message_as_s|
|
||||||
|
message = JSON.parse(message_as_s).as_h
|
||||||
|
|
||||||
|
case message["type"]
|
||||||
|
when "login"
|
||||||
|
request = Requests::Login.from_json message_as_s
|
||||||
|
|
||||||
|
login = request.login
|
||||||
|
password = request.password
|
||||||
|
|
||||||
|
user = authd.get_user? login, password
|
||||||
|
|
||||||
|
unless user
|
||||||
|
socket.send({
|
||||||
|
type: "login-error",
|
||||||
|
error: "Invalid credentials."
|
||||||
|
}.to_json)
|
||||||
|
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
users[socket] = user
|
||||||
|
sockets << socket
|
||||||
|
|
||||||
|
socket.send({
|
||||||
|
type: "login"
|
||||||
|
}.to_json)
|
||||||
|
|
||||||
|
socket.send({
|
||||||
|
type: "list-projects",
|
||||||
|
projects: Project.all
|
||||||
|
}.to_json)
|
||||||
|
when "new-project"
|
||||||
|
user = users[socket] # FIXME: make it an authentication error
|
||||||
|
|
||||||
|
request = Requests::NewProject.from_json message_as_s
|
||||||
|
|
||||||
|
project = Project.new request.name
|
||||||
|
|
||||||
|
# FIXME: Only notify concerned users.
|
||||||
|
sockets.each &.send({
|
||||||
|
type: "project",
|
||||||
|
project: project
|
||||||
|
}.to_json)
|
||||||
|
when "project"
|
||||||
|
user = users[socket] # FIXME: make it an authentication error
|
||||||
|
|
||||||
|
request = Requests::Tasks.from_json message_as_s
|
||||||
|
|
||||||
|
project = Project.get_from_id request.project
|
||||||
|
|
||||||
|
socket.send({
|
||||||
|
type: "project",
|
||||||
|
project: project
|
||||||
|
}.to_json)
|
||||||
|
when "edit-project"
|
||||||
|
user = users[socket] # FIXME: make it an authentication error
|
||||||
|
|
||||||
|
request = Requests::EditProject.from_json message_as_s
|
||||||
|
|
||||||
|
project = Project.get_from_id request.project
|
||||||
|
|
||||||
|
if name = request.name
|
||||||
|
project.name = name
|
||||||
|
end
|
||||||
|
|
||||||
|
if color = request.color
|
||||||
|
project.color = color
|
||||||
|
end
|
||||||
|
|
||||||
|
project.write!
|
||||||
|
|
||||||
|
# FIXME: Only notify concerned users.
|
||||||
|
sockets.each &.send({
|
||||||
|
type: "project",
|
||||||
|
project: project
|
||||||
|
}.to_json)
|
||||||
|
when "new-column"
|
||||||
|
user = users[socket] # FIXME: make it an authentication error
|
||||||
|
|
||||||
|
request = Requests::NewColumn.from_json message_as_s
|
||||||
|
|
||||||
|
project = Project.get_from_id request.project
|
||||||
|
|
||||||
|
project.columns << Column.new request.name
|
||||||
|
project.write!
|
||||||
|
|
||||||
|
# FIXME: Only notify concerned users.
|
||||||
|
sockets.each &.send({
|
||||||
|
type: "project",
|
||||||
|
project: project
|
||||||
|
}.to_json)
|
||||||
|
when "new-task"
|
||||||
|
user = users[socket] # FIXME: make it an authentication error
|
||||||
|
|
||||||
|
request = Requests::NewTask.from_json message_as_s
|
||||||
|
|
||||||
|
project = Project.get_from_id request.project
|
||||||
|
column = project.columns.find(&.id.==(request.column)).not_nil!
|
||||||
|
|
||||||
|
project.tasks << Task.new request.title, user.uid, request.description, column.id
|
||||||
|
|
||||||
|
project.write!
|
||||||
|
|
||||||
|
# FIXME: Only notify concerned users.
|
||||||
|
sockets.each &.send({
|
||||||
|
type: "project",
|
||||||
|
project: project
|
||||||
|
}.to_json)
|
||||||
|
when "edit-task"
|
||||||
|
user = users[socket] # FIXME: make it an authentication error
|
||||||
|
|
||||||
|
request = Requests::EditTask.from_json message_as_s
|
||||||
|
|
||||||
|
project = Project.get_from_id request.project
|
||||||
|
task = project.tasks.find(&.id.==(request.task)).not_nil!
|
||||||
|
|
||||||
|
if column = request.column
|
||||||
|
column = project.columns.find(&.id.==(column)).not_nil!
|
||||||
|
|
||||||
|
task.column = column.id
|
||||||
|
end
|
||||||
|
|
||||||
|
if title = request.title
|
||||||
|
task.title = title
|
||||||
|
end
|
||||||
|
|
||||||
|
if description = request.description
|
||||||
|
task.description = description
|
||||||
|
end
|
||||||
|
|
||||||
|
# FIXME: Check it’s a valid UID.
|
||||||
|
if assigned_to = request.assigned_to
|
||||||
|
# FIXME: Probably not the best way to handle this corner-case.
|
||||||
|
assigned_to = nil if assigned_to == -1
|
||||||
|
|
||||||
|
task.assigned_to = assigned_to
|
||||||
|
end
|
||||||
|
|
||||||
|
project.write!
|
||||||
|
|
||||||
|
# FIXME: Only notify concerned users.
|
||||||
|
sockets.each &.send({
|
||||||
|
type: "project",
|
||||||
|
project: project
|
||||||
|
}.to_json)
|
||||||
|
when "delete-task"
|
||||||
|
user = users[socket]
|
||||||
|
|
||||||
|
request = Requests::DeleteTask.from_json message_as_s
|
||||||
|
|
||||||
|
project = Project.get_from_id request.project
|
||||||
|
|
||||||
|
project.tasks.select! &.id.!=(request.task)
|
||||||
|
|
||||||
|
project.write!
|
||||||
|
|
||||||
|
# FIXME: Only notify concerned users.
|
||||||
|
sockets.each &.send({
|
||||||
|
type: "project",
|
||||||
|
project: project
|
||||||
|
}.to_json)
|
||||||
|
when "edit-column"
|
||||||
|
user = users[socket] # FIXME: make it an authentication error
|
||||||
|
|
||||||
|
request = Requests::EditColumn.from_json message_as_s
|
||||||
|
|
||||||
|
project = Project.get_from_id request.project
|
||||||
|
column = project.columns.find(&.id.==(request.column)).not_nil!
|
||||||
|
|
||||||
|
if name = request.name
|
||||||
|
column.name = name
|
||||||
|
end
|
||||||
|
|
||||||
|
puts project.columns.to_json
|
||||||
|
|
||||||
|
project.write!
|
||||||
|
|
||||||
|
# FIXME: Only notify concerned users.
|
||||||
|
sockets.each &.send({
|
||||||
|
type: "project",
|
||||||
|
project: project
|
||||||
|
}.to_json)
|
||||||
|
when "delete-column"
|
||||||
|
user = users[socket]
|
||||||
|
|
||||||
|
request = Requests::DeleteColumn.from_json message_as_s
|
||||||
|
project = Project.get_from_id request.project
|
||||||
|
|
||||||
|
project.columns.select! &.id.!=(request.column)
|
||||||
|
project.tasks.select! &.column.!=(request.column)
|
||||||
|
|
||||||
|
project.write!
|
||||||
|
|
||||||
|
# FIXME: Only notify concerned users.
|
||||||
|
sockets.each &.send({
|
||||||
|
type: "project",
|
||||||
|
project: project
|
||||||
|
}.to_json)
|
||||||
|
when "get-user"
|
||||||
|
request = Requests::GetUser.from_json message_as_s
|
||||||
|
|
||||||
|
user = authd.get_user? request.uid
|
||||||
|
|
||||||
|
socket.send({
|
||||||
|
type: "user",
|
||||||
|
user: user
|
||||||
|
}.to_json)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Kemal.run
|
||||||
|
|
Loading…
Reference in New Issue