@antelopejs/dms 0.7.13 → 0.7.15
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 +44 -0
- package/dist/db/models/userNotifications.model.js +3 -27
- package/dist/db/models/userNotifications.model.js.map +1 -1
- package/dist/implementations/dms/client-ip.d.ts +2 -0
- package/dist/implementations/dms/client-ip.js +9 -0
- package/dist/implementations/dms/client-ip.js.map +1 -0
- package/dist/implementations/dms/frontend-modules.d.ts +4 -2
- package/dist/implementations/dms/frontend-modules.js +4 -0
- package/dist/implementations/dms/frontend-modules.js.map +1 -1
- package/dist/implementations/dms/frontend-public-endpoints.d.ts +3 -0
- package/dist/implementations/dms/frontend-public-endpoints.js +70 -0
- package/dist/implementations/dms/frontend-public-endpoints.js.map +1 -0
- package/dist/implementations/dms/manifest.d.ts +1 -0
- package/dist/implementations/dms/manifest.js +3 -1
- package/dist/implementations/dms/manifest.js.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/routes/auth/client-ip.d.ts +1 -4
- package/dist/routes/auth/client-ip.js.map +1 -1
- package/dist/test/integration/notifications.test.js +6 -3
- package/dist/test/integration/notifications.test.js.map +1 -1
- package/dist/test/unit/implementations/dms/frontend-public-endpoints.test.d.ts +1 -0
- package/dist/test/unit/implementations/dms/frontend-public-endpoints.test.js +74 -0
- package/dist/test/unit/implementations/dms/frontend-public-endpoints.test.js.map +1 -0
- package/dist/test/unit/implementations/dms/manifest-strip.test.js +5 -0
- package/dist/test/unit/implementations/dms/manifest-strip.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms/client-ip.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms/client-ip.test.js +61 -0
- package/dist/test/unit/interfaces/dms/client-ip.test.js.map +1 -0
- package/frontend-vue/layers/dms-core/app/composables/period/periodLink.ts +60 -0
- package/frontend-vue/layers/dms-core/app/composables/translation/useTranslation.ts +9 -4
- package/frontend-vue/layers/dms-core/app/composables/useFieldErrors.ts +34 -4
- package/frontend-vue/layers/dms-core/app/types/confirm-dialog.ts +2 -0
- package/frontend-vue/layers/dms-layout/app/build/utils/pageValidationRedirect.ts +32 -0
- package/frontend-vue/layers/dms-layout/app/emails/components/EmailLayout.vue +35 -12
- package/frontend-vue/layers/dms-layout/app/emails/components/email-logo.ts +54 -0
- package/frontend-vue/layers/dms-layout/app/emails/components/index.ts +2 -0
- package/frontend-vue/layers/dms-layout/app/pages/[...slug].vue +29 -7
- package/frontend-vue/layers/dms-layout/app/types/page.ts +2 -0
- package/frontend-vue/layers/dms-ui/app/build/components/table/FiltersRow.vue +39 -2
- package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +18 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +28 -7
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/confirmTextMatch.ts +16 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formControlErrors.ts +10 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/form/numberRange.ts +53 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/form/useControlError.ts +8 -32
- package/frontend-vue/layers/dms-ui/app/build/composables/period/periodQuery.ts +6 -29
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/recordLink.ts +34 -3
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/urlFilters.ts +29 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +6 -3
- package/frontend-vue/layers/dms-ui/app/components/confirm/ConfirmModal.vue +12 -2
- package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +2 -2
- package/frontend-vue/layers/dms-ui/app/components/form/components/DatePicker.vue +1 -1
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputCode.vue +1 -1
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputNumber.vue +47 -8
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputTags.vue +1 -1
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputTime.vue +1 -1
- package/frontend-vue/layers/dms-ui/app/components/form/components/KeyValue.vue +1 -1
- package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +61 -2
- package/frontend-vue/layers/dms-ui/app/components/top-list/TopListCard.vue +21 -1
- package/frontend-vue/layers/dms-ui/app/composables/confirm/index.ts +1 -0
- package/frontend-vue/layers/dms-ui/app/composables/confirm/types.ts +20 -0
- package/frontend-vue/layers/dms-ui/app/composables/confirm/useConfirmFrom.ts +32 -0
- package/frontend-vue/layers/dms-ui/app/composables/form/useControlError.ts +30 -0
- package/frontend-vue/layers/dms-ui/app/composables/form/useForm.ts +2 -2
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +4 -0
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +6 -0
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +6 -0
- package/frontend-vue/tests/confirm-from.test.ts +101 -0
- package/frontend-vue/tests/confirm-typed-text.test.ts +35 -0
- package/frontend-vue/tests/context-links.test.ts +220 -0
- package/frontend-vue/tests/control-error-path.test.ts +11 -0
- package/frontend-vue/tests/email-layout.test.ts +124 -0
- package/frontend-vue/tests/field-errors.test.ts +35 -0
- package/frontend-vue/tests/form-required-legend.test.ts +34 -0
- package/frontend-vue/tests/form-server-field-errors.test.ts +18 -1
- package/frontend-vue/tests/input-number-range.test.ts +135 -0
- package/frontend-vue/tests/input-tags.test.ts +1 -1
- package/frontend-vue/tests/page-validation-redirect.test.ts +221 -0
- package/frontend-vue/tests/row-side-panel.test.ts +2 -2
- package/frontend-vue/tests/table-url-filters.test.ts +158 -0
- package/frontend-vue/tests/table-view-bulk-drawer.test.ts +23 -0
- package/package.json +2 -2
- package/skills/dms-module/SKILL.md +3 -0
- /package/frontend-vue/layers/dms-ui/app/{build/components → components}/form/FormRequiredLegend.vue +0 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import {
|
|
3
|
+
computed,
|
|
4
|
+
createSSRApp,
|
|
5
|
+
h,
|
|
6
|
+
inject,
|
|
7
|
+
onMounted,
|
|
8
|
+
onUnmounted,
|
|
9
|
+
provide,
|
|
10
|
+
ref,
|
|
11
|
+
toRef,
|
|
12
|
+
watch,
|
|
13
|
+
type FunctionalComponent,
|
|
14
|
+
} from "vue";
|
|
15
|
+
import { renderToString } from "vue/server-renderer";
|
|
16
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
17
|
+
import CatchAllPage from "../layers/dms-layout/app/pages/[...slug].vue";
|
|
18
|
+
import { pageValidationRedirect } from "../layers/dms-layout/app/build/utils/pageValidationRedirect";
|
|
19
|
+
import {
|
|
20
|
+
runCustomValidation,
|
|
21
|
+
validateRequiredQueryParams,
|
|
22
|
+
} from "../layers/dms-core/app/utils/query-validation";
|
|
23
|
+
import {
|
|
24
|
+
PAGE_FILL_HEIGHT_CLASSES,
|
|
25
|
+
providePageFillHeight,
|
|
26
|
+
usePageFillHeight,
|
|
27
|
+
} from "../layers/dms-layout/app/composables/layout/usePageFillHeight";
|
|
28
|
+
import type { PageValidation } from "../layers/dms-layout/app/types/page";
|
|
29
|
+
|
|
30
|
+
vi.mock(
|
|
31
|
+
"../layers/dms-layout/app/build/composables/dev-reload/useDevReloadHolder",
|
|
32
|
+
async () => {
|
|
33
|
+
const { ref } = await import("vue");
|
|
34
|
+
return { useDevReloading: () => ref(false) };
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const PAGE_PATH = "/modules/marketing/funnel";
|
|
39
|
+
const LIST_PATH = "/modules/marketing/funnels";
|
|
40
|
+
const NOT_FOUND = 404;
|
|
41
|
+
|
|
42
|
+
class HttpError extends Error {
|
|
43
|
+
constructor(readonly statusCode: number) {
|
|
44
|
+
super(`HTTP ${statusCode}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const navigateDms = vi.fn(async () => {});
|
|
49
|
+
const pageQuery = ref<Record<string, unknown>>({});
|
|
50
|
+
const validation = ref<PageValidation | undefined>(undefined);
|
|
51
|
+
|
|
52
|
+
const PageComponent: FunctionalComponent<{ componentId: string }> = (props) =>
|
|
53
|
+
h("section", { "data-component": props.componentId });
|
|
54
|
+
PageComponent.props = ["componentId"];
|
|
55
|
+
|
|
56
|
+
function installRuntime(): void {
|
|
57
|
+
const pageLayout = ref({
|
|
58
|
+
components: { report: { componentName: "FunnelReport" } },
|
|
59
|
+
});
|
|
60
|
+
const runtime: Record<string, unknown> = {
|
|
61
|
+
computed,
|
|
62
|
+
inject,
|
|
63
|
+
onMounted,
|
|
64
|
+
onUnmounted,
|
|
65
|
+
provide,
|
|
66
|
+
ref,
|
|
67
|
+
toRef,
|
|
68
|
+
watch,
|
|
69
|
+
HTTP_FORBIDDEN: 403,
|
|
70
|
+
HTTP_INTERNAL_SERVER_ERROR: 500,
|
|
71
|
+
HTTP_NOT_FOUND: NOT_FOUND,
|
|
72
|
+
PAGE_FILL_HEIGHT_CLASSES,
|
|
73
|
+
providePageFillHeight,
|
|
74
|
+
usePageFillHeight,
|
|
75
|
+
createError: ({ statusCode }: { statusCode: number }) =>
|
|
76
|
+
new HttpError(statusCode),
|
|
77
|
+
defineDmsPageMeta: vi.fn(),
|
|
78
|
+
firstAccessiblePagePath: vi.fn(),
|
|
79
|
+
navigateDms,
|
|
80
|
+
preloadPageLayoutComponents: vi.fn(),
|
|
81
|
+
providePageRealtime: vi.fn(),
|
|
82
|
+
resolveDmsComponent: () => "div",
|
|
83
|
+
runCustomValidation,
|
|
84
|
+
showError: vi.fn(),
|
|
85
|
+
validateRequiredQueryParams,
|
|
86
|
+
useDefinedFunctions: () => ({ getFunction: () => undefined }),
|
|
87
|
+
useDmsRoute: () => ({ path: PAGE_PATH, query: pageQuery.value }),
|
|
88
|
+
useHomepage: () => "/",
|
|
89
|
+
useI18n: () => ({ t: (key: string) => key }),
|
|
90
|
+
usePageLayout: async () => ({ pageLayout }),
|
|
91
|
+
usePageRealtime: () => ({}),
|
|
92
|
+
usePermissions: () => ({
|
|
93
|
+
permissions: ref([]),
|
|
94
|
+
refreshIfStale: async () => {},
|
|
95
|
+
}),
|
|
96
|
+
useSessionRecovery: () => ({
|
|
97
|
+
loggedIn: ref(true),
|
|
98
|
+
reconcileSession: async () => {},
|
|
99
|
+
redirectToAuth: async () => {},
|
|
100
|
+
}),
|
|
101
|
+
useSiteLayout: () => ({
|
|
102
|
+
siteLayout: ref({ pages: {} }),
|
|
103
|
+
siteLayoutTree: ref(undefined),
|
|
104
|
+
loadingError: ref(null),
|
|
105
|
+
loadSiteLayout: async () => {},
|
|
106
|
+
refreshIfStale: async () => {},
|
|
107
|
+
findMatchingRoute: () => ({
|
|
108
|
+
metadata: {
|
|
109
|
+
fullId: "marketing.funnel",
|
|
110
|
+
hasAccess: true,
|
|
111
|
+
validation: validation.value,
|
|
112
|
+
},
|
|
113
|
+
params: {},
|
|
114
|
+
}),
|
|
115
|
+
}),
|
|
116
|
+
};
|
|
117
|
+
Object.entries(runtime).forEach(([name, value]) =>
|
|
118
|
+
vi.stubGlobal(name, value),
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
interface RenderedPage {
|
|
123
|
+
host: HTMLElement;
|
|
124
|
+
errors: unknown[];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function renderPage(): Promise<RenderedPage> {
|
|
128
|
+
const errors: unknown[] = [];
|
|
129
|
+
const app = createSSRApp(() => h(CatchAllPage));
|
|
130
|
+
app.config.errorHandler = (error) => {
|
|
131
|
+
errors.push(error);
|
|
132
|
+
};
|
|
133
|
+
app.component("DmsRecursiveComponent", PageComponent);
|
|
134
|
+
const host = document.createElement("div");
|
|
135
|
+
host.innerHTML = await renderToString(app);
|
|
136
|
+
return { host, errors };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
beforeEach(() => {
|
|
140
|
+
navigateDms.mockClear();
|
|
141
|
+
pageQuery.value = {};
|
|
142
|
+
validation.value = undefined;
|
|
143
|
+
installRuntime();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
afterEach(() => {
|
|
147
|
+
vi.unstubAllGlobals();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe("pageValidationRedirect", () => {
|
|
151
|
+
const context = {
|
|
152
|
+
routeParams: { id: "42" },
|
|
153
|
+
routeQuery: { website: "site-1" },
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
it("is undefined without a redirectTo", () => {
|
|
157
|
+
expect(
|
|
158
|
+
pageValidationRedirect(
|
|
159
|
+
{ requiredQueryParams: ["id"] },
|
|
160
|
+
PAGE_PATH,
|
|
161
|
+
context,
|
|
162
|
+
),
|
|
163
|
+
).toBeUndefined();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("fills the page URL's tokens and leaves out an empty query pair", () => {
|
|
167
|
+
expect(
|
|
168
|
+
pageValidationRedirect(
|
|
169
|
+
{
|
|
170
|
+
redirectTo: `${LIST_PATH}?website={{query.website}}&tab={{query.tab}}`,
|
|
171
|
+
},
|
|
172
|
+
PAGE_PATH,
|
|
173
|
+
context,
|
|
174
|
+
),
|
|
175
|
+
).toBe(`${LIST_PATH}?website=site-1`);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("refuses a target on the page's own path, which would fail again", () => {
|
|
179
|
+
expect(
|
|
180
|
+
pageValidationRedirect(
|
|
181
|
+
{ redirectTo: `${PAGE_PATH}/?id={{query.id}}` },
|
|
182
|
+
PAGE_PATH,
|
|
183
|
+
context,
|
|
184
|
+
),
|
|
185
|
+
).toBeUndefined();
|
|
186
|
+
expect(
|
|
187
|
+
pageValidationRedirect({ redirectTo: "?id=1" }, PAGE_PATH, context),
|
|
188
|
+
).toBeUndefined();
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("a page failing its validation", () => {
|
|
193
|
+
it("answers not found without a redirectTo", async () => {
|
|
194
|
+
validation.value = { requiredQueryParams: ["id"] };
|
|
195
|
+
const { errors } = await renderPage();
|
|
196
|
+
expect(errors).toEqual([new HttpError(NOT_FOUND)]);
|
|
197
|
+
expect(navigateDms).not.toHaveBeenCalled();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("replaces the URL with its redirectTo, drawing none of its blocks", async () => {
|
|
201
|
+
validation.value = {
|
|
202
|
+
requiredQueryParams: ["id"],
|
|
203
|
+
redirectTo: `${LIST_PATH}?website={{query.website}}`,
|
|
204
|
+
};
|
|
205
|
+
pageQuery.value = { website: "site-1" };
|
|
206
|
+
const { host, errors } = await renderPage();
|
|
207
|
+
expect(errors).toEqual([]);
|
|
208
|
+
expect(navigateDms).toHaveBeenCalledWith(`${LIST_PATH}?website=site-1`, {
|
|
209
|
+
replace: true,
|
|
210
|
+
});
|
|
211
|
+
expect(host.querySelector("[data-component]")).toBeNull();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("draws its blocks once the required query is there", async () => {
|
|
215
|
+
validation.value = { requiredQueryParams: ["id"], redirectTo: LIST_PATH };
|
|
216
|
+
pageQuery.value = { id: "f1" };
|
|
217
|
+
const { host } = await renderPage();
|
|
218
|
+
expect(navigateDms).not.toHaveBeenCalled();
|
|
219
|
+
expect(host.querySelector("[data-component]")).not.toBeNull();
|
|
220
|
+
});
|
|
221
|
+
});
|
|
@@ -453,13 +453,13 @@ describe("deep link to a row the table does not list", () => {
|
|
|
453
453
|
).toEqual({ isMissing: true });
|
|
454
454
|
});
|
|
455
455
|
|
|
456
|
-
it("
|
|
456
|
+
it("marks a refused read as failed and finds nothing without a row route", async () => {
|
|
457
457
|
const refused = async () => {
|
|
458
458
|
throw Object.assign(new Error("Forbidden"), { status: 403 });
|
|
459
459
|
};
|
|
460
460
|
expect(
|
|
461
461
|
await resolveDeepLinkedRow("r9", { listed: rows, idOf, getRow: refused }),
|
|
462
|
-
).toEqual({ isMissing: false });
|
|
462
|
+
).toEqual({ isMissing: false, isFailed: true });
|
|
463
463
|
const noRoute = async () => undefined;
|
|
464
464
|
expect(
|
|
465
465
|
await resolveDeepLinkedRow("r9", { listed: rows, idOf, getRow: noRoute }),
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import {
|
|
3
|
+
computed,
|
|
4
|
+
createApp,
|
|
5
|
+
defineComponent,
|
|
6
|
+
h,
|
|
7
|
+
nextTick,
|
|
8
|
+
provide,
|
|
9
|
+
ref,
|
|
10
|
+
shallowRef,
|
|
11
|
+
type App,
|
|
12
|
+
type FunctionalComponent,
|
|
13
|
+
} from "vue";
|
|
14
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
15
|
+
import TableFiltersRow from "../layers/dms-ui/app/build/components/table/FiltersRow.vue";
|
|
16
|
+
import type { TableUrlFilter } from "../layers/dms-ui/app/build/components/table/Table.vue";
|
|
17
|
+
import {
|
|
18
|
+
visibleUrlFilters,
|
|
19
|
+
withoutQueryParam,
|
|
20
|
+
} from "../layers/dms-ui/app/build/composables/table-view/utils/urlFilters";
|
|
21
|
+
|
|
22
|
+
describe("visibleUrlFilters", () => {
|
|
23
|
+
const declared = {
|
|
24
|
+
email: { field: "email", visible: true, label: "$app.email" },
|
|
25
|
+
template: { field: "templateSlug", mode: "is_not", visible: true },
|
|
26
|
+
tenant: { field: "tenantId" },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
it("lists the visible filters the URL applies, hidden ones left out", () => {
|
|
30
|
+
expect(
|
|
31
|
+
visibleUrlFilters(declared, {
|
|
32
|
+
email: "a@b.test",
|
|
33
|
+
tenant: "t1",
|
|
34
|
+
page: "2",
|
|
35
|
+
}),
|
|
36
|
+
).toEqual([
|
|
37
|
+
{
|
|
38
|
+
param: "email",
|
|
39
|
+
label: "$app.email",
|
|
40
|
+
accessorKey: "email",
|
|
41
|
+
mode: "is",
|
|
42
|
+
value: "a@b.test",
|
|
43
|
+
},
|
|
44
|
+
]);
|
|
45
|
+
expect(visibleUrlFilters(declared, { template: "welcome" })).toEqual([
|
|
46
|
+
{
|
|
47
|
+
param: "template",
|
|
48
|
+
label: undefined,
|
|
49
|
+
accessorKey: "templateSlug",
|
|
50
|
+
mode: "is_not",
|
|
51
|
+
value: "welcome",
|
|
52
|
+
},
|
|
53
|
+
]);
|
|
54
|
+
expect(visibleUrlFilters(undefined, { email: "a@b.test" })).toEqual([]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("removes one parameter from the query", () => {
|
|
58
|
+
expect(withoutQueryParam({ email: "a", tab: "all" }, "email")).toEqual({
|
|
59
|
+
tab: "all",
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("filters row URL chips", () => {
|
|
65
|
+
let app: App | undefined;
|
|
66
|
+
const emits = vi.fn();
|
|
67
|
+
|
|
68
|
+
const mount = (urlFilters: TableUrlFilter[]) => {
|
|
69
|
+
const Host = defineComponent({
|
|
70
|
+
setup() {
|
|
71
|
+
provide(
|
|
72
|
+
"tableSharedData",
|
|
73
|
+
shallowRef({
|
|
74
|
+
columnFiltersState: ref([]),
|
|
75
|
+
labeledColumns: computed(() => [
|
|
76
|
+
{
|
|
77
|
+
column: { id: "templateSlug", columnDef: {} },
|
|
78
|
+
label: "Template",
|
|
79
|
+
},
|
|
80
|
+
]),
|
|
81
|
+
clearableFilters: computed(() => ({
|
|
82
|
+
filters: false,
|
|
83
|
+
search: false,
|
|
84
|
+
})),
|
|
85
|
+
urlFilters,
|
|
86
|
+
emits,
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
return () => h(TableFiltersRow);
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
app = createApp(Host);
|
|
93
|
+
const passthrough =
|
|
94
|
+
(tag: string): FunctionalComponent =>
|
|
95
|
+
(_props, { slots }) =>
|
|
96
|
+
h(tag, slots.default?.());
|
|
97
|
+
app.component("UPopover", passthrough("div"));
|
|
98
|
+
app.component("UButton", passthrough("button"));
|
|
99
|
+
app.component("UIcon", passthrough("i"));
|
|
100
|
+
app.component("Suspense", passthrough("div"));
|
|
101
|
+
const container = document.createElement("div");
|
|
102
|
+
app.mount(container);
|
|
103
|
+
return container;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
beforeEach(() => {
|
|
107
|
+
emits.mockClear();
|
|
108
|
+
vi.stubGlobal("computed", computed);
|
|
109
|
+
vi.stubGlobal("ref", ref);
|
|
110
|
+
vi.stubGlobal("useI18n", () => ({
|
|
111
|
+
t: (key: string) => key,
|
|
112
|
+
locale: ref("en"),
|
|
113
|
+
}));
|
|
114
|
+
vi.stubGlobal("useTranslation", () => ({
|
|
115
|
+
processI18n: (text: string) =>
|
|
116
|
+
text.startsWith("$") ? `t:${text.slice(1)}` : text,
|
|
117
|
+
}));
|
|
118
|
+
vi.stubGlobal("useDmsAppConfig", () => ({
|
|
119
|
+
ui: { icons: { close: "i-close", plus: "i-plus" } },
|
|
120
|
+
}));
|
|
121
|
+
vi.stubGlobal("useDataTypes", () => ({ getDataType: () => undefined }));
|
|
122
|
+
vi.stubGlobal("isString", (value: unknown) => typeof value === "string");
|
|
123
|
+
vi.stubGlobal("resolveDmsComponent", () => undefined);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
afterEach(() => {
|
|
127
|
+
app?.unmount();
|
|
128
|
+
app = undefined;
|
|
129
|
+
vi.unstubAllGlobals();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("draws a chip per URL filter and asks to remove its parameter", async () => {
|
|
133
|
+
const container = mount([
|
|
134
|
+
{
|
|
135
|
+
param: "email",
|
|
136
|
+
label: "$app.email",
|
|
137
|
+
accessorKey: "email",
|
|
138
|
+
mode: "is",
|
|
139
|
+
value: "a@b.test",
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
param: "template",
|
|
143
|
+
accessorKey: "templateSlug",
|
|
144
|
+
mode: "is",
|
|
145
|
+
value: "welcome",
|
|
146
|
+
},
|
|
147
|
+
]);
|
|
148
|
+
const chips = container.querySelectorAll("[data-dms-url-filter]");
|
|
149
|
+
expect(chips).toHaveLength(2);
|
|
150
|
+
expect(chips[0]!.textContent).toContain("t:app.email");
|
|
151
|
+
expect(chips[0]!.textContent).toContain("a@b.test");
|
|
152
|
+
expect(chips[1]!.textContent).toContain("Template");
|
|
153
|
+
|
|
154
|
+
chips[1]!.querySelector("button")!.click();
|
|
155
|
+
await nextTick();
|
|
156
|
+
expect(emits).toHaveBeenCalledWith("removeUrlFilter", "template");
|
|
157
|
+
});
|
|
158
|
+
});
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
rowNavigation,
|
|
14
14
|
} from "../layers/dms-ui/app/build/composables/actions/rowNavigation";
|
|
15
15
|
import {
|
|
16
|
+
deepLinkOutcome,
|
|
16
17
|
readRecordId,
|
|
17
18
|
recordUrlKey,
|
|
18
19
|
writeRecordId,
|
|
@@ -130,6 +131,28 @@ describe("record deep links", () => {
|
|
|
130
131
|
writeRecordId({ tableId: "runs", isSoleTableView: true });
|
|
131
132
|
expect(window.location.search).toBe("?tab=open");
|
|
132
133
|
});
|
|
134
|
+
|
|
135
|
+
it("notifies a link no row answers, unless a side panel shows it missing", () => {
|
|
136
|
+
const panel = {
|
|
137
|
+
target: { type: "sidePanel" },
|
|
138
|
+
} as unknown as CustomRowAction;
|
|
139
|
+
const drawer = {
|
|
140
|
+
target: { type: "drawer" },
|
|
141
|
+
deepLink: true,
|
|
142
|
+
} as unknown as CustomRowAction;
|
|
143
|
+
const row = { id: "r1" };
|
|
144
|
+
expect(deepLinkOutcome({ row, isMissing: false }, drawer)).toBe("open");
|
|
145
|
+
expect(deepLinkOutcome({ isMissing: true }, panel)).toBe("missingPanel");
|
|
146
|
+
expect(deepLinkOutcome({ isMissing: true }, drawer)).toBe("notify");
|
|
147
|
+
expect(deepLinkOutcome({ isMissing: false }, panel)).toBe("notify");
|
|
148
|
+
expect(deepLinkOutcome({ isMissing: false }, drawer)).toBe("notify");
|
|
149
|
+
expect(deepLinkOutcome({ isMissing: false, isFailed: true }, panel)).toBe(
|
|
150
|
+
"notifyFailed",
|
|
151
|
+
);
|
|
152
|
+
expect(deepLinkOutcome({ isMissing: false, isFailed: true }, drawer)).toBe(
|
|
153
|
+
"notifyFailed",
|
|
154
|
+
);
|
|
155
|
+
});
|
|
133
156
|
});
|
|
134
157
|
|
|
135
158
|
describe("action targets on a selection and on a listed row", () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antelopejs/dms",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.15",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/AntelopeJS/dms.git"
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"@antelopejs/interface-data-api": ">=0.2.0 <1.0.0",
|
|
38
38
|
"@antelopejs/interface-database": ">=0.1.8 <1.0.0",
|
|
39
39
|
"@antelopejs/interface-database-decorators": ">=0.1.7 <1.0.0",
|
|
40
|
-
"@antelopejs/interface-dms": ">=0.5.
|
|
40
|
+
"@antelopejs/interface-dms": ">=0.5.10 <0.6.0",
|
|
41
41
|
"@antelopejs/interface-email": ">=0.0.5 <1.0.0",
|
|
42
42
|
"@antelopejs/interface-file-storage": ">=0.1.3 <1.0.0",
|
|
43
43
|
"archiver": "^7.0.1",
|
|
@@ -86,6 +86,9 @@ An absolute link (a public file, a snippet to copy, a mail) is built on `await G
|
|
|
86
86
|
(`@antelopejs/interface-dms/api-base-url`, the backend) or `await GetClientBaseUrl()`
|
|
87
87
|
(`/client-base-url`, the dashboard), never on `Host` / `X-Forwarded-*`: a reverse proxy may not
|
|
88
88
|
forward them. Both answer the DMS config (`apiBaseUrl`, `clientBaseUrl`).
|
|
89
|
+
A client address (a rate limit, an anonymous visitor) comes from `await getClientIp(context)`
|
|
90
|
+
(`/client-ip`), never from `X-Forwarded-For` read by hand: it follows the DMS's
|
|
91
|
+
`auth.trustedProxies`, so the operator declares the proxies once.
|
|
89
92
|
|
|
90
93
|
## Navigation & access — module pages are owner-only
|
|
91
94
|
|
/package/frontend-vue/layers/dms-ui/app/{build/components → components}/form/FormRequiredLegend.vue
RENAMED
|
File without changes
|