@antelopejs/dms 0.7.13 → 0.7.14

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 (42) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/implementations/dms/client-ip.d.ts +2 -0
  3. package/dist/implementations/dms/client-ip.js +9 -0
  4. package/dist/implementations/dms/client-ip.js.map +1 -0
  5. package/dist/index.js +1 -0
  6. package/dist/index.js.map +1 -1
  7. package/dist/routes/auth/client-ip.d.ts +1 -4
  8. package/dist/routes/auth/client-ip.js.map +1 -1
  9. package/dist/test/unit/interfaces/dms/client-ip.test.d.ts +1 -0
  10. package/dist/test/unit/interfaces/dms/client-ip.test.js +61 -0
  11. package/dist/test/unit/interfaces/dms/client-ip.test.js.map +1 -0
  12. package/frontend-vue/layers/dms-core/app/composables/period/periodLink.ts +60 -0
  13. package/frontend-vue/layers/dms-core/app/composables/translation/useTranslation.ts +9 -4
  14. package/frontend-vue/layers/dms-core/app/composables/useFieldErrors.ts +34 -4
  15. package/frontend-vue/layers/dms-core/app/types/confirm-dialog.ts +2 -0
  16. package/frontend-vue/layers/dms-layout/app/build/utils/pageValidationRedirect.ts +32 -0
  17. package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +29 -7
  18. package/frontend-vue/layers/dms-layout/app/types/page.ts +2 -0
  19. package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +28 -7
  20. package/frontend-vue/layers/dms-ui/app/build/composables/confirm/confirmTextMatch.ts +16 -0
  21. package/frontend-vue/layers/dms-ui/app/build/composables/form/numberRange.ts +53 -0
  22. package/frontend-vue/layers/dms-ui/app/build/composables/period/periodQuery.ts +6 -29
  23. package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +6 -3
  24. package/frontend-vue/layers/dms-ui/app/components/confirm/ConfirmModal.vue +12 -2
  25. package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +1 -1
  26. package/frontend-vue/layers/dms-ui/app/components/form/components/InputNumber.vue +46 -7
  27. package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +21 -1
  28. package/frontend-vue/layers/dms-ui/app/composables/confirm/index.ts +1 -0
  29. package/frontend-vue/layers/dms-ui/app/composables/confirm/types.ts +20 -0
  30. package/frontend-vue/layers/dms-ui/app/composables/confirm/useConfirmFrom.ts +32 -0
  31. package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +2 -2
  32. package/frontend-vue/tests/confirm-from.test.ts +101 -0
  33. package/frontend-vue/tests/confirm-typed-text.test.ts +35 -0
  34. package/frontend-vue/tests/context-links.test.ts +220 -0
  35. package/frontend-vue/tests/field-errors.test.ts +35 -0
  36. package/frontend-vue/tests/form-required-legend.test.ts +34 -0
  37. package/frontend-vue/tests/form-server-field-errors.test.ts +18 -1
  38. package/frontend-vue/tests/input-number-range.test.ts +135 -0
  39. package/frontend-vue/tests/page-validation-redirect.test.ts +221 -0
  40. package/package.json +2 -2
  41. package/skills/dms-module/SKILL.md +3 -0
  42. /package/frontend-vue/layers/dms-ui/app/{build/components → components}/form/FormRequiredLegend.vue +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.7.14
