plc-cli 0.1.2 → 0.2.0

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.
Files changed (2) hide show
  1. package/dist/cli.js +101 -48
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -6,7 +6,7 @@ import { Command } from "commander";
6
6
  // src/meta.ts
7
7
  var PRODUCT_NAME = "Plane Cockpit";
8
8
  var BINARY_NAME = "plc";
9
- var VERSION = "0.1.1";
9
+ var VERSION = true ? "0.2.0" : "0.0.0-dev";
10
10
  var AUTHOR_HANDLE = "@brunoomariano";
11
11
 
12
12
  // src/commands/auth/index.ts
@@ -233,6 +233,12 @@ var profileSchema = z.strictObject({
233
233
  // Profile-wide default sort, inherited by any view that does not declare
234
234
  // its own `sort`. Same shape as a view's sort.
235
235
  sort: sortSpecSchema.optional(),
236
+ // Global state ordering for `sort: state` and the quick-transition
237
+ // navigation. An ordered list of state slugs (matched case-insensitively,
238
+ // whitespace-collapsed): listed states sort first in this order, unlisted
239
+ // ones after by workflow group. Project states are customizable, so this
240
+ // lets one list rank them across every project.
241
+ state_order: z.array(z.string().min(1)).optional(),
236
242
  // Profile-wide default column layout, inherited by views without a layout.
237
243
  layout: layoutSchema.optional()
238
244
  }).optional(),
@@ -1059,6 +1065,31 @@ function filtersFingerprint(filters) {
1059
1065
  return createHash("sha256").update(json).digest("hex").slice(0, 16);
1060
1066
  }
1061
1067
 
1068
+ // src/plane/state-rank.ts
1069
+ var STATE_GROUP_RANK = {
1070
+ backlog: 0,
1071
+ unstarted: 1,
1072
+ started: 2,
1073
+ completed: 3,
1074
+ cancelled: 4
1075
+ };
1076
+ var UNLISTED_OFFSET = 1e6;
1077
+ function normalizeStateSlug(name) {
1078
+ return name.trim().toLowerCase().replace(/\s+/g, " ");
1079
+ }
1080
+ function buildStateRank(stateOrder) {
1081
+ const position = /* @__PURE__ */ new Map();
1082
+ (stateOrder ?? []).forEach((slug, index) => {
1083
+ const normalized = normalizeStateSlug(slug);
1084
+ if (!position.has(normalized)) position.set(normalized, index);
1085
+ });
1086
+ return (state) => {
1087
+ const listed = position.get(normalizeStateSlug(state.name));
1088
+ if (listed !== void 0) return listed;
1089
+ return UNLISTED_OFFSET + STATE_GROUP_RANK[state.group];
1090
+ };
1091
+ }
1092
+
1062
1093
  // src/plane/sort-issues.ts
