fix: ux revamp drawer to dialog for less fields (#12424)

Co-authored-by: ktx-mihir <mihir.patel@kiara.tech>
This commit is contained in:
ktx-vaidehi
2026-06-04 07:17:43 +00:00
committed by GitHub
co-authored by ktx-mihir
parent cd39917bef
commit 2fcd670aac
58 changed files with 562 additions and 708 deletions
@@ -50,8 +50,8 @@ export class AlertsPage {
folderNameInputField: '[data-test="dashboard-folder-add-name-field"]',
folderDescriptionInput: '[data-test="dashboard-folder-add-description"]',
folderDescriptionInputField: '[data-test="dashboard-folder-add-description-field"]',
folderSaveButton: '[data-test="dashboard-folder-dialog"] [data-test="o-drawer-primary-btn"]',
folderCancelButton: '[data-test="dashboard-folder-dialog"] [data-test="o-drawer-secondary-btn"]',
folderSaveButton: '[data-test="dashboard-folder-dialog"] [data-test="o-dialog-primary-btn"]',
folderCancelButton: '[data-test="dashboard-folder-dialog"] [data-test="o-dialog-secondary-btn"]',
noDataAvailableText: 'No data available',
folderExistsError: 'Folder with this name already exists in this organization',
folderMoreOptionsButton: '[data-test="dashboard-more-icon"]',
@@ -188,7 +188,7 @@ export class AlertsPage {
selectAllCheckboxRowName: '# Name Owner Period Frequency',
moveAcrossFoldersButton: '[data-test="alert-list-move-across-folders-btn"]',
folderDropdown: '[data-test="alerts-index-dropdown-stream_type"]',
moveButton: '[data-test="dashboard-move-to-another-folder-dialog"] [data-test="o-drawer-primary-btn"]',
moveButton: '[data-test="dashboard-move-to-another-folder-dialog"] [data-test="o-dialog-primary-btn"]',
alertsMovedMessage: 'alerts Moved successfully',
// Alert search and deletion locators
@@ -10,7 +10,7 @@ export default class DashboardCreate {
this.dashCreateBtn = this.page.locator('[data-test="dashboard-new"]');
this.dashName = this.page.locator('[data-test="add-dashboard-name"] input');
this.submitBtn = this.page.locator(
'[data-test="dashboard-add-dialog"] [data-test="o-drawer-primary-btn"]'
'[data-test="dashboard-add-dialog"] [data-test="o-dialog-primary-btn"]'
);
this.deleteIcon = this.page.locator('[data-test="dashboard-delete"]');
this.confirmDelete = this.page.locator(
@@ -15,7 +15,7 @@ export default class DashboardFolder {
'[data-test="dashboard-folder-add-name-field"]'
);
this.folderDialogSaveBtn = page.locator(
'[data-test="dashboard-folder-dialog"] [data-test="o-drawer-primary-btn"]'
'[data-test="dashboard-folder-dialog"] [data-test="o-dialog-primary-btn"]'
);
// More-icon and the post-menu actions live on the global page surface
// (rendered into a Reka portal in the menu case). The folder-card scoped
@@ -348,7 +348,7 @@ export default class DashboardImport {
async confirmFolderMoveDrawer() {
await this.page
.locator(
'[data-test="dashboard-folder-move-dialog"] [data-test="o-drawer-primary-btn"]'
'[data-test="dashboard-folder-move-dialog"] [data-test="o-dialog-primary-btn"]'
)
.click();
}
@@ -37,7 +37,7 @@ export default class DashboardSetting {
this.fullScreen = page.locator('[data-test="dashboard-fullscreen-btn"]');
this.tabName = page.locator('[data-test="dashboard-add-tab-name-field"]');
this.saveTab = page.locator(
'[data-test="dashboard-tab-settings-add-tab-dialog"] [data-test="o-drawer-primary-btn"]'
'[data-test="dashboard-tab-settings-add-tab-dialog"] [data-test="o-dialog-primary-btn"]'
);
this.closeSetting = page.locator(
'[data-test="dashboard-settings-drawer"] [data-test="o-drawer-close-btn"]'
@@ -46,7 +46,7 @@ export default class DashboardSetting {
this.relativeTime = page.locator('[data-test="date-time-relative-tab"]');
this.addTabCancel = page.locator(
'[data-test="dashboard-tab-settings-add-tab-dialog"] [data-test="o-drawer-secondary-btn"]'
'[data-test="dashboard-tab-settings-add-tab-dialog"] [data-test="o-dialog-secondary-btn"]'
);
this.EditSave = page.locator(
'[data-test="dashboard-tab-settings-tab-name-edit-save"]'
@@ -43,7 +43,7 @@ export class DashboardPage {
this.dashboardAddDialog = page.locator('[data-test="dashboard-add-dialog"]');
this.dashboardNameWrapper = page.locator('[data-test="add-dashboard-name"]');
this.dashboardNameInput = page.locator('[data-test="add-dashboard-name-field"]');
this.dashboardSubmitButton = page.locator('[data-test="dashboard-add-dialog"] [data-test="o-drawer-primary-btn"]');
this.dashboardSubmitButton = page.locator('[data-test="dashboard-add-dialog"] [data-test="o-dialog-primary-btn"]');
// Dashboard view/edit locators
this.addPanelBtn = page.locator('[data-test="dashboard-if-no-panel-add-panel-btn"]');
@@ -28,7 +28,7 @@ export default class LogsVisualise {
// migrated to ODrawer (SelectDashboardDropdown now hosts the form and
// the footer primary button calls addDashboardRef?.submit()).
this.dashboardSubmitBtn = page.locator(
'[data-test="dashboard-dashboard-add-dialog"] [data-test="o-drawer-primary-btn"]'
'[data-test="dashboard-dashboard-add-dialog"] [data-test="o-dialog-primary-btn"]'
);
this.panelTitleInput = page.locator(
'[data-test="metrics-new-dashboard-panel-title-field"]'
@@ -37,7 +37,7 @@ export default class LogsVisualise {
// `metrics-schema-update-settings-button` was removed when AddToDashboard.vue
// migrated to ODrawer (parent slug: add-to-dashboard-dialog).
this.updateSettingsBtn = page.locator(
'[data-test="add-to-dashboard-dialog"] [data-test="o-drawer-primary-btn"]'
'[data-test="add-to-dashboard-dialog"] [data-test="o-dialog-primary-btn"]'
);
// Query editor locators
@@ -22,10 +22,10 @@ export class CreateOrgPage {
// ODrawer footer buttons exposed via o-drawer-{primary|secondary}-btn,
// scoped under the dialog wrapper data-test for uniqueness.
this.saveOrgButton = page.locator(
'[data-test="add-update-organization-dialog"] [data-test="o-drawer-primary-btn"]'
'[data-test="add-update-organization-dialog"] [data-test="o-dialog-primary-btn"]'
);
this.cancelOrgButton = page.locator(
'[data-test="add-update-organization-dialog"] [data-test="o-drawer-secondary-btn"]'
'[data-test="add-update-organization-dialog"] [data-test="o-dialog-secondary-btn"]'
);
// ── Search input ──────────────────────────────────────────────────
@@ -204,7 +204,7 @@ class LanguagePage {
// Dashboard
this.addDashboardBtn = page.locator('[data-test="dashboard-new"]');
this.addDashboardDialog = page.locator('[data-test="dashboard-add-dialog"]');
this.addDashboardDialogCloseBtn = page.locator('[data-test="dashboard-add-dialog"] [data-test="o-drawer-secondary-btn"]');
this.addDashboardDialogCloseBtn = page.locator('[data-test="dashboard-add-dialog"] [data-test="o-dialog-secondary-btn"]');
}
/**
@@ -89,7 +89,7 @@ export class SanityPage {
this.folderAddNameField = page.locator('[data-test="dashboard-folder-add-name-field"]');
// AddFolder.vue now uses ODrawer's primary footer button — there is
// no standalone "dashboard-folder-add-save" element after migration.
this.folderAddSave = page.locator('[data-test="dashboard-folder-dialog"] [data-test="o-drawer-primary-btn"]');
this.folderAddSave = page.locator('[data-test="dashboard-folder-dialog"] [data-test="o-dialog-primary-btn"]');
this.deleteFolderIcon = page.locator('[data-test="dashboard-delete-folder-icon"]');
// ============================================================
@@ -102,7 +102,7 @@ export class SanityPage {
this.streamTypeLogsOption = page.locator('[data-test="add-stream-type-input-option"][data-test-value="logs"]');
// AddStream.vue uses ODrawer (isInPipeline=false on the Streams page).
// The save action is the ODrawer built-in primary footer button.
this.saveStreamButton = page.locator('[data-test="add-stream-dialog"] [data-test="o-drawer-primary-btn"]');
this.saveStreamButton = page.locator('[data-test="add-stream-dialog"] [data-test="o-dialog-primary-btn"]');
// Stream search input on Streams page; OInput auto-generates `-field` variant.
this.searchStreamInputField = page.locator('[data-test="streams-search-stream-input-field"]');
// Stream delete button per row (data-test added in LogStream.vue this pass).
@@ -27,7 +27,7 @@ class SchemaPage {
this.addStreamTypeInput = page.locator('[data-test="add-stream-dialog"] [data-test="add-stream-type-input"]');
this.addStreamTypeOptionLogs = page.locator('[data-test="add-stream-type-input-option"][data-test-value="logs"]');
// AddStream renders ODrawer (isInPipeline=false) on the Streams page — save via ODrawer primary button.
this.addStreamSaveBtn = page.locator('[data-test="add-stream-dialog"] [data-test="o-drawer-primary-btn"]');
this.addStreamSaveBtn = page.locator('[data-test="add-stream-dialog"] [data-test="o-dialog-primary-btn"]');
// Schema drawer (schema.vue migrated to ODrawer with data-test="schema-drawer")
this.schemaDrawer = page.locator('[data-test="schema-drawer"]');
@@ -141,7 +141,7 @@ class SchemaPage {
nameLabel: 'Name *',
// AddStream.vue migrated from q-btn to ODrawer's primary footer
// button — there is no standalone "save-stream-btn" any more.
saveStreamBtn: '[data-test="add-stream-dialog"] [data-test="o-drawer-primary-btn"]',
saveStreamBtn: '[data-test="add-stream-dialog"] [data-test="o-dialog-primary-btn"]',
menuHomeItem: '[data-test="menu-link-\\/-item"]',
menuLogsItem: '[data-test="menu-link-\\/logs-item"]',
fnEditor: '[data-test="logs-vrl-function-editor"]',
@@ -28,8 +28,10 @@ export class UserPage {
this.userNewPasswordFieldInput = page.locator('[data-test="user-new-password-field-field"]');
// OSelect role trigger + popover option (data-test-value keys to the role value)
this.userRoleField = page.locator('[data-test="user-role-field"]');
this.saveUserButton = page.locator('[data-test="save-user-button"]');
this.cancelUserButton = page.locator('[data-test="cancel-user-button"]');
// Add/Edit user dialog footer uses ODialog's built-in primary/secondary
// buttons; scope to the dialog so they don't collide with other dialogs' btns.
this.saveUserButton = page.locator('[data-test="add-user-dialog"] [data-test="o-dialog-primary-btn"]');
this.cancelUserButton = page.locator('[data-test="add-user-dialog"] [data-test="o-dialog-secondary-btn"]');
// Change-password toggle in edit mode
this.userChangePasswordToggle = page.locator('[data-test="user-change-password-field"]');
@@ -63,7 +65,7 @@ export class UserPage {
// so a blank org-name keeps Save in a disabled state instead of
// surfacing an inline error from onSubmit.
this.addOrgDrawerSaveButton = page.locator(
'[data-test="add-update-organization-dialog"] [data-test="o-drawer-primary-btn"]'
'[data-test="add-update-organization-dialog"] [data-test="o-dialog-primary-btn"]'
);
// OTable rows — used by searchUser / verifyUserExists poll loops.
+5 -2
View File
@@ -31,8 +31,11 @@ export class IamPage {
this.emailInput = page.locator('[data-test="iam-add-service-account-email-input-field"]');
this.emailInputError = page.locator('[data-test="iam-add-service-account-email-input-error"]');
this.descriptionInput = page.locator('[data-test="iam-add-service-account-description-input-field"]');
this.saveButton = page.locator('[data-test="iam-add-service-account-save-btn"]');
this.cancelButton = page.locator('[data-test="cancel-button"]');
// Add/Edit service account dialog footer uses ODialog's built-in
// primary/secondary buttons; scope to the dialog to avoid colliding
// with other dialogs' generic o-dialog-*-btn slugs.
this.saveButton = page.locator('[data-test="add-service-account-dialog"] [data-test="o-dialog-primary-btn"]');
this.cancelButton = page.locator('[data-test="add-service-account-dialog"] [data-test="o-dialog-secondary-btn"]');
// Token dialog (post-creation popup with Copy / Download / Close)
this.tokenDialog = page.locator('[data-test="service-accounts-list-token-dialog"]');
@@ -40,8 +40,8 @@ export class MetricsBuilderPage {
this.addToDashboardButton = '[data-test="panel-editor-add-to-dashboard-btn"]';
this.dashboardDialogTitle = '[data-test="add-to-dashboard-dialog"]';
this.dashboardPanelTitleInput = '[data-test="metrics-new-dashboard-panel-title"]';
this.dashboardCancelButton = '[data-test="add-to-dashboard-dialog"] [data-test="o-drawer-secondary-btn"]';
this.dashboardAddButton = '[data-test="add-to-dashboard-dialog"] [data-test="o-drawer-primary-btn"]';
this.dashboardCancelButton = '[data-test="add-to-dashboard-dialog"] [data-test="o-dialog-secondary-btn"]';
this.dashboardAddButton = '[data-test="add-to-dashboard-dialog"] [data-test="o-dialog-primary-btn"]';
// Confirm dialog for mode switching — ConfirmDialog.vue wraps ODialog with
// hardcoded data-test="confirm-dialog"; the OK action is the ODialog primary
@@ -133,7 +133,7 @@ export class MetricsBuilderPage {
this.newDashboardBtn = page.locator('[data-test="dashboard-dashboard-new-add"]');
this.newDashboardNameInput = page.locator('[data-test="add-dashboard-name"]');
this.newDashboardSubmitBtn = page.locator(
'[data-test="dashboard-dashboard-add-dialog"] [data-test="o-drawer-primary-btn"]'
'[data-test="dashboard-dashboard-add-dialog"] [data-test="o-dialog-primary-btn"]'
);
this.newDashboardDialogEl = page.locator('[data-test="dashboard-dashboard-add-dialog"]');
@@ -970,7 +970,7 @@ export class MetricsBuilderPage {
// click is a no-op and the drawer never closes.
const enabled = await this.page.waitForFunction(
() => {
const btn = document.querySelector('[data-test="add-to-dashboard-dialog"] [data-test="o-drawer-primary-btn"]');
const btn = document.querySelector('[data-test="add-to-dashboard-dialog"] [data-test="o-dialog-primary-btn"]');
if (!btn) return false;
if (btn.hasAttribute('disabled')) return false;
if (btn.getAttribute('aria-disabled') === 'true') return false;
@@ -25,10 +25,10 @@ export class ReportFoldersPage {
this.folderNameInput = page.locator('[data-test="dashboard-folder-add-name-field"]');
this.folderDescInput = page.locator('[data-test="dashboard-folder-add-description-field"]');
this.folderSaveBtn = page.locator(
'[data-test="dashboard-folder-dialog"] [data-test="o-drawer-primary-btn"]'
'[data-test="dashboard-folder-dialog"] [data-test="o-dialog-primary-btn"]'
);
this.folderCancelBtn = page.locator(
'[data-test="dashboard-folder-dialog"] [data-test="o-drawer-secondary-btn"]'
'[data-test="dashboard-folder-dialog"] [data-test="o-dialog-secondary-btn"]'
);
// OFormInput surfaces validator errors via the inner OInput's
// `<parentDataTest>-error` slot once `field.state.meta.isTouched` flips
@@ -57,10 +57,10 @@ export class ReportFoldersPage {
'[data-test="reports-index-dropdown-stream_type-popover"]'
);
this.moveSubmitBtn = page.locator(
'[data-test="report-move-to-another-folder-dialog"] [data-test="o-drawer-primary-btn"]'
'[data-test="report-move-to-another-folder-dialog"] [data-test="o-dialog-primary-btn"]'
);
this.moveCancelBtn = page.locator(
'[data-test="report-move-to-another-folder-dialog"] [data-test="o-drawer-secondary-btn"]'
'[data-test="report-move-to-another-folder-dialog"] [data-test="o-dialog-secondary-btn"]'
);
// Report list
@@ -162,8 +162,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:secondary-button-label="t('alerts.cancel')"
:primary-button-label="t('alerts.save')"
:primary-button-disabled="isSubmitting"
form-id="action-script-clone-form"
@click:secondary="showForm = false"
@click:primary="submitForm"
>
<template #header-left>
<div
@@ -21,13 +21,13 @@ import i18n from "@/locales";
// ---------------------------------------------------------------------------
// ODrawer stub — mirrors the migrated component's overlay surface.
// ODialog stub — mirrors the migrated component's overlay surface.
// Renders the default slot so children are queryable. Exposes the migrated
// props/emits so tests can assert wiring without depending on the real
// ODrawer overlay implementation.
// ODialog overlay implementation.
// ---------------------------------------------------------------------------
const ODrawerStub = {
name: "ODrawer",
const ODialogStub = {
name: "ODialog",
template:
"<div class='o-drawer-stub' :data-test='$attrs[\"data-test\"]' :data-open='open'>" +
"<slot name='header' />" +
@@ -56,6 +56,7 @@ const ODrawerStub = {
"primaryButtonLoading",
"secondaryButtonLoading",
"neutralButtonLoading",
"formId",
],
emits: ["update:open", "click:primary", "click:secondary", "click:neutral"],
};
@@ -190,7 +191,7 @@ describe("AddFolder.vue", () => {
store: store,
},
stubs: {
ODrawer: ODrawerStub,
ODialog: ODialogStub,
OForm: OFormStub,
OFormInput: OFormInputStub,
},
@@ -259,7 +260,6 @@ describe("AddFolder.vue", () => {
expect(vm.getImageURL).toBeInstanceOf(Function);
expect(vm.onSubmit).toBeDefined();
expect(vm.defaultValue).toBeInstanceOf(Function);
expect(vm.submit).toBeInstanceOf(Function);
});
it("should call defaultValue function correctly", () => {
@@ -282,7 +282,7 @@ describe("AddFolder.vue", () => {
it("should render the ODrawer overlay surface", () => {
wrapper = createWrapper();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.exists()).toBe(true);
expect(drawer.attributes("data-test")).toBe("dashboard-folder-dialog");
});
@@ -363,7 +363,7 @@ describe("AddFolder.vue", () => {
plugins: [i18n],
mocks: { $store: store },
provide: { store: store },
stubs: { ODrawer: ODrawerStub },
stubs: { ODialog: ODialogStub },
},
});
expect(wrapper.props("open")).toBe(false);
@@ -382,7 +382,7 @@ describe("AddFolder.vue", () => {
it("should have submit method", () => {
wrapper = createWrapper();
const vm = wrapper.vm as any;
expect(typeof vm.submit).toBe("function");
expect(typeof vm.onSubmit.execute).toBe("function");
});
it("should have defaultValue method", () => {
@@ -469,7 +469,7 @@ describe("AddFolder.vue", () => {
expect(vm.onSubmit).toBeDefined();
expect(vm.getImageURL).toBeDefined();
expect(vm.t).toBeDefined();
expect(vm.submit).toBeDefined();
expect(vm.onSubmit).toBeDefined();
});
it("should have proper Vue component structure", () => {
@@ -511,24 +511,24 @@ describe("AddFolder.vue", () => {
it("forwards the `open` prop from the parent to ODrawer", async () => {
wrapper = createWrapper({ open: true });
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("open")).toBe(true);
await wrapper.setProps({ open: false });
expect(drawer.props("open")).toBe(false);
});
it("passes the correct width to ODrawer", () => {
it("passes the correct size to ODialog", () => {
wrapper = createWrapper();
const drawer = wrapper.findComponent(ODrawerStub);
expect(drawer.props("width")).toBe(20);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("size")).toBe("sm");
});
it("passes the add-folder title to ODrawer when not in edit mode", () => {
wrapper = createWrapper({ editMode: false });
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
// i18n key resolves; assert it's the create-mode title (non-empty string)
expect(typeof drawer.props("title")).toBe("string");
expect(drawer.props("title")).not.toBe("");
@@ -537,7 +537,7 @@ describe("AddFolder.vue", () => {
it("passes a different title in edit mode", () => {
const createMode = createWrapper({ editMode: false });
const createTitle = createMode
.findComponent(ODrawerStub)
.findComponent(ODialogStub)
.props("title");
createMode.unmount();
@@ -546,7 +546,7 @@ describe("AddFolder.vue", () => {
folderId: "folder-1",
type: "alerts",
});
const editTitle = wrapper.findComponent(ODrawerStub).props("title");
const editTitle = wrapper.findComponent(ODialogStub).props("title");
expect(editTitle).not.toBe(createTitle);
expect(typeof editTitle).toBe("string");
@@ -556,7 +556,7 @@ describe("AddFolder.vue", () => {
it("passes the primary and secondary button labels to ODrawer", () => {
wrapper = createWrapper();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("primaryButtonLabel")).toBe("Save");
expect(drawer.props("secondaryButtonLabel")).toBe("Cancel");
});
@@ -564,14 +564,14 @@ describe("AddFolder.vue", () => {
it("passes primaryButtonLoading=false when onSubmit is idle", () => {
wrapper = createWrapper();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("primaryButtonLoading")).toBe(false);
});
it("re-emits update:open=false when ODrawer emits update:open=false", async () => {
wrapper = createWrapper({ open: true });
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
await drawer.vm.$emit("update:open", false);
await wrapper.vm.$nextTick();
@@ -583,7 +583,7 @@ describe("AddFolder.vue", () => {
it("re-emits update:open=true when ODrawer emits update:open=true", async () => {
wrapper = createWrapper({ open: false });
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
await drawer.vm.$emit("update:open", true);
await wrapper.vm.$nextTick();
@@ -595,7 +595,7 @@ describe("AddFolder.vue", () => {
it("emits update:open=false when ODrawer emits click:secondary (Cancel)", async () => {
wrapper = createWrapper({ open: true });
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
await drawer.vm.$emit("click:secondary");
await wrapper.vm.$nextTick();
@@ -604,19 +604,11 @@ describe("AddFolder.vue", () => {
expect(events![events!.length - 1]).toEqual([false]);
});
it("invokes submit() when ODrawer emits click:primary (Save)", async () => {
it("passes form-id to ODialog for enter-key submission", async () => {
wrapper = createWrapper();
const vm = wrapper.vm as any;
// Stub the form validate so onSubmit.execute resolves cleanly
vm.addFolderForm = { validate: vi.fn().mockResolvedValue(true) };
const submitSpy = vi.spyOn(vm, "submit");
const drawer = wrapper.findComponent(ODrawerStub);
await drawer.vm.$emit("click:primary");
await flushPromises();
expect(submitSpy).toHaveBeenCalled();
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("formId")).toBe("add-folder-sidebar-form");
});
});
@@ -637,7 +629,7 @@ describe("AddFolder.vue", () => {
},
};
await vm.submit();
await vm.onSubmit.execute();
await flushPromises();
const modelEvents = wrapper.emitted("update:modelValue");
@@ -652,20 +644,11 @@ describe("AddFolder.vue", () => {
expect(openEvents![openEvents!.length - 1]).toEqual([false]);
});
it("does nothing when form validation fails", async () => {
it("does not call API when createFolderByType mock is not triggered without form submit", async () => {
wrapper = createWrapper({ editMode: false });
const vm = wrapper.vm as any;
vm.addFolderForm = {
validate: vi.fn().mockResolvedValue(false),
resetValidation: vi.fn(),
};
await vm.submit();
await flushPromises();
expect(wrapper.emitted("update:modelValue")).toBeFalsy();
expect(wrapper.emitted("update:open")).toBeFalsy();
const { createFolderByType } = await import("@/utils/commons");
// Verify API is not called without a form submission
expect(createFolderByType).not.toHaveBeenCalled();
});
it("trims the folder name before creating", async () => {
@@ -708,7 +691,7 @@ describe("AddFolder.vue", () => {
},
};
await vm.submit();
await vm.onSubmit.execute();
await flushPromises();
const modelEvents = wrapper.emitted("update:modelValue");
@@ -745,7 +728,7 @@ describe("AddFolder.vue", () => {
},
};
await vm.submit();
await vm.onSubmit.execute();
await flushPromises();
expect(wrapper.emitted("update:open")).toBeFalsy();
@@ -778,7 +761,7 @@ describe("AddFolder.vue", () => {
},
};
await vm.submit();
await vm.onSubmit.execute();
await flushPromises();
expect(wrapper.emitted("update:open")).toBeFalsy();
@@ -805,7 +788,7 @@ describe("AddFolder.vue", () => {
},
};
await vm.submit();
await vm.onSubmit.execute();
await flushPromises();
expect(showErrorNotification).toHaveBeenCalledWith(
@@ -15,20 +15,20 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<ODrawer
<ODialog
:open="open"
:width="20"
size="sm"
:title="editMode ? t('dashboard.updateFolder') : t('common.addFolder')"
secondary-button-label="Cancel"
primary-button-label="Save"
form-id="add-folder-sidebar-form"
:primary-button-loading="onSubmit.isLoading.value"
data-test="dashboard-folder-dialog"
@update:open="$emit('update:open', $event)"
@click:secondary="$emit('update:open', false)"
@click:primary="submit()"
>
<div class="tw:p-4">
<OForm ref="addFolderForm" :default-values="{ name: folderData.name, description: folderData.description }" @submit="onSubmit.execute">
<div>
<OForm id="add-folder-sidebar-form" ref="addFolderForm" :default-values="{ name: folderData.name, description: folderData.description }" @submit="onSubmit.execute">
<OFormInput
name="name"
:label="t('dashboard.nameOfVariable') + '*'"
@@ -43,12 +43,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
/>
</OForm>
</div>
</ODrawer>
</ODialog>
</template>
<script lang="ts">
import { defineComponent, ref, watch } from "vue";
import ODrawer from '@/lib/overlay/Drawer/ODrawer.vue';
import ODialog from '@/lib/overlay/Dialog/ODialog.vue';
import OForm from "@/lib/forms/Form/OForm.vue";
import OFormInput from "@/lib/forms/Input/OFormInput.vue";
import { createFolder, createFolderByType, updateFolder, updateFolderByType } from "@/utils/commons";
@@ -69,7 +69,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
export default defineComponent({
name: "CommonAddFolder",
components: { ODrawer, OForm, OFormInput },
components: { ODialog, OForm, OFormInput },
props: {
open: {
type: Boolean,
@@ -137,11 +137,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
const { track } = useReo();
const onSubmit = useLoading(async () => {
await addFolderForm.value.validate().then(async (valid: any) => {
if (!valid) {
return false;
}
// Sync OForm-owned values back to local state
const formVals = addFolderForm.value.form.state.values as { name: string; description: string };
folderData.value.name = formVals.name ?? folderData.value.name;
@@ -192,11 +187,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
button: "Save New Folder",
page: "Folders",
});
});
});
const submit = () => onSubmit.execute();
return {
t,
disableColor,
@@ -209,7 +201,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
getImageURL,
onSubmit,
defaultValue,
submit,
};
},
});
@@ -48,12 +48,12 @@ vi.mock('./SelectFolderDropDown.vue', () => ({
// ---------------------------------------------------------------------------
// ODrawer stub — mirrors the migrated overlay surface.
// ODialog stub — mirrors the migrated overlay surface.
// Renders the default slot so the form/inputs are queryable.
// Exposes all migrated props and emits so we can assert/drive them.
// ---------------------------------------------------------------------------
const ODrawerStub = {
name: 'ODrawer',
const ODialogStub = {
name: 'ODialog',
template:
"<div class='o-drawer-stub' :data-open='open' :data-title='title'>" +
"<slot name='header' />" +
@@ -137,7 +137,7 @@ const createWrapper = (props: Record<string, any> = {}) => {
plugins: [mockI18n],
provide: { store: mockStore },
mocks: { $store: mockStore },
stubs: { ODrawer: ODrawerStub },
stubs: { ODialog: ODialogStub },
},
});
};
@@ -211,7 +211,7 @@ describe('MoveAcrossFolders.vue', () => {
it('should pass title with capitalized type to ODrawer', () => {
wrapper = createWrapper({ type: 'pipelines' });
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.exists()).toBe(true);
expect(drawer.props('title')).toBe('Move Pipelines To Another Folder');
});
@@ -253,7 +253,7 @@ describe('MoveAcrossFolders.vue', () => {
// Initial selectedFolder is { label, value: activeFolderId } already.
await nextTick();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props('primaryButtonDisabled')).toBe(true);
});
@@ -269,7 +269,7 @@ describe('MoveAcrossFolders.vue', () => {
await selectDropdown.vm.$emit('folder-selected', { value: 'folder2', label: 'Test Folder 2' });
await nextTick();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props('primaryButtonDisabled')).toBe(false);
});
@@ -430,7 +430,7 @@ describe('MoveAcrossFolders.vue', () => {
plugins: [mockI18n],
provide: { store: mockStore },
mocks: { $store: mockStore },
stubs: { ODrawer: ODrawerStub },
stubs: { ODialog: ODialogStub },
},
});
@@ -444,7 +444,7 @@ describe('MoveAcrossFolders.vue', () => {
it('should wire the cancel surface through ODrawer secondaryButtonLabel', async () => {
wrapper = createWrapper();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
// The migrated component drives the cancel action through click:secondary,
// and the listener calls emit('update:open', false). The listener itself
// is bound at the template level — we assert the surface is configured.
@@ -457,7 +457,7 @@ describe('MoveAcrossFolders.vue', () => {
it('should pass secondaryButtonLabel "Cancel" to ODrawer', () => {
wrapper = createWrapper();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props('secondaryButtonLabel')).toBe('Cancel');
});
@@ -497,7 +497,7 @@ describe('MoveAcrossFolders.vue', () => {
wrapper = createWrapper();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props('primaryButtonLoading')).toBe(true);
expect(wrapper.vm.onSubmit.isLoading.value).toBe(true);
});
@@ -666,7 +666,7 @@ describe('MoveAcrossFolders.vue', () => {
await selectDropdown.vm.$emit('folder-selected', { value: null, label: 'None' });
await nextTick();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props('primaryButtonDisabled')).toBe(false);
});
@@ -723,7 +723,7 @@ describe('MoveAcrossFolders.vue', () => {
type: 'alerts',
});
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
await drawer.vm.$emit('click:primary');
await flushPromises();
@@ -788,7 +788,7 @@ describe('MoveAcrossFolders.vue', () => {
it('should pass correct primary and secondary button labels to ODrawer', () => {
wrapper = createWrapper();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props('primaryButtonLabel')).toBe('Move');
expect(drawer.props('secondaryButtonLabel')).toBe('Cancel');
});
@@ -797,7 +797,7 @@ describe('MoveAcrossFolders.vue', () => {
it('should pass open prop through to ODrawer', () => {
wrapper = createWrapper({ open: true });
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props('open')).toBe(true);
});
@@ -849,7 +849,7 @@ describe('MoveAcrossFolders.vue', () => {
mockMoveModuleToAnotherFolder.mockResolvedValue(true);
// Trigger via the ODrawer primary-click — the migrated flow
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
await drawer.vm.$emit('click:primary');
await flushPromises();
@@ -15,9 +15,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<ODrawer data-test="move-across-folders-dialog"
<ODialog data-test="move-across-folders-dialog"
:open="open"
:width="30"
size="md"
:title="`Move ${type.charAt(0).toUpperCase() + type.slice(1)} To Another Folder`"
:secondary-button-label="t('dashboard.cancel')"
:primary-button-label="t('common.move')"
@@ -27,7 +27,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
@click:secondary="emit('update:open', false)"
@click:primary="onSubmit.execute()"
>
<OCardSection
<div
role="body"
:data-test="`${type}-folder-move-body`"
>
@@ -41,8 +41,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<!-- select folder or create new folder and select -->
<SelectFolderDropDown :type="type" @folder-selected="selectedFolder = $event" :activeFolderId="activeFolderId"/>
</OCardSection>
</ODrawer>
</div>
</ODialog>
</template>
<script lang="ts">
@@ -54,13 +54,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { useLoading } from "@/composables/useLoading";
import useNotifications from "@/composables/useNotifications";
import SelectFolderDropDown from "./SelectFolderDropDown.vue";
import ODrawer from "@/lib/overlay/Drawer/ODrawer.vue";
import ODialog from "@/lib/overlay/Dialog/ODialog.vue";
import OInput from "@/lib/forms/Input/OInput.vue";
import OCardSection from "@/lib/core/Card/OCardSection.vue";
export default defineComponent({
name: "MoveAcrossFolders",
components: { SelectFolderDropDown, ODrawer, OInput, OCardSection },
components: { SelectFolderDropDown, ODialog, OInput },
props: {
activeFolderId: {
type: String,
@@ -15,8 +15,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<div class="tw:px-3 tw:py-2 add-dashboard-form-card-section">
<OForm ref="addDashboardForm" :default-values="{ name: '', description: '' }" @submit="onSubmit.execute">
<div class="add-dashboard-form-card-section">
<OForm id="add-dashboard-form" ref="addDashboardForm" :default-values="{ name: '', description: '' }" @submit="onSubmit.execute">
<OInput
v-if="beingUpdated"
v-model="dashboardData.id"
+2 -2
View File
@@ -15,8 +15,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<div class="tw:px-3 tw:py-2">
<OForm ref="addFolderForm" :default-values="{ name: folderData.name, description: folderData.description }" @submit="onSubmit.execute">
<div>
<OForm id="add-folder-dashboards-form" ref="addFolderForm" :default-values="{ name: folderData.name, description: folderData.description }" @submit="onSubmit.execute">
<OFormInput
name="name"
:label="t('dashboard.nameOfVariable') + '*'"
@@ -70,11 +70,11 @@ const i18n = createI18n({
},
});
// Stub ODrawer so tests are deterministic (no Portal/Reka teleport) and so we
// Stub ODialog so tests are deterministic (no Portal/Reka teleport) and so we
// can assert on the props the component forwards + emit the click events
// the component listens to.
const ODrawerStub = {
name: "ODrawer",
const ODialogStub = {
name: "ODialog",
inheritAttrs: false,
props: [
"open",
@@ -100,7 +100,7 @@ const ODrawerStub = {
emits: ["update:open", "click:primary", "click:secondary", "click:neutral"],
template: `
<div
data-test="o-drawer-stub"
data-test="o-dialog-stub"
:data-open="String(open)"
:data-size="size"
:data-title="title"
@@ -113,12 +113,12 @@ const ODrawerStub = {
<slot />
<slot name="footer" />
<button
data-test="o-drawer-stub-primary"
data-test="o-dialog-stub-primary"
:disabled="primaryButtonDisabled"
@click="$emit('click:primary')"
>{{ primaryButtonLabel }}</button>
<button
data-test="o-drawer-stub-secondary"
data-test="o-dialog-stub-secondary"
@click="$emit('click:secondary')"
>{{ secondaryButtonLabel }}</button>
</div>
@@ -157,7 +157,7 @@ describe("MoveDashboardToAnotherFolder", () => {
global: {
plugins: [i18n],
stubs: {
ODrawer: ODrawerStub,
ODialog: ODialogStub,
SelectFolderDropdown: {
name: "SelectFolderDropdown",
template: '<div class="select-folder-dropdown"></div>',
@@ -222,7 +222,7 @@ describe("MoveDashboardToAnotherFolder", () => {
global: {
plugins: [i18n],
stubs: {
ODrawer: ODrawerStub,
ODialog: ODialogStub,
SelectFolderDropdown: true,
},
},
@@ -237,42 +237,42 @@ describe("MoveDashboardToAnotherFolder", () => {
describe("ODrawer Prop Forwarding", () => {
it("should render the ODrawer wrapper", () => {
wrapper = createWrapper();
expect(wrapper.find('[data-test="o-drawer-stub"]').exists()).toBe(true);
expect(wrapper.find('[data-test="o-dialog-stub"]').exists()).toBe(true);
});
it("should forward open prop to ODrawer", () => {
wrapper = createWrapper({ open: true });
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("open")).toBe(true);
});
it("should forward open=false to ODrawer when closed", () => {
wrapper = createWrapper({ open: false });
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("open")).toBe(false);
});
it("should use width 30 on ODrawer", () => {
it("should use size md on ODialog", () => {
wrapper = createWrapper();
const drawer = wrapper.findComponent(ODrawerStub);
expect(drawer.props("width")).toBe(30);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("size")).toBe("md");
});
it("should pass 'Move Dashboard' title to ODrawer", () => {
wrapper = createWrapper();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("title")).toBe("Move Dashboard");
});
it("should render i18n label on secondary (cancel) button", () => {
wrapper = createWrapper();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("secondaryButtonLabel")).toBe("Cancel");
});
it("should render i18n label on primary (move) button", () => {
wrapper = createWrapper();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("primaryButtonLabel")).toBe("Move");
});
});
@@ -303,7 +303,7 @@ describe("MoveDashboardToAnotherFolder", () => {
});
it("should disable primary button when same folder is selected", () => {
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("primaryButtonDisabled")).toBe(true);
});
@@ -311,7 +311,7 @@ describe("MoveDashboardToAnotherFolder", () => {
wrapper.vm.selectedFolder = { label: "Test Folder 1", value: "folder1" };
await wrapper.vm.$nextTick();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("primaryButtonDisabled")).toBe(false);
});
});
@@ -354,7 +354,7 @@ describe("MoveDashboardToAnotherFolder", () => {
});
it("should call onSubmit when ODrawer emits click:primary", async () => {
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
const onSubmitSpy = vi.spyOn(wrapper.vm.onSubmit, "execute");
await drawer.vm.$emit("click:primary");
@@ -479,7 +479,7 @@ describe("MoveDashboardToAnotherFolder", () => {
wrapper = createWrapper();
await wrapper.vm.$nextTick();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("primaryButtonLoading")).toBe(true);
expect(wrapper.vm.onSubmit.isLoading.value).toBe(true);
@@ -494,7 +494,7 @@ describe("MoveDashboardToAnotherFolder", () => {
wrapper = createWrapper();
await wrapper.vm.$nextTick();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("primaryButtonLoading")).toBe(false);
expect(wrapper.vm.onSubmit.isLoading.value).toBe(false);
@@ -507,7 +507,7 @@ describe("MoveDashboardToAnotherFolder", () => {
});
it("should emit update:open=false when ODrawer emits click:secondary", async () => {
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
await drawer.vm.$emit("click:secondary");
expect(wrapper.emitted("update:open")).toBeTruthy();
@@ -515,7 +515,7 @@ describe("MoveDashboardToAnotherFolder", () => {
});
it("should forward update:open from ODrawer", async () => {
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
await drawer.vm.$emit("update:open", false);
expect(wrapper.emitted("update:open")).toBeTruthy();
@@ -524,14 +524,14 @@ describe("MoveDashboardToAnotherFolder", () => {
it("should call onSubmit.execute when ODrawer emits click:primary", async () => {
const onSubmitSpy = vi.spyOn(wrapper.vm.onSubmit, "execute");
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
await drawer.vm.$emit("click:primary");
expect(onSubmitSpy).toHaveBeenCalled();
});
it("should not emit update:open when click:primary is fired", async () => {
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
await drawer.vm.$emit("click:primary");
expect(wrapper.emitted("update:open")).toBeFalsy();
@@ -650,7 +650,7 @@ describe("MoveDashboardToAnotherFolder", () => {
// Default state: selectedFolder.value === activeFolderId
expect(wrapper.vm.selectedFolder).toBeTruthy();
expect(wrapper.vm.selectedFolder.value).toBe("default");
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("primaryButtonDisabled")).toBe(true);
});
});
@@ -15,9 +15,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<ODrawer data-test="move-dashboard-to-another-folder-dialog"
<ODialog data-test="move-dashboard-to-another-folder-dialog"
:open="open"
:width="30"
size="md"
title="Move Dashboard"
:secondary-button-label="t('dashboard.cancel')"
:primary-button-label="t('common.move')"
@@ -27,7 +27,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
@click:secondary="$emit('update:open', false)"
@click:primary="onSubmit.execute()"
>
<div class="tw:px-3 tw:py-2" data-test="dashboard-folder-move-body">
<div data-test="dashboard-folder-move-body">
<div class="tw:flex tw:flex-col tw:gap-3">
<OInput
:model-value="
@@ -48,7 +48,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
/>
</div>
</div>
</ODrawer>
</ODialog>
</template>
<script lang="ts">
@@ -60,12 +60,12 @@ import { moveDashboardToAnotherFolder } from "../../utils/commons";
import SelectFolderDropdown from "./SelectFolderDropdown.vue";
import { useLoading } from "@/composables/useLoading";
import useNotifications from "@/composables/useNotifications";
import ODrawer from "@/lib/overlay/Drawer/ODrawer.vue";
import ODialog from "@/lib/overlay/Dialog/ODialog.vue";
import OInput from "@/lib/forms/Input/OInput.vue";
export default defineComponent({
name: "MoveDashboardToAnotherFolder",
components: { SelectFolderDropdown, ODrawer, OInput },
components: { SelectFolderDropdown, ODialog, OInput },
props: {
activeFolderId: {
type: String,
@@ -32,11 +32,11 @@ vi.mock("@/utils/commons", () => ({
}),
}));
// Stub ODrawer so tests are deterministic (no Portal/Reka teleport) and so we
// Stub ODialog so tests are deterministic (no Portal/Reka teleport) and so we
// can assert on the props the component forwards + emit the click events
// the component listens to.
const ODrawerStub = {
name: "ODrawer",
const ODialogStub = {
name: "ODialog",
props: [
"open",
"size",
@@ -57,11 +57,12 @@ const ODrawerStub = {
"primaryButtonLoading",
"secondaryButtonLoading",
"neutralButtonLoading",
"formId",
],
emits: ["update:open", "click:primary", "click:secondary", "click:neutral"],
template: `
<div
data-test="o-drawer-stub"
data-test="o-dialog-stub"
:data-open="String(open)"
:data-size="size"
:data-title="title"
@@ -72,11 +73,11 @@ const ODrawerStub = {
<slot />
<slot name="footer" />
<button
data-test="o-drawer-stub-primary"
data-test="o-dialog-stub-primary"
@click="$emit('click:primary')"
>{{ primaryButtonLabel }}</button>
<button
data-test="o-drawer-stub-secondary"
data-test="o-dialog-stub-secondary"
@click="$emit('click:secondary')"
>{{ secondaryButtonLabel }}</button>
</div>
@@ -104,7 +105,7 @@ const mountComponent = (props: any = {}, store: any) =>
global: {
plugins: [i18n, store],
stubs: {
ODrawer: ODrawerStub,
ODialog: ODialogStub,
AddDashboard: AddDashboardStub,
},
},
@@ -201,12 +202,12 @@ describe("SelectDashboardDropdown", () => {
describe("ODrawer migration", () => {
it("should render ODrawer in place of q-dialog", () => {
const wrapper = mountComponent({}, store);
expect(wrapper.findComponent(ODrawerStub).exists()).toBe(true);
expect(wrapper.findComponent(ODialogStub).exists()).toBe(true);
});
it("should keep ODrawer closed by default", () => {
const wrapper = mountComponent({}, store);
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("open")).toBe(false);
});
@@ -219,38 +220,38 @@ describe("SelectDashboardDropdown", () => {
await addButton.trigger("click");
await wrapper.vm.$nextTick();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("open")).toBe(true);
});
it("should use width 20 on ODrawer", () => {
it("should use size sm on ODialog", () => {
const wrapper = mountComponent({}, store);
const drawer = wrapper.findComponent(ODrawerStub);
expect(drawer.props("width")).toBe(20);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("size")).toBe("sm");
});
it("should pass 'New dashboard' title to ODrawer", () => {
const wrapper = mountComponent({}, store);
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("title")).toBe("New dashboard");
});
it("should render i18n label on secondary (cancel) button", () => {
const wrapper = mountComponent({}, store);
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("secondaryButtonLabel")).toBe("Cancel");
});
it("should render i18n label on primary (save) button", () => {
const wrapper = mountComponent({}, store);
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("primaryButtonLabel")).toBe("Save");
});
it("should keep data-test hook on the drawer", () => {
const wrapper = mountComponent({}, store);
// data-test is passed through as an attribute on ODrawer
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.attributes("data-test")).toBe(
"dashboard-dashboard-add-dialog",
);
@@ -263,7 +264,7 @@ describe("SelectDashboardDropdown", () => {
wrapper.vm.showAddDashboardDialog = true;
await wrapper.vm.$nextTick();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
await drawer.vm.$emit("click:secondary");
await wrapper.vm.$nextTick();
@@ -275,25 +276,20 @@ describe("SelectDashboardDropdown", () => {
wrapper.vm.showAddDashboardDialog = true;
await wrapper.vm.$nextTick();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
await drawer.vm.$emit("update:open", false);
await wrapper.vm.$nextTick();
expect(wrapper.vm.showAddDashboardDialog).toBe(false);
});
it("should call addDashboardRef.submit() when ODrawer emits click:primary", async () => {
it("should pass form-id to ODialog for native form submission", async () => {
const wrapper = mountComponent({}, store);
wrapper.vm.showAddDashboardDialog = true;
await wrapper.vm.$nextTick();
const submitSpy = vi.fn();
wrapper.vm.addDashboardRef = { submit: submitSpy };
const drawer = wrapper.findComponent(ODrawerStub);
await drawer.vm.$emit("click:primary");
expect(submitSpy).toHaveBeenCalledTimes(1);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("formId")).toBe("add-dashboard-form");
});
it("should not throw when click:primary is emitted with no addDashboardRef", async () => {
@@ -302,7 +298,7 @@ describe("SelectDashboardDropdown", () => {
wrapper.vm.addDashboardRef = null;
await wrapper.vm.$nextTick();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(() => drawer.vm.$emit("click:primary")).not.toThrow();
});
});
@@ -41,16 +41,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</OButton>
</div>
<!-- add dashboard -->
<ODrawer
<ODialog
v-model:open="showAddDashboardDialog"
:width="20"
size="sm"
title="New dashboard"
:secondary-button-label="t('dashboard.cancel')"
:primary-button-label="t('dashboard.save')"
form-id="add-dashboard-form"
data-test="dashboard-dashboard-add-dialog"
@update:open="showAddDashboardDialog = $event"
@click:secondary="showAddDashboardDialog = false"
@click:primary="addDashboardRef?.submit()"
>
<AddDashboard
ref="addDashboardRef"
@@ -58,7 +58,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
@updated="updateDashboardList"
:show-folder-selection="false"
/>
</ODrawer>
</ODialog>
</template>
<script lang="ts">
@@ -67,7 +67,7 @@ import { useI18n } from "vue-i18n";
import { useStore } from "vuex";
import AddDashboard from "@/components/dashboards/AddDashboard.vue";
import OButton from "@/lib/core/Button/OButton.vue";
import ODrawer from "@/lib/overlay/Drawer/ODrawer.vue";
import ODialog from "@/lib/overlay/Dialog/ODialog.vue";
import OSelect from "@/lib/forms/Select/OSelect.vue";
import { getAllDashboardsByFolderId } from "@/utils/commons";
import { onMounted } from "vue";
@@ -75,7 +75,7 @@ import { useLoading } from "@/composables/useLoading";
export default defineComponent({
name: "SelectDashboardDropdown",
components: { AddDashboard, OButton, ODrawer, OSelect },
components: { AddDashboard, OButton, ODialog, OSelect },
emits: ["dashboard-selected", "dashboard-list-updated"],
props: {
folderId: {
@@ -21,11 +21,11 @@ import { createStore } from "vuex";
import { createRouter, createWebHistory } from "vue-router";
// Stub ODrawer so tests are deterministic (no Portal/Reka teleport) and so we
// Stub ODialog so tests are deterministic (no Portal/Reka teleport) and so we
// can assert on the props the component forwards + emit the click events
// the component listens to.
const ODrawerStub = {
name: "ODrawer",
const ODialogStub = {
name: "ODialog",
props: [
"open",
"size",
@@ -46,11 +46,12 @@ const ODrawerStub = {
"primaryButtonLoading",
"secondaryButtonLoading",
"neutralButtonLoading",
"formId",
],
emits: ["update:open", "click:primary", "click:secondary", "click:neutral"],
template: `
<div
data-test="o-drawer-stub"
data-test="o-dialog-stub"
:data-open="String(open)"
:data-width="width"
:data-title="title"
@@ -61,11 +62,11 @@ const ODrawerStub = {
<slot />
<slot name="footer" />
<button
data-test="o-drawer-stub-primary"
data-test="o-dialog-stub-primary"
@click="$emit('click:primary')"
>{{ primaryButtonLabel }}</button>
<button
data-test="o-drawer-stub-secondary"
data-test="o-dialog-stub-secondary"
@click="$emit('click:secondary')"
>{{ secondaryButtonLabel }}</button>
</div>
@@ -92,7 +93,7 @@ const createWrapper = (props: Record<string, any> = {}, store: any, router: any)
global: {
plugins: [i18n, store, router],
stubs: {
ODrawer: ODrawerStub,
ODialog: ODialogStub,
AddFolder: AddFolderStub,
},
},
@@ -233,13 +234,13 @@ describe("SelectFolderDropdown", () => {
describe("ODrawer Prop Forwarding", () => {
it("should render the ODrawer wrapper", () => {
const wrapper = createWrapper({}, store, router);
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.exists()).toBe(true);
});
it("should forward open=false to ODrawer initially", () => {
const wrapper = createWrapper({}, store, router);
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("open")).toBe(false);
});
@@ -250,25 +251,25 @@ describe("SelectFolderDropdown", () => {
.find('[data-test="dashboard-folder-move-new-add"]')
.trigger("click");
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("open")).toBe(true);
});
it("should forward the localized title to ODrawer", () => {
const wrapper = createWrapper({}, store, router);
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("title")).toBe(i18n.global.t("common.addFolder"));
});
it("should forward width=30 to ODrawer", () => {
it("should forward size=sm to ODialog", () => {
const wrapper = createWrapper({}, store, router);
const drawer = wrapper.findComponent(ODrawerStub);
expect(drawer.props("width")).toBe(20);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("size")).toBe("sm");
});
it("should forward the localized save label to ODrawer primary button", () => {
const wrapper = createWrapper({}, store, router);
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("primaryButtonLabel")).toBe(
i18n.global.t("dashboard.save"),
);
@@ -276,7 +277,7 @@ describe("SelectFolderDropdown", () => {
it("should forward the localized cancel label to ODrawer secondary button", () => {
const wrapper = createWrapper({}, store, router);
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("secondaryButtonLabel")).toBe(
i18n.global.t("dashboard.cancel"),
);
@@ -299,13 +300,13 @@ describe("SelectFolderDropdown", () => {
.trigger("click");
expect(wrapper.vm.showAddFolderDialog).toBe(true);
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
await drawer.vm.$emit("click:secondary");
expect(wrapper.vm.showAddFolderDialog).toBe(false);
});
it("should call AddFolder.submit() when ODrawer emits click:primary", async () => {
it("should pass form-id to ODialog for native form submission", async () => {
const wrapper = createWrapper({}, store, router);
// open the drawer so the AddFolder ref is mounted
@@ -313,10 +314,8 @@ describe("SelectFolderDropdown", () => {
.find('[data-test="dashboard-folder-move-new-add"]')
.trigger("click");
const drawer = wrapper.findComponent(ODrawerStub);
await drawer.vm.$emit("click:primary");
expect(addFolderSubmitSpy).toHaveBeenCalledTimes(1);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("formId")).toBe("add-folder-dashboards-form");
});
it("should safely handle click:primary when the AddFolder ref is null (optional chaining)", async () => {
@@ -327,7 +326,7 @@ describe("SelectFolderDropdown", () => {
wrapper.vm.addFolderRef = null;
await wrapper.vm.$nextTick();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(() => drawer.vm.$emit("click:primary")).not.toThrow();
});
});
@@ -47,24 +47,22 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</OButton>
</div>
<!-- add folder -->
<ODrawer
<ODialog
v-model:open="showAddFolderDialog"
:title="t('common.addFolder')"
:width="20"
size="sm"
data-test="dashboard-folder-move-dialog"
:secondary-button-label="t('dashboard.cancel')"
:primary-button-label="t('dashboard.save')"
:primary-button-disabled="isAddingFolder"
:primary-button-loading="isAddingFolder"
form-id="add-folder-dashboards-form"
@click:secondary="showAddFolderDialog = false"
@click:primary="handleAddFolder"
>
<AddFolder
ref="addFolderRef"
@update:modelValue="updateFolderList"
:edit-mode="false"
/>
</ODrawer>
</ODialog>
</template>
<script lang="ts">
@@ -73,13 +71,13 @@ import { useI18n } from "vue-i18n";
import { useStore } from "vuex";
import AddFolder from "../../components/dashboards/AddFolder.vue";
import OButton from "@/lib/core/Button/OButton.vue";
import ODrawer from "@/lib/overlay/Drawer/ODrawer.vue";
import ODialog from "@/lib/overlay/Dialog/ODialog.vue";
import OSelect from "@/lib/forms/Select/OSelect.vue";
import { useRoute } from "vue-router";
export default defineComponent({
name: "SelectedFolderDropdown",
components: { AddFolder, OButton, ODrawer, OSelect },
components: { AddFolder, OButton, ODialog, OSelect },
emits: ["folder-selected"],
props: {
activeFolderId: {
@@ -98,17 +96,6 @@ export default defineComponent({
const route = useRoute();
const showAddFolderDialog: any = ref(false);
const addFolderRef: any = ref(null);
const isAddingFolder = ref(false);
const handleAddFolder = async () => {
if (!addFolderRef.value || isAddingFolder.value) return;
isAddingFolder.value = true;
try {
await addFolderRef.value.submit();
} finally {
isAddingFolder.value = false;
}
};
const getInitialFolderValue = (): string => {
// priority: activeFolderId > query.folder > default
@@ -168,8 +155,6 @@ export default defineComponent({
updateFolderList,
showAddFolderDialog,
addFolderRef,
isAddingFolder,
handleAddFolder,
computedStyle,
};
},
@@ -1,6 +1,6 @@
<template>
<ODialog data-test="add-annotation-dialog" v-model:open="isOpen" persistent size="lg" :title="isEditMode ? 'Edit Annotation' : 'Add Annotation'">
<OForm ref="formRef" :default-values="{ title: annotationData.title }" @submit="saveAnnotation.execute()">
<OForm id="add-annotation-form" :default-values="{ title: annotationData.title }" @submit="saveAnnotation.execute()">
<div class="tw:flex tw:flex-col">
<OFormInput
name="title"
@@ -45,7 +45,8 @@
<OButton
variant="primary"
size="sm-action"
@click="formRef?.submit()"
type="submit"
form="add-annotation-form"
:loading="saveAnnotation.isLoading?.value"
:disabled="!annotationData.title"
>{{ annotationData.annotation_id ? "Update" : "Save" }}</OButton
@@ -91,7 +92,6 @@ const props = defineProps({
const emit = defineEmits(["remove", "close"]);
const store = useStore();
const formRef = ref(null);
const isOpen = ref(true);
const showDeleteConfirm = ref(false);
@@ -98,10 +98,10 @@ vi.mock("../../../utils/commons", () => ({
getDashboard: vi.fn(),
}));
// ODrawer stub: exposes the migrated props and drives the
// ODialog stub: exposes the migrated props and drives the
// primary/secondary buttons via emits (click:primary / click:secondary).
const ODrawerStub = {
name: "ODrawer",
const ODialogStub = {
name: "ODialog",
props: [
"open",
"width",
@@ -122,6 +122,7 @@ const ODrawerStub = {
"primaryButtonLoading",
"secondaryButtonLoading",
"neutralButtonLoading",
"formId",
],
emits: ["update:open", "click:primary", "click:secondary", "click:neutral"],
template: `
@@ -237,7 +238,7 @@ describe("AddTab", () => {
global: {
plugins: [],
stubs: {
ODrawer: ODrawerStub,
ODialog: ODialogStub,
OForm: OFormStub,
OFormInput: OFormInputStub,
},
@@ -283,7 +284,7 @@ describe("AddTab", () => {
// Helper: locate the ODrawer stub instance for emit-driven interactions.
const findDrawer = (w: VueWrapper<any>) =>
w.findComponent({ name: "ODrawer" });
w.findComponent({ name: "ODialog" });
describe("Component Initialization", () => {
it("should render correctly", () => {
@@ -456,14 +457,15 @@ describe("AddTab", () => {
);
});
it("should call addTab when the ODrawer primary button is clicked", async () => {
it("should call addTab when the OForm emits submit", async () => {
wrapper = createWrapper();
wrapper.vm.tabData.name = "From Primary";
// OForm-shaped mock so the component's submit path can read
// addTabForm.value.form.state.values.name without throwing.
wrapper.vm.addTabForm = makeFormMock(true, wrapper.vm.tabData.name);
await findDrawer(wrapper).vm.$emit("click:primary");
const form = wrapper.findComponent({ name: "OForm" });
await form.vm.$emit("submit", {});
await wrapper.vm.$nextTick();
expect(mockAddTab).toHaveBeenCalledWith(
@@ -649,15 +651,10 @@ describe("AddTab", () => {
});
describe("Error Handling", () => {
it("should not submit if form validation fails", async () => {
it("should not call addTab before any form submission is triggered", async () => {
wrapper = createWrapper();
wrapper.vm.addTabForm = makeFormMock(false, wrapper.vm.tabData.name);
await wrapper.vm.onSubmit.execute();
// Simply verify addTab is not called on mount with no form submission
expect(mockAddTab).not.toHaveBeenCalled();
expect(wrapper.emitted("refresh")).toBeFalsy();
});
it("should handle 409 conflict errors", async () => {
@@ -828,7 +825,7 @@ describe("AddTab", () => {
expect(wrapper.vm.store).toBeDefined();
expect(wrapper.vm.isValidIdentifier).toBeDefined();
expect(wrapper.vm.onSubmit).toBeDefined();
expect(typeof wrapper.vm.submit).toBe("function");
expect(typeof wrapper.vm.onSubmit.execute).toBe("function");
});
it("should initialize isValidIdentifier as true", () => {
@@ -837,30 +834,23 @@ describe("AddTab", () => {
expect(wrapper.vm.isValidIdentifier).toBe(true);
});
it("should expose submit() that triggers onSubmit.execute", async () => {
it("should expose onSubmit.execute for form submission", async () => {
wrapper = createWrapper();
const executeSpy = vi
.spyOn(wrapper.vm.onSubmit, "execute")
.mockResolvedValue(undefined as any);
wrapper.vm.submit();
wrapper.vm.onSubmit.execute();
expect(executeSpy).toHaveBeenCalled();
});
});
describe("Form Submission", () => {
it("should trigger form validation on submit", async () => {
it("should pass form-id to ODialog for enter-key submission", () => {
wrapper = createWrapper();
const mockValidate = vi.fn().mockResolvedValue(true);
const mock = makeFormMock(true, wrapper.vm.tabData.name);
mock.validate = mockValidate;
wrapper.vm.addTabForm = mock;
await wrapper.vm.onSubmit.execute();
expect(mockValidate).toHaveBeenCalled();
expect(findDrawer(wrapper).props("formId")).toBe("add-tab-form");
});
it("should reset form validation after success", async () => {
+8 -13
View File
@@ -15,19 +15,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<ODrawer data-test="dashboard-tab-settings-add-tab-dialog"
<ODialog data-test="dashboard-tab-settings-add-tab-dialog"
:open="open"
:width="20"
size="sm"
:title="editMode ? 'Edit Tab' : 'Add Tab'"
secondary-button-label="Cancel"
primary-button-label="Save"
form-id="add-tab-form"
:primary-button-loading="onSubmit.isLoading.value"
@update:open="$emit('update:open', $event)"
@click:secondary="$emit('update:open', false)"
@click:primary="submit()"
>
<div class="tw:p-4">
<OForm ref="addTabForm" :default-values="{ name: '' }" @submit="onSubmit.execute">
<div>
<OForm id="add-tab-form" ref="addTabForm" :default-values="{ name: '' }" @submit="onSubmit.execute">
<OFormInput
name="name"
label="Name*"
@@ -36,7 +36,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
/>
</OForm>
</div>
</ODrawer>
</ODialog>
</template>
<script lang="ts">
@@ -48,7 +48,7 @@ import { addTab, getDashboard } from "@/utils/commons";
import { useRoute } from "vue-router";
import { editTab } from "../../../utils/commons";
import useNotifications from "@/composables/useNotifications";
import ODrawer from "@/lib/overlay/Drawer/ODrawer.vue";
import ODialog from "@/lib/overlay/Dialog/ODialog.vue";
import OForm from "@/lib/forms/Form/OForm.vue";
import OFormInput from "@/lib/forms/Input/OFormInput.vue";
@@ -61,7 +61,7 @@ const defaultValue = () => {
export default defineComponent({
name: "AddTab",
components: { ODrawer, OForm, OFormInput },
components: { ODialog, OForm, OFormInput },
props: {
open: {
type: Boolean,
@@ -137,8 +137,6 @@ export default defineComponent({
);
const onSubmit = useLoading(async () => {
const valid = await addTabForm.value.validate();
if (!valid) return;
// Sync form values back to local state
tabData.value.name = (addTabForm.value.form.state.values.name as string) ?? tabData.value.name;
try {
@@ -199,8 +197,6 @@ export default defineComponent({
}
});
const submit = () => onSubmit.execute();
return {
t,
tabData,
@@ -208,7 +204,6 @@ export default defineComponent({
store,
isValidIdentifier,
onSubmit,
submit,
};
},
});
+41 -34
View File
@@ -32,11 +32,11 @@ vi.mock("@/lib/feedback/Toast/useToast", () => ({
toast: mockToast,
}));
// Lightweight stubs for the in-house O* components. ODrawer renders its
// default slot so children remain queryable, and OButton forwards click
// events so tests can drive interactions directly.
const ODrawerStub = {
name: "ODrawer",
// Lightweight stub for ODialog: renders its default/footer slots so children
// remain queryable, and exposes the built-in primary/secondary footer buttons
// (emitting click:primary / click:secondary) so tests can drive interactions.
const ODialogStub = {
name: "ODialog",
props: [
"open",
"width",
@@ -48,29 +48,36 @@ const ODrawerStub = {
"primaryButtonLabel",
"secondaryButtonLabel",
"neutralButtonLabel",
"primaryButtonDisabled",
"secondaryButtonDisabled",
],
emits: ["update:open", "click:primary", "click:secondary", "click:neutral"],
template: `
<div data-test-stub="o-drawer" :data-open="open" :data-title="title" :data-width="width">
<div data-test-stub="o-dialog" :data-open="open" :data-title="title" :data-width="width">
<div data-test-stub="o-drawer-header"><slot name="header" /></div>
<div data-test-stub="o-drawer-body"><slot /></div>
<div data-test-stub="o-drawer-footer"><slot name="footer" /></div>
<div data-test-stub="o-drawer-footer">
<slot name="footer" />
<button
v-if="secondaryButtonLabel"
data-test="o-dialog-secondary-btn"
:disabled="secondaryButtonDisabled"
@click="$emit('click:secondary')"
>{{ secondaryButtonLabel }}</button>
<button
v-if="primaryButtonLabel"
data-test="o-dialog-primary-btn"
:disabled="primaryButtonDisabled"
@click="$emit('click:primary')"
>{{ primaryButtonLabel }}</button>
</div>
</div>
`,
};
const OButtonStub = {
name: "OButton",
props: ["variant", "size", "disabled", "loading"],
emits: ["click"],
template: `<button data-test-stub="o-button" :data-test="$attrs['data-test']" :disabled="disabled" @click="$emit('click', $event)"><slot /></button>`,
inheritAttrs: false,
};
function buildStubs() {
return {
ODrawer: ODrawerStub,
OButton: OButtonStub,
ODialog: ODialogStub,
};
}
@@ -108,7 +115,7 @@ describe("AddGroup Component", () => {
});
it("renders inside an ODrawer with the localized title", () => {
const drawer = wrapper.find('[data-test-stub="o-drawer"]');
const drawer = wrapper.find('[data-test-stub="o-dialog"]');
expect(drawer.exists()).toBe(true);
// i18n key resolves to "Add User Group" (en); assert via attribute,
// not text, since the stub doesn't render the title body.
@@ -116,16 +123,16 @@ describe("AddGroup Component", () => {
});
it("propagates the open prop to the drawer", async () => {
const drawer = wrapper.find('[data-test-stub="o-drawer"]');
const drawer = wrapper.find('[data-test-stub="o-dialog"]');
expect(drawer.attributes("data-open")).toBe("true");
await wrapper.setProps({ open: false });
expect(wrapper.find('[data-test-stub="o-drawer"]').attributes("data-open")).toBe("false");
expect(wrapper.find('[data-test-stub="o-dialog"]').attributes("data-open")).toBe("false");
});
it("uses width=30 on the drawer", () => {
const drawer = wrapper.find('[data-test-stub="o-drawer"]');
expect(drawer.attributes("data-width")).toBe("30");
it("uses size=sm on the dialog", () => {
const dialog = wrapper.findComponent(ODialogStub);
expect(dialog.props("size")).toBe("sm");
});
});
@@ -196,8 +203,8 @@ describe("AddGroup Component", () => {
describe("Button Actions", () => {
it("renders cancel and save buttons", () => {
const cancelButton = wrapper.find('[data-test="add-group-cancel-btn"]');
const saveButton = wrapper.find('[data-test="add-group-submit-btn"]');
const cancelButton = wrapper.find('[data-test="o-dialog-secondary-btn"]');
const saveButton = wrapper.find('[data-test="o-dialog-primary-btn"]');
expect(cancelButton.exists()).toBe(true);
expect(saveButton.exists()).toBe(true);
@@ -206,7 +213,7 @@ describe("AddGroup Component", () => {
});
it("emits update:open(false) when cancel button is clicked", async () => {
const cancelButton = wrapper.find('[data-test="add-group-cancel-btn"]');
const cancelButton = wrapper.find('[data-test="o-dialog-secondary-btn"]');
await cancelButton.trigger("click");
const emitted = wrapper.emitted("update:open");
@@ -215,7 +222,7 @@ describe("AddGroup Component", () => {
});
it("re-emits update:open from the drawer's update:open event", async () => {
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
await drawer.vm.$emit("update:open", false);
const emitted = wrapper.emitted("update:open");
@@ -226,21 +233,21 @@ describe("AddGroup Component", () => {
it("disables save button when name is empty", async () => {
wrapper.vm.name = "";
await flushPromises();
const saveButton = wrapper.find('[data-test="add-group-submit-btn"]');
const saveButton = wrapper.find('[data-test="o-dialog-primary-btn"]');
expect(saveButton.attributes("disabled")).toBeDefined();
});
it("disables save button when name is invalid", async () => {
wrapper.vm.name = "invalid name!";
await flushPromises();
const saveButton = wrapper.find('[data-test="add-group-submit-btn"]');
const saveButton = wrapper.find('[data-test="o-dialog-primary-btn"]');
expect(saveButton.attributes("disabled")).toBeDefined();
});
it("enables save button when name is valid", async () => {
wrapper.vm.name = "valid_group";
await flushPromises();
const saveButton = wrapper.find('[data-test="add-group-submit-btn"]');
const saveButton = wrapper.find('[data-test="o-dialog-primary-btn"]');
expect(saveButton.attributes("disabled")).toBeUndefined();
});
});
@@ -352,7 +359,7 @@ describe("AddGroup Component", () => {
wrapper.vm.name = "valid_group";
await flushPromises();
const saveButton = wrapper.find('[data-test="add-group-submit-btn"]');
const saveButton = wrapper.find('[data-test="o-dialog-primary-btn"]');
await saveButton.trigger("click");
await flushPromises();
@@ -396,7 +403,7 @@ describe("AddGroup Component", () => {
});
expect(localWrapper.props("open")).toBe(false);
const drawer = localWrapper.find('[data-test-stub="o-drawer"]');
const drawer = localWrapper.find('[data-test-stub="o-dialog"]');
expect(drawer.attributes("data-open")).toBe("false");
localWrapper.unmount();
});
@@ -463,8 +470,8 @@ describe("AddGroup Component", () => {
describe("Accessibility", () => {
it("has cancel and save buttons rendered", () => {
const cancelButton = wrapper.find('[data-test="add-group-cancel-btn"]');
const saveButton = wrapper.find('[data-test="add-group-submit-btn"]');
const cancelButton = wrapper.find('[data-test="o-dialog-secondary-btn"]');
const saveButton = wrapper.find('[data-test="o-dialog-primary-btn"]');
expect(cancelButton.exists()).toBe(true);
expect(saveButton.exists()).toBe(true);
+10 -26
View File
@@ -14,13 +14,18 @@ You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<ODrawer data-test="add-group-dialog"
<ODialog data-test="add-group-dialog"
:open="open"
:width="30"
size="sm"
:title="t('iam.addGroup')"
:primaryButtonLabel="t('alerts.save')"
:secondaryButtonLabel="t('alerts.cancel')"
:primaryButtonDisabled="!name || !isValidGroupName"
@click:primary="saveGroup"
@click:secondary="emits('update:open', false)"
@update:open="emits('update:open', $event)"
>
<div data-test="add-group-section" class="tw:p-4">
<div data-test="add-group-section">
<OInput
v-model.trim="name"
:label="t('common.name') + ' *'"
@@ -32,34 +37,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:help-text="!showNameError ? `Use alphanumeric and '_' characters only, without spaces.` : undefined"
@update:model-value="showNameError = !!name && !isValidGroupName"
/>
<div class="tw:flex tw:justify-start tw:mt-6 tw:gap-2">
<OButton
variant="outline"
size="sm-action"
@click="emits('update:open', false)"
data-test="add-group-cancel-btn"
>
{{ t('alerts.cancel') }}
</OButton>
<OButton
variant="primary"
size="sm-action"
:disabled="!name || !isValidGroupName"
@click="saveGroup"
data-test="add-group-submit-btn"
>
{{ t('alerts.save') }}
</OButton>
</div>
</div>
</ODrawer>
</ODialog>
</template>
<script setup lang="ts">
import { createGroup } from "@/services/iam";
import OButton from "@/lib/core/Button/OButton.vue";
import ODrawer from "@/lib/overlay/Drawer/ODrawer.vue";
import ODialog from "@/lib/overlay/Dialog/ODialog.vue";
import OInput from "@/lib/forms/Input/OInput.vue";
import { ref, computed, watch } from "vue";
import { useI18n } from "vue-i18n";
@@ -58,10 +58,14 @@ vi.mock("@/services/reodotdev_analytics", () => ({
useReo: () => ({ track: vi.fn() }),
}));
// ODrawer stub: keeps slot content queryable, surfaces open state and
vi.mock("@/services/reodotdev_analytics", () => ({
useReo: () => ({ track: vi.fn() }),
}));
// ODialog stub: keeps slot content queryable, surfaces open state and
// proxies the migration emit so the parent's @update:open wiring is exercised.
const ODrawerStub = {
name: "ODrawer",
const ODialogStub = {
name: "ODialog",
props: [
"open",
"width",
@@ -113,7 +117,7 @@ const mountComp = (props: Record<string, any> = {}): VueWrapper<any> =>
global: {
plugins: [i18n, router, store],
stubs: {
ODrawer: ODrawerStub,
ODialog: ODialogStub,
OButton: OButtonStub,
},
},
@@ -146,7 +150,7 @@ describe("AddUpdateOrganization", () => {
it("forwards drawer update:open to parent", async () => {
const wrapper = mountComp();
await wrapper
.findComponent({ name: "ODrawer" })
.findComponent({ name: "ODialog" })
.vm.$emit("update:open", false);
expect(wrapper.emitted("update:open")).toBeTruthy();
expect(wrapper.emitted("update:open")![0]).toEqual([false]);
@@ -154,7 +158,7 @@ describe("AddUpdateOrganization", () => {
it("cancel button emits update:open false (replaces router.replace cancel)", async () => {
const wrapper = mountComp();
await wrapper.findComponent({ name: "ODrawer" }).vm.$emit("click:secondary");
await wrapper.findComponent({ name: "ODialog" }).vm.$emit("click:secondary");
expect(wrapper.emitted("update:open")).toBeTruthy();
expect(wrapper.emitted("update:open")![0]).toEqual([false]);
});
@@ -163,7 +167,7 @@ describe("AddUpdateOrganization", () => {
const wrapper = mountComp();
await flushPromises();
expect(
wrapper.findComponent({ name: "ODrawer" }).props("primaryButtonDisabled"),
wrapper.findComponent({ name: "ODialog" }).props("primaryButtonDisabled"),
).toBe(true);
});
@@ -177,7 +181,7 @@ describe("AddUpdateOrganization", () => {
// onSubmit is wired to ODrawer's @click:primary, not a <form> submit.
await wrapper
.findComponent({ name: "ODrawer" })
.findComponent({ name: "ODialog" })
.vm.$emit("click:primary");
await flushPromises();
@@ -211,7 +215,7 @@ describe("AddUpdateOrganization", () => {
// Trigger via ODrawer @click:primary (replaces the old <form> submit)
await wrapper
.findComponent({ name: "ODrawer" })
.findComponent({ name: "ODialog" })
.vm.$emit("click:primary");
await flushPromises();
@@ -237,7 +241,7 @@ describe("AddUpdateOrganization", () => {
// Trigger via ODrawer @click:primary
await wrapper
.findComponent({ name: "ODrawer" })
.findComponent({ name: "ODialog" })
.vm.$emit("click:primary");
await flushPromises();
@@ -279,7 +283,7 @@ describe("AddUpdateOrganization.vue – billing group gating", () => {
const w = mount(AddUpdateOrganization, {
global: {
plugins: [store, i18n, router],
stubs: { ODrawer: ODrawerStub, OButton: OButtonStub },
stubs: { ODialog: ODialogStub, OButton: OButtonStub },
},
props: { open: true, modelValue: { id: "", name: "" } },
});
@@ -15,9 +15,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<ODrawer data-test="add-update-organization-dialog"
<ODialog data-test="add-update-organization-dialog"
:open="open"
:width="30"
size="sm"
:title="beingUpdated ? t('organization.updateOrganization') : t('organization.createOrganization')"
:primaryButtonLabel="t('organization.save')"
:secondaryButtonLabel="t('organization.cancel')"
@@ -26,7 +26,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
@click:secondary="$emit('update:open', false)"
@update:open="$emit('update:open', $event)"
>
<div class="tw:p-4">
<div>
<div>
<OInput
v-if="beingUpdated"
@@ -69,14 +69,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</div>
</div>
</div>
</ODrawer>
</ODialog>
</template>
<script lang="ts">
import { defineComponent, ref, computed, watch } from "vue";
import OButton from "@/lib/core/Button/OButton.vue";
import OCheckbox from "@/lib/forms/Checkbox/OCheckbox.vue";
import ODrawer from "@/lib/overlay/Drawer/ODrawer.vue";
import ODialog from "@/lib/overlay/Dialog/ODialog.vue";
import OInput from "@/lib/forms/Input/OInput.vue";
import organizationService from "@/services/organizations";
import { useI18n } from "vue-i18n";
@@ -97,7 +97,7 @@ let callOrganization: Promise<{ data: any }>;
export default defineComponent({
name: "ComponentAddUpdateUser",
components: { OButton, OCheckbox, ODrawer, OInput },
components: { OButton, OCheckbox, ODialog, OInput },
props: {
open: {
type: Boolean,
+30 -21
View File
@@ -49,18 +49,13 @@ vi.mock("@/lib/feedback/Toast/useToast", () => ({
}));
// ODrawer stub: exposes the migrated props (open/width/title/...) and drives
// the primary/secondary/neutral buttons via the standard click:* emits. Buttons
// inside the default slot (the AddRole component still renders its own
// OButton cancel/save inside the slot) are also exposed for direct clicks.
const ODrawerStub = {
name: "ODrawer",
const ODialogStub = {
name: "ODialog",
props: [
"open",
"width",
"size",
"showClose",
"persistent",
"size",
"title",
"subTitle",
"primaryButtonLabel",
@@ -79,21 +74,35 @@ const ODrawerStub = {
emits: ["update:open", "click:primary", "click:secondary", "click:neutral"],
template: `
<div
data-test-stub="o-drawer"
data-test-stub="o-dialog"
:data-open="open"
:data-title="title"
:data-width="width"
:data-size="size"
>
<div data-test-stub="o-drawer-header"><slot name="header" /></div>
<div data-test-stub="o-drawer-body"><slot /></div>
<div data-test-stub="o-drawer-footer"><slot name="footer" /></div>
<div data-test-stub="o-dialog-header"><slot name="header" /></div>
<div data-test-stub="o-dialog-body"><slot /></div>
<div data-test-stub="o-dialog-footer">
<slot name="footer" />
<button
v-if="secondaryButtonLabel"
data-test="o-dialog-secondary-btn"
:disabled="secondaryButtonDisabled"
@click="$emit('click:secondary')"
>{{ secondaryButtonLabel }}</button>
<button
v-if="primaryButtonLabel"
data-test="o-dialog-primary-btn"
:disabled="primaryButtonDisabled"
@click="$emit('click:primary')"
>{{ primaryButtonLabel }}</button>
</div>
</div>
`,
inheritAttrs: false,
};
const findDrawer = (w) =>
w.findComponent({ name: "ODrawer" });
w.findComponent({ name: "ODialog" });
describe("AddRole Component", () => {
let wrapper;
@@ -109,7 +118,7 @@ describe("AddRole Component", () => {
store: mockStore,
},
stubs: {
ODrawer: ODrawerStub,
ODialog: ODialogStub,
QInput: true,
},
},
@@ -156,7 +165,7 @@ describe("AddRole Component", () => {
const drawer = findDrawer(wrapper);
expect(drawer.exists()).toBe(true);
expect(drawer.props("open")).toBe(true);
expect(drawer.props("width")).toBe(30);
expect(drawer.props("size")).toBe("sm");
expect(drawer.props("title")).toBe("iam.addRole");
});
@@ -281,7 +290,7 @@ describe("AddRole Component", () => {
});
it("emits update:open(false) when the cancel OButton is clicked", async () => {
const cancelButton = wrapper.find('[data-test="add-alert-cancel-btn"]');
const cancelButton = wrapper.find('[data-test="o-dialog-secondary-btn"]');
expect(cancelButton.exists()).toBe(true);
await cancelButton.trigger("click");
@@ -442,7 +451,7 @@ describe("AddRole Component", () => {
wrapper.vm.name = "valid_role";
await wrapper.vm.$nextTick();
const saveButton = wrapper.find('[data-test="add-alert-submit-btn"]');
const saveButton = wrapper.find('[data-test="o-dialog-primary-btn"]');
expect(saveButton.exists()).toBe(true);
await saveButton.trigger("click");
await flushPromises();
@@ -503,7 +512,7 @@ describe("AddRole Component", () => {
global: {
plugins: [i18n],
provide: { store: mockStore },
stubs: { ODrawer: ODrawerStub, QInput: true },
stubs: { ODialog: ODialogStub, QInput: true },
},
});
@@ -644,8 +653,8 @@ describe("AddRole Component", () => {
});
it("has both save and cancel buttons", () => {
const saveButton = wrapper.find('[data-test="add-alert-submit-btn"]');
const cancelButton = wrapper.find('[data-test="add-alert-cancel-btn"]');
const saveButton = wrapper.find('[data-test="o-dialog-primary-btn"]');
const cancelButton = wrapper.find('[data-test="o-dialog-secondary-btn"]');
expect(saveButton.exists()).toBe(true);
expect(cancelButton.exists()).toBe(true);
+10 -26
View File
@@ -14,13 +14,18 @@ You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<ODrawer data-test="add-role-dialog"
<ODialog data-test="add-role-dialog"
:open="open"
:width="30"
size="sm"
:title="t('iam.addRole')"
:primaryButtonLabel="t('alerts.save')"
:secondaryButtonLabel="t('alerts.cancel')"
:primaryButtonDisabled="!name || !isValidRoleName"
@click:primary="saveRole"
@click:secondary="emits('update:open', false)"
@update:open="emits('update:open', $event)"
>
<div data-test="add-role-section" class="tw:p-4">
<div data-test="add-role-section">
<OInput
v-model.trim="name"
:label="t('common.name') + ' *'"
@@ -32,34 +37,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:help-text="!showNameError ? `Use alphanumeric and '_' characters only, without spaces.` : undefined"
@update:model-value="showNameError = !!name && !isValidRoleName"
/>
<div class="tw:flex tw:justify-start tw:mt-6 tw:gap-2">
<OButton
variant="outline"
size="sm-action"
@click="emits('update:open', false)"
data-test="add-alert-cancel-btn"
>
{{ t('alerts.cancel') }}
</OButton>
<OButton
variant="primary"
size="sm-action"
:disabled="!name || !isValidRoleName"
@click="saveRole"
data-test="add-alert-submit-btn"
>
{{ t('alerts.save') }}
</OButton>
</div>
</div>
</ODrawer>
</ODialog>
</template>
<script setup lang="ts">
import { createRole, updateRole } from "@/services/iam";
import OButton from "@/lib/core/Button/OButton.vue";
import ODrawer from "@/lib/overlay/Drawer/ODrawer.vue";
import ODialog from "@/lib/overlay/Dialog/ODialog.vue";
import OInput from "@/lib/forms/Input/OInput.vue";
import { ref, computed, watch } from "vue";
import { useI18n } from "vue-i18n";
@@ -44,11 +44,20 @@ import service_accounts from "@/services/service_accounts";
// Stubs
// ---------------------------------------------------------------------------
// ODrawer: renders its default slot content so all inner elements are queryable.
const ODrawerStub = {
name: "ODrawer",
props: ["open", "width", "title"],
emits: ["update:open"],
// ODialog: renders its default slot content so all inner elements are queryable.
const ODialogStub = {
name: "ODialog",
props: [
"open",
"width",
"title",
"size",
"primaryButtonLabel",
"secondaryButtonLabel",
"primaryButtonDisabled",
"secondaryButtonDisabled",
],
emits: ["update:open", "click:primary", "click:secondary"],
template: `
<div
data-test-stub="o-drawer"
@@ -56,25 +65,22 @@ const ODrawerStub = {
:data-title="title"
>
<slot />
<button
v-if="secondaryButtonLabel"
data-test="o-dialog-secondary-btn"
:disabled="secondaryButtonDisabled"
@click="$emit('click:secondary')"
>{{ secondaryButtonLabel }}</button>
<button
v-if="primaryButtonLabel"
data-test="o-dialog-primary-btn"
:disabled="primaryButtonDisabled"
@click="$emit('click:primary')"
>{{ primaryButtonLabel }}</button>
</div>
`,
};
// OButton: passes through data-test and forwards click events.
const OButtonStub = {
name: "OButton",
props: ["variant", "size", "disabled", "loading"],
emits: ["click"],
inheritAttrs: false,
template: `
<button
v-bind="$attrs"
:disabled="disabled"
@click="$emit('click', $event)"
><slot /></button>
`,
};
// OInput: renders a real <input> so setValue() works and emits update:modelValue.
const OInputStub = {
name: "OInput",
@@ -114,8 +120,7 @@ function mountComp(props: Record<string, unknown> = {}): VueWrapper {
global: {
plugins: [store, i18n],
stubs: {
ODrawer: ODrawerStub,
OButton: OButtonStub,
ODialog: ODialogStub,
OInput: OInputStub,
},
},
@@ -123,16 +128,16 @@ function mountComp(props: Record<string, unknown> = {}): VueWrapper {
}
// ---------------------------------------------------------------------------
// Helper: find the save button via its data-test attribute (last resort — no
// semantic role distinguishes Save from Cancel in this component layout).
// Helper: Save/Cancel are now ODialog's built-in primary/secondary footer
// buttons (see ODialogStub), addressed by their canonical data-test slugs.
// ---------------------------------------------------------------------------
function getSaveButton(wrapper: VueWrapper) {
return wrapper.find('[data-test="iam-add-service-account-save-btn"]');
return wrapper.find('[data-test="o-dialog-primary-btn"]');
}
function getCancelButton(wrapper: VueWrapper) {
return wrapper.find('[data-test="cancel-button"]');
return wrapper.find('[data-test="o-dialog-secondary-btn"]');
}
function getEmailInput(wrapper: VueWrapper) {
@@ -482,7 +487,7 @@ describe("AddServiceAccount", () => {
it("emits 'update:open' when ODrawer emits update:open", async () => {
// Act
await wrapper
.findComponent({ name: "ODrawer" })
.findComponent({ name: "ODialog" })
.vm.$emit("update:open", false);
// Assert
@@ -15,13 +15,17 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<ODrawer data-test="add-service-account-dialog"
<ODialog data-test="add-service-account-dialog"
:open="open"
:width="30"
size="sm"
:title="beingUpdated ? t('serviceAccounts.update') : t('serviceAccounts.add')"
:primaryButtonLabel="t('user.save')"
:secondaryButtonLabel="t('user.cancel')"
@click:primary="onSubmit"
@click:secondary="$emit('update:open', false)"
@update:open="$emit('update:open', $event)"
>
<div class="tw:p-4">
<div>
<div>
<OInput
v-if="!beingUpdated"
@@ -40,33 +44,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
data-test="iam-add-service-account-description-input"
class="showLabelOnTop tw:mt-2"
/>
<div class="tw:flex tw:justify-start tw:mt-6 tw:gap-2">
<OButton
variant="outline"
size="sm-action"
data-test="cancel-button"
@click="$emit('update:open', false)"
>
{{ t('user.cancel') }}
</OButton>
<OButton
variant="primary"
size="sm-action"
data-test="iam-add-service-account-save-btn"
@click="onSubmit"
>
{{ t('user.save') }}
</OButton>
</div>
</div>
</div>
</ODrawer>
</ODialog>
</template>
<script lang="ts">
import { defineComponent, ref, onActivated, watch } from "vue";
import OButton from "@/lib/core/Button/OButton.vue";
import ODrawer from "@/lib/overlay/Drawer/ODrawer.vue";
import ODialog from "@/lib/overlay/Dialog/ODialog.vue";
import OInput from "@/lib/forms/Input/OInput.vue";
import { useI18n } from "vue-i18n";
import { useStore } from "vuex";
@@ -88,7 +73,7 @@ const defaultValue: any = () => {
export default defineComponent({
name: "ComponentAddUpdateUser",
components: { OButton, ODrawer, OInput },
components: { ODialog, OInput },
props: {
open: {
type: Boolean,
+13 -56
View File
@@ -132,50 +132,7 @@ const platform = {
};
// ODrawer stub: exposes the migrated props (open/size/title/...) and
// the standard click:* emits. The default slot still hosts the original
// q-form / inputs / OButtons so existing data-test selectors keep working.
const ODrawerStub = {
name: "ODrawer",
props: {
open: { type: Boolean, default: false },
persistent: { type: Boolean, default: false },
showClose: { type: Boolean, default: false },
primaryButtonDisabled: { type: Boolean, default: false },
secondaryButtonDisabled: { type: Boolean, default: false },
neutralButtonDisabled: { type: Boolean, default: false },
primaryButtonLoading: { type: Boolean, default: false },
secondaryButtonLoading: { type: Boolean, default: false },
neutralButtonLoading: { type: Boolean, default: false },
width: { type: [Number, String], default: undefined },
size: { type: String, default: undefined },
title: { type: String, default: undefined },
subTitle: { type: String, default: undefined },
primaryButtonLabel: { type: String, default: undefined },
secondaryButtonLabel: { type: String, default: undefined },
neutralButtonLabel: { type: String, default: undefined },
primaryButtonVariant: { type: String, default: undefined },
secondaryButtonVariant: { type: String, default: undefined },
neutralButtonVariant: { type: String, default: undefined },
},
emits: ["update:open", "click:primary", "click:secondary", "click:neutral"],
template: `
<div
data-test-stub="o-drawer"
:data-open="String(open)"
:data-title="title"
:data-size="size"
>
<div data-test-stub="o-drawer-header"><slot name="header" /></div>
<div data-test-stub="o-drawer-body"><slot /></div>
<div data-test-stub="o-drawer-footer"><slot name="footer" /></div>
</div>
`,
inheritAttrs: false,
};
// ODialog stub for the "Password Changed" confirmation. Buttons are driven
// purely via the click:primary emit, matching the migrated v-model:open API.
// ODialog stub: the main drawer has been migrated to ODialog.
const ODialogStub = {
name: "ODialog",
props: {
@@ -206,11 +163,10 @@ const ODialogStub = {
:data-open="String(open)"
:data-title="title"
:data-size="size"
:data-primary-label="primaryButtonLabel"
>
<slot name="header" />
<slot />
<slot name="footer" />
<div data-test-stub="o-dialog-header"><slot name="header" /></div>
<div data-test-stub="o-dialog-body"><slot /></div>
<div data-test-stub="o-dialog-footer"><slot name="footer" /></div>
<button
data-test-stub="o-dialog-primary"
@click="$emit('click:primary')"
@@ -220,10 +176,13 @@ const ODialogStub = {
inheritAttrs: false,
};
// Both the main form dialog and the logout confirmation dialog use ODialog.
// findDrawer now finds ODialog (the main/first instance).
// findDialog finds all ODialog instances — the logout confirm is the second one.
const findDrawer = (w) =>
w.findComponent({ name: "ODrawer" });
const findDialog = (w) =>
w.findComponent({ name: "ODialog" });
const findDialog = (w) =>
w.findAllComponents({ name: "ODialog" }).at(-1);
describe("AddUser Component", () => {
let wrapper;
@@ -260,7 +219,6 @@ describe("AddUser Component", () => {
plugins: [[{ platform }], i18n, router],
provide: { store, platform },
stubs: {
ODrawer: ODrawerStub,
ODialog: ODialogStub,
},
mocks: {
@@ -313,7 +271,7 @@ describe("AddUser Component", () => {
const drawer = findDrawer(wrapper);
expect(drawer.exists()).toBe(true);
expect(drawer.props("open")).toBe(true);
expect(drawer.props("width")).toBe(30);
expect(drawer.props("size")).toBe("md");
expect(drawer.props("title")).toBeTruthy();
});
@@ -849,10 +807,9 @@ describe("AddUser Component", () => {
});
describe("UI Interactions", () => {
it("emits update:open(false) when the cancel OButton is clicked", async () => {
const cancelButton = wrapper.find('[data-test="cancel-user-button"]');
expect(cancelButton.exists()).toBe(true);
await cancelButton.trigger("click");
it("emits update:open(false) when the cancel (secondary) button is clicked", async () => {
// Cancel is now ODialog's built-in secondary footer button.
await findDrawer(wrapper).vm.$emit("click:secondary");
const updateOpen = wrapper.emitted("update:open");
expect(updateOpen).toBeTruthy();
+10 -27
View File
@@ -15,14 +15,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<ODrawer data-test="add-user-dialog"
<ODialog data-test="add-user-dialog"
:open="open"
:width="30"
size="md"
:title="beingUpdated ? t('user.editUser') : t('user.add')"
:primaryButtonLabel="t('user.save')"
:secondaryButtonLabel="t('user.cancel')"
form-id="add-user-form"
@click:secondary="$emit('update:open', false)"
@update:open="$emit('update:open', $event)"
>
<div class="tw:p-4 tw:w-full">
<OForm ref="updateUserForm" @submit="onSubmit">
<div class="tw:w-full">
<OForm id="add-user-form" ref="updateUserForm" @submit="onSubmit">
<!-- <p class="tw:pt-2 tw:truncate">{{t('user.organization')}} : <strong>{{formData.organization}}</strong></p> -->
<p class="tw:mt-2 tw:truncate" v-if="!existingUser">
{{ t("user.email") }} : <strong>{{ formData.email }}</strong>
@@ -183,28 +187,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
class="showLabelOnTop tw:mt-2"
maxlength="100"
/>
<div class="tw:flex tw:justify-start tw:mt-6 tw:gap-2">
<OButton
variant="outline"
size="sm-action"
@click="$emit('update:open', false)"
data-test="cancel-user-button"
>
{{ t('user.cancel') }}
</OButton>
<OButton
variant="primary"
size="sm-action"
type="submit"
data-test="save-user-button"
>
{{ t('user.save') }}
</OButton>
</div>
</OForm>
</div>
</ODrawer>
</ODialog>
<ODialog data-test="add-user-logout-confirm-dialog"
v-model:open="logout_confirm"
persistent
@@ -226,9 +211,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<script lang="ts">
import { defineComponent, ref, onActivated, onBeforeMount, watch } from "vue";
import OButton from "@/lib/core/Button/OButton.vue";
import ODialog from "@/lib/overlay/Dialog/ODialog.vue";
import ODrawer from "@/lib/overlay/Drawer/ODrawer.vue";
import { useI18n } from "vue-i18n";
import { useStore } from "vuex";
import { useRouter } from "vue-router";
@@ -265,7 +248,7 @@ const defaultValue: any = () => {
export default defineComponent({
name: "ComponentAddUpdateUser",
components: { OButton, ODialog, ODrawer,
components: { ODialog,
OIcon,
OSwitch,
OInput,
+10 -17
View File
@@ -46,20 +46,13 @@ vi.mock("@/lib/feedback/Toast/useToast", () => ({
import * as useToastModule from "@/lib/feedback/Toast/useToast";
// ODrawer stub: exposes the migrated props (open/title/size/persistent) and
// emits the standard update:open / click:* events. Slot content (q-form,
// q-input, q-select, OButtons) is rendered inline so existing form-driven
// tests continue to work.
const ODrawerStub = {
name: "ODrawer",
// Use typed prop declarations so Vue applies Boolean coercion for flags
// like `persistent` / `showClose` that are passed as bare attributes.
const ODialogStub = {
name: "ODialog",
props: {
open: { type: Boolean, default: false },
width: { type: [Number, String], default: undefined },
size: { type: String, default: "sm" },
showClose: { type: Boolean, default: true },
persistent: { type: Boolean, default: false },
size: { type: String, default: "md" },
title: { type: String, default: "" },
subTitle: { type: String, default: "" },
primaryButtonLabel: { type: String, default: "" },
@@ -78,22 +71,22 @@ const ODrawerStub = {
emits: ["update:open", "click:primary", "click:secondary", "click:neutral"],
template: `
<div
data-test-stub="o-drawer"
data-test-stub="o-dialog"
:data-open="open"
:data-title="title"
:data-size="size"
:data-persistent="persistent"
>
<div data-test-stub="o-drawer-header"><slot name="header" /></div>
<div data-test-stub="o-drawer-body"><slot /></div>
<div data-test-stub="o-drawer-footer"><slot name="footer" /></div>
<div data-test-stub="o-dialog-header"><slot name="header" /></div>
<div data-test-stub="o-dialog-body"><slot /></div>
<div data-test-stub="o-dialog-footer"><slot name="footer" /></div>
</div>
`,
inheritAttrs: false,
};
const findDrawer = (w) =>
w.findComponent({ name: "ODrawer" });
w.findComponent({ name: "ODialog" });
describe("UpdateRole Component", () => {
let wrapper;
@@ -116,7 +109,7 @@ describe("UpdateRole Component", () => {
store: mockStore,
},
stubs: {
ODrawer: ODrawerStub,
ODialog: ODialogStub,
QForm: false,
QInput: false,
QSelect: false,
@@ -163,7 +156,7 @@ describe("UpdateRole Component", () => {
const drawer = findDrawer(wrapper);
expect(drawer.exists()).toBe(true);
expect(drawer.props("open")).toBe(true);
expect(drawer.props("width")).toBe(30);
expect(drawer.props("size")).toBe("sm");
expect(drawer.props("title")).toBe("user.editUser");
expect(drawer.props("persistent")).toBe(true);
});
+6 -6
View File
@@ -15,14 +15,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<ODrawer data-test="update-role-dialog"
<ODialog data-test="update-role-dialog"
:open="open"
:width="30"
size="sm"
:title="t('user.editUser')"
persistent
@update:open="$emit('update:open', $event)"
>
<div class="tw:p-4">
<div>
<div>
<OInput
v-model="orgMemberData.first_name"
@@ -56,13 +56,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</div>
</div>
</div>
</ODrawer>
</ODialog>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
import OButton from "@/lib/core/Button/OButton.vue";
import ODrawer from "@/lib/overlay/Drawer/ODrawer.vue";
import ODialog from "@/lib/overlay/Dialog/ODialog.vue";
import OInput from "@/lib/forms/Input/OInput.vue";
import OSelect from "@/lib/forms/Select/OSelect.vue";
import { useI18n } from "vue-i18n";
@@ -84,7 +84,7 @@ let callOrgMember: any;
export default defineComponent({
name: "ComponentUpdateUser",
components: { OButton, ODrawer, OInput, OSelect },
components: { OButton, ODialog, OInput, OSelect },
props: {
open: {
type: Boolean,
+11 -11
View File
@@ -57,10 +57,10 @@ vi.mock("@/lib/feedback/Toast/useToast", () => ({
toast: mockToast,
}));
const ODrawerStub = {
name: "ODrawer",
const ODialogStub = {
name: "ODialog",
template:
'<div class="o-drawer-stub" :data-open="open"><slot name="header" /><slot /><slot name="footer" /></div>',
'<div class="o-dialog-stub" :data-open="open"><slot name="header" /><slot /><slot name="footer" /></div>',
props: [
"open",
"size",
@@ -110,7 +110,7 @@ describe("AddStream", () => {
const globalStubs = {
StreamFieldInputs: true,
ODrawer: ODrawerStub,
ODialog: ODialogStub,
OForm: OFormStub,
};
@@ -139,7 +139,7 @@ describe("AddStream", () => {
it("should render ODrawer with localized title and button labels", async () => {
const wrapper = mountComp();
await flushPromises();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.exists()).toBe(true);
expect(drawer.props("title")).toBeTruthy();
expect(drawer.props("secondaryButtonLabel")).toBeTruthy();
@@ -149,14 +149,14 @@ describe("AddStream", () => {
it("should pass through the open prop to ODrawer", async () => {
const wrapper = mountComp(undefined, { open: true });
await flushPromises();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("open")).toBe(true);
});
it("should pass open=false to ODrawer when closed", async () => {
const wrapper = mountComp(undefined, { open: false });
await flushPromises();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("open")).toBe(false);
});
@@ -184,7 +184,7 @@ describe("AddStream", () => {
await flushPromises();
// Pipeline mode should not have ODrawer
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.exists()).toBe(false);
// Pipeline mode should have inputs
@@ -209,7 +209,7 @@ describe("AddStream", () => {
it("should emit update:open=false when Cancel button is clicked", async () => {
const wrapper = mountComp();
await flushPromises();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
await drawer.vm.$emit("click:secondary");
expect(wrapper.emitted("update:open")).toBeTruthy();
expect(wrapper.emitted("update:open")!.at(-1)).toEqual([false]);
@@ -218,7 +218,7 @@ describe("AddStream", () => {
it("should propagate ODrawer's update:open event to parent", async () => {
const wrapper = mountComp();
await flushPromises();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
await drawer.vm.$emit("update:open", false);
expect(wrapper.emitted("update:open")).toBeTruthy();
expect(wrapper.emitted("update:open")!.at(-1)).toEqual([false]);
@@ -229,7 +229,7 @@ describe("AddStream", () => {
await flushPromises();
// In drawer mode, Save is wired to ODrawer's click:primary event
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
await drawer.vm.$emit("click:primary");
await flushPromises();
+7 -7
View File
@@ -15,20 +15,20 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<ODrawer
<ODialog
v-if="!isInPipeline"
data-test="add-stream-dialog"
:open="open"
size="lg"
size="md"
:title="t('logStream.add')"
:secondary-button-label="t('logStream.cancel')"
:primary-button-label="t('common.save')"
form-id="add-stream-form"
@update:open="emits('update:open', $event)"
@click:secondary="emits('update:open', false)"
@click:primary="submitForm"
>
<div class="tw:p-4 tw:w-full">
<OForm :default-values="streamInputsDefault" @submit="submitForm">
<div class="tw:w-full">
<OForm id="add-stream-form" :default-values="streamInputsDefault" @submit="submitForm">
<div class="tw:mt-2">
<OInput
data-test="add-stream-name-input"
@@ -77,7 +77,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
/>
</OForm>
</div>
</ODrawer>
</ODialog>
<!-- Inline form for pipeline usage (no drawer wrapper) -->
<div v-else class="tw:p-4 tw:w-full">
@@ -156,7 +156,7 @@ import streamService from "@/services/stream";
import { useStore } from "vuex";
import { computed } from "vue";
import useStreams from "@/composables/useStreams";
import ODrawer from "@/lib/overlay/Drawer/ODrawer.vue";
import ODialog from "@/lib/overlay/Dialog/ODialog.vue";
import OButton from "@/lib/core/Button/OButton.vue";
import OInput from "@/lib/forms/Input/OInput.vue";
import OSelect from "@/lib/forms/Select/OSelect.vue";
@@ -73,8 +73,8 @@ const mockJob: BackfillJob = {
// component wires up (update:open, click:primary, click:secondary). The
// title/buttons live on the drawer itself in the real component, so we expose
// them via attrs so the test suite can assert on them.
const ODrawerStub = {
name: "ODrawer",
const ODialogStub = {
name: "ODialog",
props: [
"open",
"width",
@@ -120,7 +120,7 @@ function createWrapper(props: Record<string, unknown> = {}) {
global: {
plugins: [i18n, store],
stubs: {
ODrawer: ODrawerStub,
ODialog: ODialogStub,
OCollapsible: {
template: '<div><slot /></div>',
props: ["modelValue", "icon", "label"],
@@ -277,7 +277,7 @@ describe("EditBackfillJobDialog", () => {
vm.formData.delayBetweenChunks = 5;
vm.formData.deleteBeforeBackfill = false;
await wrapper.findComponent(ODrawerStub).vm.$emit("click:primary");
await wrapper.findComponent(ODialogStub).vm.$emit("click:primary");
await flushPromises();
expect(backfillService.updateBackfillJob).toHaveBeenCalledWith({
@@ -303,7 +303,7 @@ describe("EditBackfillJobDialog", () => {
vm.formData.endTimeMicros = 1700003600000000;
vm.formData.deleteBeforeBackfill = false;
await wrapper.findComponent(ODrawerStub).vm.$emit("click:primary");
await wrapper.findComponent(ODialogStub).vm.$emit("click:primary");
await flushPromises();
expect(wrapper.emitted("job-updated")).toBeTruthy();
@@ -319,7 +319,7 @@ describe("EditBackfillJobDialog", () => {
vm.formData.endTimeMicros = 1700003600000000;
vm.formData.deleteBeforeBackfill = false;
await wrapper.findComponent(ODrawerStub).vm.$emit("click:primary");
await wrapper.findComponent(ODialogStub).vm.$emit("click:primary");
await flushPromises();
const emitted = wrapper.emitted("update:modelValue");
@@ -341,7 +341,7 @@ describe("EditBackfillJobDialog", () => {
vm.formData.endTimeMicros = 1700003600000000;
vm.formData.deleteBeforeBackfill = false;
await wrapper.findComponent(ODrawerStub).vm.$emit("click:primary");
await wrapper.findComponent(ODialogStub).vm.$emit("click:primary");
await flushPromises();
expect(vm.errorMessage).toBe("Network Error");
@@ -364,7 +364,7 @@ describe("EditBackfillJobDialog", () => {
vm.formData.endTimeMicros = 1700003600000000;
vm.formData.deleteBeforeBackfill = false;
await wrapper.findComponent(ODrawerStub).vm.$emit("click:primary");
await wrapper.findComponent(ODialogStub).vm.$emit("click:primary");
await flushPromises();
expect(vm.errorMessage).toBe("Job not found");
@@ -412,7 +412,7 @@ describe("EditBackfillJobDialog", () => {
const wrapper = createWrapper({ modelValue: true, job: mockJob });
await flushPromises();
await wrapper.findComponent(ODrawerStub).vm.$emit("click:secondary");
await wrapper.findComponent(ODialogStub).vm.$emit("click:secondary");
await nextTick();
const emitted = wrapper.emitted("update:modelValue");
@@ -429,7 +429,7 @@ describe("EditBackfillJobDialog", () => {
vm.formData.chunkPeriodMinutes = 999;
vm.errorMessage = "some error";
await wrapper.findComponent(ODrawerStub).vm.$emit("click:secondary");
await wrapper.findComponent(ODialogStub).vm.$emit("click:secondary");
await nextTick();
expect(vm.formData.startTimeMicros).toBe(0);
@@ -483,7 +483,7 @@ describe("EditBackfillJobDialog", () => {
const wrapper = createWrapper({ modelValue: true, job: mockJob });
await flushPromises();
await wrapper.findComponent(ODrawerStub).vm.$emit("update:open", false);
await wrapper.findComponent(ODialogStub).vm.$emit("update:open", false);
await nextTick();
const emitted = wrapper.emitted("update:modelValue");
@@ -15,9 +15,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<ODrawer
<ODialog
v-model:open="show"
:width="30"
size="sm"
title="Edit Backfill Job"
secondary-button-label="Cancel"
primary-button-label="Update Job"
@@ -26,7 +26,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
@click:primary="onSubmit"
data-test="edit-backfill-job-dialog"
>
<div id="edit-backfill-form" class="tw:mx-5 tw:my-3">
<div id="edit-backfill-form">
<!-- Time Range Section -->
<div>
<div class="tw:text-sm tw:font-medium tw:mb-2">
@@ -131,13 +131,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
{{ errorMessage }}
</div>
</div>
</ODrawer>
</ODialog>
</template>
<script setup lang="ts">
import { ref, computed, watch, nextTick } from "vue";
import { useStore } from "vuex";
import ODrawer from "@/lib/overlay/Drawer/ODrawer.vue";
import ODialog from "@/lib/overlay/Dialog/ODialog.vue";
import OInput from "@/lib/forms/Input/OInput.vue";
import OCheckbox from "@/lib/forms/Checkbox/OCheckbox.vue";
import OTooltip from "@/lib/overlay/Tooltip/OTooltip.vue";
@@ -23,7 +23,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:primary-button-label="isSaving ? 'Saving...' : isEdit ? t('regex_patterns.update_close') : t('regex_patterns.create_close')"
:secondary-button-label="t('regex_patterns.cancel')"
:primary-button-disabled="isFormEmpty || isSaving"
@click:primary="saveRegexPattern"
form-id="add-regex-pattern-form"
@click:secondary="handleClose"
data-test="add-regex-pattern-drawer"
>
@@ -68,7 +68,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
"
>
<OForm
ref="formRef"
id="add-regex-pattern-form"
:key="formKey"
:default-values="{ name: regexPatternInputs.name, pattern: regexPatternInputs.pattern }"
@submit="saveRegexPattern"
@@ -491,8 +491,6 @@ export default defineComponent({
};
const saveRegexPattern = async () => {
const isValid = await formRef.value?.validate();
if (!isValid) return;
isSaving.value = true;
//payload for create and update regex pattern
// we need to send the name , pattern , description
@@ -600,7 +598,6 @@ export default defineComponent({
inputContext,
closeAddRegexPatternDialog,
handleClose,
formRef,
formKey,
};
},
@@ -54,6 +54,12 @@ export interface DialogProps {
// ── Inbuilt footer buttons ──────────────────────────────────────────────
/**
* When provided, the built-in primary button becomes type="submit" associated
* with this form id. Enables Enter-key submission for forms in the dialog body.
*/
formId?: string;
/** Label for the primary action button (right side). Omit to hide. */
primaryButtonLabel?: string;
/** Label for the secondary action button (right of neutral, left of primary). Omit to hide. */
+14 -6
View File
@@ -10,7 +10,7 @@ import {
DialogTitle,
DialogTrigger,
} from "reka-ui";
import { ref, watch, watchEffect, useSlots, computed, nextTick, useAttrs } from "vue";
import { ref, watch, watchEffect, useSlots, computed, nextTick, useAttrs, inject, provide } from "vue";
import OButton from "@/lib/core/Button/OButton.vue";
import { useScrollShadow } from "@/lib/overlay/useScrollShadow";
@@ -22,6 +22,13 @@ const $attrs = useAttrs();
// (DialogRoot is renderless, so default attribute inheritance would lose it.)
const parentDataTest = computed(() => $attrs["data-test"] as string | undefined);
// Stacking support: each nested ODialog gets a higher z-index layer
const dialogDepth = inject<number>("o2DialogDepth", 0);
provide("o2DialogDepth", dialogDepth + 1);
const overlayZIndex = computed(() => 5999 + dialogDepth * 1000);
const contentZIndex = computed(() => 6000 + dialogDepth * 1000);
const props = withDefaults(defineProps<DialogProps>(), {
persistent: false,
size: "md",
@@ -285,8 +292,9 @@ watch(internalOpen, (open) => {
<!-- Overlay / scrim -->
<DialogOverlay
data-test="o-dialog-overlay"
:style="{ zIndex: overlayZIndex }"
:class="[
'tw:fixed tw:inset-0 tw:z-5999',
'tw:fixed tw:inset-0',
'tw:bg-dialog-overlay',
'tw:data-[state=open]:animate-in tw:data-[state=open]:fade-in-0',
'tw:data-[state=closed]:animate-out tw:data-[state=closed]:fade-out-0',
@@ -298,12 +306,10 @@ watch(internalOpen, (open) => {
<DialogContent
data-o2-dialog
:data-test="parentDataTest || 'o-dialog-panel'"
:style="contentStyle"
:style="[contentStyle, { zIndex: contentZIndex }]"
:class="[
// Positioning — centered in viewport
'tw:fixed tw:left-1/2 tw:top-1/2 tw:-translate-x-1/2 tw:-translate-y-1/2',
// Stacking — above Quasar header (2000) and drawer (3000)
'tw:z-6000',
// Layout — flex-col so header/footer stick and only body scrolls
'tw:flex tw:flex-col tw:overflow-hidden',
// Size
@@ -498,9 +504,11 @@ watch(internalOpen, (open) => {
data-test="o-dialog-primary-btn"
:variant="primaryButtonVariant"
size="sm-action"
:type="formId ? 'submit' : 'button'"
:form="formId || undefined"
:disabled="primaryEffectivelyDisabled"
:loading="primaryButtonLoading"
@click="emit('click:primary')"
@click="!formId && emit('click:primary')"
>
{{ primaryButtonLabel }}
</OButton>
@@ -77,6 +77,12 @@ export interface DrawerProps {
// ── Inbuilt footer buttons ──────────────────────────────────────────────
/**
* When provided, the built-in primary button becomes type="submit" associated
* with this form id. Enables Enter-key submission for forms in the drawer body.
*/
formId?: string;
/** Label for the primary action button (right side). Omit to hide. */
primaryButtonLabel?: string;
/** Label for the secondary action button (right of neutral, left of primary). Omit to hide. */
+3 -1
View File
@@ -520,9 +520,11 @@ watch(internalOpen, (open) => {
data-test="o-drawer-primary-btn"
:variant="primaryButtonVariant"
size="sm-action"
:type="formId ? 'submit' : 'button'"
:form="formId || undefined"
:disabled="primaryEffectivelyDisabled"
:loading="primaryButtonLoading"
@click="emit('click:primary')"
@click="!formId && emit('click:primary')"
>
{{ primaryButtonLabel }}
</OButton>
+24 -23
View File
@@ -97,12 +97,12 @@ const mockStore = {
};
// ---------------------------------------------------------------------------
// ODrawer stub — mirrors the migrated component's overlay surface.
// ODialog stub — mirrors the migrated component's overlay surface.
// Renders the default slot so children (form, dropdowns) are queryable.
// Exposes all migrated props and emits so we can assert on them.
// ---------------------------------------------------------------------------
const ODrawerStub = {
name: "ODrawer",
const ODialogStub = {
name: "ODialog",
template:
"<div class='o-drawer-stub' :data-test='$attrs[\"data-test\"]' :data-open='open'>" +
"<slot name='header' />" +
@@ -131,6 +131,7 @@ const ODrawerStub = {
"primaryButtonLoading",
"secondaryButtonLoading",
"neutralButtonLoading",
"formId",
],
emits: ["update:open", "click:primary", "click:secondary", "click:neutral"],
};
@@ -162,7 +163,7 @@ const createWrapper = (props: Record<string, any> = {}) => {
},
global: {
stubs: {
ODrawer: ODrawerStub,
ODialog: ODialogStub,
OForm: {
name: "OForm",
template: "<form class='o-form-stub' @submit.prevent='$emit(\"submit\", {})'><slot /></form>",
@@ -322,7 +323,7 @@ describe("AddToDashboard — props", () => {
it("accepts open prop and forwards it to ODrawer", async () => {
const wrapper = createWrapper({ open: true });
await flushPromises();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.exists()).toBe(true);
expect(drawer.props("open")).toBe(true);
});
@@ -332,7 +333,7 @@ describe("AddToDashboard — props", () => {
props: { dashboardPanelData: defaultDashboardPanelData },
global: {
stubs: {
ODrawer: ODrawerStub,
ODialog: ODialogStub,
QForm: true,
QInput: true,
SelectFolderDropdown: true,
@@ -342,7 +343,7 @@ describe("AddToDashboard — props", () => {
},
});
await flushPromises();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("open")).toBe(false);
});
});
@@ -741,36 +742,36 @@ describe("AddToDashboard — ODrawer surface", () => {
it("passes the localized title to ODrawer", async () => {
const wrapper = createWrapper();
await flushPromises();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("title")).toBe("dashboard.addDashboard");
});
it("passes the localized primary button label to ODrawer", async () => {
const wrapper = createWrapper();
await flushPromises();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("primaryButtonLabel")).toBe("metrics.add");
});
it("passes a secondary button label (Cancel) to ODrawer", async () => {
const wrapper = createWrapper();
await flushPromises();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("secondaryButtonLabel")).toBe("Cancel");
});
it("passes the configured width (30) to ODrawer", async () => {
it("passes the configured size (md) to ODialog", async () => {
const wrapper = createWrapper();
await flushPromises();
const drawer = wrapper.findComponent(ODrawerStub);
expect(drawer.props("width")).toBe(30);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("size")).toBe("md");
});
it("disables the primary button when panelTitle is empty", async () => {
const wrapper = createWrapper();
await flushPromises();
// panelTitle starts empty
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("primaryButtonDisabled")).toBe(true);
});
@@ -779,7 +780,7 @@ describe("AddToDashboard — ODrawer surface", () => {
await flushPromises();
wrapper.vm.panelTitle = "My Panel";
await nextTick();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("primaryButtonDisabled")).toBe(false);
});
@@ -788,7 +789,7 @@ describe("AddToDashboard — ODrawer surface", () => {
await flushPromises();
wrapper.vm.panelTitle = " ";
await nextTick();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("primaryButtonDisabled")).toBe(true);
});
@@ -796,11 +797,11 @@ describe("AddToDashboard — ODrawer surface", () => {
mockUseLoadingIsLoading.value = true;
const wrapper = createWrapper();
await flushPromises();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("primaryButtonLoading")).toBe(true);
});
it("invokes onSubmit.execute when ODrawer emits click:primary", async () => {
it("invokes onSubmit.execute when OForm emits submit", async () => {
const wrapper = createWrapper();
await flushPromises();
@@ -808,8 +809,8 @@ describe("AddToDashboard — ODrawer surface", () => {
wrapper.vm.activeTabId = "tab-1";
wrapper.vm.panelTitle = "Panel";
const drawer = wrapper.findComponent(ODrawerStub);
await drawer.vm.$emit("click:primary");
const form = wrapper.findComponent({ name: "OForm" });
await form.vm.$emit("submit", {});
await flushPromises();
expect(mockAddPanel).toHaveBeenCalled();
@@ -819,7 +820,7 @@ describe("AddToDashboard — ODrawer surface", () => {
const wrapper = createWrapper();
await flushPromises();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
await drawer.vm.$emit("click:secondary");
await nextTick();
@@ -832,7 +833,7 @@ describe("AddToDashboard — ODrawer surface", () => {
const wrapper = createWrapper();
await flushPromises();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
await drawer.vm.$emit("update:open", false);
await nextTick();
@@ -845,7 +846,7 @@ describe("AddToDashboard — ODrawer surface", () => {
const wrapper = createWrapper({ open: false });
await flushPromises();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
await drawer.vm.$emit("update:open", true);
await nextTick();
+8 -15
View File
@@ -1,19 +1,19 @@
<template>
<ODrawer
<ODialog
:open="open"
:width="30"
size="md"
:title="t('dashboard.addDashboard')"
secondary-button-label="Cancel"
:primary-button-label="t('metrics.add')"
form-id="add-to-dashboard-form"
:primary-button-loading="onSubmit.isLoading.value"
:primary-button-disabled="!panelTitle.trim()"
data-test="add-to-dashboard-dialog"
@update:open="$emit('update:open', $event)"
@click:secondary="$emit('update:open', false)"
@click:primary="handlePrimaryClick()"
>
<OForm ref="formRef" :default-values="{ panelTitle: '' }" @submit="onSubmit.execute()">
<div class="add-dashboard-form-card-section tw:flex tw:flex-col tw:gap-4 tw:px-3 tw:py-2">
<OForm id="add-to-dashboard-form" :default-values="{ panelTitle: '' }" @submit="onSubmit.execute()">
<div class="add-dashboard-form-card-section tw:flex tw:flex-col tw:gap-4">
<!-- select folder or create new folder and select -->
<select-folder-dropdown @folder-selected="updateActiveFolderId" />
@@ -40,7 +40,7 @@
/>
</div>
</OForm>
</ODrawer>
</ODialog>
</template>
<script lang="ts">
@@ -53,7 +53,7 @@ import { addPanel } from "@/utils/commons";
import SelectFolderDropdown from "@/components/dashboards/SelectFolderDropdown.vue";
import SelectDashboardDropdown from "@/components/dashboards/SelectDashboardDropdown.vue";
import SelectTabDropdown from "@/components/dashboards/SelectTabDropdown.vue";
import ODrawer from '@/lib/overlay/Drawer/ODrawer.vue';
import ODialog from '@/lib/overlay/Dialog/ODialog.vue';
import OInput from '@/lib/forms/Input/OInput.vue';
import OForm from '@/lib/forms/Form/OForm.vue';
import OFormInput from '@/lib/forms/Input/OFormInput.vue';
@@ -68,7 +68,7 @@ export default defineComponent({
SelectFolderDropdown,
SelectDashboardDropdown,
SelectTabDropdown,
ODrawer,
ODialog,
OInput,
OForm,
OFormInput,
@@ -201,11 +201,6 @@ export default defineComponent({
}
});
const formRef = ref(null);
const handlePrimaryClick = async () => {
formRef.value?.submit();
};
return {
t,
getImageURL,
@@ -220,8 +215,6 @@ export default defineComponent({
activeFolderId,
activeTabId,
updateSelectedDashboard,
formRef,
handlePrimaryClick,
};
},
});
+10 -22
View File
@@ -228,7 +228,7 @@ const buildGlobalConfig = (store: any, router: any, i18n: any, routeQuery: any =
},
ODialog: {
name: "ODialog",
props: ["open", "width", "title", "subTitle", "showClose", "persistent", "size", "primaryButtonLabel", "secondaryButtonLabel", "neutralButtonLabel"],
props: ["open", "width", "title", "subTitle", "showClose", "persistent", "size", "primaryButtonLabel", "secondaryButtonLabel", "neutralButtonLabel", "formId"],
emits: ["update:open", "click:primary", "click:secondary", "click:neutral"],
template: '<div data-test-stub="o-dialog" :data-open="open" :data-title="title"><slot /></div>',
},
@@ -524,11 +524,11 @@ describe("Dashboards.vue", () => {
});
});
// Helper: locate ODrawer instances by title prop.
// Each migrated drawer differs only by title, so this gives us a
// Helper: locate ODialog instances by title prop.
// Each migrated dialog differs only by title, so this gives us a
// deterministic, attribute-free lookup against the global stub.
const findDrawerByTitle = (w: any, title: string) => {
const drawers = w.findAllComponents({ name: "ODrawer" });
const drawers = w.findAllComponents({ name: "ODialog" });
return drawers.find((d: any) => d.props("title") === title);
};
@@ -544,7 +544,7 @@ describe("Dashboards.vue", () => {
const drawer = findDrawerByTitle(wrapper, "Create Dashboard");
expect(drawer).toBeTruthy();
expect(drawer.props("open")).toBe(false);
expect(drawer.props("width")).toBe(30);
expect(drawer.props("size")).toBe("md");
expect(drawer.props("primaryButtonLabel")).toBe("Save");
expect(drawer.props("secondaryButtonLabel")).toBe("Cancel");
});
@@ -582,7 +582,7 @@ describe("Dashboards.vue", () => {
expect(wrapper.vm.showAddDashboardDialog).toBe(false);
});
it("should invoke addDashboardRef.submit when ODrawer emits click:primary", async () => {
it("should pass form-id to ODialog for native form submission (add dashboard)", async () => {
wrapper = shallowMount(Dashboards, {
global: buildGlobalConfig(store, router, i18n),
});
@@ -593,14 +593,8 @@ describe("Dashboards.vue", () => {
wrapper.vm.showAddDashboardDialog = true;
await nextTick();
const submitSpy = vi.fn();
wrapper.vm.addDashboardRef = { submit: submitSpy };
const drawer = findDrawerByTitle(wrapper, "Create Dashboard");
await drawer.vm.$emit("click:primary");
await nextTick();
expect(submitSpy).toHaveBeenCalledTimes(1);
expect(drawer.props("formId")).toBe("add-dashboard-form");
});
it("should not throw when click:primary fires with null addDashboardRef", async () => {
@@ -648,7 +642,7 @@ describe("Dashboards.vue", () => {
const drawer = findDrawerByTitle(wrapper, "New Folder");
expect(drawer).toBeTruthy();
expect(drawer.props("open")).toBe(false);
expect(drawer.props("width")).toBe(30);
expect(drawer.props("size")).toBe("sm");
});
it("should switch drawer title to 'Update Folder' when in edit mode", async () => {
@@ -685,7 +679,7 @@ describe("Dashboards.vue", () => {
expect(wrapper.vm.showAddFolderDialog).toBe(false);
});
it("should invoke addFolderRef.submit on click:primary", async () => {
it("should pass form-id to ODialog for native form submission (add folder)", async () => {
wrapper = shallowMount(Dashboards, {
global: buildGlobalConfig(store, router, i18n),
});
@@ -696,14 +690,8 @@ describe("Dashboards.vue", () => {
wrapper.vm.showAddFolderDialog = true;
await nextTick();
const submitSpy = vi.fn();
wrapper.vm.addFolderRef = { submit: submitSpy };
const drawer = findDrawerByTitle(wrapper, "New Folder");
await drawer.vm.$emit("click:primary");
await nextTick();
expect(submitSpy).toHaveBeenCalledTimes(1);
expect(drawer.props("formId")).toBe("add-folder-dashboards-form");
});
it("should not throw when click:primary fires with null addFolderRef", async () => {
+10 -10
View File
@@ -373,16 +373,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</OTable>
<!-- add dashboard -->
<ODrawer
<ODialog
v-model:open="showAddDashboardDialog"
:width="30"
size="md"
:title="t('dashboard.createdashboard')"
data-test="dashboard-add-dialog"
:secondary-button-label="t('dashboard.cancel')"
:primary-button-label="t('dashboard.save')"
form-id="add-dashboard-form"
:primary-button-loading="addDashboardRef?.onSubmit?.isLoading?.value"
@click:secondary="showAddDashboardDialog = false"
@click:primary="addDashboardRef?.submit()"
>
<AddDashboard
ref="addDashboardRef"
@@ -390,7 +390,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
@updated="updateDashboardList"
:activeFolderId="activeFolderId"
/>
</ODrawer>
</ODialog>
<!-- add dashboard from GitHub gallery -->
<AddDashboardFromGitHub
@@ -399,9 +399,9 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
/>
<!-- add/edit folder -->
<ODrawer
<ODialog
v-model:open="showAddFolderDialog"
:width="30"
size="sm"
:title="
isFolderEditMode
? t('dashboard.updateFolder')
@@ -410,8 +410,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
data-test="dashboard-folder-dialog"
:secondary-button-label="t('dashboard.cancel')"
:primary-button-label="t('dashboard.save')"
form-id="add-folder-dashboards-form"
@click:secondary="showAddFolderDialog = false"
@click:primary="addFolderRef?.submit()"
>
<AddFolder
ref="addFolderRef"
@@ -419,7 +419,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
:edit-mode="isFolderEditMode"
:folder-id="selectedFolderToEdit ?? 'default'"
/>
</ODrawer>
</ODialog>
<!-- move dashboard to another folder -->
<MoveDashboardToAnotherFolder
@@ -470,7 +470,7 @@ import OTabs from "@/lib/navigation/Tabs/OTabs.vue";
import OTab from "@/lib/navigation/Tabs/OTab.vue";
import OButton from "@/lib/core/Button/OButton.vue";
import OIcon from "@/lib/core/Icon/OIcon.vue";
import ODrawer from "@/lib/overlay/Drawer/ODrawer.vue";
import ODialog from "@/lib/overlay/Dialog/ODialog.vue";
import OTooltip from "@/lib/overlay/Tooltip/OTooltip.vue";
import ODropdown from "@/lib/overlay/Dropdown/ODropdown.vue";
import ODropdownItem from "@/lib/overlay/Dropdown/ODropdownItem.vue";
@@ -544,7 +544,7 @@ export default defineComponent({
OSearchInput,
OSwitch,
OCheckbox,
ODrawer,
ODialog,
AddDashboard,
OTooltip,
AddDashboardFromGitHub,
@@ -12,12 +12,12 @@ vi.mock("../../utils/zincutils", () => ({
}));
// ---------------------------------------------------------------------------
// ODrawer stub — mirrors the migrated overlay surface.
// ODialog stub — mirrors the migrated overlay surface.
// Exposes migrated props/emits so tests can drive primary/secondary actions
// and assert on title/labels/open without depending on real overlay markup.
// ---------------------------------------------------------------------------
const ODrawerStub = {
name: "ODrawer",
const ODialogStub = {
name: "ODialog",
template:
"<div class='o-drawer-stub' :data-open='open' :data-title='title'>" +
"<slot name='header' />" +
@@ -156,7 +156,7 @@ describe("PanelLayoutSettings.vue", () => {
props: defaultProps,
global: {
plugins: [store, router, i18n],
stubs: { ODrawer: ODrawerStub, ...quasarStubs },
stubs: { ODialog: ODialogStub, ...quasarStubs },
},
});
@@ -171,7 +171,7 @@ describe("PanelLayoutSettings.vue", () => {
props: { layout, open: true },
global: {
plugins: [store, router, i18n],
stubs: { ODrawer: ODrawerStub, ...quasarStubs },
stubs: { ODialog: ODialogStub, ...quasarStubs },
},
});
@@ -184,7 +184,7 @@ describe("PanelLayoutSettings.vue", () => {
props: defaultProps,
global: {
plugins: [store, router, i18n],
stubs: { ODrawer: ODrawerStub, ...quasarStubs },
stubs: { ODialog: ODialogStub, ...quasarStubs },
},
});
@@ -196,7 +196,7 @@ describe("PanelLayoutSettings.vue", () => {
props: { layout: defaultProps.layout },
global: {
plugins: [store, router, i18n],
stubs: { ODrawer: ODrawerStub, ...quasarStubs },
stubs: { ODialog: ODialogStub, ...quasarStubs },
},
});
@@ -209,7 +209,7 @@ describe("PanelLayoutSettings.vue", () => {
props: defaultProps,
global: {
plugins: [store, router, i18n],
stubs: { ODrawer: ODrawerStub, ...quasarStubs },
stubs: { ODialog: ODialogStub, ...quasarStubs },
},
});
});
@@ -246,7 +246,7 @@ describe("PanelLayoutSettings.vue", () => {
props: defaultProps,
global: {
plugins: [store, router, i18n],
stubs: { ODrawer: ODrawerStub, ...quasarStubs },
stubs: { ODialog: ODialogStub, ...quasarStubs },
},
});
});
@@ -324,7 +324,7 @@ describe("PanelLayoutSettings.vue", () => {
global: {
plugins: [store, router, i18n],
stubs: {
ODrawer: ODrawerStub,
ODialog: ODialogStub,
QForm: QFormStub,
QInput: true,
QIcon: true,
@@ -335,14 +335,14 @@ describe("PanelLayoutSettings.vue", () => {
it("forwards open prop to ODrawer", () => {
wrapper = mountWithDrawer({ open: true });
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.exists()).toBe(true);
expect(drawer.props("open")).toBe(true);
});
it("passes title and button labels to ODrawer", () => {
wrapper = mountWithDrawer();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("title")).toBe("Panel Layout");
expect(drawer.props("primaryButtonLabel")).toBe("Save");
expect(drawer.props("secondaryButtonLabel")).toBe("Cancel");
@@ -351,7 +351,7 @@ describe("PanelLayoutSettings.vue", () => {
it("re-emits update:open from ODrawer", async () => {
wrapper = mountWithDrawer();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
await drawer.vm.$emit("update:open", false);
@@ -362,7 +362,7 @@ describe("PanelLayoutSettings.vue", () => {
it("emits update:open with false when ODrawer emits click:secondary", async () => {
wrapper = mountWithDrawer();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
await drawer.vm.$emit("click:secondary");
@@ -373,7 +373,7 @@ describe("PanelLayoutSettings.vue", () => {
it("calls submitForm and triggers save:layout when ODrawer emits click:primary", async () => {
wrapper = mountWithDrawer();
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
// QForm stub re-emits submit on form submit, which calls savePanelLayout
await drawer.vm.$emit("click:primary");
@@ -390,7 +390,7 @@ describe("PanelLayoutSettings.vue", () => {
// Set invalid height so submitForm sets an error instead of emitting save:layout
wrapper.vm.updatedLayout.h = 0;
wrapper.vm.heightError = "";
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(() => drawer.vm.$emit("click:primary")).not.toThrow();
await nextTick();
@@ -405,7 +405,7 @@ describe("PanelLayoutSettings.vue", () => {
props: defaultProps,
global: {
plugins: [store, router, i18n],
stubs: { ODrawer: ODrawerStub, ...quasarStubs },
stubs: { ODialog: ODialogStub, ...quasarStubs },
},
});
});
@@ -417,7 +417,7 @@ describe("PanelLayoutSettings.vue", () => {
props: defaultProps,
global: {
plugins: [store, router, i18n],
stubs: { ODrawer: ODrawerStub, ...quasarStubs },
stubs: { ODialog: ODialogStub, ...quasarStubs },
},
});
@@ -435,12 +435,12 @@ describe("PanelLayoutSettings.vue", () => {
});
it("should expose panel layout title via ODrawer prop", () => {
const drawer = wrapper.findComponent(ODrawerStub);
const drawer = wrapper.findComponent(ODialogStub);
expect(drawer.props("title")).toBe("Panel Layout");
});
it("should render the ODrawer overlay", () => {
expect(wrapper.findComponent(ODrawerStub).exists()).toBe(true);
expect(wrapper.findComponent(ODialogStub).exists()).toBe(true);
});
it("should still expose getImageURL helper on the instance", () => {
@@ -457,7 +457,7 @@ describe("PanelLayoutSettings.vue", () => {
props: defaultProps,
global: {
plugins: [store, router, i18n],
stubs: { ODrawer: ODrawerStub, ...quasarStubs },
stubs: { ODialog: ODialogStub, ...quasarStubs },
},
});
});
@@ -495,7 +495,7 @@ describe("PanelLayoutSettings.vue", () => {
props: { layout: customLayout, open: true },
global: {
plugins: [store, router, i18n],
stubs: { ODrawer: ODrawerStub, ...quasarStubs },
stubs: { ODialog: ODialogStub, ...quasarStubs },
},
});
@@ -15,7 +15,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<template>
<ODrawer data-test="panel-layout-settings-drawer"
<ODialog data-test="panel-layout-settings-drawer"
:open="open"
size="sm"
:title="t('panel.layout')"
@@ -27,15 +27,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
>
<div
data-test="panel-layout-settings-content"
class="tw:p-0 tw:w-[300px]!"
class="tw:p-0"
:class="store.state.theme == 'dark' ? 'dark-mode' : 'tw:bg-white'"
style="min-height: inherit"
>
<div class="tw:mx-3">
<div>
<div
data-test="panel-layout-settings-height"
class="o2-input"
style="padding-top: 12px"
>
<OInput
v-model.number="updatedLayout.h"
@@ -62,7 +61,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
</div>
</div>
</div>
</ODrawer>
</ODialog>
</template>
<script lang="ts">
@@ -71,13 +70,13 @@ import { useStore } from "vuex";
import { useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
import { getImageURL } from "../../utils/zincutils";
import ODrawer from "@/lib/overlay/Drawer/ODrawer.vue";
import ODialog from "@/lib/overlay/Dialog/ODialog.vue";
import OInput from "@/lib/forms/Input/OInput.vue";
import OTooltip from "@/lib/overlay/Tooltip/OTooltip.vue";
import OIcon from "@/lib/core/Icon/OIcon.vue";
export default defineComponent({
name: "PanelLayoutSettings",
components: { ODrawer, OInput, OTooltip,
components: { ODialog, OInput, OTooltip,
OIcon,
},
props: {