checkbox-cli 3.0.1 → 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 +610 -74
- 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.1.0';
|
|
20
20
|
function loadConfig() {
|
|
21
21
|
try {
|
|
22
22
|
if (!existsSync(CONFIG_FILE))
|
|
@@ -480,40 +480,149 @@ async function mTasks(cfg, post) {
|
|
|
480
480
|
if (!frequency)
|
|
481
481
|
return mTasks(cfg, post);
|
|
482
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
|
+
}
|
|
483
505
|
const addPlan = await yn('Add to a plan?');
|
|
484
506
|
if (addPlan) {
|
|
485
507
|
const plan = await pickEntity(post, cfg.orgId, 'plan', 'plans');
|
|
486
|
-
if (plan)
|
|
508
|
+
if (plan) {
|
|
487
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
|
+
}
|
|
488
518
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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?'));
|
|
510
616
|
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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);
|
|
517
626
|
}
|
|
518
627
|
await cmd(post, cfg.orgId, 'create_task', payload, 'Creating task...');
|
|
519
628
|
return mTasks(cfg, post);
|
|
@@ -522,24 +631,126 @@ async function mTasks(cfg, post) {
|
|
|
522
631
|
const t = await pickEntity(post, cfg.orgId, 'task', 'tasks');
|
|
523
632
|
if (!t)
|
|
524
633
|
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: '
|
|
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') },
|
|
537
646
|
]);
|
|
538
|
-
if (
|
|
539
|
-
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
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
|
+
}
|
|
543
754
|
if (Object.keys(updates).length > 0) {
|
|
544
755
|
await cmd(post, cfg.orgId, 'update_task', { task_id: t.id, updates }, 'Updating...');
|
|
545
756
|
}
|
|
@@ -1009,14 +1220,67 @@ async function mTables(cfg, post) {
|
|
|
1009
1220
|
if (!fn)
|
|
1010
1221
|
break;
|
|
1011
1222
|
const ft = await sel('Column type', [
|
|
1012
|
-
{ value: 'text', label: 'Text' },
|
|
1013
|
-
{ value: '
|
|
1014
|
-
{ 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' },
|
|
1015
1237
|
]);
|
|
1016
1238
|
if (!ft)
|
|
1017
1239
|
break;
|
|
1240
|
+
const col = { name: fn, type: ft };
|
|
1018
1241
|
const req = await yn('Required?');
|
|
1019
|
-
|
|
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);
|
|
1020
1284
|
addMore = !!(await yn('Add another column?'));
|
|
1021
1285
|
}
|
|
1022
1286
|
if (fields.length > 0)
|
|
@@ -1510,20 +1774,37 @@ async function mAutomation(cfg, post) {
|
|
|
1510
1774
|
return mAutomation(cfg, post);
|
|
1511
1775
|
}
|
|
1512
1776
|
case 'val-create': {
|
|
1513
|
-
|
|
1514
|
-
|
|
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)
|
|
1515
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
|
+
}
|
|
1516
1797
|
const name = await txt('Rule name', true, 'e.g. Require photo proof');
|
|
1517
1798
|
if (!name)
|
|
1518
1799
|
return mAutomation(cfg, post);
|
|
1519
1800
|
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
|
|
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' },
|
|
1522
1805
|
{ value: 'approval_required', label: 'Approval required', hint: 'needs manager sign-off' },
|
|
1523
1806
|
{ 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' },
|
|
1807
|
+
{ value: 'record_exists', label: 'Record exists', hint: 'verify a table record was created' },
|
|
1527
1808
|
]);
|
|
1528
1809
|
if (!ruleType)
|
|
1529
1810
|
return mAutomation(cfg, post);
|
|
@@ -1531,21 +1812,201 @@ async function mAutomation(cfg, post) {
|
|
|
1531
1812
|
if (ruleType === 'proof_required') {
|
|
1532
1813
|
const proofType = await sel('What kind of proof?', [
|
|
1533
1814
|
{ value: 'photo', label: 'Photo' }, { value: 'document', label: 'Document' },
|
|
1534
|
-
{ 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' },
|
|
1918
|
+
]);
|
|
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' },
|
|
1535
1929
|
]);
|
|
1536
|
-
if (
|
|
1537
|
-
|
|
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
|
+
}
|
|
1538
1942
|
}
|
|
1539
1943
|
else if (ruleType === 'time_window') {
|
|
1540
|
-
const
|
|
1541
|
-
|
|
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');
|
|
1542
1951
|
if (start)
|
|
1543
1952
|
condition.start_time = start;
|
|
1953
|
+
const end = await txt('End time (HH:MM)', true, '17:00');
|
|
1544
1954
|
if (end)
|
|
1545
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
|
+
}
|
|
1546
2007
|
}
|
|
1547
2008
|
await cmd(post, cfg.orgId, 'create_validation_rule', {
|
|
1548
|
-
|
|
2009
|
+
...rulePayload, name, rule_type: ruleType, condition,
|
|
1549
2010
|
}, 'Creating validation rule...');
|
|
1550
2011
|
return mAutomation(cfg, post);
|
|
1551
2012
|
}
|
|
@@ -1621,25 +2082,100 @@ async function mAutomation(cfg, post) {
|
|
|
1621
2082
|
if (!name)
|
|
1622
2083
|
return mAutomation(cfg, post);
|
|
1623
2084
|
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
2085
|
{ value: 'task_completed', label: 'Another task completed' },
|
|
1627
|
-
{ value: '
|
|
1628
|
-
{ 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' },
|
|
1629
2097
|
]);
|
|
1630
2098
|
if (!eventType)
|
|
1631
2099
|
return mAutomation(cfg, post);
|
|
1632
|
-
const sourceType = await sel('Source type', [
|
|
1633
|
-
{ 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' },
|
|
1634
2106
|
{ value: 'approval', label: 'Approval' },
|
|
2107
|
+
{ value: 'plan', label: 'Plan' },
|
|
1635
2108
|
]);
|
|
1636
2109
|
if (!sourceType)
|
|
1637
2110
|
return mAutomation(cfg, post);
|
|
1638
|
-
const
|
|
1639
|
-
const
|
|
1640
|
-
if (
|
|
1641
|
-
|
|
1642
|
-
|
|
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...');
|
|
1643
2179
|
return mAutomation(cfg, post);
|
|
1644
2180
|
}
|
|
1645
2181
|
case 'trig-edit': {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "checkbox-cli",
|
|
3
|
-
"version": "3.0
|
|
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",
|