@ondrej-svec/hog 1.7.0 → 1.7.2

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/cli.js CHANGED
@@ -1317,6 +1317,36 @@ async function triggerCompletionActionAsync(action, repoName, issueNumber) {
1317
1317
  break;
1318
1318
  }
1319
1319
  }
1320
+ function applyBulkOptimisticStatusUpdates(ids, optionId, repos, config2, mutateData, registerPendingMutation) {
1321
+ for (const id of ids) {
1322
+ const ctx = findIssueContext(repos, id, config2);
1323
+ if (!(ctx.issue && ctx.repoName)) continue;
1324
+ const { issue: ctxIssue, repoName: ctxRepo, statusOptions: ctxOpts } = ctx;
1325
+ mutateData((data) => optimisticSetStatus(data, ctxRepo, ctxIssue.number, ctxOpts, optionId));
1326
+ const ctxStatusName = ctxOpts.find((o) => o.id === optionId)?.name;
1327
+ if (ctxStatusName) {
1328
+ registerPendingMutation?.(ctxRepo, ctxIssue.number, { projectStatus: ctxStatusName });
1329
+ }
1330
+ }
1331
+ }
1332
+ function resolveOptionName(repos, ids, config2, optionId) {
1333
+ for (const id of ids) {
1334
+ const name = findIssueContext(repos, id, config2).statusOptions.find(
1335
+ (o) => o.id === optionId
1336
+ )?.name;
1337
+ if (name) return name;
1338
+ }
1339
+ return optionId;
1340
+ }
1341
+ function clearFailedMutations(failedIds, clearFn) {
1342
+ if (!clearFn) return;
1343
+ for (const failedId of failedIds) {
1344
+ const lastColon = failedId.lastIndexOf(":");
1345
+ const failedRepo = failedId.slice(3, lastColon);
1346
+ const failedIssueNumber = parseInt(failedId.slice(lastColon + 1), 10);
1347
+ clearFn(failedRepo, failedIssueNumber);
1348
+ }
1349
+ }
1320
1350
  function optimisticSetStatus(data, repoName, issueNumber, statusOptions, optionId) {
1321
1351
  const statusName = statusOptions.find((o) => o.id === optionId)?.name;
1322
1352
  if (!statusName) return data;
@@ -1341,16 +1371,22 @@ function useActions({
1341
1371
  refresh,
1342
1372
  mutateData,
1343
1373
  onOverlayDone,
1344
- pushEntry
1374
+ pushEntry,
1375
+ registerPendingMutation,
1376
+ clearPendingMutation
1345
1377
  }) {
1346
1378
  const configRef = useRef2(config2);
1347
1379
  const reposRef = useRef2(repos);
1348
1380
  const selectedIdRef = useRef2(selectedId);
1349
1381
  const pushEntryRef = useRef2(pushEntry);
1382
+ const registerPendingMutationRef = useRef2(registerPendingMutation);
1383
+ const clearPendingMutationRef = useRef2(clearPendingMutation);
1350
1384
  configRef.current = config2;
1351
1385
  reposRef.current = repos;
1352
1386
  selectedIdRef.current = selectedId;
1353
1387
  pushEntryRef.current = pushEntry;
1388
+ registerPendingMutationRef.current = registerPendingMutation;
1389
+ clearPendingMutationRef.current = clearPendingMutation;
1354
1390
  const handlePick = useCallback2(() => {
1355
1391
  const ctx = findIssueContext(reposRef.current, selectedIdRef.current, configRef.current);
1356
1392
  if (!(ctx.issue && ctx.repoConfig)) return;
@@ -1433,6 +1469,12 @@ function useActions({
1433
1469
  mutateData(
1434
1470
  (data) => optimisticSetStatus(data, repoName, issue.number, statusOptions, optionId)
1435
1471
  );
1472
+ const statusName = statusOptions.find((o) => o.id === optionId)?.name;
1473
+ if (statusName) {
1474
+ registerPendingMutationRef.current?.(repoName, issue.number, {
1475
+ projectStatus: statusName
1476
+ });
1477
+ }
1436
1478
  const t = toast.loading("Moving...");
1437
1479
  const projectConfig = {
1438
1480
  projectNumber: repoConfig.projectNumber,
@@ -1472,6 +1514,7 @@ function useActions({
1472
1514
  status: "error",
1473
1515
  ago: Date.now()
1474
1516
  });
1517
+ clearPendingMutationRef.current?.(repoName, issue.number);
1475
1518
  refresh();
1476
1519
  }).finally(() => {
1477
1520
  onOverlayDone();
@@ -1694,15 +1737,14 @@ ${dueLine}` : dueLine;
1694
1737
  );
1695
1738
  const handleBulkStatusChange = useCallback2(
1696
1739
  async (ids, optionId) => {
1697
- for (const id of ids) {
1698
- const ctx = findIssueContext(reposRef.current, id, configRef.current);
1699
- if (ctx.issue && ctx.repoName) {
1700
- const { issue: ctxIssue, repoName: ctxRepo, statusOptions: ctxOpts } = ctx;
1701
- mutateData(
1702
- (data) => optimisticSetStatus(data, ctxRepo, ctxIssue.number, ctxOpts, optionId)
1703
- );
1704
- }
1705
- }
1740
+ applyBulkOptimisticStatusUpdates(
1741
+ ids,
1742
+ optionId,
1743
+ reposRef.current,
1744
+ configRef.current,
1745
+ mutateData,
1746
+ registerPendingMutationRef.current
1747
+ );
1706
1748
  const t = toast.loading(`Moving ${ids.size} issue${ids.size > 1 ? "s" : ""}...`);
1707
1749
  const failed = [];
1708
1750
  for (const id of ids) {
@@ -1724,18 +1766,12 @@ ${dueLine}` : dueLine;
1724
1766
  }
1725
1767
  const total = ids.size;
1726
1768
  const ok = total - failed.length;
1727
- const optionName = (() => {
1728
- for (const id of ids) {
1729
- const ctx = findIssueContext(reposRef.current, id, configRef.current);
1730
- const name = ctx.statusOptions.find((o) => o.id === optionId)?.name;
1731
- if (name) return name;
1732
- }
1733
- return optionId;
1734
- })();
1769
+ const optionName = resolveOptionName(reposRef.current, ids, configRef.current, optionId);
1735
1770
  if (failed.length === 0) {
1736
1771
  t.resolve(`Moved ${total} issue${total > 1 ? "s" : ""} to ${optionName}`);
1737
1772
  } else {
1738
1773
  t.reject(`${ok} moved to ${optionName}, ${failed.length} failed`);
1774
+ clearFailedMutations(failed, clearPendingMutationRef.current);
1739
1775
  refresh();
1740
1776
  }
1741
1777
  return failed;
@@ -1770,6 +1806,24 @@ var init_use_actions = __esm({
1770
1806
  // src/board/hooks/use-data.ts
1771
1807
  import { Worker } from "worker_threads";
1772
1808
  import { useCallback as useCallback3, useEffect, useRef as useRef3, useState as useState2 } from "react";
1809
+ function applyPendingMutations(data, pending) {
1810
+ const now = Date.now();
1811
+ for (const [key, m] of pending) {
1812
+ if (m.expiresAt <= now) pending.delete(key);
1813
+ }
1814
+ if (pending.size === 0) return data;
1815
+ return {
1816
+ ...data,
1817
+ repos: data.repos.map((rd) => ({
1818
+ ...rd,
1819
+ issues: rd.issues.map((issue) => {
1820
+ const mutation = pending.get(`${rd.repo.name}:${issue.number}`);
1821
+ if (!mutation || mutation.expiresAt <= now) return issue;
1822
+ return mutation.projectStatus !== void 0 ? { ...issue, projectStatus: mutation.projectStatus } : issue;
1823
+ })
1824
+ }))
1825
+ };
1826
+ }
1773
1827
  function refreshAgeColor(lastRefresh) {
1774
1828
  if (!lastRefresh) return "gray";
1775
1829
  const age = Date.now() - lastRefresh.getTime();
@@ -1782,18 +1836,21 @@ function useData(config2, options, refreshIntervalMs) {
1782
1836
  const activeRequestRef = useRef3(null);
1783
1837
  const workerRef = useRef3(null);
1784
1838
  const intervalRef = useRef3(null);
1839
+ const pendingMutationsRef = useRef3(/* @__PURE__ */ new Map());
1785
1840
  const configRef = useRef3(config2);
1786
1841
  const optionsRef = useRef3(options);
1787
1842
  configRef.current = config2;
1788
1843
  optionsRef.current = options;
1789
- const refresh = useCallback3(() => {
1844
+ const refresh = useCallback3((silent = false) => {
1790
1845
  if (activeRequestRef.current) {
1791
1846
  activeRequestRef.current.canceled = true;
1792
1847
  }
1793
1848
  workerRef.current?.terminate();
1794
1849
  const token = { canceled: false };
1795
1850
  activeRequestRef.current = token;
1796
- setState((prev) => ({ ...prev, isRefreshing: true }));
1851
+ if (!silent) {
1852
+ setState((prev) => ({ ...prev, isRefreshing: true }));
1853
+ }
1797
1854
  const worker = new Worker(
1798
1855
  new URL(
1799
1856
  import.meta.url.endsWith(".ts") ? "../fetch-worker.ts" : "./fetch-worker.js",
@@ -1809,11 +1866,12 @@ function useData(config2, options, refreshIntervalMs) {
1809
1866
  return;
1810
1867
  }
1811
1868
  if (msg.type === "success" && msg.data) {
1812
- const data = msg.data;
1813
- data.fetchedAt = new Date(data.fetchedAt);
1814
- for (const ev of data.activity) {
1869
+ const raw = msg.data;
1870
+ raw.fetchedAt = new Date(raw.fetchedAt);
1871
+ for (const ev of raw.activity) {
1815
1872
  ev.timestamp = new Date(ev.timestamp);
1816
1873
  }
1874
+ const data = applyPendingMutations(raw, pendingMutationsRef.current);
1817
1875
  setState({
1818
1876
  status: "success",
1819
1877
  data,
@@ -1862,7 +1920,7 @@ function useData(config2, options, refreshIntervalMs) {
1862
1920
  if (refreshIntervalMs <= 0) return;
1863
1921
  intervalRef.current = setInterval(() => {
1864
1922
  if (!stateRef.current.autoRefreshPaused) {
1865
- refresh();
1923
+ refresh(true);
1866
1924
  }
1867
1925
  }, refreshIntervalMs);
1868
1926
  return () => {
@@ -1891,7 +1949,27 @@ function useData(config2, options, refreshIntervalMs) {
1891
1949
  const resumeAutoRefresh = useCallback3(() => {
1892
1950
  setState((prev) => ({ ...prev, autoRefreshPaused: false }));
1893
1951
  }, []);
1894
- return { ...state, refresh, mutateData, pauseAutoRefresh, resumeAutoRefresh };
1952
+ const registerPendingMutation = useCallback3(
1953
+ (repoName, issueNumber, fields, ttlMs = 9e4) => {
1954
+ pendingMutationsRef.current.set(`${repoName}:${issueNumber}`, {
1955
+ ...fields,
1956
+ expiresAt: Date.now() + ttlMs
1957
+ });
1958
+ },
1959
+ []
1960
+ );
1961
+ const clearPendingMutation = useCallback3((repoName, issueNumber) => {
1962
+ pendingMutationsRef.current.delete(`${repoName}:${issueNumber}`);
1963
+ }, []);
1964
+ return {
1965
+ ...state,
1966
+ refresh,
1967
+ mutateData,
1968
+ pauseAutoRefresh,
1969
+ resumeAutoRefresh,
1970
+ registerPendingMutation,
1971
+ clearPendingMutation
1972
+ };
1895
1973
  }
1896
1974
  var INITIAL_STATE, STALE_THRESHOLDS, MAX_REFRESH_FAILURES;
1897
1975
  var init_use_data = __esm({
@@ -2271,7 +2349,7 @@ function navReducer(state, action) {
2271
2349
  const collapsedSections = isFirstLoad ? new Set(sections.filter((s) => s === "activity")) : state.collapsedSections;
2272
2350
  const selectionValid = state.selectedId != null && action.items.some((i) => i.id === state.selectedId);
2273
2351
  if (!isFirstLoad && selectionValid && arraysEqual(sections, state.sections)) {
2274
- return state;
2352
+ return state.allItems === action.items ? state : { ...state, allItems: action.items };
2275
2353
  }
2276
2354
  if (selectionValid) {
2277
2355
  const selected = action.items.find((i) => i.id === state.selectedId);
@@ -5197,7 +5275,9 @@ function Dashboard({ config: config2, options, activeProfile }) {
5197
5275
  refresh,
5198
5276
  mutateData,
5199
5277
  pauseAutoRefresh,
5200
- resumeAutoRefresh
5278
+ resumeAutoRefresh,
5279
+ registerPendingMutation,
5280
+ clearPendingMutation
5201
5281
  } = useData(config2, options, refreshMs);
5202
5282
  const allRepos = useMemo3(() => data?.repos ?? [], [data?.repos]);
5203
5283
  const allTasks = useMemo3(
@@ -5268,7 +5348,9 @@ function Dashboard({ config: config2, options, activeProfile }) {
5268
5348
  refresh,
5269
5349
  mutateData,
5270
5350
  onOverlayDone: ui.exitOverlay,
5271
- pushEntry
5351
+ pushEntry,
5352
+ registerPendingMutation,
5353
+ clearPendingMutation
5272
5354
  });
5273
5355
  const pendingPickRef = useRef13(null);
5274
5356
  const labelCacheRef = useRef13({});
@@ -6916,7 +6998,7 @@ function resolveProjectId(projectId) {
6916
6998
  process.exit(1);
6917
6999
  }
6918
7000
  var program = new Command();
6919
- program.name("hog").description("Personal command deck \u2014 unified task dashboard for GitHub Projects + TickTick").version("1.7.0").option("--json", "Force JSON output").option("--human", "Force human-readable output").hook("preAction", (thisCommand) => {
7001
+ program.name("hog").description("Personal command deck \u2014 unified task dashboard for GitHub Projects + TickTick").version("1.7.2").option("--json", "Force JSON output").option("--human", "Force human-readable output").hook("preAction", (thisCommand) => {
6920
7002
  const opts = thisCommand.opts();
6921
7003
  if (opts.json) setFormat("json");
6922
7004
  if (opts.human) setFormat("human");