conduyt 1.71.0 → 1.73.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/index.js +78 -21
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { readFileSync, writeFileSync } from "node:fs";
|
|
|
3
3
|
import { addPoolFilterSet, listPoolFilterSets, removePoolFilterSet, updatePoolFilterSet } from "./pool-filter-sets.js";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
|
-
import { Command, CommanderError } from "commander";
|
|
6
|
+
import { Command, CommanderError, Option } from "commander";
|
|
7
7
|
import { ConduytClient, buildQuery } from "./client.js";
|
|
8
8
|
import { minimumGradeOf, minimumGradePatch, parseGradeFlag } from "./message-check-minimum.js";
|
|
9
9
|
import { splitList, stageRulePatch, validateCardFields, validateRuleChange } from "./board-settings.js";
|
|
@@ -2121,6 +2121,10 @@ messages
|
|
|
2121
2121
|
})}`);
|
|
2122
2122
|
}));
|
|
2123
2123
|
// ---- appointments ----
|
|
2124
|
+
/** (app 10-13) a booking can name the deal (card) it came from */
|
|
2125
|
+
const APPOINTMENT_DEAL_NOTE = " contactId is looked up as a contact of your account first; when no contact has that id it is looked up as a deal (card), so a deal id may be passed there. dealId names the deal explicitly. A named deal must be open, in your account, and visible AND editable to the key (deals scope; seeing it is not enough): the appointment takes the deal's contact, is linked to the deal, and appointment.booked carries dealId; no other card is created. Errors: 400 deal_id_conflict / deal_contact_mismatch / invalid_deal_id / contact_not_found (contactId is neither a contact nor a deal you can use), 404 deal_not_found / deal_deleted, 403 deal_not_editable (you can see the deal but not change it), 409 deal_closed, 409 deal_contact_gone (the deal's contact was merged or deleted), 409 deal_changed (the deal moved to another contact while booking: retry the request).";
|
|
2126
|
+
/** (app 10-13) the catalog does not carry automation action config: find_opportunity v2, link_appointment_to_deal, update_deal_field systemField source */
|
|
2127
|
+
const DEAL_NODES_NOTE = " Deal nodes: find_opportunity config {version: 2, pipelineIds (ordered; the first pipeline holding a match wins; empty = every pipeline), status (open default | won | lost | abandoned | any), excludeSources (lead sources never matched, case-insensitive), excludeCreatedByAutomationIds (automations whose create_deal cards are skipped; cards created from 2026-10-13 on), matchField {fieldKey, value} (searches the WHOLE WORKSPACE, any contact, for the card whose deal custom field equals the value, merge fields allowed e.g. {{trigger.oes}}; a card is found when its field holds the searched value (trimmed) as the same text exactly (no case folding); a value written with separators such as dashes or spaces also matches its digits as text; it matches a stored number only when the value (or those digits) is a plain number with no leading zero and at most 15 significant digits that a number holds exactly (007 is text and does not match the number 7; a 16-digit value, 1e-400 or 3e-324 is text only); booleans and differently formatted stored values never match; status, pipelines and exclusions still apply; exactly one match is found, several are ambiguous and take not found; a found card becomes the run's contact when the run has none; the field must exist; an empty rendered value finds nothing), resolution}: searches the run's contact's cards; FOUND makes the card the run's deal and follows nextIfTrue (else next), NOT FOUND is a normal outcome that follows nextIfFalse (else next) and leaves the run's existing deal unchanged; without version 2 the step keeps the original single-pipeline lookup (a miss fails the step). Prerequisite: a version 2 find needs step cursors on this workspace; publishing refuses (find_needs_step_cursors) with \"Find opportunity needs step cursors on this workspace; ask support\", so a workflow can validate and import yet fail at publish; every version 2 find must hold its own live step cursor or it hard-stops the run without selecting or routing. A version 2 find cannot be retried on its own: retrying a failed one alone answers 409, so re-trigger the automation for the contact. An automation definition that touches deals needs the `deals` scope on an API key (403 without it). link_appointment_to_deal config {stageId, pipelineId (needs stageId), lostReason}: in an appointment-triggered run, links the trigger's appointment to the run's deal (its appointment fields follow it; a newer booking is never displaced; the appointment must be the deal's contact's), then optionally moves the card; it needs an appointment trigger and the run's deal (find_opportunity first, or a booking made with a deal id). update_deal_field config {systemField: \"source\", value, overwriteExisting}: sets the deal's own lead source instead of a custom field (a value only, trimmed, at most 500 characters; overwriteExisting false writes only while the source is the Unknown label).";
|
|
2124
2128
|
const appointments = program.command("appointments").description("Manage appointments");
|
|
2125
2129
|
appointments
|
|
2126
2130
|
.command("list")
|
|
@@ -2154,13 +2158,14 @@ appointments
|
|
|
2154
2158
|
}));
|
|
2155
2159
|
appointments
|
|
2156
2160
|
.command("create")
|
|
2157
|
-
.description("Create an appointment. --start/--end must be ISO-8601 WITH a timezone offset (e.g. 2026-07-10T15:00:00-07:00)")
|
|
2161
|
+
.description("Create an appointment. --start/--end must be ISO-8601 WITH a timezone offset (e.g. 2026-07-10T15:00:00-07:00)." + APPOINTMENT_DEAL_NOTE)
|
|
2158
2162
|
.option("--title <title>", "title (required)")
|
|
2159
2163
|
.option("--start <iso>", "start time, ISO-8601 with tz offset (required)")
|
|
2160
2164
|
.option("--end <iso>", "end time, ISO-8601 with tz offset (required)")
|
|
2161
2165
|
.option("--description <text>", "description")
|
|
2162
2166
|
.option("--calendar <id>", "calendar UUID")
|
|
2163
|
-
.option("--contact <id>", "contact UUID")
|
|
2167
|
+
.option("--contact <id>", "contact UUID, or a deal UUID (looked up as a deal when no contact has it)")
|
|
2168
|
+
.option("--deal <id>", "deal UUID: book for this deal (card; open, visible and editable to you); the appointment takes its contact and is linked to it")
|
|
2164
2169
|
.option("--assigned <id>", "assignee user UUID")
|
|
2165
2170
|
.option("--location <location>", "location")
|
|
2166
2171
|
.option("--status <status>", "status (default confirmed)")
|
|
@@ -2179,6 +2184,8 @@ appointments
|
|
|
2179
2184
|
body.calendarId = opts.calendar;
|
|
2180
2185
|
if (opts.contact !== undefined)
|
|
2181
2186
|
body.contactId = opts.contact;
|
|
2187
|
+
if (opts.deal !== undefined)
|
|
2188
|
+
body.dealId = opts.deal;
|
|
2182
2189
|
if (opts.assigned !== undefined)
|
|
2183
2190
|
body.assignedTo = opts.assigned;
|
|
2184
2191
|
if (opts.location !== undefined)
|
|
@@ -2331,13 +2338,13 @@ automations
|
|
|
2331
2338
|
}));
|
|
2332
2339
|
automations
|
|
2333
2340
|
.command("validate")
|
|
2334
|
-
.description("Validate a workflow graph before saving. Provide the body via --json or --file (needs triggerEvent + graph). A delay node waits for a fixed duration by default; waitType \"until_field_date\" (with fieldKey = a deal DATE / DATETIME custom field key and offsetHours = signed hours added to its date, default 0, e.g. 24 = a day after it) waits until that date stored on the deal, re-read at every wake (an empty or past value continues immediately). Card ownership per lane: assign_to_user config {userId | userIds (round robin) | groupId, target|applyTo: contact|deal|both, overwriteExisting, laneSource, lanePipelineId, laneStageId}: laneSource names a card lane (the Deal.source its Auto-Create step wrote, e.g. \"GSM\", max 500 chars) and makes the deal leg assign the contact's newest OPEN card from that lane (inside lanePipelineId when set) instead of the run's deal; when the contact has none and lanePipelineId is set, the card is created there (laneStageId, else the pipeline's first stage) already owned by the user; lanePipelineId or laneStageId without laneSource, or laneStageId without lanePipelineId, is refused; a lane never applies to an Assign-to-contact step. create_deal config {pipelineId (required), stageId, dealName, dealValue, allowDuplicates, status, source, assignedTo, leaveUnassigned}: leaveUnassigned true leaves the card unassigned when no assignee is set (default false: the card goes to the lead's owner). close_other_open_deals config {lostReason (required)}: closes the contact's OTHER open cards as lost with that reason, keeping the run's own card (each moves to its pipeline's lost stage with the usual stage-change effects; cards already won/lost, in the DPP Post-Close pipeline or in a pipeline with no lost stage are skipped; a re-run closes nothing twice); it needs the run's own deal, so use a deal trigger or a find_opportunity step first." + AUTOMATION_KEY_REFUSALS_NOTE)
|
|
2341
|
+
.description("Validate a workflow graph before saving. Provide the body via --json or --file (needs triggerEvent + graph). A delay node waits for a fixed duration by default; waitType \"until_field_date\" (with fieldKey = a deal DATE / DATETIME custom field key and offsetHours = signed hours added to its date, default 0, e.g. 24 = a day after it) waits until that date stored on the deal, re-read at every wake (an empty or past value continues immediately). Card ownership per lane: assign_to_user config {userId | userIds (round robin) | groupId, target|applyTo: contact|deal|both, overwriteExisting, laneSource, lanePipelineId, laneStageId}: laneSource names a card lane (the Deal.source its Auto-Create step wrote, e.g. \"GSM\", max 500 chars) and makes the deal leg assign the contact's newest OPEN card from that lane (inside lanePipelineId when set) instead of the run's deal; when the contact has none and lanePipelineId is set, the card is created there (laneStageId, else the pipeline's first stage) already owned by the user; lanePipelineId or laneStageId without laneSource, or laneStageId without lanePipelineId, is refused; a lane never applies to an Assign-to-contact step. create_deal config {pipelineId (required), stageId, dealName, dealValue, allowDuplicates, status, source, assignedTo, leaveUnassigned}: leaveUnassigned true leaves the card unassigned when no assignee is set (default false: the card goes to the lead's owner). close_other_open_deals config {lostReason (required)}: closes the contact's OTHER open cards as lost with that reason, keeping the run's own card (each moves to its pipeline's lost stage with the usual stage-change effects; cards already won/lost, in the DPP Post-Close pipeline or in a pipeline with no lost stage are skipped; a re-run closes nothing twice); it needs the run's own deal, so use a deal trigger or a find_opportunity step first." + DEAL_NODES_NOTE + AUTOMATION_KEY_REFUSALS_NOTE)
|
|
2335
2342
|
.option("--json <json>", "full request body as JSON")
|
|
2336
2343
|
.option("--file <path>", "read the request body from a JSON file")
|
|
2337
2344
|
.action(run(async (client, opts) => client.post("/api/v1/automations/validate", bodyFromJsonOrFile(opts))));
|
|
2338
2345
|
automations
|
|
2339
2346
|
.command("import")
|
|
2340
|
-
.description("Create a workflow (with optional name resolution). Provide the body via --json or --file (needs name + triggerEvent + graph). A sequence step is the action enroll_in_sequence with config {sequenceId, enrollAction: add (default) | remove}; add needs the sequence to allow automation enrollment. A delay node waits for a fixed duration by default; waitType \"until_field_date\" (with fieldKey = a deal DATE / DATETIME custom field key and offsetHours = signed hours added to its date, default 0, e.g. 24 = a day after it) waits until that date stored on the deal, re-read at every wake (an empty or past value continues immediately). Card ownership per lane: assign_to_user config {userId | userIds (round robin) | groupId, target|applyTo: contact|deal|both, overwriteExisting, laneSource, lanePipelineId, laneStageId}: laneSource names a card lane (the Deal.source its Auto-Create step wrote, e.g. \"GSM\", max 500 chars) and makes the deal leg assign the contact's newest OPEN card from that lane (inside lanePipelineId when set) instead of the run's deal; when the contact has none and lanePipelineId is set, the card is created there (laneStageId, else the pipeline's first stage) already owned by the user; lanePipelineId or laneStageId without laneSource, or laneStageId without lanePipelineId, is refused; a lane never applies to an Assign-to-contact step. create_deal config {pipelineId (required), stageId, dealName, dealValue, allowDuplicates, status, source, assignedTo, leaveUnassigned}: leaveUnassigned true leaves the card unassigned when no assignee is set (default false: the card goes to the lead's owner). close_other_open_deals config {lostReason (required)}: closes the contact's OTHER open cards as lost with that reason, keeping the run's own card (each moves to its pipeline's lost stage with the usual stage-change effects; cards already won/lost, in the DPP Post-Close pipeline or in a pipeline with no lost stage are skipped; a re-run closes nothing twice); it needs the run's own deal, so use a deal trigger or a find_opportunity step first." + AUTOMATION_KEY_REFUSALS_NOTE)
|
|
2347
|
+
.description("Create a workflow (with optional name resolution). Provide the body via --json or --file (needs name + triggerEvent + graph). A sequence step is the action enroll_in_sequence with config {sequenceId, enrollAction: add (default) | remove}; add needs the sequence to allow automation enrollment. A delay node waits for a fixed duration by default; waitType \"until_field_date\" (with fieldKey = a deal DATE / DATETIME custom field key and offsetHours = signed hours added to its date, default 0, e.g. 24 = a day after it) waits until that date stored on the deal, re-read at every wake (an empty or past value continues immediately). Card ownership per lane: assign_to_user config {userId | userIds (round robin) | groupId, target|applyTo: contact|deal|both, overwriteExisting, laneSource, lanePipelineId, laneStageId}: laneSource names a card lane (the Deal.source its Auto-Create step wrote, e.g. \"GSM\", max 500 chars) and makes the deal leg assign the contact's newest OPEN card from that lane (inside lanePipelineId when set) instead of the run's deal; when the contact has none and lanePipelineId is set, the card is created there (laneStageId, else the pipeline's first stage) already owned by the user; lanePipelineId or laneStageId without laneSource, or laneStageId without lanePipelineId, is refused; a lane never applies to an Assign-to-contact step. create_deal config {pipelineId (required), stageId, dealName, dealValue, allowDuplicates, status, source, assignedTo, leaveUnassigned}: leaveUnassigned true leaves the card unassigned when no assignee is set (default false: the card goes to the lead's owner). close_other_open_deals config {lostReason (required)}: closes the contact's OTHER open cards as lost with that reason, keeping the run's own card (each moves to its pipeline's lost stage with the usual stage-change effects; cards already won/lost, in the DPP Post-Close pipeline or in a pipeline with no lost stage are skipped; a re-run closes nothing twice); it needs the run's own deal, so use a deal trigger or a find_opportunity step first." + DEAL_NODES_NOTE + AUTOMATION_KEY_REFUSALS_NOTE)
|
|
2341
2348
|
.option("--json <json>", "full request body as JSON")
|
|
2342
2349
|
.option("--file <path>", "read the request body from a JSON file")
|
|
2343
2350
|
.option("--resolve-names", "set resolveNames=true so name fields in node configs are converted to ids")
|
|
@@ -2365,7 +2372,7 @@ automations
|
|
|
2365
2372
|
}));
|
|
2366
2373
|
automations
|
|
2367
2374
|
.command("publish <id>")
|
|
2368
|
-
.description("Publish a draft workflow to live. The API binds a publish to the draft you reviewed and answers 409 without its configRevision, so this reads GET /automations/<id> and POSTs its configRevision as expectedConfigRevision (--expected-config-revision N uses the revision you reviewed and skips the read; a draft changed since then is refused 409). A draft holding an action config key the action does not read, or an unregistered action type, is refused 422 with code unknown_action_config_key / unknown_action_type listed in data.errors (with a \"Did you mean\" suggestion); the error text prints the whole refusal")
|
|
2375
|
+
.description("Publish a draft workflow to live. The API binds a publish to the draft you reviewed and answers 409 without its configRevision, so this reads GET /automations/<id> and POSTs its configRevision as expectedConfigRevision (--expected-config-revision N uses the revision you reviewed and skips the read; a draft changed since then is refused 409). A draft holding an action config key the action does not read, or an unregistered action type, is refused 422 with code unknown_action_config_key / unknown_action_type listed in data.errors (with a \"Did you mean\" suggestion); the error text prints the whole refusal. A workflow with a find_opportunity version 2 step is refused 422 find_needs_step_cursors unless the workspace has step cursors (\"Find opportunity needs step cursors on this workspace; ask support\"); validate and import do not check this")
|
|
2369
2376
|
.option("--expected-config-revision <n>", "the configRevision of the draft you reviewed (from `automations` read / GET /automations/<id>); omit to read the current one")
|
|
2370
2377
|
.action(run(async (client, id, opts) => {
|
|
2371
2378
|
assertUuid(id, "automation id");
|
|
@@ -2663,57 +2670,104 @@ companies
|
|
|
2663
2670
|
}));
|
|
2664
2671
|
// ---- tags ----
|
|
2665
2672
|
// ---- saved filters (personal, Velocify-style) ----
|
|
2673
|
+
// Smart View icons (app branch fable/smart-view-icons): the 30 current ids in picker order. The API also accepts 17 legacy ids as
|
|
2674
|
+
// deprecated inputs (saved as the mapped new id); they are deliberately not listed here.
|
|
2675
|
+
const SMART_VIEW_ICON_IDS = [
|
|
2676
|
+
"heart", "star", "trophy", "badge", "target", "eye", "clock", "hourglass", "siren", "bolt",
|
|
2677
|
+
"fire", "rain", "funnel", "pyramid", "flag", "pin", "push-pin", "thumbs-up", "thumbs-down", "coffee",
|
|
2678
|
+
"rocket", "number-1", "number-2", "number-3", "number-4", "number-5", "number-6", "number-7", "number-8", "number-9",
|
|
2679
|
+
];
|
|
2680
|
+
const SMART_VIEW_ICON_HELP = (omitted) => `the view's icon, one of (picker order): ${SMART_VIEW_ICON_IDS.join(", ")}. ${omitted}. Legacy icon ids are accepted but saved as the new id; anything else is 400 invalid_icon`;
|
|
2681
|
+
const SAVED_FILTER_ENTITY_ERROR = "saved filters are contact Smart Views; --entity is no longer supported";
|
|
2682
|
+
function rejectEntity(body) {
|
|
2683
|
+
if ("entity" in body || "entityType" in body)
|
|
2684
|
+
throw new Error(SAVED_FILTER_ENTITY_ERROR);
|
|
2685
|
+
}
|
|
2686
|
+
/** saved-filters update/delete take a Smart View id, but the generic routes also edit/delete SHARED views for admin keys: read the view first and
|
|
2687
|
+
* refuse anything that is not one of the caller's personal views (the GET is the only request made in the refusal path). A missing view throws the server's reason. */
|
|
2688
|
+
async function requirePersonalSmartView(client, id) {
|
|
2689
|
+
const res = (await client.get(`/api/v1/smart-views/${encodeURIComponent(id)}`));
|
|
2690
|
+
if (res?.data?.isPersonal !== true) {
|
|
2691
|
+
throw new Error("this is a shared Smart View; saved filters are personal views only (use `conduyt api PATCH /smart-views/<id> --body '{...}'` or `conduyt api DELETE /smart-views/<id>`)");
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2666
2694
|
const savedFilters = program
|
|
2667
2695
|
.command("saved-filters")
|
|
2668
2696
|
.description("Manage YOUR personal saved contact filters (the web app's Saved dropdown; requires the 'saved-filters' key scope)");
|
|
2669
2697
|
savedFilters
|
|
2670
2698
|
.command("list")
|
|
2671
|
-
.description("List your saved filters")
|
|
2672
|
-
.
|
|
2673
|
-
|
|
2699
|
+
.description("List your saved filters: your PERSONAL Smart Views (GET /smart-views, every page, filtered to isPersonal). The printed id is the Smart View id that update and delete take")
|
|
2700
|
+
.action(run(async (client) => {
|
|
2701
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
2702
|
+
const first = (await client.get("/api/v1/smart-views?counts=false"));
|
|
2703
|
+
const rows = [...first.data.data];
|
|
2704
|
+
let meta = first.data.meta;
|
|
2705
|
+
let shifted = false;
|
|
2706
|
+
for (let page = 2; meta.has_more && page <= 50; page += 1) {
|
|
2707
|
+
const next = (await client.get(`/api/v1/smart-views?page=${page}&counts=false`));
|
|
2708
|
+
if (next.data.meta.order_stamp !== first.data.meta.order_stamp) {
|
|
2709
|
+
shifted = true;
|
|
2710
|
+
break;
|
|
2711
|
+
}
|
|
2712
|
+
rows.push(...next.data.data);
|
|
2713
|
+
meta = next.data.meta;
|
|
2714
|
+
}
|
|
2715
|
+
if (!shifted)
|
|
2716
|
+
return rows.filter((v) => v.isPersonal === true);
|
|
2717
|
+
}
|
|
2718
|
+
throw new Error("Smart Views kept changing while they were listed; try again.");
|
|
2719
|
+
}));
|
|
2674
2720
|
savedFilters
|
|
2675
2721
|
.command("create")
|
|
2676
|
-
.description("Save a named filter (grouped payload via --filters JSON)")
|
|
2722
|
+
.description("Save a named filter (a personal Smart View, private to you; grouped payload via --filters JSON)")
|
|
2677
2723
|
.option("--name <name>", "display name (required, unique per user, max 60 chars)")
|
|
2678
2724
|
.option("--filters <json>", `grouped filter payload JSON, e.g. [{"conjunction":"AND","rules":[{"field":"source","operator":"equals","value":"facebook"}]}]`)
|
|
2679
2725
|
.option("--sort-field <field>", "sort field (default createdAt)")
|
|
2680
2726
|
.option("--sort-order <dir>", "asc | desc (default desc)")
|
|
2681
|
-
.
|
|
2682
|
-
.option("--
|
|
2727
|
+
.addOption(new Option("--entity <type>", "removed").hideHelp()) // not in the help; passing it exits with SAVED_FILTER_ENTITY_ERROR instead of being ignored
|
|
2728
|
+
.option("--icon <id>", SMART_VIEW_ICON_HELP("Omitted: heart (the default)"))
|
|
2729
|
+
.option("--json <json>", "full JSON body, merged over the flags (may carry `icon`: same ids, same default and same 400 invalid_icon as --icon)")
|
|
2683
2730
|
.action(run(async (client, opts) => {
|
|
2731
|
+
if (opts.entity !== undefined)
|
|
2732
|
+
throw new Error(SAVED_FILTER_ENTITY_ERROR);
|
|
2684
2733
|
const body = {};
|
|
2685
2734
|
if (opts.name !== undefined)
|
|
2686
2735
|
body.name = opts.name;
|
|
2736
|
+
if (opts.icon !== undefined)
|
|
2737
|
+
body.icon = opts.icon;
|
|
2687
2738
|
if (opts.filters !== undefined)
|
|
2688
2739
|
body.filters = jsonArrayArg(opts.filters, "--filters");
|
|
2689
2740
|
if (opts.sortField !== undefined)
|
|
2690
2741
|
body.sortField = opts.sortField;
|
|
2691
2742
|
if (opts.sortOrder !== undefined)
|
|
2692
2743
|
body.sortOrder = opts.sortOrder;
|
|
2693
|
-
if (opts.entity !== undefined)
|
|
2694
|
-
body.entityType = opts.entity;
|
|
2695
2744
|
if (opts.json !== undefined)
|
|
2696
2745
|
Object.assign(body, jsonArg(opts.json, "--json"));
|
|
2746
|
+
rejectEntity(body);
|
|
2697
2747
|
// A --json merge can clobber filters with a non-array; re-assert the
|
|
2698
2748
|
// shape after ALL merges so a malformed payload fails locally.
|
|
2699
2749
|
if (body.filters !== undefined && !Array.isArray(body.filters)) {
|
|
2700
2750
|
throw new Error("filters must be a JSON ARRAY (grouped payload or flat rules).");
|
|
2701
2751
|
}
|
|
2702
|
-
|
|
2752
|
+
// a saved filter IS a personal Smart View (saved-filters write routes retired 2026-09-24, 410): personal:true keeps it private for every key
|
|
2753
|
+
return client.post("/api/v1/smart-views", { ...body, personal: true });
|
|
2703
2754
|
}));
|
|
2704
2755
|
savedFilters
|
|
2705
2756
|
.command("update <id>")
|
|
2706
|
-
.description("Rename a saved filter or replace its filters/sort (owner-only)")
|
|
2757
|
+
.description("Rename a saved filter (a personal Smart View) or replace its filters/sort (owner-only). The id must be one of YOUR personal views (see `saved-filters list`); a shared Smart View id is refused after one GET, nothing is changed (edit a shared view with `conduyt api PATCH /smart-views/<id> --body '{...}'`)")
|
|
2707
2758
|
.option("--name <name>", "new display name")
|
|
2708
2759
|
.option("--filters <json>", "grouped filter payload JSON")
|
|
2709
2760
|
.option("--sort-field <field>", "sort field")
|
|
2710
2761
|
.option("--sort-order <dir>", "asc | desc")
|
|
2711
|
-
.option("--
|
|
2762
|
+
.option("--icon <id>", SMART_VIEW_ICON_HELP("Omitted: unchanged"))
|
|
2763
|
+
.option("--json <json>", "full JSON body, merged over the flags (may carry `icon`: same ids and same 400 invalid_icon as --icon)")
|
|
2712
2764
|
.action(run(async (client, id, opts) => {
|
|
2713
2765
|
assertUuid(id, "saved filter id");
|
|
2714
2766
|
const body = {};
|
|
2715
2767
|
if (opts.name !== undefined)
|
|
2716
2768
|
body.name = opts.name;
|
|
2769
|
+
if (opts.icon !== undefined)
|
|
2770
|
+
body.icon = opts.icon;
|
|
2717
2771
|
if (opts.filters !== undefined)
|
|
2718
2772
|
body.filters = jsonArrayArg(opts.filters, "--filters");
|
|
2719
2773
|
if (opts.sortField !== undefined)
|
|
@@ -2722,18 +2776,21 @@ savedFilters
|
|
|
2722
2776
|
body.sortOrder = opts.sortOrder;
|
|
2723
2777
|
if (opts.json !== undefined)
|
|
2724
2778
|
Object.assign(body, jsonArg(opts.json, "--json"));
|
|
2779
|
+
rejectEntity(body);
|
|
2725
2780
|
// Same post-merge shape assertion as create — see above.
|
|
2726
2781
|
if (body.filters !== undefined && !Array.isArray(body.filters)) {
|
|
2727
2782
|
throw new Error("filters must be a JSON ARRAY (grouped payload or flat rules).");
|
|
2728
2783
|
}
|
|
2729
|
-
|
|
2784
|
+
await requirePersonalSmartView(client, id);
|
|
2785
|
+
return client.patch(`/api/v1/smart-views/${encodeURIComponent(id)}`, body);
|
|
2730
2786
|
}));
|
|
2731
2787
|
savedFilters
|
|
2732
2788
|
.command("delete <id>")
|
|
2733
|
-
.description("Delete one of your saved filters (owner-only)")
|
|
2789
|
+
.description("Delete one of your saved filters (a personal Smart View; owner-only). The id must be one of YOUR personal views (see `saved-filters list`); a shared Smart View id is refused after one GET, nothing is deleted (delete a shared view with `conduyt api DELETE /smart-views/<id>`)")
|
|
2734
2790
|
.action(run(async (client, id) => {
|
|
2735
2791
|
assertUuid(id, "saved filter id");
|
|
2736
|
-
|
|
2792
|
+
await requirePersonalSmartView(client, id);
|
|
2793
|
+
return client.del(`/api/v1/smart-views/${encodeURIComponent(id)}`);
|
|
2737
2794
|
}));
|
|
2738
2795
|
const tags = program.command("tags").description("Manage tags");
|
|
2739
2796
|
tags
|
|
@@ -5226,7 +5283,7 @@ reports
|
|
|
5226
5283
|
.command("deliveries <sid>")
|
|
5227
5284
|
.description("The delivery history of a schedule with download URLs")
|
|
5228
5285
|
.action(run(async (client, sid) => { assertUuid(sid, "schedule id"); return client.get(`/api/v1/reports/subscriptions/${sid}/deliveries`); }));
|
|
5229
|
-
const smartViews = program.command("smart-views").description(
|
|
5286
|
+
const smartViews = program.command("smart-views").description(`Smart Views and their Smart Dialing priorities. Creating or updating a view (conduyt api POST /smart-views, PATCH /smart-views/<id>, or saved-filters create/update) takes an optional icon, one of (picker order): ${SMART_VIEW_ICON_IDS.join(", ")}. heart is the default on create. Legacy icon ids are accepted but saved as the new id; anything else is 400 invalid_icon`);
|
|
5230
5287
|
smartViews
|
|
5231
5288
|
.command("list")
|
|
5232
5289
|
.description("List Smart Views in sidebar order (= Smart Dialing priority) with lead counts, audience (groupId / members / isPersonal) and dialing settings")
|
package/package.json
CHANGED