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.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 selected: undefined users: {} projects: {} projects-list: [] } protocol = if location.protocol == 'https' then 'wss' else 'ws' port = 9999 socket-url = protocol + '://' + location.hostname + \: + port + "/kanban.JSON" console.log socket-url KanbanSocket = (socket-url) -> self = {} 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" 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! self.socket.onmessage = (event) -> payload = JSON.parse(event.data).payload self.on-message {data: payload} self.reopen = -> self.socket.close! self.open-socket! self.open-socket! self.send = (opts) -> console.log JSON.stringify { mtype: 0, payload: opts } self.socket.send JSON.stringify { mtype: 0, payload: opts } self.get-project = (project-id) -> self.send JSON.stringify { type: "project", project: project-id } self.login = (login, password) -> self.send JSON.stringify { type: "login", login: login password: password } self.get-user = (uid) -> self.send JSON.stringify { type: "get-user" uid: uid } self.new-project = (name) -> self.send JSON.stringify { type: "new-project" name: name } self.edit-task = (project-id, task-id, options) -> payload = { type: "edit-task" project: project-id task: task-id } for key, value of options payload[key] = value self.send JSON.stringify payload self.delete-task = (project-id, task-id) -> self.send JSON.stringify { type: "delete-task" project: project-id task: task-id } self.edit-column = (project-id, column-id, options) -> payload = { type: "edit-column" project: project-id column: column-id } for key, value of options payload[key] = value self.send JSON.stringify payload self.delete-column = (project-id, column-id) -> self.send JSON.stringify { type: "delete-column" project: project-id column: column-id } self.new-column = (project-id, name) -> self.send JSON.stringify { type: "new-column" project: project-id name: name } self.new-task = (project-id, column-id, options) -> payload = { type: "new-task" project: project-id column: column-id } for key, value of options payload[key] = value self.send JSON.stringify payload self.edit-project = (project-id, options) -> payload = { type: "edit-project" project: project-id } for key, value of options payload[key] = value self.send JSON.stringify payload self socket = KanbanSocket socket-url socket.on-close = -> model.current-view := "login" socket.reopen! socket.on-message = (event) -> 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" if message.user model.users[message.user.uid] := message.user when "login-error" model.login-error = message.error else console.log "RECEIVED UNKNOWN MESSAGE TYPE: #{message.type}" projector.schedule-render! console.log message renderer = -> render-navbar = -> h \div.navbar [ h \div.navbar-start [ h \a.navbar-item.is-size-1 { onclick: -> model.viewed-project := undefined model.current-view := "projects-list" } [ "βŒ‚" ] ] h \div.navbar-end [ h \a.navbar-item { onclick: -> model.current-view := "login" socket.reopen! } [ "Logout" ] ] ] h \div.section [ switch model.current-view when "login" h \div.container [ h \div.box [ if model.login-error h \div.notification.is-danger [ model.login-error ] 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! model.login-error = undefined socket.login model.login, model.password } [ "Letsu go!" ] ] ] ] when "project" h \div [ render-navbar! if model.projects[model.viewed-project] model.projects[model.viewed-project].render! ] when "projects-list" h \div#projects-list [ render-navbar! h \div.section (model.projects-list || []).map (project) -> h \a.box { key: project.id onclick: -> model.current-view := "project" model.viewed-project := project.id socket.get-project project.id } [ bulma.title 3 project.name ] h \div.button.is-primary.is-large.is-fullwidth { onclick: -> socket.new-project "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