checkbox-cli 3.0.0 → 3.1.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 +667 -99
- 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.
|
|
19
|
+
const VERSION = '3.1.0';
|
|
20
20
|
function loadConfig() {
|
|
21
21
|
try {
|
|
22
22
|
if (!existsSync(CONFIG_FILE))
|
|
@@ -56,7 +56,7 @@ function resolveConfig() {
|
|
|
56
56
|
// ── API ─────────────────────────────────────────────────────────────
|
|
57
57
|
function extractError(obj) {
|
|
58
58
|
if (!obj)
|
|
59
|
-
return '
|
|
59
|
+
return '';
|
|
60
60
|
if (typeof obj === 'string')
|
|
61
61
|
return obj;
|
|
62
62
|
if (typeof obj.error === 'string')
|
|
@@ -78,46 +78,78 @@ function api(cfg) {
|
|
|
78
78
|
headers,
|
|
79
79
|
body: JSON.stringify(body),
|
|
80
80
|
});
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
const text = await res.text().catch(() => '');
|
|
82
|
+
let json = null;
|
|
83
|
+
try {
|
|
84
|
+
json = JSON.parse(text);
|
|
85
|
+
}
|
|
86
|
+
catch { }
|
|
87
|
+
if (!res.ok) {
|
|
88
|
+
throw new Error(extractError(json) || text || `Request failed (${res.status})`);
|
|
89
|
+
}
|
|
90
|
+
if (json && json.success === false) {
|
|
85
91
|
throw new Error(extractError(json) || 'Request failed');
|
|
92
|
+
}
|
|
86
93
|
return json;
|
|
87
94
|
}
|
|
88
95
|
async function get(path) {
|
|
89
96
|
const res = await fetch(`${cfg.apiUrl}${path}`, { headers });
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
97
|
+
const text = await res.text().catch(() => '');
|
|
98
|
+
let json = null;
|
|
99
|
+
try {
|
|
100
|
+
json = JSON.parse(text);
|
|
101
|
+
}
|
|
102
|
+
catch { }
|
|
103
|
+
if (!res.ok) {
|
|
104
|
+
throw new Error(extractError(json) || text || `Request failed (${res.status})`);
|
|
105
|
+
}
|
|
106
|
+
if (json && json.success === false) {
|
|
94
107
|
throw new Error(extractError(json) || 'Request failed');
|
|
108
|
+
}
|
|
95
109
|
return json;
|
|
96
110
|
}
|
|
97
111
|
return { post, get };
|
|
98
112
|
}
|
|
99
113
|
// ── Data Helpers ────────────────────────────────────────────────────
|
|
100
114
|
async function listEntities(post, orgId, entityType, filters) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
115
|
+
try {
|
|
116
|
+
const res = await post('/api/v2/query', {
|
|
117
|
+
type: 'list_entities',
|
|
118
|
+
organization_id: orgId,
|
|
119
|
+
payload: { entity_type: entityType, organization_id: orgId, ...filters },
|
|
120
|
+
});
|
|
121
|
+
return Array.isArray(res.data) ? res.data : [];
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
p.log.error(err.message || `Could not load ${entityType}`);
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
107
127
|
}
|
|
108
128
|
async function searchEntities(post, orgId, search, limit = 20) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
129
|
+
try {
|
|
130
|
+
const res = await post('/api/v2/introspection/entities', {
|
|
131
|
+
organization_id: orgId,
|
|
132
|
+
search,
|
|
133
|
+
limit,
|
|
134
|
+
});
|
|
135
|
+
return res.data?.entities || [];
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
p.log.error(err.message || 'Search failed');
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
115
141
|
}
|
|
116
142
|
async function getWorkspaceNodes(post, orgId) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
143
|
+
try {
|
|
144
|
+
const res = await post('/api/v2/introspection/workspace', {
|
|
145
|
+
organization_id: orgId,
|
|
146
|
+
});
|
|
147
|
+
return res.data?.nodes || [];
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
p.log.error(err.message || 'Could not load workspace');
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
121
153
|
}
|
|
122
154
|
// ── Formatters ──────────────────────────────────────────────────────
|
|
123
155
|
function truncate(s, max) {
|
|
@@ -448,40 +480,149 @@ async function mTasks(cfg, post) {
|
|
|
448
480
|
if (!frequency)
|
|
449
481
|
return mTasks(cfg, post);
|
|
450
482
|
const payload = { name, frequency };
|
|
483
|
+
// Frequency configuration (day of week, day of month, etc.)
|
|
484
|
+
if (frequency === 'weekly') {
|
|
485
|
+
const dow = await sel('Which day of the week?', [
|
|
486
|
+
{ value: 'monday', label: 'Monday' }, { value: 'tuesday', label: 'Tuesday' },
|
|
487
|
+
{ value: 'wednesday', label: 'Wednesday' }, { value: 'thursday', label: 'Thursday' },
|
|
488
|
+
{ value: 'friday', label: 'Friday' }, { value: 'saturday', label: 'Saturday' },
|
|
489
|
+
{ value: 'sunday', label: 'Sunday' },
|
|
490
|
+
]);
|
|
491
|
+
if (dow)
|
|
492
|
+
payload.frequency_config = { day_of_week: dow };
|
|
493
|
+
}
|
|
494
|
+
else if (frequency === 'monthly') {
|
|
495
|
+
const dom = await txt('Which day of the month? (1-31)', false, '1');
|
|
496
|
+
if (dom)
|
|
497
|
+
payload.frequency_config = { day_of_month: parseInt(dom, 10) };
|
|
498
|
+
}
|
|
499
|
+
else if (frequency === 'yearly') {
|
|
500
|
+
const month = await txt('Month (1-12)', false, '1');
|
|
501
|
+
const day = await txt('Day (1-31)', false, '1');
|
|
502
|
+
if (month && day)
|
|
503
|
+
payload.frequency_config = { month: parseInt(month, 10), day: parseInt(day, 10) };
|
|
504
|
+
}
|
|
451
505
|
const addPlan = await yn('Add to a plan?');
|
|
452
506
|
if (addPlan) {
|
|
453
507
|
const plan = await pickEntity(post, cfg.orgId, 'plan', 'plans');
|
|
454
|
-
if (plan)
|
|
508
|
+
if (plan) {
|
|
455
509
|
payload.plan_id = plan.id;
|
|
510
|
+
// Optionally link to a specific requirement
|
|
511
|
+
const linkReq = await yn('Link to a specific requirement?');
|
|
512
|
+
if (linkReq) {
|
|
513
|
+
const req = await pickEntity(post, cfg.orgId, 'requirement', 'requirements');
|
|
514
|
+
if (req)
|
|
515
|
+
payload.requirement_id = req.id;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
456
518
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
519
|
+
// Description
|
|
520
|
+
const desc = await txt('Description (blank to skip)');
|
|
521
|
+
if (desc)
|
|
522
|
+
payload.description = desc;
|
|
523
|
+
// Time
|
|
524
|
+
const time = await txt('Scheduled time (HH:MM, blank to skip)', false, '09:00');
|
|
525
|
+
if (time)
|
|
526
|
+
payload.time = time;
|
|
527
|
+
// Assignment
|
|
528
|
+
const assign = await yn('Assign to a specific person?');
|
|
529
|
+
if (assign) {
|
|
530
|
+
const assignId = await txt('User ID to assign to', true);
|
|
531
|
+
if (assignId) {
|
|
532
|
+
payload.assigned_to = assignId;
|
|
533
|
+
const assignName = await txt('Their display name (optional)');
|
|
534
|
+
if (assignName)
|
|
535
|
+
payload.assigned_to_name = assignName;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
// Privacy
|
|
539
|
+
const priv = await yn('Private task?');
|
|
540
|
+
if (priv)
|
|
541
|
+
payload.is_private = true;
|
|
542
|
+
// Reminder
|
|
543
|
+
const reminder = await sel('Reminder', [
|
|
544
|
+
{ value: 'none', label: 'No reminder' },
|
|
545
|
+
{ value: 'notification', label: 'Push notification' },
|
|
546
|
+
{ value: 'email', label: 'Email' },
|
|
547
|
+
]);
|
|
548
|
+
if (reminder && reminder !== 'none') {
|
|
549
|
+
payload.reminder_mode = reminder;
|
|
550
|
+
const mins = await txt('Minutes before', false, '30');
|
|
551
|
+
if (mins)
|
|
552
|
+
payload.reminder_minutes = parseInt(mins, 10);
|
|
553
|
+
}
|
|
554
|
+
// Conditional question
|
|
555
|
+
const cond = await txt('Conditional question (blank to skip)', false, 'e.g. Is the equipment available?');
|
|
556
|
+
if (cond)
|
|
557
|
+
payload.conditional_question = cond;
|
|
558
|
+
// Approval requirement
|
|
559
|
+
const approval = await yn('Requires approval before completion?');
|
|
560
|
+
if (approval)
|
|
561
|
+
payload.approval_required = true;
|
|
562
|
+
// Proof / Validation requirements (THE MOST IMPORTANT FEATURE)
|
|
563
|
+
const addProof = await yn('Require proof to complete this task? (photos, documents, location, etc.)');
|
|
564
|
+
if (addProof) {
|
|
565
|
+
const validationTypes = [];
|
|
566
|
+
let addMoreProof = true;
|
|
567
|
+
while (addMoreProof) {
|
|
568
|
+
const proofType = await sel('What type of proof?', [
|
|
569
|
+
{ value: 'photo', label: 'Photo', hint: 'require a photo to be taken' },
|
|
570
|
+
{ value: 'document', label: 'Document', hint: 'require a document upload' },
|
|
571
|
+
{ value: 'geolocation', label: 'Location check-in', hint: 'require being at a specific location' },
|
|
572
|
+
{ value: 'checklist', label: 'Checklist', hint: 'require completing a checklist' },
|
|
573
|
+
{ value: 'table', label: 'Table record', hint: 'require filling out a data table' },
|
|
574
|
+
]);
|
|
575
|
+
if (!proofType)
|
|
576
|
+
break;
|
|
577
|
+
const vt = { type: proofType };
|
|
578
|
+
// Pick the specific asset for this proof type
|
|
579
|
+
if (proofType === 'photo') {
|
|
580
|
+
const ph = await pickEntity(post, cfg.orgId, 'photo', 'photo assets');
|
|
581
|
+
if (ph)
|
|
582
|
+
vt.config_id = ph.id;
|
|
583
|
+
}
|
|
584
|
+
else if (proofType === 'document') {
|
|
585
|
+
const doc = await pickEntity(post, cfg.orgId, 'document', 'document folders');
|
|
586
|
+
if (doc) {
|
|
587
|
+
vt.config_id = doc.id;
|
|
588
|
+
const targetMode = await sel('Where should uploads go?', [
|
|
589
|
+
{ value: 'locked', label: 'Always to this folder' },
|
|
590
|
+
{ value: 'user_choice', label: 'Let user choose folder' },
|
|
591
|
+
]);
|
|
592
|
+
if (targetMode) {
|
|
593
|
+
vt.target_mode = targetMode;
|
|
594
|
+
if (targetMode === 'locked')
|
|
595
|
+
vt.target_folder_id = doc.id;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
else if (proofType === 'geolocation') {
|
|
600
|
+
const area = await pickEntity(post, cfg.orgId, 'geolocation_area', 'location areas');
|
|
601
|
+
if (area)
|
|
602
|
+
vt.config_id = area.id;
|
|
603
|
+
}
|
|
604
|
+
else if (proofType === 'checklist') {
|
|
605
|
+
const cl = await pickEntity(post, cfg.orgId, 'checklist', 'checklists');
|
|
606
|
+
if (cl)
|
|
607
|
+
vt.config_id = cl.id;
|
|
608
|
+
}
|
|
609
|
+
else if (proofType === 'table') {
|
|
610
|
+
const tbl = await pickEntity(post, cfg.orgId, 'table', 'tables');
|
|
611
|
+
if (tbl)
|
|
612
|
+
vt.config_id = tbl.id;
|
|
613
|
+
}
|
|
614
|
+
validationTypes.push(vt);
|
|
615
|
+
addMoreProof = !!(await yn('Add another proof requirement?'));
|
|
478
616
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
617
|
+
if (validationTypes.length > 0)
|
|
618
|
+
payload.validation_types = validationTypes;
|
|
619
|
+
}
|
|
620
|
+
// Share with specific users
|
|
621
|
+
const share = await yn('Share with specific users?');
|
|
622
|
+
if (share) {
|
|
623
|
+
const ids = await txt('User IDs (comma-separated)', true);
|
|
624
|
+
if (ids)
|
|
625
|
+
payload.shared_with_ids = ids.split(',').map(s => s.trim()).filter(Boolean);
|
|
485
626
|
}
|
|
486
627
|
await cmd(post, cfg.orgId, 'create_task', payload, 'Creating task...');
|
|
487
628
|
return mTasks(cfg, post);
|
|
@@ -490,24 +631,126 @@ async function mTasks(cfg, post) {
|
|
|
490
631
|
const t = await pickEntity(post, cfg.orgId, 'task', 'tasks');
|
|
491
632
|
if (!t)
|
|
492
633
|
return mTasks(cfg, post);
|
|
493
|
-
p.log.info(`Editing: ${pc.cyan(t.name)}
|
|
494
|
-
const
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
{ value: '
|
|
502
|
-
{ value: '
|
|
503
|
-
{ value: '
|
|
504
|
-
{ value: '
|
|
634
|
+
p.log.info(`Editing: ${pc.cyan(t.name)}`);
|
|
635
|
+
const field = await sel('What would you like to change?', [
|
|
636
|
+
{ value: 'name', label: 'Name' },
|
|
637
|
+
{ value: 'frequency', label: 'Frequency' },
|
|
638
|
+
{ value: 'description', label: 'Description' },
|
|
639
|
+
{ value: 'time', label: 'Scheduled time' },
|
|
640
|
+
{ value: 'assigned_to', label: 'Assignment' },
|
|
641
|
+
{ value: 'is_private', label: 'Privacy' },
|
|
642
|
+
{ value: 'reminder', label: 'Reminder settings' },
|
|
643
|
+
{ value: 'conditional_question', label: 'Conditional question' },
|
|
644
|
+
{ value: 'plan', label: 'Plan / requirement link' },
|
|
645
|
+
{ value: '__back', label: pc.dim('\u2190 Back') },
|
|
505
646
|
]);
|
|
506
|
-
if (
|
|
507
|
-
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
647
|
+
if (!field || field === '__back')
|
|
648
|
+
return mTasks(cfg, post);
|
|
649
|
+
const updates = {};
|
|
650
|
+
switch (field) {
|
|
651
|
+
case 'name': {
|
|
652
|
+
const n = await txt('New name', true);
|
|
653
|
+
if (n)
|
|
654
|
+
updates.name = n;
|
|
655
|
+
break;
|
|
656
|
+
}
|
|
657
|
+
case 'frequency': {
|
|
658
|
+
const f = await sel('New frequency', [
|
|
659
|
+
{ value: 'daily', label: 'Daily' }, { value: 'weekly', label: 'Weekly' },
|
|
660
|
+
{ value: 'monthly', label: 'Monthly' }, { value: 'yearly', label: 'Yearly' },
|
|
661
|
+
{ value: 'one-time', label: 'One time' },
|
|
662
|
+
]);
|
|
663
|
+
if (f) {
|
|
664
|
+
updates.frequency = f;
|
|
665
|
+
if (f === 'weekly') {
|
|
666
|
+
const dow = await sel('Which day?', [
|
|
667
|
+
{ value: 'monday', label: 'Monday' }, { value: 'tuesday', label: 'Tuesday' },
|
|
668
|
+
{ value: 'wednesday', label: 'Wednesday' }, { value: 'thursday', label: 'Thursday' },
|
|
669
|
+
{ value: 'friday', label: 'Friday' }, { value: 'saturday', label: 'Saturday' },
|
|
670
|
+
{ value: 'sunday', label: 'Sunday' },
|
|
671
|
+
]);
|
|
672
|
+
if (dow)
|
|
673
|
+
updates.frequency_config = { day_of_week: dow };
|
|
674
|
+
}
|
|
675
|
+
else if (f === 'monthly') {
|
|
676
|
+
const dom = await txt('Which day of the month? (1-31)', false, '1');
|
|
677
|
+
if (dom)
|
|
678
|
+
updates.frequency_config = { day_of_month: parseInt(dom, 10) };
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
break;
|
|
682
|
+
}
|
|
683
|
+
case 'description': {
|
|
684
|
+
const d = await txt('New description', true);
|
|
685
|
+
if (d)
|
|
686
|
+
updates.description = d;
|
|
687
|
+
break;
|
|
688
|
+
}
|
|
689
|
+
case 'time': {
|
|
690
|
+
const time = await txt('New scheduled time (HH:MM)', false, '09:00');
|
|
691
|
+
if (time)
|
|
692
|
+
updates.time = time;
|
|
693
|
+
break;
|
|
694
|
+
}
|
|
695
|
+
case 'assigned_to': {
|
|
696
|
+
const aid = await txt('User ID to assign to (blank to unassign)');
|
|
697
|
+
updates.assigned_to = aid || null;
|
|
698
|
+
if (aid) {
|
|
699
|
+
const aname = await txt('Their display name (optional)');
|
|
700
|
+
if (aname)
|
|
701
|
+
updates.assigned_to_name = aname;
|
|
702
|
+
}
|
|
703
|
+
break;
|
|
704
|
+
}
|
|
705
|
+
case 'is_private': {
|
|
706
|
+
const priv = await yn('Make this task private?');
|
|
707
|
+
updates.is_private = !!priv;
|
|
708
|
+
break;
|
|
709
|
+
}
|
|
710
|
+
case 'reminder': {
|
|
711
|
+
const mode = await sel('Reminder type', [
|
|
712
|
+
{ value: 'none', label: 'No reminder' },
|
|
713
|
+
{ value: 'notification', label: 'Push notification' },
|
|
714
|
+
{ value: 'email', label: 'Email' },
|
|
715
|
+
]);
|
|
716
|
+
if (mode === 'none') {
|
|
717
|
+
updates.reminder_mode = null;
|
|
718
|
+
updates.reminder_minutes = null;
|
|
719
|
+
}
|
|
720
|
+
else if (mode) {
|
|
721
|
+
updates.reminder_mode = mode;
|
|
722
|
+
const mins = await txt('Minutes before', false, '30');
|
|
723
|
+
if (mins)
|
|
724
|
+
updates.reminder_minutes = parseInt(mins, 10);
|
|
725
|
+
}
|
|
726
|
+
break;
|
|
727
|
+
}
|
|
728
|
+
case 'conditional_question': {
|
|
729
|
+
const q = await txt('Conditional question (blank to remove)');
|
|
730
|
+
updates.conditional_question = q || null;
|
|
731
|
+
break;
|
|
732
|
+
}
|
|
733
|
+
case 'plan': {
|
|
734
|
+
const plan = await pickEntity(post, cfg.orgId, 'plan', 'plans');
|
|
735
|
+
if (plan) {
|
|
736
|
+
updates.plan_id = plan.id;
|
|
737
|
+
const linkReq = await yn('Link to a requirement?');
|
|
738
|
+
if (linkReq) {
|
|
739
|
+
const req = await pickEntity(post, cfg.orgId, 'requirement', 'requirements');
|
|
740
|
+
if (req)
|
|
741
|
+
updates.requirement_id = req.id;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
else {
|
|
745
|
+
const unlink = await yn('Remove from current plan?');
|
|
746
|
+
if (unlink) {
|
|
747
|
+
updates.plan_id = null;
|
|
748
|
+
updates.requirement_id = null;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
break;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
511
754
|
if (Object.keys(updates).length > 0) {
|
|
512
755
|
await cmd(post, cfg.orgId, 'update_task', { task_id: t.id, updates }, 'Updating...');
|
|
513
756
|
}
|
|
@@ -977,14 +1220,67 @@ async function mTables(cfg, post) {
|
|
|
977
1220
|
if (!fn)
|
|
978
1221
|
break;
|
|
979
1222
|
const ft = await sel('Column type', [
|
|
980
|
-
{ value: 'text', label: 'Text' },
|
|
981
|
-
{ value: '
|
|
982
|
-
{ value: '
|
|
1223
|
+
{ value: 'text', label: 'Text' },
|
|
1224
|
+
{ value: 'number', label: 'Number' },
|
|
1225
|
+
{ value: 'date', label: 'Date' },
|
|
1226
|
+
{ value: 'select', label: 'Dropdown', hint: 'pick from a list of values' },
|
|
1227
|
+
{ value: 'multi_select', label: 'Multi-select dropdown', hint: 'pick multiple values' },
|
|
1228
|
+
{ value: 'checkbox', label: 'Checkbox', hint: 'yes/no toggle' },
|
|
1229
|
+
{ value: 'file', label: 'File upload' },
|
|
1230
|
+
{ value: 'url', label: 'URL / link' },
|
|
1231
|
+
{ value: 'email', label: 'Email address' },
|
|
1232
|
+
{ value: 'phone', label: 'Phone number' },
|
|
1233
|
+
{ value: 'currency', label: 'Currency / money' },
|
|
1234
|
+
{ value: 'percent', label: 'Percentage' },
|
|
1235
|
+
{ value: 'rating', label: 'Rating (1-5)' },
|
|
1236
|
+
{ value: 'relation', label: 'Link to another table' },
|
|
983
1237
|
]);
|
|
984
1238
|
if (!ft)
|
|
985
1239
|
break;
|
|
1240
|
+
const col = { name: fn, type: ft };
|
|
986
1241
|
const req = await yn('Required?');
|
|
987
|
-
|
|
1242
|
+
if (req)
|
|
1243
|
+
col.required = true;
|
|
1244
|
+
// Dropdown values
|
|
1245
|
+
if (ft === 'select' || ft === 'multi_select') {
|
|
1246
|
+
const optSource = await sel('Where do the options come from?', [
|
|
1247
|
+
{ value: 'manual', label: 'I\'ll type them now' },
|
|
1248
|
+
{ value: 'list', label: 'Link to an existing table/list' },
|
|
1249
|
+
]);
|
|
1250
|
+
if (optSource === 'manual') {
|
|
1251
|
+
const optStr = await txt('Options (comma-separated)', true, 'e.g. Pass, Fail, N/A');
|
|
1252
|
+
if (optStr)
|
|
1253
|
+
col.options = optStr.split(',').map(s => s.trim()).filter(Boolean);
|
|
1254
|
+
}
|
|
1255
|
+
else if (optSource === 'list') {
|
|
1256
|
+
const srcTable = await pickEntity(post, cfg.orgId, 'table', 'tables');
|
|
1257
|
+
if (srcTable) {
|
|
1258
|
+
col.source_table_id = srcTable.id;
|
|
1259
|
+
const srcField = await txt('Which column to use as the option label?', false, 'name');
|
|
1260
|
+
if (srcField)
|
|
1261
|
+
col.source_field = srcField;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
const defaultVal = await txt('Default value (blank for none)');
|
|
1265
|
+
if (defaultVal)
|
|
1266
|
+
col.default = defaultVal;
|
|
1267
|
+
}
|
|
1268
|
+
// Number/currency constraints
|
|
1269
|
+
if (ft === 'number' || ft === 'currency' || ft === 'percent') {
|
|
1270
|
+
const min = await txt('Minimum value (blank for no limit)');
|
|
1271
|
+
if (min)
|
|
1272
|
+
col.min = parseFloat(min);
|
|
1273
|
+
const max = await txt('Maximum value (blank for no limit)');
|
|
1274
|
+
if (max)
|
|
1275
|
+
col.max = parseFloat(max);
|
|
1276
|
+
}
|
|
1277
|
+
// Relation target
|
|
1278
|
+
if (ft === 'relation') {
|
|
1279
|
+
const relTable = await pickEntity(post, cfg.orgId, 'table', 'tables');
|
|
1280
|
+
if (relTable)
|
|
1281
|
+
col.relation_table_id = relTable.id;
|
|
1282
|
+
}
|
|
1283
|
+
fields.push(col);
|
|
988
1284
|
addMore = !!(await yn('Add another column?'));
|
|
989
1285
|
}
|
|
990
1286
|
if (fields.length > 0)
|
|
@@ -1478,20 +1774,37 @@ async function mAutomation(cfg, post) {
|
|
|
1478
1774
|
return mAutomation(cfg, post);
|
|
1479
1775
|
}
|
|
1480
1776
|
case 'val-create': {
|
|
1481
|
-
|
|
1482
|
-
|
|
1777
|
+
// Choose target: task or requirement
|
|
1778
|
+
const target = await sel('Apply this rule to', [
|
|
1779
|
+
{ value: 'task', label: 'A specific task' },
|
|
1780
|
+
{ value: 'requirement', label: 'A requirement', hint: 'applies to all tasks under it' },
|
|
1781
|
+
]);
|
|
1782
|
+
if (!target)
|
|
1483
1783
|
return mAutomation(cfg, post);
|
|
1784
|
+
const rulePayload = {};
|
|
1785
|
+
if (target === 'task') {
|
|
1786
|
+
const task = await pickEntity(post, cfg.orgId, 'task', 'tasks');
|
|
1787
|
+
if (!task)
|
|
1788
|
+
return mAutomation(cfg, post);
|
|
1789
|
+
rulePayload.task_id = task.id;
|
|
1790
|
+
}
|
|
1791
|
+
else {
|
|
1792
|
+
const req = await pickEntity(post, cfg.orgId, 'requirement', 'requirements');
|
|
1793
|
+
if (!req)
|
|
1794
|
+
return mAutomation(cfg, post);
|
|
1795
|
+
rulePayload.requirement_id = req.id;
|
|
1796
|
+
}
|
|
1484
1797
|
const name = await txt('Rule name', true, 'e.g. Require photo proof');
|
|
1485
1798
|
if (!name)
|
|
1486
1799
|
return mAutomation(cfg, post);
|
|
1487
1800
|
const ruleType = await sel('Rule type', [
|
|
1488
|
-
{ value: 'proof_required', label: 'Proof required', hint: 'photo, document, location,
|
|
1489
|
-
{ value: 'field_constraint', label: 'Field constraint', hint: 'validate a
|
|
1801
|
+
{ value: 'proof_required', label: 'Proof required', hint: 'photo, document, location, checklist, or table' },
|
|
1802
|
+
{ value: 'field_constraint', label: 'Field constraint', hint: 'validate a specific field value' },
|
|
1803
|
+
{ value: 'cross_field', label: 'Cross-field validation', hint: 'compare two fields' },
|
|
1804
|
+
{ value: 'aggregate', label: 'Aggregate check', hint: 'count/sum/avg of historical data' },
|
|
1490
1805
|
{ value: 'approval_required', label: 'Approval required', hint: 'needs manager sign-off' },
|
|
1491
1806
|
{ value: 'time_window', label: 'Time window', hint: 'must be done within a time range' },
|
|
1492
|
-
{ value: 'record_exists', label: 'Record exists', hint: 'verify
|
|
1493
|
-
{ value: 'cross_field', label: 'Cross-field validation' },
|
|
1494
|
-
{ value: 'aggregate', label: 'Aggregate check' },
|
|
1807
|
+
{ value: 'record_exists', label: 'Record exists', hint: 'verify a table record was created' },
|
|
1495
1808
|
]);
|
|
1496
1809
|
if (!ruleType)
|
|
1497
1810
|
return mAutomation(cfg, post);
|
|
@@ -1499,21 +1812,201 @@ async function mAutomation(cfg, post) {
|
|
|
1499
1812
|
if (ruleType === 'proof_required') {
|
|
1500
1813
|
const proofType = await sel('What kind of proof?', [
|
|
1501
1814
|
{ value: 'photo', label: 'Photo' }, { value: 'document', label: 'Document' },
|
|
1502
|
-
{ value: 'geolocation', label: 'Location' }, { value: 'checklist', label: 'Checklist' },
|
|
1815
|
+
{ value: 'geolocation', label: 'Location check-in' }, { value: 'checklist', label: 'Checklist' },
|
|
1816
|
+
{ value: 'table', label: 'Table record' },
|
|
1817
|
+
]);
|
|
1818
|
+
if (proofType) {
|
|
1819
|
+
condition.validation_type = proofType;
|
|
1820
|
+
// Let them pick the specific asset
|
|
1821
|
+
if (proofType === 'photo') {
|
|
1822
|
+
const ph = await pickEntity(post, cfg.orgId, 'photo', 'photo assets');
|
|
1823
|
+
if (ph)
|
|
1824
|
+
condition.config_id = ph.id;
|
|
1825
|
+
}
|
|
1826
|
+
else if (proofType === 'document') {
|
|
1827
|
+
const doc = await pickEntity(post, cfg.orgId, 'document', 'document folders');
|
|
1828
|
+
if (doc) {
|
|
1829
|
+
condition.config_id = doc.id;
|
|
1830
|
+
const tMode = await sel('Upload destination', [
|
|
1831
|
+
{ value: 'locked', label: 'Always to this folder' },
|
|
1832
|
+
{ value: 'user_choice', label: 'Let user choose' },
|
|
1833
|
+
]);
|
|
1834
|
+
if (tMode) {
|
|
1835
|
+
condition.target_mode = tMode;
|
|
1836
|
+
if (tMode === 'locked')
|
|
1837
|
+
condition.target_folder_id = doc.id;
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
else if (proofType === 'geolocation') {
|
|
1842
|
+
const area = await pickEntity(post, cfg.orgId, 'geolocation_area', 'location areas');
|
|
1843
|
+
if (area)
|
|
1844
|
+
condition.config_id = area.id;
|
|
1845
|
+
}
|
|
1846
|
+
else if (proofType === 'checklist') {
|
|
1847
|
+
const cl = await pickEntity(post, cfg.orgId, 'checklist', 'checklists');
|
|
1848
|
+
if (cl)
|
|
1849
|
+
condition.config_id = cl.id;
|
|
1850
|
+
}
|
|
1851
|
+
else if (proofType === 'table') {
|
|
1852
|
+
const tbl = await pickEntity(post, cfg.orgId, 'table', 'tables');
|
|
1853
|
+
if (tbl)
|
|
1854
|
+
condition.config_id = tbl.id;
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
else if (ruleType === 'field_constraint') {
|
|
1859
|
+
const field = await txt('Field name', true, 'e.g. status, temperature, score');
|
|
1860
|
+
if (field)
|
|
1861
|
+
condition.field = field;
|
|
1862
|
+
const op = await sel('Condition', [
|
|
1863
|
+
{ value: 'eq', label: 'Equals' }, { value: 'neq', label: 'Not equals' },
|
|
1864
|
+
{ value: 'gt', label: 'Greater than' }, { value: 'gte', label: 'Greater than or equal' },
|
|
1865
|
+
{ value: 'lt', label: 'Less than' }, { value: 'lte', label: 'Less than or equal' },
|
|
1866
|
+
{ value: 'in', label: 'Is one of (list)' }, { value: 'not_in', label: 'Is not one of (list)' },
|
|
1867
|
+
{ value: 'is_null', label: 'Is empty' }, { value: 'is_not_null', label: 'Is not empty' },
|
|
1868
|
+
{ value: 'matches', label: 'Matches pattern' },
|
|
1869
|
+
]);
|
|
1870
|
+
if (op)
|
|
1871
|
+
condition.operator = op;
|
|
1872
|
+
if (op && !['is_null', 'is_not_null'].includes(op)) {
|
|
1873
|
+
if (op === 'in' || op === 'not_in') {
|
|
1874
|
+
const vals = await txt('Values (comma-separated)', true);
|
|
1875
|
+
if (vals)
|
|
1876
|
+
condition.value = vals.split(',').map(s => s.trim());
|
|
1877
|
+
}
|
|
1878
|
+
else {
|
|
1879
|
+
const val = await txt('Value', true);
|
|
1880
|
+
if (val)
|
|
1881
|
+
condition.value = isNaN(Number(val)) ? val : Number(val);
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
else if (ruleType === 'cross_field') {
|
|
1886
|
+
const f1 = await txt('First field', true);
|
|
1887
|
+
if (f1)
|
|
1888
|
+
condition.field = f1;
|
|
1889
|
+
const cop = await sel('Must be', [
|
|
1890
|
+
{ value: 'eq', label: 'Equal to' }, { value: 'neq', label: 'Not equal to' },
|
|
1891
|
+
{ value: 'gt', label: 'Greater than' }, { value: 'gte', label: 'Greater or equal to' },
|
|
1892
|
+
{ value: 'lt', label: 'Less than' }, { value: 'lte', label: 'Less or equal to' },
|
|
1893
|
+
]);
|
|
1894
|
+
if (cop)
|
|
1895
|
+
condition.compare_operator = cop;
|
|
1896
|
+
const f2 = await txt('Second field', true);
|
|
1897
|
+
if (f2)
|
|
1898
|
+
condition.compare_field = f2;
|
|
1899
|
+
}
|
|
1900
|
+
else if (ruleType === 'aggregate') {
|
|
1901
|
+
const aggFn = await sel('Aggregate function', [
|
|
1902
|
+
{ value: 'count', label: 'Count' }, { value: 'sum', label: 'Sum' },
|
|
1903
|
+
{ value: 'avg', label: 'Average' }, { value: 'min', label: 'Minimum' },
|
|
1904
|
+
{ value: 'max', label: 'Maximum' },
|
|
1905
|
+
]);
|
|
1906
|
+
if (aggFn)
|
|
1907
|
+
condition.aggregate_function = aggFn;
|
|
1908
|
+
condition.aggregate_table = 'task_history';
|
|
1909
|
+
if (aggFn !== 'count') {
|
|
1910
|
+
const aggField = await txt('Field to aggregate', true);
|
|
1911
|
+
if (aggField)
|
|
1912
|
+
condition.aggregate_field = aggField;
|
|
1913
|
+
}
|
|
1914
|
+
const aggOp = await sel('Result must be', [
|
|
1915
|
+
{ value: 'eq', label: 'Equal to' }, { value: 'gt', label: 'Greater than' },
|
|
1916
|
+
{ value: 'gte', label: 'At least' }, { value: 'lt', label: 'Less than' },
|
|
1917
|
+
{ value: 'lte', label: 'At most' },
|
|
1503
1918
|
]);
|
|
1504
|
-
if (
|
|
1505
|
-
condition.
|
|
1919
|
+
if (aggOp)
|
|
1920
|
+
condition.aggregate_operator = aggOp;
|
|
1921
|
+
const aggVal = await txt('Target value', true, 'e.g. 5');
|
|
1922
|
+
if (aggVal)
|
|
1923
|
+
condition.aggregate_value = parseFloat(aggVal);
|
|
1924
|
+
}
|
|
1925
|
+
else if (ruleType === 'approval_required') {
|
|
1926
|
+
const approverType = await sel('Who must approve?', [
|
|
1927
|
+
{ value: 'role', label: 'Anyone with a specific role' },
|
|
1928
|
+
{ value: 'user', label: 'A specific person' },
|
|
1929
|
+
]);
|
|
1930
|
+
if (approverType === 'role') {
|
|
1931
|
+
const role = await sel('Required role', [
|
|
1932
|
+
{ value: 'admin', label: 'Admin' }, { value: 'super_admin', label: 'Super Admin' },
|
|
1933
|
+
]);
|
|
1934
|
+
if (role)
|
|
1935
|
+
condition.approval_approver_role = role;
|
|
1936
|
+
}
|
|
1937
|
+
else if (approverType === 'user') {
|
|
1938
|
+
const uid = await txt('Approver user ID', true);
|
|
1939
|
+
if (uid)
|
|
1940
|
+
condition.approval_approver_id = uid;
|
|
1941
|
+
}
|
|
1506
1942
|
}
|
|
1507
1943
|
else if (ruleType === 'time_window') {
|
|
1508
|
-
const
|
|
1509
|
-
|
|
1944
|
+
const wType = await sel('Window type', [
|
|
1945
|
+
{ value: 'daily', label: 'Daily time window' },
|
|
1946
|
+
{ value: 'custom', label: 'Custom date/time range' },
|
|
1947
|
+
]);
|
|
1948
|
+
if (wType)
|
|
1949
|
+
condition.window_type = wType;
|
|
1950
|
+
const start = await txt('Start time (HH:MM)', true, '08:00');
|
|
1510
1951
|
if (start)
|
|
1511
1952
|
condition.start_time = start;
|
|
1953
|
+
const end = await txt('End time (HH:MM)', true, '17:00');
|
|
1512
1954
|
if (end)
|
|
1513
1955
|
condition.end_time = end;
|
|
1956
|
+
const tz = await txt('Timezone (blank for UTC)', false, 'America/New_York');
|
|
1957
|
+
if (tz)
|
|
1958
|
+
condition.timezone = tz;
|
|
1959
|
+
}
|
|
1960
|
+
else if (ruleType === 'record_exists') {
|
|
1961
|
+
const tbl = await pickEntity(post, cfg.orgId, 'table', 'tables');
|
|
1962
|
+
if (tbl) {
|
|
1963
|
+
condition.table = tbl.id;
|
|
1964
|
+
const addFilters = await yn('Add filters? (only match records with specific values)');
|
|
1965
|
+
if (addFilters) {
|
|
1966
|
+
const filters = [];
|
|
1967
|
+
let addF = true;
|
|
1968
|
+
while (addF) {
|
|
1969
|
+
const ff = await txt('Field name', true);
|
|
1970
|
+
if (!ff)
|
|
1971
|
+
break;
|
|
1972
|
+
const fop = await sel('Condition', [
|
|
1973
|
+
{ value: 'eq', label: 'Equals' }, { value: 'neq', label: 'Not equals' },
|
|
1974
|
+
{ value: 'gt', label: 'Greater than' }, { value: 'lt', label: 'Less than' },
|
|
1975
|
+
{ value: 'is_not_null', label: 'Has a value' },
|
|
1976
|
+
]);
|
|
1977
|
+
if (!fop)
|
|
1978
|
+
break;
|
|
1979
|
+
if (fop === 'is_not_null') {
|
|
1980
|
+
filters.push({ field: ff, operator: fop, value: null });
|
|
1981
|
+
}
|
|
1982
|
+
else {
|
|
1983
|
+
const fv = await txt('Value', true);
|
|
1984
|
+
if (fv)
|
|
1985
|
+
filters.push({ field: ff, operator: fop, value: isNaN(Number(fv)) ? fv : Number(fv) });
|
|
1986
|
+
}
|
|
1987
|
+
addF = !!(await yn('Add another filter?'));
|
|
1988
|
+
}
|
|
1989
|
+
if (filters.length > 0)
|
|
1990
|
+
condition.filters = filters;
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
// Grouping options
|
|
1995
|
+
const addGroup = await yn('Add to a rule group? (for AND/OR logic with other rules)');
|
|
1996
|
+
if (addGroup) {
|
|
1997
|
+
const gid = await txt('Group ID (create a new UUID or use an existing group ID)', true);
|
|
1998
|
+
if (gid) {
|
|
1999
|
+
rulePayload.group_id = gid;
|
|
2000
|
+
const gop = await sel('Group logic', [
|
|
2001
|
+
{ value: 'AND', label: 'AND — all rules in the group must pass' },
|
|
2002
|
+
{ value: 'OR', label: 'OR — any rule in the group can pass' },
|
|
2003
|
+
]);
|
|
2004
|
+
if (gop)
|
|
2005
|
+
rulePayload.group_operator = gop;
|
|
2006
|
+
}
|
|
1514
2007
|
}
|
|
1515
2008
|
await cmd(post, cfg.orgId, 'create_validation_rule', {
|
|
1516
|
-
|
|
2009
|
+
...rulePayload, name, rule_type: ruleType, condition,
|
|
1517
2010
|
}, 'Creating validation rule...');
|
|
1518
2011
|
return mAutomation(cfg, post);
|
|
1519
2012
|
}
|
|
@@ -1589,25 +2082,100 @@ async function mAutomation(cfg, post) {
|
|
|
1589
2082
|
if (!name)
|
|
1590
2083
|
return mAutomation(cfg, post);
|
|
1591
2084
|
const eventType = await sel('What triggers this?', [
|
|
1592
|
-
{ value: 'record_created', label: 'New record added to a table' },
|
|
1593
|
-
{ value: 'record_updated', label: 'Record updated in a table' },
|
|
1594
2085
|
{ value: 'task_completed', label: 'Another task completed' },
|
|
1595
|
-
{ value: '
|
|
1596
|
-
{ value: '
|
|
2086
|
+
{ value: 'task_uncompleted', label: 'A task was reopened' },
|
|
2087
|
+
{ value: 'task_created', label: 'A new task was created' },
|
|
2088
|
+
{ value: 'task_updated', label: 'A task was updated' },
|
|
2089
|
+
{ value: 'validation_submitted', label: 'Proof was submitted' },
|
|
2090
|
+
{ value: 'approval_requested', label: 'An approval was requested' },
|
|
2091
|
+
{ value: 'approval_approved', label: 'An approval was granted' },
|
|
2092
|
+
{ value: 'approval_rejected', label: 'An approval was rejected' },
|
|
2093
|
+
{ value: 'table_record_created', label: 'New record added to a table' },
|
|
2094
|
+
{ value: 'conditional_answer', label: 'A conditional question was answered' },
|
|
2095
|
+
{ value: 'document_created', label: 'A document was uploaded' },
|
|
2096
|
+
{ value: 'checklist_created', label: 'A checklist was created' },
|
|
1597
2097
|
]);
|
|
1598
2098
|
if (!eventType)
|
|
1599
2099
|
return mAutomation(cfg, post);
|
|
1600
|
-
const sourceType = await sel('Source type', [
|
|
1601
|
-
{ value: '
|
|
2100
|
+
const sourceType = await sel('Source type (what entity fires the event)', [
|
|
2101
|
+
{ value: 'task', label: 'Task' },
|
|
2102
|
+
{ value: 'table', label: 'Table' },
|
|
2103
|
+
{ value: 'document', label: 'Document' },
|
|
2104
|
+
{ value: 'checklist', label: 'Checklist' },
|
|
2105
|
+
{ value: 'geolocation_area', label: 'Location area' },
|
|
1602
2106
|
{ value: 'approval', label: 'Approval' },
|
|
2107
|
+
{ value: 'plan', label: 'Plan' },
|
|
1603
2108
|
]);
|
|
1604
2109
|
if (!sourceType)
|
|
1605
2110
|
return mAutomation(cfg, post);
|
|
1606
|
-
const
|
|
1607
|
-
const
|
|
1608
|
-
if (
|
|
1609
|
-
|
|
1610
|
-
|
|
2111
|
+
const trigPayload = { task_id: task.id, name, event_type: eventType, source_entity_type: sourceType };
|
|
2112
|
+
const pickSource = await yn(`Pick a specific ${sourceType}? (or trigger on any)`);
|
|
2113
|
+
if (pickSource) {
|
|
2114
|
+
const source = await pickEntity(post, cfg.orgId, sourceType, `${sourceType}s`);
|
|
2115
|
+
if (source)
|
|
2116
|
+
trigPayload.source_entity_id = source.id;
|
|
2117
|
+
}
|
|
2118
|
+
// Conditions
|
|
2119
|
+
const addConds = await yn('Add conditions? (only trigger when specific criteria are met)');
|
|
2120
|
+
if (addConds) {
|
|
2121
|
+
const conditions = [];
|
|
2122
|
+
let addC = true;
|
|
2123
|
+
while (addC) {
|
|
2124
|
+
const cf = await txt('Field to check', true, 'e.g. status, frequency, name');
|
|
2125
|
+
if (!cf)
|
|
2126
|
+
break;
|
|
2127
|
+
const cop = await sel('Condition', [
|
|
2128
|
+
{ value: 'eq', label: 'Equals' }, { value: 'neq', label: 'Not equals' },
|
|
2129
|
+
{ value: 'gt', label: 'Greater than' }, { value: 'lt', label: 'Less than' },
|
|
2130
|
+
{ value: 'in', label: 'Is one of' }, { value: 'is_not_null', label: 'Has a value' },
|
|
2131
|
+
]);
|
|
2132
|
+
if (!cop)
|
|
2133
|
+
break;
|
|
2134
|
+
const csrc = await sel('Check against', [
|
|
2135
|
+
{ value: 'payload', label: 'Event data (payload)' },
|
|
2136
|
+
{ value: 'entity', label: 'Entity state' },
|
|
2137
|
+
]);
|
|
2138
|
+
let cv = null;
|
|
2139
|
+
if (cop !== 'is_not_null') {
|
|
2140
|
+
if (cop === 'in') {
|
|
2141
|
+
const cvStr = await txt('Values (comma-separated)', true);
|
|
2142
|
+
if (cvStr)
|
|
2143
|
+
cv = cvStr.split(',').map(s => s.trim());
|
|
2144
|
+
}
|
|
2145
|
+
else {
|
|
2146
|
+
const cvTxt = await txt('Value', true);
|
|
2147
|
+
if (cvTxt)
|
|
2148
|
+
cv = isNaN(Number(cvTxt)) ? cvTxt : Number(cvTxt);
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
conditions.push({ field: cf, operator: cop, value: cv, ...(csrc ? { source: csrc } : {}) });
|
|
2152
|
+
addC = !!(await yn('Add another condition?'));
|
|
2153
|
+
}
|
|
2154
|
+
if (conditions.length > 0) {
|
|
2155
|
+
trigPayload.conditions = conditions;
|
|
2156
|
+
if (conditions.length > 1) {
|
|
2157
|
+
const logic = await sel('Condition logic', [
|
|
2158
|
+
{ value: 'AND', label: 'ALL conditions must be true' },
|
|
2159
|
+
{ value: 'OR', label: 'ANY condition can be true' },
|
|
2160
|
+
]);
|
|
2161
|
+
if (logic)
|
|
2162
|
+
trigPayload.condition_operator = logic;
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
// Advanced options
|
|
2167
|
+
const advanced = await yn('Configure advanced options? (frequency, message, debounce)');
|
|
2168
|
+
if (advanced) {
|
|
2169
|
+
const respect = await yn('Respect task frequency? (only activate on scheduled days)');
|
|
2170
|
+
trigPayload.respect_frequency = !!respect;
|
|
2171
|
+
const msg = await txt('Activation message (shown when triggered)');
|
|
2172
|
+
if (msg)
|
|
2173
|
+
trigPayload.activation_message = msg;
|
|
2174
|
+
const debounce = await txt('Debounce seconds (prevent re-triggering too fast)', false, '60');
|
|
2175
|
+
if (debounce)
|
|
2176
|
+
trigPayload.debounce_seconds = parseInt(debounce, 10);
|
|
2177
|
+
}
|
|
2178
|
+
await cmd(post, cfg.orgId, 'create_trigger_rule', trigPayload, 'Creating trigger rule...');
|
|
1611
2179
|
return mAutomation(cfg, post);
|
|
1612
2180
|
}
|
|
1613
2181
|
case 'trig-edit': {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "checkbox-cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.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",
|