pipe-kan 0.23.1 → 0.23.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/pipe-kan.js +159 -132
- package/package.json +1 -1
package/dist/pipe-kan.js
CHANGED
|
@@ -300,6 +300,134 @@ function flagsToJql(flags) {
|
|
|
300
300
|
return clauses.join(" AND ");
|
|
301
301
|
}
|
|
302
302
|
|
|
303
|
+
// src/store.ts
|
|
304
|
+
var ME = {
|
|
305
|
+
displayName: "Person A",
|
|
306
|
+
emailAddress: "user@test.com",
|
|
307
|
+
name: "user@test.com"
|
|
308
|
+
};
|
|
309
|
+
var TRANSITIONS = [
|
|
310
|
+
{ id: "11", name: "To Do" },
|
|
311
|
+
{ id: "21", name: "In Progress" },
|
|
312
|
+
{ id: "31", name: "Done" }
|
|
313
|
+
];
|
|
314
|
+
var ALLOWED = {
|
|
315
|
+
"To Do": ["In Progress"],
|
|
316
|
+
"In Progress": ["To Do", "Done"],
|
|
317
|
+
Done: ["In Progress"]
|
|
318
|
+
};
|
|
319
|
+
function validIssueKey(key) {
|
|
320
|
+
return /^[A-Z][A-Z0-9]*-\d+$/i.test(key.trim());
|
|
321
|
+
}
|
|
322
|
+
function fieldValue(issue, field) {
|
|
323
|
+
switch (field) {
|
|
324
|
+
case "project":
|
|
325
|
+
return issue.key.split("-")[0] ?? "";
|
|
326
|
+
case "assignee":
|
|
327
|
+
return [
|
|
328
|
+
issue.fields.assignee?.emailAddress,
|
|
329
|
+
issue.fields.assignee?.displayName
|
|
330
|
+
].filter(Boolean).join(`
|
|
331
|
+
`);
|
|
332
|
+
case "status":
|
|
333
|
+
return issue.fields.status.name;
|
|
334
|
+
case "parent":
|
|
335
|
+
return issue.fields.parent?.key ?? "";
|
|
336
|
+
case "type": {
|
|
337
|
+
const named = issue.fields.issuetype ?? issue.fields.issueType;
|
|
338
|
+
if (named && typeof named === "object" && "name" in named && typeof named.name === "string") {
|
|
339
|
+
return named.name;
|
|
340
|
+
}
|
|
341
|
+
return "";
|
|
342
|
+
}
|
|
343
|
+
case "key":
|
|
344
|
+
return issue.key;
|
|
345
|
+
default:
|
|
346
|
+
return "";
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function matchesClause(issue, clause) {
|
|
350
|
+
const match = clause.trim().match(/^(\w+)\s*(!=|=)\s*"?([^"]+)"?$/);
|
|
351
|
+
if (!match)
|
|
352
|
+
return true;
|
|
353
|
+
const [, field, op, raw] = match;
|
|
354
|
+
const expected = raw.trim();
|
|
355
|
+
const actual = fieldValue(issue, field);
|
|
356
|
+
const hit = field === "assignee" ? actual.split(`
|
|
357
|
+
`).some((value) => value.toLowerCase() === expected.toLowerCase()) : actual.toLowerCase() === expected.toLowerCase();
|
|
358
|
+
return op === "!=" ? !hit : hit;
|
|
359
|
+
}
|
|
360
|
+
function matchJql(issue, jql) {
|
|
361
|
+
const stripped = jql.replace(/\s+ORDER BY\s+.+$/i, "").trim();
|
|
362
|
+
if (!stripped)
|
|
363
|
+
return true;
|
|
364
|
+
return stripped.split(/\s+AND\s+/i).every((clause) => matchesClause(issue, clause));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
class IssueStore {
|
|
368
|
+
issues;
|
|
369
|
+
constructor(issues) {
|
|
370
|
+
this.issues = issues;
|
|
371
|
+
}
|
|
372
|
+
static fromRaw(raw) {
|
|
373
|
+
if (!Array.isArray(raw)) {
|
|
374
|
+
throw new Error("Fixture must be a JSON array");
|
|
375
|
+
}
|
|
376
|
+
return new IssueStore(structuredClone(raw));
|
|
377
|
+
}
|
|
378
|
+
all() {
|
|
379
|
+
return this.issues;
|
|
380
|
+
}
|
|
381
|
+
rawJson() {
|
|
382
|
+
return JSON.stringify(this.issues, null, 2);
|
|
383
|
+
}
|
|
384
|
+
get(key) {
|
|
385
|
+
return this.issues.find((issue) => issue.key.toUpperCase() === key.toUpperCase());
|
|
386
|
+
}
|
|
387
|
+
list(jql = "") {
|
|
388
|
+
return this.issues.filter((issue) => matchJql(issue, jql));
|
|
389
|
+
}
|
|
390
|
+
childrenOf(keys) {
|
|
391
|
+
const listed = new Set(keys.filter(validIssueKey));
|
|
392
|
+
return this.issues.filter((issue) => {
|
|
393
|
+
if (fieldValue(issue, "type").toLowerCase() === "epic")
|
|
394
|
+
return false;
|
|
395
|
+
return listed.has(fieldValue(issue, "parent"));
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
transitions(key) {
|
|
399
|
+
const issue = this.get(key);
|
|
400
|
+
if (!issue)
|
|
401
|
+
return [];
|
|
402
|
+
const allowed = ALLOWED[issue.fields.status.name] ?? [];
|
|
403
|
+
return TRANSITIONS.map((transition) => ({
|
|
404
|
+
...transition,
|
|
405
|
+
isAvailable: allowed.includes(transition.name)
|
|
406
|
+
}));
|
|
407
|
+
}
|
|
408
|
+
move(key, status) {
|
|
409
|
+
const issue = this.get(key);
|
|
410
|
+
if (!issue)
|
|
411
|
+
return { ok: false, error: `Issue ${key} not found` };
|
|
412
|
+
const transition = this.transitions(key).find((item) => item.name.toLowerCase() === status.toLowerCase() || item.id === status);
|
|
413
|
+
if (!transition || !transition.isAvailable) {
|
|
414
|
+
const available = this.transitions(key).filter((item) => item.isAvailable).map((item) => item.name).join(", ");
|
|
415
|
+
return {
|
|
416
|
+
ok: false,
|
|
417
|
+
error: `invalid transition state "${status}"
|
|
418
|
+
Available states for issue ${key}: ${available}`
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
issue.fields.status.name = transition.name;
|
|
422
|
+
if (transition.name === "Done") {
|
|
423
|
+
issue.fields.resolution = { name: "Done" };
|
|
424
|
+
} else {
|
|
425
|
+
delete issue.fields.resolution;
|
|
426
|
+
}
|
|
427
|
+
return { ok: true };
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
303
431
|
// src/cli.ts
|
|
304
432
|
function emptyList(text) {
|
|
305
433
|
return /no result found/i.test(text);
|
|
@@ -332,7 +460,7 @@ function createStoreCli(store) {
|
|
|
332
460
|
return JSON.stringify(issues, null, 2);
|
|
333
461
|
},
|
|
334
462
|
async listChildren(keys) {
|
|
335
|
-
return JSON.stringify(store.childrenOf(keys), null, 2);
|
|
463
|
+
return JSON.stringify(store.childrenOf(keys.filter(validIssueKey)), null, 2);
|
|
336
464
|
},
|
|
337
465
|
async move(key, status) {
|
|
338
466
|
return store.move(key, status);
|
|
@@ -468,9 +596,12 @@ function createJiraCli(opts = {}) {
|
|
|
468
596
|
]);
|
|
469
597
|
},
|
|
470
598
|
async listChildren(keys) {
|
|
471
|
-
|
|
599
|
+
const validKeys = keys.filter(validIssueKey);
|
|
600
|
+
if (!validKeys.length) {
|
|
601
|
+
console.log("Refresh children skipped; no valid parent keys");
|
|
472
602
|
return "[]";
|
|
473
|
-
|
|
603
|
+
}
|
|
604
|
+
const list = validKeys.map((key) => `"${key}"`).join(", ");
|
|
474
605
|
return listAll([
|
|
475
606
|
"issue",
|
|
476
607
|
"list",
|
|
@@ -741,10 +872,10 @@ function createApp(opts) {
|
|
|
741
872
|
console.log(`Refresh children ${cards.length}`);
|
|
742
873
|
}
|
|
743
874
|
} catch (err) {
|
|
744
|
-
|
|
745
|
-
|
|
875
|
+
nextChildren = childrenRaw;
|
|
876
|
+
nextHasCache = hasChildrenCache;
|
|
746
877
|
nextError = err instanceof Error ? err.message : "Epic children list failed";
|
|
747
|
-
console.log("Refresh children failed", nextError);
|
|
878
|
+
console.log("Refresh children failed; keeping existing children", nextError);
|
|
748
879
|
}
|
|
749
880
|
payload = nextPayload;
|
|
750
881
|
epicsPayload = nextEpics;
|
|
@@ -805,131 +936,6 @@ function createApp(opts) {
|
|
|
805
936
|
return app;
|
|
806
937
|
}
|
|
807
938
|
|
|
808
|
-
// src/store.ts
|
|
809
|
-
var ME = {
|
|
810
|
-
displayName: "Person A",
|
|
811
|
-
emailAddress: "user@test.com",
|
|
812
|
-
name: "user@test.com"
|
|
813
|
-
};
|
|
814
|
-
var TRANSITIONS = [
|
|
815
|
-
{ id: "11", name: "To Do" },
|
|
816
|
-
{ id: "21", name: "In Progress" },
|
|
817
|
-
{ id: "31", name: "Done" }
|
|
818
|
-
];
|
|
819
|
-
var ALLOWED = {
|
|
820
|
-
"To Do": ["In Progress"],
|
|
821
|
-
"In Progress": ["To Do", "Done"],
|
|
822
|
-
Done: ["In Progress"]
|
|
823
|
-
};
|
|
824
|
-
function fieldValue(issue, field) {
|
|
825
|
-
switch (field) {
|
|
826
|
-
case "project":
|
|
827
|
-
return issue.key.split("-")[0] ?? "";
|
|
828
|
-
case "assignee":
|
|
829
|
-
return [
|
|
830
|
-
issue.fields.assignee?.emailAddress,
|
|
831
|
-
issue.fields.assignee?.displayName
|
|
832
|
-
].filter(Boolean).join(`
|
|
833
|
-
`);
|
|
834
|
-
case "status":
|
|
835
|
-
return issue.fields.status.name;
|
|
836
|
-
case "parent":
|
|
837
|
-
return issue.fields.parent?.key ?? "";
|
|
838
|
-
case "type": {
|
|
839
|
-
const named = issue.fields.issuetype ?? issue.fields.issueType;
|
|
840
|
-
if (named && typeof named === "object" && "name" in named && typeof named.name === "string") {
|
|
841
|
-
return named.name;
|
|
842
|
-
}
|
|
843
|
-
return "";
|
|
844
|
-
}
|
|
845
|
-
case "key":
|
|
846
|
-
return issue.key;
|
|
847
|
-
default:
|
|
848
|
-
return "";
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
function matchesClause(issue, clause) {
|
|
852
|
-
const match = clause.trim().match(/^(\w+)\s*(!=|=)\s*"?([^"]+)"?$/);
|
|
853
|
-
if (!match)
|
|
854
|
-
return true;
|
|
855
|
-
const [, field, op, raw] = match;
|
|
856
|
-
const expected = raw.trim();
|
|
857
|
-
const actual = fieldValue(issue, field);
|
|
858
|
-
const hit = field === "assignee" ? actual.split(`
|
|
859
|
-
`).some((value) => value.toLowerCase() === expected.toLowerCase()) : actual.toLowerCase() === expected.toLowerCase();
|
|
860
|
-
return op === "!=" ? !hit : hit;
|
|
861
|
-
}
|
|
862
|
-
function matchJql(issue, jql) {
|
|
863
|
-
const stripped = jql.replace(/\s+ORDER BY\s+.+$/i, "").trim();
|
|
864
|
-
if (!stripped)
|
|
865
|
-
return true;
|
|
866
|
-
return stripped.split(/\s+AND\s+/i).every((clause) => matchesClause(issue, clause));
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
class IssueStore {
|
|
870
|
-
issues;
|
|
871
|
-
constructor(issues) {
|
|
872
|
-
this.issues = issues;
|
|
873
|
-
}
|
|
874
|
-
static fromRaw(raw) {
|
|
875
|
-
if (!Array.isArray(raw)) {
|
|
876
|
-
throw new Error("Fixture must be a JSON array");
|
|
877
|
-
}
|
|
878
|
-
return new IssueStore(structuredClone(raw));
|
|
879
|
-
}
|
|
880
|
-
all() {
|
|
881
|
-
return this.issues;
|
|
882
|
-
}
|
|
883
|
-
rawJson() {
|
|
884
|
-
return JSON.stringify(this.issues, null, 2);
|
|
885
|
-
}
|
|
886
|
-
get(key) {
|
|
887
|
-
return this.issues.find((issue) => issue.key.toUpperCase() === key.toUpperCase());
|
|
888
|
-
}
|
|
889
|
-
list(jql = "") {
|
|
890
|
-
return this.issues.filter((issue) => matchJql(issue, jql));
|
|
891
|
-
}
|
|
892
|
-
childrenOf(keys) {
|
|
893
|
-
const listed = new Set(keys);
|
|
894
|
-
return this.issues.filter((issue) => {
|
|
895
|
-
if (fieldValue(issue, "type").toLowerCase() === "epic")
|
|
896
|
-
return false;
|
|
897
|
-
return listed.has(fieldValue(issue, "parent"));
|
|
898
|
-
});
|
|
899
|
-
}
|
|
900
|
-
transitions(key) {
|
|
901
|
-
const issue = this.get(key);
|
|
902
|
-
if (!issue)
|
|
903
|
-
return [];
|
|
904
|
-
const allowed = ALLOWED[issue.fields.status.name] ?? [];
|
|
905
|
-
return TRANSITIONS.map((transition) => ({
|
|
906
|
-
...transition,
|
|
907
|
-
isAvailable: allowed.includes(transition.name)
|
|
908
|
-
}));
|
|
909
|
-
}
|
|
910
|
-
move(key, status) {
|
|
911
|
-
const issue = this.get(key);
|
|
912
|
-
if (!issue)
|
|
913
|
-
return { ok: false, error: `Issue ${key} not found` };
|
|
914
|
-
const transition = this.transitions(key).find((item) => item.name.toLowerCase() === status.toLowerCase() || item.id === status);
|
|
915
|
-
if (!transition || !transition.isAvailable) {
|
|
916
|
-
const available = this.transitions(key).filter((item) => item.isAvailable).map((item) => item.name).join(", ");
|
|
917
|
-
return {
|
|
918
|
-
ok: false,
|
|
919
|
-
error: `invalid transition state "${status}"
|
|
920
|
-
Available states for issue ${key}: ${available}`
|
|
921
|
-
};
|
|
922
|
-
}
|
|
923
|
-
issue.fields.status.name = transition.name;
|
|
924
|
-
if (transition.name === "Done") {
|
|
925
|
-
issue.fields.resolution = { name: "Done" };
|
|
926
|
-
} else {
|
|
927
|
-
delete issue.fields.resolution;
|
|
928
|
-
}
|
|
929
|
-
return { ok: true };
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
|
|
933
939
|
// src/boot.ts
|
|
934
940
|
async function createBoardApp(opts) {
|
|
935
941
|
const env = opts.env ?? process.env;
|
|
@@ -11754,6 +11760,21 @@ function readBody3(req) {
|
|
|
11754
11760
|
function pathOf3(req) {
|
|
11755
11761
|
return new URL(req.url ?? "/", "http://127.0.0.1");
|
|
11756
11762
|
}
|
|
11763
|
+
function childrenJqlError(jql) {
|
|
11764
|
+
const trimmed = jql.trim();
|
|
11765
|
+
if (/\bin\s*\(\s*\)/i.test(trimmed)) {
|
|
11766
|
+
return "The value '' does not exist for the field 'parent'.";
|
|
11767
|
+
}
|
|
11768
|
+
const childrenMatch = trimmed.match(/(?:parent|"Epic Link")\s+in\s+\(([^)]+)\)/i);
|
|
11769
|
+
if (childrenMatch) {
|
|
11770
|
+
const values = childrenMatch[1].split(",").map((value) => value.trim().replace(/^["']|["']$/g, ""));
|
|
11771
|
+
const invalid = values.filter((value) => !validIssueKey(value));
|
|
11772
|
+
if (invalid.length) {
|
|
11773
|
+
return `No issues have a parent epic with key or name '${invalid[0]}'.`;
|
|
11774
|
+
}
|
|
11775
|
+
}
|
|
11776
|
+
return;
|
|
11777
|
+
}
|
|
11757
11778
|
function handleFakeJira(req, res, store) {
|
|
11758
11779
|
const url = pathOf3(req);
|
|
11759
11780
|
const path = url.pathname;
|
|
@@ -11763,7 +11784,13 @@ function handleFakeJira(req, res, store) {
|
|
|
11763
11784
|
return true;
|
|
11764
11785
|
}
|
|
11765
11786
|
if ((path === "/rest/api/3/search/jql" || path === "/rest/api/2/search" || path === "/rest/api/3/search") && method === "GET") {
|
|
11766
|
-
const
|
|
11787
|
+
const jql = url.searchParams.get("jql") ?? "";
|
|
11788
|
+
const error = childrenJqlError(jql);
|
|
11789
|
+
if (error) {
|
|
11790
|
+
json3(res, 400, { errorMessages: [error] });
|
|
11791
|
+
return true;
|
|
11792
|
+
}
|
|
11793
|
+
const issues = store.list(jql);
|
|
11767
11794
|
json3(res, 200, { expand: "schema,names", isLast: true, issues });
|
|
11768
11795
|
return true;
|
|
11769
11796
|
}
|