4
+
5
+ [compare changes](https://github.com/AntelopeJS/dms/compare/v0.7.13...v0.7.14)
6
+
7
+ ### 🚀 Enhancements
8
+
9
+ - **interface:** Expose the resolved client address to modules ([#235](https://github.com/AntelopeJS/dms/pull/235))
10
+ - **form:** Keep out-of-range numbers, interpolate field error params and expose the required legend ([#231](https://github.com/AntelopeJS/dms/pull/231))
11
+ - **confirm:** Add useConfirmFrom and a case-insensitive typed confirmation ([#232](https://github.com/AntelopeJS/dms/pull/232))
12
+ - **page:** Let a page failing its validation redirect instead of answering not found ([#236](https://github.com/AntelopeJS/dms/pull/236))
13
+ - **links:** Let row links and link actions keep the page's context ([#238](https://github.com/AntelopeJS/dms/pull/238))
14
+
15
+ ### 🏡 Chore
16
+
17
+ - **release:** @antelopejs/interface-dms v0.5.9 ([ef80c3fc](https://github.com/AntelopeJS/dms/commit/ef80c3fc))
18
+
19
+ ### ❤️ Contributors
20
+
21
+ - Antony Rizzitelli <rizzitelli.antony@pm.me>
22
+
3
23
  ## v0.7.13
4
24
 
5
25
  [compare changes](https://github.com/AntelopeJS/dms/compare/v0.7.12...v0.7.13)
@@ -0,0 +1,2 @@
1
+ import type { ClientAddressSource } from "@antelopejs/interface-dms/client-ip";
2
+ export declare function ResolveClientIp(source: ClientAddressSource): string;
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ResolveClientIp = ResolveClientIp;
4
+ const config_1 = require("../../config");
5
+ const client_ip_1 = require("../../routes/auth/client-ip");
6
+ function ResolveClientIp(source) {
7
+ return (0, client_ip_1.resolveClientIp)(source, (0, client_ip_1.trustedProxyCount)((0, config_1.getAuthConfig)()));
8
+ }
9
+ //# sourceMappingURL=client-ip.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-ip.js","sourceRoot":"","sources":["../../../src/implementations/dms/client-ip.ts"],"names":[],"mappings":";;AAOA,0CAEC;AARD,yCAA6C;AAC7C,2DAGqC;AAErC,SAAgB,eAAe,CAAC,MAA2B;IACzD,OAAO,IAAA,2BAAe,EAAC,MAAM,EAAE,IAAA,6BAAiB,EAAC,IAAA,sBAAa,GAAE,CAAC,CAAC,CAAC;AACrE,CAAC"}
package/dist/index.js CHANGED
@@ -127,6 +127,7 @@ async function implementInterfaces() {
127
127
  void (0, interface_core_1.ImplementInterface)(await Promise.resolve().then(() => __importStar(require("@antelopejs/interface-dms/base/tenant-export-archive"))), await Promise.resolve().then(() => __importStar(require("./implementations/dms-base/tenant-export-archive"))));
128
128
  void (0, interface_core_1.ImplementInterface)(await Promise.resolve().then(() => __importStar(require("@antelopejs/interface-dms/client-base-url"))), await Promise.resolve().then(() => __importStar(require("./implementations/dms/client-base-url"))));
129
129
  void (0, interface_core_1.ImplementInterface)(await Promise.resolve().then(() => __importStar(require("@antelopejs/interface-dms/api-base-url"))), await Promise.resolve().then(() => __importStar(require("./implementations/dms/api-base-url"))));
130
+ void (0, interface_core_1.ImplementInterface)(await Promise.resolve().then(() => __importStar(require("@antelopejs/interface-dms/client-ip"))), await Promise.resolve().then(() => __importStar(require("./implementations/dms/client-ip"))));
130
131
  void (0, interface_core_1.ImplementInterface)(await Promise.resolve().then(() => __importStar(require("@antelopejs/interface-dms/auth"))), await Promise.resolve().then(() => __importStar(require("./implementations/dms-auth"))));
131
132
  void (0, interface_core_1.ImplementInterface)(await Promise.resolve().then(() => __importStar(require("@antelopejs/interface-dms/notifications"))), await Promise.resolve().then(() => __importStar(require("./implementations/dms-notifications"))));
132
133
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,4 @@
1
+ import type { ClientAddressSource } from "@antelopejs/interface-dms/client-ip";
1
2
  import type { AuthConfig } from "../../config";
2
- export interface ClientAddressSource {
3
- socketAddress: string | undefined;
4
- forwardedFor: string | undefined;
5
- }
6
3
  export declare function resolveClientIp(source: ClientAddressSource, trustedProxies: number): string;
7
4
  export declare function trustedProxyCount(authConfig: AuthConfig): number;
@@ -1 +1 @@
1
- {"version":3,"file":"client-ip.js","sourceRoot":"","sources":["../../../src/routes/auth/client-ip.ts"],"names":[],"mappings":";;AAkDA,0CAUC;AAiBD,8CAIC;AAjFD,uCAAkC;AAGlC,MAAM,uBAAuB,GAAG,GAAG,CAAC;AACpC,MAAM,kBAAkB,GAAG,SAAS,CAAC;AACrC,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAc3B,SAAS,gBAAgB,CAAC,OAAe;IACvC,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAC1D,MAAM,YAAY,GAChB,OAAO,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,kBAAkB,CAAC,IAAI,IAAA,iBAAM,EAAC,QAAQ,CAAC,CAAC;IAC3E,OAAO,YAAY,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC;AAC3C,CAAC;AAED,SAAS,8BAA8B,CACrC,YAAgC;IAEhC,OAAO,CAAC,YAAY,IAAI,EAAE,CAAC;SACxB,KAAK,CAAC,uBAAuB,CAAC;SAC9B,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;SAC5B,MAAM,CAAC,OAAO,CAAC;SACf,OAAO,EAAE,CAAC;AACf,CAAC;AAgBD,SAAgB,eAAe,CAC7B,MAA2B,EAC3B,cAAsB;IAEtB,MAAM,SAAS,GAAG;QAChB,MAAM,CAAC,aAAa,IAAI,EAAE;QAC1B,GAAG,8BAA8B,CAAC,MAAM,CAAC,YAAY,CAAC;KACvD,CAAC;IACF,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,cAAc,EAAE,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC7D,OAAO,gBAAgB,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;AAC5C,CAAC;AAED,SAAS,YAAY,CAAC,KAAc;IAClC,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC;QAC3B,KAAK,IAAI,gBAAgB,CAC1B,CAAC;AACJ,CAAC;AASD,SAAgB,iBAAiB,CAAC,UAAsB;IACtD,OAAO,YAAY,CAAC,UAAU,CAAC,cAAc,CAAC;QAC5C,CAAC,CAAC,UAAU,CAAC,cAAc;QAC3B,CAAC,CAAC,gBAAgB,CAAC;AACvB,CAAC"}
1
+ {"version":3,"file":"client-ip.js","sourceRoot":"","sources":["../../../src/routes/auth/client-ip.ts"],"names":[],"mappings":";;AA2CA,0CAUC;AAiBD,8CAIC;AA1ED,uCAAkC;AAIlC,MAAM,uBAAuB,GAAG,GAAG,CAAC;AACpC,MAAM,kBAAkB,GAAG,SAAS,CAAC;AACrC,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAM3B,SAAS,gBAAgB,CAAC,OAAe;IACvC,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAC1D,MAAM,YAAY,GAChB,OAAO,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,kBAAkB,CAAC,IAAI,IAAA,iBAAM,EAAC,QAAQ,CAAC,CAAC;IAC3E,OAAO,YAAY,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC;AAC3C,CAAC;AAED,SAAS,8BAA8B,CACrC,YAAgC;IAEhC,OAAO,CAAC,YAAY,IAAI,EAAE,CAAC;SACxB,KAAK,CAAC,uBAAuB,CAAC;SAC9B,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;SAC5B,MAAM,CAAC,OAAO,CAAC;SACf,OAAO,EAAE,CAAC;AACf,CAAC;AAgBD,SAAgB,eAAe,CAC7B,MAA2B,EAC3B,cAAsB;IAEtB,MAAM,SAAS,GAAG;QAChB,MAAM,CAAC,aAAa,IAAI,EAAE;QAC1B,GAAG,8BAA8B,CAAC,MAAM,CAAC,YAAY,CAAC;KACvD,CAAC;IACF,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,cAAc,EAAE,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC7D,OAAO,gBAAgB,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;AAC5C,CAAC;AAED,SAAS,YAAY,CAAC,KAAc;IAClC,OAAO,CACL,OAAO,KAAK,KAAK,QAAQ;QACzB,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC;QAC3B,KAAK,IAAI,gBAAgB,CAC1B,CAAC;AACJ,CAAC;AASD,SAAgB,iBAAiB,CAAC,UAAsB;IACtD,OAAO,YAAY,CAAC,UAAU,CAAC,cAAc,CAAC;QAC5C,CAAC,CAAC,UAAU,CAAC,cAAc;QAC3B,CAAC,CAAC,gBAAgB,CAAC;AACvB,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const chai_1 = require("chai");
4
+ const client_ip_1 = require("@antelopejs/interface-dms/client-ip");
5
+ const config_1 = require("../../../../config");
6
+ const PROXY_ADDRESS = "::ffff:10.0.0.9";
7
+ const CLIENT_ADDRESS = "109.132.199.8";
8
+ const SPOOFED_ADDRESS = "1.2.3.4";
9
+ function requestContext(request) {
10
+ const headers = request.forwardedFor === undefined
11
+ ? {}
12
+ : { "x-forwarded-for": request.forwardedFor };
13
+ return {
14
+ rawRequest: {
15
+ socket: { remoteAddress: request.socketAddress },
16
+ headers,
17
+ },
18
+ };
19
+ }
20
+ describe("[unit] client ip interface", () => {
21
+ let configured;
22
+ beforeEach(() => {
23
+ configured = (0, config_1.getAuthConfig)().trustedProxies;
24
+ });
25
+ afterEach(() => {
26
+ (0, config_1.getAuthConfig)().trustedProxies = configured;
27
+ });
28
+ it("answers the socket peer, unmapped, when no proxy is trusted", async () => {
29
+ (0, config_1.getAuthConfig)().trustedProxies = 0;
30
+ const context = requestContext({
31
+ socketAddress: PROXY_ADDRESS,
32
+ forwardedFor: SPOOFED_ADDRESS,
33
+ });
34
+ (0, chai_1.expect)(await (0, client_ip_1.getClientIp)(context)).to.equal("10.0.0.9");
35
+ });
36
+ it("follows auth.trustedProxies through x-forwarded-for", async () => {
37
+ (0, config_1.getAuthConfig)().trustedProxies = 1;
38
+ const context = requestContext({
39
+ socketAddress: PROXY_ADDRESS,
40
+ forwardedFor: `${SPOOFED_ADDRESS}, ${CLIENT_ADDRESS}`,
41
+ });
42
+ (0, chai_1.expect)(await (0, client_ip_1.getClientIp)(context)).to.equal(CLIENT_ADDRESS);
43
+ });
44
+ it("reads a header repeated across lines as one chain", async () => {
45
+ (0, config_1.getAuthConfig)().trustedProxies = 1;
46
+ const context = requestContext({
47
+ socketAddress: PROXY_ADDRESS,
48
+ forwardedFor: [SPOOFED_ADDRESS, CLIENT_ADDRESS],
49
+ });
50
+ (0, chai_1.expect)(await (0, client_ip_1.getClientIp)(context)).to.equal(CLIENT_ADDRESS);
51
+ });
52
+ it("resolves a source handed to ResolveClientIp directly", async () => {
53
+ (0, config_1.getAuthConfig)().trustedProxies = 1;
54
+ const resolved = await (0, client_ip_1.ResolveClientIp)({
55
+ socketAddress: PROXY_ADDRESS,
56
+ forwardedFor: CLIENT_ADDRESS,
57
+ });
58
+ (0, chai_1.expect)(resolved).to.equal(CLIENT_ADDRESS);
59
+ });
60
+ });
61
+ //# sourceMappingURL=client-ip.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-ip.test.js","sourceRoot":"","sources":["../../../../../src/test/unit/interfaces/dms/client-ip.test.ts"],"names":[],"mappings":";;AACA,+BAA8B;AAC9B,mEAG6C;AAC7C,+CAAmD;AAEnD,MAAM,aAAa,GAAG,iBAAiB,CAAC;AACxC,MAAM,cAAc,GAAG,eAAe,CAAC;AACvC,MAAM,eAAe,GAAG,SAAS,CAAC;AAOlC,SAAS,cAAc,CAAC,OAAoB;IAC1C,MAAM,OAAO,GACX,OAAO,CAAC,YAAY,KAAK,SAAS;QAChC,CAAC,CAAC,EAAE;QACJ,CAAC,CAAC,EAAE,iBAAiB,EAAE,OAAO,CAAC,YAAY,EAAE,CAAC;IAClD,OAAO;QACL,UAAU,EAAE;YACV,MAAM,EAAE,EAAE,aAAa,EAAE,OAAO,CAAC,aAAa,EAAE;YAChD,OAAO;SACR;KAC2B,CAAC;AACjC,CAAC;AAED,QAAQ,CAAC,4BAA4B,EAAE,GAAG,EAAE;IAC1C,IAAI,UAA8B,CAAC;IAEnC,UAAU,CAAC,GAAG,EAAE;QACd,UAAU,GAAG,IAAA,sBAAa,GAAE,CAAC,cAAc,CAAC;IAC9C,CAAC,CAAC,CAAC;IAEH,SAAS,CAAC,GAAG,EAAE;QACb,IAAA,sBAAa,GAAE,CAAC,cAAc,GAAG,UAAU,CAAC;IAC9C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6DAA6D,EAAE,KAAK,IAAI,EAAE;QAC3E,IAAA,sBAAa,GAAE,CAAC,cAAc,GAAG,CAAC,CAAC;QACnC,MAAM,OAAO,GAAG,cAAc,CAAC;YAC7B,aAAa,EAAE,aAAa;YAC5B,YAAY,EAAE,eAAe;SAC9B,CAAC,CAAC;QACH,IAAA,aAAM,EAAC,MAAM,IAAA,uBAAW,EAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;IAC1D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qDAAqD,EAAE,KAAK,IAAI,EAAE;QACnE,IAAA,sBAAa,GAAE,CAAC,cAAc,GAAG,CAAC,CAAC;QACnC,MAAM,OAAO,GAAG,cAAc,CAAC;YAC7B,aAAa,EAAE,aAAa;YAC5B,YAAY,EAAE,GAAG,eAAe,KAAK,cAAc,EAAE;SACtD,CAAC,CAAC;QACH,IAAA,aAAM,EAAC,MAAM,IAAA,uBAAW,EAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;IAC9D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mDAAmD,EAAE,KAAK,IAAI,EAAE;QACjE,IAAA,sBAAa,GAAE,CAAC,cAAc,GAAG,CAAC,CAAC;QACnC,MAAM,OAAO,GAAG,cAAc,CAAC;YAC7B,aAAa,EAAE,aAAa;YAC5B,YAAY,EAAE,CAAC,eAAe,EAAE,cAAc,CAAC;SAChD,CAAC,CAAC;QACH,IAAA,aAAM,EAAC,MAAM,IAAA,uBAAW,EAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;IAC9D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sDAAsD,EAAE,KAAK,IAAI,EAAE;QACpE,IAAA,sBAAa,GAAE,CAAC,cAAc,GAAG,CAAC,CAAC;QACnC,MAAM,QAAQ,GAAG,MAAM,IAAA,2BAAe,EAAC;YACrC,aAAa,EAAE,aAAa;YAC5B,YAAY,EAAE,cAAc;SAC7B,CAAC,CAAC;QACH,IAAA,aAAM,EAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,60 @@
1
+ /** The keys one `PeriodSelector` keeps its period under in the page URL. */
2
+ export interface PeriodQueryKeys {
3
+ period: string;
4
+ compare: string;
5
+ from: string;
6
+ to: string;
7
+ compareFrom: string;
8
+ compareTo: string;
9
+ }
10
+
11
+ const KEY_NAMES: Array<keyof PeriodQueryKeys> = [
12
+ "period",
13
+ "compare",
14
+ "from",
15
+ "to",
16
+ "compareFrom",
17
+ "compareTo",
18
+ ];
19
+ const QUERY_SEPARATOR = "?";
20
+ const FRAGMENT_SEPARATOR = "#";
21
+
22
+ /**
23
+ * The keys of selector `id` in the page URL (`<id>.period`), or the bare keys
24
+ * (`period`) of a selection kept on its own.
25
+ */
26
+ export function periodQueryKeys(id?: string): PeriodQueryKeys {
27
+ const prefix = id ? `${id}.` : "";
28
+ return Object.fromEntries(
29
+ KEY_NAMES.map((name) => [name, `${prefix}${name}`]),
30
+ ) as unknown as PeriodQueryKeys;
31
+ }
32
+
33
+ function firstText(value: unknown): string | undefined {
34
+ const text = Array.isArray(value) ? value[0] : value;
35
+ return typeof text === "string" && text ? text : undefined;
36
+ }
37
+
38
+ /**
39
+ * `link` carrying the period selector `id` shows in `query` (the route query
40
+ * of the page it is followed from), so the page it opens, holding a selector
41
+ * with the same `id`, shows the same period. A key the link names itself
42
+ * wins, and a key the query does not hold is left out.
43
+ */
44
+ export function withPeriodQuery(
45
+ link: string,
46
+ query: Record<string, unknown>,
47
+ id: string,
48
+ ): string {
49
+ const [beforeFragment = "", ...fragment] = link.split(FRAGMENT_SEPARATOR);
50
+ const [path = "", ...search] = beforeFragment.split(QUERY_SEPARATOR);
51
+ const params = new URLSearchParams(search.join(QUERY_SEPARATOR));
52
+ for (const key of Object.values(periodQueryKeys(id))) {
53
+ const value = firstText(query[key]);
54
+ if (value && !params.has(key)) params.set(key, value);
55
+ }
56
+ const text = params.toString();
57
+ return [text ? `${path}${QUERY_SEPARATOR}${text}` : path, ...fragment].join(
58
+ FRAGMENT_SEPARATOR,
59
+ );
60
+ }
@@ -49,15 +49,17 @@ export function resolveI18nKey(
49
49
  * (`$page.settings.invites.error.not_found`). `translate` echoes the key back
50
50
  * when no translation exists in the active locale or its fallbacks: a message
51
51
  * marked with `$` then falls back to the bare key, and an unmarked one — which
52
- * may well be plain prose — is shown as it came in.
52
+ * may well be plain prose — is shown as it came in. `params` fill the
53
+ * translation's placeholders (`"At most {max}"` with `{ max: 5000 }`).
53
54
  */
54
55
  export function resolveApiMessage(
55
56
  translate: I18nTranslate,
56
57
  message: unknown,
58
+ params?: Record<string, unknown> | null,
57
59
  ): string {
58
60
  const text = typeof message === "string" ? message : String(message);
59
61
  const marked = readTranslationKey(text);
60
- const translated = translate(marked.key, {});
62
+ const translated = translate(marked.key, params ?? {});
61
63
 
62
64
  if (translated !== marked.key) return translated;
63
65
  return marked.isKey ? marked.key : text;
@@ -93,8 +95,11 @@ export const useTranslation = () => {
93
95
  return resolveI18nKey(t, key, params);
94
96
  }
95
97
 
96
- function processApiMessage(message: unknown) {
97
- return resolveApiMessage(t, message);
98
+ function processApiMessage(
99
+ message: unknown,
100
+ params?: Record<string, unknown> | null,
101
+ ) {
102
+ return resolveApiMessage(t, message, params);
98
103
  }
99
104
 
100
105
  return {
@@ -22,6 +22,12 @@ export interface ApiFieldError {
22
22
  message: string;
23
23
  /** The values of the field the error names (addresses of a list…). */
24
24
  values?: string[];
25
+ /**
26
+ * Values filling the placeholders of the translated message: a body
27
+ * `{ field: "budget", message: "$page.error.budget_max", params: { max: 5000 } }`
28
+ * shows "At most 5000" for a translation `"At most {max}"`.
29
+ */
30
+ params?: ApiFieldErrorParams;
25
31
  /**
26
32
  * The part of the field the error names, as a dotted path from the field
27
33
  * (`address.streetName`, `title.fr`); absent when it names the field.
@@ -29,6 +35,9 @@ export interface ApiFieldError {
29
35
  path?: string;
30
36
  }
31
37
 
38
+ /** Placeholder values of a field error's message. */
39
+ export type ApiFieldErrorParams = Record<string, string | number | boolean>;
40
+
32
41
  /** A code the API answers with that belongs to a field. */
33
42
  export type FieldErrorCode<F extends string = string> =
34
43
  | F
@@ -224,10 +233,26 @@ function fromIssues(issues: ValidationIssue[]): ApiFieldError[] {
224
233
  });
225
234
  }
226
235
 
227
- /** `{ field, message, values? }`, the shape the DMS answers with. */
236
+ const PARAM_TYPES = new Set(["string", "number", "boolean"]);
237
+
238
+ /** The placeholder values of a body, keeping only the plain ones. */
239
+ function paramsOf(params: unknown): ApiFieldErrorParams | undefined {
240
+ if (typeof params !== "object" || params === null || Array.isArray(params)) {
241
+ return undefined;
242
+ }
243
+ const entries = Object.entries(params).filter(([, value]) =>
244
+ PARAM_TYPES.has(typeof value),
245
+ );
246
+ return entries.length > 0
247
+ ? (Object.fromEntries(entries) as ApiFieldErrorParams)
248
+ : undefined;
249
+ }
250
+
251
+ /** `{ field, message, values?, params? }`, the shape the DMS answers with. */
228
252
  function fromFieldObject(body: Record<string, unknown>): ApiFieldError[] {
229
- const { field, message, values } = body;
253
+ const { field, message, values, params } = body;
230
254
  if (typeof field !== "string" || typeof message !== "string") return [];
255
+ const placeholders = paramsOf(params);
231
256
  return [
232
257
  {
233
258
  field,
@@ -235,6 +260,7 @@ function fromFieldObject(body: Record<string, unknown>): ApiFieldError[] {
235
260
  values: Array.isArray(values)
236
261
  ? values.filter((value): value is string => typeof value === "string")
237
262
  : undefined,
263
+ ...(placeholders && { params: placeholders }),
238
264
  },
239
265
  ];
240
266
  }
@@ -311,7 +337,7 @@ function isClientError(status: number | undefined): status is number {
311
337
  *
312
338
  * Read, in order: a code the form maps to a field (`codes`, for any 4xx), a
313
339
  * zod issue list (what `assertValidation` answers), a `{ field, message }`
314
- * body, the data API's "Missing mandatory fields: a, b" / "Invalid field
340
+ * body (with optional `values` and `params`), the data API's "Missing mandatory fields: a, b" / "Invalid field
315
341
  * type(s): a" and a `field: message` text. A network failure, a 403, a 429
316
342
  * or a 5xx never resolves to a field.
317
343
  *
@@ -478,7 +504,11 @@ export function useFieldErrors<F extends string>(
478
504
  if (fields.length === 0) return false;
479
505
  clear();
480
506
  for (const entry of fields) {
481
- errors[entry.field as F] = resolveApiMessage(translate, entry.message);
507
+ errors[entry.field as F] = resolveApiMessage(
508
+ translate,
509
+ entry.message,
510
+ entry.params,
511
+ );
482
512
  }
483
513
  await focus(fields[0]!.field as F);
484
514
  return true;
@@ -83,6 +83,8 @@ export interface ConfirmDialog {
83
83
  blocked?: boolean;
84
84
  /** Text the user must type to confirm. */
85
85
  confirmText?: string;
86
+ /** Compares the typed text with `confirmText` ignoring case. */
87
+ confirmTextCaseInsensitive?: boolean;
86
88
  /** Fields whose values are sent with the action's request. */
87
89
  fields?: ConfirmDialogField[];
88
90
  /** A component drawn in the dialog's body. */
@@ -0,0 +1,32 @@
1
+ import { parseURL, withoutTrailingSlash } from "ufo";
2
+ import {
3
+ type ReplaceUrlVariablesContext,
4
+ resolveLinkVariables,
5
+ } from "#dms-ui/app/build/utils/urlVariables";
6
+ import type { PageValidation } from "../../types/page";
7
+
8
+ const ROOT_PATH = "/";
9
+
10
+ function normalizePath(path: string): string {
11
+ return withoutTrailingSlash(path) || ROOT_PATH;
12
+ }
13
+
14
+ /**
15
+ * Where a page that failed its validation sends the user: its `redirectTo`
16
+ * with the page URL's tokens filled, or `undefined` for the not-found error.
17
+ * A target on the page's own path would fail the same way again, so it is
18
+ * refused as well.
19
+ */
20
+ export function pageValidationRedirect(
21
+ validation: PageValidation | undefined,
22
+ currentPath: string,
23
+ context: ReplaceUrlVariablesContext,
24
+ ): string | undefined {
25
+ if (!validation?.redirectTo) return undefined;
26
+ const target = resolveLinkVariables(validation.redirectTo, context);
27
+ const targetPath = parseURL(target).pathname;
28
+ if (!targetPath || normalizePath(targetPath) === normalizePath(currentPath)) {
29
+ return undefined;
30
+ }
31
+ return target;
32
+ }
@@ -3,6 +3,7 @@ import { withoutTrailingSlash } from "ufo";
3
3
  import { holdPageHeaderActions } from "../composables/layout/usePageHeaderActions";
4
4
  import { useDevReloading } from "../build/composables/dev-reload/useDevReloadHolder";
5
5
  import { usePageLiveHost } from "#dms-ui/app/build/composables/live/usePageLiveHost";
6
+ import { pageValidationRedirect } from "../build/utils/pageValidationRedirect";
6
7
 
7
8
  defineOptions({ inheritAttrs: false });
8
9
 
@@ -95,13 +96,13 @@ async function recheckAccessWithFreshSession(): Promise<void> {
95
96
  });
96
97
  }
97
98
 
98
- function validatePage(
99
+ function isPageValid(
99
100
  metadata: PageInfo | undefined,
100
101
  query: Record<string, unknown>,
101
102
  params: Record<string, unknown>,
102
- ): void {
103
+ ): boolean {
103
104
  if (!metadata?.validation) {
104
- return;
105
+ return true;
105
106
  }
106
107
 
107
108
  const { validation } = metadata;
@@ -121,12 +122,31 @@ function validatePage(
121
122
  { query, params },
122
123
  );
123
124
 
124
- if (!isQueryValid || !isCustomValid) {
125
+ return isQueryValid && isCustomValid;
126
+ }
127
+
128
+ // Set while a page failing its validation leaves for its `redirectTo`: its
129
+ // blocks are not drawn, so none of them requests a URL missing its context.
130
+ const isLeavingPage = ref(false);
131
+
132
+ async function enforcePageValidation(): Promise<void> {
133
+ const metadata = pagelayoutMetadata.value;
134
+ const query = route.query as Record<string, unknown>;
135
+ if (isPageValid(metadata, query, routeParams.value)) {
136
+ return;
137
+ }
138
+ const redirect = pageValidationRedirect(metadata?.validation, route.path, {
139
+ routeParams: routeParams.value,
140
+ routeQuery: query,
141
+ });
142
+ if (!redirect) {
125
143
  throw createError({
126
144
  statusCode: HTTP_NOT_FOUND,
127
145
  statusMessage: "Page not found",
128
146
  });
129
147
  }
148
+ isLeavingPage.value = true;
149
+ await navigateDms(redirect, { replace: true });
130
150
  }
131
151
 
132
152
  defineDmsPageMeta({
@@ -159,7 +179,7 @@ if (siteLayout.loadingError && siteLayout.loadingError.value) {
159
179
  });
160
180
  }
161
181
 
162
- validatePage(pagelayoutMetadata.value, route.query, routeParams.value);
182
+ await enforcePageValidation();
163
183
  await checkPageMetadata(pagelayoutMetadata.value);
164
184
 
165
185
  // Same reason as the guard inside `checkPageMetadata`: while a dev reload is in
@@ -207,7 +227,8 @@ watch(
207
227
  siteLayout.refreshIfStale(),
208
228
  refreshPermissionsIfStale(),
209
229
  ]);
210
- validatePage(pagelayoutMetadata.value, route.query, routeParams.value);
230
+ isLeavingPage.value = false;
231
+ await enforcePageValidation();
211
232
  await checkPageMetadata(pagelayoutMetadata.value);
212
233
  },
213
234
  );
@@ -488,7 +509,8 @@ if (import.meta.env.DEV) {
488
509
  </script>
489
510
 
490
511
  <template>
491
- <div v-if="hidesEveryBlock" class="dms-card overflow-hidden">
512
+ <template v-if="isLeavingPage" />
513
+ <div v-else-if="hidesEveryBlock" class="dms-card overflow-hidden">
492
514
  <DmsEmptyState
493
515
  variant="no-access"
494
516
  :title="t('page.no_visible_blocks.title')"
@@ -9,6 +9,8 @@ export type MenuItemType = "label" | "link";
9
9
  export interface PageValidation {
10
10
  requiredQueryParams?: string[];
11
11
  customFunctionId?: string;
12
+ /** Where a page failing its validation sends the user, not-found aside. */
13
+ redirectTo?: string;
12
14
  }
13
15
 
14
16
  export interface MenuOptions {
@@ -48,10 +48,36 @@ import {
48
48
  dispatchQuickActionTarget,
49
49
  findServedQuickAction,
50
50
  } from "../../utils/dispatchQuickActionTarget";
51
+ import {
52
+ hasUrlVariables,
53
+ resolveLinkVariables,
54
+ } from "../../utils/urlVariables";
51
55
 
52
56
  /** Runs a confirmed action from inside its dialog (see `onConfirm`). */
53
57
  type ConfirmedRun = NonNullable<ConfirmOptions["onConfirm"]>;
54
58
 
59
+ /**
60
+ * A link target's URL with the page's `{{params.X}}` / `{{query.X}}` filled
61
+ * (a query pair without a value left out), then the row's `{field}`: a row
62
+ * action keeps the page's context (`?website={{query.website}}`) as a page
63
+ * header button does. Read when the link is followed, so it carries the URL
64
+ * as it is then.
65
+ */
66
+ function linkTargetUrl(url: string, rowData?: Data): string {
67
+ const withPageValues = hasUrlVariables(url)
68
+ ? resolveLinkVariables(url, currentPageUrlValues())
69
+ : url;
70
+ return rowData ? interpolateUrl(withPageValues, rowData) : withPageValues;
71
+ }
72
+
73
+ function currentPageUrlValues() {
74
+ const route = useDmsRoute();
75
+ return {
76
+ routeParams: useSiteLayout().findMatchingRoute(route.path)?.params,
77
+ routeQuery: route.query as Record<string, unknown>,
78
+ };
79
+ }
80
+
55
81
  /** Where the buttons run: the requests they send, what they refresh. */
56
82
  export interface ActionTargetsConfig {
57
83
  api: ReturnType<typeof $fetch.create>;
@@ -410,16 +436,11 @@ export function useActionTargets(config: ActionTargetsConfig) {
410
436
  const targetHandlers: Record<string, TargetHandler> = {
411
437
  page: (target, _label, rowData) => {
412
438
  const pageTarget = target as ActionTarget & { type: "page" };
413
- const url = rowData
414
- ? interpolateUrl(pageTarget.url, rowData)
415
- : pageTarget.url;
416
- navigateDms(url);
439
+ navigateDms(linkTargetUrl(pageTarget.url, rowData));
417
440
  },
418
441
  external: (target, _label, rowData) => {
419
442
  const externalTarget = target as ActionTarget & { type: "external" };
420
- const url = rowData
421
- ? interpolateUrl(externalTarget.url, rowData)
422
- : externalTarget.url;
443
+ const url = linkTargetUrl(externalTarget.url, rowData);
423
444
  if (typeof window === "undefined" || !url) return;
424
445
  if (externalTarget.newTab) {
425
446
  window.open(url, "_blank", "noopener,noreferrer");
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Whether what the user typed confirms a type-to-confirm dialog: the text,
3
+ * trimmed, equal to `confirmText` — ignoring case when asked, in the
4
+ * reader's locale rules (`acme` matches `ACME`).
5
+ */
6
+ export function isConfirmTextMatch(
7
+ typed: string,
8
+ confirmText: string,
9
+ isCaseInsensitive = false,
10
+ ): boolean {
11
+ const text = typed.trim();
12
+ if (!isCaseInsensitive) return text === confirmText;
13
+ return (
14
+ text.localeCompare(confirmText, undefined, { sensitivity: "accent" }) === 0
15
+ );
16
+ }
@@ -0,0 +1,53 @@
1
+ /** The bounds a number field takes. */
2
+ export interface NumberBounds {
3
+ min?: number;
4
+ max?: number;
5
+ }
6
+
7
+ /** Whether a number field's steppers can still move its value. */
8
+ export interface NumberStepperState {
9
+ incrementDisabled: boolean;
10
+ decrementDisabled: boolean;
11
+ }
12
+
13
+ export const BELOW_MIN_MESSAGE = "$dms.field_errors.too_small";
14
+ export const ABOVE_MAX_MESSAGE = "$dms.field_errors.too_large";
15
+
16
+ function isNumberValue(value: unknown): value is number {
17
+ return typeof value === "number" && !Number.isNaN(value);
18
+ }
19
+
20
+ /**
21
+ * The error of a value outside its bounds, for a field that keeps what was
22
+ * typed instead of clamping it; `undefined` when it is in range or empty.
23
+ */
24
+ export function numberRangeError(
25
+ value: unknown,
26
+ bounds: NumberBounds,
27
+ ): string | undefined {
28
+ if (!isNumberValue(value)) return undefined;
29
+ if (bounds.min !== undefined && value < bounds.min) return BELOW_MIN_MESSAGE;
30
+ if (bounds.max !== undefined && value > bounds.max) return ABOVE_MAX_MESSAGE;
31
+ return undefined;
32
+ }
33
+
34
+ /**
35
+ * The steppers of a field that does not hand its bounds to the input: they
36
+ * stop at the bounds the input no longer knows of.
37
+ */
38
+ export function numberStepperState(
39
+ value: unknown,
40
+ bounds: NumberBounds,
41
+ ): NumberStepperState {
42
+ const current = isNumberValue(value) ? value : undefined;
43
+ return {
44
+ incrementDisabled:
45
+ current !== undefined &&
46
+ bounds.max !== undefined &&
47
+ current >= bounds.max,
48
+ decrementDisabled:
49
+ current !== undefined &&
50
+ bounds.min !== undefined &&
51
+ current <= bounds.min,
52
+ };
53
+ }