flowbase-cli 0.1.12__tar.gz → 0.1.14__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flowbase-cli
3
- Version: 0.1.12
3
+ Version: 0.1.14
4
4
  Summary: Command line client for the FlowBase OpenAPI
5
5
  Author: DeepFlowAI
6
6
  License: Apache-2.0
@@ -125,6 +125,7 @@ flowbase docs catalogs permissions # function permission points + data sc
125
125
  flowbase docs catalogs dashboard-filters # filter types + date-range presets
126
126
  flowbase docs catalogs page-components # Page component shapes, placement, fields, and enums
127
127
  flowbase docs catalogs page-rules # Page condition operators and action shapes
128
+ flowbase docs catalogs forms # custom form fields, rules, appearance, and settings
128
129
  flowbase docs catalogs id-prefixes # resource ID prefix meanings
129
130
  flowbase docs catalogs icons # icon guidance + examples
130
131
  ```
@@ -116,6 +116,7 @@ flowbase docs catalogs permissions # function permission points + data sc
116
116
  flowbase docs catalogs dashboard-filters # filter types + date-range presets
117
117
  flowbase docs catalogs page-components # Page component shapes, placement, fields, and enums
118
118
  flowbase docs catalogs page-rules # Page condition operators and action shapes
119
+ flowbase docs catalogs forms # custom form fields, rules, appearance, and settings
119
120
  flowbase docs catalogs id-prefixes # resource ID prefix meanings
120
121
  flowbase docs catalogs icons # icon guidance + examples
121
122
  ```
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "flowbase-cli"
7
- version = "0.1.12"
7
+ version = "0.1.14"
8
8
  description = "Command line client for the FlowBase OpenAPI"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,3 +1,3 @@
1
1
  """FlowBase OpenAPI CLI."""
2
2
 
3
- __version__ = "0.1.12"
3
+ __version__ = "0.1.14"
@@ -169,6 +169,26 @@ ENDPOINTS = (
169
169
  )),
170
170
  Endpoint("dashboards.copy", "flowbase_dashboards_copy", "POST", "/dashboards/{dashboard_id}/copy", "metadata:write", "Copy a dashboard (not added to any app).", "flowbase dashboards copy dsh_xxx"),
171
171
  Endpoint("dashboards.delete", "flowbase_dashboards_delete", "POST", "/dashboards/{dashboard_id}/delete", "metadata:write", "Logically delete a dashboard (also removed from all apps).", "flowbase dashboards delete dsh_xxx --yes", True),
172
+ Endpoint("forms.list", "flowbase_forms_list", "GET", "/forms", "metadata:read", "List custom forms with collection status.", "flowbase forms list"),
173
+ Endpoint("forms.get", "flowbase_forms_get", "GET", "/forms/{form_id}", "metadata:read", "Get a custom form definition and its placeable field options. Read docs catalogs forms before update.", "flowbase forms get frm_xxx"),
174
+ Endpoint("forms.create", "flowbase_forms_create", "POST", "/forms", "metadata:write", "Create a custom form draft; object-required fields are pre-added.", "flowbase forms create --label 'Website inquiry' --key website_inquiry --object-id obj_xxx",
175
+ request_body='{"label":"Website inquiry","key":"website_inquiry","objectId":"obj_xxx","description":""}'),
176
+ Endpoint("forms.update", "flowbase_forms_update", "PUT", "/forms/{form_id}", "metadata:write", "Update a custom form draft (full snapshot: fields, rules, appearance, settings). Read docs catalogs forms first.", "flowbase forms update frm_xxx --json-file form.json",
177
+ request_body=(
178
+ '{"label":"Website inquiry","description":"","baseRevision":3,'
179
+ '"fields":[{"fieldId":"fld_xxx","label":"","description":"","state":"required","prefill":false}],'
180
+ '"rules":[],'
181
+ '"appearance":{"title":"Website inquiry","description":"","logo":null,"cover":null,"themeColor":"#171717",'
182
+ '"background":"gray","submitText":"提交","language":"zh-CN"},'
183
+ '"settings":{"afterSubmit":"message","successTitle":"提交成功","successBody":"感谢你的填写",'
184
+ '"allowResubmit":true,"redirectUrl":"","deadline":null,"limit":null}}'
185
+ )),
186
+ Endpoint("forms.copy", "flowbase_forms_copy", "POST", "/forms/{form_id}/copy", "metadata:write", "Copy a custom form (collection state is not copied).", "flowbase forms copy frm_xxx"),
187
+ Endpoint("forms.delete", "flowbase_forms_delete", "POST", "/forms/{form_id}/delete", "metadata:write", "Logically delete a custom form.", "flowbase forms delete frm_xxx --yes", True),
188
+ Endpoint("forms.share.get", "flowbase_forms_share_get", "GET", "/forms/{form_id}/share", "metadata:read", "Get the public link and collection state in this environment.", "flowbase forms share frm_xxx"),
189
+ Endpoint("forms.share.set-accepting", "flowbase_forms_share_set_accepting", "PUT", "/forms/{form_id}/share/accepting", "metadata:write", "Turn public collection on or off in this environment (immediate, not a draft change).", "flowbase forms set-accepting frm_xxx --on",
190
+ request_body='{"accepting":true}'),
191
+ Endpoint("forms.share.reset", "flowbase_forms_share_reset", "POST", "/forms/{form_id}/share/reset", "metadata:write", "Reset the public link; the old link stops working immediately.", "flowbase forms reset-link frm_xxx --yes", True),
172
192
  Endpoint("objects.list", "flowbase_objects_list", "GET", "/objects", "metadata:read", "List object metadata.", "flowbase objects list"),
