alchemy-widget 0.3.0-alpha.2 → 0.3.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.
@@ -57,7 +57,7 @@ Toolbar.addElementGetter('button_save_all', 'al-button.save-all');
57
57
  *
58
58
  * @author Jelle De Loecker <jelle@elevenways.be>
59
59
  * @since 0.2.0
60
- * @version 0.2.0
60
+ * @version 0.3.0
61
61
  */
62
62
  Toolbar.setStatic(function show() {
63
63
 
@@ -71,6 +71,11 @@ Toolbar.setStatic(function show() {
71
71
  return;
72
72
  }
73
73
 
74
+ // Don't show if an al-editor-toolbar exists (e.g., in Chimera)
75
+ if (document.querySelector('al-editor-toolbar')) {
76
+ return;
77
+ }
78
+
74
79
  toolbar = hawkejs.createElement('al-widget-toolbar');
75
80
 
76
81
  hawkejs.scene.bottom_element.append(toolbar);
@@ -105,27 +110,27 @@ Toolbar.setMethod(function getAllRootWidgets() {
105
110
  return result;
106
111
  });
107
112
 
113
+ /**
114
+ * Get the target widgets (all root widgets for this toolbar)
115
+ *
116
+ * @author Jelle De Loecker <jelle@elevenways.be>
117
+ * @since 0.3.0
118
+ * @version 0.3.0
119
+ */
120
+ Toolbar.setMethod(function getTargetWidgets() {
121
+ return this.getAllRootWidgets();
122
+ });
123
+
108
124
  /**
109
125
  * Start editing all the widgets
110
126
  *
111
127
  * @author Jelle De Loecker <jelle@elevenways.be>
112
128
  * @since 0.2.0
113
- * @version 0.2.0
129
+ * @version 0.3.0
114
130
  */
115
131
  Toolbar.setMethod(function startEditing() {
116
-
117
- let i;
118
-
119
132
  Blast.editing = true;
120
- document.body.classList.add('editing-blocks');
121
-
122
- let elements = this.getAllRootWidgets();
123
-
124
- for (i = 0; i < elements.length; i++) {
125
- elements[i].startEditor();
126
- }
127
-
128
- this.setState('editing');
133
+ startEditing.super.call(this);
129
134
  });
130
135
 
131
136
  /**
@@ -133,7 +138,7 @@ Toolbar.setMethod(function startEditing() {
133
138
  *
134
139
  * @author Jelle De Loecker <jelle@elevenways.be>
135
140
  * @since 0.2.0
136
- * @version 0.2.0
141
+ * @version 0.3.0
137
142
  */
138
143
  Toolbar.setMethod(function stopEditing() {
139
144
 
@@ -141,123 +146,8 @@ Toolbar.setMethod(function stopEditing() {
141
146
  return;
142
147
  }
143
148
 
144
- let i;
145
-
146
149
  Blast.editing = false;
147
- document.body.classList.remove('editing-blocks');
148
-
149
- let elements = this.getAllRootWidgets();
150
-
151
- for (i = 0; i < elements.length; i++) {
152
- elements[i].stopEditor();
153
- }
154
-
155
- this.setState('ready');
156
- });
157
-
158
- /**
159
- * Save all the widgets
160
- *
161
- * @author Jelle De Loecker <jelle@elevenways.be>
162
- * @since 0.2.0
163
- * @version 0.2.0
164
- */
165
- Toolbar.setMethod(async function saveAll() {
166
-
167
- if (this._saving) {
168
- try {
169
- await this._saving;
170
- } catch (err) {
171
- // Ignore;
172
- }
173
- }
174
-
175
- this._saving = null;
176
-
177
- let elements = this.getAllRootWidgets();
178
- let widget_data = [];
179
- let pledge;
180
-
181
- for (let element of elements) {
182
- let entry = element.gatherSaveData();
183
-
184
- if (entry) {
185
- widget_data.push(entry);
186
- }
187
- }
188
-
189
- if (widget_data.length) {
190
- let config = {
191
- href : alchemy.routeUrl('AlchemyWidgets#save'),
192
- post : {
193
- widgets: widget_data
194
- }
195
- };
196
-
197
- pledge = alchemy.fetch(config);
198
- this._saving = pledge;
199
- }
200
-
201
- return pledge;
202
- });
203
-
204
- /**
205
- * Save all and update the states
206
- *
207
- * @author Jelle De Loecker <jelle@elevenways.be>
208
- * @since 0.2.0
209
- * @version 0.2.0
210
- *
211
- * @param {Boolean} before_stop
212
- */
213
- Toolbar.setMethod(async function saveAllAndUpdateButtonStates(before_stop) {
214
-
215
- let state = 'saving',
216
- button;
217
-
218
- if (before_stop) {
219
- button = this.button_stop_and_save;
220
- state += '-before-stop';
221
- } else {
222
- button = this.button_save_all;
223
- }
224
-
225
- this.setState(state);
226
- button.setState(state);
227
-
228
- let save_error = null;
229
-
230
- let restore_toolbar_state = this.wrapForCurrentState(() => {
231
- if (save_error) {
232
- this.setState('error');
233
- } else {
234
- this.setState('editing')
235
- }
236
- });
237
-
238
- let restore_button_state = button.wrapForCurrentState(() => {
239
-
240
- if (save_error) {
241
- button.setState('error');
242
- } else {
243
- button.setState('saved', 2500, 'ready');
244
- }
245
- });
246
-
247
- try {
248
- await this.saveAll();
249
- } catch (err) {
250
- save_error = err;
251
- }
252
-
253
- restore_toolbar_state();
254
- restore_button_state();
255
-
256
- if (save_error) {
257
- return false;
258
- }
259
-
260
- return true;
150
+ stopEditing.super.call(this);
261
151
  });
262
152
 
263
153
  /**
@@ -284,11 +174,6 @@ Toolbar.setMethod(function introduced() {
284
174
 
285
175
  if (this.toolbar_manager) {
286
176
  this.prepareToolbarManager(this.toolbar_manager);
287
- } else {
288
- let manager = hawkejs.scene.exposed.toolbar_manager;
289
- if (manager) {
290
- this.prepareToolbarManager(manager);
291
- }
292
177
  }
293
178
 
294
179
  this.button_start.addEventListener('activate', async e => {
@@ -354,4 +239,4 @@ Toolbar.setMethod(function connected() {
354
239
  Toolbar.setMethod(function disconnected() {
355
240
  let html = document.querySelector('html');
356
241
  html.classList.remove('with-al-widget-toolbar');
357
- });
242
+ });
@@ -24,11 +24,9 @@ const COLOURS = [
24
24
  *
25
25
  * @author Jelle De Loecker <jelle@elevenways.be>
26
26
  * @since 0.2.7
27
- * @version 0.2.7
27
+ * @version 0.3.0
28
28
  */
29
- const DocumentWatcher = Function.inherits('Alchemy.Syncable', 'Alchemy.Widget', function DocumentWatcher() {
30
- DocumentWatcher.super.call(this, 'document_watcher');
31
- });
29
+ const DocumentWatcher = Function.inherits('Alchemy.Syncable.Specialized', 'Alchemy.Widget', 'DocumentWatcher');
32
30
 
33
31
  /**
34
32
  * Create a watcher for the given document
@@ -60,6 +58,42 @@ DocumentWatcher.setStatic(function create(model, pk) {
60
58
 
61
59
  if (Blast.isNode) {
62
60
 
61
+ /**
62
+ * Recreate a watcher after server restart.
63
+ * Called by Syncable.tryRecreate() when client reconnects.
64
+ *
65
+ * @author Jelle De Loecker <jelle@elevenways.be>
66
+ * @since 0.3.0
67
+ * @version 0.3.0
68
+ *
69
+ * @param {Conduit} conduit
70
+ * @param {Object} config Contains type, id, and version from the client
71
+ *
72
+ * @return {DocumentWatcher}
73
+ */
74
+ DocumentWatcher.setStatic(async function recreate(conduit, config) {
75
+
76
+ // The config.id follows the pattern "Model:pk"
77
+ let parts = config.id.split(':');
78
+
79
+ if (parts.length < 2) {
80
+ return null;
81
+ }
82
+
83
+ let model = parts[0],
84
+ pk = parts.slice(1).join(':'); // Handle pks with colons
85
+
86
+ // Use the existing create method which handles caching
87
+ let watcher = this.create(model, pk);
88
+
89
+ if (watcher) {
90
+ // Re-add this conduit as a watcher
91
+ await watcher.addWatcher(conduit);
92
+ }
93
+
94
+ return watcher;
95
+ });
96
+
63
97
  /**
64
98
  * Add a viewer based on the conduit
65
99
  *
@@ -69,12 +103,13 @@ if (Blast.isNode) {
69
103
  *
70
104
  * @param {Alchemy.Conduit} conduit
71
105
  */
72
- DocumentWatcher.setTypedMethod([Types.Alchemy.Conduit], function addWatcher(conduit) {
106
+ DocumentWatcher.setTypedMethod([Types.Alchemy.Conduit], async function addWatcher(conduit) {
73
107
 
74
108
  let user_id = '' + conduit.getUserId(),
75
109
  scene_id = conduit.scene_id;
76
110
 
77
- this.addWatcher(user_id, scene_id);
111
+ // IMPORTANT: await the async addWatcher method!
112
+ await this.addWatcher(user_id, scene_id);
78
113
 
79
114
  // Watchers should also be registered as a client
80
115
  this.registerClient(conduit);
@@ -9,10 +9,64 @@ const SCENE_MAP = new Map(),
9
9
  *
10
10
  * @author Jelle De Loecker <jelle@elevenways.be>
11
11
  * @since 0.2.7
12
- * @version 0.2.7
12
+ * @version 0.3.0
13
+ */
14
+ const EditorToolbarManager = Function.inherits('Alchemy.Syncable.Specialized', 'Alchemy.Widget', 'EditorToolbarManager');
15
+
16
+ /**
17
+ * Storage for document button providers
18
+ *
19
+ * @author Jelle De Loecker <jelle@elevenways.be>
20
+ * @since 0.3.0
21
+ * @version 0.3.0
22
+ */
23
+ EditorToolbarManager.setStatic('document_button_providers', []);
24
+
25
+ /**
26
+ * Storage for model button providers
27
+ *
28
+ * @author Jelle De Loecker <jelle@elevenways.be>
29
+ * @since 0.3.0
30
+ * @version 0.3.0
31
+ */
32
+ EditorToolbarManager.setStatic('model_button_providers', []);
33
+
34
+ /**
35
+ * Register a callback that can add toolbar buttons when a document is set.
36
+ * This allows plugins to add their own buttons without modifying this file.
37
+ *
38
+ * @author Jelle De Loecker <jelle@elevenways.be>
39
+ * @since 0.3.0
40
+ * @version 0.3.0
41
+ *
42
+ * @param {Function} callback Function(manager, doc, model, model_name, pk_val)
43
+ */
44
+ EditorToolbarManager.setStatic(function registerDocumentButtonProvider(callback) {
45
+
46
+ if (typeof callback !== 'function') {
47
+ throw new Error('Button provider must be a function');
48
+ }
49
+
50
+ this.document_button_providers.push(callback);
51
+ });
52
+
53
+ /**
54
+ * Register a callback that can add toolbar buttons when a model is set.
55
+ * This allows plugins to add their own buttons without modifying this file.
56
+ *
57
+ * @author Jelle De Loecker <jelle@elevenways.be>
58
+ * @since 0.3.0
59
+ * @version 0.3.0
60
+ *
61
+ * @param {Function} callback Function(manager, model_name)
13
62
  */
14
- const EditorToolbarManager = Function.inherits('Alchemy.Syncable', 'Alchemy.Widget', function EditorToolbarManager() {
15
- EditorToolbarManager.super.call(this, 'editor_toolbar_manager');
63
+ EditorToolbarManager.setStatic(function registerModelButtonProvider(callback) {
64
+
65
+ if (typeof callback !== 'function') {
66
+ throw new Error('Button provider must be a function');
67
+ }
68
+
69
+ this.model_button_providers.push(callback);
16
70
  });
17
71
 
18
72
  if (Blast.isNode) {
@@ -61,6 +115,36 @@ if (Blast.isNode) {
61
115
 
62
116
  return manager;
63
117
  });
118
+
119
+ /**
120
+ * Recreate a manager after server restart.
121
+ * Called by Syncable.tryRecreate() when client reconnects.
122
+ *
123
+ * @author Jelle De Loecker <jelle@elevenways.be>
124
+ * @since 0.3.0
125
+ * @version 0.3.0
126
+ *
127
+ * @param {Conduit} conduit
128
+ * @param {Object} config Contains type, id, and version from the client
129
+ *
130
+ * @return {EditorToolbarManager}
131
+ */
132
+ EditorToolbarManager.setStatic(async function recreate(conduit, config) {
133
+
134
+ // The config.id is the scene_id from the client.
135
+ // Set it on the conduit if not already set, so create() can use it.
136
+ if (!conduit.scene_id) {
137
+ conduit.scene_id = config.id;
138
+ } else if (conduit.scene_id !== config.id) {
139
+ // Scene ID mismatch - this shouldn't happen but log if it does
140
+ log.warning('EditorToolbarManager recreate: scene_id mismatch', {
141
+ conduit_scene_id : conduit.scene_id,
142
+ config_id : config.id,
143
+ });
144
+ }
145
+
146
+ return this.create(conduit);
147
+ });
64
148
  }
65
149
 
66
150
  /**
@@ -99,6 +183,24 @@ EditorToolbarManager.setStateProperty('title', {allow_client_set: false});
99
183
  */
100
184
  EditorToolbarManager.setStateProperty('scenario');
101
185
 
186
+ /**
187
+ * Whether this is a user-specific dashboard
188
+ *
189
+ * @author Jelle De Loecker <jelle@elevenways.be>
190
+ * @since 0.3.0
191
+ * @version 0.3.0
192
+ */
193
+ EditorToolbarManager.setStateProperty('is_user_dashboard', {allow_client_set: false});
194
+
195
+ /**
196
+ * Whether the user has an ID (is logged in)
197
+ *
198
+ * @author Jelle De Loecker <jelle@elevenways.be>
199
+ * @since 0.3.0
200
+ * @version 0.3.0
201
+ */
202
+ EditorToolbarManager.setStateProperty('has_user_id', {allow_client_set: false});
203
+
102
204
  /**
103
205
  * Clear the model fallback
104
206
  *
@@ -162,10 +264,11 @@ EditorToolbarManager.setTypedMethod([Types.String.optional().nullable()], functi
162
264
  this.emitPropertyChange('model_name');
163
265
  }
164
266
 
267
+ // Call registered model button providers
165
268
  if (Blast.isNode && model_name) {
166
- this.addTemplateToRender('buttons', 'chimera/toolbar/create_button', {
167
- model_name: Blast.parseClassPath(model_name).map(entry => entry.underscore()).join('.'),
168
- });
269
+ for (let provider of this.constructor.model_button_providers) {
270
+ provider(this, model_name);
271
+ }
169
272
  }
170
273
  });
171
274
 
@@ -200,20 +303,9 @@ EditorToolbarManager.setMethod(function setDocument(doc) {
200
303
 
201
304
  this.setDocumentWatcher(document_watcher);
202
305
 
203
- if (this.scenario != 'chimera') {
204
- this.addTemplateToRender('buttons', 'chimera/toolbar/edit_in_chimera_button', {
205
- model_name: model_name.underscore(),
206
- record_pk: pk_val,
207
- });
208
- }
209
-
210
- if (model.chimera.record_preview) {
211
- if (this.scenario == 'chimera') {
212
- this.addTemplateToRender('buttons', 'chimera/toolbar/preview_button', {
213
- model_name: model_name.underscore(),
214
- record_pk: pk_val,
215
- });
216
- }
306
+ // Call registered document button providers
307
+ for (let provider of this.constructor.document_button_providers) {
308
+ provider(this, doc, model, model_name, pk_val);
217
309
  }
218
310
 
219
311
  return document_watcher;
@@ -35,6 +35,245 @@ Widget.makeAbstractClass();
35
35
  */
36
36
  Widget.startNewGroup('widgets');
37
37
 
38
+ /**
39
+ * Standard widget categories.
40
+ * These are the universal categories available in all projects.
41
+ * Projects can register their own categories using Widget.registerCategory()
42
+ *
43
+ * Translation keys are derived automatically from the category name:
44
+ * - Title: category.name (e.g., "layout")
45
+ * - Description: "widget-category-{name}-description" (e.g., "widget-category-layout-description")
46
+ *
47
+ * Templates should pass filter parameters: widget=true category=true
48
+ */
49
+ Widget.CATEGORIES = {
50
+ LAYOUT: {
51
+ name: 'layout',
52
+ icon: 'table-columns',
53
+ order: 10
54
+ },
55
+ TEXT: {
56
+ name: 'text',
57
+ icon: 'font',
58
+ order: 20
59
+ },
60
+ MEDIA: {
61
+ name: 'media',
62
+ icon: 'image',
63
+ order: 30
64
+ },
65
+ DATA: {
66
+ name: 'data',
67
+ icon: 'database',
68
+ order: 40
69
+ },
70
+ NAVIGATION: {
71
+ name: 'navigation',
72
+ icon: 'compass',
73
+ order: 50
74
+ },
75
+ INTERACTIVE: {
76
+ name: 'interactive',
77
+ icon: 'hand-pointer',
78
+ order: 60
79
+ },
80
+ ADVANCED: {
81
+ name: 'advanced',
82
+ icon: 'code',
83
+ order: 100
84
+ }
85
+ };
86
+
87
+ /**
88
+ * Storage for dynamically registered categories.
89
+ * Use Widget.registerCategory() to add new categories.
90
+ */
91
+ Widget.REGISTERED_CATEGORIES = {};
92
+
93
+ /**
94
+ * Register a new widget category.
95
+ * This allows projects to add their own categories without modifying the core.
96
+ *
97
+ * Translation keys are derived automatically from the category name:
98
+ * - Title: category.name (e.g., "monitoring")
99
+ * - Description: "widget-category-{name}-description" (e.g., "widget-category-monitoring-description")
100
+ *
101
+ * @author Jelle De Loecker <jelle@elevenways.be>
102
+ * @since 0.3.0
103
+ * @version 0.3.0
104
+ *
105
+ * @param {string} key The category key (e.g., 'MONITORING')
106
+ * @param {Object} config Category configuration
107
+ * @param {string} config.name Internal name (e.g., 'monitoring')
108
+ * @param {string} config.icon FontAwesome icon name
109
+ * @param {number} [config.order] Sort order (lower = earlier, default: 90)
110
+ */
111
+ Widget.setStatic(function registerCategory(key, config) {
112
+
113
+ if (!key || typeof key !== 'string') {
114
+ throw new Error('Category key must be a non-empty string');
115
+ }
116
+
117
+ if (!config || typeof config !== 'object') {
118
+ throw new Error('Category config must be an object');
119
+ }
120
+
121
+ if (!config.name || typeof config.name !== 'string') {
122
+ throw new Error('Category config.name must be a non-empty string');
123
+ }
124
+
125
+ if (!config.icon || typeof config.icon !== 'string') {
126
+ throw new Error('Category config.icon must be a non-empty string');
127
+ }
128
+
129
+ // Normalize the key to uppercase
130
+ key = key.toUpperCase();
131
+
132
+ // Create the category config with defaults
133
+ let category = {
134
+ name : config.name,
135
+ icon : config.icon,
136
+ order : config.order ?? 90
137
+ };
138
+
139
+ // Store in registered categories
140
+ this.REGISTERED_CATEGORIES[key] = category;
141
+
142
+ // Also add to CATEGORIES for backwards compatibility and easy access
143
+ this.CATEGORIES[key] = category;
144
+ });
145
+
146
+ /**
147
+ * Set the widget category
148
+ *
149
+ * @author Jelle De Loecker <jelle@elevenways.be>
150
+ * @since 0.3.0
151
+ * @version 0.3.0
152
+ *
153
+ * @param {String} category The category name (use CATEGORIES.*.name or custom string)
154
+ */
155
+ Widget.setStatic(function setCategory(category) {
156
+ this.category = category;
157
+ });
158
+
159
+ /**
160
+ * Set the widget description
161
+ *
162
+ * @author Jelle De Loecker <jelle@elevenways.be>
163
+ * @since 0.3.0
164
+ * @version 0.3.0
165
+ *
166
+ * @param {String} description A short description of what the widget does
167
+ */
168
+ Widget.setStatic(function setDescription(description) {
169
+ this.description = description;
170
+ });
171
+
172
+ /**
173
+ * Set the widget icon (FontAwesome icon name)
174
+ *
175
+ * @author Jelle De Loecker <jelle@elevenways.be>
176
+ * @since 0.3.0
177
+ * @version 0.3.0
178
+ *
179
+ * @param {String} icon The icon name (e.g., 'table', 'font', 'image')
180
+ */
181
+ Widget.setStatic(function setIcon(icon) {
182
+ this.icon = icon;
183
+ });
184
+
185
+ /**
186
+ * Override the auto-generated title
187
+ *
188
+ * @author Jelle De Loecker <jelle@elevenways.be>
189
+ * @since 0.3.0
190
+ * @version 0.3.0
191
+ *
192
+ * @param {String} title The display title for the widget
193
+ */
194
+ Widget.setStatic(function setTitle(title) {
195
+ this.title = title;
196
+ });
197
+
198
+ /**
199
+ * Get category info for a given category name.
200
+ * Supports built-in categories, registered categories, and custom strings.
201
+ * Falls back to ADVANCED category if no category is specified.
202
+ *
203
+ * Translation keys are derived from the category name at render time:
204
+ * - Title: category.name (e.g., "layout")
205
+ * - Description: "widget-category-{name}-description"
206
+ *
207
+ * @author Jelle De Loecker <jelle@elevenways.be>
208
+ * @since 0.3.0
209
+ * @version 0.3.0
210
+ *
211
+ * @param {String} category The category name
212
+ *
213
+ * @return {Object} Category info with name, icon, order
214
+ */
215
+ Widget.setStatic(function getCategoryInfo(category) {
216
+
217
+ // Default to 'advanced' if no category specified
218
+ if (!category) {
219
+ return this.CATEGORIES.ADVANCED;
220
+ }
221
+
222
+ // Check built-in categories (includes registered ones added via registerCategory)
223
+ for (let key in this.CATEGORIES) {
224
+ if (this.CATEGORIES[key].name === category) {
225
+ return this.CATEGORIES[key];
226
+ }
227
+ }
228
+
229
+ // Return a default structure for unknown/custom categories
230
+ // These are categories set via widget.category = 'custom' without registerCategory()
231
+ return {
232
+ name : category,
233
+ icon : 'puzzle-piece',
234
+ order : 90
235
+ };
236
+ });
237
+
238
+ /**
239
+ * Get all available categories.
240
+ * Returns built-in categories, registered categories, and any custom categories
241
+ * discovered from widgets that set their category to a custom string.
242
+ *
243
+ * @author Jelle De Loecker <jelle@elevenways.be>
244
+ * @since 0.3.0
245
+ * @version 0.3.0
246
+ *
247
+ * @return {Array} Array of category info objects, sorted by order
248
+ */
249
+ Widget.setStatic(function getAllCategories() {
250
+
251
+ let categories = new Map();
252
+
253
+ // Add built-in categories (includes registered ones added via registerCategory)
254
+ for (let key in this.CATEGORIES) {
255
+ let cat = this.CATEGORIES[key];
256
+ categories.set(cat.name, cat);
257
+ }
258
+
259
+ // Check all registered widgets for custom categories
260
+ // (widgets that set category to a custom string without using registerCategory)
261
+ let widgets = alchemy.getClassGroup('widgets');
262
+
263
+ if (widgets) {
264
+ for (let widget of Object.values(widgets)) {
265
+ let category = widget.category;
266
+
267
+ if (category && !categories.has(category)) {
268
+ categories.set(category, this.getCategoryInfo(category));
269
+ }
270
+ }
271
+ }
272
+
273
+ // Convert to array and sort by order
274
+ return Array.from(categories.values()).sortByPath(1, 'order');
275
+ });
276
+
38
277
  /**
39
278
  * Return the class-wide schema
40
279
  *
@@ -10,3 +10,7 @@
10
10
  * @param {Object} data
11
11
  */
12
12
  const Column = Function.inherits('Alchemy.Widget.Container', 'Column');
13
+
14
+ // Widget metadata
15
+ Column.setCategory('layout');
16
+ Column.setIcon('grip-lines-vertical');
@@ -11,6 +11,10 @@
11
11
  */
12
12
  const List = Function.inherits('Alchemy.Widget.Container', 'List');
13
13
 
14
+ // Widget metadata
15
+ List.setCategory('layout');
16
+ List.setIcon('list');
17
+
14
18
  /**
15
19
  * Prepare the schema
16
20
  *