code split
parent
660e7a1860
commit
3ce9915469
46
Makefile
46
Makefile
|
@ -7,18 +7,37 @@ LIBDIR := $(PREFIX)/lib
|
|||
SHAREDIR := $(PREFIX)/share
|
||||
INCLUDEDIR := $(PREFIX)/include
|
||||
MANDIR := $(SHAREDIR)/man
|
||||
CRFLAGS := --release
|
||||
|
||||
Q := @
|
||||
|
||||
all: main.js style.css
|
||||
all: todoc main.js style.css
|
||||
@:
|
||||
|
||||
todoc: src/client.cr
|
||||
@echo '[01;35m CR > [01;37mtodoc[00m'
|
||||
$(Q)crystal build $(CRFLAGS) src/client.cr -o 'todoc'
|
||||
|
||||
|
||||
todoc.install: todoc
|
||||
@echo '[01;31m IN > [01;37m$(BINDIR)/todoc[00m'
|
||||
$(Q)mkdir -p '$(DESTDIR)$(BINDIR)'
|
||||
$(Q)install -m0755 todoc $(DESTDIR)$(BINDIR)/todoc
|
||||
|
||||
todoc.clean:
|
||||
@echo '[01;37m RM > [01;37mtodoc[00m'
|
||||
$(Q)rm -f todoc
|
||||
|
||||
todoc.uninstall:
|
||||
@echo '[01;37m RM > [01;37m$(BINDIR)/todoc[00m'
|
||||
$(Q)rm -f '$(DESTDIR)$(BINDIR)/todoc'
|
||||
|
||||
main.js: main.bundle.js
|
||||
@echo '[01;31m MIN > [01;37mmain.js[00m'
|
||||
$(Q)npx babel --minified main.bundle.js -o main.js
|
||||
|
||||
|
||||
main.bundle.js: client/index.ls client/authd.ls client/bulma.ls client/todod.ls
|
||||
main.bundle.js: client/index.ls client/authd.ls client/bulma.ls client/project.ls client/task.ls client/todowebsocket.ls
|
||||
@echo '[01;32m BUN > [01;37mmain.bundle.js[00m'
|
||||
$(Q)npx browserify -t browserify-livescript client/index.ls -o main.bundle.js
|
||||
|
||||
|
@ -75,13 +94,13 @@ $(DESTDIR)$(INCLUDEDIR):
|
|||
$(DESTDIR)$(MANDIR):
|
||||
@echo '[01;35m DIR > [01;37m$(MANDIR)[00m'
|
||||
$(Q)mkdir -p $(DESTDIR)$(MANDIR)
|
||||
install: main.js.install style.css.install
|
||||
install: todoc.install main.js.install style.css.install
|
||||
@:
|
||||
|
||||
uninstall: main.js.uninstall style.css.uninstall
|
||||
uninstall: todoc.uninstall main.js.uninstall style.css.uninstall
|
||||
@:
|
||||
|
||||
clean: main.js.clean style.css.clean
|
||||
clean: todoc.clean main.js.clean style.css.clean
|
||||
distclean: clean
|
||||
dist: dist-gz dist-xz dist-bz2
|
||||
$(Q)rm -- $(PACKAGE)-$(VERSION)
|
||||
|
@ -94,31 +113,40 @@ 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)/src/client.cr \
|
||||
$(PACKAGE)-$(VERSION)/client/index.ls \
|
||||
$(PACKAGE)-$(VERSION)/client/style.sass \
|
||||
$(PACKAGE)-$(VERSION)/client/authd.ls \
|
||||
$(PACKAGE)-$(VERSION)/client/bulma.ls \
|
||||
$(PACKAGE)-$(VERSION)/client/todod.ls
|
||||
$(PACKAGE)-$(VERSION)/client/project.ls \
|
||||
$(PACKAGE)-$(VERSION)/client/task.ls \
|
||||
$(PACKAGE)-$(VERSION)/client/todowebsocket.ls
|
||||
|
||||
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)/src/client.cr \
|
||||
$(PACKAGE)-$(VERSION)/client/index.ls \
|
||||
$(PACKAGE)-$(VERSION)/client/style.sass \
|
||||
$(PACKAGE)-$(VERSION)/client/authd.ls \
|
||||
$(PACKAGE)-$(VERSION)/client/bulma.ls \
|
||||
$(PACKAGE)-$(VERSION)/client/todod.ls
|
||||
$(PACKAGE)-$(VERSION)/client/project.ls \
|
||||
$(PACKAGE)-$(VERSION)/client/task.ls \
|
||||
$(PACKAGE)-$(VERSION)/client/todowebsocket.ls
|
||||
|
||||
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)/src/client.cr \
|
||||
$(PACKAGE)-$(VERSION)/client/index.ls \
|
||||
$(PACKAGE)-$(VERSION)/client/style.sass \
|
||||
$(PACKAGE)-$(VERSION)/client/authd.ls \
|
||||
$(PACKAGE)-$(VERSION)/client/bulma.ls \
|
||||
$(PACKAGE)-$(VERSION)/client/todod.ls
|
||||
$(PACKAGE)-$(VERSION)/client/project.ls \
|
||||
$(PACKAGE)-$(VERSION)/client/task.ls \
|
||||
$(PACKAGE)-$(VERSION)/client/todowebsocket.ls
|
||||
|
||||
help:
|
||||
@echo '[01;37m :: todo-webclient-0.1[00m'
|
||||
|
@ -132,6 +160,7 @@ help:
|
|||
@echo '[00m - [01;32muninstall [37m Deinstalls the project.[00m'
|
||||
@echo ''
|
||||
@echo '[01;37mCLI-modifiable variables:[00m'
|
||||
@echo ' - [01;34mCRFLAGS [37m ${CRFLAGS}[00m'
|
||||
@echo ' - [01;34mPREFIX [37m ${PREFIX}[00m'
|
||||
@echo ' - [01;34mBINDIR [37m ${BINDIR}[00m'
|
||||
@echo ' - [01;34mLIBDIR [37m ${LIBDIR}[00m'
|
||||
|
@ -140,6 +169,7 @@ help:
|
|||
@echo ' - [01;34mMANDIR [37m ${MANDIR}[00m'
|
||||
@echo ''
|
||||
@echo '[01;37mProject targets: [00m'
|
||||
@echo ' - [01;33mtodoc [37m crystal[00m'
|
||||
@echo ' - [01;33mmain.js [37m livescript[00m'
|
||||
@echo ' - [01;33mstyle.css [37m sass[00m'
|
||||
@echo ''
|
||||
|
|
|
@ -3,7 +3,6 @@ h = require 'maquette' .h
|
|||
|
||||
|
||||
module.exports = {
|
||||
|
||||
login-widget: (model, socket) ->
|
||||
h \div.container [
|
||||
h \div.box [
|
||||
|
|
405
client/index.ls
405
client/index.ls
|
@ -9,347 +9,16 @@
|
|||
#
|
||||
|
||||
maquette = require "maquette"
|
||||
nmd = require "nano-markdown"
|
||||
authd = require "./authd.ls"
|
||||
console.log authd
|
||||
nmd = require "nano-markdown"
|
||||
authd = require "./authd.ls"
|
||||
todows = require "./todowebsocket.ls"
|
||||
bulma = require "./bulma.ls"
|
||||
Task = require "./task.ls"
|
||||
Project = require "./project.ls"
|
||||
|
||||
{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.get-user self.author
|
||||
|
||||
assigned_to = model.users[self.assigned_to]
|
||||
if self.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.get-user self.assigned_to
|
||||
|
||||
is-selected = model.selected == self.id
|
||||
|
||||
h (\div.card.is- + (self.color || "dark")), {
|
||||
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.edit-task project.id, self.id, {
|
||||
title: e.target.value
|
||||
}
|
||||
} [ self.title ]
|
||||
else
|
||||
h \a [
|
||||
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 && self.description != ""
|
||||
h \div.media-right {key: "description-icon"} [
|
||||
h \span.icon.is-size-1 [ "🗎" ]
|
||||
]
|
||||
|
||||
if is-selected
|
||||
h \div.media-right {key: "edit"} [
|
||||
h \a.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 \a.small {
|
||||
onclick: ->
|
||||
model.editing := self.id + ".delete"
|
||||
} [
|
||||
"Delete"
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
if is-selected
|
||||
h \div.content {
|
||||
key: self.description
|
||||
after-create: (dom) ->
|
||||
dom.innerHTML = nmd self.description
|
||||
} [
|
||||
if model.editing == self.id + ".description"
|
||||
h \form.form [
|
||||
h \textarea.textarea {
|
||||
value: model.editing-data
|
||||
oninput: (e) ->
|
||||
model.editing-data := e.target.value
|
||||
}
|
||||
h \div.button.is-fullwidth {
|
||||
onclick: ->
|
||||
socket.edit-task project.id, 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-data := self.description
|
||||
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.edit-task project.id, self.id, {
|
||||
assigned_to: Number e.target.value
|
||||
}
|
||||
}
|
||||
]
|
||||
else
|
||||
h \a.card-footer-item {
|
||||
key: "assign"
|
||||
onclick: ->
|
||||
model.editing := self.id + ".assigned_to"
|
||||
} [ "Assign" ]
|
||||
]
|
||||
|
||||
if is-selected
|
||||
h \div.card-footer {key: "color"} [
|
||||
if model.editing == self.id + ".color"
|
||||
h \div.card-footer-item {
|
||||
key: "color.clicked"
|
||||
} [
|
||||
h \input.input {
|
||||
onchange: (e) ->
|
||||
model.editing := undefined
|
||||
socket.edit-task project.id, self.id, {
|
||||
color: e.target.value
|
||||
}
|
||||
}
|
||||
]
|
||||
else
|
||||
h \a.card-footer-item {
|
||||
key: "assign"
|
||||
onclick: ->
|
||||
model.editing := self.id + ".color"
|
||||
} [ "Change Color" ]
|
||||
]
|
||||
|
||||
if is-selected
|
||||
h \div.card-footer {key: "move"} [
|
||||
h \a.card-footer-item {
|
||||
key: "⇐"
|
||||
onclick: ->
|
||||
socket.edit-task project.id, self.id, {
|
||||
column: get-previous project.columns.map((.id)), self.column
|
||||
}
|
||||
} [ "⇐" ]
|
||||
|
||||
if model.editing == self.id + ".delete"
|
||||
h \a.card-footer-item {
|
||||
key: "delete"
|
||||
} [
|
||||
h \div.button.is-danger {
|
||||
onclick: ->
|
||||
socket.delete-task project.id, self.id
|
||||
} [ "Delete! For real!" ]
|
||||
]
|
||||
|
||||
h \a.card-footer-item {
|
||||
key: "⇒"
|
||||
onclick: ->
|
||||
socket.edit-task project.id, 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-3 {
|
||||
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.edit-column self.id, column.id, {
|
||||
name: e.target.value
|
||||
}
|
||||
}
|
||||
else
|
||||
h \div.card-header-title [
|
||||
bulma.title 3 column.name
|
||||
]
|
||||
|
||||
h \a.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 \a.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.delete-column self.id, column.id
|
||||
} [ "Delete me!"]
|
||||
]
|
||||
]
|
||||
|
||||
for task in self.tasks
|
||||
continue if task.column != column.id
|
||||
|
||||
task.render!
|
||||
|
||||
h \div.button.is-fullwidth {
|
||||
onclick: ->
|
||||
socket.new-task self.id, 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.edit-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.new-column self.id, "Hello, there!"
|
||||
} [ "New Column" ]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
self
|
||||
|
||||
model = {
|
||||
current-view: "login"
|
||||
editing: undefined
|
||||
|
@ -357,34 +26,56 @@ model = {
|
|||
users: {}
|
||||
projects: {}
|
||||
list-todos: []
|
||||
connection: {
|
||||
port: 9999
|
||||
url: undefined
|
||||
socket: undefined
|
||||
}
|
||||
}
|
||||
|
||||
protocol = if location.protocol == 'https' then 'wss' else 'ws'
|
||||
port = 9999
|
||||
# socket-url = protocol + '://' + location.hostname + \: + port + "/kanban.JSON"
|
||||
socket-url = 'ws://www.junkos.netlib.re:' + port + "/kanban.JSON"
|
||||
# '://' + location.hostname + \: +
|
||||
model.connection.url =
|
||||
(if location.protocol == 'https' then 'wss' else 'ws') +
|
||||
"://www.junkos.netlib.re:" +
|
||||
model.connection.port +
|
||||
"/kanban.JSON"
|
||||
|
||||
console.log socket-url
|
||||
console.log model.connection.url
|
||||
|
||||
|
||||
socket = todod.create-socket socket-url
|
||||
#
|
||||
# network configuration
|
||||
#
|
||||
|
||||
socket.on-close = ->
|
||||
socket = todows.create-socket model.connection.url
|
||||
|
||||
on-websocket-error = (event) ->
|
||||
console.log "WebSocket error.", event
|
||||
model.state := "network-error"
|
||||
projector.schedule-render!
|
||||
|
||||
on-websocket-close = ->
|
||||
model.current-view := "login"
|
||||
socket.reopen!
|
||||
# socket.reopen!
|
||||
|
||||
socket.on-message = (event) ->
|
||||
message = JSON.parse event.data
|
||||
# Exporting the error in case the UI is able to deal with it.
|
||||
model.previous-state := model.state
|
||||
model.state := "network-error"
|
||||
model.websocket-error := event.reason
|
||||
projector.schedule-render!
|
||||
|
||||
on-websocket-message = (data) ->
|
||||
message = JSON.parse data
|
||||
|
||||
switch message.type
|
||||
when "login"
|
||||
model.current-view := "list-todos"
|
||||
when "list-projects"
|
||||
for project in message.projects
|
||||
model.projects[project.id] = Project project
|
||||
model.projects[project.id] = Project.new project, model, socket
|
||||
model.list-todos := message.projects
|
||||
when "project"
|
||||
model.projects[message.project.id] = Project message.project
|
||||
model.projects[message.project.id] = Project.new message.project, model, socket
|
||||
when "user"
|
||||
if message.user
|
||||
model.users[message.user.uid] := message.user
|
||||
|
@ -397,6 +88,22 @@ socket.on-message = (event) ->
|
|||
|
||||
console.log message
|
||||
|
||||
# record changes that need to happen on a network event
|
||||
socket.user-on-error ++= [ on-websocket-error ]
|
||||
socket.user-on-close ++= [ on-websocket-close ]
|
||||
socket.user-on-message ++= [ on-websocket-message ]
|
||||
|
||||
|
||||
#
|
||||
# Pages:
|
||||
# login
|
||||
# todo-list
|
||||
# todo
|
||||
# network-error
|
||||
#
|
||||
|
||||
|
||||
|
||||
renderer = ->
|
||||
render-navbar = ->
|
||||
h \div.navbar [
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
|
||||
h = require 'maquette' .h
|
||||
bulma = require "./bulma.ls"
|
||||
Task = require "./task.ls"
|
||||
|
||||
module.exports = {
|
||||
|
||||
new: (self, model, socket) ->
|
||||
self.tasks = self.tasks.map (e) -> Task.new e, self, model, socket
|
||||
|
||||
self.render-column = (column) ->
|
||||
h \div.column.is-3 {
|
||||
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.edit-column self.id, column.id, {
|
||||
name: e.target.value
|
||||
}
|
||||
}
|
||||
else
|
||||
h \div.card-header-title [
|
||||
bulma.title 3 column.name
|
||||
]
|
||||
|
||||
h \a.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 \a.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.delete-column self.id, column.id
|
||||
} [ "Delete me!"]
|
||||
]
|
||||
]
|
||||
|
||||
for task in self.tasks
|
||||
continue if task.column != column.id
|
||||
|
||||
task.render!
|
||||
|
||||
h \div.button.is-fullwidth {
|
||||
onclick: ->
|
||||
socket.new-task self.id, 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.edit-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.new-column self.id, "Hello, there!"
|
||||
} [ "New Column" ]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
self
|
||||
}
|
|
@ -0,0 +1,230 @@
|
|||
#
|
||||
# Tasks, previous version of todos
|
||||
#
|
||||
|
||||
h = require 'maquette' .h
|
||||
bulma = require "./bulma.ls"
|
||||
nmd = require "nano-markdown"
|
||||
|
||||
#
|
||||
# generic functions
|
||||
#
|
||||
|
||||
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
|
||||
|
||||
|
||||
module.exports = {
|
||||
|
||||
new: (self, project, model, socket) ->
|
||||
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.get-user self.author
|
||||
|
||||
assigned_to = model.users[self.assigned_to]
|
||||
if self.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.get-user self.assigned_to
|
||||
|
||||
is-selected = model.selected == self.id
|
||||
|
||||
h (\div.card.is- + (self.color || "dark")), {
|
||||
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.edit-task project.id, self.id, {
|
||||
title: e.target.value
|
||||
}
|
||||
} [ self.title ]
|
||||
else
|
||||
h \a [
|
||||
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 && self.description != ""
|
||||
h \div.media-right {key: "description-icon"} [
|
||||
h \span.icon.is-size-1 [ "🗎" ]
|
||||
]
|
||||
|
||||
if is-selected
|
||||
h \div.media-right {key: "edit"} [
|
||||
h \a.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 \a.small {
|
||||
onclick: ->
|
||||
model.editing := self.id + ".delete"
|
||||
} [
|
||||
"Delete"
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
if is-selected
|
||||
h \div.content {
|
||||
key: self.description
|
||||
after-create: (dom) ->
|
||||
dom.innerHTML = nmd self.description
|
||||
} [
|
||||
if model.editing == self.id + ".description"
|
||||
h \form.form [
|
||||
h \textarea.textarea {
|
||||
value: model.editing-data
|
||||
oninput: (e) ->
|
||||
model.editing-data := e.target.value
|
||||
}
|
||||
h \div.button.is-fullwidth {
|
||||
onclick: ->
|
||||
socket.edit-task project.id, 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-data := self.description
|
||||
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.edit-task project.id, self.id, {
|
||||
assigned_to: Number e.target.value
|
||||
}
|
||||
}
|
||||
]
|
||||
else
|
||||
h \a.card-footer-item {
|
||||
key: "assign"
|
||||
onclick: ->
|
||||
model.editing := self.id + ".assigned_to"
|
||||
} [ "Assign" ]
|
||||
]
|
||||
|
||||
if is-selected
|
||||
h \div.card-footer {key: "color"} [
|
||||
if model.editing == self.id + ".color"
|
||||
h \div.card-footer-item {
|
||||
key: "color.clicked"
|
||||
} [
|
||||
h \input.input {
|
||||
onchange: (e) ->
|
||||
model.editing := undefined
|
||||
socket.edit-task project.id, self.id, {
|
||||
color: e.target.value
|
||||
}
|
||||
}
|
||||
]
|
||||
else
|
||||
h \a.card-footer-item {
|
||||
key: "assign"
|
||||
onclick: ->
|
||||
model.editing := self.id + ".color"
|
||||
} [ "Change Color" ]
|
||||
]
|
||||
|
||||
if is-selected
|
||||
h \div.card-footer {key: "move"} [
|
||||
h \a.card-footer-item {
|
||||
key: "⇐"
|
||||
onclick: ->
|
||||
socket.edit-task project.id, self.id, {
|
||||
column: get-previous project.columns.map((.id)), self.column
|
||||
}
|
||||
} [ "⇐" ]
|
||||
|
||||
if model.editing == self.id + ".delete"
|
||||
h \a.card-footer-item {
|
||||
key: "delete"
|
||||
} [
|
||||
h \div.button.is-danger {
|
||||
onclick: ->
|
||||
socket.delete-task project.id, self.id
|
||||
} [ "Delete! For real!" ]
|
||||
]
|
||||
|
||||
h \a.card-footer-item {
|
||||
key: "⇒"
|
||||
onclick: ->
|
||||
socket.edit-task project.id, self.id, {
|
||||
column: get-next project.columns.map((.id)), self.column
|
||||
}
|
||||
} [ "⇒" ]
|
||||
]
|
||||
]
|
||||
|
||||
self
|
||||
}
|
|
@ -8,27 +8,29 @@ module.exports = {
|
|||
create-socket: (socket-url) ->
|
||||
self = {}
|
||||
|
||||
# users can record functions to run on events
|
||||
self.user-on-error = []
|
||||
self.user-on-close = []
|
||||
self.user-on-message = []
|
||||
|
||||
self.open-socket = ->
|
||||
console.log "Opening socket to #{socket-url}"
|
||||
self.socket := new WebSocket socket-url
|
||||
|
||||
self.socket.onerror = (event) ->
|
||||
console.log "WebSocket error.", event
|
||||
model.state := "websocket-error"
|
||||
for f in self.user-on-error
|
||||
f event
|
||||
self.socket.close!
|
||||
projector.schedule-render!
|
||||
|
||||
self.socket.onclose = (event) ->
|
||||
# Exporting the error in case the UI is able to deal with it.
|
||||
model.previous-state := model.state
|
||||
model.state := "websocket-error"
|
||||
model.websocket-error := event.reason
|
||||
projector.schedule-render!
|
||||
for f in self.user-on-close
|
||||
f event
|
||||
|
||||
self.socket.onmessage = (event) ->
|
||||
payload = JSON.parse(event.data).payload
|
||||
data = JSON.parse(event.data).payload
|
||||
|
||||
self.on-message {data: payload}
|
||||
for f in self.user-on-message
|
||||
f data
|
||||
|
||||
self.reopen = ->
|
||||
self.socket.close!
|
|
@ -2,6 +2,11 @@
|
|||
package=todo-webclient
|
||||
version=0.1
|
||||
|
||||
targets=(todoc)
|
||||
type[todoc]=crystal
|
||||
sources[todoc]=src/client.cr
|
||||
depends[todoc]="$(find src | grep .cr$ | grep -v src/client.cr)"
|
||||
|
||||
targets+=(main.js)
|
||||
type[main.js]=livescript
|
||||
sources[main.js]=client/index.ls
|
||||
|
|
Loading…
Reference in New Issue