pgkiln 0.43.0 → 0.45.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.
Files changed (69) hide show
  1. package/db/migrations/100_generated_labels.sql +381 -0
  2. package/db/migrations/101_theme_choice.sql +15 -0
  3. package/db/migrations/102_app_computations.sql +345 -0
  4. package/db/migrations/103_shortcuts.sql +341 -0
  5. package/db/migrations/104_report_queries.sql +413 -0
  6. package/db/migrations/105_component_settings_groups.sql +367 -0
  7. package/docs/apex-feature-parity.md +12 -12
  8. package/docs/guide/02-concepts.md +24 -0
  9. package/docs/guide/03-builder.md +48 -9
  10. package/docs/guide/04-pages-and-regions.md +20 -4
  11. package/docs/guide/06-processing.md +22 -0
  12. package/docs/guide/09-reference.md +14 -0
  13. package/docs/guide/12-development.md +2 -1
  14. package/docs/guide/14-globalization.md +5 -1
  15. package/docs/guide/16-files.md +23 -0
  16. package/docs/guide/18-cli.md +4 -0
  17. package/package.json +1 -1
  18. package/public/app.css +40 -1
  19. package/public/app.js +69 -1
  20. package/src/appfiles.ts +18 -2
  21. package/src/builder/advisor.ts +7 -2
  22. package/src/builder/appsheets.ts +53 -13
  23. package/src/builder/appwizard.ts +5 -3
  24. package/src/builder/code-editor.ts +6 -1
  25. package/src/builder/components.ts +85 -3
  26. package/src/builder/forms.ts +1 -1
  27. package/src/builder/shared.ts +39 -6
  28. package/src/builder/subscriptions.ts +30 -4
  29. package/src/cli/replace.ts +6 -2
  30. package/src/i18n/ar.ts +2 -0
  31. package/src/i18n/cs.ts +2 -0
  32. package/src/i18n/da.ts +2 -0
  33. package/src/i18n/de.ts +2 -0
  34. package/src/i18n/el.ts +2 -0
  35. package/src/i18n/es.ts +2 -0
  36. package/src/i18n/fi.ts +2 -0
  37. package/src/i18n/fr.ts +2 -0
  38. package/src/i18n/he.ts +2 -0
  39. package/src/i18n/it.ts +2 -0
  40. package/src/i18n/ja.ts +2 -0
  41. package/src/i18n/ko.ts +2 -0
  42. package/src/i18n/nb.ts +2 -0
  43. package/src/i18n/pl.ts +2 -0
  44. package/src/i18n/pt.ts +2 -0
  45. package/src/i18n/ru.ts +2 -0
  46. package/src/i18n/sv.ts +2 -0
  47. package/src/i18n/tr.ts +2 -0
  48. package/src/i18n/uk.ts +2 -0
  49. package/src/i18n/zh.ts +2 -0
  50. package/src/i18n.ts +4 -0
  51. package/src/metadata.ts +14 -1
  52. package/src/runtime/account.ts +5 -5
  53. package/src/runtime/component-settings.ts +42 -0
  54. package/src/runtime/context.ts +4 -0
  55. package/src/runtime/devbar.ts +16 -13
  56. package/src/runtime/documents.ts +29 -3
  57. package/src/runtime/engine.ts +2 -2
  58. package/src/runtime/facets.ts +34 -3
  59. package/src/runtime/grid.ts +22 -4
  60. package/src/runtime/items.ts +14 -11
  61. package/src/runtime/locale.ts +4 -1
  62. package/src/runtime/logic.ts +12 -7
  63. package/src/runtime/regions.ts +4 -3
  64. package/src/runtime/render.ts +8 -2
  65. package/src/runtime/report.ts +16 -10
  66. package/src/runtime/routes.ts +9 -6
  67. package/src/runtime/shortcuts.ts +43 -0
  68. package/src/security.ts +2 -1
  69. package/src/subscriptions.ts +67 -0
