directify-cli 1.3.0 → 1.4.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/README.md +43 -1
- package/package.json +1 -1
- package/src/commands/fields.js +140 -2
- package/src/commands/listings.js +4 -0
- package/src/commands/organizers.js +10 -2
package/README.md
CHANGED
|
@@ -116,10 +116,28 @@ directify tags delete 10
|
|
|
116
116
|
### Custom Fields
|
|
117
117
|
|
|
118
118
|
```bash
|
|
119
|
+
# List / get
|
|
119
120
|
directify fields list # List all custom fields
|
|
120
121
|
directify fields ls --json # Output as JSON
|
|
122
|
+
directify fields get 12
|
|
123
|
+
|
|
124
|
+
# Create (label + type required)
|
|
125
|
+
directify fields create \
|
|
126
|
+
--label "Price Range" \
|
|
127
|
+
--type select \
|
|
128
|
+
--options "$,$$,$$$" \
|
|
129
|
+
--filterable \
|
|
130
|
+
--on-card
|
|
131
|
+
|
|
132
|
+
directify fields create --label "Founded Year" --type number
|
|
133
|
+
|
|
134
|
+
# Update / delete
|
|
135
|
+
directify fields update 12 --label "Pricing Tier" --required true
|
|
136
|
+
directify fields delete 12
|
|
121
137
|
```
|
|
122
138
|
|
|
139
|
+
Field types: `text`, `number`, `date`, `file_upload`, `url`, `email`, `rich_editor`, `markdown`, `textarea`, `checkbox`, `rating`, `select`, `list`, `multi_select`, `button`, `javascript`, `html`. The `--name` (machine key) is auto-derived from `--label` if omitted. Set a field's value on a listing via `listings create/update --field "name=value"`.
|
|
140
|
+
|
|
123
141
|
### Listings
|
|
124
142
|
|
|
125
143
|
```bash
|
|
@@ -141,13 +159,15 @@ directify listings create \
|
|
|
141
159
|
--email "info@bellatrattoria.com" \
|
|
142
160
|
--categories 1,5,12 \
|
|
143
161
|
--tags 3,7 \
|
|
162
|
+
--organizers 5,8 \
|
|
144
163
|
--featured \
|
|
145
164
|
--field "price_range=2" \
|
|
146
165
|
--field "cuisine_type=Italian, Pasta"
|
|
147
166
|
|
|
148
|
-
# Update
|
|
167
|
+
# Update (--organizers replaces the listing's current organizer links)
|
|
149
168
|
directify listings update 456 \
|
|
150
169
|
--name "Bella Trattoria NYC" \
|
|
170
|
+
--organizers 5 \
|
|
151
171
|
--featured true \
|
|
152
172
|
--field "hours_of_operation=Mon | 11:00 - 22:00"
|
|
153
173
|
|
|
@@ -174,6 +194,7 @@ Create a JSON file with an array of listings:
|
|
|
174
194
|
"description": "Great food",
|
|
175
195
|
"categories": [1, 2],
|
|
176
196
|
"tags": [3],
|
|
197
|
+
"organizers": [5],
|
|
177
198
|
"price_range": "2",
|
|
178
199
|
"cuisine_type": "Italian"
|
|
179
200
|
},
|
|
@@ -194,6 +215,27 @@ Then run:
|
|
|
194
215
|
directify listings bulk-create --file ./listings.json
|
|
195
216
|
```
|
|
196
217
|
|
|
218
|
+
> **Linking organizers:** `--organizers` (and the `organizers` array in the bulk JSON) takes a comma-separated list of organizer IDs to associate the listing with. Only organizers in the same directory are linked; out-of-directory IDs are ignored. On `update`, the list replaces the listing's current organizers. Use `directify organizers list` to find the IDs.
|
|
219
|
+
|
|
220
|
+
### Organizers
|
|
221
|
+
|
|
222
|
+
Organizers (agencies, studios, event hosts, etc.) can be linked to multiple listings.
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
# List / get
|
|
226
|
+
directify organizers list
|
|
227
|
+
directify organizers get 5
|
|
228
|
+
|
|
229
|
+
# Create
|
|
230
|
+
directify organizers create --name "Acme Events" --website "https://acme-events.com"
|
|
231
|
+
|
|
232
|
+
# Update / delete
|
|
233
|
+
directify organizers update 5 --name "Acme Events Co."
|
|
234
|
+
directify organizers delete 5
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
To link an organizer to a listing, pass its ID via `--organizers` on `listings create`/`update` (see above).
|
|
238
|
+
|
|
197
239
|
### Articles
|
|
198
240
|
|
|
199
241
|
```bash
|
package/package.json
CHANGED
package/src/commands/fields.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { api, resolveDirectory } from '../utils/api.js';
|
|
3
|
-
import { printTable, printJson, printError } from '../utils/output.js';
|
|
3
|
+
import { printTable, printJson, printSuccess, printError } from '../utils/output.js';
|
|
4
4
|
import ora from 'ora';
|
|
5
5
|
|
|
6
|
-
const fields = new Command('fields').description('
|
|
6
|
+
const fields = new Command('fields').description('Manage custom fields');
|
|
7
|
+
|
|
8
|
+
const FIELD_TYPES = 'text, number, date, file_upload, url, email, rich_editor, markdown, textarea, checkbox, rating, select, list, multi_select, button, javascript, html';
|
|
7
9
|
|
|
8
10
|
fields
|
|
9
11
|
.command('list')
|
|
@@ -38,4 +40,140 @@ fields
|
|
|
38
40
|
}
|
|
39
41
|
});
|
|
40
42
|
|
|
43
|
+
fields
|
|
44
|
+
.command('get <id>')
|
|
45
|
+
.description('Get a specific custom field')
|
|
46
|
+
.option('-d, --directory <id>', 'Directory ID')
|
|
47
|
+
.action(async (id, opts) => {
|
|
48
|
+
try {
|
|
49
|
+
const dir = resolveDirectory(opts);
|
|
50
|
+
const data = await api.get(`/directories/${dir}/custom-fields/${id}`);
|
|
51
|
+
printJson(data.data || data);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
printError(err.message);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
fields
|
|
59
|
+
.command('create')
|
|
60
|
+
.description('Create a new custom field definition')
|
|
61
|
+
.requiredOption('--label <label>', 'Human-readable field label')
|
|
62
|
+
.requiredOption('--type <type>', `Field type (${FIELD_TYPES})`)
|
|
63
|
+
.option('--name <name>', 'Machine name/key (auto-derived from label if omitted)')
|
|
64
|
+
.option('--fieldable-type <type>', 'What the field attaches to: listing, organizer, article (default: listing)')
|
|
65
|
+
.option('--placeholder <text>', 'Input placeholder text')
|
|
66
|
+
.option('--description <text>', 'Help text shown under the field')
|
|
67
|
+
.option('--default <value>', 'Default value')
|
|
68
|
+
.option('--prefix <value>', 'Prefix shown before the value (e.g. $)')
|
|
69
|
+
.option('--suffix <value>', 'Suffix shown after the value (e.g. /mo)')
|
|
70
|
+
.option('--options <values>', 'Options for select/multi_select/list types (comma-separated)')
|
|
71
|
+
.option('--required', 'Mark the field as required')
|
|
72
|
+
.option('--filterable', 'Allow filtering listings by this field')
|
|
73
|
+
.option('--on-card', 'Show the value on listing cards')
|
|
74
|
+
.option('--public', 'Show the field on the public submission form')
|
|
75
|
+
.option('--hidden', 'Hide the field from the listing page')
|
|
76
|
+
.option('--order <n>', 'Sort order', parseInt)
|
|
77
|
+
.option('-d, --directory <id>', 'Directory ID')
|
|
78
|
+
.action(async (opts) => {
|
|
79
|
+
const spinner = ora('Creating custom field...').start();
|
|
80
|
+
try {
|
|
81
|
+
const dir = resolveDirectory(opts);
|
|
82
|
+
const body = { label: opts.label, type: opts.type };
|
|
83
|
+
if (opts.name) body.name = opts.name;
|
|
84
|
+
if (opts.fieldableType) body.fieldable_type = opts.fieldableType;
|
|
85
|
+
if (opts.placeholder) body.placeholder = opts.placeholder;
|
|
86
|
+
if (opts.description) body.description = opts.description;
|
|
87
|
+
if (opts.default) body.default_value = opts.default;
|
|
88
|
+
if (opts.prefix) body.value_prefix = opts.prefix;
|
|
89
|
+
if (opts.suffix) body.value_suffix = opts.suffix;
|
|
90
|
+
if (opts.options) body.options = opts.options.split(',').map((o) => o.trim());
|
|
91
|
+
if (opts.required) body.is_required = true;
|
|
92
|
+
if (opts.filterable) body.filterable = true;
|
|
93
|
+
if (opts.onCard) body.show_on_card = true;
|
|
94
|
+
if (opts.public) body.show_on_public_submission = true;
|
|
95
|
+
if (opts.hidden) body.is_visible = false;
|
|
96
|
+
if (opts.order !== undefined) body.order = opts.order;
|
|
97
|
+
|
|
98
|
+
const data = await api.post(`/directories/${dir}/custom-fields`, body);
|
|
99
|
+
spinner.stop();
|
|
100
|
+
const result = data.data || data;
|
|
101
|
+
printSuccess(`Custom field created: ${result.label} (${result.name}, ID: ${result.id})`);
|
|
102
|
+
} catch (err) {
|
|
103
|
+
spinner.stop();
|
|
104
|
+
printError(err.message);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
fields
|
|
110
|
+
.command('update <id>')
|
|
111
|
+
.description('Update a custom field definition')
|
|
112
|
+
.option('--label <label>', 'Human-readable field label')
|
|
113
|
+
.option('--type <type>', `Field type (${FIELD_TYPES})`)
|
|
114
|
+
.option('--name <name>', 'Machine name/key')
|
|
115
|
+
.option('--fieldable-type <type>', 'What the field attaches to: listing, organizer, article')
|
|
116
|
+
.option('--placeholder <text>', 'Input placeholder text')
|
|
117
|
+
.option('--description <text>', 'Help text shown under the field')
|
|
118
|
+
.option('--default <value>', 'Default value')
|
|
119
|
+
.option('--prefix <value>', 'Prefix shown before the value')
|
|
120
|
+
.option('--suffix <value>', 'Suffix shown after the value')
|
|
121
|
+
.option('--options <values>', 'Options for select/multi_select/list types (comma-separated)')
|
|
122
|
+
.option('--required <bool>', 'Required status (true/false)')
|
|
123
|
+
.option('--filterable <bool>', 'Filterable status (true/false)')
|
|
124
|
+
.option('--on-card <bool>', 'Show on cards (true/false)')
|
|
125
|
+
.option('--public <bool>', 'Show on public submission form (true/false)')
|
|
126
|
+
.option('--visible <bool>', 'Visible on the listing page (true/false)')
|
|
127
|
+
.option('--order <n>', 'Sort order', parseInt)
|
|
128
|
+
.option('-d, --directory <id>', 'Directory ID')
|
|
129
|
+
.action(async (id, opts) => {
|
|
130
|
+
const spinner = ora('Updating custom field...').start();
|
|
131
|
+
try {
|
|
132
|
+
const dir = resolveDirectory(opts);
|
|
133
|
+
const body = {};
|
|
134
|
+
if (opts.label) body.label = opts.label;
|
|
135
|
+
if (opts.type) body.type = opts.type;
|
|
136
|
+
if (opts.name) body.name = opts.name;
|
|
137
|
+
if (opts.fieldableType) body.fieldable_type = opts.fieldableType;
|
|
138
|
+
if (opts.placeholder) body.placeholder = opts.placeholder;
|
|
139
|
+
if (opts.description) body.description = opts.description;
|
|
140
|
+
if (opts.default) body.default_value = opts.default;
|
|
141
|
+
if (opts.prefix) body.value_prefix = opts.prefix;
|
|
142
|
+
if (opts.suffix) body.value_suffix = opts.suffix;
|
|
143
|
+
if (opts.options) body.options = opts.options.split(',').map((o) => o.trim());
|
|
144
|
+
if (opts.required !== undefined) body.is_required = opts.required === 'true';
|
|
145
|
+
if (opts.filterable !== undefined) body.filterable = opts.filterable === 'true';
|
|
146
|
+
if (opts.onCard !== undefined) body.show_on_card = opts.onCard === 'true';
|
|
147
|
+
if (opts.public !== undefined) body.show_on_public_submission = opts.public === 'true';
|
|
148
|
+
if (opts.visible !== undefined) body.is_visible = opts.visible === 'true';
|
|
149
|
+
if (opts.order !== undefined) body.order = opts.order;
|
|
150
|
+
|
|
151
|
+
const data = await api.put(`/directories/${dir}/custom-fields/${id}`, body);
|
|
152
|
+
spinner.stop();
|
|
153
|
+
printSuccess(`Custom field updated: ${data.data?.label || data.label}`);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
spinner.stop();
|
|
156
|
+
printError(err.message);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
fields
|
|
162
|
+
.command('delete <id>')
|
|
163
|
+
.description('Delete a custom field definition (also removes its values from all listings)')
|
|
164
|
+
.option('-d, --directory <id>', 'Directory ID')
|
|
165
|
+
.action(async (id, opts) => {
|
|
166
|
+
const spinner = ora('Deleting custom field...').start();
|
|
167
|
+
try {
|
|
168
|
+
const dir = resolveDirectory(opts);
|
|
169
|
+
await api.delete(`/directories/${dir}/custom-fields/${id}`);
|
|
170
|
+
spinner.stop();
|
|
171
|
+
printSuccess(`Custom field ${id} deleted.`);
|
|
172
|
+
} catch (err) {
|
|
173
|
+
spinner.stop();
|
|
174
|
+
printError(err.message);
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
41
179
|
export default fields;
|
package/src/commands/listings.js
CHANGED
|
@@ -73,6 +73,7 @@ listings
|
|
|
73
73
|
.option('--lng <n>', 'Longitude', parseFloat)
|
|
74
74
|
.option('--categories <ids>', 'Category IDs (comma-separated)')
|
|
75
75
|
.option('--tags <ids>', 'Tag IDs (comma-separated)')
|
|
76
|
+
.option('--organizers <ids>', 'Organizer IDs to link (comma-separated)')
|
|
76
77
|
.option('--featured', 'Mark as featured')
|
|
77
78
|
.option('--inactive', 'Create as inactive')
|
|
78
79
|
.option('--field <key=value...>', 'Custom field values (repeatable)', collect, [])
|
|
@@ -95,6 +96,7 @@ listings
|
|
|
95
96
|
if (opts.lng) body.longitude = opts.lng;
|
|
96
97
|
if (opts.categories) body.categories = opts.categories.split(',').map(Number);
|
|
97
98
|
if (opts.tags) body.tags = opts.tags.split(',').map(Number);
|
|
99
|
+
if (opts.organizers) body.organizers = opts.organizers.split(',').map(Number);
|
|
98
100
|
if (opts.featured) body.is_featured = true;
|
|
99
101
|
if (opts.inactive) body.is_active = false;
|
|
100
102
|
|
|
@@ -132,6 +134,7 @@ listings
|
|
|
132
134
|
.option('--lng <n>', 'Longitude', parseFloat)
|
|
133
135
|
.option('--categories <ids>', 'Category IDs (comma-separated)')
|
|
134
136
|
.option('--tags <ids>', 'Tag IDs (comma-separated)')
|
|
137
|
+
.option('--organizers <ids>', 'Organizer IDs to link, replaces current set (comma-separated; out-of-directory IDs are ignored)')
|
|
135
138
|
.option('--featured <bool>', 'Featured status (true/false)')
|
|
136
139
|
.option('--active <bool>', 'Active status (true/false)')
|
|
137
140
|
.option('--field <key=value...>', 'Custom field values (repeatable)', collect, [])
|
|
@@ -155,6 +158,7 @@ listings
|
|
|
155
158
|
if (opts.lng) body.longitude = opts.lng;
|
|
156
159
|
if (opts.categories) body.categories = opts.categories.split(',').map(Number);
|
|
157
160
|
if (opts.tags) body.tags = opts.tags.split(',').map(Number);
|
|
161
|
+
if (opts.organizers) body.organizers = opts.organizers.split(',').map(Number);
|
|
158
162
|
if (opts.featured !== undefined) body.is_featured = opts.featured === 'true';
|
|
159
163
|
if (opts.active !== undefined) body.is_active = opts.active === 'true';
|
|
160
164
|
|
|
@@ -56,9 +56,11 @@ organizers
|
|
|
56
56
|
.description('Create a new organizer')
|
|
57
57
|
.requiredOption('--name <name>', 'Organizer name')
|
|
58
58
|
.option('--slug <slug>', 'URL slug (auto-generated from name if not provided)')
|
|
59
|
-
.option('--description <text>', '
|
|
59
|
+
.option('--description <text>', 'Short description')
|
|
60
|
+
.option('--content <text>', 'Long-form content (markdown supported)')
|
|
60
61
|
.option('--email <email>', 'Contact email')
|
|
61
62
|
.option('--phone <phone>', 'Contact phone')
|
|
63
|
+
.option('--address <address>', 'Physical address')
|
|
62
64
|
.option('--website <url>', 'Website URL')
|
|
63
65
|
.option('--user-id <id>', 'Assign to a user (submitter) by ID')
|
|
64
66
|
.option('--inactive', 'Create as inactive')
|
|
@@ -72,8 +74,10 @@ organizers
|
|
|
72
74
|
name: opts.name,
|
|
73
75
|
...(opts.slug && { slug: opts.slug }),
|
|
74
76
|
...(opts.description && { description: opts.description }),
|
|
77
|
+
...(opts.content && { content: opts.content }),
|
|
75
78
|
...(opts.email && { email: opts.email }),
|
|
76
79
|
...(opts.phone && { phone: opts.phone }),
|
|
80
|
+
...(opts.address && { address: opts.address }),
|
|
77
81
|
...(opts.website && { website_url: opts.website }),
|
|
78
82
|
...(opts.userId && { user_id: Number(opts.userId) }),
|
|
79
83
|
is_active: !opts.inactive,
|
|
@@ -94,9 +98,11 @@ organizers
|
|
|
94
98
|
.description('Update an organizer')
|
|
95
99
|
.option('--name <name>', 'Organizer name')
|
|
96
100
|
.option('--slug <slug>', 'URL slug')
|
|
97
|
-
.option('--description <text>', '
|
|
101
|
+
.option('--description <text>', 'Short description')
|
|
102
|
+
.option('--content <text>', 'Long-form content (markdown supported)')
|
|
98
103
|
.option('--email <email>', 'Contact email')
|
|
99
104
|
.option('--phone <phone>', 'Contact phone')
|
|
105
|
+
.option('--address <address>', 'Physical address')
|
|
100
106
|
.option('--website <url>', 'Website URL')
|
|
101
107
|
.option('--user-id <id>', 'Assign to a user (submitter) by ID')
|
|
102
108
|
.option('--active <bool>', 'Active status (true/false)')
|
|
@@ -110,8 +116,10 @@ organizers
|
|
|
110
116
|
if (opts.name) body.name = opts.name;
|
|
111
117
|
if (opts.slug) body.slug = opts.slug;
|
|
112
118
|
if (opts.description) body.description = opts.description;
|
|
119
|
+
if (opts.content) body.content = opts.content;
|
|
113
120
|
if (opts.email) body.email = opts.email;
|
|
114
121
|
if (opts.phone) body.phone = opts.phone;
|
|
122
|
+
if (opts.address) body.address = opts.address;
|
|
115
123
|
if (opts.website) body.website_url = opts.website;
|
|
116
124
|
if (opts.userId) body.user_id = Number(opts.userId);
|
|
117
125
|
if (opts.active !== undefined) body.is_active = opts.active === 'true';
|