checkbox-cli 3.0.1 → 3.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.
- package/dist/checkbox.js +684 -88
- package/package.json +1 -1
package/dist/checkbox.js
CHANGED
|
@@ -16,7 +16,7 @@ import { join } from 'path';
|
|
|
16
16
|
// ── Config ──────────────────────────────────────────────────────────
|
|
17
17
|
const CONFIG_DIR = join(homedir(), '.checkbox');
|
|
18
18
|
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
19
|
-
const VERSION = '3.0
|
|
19
|
+
const VERSION = '3.2.0';
|
|
20
20
|
function loadConfig() {
|
|
21
21
|
try {
|
|
22
22
|
if (!existsSync(CONFIG_FILE))
|
|
@@ -194,6 +194,37 @@ async function pickEntity(post, orgId, entityType, label, filters) {
|
|
|
194
194
|
return null;
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
|
+
async function pickMember(post, orgId, label = 'team members') {
|
|
198
|
+
const s = p.spinner();
|
|
199
|
+
s.start(`Loading ${label}...`);
|
|
200
|
+
try {
|
|
201
|
+
const members = await listEntities(post, orgId, 'member');
|
|
202
|
+
s.stop(`${members.length} ${label}`);
|
|
203
|
+
if (members.length === 0) {
|
|
204
|
+
p.log.info(`No ${label} found.`);
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
const selected = await p.select({
|
|
208
|
+
message: `Choose a person`,
|
|
209
|
+
options: [
|
|
210
|
+
...members.slice(0, 50).map((m) => ({
|
|
211
|
+
value: m.id,
|
|
212
|
+
label: m.name || m.metadata?.email || 'Unknown',
|
|
213
|
+
hint: m.metadata?.role || '',
|
|
214
|
+
})),
|
|
215
|
+
{ value: '__back', label: pc.dim('\u2190 Back') },
|
|
216
|
+
],
|
|
217
|
+
});
|
|
218
|
+
if (p.isCancel(selected) || selected === '__back')
|
|
219
|
+
return null;
|
|
220
|
+
return members.find((m) => m.id === selected) || null;
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
s.stop(pc.red('Failed'));
|
|
224
|
+
p.log.error(err.message || `Could not load ${label}`);
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
197
228
|
async function txt(message, required = false, placeholder) {
|
|
198
229
|
const val = await p.text({
|
|
199
230
|
message,
|
|
@@ -480,40 +511,156 @@ async function mTasks(cfg, post) {
|
|
|
480
511
|
if (!frequency)
|
|
481
512
|
return mTasks(cfg, post);
|
|
482
513
|
const payload = { name, frequency };
|
|
514
|
+
// Frequency configuration (day of week, day of month, etc.)
|
|
515
|
+
if (frequency === 'weekly') {
|
|
516
|
+
const dow = await sel('Which day of the week?', [
|
|
517
|
+
{ value: 'monday', label: 'Monday' }, { value: 'tuesday', label: 'Tuesday' },
|
|
518
|
+
{ value: 'wednesday', label: 'Wednesday' }, { value: 'thursday', label: 'Thursday' },
|
|
519
|
+
{ value: 'friday', label: 'Friday' }, { value: 'saturday', label: 'Saturday' },
|
|
520
|
+
{ value: 'sunday', label: 'Sunday' },
|
|
521
|
+
]);
|
|
522
|
+
if (dow)
|
|
523
|
+
payload.frequency_config = { day_of_week: dow };
|
|
524
|
+
}
|
|
525
|
+
else if (frequency === 'monthly') {
|
|
526
|
+
const dom = await txt('Which day of the month? (1-31)', false, '1');
|
|
527
|
+
if (dom)
|
|
528
|
+
payload.frequency_config = { day_of_month: parseInt(dom, 10) };
|
|
529
|
+
}
|
|
530
|
+
else if (frequency === 'yearly') {
|
|
531
|
+
const month = await txt('Month (1-12)', false, '1');
|
|
532
|
+
const day = await txt('Day (1-31)', false, '1');
|
|
533
|
+
if (month && day)
|
|
534
|
+
payload.frequency_config = { month: parseInt(month, 10), day: parseInt(day, 10) };
|
|
535
|
+
}
|
|
483
536
|
const addPlan = await yn('Add to a plan?');
|
|
484
537
|
if (addPlan) {
|
|
485
538
|
const plan = await pickEntity(post, cfg.orgId, 'plan', 'plans');
|
|
486
|
-
if (plan)
|
|
539
|
+
if (plan) {
|
|
487
540
|
payload.plan_id = plan.id;
|
|
541
|
+
// Optionally link to a specific requirement
|
|
542
|
+
const linkReq = await yn('Link to a specific requirement?');
|
|
543
|
+
if (linkReq) {
|
|
544
|
+
const req = await pickEntity(post, cfg.orgId, 'requirement', 'requirements');
|
|
545
|
+
if (req)
|
|
546
|
+
payload.requirement_id = req.id;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
488
549
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
]);
|
|
505
|
-
if (reminder && reminder !== 'none') {
|
|
506
|
-
payload.reminder_mode = reminder;
|
|
507
|
-
const mins = await txt('Minutes before', false, '30');
|
|
508
|
-
if (mins)
|
|
509
|
-
payload.reminder_minutes = parseInt(mins, 10);
|
|
550
|
+
// Description
|
|
551
|
+
const desc = await txt('Description (blank to skip)');
|
|
552
|
+
if (desc)
|
|
553
|
+
payload.description = desc;
|
|
554
|
+
// Time
|
|
555
|
+
const time = await txt('Scheduled time (HH:MM, blank to skip)', false, '09:00');
|
|
556
|
+
if (time)
|
|
557
|
+
payload.time = time;
|
|
558
|
+
// Assignment
|
|
559
|
+
const assign = await yn('Assign to a specific person?');
|
|
560
|
+
if (assign) {
|
|
561
|
+
const member = await pickMember(post, cfg.orgId);
|
|
562
|
+
if (member) {
|
|
563
|
+
payload.assigned_to = member.id;
|
|
564
|
+
payload.assigned_to_name = member.name;
|
|
510
565
|
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
566
|
+
}
|
|
567
|
+
// Privacy
|
|
568
|
+
const priv = await yn('Private task?');
|
|
569
|
+
if (priv)
|
|
570
|
+
payload.is_private = true;
|
|
571
|
+
// Reminder
|
|
572
|
+
const reminder = await sel('Reminder', [
|
|
573
|
+
{ value: 'none', label: 'No reminder' },
|
|
574
|
+
{ value: 'notification', label: 'Push notification' },
|
|
575
|
+
{ value: 'email', label: 'Email' },
|
|
576
|
+
]);
|
|
577
|
+
if (reminder && reminder !== 'none') {
|
|
578
|
+
payload.reminder_mode = reminder;
|
|
579
|
+
const mins = await txt('Minutes before', false, '30');
|
|
580
|
+
if (mins)
|
|
581
|
+
payload.reminder_minutes = parseInt(mins, 10);
|
|
582
|
+
}
|
|
583
|
+
// Conditional question
|
|
584
|
+
const cond = await txt('Conditional question (blank to skip)', false, 'e.g. Is the equipment available?');
|
|
585
|
+
if (cond)
|
|
586
|
+
payload.conditional_question = cond;
|
|
587
|
+
// Approval requirement
|
|
588
|
+
const approval = await yn('Requires approval before completion?');
|
|
589
|
+
if (approval)
|
|
590
|
+
payload.approval_required = true;
|
|
591
|
+
// Proof / Validation requirements (THE MOST IMPORTANT FEATURE)
|
|
592
|
+
const addProof = await yn('Require proof to complete this task? (photos, documents, location, etc.)');
|
|
593
|
+
if (addProof) {
|
|
594
|
+
const validationTypes = [];
|
|
595
|
+
let addMoreProof = true;
|
|
596
|
+
while (addMoreProof) {
|
|
597
|
+
const proofType = await sel('What type of proof?', [
|
|
598
|
+
{ value: 'photo', label: 'Photo', hint: 'require a photo to be taken' },
|
|
599
|
+
{ value: 'document', label: 'Document', hint: 'require a document upload' },
|
|
600
|
+
{ value: 'geolocation', label: 'Location check-in', hint: 'require being at a specific location' },
|
|
601
|
+
{ value: 'checklist', label: 'Checklist', hint: 'require completing a checklist' },
|
|
602
|
+
{ value: 'table', label: 'Table record', hint: 'require filling out a data table' },
|
|
603
|
+
]);
|
|
604
|
+
if (!proofType)
|
|
605
|
+
break;
|
|
606
|
+
const vt = { type: proofType };
|
|
607
|
+
// Pick the specific asset for this proof type
|
|
608
|
+
if (proofType === 'photo') {
|
|
609
|
+
const ph = await pickEntity(post, cfg.orgId, 'photo', 'photo assets');
|
|
610
|
+
if (ph)
|
|
611
|
+
vt.config_id = ph.id;
|
|
612
|
+
}
|
|
613
|
+
else if (proofType === 'document') {
|
|
614
|
+
const doc = await pickEntity(post, cfg.orgId, 'document', 'document folders');
|
|
615
|
+
if (doc) {
|
|
616
|
+
vt.config_id = doc.id;
|
|
617
|
+
const targetMode = await sel('Where should uploads go?', [
|
|
618
|
+
{ value: 'locked', label: 'Always to this folder' },
|
|
619
|
+
{ value: 'user_choice', label: 'Let user choose folder' },
|
|
620
|
+
]);
|
|
621
|
+
if (targetMode) {
|
|
622
|
+
vt.target_mode = targetMode;
|
|
623
|
+
if (targetMode === 'locked')
|
|
624
|
+
vt.target_folder_id = doc.id;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
else if (proofType === 'geolocation') {
|
|
629
|
+
const area = await pickEntity(post, cfg.orgId, 'geolocation_area', 'location areas');
|
|
630
|
+
if (area)
|
|
631
|
+
vt.config_id = area.id;
|
|
632
|
+
}
|
|
633
|
+
else if (proofType === 'checklist') {
|
|
634
|
+
const cl = await pickEntity(post, cfg.orgId, 'checklist', 'checklists');
|
|
635
|
+
if (cl)
|
|
636
|
+
vt.config_id = cl.id;
|
|
637
|
+
}
|
|
638
|
+
else if (proofType === 'table') {
|
|
639
|
+
const tbl = await pickEntity(post, cfg.orgId, 'table', 'tables');
|
|
640
|
+
if (tbl)
|
|
641
|
+
vt.config_id = tbl.id;
|
|
642
|
+
}
|
|
643
|
+
validationTypes.push(vt);
|
|
644
|
+
addMoreProof = !!(await yn('Add another proof requirement?'));
|
|
645
|
+
}
|
|
646
|
+
if (validationTypes.length > 0)
|
|
647
|
+
payload.validation_types = validationTypes;
|
|
648
|
+
}
|
|
649
|
+
// Share with specific users
|
|
650
|
+
const share = await yn('Share with specific users?');
|
|
651
|
+
if (share) {
|
|
652
|
+
const shareIds = [];
|
|
653
|
+
let addMore = true;
|
|
654
|
+
while (addMore) {
|
|
655
|
+
const member = await pickMember(post, cfg.orgId);
|
|
656
|
+
if (member)
|
|
657
|
+
shareIds.push(member.id);
|
|
658
|
+
else
|
|
659
|
+
break;
|
|
660
|
+
addMore = !!(await yn('Add another person?'));
|
|
661
|
+
}
|
|
662
|
+
if (shareIds.length > 0)
|
|
663
|
+
payload.shared_with_ids = shareIds;
|
|
517
664
|
}
|
|
518
665
|
await cmd(post, cfg.orgId, 'create_task', payload, 'Creating task...');
|
|
519
666
|
return mTasks(cfg, post);
|
|
@@ -522,24 +669,131 @@ async function mTasks(cfg, post) {
|
|
|
522
669
|
const t = await pickEntity(post, cfg.orgId, 'task', 'tasks');
|
|
523
670
|
if (!t)
|
|
524
671
|
return mTasks(cfg, post);
|
|
525
|
-
p.log.info(`Editing: ${pc.cyan(t.name)}
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
{ value: '
|
|
534
|
-
{ value: '
|
|
535
|
-
{ value: '
|
|
536
|
-
{ value: '
|
|
672
|
+
p.log.info(`Editing: ${pc.cyan(t.name)}`);
|
|
673
|
+
const field = await sel('What would you like to change?', [
|
|
674
|
+
{ value: 'name', label: 'Name' },
|
|
675
|
+
{ value: 'frequency', label: 'Frequency' },
|
|
676
|
+
{ value: 'description', label: 'Description' },
|
|
677
|
+
{ value: 'time', label: 'Scheduled time' },
|
|
678
|
+
{ value: 'assigned_to', label: 'Assignment' },
|
|
679
|
+
{ value: 'is_private', label: 'Privacy' },
|
|
680
|
+
{ value: 'reminder', label: 'Reminder settings' },
|
|
681
|
+
{ value: 'conditional_question', label: 'Conditional question' },
|
|
682
|
+
{ value: 'plan', label: 'Plan / requirement link' },
|
|
683
|
+
{ value: '__back', label: pc.dim('\u2190 Back') },
|
|
537
684
|
]);
|
|
538
|
-
if (
|
|
539
|
-
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
685
|
+
if (!field || field === '__back')
|
|
686
|
+
return mTasks(cfg, post);
|
|
687
|
+
const updates = {};
|
|
688
|
+
switch (field) {
|
|
689
|
+
case 'name': {
|
|
690
|
+
const n = await txt('New name', true);
|
|
691
|
+
if (n)
|
|
692
|
+
updates.name = n;
|
|
693
|
+
break;
|
|
694
|
+
}
|
|
695
|
+
case 'frequency': {
|
|
696
|
+
const f = await sel('New frequency', [
|
|
697
|
+
{ value: 'daily', label: 'Daily' }, { value: 'weekly', label: 'Weekly' },
|
|
698
|
+
{ value: 'monthly', label: 'Monthly' }, { value: 'yearly', label: 'Yearly' },
|
|
699
|
+
{ value: 'one-time', label: 'One time' },
|
|
700
|
+
]);
|
|
701
|
+
if (f) {
|
|
702
|
+
updates.frequency = f;
|
|
703
|
+
if (f === 'weekly') {
|
|
704
|
+
const dow = await sel('Which day?', [
|
|
705
|
+
{ value: 'monday', label: 'Monday' }, { value: 'tuesday', label: 'Tuesday' },
|
|
706
|
+
{ value: 'wednesday', label: 'Wednesday' }, { value: 'thursday', label: 'Thursday' },
|
|
707
|
+
{ value: 'friday', label: 'Friday' }, { value: 'saturday', label: 'Saturday' },
|
|
708
|
+
{ value: 'sunday', label: 'Sunday' },
|
|
709
|
+
]);
|
|
710
|
+
if (dow)
|
|
711
|
+
updates.frequency_config = { day_of_week: dow };
|
|
712
|
+
}
|
|
713
|
+
else if (f === 'monthly') {
|
|
714
|
+
const dom = await txt('Which day of the month? (1-31)', false, '1');
|
|
715
|
+
if (dom)
|
|
716
|
+
updates.frequency_config = { day_of_month: parseInt(dom, 10) };
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
break;
|
|
720
|
+
}
|
|
721
|
+
case 'description': {
|
|
722
|
+
const d = await txt('New description', true);
|
|
723
|
+
if (d)
|
|
724
|
+
updates.description = d;
|
|
725
|
+
break;
|
|
726
|
+
}
|
|
727
|
+
case 'time': {
|
|
728
|
+
const time = await txt('New scheduled time (HH:MM)', false, '09:00');
|
|
729
|
+
if (time)
|
|
730
|
+
updates.time = time;
|
|
731
|
+
break;
|
|
732
|
+
}
|
|
733
|
+
case 'assigned_to': {
|
|
734
|
+
const unassign = await yn('Assign to someone? (No to unassign)');
|
|
735
|
+
if (unassign) {
|
|
736
|
+
const member = await pickMember(post, cfg.orgId);
|
|
737
|
+
if (member) {
|
|
738
|
+
updates.assigned_to = member.id;
|
|
739
|
+
updates.assigned_to_name = member.name;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
else {
|
|
743
|
+
updates.assigned_to = null;
|
|
744
|
+
updates.assigned_to_name = null;
|
|
745
|
+
}
|
|
746
|
+
break;
|
|
747
|
+
}
|
|
748
|
+
case 'is_private': {
|
|
749
|
+
const priv = await yn('Make this task private?');
|
|
750
|
+
updates.is_private = !!priv;
|
|
751
|
+
break;
|
|
752
|
+
}
|
|
753
|
+
case 'reminder': {
|
|
754
|
+
const mode = await sel('Reminder type', [
|
|
755
|
+
{ value: 'none', label: 'No reminder' },
|
|
756
|
+
{ value: 'notification', label: 'Push notification' },
|
|
757
|
+
{ value: 'email', label: 'Email' },
|
|
758
|
+
]);
|
|
759
|
+
if (mode === 'none') {
|
|
760
|
+
updates.reminder_mode = null;
|
|
761
|
+
updates.reminder_minutes = null;
|
|
762
|
+
}
|
|
763
|
+
else if (mode) {
|
|
764
|
+
updates.reminder_mode = mode;
|
|
765
|
+
const mins = await txt('Minutes before', false, '30');
|
|
766
|
+
if (mins)
|
|
767
|
+
updates.reminder_minutes = parseInt(mins, 10);
|
|
768
|
+
}
|
|
769
|
+
break;
|
|
770
|
+
}
|
|
771
|
+
case 'conditional_question': {
|
|
772
|
+
const q = await txt('Conditional question (blank to remove)');
|
|
773
|
+
updates.conditional_question = q || null;
|
|
774
|
+
break;
|
|
775
|
+
}
|
|
776
|
+
case 'plan': {
|
|
777
|
+
const plan = await pickEntity(post, cfg.orgId, 'plan', 'plans');
|
|
778
|
+
if (plan) {
|
|
779
|
+
updates.plan_id = plan.id;
|
|
780
|
+
const linkReq = await yn('Link to a requirement?');
|
|
781
|
+
if (linkReq) {
|
|
782
|
+
const req = await pickEntity(post, cfg.orgId, 'requirement', 'requirements');
|
|
783
|
+
if (req)
|
|
784
|
+
updates.requirement_id = req.id;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
else {
|
|
788
|
+
const unlink = await yn('Remove from current plan?');
|
|
789
|
+
if (unlink) {
|
|
790
|
+
updates.plan_id = null;
|
|
791
|
+
updates.requirement_id = null;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
break;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
543
797
|
if (Object.keys(updates).length > 0) {
|
|
544
798
|
await cmd(post, cfg.orgId, 'update_task', { task_id: t.id, updates }, 'Updating...');
|
|
545
799
|
}
|
|
@@ -633,9 +887,9 @@ async function mTasks(cfg, post) {
|
|
|
633
887
|
const payload = { task_id: t.id };
|
|
634
888
|
const specify = await yn('Specify an approver?');
|
|
635
889
|
if (specify) {
|
|
636
|
-
const
|
|
637
|
-
if (
|
|
638
|
-
payload.approver_id =
|
|
890
|
+
const member = await pickMember(post, cfg.orgId);
|
|
891
|
+
if (member)
|
|
892
|
+
payload.approver_id = member.id;
|
|
639
893
|
}
|
|
640
894
|
await cmd(post, cfg.orgId, 'request_approval', payload, 'Requesting approval...');
|
|
641
895
|
return mTasks(cfg, post);
|
|
@@ -655,9 +909,9 @@ async function mTasks(cfg, post) {
|
|
|
655
909
|
const t = await pickEntity(post, cfg.orgId, 'task', 'tasks');
|
|
656
910
|
if (!t)
|
|
657
911
|
return mTasks(cfg, post);
|
|
658
|
-
const
|
|
659
|
-
if (
|
|
660
|
-
await cmd(post, cfg.orgId, 'send_nudge', { task_id: t.id, receiver_id:
|
|
912
|
+
const member = await pickMember(post, cfg.orgId);
|
|
913
|
+
if (member)
|
|
914
|
+
await cmd(post, cfg.orgId, 'send_nudge', { task_id: t.id, receiver_id: member.id }, `Sending reminder to ${member.name}...`);
|
|
661
915
|
return mTasks(cfg, post);
|
|
662
916
|
}
|
|
663
917
|
case 'corrective': {
|
|
@@ -1009,14 +1263,67 @@ async function mTables(cfg, post) {
|
|
|
1009
1263
|
if (!fn)
|
|
1010
1264
|
break;
|
|
1011
1265
|
const ft = await sel('Column type', [
|
|
1012
|
-
{ value: 'text', label: 'Text' },
|
|
1013
|
-
{ value: '
|
|
1014
|
-
{ value: '
|
|
1266
|
+
{ value: 'text', label: 'Text' },
|
|
1267
|
+
{ value: 'number', label: 'Number' },
|
|
1268
|
+
{ value: 'date', label: 'Date' },
|
|
1269
|
+
{ value: 'select', label: 'Dropdown', hint: 'pick from a list of values' },
|
|
1270
|
+
{ value: 'multi_select', label: 'Multi-select dropdown', hint: 'pick multiple values' },
|
|
1271
|
+
{ value: 'checkbox', label: 'Checkbox', hint: 'yes/no toggle' },
|
|
1272
|
+
{ value: 'file', label: 'File upload' },
|
|
1273
|
+
{ value: 'url', label: 'URL / link' },
|
|
1274
|
+
{ value: 'email', label: 'Email address' },
|
|
1275
|
+
{ value: 'phone', label: 'Phone number' },
|
|
1276
|
+
{ value: 'currency', label: 'Currency / money' },
|
|
1277
|
+
{ value: 'percent', label: 'Percentage' },
|
|
1278
|
+
{ value: 'rating', label: 'Rating (1-5)' },
|
|
1279
|
+
{ value: 'relation', label: 'Link to another table' },
|
|
1015
1280
|
]);
|
|
1016
1281
|
if (!ft)
|
|
1017
1282
|
break;
|
|
1283
|
+
const col = { name: fn, type: ft };
|
|
1018
1284
|
const req = await yn('Required?');
|
|
1019
|
-
|
|
1285
|
+
if (req)
|
|
1286
|
+
col.required = true;
|
|
1287
|
+
// Dropdown values
|
|
1288
|
+
if (ft === 'select' || ft === 'multi_select') {
|
|
1289
|
+
const optSource = await sel('Where do the options come from?', [
|
|
1290
|
+
{ value: 'manual', label: 'I\'ll type them now' },
|
|
1291
|
+
{ value: 'list', label: 'Link to an existing table/list' },
|
|
1292
|
+
]);
|
|
1293
|
+
if (optSource === 'manual') {
|
|
1294
|
+
const optStr = await txt('Options (comma-separated)', true, 'e.g. Pass, Fail, N/A');
|
|
1295
|
+
if (optStr)
|
|
1296
|
+
col.options = optStr.split(',').map(s => s.trim()).filter(Boolean);
|
|
1297
|
+
}
|
|
1298
|
+
else if (optSource === 'list') {
|
|
1299
|
+
const srcTable = await pickEntity(post, cfg.orgId, 'table', 'tables');
|
|
1300
|
+
if (srcTable) {
|
|
1301
|
+
col.source_table_id = srcTable.id;
|
|
1302
|
+
const srcField = await txt('Which column to use as the option label?', false, 'name');
|
|
1303
|
+
if (srcField)
|
|
1304
|
+
col.source_field = srcField;
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
const defaultVal = await txt('Default value (blank for none)');
|
|
1308
|
+
if (defaultVal)
|
|
1309
|
+
col.default = defaultVal;
|
|
1310
|
+
}
|
|
1311
|
+
// Number/currency constraints
|
|
1312
|
+
if (ft === 'number' || ft === 'currency' || ft === 'percent') {
|
|
1313
|
+
const min = await txt('Minimum value (blank for no limit)');
|
|
1314
|
+
if (min)
|
|
1315
|
+
col.min = parseFloat(min);
|
|
1316
|
+
const max = await txt('Maximum value (blank for no limit)');
|
|
1317
|
+
if (max)
|
|
1318
|
+
col.max = parseFloat(max);
|
|
1319
|
+
}
|
|
1320
|
+
// Relation target
|
|
1321
|
+
if (ft === 'relation') {
|
|
1322
|
+
const relTable = await pickEntity(post, cfg.orgId, 'table', 'tables');
|
|
1323
|
+
if (relTable)
|
|
1324
|
+
col.relation_table_id = relTable.id;
|
|
1325
|
+
}
|
|
1326
|
+
fields.push(col);
|
|
1020
1327
|
addMore = !!(await yn('Add another column?'));
|
|
1021
1328
|
}
|
|
1022
1329
|
if (fields.length > 0)
|
|
@@ -1510,20 +1817,37 @@ async function mAutomation(cfg, post) {
|
|
|
1510
1817
|
return mAutomation(cfg, post);
|
|
1511
1818
|
}
|
|
1512
1819
|
case 'val-create': {
|
|
1513
|
-
|
|
1514
|
-
|
|
1820
|
+
// Choose target: task or requirement
|
|
1821
|
+
const target = await sel('Apply this rule to', [
|
|
1822
|
+
{ value: 'task', label: 'A specific task' },
|
|
1823
|
+
{ value: 'requirement', label: 'A requirement', hint: 'applies to all tasks under it' },
|
|
1824
|
+
]);
|
|
1825
|
+
if (!target)
|
|
1515
1826
|
return mAutomation(cfg, post);
|
|
1827
|
+
const rulePayload = {};
|
|
1828
|
+
if (target === 'task') {
|
|
1829
|
+
const task = await pickEntity(post, cfg.orgId, 'task', 'tasks');
|
|
1830
|
+
if (!task)
|
|
1831
|
+
return mAutomation(cfg, post);
|
|
1832
|
+
rulePayload.task_id = task.id;
|
|
1833
|
+
}
|
|
1834
|
+
else {
|
|
1835
|
+
const req = await pickEntity(post, cfg.orgId, 'requirement', 'requirements');
|
|
1836
|
+
if (!req)
|
|
1837
|
+
return mAutomation(cfg, post);
|
|
1838
|
+
rulePayload.requirement_id = req.id;
|
|
1839
|
+
}
|
|
1516
1840
|
const name = await txt('Rule name', true, 'e.g. Require photo proof');
|
|
1517
1841
|
if (!name)
|
|
1518
1842
|
return mAutomation(cfg, post);
|
|
1519
1843
|
const ruleType = await sel('Rule type', [
|
|
1520
|
-
{ value: 'proof_required', label: 'Proof required', hint: 'photo, document, location,
|
|
1521
|
-
{ value: 'field_constraint', label: 'Field constraint', hint: 'validate a
|
|
1844
|
+
{ value: 'proof_required', label: 'Proof required', hint: 'photo, document, location, checklist, or table' },
|
|
1845
|
+
{ value: 'field_constraint', label: 'Field constraint', hint: 'validate a specific field value' },
|
|
1846
|
+
{ value: 'cross_field', label: 'Cross-field validation', hint: 'compare two fields' },
|
|
1847
|
+
{ value: 'aggregate', label: 'Aggregate check', hint: 'count/sum/avg of historical data' },
|
|
1522
1848
|
{ value: 'approval_required', label: 'Approval required', hint: 'needs manager sign-off' },
|
|
1523
1849
|
{ value: 'time_window', label: 'Time window', hint: 'must be done within a time range' },
|
|
1524
|
-
{ value: 'record_exists', label: 'Record exists', hint: 'verify
|
|
1525
|
-
{ value: 'cross_field', label: 'Cross-field validation' },
|
|
1526
|
-
{ value: 'aggregate', label: 'Aggregate check' },
|
|
1850
|
+
{ value: 'record_exists', label: 'Record exists', hint: 'verify a table record was created' },
|
|
1527
1851
|
]);
|
|
1528
1852
|
if (!ruleType)
|
|
1529
1853
|
return mAutomation(cfg, post);
|
|
@@ -1531,21 +1855,201 @@ async function mAutomation(cfg, post) {
|
|
|
1531
1855
|
if (ruleType === 'proof_required') {
|
|
1532
1856
|
const proofType = await sel('What kind of proof?', [
|
|
1533
1857
|
{ value: 'photo', label: 'Photo' }, { value: 'document', label: 'Document' },
|
|
1534
|
-
{ value: 'geolocation', label: 'Location' }, { value: 'checklist', label: 'Checklist' },
|
|
1858
|
+
{ value: 'geolocation', label: 'Location check-in' }, { value: 'checklist', label: 'Checklist' },
|
|
1859
|
+
{ value: 'table', label: 'Table record' },
|
|
1535
1860
|
]);
|
|
1536
|
-
if (proofType)
|
|
1537
|
-
condition.
|
|
1861
|
+
if (proofType) {
|
|
1862
|
+
condition.validation_type = proofType;
|
|
1863
|
+
// Let them pick the specific asset
|
|
1864
|
+
if (proofType === 'photo') {
|
|
1865
|
+
const ph = await pickEntity(post, cfg.orgId, 'photo', 'photo assets');
|
|
1866
|
+
if (ph)
|
|
1867
|
+
condition.config_id = ph.id;
|
|
1868
|
+
}
|
|
1869
|
+
else if (proofType === 'document') {
|
|
1870
|
+
const doc = await pickEntity(post, cfg.orgId, 'document', 'document folders');
|
|
1871
|
+
if (doc) {
|
|
1872
|
+
condition.config_id = doc.id;
|
|
1873
|
+
const tMode = await sel('Upload destination', [
|
|
1874
|
+
{ value: 'locked', label: 'Always to this folder' },
|
|
1875
|
+
{ value: 'user_choice', label: 'Let user choose' },
|
|
1876
|
+
]);
|
|
1877
|
+
if (tMode) {
|
|
1878
|
+
condition.target_mode = tMode;
|
|
1879
|
+
if (tMode === 'locked')
|
|
1880
|
+
condition.target_folder_id = doc.id;
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
else if (proofType === 'geolocation') {
|
|
1885
|
+
const area = await pickEntity(post, cfg.orgId, 'geolocation_area', 'location areas');
|
|
1886
|
+
if (area)
|
|
1887
|
+
condition.config_id = area.id;
|
|
1888
|
+
}
|
|
1889
|
+
else if (proofType === 'checklist') {
|
|
1890
|
+
const cl = await pickEntity(post, cfg.orgId, 'checklist', 'checklists');
|
|
1891
|
+
if (cl)
|
|
1892
|
+
condition.config_id = cl.id;
|
|
1893
|
+
}
|
|
1894
|
+
else if (proofType === 'table') {
|
|
1895
|
+
const tbl = await pickEntity(post, cfg.orgId, 'table', 'tables');
|
|
1896
|
+
if (tbl)
|
|
1897
|
+
condition.config_id = tbl.id;
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
else if (ruleType === 'field_constraint') {
|
|
1902
|
+
const field = await txt('Field name', true, 'e.g. status, temperature, score');
|
|
1903
|
+
if (field)
|
|
1904
|
+
condition.field = field;
|
|
1905
|
+
const op = await sel('Condition', [
|
|
1906
|
+
{ value: 'eq', label: 'Equals' }, { value: 'neq', label: 'Not equals' },
|
|
1907
|
+
{ value: 'gt', label: 'Greater than' }, { value: 'gte', label: 'Greater than or equal' },
|
|
1908
|
+
{ value: 'lt', label: 'Less than' }, { value: 'lte', label: 'Less than or equal' },
|
|
1909
|
+
{ value: 'in', label: 'Is one of (list)' }, { value: 'not_in', label: 'Is not one of (list)' },
|
|
1910
|
+
{ value: 'is_null', label: 'Is empty' }, { value: 'is_not_null', label: 'Is not empty' },
|
|
1911
|
+
{ value: 'matches', label: 'Matches pattern' },
|
|
1912
|
+
]);
|
|
1913
|
+
if (op)
|
|
1914
|
+
condition.operator = op;
|
|
1915
|
+
if (op && !['is_null', 'is_not_null'].includes(op)) {
|
|
1916
|
+
if (op === 'in' || op === 'not_in') {
|
|
1917
|
+
const vals = await txt('Values (comma-separated)', true);
|
|
1918
|
+
if (vals)
|
|
1919
|
+
condition.value = vals.split(',').map(s => s.trim());
|
|
1920
|
+
}
|
|
1921
|
+
else {
|
|
1922
|
+
const val = await txt('Value', true);
|
|
1923
|
+
if (val)
|
|
1924
|
+
condition.value = isNaN(Number(val)) ? val : Number(val);
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
else if (ruleType === 'cross_field') {
|
|
1929
|
+
const f1 = await txt('First field', true);
|
|
1930
|
+
if (f1)
|
|
1931
|
+
condition.field = f1;
|
|
1932
|
+
const cop = await sel('Must be', [
|
|
1933
|
+
{ value: 'eq', label: 'Equal to' }, { value: 'neq', label: 'Not equal to' },
|
|
1934
|
+
{ value: 'gt', label: 'Greater than' }, { value: 'gte', label: 'Greater or equal to' },
|
|
1935
|
+
{ value: 'lt', label: 'Less than' }, { value: 'lte', label: 'Less or equal to' },
|
|
1936
|
+
]);
|
|
1937
|
+
if (cop)
|
|
1938
|
+
condition.compare_operator = cop;
|
|
1939
|
+
const f2 = await txt('Second field', true);
|
|
1940
|
+
if (f2)
|
|
1941
|
+
condition.compare_field = f2;
|
|
1942
|
+
}
|
|
1943
|
+
else if (ruleType === 'aggregate') {
|
|
1944
|
+
const aggFn = await sel('Aggregate function', [
|
|
1945
|
+
{ value: 'count', label: 'Count' }, { value: 'sum', label: 'Sum' },
|
|
1946
|
+
{ value: 'avg', label: 'Average' }, { value: 'min', label: 'Minimum' },
|
|
1947
|
+
{ value: 'max', label: 'Maximum' },
|
|
1948
|
+
]);
|
|
1949
|
+
if (aggFn)
|
|
1950
|
+
condition.aggregate_function = aggFn;
|
|
1951
|
+
condition.aggregate_table = 'task_history';
|
|
1952
|
+
if (aggFn !== 'count') {
|
|
1953
|
+
const aggField = await txt('Field to aggregate', true);
|
|
1954
|
+
if (aggField)
|
|
1955
|
+
condition.aggregate_field = aggField;
|
|
1956
|
+
}
|
|
1957
|
+
const aggOp = await sel('Result must be', [
|
|
1958
|
+
{ value: 'eq', label: 'Equal to' }, { value: 'gt', label: 'Greater than' },
|
|
1959
|
+
{ value: 'gte', label: 'At least' }, { value: 'lt', label: 'Less than' },
|
|
1960
|
+
{ value: 'lte', label: 'At most' },
|
|
1961
|
+
]);
|
|
1962
|
+
if (aggOp)
|
|
1963
|
+
condition.aggregate_operator = aggOp;
|
|
1964
|
+
const aggVal = await txt('Target value', true, 'e.g. 5');
|
|
1965
|
+
if (aggVal)
|
|
1966
|
+
condition.aggregate_value = parseFloat(aggVal);
|
|
1967
|
+
}
|
|
1968
|
+
else if (ruleType === 'approval_required') {
|
|
1969
|
+
const approverType = await sel('Who must approve?', [
|
|
1970
|
+
{ value: 'role', label: 'Anyone with a specific role' },
|
|
1971
|
+
{ value: 'user', label: 'A specific person' },
|
|
1972
|
+
]);
|
|
1973
|
+
if (approverType === 'role') {
|
|
1974
|
+
const role = await sel('Required role', [
|
|
1975
|
+
{ value: 'admin', label: 'Admin' }, { value: 'super_admin', label: 'Super Admin' },
|
|
1976
|
+
]);
|
|
1977
|
+
if (role)
|
|
1978
|
+
condition.approval_approver_role = role;
|
|
1979
|
+
}
|
|
1980
|
+
else if (approverType === 'user') {
|
|
1981
|
+
const member = await pickMember(post, cfg.orgId);
|
|
1982
|
+
if (member)
|
|
1983
|
+
condition.approval_approver_id = member.id;
|
|
1984
|
+
}
|
|
1538
1985
|
}
|
|
1539
1986
|
else if (ruleType === 'time_window') {
|
|
1540
|
-
const
|
|
1541
|
-
|
|
1987
|
+
const wType = await sel('Window type', [
|
|
1988
|
+
{ value: 'daily', label: 'Daily time window' },
|
|
1989
|
+
{ value: 'custom', label: 'Custom date/time range' },
|
|
1990
|
+
]);
|
|
1991
|
+
if (wType)
|
|
1992
|
+
condition.window_type = wType;
|
|
1993
|
+
const start = await txt('Start time (HH:MM)', true, '08:00');
|
|
1542
1994
|
if (start)
|
|
1543
1995
|
condition.start_time = start;
|
|
1996
|
+
const end = await txt('End time (HH:MM)', true, '17:00');
|
|
1544
1997
|
if (end)
|
|
1545
1998
|
condition.end_time = end;
|
|
1999
|
+
const tz = await txt('Timezone (blank for UTC)', false, 'America/New_York');
|
|
2000
|
+
if (tz)
|
|
2001
|
+
condition.timezone = tz;
|
|
2002
|
+
}
|
|
2003
|
+
else if (ruleType === 'record_exists') {
|
|
2004
|
+
const tbl = await pickEntity(post, cfg.orgId, 'table', 'tables');
|
|
2005
|
+
if (tbl) {
|
|
2006
|
+
condition.table = tbl.id;
|
|
2007
|
+
const addFilters = await yn('Add filters? (only match records with specific values)');
|
|
2008
|
+
if (addFilters) {
|
|
2009
|
+
const filters = [];
|
|
2010
|
+
let addF = true;
|
|
2011
|
+
while (addF) {
|
|
2012
|
+
const ff = await txt('Field name', true);
|
|
2013
|
+
if (!ff)
|
|
2014
|
+
break;
|
|
2015
|
+
const fop = await sel('Condition', [
|
|
2016
|
+
{ value: 'eq', label: 'Equals' }, { value: 'neq', label: 'Not equals' },
|
|
2017
|
+
{ value: 'gt', label: 'Greater than' }, { value: 'lt', label: 'Less than' },
|
|
2018
|
+
{ value: 'is_not_null', label: 'Has a value' },
|
|
2019
|
+
]);
|
|
2020
|
+
if (!fop)
|
|
2021
|
+
break;
|
|
2022
|
+
if (fop === 'is_not_null') {
|
|
2023
|
+
filters.push({ field: ff, operator: fop, value: null });
|
|
2024
|
+
}
|
|
2025
|
+
else {
|
|
2026
|
+
const fv = await txt('Value', true);
|
|
2027
|
+
if (fv)
|
|
2028
|
+
filters.push({ field: ff, operator: fop, value: isNaN(Number(fv)) ? fv : Number(fv) });
|
|
2029
|
+
}
|
|
2030
|
+
addF = !!(await yn('Add another filter?'));
|
|
2031
|
+
}
|
|
2032
|
+
if (filters.length > 0)
|
|
2033
|
+
condition.filters = filters;
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
// Grouping options
|
|
2038
|
+
const addGroup = await yn('Add to a rule group? (for AND/OR logic with other rules)');
|
|
2039
|
+
if (addGroup) {
|
|
2040
|
+
const gid = await txt('Group ID (create a new UUID or use an existing group ID)', true);
|
|
2041
|
+
if (gid) {
|
|
2042
|
+
rulePayload.group_id = gid;
|
|
2043
|
+
const gop = await sel('Group logic', [
|
|
2044
|
+
{ value: 'AND', label: 'AND — all rules in the group must pass' },
|
|
2045
|
+
{ value: 'OR', label: 'OR — any rule in the group can pass' },
|
|
2046
|
+
]);
|
|
2047
|
+
if (gop)
|
|
2048
|
+
rulePayload.group_operator = gop;
|
|
2049
|
+
}
|
|
1546
2050
|
}
|
|
1547
2051
|
await cmd(post, cfg.orgId, 'create_validation_rule', {
|
|
1548
|
-
|
|
2052
|
+
...rulePayload, name, rule_type: ruleType, condition,
|
|
1549
2053
|
}, 'Creating validation rule...');
|
|
1550
2054
|
return mAutomation(cfg, post);
|
|
1551
2055
|
}
|
|
@@ -1621,25 +2125,100 @@ async function mAutomation(cfg, post) {
|
|
|
1621
2125
|
if (!name)
|
|
1622
2126
|
return mAutomation(cfg, post);
|
|
1623
2127
|
const eventType = await sel('What triggers this?', [
|
|
1624
|
-
{ value: 'record_created', label: 'New record added to a table' },
|
|
1625
|
-
{ value: 'record_updated', label: 'Record updated in a table' },
|
|
1626
2128
|
{ value: 'task_completed', label: 'Another task completed' },
|
|
1627
|
-
{ value: '
|
|
1628
|
-
{ value: '
|
|
2129
|
+
{ value: 'task_uncompleted', label: 'A task was reopened' },
|
|
2130
|
+
{ value: 'task_created', label: 'A new task was created' },
|
|
2131
|
+
{ value: 'task_updated', label: 'A task was updated' },
|
|
2132
|
+
{ value: 'validation_submitted', label: 'Proof was submitted' },
|
|
2133
|
+
{ value: 'approval_requested', label: 'An approval was requested' },
|
|
2134
|
+
{ value: 'approval_approved', label: 'An approval was granted' },
|
|
2135
|
+
{ value: 'approval_rejected', label: 'An approval was rejected' },
|
|
2136
|
+
{ value: 'table_record_created', label: 'New record added to a table' },
|
|
2137
|
+
{ value: 'conditional_answer', label: 'A conditional question was answered' },
|
|
2138
|
+
{ value: 'document_created', label: 'A document was uploaded' },
|
|
2139
|
+
{ value: 'checklist_created', label: 'A checklist was created' },
|
|
1629
2140
|
]);
|
|
1630
2141
|
if (!eventType)
|
|
1631
2142
|
return mAutomation(cfg, post);
|
|
1632
|
-
const sourceType = await sel('Source type', [
|
|
1633
|
-
{ value: '
|
|
2143
|
+
const sourceType = await sel('Source type (what entity fires the event)', [
|
|
2144
|
+
{ value: 'task', label: 'Task' },
|
|
2145
|
+
{ value: 'table', label: 'Table' },
|
|
2146
|
+
{ value: 'document', label: 'Document' },
|
|
2147
|
+
{ value: 'checklist', label: 'Checklist' },
|
|
2148
|
+
{ value: 'geolocation_area', label: 'Location area' },
|
|
1634
2149
|
{ value: 'approval', label: 'Approval' },
|
|
2150
|
+
{ value: 'plan', label: 'Plan' },
|
|
1635
2151
|
]);
|
|
1636
2152
|
if (!sourceType)
|
|
1637
2153
|
return mAutomation(cfg, post);
|
|
1638
|
-
const
|
|
1639
|
-
const
|
|
1640
|
-
if (
|
|
1641
|
-
|
|
1642
|
-
|
|
2154
|
+
const trigPayload = { task_id: task.id, name, event_type: eventType, source_entity_type: sourceType };
|
|
2155
|
+
const pickSource = await yn(`Pick a specific ${sourceType}? (or trigger on any)`);
|
|
2156
|
+
if (pickSource) {
|
|
2157
|
+
const source = await pickEntity(post, cfg.orgId, sourceType, `${sourceType}s`);
|
|
2158
|
+
if (source)
|
|
2159
|
+
trigPayload.source_entity_id = source.id;
|
|
2160
|
+
}
|
|
2161
|
+
// Conditions
|
|
2162
|
+
const addConds = await yn('Add conditions? (only trigger when specific criteria are met)');
|
|
2163
|
+
if (addConds) {
|
|
2164
|
+
const conditions = [];
|
|
2165
|
+
let addC = true;
|
|
2166
|
+
while (addC) {
|
|
2167
|
+
const cf = await txt('Field to check', true, 'e.g. status, frequency, name');
|
|
2168
|
+
if (!cf)
|
|
2169
|
+
break;
|
|
2170
|
+
const cop = await sel('Condition', [
|
|
2171
|
+
{ value: 'eq', label: 'Equals' }, { value: 'neq', label: 'Not equals' },
|
|
2172
|
+
{ value: 'gt', label: 'Greater than' }, { value: 'lt', label: 'Less than' },
|
|
2173
|
+
{ value: 'in', label: 'Is one of' }, { value: 'is_not_null', label: 'Has a value' },
|
|
2174
|
+
]);
|
|
2175
|
+
if (!cop)
|
|
2176
|
+
break;
|
|
2177
|
+
const csrc = await sel('Check against', [
|
|
2178
|
+
{ value: 'payload', label: 'Event data (payload)' },
|
|
2179
|
+
{ value: 'entity', label: 'Entity state' },
|
|
2180
|
+
]);
|
|
2181
|
+
let cv = null;
|
|
2182
|
+
if (cop !== 'is_not_null') {
|
|
2183
|
+
if (cop === 'in') {
|
|
2184
|
+
const cvStr = await txt('Values (comma-separated)', true);
|
|
2185
|
+
if (cvStr)
|
|
2186
|
+
cv = cvStr.split(',').map(s => s.trim());
|
|
2187
|
+
}
|
|
2188
|
+
else {
|
|
2189
|
+
const cvTxt = await txt('Value', true);
|
|
2190
|
+
if (cvTxt)
|
|
2191
|
+
cv = isNaN(Number(cvTxt)) ? cvTxt : Number(cvTxt);
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
conditions.push({ field: cf, operator: cop, value: cv, ...(csrc ? { source: csrc } : {}) });
|
|
2195
|
+
addC = !!(await yn('Add another condition?'));
|
|
2196
|
+
}
|
|
2197
|
+
if (conditions.length > 0) {
|
|
2198
|
+
trigPayload.conditions = conditions;
|
|
2199
|
+
if (conditions.length > 1) {
|
|
2200
|
+
const logic = await sel('Condition logic', [
|
|
2201
|
+
{ value: 'AND', label: 'ALL conditions must be true' },
|
|
2202
|
+
{ value: 'OR', label: 'ANY condition can be true' },
|
|
2203
|
+
]);
|
|
2204
|
+
if (logic)
|
|
2205
|
+
trigPayload.condition_operator = logic;
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
// Advanced options
|
|
2210
|
+
const advanced = await yn('Configure advanced options? (frequency, message, debounce)');
|
|
2211
|
+
if (advanced) {
|
|
2212
|
+
const respect = await yn('Respect task frequency? (only activate on scheduled days)');
|
|
2213
|
+
trigPayload.respect_frequency = !!respect;
|
|
2214
|
+
const msg = await txt('Activation message (shown when triggered)');
|
|
2215
|
+
if (msg)
|
|
2216
|
+
trigPayload.activation_message = msg;
|
|
2217
|
+
const debounce = await txt('Debounce seconds (prevent re-triggering too fast)', false, '60');
|
|
2218
|
+
if (debounce)
|
|
2219
|
+
trigPayload.debounce_seconds = parseInt(debounce, 10);
|
|
2220
|
+
}
|
|
2221
|
+
await cmd(post, cfg.orgId, 'create_trigger_rule', trigPayload, 'Creating trigger rule...');
|
|
1643
2222
|
return mAutomation(cfg, post);
|
|
1644
2223
|
}
|
|
1645
2224
|
case 'trig-edit': {
|
|
@@ -1970,6 +2549,7 @@ async function mApprovals(cfg, post) {
|
|
|
1970
2549
|
// ══════════════════════════════════════════════════════════════════════
|
|
1971
2550
|
async function mTeam(cfg, post) {
|
|
1972
2551
|
const a = await sel('Team', [
|
|
2552
|
+
{ value: 'list', label: 'View team members' },
|
|
1973
2553
|
{ value: 'invite', label: 'Invite a member' },
|
|
1974
2554
|
{ value: 'revoke', label: 'Revoke an invitation' },
|
|
1975
2555
|
{ value: 'role', label: 'Change a member\u2019s role' },
|
|
@@ -1979,6 +2559,22 @@ async function mTeam(cfg, post) {
|
|
|
1979
2559
|
if (!a || a === '__back')
|
|
1980
2560
|
return interactiveMenu(cfg);
|
|
1981
2561
|
switch (a) {
|
|
2562
|
+
case 'list': {
|
|
2563
|
+
const s = p.spinner();
|
|
2564
|
+
s.start('Loading team members...');
|
|
2565
|
+
const members = await listEntities(post, cfg.orgId, 'member');
|
|
2566
|
+
s.stop(`${members.length} team members`);
|
|
2567
|
+
if (members.length > 0) {
|
|
2568
|
+
const lines = members.map((m) => {
|
|
2569
|
+
const name = m.name || 'Unknown';
|
|
2570
|
+
const email = m.metadata?.email || '';
|
|
2571
|
+
const role = m.metadata?.role || '';
|
|
2572
|
+
return ` ${pc.cyan(pad(truncate(name, 30), 32))} ${pc.dim(pad(email, 30))} ${pc.dim(role)}`;
|
|
2573
|
+
});
|
|
2574
|
+
p.note(lines.join('\n'), 'Team Members');
|
|
2575
|
+
}
|
|
2576
|
+
return mTeam(cfg, post);
|
|
2577
|
+
}
|
|
1982
2578
|
case 'invite': {
|
|
1983
2579
|
const email = await txt('Email address', true);
|
|
1984
2580
|
if (!email)
|
|
@@ -2001,26 +2597,26 @@ async function mTeam(cfg, post) {
|
|
|
2001
2597
|
return mTeam(cfg, post);
|
|
2002
2598
|
}
|
|
2003
2599
|
case 'role': {
|
|
2004
|
-
const
|
|
2005
|
-
if (!
|
|
2600
|
+
const member = await pickMember(post, cfg.orgId);
|
|
2601
|
+
if (!member)
|
|
2006
2602
|
return mTeam(cfg, post);
|
|
2007
|
-
const role = await sel(
|
|
2603
|
+
const role = await sel(`New role for ${member.name}`, [
|
|
2008
2604
|
{ value: 'user', label: 'Member' },
|
|
2009
2605
|
{ value: 'admin', label: 'Admin' },
|
|
2010
2606
|
{ value: 'super_admin', label: 'Owner' },
|
|
2011
2607
|
]);
|
|
2012
2608
|
if (!role)
|
|
2013
2609
|
return mTeam(cfg, post);
|
|
2014
|
-
await cmd(post, cfg.orgId, 'update_member_role', { user_id:
|
|
2610
|
+
await cmd(post, cfg.orgId, 'update_member_role', { user_id: member.id, role }, `Updating ${member.name}'s role...`);
|
|
2015
2611
|
return mTeam(cfg, post);
|
|
2016
2612
|
}
|
|
2017
2613
|
case 'remove': {
|
|
2018
|
-
const
|
|
2019
|
-
if (!
|
|
2614
|
+
const member = await pickMember(post, cfg.orgId);
|
|
2615
|
+
if (!member)
|
|
2020
2616
|
return mTeam(cfg, post);
|
|
2021
|
-
const sure = await yn(
|
|
2617
|
+
const sure = await yn(`Remove ${member.name}? They will lose access to the workspace.`);
|
|
2022
2618
|
if (sure)
|
|
2023
|
-
await cmd(post, cfg.orgId, 'remove_member', { user_id:
|
|
2619
|
+
await cmd(post, cfg.orgId, 'remove_member', { user_id: member.id }, `Removing ${member.name}...`);
|
|
2024
2620
|
return mTeam(cfg, post);
|
|
2025
2621
|
}
|
|
2026
2622
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "checkbox-cli",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Beautiful, interactive CLI for Checkbox compliance management. Setup wizard, guided workflows, and full workspace control from your terminal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/checkbox.js",
|