@monkey-mini-app/ui 0.1.1 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -6
- package/dist/globals.css +1 -1
- package/dist/index.js +12 -2
- package/dist/manifest.json +6 -6
- package/dist/runtime.js +9 -0
- package/dist/sdk.js +158 -0
- package/dist/src/blocks/activity-feed.tsx +7 -0
- package/dist/src/blocks/app-shell.tsx +1 -0
- package/dist/src/blocks/attachment-gallery.tsx +7 -0
- package/dist/src/blocks/bar-chart.tsx +11 -0
- package/dist/src/blocks/comment-thread.tsx +7 -0
- package/dist/src/blocks/dashboard-shell.tsx +109 -0
- package/dist/src/blocks/description-list.tsx +7 -0
- package/dist/src/blocks/detail-panel.tsx +1 -0
- package/dist/src/blocks/donut-chart.tsx +7 -0
- package/dist/src/blocks/env-badge.tsx +7 -0
- package/dist/src/blocks/env-table.tsx +316 -0
- package/dist/src/blocks/file-tree.tsx +7 -0
- package/dist/src/blocks/filter-bar.tsx +1 -0
- package/dist/src/blocks/form-sheet.tsx +160 -0
- package/dist/src/blocks/gauge.tsx +7 -0
- package/dist/src/blocks/list-detail.tsx +121 -0
- package/dist/src/blocks/notification-center.tsx +7 -0
- package/dist/src/blocks/page-header.tsx +1 -0
- package/dist/src/blocks/progress-ring.tsx +7 -0
- package/dist/src/blocks/radar-chart.tsx +7 -0
- package/dist/src/blocks/request-inspector.tsx +7 -0
- package/dist/src/blocks/reveal.tsx +56 -0
- package/dist/src/blocks/run-timeline.tsx +7 -0
- package/dist/src/blocks/scrollspy.tsx +7 -0
- package/dist/src/blocks/settings-split.tsx +154 -0
- package/dist/src/blocks/severity-chip.tsx +7 -0
- package/dist/src/blocks/sparkline.tsx +7 -0
- package/dist/src/blocks/stat-card.tsx +1 -0
- package/dist/src/blocks/status-badge.tsx +1 -0
- package/dist/src/blocks/table-page.tsx +83 -0
- package/dist/src/blocks/terminal.tsx +21 -3
- package/dist/src/blocks/test-step-list.tsx +7 -0
- package/dist/src/blocks/trend-card.tsx +7 -0
- package/dist/src/blocks/wizard-shell.tsx +132 -0
- package/dist/src/cdn-modules.d.ts +5 -0
- package/dist/src/components/accordion.tsx +7 -0
- package/dist/src/components/alert-dialog.tsx +7 -0
- package/dist/src/components/alert.tsx +7 -0
- package/dist/src/components/aspect-ratio.tsx +4 -0
- package/dist/src/components/attachment.tsx +4 -0
- package/dist/src/components/avatar.tsx +4 -0
- package/dist/src/components/badge.tsx +7 -0
- package/dist/src/components/breadcrumb.tsx +4 -0
- package/dist/src/components/bubble.tsx +4 -0
- package/dist/src/components/button-group.tsx +6 -0
- package/dist/src/components/button.tsx +7 -0
- package/dist/src/components/calendar.tsx +4 -0
- package/dist/src/components/card.tsx +8 -1
- package/dist/src/components/chart.tsx +7 -0
- package/dist/src/components/checkbox.tsx +5 -0
- package/dist/src/components/collapsible.tsx +4 -0
- package/dist/src/components/combobox.tsx +4 -0
- package/dist/src/components/command.tsx +6 -0
- package/dist/src/components/context-menu.tsx +4 -0
- package/dist/src/components/dialog.tsx +21 -0
- package/dist/src/components/drawer.tsx +4 -0
- package/dist/src/components/dropdown-menu.tsx +7 -0
- package/dist/src/components/empty.tsx +7 -0
- package/dist/src/components/field.tsx +6 -0
- package/dist/src/components/hover-card.tsx +4 -0
- package/dist/src/components/input-group.tsx +4 -0
- package/dist/src/components/input.tsx +7 -0
- package/dist/src/components/item.tsx +4 -0
- package/dist/src/components/kbd.tsx +4 -0
- package/dist/src/components/label.tsx +7 -0
- package/dist/src/components/marker.tsx +4 -0
- package/dist/src/components/menubar.tsx +4 -0
- package/dist/src/components/message-scroller.tsx +4 -0
- package/dist/src/components/message.tsx +4 -0
- package/dist/src/components/native-select.tsx +4 -0
- package/dist/src/components/navigation-menu.tsx +4 -0
- package/dist/src/components/pagination.tsx +4 -0
- package/dist/src/components/popover.tsx +7 -0
- package/dist/src/components/progress.tsx +7 -0
- package/dist/src/components/questionnaire.tsx +4 -0
- package/dist/src/components/radio-group.tsx +4 -0
- package/dist/src/components/resizable.tsx +7 -0
- package/dist/src/components/scroll-area.tsx +4 -0
- package/dist/src/components/select.tsx +7 -0
- package/dist/src/components/separator.tsx +5 -0
- package/dist/src/components/sheet.tsx +22 -0
- package/dist/src/components/sidebar.tsx +4 -0
- package/dist/src/components/skeleton.tsx +7 -0
- package/dist/src/components/slider.tsx +4 -0
- package/dist/src/components/spinner.tsx +6 -0
- package/dist/src/components/switch.tsx +7 -0
- package/dist/src/components/table.tsx +7 -0
- package/dist/src/components/tabs.tsx +7 -0
- package/dist/src/components/textarea.tsx +7 -0
- package/dist/src/components/toast.tsx +7 -0
- package/dist/src/components/toggle-group.tsx +4 -0
- package/dist/src/components/toggle.tsx +4 -0
- package/dist/src/components/tooltip.tsx +7 -0
- package/dist/src/composites/autocomplete.tsx +7 -0
- package/dist/src/composites/cascader.tsx +7 -0
- package/dist/src/composites/color-picker.tsx +7 -0
- package/dist/src/composites/confirm-dialog.tsx +7 -0
- package/dist/src/composites/copyable.tsx +9 -1
- package/dist/src/composites/currency-input.tsx +7 -0
- package/dist/src/composites/date-picker.tsx +7 -0
- package/dist/src/composites/date-range-picker.tsx +4 -0
- package/dist/src/composites/date-time-picker.tsx +7 -0
- package/dist/src/composites/date-time-range-picker.tsx +7 -0
- package/dist/src/composites/duration-input.tsx +7 -0
- package/dist/src/composites/mini-calendar.tsx +7 -0
- package/dist/src/composites/number-field.tsx +7 -0
- package/dist/src/composites/password-field.tsx +7 -0
- package/dist/src/composites/phone-input.tsx +7 -0
- package/dist/src/composites/rating.tsx +7 -0
- package/dist/src/composites/relative-date-picker.tsx +7 -0
- package/dist/src/composites/search-input.tsx +7 -0
- package/dist/src/composites/slider-range.tsx +7 -0
- package/dist/src/composites/tag-input.tsx +7 -0
- package/dist/src/composites/time-picker.tsx +11 -1
- package/dist/src/composites/time-range-picker.tsx +7 -0
- package/dist/src/composites/timezone-select.tsx +7 -0
- package/dist/src/composites/transfer.tsx +7 -0
- package/dist/src/composites/user-picker.tsx +7 -0
- package/dist/src/hooks/use-html-dark.ts +37 -0
- package/dist/src/hooks/use-mobile.ts +3 -0
- package/dist/src/i18n/en.ts +109 -2
- package/dist/src/i18n/zh.ts +99 -2
- package/dist/src/index.ts +147 -134
- package/dist/src/lib/clipboard.ts +10 -0
- package/dist/src/lib/illustrations.tsx +2 -2
- package/dist/src/products/code-block.tsx +35 -18
- package/dist/src/products/code-editor.tsx +156 -24
- package/dist/src/products/commit-graph.tsx +644 -0
- package/dist/src/products/data-grid.tsx +2 -1
- package/dist/src/products/diff-viewer.tsx +104 -8
- package/dist/src/products/event-calendar.tsx +28 -823
- package/dist/src/products/file-dropzone.tsx +7 -0
- package/dist/src/products/full-calendar/animations.ts +38 -0
- package/dist/src/products/full-calendar/calendar-body.tsx +77 -0
- package/dist/src/products/full-calendar/constants.ts +10 -0
- package/dist/src/products/full-calendar/contexts/calendar-context.tsx +255 -0
- package/dist/src/products/full-calendar/contexts/dnd-context.tsx +157 -0
- package/dist/src/products/full-calendar/dialogs/add-edit-event-dialog.tsx +187 -0
- package/dist/src/products/full-calendar/dialogs/delete-event-dialog.tsx +39 -0
- package/dist/src/products/full-calendar/dialogs/event-details-dialog.tsx +100 -0
- package/dist/src/products/full-calendar/dialogs/events-list-dialog.tsx +87 -0
- package/dist/src/products/full-calendar/dnd/draggable-event.tsx +45 -0
- package/dist/src/products/full-calendar/dnd/droppable-area.tsx +45 -0
- package/dist/src/products/full-calendar/dnd/resizable-event.tsx +179 -0
- package/dist/src/products/full-calendar/header/calendar-header.tsx +65 -0
- package/dist/src/products/full-calendar/header/date-navigator.tsx +110 -0
- package/dist/src/products/full-calendar/header/filter.tsx +58 -0
- package/dist/src/products/full-calendar/header/today-button.tsx +45 -0
- package/dist/src/products/full-calendar/header/user-select.tsx +64 -0
- package/dist/src/products/full-calendar/header/view-tabs.tsx +36 -0
- package/dist/src/products/full-calendar/helpers.ts +453 -0
- package/dist/src/products/full-calendar/hooks/use-time-range-create.ts +152 -0
- package/dist/src/products/full-calendar/hooks.ts +70 -0
- package/dist/src/products/full-calendar/interfaces.ts +23 -0
- package/dist/src/products/full-calendar/settings/settings.tsx +80 -0
- package/dist/src/products/full-calendar/shims/avatar-group.tsx +16 -0
- package/dist/src/products/full-calendar/shims/day-picker.tsx +1 -0
- package/dist/src/products/full-calendar/shims/motion.tsx +97 -0
- package/dist/src/products/full-calendar/shims/next-themes.ts +6 -0
- package/dist/src/products/full-calendar/shims/re-resizable.tsx +25 -0
- package/dist/src/products/full-calendar/shims/sonner.ts +4 -0
- package/dist/src/products/full-calendar/types.ts +8 -0
- package/dist/src/products/full-calendar/views/agenda-view/agenda-events.tsx +140 -0
- package/dist/src/products/full-calendar/views/month-view/calendar-month-view.tsx +69 -0
- package/dist/src/products/full-calendar/views/month-view/day-cell.tsx +201 -0
- package/dist/src/products/full-calendar/views/month-view/event-bullet.tsx +40 -0
- package/dist/src/products/full-calendar/views/month-view/month-event-badge.tsx +143 -0
- package/dist/src/products/full-calendar/views/week-and-day-view/calendar-day-view.tsx +295 -0
- package/dist/src/products/full-calendar/views/week-and-day-view/calendar-time-line.tsx +35 -0
- package/dist/src/products/full-calendar/views/week-and-day-view/calendar-week-view.tsx +253 -0
- package/dist/src/products/full-calendar/views/week-and-day-view/day-view-multi-day-events-row.tsx +73 -0
- package/dist/src/products/full-calendar/views/week-and-day-view/event-block.tsx +114 -0
- package/dist/src/products/full-calendar/views/week-and-day-view/render-grouped-events.tsx +49 -0
- package/dist/src/products/full-calendar/views/week-and-day-view/week-view-multi-day-events-row.tsx +143 -0
- package/dist/src/products/full-calendar/views/year-view/calendar-year-view.tsx +161 -0
- package/dist/src/products/gantt.tsx +7 -0
- package/dist/src/products/jira-wiki.tsx +1 -0
- package/dist/src/products/jql-input.tsx +340 -37
- package/dist/src/products/json-viewer.tsx +7 -0
- package/dist/src/products/kanban-issue-panel.tsx +7 -0
- package/dist/src/products/kanban.tsx +1 -0
- package/dist/src/products/log-viewer.tsx +390 -18
- package/dist/src/products/markdown-editor.tsx +21 -6
- package/dist/src/products/markdown.tsx +7 -0
- package/dist/src/products/rich-text-editor.tsx +65 -59
- package/dist/src/products/sortable-list.tsx +7 -0
- package/dist/src/products/stepper.tsx +219 -70
- package/dist/src/products/timeline.tsx +7 -0
- package/dist/src/products/tree-view.tsx +7 -0
- package/dist/src/sdk/app-error-boundary.tsx +240 -0
- package/dist/src/sdk/app-id.ts +7 -0
- package/dist/src/sdk/use-app.ts +126 -0
- package/dist/src/styles/globals.css +1 -0
- package/dist/vendors/lodash.js +20 -0
- package/dist/vendors/motion.js +12 -0
- package/package.json +20 -19
- package/dist/src/products/jql-language.ts +0 -70
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { format, getYear, isSameDay, isSameMonth } from "date-fns";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { motion } from "../../shims/motion";
|
|
4
|
+
import { cn } from "@monkey-mini-app/ui/lib/utils";
|
|
5
|
+
import { useDateLocale } from "@monkey-mini-app/ui/i18n/context";
|
|
6
|
+
import { staggerContainer, transition } from "../../animations";
|
|
7
|
+
import { useCalendar } from "../../contexts/calendar-context";
|
|
8
|
+
import { EventListDialog } from "../../dialogs/events-list-dialog";
|
|
9
|
+
import { getCalendarCells } from "../../helpers";
|
|
10
|
+
import type { IEvent } from "../../interfaces";
|
|
11
|
+
import { EventBullet } from "../month-view/event-bullet";
|
|
12
|
+
|
|
13
|
+
interface IProps {
|
|
14
|
+
singleDayEvents: IEvent[];
|
|
15
|
+
multiDayEvents: IEvent[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function CalendarYearView({ singleDayEvents, multiDayEvents }: IProps) {
|
|
19
|
+
const { selectedDate, setSelectedDate } = useCalendar();
|
|
20
|
+
const dateLocale = useDateLocale();
|
|
21
|
+
const currentYear = getYear(selectedDate);
|
|
22
|
+
const allEvents = [...multiDayEvents, ...singleDayEvents];
|
|
23
|
+
|
|
24
|
+
const months = useMemo(
|
|
25
|
+
() =>
|
|
26
|
+
Array.from({ length: 12 }, (_, i) =>
|
|
27
|
+
format(new Date(2000, i, 1), "MMMM", { locale: dateLocale }),
|
|
28
|
+
),
|
|
29
|
+
[dateLocale],
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// Jan 2, 2000 was a Sunday — generate Sun..Sat short weekday labels.
|
|
33
|
+
const weekdays = useMemo(
|
|
34
|
+
() =>
|
|
35
|
+
Array.from({ length: 7 }, (_, i) =>
|
|
36
|
+
format(new Date(2000, 0, 2 + i), "EE", { locale: dateLocale }),
|
|
37
|
+
),
|
|
38
|
+
[dateLocale],
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="flex flex-col h-full overflow-y-auto p-4 sm:p-6">
|
|
43
|
+
{/* Year grid */}
|
|
44
|
+
<motion.div
|
|
45
|
+
initial="initial"
|
|
46
|
+
animate="animate"
|
|
47
|
+
variants={staggerContainer}
|
|
48
|
+
className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 auto-rows-fr"
|
|
49
|
+
>
|
|
50
|
+
{months.map((month, monthIndex) => {
|
|
51
|
+
const monthDate = new Date(currentYear, monthIndex, 1);
|
|
52
|
+
const cells = getCalendarCells(monthDate);
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<motion.div
|
|
56
|
+
key={month}
|
|
57
|
+
className="flex flex-col border border-border rounded-lg shadow-sm overflow-hidden"
|
|
58
|
+
initial={{ opacity: 0, scale: 0.95 }}
|
|
59
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
60
|
+
transition={{ delay: monthIndex * 0.05, ...transition }}
|
|
61
|
+
aria-label={`${month} ${currentYear} calendar`}
|
|
62
|
+
>
|
|
63
|
+
{/* Month header */}
|
|
64
|
+
<button
|
|
65
|
+
type="button"
|
|
66
|
+
className="w-full px-3 py-2 text-center font-semibold text-sm sm:text-base cursor-pointer hover:bg-primary/20 transition-colors bg-transparent border-none appearance-none"
|
|
67
|
+
onClick={() =>
|
|
68
|
+
setSelectedDate(new Date(currentYear, monthIndex, 1))
|
|
69
|
+
}
|
|
70
|
+
aria-label={`Select ${month}`}
|
|
71
|
+
>
|
|
72
|
+
{month}
|
|
73
|
+
</button>
|
|
74
|
+
|
|
75
|
+
<div className="grid grid-cols-7 text-center text-xs font-medium text-muted-foreground py-2">
|
|
76
|
+
{weekdays.map((day) => (
|
|
77
|
+
<div key={day} className="p-1">
|
|
78
|
+
{day}
|
|
79
|
+
</div>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<div className="grid grid-cols-7 gap-0.5 p-1.5 flex-grow text-xs">
|
|
84
|
+
{cells.map((cell) => {
|
|
85
|
+
const isCurrentMonth = isSameMonth(cell.date, monthDate);
|
|
86
|
+
const isToday = isSameDay(cell.date, new Date());
|
|
87
|
+
const dayEvents = allEvents.filter((event) =>
|
|
88
|
+
isSameDay(new Date(event.startDate), cell.date),
|
|
89
|
+
);
|
|
90
|
+
const hasEvents = dayEvents.length > 0;
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div
|
|
94
|
+
key={cell.date.toISOString()}
|
|
95
|
+
className={cn(
|
|
96
|
+
"flex flex-col items-center justify-start p-1 min-h-[2rem] relative",
|
|
97
|
+
!isCurrentMonth && "text-muted-foreground/40",
|
|
98
|
+
hasEvents && isCurrentMonth
|
|
99
|
+
? "cursor-pointer hover:bg-accent/20 hover:rounded-md"
|
|
100
|
+
: "cursor-default",
|
|
101
|
+
)}
|
|
102
|
+
>
|
|
103
|
+
{isCurrentMonth && hasEvents ? (
|
|
104
|
+
<EventListDialog date={cell.date} events={dayEvents}>
|
|
105
|
+
<div className="w-full h-full flex flex-col items-center justify-start gap-0.5">
|
|
106
|
+
<span
|
|
107
|
+
className={cn(
|
|
108
|
+
"size-5 flex items-center justify-center font-medium",
|
|
109
|
+
isToday &&
|
|
110
|
+
"rounded-full bg-primary text-primary-foreground",
|
|
111
|
+
)}
|
|
112
|
+
>
|
|
113
|
+
{cell.day}
|
|
114
|
+
</span>
|
|
115
|
+
<div className="flex justify-center items-center gap-0.5">
|
|
116
|
+
{dayEvents.length <= 2 ? (
|
|
117
|
+
dayEvents
|
|
118
|
+
.slice(0, 2)
|
|
119
|
+
.map((event) => (
|
|
120
|
+
<EventBullet
|
|
121
|
+
key={event.id}
|
|
122
|
+
color={event.color}
|
|
123
|
+
className="size-1.5"
|
|
124
|
+
/>
|
|
125
|
+
))
|
|
126
|
+
) : (
|
|
127
|
+
<div className="flex flex-col justify-center items-center">
|
|
128
|
+
<EventBullet
|
|
129
|
+
color={dayEvents[0].color}
|
|
130
|
+
className="size-1.5"
|
|
131
|
+
/>
|
|
132
|
+
<span className="text-[0.6rem]">
|
|
133
|
+
+{dayEvents.length - 1}
|
|
134
|
+
</span>
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
</EventListDialog>
|
|
140
|
+
) : (
|
|
141
|
+
<div className="w-full h-full flex flex-col items-center justify-start">
|
|
142
|
+
<span
|
|
143
|
+
className={cn(
|
|
144
|
+
"size-5 flex items-center justify-center font-medium",
|
|
145
|
+
)}
|
|
146
|
+
>
|
|
147
|
+
{cell.day}
|
|
148
|
+
</span>
|
|
149
|
+
</div>
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
})}
|
|
154
|
+
</div>
|
|
155
|
+
</motion.div>
|
|
156
|
+
);
|
|
157
|
+
})}
|
|
158
|
+
</motion.div>
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
@@ -9,6 +9,13 @@ export type GanttTask = {
|
|
|
9
9
|
end: Date
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Task bars on a date axis.
|
|
14
|
+
* @when Plans/schedules with start+end per row. Ordered events → `Timeline`.
|
|
15
|
+
* @example
|
|
16
|
+
* <Gantt tasks={[{ id: "t1", name: "联调", start: "2026-09-01", end: "2026-09-05" }]} />
|
|
17
|
+
* @family Calendar & date
|
|
18
|
+
*/
|
|
12
19
|
export function Gantt({ tasks }: { tasks: GanttTask[] }) {
|
|
13
20
|
if (tasks.length === 0) {
|
|
14
21
|
return <div data-testid="gantt" className="text-muted-foreground text-sm">No tasks</div>
|
|
@@ -1,62 +1,365 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { autocompletion } from "@codemirror/autocomplete"
|
|
5
|
-
import CodeMirror from "@uiw/react-codemirror"
|
|
3
|
+
import { useEffect, useRef, useState } from "react"
|
|
6
4
|
|
|
7
|
-
import {
|
|
5
|
+
import { useHtmlDark } from "@monkey-mini-app/ui/hooks/use-html-dark"
|
|
8
6
|
import { useLabels } from "@monkey-mini-app/ui/i18n/context"
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
7
|
+
import { cn } from "@monkey-mini-app/ui/lib/utils"
|
|
8
|
+
|
|
9
|
+
export type JqlSuggestItem = { name: string; type?: string }
|
|
10
|
+
|
|
11
|
+
/** JQL words that are never a field name or a value. */
|
|
12
|
+
const KEYWORDS = [
|
|
13
|
+
"AND",
|
|
14
|
+
"OR",
|
|
15
|
+
"NOT",
|
|
16
|
+
"IN",
|
|
17
|
+
"IS",
|
|
18
|
+
"EMPTY",
|
|
19
|
+
"NULL",
|
|
20
|
+
"WAS",
|
|
21
|
+
"CHANGED",
|
|
22
|
+
"ON",
|
|
23
|
+
"BY",
|
|
24
|
+
"ORDER",
|
|
25
|
+
"GROUP",
|
|
26
|
+
"ASC",
|
|
27
|
+
"DESC",
|
|
28
|
+
"BEFORE",
|
|
29
|
+
"AFTER",
|
|
30
|
+
"DURING",
|
|
31
|
+
"FOLLOWED",
|
|
32
|
+
"TO",
|
|
33
|
+
"FROM",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
const OPERATORS = ["=", "!=", ">", ">=", "<", "<=", "~", "!~"]
|
|
37
|
+
|
|
38
|
+
/** Words that start a clause, so `ORDER` + `BY` completes as one pair. */
|
|
39
|
+
const PHRASES = ["ORDER BY", "GROUP BY", "IS EMPTY", "IS NOT NULL", "FOLLOWED BY", "CHANGED ON", "WAS BEFORE", "IN ("]
|
|
16
40
|
|
|
41
|
+
const FUNCTIONS = [
|
|
42
|
+
"now()",
|
|
43
|
+
"startOfDay()",
|
|
44
|
+
"startOfWeek()",
|
|
45
|
+
"startOfMonth()",
|
|
46
|
+
"startOfQuarter()",
|
|
47
|
+
"startOfYear()",
|
|
48
|
+
"-1d",
|
|
49
|
+
"-7d",
|
|
50
|
+
"+2w",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
function bare<T extends object>(m: T & { default?: T }): T {
|
|
54
|
+
return (m.default ?? m) as T
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type CmCore = {
|
|
58
|
+
EditorView: any
|
|
59
|
+
keymap: any
|
|
60
|
+
placeholder: any
|
|
61
|
+
syntaxHighlighting: any
|
|
62
|
+
HighlightStyle: any
|
|
63
|
+
StreamLanguage: any
|
|
64
|
+
autocompletion: any
|
|
65
|
+
completionKeymap: any
|
|
66
|
+
tags: any
|
|
67
|
+
EditorState: any
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** CodeMirror 6 core, loaded once per page (esm.sh, never npm). */
|
|
71
|
+
let corePromise: Promise<CmCore> | null = null
|
|
72
|
+
function loadCore(): Promise<CmCore> {
|
|
73
|
+
corePromise ??= (async () => {
|
|
74
|
+
const [viewMod, stateMod, langMod, acMod, tagsMod] = await Promise.all([
|
|
75
|
+
import("https://esm.sh/@codemirror/view@6"),
|
|
76
|
+
import("https://esm.sh/@codemirror/state@6"),
|
|
77
|
+
import("https://esm.sh/@codemirror/language@6"),
|
|
78
|
+
import("https://esm.sh/@codemirror/autocomplete@6"),
|
|
79
|
+
import("https://esm.sh/@lezer/highlight@1"),
|
|
80
|
+
])
|
|
81
|
+
const view = bare(viewMod as any)
|
|
82
|
+
const state = bare(stateMod as any)
|
|
83
|
+
const lang = bare(langMod as any)
|
|
84
|
+
const ac = bare(acMod as any)
|
|
85
|
+
const hl = bare(tagsMod as any)
|
|
86
|
+
return {
|
|
87
|
+
EditorView: view.EditorView,
|
|
88
|
+
keymap: view.keymap,
|
|
89
|
+
placeholder: view.placeholder,
|
|
90
|
+
EditorState: state.EditorState,
|
|
91
|
+
syntaxHighlighting: lang.syntaxHighlighting,
|
|
92
|
+
HighlightStyle: lang.HighlightStyle,
|
|
93
|
+
StreamLanguage: lang.StreamLanguage,
|
|
94
|
+
autocompletion: ac.autocompletion,
|
|
95
|
+
completionKeymap: ac.completionKeymap,
|
|
96
|
+
tags: hl.tags,
|
|
97
|
+
}
|
|
98
|
+
})().catch((err) => {
|
|
99
|
+
corePromise = null
|
|
100
|
+
throw err
|
|
101
|
+
})
|
|
102
|
+
return corePromise
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Stream tokenizer: `"quoted"` / `'quoted'`, numbers, dates, relative intervals,
|
|
107
|
+
* operators, keywords, then a word is a **field** when an operator follows it.
|
|
108
|
+
*/
|
|
17
109
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
110
|
+
* The subset of CodeMirror's `StringStream` this tokenizer touches. `StreamLanguage` arrives
|
|
111
|
+
* as `any` (the module is loaded from a CDN at runtime), so without this the `token`
|
|
112
|
+
* parameter silently degrades to an implicit any and the guard calls go unchecked.
|
|
113
|
+
*/
|
|
114
|
+
type TokenStream = {
|
|
115
|
+
eatSpace(): boolean
|
|
116
|
+
next(): string | false
|
|
117
|
+
match(re: RegExp, consume?: boolean): RegExpMatchArray | null
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function jqlLanguage(core: CmCore) {
|
|
121
|
+
const kw = new Set(KEYWORDS)
|
|
122
|
+
return core.StreamLanguage.define({
|
|
123
|
+
name: "jql",
|
|
124
|
+
token(stream: TokenStream) {
|
|
125
|
+
if (stream.eatSpace()) return null
|
|
126
|
+
if (stream.match(/^'[^']*'/) || stream.match(/^"[^"]*"/)) return "string"
|
|
127
|
+
if (stream.match(/^[+-]?\d+(h|d|w|mo|q|y)\b/)) return "number"
|
|
128
|
+
if (stream.match(/^\d{4}-\d{2}-\d{2}/)) return "number"
|
|
129
|
+
if (stream.match(/^[+-]?\d+(\.\d+)?/)) return "number"
|
|
130
|
+
if (stream.match(/^(!=|>=|<=|!~|=|>|<|~)/)) return "operator"
|
|
131
|
+
if (stream.match(/^[,()[\]]/)) return "operator"
|
|
132
|
+
const word = stream.match(/^[A-Za-z_][\w.()-]*/)
|
|
133
|
+
if (word) {
|
|
134
|
+
const raw = String(word[0])
|
|
135
|
+
if (kw.has(raw.toUpperCase())) return "keyword"
|
|
136
|
+
const followedByOp =
|
|
137
|
+
stream.match(/^\s*(=|!=|>=|<=|>|<|~|!~)/, false) ||
|
|
138
|
+
stream.match(/^\s*(IN|IS|WAS|CHANGED|FOLLOWED|ORDER|GROUP)\b/i, false)
|
|
139
|
+
return followedByOp ? "property" : "value"
|
|
140
|
+
}
|
|
141
|
+
if (stream.match(/^[^\s]+/)) return "value"
|
|
142
|
+
stream.next()
|
|
143
|
+
return null
|
|
144
|
+
},
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Token classes are coloured from CSS vars so light/dark and palettes apply. */
|
|
149
|
+
function jqlHighlight(core: CmCore) {
|
|
150
|
+
const { tags } = core
|
|
151
|
+
return core.HighlightStyle.define([
|
|
152
|
+
{ tag: tags.keyword, class: "cm-jql-kw" },
|
|
153
|
+
{ tag: tags.propertyName, class: "cm-jql-prop" },
|
|
154
|
+
{ tag: tags.string, class: "cm-jql-str" },
|
|
155
|
+
{ tag: tags.number, class: "cm-jql-num" },
|
|
156
|
+
{ tag: tags.operator, class: "cm-jql-op" },
|
|
157
|
+
])
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function jqlTheme(core: CmCore, dark: boolean) {
|
|
161
|
+
return core.EditorView.theme({
|
|
162
|
+
"&": { backgroundColor: "transparent", color: "var(--foreground)", height: "100%" },
|
|
163
|
+
".cm-scroller": { overflow: "auto", fontFamily: "var(--font-mono, ui-monospace, monospace)" },
|
|
164
|
+
".cm-content": { caretColor: "var(--foreground)", padding: "8px 12px" },
|
|
165
|
+
".cm-cursor, .cm-dropCursor": { borderLeftColor: "var(--foreground)" },
|
|
166
|
+
".cm-activeLine": { backgroundColor: "transparent" },
|
|
167
|
+
".cm-selectionBackground, ::selection": {
|
|
168
|
+
backgroundColor: dark ? "rgba(99,130,255,.32)" : "color-mix(in oklch, var(--primary) 22%, transparent)",
|
|
169
|
+
},
|
|
170
|
+
".cm-jql-kw": { color: "var(--primary)", fontWeight: "600" },
|
|
171
|
+
".cm-jql-prop": { color: "var(--foreground)", fontWeight: "600" },
|
|
172
|
+
".cm-jql-str": { color: "var(--chart-2)" },
|
|
173
|
+
".cm-jql-num": { color: "var(--chart-3)" },
|
|
174
|
+
".cm-jql-op": { color: "var(--muted-foreground)" },
|
|
175
|
+
// Completion pop-up follows the panel tokens instead of CM's defaults.
|
|
176
|
+
".cm-tooltip": {
|
|
177
|
+
backgroundColor: "var(--popover)",
|
|
178
|
+
border: "1px solid var(--border)",
|
|
179
|
+
borderRadius: "10px",
|
|
180
|
+
color: "var(--popover-foreground)",
|
|
181
|
+
fontSize: "12px",
|
|
182
|
+
},
|
|
183
|
+
".cm-tooltip-autocomplete ul li[aria-selected]": {
|
|
184
|
+
backgroundColor: "color-mix(in oklch, var(--primary) 18%, transparent)",
|
|
185
|
+
color: "var(--popover-foreground)",
|
|
186
|
+
},
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
type Opt = { label: string; detail?: string; type?: string; boost?: number }
|
|
191
|
+
|
|
192
|
+
function optionFor(name: string, kind: string): Opt {
|
|
193
|
+
return { label: name, type: kind }
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Field names → operators → values, based on what precedes the cursor.
|
|
198
|
+
*/
|
|
199
|
+
function complete(fields: JqlSuggestItem[]) {
|
|
200
|
+
const fieldNames = fields.map((f) => f.name)
|
|
201
|
+
const byName = new Map(fields.map((f) => [f.name, f]))
|
|
202
|
+
const WORD = /([\w."'-]*)$/
|
|
203
|
+
|
|
204
|
+
return (ctx: any) => {
|
|
205
|
+
const before = ctx.state.doc.sliceString(0, ctx.pos)
|
|
206
|
+
const word = String(WORD.exec(before)?.[0] ?? "")
|
|
207
|
+
const from = ctx.pos - word.length
|
|
208
|
+
const clause = before.slice(before.lastIndexOf("\n") + 1)
|
|
209
|
+
const options: Opt[] = []
|
|
210
|
+
|
|
211
|
+
// `field <op> ` → values for that field (plus functions and phrases).
|
|
212
|
+
const assign = /([\w.]+)\s*(=|!=|>=|<=|>|<|~|!~|IS|IN|WAS)\s*$/i.exec(clause)
|
|
213
|
+
if (assign) {
|
|
214
|
+
const name = assign[1]
|
|
215
|
+
if (byName.has(name)) {
|
|
216
|
+
// quoted literal first, then the values worth trying for this field
|
|
217
|
+
options.push({ label: '\"...\"', detail: "literal", type: "text", boost: 999 })
|
|
218
|
+
for (const v of sampleValues(name)) options.push({ label: v, detail: name, type: "value", boost: 500 })
|
|
219
|
+
}
|
|
220
|
+
for (const fn of FUNCTIONS) options.push({ label: fn, detail: "jql value", type: "function", boost: 400 })
|
|
221
|
+
return { from, options, validFor: /^[\w."'-]*$/ }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Just typed a field name → offer operators.
|
|
225
|
+
if (/([\w.]+)\s*$/i.test(clause) && !/\s(and|or|not|by|in|is|was)$/i.test(clause)) {
|
|
226
|
+
for (const op of OPERATORS) options.push({ label: op, type: "operator", boost: 600 })
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Otherwise: fields and clause keywords.
|
|
230
|
+
for (const f of fieldNames) options.push(optionFor(f, "property"))
|
|
231
|
+
for (const p of PHRASES) options.push({ label: p, type: "keyword", boost: -100 })
|
|
232
|
+
for (const k of KEYWORDS) options.push({ label: k, type: "keyword", boost: -200 })
|
|
233
|
+
return { from, options, validFor: /^[\w."'-]*$/ }
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Values worth offering per field — real installs would fetch these from the backend. */
|
|
238
|
+
function sampleValues(field: string): string[] {
|
|
239
|
+
const table: Record<string, string[]> = {
|
|
240
|
+
status: ["Open", "In Progress", "Blocked", "Done"],
|
|
241
|
+
assignee: ["currentUser()", "pwang12", "unassigned"],
|
|
242
|
+
reporter: ["currentUser()"],
|
|
243
|
+
priority: ["Blocker", "Critical", "Major", "Minor", "Trivial"],
|
|
244
|
+
resolution: ["Done", "Won't Do", "Duplicate"],
|
|
245
|
+
issuetype: ["Bug", "Task", "Story", "Sub-task"],
|
|
246
|
+
labels: ["cn", "stg", "hotfix"],
|
|
247
|
+
project: ["TMS", "GLS", "LMA"],
|
|
248
|
+
}
|
|
249
|
+
return table[field.toLowerCase()] ?? []
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* JQL query field: CodeMirror 6 (esm.sh, on demand) with syntax highlighting and
|
|
254
|
+
* field/operator/value completion that opens as you type. Falls back to a plain
|
|
255
|
+
* textarea when the CDN is unreachable — the query still edits.
|
|
256
|
+
* @when Issue search / saved filters. Needs a real field list: pass `fields`.
|
|
20
257
|
* @example
|
|
21
|
-
* <JqlInput value={jql} onChange={setJql} fields={[{ name: "status" }]} />
|
|
258
|
+
* <JqlInput value={jql} onChange={setJql} fields={[{ name: "status" }, { name: "assignee" }]} />
|
|
259
|
+
* @family Form
|
|
22
260
|
*/
|
|
23
261
|
export function JqlInput({
|
|
24
262
|
value,
|
|
25
263
|
onChange,
|
|
26
|
-
fields =
|
|
27
|
-
functions = defaultJqlFunctions,
|
|
28
|
-
height = "88px",
|
|
264
|
+
fields = [],
|
|
29
265
|
className,
|
|
266
|
+
height = "88px",
|
|
30
267
|
}: {
|
|
31
268
|
value: string
|
|
32
269
|
onChange?: (value: string) => void
|
|
270
|
+
/** Field names offered by completion (from your JQL backend). */
|
|
33
271
|
fields?: JqlSuggestItem[]
|
|
34
|
-
functions?: JqlSuggestItem[]
|
|
35
|
-
height?: string
|
|
36
272
|
className?: string
|
|
273
|
+
height?: string
|
|
37
274
|
}) {
|
|
38
275
|
const t = useLabels("jqlInput")
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
276
|
+
const hostRef = useRef<HTMLDivElement>(null)
|
|
277
|
+
const viewRef = useRef<any>(null)
|
|
278
|
+
const onChangeRef = useRef(onChange)
|
|
279
|
+
onChangeRef.current = onChange
|
|
280
|
+
const valueRef = useRef(value)
|
|
281
|
+
valueRef.current = value
|
|
282
|
+
const fieldsRef = useRef(fields)
|
|
283
|
+
fieldsRef.current = fields
|
|
284
|
+
const [cm, setCm] = useState(false)
|
|
285
|
+
const dark = useHtmlDark()
|
|
286
|
+
|
|
287
|
+
useEffect(() => {
|
|
288
|
+
if (!hostRef.current) return
|
|
289
|
+
let cancelled = false
|
|
290
|
+
;(async () => {
|
|
291
|
+
try {
|
|
292
|
+
const core = await loadCore()
|
|
293
|
+
if (cancelled || !hostRef.current) return
|
|
294
|
+
const view = new core.EditorView({
|
|
295
|
+
parent: hostRef.current,
|
|
296
|
+
doc: valueRef.current,
|
|
297
|
+
extensions: [
|
|
298
|
+
core.EditorView.lineWrapping,
|
|
299
|
+
jqlLanguage(core),
|
|
300
|
+
core.syntaxHighlighting(jqlHighlight(core)),
|
|
301
|
+
core.EditorView.darkTheme.of(dark),
|
|
302
|
+
jqlTheme(core, dark),
|
|
303
|
+
core.placeholder(t.placeholder),
|
|
304
|
+
core.autocompletion({
|
|
305
|
+
activateOnTyping: true,
|
|
306
|
+
icons: false,
|
|
307
|
+
defaultTable: undefined,
|
|
308
|
+
override: [complete(fieldsRef.current)],
|
|
309
|
+
}),
|
|
310
|
+
core.keymap.of(core.completionKeymap),
|
|
311
|
+
core.EditorView.updateListener.of((u: any) => {
|
|
312
|
+
if (u.docChanged) onChangeRef.current?.(view.state.doc.toString())
|
|
313
|
+
}),
|
|
314
|
+
],
|
|
315
|
+
})
|
|
316
|
+
viewRef.current = view
|
|
317
|
+
setCm(true)
|
|
318
|
+
} catch (err) {
|
|
319
|
+
// CDN down or build failure: the textarea fallback still edits the query.
|
|
320
|
+
console.warn("[JqlInput] CodeMirror unavailable, using plain textarea", err)
|
|
321
|
+
}
|
|
322
|
+
})()
|
|
323
|
+
return () => {
|
|
324
|
+
cancelled = true
|
|
325
|
+
viewRef.current?.destroy()
|
|
326
|
+
viewRef.current = null
|
|
327
|
+
}
|
|
328
|
+
// `fields` is read through a ref so a new list never remounts the editor.
|
|
329
|
+
}, [dark, t.placeholder])
|
|
330
|
+
|
|
331
|
+
useEffect(() => {
|
|
332
|
+
const view = viewRef.current
|
|
333
|
+
if (!view) return
|
|
334
|
+
const cur = view.state.doc.toString()
|
|
335
|
+
if (value === cur) return
|
|
336
|
+
view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: value } })
|
|
337
|
+
}, [value])
|
|
42
338
|
|
|
43
339
|
return (
|
|
44
|
-
<div
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
340
|
+
<div
|
|
341
|
+
className={cn(
|
|
342
|
+
"overflow-hidden rounded-xl border border-border bg-card",
|
|
343
|
+
className,
|
|
344
|
+
)}
|
|
345
|
+
>
|
|
346
|
+
{!cm && (
|
|
347
|
+
<textarea
|
|
348
|
+
data-testid="jql-input"
|
|
349
|
+
value={value}
|
|
350
|
+
onChange={(e) => onChange?.(e.target.value)}
|
|
351
|
+
placeholder={t.placeholder}
|
|
352
|
+
spellCheck={false}
|
|
353
|
+
className="w-full resize-y overflow-auto bg-transparent px-3 py-2 font-mono text-xs leading-6 outline-none"
|
|
354
|
+
style={{ height }}
|
|
355
|
+
/>
|
|
356
|
+
)}
|
|
357
|
+
<div
|
|
358
|
+
ref={hostRef}
|
|
359
|
+
data-testid={cm ? "jql-input" : undefined}
|
|
360
|
+
className={cn("[&_.cm-editor]:h-full [&_.cm-editor]:outline-none", !cm && "hidden")}
|
|
361
|
+
style={{ height: cm ? height : undefined, minHeight: cm ? height : 0 }}
|
|
56
362
|
/>
|
|
57
363
|
</div>
|
|
58
364
|
)
|
|
59
365
|
}
|
|
60
|
-
|
|
61
|
-
export { defaultJqlFields, defaultJqlFunctions } from "./jql-language"
|
|
62
|
-
export type { JqlSuggestItem }
|
|
@@ -28,6 +28,13 @@ function Node({ label, value, depth }: { label?: string; value: unknown; depth:
|
|
|
28
28
|
)
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Collapsible JSON tree.
|
|
33
|
+
* @when API responses/config. Plain text with wrapping → `CodeBlock`.
|
|
34
|
+
* @example
|
|
35
|
+
* <JsonViewer value={data} />
|
|
36
|
+
* @family Discovery & inspect
|
|
37
|
+
*/
|
|
31
38
|
export function JsonViewer({ value }: { value: unknown }) {
|
|
32
39
|
return (
|
|
33
40
|
<div data-testid="json-viewer" className="rounded-xl border bg-card p-3">
|
|
@@ -17,6 +17,13 @@ import { StatusBadge } from "@monkey-mini-app/ui/blocks/status-badge"
|
|
|
17
17
|
import type { KanbanCard } from "@monkey-mini-app/ui/products/kanban"
|
|
18
18
|
import { useLabels } from "@monkey-mini-app/ui/i18n/context"
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Slide-over detail panel for a `Kanban` card.
|
|
22
|
+
* @when Board + click-to-edit issue. Pair with `Kanban`'s selected card.
|
|
23
|
+
* @example
|
|
24
|
+
* <KanbanIssuePanel card={card} open={open} onOpenChange={setOpen} />
|
|
25
|
+
* @family Surface
|
|
26
|
+
*/
|
|
20
27
|
export function KanbanIssuePanel({
|
|
21
28
|
card,
|
|
22
29
|
columnTitle,
|