tickets
All repositories: gitoria
9.2 KB
// components/ticket_list.hl — `/`, `/project/:projectName` (also `/projects/:projectName`),// `/state/:stateSlug` and both:// the tickets, newest update first, filtered by project and state, and the form that// opens a new ticket. Live: a ticket created or changed anywhere (web or API) lands in,// moves within or leaves this list without a reload. Ticket #12: the summary is an <md-editor>// around the textarea (shared/md-editor.js; the textarea keeps the Markdown).// Ticket #20: a project's page (`/projects/<slug>`, also `/project/<slug>`) shows its title, description// (Markdown), its tickets and — for an admin — the link to its settings page. The new-ticket form needs the// role edit or admin: its project choice holds only the projects the user may open tickets in.parent './main.hl'import { ticketRows, projectLinks, stateLinks, stateOfSlug, createTicket, projectByAnySlug, editableProjects, isAdminOf, roleOf } from '../store.hl'import { markdownBlocks } from '../markdown.hl'import { siteName } from '../project.hl'import { canWriteSession, writerOfSession, userOfSession } from '../users.hl'import { connectionRows, disconnect } from '../connections.hl'import Markdown from './markdown.hl'projectName = nullstateSlug = nullfilterState = stateOfSlug(stateSlug)rows = ticketRows(projectName, filterState)projects = projectLinks(projectName, filterState)stateFilters = stateLinks(projectName, filterState)session = nullproject = projectName != null ? projectByAnySlug(projectName) : nullnoSuchProject = projectName != null && project == nullprojectId = project != null ? project.id : ''hasDescription = project != null && project.description != null && project.description != ''descriptionMd = hasDescription ? markdownBlocks(project.description) : []isAdmin = project != null && isAdminOf(project.id, userOfSession(session))// ticket #21: the apps connected to this project (connections.hl), shown to everybody; an admin can disconnectconnections = project != null ? connectionRows(project.id) : []hasConnections = connections.length > 0connectionMessage = ''settingsHref = project != null ? '/projects/' + project.slug + '/settings' : ''isEmpty = rows.length == 0heading = (projectName != null ? (project != null ? project.title : projectName) : 'All projects') + (filterState != null ? ' · ' + filterState : '')__title = siteName + ' | ' + heading// the new-ticket formmyProjects = editableProjects(userOfSession(session))newProject = projectName != null && project != null ? project.slug : (myProjects.length > 0 ? myProjects[0].slug : '')newSubject = ''newSummary = ''// only for a member with the role edit or admin (ticket #20): the author is that usercanWrite = myProjects.length > 0canCreateProject = canWriteSession(session)mustLogIn = userOfSession(session) == nullnoRole = userOfSession(session) != null && myProjects.length == 0notice = ''noticeHref = ''hasNotice = falseView {ticketBoard {h1 { id = "heading" heading }ticketFilters {nav { id = "projectfilter"for (f of projects) { a { href = f.href class = f.cls f.label } }}nav { id = "statefilter"for (f of stateFilters) { a { href = f.href class = f.cls f.label } }}}if (noSuchProject) { p { id = "noproject" class = "empty" "There is no such project." } }if (hasDescription) { projectDescription { id = "description" Markdown { blocks = descriptionMd } } }if (hasConnections) { ul { id = "connections" class = "connections"for (c of connections) {li { class = "connection""Connected to " strong { c.text }if (isAdmin) { button { type = "button" class = "quiet danger disconnect" "Disconnect" on click(e) { emit dropConnection(c.id) } } }}}} }p { id = "connectionmessage" class = "message" connectionMessage }projectActions {if (canCreateProject) { a { id = "newprojectlink" class = "button" href = "/new-project" "New project" } }if (isAdmin) { a { id = "settingslink" class = "button" href = settingsHref "Project settings" } }}if (mustLogIn) { p { id = "loginhint" class = "hint" "Reading is open to everyone. To open a ticket, log in with ident (top right)." } }if (noRole) { p { id = "rolehint" class = "hint" "To open a ticket you need the role edit or admin in a project — ask an admin of the project to invite you." } }if (canWrite) { details { id = "newticket"summary { "New ticket" }form { id = "newticketform"on submit(e) {e.preventDefault()emit create(e)}label { "Project"select { id = "newproject" name = "project" value = newProject on change(e) { emit setProject(e.target.value) }for (p of myProjects) { option { value = p.slug p.title } }}}label { "Subject"input { id = "newsubject" name = "subject" required = "required" maxlength = "200" value = newSubject on input(e) { emit setSubject(e.target.value) } }}label { "Summary"mdEditor { textarea { id = "newsummary" name = "summary" rows = "4" value = newSummary on input(e) { emit setSummary(e.target.value) } } }}button { id = "newsubmit" type = "submit" "Open ticket" }}} }if (hasNotice) { p { id = "notice" class = "notice" notice " " a { id = "noticelink" href = noticeHref "open it" } } }if (isEmpty) { p { id = "empty" class = "empty" "No tickets here." } }ol { id = "tickets" class = "tickets"for (row of rows) {li {ticketState { class = row.stateSlug row.state }a { class = "subject" href = row.href ticketRef { row.ref } row.subject }ticketMeta {a { href = row.projectHref row.projectTitle }if (row.hasAssignee) { assignee { row.assignee } }time { row.updated }}}}}}}on setProject(v) { newProject = v }on setSubject(v) { newSubject = v }on setSummary(v) { newSummary = v }on create(e) {if (newProject.trim() == '' || newSubject.trim() == '') { return null }let r = emit server openTicket(newProject, newSubject, newSummary)if (r != null && r.error == null) {notice = 'Opened ' + r.project + ' ' + r.ref + '.'noticeHref = r.hrefhasNotice = truenewSubject = ''newSummary = ''} else {notice = r != null ? r.error : 'could not open the ticket'noticeHref = ''hasNotice = true}}// does a row belong in THIS list (its filters)?belongs = (row) => {return (projectName == null || row.projectId == projectId) && (filterState == null || row.state == filterState)}// the row goes to the top (newest update first) if it belongs, and out if notplace = (row) => {let next = []if (belongs(row)) { next.push(row) }for (r of rows) { if (r.id != row.id) { next.push(r) } }rows = nextisEmpty = next.length == 0}on client ticketCreated(row) { place(row) }on dropConnection(id) {let r = emit server disconnectApp(projectId, id)if (r == null || r.error != null) {connectionMessage = r != null ? r.error : 'could not disconnect'return null}connectionMessage = ''connections = r.connectionshasConnections = r.connections.length > 0}on client connectionsChanged(slug) {let r = emit server connectionsOfProject(projectId)if (r != null && r.connections != null) {connections = r.connectionshasConnections = r.connections.length > 0}}on client signedIn(tag, info) {let r = emit server whatICanOpen(projectName != null ? projectName : '')if (r != null && r.projects != null) {myProjects = r.projectscanWrite = r.projects.length > 0noRole = r.projects.length == 0 && info.namedcanCreateProject = info.namedmustLogIn = falseisAdmin = r.admin == trueif (newProject == '' && r.projects.length > 0) { newProject = r.projects[0].slug }}}on client signedOut(tag) {canWrite = falsecanCreateProject = falseisAdmin = falsenoRole = falsemustLogIn = truemyProjects = []}on client ticketEvent(ticketId, ev, row) { place(row) }// THE FACE: a ticket opened from the web by the session's user (#31: only a real session).// The same push the API sends.on server openTicket(project, subject, summary, session) {let w = writerOfSession(session)if (w.error != null) { return { error = w.error } }let r = createTicket(project, subject, summary, w.user, null, null)if (r.error != null) { return { error = r.error } }emit client ticketCreated(r.ticket)return r.ticket}// what this session's user may do here after a login without a reload: the projects they may open tickets in,// and whether they are an admin of THIS page's project (`slug` null = the all-projects page)on server whatICanOpen(slug, session) {let u = userOfSession(session)if (u == null) { return { error = 'log in first' } }let p = slug != null && slug != '' ? projectByAnySlug(slug) : nullreturn { projects = editableProjects(u) admin = p != null && isAdminOf(p.id, u) }}// ticket #21: an admin ends a connection — the key is dead at once (connections.hl)on server disconnectApp(projectId, connectionId, session) {let w = writerOfSession(session)if (w.error != null) { return { error = w.error } }let r = disconnect('' + projectId, w.user, '' + connectionId)if (r.error != null) { return { error = r.error } }emit client connectionsChanged('')return { connections = r.connections }}on server connectionsOfProject(projectId, session) {return { connections = connectionRows('' + projectId) }}
Branches
- mainmain branch
Latest commits
- d3db6139tickets: Hybriel master 190aa11d (fc838894 GC correctness, #126 closure scopes, #127); gate 249/0, connect 60/0mre
- bce182e3tickets: Hybriel master 7eea0d32 (#126 memory, #48 lambda copies its argument); migrate.hl lambdas take &logmre
- 4137be0fantcolony#40: mission references point to the moved missionsmre
- 9bfba36aantcolony#40: history (LOG.md), worker briefs (missions/) and reports moved here from antcolony, numbered per project; old numbers in antcolony docs/mission-map.mdmre
- c7bd2645tickets: Hybriel master 73267707 (#122 fixed); compactNow workaround removed (#110 covered)mre
- 2ab91ee9tickets: gate checks rows appear once (session sync); re-vendor to ff51cf46 stopped on hybriel#122, stays 837fe120mre
- e01c2b1dtickets#24: installable app (manifest, service worker, offline list), own icon; gate waits for the hello's pongmre
- 752fbb7fdeploy.sh: back up live storage/.sessions/.env before every deploy (newest 5 kept)mre
- 38bdd5e4deploy.sh: never send .git or .gitignore to Byrodinmre
- f12fa1bcState of 2026-09-27, before the move to gitoriamre