This commit is contained in:
@@ -107,6 +107,7 @@ type BoardFilter struct {
|
||||
AssigneeID int64
|
||||
Priority string
|
||||
Active bool
|
||||
Count int
|
||||
}
|
||||
|
||||
type ReportTotals struct {
|
||||
@@ -786,6 +787,15 @@ func (s *Server) boardPage(w http.ResponseWriter, r *http.Request) {
|
||||
if !validBoardPriority(filter.Priority) {
|
||||
filter.Priority = ""
|
||||
}
|
||||
if filter.TagID > 0 {
|
||||
filter.Count++
|
||||
}
|
||||
if filter.AssigneeID > 0 {
|
||||
filter.Count++
|
||||
}
|
||||
if filter.Priority != "" {
|
||||
filter.Count++
|
||||
}
|
||||
filter.Active = filter.TagID > 0 || filter.AssigneeID > 0 || filter.Priority != ""
|
||||
if filter.Active {
|
||||
filtered := make([]BoardTask, 0, len(tasks))
|
||||
|
||||
@@ -716,7 +716,7 @@ func TestSharedBoardCreateAssignMoveAndPermissions(t *testing.T) {
|
||||
if boardResponse.Code != http.StatusOK {
|
||||
t.Fatalf("board page status: %d body %s", boardResponse.Code, boardResponse.Body.String())
|
||||
}
|
||||
for _, expected := range []string{"Ship the onboarding flow", "Board Member", "Workspace Admin", "1405-05-06", "High", "Manage tags", "Tags", "select multiple", "Create a new tag", "People & importance", "Card controls", "Backend", "Frontend", "Urgent", "/static/board.js", `data-board-column="backlog"`} {
|
||||
for _, expected := range []string{"Ship the onboarding flow", "Board Member", "Workspace Admin", "1405-05-06", "High", "Filters", "Manage tags", "Tags", "select multiple", "Create a new tag", "People & importance", "Card controls", "Backend", "Frontend", "Urgent", "/static/board.js", `data-board-column="backlog"`} {
|
||||
if !strings.Contains(boardResponse.Body.String(), expected) {
|
||||
t.Fatalf("board page does not include %q", expected)
|
||||
}
|
||||
@@ -762,7 +762,7 @@ func TestSharedBoardCreateAssignMoveAndPermissions(t *testing.T) {
|
||||
matchingFilterRequest.AddCookie(memberCookie)
|
||||
matchingFilter := httptest.NewRecorder()
|
||||
s.http.Handler.ServeHTTP(matchingFilter, matchingFilterRequest)
|
||||
if matchingFilter.Code != http.StatusOK || !strings.Contains(matchingFilter.Body.String(), "Ship the onboarding flow") || !strings.Contains(matchingFilter.Body.String(), "Clear") {
|
||||
if matchingFilter.Code != http.StatusOK || !strings.Contains(matchingFilter.Body.String(), "Ship the onboarding flow") || !strings.Contains(matchingFilter.Body.String(), "Filters (3)") || !strings.Contains(matchingFilter.Body.String(), "Clear all") {
|
||||
t.Fatalf("matching board filter: %d %s", matchingFilter.Code, matchingFilter.Body.String())
|
||||
}
|
||||
for _, path := range []string{
|
||||
|
||||
@@ -7,6 +7,20 @@
|
||||
return;
|
||||
}
|
||||
|
||||
var filterMenu = document.querySelector(".board-filter-menu");
|
||||
if (filterMenu) {
|
||||
document.addEventListener("click", function (event) {
|
||||
if (filterMenu.open && !filterMenu.contains(event.target)) {
|
||||
filterMenu.open = false;
|
||||
}
|
||||
});
|
||||
document.addEventListener("keydown", function (event) {
|
||||
if (event.key === "Escape") {
|
||||
filterMenu.open = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var dragged = null;
|
||||
var csrf = board.dataset.csrf;
|
||||
|
||||
|
||||
@@ -314,16 +314,21 @@ blockquote { margin: 10px 0; padding-left: 12px; border-left: 2px solid #d8dfdb;
|
||||
.assignee-picker label > span .avatar, .compact-assignee-picker label > span .avatar { width: 21px; height: 21px; font-size: .5rem; }
|
||||
.assignee-picker input:checked + span, .compact-assignee-picker input:checked + span { border-color: #5f8f78; background: #edf6f1; color: var(--green-2); box-shadow: inset 0 0 0 1px #5f8f78; }
|
||||
.assignee-picker input:focus-visible + span, .compact-assignee-picker input:focus-visible + span { outline: 2px solid var(--ink); outline-offset: 2px; }
|
||||
.board-filter-bar { display: flex; gap: 16px; align-items: center; max-width: 1380px; margin: 0 auto 14px; padding: 10px 12px; border: 1px solid var(--line); border-radius: 13px; background: var(--white); box-shadow: var(--shadow); }
|
||||
.board-filter-title { display: flex; gap: 8px; align-items: center; flex: 0 0 auto; padding-right: 15px; border-right: 1px solid var(--line); color: var(--ink); font-size: .66rem; }
|
||||
.board-filter-title span { display: grid; width: 31px; height: 31px; place-items: center; border-radius: 8px; background: var(--mint); color: var(--green); font-size: .9rem; }
|
||||
.board-filter-bar form { display: grid; grid-template-columns: repeat(3, minmax(135px, 180px)) auto; gap: 8px; align-items: end; }
|
||||
.board-filter-bar label { min-width: 0; color: var(--muted); font-size: .52rem; }
|
||||
.board-filter-bar label > span { display: block; padding-left: 2px; }
|
||||
.board-filter-bar select { min-height: 33px; margin-top: 3px; padding: 6px 27px 6px 9px; border-radius: 8px; background-color: var(--paper); font-size: .59rem; }
|
||||
.board-filter-actions { display: flex; gap: 7px; align-items: center; min-height: 33px; }
|
||||
.board-filter-actions .button { min-height: 33px; padding: 6px 13px; font-size: .59rem; }
|
||||
.board-filter-actions .text-button { padding: 6px 2px; color: var(--muted); font-size: .57rem; }
|
||||
.board-filter-menu { position: relative; }
|
||||
.board-filter-menu > summary { list-style: none; user-select: none; }
|
||||
.board-filter-menu > summary::-webkit-details-marker { display: none; }
|
||||
.board-filter-menu > summary span { display: inline-grid; min-width: 20px; height: 20px; margin-left: 4px; place-items: center; border-radius: 20px; background: var(--green); color: #fff; font-size: .53rem; }
|
||||
.board-filter-menu[open] > summary { border-color: var(--green); background: var(--mint); color: var(--green-2); }
|
||||
.board-filter-popover { position: absolute; z-index: 62; top: calc(100% + 9px); right: 0; width: min(330px, calc(100vw - 40px)); padding: 19px; border: 1px solid var(--line); border-radius: 14px; background: var(--white); box-shadow: 0 22px 60px rgba(24, 36, 31, .22); }
|
||||
.board-filter-popover > header { display: flex; justify-content: space-between; gap: 15px; align-items: center; margin-bottom: 12px; }
|
||||
.board-filter-popover h2 { margin: 0; font-size: 1.05rem; }
|
||||
.board-filter-popover > header a { color: var(--green); font-size: .61rem; font-weight: 700; }
|
||||
.board-filter-popover ul { margin: 0 0 13px; padding: 0; border: 1px solid var(--line); border-radius: 10px; list-style: none; overflow: hidden; }
|
||||
.board-filter-popover li { padding: 11px 12px; border-bottom: 1px solid var(--line); }
|
||||
.board-filter-popover li:last-child { border-bottom: 0; }
|
||||
.board-filter-popover label > span { display: block; color: var(--muted); font-size: .57rem; }
|
||||
.board-filter-popover select { min-height: 35px; margin-top: 5px; padding: 7px 29px 7px 9px; border: 0; background-color: var(--paper); font-size: .62rem; }
|
||||
.board-filter-popover .button { min-height: 37px; font-size: .62rem; }
|
||||
.kanban-board { display: grid; grid-template-columns: repeat(4, minmax(270px, 1fr)); gap: 14px; align-items: start; max-width: 1380px; margin: 0 auto; overflow-x: auto; padding-bottom: 18px; }
|
||||
.kanban-column { min-width: 270px; padding: 13px; border: 1px solid var(--line); border-radius: 15px; background: #eef2ef; }
|
||||
.kanban-column-head { display: flex; justify-content: space-between; align-items: start; min-height: 51px; padding: 4px 5px 12px; }
|
||||
@@ -621,6 +626,7 @@ html[data-theme="dark"] .metric.pending,
|
||||
html[data-theme="dark"] .pending-icon { background: #453820; color: #e3bd72; }
|
||||
html[data-theme="dark"] .new-task-form { box-shadow: 0 22px 65px rgba(0, 0, 0, .52); }
|
||||
html[data-theme="dark"] .tag-manager,
|
||||
html[data-theme="dark"] .board-filter-popover,
|
||||
html[data-theme="dark"] .card-tag-editor form,
|
||||
html[data-theme="dark"] .card-detail-editor form { box-shadow: 0 22px 65px rgba(0, 0, 0, .52); }
|
||||
html[data-theme="dark"] .assignee-picker input:checked + span,
|
||||
@@ -718,12 +724,7 @@ html[data-theme="dark"] .kanban-cards.drag-over { background: rgba(107, 172, 142
|
||||
.tag-manager { position: fixed; top: 76px; right: 15px; left: 15px; width: auto; max-height: calc(100vh - 100px); overflow-y: auto; }
|
||||
.tag-create-form { grid-template-columns: 1fr 1fr; }
|
||||
.tag-create-form .button { grid-column: 1 / -1; }
|
||||
.board-filter-bar { display: block; padding: 10px; }
|
||||
.board-filter-title { margin-bottom: 9px; padding: 0 0 8px; border-right: 0; border-bottom: 1px solid var(--line); }
|
||||
.board-filter-title span { width: 27px; height: 27px; }
|
||||
.board-filter-bar form { grid-template-columns: 1fr 1fr; }
|
||||
.board-filter-bar form > label:nth-child(3) { grid-column: 1 / -1; }
|
||||
.board-filter-actions { justify-content: flex-end; }
|
||||
.board-filter-popover { position: fixed; top: 76px; right: 15px; left: 15px; width: auto; max-height: calc(100vh - 100px); overflow-y: auto; }
|
||||
.kanban-board { grid-template-columns: repeat(4, 82vw); margin-right: -15px; padding-right: 0; padding-left: 15px; scroll-snap-type: x mandatory; }
|
||||
.kanban-column { scroll-snap-align: start; }
|
||||
.archived-card { align-items: flex-start; flex-direction: column; gap: 11px; }
|
||||
|
||||
@@ -1,8 +1,35 @@
|
||||
{{define "board.html"}}
|
||||
{{template "shell-start" .}}
|
||||
{{$filter := .BoardFilter}}
|
||||
<header class="page-head board-page-head">
|
||||
<div><p class="eyebrow">SHARED WORKSPACE</p><h1>Team board</h1><p>Plan, assign, and move work forward together.</p></div>
|
||||
<div class="board-head-actions">
|
||||
<details class="board-filter-menu">
|
||||
<summary class="button secondary">⌕ Filters{{if $filter.Active}} <span>{{$filter.Count}}</span>{{end}}</summary>
|
||||
<div class="board-filter-popover">
|
||||
<header><div><p class="eyebrow">BOARD VIEW</p><h2>Filters{{if $filter.Active}} ({{$filter.Count}}){{end}}</h2></div>{{if $filter.Active}}<a href="/board">Clear all</a>{{end}}</header>
|
||||
<form method="get" action="/board">
|
||||
<ul>
|
||||
<li><label><span>Tag</span><select name="tag" aria-label="Filter by tag">
|
||||
<option value="">All tags</option>
|
||||
{{range .BoardTags}}<option value="{{.ID}}" {{if eq $filter.TagID .ID}}selected{{end}}>{{.Name}}</option>{{end}}
|
||||
</select></label></li>
|
||||
<li><label><span>Assignee</span><select name="assignee" aria-label="Filter by assignee">
|
||||
<option value="">Everyone</option>
|
||||
{{range .Users}}<option value="{{.ID}}" {{if eq $filter.AssigneeID .ID}}selected{{end}}>{{.DisplayName}}</option>{{end}}
|
||||
</select></label></li>
|
||||
<li><label><span>Priority</span><select name="priority" aria-label="Filter by priority">
|
||||
<option value="">All priorities</option>
|
||||
<option value="urgent" {{if eq $filter.Priority "urgent"}}selected{{end}}>Urgent</option>
|
||||
<option value="high" {{if eq $filter.Priority "high"}}selected{{end}}>High</option>
|
||||
<option value="normal" {{if eq $filter.Priority "normal"}}selected{{end}}>Normal</option>
|
||||
<option value="low" {{if eq $filter.Priority "low"}}selected{{end}}>Low</option>
|
||||
</select></label></li>
|
||||
</ul>
|
||||
<button class="button primary full" type="submit">Apply filters</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
<details class="tag-manager-panel">
|
||||
<summary class="button secondary">◉ Manage tags</summary>
|
||||
<div class="tag-manager">
|
||||
@@ -58,32 +85,6 @@
|
||||
</header>
|
||||
{{template "notice" .}}
|
||||
|
||||
{{$filter := .BoardFilter}}
|
||||
<section class="board-filter-bar">
|
||||
<div class="board-filter-title"><span>⌕</span><strong>Filter cards</strong></div>
|
||||
<form method="get" action="/board">
|
||||
<label><span>Tag</span><select name="tag" aria-label="Filter by tag">
|
||||
<option value="">All tags</option>
|
||||
{{range .BoardTags}}<option value="{{.ID}}" {{if eq $filter.TagID .ID}}selected{{end}}>{{.Name}}</option>{{end}}
|
||||
</select></label>
|
||||
<label><span>Assignee</span><select name="assignee" aria-label="Filter by assignee">
|
||||
<option value="">Everyone</option>
|
||||
{{range .Users}}<option value="{{.ID}}" {{if eq $filter.AssigneeID .ID}}selected{{end}}>{{.DisplayName}}</option>{{end}}
|
||||
</select></label>
|
||||
<label><span>Priority</span><select name="priority" aria-label="Filter by priority">
|
||||
<option value="">All priorities</option>
|
||||
<option value="urgent" {{if eq $filter.Priority "urgent"}}selected{{end}}>Urgent</option>
|
||||
<option value="high" {{if eq $filter.Priority "high"}}selected{{end}}>High</option>
|
||||
<option value="normal" {{if eq $filter.Priority "normal"}}selected{{end}}>Normal</option>
|
||||
<option value="low" {{if eq $filter.Priority "low"}}selected{{end}}>Low</option>
|
||||
</select></label>
|
||||
<div class="board-filter-actions">
|
||||
{{if $filter.Active}}<a href="/board" class="text-button">Clear</a>{{end}}
|
||||
<button class="button secondary" type="submit">Apply</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<div class="kanban-board" data-board data-csrf="{{.CSRF}}">
|
||||
{{range .Board}}
|
||||
<section class="kanban-column {{.Status}}" data-board-column="{{.Status}}">
|
||||
|
||||
Reference in New Issue
Block a user