@burdenoff/planmagnet-fe-sdk 2026.706.7 → 2026.708.1

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/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import { onError } from '@apollo/client/link/error';
6
6
  import { CombinedGraphQLErrors } from '@apollo/client/errors';
7
7
  import { jsx, jsxs } from 'react/jsx-runtime';
8
8
  import { gql } from '@apollo/client';
9
- import { CircleX, CircleCheckBig, CircleDotDashed, CircleDot, Circle, CircleDashed, User, Link2, CalendarDays, Trash2, ChevronUp, ChevronDown, ChevronLeft, ChevronRight, Flag, Plus, CalendarRange, List, LayoutGrid, Search, Pencil, SmilePlus } from 'lucide-react';
9
+ import { CircleX, CircleCheckBig, CircleDotDashed, CircleDot, Circle, CircleDashed, User, Link2, CalendarDays, Trash2, ChevronUp, ChevronDown, ChevronLeft, ChevronRight, Flag, Plus, CalendarRange, List, LayoutGrid, Search, Bookmark, Check, Pencil, SmilePlus } from 'lucide-react';
10
10
  import { clsx } from 'clsx';
11
11
  import { twMerge } from 'tailwind-merge';
12
12
  import { startOfWeek, format, addDays, addWeeks, endOfWeek, eachDayOfInterval, addMonths, startOfMonth, endOfMonth, isToday, isSameMonth, startOfDay, parseISO, isValid } from 'date-fns';
@@ -282,6 +282,12 @@ var WORK_ITEM_SUMMARY_FIELDS = gql`
282
282
  storyPoints
283
283
  dueDate
284
284
  labels
285
+ # Board reorder ordering. sortOrder maps a Prisma \`position\` column and is
286
+ # unsafe to SELECT BACK FROM A MUTATION (serialization crash), but SAFE on
287
+ # this QUERY read path — a WorkItem.sortOrder field resolver maps position →
288
+ # sortOrder. The board sorts cards by this ascending so a drag-to-reorder
289
+ # persists across refetch (the list query itself orders by createdAt).
290
+ sortOrder
285
291
  }
286
292
  `;
287
293
  var WORK_ITEM_FIELDS = gql`
@@ -406,10 +412,11 @@ var UPDATE_WORK_ITEM = gql`
406
412
  var MOVE_WORK_ITEM = gql`
407
413
  mutation MoveWorkItem($id: ID!, $input: PlanmagnetMoveWorkItemInput!) {
408
414
  planmagnetMoveWorkItem(id: $id, input: $input) {
409
- ...WorkItemFields
415
+ id
416
+ status
417
+ sprintId
410
418
  }
411
419
  }
412
- ${WORK_ITEM_FIELDS}
413
420
  `;
