@antelopejs/dms 0.7.20 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -0
- package/dist/implementations/dms-auth/account-emails.d.ts +4 -0
- package/dist/implementations/dms-auth/account-emails.js +62 -0
- package/dist/implementations/dms-auth/account-emails.js.map +1 -0
- package/dist/implementations/dms-auth/index.d.ts +1 -3
- package/dist/implementations/dms-auth/index.js +15 -61
- package/dist/implementations/dms-auth/index.js.map +1 -1
- package/dist/pages/settings/users/email-change.js +3 -4
- package/dist/pages/settings/users/email-change.js.map +1 -1
- package/dist/pages/settings/users/invites.js +1 -1
- package/dist/pages/settings/users/invites.js.map +1 -1
- package/dist/pages/settings/users/members.js +1 -1
- package/dist/pages/settings/users/members.js.map +1 -1
- package/dist/pages/settings/users/notifications.js +1 -0
- package/dist/pages/settings/users/notifications.js.map +1 -1
- package/dist/test/unit/implementations/dms-html-render/built-in-emails.test.d.ts +1 -0
- package/dist/test/unit/implementations/dms-html-render/built-in-emails.test.js +34 -0
- package/dist/test/unit/implementations/dms-html-render/built-in-emails.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms/hook-handler-results.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms/hook-handler-results.test.js +49 -0
- package/dist/test/unit/interfaces/dms/hook-handler-results.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms/page-topics.test.js +17 -0
- package/dist/test/unit/interfaces/dms/page-topics.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js +2 -2
- package/dist/test/unit/interfaces/dms-base/confirm-dialog-secondary-action.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/query-response-delta.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/query-response-delta.test.js +29 -0
- package/dist/test/unit/interfaces/dms-base/query-response-delta.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/resource-form.test.js +5 -0
- package/dist/test/unit/interfaces/dms-base/resource-form.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/table-view-form-redirect.test.js +12 -2
- package/dist/test/unit/interfaces/dms-base/table-view-form-redirect.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js +29 -4
- package/dist/test/unit/interfaces/dms-base/table-view-reduced-chrome.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/table-view-row-menu-label.test.d.ts +1 -0
- package/dist/test/unit/interfaces/dms-base/table-view-row-menu-label.test.js +27 -0
- package/dist/test/unit/interfaces/dms-base/table-view-row-menu-label.test.js.map +1 -0
- package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js +18 -4
- package/dist/test/unit/interfaces/dms-base/table-view-source-reorder.test.js.map +1 -1
- package/dist/test/unit/interfaces/dms-base/table-view-views.test.js +8 -5
- package/dist/test/unit/interfaces/dms-base/table-view-views.test.js.map +1 -1
- package/dist/test/unit/utils/email-subjects.test.d.ts +1 -0
- package/dist/test/unit/utils/email-subjects.test.js +27 -0
- package/dist/test/unit/utils/email-subjects.test.js.map +1 -0
- package/dist/utils/admin-invite-email.js +2 -4
- package/dist/utils/admin-invite-email.js.map +1 -1
- package/dist/utils/email-change-mail.d.ts +1 -1
- package/dist/utils/email-change-mail.js +11 -6
- package/dist/utils/email-change-mail.js.map +1 -1
- package/dist/utils/email-subjects.d.ts +3 -0
- package/dist/utils/email-subjects.js +32 -0
- package/dist/utils/email-subjects.js.map +1 -0
- package/dist/utils/export-jobs.js +15 -10
- package/dist/utils/export-jobs.js.map +1 -1
- package/frontend-vue/layers/dms-core/app/utils/url-interpolation.ts +29 -11
- package/frontend-vue/layers/dms-core/shared/types/app-config.ts +17 -0
- package/frontend-vue/layers/dms-layout/app/app.config.ts +5 -0
- package/frontend-vue/layers/dms-layout/app/build/components/layout/DashboardHeader.vue +18 -2
- package/frontend-vue/layers/dms-layout/app/build/components/layout/Footer.vue +16 -1
- package/frontend-vue/layers/dms-layout/app/build/composables/layout/useHeaderActionControls.ts +14 -0
- package/frontend-vue/layers/dms-layout/app/components/layout/HStack.vue +16 -0
- package/frontend-vue/layers/dms-layout/app/components/layout/VStack.vue +2 -0
- package/frontend-vue/layers/dms-layout/app/composables/useFooterLinks.ts +11 -1
- package/frontend-vue/layers/dms-layout/app/custom-pages/settings/roles.vue +1 -1
- package/frontend-vue/layers/dms-layout/app/emails/EmailAdminInvite.vue +1 -1
- package/frontend-vue/layers/dms-layout/app/emails/EmailChangeNotice.vue +13 -7
- package/frontend-vue/layers/dms-layout/app/emails/EmailChangeVerification.vue +18 -7
- package/frontend-vue/layers/dms-layout/app/emails/EmailExportReady.vue +23 -12
- package/frontend-vue/layers/dms-layout/app/emails/EmailResetPassword.vue +18 -7
- package/frontend-vue/layers/dms-layout/app/emails/EmailTwoFactor.vue +23 -4
- package/frontend-vue/layers/dms-layout/app/emails/EmailUserValidation.vue +18 -6
- package/frontend-vue/layers/dms-layout/app/emails/components/EmailOTP.vue +8 -2
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-en-GB.json +40 -0
- package/frontend-vue/layers/dms-layout/i18n/locales/layout-fr-FR.json +40 -0
- package/frontend-vue/layers/dms-ui/app/build/components/form/FormFieldControl.vue +2 -1
- package/frontend-vue/layers/dms-ui/app/build/components/form/FormLoadError.vue +11 -3
- package/frontend-vue/layers/dms-ui/app/build/components/table/Table.vue +13 -3
- package/frontend-vue/layers/dms-ui/app/build/components/table-view/CardsDisplay.vue +1 -1
- package/frontend-vue/layers/dms-ui/app/build/composables/actions/rowNavigation.ts +14 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/actions/useActionTargets.ts +90 -43
- package/frontend-vue/layers/dms-ui/app/build/composables/chart/apexTheme.ts +59 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/confirmDialogTexts.ts +27 -4
- package/frontend-vue/layers/dms-ui/app/build/composables/confirm/useActionConfirm.ts +10 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/data-types/registerDefaults.ts +0 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formEntryOrder.ts +23 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formFooter.ts +19 -15
- package/frontend-vue/layers/dms-ui/app/build/composables/form/formRecordLoad.ts +26 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/page/pageUrlValues.ts +41 -0
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/registerDefaultFunctions.ts +2 -2
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableRows.ts +3 -5
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableUrlTab.ts +42 -12
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewConfig.ts +7 -11
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/useTableViewRowActions.ts +3 -3
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/recordLink.ts +10 -11
- package/frontend-vue/layers/dms-ui/app/build/composables/table-view/utils/views.ts +29 -0
- package/frontend-vue/layers/dms-ui/app/build/utils/link.ts +7 -3
- package/frontend-vue/layers/dms-ui/app/build/utils/urlVariables.ts +32 -9
- package/frontend-vue/layers/dms-ui/app/components/activity-feed/ActivityFeed.vue +8 -1
- package/frontend-vue/layers/dms-ui/app/components/banner/Banner.vue +9 -4
- package/frontend-vue/layers/dms-ui/app/components/blocks/BannerBlock.vue +5 -11
- package/frontend-vue/layers/dms-ui/app/components/blocks/NavCardGridBlock.vue +2 -1
- package/frontend-vue/layers/dms-ui/app/components/chart/ApexChartHost.vue +32 -1
- package/frontend-vue/layers/dms-ui/app/components/check-list/CheckList.vue +1 -1
- package/frontend-vue/layers/dms-ui/app/components/form/Form.vue +12 -2
- package/frontend-vue/layers/dms-ui/app/components/form/components/Cascader.vue +7 -3
- package/frontend-vue/layers/dms-ui/app/components/form/components/File.vue +7 -0
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputAddress.vue +4 -1
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputCode.vue +4 -2
- package/frontend-vue/layers/dms-ui/app/components/form/components/InputSecret.vue +3 -2
- package/frontend-vue/layers/dms-ui/app/components/form/components/Relation.vue +4 -1
- package/frontend-vue/layers/dms-ui/app/components/grid/columns.ts +15 -3
- package/frontend-vue/layers/dms-ui/app/components/key-value-list/KeyValueList.vue +2 -1
- package/frontend-vue/layers/dms-ui/app/components/list-row/ListRow.vue +9 -2
- package/frontend-vue/layers/dms-ui/app/components/save-bar/InstantSaveBadge.vue +13 -5
- package/frontend-vue/layers/dms-ui/app/components/save-bar/SaveBar.vue +1 -1
- package/frontend-vue/layers/dms-ui/app/components/stat-group/StatGroup.vue +3 -2
- package/frontend-vue/layers/dms-ui/app/components/table-view/CascaderPath.vue +53 -31
- package/frontend-vue/layers/dms-ui/app/components/table-view/TableView.vue +14 -6
- package/frontend-vue/layers/dms-ui/app/composables/chart/apexPlotOptions.ts +15 -4
- package/frontend-vue/layers/dms-ui/app/composables/chart/apexYRange.ts +123 -0
- package/frontend-vue/layers/dms-ui/app/composables/chart/useApexChart.ts +49 -49
- package/frontend-vue/layers/dms-ui/app/composables/chart/useChartTheme.ts +20 -1
- package/frontend-vue/layers/dms-ui/app/composables/confirm/types.ts +1 -1
- 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 +8 -6
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/action-target.ts +1 -1
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/config.ts +3 -3
- package/frontend-vue/layers/dms-ui/app/composables/table-view/types/display.ts +6 -0
- package/frontend-vue/layers/dms-ui/app/utils/bannerToneIcons.ts +13 -0
- package/frontend-vue/layers/dms-ui/app/utils/cascader.ts +26 -0
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-en-GB.json +4 -0
- package/frontend-vue/layers/dms-ui/i18n/locales/ui-fr-FR.json +4 -0
- package/frontend-vue/tests/activity-feed-fill-height.test.ts +7 -0
- package/frontend-vue/tests/banner-tone-icon.test.ts +55 -0
- package/frontend-vue/tests/builtin-emails.test.ts +141 -0
- package/frontend-vue/tests/cascader.test.ts +27 -0
- package/frontend-vue/tests/chart-auto-y-range.test.ts +25 -9
- package/frontend-vue/tests/chart-colors.test.ts +8 -0
- package/frontend-vue/tests/chart-resize-after-animation.test.ts +100 -0
- package/frontend-vue/tests/chart-type-defaults.test.ts +128 -0
- package/frontend-vue/tests/check-list-marker.test.ts +58 -0
- package/frontend-vue/tests/confirm-from.test.ts +1 -1
- package/frontend-vue/tests/confirm-secondary-action.test.ts +2 -2
- package/frontend-vue/tests/context-links.test.ts +2 -2
- package/frontend-vue/tests/custom-action-confirm-from.test.ts +2 -2
- package/frontend-vue/tests/download-action-target.test.ts +2 -2
- package/frontend-vue/tests/file-drop-prompt.test.ts +72 -0
- package/frontend-vue/tests/footer-config.test.ts +135 -0
- package/frontend-vue/tests/form-actions.test.ts +7 -0
- package/frontend-vue/tests/form-entry-order.test.ts +45 -0
- package/frontend-vue/tests/form-footer-kind.test.ts +11 -6
- package/frontend-vue/tests/form-load-error.test.ts +38 -1
- package/frontend-vue/tests/grid-responsive-columns.test.ts +9 -0
- package/frontend-vue/tests/header-actions.test.ts +55 -9
- package/frontend-vue/tests/instant-save-badge.test.ts +11 -4
- package/frontend-vue/tests/list-row-truncate.test.ts +46 -0
- package/frontend-vue/tests/roles-name-errors.test.ts +2 -3
- package/frontend-vue/tests/row-side-panel.test.ts +93 -2
- package/frontend-vue/tests/save-bar.test.ts +15 -3
- package/frontend-vue/tests/save-status-error.test.ts +2 -2
- package/frontend-vue/tests/table-row-action-errors.test.ts +5 -5
- package/frontend-vue/tests/table-row-hover.test.ts +2 -2
- package/frontend-vue/tests/table-view-bulk-drawer.test.ts +114 -0
- package/frontend-vue/tests/table-view-cards-reread.test.ts +77 -0
- package/frontend-vue/tests/table-view-sources.test.ts +1 -1
- package/frontend-vue/tests/table-view-url-tab.test.ts +84 -2
- package/frontend-vue/tests/template-tokens.test.ts +235 -0
- package/frontend-vue/tests/url-interpolation.test.ts +6 -6
- package/package.json +2 -2
- package/skills/dms/SKILL.md +2 -1
- package/skills/dms-pages/REFERENCE.md +2 -2
- package/skills/dms-project/SKILL.md +0 -1
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { orderFormEntries } from "../layers/dms-ui/app/build/composables/form/formEntryOrder";
|
|
3
|
+
import type { FormFieldOrGroup } from "../layers/dms-ui/app/composables/form/types/field";
|
|
4
|
+
|
|
5
|
+
const field = (id: string) => ({
|
|
6
|
+
id,
|
|
7
|
+
component: { componentName: "dms-input-text" },
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const group = (id: string, order?: number) => ({
|
|
11
|
+
id,
|
|
12
|
+
order,
|
|
13
|
+
fields: [field(`${id}Member`)],
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const ids = (entries: FormFieldOrGroup[]) => entries.map((entry) => entry.id);
|
|
17
|
+
|
|
18
|
+
describe("orderFormEntries", () => {
|
|
19
|
+
it("moves the groups giving an order into their places, lowest first", () => {
|
|
20
|
+
const entries: FormFieldOrGroup[] = [
|
|
21
|
+
field("username"),
|
|
22
|
+
group("contact"),
|
|
23
|
+
field("bio"),
|
|
24
|
+
group("social", 2),
|
|
25
|
+
group("address", 1),
|
|
26
|
+
];
|
|
27
|
+
expect(ids(orderFormEntries(entries))).toEqual([
|
|
28
|
+
"username",
|
|
29
|
+
"contact",
|
|
30
|
+
"bio",
|
|
31
|
+
"address",
|
|
32
|
+
"social",
|
|
33
|
+
]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("keeps the declaration order of equal orders and of no order at all", () => {
|
|
37
|
+
const entries: FormFieldOrGroup[] = [
|
|
38
|
+
group("first", 1),
|
|
39
|
+
field("name"),
|
|
40
|
+
group("second", 1),
|
|
41
|
+
group("loose"),
|
|
42
|
+
];
|
|
43
|
+
expect(ids(orderFormEntries(entries))).toEqual(ids(entries));
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -69,23 +69,28 @@ describe("saveBarState of an action form", () => {
|
|
|
69
69
|
});
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
-
it("shows
|
|
73
|
-
|
|
72
|
+
it("shows its submit alone, ready, on a page while untouched", () => {
|
|
73
|
+
// A pre-filled form sends its defaults as they are.
|
|
74
|
+
expect(action({})).toEqual({
|
|
75
|
+
showsStatus: false,
|
|
76
|
+
showsSubmit: true,
|
|
77
|
+
isSubmitHeld: false,
|
|
78
|
+
isHidden: false,
|
|
79
|
+
});
|
|
74
80
|
});
|
|
75
81
|
|
|
76
|
-
it("offers Cancel beside its
|
|
77
|
-
// The invite modal: empty, it still shows the way out and its "Invite".
|
|
82
|
+
it("offers Cancel beside its ready submit in a drawer or a modal", () => {
|
|
78
83
|
expect(action({ cancellable: true })).toEqual({
|
|
79
84
|
showsStatus: false,
|
|
80
85
|
secondary: "cancel",
|
|
81
86
|
showsSubmit: true,
|
|
82
|
-
isSubmitHeld:
|
|
87
|
+
isSubmitHeld: false,
|
|
83
88
|
isHidden: false,
|
|
84
89
|
});
|
|
85
90
|
});
|
|
86
91
|
|
|
87
92
|
it("shows its submit alone, ready, when nothing in it can be changed", () => {
|
|
88
|
-
const bar = action({ resettable: false });
|
|
93
|
+
const bar = action({ resettable: false, dirty: true });
|
|
89
94
|
expect(bar.secondary).toBeUndefined();
|
|
90
95
|
expect(bar).toMatchObject({
|
|
91
96
|
showsSubmit: true,
|
|
@@ -4,7 +4,9 @@ import { renderToString } from "vue/server-renderer";
|
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
5
|
import FormLoadError from "../layers/dms-ui/app/build/components/form/FormLoadError.vue";
|
|
6
6
|
import {
|
|
7
|
+
type FormLoadFailure,
|
|
7
8
|
type FormLoadSource,
|
|
9
|
+
missingRecordFailure,
|
|
8
10
|
trackFormRecordLoad,
|
|
9
11
|
} from "../layers/dms-ui/app/build/composables/form/formRecordLoad";
|
|
10
12
|
|
|
@@ -72,6 +74,32 @@ describe("a form's record load", () => {
|
|
|
72
74
|
});
|
|
73
75
|
});
|
|
74
76
|
|
|
77
|
+
describe("a form about a record its address names none of", () => {
|
|
78
|
+
const FETCH_URL = "/api/tasks/get?id={{query.id}}";
|
|
79
|
+
|
|
80
|
+
it("reports the record missing while the id is not in the address", () => {
|
|
81
|
+
expect(
|
|
82
|
+
missingRecordFailure({ requiresRecord: true, fetchUrl: FETCH_URL }),
|
|
83
|
+
).toBe("missing");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("reports nothing once the address names the record", () => {
|
|
87
|
+
expect(
|
|
88
|
+
missingRecordFailure({
|
|
89
|
+
requiresRecord: true,
|
|
90
|
+
fetchUrl: FETCH_URL,
|
|
91
|
+
resolvedFetchUrl: "/api/tasks/get?id=1",
|
|
92
|
+
}),
|
|
93
|
+
).toBe(null);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("leaves a form that may open without a record to its defaults", () => {
|
|
97
|
+
// A create form seeded by `?duplicate=` when there is one.
|
|
98
|
+
expect(missingRecordFailure({ fetchUrl: FETCH_URL })).toBe(null);
|
|
99
|
+
expect(missingRecordFailure({ requiresRecord: true })).toBe(null);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
75
103
|
describe("the load error a form shows", () => {
|
|
76
104
|
const Slot = defineComponent({
|
|
77
105
|
props: {
|
|
@@ -91,7 +119,7 @@ describe("the load error a form shows", () => {
|
|
|
91
119
|
]),
|
|
92
120
|
});
|
|
93
121
|
|
|
94
|
-
async function render(failure:
|
|
122
|
+
async function render(failure: FormLoadFailure): Promise<string> {
|
|
95
123
|
const app = createSSRApp({
|
|
96
124
|
render: () => h(FormLoadError, { failure }),
|
|
97
125
|
});
|
|
@@ -118,6 +146,13 @@ describe("the load error a form shows", () => {
|
|
|
118
146
|
expect(html).toContain("dms.form.load_error.retry");
|
|
119
147
|
});
|
|
120
148
|
|
|
149
|
+
it("says no entry is selected, with nothing to retry", async () => {
|
|
150
|
+
const html = await render("missing");
|
|
151
|
+
expect(html).toContain("dms.form.load_error.missing_title");
|
|
152
|
+
expect(html).toContain("dms.form.load_error.missing_description");
|
|
153
|
+
expect(html).not.toContain("dms.form.load_error.retry");
|
|
154
|
+
});
|
|
155
|
+
|
|
121
156
|
it("has its texts in English and French", () => {
|
|
122
157
|
for (const file of ["ui-en-GB.json", "ui-fr-FR.json"]) {
|
|
123
158
|
const locale = JSON.parse(
|
|
@@ -131,6 +166,8 @@ describe("the load error a form shows", () => {
|
|
|
131
166
|
"error_title",
|
|
132
167
|
"forbidden_description",
|
|
133
168
|
"forbidden_title",
|
|
169
|
+
"missing_description",
|
|
170
|
+
"missing_title",
|
|
134
171
|
"retry",
|
|
135
172
|
]);
|
|
136
173
|
}
|
|
@@ -87,6 +87,15 @@ describe("grid column template", () => {
|
|
|
87
87
|
expect(gridColumnsTemplate(1, GAP, MIN)).toContain("(100% - 0 * 1rem)");
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
+
it("reads a unitless gap or minimum as pixels, which calc() requires", () => {
|
|
91
|
+
const template = gridColumnsTemplate(2, "0", "160");
|
|
92
|
+
expect(template).toContain("(100% - 1 * 0px)");
|
|
93
|
+
expect(template).toContain("max(160px,");
|
|
94
|
+
expect(resolveColumnCount(template, 553, 0)).toBe(2);
|
|
95
|
+
const css = gridResponsiveStyles(gridScopeId("nested"), 2, "0", MIN);
|
|
96
|
+
expect(css).toContain(`calc(2 * ${MIN} + 1 * 0px)`);
|
|
97
|
+
});
|
|
98
|
+
|
|
90
99
|
it("honours a caller-supplied minimum", () => {
|
|
91
100
|
expect(gridColumnsTemplate(4, GAP, "120px")).toContain("120px");
|
|
92
101
|
expect(
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
computed,
|
|
4
4
|
createApp,
|
|
5
|
+
createSSRApp,
|
|
5
6
|
defineComponent,
|
|
6
7
|
h,
|
|
7
8
|
nextTick,
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
type App,
|
|
11
12
|
type Ref,
|
|
12
13
|
} from "vue";
|
|
14
|
+
import { renderToString } from "vue/server-renderer";
|
|
13
15
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
14
16
|
import DashboardHeader from "../layers/dms-layout/app/build/components/layout/DashboardHeader.vue";
|
|
15
17
|
import {
|
|
@@ -97,18 +99,44 @@ function installRuntime(): void {
|
|
|
97
99
|
);
|
|
98
100
|
}
|
|
99
101
|
|
|
100
|
-
function
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
app = createApp(DashboardHeader);
|
|
104
|
-
app.component("UButton", ButtonStub);
|
|
102
|
+
function createHeaderApp(create: typeof createApp): App {
|
|
103
|
+
const header = create(DashboardHeader);
|
|
104
|
+
header.component("UButton", ButtonStub);
|
|
105
105
|
["UDashboardNavbar", "UTooltip", "UBreadcrumb", "UDropdownMenu"].forEach(
|
|
106
|
-
(name) =>
|
|
106
|
+
(name) => header.component(name, SlotsStub),
|
|
107
107
|
);
|
|
108
|
+
return header;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function mountHeader(): Promise<HTMLElement> {
|
|
112
|
+
const host = document.createElement("div");
|
|
113
|
+
document.body.append(host);
|
|
114
|
+
app = createHeaderApp(createApp);
|
|
115
|
+
app.mount(host);
|
|
116
|
+
await nextTick();
|
|
117
|
+
return host;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Renders the header on the "server", runs what only the client runs (a
|
|
122
|
+
* `.client` plugin), then hydrates the server markup.
|
|
123
|
+
*/
|
|
124
|
+
async function hydrateHeader(runOnClient: () => void): Promise<HTMLElement> {
|
|
125
|
+
const host = document.createElement("div");
|
|
126
|
+
host.innerHTML = await renderToString(createHeaderApp(createSSRApp));
|
|
127
|
+
document.body.append(host);
|
|
128
|
+
runOnClient();
|
|
129
|
+
app = createHeaderApp(createSSRApp);
|
|
108
130
|
app.mount(host);
|
|
109
131
|
return host;
|
|
110
132
|
}
|
|
111
133
|
|
|
134
|
+
function hydrationWarnings(spies: ReturnType<typeof vi.spyOn>[]): string[] {
|
|
135
|
+
return spies
|
|
136
|
+
.flatMap((spy) => spy.mock.calls.map((call) => String(call[0])))
|
|
137
|
+
.filter((message) => message.includes("Hydration"));
|
|
138
|
+
}
|
|
139
|
+
|
|
112
140
|
function button(host: HTMLElement, label: string): HTMLButtonElement | null {
|
|
113
141
|
return host.querySelector(`button[aria-label="${label}"]`);
|
|
114
142
|
}
|
|
@@ -145,7 +173,7 @@ describe("header action buttons", () => {
|
|
|
145
173
|
action("toggle", { onSelect: vi.fn(), isActive: () => isOn.value }),
|
|
146
174
|
);
|
|
147
175
|
registerHeaderAction(action("plain", { onSelect: vi.fn() }));
|
|
148
|
-
const host = mountHeader();
|
|
176
|
+
const host = await mountHeader();
|
|
149
177
|
const toggle = button(host, "toggle");
|
|
150
178
|
expect(toggle?.dataset.color).toBe(IDLE_COLOR);
|
|
151
179
|
expect(toggle?.getAttribute("aria-pressed")).toBe("false");
|
|
@@ -160,7 +188,7 @@ describe("header action buttons", () => {
|
|
|
160
188
|
registerHeaderAction(
|
|
161
189
|
action("panel", { label: "$panel.toggle", sidePanelId: PANEL_ID }),
|
|
162
190
|
);
|
|
163
|
-
const host = mountHeader();
|
|
191
|
+
const host = await mountHeader();
|
|
164
192
|
const toggle = button(host, "t:panel.toggle");
|
|
165
193
|
const { isOpen } = useSidePanel(PANEL_ID);
|
|
166
194
|
toggle?.click();
|
|
@@ -182,10 +210,28 @@ describe("header action buttons", () => {
|
|
|
182
210
|
isActive: () => true,
|
|
183
211
|
}),
|
|
184
212
|
);
|
|
185
|
-
const host = mountHeader();
|
|
213
|
+
const host = await mountHeader();
|
|
186
214
|
const builder = button(host, "Edit");
|
|
187
215
|
builder?.click();
|
|
188
216
|
expect(onSelect).toHaveBeenCalledOnce();
|
|
189
217
|
expect(builder?.dataset.color).toBe(ACTIVE_COLOR);
|
|
190
218
|
});
|
|
191
219
|
});
|
|
220
|
+
|
|
221
|
+
describe("header action hydration", () => {
|
|
222
|
+
it("draws a client-only action once mounted, not over the server markup", async () => {
|
|
223
|
+
const spies = [
|
|
224
|
+
vi.spyOn(console, "warn").mockImplementation(() => undefined),
|
|
225
|
+
vi.spyOn(console, "error").mockImplementation(() => undefined),
|
|
226
|
+
];
|
|
227
|
+
registerHeaderAction(action("panel", { sidePanelId: PANEL_ID }));
|
|
228
|
+
const host = await hydrateHeader(() =>
|
|
229
|
+
registerHeaderAction(action("client", { onSelect: vi.fn() })),
|
|
230
|
+
);
|
|
231
|
+
expect(hydrationWarnings(spies)).toEqual([]);
|
|
232
|
+
expect(button(host, "panel")).not.toBeNull();
|
|
233
|
+
await nextTick();
|
|
234
|
+
expect(button(host, "client")).not.toBeNull();
|
|
235
|
+
spies.forEach((spy) => spy.mockRestore());
|
|
236
|
+
});
|
|
237
|
+
});
|
|
@@ -51,13 +51,20 @@ afterEach(() => {
|
|
|
51
51
|
});
|
|
52
52
|
|
|
53
53
|
describe("DmsInstantSaveBadge", () => {
|
|
54
|
-
it("
|
|
54
|
+
it("says what the save state is, in en-GB", async () => {
|
|
55
55
|
const { default: InstantSaveBadge } = await import(
|
|
56
56
|
"../layers/dms-ui/app/components/save-bar/InstantSaveBadge.vue"
|
|
57
57
|
);
|
|
58
|
-
|
|
58
|
+
const labels = [
|
|
59
|
+
[undefined, "Saved instantly"],
|
|
60
|
+
["idle", "Saved instantly"],
|
|
61
|
+
["saving", "Saving"],
|
|
62
|
+
["saved", "Saved instantly"],
|
|
63
|
+
["error", "Not saved"],
|
|
64
|
+
] as const;
|
|
65
|
+
for (const [state, label] of labels) {
|
|
59
66
|
await mount({ render: () => h(InstantSaveBadge, { state }) });
|
|
60
|
-
expect(badge()?.textContent?.trim()).toBe(
|
|
67
|
+
expect(badge()?.textContent?.trim()).toBe(label);
|
|
61
68
|
expect(badge()?.dataset.state).toBe(state ?? "idle");
|
|
62
69
|
expect(badge()?.title).toBeTruthy();
|
|
63
70
|
app?.unmount();
|
|
@@ -82,7 +89,7 @@ describe("DmsInstantSaveBadge", () => {
|
|
|
82
89
|
}
|
|
83
90
|
});
|
|
84
91
|
|
|
85
|
-
it("
|
|
92
|
+
it("is translated in French", async () => {
|
|
86
93
|
vi.stubGlobal("useI18n", () => ({ t: translator(uiFr) }));
|
|
87
94
|
const { default: InstantSaveBadge } = await import(
|
|
88
95
|
"../layers/dms-ui/app/components/save-bar/InstantSaveBadge.vue"
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { createApp, h, nextTick, type App } from "vue";
|
|
4
|
+
import ListRow from "../layers/dms-ui/app/components/list-row/ListRow.vue";
|
|
5
|
+
|
|
6
|
+
const LONG_TITLE = "Safari on iPhone 15 with a very long device name";
|
|
7
|
+
|
|
8
|
+
let app: App | undefined;
|
|
9
|
+
let host: HTMLDivElement;
|
|
10
|
+
|
|
11
|
+
async function mount(truncate: boolean) {
|
|
12
|
+
app = createApp({ render: () => h(ListRow, { title: LONG_TITLE, truncate }) });
|
|
13
|
+
app.mount(host);
|
|
14
|
+
await nextTick();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const titleText = () =>
|
|
18
|
+
[...host.querySelectorAll("span")].find(
|
|
19
|
+
(span) => span.textContent?.trim() === LONG_TITLE,
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
vi.stubGlobal("useI18n", () => ({ t: (key: string) => key }));
|
|
24
|
+
host = document.createElement("div");
|
|
25
|
+
document.body.append(host);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
app?.unmount();
|
|
30
|
+
app = undefined;
|
|
31
|
+
host.remove();
|
|
32
|
+
vi.unstubAllGlobals();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("DmsListRow truncate", () => {
|
|
36
|
+
it("puts the ellipsis on the title text, not on its flex container", async () => {
|
|
37
|
+
await mount(true);
|
|
38
|
+
expect(titleText()?.classList.contains("truncate")).toBe(true);
|
|
39
|
+
expect(titleText()?.parentElement?.title).toBe(LONG_TITLE);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("leaves the title text alone without truncate", async () => {
|
|
43
|
+
await mount(false);
|
|
44
|
+
expect(titleText()?.classList.contains("truncate")).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
|
+
import { interpolateUrl } from "../layers/dms-core/app/utils/url-interpolation";
|
|
2
3
|
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
|
3
4
|
import {
|
|
4
5
|
type App,
|
|
@@ -203,9 +204,7 @@ beforeEach(() => {
|
|
|
203
204
|
vi.stubGlobal("useToast", () => ({ add: addToast }));
|
|
204
205
|
vi.stubGlobal("useConfirm", () => ({ confirm }));
|
|
205
206
|
vi.stubGlobal("useAuthFetch", () => ({ $authFetch: authFetch }));
|
|
206
|
-
vi.stubGlobal("interpolateUrl",
|
|
207
|
-
url.replace(/\{(\w+)\}/g, (_, key: string) => data[key] ?? ""),
|
|
208
|
-
);
|
|
207
|
+
vi.stubGlobal("interpolateUrl", interpolateUrl);
|
|
209
208
|
vi.stubGlobal("useTranslation", () => ({
|
|
210
209
|
processApiMessage: (message: string) => `t(${message})`,
|
|
211
210
|
}));
|
|
@@ -188,7 +188,12 @@ interface Mounted {
|
|
|
188
188
|
showTable: Ref<boolean>;
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
|
|
191
|
+
type ReadRow = (id: string) => Promise<Row | undefined>;
|
|
192
|
+
|
|
193
|
+
function mountPanel(
|
|
194
|
+
shownRows: Ref<Row[]> = ref(rows),
|
|
195
|
+
getRow?: ReadRow,
|
|
196
|
+
): Mounted {
|
|
192
197
|
let targets: ReturnType<typeof useActionTargets> | undefined;
|
|
193
198
|
const showTable = ref(true);
|
|
194
199
|
const Table = defineComponent({
|
|
@@ -201,7 +206,11 @@ function mountPanel(shownRows: Ref<Row[]> = ref(rows)): Mounted {
|
|
|
201
206
|
refreshCallback: () => {
|
|
202
207
|
refreshes += 1;
|
|
203
208
|
},
|
|
204
|
-
rowNavigation: {
|
|
209
|
+
rowNavigation: {
|
|
210
|
+
rows: () => shownRows.value,
|
|
211
|
+
rowIdKey: "_id",
|
|
212
|
+
getRow,
|
|
213
|
+
},
|
|
205
214
|
recordScope: { tableId: "requests", isSoleTableView: true },
|
|
206
215
|
});
|
|
207
216
|
return () => h("table");
|
|
@@ -480,6 +489,88 @@ describe("deep link to a row the table does not list", () => {
|
|
|
480
489
|
});
|
|
481
490
|
});
|
|
482
491
|
|
|
492
|
+
describe("showing another row from the panel", () => {
|
|
493
|
+
const older: Row = { _id: "r9", name: "Older" };
|
|
494
|
+
const notFoundError = () =>
|
|
495
|
+
Object.assign(new Error("Not Found"), { statusCode: 404 });
|
|
496
|
+
const showRow = (rowOrId: Row | string) =>
|
|
497
|
+
(
|
|
498
|
+
useRowSidePanelContent().value?.componentOptions.showRow as (
|
|
499
|
+
rowOrId: Row | string,
|
|
500
|
+
) => Promise<void>
|
|
501
|
+
)(rowOrId);
|
|
502
|
+
|
|
503
|
+
it("moves the highlight, the position and ?record= onto a listed row", async () => {
|
|
504
|
+
const { host, targets } = mountPanel();
|
|
505
|
+
targets.handleCustomRowAction(panelAction(), rows[0]);
|
|
506
|
+
await settle();
|
|
507
|
+
await showRow("r3");
|
|
508
|
+
await settle();
|
|
509
|
+
expect(shownName(host)).toBe("Third");
|
|
510
|
+
expect(position(host)).toBe("side_panel.position(3/3)");
|
|
511
|
+
expect(useRowSidePanelContent().value?.rowKey).toBe("r3");
|
|
512
|
+
expect(window.location.search).toBe("?record=r3");
|
|
513
|
+
await showRow(rows[1]!);
|
|
514
|
+
await settle();
|
|
515
|
+
expect(position(host)).toBe("side_panel.position(2/3)");
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it("reads an unlisted row by id, shown without a position", async () => {
|
|
519
|
+
const getRow = vi.fn(async () => older);
|
|
520
|
+
const { host, targets } = mountPanel(ref(rows), getRow);
|
|
521
|
+
targets.handleCustomRowAction(panelAction(), rows[0]);
|
|
522
|
+
await settle();
|
|
523
|
+
await showRow("r9");
|
|
524
|
+
await settle();
|
|
525
|
+
expect(getRow).toHaveBeenCalledWith("r9");
|
|
526
|
+
expect(shownName(host)).toBe("Older");
|
|
527
|
+
expect(position(host)).toBeUndefined();
|
|
528
|
+
expect(useRowSidePanelContent().value?.rowKey).toBe("r9");
|
|
529
|
+
expect(window.location.search).toBe("?record=r9");
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
it("says a row its route answers 404 for is gone", async () => {
|
|
533
|
+
const { host, targets } = mountPanel(ref(rows), async () => {
|
|
534
|
+
throw notFoundError();
|
|
535
|
+
});
|
|
536
|
+
targets.handleCustomRowAction(panelAction(), rows[0]);
|
|
537
|
+
await settle();
|
|
538
|
+
await showRow("gone");
|
|
539
|
+
await settle();
|
|
540
|
+
expect(query(host, "[data-dms-row-side-panel-not-found]")).not.toBeNull();
|
|
541
|
+
expect(useRowSidePanelContent().value?.rowKey).toBeUndefined();
|
|
542
|
+
expect(window.location.search).toBe("?record=gone");
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it("rejects a row it cannot read, keeping the row it shows", async () => {
|
|
546
|
+
const { host, targets } = mountPanel();
|
|
547
|
+
targets.handleCustomRowAction(panelAction(), rows[0]);
|
|
548
|
+
await settle();
|
|
549
|
+
await expect(showRow("r9")).rejects.toThrow("r9");
|
|
550
|
+
await settle();
|
|
551
|
+
expect(shownName(host)).toBe("First");
|
|
552
|
+
expect(window.location.search).toBe("?record=r1");
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
it("leaves the panel shut when it closed while the row was read", async () => {
|
|
556
|
+
let answer!: (row: Row) => void;
|
|
557
|
+
const { host, targets } = mountPanel(
|
|
558
|
+
ref(rows),
|
|
559
|
+
() => new Promise((resolve) => (answer = resolve)),
|
|
560
|
+
);
|
|
561
|
+
targets.handleCustomRowAction(panelAction(), rows[0]);
|
|
562
|
+
await settle();
|
|
563
|
+
const shown = showRow("r9");
|
|
564
|
+
closeRowSidePanel();
|
|
565
|
+
answer(older);
|
|
566
|
+
await shown;
|
|
567
|
+
await settle();
|
|
568
|
+
expect(useRowSidePanelContent().value).toBeNull();
|
|
569
|
+
expect(query(host, "[data-dms-side-panel]")).toBeNull();
|
|
570
|
+
expect(window.location.search).toBe("");
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
|
|
483
574
|
describe("side panel on a missing row", () => {
|
|
484
575
|
const notFound = (host: ParentNode) =>
|
|
485
576
|
query(host, "[data-dms-row-side-panel-not-found]");
|
|
@@ -61,7 +61,7 @@ afterEach(() => {
|
|
|
61
61
|
vi.unstubAllGlobals();
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
-
it("shows
|
|
64
|
+
it("shows a modal action form's way out and its ready submit", async () => {
|
|
65
65
|
const { onCancel } = await mount({
|
|
66
66
|
variant: "band",
|
|
67
67
|
kind: "action",
|
|
@@ -73,12 +73,24 @@ it("shows an empty modal action form's way out and its held submit", async () =>
|
|
|
73
73
|
expect(host.firstElementChild!.getAttribute("aria-hidden")).toBeNull();
|
|
74
74
|
expect(buttons()).toEqual([
|
|
75
75
|
{ label: "dms.button.cancel", disabled: false },
|
|
76
|
-
{ label: "Invite", disabled:
|
|
76
|
+
{ label: "Invite", disabled: false },
|
|
77
77
|
]);
|
|
78
78
|
host.querySelector("button")!.click();
|
|
79
79
|
expect(onCancel).toHaveBeenCalledOnce();
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
+
it("sends an untouched page action form as it is, its submit alone", async () => {
|
|
83
|
+
await mount({
|
|
84
|
+
variant: "band",
|
|
85
|
+
kind: "action",
|
|
86
|
+
dirty: false,
|
|
87
|
+
saveLabel: "Send",
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
expect(host.firstElementChild!.getAttribute("aria-hidden")).toBeNull();
|
|
91
|
+
expect(buttons()).toEqual([{ label: "Send", disabled: false }]);
|
|
92
|
+
});
|
|
93
|
+
|
|
82
94
|
it("resets an action form once a value changed, and sends it", async () => {
|
|
83
95
|
await mount({
|
|
84
96
|
variant: "band",
|
|
@@ -146,7 +158,7 @@ it("gives Cancel and Unsaved changes the same fixed height", async () => {
|
|
|
146
158
|
});
|
|
147
159
|
|
|
148
160
|
it("keeps a hidden band's place in its footer", async () => {
|
|
149
|
-
await mount({ variant: "band", kind: "
|
|
161
|
+
await mount({ variant: "band", kind: "record", dirty: false });
|
|
150
162
|
|
|
151
163
|
expect(anchor().getAttribute("aria-hidden")).toBe("true");
|
|
152
164
|
expect(classesOf(anchor())).toContain("invisible");
|
|
@@ -56,11 +56,11 @@ describe("DmsSaveStatus: a failed save", () => {
|
|
|
56
56
|
expect(onRetry).toHaveBeenCalledOnce();
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
-
it("turns the header pill red
|
|
59
|
+
it("turns the header pill red and says it was not saved", async () => {
|
|
60
60
|
await mount(() => h(InstantSaveBadge, { state: "error" }));
|
|
61
61
|
const pill = host.querySelector<HTMLElement>("[data-instant-save-badge]")!;
|
|
62
62
|
|
|
63
|
-
expect(pill.textContent?.trim()).toBe("
|
|
63
|
+
expect(pill.textContent?.trim()).toBe("Not saved");
|
|
64
64
|
expect(pill.className).toContain("text-error");
|
|
65
65
|
});
|
|
66
66
|
});
|
|
@@ -140,7 +140,7 @@ describe("Table row actions: refusals", () => {
|
|
|
140
140
|
label: "Resend",
|
|
141
141
|
target: {
|
|
142
142
|
type: "api",
|
|
143
|
-
url: "/api/invites/{_id}/resend",
|
|
143
|
+
url: "/api/invites/{{row._id}}/resend",
|
|
144
144
|
successMessage: "Sent",
|
|
145
145
|
},
|
|
146
146
|
confirm: { title: "Resend?", description: "" },
|
|
@@ -170,7 +170,7 @@ describe("Table row actions: refusals", () => {
|
|
|
170
170
|
target: {
|
|
171
171
|
type: "api",
|
|
172
172
|
method: "GET",
|
|
173
|
-
url: "/api/invites/{_id}/link",
|
|
173
|
+
url: "/api/invites/{{row._id}}/link",
|
|
174
174
|
successMessage: "Copied",
|
|
175
175
|
copy: "url",
|
|
176
176
|
},
|
|
@@ -267,7 +267,7 @@ describe("Table row actions: refusals", () => {
|
|
|
267
267
|
label: "Ban",
|
|
268
268
|
target: {
|
|
269
269
|
type: "api",
|
|
270
|
-
url: "/api/users/{_id}/ban",
|
|
270
|
+
url: "/api/users/{{row._id}}/ban",
|
|
271
271
|
body: { notify: true, scope: "all" },
|
|
272
272
|
successMessage: "Banned",
|
|
273
273
|
},
|
|
@@ -293,7 +293,7 @@ describe("Table row actions: refusals", () => {
|
|
|
293
293
|
const { deleteRows } = await rowActions();
|
|
294
294
|
|
|
295
295
|
const done = await deleteRows(["t1"], {
|
|
296
|
-
confirm: { from: "/api/task/{id}/delete-confirm" },
|
|
296
|
+
confirm: { from: "/api/task/{{row.id}}/delete-confirm" },
|
|
297
297
|
});
|
|
298
298
|
|
|
299
299
|
expect(done).toBe(true);
|
|
@@ -329,7 +329,7 @@ describe("Table row actions: refusals", () => {
|
|
|
329
329
|
const { deleteRows } = await rowActions();
|
|
330
330
|
|
|
331
331
|
await deleteRows(["t1", "t2"], {
|
|
332
|
-
confirm: { from: "/api/task/{id}/delete-confirm" },
|
|
332
|
+
confirm: { from: "/api/task/{{row.id}}/delete-confirm" },
|
|
333
333
|
});
|
|
334
334
|
|
|
335
335
|
expect(api).not.toHaveBeenCalledWith("/api/task/t1/delete-confirm");
|
|
@@ -164,13 +164,13 @@ describe("rowCustomActions", () => {
|
|
|
164
164
|
const stopSelling: CustomRowAction = {
|
|
165
165
|
id: "stop_selling",
|
|
166
166
|
label: "Stop selling",
|
|
167
|
-
target: { type: "api", url: "/plans/{_id}/stop", method: "POST" },
|
|
167
|
+
target: { type: "api", url: "/plans/{{row._id}}/stop", method: "POST" },
|
|
168
168
|
rule: { field: "isActive", equals: true },
|
|
169
169
|
};
|
|
170
170
|
const remove: CustomRowAction = {
|
|
171
171
|
id: "delete",
|
|
172
172
|
label: "Delete",
|
|
173
|
-
target: { type: "api", url: "/plans/{_id}", method: "DELETE" },
|
|
173
|
+
target: { type: "api", url: "/plans/{{row._id}}", method: "DELETE" },
|
|
174
174
|
rule: { field: "workspaces", equals: 0 },
|
|
175
175
|
};
|
|
176
176
|
|