commit 8b5e0a0c026a08462ba6f0728eb0b56d50387a92 Author: Luka Vandervelden Date: Sat Jul 6 02:35:50 2019 +0200 Initial commit. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..69fba48 --- /dev/null +++ b/Makefile @@ -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 ' MIN > public/main.js' + $(Q)npx babel --minified public/main.bundle.js -o public/main.js + + +public/main.bundle.js: client/index.ls public + @echo ' BUN > public/main.bundle.js' + $(Q)npx browserify -t browserify-livescript client/index.ls -o public/main.bundle.js + + +public/main.js.clean: + @echo ' RM > public/main.js' + $(Q)rm -f public/main.js + @echo ' RM > public/main.bundle.js' + $(Q)rm -f public/main.bundle.js + + +public/style.css: client/style.sass public + @echo ' CSS > public/style.css' + $(Q)sassc client/style.sass > public/style.css + + +public/style.css.clean: + +public: + $(Q)mkdir -p public +$(DESTDIR)$(PREFIX): + @echo ' DIR > $(PREFIX)' + $(Q)mkdir -p $(DESTDIR)$(PREFIX) +$(DESTDIR)$(BINDIR): + @echo ' DIR > $(BINDIR)' + $(Q)mkdir -p $(DESTDIR)$(BINDIR) +$(DESTDIR)$(LIBDIR): + @echo ' DIR > $(LIBDIR)' + $(Q)mkdir -p $(DESTDIR)$(LIBDIR) +$(DESTDIR)$(SHAREDIR): + @echo ' DIR > $(SHAREDIR)' + $(Q)mkdir -p $(DESTDIR)$(SHAREDIR) +$(DESTDIR)$(INCLUDEDIR): + @echo ' DIR > $(INCLUDEDIR)' + $(Q)mkdir -p $(DESTDIR)$(INCLUDEDIR) +$(DESTDIR)$(MANDIR): + @echo ' DIR > $(MANDIR)' + $(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 ' TAR > $(PACKAGE)-$(VERSION).tar.gz' + $(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 ' TAR > $(PACKAGE)-$(VERSION).tar.xz' + $(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 ' TAR > $(PACKAGE)-$(VERSION).tar.bz2' + $(Q)tar cjf $(PACKAGE)-$(VERSION).tar.bz2 \ + $(PACKAGE)-$(VERSION)/client/index.ls \ + $(PACKAGE)-$(VERSION)/client/style.sass + +help: + @echo ' :: kanban-0.1' + @echo '' + @echo 'Generic targets:' + @echo ' - help  Prints this help message.' + @echo ' - all  Builds all targets.' + @echo ' - dist  Creates tarballs of the files of the project.' + @echo ' - install  Installs the project.' + @echo ' - clean  Removes compiled files.' + @echo ' - uninstall  Deinstalls the project.' + @echo '' + @echo 'CLI-modifiable variables:' + @echo ' - CC  ${CC}' + @echo ' - CFLAGS  ${CFLAGS}' + @echo ' - LDFLAGS  ${LDFLAGS}' + @echo ' - DESTDIR  ${DESTDIR}' + @echo ' - PREFIX  ${PREFIX}' + @echo ' - BINDIR  ${BINDIR}' + @echo ' - LIBDIR  ${LIBDIR}' + @echo ' - SHAREDIR  ${SHAREDIR}' + @echo ' - INCLUDEDIR  ${INCLUDEDIR}' + @echo ' - MANDIR  ${MANDIR}' + @echo '' + @echo 'Project targets: ' + @echo ' - public/main.js livescript' + @echo ' - public/style.css css' + @echo '' + @echo 'Makefile options:' + @echo ' - gnu: false' + @echo ' - colors: true' + @echo '' + @echo 'Rebuild the Makefile with:' + @echo ' zsh ./build.zsh -c' +.PHONY: all subdirs clean distclean dist install uninstall help + diff --git a/client/bulma.ls b/client/bulma.ls new file mode 100644 index 0000000..ad315a6 --- /dev/null +++ b/client/bulma.ls @@ -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 +} + diff --git a/client/index.ls b/client/index.ls new file mode 100644 index 0000000..2794895 --- /dev/null +++ b/client/index.ls @@ -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 + diff --git a/client/style.sass b/client/style.sass new file mode 100644 index 0000000..0bd170b --- /dev/null +++ b/client/style.sass @@ -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 + diff --git a/index.html b/index.html new file mode 100644 index 0000000..779c98b --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + Kanban + + + + + + + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..a2a4465 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/project.zsh b/project.zsh new file mode 100644 index 0000000..a589ab6 --- /dev/null +++ b/project.zsh @@ -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 +} + diff --git a/shard.yml b/shard.yml new file mode 100644 index 0000000..a4656c8 --- /dev/null +++ b/shard.yml @@ -0,0 +1,26 @@ +name: kanban +version: 0.1.0 + +# authors: +# - name + +# 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 diff --git a/src/main.cr b/src/main.cr new file mode 100644 index 0000000..cbad105 --- /dev/null +++ b/src/main.cr @@ -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 +