@antelopejs/dms 0.4.4 → 0.4.5
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/config.d.ts +1 -0
- package/dist/config.js.map +1 -1
- package/dist/implementations/dms/page.d.ts +1 -0
- package/dist/implementations/dms/page.js +8 -1
- package/dist/implementations/dms/page.js.map +1 -1
- package/dist/implementations/dms/stale-session.d.ts +2 -0
- package/dist/implementations/dms/stale-session.js +16 -0
- package/dist/implementations/dms/stale-session.js.map +1 -0
- package/dist/implementations/dms-auth/index.d.ts +1 -0
- package/dist/implementations/dms-auth/index.js +19 -0
- package/dist/implementations/dms-auth/index.js.map +1 -1
- package/dist/routes/auth/client-ip.d.ts +7 -0
- package/dist/routes/auth/client-ip.js +42 -0
- package/dist/routes/auth/client-ip.js.map +1 -0
- package/dist/routes/auth/index.d.ts +1 -0
- package/dist/routes/auth/index.js +1 -0
- package/dist/routes/auth/index.js.map +1 -1
- package/dist/routes/auth.d.ts +3 -0
- package/dist/routes/auth.js +15 -4
- package/dist/routes/auth.js.map +1 -1
- package/dist/test/integration/auth/client-ip.test.d.ts +1 -0
- package/dist/test/integration/auth/client-ip.test.js +14 -0
- package/dist/test/integration/auth/client-ip.test.js.map +1 -0
- package/dist/test/integration/page-session.test.d.ts +1 -0
- package/dist/test/integration/page-session.test.js +52 -0
- package/dist/test/integration/page-session.test.js.map +1 -0
- package/dist/test/unit/client-ip.test.d.ts +1 -0
- package/dist/test/unit/client-ip.test.js +77 -0
- package/dist/test/unit/client-ip.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/table-view-tab-count-warning.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/table-view-tab-count-warning.test.js +96 -0
- package/dist/test/unit/interfaces/dms-base/table-view-tab-count-warning.test.js.map +1 -0
- package/frontend-vue/layers/dms-auth/app/middleware/auth.ts +5 -2
- package/frontend-vue/layers/dms-core/app/composables/useApiError.ts +27 -2
- package/frontend-vue/layers/dms-core/app/utils/menu.ts +26 -10
- package/frontend-vue/layers/dms-core/app/utils/routePath.ts +23 -0
- package/frontend-vue/layers/dms-core/i18n/locales/core-en-GB.json +3 -0
- package/frontend-vue/layers/dms-core/i18n/locales/core-fr-FR.json +3 -0
- package/frontend-vue/layers/dms-core/shared/types/runtime-config.d.ts +4 -1
- package/frontend-vue/layers/dms-layout/app/app.config.ts +9 -1
- package/frontend-vue/layers/dms-layout/app/components/navigation/CollapsedMenuItem.vue +0 -5
- package/frontend-vue/layers/dms-layout/app/components/profile/ProfileLanguage.vue +3 -23
- package/frontend-vue/layers/dms-layout/app/composables/profile/useUserLanguage.ts +33 -0
- package/frontend-vue/layers/dms-layout/app/plugins/command-palette-session.client.ts +2 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +31 -3
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/relationBadges.ts +36 -0
- package/frontend-vue/layers/dms-ui/app/build/types/tree.ts +5 -10
- package/frontend-vue/tests/api-error-title.test.ts +24 -0
- package/frontend-vue/tests/auth-middleware-error.test.ts +58 -0
- package/frontend-vue/tests/button-disabled-theme.test.ts +21 -0
- package/frontend-vue/tests/menu-navigation.test.ts +39 -10
- package/frontend-vue/tests/relation-badges.test.ts +32 -0
- package/frontend-vue/tests/user-language.test.ts +58 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const chai_1 = require("chai");
|
|
4
|
+
const client_ip_1 = require("../../routes/auth/client-ip");
|
|
5
|
+
const SOCKET_ADDRESS = "10.0.0.9";
|
|
6
|
+
const CLIENT_ADDRESS = "109.132.199.8";
|
|
7
|
+
const FRONTEND_EGRESS = "195.201.71.66";
|
|
8
|
+
const CDN_EDGE = "104.23.166.64";
|
|
9
|
+
const STAGING_CHAIN = `${CLIENT_ADDRESS},${FRONTEND_EGRESS},${CDN_EDGE}`;
|
|
10
|
+
function authConfig(overrides) {
|
|
11
|
+
return {
|
|
12
|
+
jwtSecret: "",
|
|
13
|
+
emailValidationTokenLifetime: 0,
|
|
14
|
+
passwordRecoverTokenLifetime: 0,
|
|
15
|
+
accessTokenLifetime: 0,
|
|
16
|
+
refreshTokenLifetime: 0,
|
|
17
|
+
userSensitiveKeys: [],
|
|
18
|
+
mustValidateEmail: false,
|
|
19
|
+
...overrides,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
describe("[unit] resolveClientIp", () => {
|
|
23
|
+
it("records the socket peer and ignores the header without trusted proxies", () => {
|
|
24
|
+
(0, chai_1.expect)((0, client_ip_1.resolveClientIp)({ socketAddress: SOCKET_ADDRESS, forwardedFor: STAGING_CHAIN }, 0)).to.equal(SOCKET_ADDRESS);
|
|
25
|
+
});
|
|
26
|
+
it("walks back one entry per trusted proxy from the right", () => {
|
|
27
|
+
const source = {
|
|
28
|
+
socketAddress: SOCKET_ADDRESS,
|
|
29
|
+
forwardedFor: STAGING_CHAIN,
|
|
30
|
+
};
|
|
31
|
+
(0, chai_1.expect)((0, client_ip_1.resolveClientIp)(source, 1)).to.equal(CDN_EDGE);
|
|
32
|
+
(0, chai_1.expect)((0, client_ip_1.resolveClientIp)(source, 2)).to.equal(FRONTEND_EGRESS);
|
|
33
|
+
(0, chai_1.expect)((0, client_ip_1.resolveClientIp)(source, 3)).to.equal(CLIENT_ADDRESS);
|
|
34
|
+
});
|
|
35
|
+
it("never reaches the entries a client wrote ahead of the trusted ones", () => {
|
|
36
|
+
(0, chai_1.expect)((0, client_ip_1.resolveClientIp)({
|
|
37
|
+
socketAddress: SOCKET_ADDRESS,
|
|
38
|
+
forwardedFor: `6.6.6.6, 7.7.7.7, ${CLIENT_ADDRESS}`,
|
|
39
|
+
}, 1)).to.equal(CLIENT_ADDRESS);
|
|
40
|
+
});
|
|
41
|
+
it("yields the leftmost entry of a chain shorter than configured", () => {
|
|
42
|
+
(0, chai_1.expect)((0, client_ip_1.resolveClientIp)({ socketAddress: SOCKET_ADDRESS, forwardedFor: CLIENT_ADDRESS }, 3)).to.equal(CLIENT_ADDRESS);
|
|
43
|
+
});
|
|
44
|
+
it("falls back to the socket peer without a header", () => {
|
|
45
|
+
(0, chai_1.expect)((0, client_ip_1.resolveClientIp)({ socketAddress: SOCKET_ADDRESS, forwardedFor: undefined }, 2)).to.equal(SOCKET_ADDRESS);
|
|
46
|
+
(0, chai_1.expect)((0, client_ip_1.resolveClientIp)({ socketAddress: SOCKET_ADDRESS, forwardedFor: " , " }, 2)).to.equal(SOCKET_ADDRESS);
|
|
47
|
+
});
|
|
48
|
+
it("returns an empty string when nothing is known", () => {
|
|
49
|
+
(0, chai_1.expect)((0, client_ip_1.resolveClientIp)({ socketAddress: undefined, forwardedFor: undefined }, 1)).to.equal("");
|
|
50
|
+
});
|
|
51
|
+
it("keeps IPv6 addresses whole and trims the separators", () => {
|
|
52
|
+
(0, chai_1.expect)((0, client_ip_1.resolveClientIp)({ socketAddress: "::1", forwardedFor: " 2001:db8::1 , fd00::2 " }, 2)).to.equal("2001:db8::1");
|
|
53
|
+
});
|
|
54
|
+
it("unmaps IPv4-mapped IPv6 addresses", () => {
|
|
55
|
+
(0, chai_1.expect)((0, client_ip_1.resolveClientIp)({ socketAddress: "::ffff:127.0.0.1", forwardedFor: undefined }, 0)).to.equal("127.0.0.1");
|
|
56
|
+
(0, chai_1.expect)((0, client_ip_1.resolveClientIp)({ socketAddress: "::ffff:abcd", forwardedFor: "" }, 0)).to.equal("::ffff:abcd");
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
describe("[unit] trustedProxyCount", () => {
|
|
60
|
+
it("trusts no proxy by default", () => {
|
|
61
|
+
(0, chai_1.expect)((0, client_ip_1.trustedProxyCount)(authConfig({}))).to.equal(0);
|
|
62
|
+
});
|
|
63
|
+
it("reads trustedProxies", () => {
|
|
64
|
+
(0, chai_1.expect)((0, client_ip_1.trustedProxyCount)(authConfig({ trustedProxies: 3 }))).to.equal(3);
|
|
65
|
+
});
|
|
66
|
+
it("maps the deprecated oauth.trustProxy to one hop", () => {
|
|
67
|
+
(0, chai_1.expect)((0, client_ip_1.trustedProxyCount)(authConfig({ oauth: { trustProxy: true } }))).to.equal(1);
|
|
68
|
+
});
|
|
69
|
+
it("prefers trustedProxies over oauth.trustProxy", () => {
|
|
70
|
+
(0, chai_1.expect)((0, client_ip_1.trustedProxyCount)(authConfig({ trustedProxies: 0, oauth: { trustProxy: true } }))).to.equal(0);
|
|
71
|
+
});
|
|
72
|
+
it("ignores a value that is not a non-negative integer", () => {
|
|
73
|
+
(0, chai_1.expect)((0, client_ip_1.trustedProxyCount)(authConfig({ trustedProxies: -1 }))).to.equal(0);
|
|
74
|
+
(0, chai_1.expect)((0, client_ip_1.trustedProxyCount)(authConfig({ trustedProxies: 1.5 }))).to.equal(0);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
//# sourceMappingURL=client-ip.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-ip.test.js","sourceRoot":"","sources":["../../../src/test/unit/client-ip.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,96 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
const interface_api_1 = require("@antelopejs/interface-api");
|
|
10
|
+
const interface_data_api_1 = require("@antelopejs/interface-data-api");
|
|
11
|
+
const interface_database_decorators_1 = require("@antelopejs/interface-database-decorators");
|
|
12
|
+
const chai_1 = require("chai");
|
|
13
|
+
const constants_1 = require("@antelopejs/interface-dms/constants");
|
|
14
|
+
const table_view_1 = require("@antelopejs/interface-dms/base/table-view");
|
|
15
|
+
const logging_1 = require("../../../helpers/logging");
|
|
16
|
+
const TABLE = "tab-count-warning-rows";
|
|
17
|
+
const MISSING_ROUTE = "count/batch";
|
|
18
|
+
const TABS = [
|
|
19
|
+
{
|
|
20
|
+
id: "open",
|
|
21
|
+
label: "Open",
|
|
22
|
+
filters: [{ accessorKey: "status", value: "open", mode: "is" }],
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
let Row = class Row extends interface_database_decorators_1.Table {
|
|
26
|
+
};
|
|
27
|
+
Row = __decorate([
|
|
28
|
+
(0, interface_database_decorators_1.RegisterTable)(TABLE, constants_1.CORE_SCHEMA_NAME)
|
|
29
|
+
], Row);
|
|
30
|
+
let CountOnlyAPI = class CountOnlyAPI extends (0, interface_data_api_1.DataController)(Row, { list: table_view_1.TableViewRoutes.List, count: table_view_1.TableViewRoutes.Count }, (0, interface_api_1.Controller)("/api/tab-count-warning/count-only")) {
|
|
31
|
+
};
|
|
32
|
+
CountOnlyAPI = __decorate([
|
|
33
|
+
(0, interface_data_api_1.RegisterDataController)()
|
|
34
|
+
], CountOnlyAPI);
|
|
35
|
+
let RepeatedAPI = class RepeatedAPI extends (0, interface_data_api_1.DataController)(Row, { list: table_view_1.TableViewRoutes.List }, (0, interface_api_1.Controller)("/api/tab-count-warning/repeated")) {
|
|
36
|
+
};
|
|
37
|
+
RepeatedAPI = __decorate([
|
|
38
|
+
(0, interface_data_api_1.RegisterDataController)()
|
|
39
|
+
], RepeatedAPI);
|
|
40
|
+
let BatchAPI = class BatchAPI extends (0, interface_data_api_1.DataController)(Row, { list: table_view_1.TableViewRoutes.List, countBatch: table_view_1.TableViewRoutes.CountBatch }, (0, interface_api_1.Controller)("/api/tab-count-warning/batch")) {
|
|
41
|
+
};
|
|
42
|
+
BatchAPI = __decorate([
|
|
43
|
+
(0, interface_data_api_1.RegisterDataController)()
|
|
44
|
+
], BatchAPI);
|
|
45
|
+
let RenamedBatchAPI = class RenamedBatchAPI extends (0, interface_data_api_1.DataController)(Row, { list: table_view_1.TableViewRoutes.List, tabCounters: table_view_1.TableViewRoutes.CountBatch }, (0, interface_api_1.Controller)("/api/tab-count-warning/renamed")) {
|
|
46
|
+
};
|
|
47
|
+
RenamedBatchAPI = __decorate([
|
|
48
|
+
(0, interface_data_api_1.RegisterDataController)()
|
|
49
|
+
], RenamedBatchAPI);
|
|
50
|
+
let NoTabsAPI = class NoTabsAPI extends (0, interface_data_api_1.DataController)(Row, { list: table_view_1.TableViewRoutes.List }, (0, interface_api_1.Controller)("/api/tab-count-warning/no-tabs")) {
|
|
51
|
+
};
|
|
52
|
+
NoTabsAPI = __decorate([
|
|
53
|
+
(0, interface_data_api_1.RegisterDataController)()
|
|
54
|
+
], NoTabsAPI);
|
|
55
|
+
function countBatchWarnings(register) {
|
|
56
|
+
const captured = (0, logging_1.captureWarnings)();
|
|
57
|
+
try {
|
|
58
|
+
register();
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
captured.restore();
|
|
62
|
+
}
|
|
63
|
+
return captured.messages.filter((message) => message.includes(MISSING_ROUTE));
|
|
64
|
+
}
|
|
65
|
+
describe("[unit] interfaces/dms-base/table-view — filter tabs require countBatch", () => {
|
|
66
|
+
it("warns when a table with tabs is mounted on a controller without countBatch", () => {
|
|
67
|
+
const warnings = countBatchWarnings(() => (0, table_view_1.TableView)(CountOnlyAPI, { tabs: TABS, realtime: false }));
|
|
68
|
+
(0, chai_1.expect)(warnings).to.have.length(1);
|
|
69
|
+
(0, chai_1.expect)(warnings[0]).to.include("CountOnlyAPI");
|
|
70
|
+
(0, chai_1.expect)(warnings[0]).to.include("/api/tab-count-warning/count-only");
|
|
71
|
+
(0, chai_1.expect)(warnings[0]).to.include("TableViewRoutes.CountBatch");
|
|
72
|
+
});
|
|
73
|
+
it("warns once per controller however many tables mount it", () => {
|
|
74
|
+
const warnings = countBatchWarnings(() => {
|
|
75
|
+
(0, table_view_1.TableView)(RepeatedAPI, { tabs: TABS, realtime: false });
|
|
76
|
+
(0, table_view_1.TableView)(RepeatedAPI, { tabs: TABS, realtime: false });
|
|
77
|
+
});
|
|
78
|
+
(0, chai_1.expect)(warnings).to.have.length(1);
|
|
79
|
+
});
|
|
80
|
+
it("stays silent when the controller mounts countBatch", () => {
|
|
81
|
+
const warnings = countBatchWarnings(() => (0, table_view_1.TableView)(BatchAPI, { tabs: TABS, realtime: false }));
|
|
82
|
+
(0, chai_1.expect)(warnings).to.be.empty;
|
|
83
|
+
});
|
|
84
|
+
it("stays silent when countBatch is mounted under another key", () => {
|
|
85
|
+
const warnings = countBatchWarnings(() => (0, table_view_1.TableView)(RenamedBatchAPI, { tabs: TABS, realtime: false }));
|
|
86
|
+
(0, chai_1.expect)(warnings).to.be.empty;
|
|
87
|
+
});
|
|
88
|
+
it("stays silent when the table declares no tabs", () => {
|
|
89
|
+
const warnings = countBatchWarnings(() => {
|
|
90
|
+
(0, table_view_1.TableView)(NoTabsAPI, { realtime: false });
|
|
91
|
+
(0, table_view_1.TableView)(NoTabsAPI, { tabs: [], realtime: false });
|
|
92
|
+
});
|
|
93
|
+
(0, chai_1.expect)(warnings).to.be.empty;
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
//# sourceMappingURL=table-view-tab-count-warning.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"table-view-tab-count-warning.test.js","sourceRoot":"","sources":["../../../../../src/test/unit/interfaces/dms-base/table-view-tab-count-warning.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -72,8 +72,11 @@ async function isPublicAccessRoute(to: ReturnType<typeof useDmsRoute>) {
|
|
|
72
72
|
export default defineDmsMiddleware(async (to) => {
|
|
73
73
|
if (to.matched.length === 0) return;
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
// An error the page already raised (a failed `validation`, a missing page)
|
|
76
|
+
// is committed and rendered with its own status. Re-throwing it here would
|
|
77
|
+
// reject the pending navigation the SSR render awaits, turning that 404 into
|
|
78
|
+
// a 500 fallback render.
|
|
79
|
+
if (useError().value) return abortNavigation();
|
|
77
80
|
|
|
78
81
|
const options = to.meta.auth as MiddlewareMeta | undefined;
|
|
79
82
|
if (!options) return;
|
|
@@ -6,12 +6,16 @@ interface Options {
|
|
|
6
6
|
|
|
7
7
|
interface ApiError {
|
|
8
8
|
statusCode?: number;
|
|
9
|
+
status?: number;
|
|
9
10
|
message?: string;
|
|
10
11
|
data?: unknown;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
const
|
|
14
|
+
const SERVER_ERROR_TITLE_KEY = "error.500.title";
|
|
15
|
+
const CLIENT_ERROR_TITLE_KEY = "error.request_refused.title";
|
|
14
16
|
const FALLBACK_ERROR_KEY = "error.500.description";
|
|
17
|
+
const CLIENT_ERROR_STATUS_MIN = 400;
|
|
18
|
+
const CLIENT_ERROR_STATUS_MAX = 499;
|
|
15
19
|
|
|
16
20
|
function isApiError(error: unknown): error is ApiError {
|
|
17
21
|
return typeof error === "object" && error !== null;
|
|
@@ -32,6 +36,27 @@ function resolveDescription(error: ApiError): string {
|
|
|
32
36
|
return FALLBACK_ERROR_KEY;
|
|
33
37
|
}
|
|
34
38
|
|
|
39
|
+
function isClientErrorStatus(status: number | undefined): boolean {
|
|
40
|
+
return (
|
|
41
|
+
status !== undefined &&
|
|
42
|
+
status >= CLIENT_ERROR_STATUS_MIN &&
|
|
43
|
+
status <= CLIENT_ERROR_STATUS_MAX
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The i18n title key matching a fetch error's HTTP status: a 4xx is a request
|
|
49
|
+
* the backend refused, anything else (5xx, network failure) a server error.
|
|
50
|
+
*/
|
|
51
|
+
export function resolveApiErrorTitle(error: unknown): string {
|
|
52
|
+
const status = isApiError(error)
|
|
53
|
+
? (error.statusCode ?? error.status)
|
|
54
|
+
: undefined;
|
|
55
|
+
return isClientErrorStatus(status)
|
|
56
|
+
? CLIENT_ERROR_TITLE_KEY
|
|
57
|
+
: SERVER_ERROR_TITLE_KEY;
|
|
58
|
+
}
|
|
59
|
+
|
|
35
60
|
/**
|
|
36
61
|
* Extract the backend's error body from a fetch error — an i18n message key
|
|
37
62
|
* per the API error contract — falling back to the generic 500 key.
|
|
@@ -46,7 +71,7 @@ export function useApiError(error: unknown, options?: Options) {
|
|
|
46
71
|
const { t } = dmsApp.$i18n;
|
|
47
72
|
const translate = (message: string) => resolveApiMessage(t, message);
|
|
48
73
|
|
|
49
|
-
const title = options?.title ||
|
|
74
|
+
const title = options?.title || resolveApiErrorTitle(error);
|
|
50
75
|
const description = options?.description || resolveApiErrorMessage(error);
|
|
51
76
|
|
|
52
77
|
toast.add({
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { NavigationMenuItem } from "@nuxt/ui";
|
|
2
|
+
import { parseLinkQuery, stripQueryAndHash } from "./routePath";
|
|
2
3
|
|
|
3
4
|
// Menu items carry the registered page/category's dot-separated `fullId`
|
|
4
5
|
// (e.g. `pages.erp.orders`) plus the DMS semantic fields; Nuxt UI's
|
|
@@ -88,7 +89,9 @@ const matchesRoutePrefix = (currentPath: string, to: string): boolean => {
|
|
|
88
89
|
const resolveItemTarget = (
|
|
89
90
|
to: NavigationMenuItem["to"],
|
|
90
91
|
): MenuItemTarget | null => {
|
|
91
|
-
if (typeof to === "string")
|
|
92
|
+
if (typeof to === "string") {
|
|
93
|
+
return { path: stripQueryAndHash(to), query: parseLinkQuery(to) };
|
|
94
|
+
}
|
|
92
95
|
const target = to as MenuItemTarget | undefined;
|
|
93
96
|
if (target && typeof target.path === "string") {
|
|
94
97
|
return { path: target.path, query: target.query };
|
|
@@ -96,9 +99,11 @@ const resolveItemTarget = (
|
|
|
96
99
|
return null;
|
|
97
100
|
};
|
|
98
101
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
+
const hasQuery = (target: MenuItemTarget | null): boolean =>
|
|
103
|
+
Object.keys(target?.query ?? {}).length > 0;
|
|
104
|
+
|
|
105
|
+
// Subset match: the entry's own parameters must be the current ones, while
|
|
106
|
+
// unrelated parameters the page adds (tabs, filters) are ignored.
|
|
102
107
|
const queryMatches = (
|
|
103
108
|
declared: MenuItemTarget["query"],
|
|
104
109
|
currentQuery: Record<string, unknown>,
|
|
@@ -133,18 +138,29 @@ export const isMenuItemActive = (
|
|
|
133
138
|
return matchedFullId !== null && fullId === matchedFullId;
|
|
134
139
|
};
|
|
135
140
|
|
|
136
|
-
// Only the boolean `true` is set: Nuxt UI forwards an explicit
|
|
137
|
-
// underlying link, and a `false` there would suppress the
|
|
138
|
-
//
|
|
141
|
+
// Only the boolean `true` is set on plain entries: Nuxt UI forwards an explicit
|
|
142
|
+
// `active` to the underlying link, and a `false` there would suppress the
|
|
143
|
+
// built-in route match. Entries carrying a query always get an explicit value,
|
|
144
|
+
// because that built-in match is a URL prefix test that would light up
|
|
145
|
+
// `?project=a` on `?project=ab`.
|
|
146
|
+
const resolveActiveState = (
|
|
147
|
+
item: NavigationMenuItem,
|
|
148
|
+
matchedFullId: string | null,
|
|
149
|
+
currentRoute: MenuRouteState,
|
|
150
|
+
): Partial<Pick<NavigationMenuItem, "active">> => {
|
|
151
|
+
if (isMenuItemActive(item, matchedFullId, currentRoute)) {
|
|
152
|
+
return { active: true };
|
|
153
|
+
}
|
|
154
|
+
return hasQuery(resolveItemTarget(item.to)) ? { active: false } : {};
|
|
155
|
+
};
|
|
156
|
+
|
|
139
157
|
export const addActiveStateToMenuItem = (
|
|
140
158
|
item: NavigationMenuItem,
|
|
141
159
|
matchedFullId: string | null,
|
|
142
160
|
currentRoute: MenuRouteState,
|
|
143
161
|
): NavigationMenuItem => ({
|
|
144
162
|
...item,
|
|
145
|
-
...(
|
|
146
|
-
? { active: true }
|
|
147
|
-
: {}),
|
|
163
|
+
...resolveActiveState(item, matchedFullId, currentRoute),
|
|
148
164
|
children: item.children?.map((child) =>
|
|
149
165
|
addActiveStateToMenuItem(child, matchedFullId, currentRoute),
|
|
150
166
|
),
|
|
@@ -9,3 +9,26 @@ export function stripQueryAndHash(link: string): string {
|
|
|
9
9
|
const [path] = link.split(QUERY_OR_HASH);
|
|
10
10
|
return path ?? link;
|
|
11
11
|
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A link to `path` narrowed by `query`, as the single string every link
|
|
15
|
+
* renderer accepts: the Inertia-backed `ULink` only takes a string `to`, and
|
|
16
|
+
* stringifies an object into `[object Object]`.
|
|
17
|
+
*/
|
|
18
|
+
export function buildLinkWithQuery(
|
|
19
|
+
path: string,
|
|
20
|
+
query: Record<string, string> | undefined,
|
|
21
|
+
): string {
|
|
22
|
+
const search = new URLSearchParams(query ?? {}).toString();
|
|
23
|
+
return search === "" ? path : `${path}?${search}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The query parameters a link carries, the inverse of `buildLinkWithQuery`. */
|
|
27
|
+
export function parseLinkQuery(link: string): Record<string, string> {
|
|
28
|
+
const [withoutHash = ""] = link.split("#");
|
|
29
|
+
const queryStart = withoutHash.indexOf("?");
|
|
30
|
+
if (queryStart === -1) return {};
|
|
31
|
+
return Object.fromEntries(
|
|
32
|
+
new URLSearchParams(withoutHash.slice(queryStart + 1)),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -37,6 +37,9 @@
|
|
|
37
37
|
"title": "Server error",
|
|
38
38
|
"description": "An unexpected error occurred. Please try again later."
|
|
39
39
|
},
|
|
40
|
+
"request_refused": {
|
|
41
|
+
"title": "Request refused"
|
|
42
|
+
},
|
|
40
43
|
"recovering": {
|
|
41
44
|
"title": "Restoring your session...",
|
|
42
45
|
"description": "Your session has expired. Refreshing your access and reloading the page."
|
|
@@ -37,6 +37,9 @@
|
|
|
37
37
|
"title": "Erreur serveur",
|
|
38
38
|
"description": "Une erreur inattendue s'est produite. Veuillez réessayer plus tard."
|
|
39
39
|
},
|
|
40
|
+
"request_refused": {
|
|
41
|
+
"title": "Requête refusée"
|
|
42
|
+
},
|
|
40
43
|
"recovering": {
|
|
41
44
|
"title": "Restauration de votre session...",
|
|
42
45
|
"description": "Votre session a expiré. Renouvellement de l'accès et rechargement de la page."
|
|
@@ -43,7 +43,10 @@ interface DmsPrivateConfig {
|
|
|
43
43
|
relaySecret: string;
|
|
44
44
|
/**
|
|
45
45
|
* Whether a trusted reverse proxy sits immediately in front and appends
|
|
46
|
-
* to `x-forwarded-for
|
|
46
|
+
* to `x-forwarded-for`.
|
|
47
|
+
*
|
|
48
|
+
* @deprecated Mirrors the deprecated `auth.oauth.trustProxy`. The frontend
|
|
49
|
+
* server keys its OAuth rate limit on its own `DMS_TRUSTED_PROXY_HOPS`.
|
|
47
50
|
*/
|
|
48
51
|
trustProxy?: boolean;
|
|
49
52
|
};
|
|
@@ -134,7 +134,15 @@ export default {
|
|
|
134
134
|
button: {
|
|
135
135
|
slots: {
|
|
136
136
|
// Tactile press + full transition (design .btn active:scale(.97)).
|
|
137
|
-
|
|
137
|
+
// Nuxt UI only fades a disabled button to 75%, which still reads as
|
|
138
|
+
// enabled on a filled primary: fade it further and drop the press and
|
|
139
|
+
// the inset highlight so every variant looks inert.
|
|
140
|
+
base: [
|
|
141
|
+
"transition active:scale-[0.98]",
|
|
142
|
+
"disabled:opacity-50 aria-disabled:opacity-50",
|
|
143
|
+
"disabled:active:scale-100 aria-disabled:active:scale-100",
|
|
144
|
+
"disabled:shadow-none aria-disabled:shadow-none",
|
|
145
|
+
].join(" "),
|
|
138
146
|
},
|
|
139
147
|
compoundVariants: [
|
|
140
148
|
// Inset top highlight on the solid primary fill (design .btn-primary).
|
|
@@ -82,11 +82,6 @@ function onLinkHover(): void {
|
|
|
82
82
|
const { to } = props.item;
|
|
83
83
|
if (typeof to === "string") {
|
|
84
84
|
prefetchPageLayout(stripQueryAndHash(to));
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
const path = (to as MenuItemTarget | undefined)?.path;
|
|
88
|
-
if (path) {
|
|
89
|
-
prefetchPageLayout(path);
|
|
90
85
|
}
|
|
91
86
|
}
|
|
92
87
|
</script>
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
const { locale
|
|
2
|
+
const { locale } = useI18n();
|
|
3
3
|
const { uniqueLocales } = useUniqueLocales();
|
|
4
|
-
const {
|
|
5
|
-
const { user, fetch: refreshUser } = useUserSession();
|
|
4
|
+
const { changeLanguage } = useUserLanguage();
|
|
6
5
|
|
|
7
6
|
const languageOptions = computed(() =>
|
|
8
7
|
uniqueLocales.value.map((lang) => ({
|
|
@@ -10,25 +9,6 @@ const languageOptions = computed(() =>
|
|
|
10
9
|
value: lang.code,
|
|
11
10
|
})),
|
|
12
11
|
);
|
|
13
|
-
|
|
14
|
-
const handleLanguageChange = async (newLanguage: "en" | "fr") => {
|
|
15
|
-
setLocale(newLanguage);
|
|
16
|
-
|
|
17
|
-
try {
|
|
18
|
-
await $authFetch("/settings/user/profile", {
|
|
19
|
-
method: "POST",
|
|
20
|
-
body: {
|
|
21
|
-
name: user.value?.name,
|
|
22
|
-
email: user.value?.email,
|
|
23
|
-
language: newLanguage,
|
|
24
|
-
},
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
await refreshUser();
|
|
28
|
-
} catch {
|
|
29
|
-
/* ignore — silently skip the update on network / backend errors */
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
12
|
</script>
|
|
33
13
|
|
|
34
14
|
<template>
|
|
@@ -62,7 +42,7 @@ const handleLanguageChange = async (newLanguage: "en" | "fr") => {
|
|
|
62
42
|
class="w-full"
|
|
63
43
|
:items="languageOptions"
|
|
64
44
|
:model-value="locale"
|
|
65
|
-
@update:model-value="
|
|
45
|
+
@update:model-value="changeLanguage"
|
|
66
46
|
/>
|
|
67
47
|
</div>
|
|
68
48
|
</section>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const PROFILE_ENDPOINT = "/settings/user/profile";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Switches the interface language and stores it on the user's profile, so the
|
|
5
|
+
* `language-sync` plugin re-applies it on the next load instead of reverting
|
|
6
|
+
* to the previously stored language. Every language switcher goes through it.
|
|
7
|
+
*/
|
|
8
|
+
export function useUserLanguage() {
|
|
9
|
+
const dmsApp = useDmsApp();
|
|
10
|
+
const { $authFetch } = useAuthFetch();
|
|
11
|
+
const { user, fetch: refreshUser } = useUserSession();
|
|
12
|
+
|
|
13
|
+
async function changeLanguage(language: string): Promise<void> {
|
|
14
|
+
await dmsApp.$i18n.setLocale(language);
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
await $authFetch(PROFILE_ENDPOINT, {
|
|
18
|
+
method: "POST",
|
|
19
|
+
body: {
|
|
20
|
+
name: user.value?.name,
|
|
21
|
+
email: user.value?.email,
|
|
22
|
+
language,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
await refreshUser();
|
|
27
|
+
} catch {
|
|
28
|
+
/* ignore — silently skip the update on network / backend errors */
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return { changeLanguage };
|
|
33
|
+
}
|
|
@@ -34,6 +34,7 @@ export default defineDmsPlugin(() => {
|
|
|
34
34
|
const { i18n, translate } = getPluginI18n();
|
|
35
35
|
const toast = useToast();
|
|
36
36
|
const { logout } = useLogout();
|
|
37
|
+
const { changeLanguage } = useUserLanguage();
|
|
37
38
|
const { copy } = useClipboard({ legacy: true });
|
|
38
39
|
|
|
39
40
|
const localeItems = (): CommandPaletteItem[] =>
|
|
@@ -41,7 +42,7 @@ export default defineDmsPlugin(() => {
|
|
|
41
42
|
label: locale.name ?? locale.code,
|
|
42
43
|
active: i18n.locale.value === locale.code,
|
|
43
44
|
onSelect: () => {
|
|
44
|
-
void
|
|
45
|
+
void changeLanguage(locale.code);
|
|
45
46
|
},
|
|
46
47
|
}));
|
|
47
48
|
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { defineAsyncComponent, type VNode } from "vue";
|
|
2
2
|
import type { DataType } from "#dms-core/app/composables/data-types/useDataType";
|
|
3
3
|
import UAvatar from "@nuxt/ui/components/Avatar.vue";
|
|
4
|
+
import UBadge from "@nuxt/ui/components/Badge.vue";
|
|
4
5
|
import UIcon from "@nuxt/ui/runtime/vue/components/Icon.vue";
|
|
5
6
|
import ULink from "@nuxt/ui/components/Link.vue";
|
|
7
|
+
import { buildRelationBadges } from "./relationBadges";
|
|
6
8
|
|
|
7
9
|
const FilePreview = defineAsyncComponent(
|
|
8
10
|
() => import("../../../components/table-view/FilePreview.vue"),
|
|
@@ -37,6 +39,9 @@ const LINK_CLASS =
|
|
|
37
39
|
const LINK_ICON_CLASS = "size-3 text-primary/70 flex-shrink-0";
|
|
38
40
|
const LINK_ICON_NAME = "i-ph-arrow-up-right-light";
|
|
39
41
|
const DEFAULT_RELATION_VALUE_KEY = "_id";
|
|
42
|
+
const DEFAULT_RELATION_LABEL_KEY = "name";
|
|
43
|
+
const RELATION_BADGES_CLASS = "inline-flex flex-wrap items-center gap-1";
|
|
44
|
+
const RELATION_BADGE_CLASS = "max-w-40";
|
|
40
45
|
const PERMISSION_SEPARATOR = ".";
|
|
41
46
|
|
|
42
47
|
interface LinkRendererOptions {
|
|
@@ -263,18 +268,41 @@ function countLeafPermissions(value: string[]): number {
|
|
|
263
268
|
return value.filter((id) => !parentIds.has(id)).length;
|
|
264
269
|
}
|
|
265
270
|
|
|
271
|
+
function renderRelationBadge(label: string, title?: string) {
|
|
272
|
+
return h(UBadge, {
|
|
273
|
+
label,
|
|
274
|
+
title: title ?? label,
|
|
275
|
+
color: "neutral",
|
|
276
|
+
variant: "subtle",
|
|
277
|
+
size: Size.small,
|
|
278
|
+
class: RELATION_BADGE_CLASS,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function renderRelationBadges(values: unknown[], labelKey: string) {
|
|
283
|
+
const { processI18n } = useTranslation();
|
|
284
|
+
const { visible, hidden } = buildRelationBadges(values, labelKey);
|
|
285
|
+
const badges = visible.map((label) =>
|
|
286
|
+
renderRelationBadge(processI18n(label)),
|
|
287
|
+
);
|
|
288
|
+
if (hidden.length > 0) {
|
|
289
|
+
const hiddenLabels = hidden.map((label) => processI18n(label)).join(", ");
|
|
290
|
+
badges.push(renderRelationBadge(`+${hidden.length}`, hiddenLabels));
|
|
291
|
+
}
|
|
292
|
+
return h("span", { class: RELATION_BADGES_CLASS }, badges);
|
|
293
|
+
}
|
|
294
|
+
|
|
266
295
|
function renderRelation(value: unknown, options: unknown) {
|
|
267
|
-
const { t } = useI18n();
|
|
268
296
|
const opts = options as RelationOptions | undefined;
|
|
297
|
+
const labelKey = opts?.keyMapping?.label || DEFAULT_RELATION_LABEL_KEY;
|
|
269
298
|
|
|
270
299
|
if (opts?.multiple && Array.isArray(value)) {
|
|
271
|
-
return
|
|
300
|
+
return renderRelationBadges(value, labelKey);
|
|
272
301
|
}
|
|
273
302
|
|
|
274
303
|
if (!value || !isObject(value)) return value;
|
|
275
304
|
|
|
276
305
|
const valueObject = value as Record<string, string>;
|
|
277
|
-
const labelKey = opts?.keyMapping?.label || "name";
|
|
278
306
|
const avatarKey = opts?.keyMapping?.avatar;
|
|
279
307
|
const label = valueObject[labelKey] || String(value);
|
|
280
308
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** How many labels a multi-value relation cell shows before collapsing. */
|
|
2
|
+
export const MAX_RELATION_BADGES = 3;
|
|
3
|
+
|
|
4
|
+
/** The labels a multi-value cell shows, and how many it folds into `+N`. */
|
|
5
|
+
export interface RelationBadges {
|
|
6
|
+
visible: string[];
|
|
7
|
+
hidden: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function readRelationLabel(item: unknown, labelKey: string): string {
|
|
11
|
+
if (typeof item === "object" && item !== null) {
|
|
12
|
+
const label = (item as Record<string, unknown>)[labelKey];
|
|
13
|
+
return label === undefined || label === null ? "" : String(label);
|
|
14
|
+
}
|
|
15
|
+
return item === undefined || item === null ? "" : String(item);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Split a multi-value relation into the labels rendered as badges and the ones
|
|
20
|
+
* folded into a trailing `+N`. List rows carry the related rows resolved by the
|
|
21
|
+
* relation join, so each item is read through the `label` key mapping; a raw id
|
|
22
|
+
* (join skipped) is shown as is rather than dropped.
|
|
23
|
+
*/
|
|
24
|
+
export function buildRelationBadges(
|
|
25
|
+
values: unknown[],
|
|
26
|
+
labelKey: string,
|
|
27
|
+
maxVisible = MAX_RELATION_BADGES,
|
|
28
|
+
): RelationBadges {
|
|
29
|
+
const labels = values
|
|
30
|
+
.map((item) => readRelationLabel(item, labelKey))
|
|
31
|
+
.filter((label) => label !== "");
|
|
32
|
+
return {
|
|
33
|
+
visible: labels.slice(0, maxVisible),
|
|
34
|
+
hidden: labels.slice(maxVisible),
|
|
35
|
+
};
|
|
36
|
+
}
|