414
421
  var WORK_ITEM_LINK_FIELDS = gql`
415
422
  fragment WorkItemLinkFields on WorkItemLink {
@@ -808,6 +815,22 @@ var DEFAULT_TASK_LABELS = {
808
815
  allTypes: "All types",
809
816
  newTask: "New task",
810
817
  myTasks: "My tasks",
818
+ savedViews: "Views",
819
+ saveView: "Save current view",
820
+ viewName: "View name",
821
+ saveViewAction: "Save",
822
+ deleteView: "Delete",
823
+ noSavedViews: "No saved views",
824
+ defaultViewName: "Default",
825
+ keyboardShortcuts: "Keyboard shortcuts",
826
+ shortcutNewTask: "New task",
827
+ shortcutSearch: "Focus search",
828
+ shortcutBoardView: "Board view",
829
+ shortcutListView: "List view",
830
+ shortcutTimelineView: "Timeline view",
831
+ shortcutMyTasks: "Toggle My tasks",
832
+ shortcutHelp: "Toggle this help",
833
+ shortcutClose: "Close overlay",
811
834
  noTasks: "No tasks",
812
835
  noTasksFound: "No tasks found.",
813
836
  dropHere: "Drop here",
@@ -896,6 +919,7 @@ var DEFAULT_TASK_LABELS = {
896
919
  projectRequired: "A projectId is required to create a task.",
897
920
  errorLoading: "Error loading tasks",
898
921
  moveFailed: "Couldn't move the task. Please try again.",
922
+ reorderFailed: "Couldn't reorder the task. Please try again.",
899
923
  failedToCreate: "Failed to create task",
900
924
  failedToSave: "Failed to save",
901
925
  failedToDelete: "Failed to delete",
@@ -1094,6 +1118,7 @@ function TaskCard({
1094
1118
  draggable,
1095
1119
  onDragStart,
1096
1120
  onDragEnd,
1121
+ onDragOver,
1097
1122
  dragging,
1098
1123
  resolveAssignee,
1099
1124
  links,
@@ -1128,6 +1153,7 @@ function TaskCard({
1128
1153
  draggable,
1129
1154
  onDragStart: draggable ? (e) => onDragStart?.(task, e) : void 0,
1130
1155
  onDragEnd: draggable ? (e) => onDragEnd?.(task, e) : void 0,
1156
+ onDragOver: draggable ? onDragOver : void 0,
1131
1157
  onClick: onClick ? () => onClick(task) : void 0,
1132
1158
  onKeyDown: handleKey,
1133
1159
  className: cn(
@@ -1223,6 +1249,13 @@ function useWorkItemLinks(workspaceId, sourceProduct, limit = 500) {
1223
1249
  }, [data]);
1224
1250
  return { linksByItem, loading };
1225
1251
  }
1252
+ function bySortOrder(tasks) {
1253
+ return tasks.map((task, index) => ({ task, index })).sort((a, b) => {
1254
+ const ao = a.task.sortOrder ?? Number.MAX_SAFE_INTEGER;
1255
+ const bo = b.task.sortOrder ?? Number.MAX_SAFE_INTEGER;
1256
+ return ao === bo ? a.index - b.index : ao - bo;
1257
+ }).map((entry) => entry.task);
1258
+ }
1226
1259
  function TaskBoardCore({
1227
1260
  projectId,
1228
1261
  columns = DEFAULT_BOARD_COLUMNS,
@@ -1249,9 +1282,11 @@ function TaskBoardCore({
1249
1282
  const { linksByItem } = useWorkItemLinks(workspaceId, linkSourceProduct);
1250
1283
  const [draggingId, setDraggingId] = useState(null);
1251
1284
  const [dragOver, setDragOver] = useState(null);
1285
+ const [dropIndex, setDropIndex] = useState(null);
1252
1286
  const [moveError, setMoveError] = useState(null);
1253
1287
  const [pendingMove, setPendingMove] = useState(null);
1254
1288
  const [updateItem] = useMutation(UPDATE_WORK_ITEM);
1289
+ const [moveItem] = useMutation(MOVE_WORK_ITEM);
1255
1290
  const { data, loading, error } = useQuery(
1256
1291
  LIST_WORK_ITEMS,
1257
1292
  {
@@ -1280,6 +1315,44 @@ function TaskBoardCore({
1280
1315
  },
1281
1316
  [client, updateItem, L]
1282
1317
  );
1318
+ const reorderTask = useCallback(
1319
+ (columnTasks, fromIndex, toIndex) => {
1320
+ if (fromIndex === toIndex || fromIndex < 0) return;
1321
+ const ordered = columnTasks.slice();
1322
+ const [moved] = ordered.splice(fromIndex, 1);
1323
+ if (!moved) return;
1324
+ const insertAt = Math.max(0, Math.min(toIndex, ordered.length));
1325
+ ordered.splice(insertAt, 0, moved);
1326
+ const changed = [];
1327
+ ordered.forEach((task, index) => {
1328
+ const prev = task.sortOrder ?? null;
1329
+ if (prev !== index) {
1330
+ changed.push({ id: task.id, from: prev, to: index });
1331
+ }
1332
+ });
1333
+ if (changed.length === 0) return;
1334
+ setMoveError(null);
1335
+ const setOrder = (id, value) => {
1336
+ const cacheId = client.cache.identify({ __typename: "WorkItem", id });
1337
+ if (cacheId) {
1338
+ client.cache.modify({
1339
+ id: cacheId,
1340
+ fields: { sortOrder: () => value }
1341
+ });
1342
+ }
1343
+ };
1344
+ for (const c of changed) setOrder(c.id, c.to);
1345
+ Promise.all(
1346
+ changed.map(
1347
+ (c) => moveItem({ variables: { id: c.id, input: { sortOrder: c.to } } })
1348
+ )
1349
+ ).catch(() => {
1350
+ for (const c of changed) setOrder(c.id, c.from);
1351
+ setMoveError(L.reorderFailed);
1352
+ });
1353
+ },
1354
+ [client, moveItem, L]
1355
+ );
1283
1356
  if (!workspaceId) {
1284
1357
  return /* @__PURE__ */ jsx("div", { className: cn("p-6 text-sm text-destructive", className), children: "TaskBoard: no workspaceId provided (prop or PlanMagnetProvider)." });
1285
1358
  }
@@ -1306,18 +1379,41 @@ function TaskBoardCore({
1306
1379
  return true;
1307
1380
  });
1308
1381
  const canDrag = !readonly;
1382
+ const columnTasksFor = (status) => bySortOrder(filtered.filter((t) => t.status === status));
1309
1383
  const onColumnDragOver = (status) => (e) => {
1310
1384
  if (!canDrag || !draggingId) return;
1311
1385
  e.preventDefault();
1312
1386
  if (dragOver !== status) setDragOver(status);
1387
+ if (dropIndex === null) setDropIndex(columnTasksFor(status).length);
1388
+ };
1389
+ const onCardDragOver = (status, index) => (e) => {
1390
+ if (!canDrag || !draggingId) return;
1391
+ e.preventDefault();
1392
+ e.stopPropagation();
1393
+ if (dragOver !== status) setDragOver(status);
1394
+ const rect = e.currentTarget.getBoundingClientRect();
1395
+ const after = e.clientY - rect.top > rect.height / 2;
1396
+ const next = after ? index + 1 : index;
1397
+ if (dropIndex !== next) setDropIndex(next);
1398
+ };
1399
+ const clearDrag = () => {
1400
+ setDragOver(null);
1401
+ setDraggingId(null);
1402
+ setDropIndex(null);
1313
1403
  };
1314
1404
  const onColumnDrop = (status) => (e) => {
1315
1405
  if (!canDrag) return;
1316
1406
  e.preventDefault();
1317
1407
  const task = tasks.find((t) => t.id === draggingId);
1318
- setDragOver(null);
1319
- setDraggingId(null);
1320
- if (!task || task.status === status) return;
1408
+ const targetIndex = dropIndex;
1409
+ clearDrag();
1410
+ if (!task) return;
1411
+ if (task.status === status) {
1412
+ const colTasks = columnTasksFor(status);
1413
+ const fromIndex = colTasks.findIndex((t) => t.id === task.id);
1414
+ if (targetIndex !== null) reorderTask(colTasks, fromIndex, targetIndex);
1415
+ return;
1416
+ }
1321
1417
  if (confirmMove) setPendingMove({ task, toStatus: status });
1322
1418
  else moveTask(task, status);
1323
1419
  };
@@ -1332,8 +1428,9 @@ function TaskBoardCore({
1332
1428
  ] }),
1333
1429
  moveError && /* @__PURE__ */ jsx("div", { className: "rounded-md border border-destructive/40 bg-destructive/10 px-3 py-1.5 text-xs text-destructive", children: moveError }),
1334
1430
  /* @__PURE__ */ jsx("div", { className: "flex gap-3 overflow-x-auto pb-2", children: columns.map((col) => {
1335
- const colTasks = filtered.filter((t) => t.status === col.status);
1431
+ const colTasks = columnTasksFor(col.status);
1336
1432
  const isOver = dragOver === col.status;
1433
+ const showDropLine = canDrag && isOver && dropIndex !== null && colTasks.some((t) => t.id === draggingId);
1337
1434
  return /* @__PURE__ */ jsxs(
1338
1435
  "div",
1339
1436
  {
@@ -1352,29 +1449,45 @@ function TaskBoardCore({
1352
1449
  ] }),
1353
1450
  /* @__PURE__ */ jsx("span", { className: "rounded-full bg-muted px-1.5 text-xs font-medium text-muted-foreground", children: colTasks.length })
1354
1451
  ] }),
1355
- /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2 px-2 pb-2", children: colTasks.length === 0 ? /* @__PURE__ */ jsx("div", { className: "rounded-md border border-dashed border-border/70 px-3 py-6 text-center text-xs text-muted-foreground", children: isOver ? L.dropHere : L.noTasks }) : colTasks.map((task) => /* @__PURE__ */ jsx(
1356
- TaskCard,
1357
- {
1358
- task,
1359
- hidden,
1360
- onClick: onTaskClick,
1361
- draggable: canDrag,
1362
- dragging: draggingId === task.id,
1363
- resolveAssignee,
1364
- links: linksByItem.get(task.id),
1365
- renderLinkIcon,
1366
- onDragStart: (t, e) => {
1367
- setDraggingId(t.id);
1368
- e.dataTransfer.effectAllowed = "move";
1369
- e.dataTransfer.setData("text/plain", t.id);
1370
- },
1371
- onDragEnd: () => {
1372
- setDraggingId(null);
1373
- setDragOver(null);
1452
+ /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-2 px-2 pb-2", children: colTasks.length === 0 ? /* @__PURE__ */ jsx("div", { className: "rounded-md border border-dashed border-border/70 px-3 py-6 text-center text-xs text-muted-foreground", children: isOver ? L.dropHere : L.noTasks }) : colTasks.map((task, index) => /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
1453
+ showDropLine && dropIndex === index && /* @__PURE__ */ jsx(
1454
+ "div",
1455
+ {
1456
+ "aria-hidden": "true",
1457
+ "data-testid": "reorder-drop-line",
1458
+ className: "h-0.5 rounded-full bg-primary"
1374
1459
  }
1375
- },
1376
- task.id
1377
- )) })
1460
+ ),
1461
+ /* @__PURE__ */ jsx(
1462
+ TaskCard,
1463
+ {
1464
+ task,
1465
+ hidden,
1466
+ onClick: onTaskClick,
1467
+ draggable: canDrag,
1468
+ dragging: draggingId === task.id,
1469
+ resolveAssignee,
1470
+ links: linksByItem.get(task.id),
1471
+ renderLinkIcon,
1472
+ onDragOver: onCardDragOver(col.status, index),
1473
+ onDragStart: (t, e) => {
1474
+ setDraggingId(t.id);
1475
+ setDropIndex(null);
1476
+ e.dataTransfer.effectAllowed = "move";
1477
+ e.dataTransfer.setData("text/plain", t.id);
1478
+ },
1479
+ onDragEnd: clearDrag
1480
+ }
1481
+ ),
1482
+ showDropLine && index === colTasks.length - 1 && dropIndex >= colTasks.length && /* @__PURE__ */ jsx(
1483
+ "div",
1484
+ {
1485
+ "aria-hidden": "true",
1486
+ "data-testid": "reorder-drop-line",
1487
+ className: "h-0.5 rounded-full bg-primary"
1488
+ }
1489
+ )
1490
+ ] }, task.id)) })
1378
1491
  ]
1379
1492
  },
1380
1493
  col.status
@@ -2796,6 +2909,7 @@ function TaskToolbar({
2796
2909
  views = ["board", "list", "timeline"],
2797
2910
  search,
2798
2911
  onSearchChange,
2912
+ searchInputRef,
2799
2913
  priority,
2800
2914
  onPriorityChange,
2801
2915
  type,
@@ -2804,6 +2918,7 @@ function TaskToolbar({
2804
2918
  canCreate,
2805
2919
  mine,
2806
2920
  onMineChange,
2921
+ leadingActions,
2807
2922
  className
2808
2923
  }) {
2809
2924
  const L = useTaskLabels();
@@ -2852,6 +2967,7 @@ function TaskToolbar({
2852
2967
  }
2853
2968
  ),
2854
2969
  /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [
2970
+ leadingActions,
2855
2971
  /* @__PURE__ */ jsxs("div", { className: "relative", children: [
2856
2972
  /* @__PURE__ */ jsx(
2857
2973
  Search,
@@ -2863,6 +2979,7 @@ function TaskToolbar({
2863
2979
  /* @__PURE__ */ jsx(
2864
2980
  "input",
2865
2981
  {
2982
+ ref: searchInputRef,
2866
2983
  type: "search",
2867
2984
  value: search,
2868
2985
  onChange: (e) => onSearchChange(e.target.value),
@@ -3921,6 +4038,300 @@ function TaskDetail({
3921
4038
  }
3922
4039
  );
3923
4040
  }
4041
+ function SavedViewsMenu({
4042
+ views,
4043
+ activeViewId,
4044
+ onSaveView,
4045
+ onApplyView,
4046
+ onDeleteView,
4047
+ className
4048
+ }) {
4049
+ const L = useTaskLabels();
4050
+ const [open, setOpen] = useState(false);
4051
+ const [naming, setNaming] = useState(false);
4052
+ const [name, setName] = useState("");
4053
+ const rootRef = useRef(null);
4054
+ useEffect(() => {
4055
+ if (!open) return;
4056
+ const onDown = (e) => {
4057
+ if (rootRef.current && !rootRef.current.contains(e.target)) {
4058
+ setOpen(false);
4059
+ setNaming(false);
4060
+ }
4061
+ };
4062
+ const onKey = (e) => {
4063
+ if (e.key === "Escape") {
4064
+ setOpen(false);
4065
+ setNaming(false);
4066
+ }
4067
+ };
4068
+ document.addEventListener("mousedown", onDown);
4069
+ document.addEventListener("keydown", onKey);
4070
+ return () => {
4071
+ document.removeEventListener("mousedown", onDown);
4072
+ document.removeEventListener("keydown", onKey);
4073
+ };
4074
+ }, [open]);
4075
+ const activeName = views.find((v) => v.id === activeViewId)?.name ?? L.defaultViewName;
4076
+ const commitSave = () => {
4077
+ const trimmed = name.trim();
4078
+ if (!trimmed) return;
4079
+ onSaveView(trimmed);
4080
+ setName("");
4081
+ setNaming(false);
4082
+ setOpen(false);
4083
+ };
4084
+ return /* @__PURE__ */ jsxs("div", { ref: rootRef, className: cn("relative", className), children: [
4085
+ /* @__PURE__ */ jsxs(
4086
+ "button",
4087
+ {
4088
+ type: "button",
4089
+ onClick: () => setOpen((o) => !o),
4090
+ "aria-haspopup": "menu",
4091
+ "aria-expanded": open,
4092
+ "aria-label": L.savedViews,
4093
+ className: cn(
4094
+ "flex h-8 max-w-[10rem] items-center gap-1.5 rounded-md border px-2.5 text-xs font-medium transition-colors",
4095
+ activeViewId ? "border-primary bg-primary/10 text-primary" : "border-border bg-background text-muted-foreground hover:bg-muted/50 hover:text-foreground"
4096
+ ),
4097
+ children: [
4098
+ /* @__PURE__ */ jsx(Bookmark, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }),
4099
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: activeName })
4100
+ ]
4101
+ }
4102
+ ),
4103
+ open && /* @__PURE__ */ jsxs(
4104
+ "div",
4105
+ {
4106
+ role: "menu",
4107
+ className: "absolute right-0 top-full z-30 mt-1 w-56 rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md",
4108
+ children: [
4109
+ /* @__PURE__ */ jsx("div", { className: "px-2 py-1 text-[0.65rem] font-semibold uppercase tracking-wide text-muted-foreground", children: L.savedViews }),
4110
+ views.length === 0 ? /* @__PURE__ */ jsx("div", { className: "px-2 py-1.5 text-xs text-muted-foreground", children: L.noSavedViews }) : /* @__PURE__ */ jsx("ul", { className: "max-h-56 overflow-auto", children: views.map((v) => {
4111
+ const active = v.id === activeViewId;
4112
+ return /* @__PURE__ */ jsxs("li", { className: "flex items-center gap-1", children: [
4113
+ /* @__PURE__ */ jsxs(
4114
+ "button",
4115
+ {
4116
+ type: "button",
4117
+ role: "menuitemradio",
4118
+ "aria-checked": active,
4119
+ onClick: () => {
4120
+ onApplyView(v.id);
4121
+ setOpen(false);
4122
+ },
4123
+ className: cn(
4124
+ "flex min-w-0 flex-1 items-center gap-1.5 rounded px-2 py-1.5 text-left text-xs transition-colors hover:bg-muted/60",
4125
+ active ? "text-foreground" : "text-muted-foreground"
4126
+ ),
4127
+ children: [
4128
+ /* @__PURE__ */ jsx(
4129
+ Check,
4130
+ {
4131
+ className: cn(
4132
+ "h-3.5 w-3.5 shrink-0",
4133
+ active ? "opacity-100" : "opacity-0"
4134
+ ),
4135
+ "aria-hidden": "true"
4136
+ }
4137
+ ),
4138
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: v.name })
4139
+ ]
4140
+ }
4141
+ ),
4142
+ /* @__PURE__ */ jsx(
4143
+ "button",
4144
+ {
4145
+ type: "button",
4146
+ onClick: () => onDeleteView(v.id),
4147
+ "aria-label": `${L.deleteView}: ${v.name}`,
4148
+ className: "shrink-0 rounded p-1 text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive",
4149
+ children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5", "aria-hidden": "true" })
4150
+ }
4151
+ )
4152
+ ] }, v.id);
4153
+ }) }),
4154
+ /* @__PURE__ */ jsx("div", { className: "my-1 h-px bg-border" }),
4155
+ naming ? /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 p-1", children: [
4156
+ /* @__PURE__ */ jsx(
4157
+ "input",
4158
+ {
4159
+ type: "text",
4160
+ autoFocus: true,
4161
+ value: name,
4162
+ onChange: (e) => setName(e.target.value),
4163
+ onKeyDown: (e) => {
4164
+ if (e.key === "Enter") commitSave();
4165
+ },
4166
+ placeholder: L.viewName,
4167
+ "aria-label": L.viewName,
4168
+ className: "h-7 min-w-0 flex-1 rounded border border-border bg-background px-2 text-xs text-foreground placeholder:text-muted-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-ring"
4169
+ }
4170
+ ),
4171
+ /* @__PURE__ */ jsx(
4172
+ "button",
4173
+ {
4174
+ type: "button",
4175
+ onClick: commitSave,
4176
+ disabled: !name.trim(),
4177
+ className: "h-7 shrink-0 rounded bg-primary px-2 text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-50",
4178
+ children: L.saveViewAction
4179
+ }
4180
+ )
4181
+ ] }) : /* @__PURE__ */ jsxs(
4182
+ "button",
4183
+ {
4184
+ type: "button",
4185
+ role: "menuitem",
4186
+ onClick: () => setNaming(true),
4187
+ className: "flex w-full items-center gap-1.5 rounded px-2 py-1.5 text-left text-xs font-medium text-foreground transition-colors hover:bg-muted/60",
4188
+ children: [
4189
+ /* @__PURE__ */ jsx(Plus, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
4190
+ L.saveView
4191
+ ]
4192
+ }
4193
+ )
4194
+ ]
4195
+ }
4196
+ )
4197
+ ] });
4198
+ }
4199
+ var STORAGE_PREFIX = "planmagnet:views:";
4200
+ var DEBOUNCE_MS = 400;
4201
+ function makeStorageKey(workspaceId, suffix) {
4202
+ return suffix ? `${STORAGE_PREFIX}${workspaceId}:${suffix}` : `${STORAGE_PREFIX}${workspaceId}`;
4203
+ }
4204
+ function safeStorage() {
4205
+ try {
4206
+ if (typeof window === "undefined") return null;
4207
+ const ls = window.localStorage;
4208
+ const probe = "__pm_probe__";
4209
+ ls.setItem(probe, probe);
4210
+ ls.removeItem(probe);
4211
+ return ls;
4212
+ } catch {
4213
+ return null;
4214
+ }
4215
+ }
4216
+ function readPayload(key) {
4217
+ const ls = safeStorage();
4218
+ if (!ls) return null;
4219
+ try {
4220
+ const raw = ls.getItem(key);
4221
+ if (!raw) return null;
4222
+ const parsed = JSON.parse(raw);
4223
+ if (!parsed || parsed.v !== 1) return null;
4224
+ return parsed;
4225
+ } catch {
4226
+ return null;
4227
+ }
4228
+ }
4229
+ function writePayload(key, payload) {
4230
+ const ls = safeStorage();
4231
+ if (!ls) return;
4232
+ try {
4233
+ ls.setItem(key, JSON.stringify(payload));
4234
+ } catch {
4235
+ }
4236
+ }
4237
+ function makeId() {
4238
+ const rand = Math.random().toString(36).slice(2, 10);
4239
+ return `v_${Date.now().toString(36)}_${rand}`;
4240
+ }
4241
+ function useSavedViews({
4242
+ storageKey,
4243
+ enabled,
4244
+ current
4245
+ }) {
4246
+ const initial = useMemo(
4247
+ () => enabled ? readPayload(storageKey) : null,
4248
+ [enabled, storageKey]
4249
+ );
4250
+ const [views, setViews] = useState(initial?.views ?? []);
4251
+ const [activeViewId, setActiveViewId] = useState(
4252
+ initial?.activeViewId ?? null
4253
+ );
4254
+ const restored = enabled ? initial?.current ?? null : null;
4255
+ const viewsRef = useRef(views);
4256
+ viewsRef.current = views;
4257
+ const activeViewIdRef = useRef(activeViewId);
4258
+ activeViewIdRef.current = activeViewId;
4259
+ useEffect(() => {
4260
+ if (!enabled) return;
4261
+ const handle = setTimeout(() => {
4262
+ writePayload(storageKey, {
4263
+ v: 1,
4264
+ current,
4265
+ views: viewsRef.current,
4266
+ activeViewId: activeViewIdRef.current
4267
+ });
4268
+ }, DEBOUNCE_MS);
4269
+ return () => clearTimeout(handle);
4270
+ }, [enabled, storageKey, current]);
4271
+ const flush = useCallback(
4272
+ (nextViews, nextActive) => {
4273
+ if (!enabled) return;
4274
+ writePayload(storageKey, {
4275
+ v: 1,
4276
+ current,
4277
+ views: nextViews,
4278
+ activeViewId: nextActive
4279
+ });
4280
+ },
4281
+ [enabled, storageKey, current]
4282
+ );
4283
+ const saveView = useCallback(
4284
+ (name) => {
4285
+ if (!enabled) return null;
4286
+ const trimmed = name.trim();
4287
+ if (!trimmed) return null;
4288
+ const id = makeId();
4289
+ const next = [
4290
+ ...viewsRef.current,
4291
+ { id, name: trimmed, state: current }
4292
+ ];
4293
+ setViews(next);
4294
+ setActiveViewId(id);
4295
+ flush(next, id);
4296
+ return id;
4297
+ },
4298
+ [enabled, current, flush]
4299
+ );
4300
+ const applyView = useCallback(
4301
+ (id) => {
4302
+ const found = viewsRef.current.find((v) => v.id === id);
4303
+ if (!found) return null;
4304
+ setActiveViewId(id);
4305
+ flush(viewsRef.current, id);
4306
+ return found.state;
4307
+ },
4308
+ [flush]
4309
+ );
4310
+ const deleteView = useCallback(
4311
+ (id) => {
4312
+ const next = viewsRef.current.filter((v) => v.id !== id);
4313
+ const nextActive = activeViewIdRef.current === id ? null : activeViewIdRef.current;
4314
+ setViews(next);
4315
+ setActiveViewId(nextActive);
4316
+ flush(next, nextActive);
4317
+ },
4318
+ [flush]
4319
+ );
4320
+ const clearActiveView = useCallback(() => {
4321
+ if (activeViewIdRef.current == null) return;
4322
+ setActiveViewId(null);
4323
+ flush(viewsRef.current, null);
4324
+ }, [flush]);
4325
+ return {
4326
+ restored,
4327
+ views,
4328
+ activeViewId,
4329
+ saveView,
4330
+ applyView,
4331
+ deleteView,
4332
+ clearActiveView
4333
+ };
4334
+ }
3924
4335
  function TasksPageCore({
3925
4336
  workspaceId: workspaceIdProp,
3926
4337
  projectId,
@@ -3946,19 +4357,26 @@ function TasksPageCore({
3946
4357
  onTaskSaved,
3947
4358
  createSurface = "modal",
3948
4359
  detailSurface = "drawer",
4360
+ enableSavedViews,
4361
+ persistViewKey,
4362
+ enableShortcuts,
3949
4363
  className
3950
4364
  }) {
3951
4365
  const ctx = usePlanMagnetContextSafe();
3952
4366
  const workspaceId = workspaceIdProp ?? ctx?.workspaceId;
3953
4367
  const client = useApolloClient();
3954
4368
  const mergedLabels = mergeTaskLabels(labels);
4369
+ const savedViewsEnabled = Boolean(enableSavedViews || persistViewKey);
3955
4370
  const [view, setView] = useState(defaultView);
3956
4371
  const [search, setSearch] = useState("");
3957
4372
  const [priority, setPriority] = useState("ALL");
3958
4373
  const [type, setType] = useState("ALL");
3959
4374
  const [mine, setMine] = useState(false);
4375
+ const [savedListColumns, setSavedListColumns] = useState(null);
3960
4376
  const [creating, setCreating] = useState(false);
3961
4377
  const [selectedId, setSelectedId] = useState(null);
4378
+ const [showHelp, setShowHelp] = useState(false);
4379
+ const searchInputRef = useRef(null);
3962
4380
  const [createDefaults, setCreateDefaults] = useState(null);
3963
4381
  useEffect(() => {
3964
4382
  if (openItemId) setSelectedId(openItemId);
@@ -4010,6 +4428,169 @@ function TasksPageCore({
4010
4428
  setSelectedId(task.id);
4011
4429
  }, []);
4012
4430
  const handleSelect = useCallback((id) => setSelectedId(id), []);
4431
+ const persistEnabled = savedViewsEnabled && Boolean(workspaceId);
4432
+ const storageKey = useMemo(
4433
+ () => makeStorageKey(workspaceId ?? "", persistViewKey),
4434
+ [workspaceId, persistViewKey]
4435
+ );
4436
+ const currentViewState = useMemo(
4437
+ () => ({
4438
+ view,
4439
+ priority,
4440
+ type,
4441
+ search,
4442
+ mine,
4443
+ listColumns: savedListColumns
4444
+ }),
4445
+ [view, priority, type, search, mine, savedListColumns]
4446
+ );
4447
+ const applyViewState = useCallback((s) => {
4448
+ setView(s.view);
4449
+ setPriority(s.priority);
4450
+ setType(s.type);
4451
+ setSearch(s.search);
4452
+ setMine(s.mine);
4453
+ setSavedListColumns(s.listColumns);
4454
+ }, []);
4455
+ const {
4456
+ restored,
4457
+ views: savedViews,
4458
+ activeViewId,
4459
+ saveView,
4460
+ applyView,
4461
+ deleteView,
4462
+ clearActiveView
4463
+ } = useSavedViews({
4464
+ storageKey,
4465
+ enabled: persistEnabled,
4466
+ current: currentViewState
4467
+ });
4468
+ const [didRestore, setDidRestore] = useState(false);
4469
+ useEffect(() => {
4470
+ if (didRestore || !persistEnabled) return;
4471
+ if (restored) applyViewState(restored);
4472
+ setDidRestore(true);
4473
+ }, [persistEnabled]);
4474
+ const handleApplySavedView = useCallback(
4475
+ (id) => {
4476
+ const s = applyView(id);
4477
+ if (s) applyViewState(s);
4478
+ },
4479
+ [applyView, applyViewState]
4480
+ );
4481
+ const detachOnEdit = useCallback(() => {
4482
+ if (activeViewId != null) clearActiveView();
4483
+ }, [activeViewId, clearActiveView]);
4484
+ const handleViewChange = useCallback(
4485
+ (v) => {
4486
+ setView(v);
4487
+ detachOnEdit();
4488
+ },
4489
+ [detachOnEdit]
4490
+ );
4491
+ const handleSearchChange = useCallback(
4492
+ (s) => {
4493
+ setSearch(s);
4494
+ detachOnEdit();
4495
+ },
4496
+ [detachOnEdit]
4497
+ );
4498
+ const handlePriorityChange = useCallback(
4499
+ (p) => {
4500
+ setPriority(p);
4501
+ detachOnEdit();
4502
+ },
4503
+ [detachOnEdit]
4504
+ );
4505
+ const handleTypeChange = useCallback(
4506
+ (t) => {
4507
+ setType(t);
4508
+ detachOnEdit();
4509
+ },
4510
+ [detachOnEdit]
4511
+ );
4512
+ const handleMineChange = useCallback(
4513
+ (m) => {
4514
+ setMine(m);
4515
+ detachOnEdit();
4516
+ },
4517
+ [detachOnEdit]
4518
+ );
4519
+ const enabledViews = useMemo(
4520
+ () => views ?? ["board", "list", "timeline"],
4521
+ [views]
4522
+ );
4523
+ const shortcutsCanCreate = !readonly && Boolean(projectId);
4524
+ useEffect(() => {
4525
+ if (!enableShortcuts) return;
4526
+ const onKeyDown = (e) => {
4527
+ if (e.ctrlKey || e.metaKey || e.altKey) return;
4528
+ const target = e.target;
4529
+ if (target) {
4530
+ const tag = target.tagName;
4531
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || target.isContentEditable) {
4532
+ if (e.key !== "Escape") return;
4533
+ }
4534
+ }
4535
+ switch (e.key) {
4536
+ case "c":
4537
+ if (shortcutsCanCreate) {
4538
+ e.preventDefault();
4539
+ startCreate();
4540
+ }
4541
+ break;
4542
+ case "/":
4543
+ e.preventDefault();
4544
+ searchInputRef.current?.focus();
4545
+ break;
4546
+ case "b":
4547
+ if (enabledViews.includes("board")) {
4548
+ e.preventDefault();
4549
+ setView("board");
4550
+ }
4551
+ break;
4552
+ case "l":
4553
+ if (enabledViews.includes("list")) {
4554
+ e.preventDefault();
4555
+ setView("list");
4556
+ }
4557
+ break;
4558
+ case "g":
4559
+ if (enabledViews.includes("timeline")) {
4560
+ e.preventDefault();
4561
+ setView("timeline");
4562
+ }
4563
+ break;
4564
+ case "m":
4565
+ if (currentActorId) {
4566
+ e.preventDefault();
4567
+ setMine((prev) => !prev);
4568
+ }
4569
+ break;
4570
+ case "?":
4571
+ e.preventDefault();
4572
+ setShowHelp((v) => !v);
4573
+ break;
4574
+ case "Escape":
4575
+ if (showHelp) setShowHelp(false);
4576
+ if (creating) cancelCreate();
4577
+ if (selectedId != null) setSelectedId(null);
4578
+ break;
4579
+ }
4580
+ };
4581
+ document.addEventListener("keydown", onKeyDown);
4582
+ return () => document.removeEventListener("keydown", onKeyDown);
4583
+ }, [
4584
+ enableShortcuts,
4585
+ enabledViews,
4586
+ shortcutsCanCreate,
4587
+ currentActorId,
4588
+ creating,
4589
+ selectedId,
4590
+ showHelp,
4591
+ startCreate,
4592
+ cancelCreate
4593
+ ]);
4013
4594
  if (!workspaceId) {
4014
4595
  return /* @__PURE__ */ jsx("div", { className: cn("p-6 text-sm text-destructive", className), children: "TasksPage: no workspaceId provided (prop or PlanMagnetProvider)." });
4015
4596
  }
@@ -4050,32 +4631,37 @@ function TasksPageCore({
4050
4631
  const listPriority = priority === "ALL" ? void 0 : priority;
4051
4632
  const listType = type === "ALL" ? void 0 : type;
4052
4633
  const mineAssignee = mine && currentActorId ? currentActorId : void 0;
4053
- const listColumns = linkSourceProduct ? [
4054
- "title",
4055
- "status",
4056
- "priority",
4057
- "assignee",
4058
- "dueDate",
4059
- "linkedRecord"
4060
- ] : void 0;
4634
+ const derivedListColumns = linkSourceProduct ? ["title", "status", "priority", "assignee", "dueDate", "linkedRecord"] : void 0;
4635
+ const listColumns = savedListColumns ?? derivedListColumns;
4061
4636
  return /* @__PURE__ */ jsx(TaskLabelsProvider, { value: mergedLabels, children: /* @__PURE__ */ jsxs("div", { className: cn("space-y-4 p-6", className), children: [
4062
4637
  header && /* @__PURE__ */ jsx("div", { className: "min-w-0", children: typeof header === "string" ? /* @__PURE__ */ jsx("h1", { className: "truncate text-2xl font-semibold text-foreground", children: header }) : header }),
4063
4638
  /* @__PURE__ */ jsx(
4064
4639
  TaskToolbar,
4065
4640
  {
4066
4641
  view,
4067
- onViewChange: setView,
4642
+ onViewChange: savedViewsEnabled ? handleViewChange : setView,
4068
4643
  views,
4069
4644
  search,
4070
- onSearchChange: setSearch,
4645
+ onSearchChange: savedViewsEnabled ? handleSearchChange : setSearch,
4646
+ searchInputRef: enableShortcuts ? searchInputRef : void 0,
4071
4647
  priority,
4072
- onPriorityChange: setPriority,
4648
+ onPriorityChange: savedViewsEnabled ? handlePriorityChange : setPriority,
4073
4649
  type,
4074
- onTypeChange: setType,
4650
+ onTypeChange: savedViewsEnabled ? handleTypeChange : setType,
4075
4651
  onNewTask: startCreate,
4076
4652
  canCreate,
4077
4653
  mine,
4078
- onMineChange: currentActorId ? setMine : void 0
4654
+ onMineChange: currentActorId ? savedViewsEnabled ? handleMineChange : setMine : void 0,
4655
+ leadingActions: savedViewsEnabled ? /* @__PURE__ */ jsx(
4656
+ SavedViewsMenu,
4657
+ {
4658
+ views: savedViews,
4659
+ activeViewId,
4660
+ onSaveView: saveView,
4661
+ onApplyView: handleApplySavedView,
4662
+ onDeleteView: deleteView
4663
+ }
4664
+ ) : void 0
4079
4665
  }
4080
4666
  ),
4081
4667
  showInlineCreate && /* @__PURE__ */ jsx("div", { children: createForm }),
@@ -4153,9 +4739,60 @@ function TasksPageCore({
4153
4739
  ariaLabel: "Task detail",
4154
4740
  children: /* @__PURE__ */ jsx("div", { className: "p-2 pt-8", children: detailView })
4155
4741
  }
4742
+ ),
4743
+ enableShortcuts && /* @__PURE__ */ jsx(
4744
+ Modal,
4745
+ {
4746
+ open: showHelp,
4747
+ onClose: () => setShowHelp(false),
4748
+ ariaLabel: mergedLabels.keyboardShortcuts,
4749
+ className: "max-w-sm",
4750
+ children: /* @__PURE__ */ jsx(
4751
+ ShortcutsHelp,
4752
+ {
4753
+ labels: mergedLabels,
4754
+ enabledViews,
4755
+ canCreate,
4756
+ showMine: Boolean(currentActorId)
4757
+ }
4758
+ )
4759
+ }
4156
4760
  )
4157
4761
  ] }) });
4158
4762
  }
4763
+ function ShortcutRow({ keys, label }) {
4764
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-4 py-1.5", children: [
4765
+ /* @__PURE__ */ jsx("span", { className: "text-sm text-foreground", children: label }),
4766
+ /* @__PURE__ */ jsx("span", { className: "flex items-center gap-1", children: keys.map((k) => /* @__PURE__ */ jsx(
4767
+ "kbd",
4768
+ {
4769
+ className: "inline-flex min-w-[1.5rem] items-center justify-center rounded border border-border bg-muted px-1.5 py-0.5 text-xs font-medium text-muted-foreground",
4770
+ children: k
4771
+ },
4772
+ k
4773
+ )) })
4774
+ ] });
4775
+ }
4776
+ function ShortcutsHelp({
4777
+ labels: L,
4778
+ enabledViews,
4779
+ canCreate,
4780
+ showMine
4781
+ }) {
4782
+ return /* @__PURE__ */ jsxs("div", { className: "p-6 pt-8", children: [
4783
+ /* @__PURE__ */ jsx("h2", { className: "mb-3 text-base font-semibold text-foreground", children: L.keyboardShortcuts }),
4784
+ /* @__PURE__ */ jsxs("div", { className: "divide-y divide-border", children: [
4785
+ canCreate && /* @__PURE__ */ jsx(ShortcutRow, { keys: ["c"], label: L.shortcutNewTask }),
4786
+ /* @__PURE__ */ jsx(ShortcutRow, { keys: ["/"], label: L.shortcutSearch }),
4787
+ enabledViews.includes("board") && /* @__PURE__ */ jsx(ShortcutRow, { keys: ["b"], label: L.shortcutBoardView }),
4788
+ enabledViews.includes("list") && /* @__PURE__ */ jsx(ShortcutRow, { keys: ["l"], label: L.shortcutListView }),
4789
+ enabledViews.includes("timeline") && /* @__PURE__ */ jsx(ShortcutRow, { keys: ["g"], label: L.shortcutTimelineView }),
4790
+ showMine && /* @__PURE__ */ jsx(ShortcutRow, { keys: ["m"], label: L.shortcutMyTasks }),
4791
+ /* @__PURE__ */ jsx(ShortcutRow, { keys: ["?"], label: L.shortcutHelp }),
4792
+ /* @__PURE__ */ jsx(ShortcutRow, { keys: ["Esc"], label: L.shortcutClose })
4793
+ ] })
4794
+ ] });
4795
+ }
4159
4796
  function TasksPage({
4160
4797
  apiGatewayUrl,
4161
4798
  authToken,
@@ -4185,6 +4822,6 @@ function TasksPage({
4185
4822
  );
4186
4823
  }
4187
4824
 
4188
- export { Avatar, BULK_UPDATE_WORK_ITEMS, Badge, CREATE_WORK_ITEM, CREATE_WORK_ITEM_LINK, CreateTask, CreateTaskCore, DEFAULT_BOARD_COLUMNS, DEFAULT_TASK_LABELS, DELETE_WORK_ITEM, DELETE_WORK_ITEM_LINK, Drawer, GET_PROJECTS, GET_WORK_ITEM, LIST_MILESTONES, LIST_WORK_ITEMS, LIST_WORK_ITEM_LINKS, LinkChip, MILESTONE_FIELDS, MOVE_WORK_ITEM, Modal, PRIORITY_META, PROJECT_SUMMARY_FIELDS, PlanMagnetProvider, PriorityIndicator, STATUS_META, StatusIcon, StatusPill, TASK_PRIORITIES, TASK_STATUSES, TASK_TYPES, TaskBoard, TaskBoardCore, TaskCard, TaskCommentsCore, TaskDetail, TaskDetailCore, TaskLabelsProvider, TaskList, TaskListCore, TaskTimelineCore, TaskToolbar, TasksPage, TasksPageCore, TypeBadge, UPDATE_WORK_ITEM, WORK_ITEM_FIELDS, WORK_ITEM_LINK_FIELDS, WORK_ITEM_SUMMARY_FIELDS, badgeVariants, cn, config, createPlanMagnetClient, enumLabel, getGraphQLEndpoint, getPlanMagnetClient, getPlanMagnetClientSafe, getPlanMagnetConfig, isPlanMagnetClientReady, mergeTaskLabels, resetPlanMagnetClient, resetPlanMagnetConfig, setPlanMagnetClient, setPlanMagnetConfig, useMilestones, usePlanMagnetContext, usePlanMagnetContextSafe, useTaskLabels, useWorkItemLinks, validateTrustedOrigin, waitForPlanMagnetClient };
4825
+ export { Avatar, BULK_UPDATE_WORK_ITEMS, Badge, CREATE_WORK_ITEM, CREATE_WORK_ITEM_LINK, CreateTask, CreateTaskCore, DEFAULT_BOARD_COLUMNS, DEFAULT_TASK_LABELS, DELETE_WORK_ITEM, DELETE_WORK_ITEM_LINK, Drawer, GET_PROJECTS, GET_WORK_ITEM, LIST_MILESTONES, LIST_WORK_ITEMS, LIST_WORK_ITEM_LINKS, LinkChip, MILESTONE_FIELDS, MOVE_WORK_ITEM, Modal, PRIORITY_META, PROJECT_SUMMARY_FIELDS, PlanMagnetProvider, PriorityIndicator, STATUS_META, SavedViewsMenu, StatusIcon, StatusPill, TASK_PRIORITIES, TASK_STATUSES, TASK_TYPES, TaskBoard, TaskBoardCore, TaskCard, TaskCommentsCore, TaskDetail, TaskDetailCore, TaskLabelsProvider, TaskList, TaskListCore, TaskTimelineCore, TaskToolbar, TasksPage, TasksPageCore, TypeBadge, UPDATE_WORK_ITEM, WORK_ITEM_FIELDS, WORK_ITEM_LINK_FIELDS, WORK_ITEM_SUMMARY_FIELDS, badgeVariants, cn, config, createPlanMagnetClient, enumLabel, getGraphQLEndpoint, getPlanMagnetClient, getPlanMagnetClientSafe, getPlanMagnetConfig, isPlanMagnetClientReady, makeStorageKey, mergeTaskLabels, resetPlanMagnetClient, resetPlanMagnetConfig, setPlanMagnetClient, setPlanMagnetConfig, useMilestones, usePlanMagnetContext, usePlanMagnetContextSafe, useSavedViews, useTaskLabels, useWorkItemLinks, validateTrustedOrigin, waitForPlanMagnetClient };
4189
4826
  //# sourceMappingURL=index.js.map
4190
4827
  //# sourceMappingURL=index.js.map