mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Dashboard] Listing Page Callout When New Dashboard In Progress (#117237)
* Added dashboard listing state for when no dashboards are available, but the user has one in progress
This commit is contained in:
parent
18f601dc49
commit
5d5fb3f91c
5 changed files with 300 additions and 83 deletions
|
@ -34,13 +34,13 @@ exports[`after fetch When given a title that matches multiple dashboards, filter
|
|||
iconType="plusInCircle"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Create new dashboard
|
||||
Create a dashboard
|
||||
</EuiButton>
|
||||
}
|
||||
body={
|
||||
<React.Fragment>
|
||||
<p>
|
||||
You can combine data views from any Kibana app into one dashboard and see everything in one place.
|
||||
Analyze all of your Elastic data in one place by creating a dashboard and adding visualizations.
|
||||
</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
|
@ -51,7 +51,7 @@ exports[`after fetch When given a title that matches multiple dashboards, filter
|
|||
"sampleDataInstallLink": <EuiLink
|
||||
onClick={[Function]}
|
||||
>
|
||||
Install some sample data
|
||||
Add some sample data
|
||||
</EuiLink>,
|
||||
}
|
||||
}
|
||||
|
@ -146,13 +146,13 @@ exports[`after fetch initialFilter 1`] = `
|
|||
iconType="plusInCircle"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Create new dashboard
|
||||
Create a dashboard
|
||||
</EuiButton>
|
||||
}
|
||||
body={
|
||||
<React.Fragment>
|
||||
<p>
|
||||
You can combine data views from any Kibana app into one dashboard and see everything in one place.
|
||||
Analyze all of your Elastic data in one place by creating a dashboard and adding visualizations.
|
||||
</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
|
@ -163,7 +163,7 @@ exports[`after fetch initialFilter 1`] = `
|
|||
"sampleDataInstallLink": <EuiLink
|
||||
onClick={[Function]}
|
||||
>
|
||||
Install some sample data
|
||||
Add some sample data
|
||||
</EuiLink>,
|
||||
}
|
||||
}
|
||||
|
@ -257,13 +257,13 @@ exports[`after fetch renders all table rows 1`] = `
|
|||
iconType="plusInCircle"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Create new dashboard
|
||||
Create a dashboard
|
||||
</EuiButton>
|
||||
}
|
||||
body={
|
||||
<React.Fragment>
|
||||
<p>
|
||||
You can combine data views from any Kibana app into one dashboard and see everything in one place.
|
||||
Analyze all of your Elastic data in one place by creating a dashboard and adding visualizations.
|
||||
</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
|
@ -274,7 +274,7 @@ exports[`after fetch renders all table rows 1`] = `
|
|||
"sampleDataInstallLink": <EuiLink
|
||||
onClick={[Function]}
|
||||
>
|
||||
Install some sample data
|
||||
Add some sample data
|
||||
</EuiLink>,
|
||||
}
|
||||
}
|
||||
|
@ -368,13 +368,13 @@ exports[`after fetch renders call to action when no dashboards exist 1`] = `
|
|||
iconType="plusInCircle"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Create new dashboard
|
||||
Create a dashboard
|
||||
</EuiButton>
|
||||
}
|
||||
body={
|
||||
<React.Fragment>
|
||||
<p>
|
||||
You can combine data views from any Kibana app into one dashboard and see everything in one place.
|
||||
Analyze all of your Elastic data in one place by creating a dashboard and adding visualizations.
|
||||
</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
|
@ -385,7 +385,7 @@ exports[`after fetch renders call to action when no dashboards exist 1`] = `
|
|||
"sampleDataInstallLink": <EuiLink
|
||||
onClick={[Function]}
|
||||
>
|
||||
Install some sample data
|
||||
Add some sample data
|
||||
</EuiLink>,
|
||||
}
|
||||
}
|
||||
|
@ -446,6 +446,128 @@ exports[`after fetch renders call to action when no dashboards exist 1`] = `
|
|||
</DashboardListing>
|
||||
`;
|
||||
|
||||
exports[`after fetch renders call to action with continue when no dashboards exist but one is in progress 1`] = `
|
||||
<DashboardListing
|
||||
kbnUrlStateStorage={
|
||||
Object {
|
||||
"cancel": [Function],
|
||||
"change$": [Function],
|
||||
"get": [Function],
|
||||
"kbnUrlControls": Object {
|
||||
"cancel": [Function],
|
||||
"flush": [Function],
|
||||
"getPendingUrl": [Function],
|
||||
"listen": [Function],
|
||||
"update": [Function],
|
||||
"updateAsync": [Function],
|
||||
},
|
||||
"set": [Function],
|
||||
}
|
||||
}
|
||||
redirectTo={[MockFunction]}
|
||||
>
|
||||
<TableListView
|
||||
createItem={[Function]}
|
||||
deleteItems={[Function]}
|
||||
editItem={[Function]}
|
||||
emptyPrompt={
|
||||
<EuiEmptyPrompt
|
||||
actions={
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
gutterSize="s"
|
||||
justifyContent="center"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiButtonEmpty
|
||||
aria-label="Discard changes to New Dashboard"
|
||||
color="danger"
|
||||
data-test-subj="discardDashboardPromptButton"
|
||||
onClick={[Function]}
|
||||
size="s"
|
||||
>
|
||||
Discard changes
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<EuiButton
|
||||
aria-label="Continue editing New Dashboard"
|
||||
color="primary"
|
||||
data-test-subj="createDashboardPromptButton"
|
||||
fill={true}
|
||||
iconType="pencil"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Continue editing
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
body={
|
||||
<React.Fragment>
|
||||
<p>
|
||||
Analyze all of your Elastic data in one place by creating a dashboard and adding visualizations.
|
||||
</p>
|
||||
</React.Fragment>
|
||||
}
|
||||
iconType="dashboardApp"
|
||||
title={
|
||||
<h1
|
||||
id="dashboardListingHeading"
|
||||
>
|
||||
Dashboard in progress
|
||||
</h1>
|
||||
}
|
||||
/>
|
||||
}
|
||||
entityName="dashboard"
|
||||
entityNamePlural="dashboards"
|
||||
findItems={[Function]}
|
||||
headingId="dashboardListingHeading"
|
||||
initialFilter=""
|
||||
initialPageSize={20}
|
||||
listingLimit={100}
|
||||
rowHeader="title"
|
||||
searchFilters={Array []}
|
||||
tableCaption="Dashboards"
|
||||
tableColumns={
|
||||
Array [
|
||||
Object {
|
||||
"field": "title",
|
||||
"name": "Title",
|
||||
"render": [Function],
|
||||
"sortable": true,
|
||||
},
|
||||
Object {
|
||||
"field": "description",
|
||||
"name": "Description",
|
||||
"render": [Function],
|
||||
"sortable": true,
|
||||
},
|
||||
]
|
||||
}
|
||||
tableListTitle="Dashboards"
|
||||
toastNotifications={
|
||||
Object {
|
||||
"add": [MockFunction],
|
||||
"addDanger": [MockFunction],
|
||||
"addError": [MockFunction],
|
||||
"addInfo": [MockFunction],
|
||||
"addSuccess": [MockFunction],
|
||||
"addWarning": [MockFunction],
|
||||
"get$": [MockFunction],
|
||||
"remove": [MockFunction],
|
||||
}
|
||||
}
|
||||
/>
|
||||
</DashboardListing>
|
||||
`;
|
||||
|
||||
exports[`after fetch renders warning when listingLimit is exceeded 1`] = `
|
||||
<DashboardListing
|
||||
kbnUrlStateStorage={
|
||||
|
@ -479,13 +601,13 @@ exports[`after fetch renders warning when listingLimit is exceeded 1`] = `
|
|||
iconType="plusInCircle"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Create new dashboard
|
||||
Create a dashboard
|
||||
</EuiButton>
|
||||
}
|
||||
body={
|
||||
<React.Fragment>
|
||||
<p>
|
||||
You can combine data views from any Kibana app into one dashboard and see everything in one place.
|
||||
Analyze all of your Elastic data in one place by creating a dashboard and adding visualizations.
|
||||
</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
|
@ -496,7 +618,7 @@ exports[`after fetch renders warning when listingLimit is exceeded 1`] = `
|
|||
"sampleDataInstallLink": <EuiLink
|
||||
onClick={[Function]}
|
||||
>
|
||||
Install some sample data
|
||||
Add some sample data
|
||||
</EuiLink>,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import { KibanaContextProvider } from '../../services/kibana_react';
|
|||
import { createKbnUrlStateStorage } from '../../services/kibana_utils';
|
||||
import { DashboardListing, DashboardListingProps } from './dashboard_listing';
|
||||
import { makeDefaultServices } from '../test_helpers';
|
||||
import { DASHBOARD_PANELS_UNSAVED_ID } from '../lib/dashboard_session_storage';
|
||||
|
||||
function makeDefaultProps(): DashboardListingProps {
|
||||
return {
|
||||
|
@ -72,6 +73,25 @@ describe('after fetch', () => {
|
|||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('renders call to action with continue when no dashboards exist but one is in progress', async () => {
|
||||
const services = makeDefaultServices();
|
||||
services.savedDashboards.find = () => {
|
||||
return Promise.resolve({
|
||||
total: 0,
|
||||
hits: [],
|
||||
});
|
||||
};
|
||||
services.dashboardSessionStorage.getDashboardIdsWithUnsavedChanges = () => [
|
||||
DASHBOARD_PANELS_UNSAVED_ID,
|
||||
];
|
||||
const { component } = mountWith({ services });
|
||||
// Ensure all promises resolve
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
// Ensure the state changes are reflected
|
||||
component.update();
|
||||
expect(component).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('initialFilter', async () => {
|
||||
const props = makeDefaultProps();
|
||||
props.initialFilter = 'testFilter';
|
||||
|
|
|
@ -7,7 +7,15 @@
|
|||
*/
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiLink, EuiButton, EuiEmptyPrompt, EuiBasicTableColumn } from '@elastic/eui';
|
||||
import {
|
||||
EuiLink,
|
||||
EuiButton,
|
||||
EuiEmptyPrompt,
|
||||
EuiBasicTableColumn,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiButtonEmpty,
|
||||
} from '@elastic/eui';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { attemptLoadDashboardByTitle } from '../lib';
|
||||
import { DashboardAppServices, DashboardRedirect } from '../../types';
|
||||
|
@ -15,6 +23,8 @@ import {
|
|||
getDashboardBreadcrumb,
|
||||
dashboardListingTable,
|
||||
noItemsStrings,
|
||||
dashboardUnsavedListingStrings,
|
||||
getNewDashboardTitle,
|
||||
} from '../../dashboard_strings';
|
||||
import { ApplicationStart, SavedObjectsFindOptionsReference } from '../../../../../core/public';
|
||||
import { syncQueryStateWithUrl } from '../../services/data';
|
||||
|
@ -22,8 +32,9 @@ import { IKbnUrlStateStorage } from '../../services/kibana_utils';
|
|||
import { TableListView, useKibana } from '../../services/kibana_react';
|
||||
import { SavedObjectsTaggingApi } from '../../services/saved_objects_tagging_oss';
|
||||
import { DashboardUnsavedListing } from './dashboard_unsaved_listing';
|
||||
import { confirmCreateWithUnsaved } from './confirm_overlays';
|
||||
import { confirmCreateWithUnsaved, confirmDiscardUnsavedChanges } from './confirm_overlays';
|
||||
import { getDashboardListItemLink } from './get_dashboard_list_item_link';
|
||||
import { DASHBOARD_PANELS_UNSAVED_ID } from '../lib/dashboard_session_storage';
|
||||
|
||||
export interface DashboardListingProps {
|
||||
kbnUrlStateStorage: IKbnUrlStateStorage;
|
||||
|
@ -117,10 +128,109 @@ export const DashboardListing = ({
|
|||
}
|
||||
}, [dashboardSessionStorage, redirectTo, core.overlays]);
|
||||
|
||||
const emptyPrompt = useMemo(
|
||||
() => getNoItemsMessage(showWriteControls, core.application, createItem),
|
||||
[createItem, core.application, showWriteControls]
|
||||
);
|
||||
const emptyPrompt = useMemo(() => {
|
||||
if (!showWriteControls) {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
iconType="glasses"
|
||||
title={<h1 id="dashboardListingHeading">{noItemsStrings.getReadonlyTitle()}</h1>}
|
||||
body={<p>{noItemsStrings.getReadonlyBody()}</p>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const isEditingFirstDashboard = unsavedDashboardIds.length === 1;
|
||||
|
||||
const emptyAction = isEditingFirstDashboard ? (
|
||||
<EuiFlexGroup alignItems="center" justifyContent="center" gutterSize="s" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
size="s"
|
||||
color="danger"
|
||||
onClick={() =>
|
||||
confirmDiscardUnsavedChanges(core.overlays, () => {
|
||||
dashboardSessionStorage.clearState(DASHBOARD_PANELS_UNSAVED_ID);
|
||||
setUnsavedDashboardIds(dashboardSessionStorage.getDashboardIdsWithUnsavedChanges());
|
||||
})
|
||||
}
|
||||
data-test-subj="discardDashboardPromptButton"
|
||||
aria-label={dashboardUnsavedListingStrings.getDiscardAriaLabel(getNewDashboardTitle())}
|
||||
>
|
||||
{dashboardUnsavedListingStrings.getDiscardTitle()}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
fill
|
||||
iconType="pencil"
|
||||
color="primary"
|
||||
onClick={() => redirectTo({ destination: 'dashboard' })}
|
||||
data-test-subj="createDashboardPromptButton"
|
||||
aria-label={dashboardUnsavedListingStrings.getEditAriaLabel(getNewDashboardTitle())}
|
||||
>
|
||||
{dashboardUnsavedListingStrings.getEditTitle()}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) : (
|
||||
<EuiButton
|
||||
onClick={createItem}
|
||||
fill
|
||||
iconType="plusInCircle"
|
||||
data-test-subj="createDashboardPromptButton"
|
||||
>
|
||||
{noItemsStrings.getCreateNewDashboardText()}
|
||||
</EuiButton>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
iconType="dashboardApp"
|
||||
title={
|
||||
<h1 id="dashboardListingHeading">
|
||||
{isEditingFirstDashboard
|
||||
? noItemsStrings.getReadEditInProgressTitle()
|
||||
: noItemsStrings.getReadEditTitle()}
|
||||
</h1>
|
||||
}
|
||||
body={
|
||||
<>
|
||||
<p>{noItemsStrings.getReadEditDashboardDescription()}</p>
|
||||
{!isEditingFirstDashboard && (
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="dashboard.listing.createNewDashboard.newToKibanaDescription"
|
||||
defaultMessage="New to Kibana? {sampleDataInstallLink} to take a test drive."
|
||||
values={{
|
||||
sampleDataInstallLink: (
|
||||
<EuiLink
|
||||
onClick={() =>
|
||||
core.application.navigateToApp('home', {
|
||||
path: '#/tutorial_directory/sampleData',
|
||||
})
|
||||
}
|
||||
>
|
||||
{noItemsStrings.getSampleDataLinkText()}
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
actions={emptyAction}
|
||||
/>
|
||||
);
|
||||
}, [
|
||||
redirectTo,
|
||||
createItem,
|
||||
core.overlays,
|
||||
core.application,
|
||||
showWriteControls,
|
||||
unsavedDashboardIds,
|
||||
dashboardSessionStorage,
|
||||
]);
|
||||
|
||||
const fetchItems = useCallback(
|
||||
(filter: string) => {
|
||||
|
@ -233,60 +343,3 @@ const getTableColumns = (
|
|||
...(savedObjectsTagging ? [savedObjectsTagging.ui.getTableColumnDefinition()] : []),
|
||||
] as unknown as Array<EuiBasicTableColumn<Record<string, unknown>>>;
|
||||
};
|
||||
|
||||
const getNoItemsMessage = (
|
||||
showWriteControls: boolean,
|
||||
application: ApplicationStart,
|
||||
createItem: () => void
|
||||
) => {
|
||||
if (!showWriteControls) {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
iconType="glasses"
|
||||
title={<h1 id="dashboardListingHeading">{noItemsStrings.getReadonlyTitle()}</h1>}
|
||||
body={<p>{noItemsStrings.getReadonlyBody()}</p>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
iconType="dashboardApp"
|
||||
title={<h1 id="dashboardListingHeading">{noItemsStrings.getReadEditTitle()}</h1>}
|
||||
body={
|
||||
<>
|
||||
<p>{noItemsStrings.getReadEditDashboardDescription()}</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="dashboard.listing.createNewDashboard.newToKibanaDescription"
|
||||
defaultMessage="New to Kibana? {sampleDataInstallLink} to take a test drive."
|
||||
values={{
|
||||
sampleDataInstallLink: (
|
||||
<EuiLink
|
||||
onClick={() =>
|
||||
application.navigateToApp('home', {
|
||||
path: '#/tutorial_directory/sampleData',
|
||||
})
|
||||
}
|
||||
>
|
||||
{noItemsStrings.getSampleDataLinkText()}
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
actions={
|
||||
<EuiButton
|
||||
onClick={createItem}
|
||||
fill
|
||||
iconType="plusInCircle"
|
||||
data-test-subj="createDashboardPromptButton"
|
||||
>
|
||||
{noItemsStrings.getCreateNewDashboardText()}
|
||||
</EuiButton>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -321,7 +321,7 @@ export const createConfirmStrings = {
|
|||
}),
|
||||
getCreateSubtitle: () =>
|
||||
i18n.translate('dashboard.createConfirmModal.unsavedChangesSubtitle', {
|
||||
defaultMessage: 'You can continue editing or start with a blank dashboard.',
|
||||
defaultMessage: 'Continue editing or start over with a blank dashboard.',
|
||||
}),
|
||||
getStartOverButtonText: () =>
|
||||
i18n.translate('dashboard.createConfirmModal.confirmButtonLabel', {
|
||||
|
@ -420,7 +420,7 @@ export const dashboardListingTable = {
|
|||
export const dashboardUnsavedListingStrings = {
|
||||
getUnsavedChangesTitle: (plural = false) =>
|
||||
i18n.translate('dashboard.listing.unsaved.unsavedChangesTitle', {
|
||||
defaultMessage: 'You have unsaved changes in the following {dash}.',
|
||||
defaultMessage: 'You have unsaved changes in the following {dash}:',
|
||||
values: {
|
||||
dash: plural
|
||||
? dashboardListingTable.getEntityNamePlural()
|
||||
|
@ -469,17 +469,21 @@ export const noItemsStrings = {
|
|||
i18n.translate('dashboard.listing.createNewDashboard.title', {
|
||||
defaultMessage: 'Create your first dashboard',
|
||||
}),
|
||||
getReadEditInProgressTitle: () =>
|
||||
i18n.translate('dashboard.listing.createNewDashboard.inProgressTitle', {
|
||||
defaultMessage: 'Dashboard in progress',
|
||||
}),
|
||||
getReadEditDashboardDescription: () =>
|
||||
i18n.translate('dashboard.listing.createNewDashboard.combineDataViewFromKibanaAppDescription', {
|
||||
defaultMessage:
|
||||
'You can combine data views from any Kibana app into one dashboard and see everything in one place.',
|
||||
'Analyze all of your Elastic data in one place by creating a dashboard and adding visualizations.',
|
||||
}),
|
||||
getSampleDataLinkText: () =>
|
||||
i18n.translate('dashboard.listing.createNewDashboard.sampleDataInstallLinkText', {
|
||||
defaultMessage: `Install some sample data`,
|
||||
defaultMessage: `Add some sample data`,
|
||||
}),
|
||||
getCreateNewDashboardText: () =>
|
||||
i18n.translate('dashboard.listing.createNewDashboard.createButtonLabel', {
|
||||
defaultMessage: `Create new dashboard`,
|
||||
defaultMessage: `Create a dashboard`,
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -292,6 +292,15 @@ export class DashboardPageObject extends FtrService {
|
|||
}
|
||||
|
||||
public async clickNewDashboard(continueEditing = false) {
|
||||
const discardButtonExists = await this.testSubjects.exists('discardDashboardPromptButton');
|
||||
if (!continueEditing && discardButtonExists) {
|
||||
this.log.debug('found discard button');
|
||||
await this.testSubjects.click('discardDashboardPromptButton');
|
||||
const confirmation = await this.testSubjects.exists('confirmModalTitleText');
|
||||
if (confirmation) {
|
||||
await this.common.clickConfirmOnModal();
|
||||
}
|
||||
}
|
||||
await this.listingTable.clickNewButton('createDashboardPromptButton');
|
||||
if (await this.testSubjects.exists('dashboardCreateConfirm')) {
|
||||
if (continueEditing) {
|
||||
|
@ -305,6 +314,15 @@ export class DashboardPageObject extends FtrService {
|
|||
}
|
||||
|
||||
public async clickNewDashboardExpectWarning(continueEditing = false) {
|
||||
const discardButtonExists = await this.testSubjects.exists('discardDashboardPromptButton');
|
||||
if (!continueEditing && discardButtonExists) {
|
||||
this.log.debug('found discard button');
|
||||
await this.testSubjects.click('discardDashboardPromptButton');
|
||||
const confirmation = await this.testSubjects.exists('confirmModalTitleText');
|
||||
if (confirmation) {
|
||||
await this.common.clickConfirmOnModal();
|
||||
}
|
||||
}
|
||||
await this.listingTable.clickNewButton('createDashboardPromptButton');
|
||||
await this.testSubjects.existOrFail('dashboardCreateConfirm');
|
||||
if (continueEditing) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue