@stonecrop/nuxt 0.7.3 → 0.7.5

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.
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Utilities for manipulating nuxt.config.ts
3
+ */
4
+
5
+ import { readFile, writeFile } from 'node:fs/promises'
6
+ import { existsSync } from 'node:fs'
7
+ import { join } from 'pathe'
8
+ import consola from 'consola'
9
+
10
+ export interface NuxtConfigUpdate {
11
+ /** Module to add to the modules array */
12
+ module?: string
13
+ /** Module options to add (key in defineNuxtConfig) */
14
+ moduleOptions?: {
15
+ key: string
16
+ value: string
17
+ }
18
+ /** Import to add at the top of the file */
19
+ import?: string
20
+ /** Nitro configuration to add */
21
+ nitroConfig?: {
22
+ externalsInline?: string[]
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Check if a module is already configured in nuxt.config.ts
28
+ */
29
+ export async function hasModule(cwd: string, moduleName: string): Promise<boolean> {
30
+ const configPath = findNuxtConfig(cwd)
31
+ if (!configPath) return false
32
+
33
+ const content = await readFile(configPath, 'utf-8')
34
+
35
+ // Check for module in modules array
36
+ const modulePatterns = [
37
+ new RegExp(`['"\`]${escapeRegex(moduleName)}['"\`]`),
38
+ new RegExp(`from\\s+['"\`]${escapeRegex(moduleName)}['"\`]`),
39
+ ]
40
+
41
+ return modulePatterns.some(pattern => pattern.test(content))
42
+ }
43
+
44
+ /**
45
+ * Find the nuxt.config file in the project
46
+ */
47
+ export function findNuxtConfig(cwd: string): string | null {
48
+ const candidates = ['nuxt.config.ts', 'nuxt.config.js', 'nuxt.config.mjs']
49
+
50
+ for (const candidate of candidates) {
51
+ const fullPath = join(cwd, candidate)
52
+ if (existsSync(fullPath)) {
53
+ return fullPath
54
+ }
55
+ }
56
+
57
+ return null
58
+ }
59
+
60
+ /**
61
+ * Update nuxt.config.ts with new module configuration
62
+ *
63
+ * This uses string manipulation rather than AST parsing for simplicity
64
+ * and to preserve formatting/comments as much as possible.
65
+ */
66
+ export async function updateNuxtConfig(cwd: string, updates: NuxtConfigUpdate): Promise<boolean> {
67
+ const configPath = findNuxtConfig(cwd)
68
+ if (!configPath) {
69
+ consola.error('Could not find nuxt.config.ts in', cwd)
70
+ return false
71
+ }
72
+
73
+ let content = await readFile(configPath, 'utf-8')
74
+ let modified = false
75
+
76
+ // Add import if specified
77
+ if (updates.import) {
78
+ if (!content.includes(updates.import)) {
79
+ // Find the first import or the start of the file
80
+ const importMatch = content.match(/^import\s+/m)
81
+ if (importMatch && importMatch.index !== undefined) {
82
+ // Add after existing imports
83
+ const lastImportMatch = content.match(/^import\s.*$/gm)
84
+ if (lastImportMatch && lastImportMatch.length > 0) {
85
+ const lastImport = lastImportMatch[lastImportMatch.length - 1]
86
+ if (lastImport) {
87
+ const lastImportIndex = content.lastIndexOf(lastImport)
88
+ const insertIndex = lastImportIndex + lastImport.length
89
+ content = content.slice(0, insertIndex) + '\n' + updates.import + content.slice(insertIndex)
90
+ }
91
+ }
92
+ } else {
93
+ // Add at the start of the file
94
+ content = updates.import + '\n\n' + content
95
+ }
96
+ modified = true
97
+ }
98
+ }
99
+
100
+ // Add module to modules array
101
+ if (updates.module) {
102
+ const moduleEntry = updates.module
103
+
104
+ // Find the modules array
105
+ const modulesMatch = content.match(/modules\s*:\s*\[/)
106
+ if (modulesMatch && modulesMatch.index !== undefined) {
107
+ // Find the closing bracket
108
+ const startIndex = modulesMatch.index + modulesMatch[0].length
109
+ const closingIndex = findMatchingBracket(content, startIndex - 1)
110
+
111
+ if (closingIndex !== -1) {
112
+ // Check if module is already present in the modules array (not just anywhere in the file)
113
+ const arrayContent = content.slice(startIndex, closingIndex)
114
+ const moduleAlreadyExists = arrayContent.includes(moduleEntry)
115
+
116
+ if (!moduleAlreadyExists) {
117
+ // Check if array is empty
118
+ const separator = arrayContent.trim().length > 0 ? ', ' : ''
119
+
120
+ content = content.slice(0, closingIndex) + separator + moduleEntry + content.slice(closingIndex)
121
+ modified = true
122
+ }
123
+ }
124
+ } else {
125
+ // modules array doesn't exist, we need to add it
126
+ const defineNuxtConfigMatch = content.match(/defineNuxtConfig\s*\(\s*\{/)
127
+ if (defineNuxtConfigMatch && defineNuxtConfigMatch.index !== undefined) {
128
+ const insertIndex = defineNuxtConfigMatch.index + defineNuxtConfigMatch[0].length
129
+ content = content.slice(0, insertIndex) + `\n\tmodules: [${moduleEntry}],` + content.slice(insertIndex)
130
+ modified = true
131
+ }
132
+ }
133
+ }
134
+
135
+ // Add module options
136
+ if (updates.moduleOptions) {
137
+ const { key, value } = updates.moduleOptions
138
+
139
+ // Check if the key already exists
140
+ const keyPattern = new RegExp(`${escapeRegex(key)}\\s*:`)
141
+ if (!keyPattern.test(content)) {
142
+ // Find defineNuxtConfig and add the option
143
+ const defineNuxtConfigMatch = content.match(/defineNuxtConfig\s*\(\s*\{/)
144
+ if (defineNuxtConfigMatch && defineNuxtConfigMatch.index !== undefined) {
145
+ // Find a good place to insert (after modules if it exists, otherwise at the start)
146
+ const modulesEndMatch = content.match(/modules\s*:\s*\[[^\]]*\]\s*,?/)
147
+ if (modulesEndMatch && modulesEndMatch.index !== undefined) {
148
+ const insertIndex = modulesEndMatch.index + modulesEndMatch[0].length
149
+ content = content.slice(0, insertIndex) + `\n\n\t${key}: ${value},` + content.slice(insertIndex)
150
+ } else {
151
+ const insertIndex = defineNuxtConfigMatch.index + defineNuxtConfigMatch[0].length
152
+ content = content.slice(0, insertIndex) + `\n\t${key}: ${value},` + content.slice(insertIndex)
153
+ }
154
+ modified = true
155
+ }
156
+ }
157
+ }
158
+
159
+ // Add Nitro configuration
160
+ if (updates.nitroConfig) {
161
+ // Check if nitro config already exists
162
+ const nitroMatch = content.match(/nitro\s*:\s*\{/)
163
+
164
+ if (updates.nitroConfig.externalsInline) {
165
+ const packagesStr = updates.nitroConfig.externalsInline.map(pkg => `'${pkg}'`).join(', ')
166
+
167
+ if (nitroMatch && nitroMatch.index !== undefined) {
168
+ // Nitro config exists, check if externals.inline exists
169
+ const externalsMatch = content.match(/externals\s*:\s*\{/)
170
+
171
+ if (externalsMatch && externalsMatch.index !== undefined && externalsMatch.index > nitroMatch.index) {
172
+ // Check if inline array exists
173
+ const inlineMatch = content.match(/inline\s*:\s*\[/)
174
+
175
+ if (inlineMatch && inlineMatch.index !== undefined && inlineMatch.index > externalsMatch.index) {
176
+ // inline array exists, add packages if not present
177
+ const startIndex = inlineMatch.index + inlineMatch[0].length
178
+ const closingIndex = findMatchingBracket(content, startIndex - 1)
179
+
180
+ if (closingIndex !== -1) {
181
+ const arrayContent = content.slice(startIndex, closingIndex).trim()
182
+ const separator = arrayContent.length > 0 ? ', ' : ''
183
+ content = content.slice(0, closingIndex) + separator + packagesStr + content.slice(closingIndex)
184
+ modified = true
185
+ }
186
+ } else {
187
+ // externals exists but no inline, add it
188
+ const startIndex = externalsMatch.index + externalsMatch[0].length
189
+ content = content.slice(0, startIndex) + `\n\t\t\tinline: [${packagesStr}],` + content.slice(startIndex)
190
+ modified = true
191
+ }
192
+ } else {
193
+ // nitro exists but no externals, add it
194
+ const startIndex = nitroMatch.index + nitroMatch[0].length
195
+ content =
196
+ content.slice(0, startIndex) +
197
+ `\n\t\texternals: {\n\t\t\tinline: [${packagesStr}],\n\t\t},` +
198
+ content.slice(startIndex)
199
+ modified = true
200
+ }
201
+ } else {
202
+ // No nitro config, add everything - insert after modules array
203
+ const defineNuxtConfigMatch = content.match(/defineNuxtConfig\s*\(\s*\{/)
204
+ if (defineNuxtConfigMatch && defineNuxtConfigMatch.index !== undefined) {
205
+ // Try to find modules array to insert after it
206
+ const modulesEndMatch = content.match(/modules\s*:\s*\[[^\]]*\]\s*,?/)
207
+ if (modulesEndMatch && modulesEndMatch.index !== undefined) {
208
+ const insertIndex = modulesEndMatch.index + modulesEndMatch[0].length
209
+ content =
210
+ content.slice(0, insertIndex) +
211
+ `\n\n\t// Nitro configuration for Stonecrop CSS handling\n\tnitro: {\n\t\texternals: {\n\t\t\tinline: [${packagesStr}],\n\t\t},\n\t},` +
212
+ content.slice(insertIndex)
213
+ } else {
214
+ // No modules, add at the start of config
215
+ const insertIndex = defineNuxtConfigMatch.index + defineNuxtConfigMatch[0].length
216
+ content =
217
+ content.slice(0, insertIndex) +
218
+ `\n\t// Nitro configuration for Stonecrop CSS handling\n\tnitro: {\n\t\texternals: {\n\t\t\tinline: [${packagesStr}],\n\t\t},\n\t},` +
219
+ content.slice(insertIndex)
220
+ }
221
+ modified = true
222
+ }
223
+ }
224
+ }
225
+ }
226
+
227
+ if (modified) {
228
+ await writeFile(configPath, content, 'utf-8')
229
+ consola.success(`Updated ${configPath}`)
230
+ }
231
+
232
+ return modified
233
+ }
234
+
235
+ /**
236
+ * Find the matching closing bracket
237
+ */
238
+ function findMatchingBracket(content: string, openIndex: number): number {
239
+ const openChar = content[openIndex]
240
+ const closeChar = openChar === '[' ? ']' : openChar === '{' ? '}' : ')'
241
+
242
+ let depth = 1
243
+ let i = openIndex + 1
244
+
245
+ while (i < content.length && depth > 0) {
246
+ const char = content[i]
247
+ if (char === openChar) depth++
248
+ else if (char === closeChar) depth--
249
+ i++
250
+ }
251
+
252
+ return depth === 0 ? i - 1 : -1
253
+ }
254
+
255
+ /**
256
+ * Escape special regex characters
257
+ */
258
+ function escapeRegex(str: string): string {
259
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
260
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * CLI utilities barrel export
3
+ */
4
+
5
+ export { hasModule, findNuxtConfig, updateNuxtConfig, type NuxtConfigUpdate } from './config'
6
+
7
+ export {
8
+ readPackageJson,
9
+ writePackageJson,
10
+ hasPackage,
11
+ addDependencies,
12
+ detectPackageManager,
13
+ getInstallCommand,
14
+ type PackageJson,
15
+ } from './package'
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Utilities for manipulating package.json
3
+ */
4
+
5
+ import { readFile, writeFile } from 'node:fs/promises'
6
+ import { existsSync } from 'node:fs'
7
+ import { join } from 'pathe'
8
+ import consola from 'consola'
9
+
10
+ export interface PackageJson {
11
+ name?: string
12
+ version?: string
13
+ dependencies?: Record<string, string>
14
+ devDependencies?: Record<string, string>
15
+ [key: string]: unknown
16
+ }
17
+
18
+ /**
19
+ * Read package.json from the project
20
+ */
21
+ export async function readPackageJson(cwd: string): Promise<PackageJson | null> {
22
+ const packagePath = join(cwd, 'package.json')
23
+
24
+ if (!existsSync(packagePath)) {
25
+ return null
26
+ }
27
+
28
+ const content = await readFile(packagePath, 'utf-8')
29
+ return JSON.parse(content)
30
+ }
31
+
32
+ /**
33
+ * Write package.json to the project
34
+ */
35
+ export async function writePackageJson(cwd: string, pkg: PackageJson): Promise<void> {
36
+ const packagePath = join(cwd, 'package.json')
37
+ await writeFile(packagePath, JSON.stringify(pkg, null, '\t') + '\n', 'utf-8')
38
+ }
39
+
40
+ /**
41
+ * Check if a package is installed (in dependencies or devDependencies)
42
+ */
43
+ export async function hasPackage(cwd: string, packageName: string): Promise<boolean> {
44
+ const pkg = await readPackageJson(cwd)
45
+ if (!pkg) return false
46
+
47
+ return !!(pkg.dependencies?.[packageName] || pkg.devDependencies?.[packageName])
48
+ }
49
+
50
+ /**
51
+ * Add dependencies to package.json
52
+ */
53
+ export async function addDependencies(
54
+ cwd: string,
55
+ dependencies: Record<string, string>,
56
+ options: { dev?: boolean } = {}
57
+ ): Promise<boolean> {
58
+ const pkg = await readPackageJson(cwd)
59
+ if (!pkg) {
60
+ consola.error('Could not find package.json in', cwd)
61
+ return false
62
+ }
63
+
64
+ const key = options.dev ? 'devDependencies' : 'dependencies'
65
+ pkg[key] = pkg[key] || {}
66
+
67
+ let modified = false
68
+ for (const [name, version] of Object.entries(dependencies)) {
69
+ if (!pkg[key]![name]) {
70
+ pkg[key]![name] = version
71
+ modified = true
72
+ consola.info(`Adding ${name}@${version} to ${key}`)
73
+ }
74
+ }
75
+
76
+ if (modified) {
77
+ // Sort dependencies alphabetically
78
+ pkg[key] = sortObject(pkg[key] as Record<string, string>)
79
+ await writePackageJson(cwd, pkg)
80
+ }
81
+
82
+ return modified
83
+ }
84
+
85
+ /**
86
+ * Get the package manager used in the project
87
+ */
88
+ export function detectPackageManager(cwd: string): 'npm' | 'yarn' | 'pnpm' | 'bun' {
89
+ if (existsSync(join(cwd, 'bun.lockb')) || existsSync(join(cwd, 'bun.lock'))) {
90
+ return 'bun'
91
+ }
92
+ if (existsSync(join(cwd, 'pnpm-lock.yaml'))) {
93
+ return 'pnpm'
94
+ }
95
+ if (existsSync(join(cwd, 'yarn.lock'))) {
96
+ return 'yarn'
97
+ }
98
+ return 'npm'
99
+ }
100
+
101
+ /**
102
+ * Get the install command for the detected package manager
103
+ */
104
+ export function getInstallCommand(cwd: string): string {
105
+ const pm = detectPackageManager(cwd)
106
+ switch (pm) {
107
+ case 'bun':
108
+ return 'bun install'
109
+ case 'pnpm':
110
+ return 'pnpm install'
111
+ case 'yarn':
112
+ return 'yarn'
113
+ default:
114
+ return 'npm install'
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Sort an object's keys alphabetically
120
+ */
121
+ function sortObject(obj: Record<string, string>): Record<string, string> {
122
+ return Object.keys(obj)
123
+ .sort()
124
+ .reduce((acc, key) => {
125
+ acc[key] = obj[key]
126
+ return acc
127
+ }, {} as Record<string, string>)
128
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Utilities for managing Grafserv plugin configuration in nuxt.config.ts
3
+ */
4
+
5
+ import { readFile, writeFile } from 'node:fs/promises'
6
+ import consola from 'consola'
7
+
8
+ import { findNuxtConfig } from './config'
9
+
10
+ /**
11
+ * Add a plugin to the grafserv preset plugins array in nuxt.config.ts
12
+ *
13
+ * @param cwd - Current working directory
14
+ * @param pluginCode - The plugin code to add (e.g., "pglCaslPlugin" or "createPglRockfoilPlugin({})")
15
+ * @returns true if successfully added, false otherwise
16
+ */
17
+ export async function addPluginToGrafservConfig(cwd: string, pluginCode: string): Promise<boolean> {
18
+ const configPath = findNuxtConfig(cwd)
19
+ if (!configPath) {
20
+ consola.error('Could not find nuxt.config.ts')
21
+ return false
22
+ }
23
+
24
+ let content = await readFile(configPath, 'utf-8')
25
+
26
+ // Check if plugin is already configured
27
+ if (content.includes(pluginCode)) {
28
+ consola.info('Plugin already configured in nuxt.config.ts')
29
+ return true
30
+ }
31
+
32
+ // Find grafserv configuration
33
+ const grafservMatch = content.match(/grafserv\s*:\s*\{/)
34
+ if (!grafservMatch || grafservMatch.index === undefined) {
35
+ consola.warn('Could not find grafserv configuration in nuxt.config.ts')
36
+ return false
37
+ }
38
+
39
+ // Check if preset exists within grafserv config
40
+ const grafservStart = grafservMatch.index
41
+ const presetMatch = content.slice(grafservStart).match(/preset\s*:\s*\{/)
42
+
43
+ if (presetMatch && presetMatch.index !== undefined) {
44
+ const presetStart = grafservStart + presetMatch.index
45
+ // Check if plugins array exists within preset
46
+ const pluginsMatch = content.slice(presetStart).match(/plugins\s*:\s*\[/)
47
+
48
+ if (pluginsMatch && pluginsMatch.index !== undefined) {
49
+ // Plugins array exists, add to it
50
+ const pluginsStart = presetStart + pluginsMatch.index + pluginsMatch[0].length
51
+ const closingBracket = findMatchingBracket(content, pluginsStart - 1)
52
+
53
+ if (closingBracket !== -1) {
54
+ const arrayContent = content.slice(pluginsStart, closingBracket).trim()
55
+ const separator = arrayContent.length > 0 ? ', ' : ''
56
+
57
+ content = content.slice(0, closingBracket) + separator + pluginCode + content.slice(closingBracket)
58
+
59
+ await writeFile(configPath, content, 'utf-8')
60
+ consola.success('Added plugin to grafserv.preset.plugins array')
61
+ return true
62
+ }
63
+ } else {
64
+ // preset exists but no plugins array, add it
65
+ const presetContentStart = presetStart + presetMatch[0].length
66
+ content =
67
+ content.slice(0, presetContentStart) + `\n\t\t\tplugins: [${pluginCode}],` + content.slice(presetContentStart)
68
+
69
+ await writeFile(configPath, content, 'utf-8')
70
+ consola.success('Added plugins array to grafserv.preset configuration')
71
+ return true
72
+ }
73
+ } else {
74
+ // grafserv exists but no preset, add preset with plugins
75
+ const grafservContentStart = grafservStart + grafservMatch[0].length
76
+ content =
77
+ content.slice(0, grafservContentStart) +
78
+ `\n\t\tpreset: {\n\t\t\tplugins: [${pluginCode}],\n\t\t},` +
79
+ content.slice(grafservContentStart)
80
+
81
+ await writeFile(configPath, content, 'utf-8')
82
+ consola.success('Added preset.plugins configuration to grafserv')
83
+ return true
84
+ }
85
+
86
+ return false
87
+ }
88
+
89
+ /**
90
+ * Find the matching closing bracket
91
+ */
92
+ function findMatchingBracket(content: string, openIndex: number): number {
93
+ const openChar = content[openIndex]
94
+ const closeChar = openChar === '[' ? ']' : openChar === '{' ? '}' : ')'
95
+
96
+ let depth = 1
97
+ let i = openIndex + 1
98
+
99
+ while (i < content.length && depth > 0) {
100
+ const char = content[i]
101
+ if (char === openChar) depth++
102
+ else if (char === closeChar) depth--
103
+ i++
104
+ }
105
+
106
+ return depth === 0 ? i - 1 : -1
107
+ }
@@ -0,0 +1,85 @@
1
+ {
2
+ "name": "Example",
3
+ "slug": "example/:id",
4
+ "tableName": "examples",
5
+ "fields": [
6
+ {
7
+ "fieldname": "id",
8
+ "fieldtype": "Data",
9
+ "label": "ID",
10
+ "readOnly": true
11
+ },
12
+ {
13
+ "fieldname": "title",
14
+ "fieldtype": "Data",
15
+ "label": "Title",
16
+ "required": true,
17
+ "width": "30ch"
18
+ },
19
+ {
20
+ "fieldname": "description",
21
+ "fieldtype": "Text",
22
+ "label": "Description"
23
+ },
24
+ {
25
+ "fieldname": "status",
26
+ "fieldtype": "Select",
27
+ "label": "Status",
28
+ "options": ["Draft", "Active", "Archived"],
29
+ "default": "Draft"
30
+ },
31
+ {
32
+ "fieldname": "priority",
33
+ "fieldtype": "Select",
34
+ "label": "Priority",
35
+ "options": ["Low", "Medium", "High"],
36
+ "default": "Medium"
37
+ },
38
+ {
39
+ "fieldname": "assignee",
40
+ "fieldtype": "Link",
41
+ "label": "Assignee",
42
+ "options": "User"
43
+ },
44
+ {
45
+ "fieldname": "dueDate",
46
+ "fieldtype": "Date",
47
+ "label": "Due Date"
48
+ },
49
+ {
50
+ "fieldname": "createdAt",
51
+ "fieldtype": "Datetime",
52
+ "label": "Created At",
53
+ "readOnly": true
54
+ },
55
+ {
56
+ "fieldname": "updatedAt",
57
+ "fieldtype": "Datetime",
58
+ "label": "Updated At",
59
+ "readOnly": true
60
+ }
61
+ ],
62
+ "workflow": {
63
+ "states": ["Draft", "Active", "Archived"],
64
+ "actions": {
65
+ "activate": {
66
+ "label": "Activate",
67
+ "handler": "activate_example",
68
+ "allowedStates": ["Draft"],
69
+ "confirm": true
70
+ },
71
+ "archive": {
72
+ "label": "Archive",
73
+ "handler": "archive_example",
74
+ "allowedStates": ["Active"],
75
+ "confirm": true
76
+ },
77
+ "reopen": {
78
+ "label": "Reopen",
79
+ "handler": "reopen_example",
80
+ "allowedStates": ["Archived"],
81
+ "confirm": false
82
+ }
83
+ }
84
+ }
85
+ }
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "Example",
3
+ "slug": "example",
4
+ "tableName": "examples",
5
+ "listDoctype": "Example",
6
+ "schema": [
7
+ {
8
+ "component": "ATable",
9
+ "columns": [
10
+ {
11
+ "name": "id",
12
+ "label": "ID",
13
+ "fieldtype": "Data",
14
+ "width": "8ch"
15
+ },
16
+ {
17
+ "name": "title",
18
+ "label": "Title",
19
+ "fieldtype": "Data",
20
+ "width": "25ch"
21
+ },
22
+ {
23
+ "name": "status",
24
+ "label": "Status",
25
+ "fieldtype": "Data",
26
+ "width": "10ch"
27
+ },
28
+ {
29
+ "name": "priority",
30
+ "label": "Priority",
31
+ "fieldtype": "Data",
32
+ "width": "10ch"
33
+ },
34
+ {
35
+ "name": "assignee",
36
+ "label": "Assignee",
37
+ "fieldtype": "Data",
38
+ "width": "15ch"
39
+ },
40
+ {
41
+ "name": "dueDate",
42
+ "label": "Due Date",
43
+ "fieldtype": "Date",
44
+ "width": "12ch"
45
+ },
46
+ {
47
+ "name": "createdAt",
48
+ "label": "Created",
49
+ "fieldtype": "Datetime",
50
+ "width": "18ch"
51
+ }
52
+ ],
53
+ "config": {
54
+ "view": "list",
55
+ "sortable": true,
56
+ "filterable": true
57
+ }
58
+ }
59
+ ]
60
+ }