@objectstack/plugin-approvals 16.0.0-rc.0 → 16.0.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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +220 -0
- package/dist/index.d.mts +44 -22
- package/dist/index.d.ts +44 -22
- package/dist/index.js +51 -21
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +51 -21
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -7
- package/src/approval-service.test.ts +27 -0
- package/src/approval-service.ts +22 -0
- package/src/sys-approval-approver.object.ts +1 -1
- package/src/sys-approval-request.object.test.ts +14 -5
- package/src/sys-approval-request.object.ts +26 -17
- package/src/sys-approval-token.object.ts +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -220,7 +220,7 @@ declare const SysApprovalRequest: Omit<{
|
|
|
220
220
|
pluralLabel?: string | undefined;
|
|
221
221
|
description?: string | undefined;
|
|
222
222
|
icon?: string | undefined;
|
|
223
|
-
managedBy?: "platform" | "system" | "config" | "append-only" | "better-auth" | undefined;
|
|
223
|
+
managedBy?: "platform" | "system" | "config" | "engine-owned" | "append-only" | "better-auth" | undefined;
|
|
224
224
|
ownership?: "none" | "org" | "user" | undefined;
|
|
225
225
|
userActions?: {
|
|
226
226
|
create?: boolean | undefined;
|
|
@@ -418,7 +418,7 @@ declare const SysApprovalRequest: Omit<{
|
|
|
418
418
|
}[] | undefined;
|
|
419
419
|
filter?: {
|
|
420
420
|
field: string;
|
|
421
|
-
operator:
|
|
421
|
+
operator: "in" | "contains" | "after" | "starts_with" | "ends_with" | "not_in" | "not_contains" | "between" | "is_null" | "is_not_null" | "is_empty" | "is_not_empty" | "equals" | "not_equals" | "greater_than" | "less_than" | "greater_than_or_equal" | "less_than_or_equal" | "before";
|
|
422
422
|
value?: string | number | boolean | (string | number)[] | null | undefined;
|
|
423
423
|
}[] | undefined;
|
|
424
424
|
description?: string | undefined;
|
|
@@ -577,7 +577,7 @@ declare const SysApprovalRequest: Omit<{
|
|
|
577
577
|
view?: string | undefined;
|
|
578
578
|
filter?: {
|
|
579
579
|
field: string;
|
|
580
|
-
operator:
|
|
580
|
+
operator: "in" | "contains" | "after" | "starts_with" | "ends_with" | "not_in" | "not_contains" | "between" | "is_null" | "is_not_null" | "is_empty" | "is_not_empty" | "equals" | "not_equals" | "greater_than" | "less_than" | "greater_than_or_equal" | "less_than_or_equal" | "before";
|
|
581
581
|
value?: string | number | boolean | (string | number)[] | null | undefined;
|
|
582
582
|
}[] | undefined;
|
|
583
583
|
order?: number | undefined;
|
|
@@ -833,7 +833,7 @@ declare const SysApprovalRequest: Omit<{
|
|
|
833
833
|
readonly pluralLabel: "Approval Requests";
|
|
834
834
|
readonly icon: "inbox";
|
|
835
835
|
readonly isSystem: true;
|
|
836
|
-
readonly managedBy: "
|
|
836
|
+
readonly managedBy: "engine-owned";
|
|
837
837
|
readonly description: "Live approval instance tracked per submission";
|
|
838
838
|
readonly displayNameField: "id";
|
|
839
839
|
readonly nameField: "id";
|
|
@@ -3349,8 +3349,14 @@ declare const SysApprovalRequest: Omit<{
|
|
|
3349
3349
|
readonly label: "Comment";
|
|
3350
3350
|
readonly type: "textarea";
|
|
3351
3351
|
readonly required: false;
|
|
3352
|
+
}, {
|
|
3353
|
+
readonly name: "attachments";
|
|
3354
|
+
readonly label: "Attachments";
|
|
3355
|
+
readonly type: "file";
|
|
3356
|
+
readonly multiple: true;
|
|
3357
|
+
readonly required: false;
|
|
3352
3358
|
}];
|
|
3353
|
-
readonly visible: "record.
|
|
3359
|
+
readonly visible: "record.viewer.can_act";
|
|
3354
3360
|
readonly locations: ["record_section", "list_item"];
|
|
3355
3361
|
readonly successMessage: "Approved.";
|
|
3356
3362
|
readonly refreshAfter: true;
|
|
@@ -3366,8 +3372,14 @@ declare const SysApprovalRequest: Omit<{
|
|
|
3366
3372
|
readonly label: "Comment";
|
|
3367
3373
|
readonly type: "textarea";
|
|
3368
3374
|
readonly required: false;
|
|
3375
|
+
}, {
|
|
3376
|
+
readonly name: "attachments";
|
|
3377
|
+
readonly label: "Attachments";
|
|
3378
|
+
readonly type: "file";
|
|
3379
|
+
readonly multiple: true;
|
|
3380
|
+
readonly required: false;
|
|
3369
3381
|
}];
|
|
3370
|
-
readonly visible: "record.
|
|
3382
|
+
readonly visible: "record.viewer.can_act";
|
|
3371
3383
|
readonly confirmText: "Reject this request? A rejection is final for every approver.";
|
|
3372
3384
|
readonly locations: ["record_section", "list_item"];
|
|
3373
3385
|
readonly successMessage: "Rejected.";
|
|
@@ -3391,7 +3403,7 @@ declare const SysApprovalRequest: Omit<{
|
|
|
3391
3403
|
readonly type: "textarea";
|
|
3392
3404
|
readonly required: false;
|
|
3393
3405
|
}];
|
|
3394
|
-
readonly visible: "record.
|
|
3406
|
+
readonly visible: "record.viewer.can_act";
|
|
3395
3407
|
readonly locations: ["record_section"];
|
|
3396
3408
|
readonly successMessage: "Reassigned.";
|
|
3397
3409
|
readonly refreshAfter: true;
|
|
@@ -3408,7 +3420,7 @@ declare const SysApprovalRequest: Omit<{
|
|
|
3408
3420
|
readonly type: "textarea";
|
|
3409
3421
|
readonly required: false;
|
|
3410
3422
|
}];
|
|
3411
|
-
readonly visible: "record.
|
|
3423
|
+
readonly visible: "record.viewer.can_act";
|
|
3412
3424
|
readonly locations: ["record_section"];
|
|
3413
3425
|
readonly successMessage: "Sent back for revision.";
|
|
3414
3426
|
readonly refreshAfter: true;
|
|
@@ -3425,7 +3437,7 @@ declare const SysApprovalRequest: Omit<{
|
|
|
3425
3437
|
readonly type: "textarea";
|
|
3426
3438
|
readonly required: true;
|
|
3427
3439
|
}];
|
|
3428
|
-
readonly visible: "record.
|
|
3440
|
+
readonly visible: "record.viewer.can_act";
|
|
3429
3441
|
readonly locations: ["record_section"];
|
|
3430
3442
|
readonly successMessage: "Information requested.";
|
|
3431
3443
|
readonly refreshAfter: true;
|
|
@@ -3442,7 +3454,7 @@ declare const SysApprovalRequest: Omit<{
|
|
|
3442
3454
|
readonly type: "textarea";
|
|
3443
3455
|
readonly required: false;
|
|
3444
3456
|
}];
|
|
3445
|
-
readonly visible: "record.status == \"pending\" && record.
|
|
3457
|
+
readonly visible: "record.status == \"pending\" && record.viewer.is_submitter";
|
|
3446
3458
|
readonly locations: ["record_section"];
|
|
3447
3459
|
readonly successMessage: "Reminder sent.";
|
|
3448
3460
|
readonly refreshAfter: true;
|
|
@@ -3459,7 +3471,7 @@ declare const SysApprovalRequest: Omit<{
|
|
|
3459
3471
|
readonly type: "textarea";
|
|
3460
3472
|
readonly required: false;
|
|
3461
3473
|
}];
|
|
3462
|
-
readonly visible: "(record.status == \"pending\" || record.status == \"returned\") && record.
|
|
3474
|
+
readonly visible: "(record.status == \"pending\" || record.status == \"returned\") && record.viewer.is_submitter";
|
|
3463
3475
|
readonly confirmText: "Recall this request? Approvers can no longer act on it and the record is unlocked.";
|
|
3464
3476
|
readonly locations: ["record_section"];
|
|
3465
3477
|
readonly successMessage: "Recalled.";
|
|
@@ -3477,7 +3489,7 @@ declare const SysApprovalRequest: Omit<{
|
|
|
3477
3489
|
readonly type: "textarea";
|
|
3478
3490
|
readonly required: false;
|
|
3479
3491
|
}];
|
|
3480
|
-
readonly visible: "record.status == \"returned\" && record.
|
|
3492
|
+
readonly visible: "record.status == \"returned\" && record.viewer.is_submitter";
|
|
3481
3493
|
readonly locations: ["record_section"];
|
|
3482
3494
|
readonly successMessage: "Resubmitted.";
|
|
3483
3495
|
readonly refreshAfter: true;
|
|
@@ -3694,7 +3706,7 @@ declare const SysApprovalAction: Omit<{
|
|
|
3694
3706
|
pluralLabel?: string | undefined;
|
|
3695
3707
|
description?: string | undefined;
|
|
3696
3708
|
icon?: string | undefined;
|
|
3697
|
-
managedBy?: "platform" | "system" | "config" | "append-only" | "better-auth" | undefined;
|
|
3709
|
+
managedBy?: "platform" | "system" | "config" | "engine-owned" | "append-only" | "better-auth" | undefined;
|
|
3698
3710
|
ownership?: "none" | "org" | "user" | undefined;
|
|
3699
3711
|
userActions?: {
|
|
3700
3712
|
create?: boolean | undefined;
|
|
@@ -3892,7 +3904,7 @@ declare const SysApprovalAction: Omit<{
|
|
|
3892
3904
|
}[] | undefined;
|
|
3893
3905
|
filter?: {
|
|
3894
3906
|
field: string;
|
|
3895
|
-
operator:
|
|
3907
|
+
operator: "in" | "contains" | "after" | "starts_with" | "ends_with" | "not_in" | "not_contains" | "between" | "is_null" | "is_not_null" | "is_empty" | "is_not_empty" | "equals" | "not_equals" | "greater_than" | "less_than" | "greater_than_or_equal" | "less_than_or_equal" | "before";
|
|
3896
3908
|
value?: string | number | boolean | (string | number)[] | null | undefined;
|
|
3897
3909
|
}[] | undefined;
|
|
3898
3910
|
description?: string | undefined;
|
|
@@ -4051,7 +4063,7 @@ declare const SysApprovalAction: Omit<{
|
|
|
4051
4063
|
view?: string | undefined;
|
|
4052
4064
|
filter?: {
|
|
4053
4065
|
field: string;
|
|
4054
|
-
operator:
|
|
4066
|
+
operator: "in" | "contains" | "after" | "starts_with" | "ends_with" | "not_in" | "not_contains" | "between" | "is_null" | "is_not_null" | "is_empty" | "is_not_empty" | "equals" | "not_equals" | "greater_than" | "less_than" | "greater_than_or_equal" | "less_than_or_equal" | "before";
|
|
4055
4067
|
value?: string | number | boolean | (string | number)[] | null | undefined;
|
|
4056
4068
|
}[] | undefined;
|
|
4057
4069
|
order?: number | undefined;
|
|
@@ -5940,7 +5952,7 @@ declare const SysApprovalApprover: Omit<{
|
|
|
5940
5952
|
pluralLabel?: string | undefined;
|
|
5941
5953
|
description?: string | undefined;
|
|
5942
5954
|
icon?: string | undefined;
|
|
5943
|
-
managedBy?: "platform" | "system" | "config" | "append-only" | "better-auth" | undefined;
|
|
5955
|
+
managedBy?: "platform" | "system" | "config" | "engine-owned" | "append-only" | "better-auth" | undefined;
|
|
5944
5956
|
ownership?: "none" | "org" | "user" | undefined;
|
|
5945
5957
|
userActions?: {
|
|
5946
5958
|
create?: boolean | undefined;
|
|
@@ -6138,7 +6150,7 @@ declare const SysApprovalApprover: Omit<{
|
|
|
6138
6150
|
}[] | undefined;
|
|
6139
6151
|
filter?: {
|
|
6140
6152
|
field: string;
|
|
6141
|
-
operator:
|
|
6153
|
+
operator: "in" | "contains" | "after" | "starts_with" | "ends_with" | "not_in" | "not_contains" | "between" | "is_null" | "is_not_null" | "is_empty" | "is_not_empty" | "equals" | "not_equals" | "greater_than" | "less_than" | "greater_than_or_equal" | "less_than_or_equal" | "before";
|
|
6142
6154
|
value?: string | number | boolean | (string | number)[] | null | undefined;
|
|
6143
6155
|
}[] | undefined;
|
|
6144
6156
|
description?: string | undefined;
|
|
@@ -6297,7 +6309,7 @@ declare const SysApprovalApprover: Omit<{
|
|
|
6297
6309
|
view?: string | undefined;
|
|
6298
6310
|
filter?: {
|
|
6299
6311
|
field: string;
|
|
6300
|
-
operator:
|
|
6312
|
+
operator: "in" | "contains" | "after" | "starts_with" | "ends_with" | "not_in" | "not_contains" | "between" | "is_null" | "is_not_null" | "is_empty" | "is_not_empty" | "equals" | "not_equals" | "greater_than" | "less_than" | "greater_than_or_equal" | "less_than_or_equal" | "before";
|
|
6301
6313
|
value?: string | number | boolean | (string | number)[] | null | undefined;
|
|
6302
6314
|
}[] | undefined;
|
|
6303
6315
|
order?: number | undefined;
|
|
@@ -6553,7 +6565,7 @@ declare const SysApprovalApprover: Omit<{
|
|
|
6553
6565
|
readonly pluralLabel: "Approval Approvers";
|
|
6554
6566
|
readonly icon: "users";
|
|
6555
6567
|
readonly isSystem: true;
|
|
6556
|
-
readonly managedBy: "
|
|
6568
|
+
readonly managedBy: "engine-owned";
|
|
6557
6569
|
readonly description: "Normalized pending-approver rows for indexed inbox queries";
|
|
6558
6570
|
readonly displayNameField: "id";
|
|
6559
6571
|
readonly nameField: "id";
|
|
@@ -7457,7 +7469,7 @@ declare const SysApprovalDelegation: Omit<{
|
|
|
7457
7469
|
pluralLabel?: string | undefined;
|
|
7458
7470
|
description?: string | undefined;
|
|
7459
7471
|
icon?: string | undefined;
|
|
7460
|
-
managedBy?: "platform" | "system" | "config" | "append-only" | "better-auth" | undefined;
|
|
7472
|
+
managedBy?: "platform" | "system" | "config" | "engine-owned" | "append-only" | "better-auth" | undefined;
|
|
7461
7473
|
ownership?: "none" | "org" | "user" | undefined;
|
|
7462
7474
|
userActions?: {
|
|
7463
7475
|
create?: boolean | undefined;
|
|
@@ -7655,7 +7667,7 @@ declare const SysApprovalDelegation: Omit<{
|
|
|
7655
7667
|
}[] | undefined;
|
|
7656
7668
|
filter?: {
|
|
7657
7669
|
field: string;
|
|
7658
|
-
operator:
|
|
7670
|
+
operator: "in" | "contains" | "after" | "starts_with" | "ends_with" | "not_in" | "not_contains" | "between" | "is_null" | "is_not_null" | "is_empty" | "is_not_empty" | "equals" | "not_equals" | "greater_than" | "less_than" | "greater_than_or_equal" | "less_than_or_equal" | "before";
|
|
7659
7671
|
value?: string | number | boolean | (string | number)[] | null | undefined;
|
|
7660
7672
|
}[] | undefined;
|
|
7661
7673
|
description?: string | undefined;
|
|
@@ -7814,7 +7826,7 @@ declare const SysApprovalDelegation: Omit<{
|
|
|
7814
7826
|
view?: string | undefined;
|
|
7815
7827
|
filter?: {
|
|
7816
7828
|
field: string;
|
|
7817
|
-
operator:
|
|
7829
|
+
operator: "in" | "contains" | "after" | "starts_with" | "ends_with" | "not_in" | "not_contains" | "between" | "is_null" | "is_not_null" | "is_empty" | "is_not_empty" | "equals" | "not_equals" | "greater_than" | "less_than" | "greater_than_or_equal" | "less_than_or_equal" | "before";
|
|
7818
7830
|
value?: string | number | boolean | (string | number)[] | null | undefined;
|
|
7819
7831
|
}[] | undefined;
|
|
7820
7832
|
order?: number | undefined;
|
|
@@ -9780,6 +9792,16 @@ declare class ApprovalService implements IApprovalService {
|
|
|
9780
9792
|
* Display-only and best-effort — errors leave the row untouched.
|
|
9781
9793
|
*/
|
|
9782
9794
|
private attachDecisionProgress;
|
|
9795
|
+
/**
|
|
9796
|
+
* Attach the per-viewer capability block (#3310) from the caller's context.
|
|
9797
|
+
* `can_act` mirrors the exact authorization the decision methods enforce — the
|
|
9798
|
+
* caller's user id is in the resolved `pending_approvers` while the request is
|
|
9799
|
+
* still `pending` (position/team/manager approvers are already resolved to
|
|
9800
|
+
* concrete user ids at open time, so a plain membership test is faithful).
|
|
9801
|
+
* `is_submitter` is a straight owner check. System/tokenless contexts get a
|
|
9802
|
+
* both-false block. Cheap + synchronous — safe on list reads.
|
|
9803
|
+
*/
|
|
9804
|
+
private attachViewers;
|
|
9783
9805
|
/**
|
|
9784
9806
|
* Derive approval-step progress from the owning flow's graph (single-read
|
|
9785
9807
|
* enrichment only — list reads skip it). Walks from the start node
|
package/dist/index.js
CHANGED
|
@@ -880,7 +880,7 @@ var SysApprovalRequest = import_data.ObjectSchema.create({
|
|
|
880
880
|
pluralLabel: "Approval Requests",
|
|
881
881
|
icon: "inbox",
|
|
882
882
|
isSystem: true,
|
|
883
|
-
managedBy: "
|
|
883
|
+
managedBy: "engine-owned",
|
|
884
884
|
description: "Live approval instance tracked per submission",
|
|
885
885
|
displayNameField: "id",
|
|
886
886
|
nameField: "id",
|
|
@@ -1068,8 +1068,12 @@ var SysApprovalRequest = import_data.ObjectSchema.create({
|
|
|
1068
1068
|
// (and their params) ship as metadata, not as hand-written buttons. Each
|
|
1069
1069
|
// targets the existing approvals REST route; `{id}` resolves from the row
|
|
1070
1070
|
// and `actorId` defaults to the caller server-side. The service remains the
|
|
1071
|
-
// authority on who may act
|
|
1072
|
-
//
|
|
1071
|
+
// authority on who may act; `visible` gates on the server-computed
|
|
1072
|
+
// per-viewer block (#3310): approver actions on `record.viewer.can_act`
|
|
1073
|
+
// (the caller is a current pending approver — same check the service
|
|
1074
|
+
// authorizes a decision with, so position/team approvers resolve correctly),
|
|
1075
|
+
// submitter actions on `record.viewer.is_submitter`. `viewer` is attached by
|
|
1076
|
+
// getRequest/listRequests; where it is absent the predicate fails closed.
|
|
1073
1077
|
actions: [
|
|
1074
1078
|
{
|
|
1075
1079
|
name: "approval_approve",
|
|
@@ -1079,9 +1083,13 @@ var SysApprovalRequest = import_data.ObjectSchema.create({
|
|
|
1079
1083
|
method: "POST",
|
|
1080
1084
|
target: "/api/v1/approvals/requests/{id}/approve",
|
|
1081
1085
|
params: [
|
|
1082
|
-
{ name: "comment", label: "Comment", type: "textarea", required: false }
|
|
1086
|
+
{ name: "comment", label: "Comment", type: "textarea", required: false },
|
|
1087
|
+
// Decision attachments (#3266). The console renders `type:'file'` params
|
|
1088
|
+
// through the shared upload widget and POSTs the resolved `attachments:
|
|
1089
|
+
// string[]`; the decision route persists them on `sys_approval_action`.
|
|
1090
|
+
{ name: "attachments", label: "Attachments", type: "file", multiple: true, required: false }
|
|
1083
1091
|
],
|
|
1084
|
-
visible:
|
|
1092
|
+
visible: "record.viewer.can_act",
|
|
1085
1093
|
locations: ["record_section", "list_item"],
|
|
1086
1094
|
successMessage: "Approved.",
|
|
1087
1095
|
refreshAfter: true
|
|
@@ -1094,9 +1102,10 @@ var SysApprovalRequest = import_data.ObjectSchema.create({
|
|
|
1094
1102
|
method: "POST",
|
|
1095
1103
|
target: "/api/v1/approvals/requests/{id}/reject",
|
|
1096
1104
|
params: [
|
|
1097
|
-
{ name: "comment", label: "Comment", type: "textarea", required: false }
|
|
1105
|
+
{ name: "comment", label: "Comment", type: "textarea", required: false },
|
|
1106
|
+
{ name: "attachments", label: "Attachments", type: "file", multiple: true, required: false }
|
|
1098
1107
|
],
|
|
1099
|
-
visible:
|
|
1108
|
+
visible: "record.viewer.can_act",
|
|
1100
1109
|
confirmText: "Reject this request? A rejection is final for every approver.",
|
|
1101
1110
|
locations: ["record_section", "list_item"],
|
|
1102
1111
|
successMessage: "Rejected.",
|
|
@@ -1118,15 +1127,15 @@ var SysApprovalRequest = import_data.ObjectSchema.create({
|
|
|
1118
1127
|
{ field: "submitter_id", name: "to", label: "New approver", required: true, helpText: "User to hand this step to" },
|
|
1119
1128
|
{ name: "comment", label: "Comment", type: "textarea", required: false }
|
|
1120
1129
|
],
|
|
1121
|
-
visible:
|
|
1130
|
+
visible: "record.viewer.can_act",
|
|
1122
1131
|
locations: ["record_section"],
|
|
1123
1132
|
successMessage: "Reassigned.",
|
|
1124
1133
|
refreshAfter: true
|
|
1125
1134
|
},
|
|
1126
1135
|
// ── Approver secondary decisions ────────────────────────────────
|
|
1127
1136
|
// Send back for revision / request more info (ADR-0044). Both are approver
|
|
1128
|
-
// actions
|
|
1129
|
-
//
|
|
1137
|
+
// actions, so `visible` gates on `record.viewer.can_act` (a current pending
|
|
1138
|
+
// approver) — same as approve/reject. The service stays the authority.
|
|
1130
1139
|
{
|
|
1131
1140
|
name: "approval_send_back",
|
|
1132
1141
|
label: "Send back",
|
|
@@ -1137,7 +1146,7 @@ var SysApprovalRequest = import_data.ObjectSchema.create({
|
|
|
1137
1146
|
params: [
|
|
1138
1147
|
{ name: "comment", label: "Reason", type: "textarea", required: false }
|
|
1139
1148
|
],
|
|
1140
|
-
visible:
|
|
1149
|
+
visible: "record.viewer.can_act",
|
|
1141
1150
|
locations: ["record_section"],
|
|
1142
1151
|
successMessage: "Sent back for revision.",
|
|
1143
1152
|
refreshAfter: true
|
|
@@ -1152,17 +1161,17 @@ var SysApprovalRequest = import_data.ObjectSchema.create({
|
|
|
1152
1161
|
params: [
|
|
1153
1162
|
{ name: "comment", label: "What do you need?", type: "textarea", required: true }
|
|
1154
1163
|
],
|
|
1155
|
-
visible:
|
|
1164
|
+
visible: "record.viewer.can_act",
|
|
1156
1165
|
locations: ["record_section"],
|
|
1157
1166
|
successMessage: "Information requested.",
|
|
1158
1167
|
refreshAfter: true
|
|
1159
1168
|
},
|
|
1160
1169
|
// ── Submitter continuity actions ────────────────────────────────
|
|
1161
1170
|
// Remind / recall (pending) and resubmit / recall (returned). These are the
|
|
1162
|
-
// submitter's own levers, so `visible` gates on `
|
|
1163
|
-
//
|
|
1164
|
-
//
|
|
1165
|
-
//
|
|
1171
|
+
// submitter's own levers, so `visible` gates on `record.viewer.is_submitter`
|
|
1172
|
+
// (server-computed on the current viewer). The service re-checks ownership;
|
|
1173
|
+
// the predicate keeps a non-submitter from ever seeing a button they cannot
|
|
1174
|
+
// use.
|
|
1166
1175
|
{
|
|
1167
1176
|
name: "approval_remind",
|
|
1168
1177
|
label: "Send reminder",
|
|
@@ -1173,7 +1182,7 @@ var SysApprovalRequest = import_data.ObjectSchema.create({
|
|
|
1173
1182
|
params: [
|
|
1174
1183
|
{ name: "comment", label: "Note", type: "textarea", required: false }
|
|
1175
1184
|
],
|
|
1176
|
-
visible: 'record.status == "pending" && record.
|
|
1185
|
+
visible: 'record.status == "pending" && record.viewer.is_submitter',
|
|
1177
1186
|
locations: ["record_section"],
|
|
1178
1187
|
successMessage: "Reminder sent.",
|
|
1179
1188
|
refreshAfter: true
|
|
@@ -1190,7 +1199,7 @@ var SysApprovalRequest = import_data.ObjectSchema.create({
|
|
|
1190
1199
|
],
|
|
1191
1200
|
// Recall applies while the request is live for the submitter — pending
|
|
1192
1201
|
// (withdraw) or returned (abandon the revision instead of resubmitting).
|
|
1193
|
-
visible: '(record.status == "pending" || record.status == "returned") && record.
|
|
1202
|
+
visible: '(record.status == "pending" || record.status == "returned") && record.viewer.is_submitter',
|
|
1194
1203
|
confirmText: "Recall this request? Approvers can no longer act on it and the record is unlocked.",
|
|
1195
1204
|
locations: ["record_section"],
|
|
1196
1205
|
successMessage: "Recalled.",
|
|
@@ -1206,7 +1215,7 @@ var SysApprovalRequest = import_data.ObjectSchema.create({
|
|
|
1206
1215
|
params: [
|
|
1207
1216
|
{ name: "comment", label: "What changed?", type: "textarea", required: false }
|
|
1208
1217
|
],
|
|
1209
|
-
visible: 'record.status == "returned" && record.
|
|
1218
|
+
visible: 'record.status == "returned" && record.viewer.is_submitter',
|
|
1210
1219
|
locations: ["record_section"],
|
|
1211
1220
|
successMessage: "Resubmitted.",
|
|
1212
1221
|
refreshAfter: true
|
|
@@ -1344,7 +1353,7 @@ var SysApprovalApprover = import_data3.ObjectSchema.create({
|
|
|
1344
1353
|
pluralLabel: "Approval Approvers",
|
|
1345
1354
|
icon: "users",
|
|
1346
1355
|
isSystem: true,
|
|
1347
|
-
managedBy: "
|
|
1356
|
+
managedBy: "engine-owned",
|
|
1348
1357
|
description: "Normalized pending-approver rows for indexed inbox queries",
|
|
1349
1358
|
displayNameField: "id",
|
|
1350
1359
|
nameField: "id",
|
|
@@ -3350,6 +3359,7 @@ var _ApprovalService = class _ApprovalService {
|
|
|
3350
3359
|
const rows = await this.engine.find("sys_approval_request", findOpts);
|
|
3351
3360
|
const list = Array.isArray(rows) ? rows.map(rowFromRequest) : [];
|
|
3352
3361
|
await this.enrichRows(list);
|
|
3362
|
+
this.attachViewers(list, context);
|
|
3353
3363
|
return list;
|
|
3354
3364
|
}
|
|
3355
3365
|
async countRequests(filter, context) {
|
|
@@ -3391,6 +3401,7 @@ var _ApprovalService = class _ApprovalService {
|
|
|
3391
3401
|
await this.enrichRows([row]);
|
|
3392
3402
|
await this.attachFlowSteps(row);
|
|
3393
3403
|
await this.attachDecisionProgress(row, rows[0]);
|
|
3404
|
+
this.attachViewers([row], context);
|
|
3394
3405
|
return row;
|
|
3395
3406
|
}
|
|
3396
3407
|
/**
|
|
@@ -3438,6 +3449,25 @@ var _ApprovalService = class _ApprovalService {
|
|
|
3438
3449
|
} catch {
|
|
3439
3450
|
}
|
|
3440
3451
|
}
|
|
3452
|
+
/**
|
|
3453
|
+
* Attach the per-viewer capability block (#3310) from the caller's context.
|
|
3454
|
+
* `can_act` mirrors the exact authorization the decision methods enforce — the
|
|
3455
|
+
* caller's user id is in the resolved `pending_approvers` while the request is
|
|
3456
|
+
* still `pending` (position/team/manager approvers are already resolved to
|
|
3457
|
+
* concrete user ids at open time, so a plain membership test is faithful).
|
|
3458
|
+
* `is_submitter` is a straight owner check. System/tokenless contexts get a
|
|
3459
|
+
* both-false block. Cheap + synchronous — safe on list reads.
|
|
3460
|
+
*/
|
|
3461
|
+
attachViewers(rows, context) {
|
|
3462
|
+
const uid2 = context?.userId != null ? String(context.userId) : null;
|
|
3463
|
+
for (const row of rows) {
|
|
3464
|
+
const pending = row.pending_approvers ?? [];
|
|
3465
|
+
row.viewer = {
|
|
3466
|
+
can_act: row.status === "pending" && !!uid2 && pending.includes(uid2),
|
|
3467
|
+
is_submitter: !!uid2 && row.submitter_id != null && String(row.submitter_id) === uid2
|
|
3468
|
+
};
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3441
3471
|
/**
|
|
3442
3472
|
* Derive approval-step progress from the owning flow's graph (single-read
|
|
3443
3473
|
* enrichment only — list reads skip it). Walks from the start node
|
|
@@ -3506,7 +3536,7 @@ var SysApprovalToken = import_data5.ObjectSchema.create({
|
|
|
3506
3536
|
pluralLabel: "Approval Action Tokens",
|
|
3507
3537
|
icon: "key",
|
|
3508
3538
|
isSystem: true,
|
|
3509
|
-
managedBy: "
|
|
3539
|
+
managedBy: "engine-owned",
|
|
3510
3540
|
description: "Single-use tokens behind actionable approval links",
|
|
3511
3541
|
displayNameField: "id",
|
|
3512
3542
|
nameField: "id",
|