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.
- package/db/migrations/100_generated_labels.sql +381 -0
- package/db/migrations/101_theme_choice.sql +15 -0
- package/db/migrations/102_app_computations.sql +345 -0
- package/db/migrations/103_shortcuts.sql +341 -0
- package/db/migrations/104_report_queries.sql +413 -0
- package/db/migrations/105_component_settings_groups.sql +367 -0
- package/docs/apex-feature-parity.md +12 -12
- package/docs/guide/02-concepts.md +24 -0
- package/docs/guide/03-builder.md +48 -9
- package/docs/guide/04-pages-and-regions.md +20 -4
- package/docs/guide/06-processing.md +22 -0
- package/docs/guide/09-reference.md +14 -0
- package/docs/guide/12-development.md +2 -1
- package/docs/guide/14-globalization.md +5 -1
- package/docs/guide/16-files.md +23 -0
- package/docs/guide/18-cli.md +4 -0
- package/package.json +1 -1
- package/public/app.css +40 -1
- package/public/app.js +69 -1
- package/src/appfiles.ts +18 -2
- package/src/builder/advisor.ts +7 -2
- package/src/builder/appsheets.ts +53 -13
- package/src/builder/appwizard.ts +5 -3
- package/src/builder/code-editor.ts +6 -1
- package/src/builder/components.ts +85 -3
- package/src/builder/forms.ts +1 -1
- package/src/builder/shared.ts +39 -6
- package/src/builder/subscriptions.ts +30 -4
- package/src/cli/replace.ts +6 -2
- package/src/i18n/ar.ts +2 -0
- package/src/i18n/cs.ts +2 -0
- package/src/i18n/da.ts +2 -0
- package/src/i18n/de.ts +2 -0
- package/src/i18n/el.ts +2 -0
- package/src/i18n/es.ts +2 -0
- package/src/i18n/fi.ts +2 -0
- package/src/i18n/fr.ts +2 -0
- package/src/i18n/he.ts +2 -0
- package/src/i18n/it.ts +2 -0
- package/src/i18n/ja.ts +2 -0
- package/src/i18n/ko.ts +2 -0
- package/src/i18n/nb.ts +2 -0
- package/src/i18n/pl.ts +2 -0
- package/src/i18n/pt.ts +2 -0
- package/src/i18n/ru.ts +2 -0
- package/src/i18n/sv.ts +2 -0
- package/src/i18n/tr.ts +2 -0
- package/src/i18n/uk.ts +2 -0
- package/src/i18n/zh.ts +2 -0
- package/src/i18n.ts +4 -0
- package/src/metadata.ts +14 -1
- package/src/runtime/account.ts +5 -5
- package/src/runtime/component-settings.ts +42 -0
- package/src/runtime/context.ts +4 -0
- package/src/runtime/devbar.ts +16 -13
- package/src/runtime/documents.ts +29 -3
- package/src/runtime/engine.ts +2 -2
- package/src/runtime/facets.ts +34 -3
- package/src/runtime/grid.ts +22 -4
- package/src/runtime/items.ts +14 -11
- package/src/runtime/locale.ts +4 -1
- package/src/runtime/logic.ts +12 -7
- package/src/runtime/regions.ts +4 -3
- package/src/runtime/render.ts +8 -2
- package/src/runtime/report.ts +16 -10
- package/src/runtime/routes.ts +9 -6
- package/src/runtime/shortcuts.ts +43 -0
- package/src/security.ts +2 -1
- 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).';
|