@@ -0,0 +1,381 @@
1
+ -- =====================================================================
2
+ -- 100: generated reports and grids read the same (sprint 78; parity
3
+ -- handoff GEN-2, GEN-1, GEN-7)
4
+ --
5
+ -- - A faceted search page's report shows one column per foreign key: the
6
+ -- parent's display value (Manager → KING) under the key's label. The key
7
+ -- itself is hidden (config.hidden: not shown, not in Actions › Columns,
8
+ -- still in the query for filters and links). Before, both showed, so the
9
+ -- report had two "Manager" and two "Department" headings.
10
+ -- - Interactive grids (the grid page, both grids of a master-detail page)
11
+ -- get headings from meta.wizard_column_label (Deptno → Department Number)
12
+ -- and the number format masks of meta.wizard_mask, like the reports.
13
+ -- - The faceted search report and a map's list report get the format masks
14
+ -- too, so a salary reads 2,850.00 in the report, the grid and the form.
15
+ -- - A faceted search report's rows start with a pencil that opens the form
16
+ -- (link.icon), like a report and form page; the key is plain text.
17
+ --
18
+ -- meta.generate_page is 097's with those changes. Pages generated before
19
+ -- keep their configuration.
20
+ -- =====================================================================
21
+
22
+ -- a grid's headings and number formats: {"headings": {...}, "formats": {...}}
23
+ create or replace function meta.wizard_grid_labels(p_table regclass) returns jsonb
24
+ language sql stable as $$
25
+ select jsonb_build_object(
26
+ 'headings', coalesce(jsonb_object_agg(column_name, meta.wizard_column_label(p_table, column_name)), '{}'),
27
+ 'formats', coalesce(jsonb_object_agg(column_name, meta.wizard_mask(type_sql, kind, is_pk or fk_table is not null))
28
+ filter (where meta.wizard_mask(type_sql, kind, is_pk or fk_table is not null) is not null), '{}'))
29
+ from meta.wizard_catalog(p_table)
30
+ where kind not in ('binary', 'geometry')
31
+ $$;
32
+
33
+ create or replace function meta.generate_page(
34
+ p_app text,
35
+ p_kind text,
36
+ p_table regclass,
37
+ p_page int,
38
+ p_options jsonb default '{}'
39
+ ) returns int
40
+ language plpgsql as $$
41
+ declare
42
+ v_app meta.app;
43
+ o jsonb;
44
+ v_label text;
45
+ v_flabel text;
46
+ v_icon text;
47
+ v_pk text := meta.wizard_pk(p_table);
48
+ v_page int;
49
+ v_region int;
50
+ v_report int;
51
+ v_form int;
52
+ v_fpage int;
53
+ v_link jsonb;
54
+ v_cols text;
55
+ v_heads jsonb := '{}';
56
+ v_src text;
57
+ v_cfg jsonb;
58
+ v_a text;
59
+ v_b text;
60
+ v_c text;
61
+ v_d text;
62
+ v_kind text;
63
+ v_detail regclass;
64
+ v_dpk text;
65
+ v_item text;
66
+ v_facets jsonb := '[]';
67
+ v_hidden jsonb := '[]';
68
+ v_fmts jsonb := '{}';
69
+ v_mask text;
70
+ v_geo text;
71
+ v_key text;
72
+ v_skip text[] := meta.wizard_sidecars(p_table);
73
+ c record;
74
+ begin
75
+ select * into v_app from meta.app where alias = p_app;
76
+ if v_app.id is null then
77
+ raise exception 'application "%" does not exist', p_app;
78
+ end if;
79
+ if p_kind not in ('report_form', 'grid', 'form', 'cards', 'calendar', 'chart', 'map', 'facets', 'master_detail') then
80
+ raise exception 'unknown page type "%"', p_kind;
81
+ end if;
82
+ -- the application's own data: not pgkiln's or the system's tables
83
+ if (select n.nspname ~ '^pg_' or n.nspname in ('information_schema', 'meta')
84
+ from pg_class rel join pg_namespace n on n.oid = rel.relnamespace where rel.oid = p_table) then
85
+ raise exception 'pages can''t be generated on %', p_table;
86
+ end if;
87
+ if (select relkind from pg_class where oid = p_table) not in ('r', 'p', 'v', 'm') then
88
+ raise exception '% is not a table or view', p_table;
89
+ end if;
90
+ if p_options is null or jsonb_typeof(p_options) <> 'object' then
91
+ p_options := '{}';
92
+ end if;
93
+ -- an option given as JSON null or '' means "none"; a missing one takes the default
94
+ o := meta.wizard_defaults(p_kind, p_table) || p_options;
95
+ v_label := coalesce(nullif(btrim(o ->> 'label'), ''), meta.wizard_table_label(p_table, p_kind <> 'form'));
96
+ -- (097) a form page of a listing is titled with the singular: Employees → Employee
97
+ v_flabel := coalesce(nullif(btrim(o ->> 'form_label'), ''), meta.wizard_table_label(p_table, false));
98
+ v_icon := nullif(o ->> 'icon', '');
99
+ v_fpage := nullif(o ->> 'form_page', '')::int;
100
+ if v_fpage = p_page then
101
+ raise exception 'the form page needs a page number of its own';
102
+ end if;
103
+
104
+ if p_kind = 'report_form' then
105
+ v_page := meta.wizard_report_form(v_app, p_table, p_page, v_fpage, v_label, v_flabel);
106
+ elsif p_kind = 'grid' then
107
+ perform meta.generate_grid(p_app, p_table, p_page, v_label, coalesce(v_icon, 'grid'));
108
+ v_page := (select id from meta.page where app_id = v_app.id and page_no = p_page);
109
+ -- (100) the grid's headings and number formats, like the report's
110
+ update meta.region set config = config || meta.wizard_grid_labels(p_table) where page_id = v_page and type = 'grid';
111
+ return v_page;
112
+ elsif p_kind = 'form' then
113
+ v_page := meta.wizard_form(v_app, p_table, p_page, v_label, nullif(o ->> 'return_page', '')::int, o ->> 'mode' = 'modal',
114
+ case when jsonb_typeof(o -> 'columns') = 'array'
115
+ then (select array_agg(meta.wizard_col(p_table, x, 'form column')) from jsonb_array_elements_text(o -> 'columns') x) end);
116
+ elsif p_kind = 'master_detail' then
117
+ if v_pk is null then
118
+ raise exception 'the master table % needs a single-column primary key', p_table;
119
+ end if;
120
+ if nullif(o ->> 'detail', '') is null then
121
+ raise exception 'no detail table: % has no table with a foreign key to it', p_table;
122
+ end if;
123
+ v_detail := (o ->> 'detail')::regclass;
124
+ v_dpk := meta.wizard_pk(v_detail);
125
+ if v_dpk is null then
126
+ raise exception 'the detail table % needs a single-column primary key', v_detail;
127
+ end if;
128
+ v_a := meta.wizard_col(v_detail, o ->> 'detail_column', 'detail column');
129
+ if v_a is null then
130
+ raise exception 'choose the detail column that refers to the master row';
131
+ end if;
132
+ v_item := meta.wizard_item(p_page, v_pk);
133
+ v_page := meta.wizard_new_page(v_app, p_page, v_label, v_app.home_page);
134
+ insert into meta.region (page_id, seq, title, type, columns, source, table_name, pk_column, config)
135
+ values (v_page, 10, meta.wizard_table_label(p_table), 'grid', 12,
136
+ format(E'select %s\n from %s', (select string_agg(quote_ident(column_name), ', ' order by ordinal)
137
+ from meta.wizard_catalog(p_table) where kind <> 'binary' and not (column_name = any (v_skip))),
138
+ meta.wizard_qname(p_table)),
139
+ meta.wizard_qname(p_table), v_pk,
140
+ jsonb_build_object('page_size', 10, 'columns', meta.wizard_grid_columns(p_table),
141
+ 'select_row', jsonb_build_object('column', v_pk, 'item', v_item))
142
+ || meta.wizard_grid_labels(p_table))
143
+ returning id into v_region;
144
+ insert into meta.item (page_id, region_id, seq, name, label, type)
145
+ values (v_page, v_region, 10, v_item, meta.wizard_column_label(p_table, v_pk), 'hidden');
146
+ insert into meta.region (page_id, seq, title, type, columns, source, table_name, pk_column, config)
147
+ values (v_page, 20, meta.wizard_table_label(v_detail), 'grid', 12,
148
+ format(E'select %s\n from %s\n where %I = :%s::%s',
149
+ (select string_agg(quote_ident(column_name), ', ' order by ordinal)
150
+ from meta.wizard_catalog(v_detail) where kind <> 'binary' and column_name <> v_a
151
+ and not (column_name = any (meta.wizard_sidecars(v_detail)))),
152
+ meta.wizard_qname(v_detail), v_a, v_item, (select type_sql from meta.wizard_catalog(p_table) where column_name = v_pk)),
153
+ meta.wizard_qname(v_detail), v_dpk,
154
+ jsonb_build_object('page_size', 10, 'columns', meta.wizard_grid_columns(v_detail, v_a),
155
+ 'master', jsonb_build_object('item', v_item, 'column', v_a))
156
+ || meta.wizard_grid_labels(v_detail))
157
+ returning id into v_report;
158
+ insert into meta.process (page_id, seq, name, type, region_id) values
159
+ (v_page, 10, 'Save ' || meta.wizard_table_label(p_table), 'grid_dml', v_region),
160
+ (v_page, 20, 'Save ' || meta.wizard_table_label(v_detail), 'grid_dml', v_report);
161
+ else
162
+ -- the listing kinds, with an optional modal form page for a row
163
+ if v_fpage is not null and v_pk is null then
164
+ raise exception 'a form page needs a table with a single-column primary key (% has none)', p_table;
165
+ end if;
166
+ v_page := meta.wizard_new_page(v_app, p_page, v_label, v_app.home_page);
167
+ v_key := case when v_pk is not null then format('t.%I', v_pk) end;
168
+ if v_fpage is not null then
169
+ v_link := jsonb_build_object('page', v_fpage, 'items', jsonb_build_object(meta.wizard_item(v_fpage, v_pk), '#' || v_pk || '#'));
170
+ end if;
171
+
172
+ if p_kind = 'cards' then
173
+ v_a := meta.wizard_col(p_table, o ->> 'title', 'title column');
174
+ v_b := meta.wizard_col(p_table, o ->> 'subtitle', 'subtitle column');
175
+ v_c := meta.wizard_col(p_table, o ->> 'body', 'body column');
176
+ v_d := meta.wizard_col(p_table, o ->> 'badge', 'badge column');
177
+ if v_a is null then
178
+ raise exception 'choose the column for the card titles';
179
+ end if;
180
+ v_src := concat_ws(E',\n ',
181
+ meta.wizard_expr(p_table, v_a) || ' as title',
182
+ meta.wizard_expr(p_table, v_b) || ' as subtitle',
183
+ meta.wizard_expr(p_table, v_c) || ' as body',
184
+ meta.wizard_expr(p_table, v_d) || ' as badge',
185
+ v_key);
186
+ insert into meta.region (page_id, seq, title, type, source, config)
187
+ values (v_page, 10, v_label, 'cards', format(E'select %s\n from %s t\n order by 1', v_src, meta.wizard_qname(p_table)),
188
+ jsonb_strip_nulls(jsonb_build_object('link', v_link)))
189
+ returning id into v_region;
190
+
191
+ elsif p_kind = 'calendar' then
192
+ v_a := meta.wizard_col(p_table, o ->> 'start', 'start column');
193
+ v_b := meta.wizard_col(p_table, o ->> 'end', 'end column');
194
+ v_c := meta.wizard_col(p_table, o ->> 'title', 'title column');
195
+ if v_a is null or (select kind from meta.wizard_catalog(p_table) where column_name = v_a) not in ('date', 'timestamp') then
196
+ raise exception 'a calendar needs a date or timestamp start column';
197
+ end if;
198
+ if v_b is not null and (select kind from meta.wizard_catalog(p_table) where column_name = v_b) not in ('date', 'timestamp') then
199
+ raise exception 'the end column must be a date or timestamp';
200
+ end if;
201
+ v_src := concat_ws(E',\n ',
202
+ format('t.%I as start_date', v_a),
203
+ case when v_b is not null then format('t.%I as end_date', v_b) end,
204
+ coalesce(meta.wizard_expr(p_table, v_c), quote_literal(v_label)) || ' as title',
205
+ v_key);
206
+ v_cfg := jsonb_strip_nulls(jsonb_build_object('link', v_link, 'key', v_pk));
207
+ if v_fpage is not null then
208
+ v_cfg := v_cfg || jsonb_build_object('create', jsonb_build_object('page', v_fpage, 'items',
209
+ jsonb_strip_nulls(jsonb_build_object(meta.wizard_item(v_fpage, v_a), '#start#')
210
+ || case when v_b is not null then jsonb_build_object(meta.wizard_item(v_fpage, v_b), '#end#') else '{}' end)));
211
+ end if;
212
+ if coalesce((o ->> 'drag')::boolean, false) then
213
+ if v_pk is null then
214
+ raise exception 'drag and drop needs a table with a single-column primary key (% has none)', p_table;
215
+ end if;
216
+ v_cfg := v_cfg || jsonb_build_object('move', format('update %s set %I = :NEW_START::%s%s where %I = :EVENT_ID::%s',
217
+ meta.wizard_qname(p_table), v_a, (select type_sql from meta.wizard_catalog(p_table) where column_name = v_a),
218
+ case when v_b is not null then format(', %I = :NEW_END::%s', v_b, (select type_sql from meta.wizard_catalog(p_table) where column_name = v_b)) else '' end,
219
+ v_pk, (select type_sql from meta.wizard_catalog(p_table) where column_name = v_pk)));
220
+ end if;
221
+ insert into meta.region (page_id, seq, title, type, source, config)
222
+ values (v_page, 10, v_label, 'calendar', format(E'select %s\n from %s t', v_src, meta.wizard_qname(p_table)), v_cfg)
223
+ returning id into v_region;
224
+
225
+ elsif p_kind = 'chart' then
226
+ v_a := meta.wizard_col(p_table, o ->> 'label_column', 'label column');
227
+ v_b := meta.wizard_col(p_table, o ->> 'value_column', 'value column');
228
+ v_kind := coalesce(nullif(o ->> 'chart', ''), 'bar');
229
+ v_c := lower(coalesce(nullif(o ->> 'function', ''), 'count'));
230
+ if v_a is null then
231
+ raise exception 'choose the label column of the chart';
232
+ end if;
233
+ if v_kind not in ('bar', 'column', 'line', 'area', 'donut', 'pie', 'funnel') then
234
+ raise exception 'unknown chart type "%"', v_kind;
235
+ end if;
236
+ if v_c not in ('count', 'sum', 'avg', 'min', 'max') then
237
+ raise exception 'unknown function "%"', v_c;
238
+ end if;
239
+ if v_c <> 'count' and (v_b is null or (select kind from meta.wizard_catalog(p_table) where column_name = v_b) <> 'number') then
240
+ raise exception 'the function % needs a number column', v_c;
241
+ end if;
242
+ v_d := case when v_c = 'count' and v_b is null then 'Count'
243
+ else initcap(case v_c when 'avg' then 'average' when 'min' then 'minimum' when 'max' then 'maximum' else v_c end)
244
+ || ' of ' || meta.wizard_column_label(p_table, v_b) end;
245
+ -- the label as a derived column, so a foreign key's display value can be grouped;
246
+ -- (097) rows without a value are labelled "(none)" instead of an empty bar
247
+ v_src := meta.wizard_expr(p_table, v_a);
248
+ if exists (select 1 from meta.wizard_catalog(p_table) where column_name = v_a and (kind = 'text' or fk_table is not null) and not (not_null and fk_table is null)) then
249
+ v_src := format('coalesce((%s)::text, %L)', v_src, '(none)');
250
+ end if;
251
+ insert into meta.region (page_id, seq, title, type, source, config)
252
+ values (v_page, 10, v_label, 'chart',
253
+ format(E'select t.%I,\n %s as %I\n from (select %s as %I%s from %s t) t\n group by 1\n order by %s',
254
+ meta.wizard_column_label(p_table, v_a),
255
+ case when v_b is null then 'count(*)' else format('%s(t.%I)', v_c, v_b) end, v_d,
256
+ v_src, meta.wizard_column_label(p_table, v_a),
257
+ case when v_b is null then '' else format(', t.%I', v_b) end,
258
+ meta.wizard_qname(p_table), case when v_kind in ('bar', 'donut', 'pie', 'funnel') then '2 desc' else '1' end),
259
+ jsonb_build_object('kind', v_kind) || case when v_kind in ('bar', 'donut', 'pie', 'funnel') then '{"top": 10}'::jsonb else '{}' end)
260
+ returning id into v_region;
261
+
262
+ elsif p_kind = 'map' then
263
+ v_a := meta.wizard_col(p_table, o ->> 'lat', 'latitude column');
264
+ v_b := meta.wizard_col(p_table, o ->> 'lng', 'longitude column');
265
+ v_c := meta.wizard_col(p_table, o ->> 'location', 'location column');
266
+ if v_c is not null then
267
+ v_kind := (select kind from meta.wizard_catalog(p_table) where column_name = v_c);
268
+ v_src := case v_kind
269
+ when 'geometry' then format('st_y(st_pointonsurface(t.%1$I::geometry)) as lat, st_x(st_pointonsurface(t.%1$I::geometry)) as lng', v_c)
270
+ when 'point' then format('t.%1$I[1] as lat, t.%1$I[0] as lng', v_c)
271
+ when 'text' then format('t.%I as location', v_c)
272
+ else null end;
273
+ -- lines and areas are drawn as shapes too
274
+ if v_kind = 'geometry' then
275
+ v_geo := format('case when geometrytype(t.%1$I::geometry) <> ''POINT'' then st_asgeojson(t.%1$I::geometry) end as geojson', v_c);
276
+ end if;
277
+ if v_src is null then
278
+ raise exception 'the location column must hold "latitude,longitude" text, a point or a PostGIS geometry';
279
+ end if;
280
+ v_d := format('t.%I is not null', v_c);
281
+ elsif v_a is not null and v_b is not null then
282
+ if (select count(*) from meta.wizard_catalog(p_table) where column_name in (v_a, v_b) and kind = 'number') <> 2 then
283
+ raise exception 'latitude and longitude must be number columns';
284
+ end if;
285
+ v_src := format('t.%I as lat, t.%I as lng', v_a, v_b);
286
+ v_d := format('t.%I is not null and t.%I is not null', v_a, v_b);
287
+ else
288
+ raise exception 'a map needs latitude and longitude columns, or a location, point or geometry column';
289
+ end if;
290
+ v_cols := concat_ws(E',\n ', v_src, v_geo,
291
+ meta.wizard_expr(p_table, meta.wizard_col(p_table, o ->> 'title', 'title column')) || ' as title',
292
+ meta.wizard_expr(p_table, meta.wizard_col(p_table, o ->> 'body', 'body column')) || ' as body',
293
+ v_key);
294
+ insert into meta.region (page_id, seq, title, type, source, config)
295
+ values (v_page, 10, v_label, 'map', format(E'select %s\n from %s t\n where %s', v_cols, meta.wizard_qname(p_table), v_d),
296
+ jsonb_strip_nulls(jsonb_build_object('link', v_link)))
297
+ returning id into v_region;
298
+ if coalesce((o ->> 'report')::boolean, true) then
299
+ -- a report of the same rows, filtered by the map area ("Show this area in the list")
300
+ insert into meta.region (page_id, seq, title, type, source, config)
301
+ values (v_page, 20, v_label || ' list', 'report',
302
+ format(E'select %s,\n %s\n from %s t',
303
+ (select string_agg(meta.wizard_expr(p_table, column_name) || ' as ' || quote_ident(column_name), ', ' order by ordinal)
304
+ from meta.wizard_catalog(p_table)
305
+ where kind not in ('binary', 'geometry', 'point') and column_name not in ('lat', 'lng', 'location')
306
+ and not (column_name = any (v_skip))),
307
+ v_src, meta.wizard_qname(p_table)),
308
+ jsonb_strip_nulls(jsonb_build_object('link', v_link || jsonb_build_object('column', v_pk),
309
+ 'headings', (select jsonb_object_agg(column_name, meta.wizard_column_label(p_table, column_name)) from meta.wizard_catalog(p_table)),
310
+ 'formats', nullif(meta.wizard_grid_labels(p_table) -> 'formats', '{}'))))
311
+ returning id into v_report;
312
+ update meta.region set config = config || jsonb_build_object('report', v_report) where id = v_region;
313
+ end if;
314
+
315
+ elsif p_kind = 'facets' then
316
+ perform meta.wizard_col(p_table, x, 'report column') from jsonb_array_elements_text(coalesce(o -> 'columns', '[]')) x;
317
+ perform meta.wizard_col(p_table, x, 'facet column') from jsonb_array_elements_text(coalesce(o -> 'facets', '[]')) x;
318
+ -- the report: the chosen columns, and the parent's display column next to each foreign key
319
+ v_cols := null;
320
+ for c in
321
+ select w.* from meta.wizard_catalog(p_table) w
322
+ where w.column_name in (select x from jsonb_array_elements_text(coalesce(o -> 'columns', '[]')) x)
323
+ or w.is_pk
324
+ or w.column_name in (select x from jsonb_array_elements_text(coalesce(o -> 'facets', '[]')) x)
325
+ order by w.ordinal
326
+ loop
327
+ continue when c.kind in ('binary', 'geometry');
328
+ v_cols := concat_ws(E',\n ', v_cols, format('t.%I', c.column_name));
329
+ if c.fk_display is not null and c.fk_display <> c.fk_column and not c.is_pk then
330
+ -- (100) one column per foreign key: the parent's display value; the key itself is hidden (still filterable)
331
+ v_cols := concat_ws(E',\n ', v_cols, meta.wizard_expr(p_table, c.column_name) || ' as ' || quote_ident(c.column_name || '_' || c.fk_display));
332
+ v_heads := v_heads || jsonb_build_object(c.column_name || '_' || c.fk_display, meta.wizard_column_label(p_table, c.column_name));
333
+ v_hidden := v_hidden || to_jsonb(c.column_name);
334
+ else
335
+ v_heads := v_heads || jsonb_build_object(c.column_name, meta.wizard_column_label(p_table, c.column_name));
336
+ v_mask := meta.wizard_mask(c.type_sql, c.kind, c.is_pk or c.fk_table is not null);
337
+ if v_mask is not null then
338
+ v_fmts := v_fmts || jsonb_build_object(c.column_name, v_mask);
339
+ end if;
340
+ end if;
341
+ end loop;
342
+ if v_cols is null then
343
+ raise exception 'choose at least one report column';
344
+ end if;
345
+ for c in
346
+ select w.* from jsonb_array_elements_text(coalesce(o -> 'facets', '[]')) with ordinality x(col, n)
347
+ join meta.wizard_catalog(p_table) w on w.column_name = x.col
348
+ order by x.n
349
+ loop
350
+ continue when c.kind in ('binary', 'geometry', 'point', 'other');
351
+ v_facets := v_facets || jsonb_build_array(jsonb_strip_nulls(jsonb_build_object(
352
+ 'column', case when c.fk_display is not null and c.fk_display <> c.fk_column then c.column_name || '_' || c.fk_display else c.column_name end,
353
+ 'label', meta.wizard_column_label(p_table, c.column_name),
354
+ 'type', case when c.kind in ('number', 'date', 'timestamp') and c.fk_table is null then 'range' end,
355
+ 'custom', case when c.kind in ('number', 'date', 'timestamp') and c.fk_table is null then true end)));
356
+ end loop;
357
+ insert into meta.region (page_id, seq, title, type, columns, source, config)
358
+ values (v_page, 20, v_label, 'report', 9, format(E'select %s\n from %s t', v_cols, meta.wizard_qname(p_table)),
359
+ jsonb_strip_nulls(jsonb_build_object('link', v_link || jsonb_build_object('column', v_pk, 'icon', true), 'headings', v_heads,
360
+ 'hidden', nullif(v_hidden, '[]'), 'formats', nullif(v_fmts, '{}'))))
361
+ returning id into v_region;
362
+ insert into meta.region (page_id, seq, title, type, columns, template, config)
363
+ values (v_page, 10, 'Filters', 'facets', 3, 'collapsible',
364
+ jsonb_build_object('report', v_region, 'search', coalesce((o ->> 'search')::boolean, true), 'facets', v_facets));
365
+ end if;
366
+
367
+ if v_fpage is not null then
368
+ perform meta.wizard_form(v_app, p_table, v_fpage, v_flabel, p_page, true, null);
369
+ insert into meta.button (page_id, region_id, seq, name, label, action, target_page, hot)
370
+ values (v_page, v_region, 10, 'CREATE', 'Create', 'redirect', v_fpage, true);
371
+ end if;
372
+ end if;
373
+
374
+ if coalesce((o ->> 'nav')::boolean, false) then
375
+ perform meta.wizard_nav(v_app, v_label, v_icon, p_page);
376
+ end if;
377
+ return v_page;
378
+ end
379
+ $$;
380
+
381
+ revoke all on function meta.wizard_grid_labels(regclass) from public;
@@ -0,0 +1,15 @@
1
+ -- =====================================================================
2
+ -- 101: an application opens in its own light or dark mode until the user
3
+ -- chooses (sprint 79; parity handoff UI-2)
4
+ --
5
+ -- meta.account.theme_pref was 'auto' unless the user chose: at sign-in that
6
+ -- choice replaced the application's mode, so an Iris application (light)
7
+ -- followed the operating system's dark mode for every signed-in user. Null
8
+ -- now means "no choice": the application's mode (meta.app.theme.mode).
9
+ -- The 'auto' that every account had is cleared; a user who chose
10
+ -- Automatic chooses it again from the Appearance menu.
11
+ -- =====================================================================
12
+ alter table meta.account alter column theme_pref drop not null;
13
+ alter table meta.account alter column theme_pref set default null;
14
+ update meta.account set theme_pref = null where theme_pref = 'auto';
15
+ comment on column meta.account.theme_pref is 'The user''s light/dark choice (auto, light, dark); null: the application''s mode (101).';