WIP components (as built)
This commit is contained in:
102 files changed
+6538
-1
No files matched your search
@@ -1,3 +1,82 @@
|
||||
package main
|
||||
|
||||
var projectsPages = []page{}
|
||||
const projectsCategory = "Projects"
|
||||
|
||||
var projectsPages = append(append(projectsTaskPages, projectsFieldPages...), projectsTimePages...)
|
||||
|
||||
var projectsTaskPages = []page{
|
||||
{ID: "361", Name: "KanbanBoard", ZhName: "任务看板", Category: projectsCategory, Demo: projectsDemoKanbanBoard, Keys: "Tab 聚焦卡片;Enter/Space 打开;Alt+左右跨列;Alt+上下排序;可拖放到卡片之前或列末。", Code: `statuses := []mujica.WorkflowStatus{{ID: "todo", Name: "To do", Group: mujica.StatusUnstarted}, {ID: "done", Name: "Done", Group: mujica.StatusCompleted}}
|
||||
tasks := []mujica.Task{{ID: "ENG-1", Title: "Plan the release", Status: "todo"}}
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
r := mujica.KanbanBoard(c, &tasks, mujica.KanbanBoardOptions{Columns: []mujica.KanbanColumn{{Status: "todo"}, {Status: "done", WIP: 2}}, Statuses: statuses})
|
||||
if move, ok := r.Moved(); ok {
|
||||
log.Println("move", move.ID, move.To, move.Index)
|
||||
}
|
||||
if id, ok := r.Opened(); ok {
|
||||
log.Println("open", id)
|
||||
}
|
||||
}
|
||||
ui.Render(view, 1000, 700, 1)`},
|
||||
{ID: "362", Name: "TaskList", ZhName: "任务列表", Category: projectsCategory, Demo: projectsDemoTaskList, Keys: "Tab 在筛选器和任务控件间移动;空格切换完成;Enter 打开;F2 编辑标题。", Code: `statuses := []mujica.WorkflowStatus{{ID: "todo", Name: "To do", Group: mujica.StatusUnstarted}, {ID: "done", Name: "Done", Group: mujica.StatusCompleted}}
|
||||
tasks := []mujica.Task{{ID: "ENG-1", Title: "Plan the release", Status: "todo"}}
|
||||
filter := mujica.TaskShowAll
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
r := mujica.TaskList(c, tasks, &filter, mujica.TaskListOptions{GroupBy: mujica.TaskGroupStatus, Statuses: statuses})
|
||||
if id, ok := r.Changed(); ok {
|
||||
log.Println("edit", id)
|
||||
}
|
||||
if id, ok := r.Opened(); ok {
|
||||
log.Println("open", id)
|
||||
}
|
||||
}
|
||||
ui.Render(view, 1000, 700, 1)`},
|
||||
{ID: "363", Name: "TaskItem", ZhName: "任务行", Category: projectsCategory, Demo: projectsDemoTaskItem, Keys: "Tab 在复选框和标题间移动;空格切换完成;Enter 打开;F2 或双击开始编辑;Enter 保存;Escape 取消。", Code: `task := mujica.Task{ID: "ENG-1", Title: "Plan the release", Status: "todo", Priority: mujica.PriorityHigh}
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
r := mujica.TaskItem(c, &task, mujica.TaskItemOptions{})
|
||||
if id, ok := r.Changed(); ok {
|
||||
log.Println("edit", id)
|
||||
}
|
||||
if id, ok := r.Opened(); ok {
|
||||
log.Println("open", id)
|
||||
}
|
||||
}
|
||||
ui.Render(view, 1000, 700, 1)`},
|
||||
{ID: "364", Name: "TaskDetailPanel", ZhName: "任务详情侧栏", Category: projectsCategory, Demo: projectsDemoTaskDetailPanel, Keys: "Tab 在字段间移动;选择框使用方向键和 Enter;Escape 返回关闭事件;子任务 Enter 新增。", Code: `statuses := []mujica.WorkflowStatus{{ID: "todo", Name: "To do", Group: mujica.StatusUnstarted}, {ID: "done", Name: "Done", Group: mujica.StatusCompleted}}
|
||||
task := mujica.Task{ID: "ENG-1", Title: "Plan the release", Status: "todo", Priority: mujica.PriorityHigh}
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
r := mujica.TaskDetailPanel(c, &task, mujica.TaskDetailPanelOptions{Statuses: statuses, Activity: func() { ui.Text(c, "Release planned") }})
|
||||
if r.Changed() {
|
||||
log.Println("task edited", task.ID)
|
||||
}
|
||||
if r.Closed() {
|
||||
log.Println("close", task.ID)
|
||||
}
|
||||
}
|
||||
ui.Render(view, 1000, 700, 1)`},
|
||||
{ID: "365", Name: "SubtaskList", ZhName: "子任务列表", Category: projectsCategory, Demo: projectsDemoSubtaskList, Keys: "Tab 移动;Space 切换完成;新增输入框 Enter 提交;Alt+上下排序;拖动握柄排序。", Code: `subtasks := []mujica.Subtask{{ID: "one", Title: "Review", Done: true}, {ID: "two", Title: "Ship"}}
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
if mujica.SubtaskList(c, &subtasks, mujica.SubtaskListOptions{}).Changed() {
|
||||
log.Println("subtasks changed", len(subtasks))
|
||||
}
|
||||
}
|
||||
ui.Render(view, 1000, 700, 1)`},
|
||||
{ID: "368", Name: "SprintBoard", ZhName: "迭代看板", Category: projectsCategory, Demo: projectsDemoSprintBoard, Keys: "看板卡片支持 Tab、Enter/Space、Alt+方向键与拖放。", Code: `statuses := []mujica.WorkflowStatus{{ID: "todo", Name: "To do", Group: mujica.StatusUnstarted}, {ID: "done", Name: "Done", Group: mujica.StatusCompleted}}
|
||||
tasks := []mujica.Task{{ID: "ENG-1", Title: "Plan the release", Status: "todo"}}
|
||||
sprint := mujica.Sprint{Name: "Release sprint", Goal: "Ship", Start: mujica.Date{Year: 2026, Month: 10, Day: 1}, End: mujica.Date{Year: 2026, Month: 10, Day: 14}}
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
r := mujica.SprintBoard(c, sprint, &tasks, mujica.SprintBoardOptions{Board: mujica.KanbanBoardOptions{Columns: []mujica.KanbanColumn{{Status: "todo"}, {Status: "done", WIP: 2}}, Statuses: statuses}})
|
||||
if move, ok := r.Moved(); ok {
|
||||
log.Println("move", move.ID, move.To)
|
||||
}
|
||||
if id, ok := r.Opened(); ok {
|
||||
log.Println("open", id)
|
||||
}
|
||||
}
|
||||
ui.Render(view, 1000, 700, 1)`},
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
mujica "mujicaui"
|
||||
)
|
||||
|
||||
var projectsStatuses = []mujica.WorkflowStatus{
|
||||
{ID: "todo", Name: "To do", Group: mujica.StatusUnstarted},
|
||||
{ID: "doing", Name: "In progress", Group: mujica.StatusStarted},
|
||||
{ID: "done", Name: "Done", Group: mujica.StatusCompleted},
|
||||
}
|
||||
var projectsMembers = []mujica.Member{{ID: "ann", Name: "Ann"}, {ID: "ben", Name: "Ben"}, {ID: "cy", Name: "Cy"}}
|
||||
var projectsLabels = []mujica.TaskLabel{{ID: "ui", Name: "UI", Color: mujica.EventInfo}, {ID: "release", Name: "Release", Color: mujica.EventSuccess}}
|
||||
|
||||
func projectsCheck(c *ui.Context, log func(string), value *bool, label string) {
|
||||
if mujica.Checkbox(c, value, label, mujica.CheckboxOptions{}).Changed() {
|
||||
log(fmt.Sprintf("%s = %v", label, *value))
|
||||
}
|
||||
}
|
||||
|
||||
func projectsChoice[T comparable](c *ui.Context, log func(string), value *T, options []mujica.SelectOption[T], label string) {
|
||||
if mujica.Select(c, value, options, mujica.SelectOptions{Label: label}).Changed() {
|
||||
log(fmt.Sprintf("%s = %v", label, *value))
|
||||
}
|
||||
}
|
||||
|
||||
type projectsTaskState struct {
|
||||
tasks []mujica.Task
|
||||
disabled bool
|
||||
version int
|
||||
group mujica.TaskGrouping
|
||||
filter mujica.TaskFilter
|
||||
wip float64
|
||||
closed bool
|
||||
}
|
||||
|
||||
func projectsTasks(c *ui.Context, key string) *projectsTaskState {
|
||||
return *ui.Local(c.Root(), key, func() *projectsTaskState {
|
||||
return &projectsTaskState{wip: 2, tasks: []mujica.Task{
|
||||
{ID: "ENG-1", Title: "Plan the release", Status: "todo", Priority: mujica.PriorityHigh, Due: mujica.Date{Year: 2026, Month: 10, Day: 3}, Assignees: []string{"ann"}, Labels: []string{"ui"}, Subtasks: []mujica.Subtask{{ID: "one", Title: "Review", Done: true}, {ID: "two", Title: "Ship"}}},
|
||||
{ID: "ENG-2", Title: "Check keyboard paths", Status: "doing", Priority: mujica.PriorityUrgent, Assignees: []string{"ben"}, Labels: []string{"release"}},
|
||||
{ID: "ENG-3", Title: "Publish docs", Status: "done", Done: true, Priority: mujica.PriorityLow},
|
||||
}}
|
||||
})
|
||||
}
|
||||
|
||||
func projectsBoardOptions(s *projectsTaskState) mujica.KanbanBoardOptions {
|
||||
return mujica.KanbanBoardOptions{Columns: []mujica.KanbanColumn{{Status: "todo"}, {Status: "doing", WIP: int(s.wip)}, {Status: "done"}}, Statuses: projectsStatuses, Members: projectsMembers, Labels: projectsLabels, Version: s.version, Disabled: s.disabled}
|
||||
}
|
||||
|
||||
func projectsLogBoard(s *projectsTaskState, r mujica.KanbanResult, log func(string)) {
|
||||
if move, ok := r.Moved(); ok {
|
||||
s.version++
|
||||
log(fmt.Sprintf("move %s: %s -> %s at %d", move.ID, move.From, move.To, move.Index))
|
||||
}
|
||||
if id, ok := r.Opened(); ok {
|
||||
log("open " + id)
|
||||
}
|
||||
}
|
||||
|
||||
func projectsDemoKanbanBoard(c *ui.Context, log func(string)) {
|
||||
s := projectsTasks(c, "361")
|
||||
projectsCheck(c, log, &s.disabled, "Disabled")
|
||||
if mujica.NumberInput(c, &s.wip, mujica.NumberInputOptions{Min: 0, Max: 10, Step: 1, Label: "WIP limit"}).Changed() {
|
||||
log(fmt.Sprintf("WIP = %g", s.wip))
|
||||
}
|
||||
ui.ScrollHorizontal(c).Height(420).FillWidth().Children(func() {
|
||||
ui.Box(c).Width(880).Height(420).Children(func() { projectsLogBoard(s, mujica.KanbanBoard(c, &s.tasks, projectsBoardOptions(s)), log) })
|
||||
})
|
||||
}
|
||||
|
||||
func projectsDemoTaskList(c *ui.Context, log func(string)) {
|
||||
s := projectsTasks(c, "362")
|
||||
projectsCheck(c, log, &s.disabled, "Disabled")
|
||||
projectsChoice(c, log, &s.group, []mujica.SelectOption[mujica.TaskGrouping]{{Value: mujica.TaskGroupNone, Label: "Ungrouped"}, {Value: mujica.TaskGroupStatus, Label: "By status"}, {Value: mujica.TaskGroupPriority, Label: "By priority"}}, "Grouping")
|
||||
ui.Box(c).Height(360).FillWidth().Children(func() {
|
||||
before := s.filter
|
||||
r := mujica.TaskList(c, s.tasks, &s.filter, mujica.TaskListOptions{Statuses: projectsStatuses, Members: projectsMembers, GroupBy: s.group, Version: s.version, Disabled: s.disabled})
|
||||
if s.filter != before {
|
||||
log(fmt.Sprintf("filter = %d", s.filter))
|
||||
}
|
||||
if id, ok := r.Changed(); ok {
|
||||
s.version++
|
||||
log("edit " + id)
|
||||
}
|
||||
if id, ok := r.Opened(); ok {
|
||||
log("open " + id)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func projectsDemoTaskItem(c *ui.Context, log func(string)) {
|
||||
s := projectsTasks(c, "363")
|
||||
projectsCheck(c, log, &s.disabled, "Disabled")
|
||||
projectsChoice(c, log, &s.tasks[0].Priority, []mujica.SelectOption[mujica.Priority]{{Value: mujica.PriorityLow, Label: "Low"}, {Value: mujica.PriorityHigh, Label: "High"}, {Value: mujica.PriorityUrgent, Label: "Urgent"}}, "Priority")
|
||||
r := mujica.TaskItem(c, &s.tasks[0], mujica.TaskItemOptions{Members: projectsMembers, Disabled: s.disabled})
|
||||
if id, ok := r.Changed(); ok {
|
||||
log("edit " + id)
|
||||
}
|
||||
if id, ok := r.Opened(); ok {
|
||||
log("open " + id)
|
||||
}
|
||||
}
|
||||
|
||||
func projectsDemoTaskDetailPanel(c *ui.Context, log func(string)) {
|
||||
s := projectsTasks(c, "364")
|
||||
projectsCheck(c, log, &s.disabled, "Disabled")
|
||||
if s.closed {
|
||||
if mujica.Button(c, "Open details", mujica.ButtonOptions{}).Clicked() {
|
||||
s.closed = false
|
||||
log("open details")
|
||||
}
|
||||
return
|
||||
}
|
||||
ui.Scroll(c).Height(620).FillWidth().Children(func() {
|
||||
ui.Box(c).MaxWidth(500).FillWidth().Children(func() {
|
||||
r := mujica.TaskDetailPanel(c, &s.tasks[0], mujica.TaskDetailPanelOptions{Statuses: projectsStatuses, Members: projectsMembers, Labels: projectsLabels, Disabled: s.disabled, Activity: func() { ui.Text(c, "Ann created this task") }})
|
||||
if r.Changed() {
|
||||
log("task fields changed")
|
||||
}
|
||||
if r.Closed() {
|
||||
s.closed = true
|
||||
log("close details")
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func projectsDemoSubtaskList(c *ui.Context, log func(string)) {
|
||||
s := projectsTasks(c, "365")
|
||||
projectsCheck(c, log, &s.disabled, "Disabled")
|
||||
if mujica.Button(c, "Clear subtasks", mujica.ButtonOptions{Disabled: s.disabled}).Clicked() {
|
||||
s.tasks[0].Subtasks = nil
|
||||
log("clear subtasks")
|
||||
}
|
||||
if mujica.SubtaskList(c, &s.tasks[0].Subtasks, mujica.SubtaskListOptions{Disabled: s.disabled}).Changed() {
|
||||
log("subtasks changed")
|
||||
}
|
||||
}
|
||||
|
||||
func projectsDemoSprintBoard(c *ui.Context, log func(string)) {
|
||||
s := projectsTasks(c, "368")
|
||||
projectsCheck(c, log, &s.disabled, "Disabled")
|
||||
sprint := mujica.Sprint{Name: "Release sprint", Goal: "Ship the first release", Start: mujica.Date{Year: 2026, Month: 10, Day: 1}, End: mujica.Date{Year: 2026, Month: 10, Day: 14}}
|
||||
ui.ScrollHorizontal(c).Height(540).FillWidth().Children(func() {
|
||||
ui.Box(c).Width(880).Height(540).Children(func() {
|
||||
projectsLogBoard(s, mujica.SprintBoard(c, sprint, &s.tasks, mujica.SprintBoardOptions{Board: projectsBoardOptions(s)}), log)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func projectsDemoLabelManager(c *ui.Context, log func(string)) {
|
||||
s := *ui.Local(c.Root(), "374", func() *struct {
|
||||
labels []mujica.TaskLabel
|
||||
selected []string
|
||||
disabled bool
|
||||
} {
|
||||
return &struct {
|
||||
labels []mujica.TaskLabel
|
||||
selected []string
|
||||
disabled bool
|
||||
}{labels: slices.Clone(projectsLabels)}
|
||||
})
|
||||
projectsCheck(c, log, &s.disabled, "Disabled")
|
||||
if mujica.LabelManager(c, &s.labels, &s.selected, mujica.LabelManagerOptions{Disabled: s.disabled}).Changed() {
|
||||
log(fmt.Sprintf("labels changed: %d, selected %v", len(s.labels), s.selected))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package main
|
||||
|
||||
var projectsFieldPages = []page{
|
||||
{ID: "369", Name: "BurndownChart", ZhName: "燃尽图", Category: projectsCategory, Demo: projectsDemoBurndownChart, Keys: "Tab 聚焦图例与图表;Space 切换图例;方向键逐日读取;Home/End 首尾;Escape 清除读数。", Code: `remaining := []float64{40, 32, 25, 18}
|
||||
opts := mujica.BurndownChartOptions{Frame: mujica.ChartFrame{Label: "Release burndown"}, Start: mujica.Date{Year: 2026, Month: 10, Day: 1}, End: mujica.Date{Year: 2026, Month: 10, Day: 14}, Total: 40}
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
r := mujica.BurndownChart(c, remaining, opts)
|
||||
if r.Changed() {
|
||||
log.Println("point", r.Point)
|
||||
}
|
||||
if name, ok := r.Toggled(); ok {
|
||||
log.Println("series", name)
|
||||
}
|
||||
if r.Retried() {
|
||||
log.Println("retry")
|
||||
}
|
||||
}
|
||||
ui.Render(view, 1000, 700, 1)`},
|
||||
{ID: "370", Name: "PriorityIndicator", ZhName: "优先级指示", Category: projectsCategory, Demo: projectsDemoPriorityIndicator, Keys: "静态展示,通过无障碍名称读取完整等级。", Code: `priority := mujica.PriorityHigh
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.PriorityIndicator(c, priority, mujica.PriorityIndicatorOptions{})
|
||||
}
|
||||
ui.Render(view, 1000, 700, 1)`},
|
||||
{ID: "371", Name: "StatusSelect", ZhName: "状态选择", Category: projectsCategory, Demo: projectsDemoStatusSelect, Keys: "Enter/Space 或上下键打开;上下、Home/End 定位;Enter 确认;Escape 关闭。", Code: `statuses := []mujica.WorkflowStatus{{ID: "todo", Name: "To do", Group: mujica.StatusUnstarted}, {ID: "done", Name: "Done", Group: mujica.StatusCompleted}}
|
||||
value := "todo"
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
if mujica.StatusSelect(c, &value, statuses, mujica.StatusSelectOptions{}).Changed() {
|
||||
log.Println("status", value)
|
||||
}
|
||||
}
|
||||
ui.Render(view, 1000, 700, 1)`},
|
||||
{ID: "372", Name: "AssigneePicker", ZhName: "负责人选择", Category: projectsCategory, Demo: projectsDemoAssigneePicker, Keys: "Enter/Space 打开;输入搜索;下键进入选项;Space/Enter 勾选;Backspace 移除最后一项;Escape 关闭。", Code: `members := []mujica.Member{{ID: "ann", Name: "Ann"}, {ID: "ben", Name: "Ben"}}
|
||||
ids := []string{"ann"}
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
if mujica.AssigneePicker(c, &ids, members, mujica.AssigneePickerOptions{Max: 2}).Changed() {
|
||||
log.Println("assignees", ids)
|
||||
}
|
||||
}
|
||||
ui.Render(view, 1000, 700, 1)`},
|
||||
{ID: "373", Name: "DueDatePicker", ZhName: "截止日期选择", Category: projectsCategory, Demo: projectsDemoDueDatePicker, Keys: "Tab 到快捷项、日期输入与日历按钮;Enter 选择;日历方向键移动;Escape 关闭。", Code: `var due mujica.Date
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
if mujica.DueDatePicker(c, &due, mujica.DueDatePickerOptions{}).Changed() {
|
||||
log.Println("due", due)
|
||||
}
|
||||
}
|
||||
ui.Render(view, 1000, 700, 1)`},
|
||||
{ID: "374", Name: "LabelManager", ZhName: "标签管理", Category: projectsCategory, Demo: projectsDemoLabelManager, Keys: "Tab 移动;Space 勾选;新增输入 Enter 创建;编辑颜色按钮打开编辑;Enter 保存名称;Escape 结束编辑。", Code: `labels := []mujica.TaskLabel{{ID: "ui", Name: "UI", Color: mujica.EventInfo}}
|
||||
selected := []string{}
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
if mujica.LabelManager(c, &labels, &selected, mujica.LabelManagerOptions{}).Changed() {
|
||||
log.Println("labels changed", len(labels), selected)
|
||||
}
|
||||
}
|
||||
ui.Render(view, 1000, 700, 1)`},
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/egoist/mygo/ui"
|
||||
"math"
|
||||
mujica "mujicaui"
|
||||
)
|
||||
|
||||
func projectsDemoBurndownChart(c *ui.Context, log func(string)) {
|
||||
s := ui.Local(c.Root(), "369", func() struct {
|
||||
disabled bool
|
||||
status mujica.DataStatus
|
||||
total float64
|
||||
values []float64
|
||||
} {
|
||||
return struct {
|
||||
disabled bool
|
||||
status mujica.DataStatus
|
||||
total float64
|
||||
values []float64
|
||||
}{total: 40, values: []float64{40, 38, 32, 34, 26, math.NaN(), 20, 18}}
|
||||
})
|
||||
projectsCheck(c, log, &s.disabled, "Disabled")
|
||||
projectsChoice(c, log, &s.status, []mujica.SelectOption[mujica.DataStatus]{{Value: mujica.DataReady, Label: "Ready"}, {Value: mujica.DataLoading, Label: "Loading"}, {Value: mujica.DataFailed, Label: "Error"}}, "Data state")
|
||||
if mujica.NumberInput(c, &s.total, mujica.NumberInputOptions{Min: 1, Max: 100, Step: 1, Label: "Total work"}).Changed() {
|
||||
log(fmt.Sprintf("total = %g", s.total))
|
||||
}
|
||||
ui.Box(c).Disabled(s.disabled).FillWidth().Children(func() {
|
||||
r := mujica.BurndownChart(c, s.values, mujica.BurndownChartOptions{Frame: mujica.ChartFrame{Label: "Release burndown", Status: s.status, Error: "Data unavailable"}, Start: mujica.Date{Year: 2026, Month: 10, Day: 1}, End: mujica.Date{Year: 2026, Month: 10, Day: 14}, Total: s.total})
|
||||
if r.Changed() {
|
||||
log(fmt.Sprintf("point = %d", r.Point))
|
||||
}
|
||||
if name, ok := r.Toggled(); ok {
|
||||
log("toggle " + name)
|
||||
}
|
||||
if r.Retried() {
|
||||
s.status = mujica.DataReady
|
||||
log("retry")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func projectsDemoPriorityIndicator(c *ui.Context, log func(string)) {
|
||||
s := ui.Local(c.Root(), "370", func() struct {
|
||||
priority mujica.Priority
|
||||
iconOnly bool
|
||||
} {
|
||||
return struct {
|
||||
priority mujica.Priority
|
||||
iconOnly bool
|
||||
}{priority: mujica.PriorityHigh}
|
||||
})
|
||||
projectsCheck(c, log, &s.iconOnly, "Icon only")
|
||||
projectsChoice(c, log, &s.priority, []mujica.SelectOption[mujica.Priority]{{Value: mujica.PriorityNone, Label: "None"}, {Value: mujica.PriorityLow, Label: "Low"}, {Value: mujica.PriorityMedium, Label: "Medium"}, {Value: mujica.PriorityHigh, Label: "High"}, {Value: mujica.PriorityUrgent, Label: "Urgent"}}, "Priority")
|
||||
mujica.PriorityIndicator(c, s.priority, mujica.PriorityIndicatorOptions{IconOnly: s.iconOnly})
|
||||
}
|
||||
|
||||
func projectsDemoStatusSelect(c *ui.Context, log func(string)) {
|
||||
s := ui.Local(c.Root(), "371", func() struct {
|
||||
value string
|
||||
disabled bool
|
||||
} {
|
||||
return struct {
|
||||
value string
|
||||
disabled bool
|
||||
}{value: "todo"}
|
||||
})
|
||||
projectsCheck(c, log, &s.disabled, "Disabled")
|
||||
if mujica.StatusSelect(c, &s.value, projectsStatuses, mujica.StatusSelectOptions{Disabled: s.disabled}).Changed() {
|
||||
log("status " + s.value)
|
||||
}
|
||||
}
|
||||
|
||||
func projectsDemoAssigneePicker(c *ui.Context, log func(string)) {
|
||||
s := ui.Local(c.Root(), "372", func() struct {
|
||||
ids []string
|
||||
disabled bool
|
||||
max float64
|
||||
} {
|
||||
return struct {
|
||||
ids []string
|
||||
disabled bool
|
||||
max float64
|
||||
}{ids: []string{"ann"}, max: 2}
|
||||
})
|
||||
projectsCheck(c, log, &s.disabled, "Disabled")
|
||||
if mujica.NumberInput(c, &s.max, mujica.NumberInputOptions{Min: 0, Max: 3, Step: 1, Label: "Selection limit"}).Changed() {
|
||||
log(fmt.Sprintf("limit = %g", s.max))
|
||||
}
|
||||
if mujica.AssigneePicker(c, &s.ids, projectsMembers, mujica.AssigneePickerOptions{Max: int(s.max), Disabled: s.disabled}).Changed() {
|
||||
log(fmt.Sprintf("assignees %v", s.ids))
|
||||
}
|
||||
}
|
||||
|
||||
func projectsDemoDueDatePicker(c *ui.Context, log func(string)) {
|
||||
s := ui.Local(c.Root(), "373", func() struct {
|
||||
due mujica.Date
|
||||
disabled, done bool
|
||||
} {
|
||||
return struct {
|
||||
due mujica.Date
|
||||
disabled, done bool
|
||||
}{due: mujica.Date{Year: 2026, Month: 10, Day: 3}}
|
||||
})
|
||||
projectsCheck(c, log, &s.disabled, "Disabled")
|
||||
projectsCheck(c, log, &s.done, "Completed")
|
||||
if mujica.DueDatePicker(c, &s.due, mujica.DueDatePickerOptions{Done: s.done, Disabled: s.disabled}).Changed() {
|
||||
log(fmt.Sprintf("due %v", s.due))
|
||||
}
|
||||
}
|
||||
|
||||
func projectsDemoIssueCard(c *ui.Context, log func(string)) {
|
||||
s := projectsTasks(c, "375")
|
||||
selected := ui.Local(c.Root(), "375-selected", func() bool { return false })
|
||||
projectsCheck(c, log, &s.disabled, "Disabled")
|
||||
projectsCheck(c, log, selected, "Selected")
|
||||
ui.Box(c).Width(300).Children(func() {
|
||||
if mujica.IssueCard(c, s.tasks[0], mujica.IssueCardOptions{Statuses: projectsStatuses, Members: projectsMembers, Labels: projectsLabels, Selected: *selected, Disabled: s.disabled}).Clicked() {
|
||||
*selected = !*selected
|
||||
log("open " + s.tasks[0].ID)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func projectsDemoIssueIdBadge(c *ui.Context, log func(string)) {
|
||||
s := ui.Local(c.Root(), "376", func() struct {
|
||||
disabled, copyable bool
|
||||
id string
|
||||
} {
|
||||
return struct {
|
||||
disabled, copyable bool
|
||||
id string
|
||||
}{copyable: true, id: "ENG-123"}
|
||||
})
|
||||
projectsCheck(c, log, &s.disabled, "Disabled")
|
||||
projectsCheck(c, log, &s.copyable, "Copyable")
|
||||
projectsChoice(c, log, &s.id, []mujica.SelectOption[string]{{Value: "ENG-123", Label: "ENG-123"}, {Value: "OPS-7", Label: "OPS-7"}}, "Issue key")
|
||||
if mujica.IssueIdBadge(c, s.id, mujica.IssueIdBadgeOptions{Copyable: s.copyable, Disabled: s.disabled}).Changed() {
|
||||
log("copied " + s.id)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
mujica "mujicaui"
|
||||
)
|
||||
|
||||
func projectsGalleryTester(t *testing.T, id string) (*ui.Tester, *[]string) {
|
||||
t.Helper()
|
||||
var events []string
|
||||
for _, p := range projectsPages {
|
||||
if p.ID == id {
|
||||
tt := ui.NewTester(func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
ui.Column(c).Fill().Gap(12).Padding(16).Children(func() { p.Demo(c, func(s string) { events = append(events, s) }) })
|
||||
}, 1200, 1000)
|
||||
tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1})
|
||||
return tt, &events
|
||||
}
|
||||
}
|
||||
t.Fatal("unknown project page " + id)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func TestProjectsGalleryCatalog(t *testing.T) {
|
||||
if len(projectsPages) != 18 {
|
||||
t.Fatalf("pages = %d", len(projectsPages))
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, p := range projectsPages {
|
||||
if p.Category != "Projects" || p.Keys == "" || p.Code == "" || seen[p.ID] {
|
||||
t.Fatalf("invalid page %s", p.ID)
|
||||
}
|
||||
seen[p.ID] = true
|
||||
projectsGalleryTester(t, p.ID)
|
||||
}
|
||||
if seen["366"] || seen["367"] {
|
||||
t.Fatal("dropped components have gallery pages")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjectsGalleryEvents(t *testing.T) {
|
||||
for _, tc := range []struct{ id, action, event string }{
|
||||
{"361", "ENG-1 Plan the release", "open ENG-1"},
|
||||
{"362", "Done 1", "filter = 2"},
|
||||
{"363", "Plan the release", "open ENG-1"},
|
||||
{"364", "Close details", "close details"},
|
||||
{"365", "Clear subtasks", "clear subtasks"},
|
||||
{"368", "ENG-1 Plan the release", "open ENG-1"},
|
||||
{"369", "Ideal", "toggle Ideal"},
|
||||
{"370", "Icon only", "Icon only = true"},
|
||||
{"371", "Status", ""},
|
||||
{"372", "Disabled", "Disabled = true"},
|
||||
{"373", "Today", "due "},
|
||||
{"374", "UI", "labels changed:"},
|
||||
{"375", "ENG-1 Plan the release", "open ENG-1"},
|
||||
{"376", "Copy ENG-123", "copied ENG-123"},
|
||||
{"377", "Completed tasks", ""},
|
||||
{"378", "Ben assigned hours", ""},
|
||||
{"379", "Start", "tracked "},
|
||||
{"380", "Start", "timer phase = 0, running = true"},
|
||||
} {
|
||||
t.Run(tc.id, func(t *testing.T) {
|
||||
tt, events := projectsGalleryTester(t, tc.id)
|
||||
if err := tt.Click(tc.action); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
switch tc.id {
|
||||
case "371":
|
||||
tt.Key(0, ui.KeyEnd)
|
||||
tt.Key(0, ui.KeyEnter)
|
||||
tc.event = "status done"
|
||||
case "377", "378":
|
||||
tt.Command("selectAll")
|
||||
tt.Type("5")
|
||||
tt.Key(0, ui.KeyEnter)
|
||||
if tc.id == "377" {
|
||||
tc.event = "completed = 5"
|
||||
} else {
|
||||
tc.event = "assigned = 5"
|
||||
}
|
||||
}
|
||||
found := false
|
||||
for _, e := range *events {
|
||||
if strings.HasPrefix(e, tc.event) {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("event %q missing in %v", tc.event, *events)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package main
|
||||
|
||||
var projectsTimePages = []page{
|
||||
{ID: "375", Name: "IssueCard", ZhName: "任务卡片", Category: projectsCategory, Demo: projectsDemoIssueCard, Keys: "Tab 聚焦;Enter/Space 激活。", Code: `statuses := []mujica.WorkflowStatus{{ID: "todo", Name: "To do", Group: mujica.StatusUnstarted}, {ID: "done", Name: "Done", Group: mujica.StatusCompleted}}
|
||||
task := mujica.Task{ID: "ENG-1", Title: "Plan the release", Status: "todo", Priority: mujica.PriorityHigh}
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
if mujica.IssueCard(c, task, mujica.IssueCardOptions{Statuses: statuses}).Clicked() {
|
||||
log.Println("open", task.ID)
|
||||
}
|
||||
}
|
||||
ui.Render(view, 1000, 700, 1)`},
|
||||
{ID: "376", Name: "IssueIdBadge", ZhName: "任务编号", Category: projectsCategory, Demo: projectsDemoIssueIdBadge, Keys: "Copyable 开启时 Tab 聚焦,Enter/Space 复制。", Code: `id := "ENG-123"
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
if mujica.IssueIdBadge(c, id, mujica.IssueIdBadgeOptions{Copyable: true}).Changed() {
|
||||
log.Println("copied", id)
|
||||
}
|
||||
}
|
||||
ui.Render(view, 1000, 700, 1)`},
|
||||
{ID: "377", Name: "MilestoneProgress", ZhName: "里程碑进度", Category: projectsCategory, Demo: projectsDemoMilestoneProgress, Keys: "静态展示,进度条通过无障碍名称与百分比读取。", Code: `milestone := mujica.Milestone{Title: "Release", Due: mujica.Date{Year: 2026, Month: 10, Day: 14}, Done: 3, Total: 8}
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.MilestoneProgress(c, milestone, mujica.MilestoneProgressOptions{})
|
||||
}
|
||||
ui.Render(view, 1000, 700, 1)`},
|
||||
{ID: "378", Name: "WorkloadView", ZhName: "成员负载", Category: projectsCategory, Demo: projectsDemoWorkloadView, Keys: "静态行通过无障碍名称读取负载与超出量;滚动查看更多成员。", Code: `loads := []mujica.Workload{{Member: mujica.Member{ID: "ann", Name: "Ann"}, Assigned: 46, Capacity: 40}}
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
mujica.WorkloadView(c, loads, mujica.WorkloadViewOptions{})
|
||||
}
|
||||
ui.Render(view, 1000, 700, 1)`},
|
||||
{ID: "379", Name: "TimeTracker", ZhName: "工时计时", Category: projectsCategory, Demo: projectsDemoTimeTracker, Keys: "Tab 移动;Enter/Space 开始停止;停止后打开修正,编辑时长,Apply 提交。", Code: `var state mujica.TimeTrackerState
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
if mujica.TimeTracker(c, &state, mujica.TimeTrackerOptions{}).Changed() {
|
||||
log.Println("tracked", state.Total(c.Now()))
|
||||
}
|
||||
}
|
||||
ui.Render(view, 1000, 700, 1)`},
|
||||
{ID: "380", Name: "PomodoroTimer", ZhName: "番茄钟", Category: projectsCategory, Demo: projectsDemoPomodoroTimer, Keys: "Tab 移动;Enter/Space 开始、暂停、跳过与重置。", Code: `var state mujica.PomodoroState
|
||||
view := func(c *ui.Context) {
|
||||
mujica.Use(c, mujica.Settings{})
|
||||
r := mujica.PomodoroTimer(c, &state, mujica.PomodoroTimerOptions{Focus: 25 * time.Minute})
|
||||
if r.Changed() {
|
||||
log.Println("timer", state.Phase(), state.Running())
|
||||
}
|
||||
if phase, ok := r.Finished(); ok {
|
||||
log.Println("finished", phase)
|
||||
}
|
||||
}
|
||||
ui.Render(view, 1000, 700, 1)`},
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/egoist/mygo/ui"
|
||||
mujica "mujicaui"
|
||||
"time"
|
||||
)
|
||||
|
||||
func projectsDemoMilestoneProgress(c *ui.Context, log func(string)) {
|
||||
done := ui.Local(c.Root(), "377", func() float64 { return 3 })
|
||||
if mujica.NumberInput(c, done, mujica.NumberInputOptions{Min: 0, Max: 8, Step: 1, Label: "Completed tasks"}).Changed() {
|
||||
log(fmt.Sprintf("completed = %g", *done))
|
||||
}
|
||||
mujica.MilestoneProgress(c, mujica.Milestone{Title: "Release", Due: mujica.Date{Year: 2026, Month: 10, Day: 14}, Done: int(*done), Total: 8}, mujica.MilestoneProgressOptions{})
|
||||
}
|
||||
|
||||
func projectsDemoWorkloadView(c *ui.Context, log func(string)) {
|
||||
s := ui.Local(c.Root(), "378", func() struct {
|
||||
assigned float64
|
||||
loads []mujica.Workload
|
||||
version int
|
||||
} {
|
||||
return struct {
|
||||
assigned float64
|
||||
loads []mujica.Workload
|
||||
version int
|
||||
}{assigned: 46, loads: []mujica.Workload{{Member: projectsMembers[0], Assigned: 32, Capacity: 40}, {Member: projectsMembers[1], Assigned: 46, Capacity: 40}, {Member: projectsMembers[2], Assigned: 12, Capacity: 20}}}
|
||||
})
|
||||
if mujica.NumberInput(c, &s.assigned, mujica.NumberInputOptions{Min: 0, Max: 80, Step: 1, Label: "Ben assigned hours"}).Changed() {
|
||||
s.loads[1].Assigned = s.assigned
|
||||
s.version++
|
||||
log(fmt.Sprintf("assigned = %g", s.assigned))
|
||||
}
|
||||
ui.Box(c).Height(240).FillWidth().Children(func() { mujica.WorkloadView(c, s.loads, mujica.WorkloadViewOptions{Version: s.version}) })
|
||||
}
|
||||
|
||||
func projectsDemoTimeTracker(c *ui.Context, log func(string)) {
|
||||
s := ui.Local(c.Root(), "379", func() struct {
|
||||
timer mujica.TimeTrackerState
|
||||
disabled bool
|
||||
} {
|
||||
return struct {
|
||||
timer mujica.TimeTrackerState
|
||||
disabled bool
|
||||
}{}
|
||||
})
|
||||
projectsCheck(c, log, &s.disabled, "Disabled")
|
||||
ui.ScrollHorizontal(c).Height(120).FillWidth().Children(func() {
|
||||
if mujica.TimeTracker(c, &s.timer, mujica.TimeTrackerOptions{Disabled: s.disabled}).Changed() {
|
||||
log("tracked " + s.timer.Total(c.Now()).String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func projectsDemoPomodoroTimer(c *ui.Context, log func(string)) {
|
||||
s := ui.Local(c.Root(), "380", func() struct {
|
||||
timer mujica.PomodoroState
|
||||
disabled, hidden bool
|
||||
seconds float64
|
||||
} {
|
||||
return struct {
|
||||
timer mujica.PomodoroState
|
||||
disabled, hidden bool
|
||||
seconds float64
|
||||
}{seconds: 10}
|
||||
})
|
||||
projectsCheck(c, log, &s.disabled, "Disabled")
|
||||
projectsCheck(c, log, &s.hidden, "Hidden")
|
||||
if mujica.NumberInput(c, &s.seconds, mujica.NumberInputOptions{Min: 1, Max: 1500, Step: 1, Label: "Focus seconds", Disabled: s.timer.Running()}).Changed() {
|
||||
log(fmt.Sprintf("focus = %g seconds", s.seconds))
|
||||
}
|
||||
if s.hidden {
|
||||
return
|
||||
}
|
||||
r := mujica.PomodoroTimer(c, &s.timer, mujica.PomodoroTimerOptions{Focus: time.Duration(s.seconds) * time.Second, Break: 5 * time.Second, LongBreak: 10 * time.Second, LongEvery: 2, Disabled: s.disabled})
|
||||
if r.Changed() {
|
||||
log(fmt.Sprintf("timer phase = %d, running = %v", s.timer.Phase(), s.timer.Running()))
|
||||
}
|
||||
if phase, ok := r.Finished(); ok {
|
||||
log(fmt.Sprintf("finished phase %d", phase))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user