mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-11 13:28:57 +00:00
feat(cli): add --schema flag to pad collection create (TASK-1334) (#482)
* feat(cli): add --schema flag to pad collection create (TASK-1334) The existing --fields DSL (key:type[:options]) had no syntax for terminal_options, default, required, computed, suffix, or relation collection — every CLI-created collection lost those FieldDef properties even though the model already supports them. Symptom from BUG-1284: dashboard "active" counts treat published/archived items as in-progress because the persisted schema has no terminal_options. Adds a new --schema flag that accepts the full CollectionSchema JSON, which captures every current and future FieldDef property automatically. Three input modes: --schema '<json>' inline literal (agent-natural; CLI is agent-first) --schema @./path.json file path --schema - stdin --fields and --schema are mutually exclusive; --fields keeps working unchanged for backward compat (no deprecation). Refactors the inline parser into three testable helpers in main.go: collectionSchemaJSONFromFlags (orchestrator), readSchemaInputBytes (input resolver), and parseFieldsDSL (legacy DSL parser preserving the "first status select gets required+default" heuristic). Tests: 9 table-style cases in collection_create_schema_test.go covering all three input modes, the mutually-exclusive guard, malformed JSON, missing file, fallthrough-to-DSL, both-empty, and a regression test that verifies terminal_options + computed + suffix + relation.collection all round-trip through --schema. Parent: PLAN-1333. * fix(cli): backfill missing labels in --schema fields per Codex review (round 2) Codex flagged that the --schema example omitted "label", which the parser preserved as label:"" — agents constructing JSON could create collections that render blank field headers in the web UI. Fix: after unmarshaling --schema input, backfill any FieldDef with an empty Label using the same Title-Case-of-key heuristic the legacy --fields DSL applies (e.g. "due_date" → "Due Date"). Explicit labels are preserved. Also updated the help-text example to include "label" on the status field so the canonical shape is visible, plus a tip line documenting the auto-fill behavior so users know it's safe to omit labels. Test: TestCollectionSchemaJSONFromFlags_BackfillsMissingLabels covers auto-fill, multi-word key normalization, and the explicit-label-not- clobbered case. Parent: PLAN-1333 / TASK-1334.
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/PerpetualSoftware/pad/internal/models"
|
||||
)
|
||||
|
||||
// TestCollectionSchemaJSONFromFlags_Inline verifies the inline JSON path:
|
||||
// schemaInput is the literal JSON, gets unmarshaled then re-marshaled
|
||||
// through the CollectionSchema shape (which normalizes / validates it).
|
||||
func TestCollectionSchemaJSONFromFlags_Inline(t *testing.T) {
|
||||
in := `{"fields":[{"key":"status","label":"Status","type":"select","options":["new","done"],"terminal_options":["done"],"default":"new","required":true}]}`
|
||||
|
||||
out, err := collectionSchemaJSONFromFlags(in, "", strings.NewReader(""))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
var got models.CollectionSchema
|
||||
if err := json.Unmarshal([]byte(out), &got); err != nil {
|
||||
t.Fatalf("re-unmarshal: %v", err)
|
||||
}
|
||||
if len(got.Fields) != 1 {
|
||||
t.Fatalf("expected 1 field, got %d", len(got.Fields))
|
||||
}
|
||||
f := got.Fields[0]
|
||||
if f.Key != "status" || f.Type != "select" {
|
||||
t.Fatalf("unexpected key/type: %+v", f)
|
||||
}
|
||||
if len(f.TerminalOptions) != 1 || f.TerminalOptions[0] != "done" {
|
||||
t.Fatalf("expected terminal_options=[done], got %v", f.TerminalOptions)
|
||||
}
|
||||
if !f.Required || f.Default != "new" {
|
||||
t.Fatalf("expected required=true default=new, got required=%v default=%v", f.Required, f.Default)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCollectionSchemaJSONFromFlags_File verifies the @<path> file path resolver.
|
||||
func TestCollectionSchemaJSONFromFlags_File(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "schema.json")
|
||||
payload := `{"fields":[{"key":"status","type":"select","options":["a","b"],"terminal_options":["b"]}]}`
|
||||
if err := os.WriteFile(path, []byte(payload), 0o600); err != nil {
|
||||
t.Fatalf("write tmpfile: %v", err)
|
||||
}
|
||||
|
||||
out, err := collectionSchemaJSONFromFlags("@"+path, "", strings.NewReader(""))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
var got models.CollectionSchema
|
||||
if err := json.Unmarshal([]byte(out), &got); err != nil {
|
||||
t.Fatalf("re-unmarshal: %v", err)
|
||||
}
|
||||
if len(got.Fields) != 1 || got.Fields[0].TerminalOptions[0] != "b" {
|
||||
t.Fatalf("expected terminal_options=[b], got %+v", got.Fields)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCollectionSchemaJSONFromFlags_Stdin verifies the "-" stdin resolver.
|
||||
func TestCollectionSchemaJSONFromFlags_Stdin(t *testing.T) {
|
||||
stdin := strings.NewReader(`{"fields":[{"key":"priority","type":"select","options":["lo","hi"]}]}`)
|
||||
out, err := collectionSchemaJSONFromFlags("-", "", stdin)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
var got models.CollectionSchema
|
||||
if err := json.Unmarshal([]byte(out), &got); err != nil {
|
||||
t.Fatalf("re-unmarshal: %v", err)
|
||||
}
|
||||
if len(got.Fields) != 1 || got.Fields[0].Key != "priority" {
|
||||
t.Fatalf("unexpected schema: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCollectionSchemaJSONFromFlags_BothFlagsError checks the mutually-exclusive guard.
|
||||
func TestCollectionSchemaJSONFromFlags_BothFlagsError(t *testing.T) {
|
||||
_, err := collectionSchemaJSONFromFlags(`{"fields":[]}`, "status:select:open,done", strings.NewReader(""))
|
||||
if err == nil {
|
||||
t.Fatal("expected error when both --fields and --schema set, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "mutually exclusive") {
|
||||
t.Fatalf("expected mutually-exclusive error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCollectionSchemaJSONFromFlags_MalformedJSON ensures the unmarshal error
|
||||
// is wrapped with the "invalid --schema JSON" prefix so users can see which
|
||||
// flag caused the problem.
|
||||
func TestCollectionSchemaJSONFromFlags_MalformedJSON(t *testing.T) {
|
||||
_, err := collectionSchemaJSONFromFlags(`{this is not json`, "", strings.NewReader(""))
|
||||
if err == nil {
|
||||
t.Fatal("expected error for malformed JSON, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "invalid --schema JSON") {
|
||||
t.Fatalf("expected 'invalid --schema JSON' in error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCollectionSchemaJSONFromFlags_MissingFile ensures a clear error when
|
||||
// the @path target doesn't exist.
|
||||
func TestCollectionSchemaJSONFromFlags_MissingFile(t *testing.T) {
|
||||
_, err := collectionSchemaJSONFromFlags("@/nonexistent/path/schema.json", "", strings.NewReader(""))
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing file, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "read --schema file") {
|
||||
t.Fatalf("expected 'read --schema file' in error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCollectionSchemaJSONFromFlags_EmptyFallsThroughToFields verifies that an
|
||||
// empty --schema falls through to the --fields DSL parser (backward compat).
|
||||
func TestCollectionSchemaJSONFromFlags_EmptyFallsThroughToFields(t *testing.T) {
|
||||
out, err := collectionSchemaJSONFromFlags("", "status:select:open,done", strings.NewReader(""))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
var got models.CollectionSchema
|
||||
if err := json.Unmarshal([]byte(out), &got); err != nil {
|
||||
t.Fatalf("re-unmarshal: %v", err)
|
||||
}
|
||||
if len(got.Fields) != 1 {
|
||||
t.Fatalf("expected 1 field, got %d", len(got.Fields))
|
||||
}
|
||||
f := got.Fields[0]
|
||||
if f.Key != "status" || f.Type != "select" {
|
||||
t.Fatalf("unexpected key/type: %+v", f)
|
||||
}
|
||||
// DSL preserves the legacy "first status select gets required+default" heuristic.
|
||||
if !f.Required || f.Default != "open" {
|
||||
t.Fatalf("expected legacy DSL heuristic (required=true, default=open), got required=%v default=%v", f.Required, f.Default)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCollectionSchemaJSONFromFlags_BothEmptyReturnsEmptySchema verifies the
|
||||
// no-flags case yields an empty schema (and no error).
|
||||
func TestCollectionSchemaJSONFromFlags_BothEmptyReturnsEmptySchema(t *testing.T) {
|
||||
out, err := collectionSchemaJSONFromFlags("", "", strings.NewReader(""))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if out != `{"fields":null}` && out != `{"fields":[]}` && out != `{}` {
|
||||
// We accept any JSON shape that round-trips to a Fields-less schema —
|
||||
// the wire format here is opaque to callers.
|
||||
var got models.CollectionSchema
|
||||
if jsonErr := json.Unmarshal([]byte(out), &got); jsonErr != nil {
|
||||
t.Fatalf("unparseable empty-schema output %q: %v", out, jsonErr)
|
||||
}
|
||||
if len(got.Fields) != 0 {
|
||||
t.Fatalf("expected empty Fields, got %d", len(got.Fields))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestCollectionSchemaJSONFromFlags_BackfillsMissingLabels verifies that a
|
||||
// schema field with no `label` gets one auto-filled from its `key` using
|
||||
// Title-Case, matching the legacy --fields DSL behavior. Without this,
|
||||
// agents constructing JSON that omits `label` would create collections
|
||||
// that render blank field headers in the web UI.
|
||||
func TestCollectionSchemaJSONFromFlags_BackfillsMissingLabels(t *testing.T) {
|
||||
in := `{"fields":[
|
||||
{"key":"status","type":"select","options":["open","done"]},
|
||||
{"key":"due_date","type":"date"},
|
||||
{"key":"already_labeled","label":"Custom Label","type":"text"}
|
||||
]}`
|
||||
out, err := collectionSchemaJSONFromFlags(in, "", strings.NewReader(""))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
var got models.CollectionSchema
|
||||
if err := json.Unmarshal([]byte(out), &got); err != nil {
|
||||
t.Fatalf("re-unmarshal: %v", err)
|
||||
}
|
||||
if len(got.Fields) != 3 {
|
||||
t.Fatalf("expected 3 fields, got %d", len(got.Fields))
|
||||
}
|
||||
if got.Fields[0].Label != "Status" {
|
||||
t.Errorf("expected label 'Status' for key 'status', got %q", got.Fields[0].Label)
|
||||
}
|
||||
if got.Fields[1].Label != "Due Date" {
|
||||
t.Errorf("expected label 'Due Date' for key 'due_date', got %q", got.Fields[1].Label)
|
||||
}
|
||||
if got.Fields[2].Label != "Custom Label" {
|
||||
t.Errorf("explicit label clobbered: got %q want 'Custom Label'", got.Fields[2].Label)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCollectionSchemaJSONFromFlags_PreservesAllFieldDefProperties is the
|
||||
// regression test for BUG-1284: confirms that every FieldDef property — not
|
||||
// just the DSL-expressible subset — round-trips through --schema unchanged.
|
||||
func TestCollectionSchemaJSONFromFlags_PreservesAllFieldDefProperties(t *testing.T) {
|
||||
in := `{
|
||||
"fields": [
|
||||
{
|
||||
"key": "status",
|
||||
"label": "Status",
|
||||
"type": "select",
|
||||
"options": ["idea","drafting","review","approved","scheduled","published","archived"],
|
||||
"terminal_options": ["published","archived"],
|
||||
"default": "idea",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"key": "progress",
|
||||
"label": "Progress",
|
||||
"type": "number",
|
||||
"computed": true,
|
||||
"suffix": "%"
|
||||
},
|
||||
{
|
||||
"key": "parent_plan",
|
||||
"label": "Parent Plan",
|
||||
"type": "relation",
|
||||
"collection": "plans"
|
||||
}
|
||||
]
|
||||
}`
|
||||
out, err := collectionSchemaJSONFromFlags(in, "", strings.NewReader(""))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
var got models.CollectionSchema
|
||||
if err := json.Unmarshal([]byte(out), &got); err != nil {
|
||||
t.Fatalf("re-unmarshal: %v", err)
|
||||
}
|
||||
if len(got.Fields) != 3 {
|
||||
t.Fatalf("expected 3 fields, got %d", len(got.Fields))
|
||||
}
|
||||
|
||||
status := got.Fields[0]
|
||||
if len(status.TerminalOptions) != 2 || status.TerminalOptions[0] != "published" || status.TerminalOptions[1] != "archived" {
|
||||
t.Errorf("status.terminal_options not preserved: %v", status.TerminalOptions)
|
||||
}
|
||||
if status.Default != "idea" {
|
||||
t.Errorf("status.default not preserved: %v", status.Default)
|
||||
}
|
||||
if !status.Required {
|
||||
t.Errorf("status.required not preserved")
|
||||
}
|
||||
|
||||
progress := got.Fields[1]
|
||||
if !progress.Computed {
|
||||
t.Errorf("progress.computed not preserved")
|
||||
}
|
||||
if progress.Suffix != "%" {
|
||||
t.Errorf("progress.suffix not preserved: %q", progress.Suffix)
|
||||
}
|
||||
|
||||
parent := got.Fields[2]
|
||||
if parent.Collection != "plans" {
|
||||
t.Errorf("relation.collection not preserved: %q", parent.Collection)
|
||||
}
|
||||
}
|
||||
+139
-37
@@ -4820,11 +4820,123 @@ func collectionsCmd() *cobra.Command {
|
||||
}
|
||||
}
|
||||
|
||||
// collectionSchemaJSONFromFlags resolves the --schema and --fields flags into
|
||||
// a marshaled CollectionSchema JSON string.
|
||||
//
|
||||
// Exactly one of schemaInput or fieldsDSL may be non-empty. When both are
|
||||
// empty, returns "{}" — an empty schema with no fields.
|
||||
//
|
||||
// schemaInput input modes:
|
||||
// - "" — fall through to fieldsDSL (or empty schema if that is also empty)
|
||||
// - "-" — read full JSON from stdin
|
||||
// - "@<path>" — read full JSON from the file at <path>
|
||||
// - anything else — treat the value itself as an inline JSON literal
|
||||
func collectionSchemaJSONFromFlags(schemaInput, fieldsDSL string, stdin io.Reader) (string, error) {
|
||||
if schemaInput != "" && fieldsDSL != "" {
|
||||
return "", fmt.Errorf("--fields and --schema are mutually exclusive")
|
||||
}
|
||||
|
||||
if schemaInput != "" {
|
||||
data, err := readSchemaInputBytes(schemaInput, stdin)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var schema models.CollectionSchema
|
||||
if err := json.Unmarshal(data, &schema); err != nil {
|
||||
return "", fmt.Errorf("invalid --schema JSON: %w", err)
|
||||
}
|
||||
// Backfill missing labels from keys using the same Title-Case-of-key
|
||||
// heuristic the legacy --fields DSL applies. Without this, schemas
|
||||
// that omit `label` render blank field headers in the web UI — easy
|
||||
// for an agent constructing JSON to forget.
|
||||
for i := range schema.Fields {
|
||||
if schema.Fields[i].Label == "" && schema.Fields[i].Key != "" {
|
||||
schema.Fields[i].Label = cases.Title(language.English).String(strings.ReplaceAll(schema.Fields[i].Key, "_", " "))
|
||||
}
|
||||
}
|
||||
out, err := json.Marshal(schema)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("re-marshal schema: %w", err)
|
||||
}
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
schema, err := parseFieldsDSL(fieldsDSL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
out, err := json.Marshal(schema)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("marshal schema from --fields: %w", err)
|
||||
}
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
// readSchemaInputBytes resolves the --schema flag value into raw JSON bytes,
|
||||
// honoring the "-" (stdin), "@path" (file), and inline-literal modes.
|
||||
func readSchemaInputBytes(input string, stdin io.Reader) ([]byte, error) {
|
||||
switch {
|
||||
case input == "-":
|
||||
data, err := io.ReadAll(stdin)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read --schema from stdin: %w", err)
|
||||
}
|
||||
return data, nil
|
||||
case strings.HasPrefix(input, "@"):
|
||||
path := input[1:]
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read --schema file %q: %w", path, err)
|
||||
}
|
||||
return data, nil
|
||||
default:
|
||||
return []byte(input), nil
|
||||
}
|
||||
}
|
||||
|
||||
// parseFieldsDSL parses the legacy --fields DSL (key:type[:options];...) into
|
||||
// a CollectionSchema. Empty input returns an empty schema with no error.
|
||||
func parseFieldsDSL(fieldsDSL string) (models.CollectionSchema, error) {
|
||||
schema := models.CollectionSchema{}
|
||||
if fieldsDSL == "" {
|
||||
return schema, nil
|
||||
}
|
||||
for _, f := range strings.Split(fieldsDSL, ";") {
|
||||
f = strings.TrimSpace(f)
|
||||
if f == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(f, ":", 3)
|
||||
if len(parts) < 2 {
|
||||
return schema, fmt.Errorf("invalid field definition: %q (expected key:type[:options])", f)
|
||||
}
|
||||
fd := models.FieldDef{
|
||||
Key: parts[0],
|
||||
Label: cases.Title(language.English).String(strings.ReplaceAll(parts[0], "_", " ")),
|
||||
Type: parts[1],
|
||||
}
|
||||
if len(parts) == 3 && parts[2] != "" {
|
||||
fd.Options = strings.Split(parts[2], ",")
|
||||
}
|
||||
// First select field gets required+default — preserved for
|
||||
// backward compat with the pre-existing DSL behavior.
|
||||
if fd.Type == "select" && fd.Key == "status" {
|
||||
fd.Required = true
|
||||
if len(fd.Options) > 0 {
|
||||
fd.Default = fd.Options[0]
|
||||
}
|
||||
}
|
||||
schema.Fields = append(schema.Fields, fd)
|
||||
}
|
||||
return schema, nil
|
||||
}
|
||||
|
||||
func collectionsCreateCmd() *cobra.Command {
|
||||
var (
|
||||
icon string
|
||||
description string
|
||||
fieldsDSL string
|
||||
schemaInput string
|
||||
layout string
|
||||
defaultView string
|
||||
boardGroup string
|
||||
@@ -4835,12 +4947,30 @@ func collectionsCreateCmd() *cobra.Command {
|
||||
Short: "Create a custom collection",
|
||||
Long: `Create a new collection with custom fields.
|
||||
|
||||
Fields DSL format: key:type[:option1,option2,...]
|
||||
Separate multiple fields with newlines or semicolons.
|
||||
Two ways to define the schema:
|
||||
|
||||
--fields Compact DSL for the simple case: key:type[:option1,option2,...]
|
||||
Separate multiple fields with semicolons. Does not support
|
||||
terminal_options, custom defaults, computed fields, suffixes,
|
||||
or relation collections.
|
||||
|
||||
--schema Full CollectionSchema JSON for everything else. Accepts:
|
||||
inline JSON: --schema '{"fields":[...]}'
|
||||
file path: --schema @./schema.json
|
||||
stdin: --schema -
|
||||
|
||||
--fields and --schema are mutually exclusive.
|
||||
|
||||
Examples:
|
||||
pad collection create "Bugs" --fields "status:select:new,triaged,fixing,resolved;severity:select:low,medium,high,critical;component:text"
|
||||
pad collection create "Decisions" --icon "⚖️" --fields "status:select:proposed,accepted,rejected;impact:select:low,medium,high"`,
|
||||
pad collection create "Decisions" --icon "⚖️" --fields "status:select:proposed,accepted,rejected;impact:select:low,medium,high"
|
||||
pad collection create "Marketing" --schema '{"fields":[{"key":"status","label":"Status","type":"select","options":["idea","drafting","review","published","archived"],"terminal_options":["published","archived"],"default":"idea","required":true}]}'
|
||||
pad collection create "Marketing" --schema @./marketing-schema.json
|
||||
cat schema.json | pad collection create "Marketing" --schema -
|
||||
|
||||
Tip: if you omit "label" on a --schema field, the CLI auto-fills it from
|
||||
the key using Title Case (e.g. "due_date" → "Due Date") — matching what
|
||||
the --fields DSL does. Set "label" explicitly when you want a custom display name.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client, _ := getClient()
|
||||
@@ -4848,40 +4978,11 @@ Examples:
|
||||
|
||||
name := args[0]
|
||||
|
||||
// Parse fields DSL into schema JSON
|
||||
schema := models.CollectionSchema{}
|
||||
if fieldsDSL != "" {
|
||||
fields := strings.Split(fieldsDSL, ";")
|
||||
for _, f := range fields {
|
||||
f = strings.TrimSpace(f)
|
||||
if f == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(f, ":", 3)
|
||||
if len(parts) < 2 {
|
||||
return fmt.Errorf("invalid field definition: %q (expected key:type[:options])", f)
|
||||
}
|
||||
fd := models.FieldDef{
|
||||
Key: parts[0],
|
||||
Label: cases.Title(language.English).String(strings.ReplaceAll(parts[0], "_", " ")),
|
||||
Type: parts[1],
|
||||
}
|
||||
if len(parts) == 3 && parts[2] != "" {
|
||||
fd.Options = strings.Split(parts[2], ",")
|
||||
}
|
||||
// First select field gets required+default
|
||||
if fd.Type == "select" && fd.Key == "status" {
|
||||
fd.Required = true
|
||||
if len(fd.Options) > 0 {
|
||||
fd.Default = fd.Options[0]
|
||||
}
|
||||
}
|
||||
schema.Fields = append(schema.Fields, fd)
|
||||
}
|
||||
schemaJSON, err := collectionSchemaJSONFromFlags(schemaInput, fieldsDSL, os.Stdin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
schemaJSON, _ := json.Marshal(schema)
|
||||
|
||||
// Build settings
|
||||
settings := models.CollectionSettings{
|
||||
Layout: layout,
|
||||
@@ -4900,7 +5001,7 @@ Examples:
|
||||
Name: name,
|
||||
Icon: icon,
|
||||
Description: description,
|
||||
Schema: string(schemaJSON),
|
||||
Schema: schemaJSON,
|
||||
Settings: string(settingsJSON),
|
||||
}
|
||||
|
||||
@@ -4924,7 +5025,8 @@ Examples:
|
||||
|
||||
cmd.Flags().StringVar(&icon, "icon", "", "collection emoji icon")
|
||||
cmd.Flags().StringVar(&description, "description", "", "collection description")
|
||||
cmd.Flags().StringVar(&fieldsDSL, "fields", "", "field definitions (key:type[:options]; ...)")
|
||||
cmd.Flags().StringVar(&fieldsDSL, "fields", "", "field definitions DSL (key:type[:options]; ...); use --schema for terminal_options, computed, defaults, etc.")
|
||||
cmd.Flags().StringVar(&schemaInput, "schema", "", "full CollectionSchema JSON: inline, @path, or - for stdin; mutually exclusive with --fields")
|
||||
cmd.Flags().StringVar(&layout, "layout", "fields-primary", "item detail layout: fields-primary, content-primary, balanced")
|
||||
cmd.Flags().StringVar(&defaultView, "default-view", "list", "default view type: list, board, table")
|
||||
cmd.Flags().StringVar(&boardGroup, "board-group-by", "status", "field to group by in board view")
|
||||
|
||||
Reference in New Issue
Block a user