gitoriaLog in with ident

tickets

All repositories: gitoria

ReadmeCodePull requestsReleasesTicketsSettings
Commitc7bd2645c7bd2645tickets: Hybriel master 73267707 (#122 fixed); compactNow workaround removed (#110 covered)mrec7bd2645/components/ticket_list.hl

9.2 KB

  1. // components/ticket_list.hl — `/`, `/project/:projectName` (also `/projects/:projectName`),
  2. // `/state/:stateSlug` and both:
  3. // the tickets, newest update first, filtered by project and state, and the form that
  4. // opens a new ticket. Live: a ticket created or changed anywhere (web or API) lands in,
  5. // moves within or leaves this list without a reload. Ticket #12: the summary is an <md-editor>
  6. // around the textarea (shared/md-editor.js; the textarea keeps the Markdown).
  7. // Ticket #20: a project's page (`/projects/<slug>`, also `/project/<slug>`) shows its title, description
  8. // (Markdown), its tickets and — for an admin — the link to its settings page. The new-ticket form needs the
  9. // role edit or admin: its project choice holds only the projects the user may open tickets in.
  10. parent './main.hl'
  11. import { ticketRows, projectLinks, stateLinks, stateOfSlug, createTicket, projectByAnySlug, editableProjects, isAdminOf, roleOf } from '../store.hl'
  12. import { markdownBlocks } from '../markdown.hl'
  13. import { siteName } from '../project.hl'
  14. import { canWriteSession, writerOfSession, userOfSession } from '../users.hl'
  15. import { connectionRows, disconnect } from '../connections.hl'
  16. import Markdown from './markdown.hl'
  17. projectName = null
  18. stateSlug = null
  19. filterState = stateOfSlug(stateSlug)
  20. rows = ticketRows(projectName, filterState)
  21. projects = projectLinks(projectName, filterState)
  22. stateFilters = stateLinks(projectName, filterState)
  23. session = null
  24. project = projectName != null ? projectByAnySlug(projectName) : null
  25. noSuchProject = projectName != null && project == null
  26. projectId = project != null ? project.id : ''
  27. hasDescription = project != null && project.description != null && project.description != ''
  28. descriptionMd = hasDescription ? markdownBlocks(project.description) : []
  29. isAdmin = project != null && isAdminOf(project.id, userOfSession(session))
  30. // ticket #21: the apps connected to this project (connections.hl), shown to everybody; an admin can disconnect
  31. connections = project != null ? connectionRows(project.id) : []
  32. hasConnections = connections.length > 0
  33. connectionMessage = ''
  34. settingsHref = project != null ? '/projects/' + project.slug + '/settings' : ''
  35. isEmpty = rows.length == 0
  36. heading = (projectName != null ? (project != null ? project.title : projectName) : 'All projects') + (filterState != null ? ' · ' + filterState : '')
  37. __title = siteName + ' | ' + heading
  38. // the new-ticket form
  39. myProjects = editableProjects(userOfSession(session))
  40. newProject = projectName != null && project != null ? project.slug : (myProjects.length > 0 ? myProjects[0].slug : '')
  41. newSubject = ''
  42. newSummary = ''
  43. // only for a member with the role edit or admin (ticket #20): the author is that user
  44. canWrite = myProjects.length > 0
  45. canCreateProject = canWriteSession(session)
  46. mustLogIn = userOfSession(session) == null
  47. noRole = userOfSession(session) != null && myProjects.length == 0
  48. notice = ''
  49. noticeHref = ''
  50. hasNotice = false
  51. View {
  52. ticketBoard {
  53. h1 { id = "heading" heading }
  54. ticketFilters {
  55. nav { id = "projectfilter"
  56. for (f of projects) { a { href = f.href class = f.cls f.label } }
  57. }
  58. nav { id = "statefilter"
  59. for (f of stateFilters) { a { href = f.href class = f.cls f.label } }
  60. }
  61. }
  62. if (noSuchProject) { p { id = "noproject" class = "empty" "There is no such project." } }
  63. if (hasDescription) { projectDescription { id = "description" Markdown { blocks = descriptionMd } } }
  64. if (hasConnections) { ul { id = "connections" class = "connections"
  65. for (c of connections) {
  66. li { class = "connection"
  67. "Connected to " strong { c.text }
  68. if (isAdmin) { button { type = "button" class = "quiet danger disconnect" "Disconnect" on click(e) { emit dropConnection(c.id) } } }
  69. }
  70. }
  71. } }
  72. p { id = "connectionmessage" class = "message" connectionMessage }
  73. projectActions {
  74. if (canCreateProject) { a { id = "newprojectlink" class = "button" href = "/new-project" "New project" } }
  75. if (isAdmin) { a { id = "settingslink" class = "button" href = settingsHref "Project settings" } }
  76. }
  77. if (mustLogIn) { p { id = "loginhint" class = "hint" "Reading is open to everyone. To open a ticket, log in with ident (top right)." } }
  78. 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." } }
  79. if (canWrite) { details { id = "newticket"
  80. summary { "New ticket" }
  81. form { id = "newticketform"
  82. on submit(e) {
  83. e.preventDefault()
  84. emit create(e)
  85. }
  86. label { "Project"
  87. select { id = "newproject" name = "project" value = newProject on change(e) { emit setProject(e.target.value) }
  88. for (p of myProjects) { option { value = p.slug p.title } }
  89. }
  90. }
  91. label { "Subject"
  92. input { id = "newsubject" name = "subject" required = "required" maxlength = "200" value = newSubject on input(e) { emit setSubject(e.target.value) } }
  93. }
  94. label { "Summary"
  95. mdEditor { textarea { id = "newsummary" name = "summary" rows = "4" value = newSummary on input(e) { emit setSummary(e.target.value) } } }
  96. }
  97. button { id = "newsubmit" type = "submit" "Open ticket" }
  98. }
  99. } }
  100. if (hasNotice) { p { id = "notice" class = "notice" notice " " a { id = "noticelink" href = noticeHref "open it" } } }
  101. if (isEmpty) { p { id = "empty" class = "empty" "No tickets here." } }
  102. ol { id = "tickets" class = "tickets"
  103. for (row of rows) {
  104. li {
  105. ticketState { class = row.stateSlug row.state }
  106. a { class = "subject" href = row.href ticketRef { row.ref } row.subject }
  107. ticketMeta {
  108. a { href = row.projectHref row.projectTitle }
  109. if (row.hasAssignee) { assignee { row.assignee } }
  110. time { row.updated }
  111. }
  112. }
  113. }
  114. }
  115. }
  116. }
  117. on setProject(v) { newProject = v }
  118. on setSubject(v) { newSubject = v }
  119. on setSummary(v) { newSummary = v }
  120. on create(e) {
  121. if (newProject.trim() == '' || newSubject.trim() == '') { return null }
  122. let r = emit server openTicket(newProject, newSubject, newSummary)
  123. if (r != null && r.error == null) {
  124. notice = 'Opened ' + r.project + ' ' + r.ref + '.'
  125. noticeHref = r.href
  126. hasNotice = true
  127. newSubject = ''
  128. newSummary = ''
  129. } else {
  130. notice = r != null ? r.error : 'could not open the ticket'
  131. noticeHref = ''
  132. hasNotice = true
  133. }
  134. }
  135. // does a row belong in THIS list (its filters)?
  136. belongs = (row) => {
  137. return (projectName == null || row.projectId == projectId) && (filterState == null || row.state == filterState)
  138. }
  139. // the row goes to the top (newest update first) if it belongs, and out if not
  140. place = (row) => {
  141. let next = []
  142. if (belongs(row)) { next.push(row) }
  143. for (r of rows) { if (r.id != row.id) { next.push(r) } }
  144. rows = next
  145. isEmpty = next.length == 0
  146. }
  147. on client ticketCreated(row) { place(row) }
  148. on dropConnection(id) {
  149. let r = emit server disconnectApp(projectId, id)
  150. if (r == null || r.error != null) {
  151. connectionMessage = r != null ? r.error : 'could not disconnect'
  152. return null
  153. }
  154. connectionMessage = ''
  155. connections = r.connections
  156. hasConnections = r.connections.length > 0
  157. }
  158. on client connectionsChanged(slug) {
  159. let r = emit server connectionsOfProject(projectId)
  160. if (r != null && r.connections != null) {
  161. connections = r.connections
  162. hasConnections = r.connections.length > 0
  163. }
  164. }
  165. on client signedIn(tag, info) {
  166. let r = emit server whatICanOpen(projectName != null ? projectName : '')
  167. if (r != null && r.projects != null) {
  168. myProjects = r.projects
  169. canWrite = r.projects.length > 0
  170. noRole = r.projects.length == 0 && info.named
  171. canCreateProject = info.named
  172. mustLogIn = false
  173. isAdmin = r.admin == true
  174. if (newProject == '' && r.projects.length > 0) { newProject = r.projects[0].slug }
  175. }
  176. }
  177. on client signedOut(tag) {
  178. canWrite = false
  179. canCreateProject = false
  180. isAdmin = false
  181. noRole = false
  182. mustLogIn = true
  183. myProjects = []
  184. }
  185. on client ticketEvent(ticketId, ev, row) { place(row) }
  186. // THE FACE: a ticket opened from the web by the session's user (#31: only a real session).
  187. // The same push the API sends.
  188. on server openTicket(project, subject, summary, session) {
  189. let w = writerOfSession(session)
  190. if (w.error != null) { return { error = w.error } }
  191. let r = createTicket(project, subject, summary, w.user, null, null)
  192. if (r.error != null) { return { error = r.error } }
  193. emit client ticketCreated(r.ticket)
  194. return r.ticket
  195. }
  196. // what this session's user may do here after a login without a reload: the projects they may open tickets in,
  197. // and whether they are an admin of THIS page's project (`slug` null = the all-projects page)
  198. on server whatICanOpen(slug, session) {
  199. let u = userOfSession(session)
  200. if (u == null) { return { error = 'log in first' } }
  201. let p = slug != null && slug != '' ? projectByAnySlug(slug) : null
  202. return { projects = editableProjects(u) admin = p != null && isAdminOf(p.id, u) }
  203. }
  204. // ticket #21: an admin ends a connection — the key is dead at once (connections.hl)
  205. on server disconnectApp(projectId, connectionId, session) {
  206. let w = writerOfSession(session)
  207. if (w.error != null) { return { error = w.error } }
  208. let r = disconnect('' + projectId, w.user, '' + connectionId)
  209. if (r.error != null) { return { error = r.error } }
  210. emit client connectionsChanged('')
  211. return { connections = r.connections }
  212. }
  213. on server connectionsOfProject(projectId, session) {
  214. return { connections = connectionRows('' + projectId) }
  215. }

Branches

Latest commits

  • 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