1063
1094
  var PRIORITY_RANK = {
1064
1095
  urgent: 0,
@@ -1067,45 +1098,41 @@ var PRIORITY_RANK = {
1067
1098
  low: 3,
1068
1099
  none: 4
1069
1100
  };
1070
- var STATE_GROUP_RANK = {
1071
- backlog: 0,
1072
- unstarted: 1,
1073
- started: 2,
1074
- completed: 3,
1075
- cancelled: 4
1076
- };
1077
1101
  var DEFAULT_SORT = [
1078
1102
  { field: "project", direction: "asc" },
1079
1103
  { field: "priority", direction: "desc" },
1080
1104
  { field: "state", direction: "asc" },
1081
1105
  { field: "updated_at", direction: "desc" }
1082
1106
  ];
1083
- var ASC_COMPARATORS = {
1084
- project: (a, b) => a.project_identifier.localeCompare(b.project_identifier),
1085
- priority: (a, b) => PRIORITY_RANK[b.priority] - PRIORITY_RANK[a.priority],
1086
- state: (a, b) => STATE_GROUP_RANK[a.state.group] - STATE_GROUP_RANK[b.state.group],
1087
- created_at: (a, b) => a.created_at.localeCompare(b.created_at),
1088
- updated_at: (a, b) => a.updated_at.localeCompare(b.updated_at),
1089
- assign: (a, b) => firstAssignee(a).localeCompare(firstAssignee(b))
1090
- };
1107
+ function ascComparators(stateRank) {
1108
+ return {
1109
+ project: (a, b) => a.project_identifier.localeCompare(b.project_identifier),
1110
+ priority: (a, b) => PRIORITY_RANK[b.priority] - PRIORITY_RANK[a.priority],
1111
+ state: (a, b) => stateRank(a.state) - stateRank(b.state),
1112
+ created_at: (a, b) => a.created_at.localeCompare(b.created_at),
1113
+ updated_at: (a, b) => a.updated_at.localeCompare(b.updated_at),
1114
+ assign: (a, b) => firstAssignee(a).localeCompare(firstAssignee(b))
1115
+ };
1116
+ }
1091
1117
  function firstAssignee(issue) {
1092
1118
  return issue.assignees[0]?.display_name ?? "";
1093
1119
  }
1094
- function compareKey(a, b, field, direction) {
1120
+ function compareKey(a, b, { field, direction }, comparators) {
1095
1121
  if (field === "assign") {
1096
1122
  const aUnassigned = a.assignees.length === 0;
1097
1123
  const bUnassigned = b.assignees.length === 0;
1098
1124
  if (aUnassigned !== bUnassigned) return aUnassigned ? 1 : -1;
1099
1125
  if (aUnassigned && bUnassigned) return 0;
1100
1126
  }
1101
- const base = ASC_COMPARATORS[field](a, b);
1127
+ const base = comparators[field](a, b);
1102
1128
  return direction === "desc" ? -base : base;
1103
1129
  }
1104
- function sortIssues(issues, sort) {
1130
+ function sortIssues(issues, sort, stateOrder) {
1105
1131
  if (!sort || sort.length === 0) return issues;
1132
+ const comparators = ascComparators(buildStateRank(stateOrder));
1106
1133
  return [...issues].sort((a, b) => {
1107
- for (const { field, direction } of sort) {
1108
- const cmp = compareKey(a, b, field, direction);
1134
+ for (const key of sort) {
1135
+ const cmp = compareKey(a, b, key, comparators);
1109
1136
  if (cmp !== 0) return cmp;
1110
1137
  }
1111
1138
  return 0;
@@ -1412,18 +1439,21 @@ var IssuesService = class {
1412
1439
  * `defaultsSort` is the profile-level `defaults.sort`; the effective sort is
1413
1440
  * resolved once as `view.sort ?? defaultsSort ?? DEFAULT_SORT` and applied
1414
1441
  * both as the per-project server hint and the authoritative client-side sort.
1442
+ * `stateOrder` is the profile-level `defaults.state_order`, used by the
1443
+ * client-side `state` sort to rank states by the configured slug order.
1415
1444
  */
1416
1445
  // `signal` is threaded down to each per-project fetch so the dashboard can
1417
1446
  // abort an in-flight refresh (e.g. when a new one starts before the previous
1418
1447
  // resolves), preventing requests from piling up and timing out. The positional
1419
1448
  // list mirrors the call sites; the cap is waived rather than reshaping both.
1420
1449
  // eslint-disable-next-line max-params
1421
- async list(projectIdentifiers, view, queryLimit, defaultsSort, signal) {
1450
+ async list(projectIdentifiers, view, queryLimit, defaultsSort, stateOrder, signal) {
1422
1451
  const { issues, failedProjects } = await this.listResilient(
1423
1452
  projectIdentifiers,
1424
1453
  view,
1425
1454
  queryLimit,
1426
1455
  defaultsSort,
1456
+ stateOrder,
1427
1457
  signal
1428
1458
  );
1429
1459
  if (failedProjects.length > 0) {
@@ -1442,7 +1472,7 @@ var IssuesService = class {
1442
1472
  */
1443
1473
  // Same positional shape as `list`; the cap is waived for the same reason.
1444
1474
  // eslint-disable-next-line max-params
1445
- async listResilient(projectIdentifiers, view, queryLimit, defaultsSort, signal) {
1475
+ async listResilient(projectIdentifiers, view, queryLimit, defaultsSort, stateOrder, signal) {
1446
1476
  const assigneeIds = await this.resolveAssigneeIds(view);
1447
1477
  const sort = resolveSort(view?.sort, defaultsSort);
1448
1478
  const effectiveView = view ? { ...view, sort } : { name: "", sort };
@@ -1461,7 +1491,7 @@ var IssuesService = class {
1461
1491
  const byGroup = refineByStateGroup(fetched, view?.filters?.state_group);
1462
1492
  const byState = refineByStateSearch(byGroup, view?.filters);
1463
1493
  const byAssignee = refineByAssignee(byState, assigneeIds);
1464
- const sorted = sortIssues(byAssignee, sort);
1494
+ const sorted = sortIssues(byAssignee, sort, stateOrder);
1465
1495
  const issues = queryLimit !== void 0 ? sorted.slice(0, queryLimit) : sorted;
1466
1496
  return { issues, failedProjects };
1467
1497
  }
@@ -2413,7 +2443,8 @@ function registerIssue(program2) {
2413
2443
  projects,
2414
2444
  view,
2415
2445
  limit ?? view?.query_limit,
2416
- ctx.runtime.profile.defaults?.sort
2446
+ ctx.runtime.profile.defaults?.sort,
2447
+ ctx.runtime.profile.defaults?.state_order
2417
2448
  );
2418
2449
  process.stdout.write(renderIssues(issues, format, ctx.theme));
2419
2450
  process.stdout.write("\n");
@@ -2930,6 +2961,16 @@ var PRIORITY_LETTER = {
2930
2961
  low: "L",
2931
2962
  none: "\xB7"
2932
2963
  };
2964
+ var SORT_FIELD_COLUMN = {
2965
+ priority: "priority",
2966
+ state: "state",
2967
+ assign: "assign"
2968
+ };
2969
+ function sortIndicator(column, sort) {
2970
+ const primary = sort?.[0];
2971
+ if (!primary || SORT_FIELD_COLUMN[primary.field] !== column) return "";
2972
+ return primary.direction === "asc" ? " \u2191" : " \u2193";
2973
+ }
2933
2974
  function computeViewport(total, selected, rows, previousStart = 0) {
2934
2975
  if (rows <= 0 || total === 0) return { start: 0, end: 0 };
2935
2976
  if (rows >= total) return { start: 0, end: total };
@@ -2964,10 +3005,16 @@ function IssueList(props) {
2964
3005
  return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", borderStyle: "round", paddingX: 1, flexGrow: 1, children: [
2965
3006
  /* @__PURE__ */ jsxs4(Box4, { columnGap: 1, children: [
2966
3007
  /* @__PURE__ */ jsx5(Box4, { ...cell("key", cols.keyWidth), children: /* @__PURE__ */ jsx5(Text4, { bold: true, children: alignText("KEY", cols.keyWidth, cols.align.key) }) }),
2967
- /* @__PURE__ */ jsx5(Box4, { ...cell("priority", cols.priorityWidth), children: /* @__PURE__ */ jsx5(Text4, { bold: true, children: cols.compactPriority ? "PR" : "PRIORITY" }) }),
2968
- cols.showState ? /* @__PURE__ */ jsx5(Box4, { ...cell("state", cols.stateWidth), children: /* @__PURE__ */ jsx5(Text4, { bold: true, children: "STATE" }) }) : null,
3008
+ /* @__PURE__ */ jsx5(Box4, { ...cell("priority", cols.priorityWidth), children: /* @__PURE__ */ jsx5(Text4, { bold: true, children: cols.compactPriority ? "PR" : `PRIORITY${sortIndicator("priority", props.sort)}` }) }),
3009
+ cols.showState ? /* @__PURE__ */ jsx5(Box4, { ...cell("state", cols.stateWidth), children: /* @__PURE__ */ jsxs4(Text4, { bold: true, children: [
3010
+ "STATE",
3011
+ sortIndicator("state", props.sort)
3012
+ ] }) }) : null,
2969
3013
  /* @__PURE__ */ jsx5(Box4, { ...cell("title", cols.title), children: /* @__PURE__ */ jsx5(Text4, { bold: true, children: "TITLE" }) }),
2970
- cols.showAssign ? /* @__PURE__ */ jsx5(Box4, { ...cell("assign", cols.assignWidth), children: /* @__PURE__ */ jsx5(Text4, { bold: true, children: "ASSIGN" }) }) : null
3014
+ cols.showAssign ? /* @__PURE__ */ jsx5(Box4, { ...cell("assign", cols.assignWidth), children: /* @__PURE__ */ jsxs4(Text4, { bold: true, children: [
3015
+ "ASSIGN",
3016
+ sortIndicator("assign", props.sort)
3017
+ ] }) }) : null
2971
3018
  ] }),
2972
3019
  hiddenAbove > 0 ? /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
2973
3020
  "\u2191 ",
@@ -4372,7 +4419,10 @@ var EMPTY_VIEW_DATA = {
4372
4419
  failedProjects: []
4373
4420
  };
4374
4421
  function useViewsData(opts) {
4375
- const { views, issuesService, defaultProjects, defaultsSort, logger } = opts;
4422
+ const { views, issuesService, defaults, logger } = opts;
4423
+ const defaultProjects = defaults?.projects;
4424
+ const defaultsSort = defaults?.sort;
4425
+ const stateOrder = defaults?.state_order;
4376
4426
  const [byView, setByView] = useState6(() => views.map(() => EMPTY_VIEW_DATA));
4377
4427
  const viewsRef = useRef4(views);
4378
4428
  viewsRef.current = views;
@@ -4414,6 +4464,7 @@ function useViewsData(opts) {
4414
4464
  view,
4415
4465
  view.query_limit ?? 100,
4416
4466
  defaultsSort,
4467
+ stateOrder,
4417
4468
  controller.signal
4418
4469
  );
4419
4470
  patch(viewIdx, {
@@ -4446,7 +4497,7 @@ function useViewsData(opts) {
4446
4497
  }
4447
4498
  }
4448
4499
  },
4449
- [issuesService, defaultProjects, defaultsSort, logger, patch]
4500
+ [issuesService, defaultProjects, defaultsSort, stateOrder, logger, patch]
4450
4501
  );
4451
4502
  const refreshAll = useCallback4(() => {
4452
4503
  const indices = viewsRef.current.map((_, idx) => idx);
@@ -4534,23 +4585,17 @@ function useIssueFilter(opts) {
4534
4585
  import { useCallback as useCallback5, useState as useState9 } from "react";
4535
4586
 
4536
4587
  // src/plane/state-order.ts
4537
- var GROUP_ORDER = [
4538
- "backlog",
4539
- "unstarted",
4540
- "started",
4541
- "completed",
4542
- "cancelled"
4543
- ];
4544
- function orderStates(states) {
4588
+ function orderStates(states, stateOrder) {
4589
+ const rank = buildStateRank(stateOrder);
4545
4590
  return states.map((state, index) => ({ state, index })).sort((a, b) => {
4546
- const ga = GROUP_ORDER.indexOf(a.state.group);
4547
- const gb = GROUP_ORDER.indexOf(b.state.group);
4548
- if (ga !== gb) return ga - gb;
4591
+ const ra = rank(a.state);
4592
+ const rb = rank(b.state);
4593
+ if (ra !== rb) return ra - rb;
4549
4594
  return a.index - b.index;
4550
4595
  }).map((entry) => entry.state);
4551
4596
  }
4552
- function neighbourState(states, currentId, direction) {
4553
- const ordered = orderStates(states);
4597
+ function neighbourState(states, currentId, direction, stateOrder) {
4598
+ const ordered = orderStates(states, stateOrder);
4554
4599
  const idx = ordered.findIndex((s) => s.id === currentId);
4555
4600
  if (idx < 0) return void 0;
4556
4601
  const target = idx + direction;
@@ -4581,7 +4626,8 @@ function useQuickTransition(opts) {
4581
4626
  if (!issue) return;
4582
4627
  try {
4583
4628
  const states = await ctx.states.list(projectOf(issue));
4584
- const next = neighbourState(states, issue.state.id, direction);
4629
+ const stateOrder = ctx.runtime.profile.defaults?.state_order;
4630
+ const next = neighbourState(states, issue.state.id, direction, stateOrder);
4585
4631
  if (!next) {
4586
4632
  setMessage(`${issue.key}: already at the ${direction === 1 ? "last" : "first"} state`);
4587
4633
  return;
@@ -4847,6 +4893,12 @@ function overlayStatusPosition(opts) {
4847
4893
  if (opts.isDetail && !opts.current) return void 0;
4848
4894
  return opts.listPosition;
4849
4895
  }
4896
+ function resolveViewPresentation(view, defaults) {
4897
+ return {
4898
+ layout: resolveLayout(view?.layout, defaults?.layout),
4899
+ sort: resolveSort(view?.sort, defaults?.sort)
4900
+ };
4901
+ }
4850
4902
  function Dashboard({ ctx, logger }) {
4851
4903
  const { exit } = useApp();
4852
4904
  const { rows: terminalRows, columns: terminalCols } = useTerminalSize();
@@ -4863,16 +4915,15 @@ function Dashboard({ ctx, logger }) {
4863
4915
  const [panel, setPanel] = useState11("list");
4864
4916
  const [helpOpen, setHelpOpen] = useState11(false);
4865
4917
  const activeView = views[viewIdx];
4866
- const activeLayout = useMemo6(
4867
- () => resolveLayout(activeView?.layout, ctx.runtime.profile.defaults?.layout),
4918
+ const { layout: activeLayout, sort: activeSort } = useMemo6(
4919
+ () => resolveViewPresentation(activeView, ctx.runtime.profile.defaults),
4868
4920
  [activeView, ctx]
4869
4921
  );
4870
4922
  const selectedKeyRef = React7.useRef(void 0);
4871
4923
  const viewsData = useViewsData({
4872
4924
  views,
4873
4925
  issuesService: ctx.issues,
4874
- defaultProjects: ctx.runtime.profile.defaults?.projects,
4875
- defaultsSort: ctx.runtime.profile.defaults?.sort,
4926
+ defaults: ctx.runtime.profile.defaults,
4876
4927
  logger
4877
4928
  });
4878
4929
  const active = viewsData.byView[viewIdx] ?? {
@@ -5198,6 +5249,7 @@ function Dashboard({ ctx, logger }) {
5198
5249
  filtering,
5199
5250
  viewportRows,
5200
5251
  layout: activeLayout,
5252
+ sort: activeSort,
5201
5253
  loading,
5202
5254
  statusBar: /* @__PURE__ */ jsx13(StatusBar, { ...statusBarBase, loading, position: listPosition })
5203
5255
  }
@@ -5225,6 +5277,7 @@ function ListLayout(props) {
5225
5277
  viewportRows: props.viewportRows,
5226
5278
  width: props.narrow ? props.width : props.width - SIDE_PANEL_WIDTH,
5227
5279
  layout: props.layout,
5280
+ sort: props.sort,
5228
5281
  loading: props.loading
5229
5282
  }
5230
5283
  ),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plc-cli",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "CLI and TUI for Plane (Cloud and self-hosted), inspired by gh and gh dash.",
5
5
  "license": "MIT",
6
6
  "author": "Bruno Mariano",