hazo_admin 0.7.1 → 0.11.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.
- package/CHANGE_LOG.md +60 -0
- package/README.md +65 -4
- package/SETUP_CHECKLIST.md +24 -0
- package/dist/api/index.d.ts +6 -1
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +166 -2
- package/dist/components/issues_panel/board_columns.d.ts +17 -0
- package/dist/components/issues_panel/board_columns.d.ts.map +1 -0
- package/dist/components/issues_panel/board_columns.js +37 -0
- package/dist/components/issues_panel/card_assignee_control.d.ts +17 -0
- package/dist/components/issues_panel/card_assignee_control.d.ts.map +1 -0
- package/dist/components/issues_panel/card_assignee_control.js +51 -0
- package/dist/components/issues_panel/card_type_control.d.ts +18 -0
- package/dist/components/issues_panel/card_type_control.d.ts.map +1 -0
- package/dist/components/issues_panel/card_type_control.js +42 -0
- package/dist/components/issues_panel/facet_sidebar.d.ts +25 -0
- package/dist/components/issues_panel/facet_sidebar.d.ts.map +1 -0
- package/dist/components/issues_panel/facet_sidebar.js +72 -0
- package/dist/components/issues_panel/facet_topbar.d.ts +20 -0
- package/dist/components/issues_panel/facet_topbar.d.ts.map +1 -0
- package/dist/components/issues_panel/facet_topbar.js +42 -0
- package/dist/components/issues_panel/filter.d.ts +12 -0
- package/dist/components/issues_panel/filter.d.ts.map +1 -0
- package/dist/components/issues_panel/filter.js +41 -0
- package/dist/components/issues_panel/index.d.ts.map +1 -1
- package/dist/components/issues_panel/index.js +145 -43
- package/dist/components/issues_panel/manage_types_dialog.d.ts +28 -0
- package/dist/components/issues_panel/manage_types_dialog.d.ts.map +1 -0
- package/dist/components/issues_panel/manage_types_dialog.js +84 -0
- package/dist/components/issues_panel/ui_helpers.d.ts +21 -0
- package/dist/components/issues_panel/ui_helpers.d.ts.map +1 -0
- package/dist/components/issues_panel/ui_helpers.js +136 -0
- package/dist/components/issues_panel/use_issue_types.d.ts +30 -0
- package/dist/components/issues_panel/use_issue_types.d.ts.map +1 -0
- package/dist/components/issues_panel/use_issue_types.js +81 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.ui.d.ts +2 -0
- package/dist/index.ui.d.ts.map +1 -1
- package/dist/index.ui.js +1 -0
- package/dist/issues/archive_handler.d.ts +1 -1
- package/dist/issues/archive_handler.js +2 -2
- package/dist/issues/index.d.ts +6 -0
- package/dist/issues/index.d.ts.map +1 -1
- package/dist/issues/index.js +4 -0
- package/dist/issues/notify_handler.d.ts +26 -0
- package/dist/issues/notify_handler.d.ts.map +1 -0
- package/dist/issues/notify_handler.js +97 -0
- package/dist/issues/raise.d.ts +44 -0
- package/dist/issues/raise.d.ts.map +1 -0
- package/dist/issues/raise.js +77 -0
- package/dist/issues/recipients.d.ts +20 -0
- package/dist/issues/recipients.d.ts.map +1 -0
- package/dist/issues/recipients.js +61 -0
- package/dist/issues/registry.client.d.ts +1 -0
- package/dist/issues/registry.client.d.ts.map +1 -1
- package/dist/issues/registry.client.js +4 -1
- package/dist/issues/registry.d.ts +3 -3
- package/dist/issues/registry.d.ts.map +1 -1
- package/dist/issues/store.d.ts +10 -5
- package/dist/issues/store.d.ts.map +1 -1
- package/dist/issues/store.js +58 -28
- package/dist/issues/type_catalog.d.ts +48 -0
- package/dist/issues/type_catalog.d.ts.map +1 -0
- package/dist/issues/type_catalog.js +185 -0
- package/package.json +4 -4
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
import type { IssueRecord } from './store.js';
|
|
3
|
+
export type IssueSeverity = 'low' | 'medium' | 'high' | 'critical';
|
|
4
|
+
export interface RaiseIssueInput {
|
|
5
|
+
/** Registered IssueTypeDef.typeKey. Required — raiseIssue throws if unknown. */
|
|
6
|
+
type: string;
|
|
7
|
+
/** Scope id. Present ⇒ scoped (local admin). Omitted/null ⇒ global (super admin). */
|
|
8
|
+
scope_id?: string | null;
|
|
9
|
+
severity?: IssueSeverity;
|
|
10
|
+
/** Raising package/app id, e.g. 'hazo_files'. */
|
|
11
|
+
source?: string;
|
|
12
|
+
subject_user_id?: string | null;
|
|
13
|
+
recipient_scope_id?: string;
|
|
14
|
+
/** Overrides the typed-def descriptor when supplied. */
|
|
15
|
+
title?: string;
|
|
16
|
+
summary?: string;
|
|
17
|
+
payload?: Record<string, unknown>;
|
|
18
|
+
/** Auto-derived from source/type/title when omitted. */
|
|
19
|
+
dedupe_key?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface RaiseIssueOptions {
|
|
22
|
+
/**
|
|
23
|
+
* Best-effort notify enqueue override. When omitted, raiseIssue lazily loads
|
|
24
|
+
* hazo_jobs/server and submits an ADMIN_ISSUE_NOTIFY_JOB_TYPE job itself.
|
|
25
|
+
* Tests and test-apps inject this to observe/enqueue deterministically.
|
|
26
|
+
*/
|
|
27
|
+
enqueueNotify?: (payload: {
|
|
28
|
+
issueId: string;
|
|
29
|
+
}) => Promise<void> | void;
|
|
30
|
+
/** SQL dialect for the fallback hazo_jobs client. */
|
|
31
|
+
dialect?: 'pg' | 'sqlite';
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The single documented ingest protocol for raising an issue into the admin
|
|
35
|
+
* Kanban. Trusted-by-construction (server-side caller already holds a connect
|
|
36
|
+
* adapter). Validates the type is registered, derives title/summary/dedupe_key,
|
|
37
|
+
* persists via the store, and — only when the issue is newly created — best-effort
|
|
38
|
+
* enqueues the notify job.
|
|
39
|
+
*/
|
|
40
|
+
export declare function raiseIssue(connect: any, input: RaiseIssueInput, opts?: RaiseIssueOptions): Promise<{
|
|
41
|
+
issue: IssueRecord;
|
|
42
|
+
isNew: boolean;
|
|
43
|
+
}>;
|
|
44
|
+
//# sourceMappingURL=raise.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"raise.d.ts","sourceRoot":"","sources":["../../src/issues/raise.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AAGrB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAO9C,MAAM,MAAM,aAAa,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,CAAC;AAEnE,MAAM,WAAW,eAAe;IAC9B,gFAAgF;IAChF,IAAI,EAAE,MAAM,CAAC;IACb,qFAAqF;IACrF,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,wDAAwD;IACxD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,wDAAwD;IACxD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACvE,qDAAqD;IACrD,OAAO,CAAC,EAAE,IAAI,GAAG,QAAQ,CAAC;CAC3B;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAC9B,OAAO,EAAE,GAAG,EACZ,KAAK,EAAE,eAAe,EACtB,IAAI,GAAE,iBAAsB,GAC3B,OAAO,CAAC;IAAE,KAAK,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC,CAmEjD"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
import { createLogger } from 'hazo_core';
|
|
3
|
+
import { createIssueStoreFromConnect } from './store.js';
|
|
4
|
+
import { getIssueType } from './registry.js';
|
|
5
|
+
import { GLOBAL_ISSUE_SCOPE_ID } from './recipients.js';
|
|
6
|
+
import { ADMIN_ISSUE_NOTIFY_JOB_TYPE } from './notify_handler.js';
|
|
7
|
+
const log = createLogger('hazo_admin:issues:raise');
|
|
8
|
+
/**
|
|
9
|
+
* The single documented ingest protocol for raising an issue into the admin
|
|
10
|
+
* Kanban. Trusted-by-construction (server-side caller already holds a connect
|
|
11
|
+
* adapter). Validates the type is registered, derives title/summary/dedupe_key,
|
|
12
|
+
* persists via the store, and — only when the issue is newly created — best-effort
|
|
13
|
+
* enqueues the notify job.
|
|
14
|
+
*/
|
|
15
|
+
export async function raiseIssue(connect, input, opts = {}) {
|
|
16
|
+
const typeDef = getIssueType(input.type);
|
|
17
|
+
if (!typeDef) {
|
|
18
|
+
throw new Error(`hazo_admin:raiseIssue — unknown issue type '${input.type}'. Register it via registerIssueType() first.`);
|
|
19
|
+
}
|
|
20
|
+
const payload = input.payload ?? {};
|
|
21
|
+
let title = input.title;
|
|
22
|
+
let summary = input.summary;
|
|
23
|
+
if ((!title || !summary) && typeof typeDef.buildDescriptor === 'function') {
|
|
24
|
+
try {
|
|
25
|
+
const d = typeDef.buildDescriptor(payload);
|
|
26
|
+
title = title ?? d.title;
|
|
27
|
+
summary = summary ?? d.summary;
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
log.debug('raiseIssue: buildDescriptor threw', { type: input.type, error: err?.message });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
title = title ?? input.type;
|
|
34
|
+
summary = summary ?? '';
|
|
35
|
+
const source = input.source ?? null;
|
|
36
|
+
const scopeId = input.scope_id ?? GLOBAL_ISSUE_SCOPE_ID;
|
|
37
|
+
const dedupe_key = input.dedupe_key ?? `${source ?? 'unknown'}:${input.type}:${title}`;
|
|
38
|
+
const store = await createIssueStoreFromConnect(connect);
|
|
39
|
+
const result = await store.createOrBumpIssue({
|
|
40
|
+
scope_id: scopeId,
|
|
41
|
+
type: input.type,
|
|
42
|
+
severity: input.severity ?? 'medium',
|
|
43
|
+
source: source ?? undefined,
|
|
44
|
+
subject_user_id: input.subject_user_id ?? null,
|
|
45
|
+
recipient_scope_id: input.recipient_scope_id,
|
|
46
|
+
title,
|
|
47
|
+
summary,
|
|
48
|
+
payload,
|
|
49
|
+
dedupe_key,
|
|
50
|
+
});
|
|
51
|
+
// Only a brand-new issue notifies — dedupe bumps do not re-notify.
|
|
52
|
+
if (result.isNew) {
|
|
53
|
+
try {
|
|
54
|
+
if (opts.enqueueNotify) {
|
|
55
|
+
await opts.enqueueNotify({ issueId: result.issue.id });
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
const jobsMod = (await import('hazo_jobs/server').catch(() => null));
|
|
59
|
+
if (jobsMod?.createJobsClient && typeof connect?.raw === 'function') {
|
|
60
|
+
const client = jobsMod.createJobsClient({
|
|
61
|
+
connect: { adapter: connect },
|
|
62
|
+
dialect: opts.dialect ?? 'pg',
|
|
63
|
+
});
|
|
64
|
+
await client.submit({
|
|
65
|
+
description: `Notify admins of raised issue ${result.issue.id}`,
|
|
66
|
+
type: ADMIN_ISSUE_NOTIFY_JOB_TYPE,
|
|
67
|
+
payload: { issueId: result.issue.id },
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
log.debug('raiseIssue: notify enqueue skipped (best-effort)', { error: err?.message });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
/** Permission that identifies a super/global admin (matches hazo_auth). */
|
|
3
|
+
export declare const GLOBAL_ADMIN_PERMISSION = "hazo_org_global_admin";
|
|
4
|
+
/** Permission that identifies a scope-local admin able to triage issues. */
|
|
5
|
+
export declare const SCOPE_ADMIN_PERMISSION = "admin_issue_triage";
|
|
6
|
+
/** Sentinel scope_id stored for issues raised without a scope (global/super-admin issues). */
|
|
7
|
+
export declare const GLOBAL_ISSUE_SCOPE_ID = "global";
|
|
8
|
+
/**
|
|
9
|
+
* Resolve local admins of a given scope (recipients for a scoped issue).
|
|
10
|
+
* Defaults to the `admin_issue_triage` permission.
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveScopeAdmins(connect: any, scope_id: string, permission?: string): Promise<string[]>;
|
|
13
|
+
/**
|
|
14
|
+
* Resolve global/super admins (recipients for a global issue, no scope).
|
|
15
|
+
* find_roles_with_permission is scope-bound, so global admins (who may hold the
|
|
16
|
+
* permission at any scope) are resolved via a direct join when the adapter
|
|
17
|
+
* exposes raw(). Best-effort: [] otherwise.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveGlobalAdmins(connect: any): Promise<string[]>;
|
|
20
|
+
//# sourceMappingURL=recipients.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recipients.d.ts","sourceRoot":"","sources":["../../src/issues/recipients.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AAErB,2EAA2E;AAC3E,eAAO,MAAM,uBAAuB,0BAA0B,CAAC;AAE/D,4EAA4E;AAC5E,eAAO,MAAM,sBAAsB,uBAAuB,CAAC;AAE3D,8FAA8F;AAC9F,eAAO,MAAM,qBAAqB,WAAW,CAAC;AA2B9C;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,GAAG,EACZ,QAAQ,EAAE,MAAM,EAChB,UAAU,GAAE,MAA+B,GAC1C,OAAO,CAAC,MAAM,EAAE,CAAC,CAGnB;AAED;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAgBzE"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
/** Permission that identifies a super/global admin (matches hazo_auth). */
|
|
3
|
+
export const GLOBAL_ADMIN_PERMISSION = 'hazo_org_global_admin';
|
|
4
|
+
/** Permission that identifies a scope-local admin able to triage issues. */
|
|
5
|
+
export const SCOPE_ADMIN_PERMISSION = 'admin_issue_triage';
|
|
6
|
+
/** Sentinel scope_id stored for issues raised without a scope (global/super-admin issues). */
|
|
7
|
+
export const GLOBAL_ISSUE_SCOPE_ID = 'global';
|
|
8
|
+
/**
|
|
9
|
+
* User ids holding `permission` within a specific scope.
|
|
10
|
+
* Intersects find_roles_with_permission (roles that grant the permission in the
|
|
11
|
+
* scope) with get_users_by_scope (users assigned to the scope) on role_id.
|
|
12
|
+
* Best-effort: returns [] if hazo_auth is absent or anything throws.
|
|
13
|
+
*/
|
|
14
|
+
async function usersWithPermissionInScope(connect, scope_id, permission) {
|
|
15
|
+
const authLib = await import('hazo_auth/server-lib').catch(() => null);
|
|
16
|
+
if (!authLib?.find_roles_with_permission || !authLib?.get_users_by_scope)
|
|
17
|
+
return [];
|
|
18
|
+
const roles = await authLib.find_roles_with_permission(connect, scope_id, permission).catch(() => []);
|
|
19
|
+
if (!Array.isArray(roles) || roles.length === 0)
|
|
20
|
+
return [];
|
|
21
|
+
const roleIds = new Set(roles.map((r) => r.id));
|
|
22
|
+
const res = await authLib.get_users_by_scope(connect, scope_id).catch(() => null);
|
|
23
|
+
const scopes = res?.scopes ?? [];
|
|
24
|
+
const userIds = scopes
|
|
25
|
+
.filter((s) => roleIds.has(s.role_id))
|
|
26
|
+
.map((s) => s.user_id)
|
|
27
|
+
.filter(Boolean);
|
|
28
|
+
return [...new Set(userIds)];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Resolve local admins of a given scope (recipients for a scoped issue).
|
|
32
|
+
* Defaults to the `admin_issue_triage` permission.
|
|
33
|
+
*/
|
|
34
|
+
export async function resolveScopeAdmins(connect, scope_id, permission = SCOPE_ADMIN_PERMISSION) {
|
|
35
|
+
if (!scope_id)
|
|
36
|
+
return [];
|
|
37
|
+
return usersWithPermissionInScope(connect, scope_id, permission);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Resolve global/super admins (recipients for a global issue, no scope).
|
|
41
|
+
* find_roles_with_permission is scope-bound, so global admins (who may hold the
|
|
42
|
+
* permission at any scope) are resolved via a direct join when the adapter
|
|
43
|
+
* exposes raw(). Best-effort: [] otherwise.
|
|
44
|
+
*/
|
|
45
|
+
export async function resolveGlobalAdmins(connect) {
|
|
46
|
+
if (typeof connect?.raw !== 'function')
|
|
47
|
+
return [];
|
|
48
|
+
try {
|
|
49
|
+
const rows = await connect.raw(`SELECT DISTINCT us.user_id
|
|
50
|
+
FROM hazo_user_scopes us
|
|
51
|
+
JOIN hazo_role_permissions rp ON rp.role_id = us.role_id
|
|
52
|
+
JOIN hazo_permissions p ON p.id = rp.permission_id
|
|
53
|
+
WHERE p.permission_name = $1`, [GLOBAL_ADMIN_PERMISSION]);
|
|
54
|
+
if (!Array.isArray(rows))
|
|
55
|
+
return [];
|
|
56
|
+
return [...new Set(rows.map((r) => r.user_id).filter(Boolean))];
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.client.d.ts","sourceRoot":"","sources":["../../src/issues/registry.client.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"registry.client.d.ts","sourceRoot":"","sources":["../../src/issues/registry.client.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,gBAAgB,EAAE,MAAM,CAAC;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,aAAa,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAClF;AAED,MAAM,MAAM,iBAAiB,GAAG,KAAK,CAAC,aAAa,CAAC,sBAAsB,CAAC,CAAC;AAI5E,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAE5F;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS,CAEnF;AAED,wBAAgB,sBAAsB,IAAI,KAAK,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,iBAAiB,CAAA;CAAE,CAAC,CAEhG;AAED,wBAAgB,gBAAgB,CAAC,EAAE,KAAK,EAAE,EAAE,sBAAsB,qBAmDjE"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { cx, severityMeta, relativeTime } from '../components/issues_panel/ui_helpers.js';
|
|
3
4
|
const registry = new Map();
|
|
4
5
|
export function registerIssueCardRenderer(typeKey, renderer) {
|
|
5
6
|
registry.set(typeKey, renderer);
|
|
@@ -11,5 +12,7 @@ export function listIssueCardRenderers() {
|
|
|
11
12
|
return Array.from(registry.entries()).map(([typeKey, renderer]) => ({ typeKey, renderer }));
|
|
12
13
|
}
|
|
13
14
|
export function DefaultIssueCard({ issue }) {
|
|
14
|
-
|
|
15
|
+
const sev = severityMeta(issue.severity);
|
|
16
|
+
const when = relativeTime(issue.last_seen_at);
|
|
17
|
+
return (_jsxs("div", { className: cx('relative pl-3', 'before:absolute before:left-0 before:top-0.5 before:bottom-0.5 before:w-1 before:rounded-full', sev.rail), children: [_jsx("h4", { className: "text-[13px] font-semibold leading-snug text-slate-800 line-clamp-2", children: issue.title }), issue.summary && (_jsx("p", { className: "mt-1 text-xs leading-relaxed text-slate-500 line-clamp-2", children: issue.summary })), _jsxs("div", { className: "mt-2.5 flex flex-wrap items-center gap-1.5", children: [_jsxs("span", { className: cx('inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[10px] font-semibold', sev.badge), children: [_jsx("span", { className: cx('h-1.5 w-1.5 rounded-full', sev.dot) }), sev.label] }), _jsx("span", { className: "inline-flex items-center rounded-full bg-slate-100 px-2 py-0.5 text-[10px] font-medium text-slate-600 ring-1 ring-inset ring-slate-200/70", children: issue.type }), issue.occurrence_count > 1 && (_jsxs("span", { title: `Seen ${issue.occurrence_count} times`, className: "inline-flex items-center rounded-full bg-amber-50 px-1.5 py-0.5 text-[10px] font-semibold text-amber-700 ring-1 ring-inset ring-amber-100", children: ["\u00D7", issue.occurrence_count] })), when && (_jsx("span", { className: "ml-auto text-[10px] tabular-nums text-slate-400", children: when }))] })] }));
|
|
15
18
|
}
|
|
@@ -35,9 +35,9 @@ export interface IssueTypeDef {
|
|
|
35
35
|
title: string;
|
|
36
36
|
summary: string;
|
|
37
37
|
};
|
|
38
|
-
resolveRecipients(issue: IssueRecord, ctx: IssueActionCtx): Promise<RecipientResult>;
|
|
39
|
-
actions
|
|
40
|
-
buildResolutionNotice(issue: IssueRecord, actionKey: string, result: IssueActionResult): NotifyPayload;
|
|
38
|
+
resolveRecipients?(issue: IssueRecord, ctx: IssueActionCtx): Promise<RecipientResult>;
|
|
39
|
+
actions?: IssueActionDef[];
|
|
40
|
+
buildResolutionNotice?(issue: IssueRecord, actionKey: string, result: IssueActionResult): NotifyPayload;
|
|
41
41
|
}
|
|
42
42
|
export declare function registerIssueType(def: IssueTypeDef): void;
|
|
43
43
|
export declare function getIssueType(typeKey: string): IssueTypeDef | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/issues/registry.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,YAAY,EAAE,WAAW,EAAE,CAAC;AAE5B,MAAM,WAAW,cAAc;IAC7B,sEAAsE;IACtE,cAAc,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,kBAAkB,EAAE,MAAM,CAAC;IAC3B,GAAG,CACD,KAAK,EAAE,WAAW,EAClB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC,iBAAiB,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACtF,iBAAiB,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/issues/registry.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,YAAY,EAAE,WAAW,EAAE,CAAC;AAE5B,MAAM,WAAW,cAAc;IAC7B,sEAAsE;IACtE,cAAc,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;IACzC,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,cAAc;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,kBAAkB,EAAE,MAAM,CAAC;IAC3B,GAAG,CACD,KAAK,EAAE,WAAW,EAClB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC,iBAAiB,CAAC,CAAC;CAC/B;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACtF,iBAAiB,CAAC,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC;IACtF,OAAO,CAAC,EAAE,cAAc,EAAE,CAAC;IAC3B,qBAAqB,CAAC,CACpB,KAAK,EAAE,WAAW,EAClB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,iBAAiB,GACxB,aAAa,CAAC;CAClB;AAID,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAEzD;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAEtE;AAED,wBAAgB,cAAc,IAAI,YAAY,EAAE,CAE/C"}
|
package/dist/issues/store.d.ts
CHANGED
|
@@ -3,10 +3,12 @@ export interface IssueRecord {
|
|
|
3
3
|
id: string;
|
|
4
4
|
scope_id: string;
|
|
5
5
|
type: string;
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
severity: 'low' | 'medium' | 'high' | 'critical';
|
|
7
|
+
status: 'new' | 'wip' | 'on_hold' | 'closed' | 'archived';
|
|
8
|
+
subject_user_id: string | null;
|
|
8
9
|
assigned_to: string | null;
|
|
9
10
|
recipient_scope_id: string | null;
|
|
11
|
+
source: string | null;
|
|
10
12
|
title: string;
|
|
11
13
|
summary: string;
|
|
12
14
|
payload: Record<string, unknown>;
|
|
@@ -25,12 +27,14 @@ export interface IssueRecord {
|
|
|
25
27
|
export interface CreateIssueInput {
|
|
26
28
|
scope_id: string;
|
|
27
29
|
type: string;
|
|
28
|
-
|
|
30
|
+
severity?: 'low' | 'medium' | 'high' | 'critical';
|
|
31
|
+
subject_user_id?: string | null;
|
|
29
32
|
recipient_scope_id?: string;
|
|
33
|
+
source?: string;
|
|
30
34
|
title: string;
|
|
31
35
|
summary: string;
|
|
32
36
|
payload: Record<string, unknown>;
|
|
33
|
-
dedupe_key
|
|
37
|
+
dedupe_key?: string;
|
|
34
38
|
}
|
|
35
39
|
export interface IssueStore {
|
|
36
40
|
createOrBumpIssue(input: CreateIssueInput): Promise<{
|
|
@@ -47,8 +51,9 @@ export interface IssueStore {
|
|
|
47
51
|
offset?: number;
|
|
48
52
|
}): Promise<IssueRecord[]>;
|
|
49
53
|
getIssue(id: string): Promise<IssueRecord | null>;
|
|
50
|
-
transitionStatus(id: string, status: '
|
|
54
|
+
transitionStatus(id: string, status: IssueRecord['status'], actorUserId: string): Promise<IssueRecord>;
|
|
51
55
|
setAssignee(id: string, userId: string | null): Promise<IssueRecord>;
|
|
56
|
+
setType(id: string, type: string): Promise<IssueRecord>;
|
|
52
57
|
resolveIssue(id: string, opts: {
|
|
53
58
|
resolution: 'granted' | 'denied';
|
|
54
59
|
role_id?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/issues/store.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AAOrB,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,KAAK,GAAG,KAAK,GAAG,QAAQ,GAAG,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/issues/store.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AAOrB,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,CAAC;IACjD,MAAM,EAAE,KAAK,GAAG,KAAK,GAAG,SAAS,GAAG,QAAQ,GAAG,UAAU,CAAC;IAC1D,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,SAAS,GAAG,QAAQ,GAAG,IAAI,CAAC;IACxC,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,CAAC;IAClD,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IACzB,iBAAiB,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,WAAW,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAC5F,UAAU,CAAC,IAAI,EAAE;QACf,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;QACzB,aAAa,CAAC,EAAE,OAAO,CAAC;QACxB,MAAM,CAAC,EAAE,WAAW,CAAC,QAAQ,CAAC,GAAG,WAAW,CAAC,QAAQ,CAAC,EAAE,CAAC;QACzD,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAC3B,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAClD,gBAAgB,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,CAAC,QAAQ,CAAC,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IACvG,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IACrE,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IACxD,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE;QAC7B,UAAU,EAAE,SAAS,GAAG,QAAQ,CAAC;QACjC,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,WAAW,EAAE,MAAM,CAAC;KACrB,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IACzB,sBAAsB,CAAC,UAAU,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC3D;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,WAAW,CA4BvE;AAqBD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,GAAG,GAAG,UAAU,CAmRzD;AAED;;;;;;;;GAQG;AACH,wBAAsB,2BAA2B,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO,CAAC,UAAU,CAAC,CAwOnF"}
|
package/dist/issues/store.js
CHANGED
|
@@ -7,10 +7,12 @@ export function toIssueRecord(raw) {
|
|
|
7
7
|
id: raw.id,
|
|
8
8
|
scope_id: raw.scope_id,
|
|
9
9
|
type: raw.type,
|
|
10
|
+
severity: raw.severity ?? 'medium',
|
|
10
11
|
status: raw.status,
|
|
11
|
-
subject_user_id: raw.subject_user_id,
|
|
12
|
+
subject_user_id: raw.subject_user_id ?? null,
|
|
12
13
|
assigned_to: raw.assigned_to ?? null,
|
|
13
14
|
recipient_scope_id: raw.recipient_scope_id ?? null,
|
|
15
|
+
source: raw.source ?? null,
|
|
14
16
|
title: raw.title,
|
|
15
17
|
summary: raw.summary,
|
|
16
18
|
payload: typeof raw.payload === 'string'
|
|
@@ -30,21 +32,17 @@ export function toIssueRecord(raw) {
|
|
|
30
32
|
};
|
|
31
33
|
}
|
|
32
34
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
35
|
+
* Every known status may transition to any other known status. Admins move
|
|
36
|
+
* cards freely across the board — including reopening a closed issue (closed →
|
|
37
|
+
* new/wip) and parking one (→ on_hold). Only an unknown target status is
|
|
38
|
+
* rejected. `archived` is normally reached via archiveClosedOlderThan, but a
|
|
39
|
+
* manual move is permitted too.
|
|
40
|
+
*
|
|
41
|
+
* Side effect: a → wip that lands on an unassigned issue auto-assigns the actor.
|
|
38
42
|
*/
|
|
39
|
-
const
|
|
40
|
-
new: ['wip', 'closed'],
|
|
41
|
-
wip: ['closed'],
|
|
42
|
-
closed: [],
|
|
43
|
-
archived: [],
|
|
44
|
-
};
|
|
43
|
+
const KNOWN_STATUSES = new Set(['new', 'wip', 'on_hold', 'closed', 'archived']);
|
|
45
44
|
function assertTransition(from, to) {
|
|
46
|
-
|
|
47
|
-
if (!allowed.includes(to)) {
|
|
45
|
+
if (!KNOWN_STATUSES.has(to)) {
|
|
48
46
|
throw new Error(`hazo_admin:issues — illegal status transition: ${from} → ${to}`);
|
|
49
47
|
}
|
|
50
48
|
}
|
|
@@ -56,9 +54,13 @@ export function createIssueStore(adapter) {
|
|
|
56
54
|
return adapter.raw(sql, params);
|
|
57
55
|
}
|
|
58
56
|
async function createOrBumpIssue(input) {
|
|
59
|
-
|
|
57
|
+
const dedupe_key = input.dedupe_key;
|
|
58
|
+
if (!dedupe_key) {
|
|
59
|
+
throw new Error('hazo_admin:issues — dedupe_key required at store layer');
|
|
60
|
+
}
|
|
61
|
+
log.debug('createOrBumpIssue', { dedupe_key });
|
|
60
62
|
// Check for an existing open issue with this dedupe_key.
|
|
61
|
-
const existing = await raw(`SELECT * FROM ${TABLE} WHERE dedupe_key = $1 AND status NOT IN ('closed', 'archived') LIMIT 1`, [
|
|
63
|
+
const existing = await raw(`SELECT * FROM ${TABLE} WHERE dedupe_key = $1 AND status NOT IN ('closed', 'archived') LIMIT 1`, [dedupe_key]);
|
|
62
64
|
if (existing && existing.length > 0) {
|
|
63
65
|
const ts = now();
|
|
64
66
|
const bumped = await raw(`UPDATE ${TABLE}
|
|
@@ -74,27 +76,29 @@ export function createIssueStore(adapter) {
|
|
|
74
76
|
const id = crypto.randomUUID();
|
|
75
77
|
const payloadStr = JSON.stringify(input.payload);
|
|
76
78
|
const inserted = await raw(`INSERT INTO ${TABLE} (
|
|
77
|
-
id, scope_id, type, status, subject_user_id, assigned_to,
|
|
78
|
-
recipient_scope_id, title, summary, payload, dedupe_key,
|
|
79
|
+
id, scope_id, type, severity, status, subject_user_id, assigned_to,
|
|
80
|
+
recipient_scope_id, source, title, summary, payload, dedupe_key,
|
|
79
81
|
occurrence_count, first_seen_at, last_seen_at,
|
|
80
82
|
resolution, resolution_role_id, resolution_reason,
|
|
81
83
|
resolved_by, resolved_at, created_at, updated_at
|
|
82
84
|
) VALUES (
|
|
83
|
-
$1, $2, $3, 'new', $
|
|
84
|
-
$
|
|
85
|
-
1, $
|
|
85
|
+
$1, $2, $3, $4, 'new', $5, NULL,
|
|
86
|
+
$6, $7, $8, $9, $10, $11,
|
|
87
|
+
1, $12, $13,
|
|
86
88
|
NULL, NULL, NULL,
|
|
87
|
-
NULL, NULL, $
|
|
89
|
+
NULL, NULL, $14, $15
|
|
88
90
|
) RETURNING *`, [
|
|
89
91
|
id,
|
|
90
92
|
input.scope_id,
|
|
91
93
|
input.type,
|
|
92
|
-
input.
|
|
94
|
+
input.severity ?? 'medium',
|
|
95
|
+
input.subject_user_id ?? null,
|
|
93
96
|
input.recipient_scope_id ?? null,
|
|
97
|
+
input.source ?? null,
|
|
94
98
|
input.title,
|
|
95
99
|
input.summary,
|
|
96
100
|
payloadStr,
|
|
97
|
-
|
|
101
|
+
dedupe_key,
|
|
98
102
|
ts,
|
|
99
103
|
ts,
|
|
100
104
|
ts,
|
|
@@ -179,6 +183,15 @@ export function createIssueStore(adapter) {
|
|
|
179
183
|
throw new Error(`hazo_admin:issues — issue ${id} not found`);
|
|
180
184
|
return toIssueRecord(row);
|
|
181
185
|
}
|
|
186
|
+
// Dedupe-safe: the partial unique index is on dedupe_key only; changing type never touches it.
|
|
187
|
+
async function setType(id, type) {
|
|
188
|
+
const ts = now();
|
|
189
|
+
const updated = await raw(`UPDATE ${TABLE} SET type = $1, updated_at = $2 WHERE id = $3 RETURNING *`, [type, ts, id]);
|
|
190
|
+
const row = updated && updated.length > 0 ? updated[0] : null;
|
|
191
|
+
if (!row)
|
|
192
|
+
throw new Error(`hazo_admin:issues — issue ${id} not found`);
|
|
193
|
+
return toIssueRecord(row);
|
|
194
|
+
}
|
|
182
195
|
async function resolveIssue(id, opts) {
|
|
183
196
|
const ts = now();
|
|
184
197
|
const updated = await raw(`UPDATE ${TABLE}
|
|
@@ -221,6 +234,7 @@ export function createIssueStore(adapter) {
|
|
|
221
234
|
getIssue,
|
|
222
235
|
transitionStatus,
|
|
223
236
|
setAssignee,
|
|
237
|
+
setType,
|
|
224
238
|
resolveIssue,
|
|
225
239
|
archiveClosedOlderThan,
|
|
226
240
|
};
|
|
@@ -248,12 +262,16 @@ export async function createIssueStoreFromConnect(adapter) {
|
|
|
248
262
|
return new Date().toISOString();
|
|
249
263
|
}
|
|
250
264
|
async function createOrBumpIssue(input) {
|
|
251
|
-
|
|
265
|
+
const dedupe_key = input.dedupe_key;
|
|
266
|
+
if (!dedupe_key) {
|
|
267
|
+
throw new Error('hazo_admin:issues — dedupe_key required at store layer');
|
|
268
|
+
}
|
|
269
|
+
log.debug('createIssueStoreFromConnect:createOrBumpIssue', { dedupe_key });
|
|
252
270
|
// Atomically bump an existing open issue via claimRows.
|
|
253
271
|
const bumped = await adapter.claimRows({
|
|
254
272
|
table: TABLE,
|
|
255
273
|
where: {
|
|
256
|
-
dedupe_key
|
|
274
|
+
dedupe_key,
|
|
257
275
|
status: { in: ['new', 'wip'] },
|
|
258
276
|
},
|
|
259
277
|
set: {
|
|
@@ -271,14 +289,16 @@ export async function createIssueStoreFromConnect(adapter) {
|
|
|
271
289
|
const inserted = await crud.insert({
|
|
272
290
|
scope_id: input.scope_id,
|
|
273
291
|
type: input.type,
|
|
292
|
+
severity: input.severity ?? 'medium',
|
|
274
293
|
status: 'new',
|
|
275
|
-
subject_user_id: input.subject_user_id,
|
|
294
|
+
subject_user_id: input.subject_user_id ?? null,
|
|
276
295
|
assigned_to: null,
|
|
277
296
|
recipient_scope_id: input.recipient_scope_id ?? null,
|
|
297
|
+
source: input.source ?? null,
|
|
278
298
|
title: input.title,
|
|
279
299
|
summary: input.summary,
|
|
280
300
|
payload: input.payload,
|
|
281
|
-
dedupe_key
|
|
301
|
+
dedupe_key,
|
|
282
302
|
occurrence_count: 1,
|
|
283
303
|
first_seen_at: ts,
|
|
284
304
|
last_seen_at: ts,
|
|
@@ -353,6 +373,15 @@ export async function createIssueStoreFromConnect(adapter) {
|
|
|
353
373
|
throw new Error(`hazo_admin:issues — issue ${id} not found`);
|
|
354
374
|
return toIssueRecord(row);
|
|
355
375
|
}
|
|
376
|
+
// Dedupe-safe: the partial unique index is on dedupe_key only; changing type never touches it.
|
|
377
|
+
async function setType(id, type) {
|
|
378
|
+
const ts = now();
|
|
379
|
+
const updated = await crud.updateById(id, { type, updated_at: ts });
|
|
380
|
+
const row = Array.isArray(updated) ? updated[0] : updated;
|
|
381
|
+
if (!row)
|
|
382
|
+
throw new Error(`hazo_admin:issues — issue ${id} not found`);
|
|
383
|
+
return toIssueRecord(row);
|
|
384
|
+
}
|
|
356
385
|
async function resolveIssue(id, opts) {
|
|
357
386
|
const ts = now();
|
|
358
387
|
const updated = await crud.updateById(id, {
|
|
@@ -395,6 +424,7 @@ export async function createIssueStoreFromConnect(adapter) {
|
|
|
395
424
|
getIssue,
|
|
396
425
|
transitionStatus,
|
|
397
426
|
setAssignee,
|
|
427
|
+
setType,
|
|
398
428
|
resolveIssue,
|
|
399
429
|
archiveClosedOlderThan,
|
|
400
430
|
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import 'server-only';
|
|
2
|
+
export interface IssueTypeCatalogRecord {
|
|
3
|
+
type_key: string;
|
|
4
|
+
label: string;
|
|
5
|
+
color: string | null;
|
|
6
|
+
description: string | null;
|
|
7
|
+
scope_id: string | null;
|
|
8
|
+
sort_order: number;
|
|
9
|
+
created_at: string;
|
|
10
|
+
updated_at: string;
|
|
11
|
+
}
|
|
12
|
+
export interface CreateIssueTypeInput {
|
|
13
|
+
type_key: string;
|
|
14
|
+
label: string;
|
|
15
|
+
color?: string | null;
|
|
16
|
+
description?: string | null;
|
|
17
|
+
scope_id?: string | null;
|
|
18
|
+
sort_order?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface UpdateIssueTypeInput {
|
|
21
|
+
label?: string;
|
|
22
|
+
color?: string | null;
|
|
23
|
+
description?: string | null;
|
|
24
|
+
sort_order?: number;
|
|
25
|
+
}
|
|
26
|
+
export interface IssueTypeCatalogStore {
|
|
27
|
+
listTypes(opts: {
|
|
28
|
+
scopeIds?: string[];
|
|
29
|
+
isGlobalAdmin?: boolean;
|
|
30
|
+
}): Promise<IssueTypeCatalogRecord[]>;
|
|
31
|
+
getType(type_key: string): Promise<IssueTypeCatalogRecord | null>;
|
|
32
|
+
createType(input: CreateIssueTypeInput): Promise<IssueTypeCatalogRecord>;
|
|
33
|
+
updateType(type_key: string, patch: UpdateIssueTypeInput): Promise<IssueTypeCatalogRecord>;
|
|
34
|
+
deleteType(type_key: string): Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
export declare function toIssueTypeCatalogRecord(raw: Record<string, unknown>): IssueTypeCatalogRecord;
|
|
37
|
+
export declare function createIssueTypeCatalogStore(adapter: any): IssueTypeCatalogStore;
|
|
38
|
+
/**
|
|
39
|
+
* PostgREST-native implementation of IssueTypeCatalogStore.
|
|
40
|
+
* Uses createCrudService so no raw SQL is issued.
|
|
41
|
+
* Falls back to createIssueTypeCatalogStore when the adapter exposes a raw()
|
|
42
|
+
* method (i.e. it is a direct-DB adapter that already handles SQL).
|
|
43
|
+
*
|
|
44
|
+
* Returns a Promise because it lazily imports hazo_connect/server on the
|
|
45
|
+
* CRUD path (avoiding loading the full hazo_connect stack for raw adapters).
|
|
46
|
+
*/
|
|
47
|
+
export declare function createIssueTypeCatalogStoreFromConnect(adapter: any): Promise<IssueTypeCatalogStore>;
|
|
48
|
+
//# sourceMappingURL=type_catalog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"type_catalog.d.ts","sourceRoot":"","sources":["../../src/issues/type_catalog.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AAOrB,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,qBAAqB;IACpC,SAAS,CAAC,IAAI,EAAE;QACd,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;QACpB,aAAa,CAAC,EAAE,OAAO,CAAC;KACzB,GAAG,OAAO,CAAC,sBAAsB,EAAE,CAAC,CAAC;IACtC,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC,CAAC;IAClE,UAAU,CAAC,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;IACzE,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,oBAAoB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAC3F,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7C;AAED,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,sBAAsB,CAW7F;AAwBD,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,GAAG,GAAG,qBAAqB,CAuG/E;AAED;;;;;;;;GAQG;AACH,wBAAsB,sCAAsC,CAC1D,OAAO,EAAE,GAAG,GACX,OAAO,CAAC,qBAAqB,CAAC,CAyFhC"}
|