173
193
  Endpoint("objects.get", "flowbase_objects_get", "GET", "/objects/{object_id}", "metadata:read", "Get object and field metadata.", "flowbase objects get obj_xxx"),
174
194
  Endpoint("objects.create", "flowbase_objects_create", "POST", "/objects", "metadata:write", "Create an object draft.", "flowbase objects create --json-file object.json",
@@ -324,19 +344,19 @@ ENDPOINTS = (
324
344
  request_body='{"baseRevision":4,"enabled":true}'),
325
345
  Endpoint("approvals.delete", "flowbase_approvals_delete", "POST", "/objects/{object_id}/approvals/{approval_id}/delete", "metadata:write", "Logically delete an approval flow.", "flowbase approvals delete obj_xxx apr_xxx --base-revision 4 --yes", True,
326
346
  request_body='{"baseRevision":4}'),
327
- Endpoint("print-templates.list", "flowbase_print_templates_list", "GET", "/objects/{object_id}/print-templates", "metadata:read", "List object print templates.", "flowbase print-templates list obj_xxx"),
328
- Endpoint("print-templates.get", "flowbase_print_templates_get", "GET", "/objects/{object_id}/print-templates/{template_id}", "metadata:read", "Get a print template with HTML and data code.", "flowbase print-templates get obj_xxx ptp_xxx"),
329
- Endpoint("print-templates.create", "flowbase_print_templates_create", "POST", "/objects/{object_id}/print-templates", "metadata:write", "Create a print template draft.", "flowbase print-templates create obj_xxx --json-file template.json",
347
+ Endpoint("print-templates.list", "flowbase_print_templates_list", "GET", "/objects/{object_id}/print-templates", "metadata:read", "List object print templates. draftRevision is on the envelope.", "flowbase print-templates list obj_xxx"),
348
+ Endpoint("print-templates.get", "flowbase_print_templates_get", "GET", "/objects/{object_id}/print-templates/{template_id}", "metadata:read", "Get a print template with HTML and data code. Returns source, not rendered HTML.", "flowbase print-templates get obj_xxx ptp_xxx"),
349
+ Endpoint("print-templates.create", "flowbase_print_templates_create", "POST", "/objects/{object_id}/print-templates", "metadata:write", "Create a print template draft. Read docs catalogs print-templates first.", "flowbase print-templates create obj_xxx --json-file template.json",
330
350
  request_body=(
331
351
  '{"baseRevision":3,"name":"Sales order","key":"sales_order","description":"",'
332
352
  '"paper":{"size":"A4","width":null,"height":null,"orientation":"portrait",'
333
353
  '"margins":{"top":15,"right":15,"bottom":15,"left":15}},'
334
354
  '"header":{"left":"","center":"","right":""},'
335
- '"footer":{"left":"","center":"Page {page} / {pages}","right":""},'
336
- '"html":"<h1>{{ record.summary }}</h1>\\n<p>{{ data.total | money(\\"CNY\\") }}</p>",'
337
- '"code":"import flowbase\\n\\ndef run():\\n return {\\"total\\": 0}"}'
355
+ '"footer":{"left":"","center":"第 {page} 页 / 共 {pages} 页","right":""},'
356
+ '"html":"<h1>{{ record.summary }}</h1>\\n<p>{{ record.name }}</p>\\n<p>{{ data.total | money(\\"CNY\\") }}</p>",'
357
+ '"code":"import flowbase\\n\\ndef run():\\n amount = flowbase.record.values.get(\\"amount\\") or 0\\n return {\\"total\\": amount}"}'
338
358
  )),
339
- Endpoint("print-templates.update", "flowbase_print_templates_update", "PUT", "/objects/{object_id}/print-templates/{template_id}", "metadata:write", "Update a print template; omitted fields keep their value.", "flowbase print-templates update obj_xxx ptp_xxx --json-file template.json",
359
+ Endpoint("print-templates.update", "flowbase_print_templates_update", "PUT", "/objects/{object_id}/print-templates/{template_id}", "metadata:write", "Partial update. Send changed fields plus baseRevision and baseUpdatedAt. Do not resend id, key, enabled, updatedAt, or draftRevision.", "flowbase print-templates update obj_xxx ptp_xxx --json-file template.json",
340
360
  request_body=(
341
361
  '{"baseRevision":4,"baseUpdatedAt":"2026-10-13T08:00:00+00:00",'
342
362
  '"html":"<h1>{{ record.summary }}</h1>"}'
@@ -563,7 +583,8 @@ ENDPOINT_BY_KEY = {endpoint.key: endpoint for endpoint in ENDPOINTS}
563
583
 
564
584
  # Closed-domain catalogs for offline authoring of complex request bodies.
565
585
  # Keep in sync with server registries (field types, workflow/approval node kinds,
566
- # permission points, dashboard filter presets, page component kinds).
586
+ # permission points, dashboard filter presets, page component kinds, form
587
+ # snapshot validation).
567
588
  # Icons intentionally reference the server file because the full Tabler set is large.
568
589
  _PERMISSIONS = ("required", "optional", "readonly", "hidden")
569
590
  _TEXT_FILTERS = ("eq", "neq", "contains", "not_contains", "is_empty", "is_not_empty")
@@ -802,7 +823,10 @@ AUTHORING = {
802
823
  ],
803
824
  },
804
825
  "documentationKeys": "docs list returns endpoint keys. Use apps.objects.add for apps add-objects; docs show includes the CLI example.",
805
- "requiredUpdateFields": {"apps.update": ["label", "icon", "color"]},
826
+ "requiredUpdateFields": {
827
+ "apps.update": ["label", "icon", "color"],
828
+ "forms.update": ["label", "appearance", "settings"],
829
+ },
806
830
  "requiredCreateFields": {
807
831
  "objects.create": ["label", "key", "description"],
808
832
  "fields.create": ["label", "key", "description", "type"],
@@ -810,19 +834,28 @@ AUTHORING = {
810
834
  "views.create": ["baseRevision", "name", "defaultFieldIds"],
811
835
  "pages.create": ["baseRevision", "page"],
812
836
  "records.create": ["values"],
837
+ "print-templates.create": ["name", "html"],
838
+ "forms.create": ["label", "key", "objectId"],
813
839
  },
814
840
  "draftRevision": {
815
841
  "notReturnedBy": ["objects.get", "objects.list", "apps.get", "apps.list"],
816
842
  "returnedBy": [
817
843
  "views.list", "pages.list", "pages.get",
844
+ "print-templates.list", "print-templates.get",
845
+ "forms.get",
818
846
  "every metadata write response (draftRevision)",
819
847
  ],
820
- "note": "Capture draftRevision from the last write or from views.list/pages.list before views/pages writes.",
848
+ "note": "Capture draftRevision from the last write or from views.list/pages.list/print-templates.list before writes.",
821
849
  },
822
850
  "readThenWrite": (
823
- "For update of views/pages/dashboards/fields with constraints: get the snapshot, "
824
- "edit it, set baseRevision from draftRevision. fields.update.constraints is a full replace; "
825
- "sending {} wipes options."
851
+ "For update of views/pages/dashboards/forms/fields with constraints: get the snapshot, "
852
+ "edit it, set baseRevision from draftRevision. forms.update unwraps forms.get: send "
853
+ "label, description, fields, rules, appearance, settings, and baseRevision; omit the "
854
+ "form wrapper, id, key, objectId, updatedAt, and fieldOptions. Omitting fields or rules "
855
+ "clears them. fields.update.constraints is a full replace; sending {} wipes options. "
856
+ "print-templates.update is partial: send only changed fields, baseRevision, and "
857
+ "baseUpdatedAt copied from updatedAt. Resending id, key, enabled, updatedAt, or "
858
+ "draftRevision is rejected."
826
859
  ),
827
860
  "recordsNeedReleasedMetadata": (
828
861
  "On production, records.create/list/get use the released runtime, not the current draft. "
@@ -836,6 +869,7 @@ AUTHORING = {
836
869
  "dashboards.run": "{ok,output,error,logs,durationMs,cached,mode:draft-preview}; inspect ok and error.",
837
870
  "apps.create": "{appId,draftRevision}",
838
871
  "dashboards.create": "{dashboardId,draftRevision}",
872
+ "forms.create": "{formId,draftRevision}",
839
873
  "workflows.create": "{workflowId,draftRevision}",
840
874
  "approvals.create": "{approvalId,draftRevision}",
841
875
  "print-templates.create": "{templateId,draftRevision}",
@@ -872,6 +906,10 @@ AUTHORING = {
872
906
  "wrong": "select/lookup filter or record value uses the display label",
873
907
  "right": "Use opt_/rec_/emp_ ids from objects.get / records.get.",
874
908
  },
909
+ {
910
+ "wrong": "forms update with the forms.get body, or fields omitted",
911
+ "right": "Unwrap form, keep every placed field, and set baseRevision from draftRevision. See docs catalogs forms.",
912
+ },
875
913
  {
876
914
  "wrong": "records.list.values is the full record",
877
915
  "right": "List is a column projection. records.get for all field keys.",
@@ -928,6 +966,18 @@ AUTHORING = {
928
966
  "wrong": "dashboard date_range filter with no default",
929
967
  "right": "Unset date_range defaults to last_30_days at runtime; set default.preset or omit the filter.",
930
968
  },
969
+ {
970
+ "wrong": "print HTML {name} or {{ name }} or {{ record.客户名称 }}",
971
+ "right": "Use {{ record.<field key> }} with the key from objects get. See docs catalogs print-templates.",
972
+ },
973
+ {
974
+ "wrong": "print footer {{ page }} / {{ pages }}",
975
+ "right": "Page numbers are the literal tokens {page} and {pages}, only in header and footer.",
976
+ },
977
+ {
978
+ "wrong": "print-templates.update with the whole get document",
979
+ "right": "Partial update. Omit id, key, enabled, updatedAt, and draftRevision. Copy updatedAt to baseUpdatedAt.",
980
+ },
931
981
  ],
932
982
  }
933
983
 
@@ -968,7 +1018,7 @@ APPROVAL_NODE_TYPES = (
968
1018
  )
969
1019
 
970
1020
  FUNCTION_PERMISSIONS = (
971
- "workspace.access", "app.manage", "object.manage", "dashboard.manage",
1021
+ "workspace.access", "app.manage", "object.manage", "dashboard.manage", "form.manage",
972
1022
  "version.manage", "org.manage", "system.manage",
973
1023
  )
974
1024
 
@@ -1269,6 +1319,8 @@ RESOURCE_ID_PREFIXES = {
1269
1319
  "reg_": "page region", "cmp_": "page component",
1270
1320
  "dsh_": "dashboard", "dcm_": "dashboard component", "rol_": "role",
1271
1321
  "rec_": "record", "att_": "attachment", "emp_": "employee", "dep_": "department",
1322
+ "ptp_": "print template", "frm_": "custom form", "fim_": "form image",
1323
+ "rule_": "form or page rule",
1272
1324
  }
1273
1325
 
1274
1326
  WORKFLOW_BRANCHES = {
@@ -1323,11 +1375,155 @@ DASHBOARD_RUNTIME = {
1323
1375
  "example": "import flowbase\n\ndef run():\n return {\"type\": \"metric\", \"value\": 1}\n",
1324
1376
  }
1325
1377
 
1378
+ PRINT_TEMPLATES = {
1379
+ "command": "flowbase docs catalogs print-templates",
1380
+ "readFirst": "objects get supplies field keys. print-templates list supplies draftRevision. print-templates get supplies html, code, header, footer, and updatedAt.",
1381
+ "placeholders": {
1382
+ "html": "Jinja2: {{ }} output, {% for %} loops, {% if %} conditions. An unknown name renders blank and still saves.",
1383
+ "record": "{{ record.<fieldKey> }} is display text. {{ record.values.<fieldKey> }} is the raw value. {{ record.summary }} is the record summary. Built-in id, summary, and values win over a field with the same key.",
1384
+ "keys": "fieldKey is the field key from objects get, never the label and never a fld_ id. Replace name and amount in the create example with keys from that object.",
1385
+ "data": "data is only the dict returned by run(). With no code, data is {}. {{ data.total }} stays blank unless run() returns total.",
1386
+ "other": "employee.name, employee.email, now (ISO string; format with the date filter), template.name, template.key.",
1387
+ "pageTokens": "{page} and {pages} are not Jinja. They work only in header and footer, as literal single-brace text. {{ page }} is erased before page numbers are applied. In the HTML body, {page} stays as the text {page}.",
1388
+ },
1389
+ "filters": {
1390
+ "money": "{{ record.values.amount | money(\"CNY\") }} thousands, 2 decimals, currency symbol. Use the raw value.",
1391
+ "money_upper": "{{ data.total | money_upper }} RMB amount in Chinese capitals.",
1392
+ "date": "{{ record.values.signed_on | date(\"YYYY-MM-DD HH:mm\") }} or {{ now | date(\"YYYY-MM-DD\") }}.",
1393
+ "rich": "{{ record.values.remark | rich }} sanitized HTML for a rich-text field.",
1394
+ "image": "{{ record.values.photo | image(width=\"40mm\") }} inlines a visible image attachment.",
1395
+ "qrcode": "{{ record.values.order_no | qrcode(size=\"25mm\") }}",
1396
+ "barcode": "{{ record.values.order_no | barcode(\"code128\", height=\"12mm\") }} code128 or ean13.",
1397
+ "nl2br": "{{ record.notes | nl2br }} turns newlines into <br>.",
1398
+ "removed": "safe, urlize, and xmlattr are unavailable. Output is HTML-escaped unless a filter above returns HTML.",
1399
+ },
1400
+ "code": {
1401
+ "shape": "Optional. import flowbase only. Define run() with no arguments and return a JSON object, max 2 MB.",
1402
+ "record": "flowbase.record.values[key] is raw; flowbase.record.display[key] is display text; flowbase.record.summary is the summary.",
1403
+ "related": "flowbase.records.related(relation, fields=None, order_by=None, limit=200, record=None). relation is a lookup field key, forward or reverse. Return line.display dicts so the template can use {{ line.<fieldKey> }}.",
1404
+ "get": "flowbase.records.get(object, record_id, fields=None) returns the same shape or None. object is an object key.",
1405
+ "query": "flowbase.query.records.count/group_by/timeseries/list match dashboard-runtime but have no inherit_filters argument. scope is own or omitted.",
1406
+ "approvals": "flowbase.approvals.history(record=None) returns up to 50 instances.",
1407
+ "example": "import flowbase\n\ndef run():\n record = flowbase.record\n lines = flowbase.records.related(\"lines\", fields=[\"product\", \"amount\"])\n return {\n \"total\": record.values.get(\"amount\") or 0,\n \"lines\": [line.display for line in lines],\n }\n",
1408
+ },
1409
+ "write": {
1410
+ "create": "Requires name and html. Other settings have defaults. New templates are enabled. key cannot be changed later. enabled is not a create/update field; use set-enabled.",
1411
+ "update": "Partial. Omitted fields stay. Copy updatedAt to baseUpdatedAt and draftRevision to baseRevision. Do not send id, key, enabled, updatedAt, or draftRevision.",
1412
+ "limits": "At most 20 templates per object. HTML 200 KB, code 100 KB, each header/footer slot 200 characters. A non-empty header needs top margin >= 10; a non-empty footer needs bottom margin >= 10.",
1413
+ },
1414
+ "verify": "The management CLI cannot render a template. get returns the source. A saved {{ }} that names the wrong key prints blank. There is no print-templates run command.",
1415
+ }
1416
+
1417
+ FORMS = {
1418
+ "command": "flowbase docs catalogs forms",
1419
+ "readFirst": (
1420
+ "forms get returns form, fieldOptions, and draftRevision. "
1421
+ "fieldOptions[].availability is the placement contract: placeable, reason, states, "
1422
+ "preset, presetRequired, prefill, plus objectRequired and usableDefault. "
1423
+ "Use field IDs from fieldOptions, never field keys."
1424
+ ),
1425
+ "write": {
1426
+ "create": (
1427
+ "Requires label, key, and objectId. description defaults to empty. "
1428
+ "Placeable object-required fields are pre-added. key starts with a lowercase letter, "
1429
+ "contains only lowercase letters, digits, and underscores, max 50, and cannot change later."
1430
+ ),
1431
+ "update": (
1432
+ "Full snapshot. Requires label, appearance, and settings. "
1433
+ "fields and rules default to [] and clear the form when omitted. "
1434
+ "Copy draftRevision to baseRevision. Unwrap forms.get: do not send the form wrapper, "
1435
+ "id, key, objectId, updatedAt, or fieldOptions."
1436
+ ),
1437
+ "channel": (
1438
+ "set-accepting and reset-link take effect immediately in this environment and are not "
1439
+ "draft changes. copy does not copy collection state or the public link. delete and "
1440
+ "reset-link require --yes."
1441
+ ),
1442
+ },
1443
+ "limits": {
1444
+ "formsPerEnvironment": 200,
1445
+ "fields": 100,
1446
+ "rules": 50,
1447
+ "label": "1 to 50 characters",
1448
+ "description": "max 200 characters",
1449
+ "conditionsPerRule": "1 to 10",
1450
+ },
1451
+ "field": {
1452
+ "requiredKeys": ["fieldId", "label", "description", "state", "prefill"],
1453
+ "optionalKeys": ["preset"],
1454
+ "label": "Display override. Empty string keeps the object field label. Max 100.",
1455
+ "description": "Max 500. Empty string is allowed.",
1456
+ "state": (
1457
+ "One of availability.states. Ordinary fields use required, optional, readonly, hidden. "
1458
+ "lookup.record, lookup.record.multiple, lookup.employee, and lookup.department are "
1459
+ "hidden only. attachment cannot be readonly."
1460
+ ),
1461
+ "prefill": "true only when availability.prefill is true. Hidden-only, attachment, and text.rich cannot be prefilled.",
1462
+ "preset": (
1463
+ "Omit the key when there is no preset. Required when availability.presetRequired. "
1464
+ "A fixed value uses the record value shape from record-values. "
1465
+ "A dynamic preset is {\"mode\":\"dynamic\",\"value\":\"<dynamic default>\"}; currentEmployee is rejected."
1466
+ ),
1467
+ "objectRequired": (
1468
+ "Every placeable object-required field must stay in fields. It cannot be optional. "
1469
+ "readonly or hidden needs a preset unless usableDefault is true."
1470
+ ),
1471
+ "fillable": "At least one placed field is required or optional, or an enabled rule sets one of those states.",
1472
+ },
1473
+ "rule": {
1474
+ "requiredKeys": ["name", "enabled", "match", "conditions", "actions"],
1475
+ "optionalKeys": ["id"],
1476
+ "id": "Omit id, or send a value that does not start with rule_, to allocate a new rule_ id. Preserve an existing rule_ id on update.",
1477
+ "name": "1 to 50 characters, unique in the form.",
1478
+ "match": ["all", "any"],
1479
+ "conditions": (
1480
+ "Same operators as page-rules. Candidates are placed fields that are not "
1481
+ "lookup.record, lookup.record.multiple, lookup.employee, or lookup.department."
1482
+ ),
1483
+ "actions": (
1484
+ "Only setFieldState: {\"type\":\"setFieldState\",\"fieldId\":\"fld_...\",\"state\":\"required\"}. "
1485
+ "state must be in that field's availability.states. No component visibility."
1486
+ ),
1487
+ },
1488
+ "appearance": {
1489
+ "requiredKeys": [
1490
+ "title", "description", "logo", "cover", "themeColor",
1491
+ "background", "submitText", "language",
1492
+ ],
1493
+ "title": "1 to 100 characters.",
1494
+ "description": "Max 2000 characters.",
1495
+ "logo": "null or an existing fim_ id. The management CLI cannot upload images.",
1496
+ "cover": "null or an existing fim_ id. The management CLI cannot upload images.",
1497
+ "themeColor": "#RRGGBB",
1498
+ "background": ["gray", "white", "tint"],
1499
+ "submitText": "Max 20 characters. Empty string is allowed.",
1500
+ "language": ["zh-CN", "en"],
1501
+ },
1502
+ "settings": {
1503
+ "requiredKeys": [
1504
+ "afterSubmit", "successTitle", "successBody", "allowResubmit",
1505
+ "redirectUrl", "deadline", "limit",
1506
+ ],
1507
+ "afterSubmit": ["message", "redirect"],
1508
+ "successTitle": "Max 50 characters.",
1509
+ "successBody": "Max 500 characters.",
1510
+ "redirectUrl": "An https URL when afterSubmit is redirect, max 2048. Otherwise an empty string is allowed.",
1511
+ "deadline": "null, or an ISO-8601 datetime with a timezone. A newly set deadline cannot be in the past.",
1512
+ "limit": "null, or an integer from 1 to 1000000.",
1513
+ },
1514
+ "collectStatus": ["inactive", "invalid", "closed", "deadline", "full", "collecting"],
1515
+ "notInCli": (
1516
+ "Public form read, submit, and attachment upload need no API key and are not management commands. "
1517
+ "Logo and cover upload exist only on the signed-in admin API."
1518
+ ),
1519
+ }
1520
+
1326
1521
  CATALOGS = {
1327
1522
  "workflow-branches": WORKFLOW_BRANCHES,
1328
1523
  "approval-rules": APPROVAL_RULES,
1329
1524
  "page-buttons": PAGE_BUTTONS,
1330
1525
  "dashboard-runtime": DASHBOARD_RUNTIME,
1526
+ "print-templates": PRINT_TEMPLATES,
1331
1527
  "field-types": FIELD_TYPES,
1332
1528
  "view-filters": VIEW_FILTERS,
1333
1529
  "record-values": RECORD_VALUES,
@@ -1345,6 +1541,7 @@ CATALOGS = {
1345
1541
  },
1346
1542
  "page-components": PAGE_COMPONENTS,
1347
1543
  "page-rules": PAGE_RULES,
1544
+ "forms": FORMS,
1348
1545
  "id-prefixes": RESOURCE_ID_PREFIXES,
1349
1546
  "icons": {
1350
1547
  "note": "6146 Tabler icon names are supported; any Icon* name from the "
@@ -8,7 +8,8 @@ GUIDE = {
8
8
  "flowbase apps list;flowbase objects list:发现已有应用和对象,优先复用,不猜测 ID。",
9
9
  "flowbase objects get <object_id>:读取字段 key、类型、约束和真实选项 ID;关联字段继续读取目标对象。",
10
10
  "flowbase docs catalogs authoring;flowbase docs list;flowbase docs show <endpoint_key>:查请求和更新方式。",
11
- "配置页面前读取 pages options、page-components、page-rules、page-buttons;配置流程前读取 workflow-nodes、workflow-branches 或 approval-rules。",
11
+ "配置页面前读取 pages options、page-components、page-rules、page-buttons;配置流程前读取 workflow-nodes、workflow-branches 或 approval-rules;配置打印模板前读取 print-templates;配置自定义表单前读取 forms。",
12
+ "表单先 forms get,按目录把快照拆成 label、fields、rules、appearance、settings 再 update,baseRevision 用返回的 draftRevision。对象必填且可放入的字段必须保留。set-accepting 与 reset-link 立即生效,不进草稿。",
12
13
  "先 get 再修改,保留未修改的配置。按目录转换更新 envelope;从最近读取或写入返回值获取 draftRevision,作为 baseRevision。",
13
14
  "写后回读验证。records create 返回 recordId,再 records get;values 使用字段 key,选择和关联使用真实 ID。",
14
15
  "遍历列表的所有分页。records list --view-id <view_id> 验证特定视图,records get 获取完整记录。",
@@ -29,8 +30,9 @@ GUIDE = {
29
30
  ],
30
31
  "limits": [
31
32
  "docs/guide 离线可读;业务调用需要权限和已启用环境。",
32
- "页面规则求值不是浏览器渲染;仪表盘输出验证不是图表视觉验证。",
33
+ "页面规则求值不是浏览器渲染;仪表盘输出验证不是图表视觉验证。打印模板 get 返回源码,管理 CLI 不能生成打印内容;写错的 {{ }} 会保存成功并打印成空白。",
33
34
  "组织候选只读;不能通过管理 CLI 替员工做审批决定。",
35
+ "公开表单的读取、提交和附件上传不是管理命令。logo 与 cover 只能引用已有 fim_ 图片,管理 CLI 不能上传图片。",
34
36
  "删除对象/字段/页面是逻辑删除;没有物理 purge,审计与回收站会保留。",
35
37
  ],
36
38
  }
@@ -522,6 +522,40 @@ def handle_dashboards(args: argparse.Namespace) -> int:
522
522
  )
523
523
 
524
524
 
525
+ def handle_forms(args: argparse.Namespace) -> int:
526
+ action = args.forms_action
527
+ if action == "list":
528
+ return run_request(args, "forms.list")
529
+ if action == "create":
530
+ body = _payload_from_args(
531
+ args,
532
+ {
533
+ "label": args.label,
534
+ "key": args.key,
535
+ "objectId": args.object_id,
536
+ "description": args.description,
537
+ },
538
+ ("label", "key", "objectId"),
539
+ )
540
+ return run_request(args, "forms.create", body=body)
541
+ path_params = {"form_id": args.form_id}
542
+ if action == "get":
543
+ return run_request(args, "forms.get", path_params=path_params)
544
+ if action == "update":
545
+ body = load_json_payload(args.json, args.json_file)
546
+ require_fields(body, "label", "appearance", "settings")
547
+ return run_request(args, "forms.update", path_params=path_params, body=body)
548
+ if action == "copy":
549
+ return run_request(args, "forms.copy", path_params=path_params)
550
+ if action == "share":
551
+ return run_request(args, "forms.share.get", path_params=path_params)
552
+ if action == "set-accepting":
553
+ return run_request(args, "forms.share.set-accepting", path_params=path_params, body={"accepting": args.on})
554
+ if action == "reset-link":
555
+ return run_request(args, "forms.share.reset", path_params=path_params)
556
+ return run_request(args, "forms.delete", path_params=path_params)
557
+
558
+
525
559
  def handle_groups(args: argparse.Namespace) -> int:
526
560
  action = args.groups_action
527
561
  path = {"app_id": args.app_id}
@@ -1136,6 +1170,37 @@ def build_parser() -> argparse.ArgumentParser:
1136
1170
  add_yes(dashboard_delete)
1137
1171
  dashboards.set_defaults(handler=handle_dashboards)
1138
1172
 
1173
+ forms = groups.add_parser("forms", help="Manage custom forms (public collection links).")
1174
+ form_actions = forms.add_subparsers(dest="forms_action", required=True)
1175
+ form_actions.add_parser("list")
1176
+ form_get = form_actions.add_parser("get")
1177
+ form_get.add_argument("form_id")
1178
+ form_create = form_actions.add_parser("create")
1179
+ form_create.add_argument("--label")
1180
+ form_create.add_argument("--key")
1181
+ form_create.add_argument("--object-id")
1182
+ form_create.add_argument("--description")
1183
+ add_json_args(form_create, endpoint_key="forms.create")
1184
+ form_update = form_actions.add_parser("update")
1185
+ form_update.add_argument("form_id")
1186
+ add_json_args(form_update, endpoint_key="forms.update")
1187
+ form_copy = form_actions.add_parser("copy")
1188
+ form_copy.add_argument("form_id")
1189
+ form_delete = form_actions.add_parser("delete")
1190
+ form_delete.add_argument("form_id")
1191
+ add_yes(form_delete)
1192
+ form_share = form_actions.add_parser("share", help="Show the public link and collection state.")
1193
+ form_share.add_argument("form_id")
1194
+ form_accepting = form_actions.add_parser("set-accepting", help="Turn public collection on or off (immediate).")
1195
+ form_accepting.add_argument("form_id")
1196
+ accepting_group = form_accepting.add_mutually_exclusive_group(required=True)
1197
+ accepting_group.add_argument("--on", dest="on", action="store_true")
1198
+ accepting_group.add_argument("--off", dest="on", action="store_false")
1199
+ form_reset = form_actions.add_parser("reset-link", help="Reset the public link; the old link stops working.")
1200
+ form_reset.add_argument("form_id")
1201
+ add_yes(form_reset)
1202
+ forms.set_defaults(handler=handle_forms)
1203
+
1139
1204
  app_groups = groups.add_parser("groups", help="Manage app object groups.")
1140
1205
  group_actions = app_groups.add_subparsers(dest="groups_action", required=True)
1141
1206
  group_create = group_actions.add_parser("create")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flowbase-cli
3
- Version: 0.1.12
3
+ Version: 0.1.14
4
4
  Summary: Command line client for the FlowBase OpenAPI
5
5
  Author: DeepFlowAI
6
6
  License: Apache-2.0
@@ -125,6 +125,7 @@ flowbase docs catalogs permissions # function permission points + data sc
125
125
  flowbase docs catalogs dashboard-filters # filter types + date-range presets
126
126
  flowbase docs catalogs page-components # Page component shapes, placement, fields, and enums
127
127
  flowbase docs catalogs page-rules # Page condition operators and action shapes
128
+ flowbase docs catalogs forms # custom form fields, rules, appearance, and settings
128
129
  flowbase docs catalogs id-prefixes # resource ID prefix meanings
129
130
  flowbase docs catalogs icons # icon guidance + examples
130
131
  ```
@@ -23,10 +23,25 @@ class AutonomyTest(unittest.TestCase):
23
23
  self.assertIn('低代码平台', guide['product'])
24
24
  self.assertTrue(any('objects get' in item for item in guide['workflow']))
25
25
  self.assertEqual(code, 0)
26
+ self.assertTrue(any('print-templates' in item for item in guide['workflow']))
26
27
  code, out, _ = self.invoke(['docs', 'catalogs'])
27
- self.assertTrue({'page-buttons', 'workflow-branches', 'approval-rules', 'dashboard-runtime'}.issubset(json.loads(out)))
28
+ self.assertTrue({'page-buttons', 'workflow-branches', 'approval-rules', 'dashboard-runtime', 'print-templates'}.issubset(json.loads(out)))
28
29
  request.assert_not_called()
29
30
 
31
+ def test_print_template_catalog_separates_field_and_page_placeholders(self):
32
+ code, out, err = self.invoke(['docs', 'catalogs', 'print-templates'])
33
+ self.assertEqual(code, 0, err)
34
+ catalog = json.loads(out)
35
+ placeholders = catalog['placeholders']
36
+ self.assertIn('{{ record.<fieldKey> }}', placeholders['record'])
37
+ self.assertIn('objects get', placeholders['keys'])
38
+ self.assertIn('{page}', placeholders['pageTokens'])
39
+ self.assertIn('{{ page }}', placeholders['pageTokens'])
40
+ self.assertIn('header and footer', placeholders['pageTokens'])
41
+ self.assertIn('no inherit_filters', catalog['code']['query'])
42
+ self.assertIn('baseUpdatedAt', catalog['write']['update'])
43
+ self.assertIn('cannot render', catalog['verify'])
44
+
30
45
  def test_write_disconnect_is_not_retried_or_reported_as_definite_failure(self):
31
46
  for error in (RemoteDisconnected('connection closed'), TimeoutError('timed out'), URLError('fbk_secret rejected')):
32
47
  with self.subTest(error=error), patch('flowbase_cli.main.urlopen', side_effect=error) as request:
@@ -334,6 +334,23 @@ class FlowBaseCliTest(unittest.TestCase):
334
334
  prefixes = json.loads(stdout.getvalue())
335
335
  self.assertEqual(prefixes["reg_"], "page region")
336
336
  self.assertEqual(prefixes["cmp_"], "page component")
337
+ self.assertEqual(prefixes["ptp_"], "print template")
338
+ self.assertEqual(prefixes["frm_"], "custom form")
339
+ self.assertEqual(prefixes["fim_"], "form image")
340
+
341
+ stdout = io.StringIO()
342
+ with patch("sys.stdout", stdout):
343
+ code = main(["docs", "catalogs", "forms"])
344
+ self.assertEqual(code, 0)
345
+ forms = json.loads(stdout.getvalue())
346
+ self.assertEqual(
347
+ forms["field"]["requiredKeys"],
348
+ ["fieldId", "label", "description", "state", "prefill"],
349
+ )
350
+ self.assertEqual(forms["appearance"]["background"], ["gray", "white", "tint"])
351
+ self.assertEqual(forms["settings"]["afterSubmit"], ["message", "redirect"])
352
+ self.assertIn("setFieldState", forms["rule"]["actions"])
353
+ self.assertIn("forms", names)
337
354
 
338
355
  def test_docs_list_marks_body_and_confirmation(self):
339
356
  stdout = io.StringIO()
File without changes