Lands the view-preset / server-persisted personal-layer feature that has been running in production, plus the hotfix for "the same preset shows different filter configs to different users" (物流问题-已出轨迹 on tblgsJTXhqwiDpsjeL5 / viwyszJmLQNOLhA9sh5). Root cause of the divergence: - usePersonalViewSync implicitly flushed any overlay edit by a view|update user INTO the shared preset row (last-writer-wins). Overlays are stale snapshots seeded at apply time, so one user's seed silently erased colleagues' filter changes (2026-09-17 02:55 the preset lost the 流转角色=售后 condition), and active overlays never converged because preset updates do not propagate. Fix (semantics: presets are shared definitions, edits are personal): - fork-on-edit: the first user edit strips the seed markers from the overlay (toolbar switches to the personal config); edits only ever write the user's personal row - shared presets change ONLY through the new explicit, confirmed "Update to current config" row action (canManage users) - un-edited preset overlays auto-follow redefinitions: applyPreset stamps a presetVersion (preset.lastModifiedTime); when the preset list refetches with a newer row the overlay re-seeds. Edited overlays carry no markers and are never clobbered. Legacy rows without a version re-seed exactly once - exits strip both seed markers from the persisted personal row Also lands the pending WIP shipped with the deployed build: select-choice visibility constraint, personal-view server sync + proxy, grid async records scope/request tracking, CLI resource display, preset i18n (10 locales), view_preset migrations, docs/skill updates, .zcode gitignore. Verified end-to-end on the live instance (headless chromium, real session): apply -> edit flips toolbar to Personal with ZERO shared-row PUTs; explicit update PUTs the shared row; API-side redefinition auto-reseeds an un-edited overlay on the next list refetch.
740 lines
30 KiB
JavaScript
740 lines
30 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.presetConfigFromOptions = exports.baseResourceCommands = void 0;
|
|
/**
|
|
* `inkreach-teable base|table|field|record|view` — resource CRUD commands.
|
|
*
|
|
* These endpoints are compatible with official teable open-api.
|
|
*/
|
|
const commander_1 = require("commander");
|
|
const ctx_1 = require("../lib/ctx");
|
|
const output_1 = require("../lib/output");
|
|
const client_1 = require("../lib/client");
|
|
const field_normalize_1 = require("../lib/field-normalize");
|
|
const field_display_1 = require("../lib/field-display");
|
|
/**
|
|
* Fetch a field by ID or display name. A direct ID hit needs no extra call;
|
|
* a 400/404 miss (the API rejects non-ID paths) falls back to resolving the
|
|
* arg against the table's field list. Unresolvable input rethrows the
|
|
* original error so the user sees the underlying cause.
|
|
*/
|
|
async function getFieldFlexible(client, tableId, fieldIdOrName) {
|
|
try {
|
|
return (await client.getField(tableId, fieldIdOrName));
|
|
}
|
|
catch (err) {
|
|
if (err instanceof client_1.ApiError && (err.status === 400 || err.status === 404)) {
|
|
const list = (await client.listFields(tableId));
|
|
const hit = (0, field_normalize_1.findFieldByNameOrId)(list, fieldIdOrName);
|
|
if (hit)
|
|
return (await client.getField(tableId, hit.id));
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
/** fieldId → display-name map of a table, to humanize visibilityFilter references. */
|
|
async function fieldNameMap(client, tableId) {
|
|
const list = (await client.listFields(tableId));
|
|
return Object.fromEntries(list.map((f) => [f.id, f.name]));
|
|
}
|
|
function baseResourceCommands() {
|
|
return [baseCmd(), tableCmd(), fieldCmd(), recordCmd(), viewCmd(), presetCmd()];
|
|
}
|
|
exports.baseResourceCommands = baseResourceCommands;
|
|
// ===================== base =====================
|
|
function baseCmd() {
|
|
const cmd = new commander_1.Command('base');
|
|
cmd.description('Manage bases (database containers)');
|
|
cmd
|
|
.command('list')
|
|
.description('List all accessible bases')
|
|
.action((0, ctx_1.run)(async (ctx) => {
|
|
const list = (await ctx.client.listBases());
|
|
if (ctx.json)
|
|
(0, output_1.printJson)(list);
|
|
else {
|
|
if (!list.length) {
|
|
process.stdout.write('No bases found.\n');
|
|
return;
|
|
}
|
|
(0, output_1.printTable)([
|
|
{ key: 'id', header: 'ID' },
|
|
{ key: 'name', header: 'NAME' },
|
|
], list.map((b) => ({ id: (0, output_1.truncate)(b.id), name: (0, output_1.truncate)(b.name, 30) })));
|
|
}
|
|
}));
|
|
cmd
|
|
.command('get <baseId>')
|
|
.description('Get base details')
|
|
.action((0, ctx_1.run)(async (ctx, args) => {
|
|
const baseId = args[0];
|
|
const base = await ctx.client.getBase(baseId);
|
|
if (ctx.json)
|
|
(0, output_1.printJson)(base);
|
|
else {
|
|
const b = base;
|
|
(0, output_1.printKv)(Object.entries(b).map(([k, v]) => [k, v]));
|
|
}
|
|
}));
|
|
return cmd;
|
|
}
|
|
// ===================== table =====================
|
|
function tableCmd() {
|
|
const cmd = new commander_1.Command('table');
|
|
cmd.description('Manage tables within a base');
|
|
cmd
|
|
.command('list')
|
|
.description('List tables in a base')
|
|
.option('--base-id <id>', 'Base id (overrides default)')
|
|
.action((0, ctx_1.run)(async (ctx, _args, o) => {
|
|
const opts = o;
|
|
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
|
const list = (await ctx.client.listTables(baseId));
|
|
if (ctx.json)
|
|
(0, output_1.printJson)(list);
|
|
else {
|
|
if (!list.length) {
|
|
process.stdout.write('No tables found.\n');
|
|
return;
|
|
}
|
|
(0, output_1.printTable)([
|
|
{ key: 'id', header: 'ID' },
|
|
{ key: 'name', header: 'NAME' },
|
|
{ key: 'dbTableName', header: 'DB TABLE' },
|
|
], list.map((t) => ({
|
|
id: (0, output_1.truncate)(t.id),
|
|
name: (0, output_1.truncate)(t.name, 30),
|
|
dbTableName: (0, output_1.truncate)(t.dbTableName ?? ''),
|
|
})));
|
|
}
|
|
}));
|
|
cmd
|
|
.command('get <tableId>')
|
|
.description('Get table details')
|
|
.action((0, ctx_1.run)(async (ctx, args) => {
|
|
const tableId = args[0];
|
|
const table = await ctx.client.getTable(tableId);
|
|
if (ctx.json)
|
|
(0, output_1.printJson)(table);
|
|
else {
|
|
const t = table;
|
|
(0, output_1.printKv)(Object.entries(t).map(([k, v]) => [k, v]));
|
|
}
|
|
}));
|
|
cmd
|
|
.command('create')
|
|
.description('Create a new table')
|
|
.requiredOption('--name <name>', 'Table name')
|
|
.option('--base-id <id>', 'Base id (overrides default)')
|
|
.option('--fields <json>', 'Field definitions JSON array, e.g. \'[{"name":"Title","type":"singleLineText"}]\'')
|
|
.action((0, ctx_1.run)(async (ctx, _args, o) => {
|
|
const opts = o;
|
|
const baseId = (0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
|
const body = { name: opts.name };
|
|
if (opts.fields) {
|
|
body.fields = (0, field_normalize_1.normalizeFieldRoArray)(JSON.parse(opts.fields));
|
|
// Resolve foreignTableName references against existing tables in the base.
|
|
const fields = body.fields;
|
|
const tables = await ctx.client.listTables(baseId).catch(() => []);
|
|
for (const f of fields) {
|
|
const options = f?.options;
|
|
if (options && typeof options.foreignTableName === 'string' && !options.foreignTableId) {
|
|
const t = (0, field_normalize_1.findTableByNameOrId)(tables || [], options.foreignTableName);
|
|
if (t)
|
|
options.foreignTableId = t.id;
|
|
}
|
|
}
|
|
}
|
|
const table = await ctx.client.createTable(baseId, body);
|
|
if (ctx.json)
|
|
(0, output_1.printJson)(table);
|
|
else {
|
|
const t = table;
|
|
(0, output_1.printKv)([
|
|
['id', t.id],
|
|
['name', t.name],
|
|
]);
|
|
}
|
|
}));
|
|
cmd
|
|
.command('update <tableId>')
|
|
.description('Update table (rename)')
|
|
.requiredOption('--name <name>', 'New table name')
|
|
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
|
const tableId = args[0];
|
|
const opts = o;
|
|
const table = await ctx.client.updateTable(tableId, { name: opts.name });
|
|
if (ctx.json)
|
|
(0, output_1.printJson)(table);
|
|
else
|
|
process.stdout.write(`✓ Table ${tableId} updated\n`);
|
|
}));
|
|
cmd
|
|
.command('delete <tableId>')
|
|
.description('Delete a table')
|
|
.action((0, ctx_1.run)(async (ctx, args) => {
|
|
const tableId = args[0];
|
|
await ctx.client.deleteTable(tableId);
|
|
if (ctx.json)
|
|
(0, output_1.printJson)({ ok: true, id: tableId });
|
|
else
|
|
process.stdout.write(`✓ Table ${tableId} deleted\n`);
|
|
}));
|
|
return cmd;
|
|
}
|
|
// ===================== field =====================
|
|
function fieldCmd() {
|
|
const cmd = new commander_1.Command('field');
|
|
cmd.description('Manage fields (columns) in a table');
|
|
cmd
|
|
.command('list <tableId>')
|
|
.description('List fields in a table')
|
|
.option('--with-options', 'Add an OPTIONS summary column and expand select choices with their visibility conditions')
|
|
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
|
const opts = o;
|
|
const tableId = args[0];
|
|
const list = (await ctx.client.listFields(tableId));
|
|
if (ctx.json)
|
|
(0, output_1.printJson)(list);
|
|
else {
|
|
if (!list.length) {
|
|
process.stdout.write('No fields found.\n');
|
|
return;
|
|
}
|
|
if (opts.withOptions) {
|
|
const nameMap = Object.fromEntries(list.map((f) => [String(f.id), String(f.name)]));
|
|
(0, output_1.printTable)([
|
|
{ key: 'id', header: 'ID' },
|
|
{ key: 'name', header: 'NAME' },
|
|
{ key: 'type', header: 'TYPE' },
|
|
{ key: 'options', header: 'OPTIONS' },
|
|
], list.map((f) => ({
|
|
id: (0, output_1.truncate)(f.id),
|
|
name: (0, output_1.truncate)(f.name, 30),
|
|
type: f.type,
|
|
options: (0, field_display_1.summarizeFieldOptions)(f),
|
|
})));
|
|
const selectFields = list.filter((f) => Array.isArray(f.options?.choices));
|
|
for (const f of selectFields) {
|
|
const rows = (0, field_display_1.choiceRows)(f, nameMap);
|
|
const conditional = rows.filter((r) => r.when !== '(always)').length;
|
|
process.stdout.write(`\n◇ ${f.name} (${f.id}) — ${rows.length} choices · ${conditional} conditional\n`);
|
|
for (const r of rows) {
|
|
const color = r.color ? ` [${r.color}]` : '';
|
|
process.stdout.write(` ${r.name}${color}\n`);
|
|
if (r.when !== '(always)') {
|
|
process.stdout.write(` when: ${r.when}\n`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
(0, output_1.printTable)([
|
|
{ key: 'id', header: 'ID' },
|
|
{ key: 'name', header: 'NAME' },
|
|
{ key: 'type', header: 'TYPE' },
|
|
], list.map((f) => ({
|
|
id: (0, output_1.truncate)(f.id),
|
|
name: (0, output_1.truncate)(f.name, 30),
|
|
type: f.type,
|
|
})));
|
|
}
|
|
}
|
|
}));
|
|
cmd
|
|
.command('get <tableId> <fieldIdOrName>')
|
|
.description('Get field details by id or name (full options; select choices rendered with visibility conditions)')
|
|
.action((0, ctx_1.run)(async (ctx, args) => {
|
|
const [tableId, fieldIdOrName] = args;
|
|
const field = await getFieldFlexible(ctx.client, tableId, fieldIdOrName);
|
|
if (ctx.json)
|
|
(0, output_1.printJson)(field);
|
|
else {
|
|
const choices = field.options?.choices;
|
|
const names = Array.isArray(choices)
|
|
? await fieldNameMap(ctx.client, tableId)
|
|
: undefined;
|
|
process.stdout.write((0, field_display_1.formatFieldDetail)(field, names) + '\n');
|
|
}
|
|
}));
|
|
cmd
|
|
.command('create')
|
|
.description('Create a new field')
|
|
.requiredOption('--table-id <id>', 'Table id')
|
|
.requiredOption('--name <name>', 'Field name')
|
|
.option('--type <type>', 'Field type — full value (singleLineText, singleSelect, link…) or alias (text, sel, num…). Omitted: inferred from --options shape / --name')
|
|
.option('--options <json>', 'Field options JSON (type-specific; select choices accept plain strings)')
|
|
.option('--description <d>', 'Field description')
|
|
.option('--lookup-options <json>', 'Lookup options JSON (names like linkFieldName/lookupFieldName are resolved to IDs)')
|
|
.option('--is-lookup', 'Mark the field as a lookup field')
|
|
.action((0, ctx_1.run)(async (ctx, _args, o) => {
|
|
const opts = o;
|
|
const body = { name: opts.name };
|
|
if (opts.options)
|
|
body.options = JSON.parse(opts.options);
|
|
if (opts.description)
|
|
body.description = opts.description;
|
|
if (opts.lookupOptions)
|
|
body.lookupOptions = JSON.parse(opts.lookupOptions);
|
|
if (opts.isLookup)
|
|
body.isLookup = true;
|
|
body.type = opts.type
|
|
? (0, field_normalize_1.resolveFieldType)(opts.type)
|
|
: (0, field_normalize_1.inferFieldType)(opts.name, body.options);
|
|
(0, field_normalize_1.normalizeFieldRo)(body);
|
|
const errors = await (0, field_normalize_1.resolveFieldReferences)(body, {
|
|
listTables: (baseId) => ctx.client.listTables(baseId),
|
|
listFields: (tableId) => ctx.client.listFields(tableId),
|
|
baseId: ctx.baseId,
|
|
tableId: opts.tableId,
|
|
});
|
|
if (errors.length)
|
|
throw new Error(errors.join('; '));
|
|
const field = await ctx.client.createField(opts.tableId, body);
|
|
if (ctx.json)
|
|
(0, output_1.printJson)(field);
|
|
else {
|
|
const f = field;
|
|
(0, output_1.printKv)([
|
|
['id', f.id],
|
|
['name', f.name],
|
|
['type', f.type],
|
|
]);
|
|
}
|
|
}));
|
|
cmd
|
|
.command('update <tableId> <fieldIdOrName>')
|
|
.description('Update a field (by id or name). --name renames (PATCH); --options rewrites options via the convert API (the update API does not accept options)')
|
|
.option('--name <name>', 'New field name')
|
|
.option('--options <json>', 'New field options JSON (written via convert API)')
|
|
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
|
const [tableId, fieldIdOrName] = args;
|
|
const opts = o;
|
|
if (!opts.name && !opts.options) {
|
|
throw new Error('Nothing to update: pass --name and/or --options');
|
|
}
|
|
if (opts.name && !opts.options) {
|
|
const current = (await getFieldFlexible(ctx.client, tableId, fieldIdOrName));
|
|
const field = await ctx.client.updateField(tableId, current.id, { name: opts.name });
|
|
if (ctx.json)
|
|
(0, output_1.printJson)(field);
|
|
else
|
|
process.stdout.write(`✓ Field ${current.id} renamed\n`);
|
|
return;
|
|
}
|
|
// Options (choices / visibilityFilter / ...) must go through convert.
|
|
// Fetch the current field to reuse its type; merge name if given.
|
|
const current = (await getFieldFlexible(ctx.client, tableId, fieldIdOrName));
|
|
const body = {
|
|
type: current.type,
|
|
options: JSON.parse(opts.options),
|
|
};
|
|
if (opts.name)
|
|
body.name = opts.name;
|
|
(0, field_normalize_1.normalizeFieldRo)(body);
|
|
const field = await ctx.client.convertField(tableId, current.id, body);
|
|
if (ctx.json)
|
|
(0, output_1.printJson)(field);
|
|
else
|
|
process.stdout.write(`✓ Field ${current.id} options updated (via convert)\n`);
|
|
}));
|
|
cmd
|
|
.command('visible-choices <tableId> <recordId> <fieldIdOrName>')
|
|
.description('Evaluate select-choice visibility for a record (respects choice visibilityFilter dependencies)')
|
|
.action((0, ctx_1.run)(async (ctx, args) => {
|
|
const [tableId, recordId, fieldIdOrName] = args;
|
|
const field = await getFieldFlexible(ctx.client, tableId, fieldIdOrName);
|
|
const res = await ctx.client.getVisibleChoices(tableId, recordId, String(field.id));
|
|
if (ctx.json)
|
|
(0, output_1.printJson)(res);
|
|
else {
|
|
(0, output_1.printTable)([
|
|
{ key: 'id', header: 'ID' },
|
|
{ key: 'name', header: 'NAME' },
|
|
{ key: 'color', header: 'COLOR' },
|
|
], res.visibleChoices.map((c) => ({ ...c, color: c.color ?? '' })));
|
|
}
|
|
}));
|
|
cmd
|
|
.command('delete <tableId> <fieldId>')
|
|
.description('Delete a field')
|
|
.action((0, ctx_1.run)(async (ctx, args) => {
|
|
const [tableId, fieldId] = args;
|
|
await ctx.client.deleteField(tableId, fieldId);
|
|
if (ctx.json)
|
|
(0, output_1.printJson)({ ok: true, id: fieldId });
|
|
else
|
|
process.stdout.write(`✓ Field ${fieldId} deleted\n`);
|
|
}));
|
|
return cmd;
|
|
}
|
|
// ===================== record =====================
|
|
function recordCmd() {
|
|
const cmd = new commander_1.Command('record');
|
|
cmd.description('Manage records (rows) in a table');
|
|
cmd
|
|
.command('get')
|
|
.description('Query records with filters, pagination, and search')
|
|
.requiredOption('--table-id <id>', 'Table id')
|
|
.option('--filter <json>', 'Filter conditions JSON')
|
|
.option('--projection <json>', 'Field projection JSON array')
|
|
.option('--page-size <n>', 'Records per page')
|
|
.option('--page <n>', 'Page number (1-based)')
|
|
.option('--search <kw>', 'Search keyword')
|
|
.option('--base-id <id>', 'Base id (overrides default)')
|
|
.action((0, ctx_1.run)(async (ctx, _args, o) => {
|
|
const opts = o;
|
|
// validate baseId is present (even though getRecords only needs tableId,
|
|
// we follow the convention that base-scoped commands require baseId config)
|
|
(0, ctx_1.requireBaseId)(ctx, opts.baseId);
|
|
const query = {};
|
|
if (opts.filter)
|
|
query.filter = JSON.parse(opts.filter);
|
|
if (opts.projection)
|
|
query.projection = JSON.parse(opts.projection);
|
|
if (opts.search)
|
|
query.search = opts.search;
|
|
const page = opts.page ? parseInt(opts.page, 10) : undefined;
|
|
const pageSize = opts.pageSize ? parseInt(opts.pageSize, 10) : undefined;
|
|
if (pageSize)
|
|
query.take = pageSize;
|
|
if (page) {
|
|
const take = pageSize || 100;
|
|
query.skip = (page - 1) * take;
|
|
if (!pageSize)
|
|
query.take = take;
|
|
}
|
|
const result = (await ctx.client.getRecords(opts.tableId, query));
|
|
if (ctx.json) {
|
|
(0, output_1.printJson)(result);
|
|
}
|
|
else {
|
|
if (!result.records?.length) {
|
|
process.stdout.write('No records found.\n');
|
|
return;
|
|
}
|
|
const firstFields = result.records[0].fields || {};
|
|
const fieldKeys = Object.keys(firstFields);
|
|
const columns = fieldKeys.map((k) => ({ key: k, header: k }));
|
|
(0, output_1.printTable)(columns, result.records.map((rec) => {
|
|
const f = rec.fields || {};
|
|
const row = { id: (0, output_1.truncate)(rec.id) };
|
|
for (const k of fieldKeys) {
|
|
row[k] = (0, output_1.truncate)(f[k], 30);
|
|
}
|
|
return row;
|
|
}));
|
|
if (result.total !== undefined) {
|
|
process.stdout.write(`\n${result.records.length} of ${result.total} record(s).\n`);
|
|
}
|
|
else {
|
|
process.stdout.write(`\n${result.records.length} record(s).\n`);
|
|
}
|
|
}
|
|
}));
|
|
cmd
|
|
.command('get-one <recordId>')
|
|
.description('Get a single record by id')
|
|
.action((0, ctx_1.run)(async (ctx, args) => {
|
|
const recordId = args[0];
|
|
const record = await ctx.client.getRecord(recordId);
|
|
if (ctx.json)
|
|
(0, output_1.printJson)(record);
|
|
else {
|
|
const rec = record;
|
|
const fields = rec.fields || {};
|
|
(0, output_1.printKv)([
|
|
['id', rec.id],
|
|
...Object.entries(fields).map(([k, v]) => [k, v]),
|
|
]);
|
|
}
|
|
}));
|
|
cmd
|
|
.command('create')
|
|
.description('Create record(s). Use --fields for a single record, --records for bulk.')
|
|
.requiredOption('--table-id <id>', 'Table id')
|
|
.option('--fields <json>', 'Fields object JSON, e.g. \'{"Title":"hello"}\'')
|
|
.option('--records <json>', 'Records array JSON for bulk, e.g. \'[{"fields":{"Title":"a"}}]\'')
|
|
.action((0, ctx_1.run)(async (ctx, _args, o) => {
|
|
const opts = o;
|
|
let body;
|
|
if (opts.records) {
|
|
body = { records: JSON.parse(opts.records) };
|
|
}
|
|
else if (opts.fields) {
|
|
body = { fields: JSON.parse(opts.fields) };
|
|
}
|
|
else {
|
|
throw Object.assign(new Error('Provide --fields or --records'), {
|
|
code: 'MISSING_FIELDS',
|
|
});
|
|
}
|
|
const result = await ctx.client.createRecord(opts.tableId, body);
|
|
if (ctx.json)
|
|
(0, output_1.printJson)(result);
|
|
else
|
|
process.stdout.write(`✓ Record(s) created\n`);
|
|
}));
|
|
cmd
|
|
.command('update')
|
|
.description('Batch update records')
|
|
.requiredOption('--table-id <id>', 'Table id')
|
|
.requiredOption('--records <json>', 'Records JSON, e.g. \'[{"id":"recX","fields":{"Title":"b"}}]\'')
|
|
.action((0, ctx_1.run)(async (ctx, _args, o) => {
|
|
const opts = o;
|
|
const body = { records: JSON.parse(opts.records) };
|
|
const result = await ctx.client.updateRecord(opts.tableId, body);
|
|
if (ctx.json)
|
|
(0, output_1.printJson)(result);
|
|
else
|
|
process.stdout.write(`✓ Records updated\n`);
|
|
}));
|
|
cmd
|
|
.command('delete')
|
|
.description('Batch delete records by ids')
|
|
.requiredOption('--table-id <id>', 'Table id')
|
|
.requiredOption('--record-ids <csv>', 'Record ids (comma-separated)')
|
|
.action((0, ctx_1.run)(async (ctx, _args, o) => {
|
|
const opts = o;
|
|
const ids = opts.recordIds
|
|
.split(',')
|
|
.map((s) => s.trim())
|
|
.filter(Boolean);
|
|
const result = await ctx.client.deleteRecords(opts.tableId, ids);
|
|
if (ctx.json)
|
|
(0, output_1.printJson)(result);
|
|
else
|
|
process.stdout.write(`✓ ${ids.length} record(s) deleted\n`);
|
|
}));
|
|
return cmd;
|
|
}
|
|
// ===================== view =====================
|
|
function viewCmd() {
|
|
const cmd = new commander_1.Command('view');
|
|
cmd.description('Manage views in a table');
|
|
cmd
|
|
.command('list <tableId>')
|
|
.description('List views in a table')
|
|
.action((0, ctx_1.run)(async (ctx, args) => {
|
|
const tableId = args[0];
|
|
const list = (await ctx.client.listViews(tableId));
|
|
if (ctx.json)
|
|
(0, output_1.printJson)(list);
|
|
else {
|
|
if (!list.length) {
|
|
process.stdout.write('No views found.\n');
|
|
return;
|
|
}
|
|
(0, output_1.printTable)([
|
|
{ key: 'id', header: 'ID' },
|
|
{ key: 'name', header: 'NAME' },
|
|
{ key: 'type', header: 'TYPE' },
|
|
], list.map((v) => ({
|
|
id: (0, output_1.truncate)(v.id),
|
|
name: (0, output_1.truncate)(v.name, 30),
|
|
type: v.type,
|
|
})));
|
|
}
|
|
}));
|
|
cmd
|
|
.command('get <viewId>')
|
|
.description('Get view details')
|
|
.action((0, ctx_1.run)(async (ctx, args) => {
|
|
const viewId = args[0];
|
|
const view = await ctx.client.getView(viewId);
|
|
if (ctx.json)
|
|
(0, output_1.printJson)(view);
|
|
else {
|
|
const v = view;
|
|
(0, output_1.printKv)(Object.entries(v).map(([k, val]) => [k, val]));
|
|
}
|
|
}));
|
|
cmd
|
|
.command('create')
|
|
.description('Create a new view')
|
|
.requiredOption('--table-id <id>', 'Table id')
|
|
.requiredOption('--name <name>', 'View name')
|
|
.option('--type <type>', 'View type (grid, calendar, kanban, gallery, form)', 'grid')
|
|
.action((0, ctx_1.run)(async (ctx, _args, o) => {
|
|
const opts = o;
|
|
const body = { name: opts.name, type: opts.type };
|
|
const view = await ctx.client.createView(opts.tableId, body);
|
|
if (ctx.json)
|
|
(0, output_1.printJson)(view);
|
|
else {
|
|
const v = view;
|
|
(0, output_1.printKv)([
|
|
['id', v.id],
|
|
['name', v.name],
|
|
['type', v.type],
|
|
]);
|
|
}
|
|
}));
|
|
cmd
|
|
.command('update <viewId>')
|
|
.description('Update a view (rename)')
|
|
.option('--name <name>', 'New view name')
|
|
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
|
const viewId = args[0];
|
|
const opts = o;
|
|
const body = {};
|
|
if (opts.name)
|
|
body.name = opts.name;
|
|
const view = await ctx.client.updateView(viewId, body);
|
|
if (ctx.json)
|
|
(0, output_1.printJson)(view);
|
|
else
|
|
process.stdout.write(`✓ View ${viewId} updated\n`);
|
|
}));
|
|
cmd
|
|
.command('delete <viewId>')
|
|
.description('Delete a view')
|
|
.action((0, ctx_1.run)(async (ctx, args) => {
|
|
const viewId = args[0];
|
|
await ctx.client.deleteView(viewId);
|
|
if (ctx.json)
|
|
(0, output_1.printJson)({ ok: true, id: viewId });
|
|
else
|
|
process.stdout.write(`✓ View ${viewId} deleted\n`);
|
|
}));
|
|
return cmd;
|
|
}
|
|
// ===================== view preset =====================
|
|
/** Parse a --filter/--sort/... JSON option; empty/absent → undefined. */
|
|
function parseJsonOption(value, optionName) {
|
|
if (value == null || value === '')
|
|
return undefined;
|
|
try {
|
|
return JSON.parse(value);
|
|
}
|
|
catch (err) {
|
|
throw new Error(`Invalid JSON for ${optionName}: ${err.message}`);
|
|
}
|
|
}
|
|
/** Collect preset config keys from options; absent keys stay out of the config. */
|
|
function presetConfigFromOptions(opts) {
|
|
const config = {};
|
|
const entries = [
|
|
['filter', '--filter'],
|
|
['sort', '--sort'],
|
|
['group', '--group'],
|
|
['options', '--options'],
|
|
['columnMeta', '--column-meta'],
|
|
];
|
|
for (const [key, optionName] of entries) {
|
|
const parsed = parseJsonOption(opts[key], optionName);
|
|
if (parsed !== undefined)
|
|
config[key] = parsed;
|
|
}
|
|
return config;
|
|
}
|
|
exports.presetConfigFromOptions = presetConfigFromOptions;
|
|
function presetCmd() {
|
|
const cmd = new commander_1.Command('preset');
|
|
cmd.description('Manage preset view configurations (named filter/sort/group bundles, shared with all collaborators)');
|
|
cmd
|
|
.command('list <tableId>')
|
|
.description('List view presets of a table (pass --view-id to scope to one view)')
|
|
.option('--view-id <id>', 'Only presets bound to this view')
|
|
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
|
const tableId = args[0];
|
|
const opts = o;
|
|
const list = (await ctx.client.listViewPresets(tableId, opts.viewId));
|
|
if (ctx.json)
|
|
(0, output_1.printJson)(list);
|
|
else {
|
|
if (!list.length) {
|
|
process.stdout.write('No view presets found.\n');
|
|
return;
|
|
}
|
|
(0, output_1.printTable)([
|
|
{ key: 'id', header: 'ID' },
|
|
{ key: 'name', header: 'NAME' },
|
|
{ key: 'config', header: 'CONFIG' },
|
|
], list.map((p) => {
|
|
const config = (p.config ?? {});
|
|
const keys = Object.keys(config).filter((k) => config[k] != null);
|
|
return {
|
|
id: (0, output_1.truncate)(p.id),
|
|
name: (0, output_1.truncate)(p.name, 30),
|
|
config: keys.join(',') || '(empty)',
|
|
};
|
|
}));
|
|
}
|
|
}));
|
|
cmd
|
|
.command('create')
|
|
.description('Create a view preset bound to one view (keys not passed stay unset — they keep the view values on apply)')
|
|
.requiredOption('--table-id <id>', 'Table id')
|
|
.requiredOption('--view-id <id>', 'View id the preset belongs to')
|
|
.requiredOption('--name <name>', 'Preset name')
|
|
.option('--description <text>', 'Preset description')
|
|
.option('--filter <json>', 'Filter config, e.g. \'{"conjunction":"and","filterSet":[{"fieldId":"fldX","operator":"is","value":"Pending"}]}\'')
|
|
.option('--sort <json>', 'Sort config, e.g. \'{"sortObjs":[{"fieldId":"fldX","order":"asc"}]}\'')
|
|
.option('--group <json>', 'Group config (JSON array of group items)')
|
|
.option('--options <json>', 'View options (JSON object)')
|
|
.option('--column-meta <json>', 'Column meta (field visibility/order, JSON object)')
|
|
.action((0, ctx_1.run)(async (ctx, _args, o) => {
|
|
const opts = o;
|
|
const body = {
|
|
name: opts.name,
|
|
viewId: opts.viewId,
|
|
config: presetConfigFromOptions(opts),
|
|
};
|
|
if (opts.description)
|
|
body.description = opts.description;
|
|
const preset = (await ctx.client.createViewPreset(opts.tableId, body));
|
|
if (ctx.json)
|
|
(0, output_1.printJson)(preset);
|
|
else {
|
|
(0, output_1.printKv)([
|
|
['id', preset.id],
|
|
['name', preset.name],
|
|
['order', preset.order],
|
|
]);
|
|
}
|
|
}));
|
|
cmd
|
|
.command('update <presetId>')
|
|
.description('Update a view preset (rename and/or replace config keys)')
|
|
.requiredOption('--table-id <id>', 'Table id')
|
|
.option('--name <name>', 'New preset name')
|
|
.option('--description <text>', 'New preset description')
|
|
.option('--filter <json>', 'Replace filter config (JSON)')
|
|
.option('--sort <json>', 'Replace sort config (JSON)')
|
|
.option('--group <json>', 'Replace group config (JSON)')
|
|
.option('--options <json>', 'Replace view options (JSON object)')
|
|
.option('--column-meta <json>', 'Replace column meta (JSON object)')
|
|
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
|
const presetId = args[0];
|
|
const opts = o;
|
|
const body = {};
|
|
if (opts.name)
|
|
body.name = opts.name;
|
|
if (opts.description !== undefined)
|
|
body.description = opts.description;
|
|
const config = presetConfigFromOptions(opts);
|
|
if (Object.keys(config).length)
|
|
body.config = config;
|
|
await ctx.client.updateViewPreset(opts.tableId, presetId, body);
|
|
if (ctx.json)
|
|
(0, output_1.printJson)({ ok: true, id: presetId });
|
|
else
|
|
process.stdout.write(`✓ Preset ${presetId} updated\n`);
|
|
}));
|
|
cmd
|
|
.command('delete <presetId>')
|
|
.description('Delete a view preset')
|
|
.requiredOption('--table-id <id>', 'Table id')
|
|
.action((0, ctx_1.run)(async (ctx, args, o) => {
|
|
const presetId = args[0];
|
|
const opts = o;
|
|
await ctx.client.deleteViewPreset(opts.tableId, presetId);
|
|
if (ctx.json)
|
|
(0, output_1.printJson)({ ok: true, id: presetId });
|
|
else
|
|
process.stdout.write(`✓ Preset ${presetId} deleted\n`);
|
|
}));
|
|
return cmd;
|
|
}
|