@antelopejs/dms 0.4.3 → 0.4.4
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/CHANGELOG.md +16 -0
- package/dist/test/unit/interfaces/dms-base/form-catalog.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/form-catalog.test.js +62 -0
- package/dist/test/unit/interfaces/dms-base/form-catalog.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/period-selector-catalog.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/period-selector-catalog.test.js +36 -0
- package/dist/test/unit/interfaces/dms-base/period-selector-catalog.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/resource-form.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/resource-form.test.js +117 -0
- package/dist/test/unit/interfaces/dms-base/resource-form.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/table-and-card-catalog.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/table-and-card-catalog.test.js +62 -0
- package/dist/test/unit/interfaces/dms-base/table-and-card-catalog.test.js.map +1 -0
- package/frontend-vue/layers/dms-ui/app/components/Placeholder.vue +17 -5
- package/frontend-vue/layers/dms-ui/app/components/chart/ChartCard.vue +14 -9
- package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +8 -5
- package/frontend-vue/layers/dms-ui/app/components/grid/Grid.vue +15 -5
- package/frontend-vue/layers/dms-ui/app/components/grid/GridRow.vue +7 -4
- package/frontend-vue/layers/dms-ui/app/components/grid/constants.ts +8 -1
- package/frontend-vue/layers/dms-ui/app/components/kpi/KpiCard.vue +14 -9
- package/frontend-vue/layers/dms-ui/app/composables/chart/formatValue.ts +15 -0
- package/frontend-vue/layers/dms-ui/app/composables/chart/types.ts +4 -2
- package/frontend-vue/layers/dms-ui/app/composables/form/types/props.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +53 -4
- package/frontend-vue/layers/dms-ui/app/composables/tree/useTree.ts +5 -7
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +1 -0
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +1 -0
- package/frontend-vue/tests/chart-absent-value.test.ts +78 -0
- package/frontend-vue/tests/form-actions.test.ts +27 -0
- package/frontend-vue/tests/form-submit-target.test.ts +28 -0
- package/frontend-vue/tests/tree-without-source.test.ts +79 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.4.4
|
|
4
|
+
|
|
5
|
+
[compare changes](https://github.com/AntelopeJS/dms/compare/v0.4.3...v0.4.4)
|
|
6
|
+
|
|
7
|
+
### 🚀 Enhancements
|
|
8
|
+
|
|
9
|
+
- **base:** Declare where a block's data comes from, and arrange the answer ([#15](https://github.com/AntelopeJS/dms/pull/15))
|
|
10
|
+
|
|
11
|
+
### 🏡 Chore
|
|
12
|
+
|
|
13
|
+
- **release:** @antelopejs/interface-dms v0.2.3 ([726ddbb](https://github.com/AntelopeJS/dms/commit/726ddbb))
|
|
14
|
+
|
|
15
|
+
### ❤️ Contributors
|
|
16
|
+
|
|
17
|
+
- Fabrice Cst <fabrice@altab.be>
|
|
18
|
+
|
|
3
19
|
## v0.4.3
|
|
4
20
|
|
|
5
21
|
[compare changes](https://github.com/AntelopeJS/dms/compare/v0.4.2...v0.4.3)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const chai_1 = require("chai");
|
|
4
|
+
const block_types_1 = require("@antelopejs/interface-dms/base/block-types");
|
|
5
|
+
describe("[unit] interfaces/dms-base/form — its catalog entry", () => {
|
|
6
|
+
function entryBranches() {
|
|
7
|
+
const declared = (0, block_types_1.ListBlockTypes)().find((block) => block.type === "Form");
|
|
8
|
+
return declared?.config.fields?.items?.oneOf ?? [];
|
|
9
|
+
}
|
|
10
|
+
it("names both kinds of entry a form's fields can hold", () => {
|
|
11
|
+
(0, chai_1.expect)(entryBranches().map((branch) => branch.ui?.label)).to.deep.equal([
|
|
12
|
+
"Group",
|
|
13
|
+
"Field",
|
|
14
|
+
]);
|
|
15
|
+
});
|
|
16
|
+
it("says a field's key and a group's can be derived from their label", () => {
|
|
17
|
+
const [group, field] = entryBranches();
|
|
18
|
+
(0, chai_1.expect)(field?.properties?.id?.ui?.derivedFrom).to.equal("label");
|
|
19
|
+
(0, chai_1.expect)(group?.properties?.id?.ui?.derivedFrom).to.equal("label");
|
|
20
|
+
});
|
|
21
|
+
it("says a field's default takes the field's own type", () => {
|
|
22
|
+
const [, field] = entryBranches();
|
|
23
|
+
(0, chai_1.expect)(field?.properties?.defaultValue?.ui?.typedBy).to.equal("type");
|
|
24
|
+
});
|
|
25
|
+
it("is placed showing its buttons, a setting of the advanced view", () => {
|
|
26
|
+
const config = (0, block_types_1.ListBlockTypes)().find((block) => block.type === "Form")?.config;
|
|
27
|
+
(0, chai_1.expect)(config?.showActions?.ui).to.include({
|
|
28
|
+
initial: true,
|
|
29
|
+
advanced: true,
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
it("leaves its addresses and methods to the advanced view", () => {
|
|
33
|
+
const config = (0, block_types_1.ListBlockTypes)().find((block) => block.type === "Form")?.config;
|
|
34
|
+
for (const key of [
|
|
35
|
+
"fetchUrl",
|
|
36
|
+
"fetchUrlMethod",
|
|
37
|
+
"submitUrl",
|
|
38
|
+
"submitUrlMethod",
|
|
39
|
+
]) {
|
|
40
|
+
(0, chai_1.expect)(config?.[key]?.ui?.advanced, key).to.equal(true);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
it("names what a group of fields is set up with", () => {
|
|
44
|
+
const [group] = entryBranches();
|
|
45
|
+
(0, chai_1.expect)(group?.properties?.label?.ui?.label).to.equal("Title");
|
|
46
|
+
(0, chai_1.expect)(group?.properties?.fields?.ui?.label).to.equal("Fields");
|
|
47
|
+
});
|
|
48
|
+
for (const type of ["Form", "ResourceForm"]) {
|
|
49
|
+
it(`puts the submit messages of a ${type} behind one switch`, () => {
|
|
50
|
+
const config = (0, block_types_1.ListBlockTypes)().find((block) => block.type === type)?.config;
|
|
51
|
+
(0, chai_1.expect)(config?.successMessage?.ui).to.include({
|
|
52
|
+
optIn: "Custom submit messages",
|
|
53
|
+
placeholder: "Data has been successfully saved",
|
|
54
|
+
});
|
|
55
|
+
(0, chai_1.expect)(config?.errorMessage?.ui).to.include({
|
|
56
|
+
optIn: "Custom submit messages",
|
|
57
|
+
placeholder: "An unknown error occurred",
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
//# sourceMappingURL=form-catalog.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"form-catalog.test.js","sourceRoot":"","sources":["../../../../../src/test/unit/interfaces/dms-base/form-catalog.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const chai_1 = require("chai");
|
|
4
|
+
const block_types_1 = require("@antelopejs/interface-dms/base/block-types");
|
|
5
|
+
function periodSelectorConfig() {
|
|
6
|
+
const declared = (0, block_types_1.ListBlockTypes)().find((block) => block.type === "PeriodSelector");
|
|
7
|
+
(0, chai_1.expect)(declared, "PeriodSelector is declared").to.not.equal(undefined);
|
|
8
|
+
return declared.config;
|
|
9
|
+
}
|
|
10
|
+
describe("[unit] interfaces/dms-base/period-selector — its catalog entry", () => {
|
|
11
|
+
it("names the ranges a range label can be given for", () => {
|
|
12
|
+
const labels = periodSelectorConfig().presetLabels;
|
|
13
|
+
(0, chai_1.expect)(labels?.type).to.equal("record");
|
|
14
|
+
(0, chai_1.expect)(labels?.ui?.widget, "no longer a JSON box").to.equal(undefined);
|
|
15
|
+
(0, chai_1.expect)(labels?.keys?.enum).to.include("last-30-days");
|
|
16
|
+
(0, chai_1.expect)(labels?.keys?.ui?.valueLabels?.["ytd"]).to.equal("Year to date");
|
|
17
|
+
(0, chai_1.expect)(labels?.values?.type).to.equal("string");
|
|
18
|
+
});
|
|
19
|
+
it("names the comparisons a comparison label can be given for", () => {
|
|
20
|
+
const labels = periodSelectorConfig().comparisonLabels;
|
|
21
|
+
(0, chai_1.expect)(labels?.keys?.enum).to.deep.equal([
|
|
22
|
+
"none",
|
|
23
|
+
"previous-period",
|
|
24
|
+
"previous-year",
|
|
25
|
+
"custom",
|
|
26
|
+
]);
|
|
27
|
+
(0, chai_1.expect)(labels?.keys?.ui?.valueLabels?.["previous-period"]).to.equal("vs previous period");
|
|
28
|
+
});
|
|
29
|
+
it("names the values of the pickers that choose a range", () => {
|
|
30
|
+
const config = periodSelectorConfig();
|
|
31
|
+
(0, chai_1.expect)(config.defaultPreset?.ui?.label, "its own hints still apply").to.equal("Default range");
|
|
32
|
+
(0, chai_1.expect)(config.defaultPreset?.ui?.valueLabels?.["last-7-days"]).to.equal("Last 7 days");
|
|
33
|
+
(0, chai_1.expect)(config.presets?.items?.ui?.valueLabels?.["today"]).to.equal("Today");
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
//# sourceMappingURL=period-selector-catalog.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"period-selector-catalog.test.js","sourceRoot":"","sources":["../../../../../src/test/unit/interfaces/dms-base/period-selector-catalog.test.ts"],"names":[],"mappings":";;AAAA,+BAA8B;AAC9B,4EAA4E;AAE5E,SAAS,oBAAoB;IAC3B,MAAM,QAAQ,GAAG,IAAA,4BAAc,GAAE,CAAC,IAAI,CACpC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,gBAAgB,CAC3C,CAAC;IACF,IAAA,aAAM,EAAC,QAAQ,EAAE,4BAA4B,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IACvE,OAAO,QAAS,CAAC,MAAM,CAAC;AAC1B,CAAC;AAOD,QAAQ,CAAC,gEAAgE,EAAE,GAAG,EAAE;IAC9E,EAAE,CAAC,iDAAiD,EAAE,GAAG,EAAE;QACzD,MAAM,MAAM,GAAG,oBAAoB,EAAE,CAAC,YAAY,CAAC;QAEnD,IAAA,aAAM,EAAC,MAAM,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QACxC,IAAA,aAAM,EAAC,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,sBAAsB,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QACvE,IAAA,aAAM,EAAC,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;QACtD,IAAA,aAAM,EAAC,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;QACxE,IAAA,aAAM,EAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2DAA2D,EAAE,GAAG,EAAE;QACnE,MAAM,MAAM,GAAG,oBAAoB,EAAE,CAAC,gBAAgB,CAAC;QAEvD,IAAA,aAAM,EAAC,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC;YACvC,MAAM;YACN,iBAAiB;YACjB,eAAe;YACf,QAAQ;SACT,CAAC,CAAC;QACH,IAAA,aAAM,EAAC,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE,WAAW,EAAE,CAAC,iBAAiB,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CACjE,oBAAoB,CACrB,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qDAAqD,EAAE,GAAG,EAAE;QAC7D,MAAM,MAAM,GAAG,oBAAoB,EAAE,CAAC;QAEtC,IAAA,aAAM,EACJ,MAAM,CAAC,aAAa,EAAE,EAAE,EAAE,KAAK,EAC/B,2BAA2B,CAC5B,CAAC,EAAE,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;QAC5B,IAAA,aAAM,EAAC,MAAM,CAAC,aAAa,EAAE,EAAE,EAAE,WAAW,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CACrE,aAAa,CACd,CAAC;QACF,IAAA,aAAM,EAAC,MAAM,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,WAAW,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAC9E,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
const interface_api_1 = require("@antelopejs/interface-api");
|
|
13
|
+
const interface_data_api_1 = require("@antelopejs/interface-data-api");
|
|
14
|
+
const metadata_1 = require("@antelopejs/interface-data-api/metadata");
|
|
15
|
+
const interface_database_decorators_1 = require("@antelopejs/interface-database-decorators");
|
|
16
|
+
const chai_1 = require("chai");
|
|
17
|
+
const block_types_1 = require("@antelopejs/interface-dms/base/block-types");
|
|
18
|
+
const default_types_1 = require("@antelopejs/interface-dms/base/data-types/default-types");
|
|
19
|
+
const factory_1 = require("@antelopejs/interface-dms/base/table-view/factory");
|
|
20
|
+
const meta_1 = require("@antelopejs/interface-dms/base/table-view/meta");
|
|
21
|
+
const resource_form_1 = require("@antelopejs/interface-dms/base/table-view/resource-form");
|
|
22
|
+
const routes_1 = require("@antelopejs/interface-dms/base/table-view/routes");
|
|
23
|
+
const http_1 = require("@antelopejs/interface-dms/base/types/http");
|
|
24
|
+
const SCHEMA = "resource-form-test";
|
|
25
|
+
const TABLE = "tickets";
|
|
26
|
+
const LOCATION = "/resource-form-test";
|
|
27
|
+
let Ticket = class Ticket extends interface_database_decorators_1.Table {
|
|
28
|
+
};
|
|
29
|
+
__decorate([
|
|
30
|
+
(0, interface_database_decorators_1.Field)("string"),
|
|
31
|
+
__metadata("design:type", String)
|
|
32
|
+
], Ticket.prototype, "title", void 0);
|
|
33
|
+
__decorate([
|
|
34
|
+
(0, interface_database_decorators_1.Field)("string"),
|
|
35
|
+
__metadata("design:type", String)
|
|
36
|
+
], Ticket.prototype, "reference", void 0);
|
|
37
|
+
Ticket = __decorate([
|
|
38
|
+
(0, interface_database_decorators_1.RegisterTable)(TABLE, SCHEMA)
|
|
39
|
+
], Ticket);
|
|
40
|
+
class TicketModel extends (0, interface_database_decorators_1.BasicDataModel)(Ticket, TABLE) {
|
|
41
|
+
}
|
|
42
|
+
let TicketAPI = class TicketAPI extends (0, interface_data_api_1.DataController)(Ticket, routes_1.TableViewRoutes.All, (0, interface_api_1.Controller)(LOCATION)) {
|
|
43
|
+
};
|
|
44
|
+
__decorate([
|
|
45
|
+
(0, metadata_1.ModelReference)(),
|
|
46
|
+
(0, interface_database_decorators_1.Model)(TicketModel),
|
|
47
|
+
__metadata("design:type", TicketModel)
|
|
48
|
+
], TicketAPI.prototype, "model", void 0);
|
|
49
|
+
__decorate([
|
|
50
|
+
(0, meta_1.Column)({ name: "Title", type: new default_types_1.DefaultDataTypes.StringType() }),
|
|
51
|
+
(0, metadata_1.Access)(metadata_1.AccessMode.ReadWrite),
|
|
52
|
+
__metadata("design:type", String)
|
|
53
|
+
], TicketAPI.prototype, "title", void 0);
|
|
54
|
+
__decorate([
|
|
55
|
+
(0, meta_1.Column)({ name: "Reference", type: new default_types_1.DefaultDataTypes.StringType() }),
|
|
56
|
+
(0, metadata_1.Access)(metadata_1.AccessMode.ReadOnly),
|
|
57
|
+
__metadata("design:type", String)
|
|
58
|
+
], TicketAPI.prototype, "reference", void 0);
|
|
59
|
+
TicketAPI = __decorate([
|
|
60
|
+
(0, interface_data_api_1.RegisterDataController)()
|
|
61
|
+
], TicketAPI);
|
|
62
|
+
function optionsOf(builder) {
|
|
63
|
+
return builder.serializeSync().options;
|
|
64
|
+
}
|
|
65
|
+
function fieldIds(options) {
|
|
66
|
+
return options.fields.map((field) => field.id);
|
|
67
|
+
}
|
|
68
|
+
describe("[unit] interfaces/dms-base/resource-form — a form derived from its table", () => {
|
|
69
|
+
it("is the form a TableView opens for the mode", () => {
|
|
70
|
+
const table = (0, factory_1.TableView)(TicketAPI).serializeSync()
|
|
71
|
+
.options;
|
|
72
|
+
const created = optionsOf((0, resource_form_1.ResourceForm)(TicketAPI, { mode: "new" }));
|
|
73
|
+
(0, chai_1.expect)(created.submitUrl).to.equal(`${LOCATION}/new`);
|
|
74
|
+
(0, chai_1.expect)(created.submitUrlMethod).to.equal(http_1.HttpMethod.post);
|
|
75
|
+
const opened = table.formComponents.new?.options;
|
|
76
|
+
(0, chai_1.expect)(created.fetchUrl).to.equal(opened.fetchUrl);
|
|
77
|
+
(0, chai_1.expect)(fieldIds(created)).to.deep.equal(fieldIds(opened));
|
|
78
|
+
});
|
|
79
|
+
it("opens the row the page's query string names, in edit and view", () => {
|
|
80
|
+
const edited = optionsOf((0, resource_form_1.ResourceForm)(TicketAPI, { mode: "edit" }));
|
|
81
|
+
const shown = optionsOf((0, resource_form_1.ResourceForm)(TicketAPI, { mode: "view" }));
|
|
82
|
+
(0, chai_1.expect)(edited.fetchUrl).to.match(new RegExp(`^${LOCATION}/get\\?id=\\{\\{query\\.id\\}\\}&`));
|
|
83
|
+
(0, chai_1.expect)(edited.submitUrl).to.equal(`${LOCATION}/edit?id={{query.id}}`);
|
|
84
|
+
(0, chai_1.expect)(edited.submitUrlMethod).to.equal(http_1.HttpMethod.put);
|
|
85
|
+
(0, chai_1.expect)(shown.fetchUrl).to.equal(`${LOCATION}/get?id={{query.id}}`);
|
|
86
|
+
});
|
|
87
|
+
it("keeps the TableView's own pages on their :id segment", () => {
|
|
88
|
+
const table = (0, factory_1.TableView)(TicketAPI).serializeSync()
|
|
89
|
+
.options;
|
|
90
|
+
const opened = table.formComponents.edit?.options;
|
|
91
|
+
(0, chai_1.expect)(opened.submitUrl).to.equal(`${LOCATION}/edit?id={{params.id}}`);
|
|
92
|
+
});
|
|
93
|
+
it("reads a row without offering anywhere to send it", () => {
|
|
94
|
+
const shown = optionsOf((0, resource_form_1.ResourceForm)(TicketAPI, { mode: "view" }));
|
|
95
|
+
(0, chai_1.expect)(shown.submitUrl).to.equal(undefined);
|
|
96
|
+
(0, chai_1.expect)(shown.fields.every((field) => field.disabled)).to.equal(true);
|
|
97
|
+
});
|
|
98
|
+
it("carries its own wording and the row it was pointed at", () => {
|
|
99
|
+
const edited = optionsOf((0, resource_form_1.ResourceForm)(TicketAPI, {
|
|
100
|
+
mode: "edit",
|
|
101
|
+
title: "Edit the ticket",
|
|
102
|
+
submitLabel: "Save the ticket",
|
|
103
|
+
rowId: "{{params.ticket}}",
|
|
104
|
+
}));
|
|
105
|
+
(0, chai_1.expect)(edited.title).to.equal("Edit the ticket");
|
|
106
|
+
(0, chai_1.expect)(edited.submitLabel).to.equal("Save the ticket");
|
|
107
|
+
(0, chai_1.expect)(edited.submitUrl).to.equal(`${LOCATION}/edit?id={{params.ticket}}`);
|
|
108
|
+
(0, chai_1.expect)(edited, "the row token is the form's to resolve, not an option").to.not.have.property("rowId");
|
|
109
|
+
});
|
|
110
|
+
it("is declared as a block over a table, picked by mode", () => {
|
|
111
|
+
const declared = (0, block_types_1.ListBlockTypes)().find((block) => block.type === "ResourceForm");
|
|
112
|
+
(0, chai_1.expect)(declared?.controllerArg).to.equal(true);
|
|
113
|
+
(0, chai_1.expect)(declared?.config.mode?.enum).to.deep.equal(["new", "edit", "view"]);
|
|
114
|
+
(0, chai_1.expect)(declared?.config.fields, "fields come from the table").to.equal(undefined);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
//# sourceMappingURL=resource-form.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resource-form.test.js","sourceRoot":"","sources":["../../../../../src/test/unit/interfaces/dms-base/resource-form.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const chai_1 = require("chai");
|
|
4
|
+
const block_types_1 = require("@antelopejs/interface-dms/base/block-types");
|
|
5
|
+
function configOf(type) {
|
|
6
|
+
const declared = (0, block_types_1.ListBlockTypes)().find((block) => block.type === type);
|
|
7
|
+
(0, chai_1.expect)(declared, `${type} is declared`).to.not.equal(undefined);
|
|
8
|
+
return declared.config;
|
|
9
|
+
}
|
|
10
|
+
describe("[unit] interfaces/dms-base/table-view — its row actions in the catalog", () => {
|
|
11
|
+
const actions = () => configOf("TableView").rowActions?.properties ?? {};
|
|
12
|
+
it("says the actions a table offers by itself are on", () => {
|
|
13
|
+
for (const action of ["add", "edit", "delete", "duplicate", "copyLink"]) {
|
|
14
|
+
(0, chai_1.expect)(actions()[action]?.default, action).to.equal(true);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
it("says when an action depends on another one", () => {
|
|
18
|
+
(0, chai_1.expect)(actions().details?.default).to.equal(undefined);
|
|
19
|
+
(0, chai_1.expect)(actions().details?.description).to.match(/editing is off/);
|
|
20
|
+
(0, chai_1.expect)(actions().archive?.default).to.equal(true);
|
|
21
|
+
(0, chai_1.expect)(actions().archive?.description).to.match(/ghost delete/);
|
|
22
|
+
});
|
|
23
|
+
it("leaves row selection off until it is turned on", () => {
|
|
24
|
+
(0, chai_1.expect)(actions().hasSelection?.default).to.equal(undefined);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
describe("[unit] interfaces/dms-base/chart-card — the chart it wraps, in the catalog", () => {
|
|
28
|
+
it("names the options of its chart it supplies itself", () => {
|
|
29
|
+
const supplied = configOf("ChartCard").chart?.ui?.supplies;
|
|
30
|
+
(0, chai_1.expect)(supplied).to.include.members([
|
|
31
|
+
"fetchUrl",
|
|
32
|
+
"fetchUrlMethod",
|
|
33
|
+
"periodScope",
|
|
34
|
+
"title",
|
|
35
|
+
]);
|
|
36
|
+
});
|
|
37
|
+
it("keeps a chart's backend-only settings for the advanced view", () => {
|
|
38
|
+
const chart = configOf("ChartLine");
|
|
39
|
+
(0, chai_1.expect)(chart.realtimeTopic?.ui?.advanced).to.equal(true);
|
|
40
|
+
(0, chai_1.expect)(chart.syncGroup?.ui?.advanced).to.equal(true);
|
|
41
|
+
(0, chai_1.expect)(chart.annotations?.ui?.advanced).to.equal(true);
|
|
42
|
+
(0, chai_1.expect)(chart.showGrid?.ui?.advanced, "a setting anyone reads").to.equal(undefined);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
describe("[unit] interfaces/dms-base/chart-card — the switches it draws on", () => {
|
|
46
|
+
it("says a card shows its variation and its legend", () => {
|
|
47
|
+
const card = configOf("ChartCard");
|
|
48
|
+
(0, chai_1.expect)(card.showDelta?.default).to.equal(true);
|
|
49
|
+
(0, chai_1.expect)(card.showLegend?.default).to.equal(true);
|
|
50
|
+
});
|
|
51
|
+
it("says a chart shows what it draws unless told not to", () => {
|
|
52
|
+
const chart = configOf("ChartColumn");
|
|
53
|
+
for (const option of ["showTooltip", "showGrid", "roundedCorners"]) {
|
|
54
|
+
(0, chai_1.expect)(chart[option]?.default, option).to.equal(true);
|
|
55
|
+
}
|
|
56
|
+
(0, chai_1.expect)(configOf("ChartLine").smooth?.default).to.equal(true);
|
|
57
|
+
});
|
|
58
|
+
it("leaves what it only does when asked unset", () => {
|
|
59
|
+
(0, chai_1.expect)(configOf("ChartColumn").stacked?.default).to.equal(undefined);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
//# sourceMappingURL=table-and-card-catalog.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"table-and-card-catalog.test.js","sourceRoot":"","sources":["../../../../../src/test/unit/interfaces/dms-base/table-and-card-catalog.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,19 +5,31 @@ interface PlaceholderProps {
|
|
|
5
5
|
width?: string;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
/** What a box nobody sized is worth showing at, when nothing stretches it. */
|
|
9
|
+
const MIN_HEIGHT = "120px";
|
|
10
|
+
|
|
8
11
|
const props = withDefaults(defineProps<PlaceholderProps>(), {
|
|
9
12
|
label: undefined,
|
|
10
|
-
height:
|
|
13
|
+
height: undefined,
|
|
11
14
|
width: undefined,
|
|
12
15
|
});
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A placeholder stands in for content still to come, so it takes the room it is
|
|
19
|
+
* given: a height of its own would keep it short in a row sized by its tallest
|
|
20
|
+
* cell, and a layout laid out with these could not be read for what it will be.
|
|
21
|
+
* A given height still wins, and a floor keeps a lone one visible.
|
|
22
|
+
*/
|
|
23
|
+
const box = computed(() => ({
|
|
24
|
+
height: props.height,
|
|
25
|
+
minHeight: props.height ? undefined : MIN_HEIGHT,
|
|
26
|
+
width: props.width,
|
|
27
|
+
}));
|
|
13
28
|
</script>
|
|
14
29
|
|
|
15
30
|
<template>
|
|
16
31
|
<div
|
|
17
|
-
:style="
|
|
18
|
-
height: props.height,
|
|
19
|
-
width: props.width,
|
|
20
|
-
}"
|
|
32
|
+
:style="box"
|
|
21
33
|
class="border-accented relative flex items-center justify-center overflow-hidden rounded-md border border-dashed px-4 opacity-75"
|
|
22
34
|
>
|
|
23
35
|
<svg
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, provide, reactive, watchEffect } from "vue";
|
|
3
3
|
import { useChartFetch } from "../../composables/chart/useChartFetch";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
ABSENT_VALUE_TEXT,
|
|
6
|
+
formatValue,
|
|
7
|
+
} from "../../composables/chart/formatValue";
|
|
5
8
|
import { resolveChartColor } from "../../composables/chart/useChartTheme";
|
|
6
9
|
import { useThemeRevision } from "../../composables/chart/useThemeRevision";
|
|
7
10
|
import {
|
|
@@ -64,7 +67,7 @@ const { data, isLoading } = useChartFetch<ChartCardResponse>({
|
|
|
64
67
|
watchSource: () => watchKey.value,
|
|
65
68
|
});
|
|
66
69
|
|
|
67
|
-
const value = computed(() => data.value?.value
|
|
70
|
+
const value = computed(() => data.value?.value);
|
|
68
71
|
const previousValue = computed(() => data.value?.previousValue);
|
|
69
72
|
const delta = computed(() => data.value?.delta ?? null);
|
|
70
73
|
|
|
@@ -87,13 +90,15 @@ const cardComparisonSeries = computed<ChartSeries[]>(() =>
|
|
|
87
90
|
);
|
|
88
91
|
|
|
89
92
|
const formatted = computed(() =>
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
value.value === undefined
|
|
94
|
+
? ABSENT_VALUE_TEXT
|
|
95
|
+
: formatValue(
|
|
96
|
+
value.value,
|
|
97
|
+
props.valueFormat,
|
|
98
|
+
locale.value,
|
|
99
|
+
props.currencyCode,
|
|
100
|
+
props.valuePrecision,
|
|
101
|
+
),
|
|
97
102
|
);
|
|
98
103
|
|
|
99
104
|
const previousFormatted = computed(() =>
|
|
@@ -3,6 +3,7 @@ import type { FormProps } from "../../composables/form/types";
|
|
|
3
3
|
import type { FormFieldValue } from "../../composables/form/types/value";
|
|
4
4
|
import {
|
|
5
5
|
cloneFormValue,
|
|
6
|
+
formShowsActions,
|
|
6
7
|
isFieldMarkedRequired,
|
|
7
8
|
} from "../../composables/form/useForm";
|
|
8
9
|
import {
|
|
@@ -20,7 +21,12 @@ const REALTIME_ROW_TOPIC_PREFIX = "tableview:row:";
|
|
|
20
21
|
const REALTIME_GET_SEGMENT = "/get";
|
|
21
22
|
const REALTIME_EVENT_UPDATED = "updated";
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
// `showActions` left out has to read as left out: Vue casts an absent boolean
|
|
25
|
+
// prop to `false`, which would hide the buttons of every form not asking for
|
|
26
|
+
// them.
|
|
27
|
+
const props = withDefaults(defineProps<FormProps>(), {
|
|
28
|
+
showActions: undefined,
|
|
29
|
+
});
|
|
24
30
|
const form = useTemplateRef("form");
|
|
25
31
|
|
|
26
32
|
// Inside a DynamicModal/DynamicDrawer the container already provides the
|
|
@@ -47,10 +53,7 @@ const {
|
|
|
47
53
|
resolvedFetchUrl,
|
|
48
54
|
} = useForm(props);
|
|
49
55
|
|
|
50
|
-
const showActions = computed(
|
|
51
|
-
() =>
|
|
52
|
-
!!props.submitUrl && !toValue(allFields).every((field) => field.disabled),
|
|
53
|
-
);
|
|
56
|
+
const showActions = computed(() => formShowsActions(props, toValue(allFields)));
|
|
54
57
|
|
|
55
58
|
const { addGuard } = useLeaveGuard();
|
|
56
59
|
const { confirm } = useConfirm();
|
|
@@ -13,19 +13,29 @@ const props = withDefaults(defineProps<GridProps>(), {
|
|
|
13
13
|
minColumnWidth: GRID_DEFAULT_MIN_COLUMN_WIDTH,
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Keyed by row rather than appended to: a list that only grows keeps counting
|
|
18
|
+
* rows that have gone and widths that were never asked for, so the grid ends up
|
|
19
|
+
* reserving columns nothing fills.
|
|
20
|
+
*/
|
|
21
|
+
const rowColumnCounts = ref(new Map<symbol, number>());
|
|
17
22
|
|
|
18
23
|
const maxColumns = computed(() => {
|
|
19
|
-
|
|
20
|
-
|
|
24
|
+
const counts = [...rowColumnCounts.value.values()].filter(
|
|
25
|
+
(count) => count > 0,
|
|
26
|
+
);
|
|
27
|
+
return counts.length === 0 ? 1 : Math.max(...counts);
|
|
21
28
|
});
|
|
22
29
|
|
|
23
30
|
const gapRef = computed(() => props.gap);
|
|
24
31
|
const minColumnWidthRef = computed(() => props.minColumnWidth);
|
|
25
32
|
|
|
26
33
|
provide<GridContext>(GRID_CONTEXT, {
|
|
27
|
-
|
|
28
|
-
rowColumnCounts.value.
|
|
34
|
+
setRowColumnCount: (row: symbol, columnCount: number) => {
|
|
35
|
+
rowColumnCounts.value.set(row, columnCount);
|
|
36
|
+
},
|
|
37
|
+
dropRow: (row: symbol) => {
|
|
38
|
+
rowColumnCounts.value.delete(row);
|
|
29
39
|
},
|
|
30
40
|
maxColumns,
|
|
31
41
|
gap: gapRef,
|
|
@@ -11,10 +11,13 @@ const props = defineProps<GridRowProps>();
|
|
|
11
11
|
|
|
12
12
|
const gridContext = inject<GridContext>(GRID_CONTEXT);
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
/** The identity this row counts under, so its width dies with it. */
|
|
15
|
+
const rowId = Symbol("grid-row");
|
|
16
|
+
|
|
17
|
+
const columnCount = computed(() => props.childCount || 0);
|
|
18
|
+
|
|
19
|
+
watchEffect(() => gridContext?.setRowColumnCount(rowId, columnCount.value));
|
|
20
|
+
onUnmounted(() => gridContext?.dropRow(rowId));
|
|
18
21
|
|
|
19
22
|
const rowStyle = computed(() => {
|
|
20
23
|
const gap = gridContext?.gap.value ?? DEFAULT_GAP;
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
export const GRID_CONTEXT = Symbol("grid-context");
|
|
2
2
|
|
|
3
3
|
export interface GridContext {
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* A row states how many columns it holds, and says so again whenever that
|
|
6
|
+
* changes: a row built by hand never moves, but one an editor fills gains and
|
|
7
|
+
* loses children while the page is open.
|
|
8
|
+
*/
|
|
9
|
+
setRowColumnCount: (row: symbol, count: number) => void;
|
|
10
|
+
/** A row that unmounts stops counting, so a stale width cannot outlive it. */
|
|
11
|
+
dropRow: (row: symbol) => void;
|
|
5
12
|
maxColumns: ComputedRef<number>;
|
|
6
13
|
gap: ComputedRef<string>;
|
|
7
14
|
minColumnWidth: ComputedRef<string>;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed } from "vue";
|
|
3
3
|
import { useChartFetch } from "../../composables/chart/useChartFetch";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
ABSENT_VALUE_PARTS,
|
|
6
|
+
formatValueParts,
|
|
7
|
+
} from "../../composables/chart/formatValue";
|
|
5
8
|
import { resolveSparklineAccent } from "../../composables/chart/resolveSparklineAccent";
|
|
6
9
|
import type {
|
|
7
10
|
KpiCardResponse,
|
|
@@ -69,18 +72,20 @@ const { data, isLoading } = useChartFetch<KpiCardResponse>({
|
|
|
69
72
|
watchSource: () => watchKey.value,
|
|
70
73
|
});
|
|
71
74
|
|
|
72
|
-
const value = computed(() => data.value?.value
|
|
75
|
+
const value = computed(() => data.value?.value);
|
|
73
76
|
const delta = computed(() => data.value?.delta ?? null);
|
|
74
77
|
const sparkline = computed(() => data.value?.sparkline ?? []);
|
|
75
78
|
|
|
76
79
|
const formattedParts = computed(() =>
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
value.value === undefined
|
|
81
|
+
? ABSENT_VALUE_PARTS
|
|
82
|
+
: formatValueParts(
|
|
83
|
+
value.value,
|
|
84
|
+
props.valueFormat,
|
|
85
|
+
locale.value,
|
|
86
|
+
props.currencyCode,
|
|
87
|
+
props.valuePrecision,
|
|
88
|
+
),
|
|
84
89
|
);
|
|
85
90
|
|
|
86
91
|
const showTrend = computed(() => props.showDelta && delta.value !== null);
|
|
@@ -112,6 +112,21 @@ export interface FormattedValueParts {
|
|
|
112
112
|
unitIsPrefix: boolean;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
/**
|
|
116
|
+
* What a card prints where its figure would go when the query measured nothing.
|
|
117
|
+
*
|
|
118
|
+
* An em dash and not a zero: a zero is itself a measurement, so a card printing
|
|
119
|
+
* one for a period with no rows in it states a result nobody computed. No unit
|
|
120
|
+
* either — there is no quantity for it to qualify.
|
|
121
|
+
*/
|
|
122
|
+
export const ABSENT_VALUE_TEXT = "—";
|
|
123
|
+
|
|
124
|
+
export const ABSENT_VALUE_PARTS: FormattedValueParts = {
|
|
125
|
+
value: ABSENT_VALUE_TEXT,
|
|
126
|
+
unit: "",
|
|
127
|
+
unitIsPrefix: false,
|
|
128
|
+
};
|
|
129
|
+
|
|
115
130
|
/** Splits the same formatted value into numeric and unit parts for KPI cards. */
|
|
116
131
|
export function formatValueParts(
|
|
117
132
|
value: number,
|
|
@@ -82,7 +82,8 @@ export interface DonutRecord {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
export interface ChartCardResponse {
|
|
85
|
-
|
|
85
|
+
/** Absent when the query measured no group; the card shows no figure. */
|
|
86
|
+
value?: number;
|
|
86
87
|
delta?: number;
|
|
87
88
|
previousValue?: number;
|
|
88
89
|
series: ChartSeries[];
|
|
@@ -90,7 +91,8 @@ export interface ChartCardResponse {
|
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
export interface KpiCardResponse {
|
|
93
|
-
|
|
94
|
+
/** Absent when the query measured no group; the card shows no figure. */
|
|
95
|
+
value?: number;
|
|
94
96
|
delta?: number;
|
|
95
97
|
previousValue?: number;
|
|
96
98
|
sparkline?: number[];
|
|
@@ -17,6 +17,8 @@ export interface FormProps extends FormComponentProps {
|
|
|
17
17
|
submitUrl?: string;
|
|
18
18
|
submitUrlMethod?: HttpMethod;
|
|
19
19
|
submitLabel?: string;
|
|
20
|
+
/** Whether the buttons show; left out, once there is somewhere to submit to. */
|
|
21
|
+
showActions?: boolean;
|
|
20
22
|
successMessage?: string;
|
|
21
23
|
errorMessage?: string;
|
|
22
24
|
schema?: Record<string, unknown>;
|
|
@@ -28,6 +28,23 @@ export function cloneFormValue(
|
|
|
28
28
|
return JSON.parse(JSON.stringify(value));
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Whether a form shows its reset and submit buttons.
|
|
33
|
+
*
|
|
34
|
+
* Asked for, they show whatever else holds — a form placed in the builder shows
|
|
35
|
+
* them before it has somewhere to submit to. Left to the form, they show once
|
|
36
|
+
* it has an address and a field someone can fill in.
|
|
37
|
+
*/
|
|
38
|
+
export function formShowsActions(
|
|
39
|
+
options: { showActions?: boolean; submitUrl?: string },
|
|
40
|
+
fields: ReadonlyArray<{ disabled?: boolean }>,
|
|
41
|
+
): boolean {
|
|
42
|
+
return (
|
|
43
|
+
options.showActions ??
|
|
44
|
+
(!!options.submitUrl && !fields.every((field) => field.disabled))
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
31
48
|
export function computeResetState(
|
|
32
49
|
state: Record<string, unknown>,
|
|
33
50
|
initialValues: FormData,
|
|
@@ -137,6 +154,25 @@ export function replaceUrlVariables(
|
|
|
137
154
|
return processedUrl;
|
|
138
155
|
}
|
|
139
156
|
|
|
157
|
+
/** Where a submit goes, or what it lacks to go anywhere. */
|
|
158
|
+
export type SubmitTarget = { url: string } | { missing: "url" | "token" };
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Where a submit is sent. A form with no URL has nowhere to send it — it is a
|
|
162
|
+
* read-only one — and a URL naming a token the page does not carry, such as an
|
|
163
|
+
* edit form opened without the id of its row, is not a route either: loading
|
|
164
|
+
* already skips such a URL, and submitting to it wrote to a path that was never
|
|
165
|
+
* meant to exist.
|
|
166
|
+
*/
|
|
167
|
+
export function resolveSubmitTarget(
|
|
168
|
+
submitUrl: string | undefined,
|
|
169
|
+
context: ReplaceUrlVariablesContext,
|
|
170
|
+
): SubmitTarget {
|
|
171
|
+
if (!submitUrl) return { missing: "url" };
|
|
172
|
+
const url = replaceUrlVariables(submitUrl, context);
|
|
173
|
+
return url.includes("{{") ? { missing: "token" } : { url };
|
|
174
|
+
}
|
|
175
|
+
|
|
140
176
|
function processBeforeStateMappers(
|
|
141
177
|
data: FormData,
|
|
142
178
|
fields: FormFieldOrGroup[],
|
|
@@ -496,16 +532,29 @@ export const useForm = (props: FormProps) => {
|
|
|
496
532
|
}
|
|
497
533
|
};
|
|
498
534
|
|
|
535
|
+
const showUnresolvedTargetToast = () => {
|
|
536
|
+
toast.add({
|
|
537
|
+
title: processI18n("$dms.form.error_title"),
|
|
538
|
+
description: processI18n("$dms.form.error_no_target"),
|
|
539
|
+
color: Color.error,
|
|
540
|
+
});
|
|
541
|
+
};
|
|
542
|
+
|
|
499
543
|
const onSubmit = async (event: FormSubmitEvent<FormData>) => {
|
|
544
|
+
const target = resolveSubmitTarget(props.submitUrl, buildUrlContext());
|
|
545
|
+
if ("missing" in target) {
|
|
546
|
+
// A read-only form renders no submit button, but Enter in one of its
|
|
547
|
+
// fields still submits it: that does nothing. Falling back on `/` sent
|
|
548
|
+
// the values to the site root with a PUT and reported nothing.
|
|
549
|
+
if (target.missing === "token") showUnresolvedTargetToast();
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
const submitUrl = target.url;
|
|
500
553
|
const plainData = collectSubmitData(
|
|
501
554
|
event,
|
|
502
555
|
allFields.value,
|
|
503
556
|
effectiveSubmitDefaults.value,
|
|
504
557
|
);
|
|
505
|
-
const submitUrl = replaceUrlVariables(
|
|
506
|
-
props.submitUrl || "/",
|
|
507
|
-
buildUrlContext(),
|
|
508
|
-
);
|
|
509
558
|
|
|
510
559
|
loading.value = true;
|
|
511
560
|
try {
|
|
@@ -106,12 +106,6 @@ export async function useTree<T = unknown>(props: TreeProps) {
|
|
|
106
106
|
},
|
|
107
107
|
});
|
|
108
108
|
|
|
109
|
-
if (!props.fetchUrl && !props.staticNodes) {
|
|
110
|
-
throw new Error(
|
|
111
|
-
"Either fetchUrl or staticNodes must be provided to useTree",
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
109
|
const loadingNodes = ref<Set<string>>(new Set());
|
|
116
110
|
// Shallow on purpose: `TreeItem` carries a string index signature and
|
|
117
111
|
// recurses through `children`, so Vue's deep `UnwrapRef` on a tree node
|
|
@@ -194,12 +188,16 @@ export async function useTree<T = unknown>(props: TreeProps) {
|
|
|
194
188
|
}
|
|
195
189
|
}
|
|
196
190
|
|
|
191
|
+
// A tree with neither a URL nor nodes of its own holds nothing yet, which is
|
|
192
|
+
// what a block just placed on a page looks like: it is configured after it is
|
|
193
|
+
// placed, and the editor says what is still missing. Refusing to render at
|
|
194
|
+
// all would answer that gesture with a crash.
|
|
197
195
|
const dataLoader = props.fetchUrl
|
|
198
196
|
? () =>
|
|
199
197
|
$authFetch<TreeNode<T>[]>(props.fetchUrl!, {
|
|
200
198
|
method: props.fetchUrlMethod || "GET",
|
|
201
199
|
})
|
|
202
|
-
: () => Promise.resolve(props.staticNodes as TreeNode<T>[]);
|
|
200
|
+
: () => Promise.resolve((props.staticNodes ?? []) as TreeNode<T>[]);
|
|
203
201
|
|
|
204
202
|
const { data, status, refresh } = await useDmsAsyncData(
|
|
205
203
|
`tree-${props.componentId}-${props.pageId}-${props.componentId}`,
|
|
@@ -161,6 +161,7 @@
|
|
|
161
161
|
"success_message": "Data has been successfully saved",
|
|
162
162
|
"error_title": "Error",
|
|
163
163
|
"error_unknown": "An unknown error occurred",
|
|
164
|
+
"error_no_target": "This form does not know which item to save. Open it from a link that names one.",
|
|
164
165
|
"no_data": "No data available",
|
|
165
166
|
"empty_value": "—",
|
|
166
167
|
"actions": "Actions",
|
|
@@ -161,6 +161,7 @@
|
|
|
161
161
|
"success_message": "Les données ont été enregistrées avec succès",
|
|
162
162
|
"error_title": "Erreur",
|
|
163
163
|
"error_unknown": "Une erreur inconnue s'est produite",
|
|
164
|
+
"error_no_target": "Ce formulaire ne sait pas quel élément enregistrer. Ouvrez-le depuis un lien qui en désigne un.",
|
|
164
165
|
"no_data": "Aucune donnée disponible",
|
|
165
166
|
"empty_value": "—",
|
|
166
167
|
"actions": "Actions",
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { createSSRApp, defineComponent, h, ref } from "vue";
|
|
2
|
+
import { renderToString } from "vue/server-renderer";
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import ChartCard from "../layers/dms-ui/app/components/chart/ChartCard.vue";
|
|
5
|
+
import KpiCard from "../layers/dms-ui/app/components/kpi/KpiCard.vue";
|
|
6
|
+
import { ABSENT_VALUE_TEXT } from "../layers/dms-ui/app/composables/chart/formatValue";
|
|
7
|
+
|
|
8
|
+
// A period the query measured nothing in: the response carries its series and no
|
|
9
|
+
// figure at all, which is what the contract answers rather than a zero.
|
|
10
|
+
vi.mock("../layers/dms-ui/app/composables/chart/useChartFetch", () => ({
|
|
11
|
+
useChartFetch: () => ({
|
|
12
|
+
data: ref({ series: [{ name: "Revenue", data: [] }] }),
|
|
13
|
+
isLoading: ref(false),
|
|
14
|
+
}),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
const Slot = defineComponent({
|
|
18
|
+
setup:
|
|
19
|
+
(_, { slots }) =>
|
|
20
|
+
() =>
|
|
21
|
+
h("div", slots.default?.()),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const SLOT_COMPONENTS = [
|
|
25
|
+
"DmsCard",
|
|
26
|
+
"DmsClientOnly",
|
|
27
|
+
"UIcon",
|
|
28
|
+
"USkeleton",
|
|
29
|
+
"DmsTrendBadge",
|
|
30
|
+
"DmsSparkline",
|
|
31
|
+
"UBadge",
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
function stubNuxt() {
|
|
35
|
+
vi.stubGlobal("useI18n", () => ({
|
|
36
|
+
locale: ref("en-US"),
|
|
37
|
+
t: (key: string) => key,
|
|
38
|
+
}));
|
|
39
|
+
vi.stubGlobal("useTranslation", () => ({
|
|
40
|
+
processI18n: (text: string) => text,
|
|
41
|
+
}));
|
|
42
|
+
vi.stubGlobal("useComponentEvent", () => ({}));
|
|
43
|
+
vi.stubGlobal("useWatch", () => ({ state: ref({}) }));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type CardProps = InstanceType<typeof ChartCard>["$props"];
|
|
47
|
+
|
|
48
|
+
async function renderCards() {
|
|
49
|
+
stubNuxt();
|
|
50
|
+
const props: CardProps = {
|
|
51
|
+
title: "Revenue",
|
|
52
|
+
componentId: "card",
|
|
53
|
+
pageId: "absent",
|
|
54
|
+
valueFormat: "currency",
|
|
55
|
+
};
|
|
56
|
+
const app = createSSRApp({
|
|
57
|
+
render: () => h("main", [h(ChartCard, props), h(KpiCard, props)]),
|
|
58
|
+
});
|
|
59
|
+
for (const name of SLOT_COMPONENTS) app.component(name, Slot);
|
|
60
|
+
return renderToString(app);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
afterEach(() => vi.unstubAllGlobals());
|
|
64
|
+
|
|
65
|
+
describe("a card whose query measured nothing", () => {
|
|
66
|
+
it("shows an absence where the figure goes, on both cards", async () => {
|
|
67
|
+
const html = await renderCards();
|
|
68
|
+
expect(html.split(ABSENT_VALUE_TEXT)).toHaveLength(3);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("prints no zero, and no unit for a quantity there is none of", async () => {
|
|
72
|
+
const html = await renderCards();
|
|
73
|
+
expect(html, "a zero reads exactly like a measured figure").not.toContain(
|
|
74
|
+
"<span>0</span>",
|
|
75
|
+
);
|
|
76
|
+
expect(html).not.toContain("€");
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { formShowsActions } from "../layers/dms-ui/app/composables/form/useForm";
|
|
3
|
+
|
|
4
|
+
describe("formShowsActions", () => {
|
|
5
|
+
it("shows the buttons once the form has somewhere to submit to", () => {
|
|
6
|
+
expect(formShowsActions({ submitUrl: "/api/order/new" }, [{}])).toBe(true);
|
|
7
|
+
expect(formShowsActions({}, [{}])).toBe(false);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("hides them on a form nothing in which can be filled in", () => {
|
|
11
|
+
expect(
|
|
12
|
+
formShowsActions({ submitUrl: "/api/order/new" }, [{ disabled: true }]),
|
|
13
|
+
).toBe(false);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("shows them when asked, before there is an address or a field", () => {
|
|
17
|
+
expect(formShowsActions({ showActions: true }, [])).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("hides them when asked, whatever the form could submit", () => {
|
|
21
|
+
expect(
|
|
22
|
+
formShowsActions({ showActions: false, submitUrl: "/api/order/new" }, [
|
|
23
|
+
{},
|
|
24
|
+
]),
|
|
25
|
+
).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { resolveSubmitTarget } from "../layers/dms-ui/app/composables/form/useForm";
|
|
3
|
+
|
|
4
|
+
const page = { routeParams: { id: "42" }, routeQuery: { id: "7" } };
|
|
5
|
+
|
|
6
|
+
describe("resolveSubmitTarget", () => {
|
|
7
|
+
it("sends a submit where its URL points, tokens filled in", () => {
|
|
8
|
+
expect(resolveSubmitTarget("/api/ticket/edit?id={{query.id}}", page)).toEqual(
|
|
9
|
+
{ url: "/api/ticket/edit?id=7" },
|
|
10
|
+
);
|
|
11
|
+
expect(resolveSubmitTarget("/api/ticket/edit?id={{params.id}}", page)).toEqual(
|
|
12
|
+
{ url: "/api/ticket/edit?id=42" },
|
|
13
|
+
);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("has nowhere to send a read-only form's values", () => {
|
|
17
|
+
expect(resolveSubmitTarget(undefined, page)).toEqual({ missing: "url" });
|
|
18
|
+
expect(resolveSubmitTarget("", page)).toEqual({ missing: "url" });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("refuses a URL naming a row the page does not carry", () => {
|
|
22
|
+
expect(
|
|
23
|
+
resolveSubmitTarget("/api/ticket/edit?id={{query.id}}", {
|
|
24
|
+
routeQuery: {},
|
|
25
|
+
}),
|
|
26
|
+
).toEqual({ missing: "token" });
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
/**
|
|
3
|
+
* A tree is configured after it is placed.
|
|
4
|
+
*
|
|
5
|
+
* The editor drops a block and the author points it at its data afterwards, so
|
|
6
|
+
* a tree with neither a URL nor nodes of its own is a page under construction,
|
|
7
|
+
* not a mistake. It used to throw during setup, which the canvas could only
|
|
8
|
+
* report as a crash on the block the author had just placed.
|
|
9
|
+
*/
|
|
10
|
+
import * as vue from "vue";
|
|
11
|
+
import { ref } from "vue";
|
|
12
|
+
import { describe, expect, it, vi } from "vitest";
|
|
13
|
+
import type { TreeProps } from "../layers/dms-ui/app/composables/tree/types";
|
|
14
|
+
|
|
15
|
+
// The layer's auto-imports, as the Nuxt build would supply them: Vue's own
|
|
16
|
+
// first, then the DMS helpers the composable reaches for.
|
|
17
|
+
for (const name of [
|
|
18
|
+
"ref",
|
|
19
|
+
"shallowRef",
|
|
20
|
+
"computed",
|
|
21
|
+
"watch",
|
|
22
|
+
"watchEffect",
|
|
23
|
+
"toValue",
|
|
24
|
+
"nextTick",
|
|
25
|
+
] as const) {
|
|
26
|
+
vi.stubGlobal(name, vue[name]);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
vi.stubGlobal(
|
|
30
|
+
"TreeEvents",
|
|
31
|
+
new Proxy({}, { get: (_target, key) => String(key) }),
|
|
32
|
+
);
|
|
33
|
+
vi.stubGlobal("createError", (payload: unknown) => new Error(String(payload)));
|
|
34
|
+
vi.stubGlobal("useAuthFetch", () => ({
|
|
35
|
+
$authFetch: async () => [],
|
|
36
|
+
}));
|
|
37
|
+
vi.stubGlobal("useComponentEvent", () => ({ sendComponentEvent: () => {} }));
|
|
38
|
+
vi.stubGlobal("useDefinedFunctions", () => ({ getFunction: () => undefined }));
|
|
39
|
+
vi.stubGlobal("useTranslation", () => ({ processI18n: (text: string) => text }));
|
|
40
|
+
vi.stubGlobal("useWatch", () => ({ isLoading: ref(false), state: ref({}) }));
|
|
41
|
+
vi.stubGlobal("useToast", () => ({ add: () => {} }));
|
|
42
|
+
vi.stubGlobal("useI18n", () => ({ t: (key: string) => key }));
|
|
43
|
+
vi.stubGlobal("useEventedAction", () => ({ execute: async () => [] }));
|
|
44
|
+
vi.stubGlobal(
|
|
45
|
+
"useDmsAsyncData",
|
|
46
|
+
async (_key: string, handler: () => Promise<unknown>) => ({
|
|
47
|
+
data: ref(await handler()),
|
|
48
|
+
status: ref("success"),
|
|
49
|
+
refresh: async () => {},
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const { useTree } = await import(
|
|
54
|
+
"../layers/dms-ui/app/composables/tree/useTree"
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
describe("a tree with no source yet", () => {
|
|
58
|
+
const props = (extra: Partial<TreeProps> = {}) =>
|
|
59
|
+
({
|
|
60
|
+
selectionBehavior: "toggle",
|
|
61
|
+
componentId: "tree",
|
|
62
|
+
pageId: "page",
|
|
63
|
+
...extra,
|
|
64
|
+
}) as TreeProps;
|
|
65
|
+
|
|
66
|
+
it("holds nothing, rather than refusing to render", async () => {
|
|
67
|
+
const tree = await useTree(props());
|
|
68
|
+
|
|
69
|
+
expect(tree.items.value).toEqual([]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("still reads the nodes it is given", async () => {
|
|
73
|
+
const tree = await useTree(
|
|
74
|
+
props({ staticNodes: [{ value: "one", label: "One" }] }),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
expect(tree.items.value.map((node) => node.value)).toEqual(["one"]);
|
|
78
|
+
});
|
|
79
|
+
});
|