From 34ea6ec14f2ccea1ba6cdec9e910dee037972d37 Mon Sep 17 00:00:00 2001 From: Patrik Date: Thu, 17 Apr 2025 14:45:10 -0400 Subject: [PATCH] feat: adds `showSaveDraftButton` option to show draft button with autosave enabled (#12150) This adds a new `showSaveDraftButton` option to the `versions.drafts.autosave` config for collections and globals. By default, the "Save as draft" button is hidden when autosave is enabled. This new option allows the button to remain visible for manual saves while autosave is active. Also updates the admin UI logic to conditionally render the button when this flag is set, and updates the documentation with an example usage. --- docs/versions/autosave.mdx | 7 +- packages/payload/src/versions/types.ts | 7 + .../src/elements/DocumentControls/index.tsx | 22 +- .../collections/AutosaveWithDraftButton.ts | 32 +++ test/versions/config.ts | 12 +- test/versions/e2e.spec.ts | 204 ++++++++++-------- .../globals/AutosaveWithDraftButton.ts | 26 +++ test/versions/payload-types.ts | 64 +++++- test/versions/slugs.ts | 6 + 9 files changed, 282 insertions(+), 98 deletions(-) create mode 100644 test/versions/collections/AutosaveWithDraftButton.ts create mode 100644 test/versions/globals/AutosaveWithDraftButton.ts diff --git a/docs/versions/autosave.mdx b/docs/versions/autosave.mdx index 077c473259..5d1b9d6a73 100644 --- a/docs/versions/autosave.mdx +++ b/docs/versions/autosave.mdx @@ -22,6 +22,7 @@ Collections and Globals both support the same options for configuring autosave. | Drafts Autosave Options | Description | | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `interval` | Define an `interval` in milliseconds to automatically save progress while documents are edited. Document updates are "debounced" at this interval. Defaults to `800`. | +| `showSaveDraftButton` | Set this to `true` to show the "Save as draft" button even while autosave is enabled. Defaults to `false`. | **Example config with versions, drafts, and autosave enabled:** @@ -50,9 +51,13 @@ export const Pages: CollectionConfig = { drafts: { autosave: true, - // Alternatively, you can specify an `interval`: + // Alternatively, you can specify an object to customize autosave: // autosave: { + // Define how often the document should be autosaved (in milliseconds) // interval: 1500, + // + // Show the "Save as draft" button even while autosave is enabled + // showSaveDraftButton: true, // }, }, }, diff --git a/packages/payload/src/versions/types.ts b/packages/payload/src/versions/types.ts index 43c0d21854..efbc4e3c95 100644 --- a/packages/payload/src/versions/types.ts +++ b/packages/payload/src/versions/types.ts @@ -6,6 +6,13 @@ export type Autosave = { * @default 800 */ interval?: number + /** + * When set to `true`, the "Save as draft" button will be displayed even while autosave is enabled. + * By default, this button is hidden to avoid redundancy with autosave behavior. + * + * @default false + */ + showSaveDraftButton?: boolean } export type SchedulePublish = { diff --git a/packages/ui/src/elements/DocumentControls/index.tsx b/packages/ui/src/elements/DocumentControls/index.tsx index 0e99c0dcd5..2c0a6ed3e5 100644 --- a/packages/ui/src/elements/DocumentControls/index.tsx +++ b/packages/ui/src/elements/DocumentControls/index.tsx @@ -133,9 +133,23 @@ export const DocumentControls: React.FC<{ const unsavedDraftWithValidations = !id && collectionConfig?.versions?.drafts && collectionConfig.versions?.drafts.validate + const collectionConfigDrafts = collectionConfig?.versions?.drafts + const globalConfigDrafts = globalConfig?.versions?.drafts + const autosaveEnabled = - (collectionConfig?.versions?.drafts && collectionConfig?.versions?.drafts?.autosave) || - (globalConfig?.versions?.drafts && globalConfig?.versions?.drafts?.autosave) + (collectionConfigDrafts && collectionConfigDrafts?.autosave) || + (globalConfigDrafts && globalConfigDrafts?.autosave) + + const collectionAutosaveEnabled = collectionConfigDrafts && collectionConfigDrafts?.autosave + const globalAutosaveEnabled = globalConfigDrafts && globalConfigDrafts?.autosave + + const showSaveDraftButton = + (collectionAutosaveEnabled && + collectionConfigDrafts.autosave !== false && + collectionConfigDrafts.autosave.showSaveDraftButton === true) || + (globalAutosaveEnabled && + globalConfigDrafts.autosave !== false && + globalConfigDrafts.autosave.showSaveDraftButton === true) const showCopyToLocale = localization && !collectionConfig?.admin?.disableCopyToLocale @@ -218,7 +232,9 @@ export const DocumentControls: React.FC<{ {collectionConfig?.versions?.drafts || globalConfig?.versions?.drafts ? ( - {(unsavedDraftWithValidations || !autosaveEnabled) && ( + {(unsavedDraftWithValidations || + !autosaveEnabled || + (autosaveEnabled && showSaveDraftButton)) && ( } diff --git a/test/versions/collections/AutosaveWithDraftButton.ts b/test/versions/collections/AutosaveWithDraftButton.ts new file mode 100644 index 0000000000..3fc719a78d --- /dev/null +++ b/test/versions/collections/AutosaveWithDraftButton.ts @@ -0,0 +1,32 @@ +import type { CollectionConfig } from 'payload' + +import { autosaveWithDraftButtonSlug } from '../slugs.js' + +const AutosaveWithDraftButtonPosts: CollectionConfig = { + slug: autosaveWithDraftButtonSlug, + labels: { + singular: 'Autosave with Draft Button Post', + plural: 'Autosave with Draft Button Posts', + }, + admin: { + useAsTitle: 'title', + defaultColumns: ['title', 'subtitle', 'createdAt', '_status'], + }, + versions: { + drafts: { + autosave: { + showSaveDraftButton: true, + interval: 1000, + }, + }, + }, + fields: [ + { + name: 'title', + type: 'text', + required: true, + }, + ], +} + +export default AutosaveWithDraftButtonPosts diff --git a/test/versions/config.ts b/test/versions/config.ts index 1b23833611..36fdb76728 100644 --- a/test/versions/config.ts +++ b/test/versions/config.ts @@ -4,6 +4,7 @@ const filename = fileURLToPath(import.meta.url) const dirname = path.dirname(filename) import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js' import AutosavePosts from './collections/Autosave.js' +import AutosaveWithDraftButtonPosts from './collections/AutosaveWithDraftButton.js' import AutosaveWithValidate from './collections/AutosaveWithValidate.js' import CustomIDs from './collections/CustomIDs.js' import { Diff } from './collections/Diff/index.js' @@ -17,6 +18,7 @@ import Posts from './collections/Posts.js' import { TextCollection } from './collections/Text.js' import VersionPosts from './collections/Versions.js' import AutosaveGlobal from './globals/Autosave.js' +import AutosaveWithDraftButtonGlobal from './globals/AutosaveWithDraftButton.js' import DisablePublishGlobal from './globals/DisablePublish.js' import DraftGlobal from './globals/Draft.js' import DraftWithMaxGlobal from './globals/DraftWithMax.js' @@ -35,6 +37,7 @@ export default buildConfigWithDefaults({ DisablePublish, Posts, AutosavePosts, + AutosaveWithDraftButtonPosts, AutosaveWithValidate, DraftPosts, DraftWithMax, @@ -46,7 +49,14 @@ export default buildConfigWithDefaults({ TextCollection, Media, ], - globals: [AutosaveGlobal, DraftGlobal, DraftWithMaxGlobal, DisablePublishGlobal, LocalizedGlobal], + globals: [ + AutosaveGlobal, + AutosaveWithDraftButtonGlobal, + DraftGlobal, + DraftWithMaxGlobal, + DisablePublishGlobal, + LocalizedGlobal, + ], indexSortableFields: true, localization: { defaultLocale: 'en', diff --git a/test/versions/e2e.spec.ts b/test/versions/e2e.spec.ts index e5d9e7cd1a..2798fb52cd 100644 --- a/test/versions/e2e.spec.ts +++ b/test/versions/e2e.spec.ts @@ -48,6 +48,8 @@ import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../playwright.config.js' import { autosaveCollectionSlug, autoSaveGlobalSlug, + autosaveWithDraftButtonGlobal, + autosaveWithDraftButtonSlug, autosaveWithValidateCollectionSlug, customIDSlug, diffCollectionSlug, @@ -78,6 +80,7 @@ describe('Versions', () => { let url: AdminUrlUtil let serverURL: string let autosaveURL: AdminUrlUtil + let autosaveWithDraftButtonURL: AdminUrlUtil let autosaveWithValidateURL: AdminUrlUtil let draftWithValidateURL: AdminUrlUtil let disablePublishURL: AdminUrlUtil @@ -116,6 +119,7 @@ describe('Versions', () => { beforeAll(() => { url = new AdminUrlUtil(serverURL, draftCollectionSlug) autosaveURL = new AdminUrlUtil(serverURL, autosaveCollectionSlug) + autosaveWithDraftButtonURL = new AdminUrlUtil(serverURL, autosaveWithDraftButtonSlug) autosaveWithValidateURL = new AdminUrlUtil(serverURL, autosaveWithValidateCollectionSlug) disablePublishURL = new AdminUrlUtil(serverURL, disablePublishSlug) customIDURL = new AdminUrlUtil(serverURL, customIDSlug) @@ -201,78 +205,6 @@ describe('Versions', () => { await expect(page.locator('#field-title')).toHaveValue('v1') }) - test('should show global versions view level action in globals versions view', async () => { - const global = new AdminUrlUtil(serverURL, draftGlobalSlug) - await page.goto(`${global.global(draftGlobalSlug)}/versions`) - await expect(page.locator('.app-header .global-versions-button')).toHaveCount(1) - }) - - // TODO: Check versions/:version-id view for collections / globals - - test('global — has versions tab', async () => { - const global = new AdminUrlUtil(serverURL, draftGlobalSlug) - await page.goto(global.global(draftGlobalSlug)) - - const docURL = page.url() - const pathname = new URL(docURL).pathname - - const versionsTab = page.locator('.doc-tab', { - hasText: 'Versions', - }) - await versionsTab.waitFor({ state: 'visible' }) - - expect(versionsTab).toBeTruthy() - const href = versionsTab.locator('a').first() - await expect(href).toHaveAttribute('href', `${pathname}/versions`) - }) - - test('global — respects max number of versions', async () => { - await payload.updateGlobal({ - slug: draftWithMaxGlobalSlug, - data: { - title: 'initial title', - }, - }) - - const global = new AdminUrlUtil(serverURL, draftWithMaxGlobalSlug) - await page.goto(global.global(draftWithMaxGlobalSlug)) - - const titleFieldInitial = page.locator('#field-title') - await titleFieldInitial.fill('updated title') - await saveDocAndAssert(page, '#action-save-draft') - await expect(titleFieldInitial).toHaveValue('updated title') - - const versionsTab = page.locator('.doc-tab', { - hasText: '1', - }) - - await versionsTab.waitFor({ state: 'visible' }) - - expect(versionsTab).toBeTruthy() - - const titleFieldUpdated = page.locator('#field-title') - await titleFieldUpdated.fill('latest title') - await saveDocAndAssert(page, '#action-save-draft') - await expect(titleFieldUpdated).toHaveValue('latest title') - - const versionsTabUpdated = page.locator('.doc-tab', { - hasText: '1', - }) - - await versionsTabUpdated.waitFor({ state: 'visible' }) - - expect(versionsTabUpdated).toBeTruthy() - }) - - test('global — has versions route', async () => { - const global = new AdminUrlUtil(serverURL, autoSaveGlobalSlug) - const versionsURL = `${global.global(autoSaveGlobalSlug)}/versions` - await page.goto(versionsURL) - await expect(() => { - expect(page.url()).toMatch(/\/versions/) - }).toPass({ timeout: 10000, intervals: [100] }) - }) - test('collection - should autosave', async () => { await page.goto(autosaveURL.create) await page.locator('#field-title').fill('autosave title') @@ -309,6 +241,16 @@ describe('Versions', () => { await expect(drawer.locator('.id-label')).toBeVisible() }) + test('collection - should show "save as draft" button when showSaveDraftButton is true', async () => { + await page.goto(autosaveWithDraftButtonURL.create) + await expect(page.locator('#action-save-draft')).toBeVisible() + }) + + test('collection - should not show "save as draft" button when showSaveDraftButton is false', async () => { + await page.goto(autosaveURL.create) + await expect(page.locator('#action-save-draft')).toBeHidden() + }) + test('collection - autosave - should not create duplicates when clicking Create new', async () => { // This test checks that when we click "Create new" in the list view, it only creates 1 extra document and not more const { totalDocs: initialDocsCount } = await payload.find({ @@ -402,17 +344,6 @@ describe('Versions', () => { await expect(newUpdatedAt).not.toHaveText(initialUpdatedAt) }) - test('global - should autosave', async () => { - const url = new AdminUrlUtil(serverURL, autoSaveGlobalSlug) - await page.goto(url.global(autoSaveGlobalSlug)) - const titleField = page.locator('#field-title') - await titleField.fill('global title') - await waitForAutoSaveToRunAndComplete(page) - await expect(titleField).toHaveValue('global title') - await page.goto(url.global(autoSaveGlobalSlug)) - await expect(page.locator('#field-title')).toHaveValue('global title') - }) - test('should retain localized data during autosave', async () => { const en = 'en' const es = 'es' @@ -519,12 +450,6 @@ describe('Versions', () => { await expect(page.locator('#field-title')).toHaveValue('title') }) - test('globals — should hide publish button when access control prevents update', async () => { - const url = new AdminUrlUtil(serverURL, disablePublishGlobalSlug) - await page.goto(url.global(disablePublishGlobalSlug)) - await expect(page.locator('#action-save')).not.toBeAttached() - }) - test('collections — should hide publish button when access control prevents create', async () => { await page.goto(disablePublishURL.create) await expect(page.locator('#action-save')).not.toBeAttached() @@ -652,6 +577,107 @@ describe('Versions', () => { }) }) + describe('draft globals', () => { + test('should show global versions view level action in globals versions view', async () => { + const global = new AdminUrlUtil(serverURL, draftGlobalSlug) + await page.goto(`${global.global(draftGlobalSlug)}/versions`) + await expect(page.locator('.app-header .global-versions-button')).toHaveCount(1) + }) + + test('global — has versions tab', async () => { + const global = new AdminUrlUtil(serverURL, draftGlobalSlug) + await page.goto(global.global(draftGlobalSlug)) + + const docURL = page.url() + const pathname = new URL(docURL).pathname + + const versionsTab = page.locator('.doc-tab', { + hasText: 'Versions', + }) + await versionsTab.waitFor({ state: 'visible' }) + + expect(versionsTab).toBeTruthy() + const href = versionsTab.locator('a').first() + await expect(href).toHaveAttribute('href', `${pathname}/versions`) + }) + + test('global — respects max number of versions', async () => { + await payload.updateGlobal({ + slug: draftWithMaxGlobalSlug, + data: { + title: 'initial title', + }, + }) + + const global = new AdminUrlUtil(serverURL, draftWithMaxGlobalSlug) + await page.goto(global.global(draftWithMaxGlobalSlug)) + + const titleFieldInitial = page.locator('#field-title') + await titleFieldInitial.fill('updated title') + await saveDocAndAssert(page, '#action-save-draft') + await expect(titleFieldInitial).toHaveValue('updated title') + + const versionsTab = page.locator('.doc-tab', { + hasText: '1', + }) + + await versionsTab.waitFor({ state: 'visible' }) + + expect(versionsTab).toBeTruthy() + + const titleFieldUpdated = page.locator('#field-title') + await titleFieldUpdated.fill('latest title') + await saveDocAndAssert(page, '#action-save-draft') + await expect(titleFieldUpdated).toHaveValue('latest title') + + const versionsTabUpdated = page.locator('.doc-tab', { + hasText: '1', + }) + + await versionsTabUpdated.waitFor({ state: 'visible' }) + + expect(versionsTabUpdated).toBeTruthy() + }) + + test('global — has versions route', async () => { + const global = new AdminUrlUtil(serverURL, autoSaveGlobalSlug) + const versionsURL = `${global.global(autoSaveGlobalSlug)}/versions` + await page.goto(versionsURL) + await expect(() => { + expect(page.url()).toMatch(/\/versions/) + }).toPass({ timeout: 10000, intervals: [100] }) + }) + + test('global - should show "save as draft" button when showSaveDraftButton is true', async () => { + const url = new AdminUrlUtil(serverURL, autosaveWithDraftButtonGlobal) + await page.goto(url.global(autosaveWithDraftButtonGlobal)) + await expect(page.locator('#action-save-draft')).toBeVisible() + }) + + test('global - should not show "save as draft" button when showSaveDraftButton is false', async () => { + const url = new AdminUrlUtil(serverURL, autoSaveGlobalSlug) + await page.goto(url.global(autoSaveGlobalSlug)) + await expect(page.locator('#action-save-draft')).toBeHidden() + }) + + test('global - should autosave', async () => { + const url = new AdminUrlUtil(serverURL, autoSaveGlobalSlug) + await page.goto(url.global(autoSaveGlobalSlug)) + const titleField = page.locator('#field-title') + await titleField.fill('global title') + await waitForAutoSaveToRunAndComplete(page) + await expect(titleField).toHaveValue('global title') + await page.goto(url.global(autoSaveGlobalSlug)) + await expect(page.locator('#field-title')).toHaveValue('global title') + }) + + test('globals — should hide publish button when access control prevents update', async () => { + const url = new AdminUrlUtil(serverURL, disablePublishGlobalSlug) + await page.goto(url.global(disablePublishGlobalSlug)) + await expect(page.locator('#action-save')).not.toBeAttached() + }) + }) + describe('Scheduled publish', () => { beforeAll(() => { url = new AdminUrlUtil(serverURL, draftCollectionSlug) diff --git a/test/versions/globals/AutosaveWithDraftButton.ts b/test/versions/globals/AutosaveWithDraftButton.ts new file mode 100644 index 0000000000..5d8f1372a8 --- /dev/null +++ b/test/versions/globals/AutosaveWithDraftButton.ts @@ -0,0 +1,26 @@ +import type { GlobalConfig } from 'payload' + +import { autosaveWithDraftButtonGlobal } from '../slugs.js' + +const AutosaveWithDraftButtonGlobal: GlobalConfig = { + slug: autosaveWithDraftButtonGlobal, + fields: [ + { + name: 'title', + type: 'text', + localized: true, + required: true, + }, + ], + label: 'Autosave with Draft Button Global', + versions: { + drafts: { + autosave: { + showSaveDraftButton: true, + interval: 1000, + }, + }, + }, +} + +export default AutosaveWithDraftButtonGlobal diff --git a/test/versions/payload-types.ts b/test/versions/payload-types.ts index 6e5e4fc8c6..c016d16822 100644 --- a/test/versions/payload-types.ts +++ b/test/versions/payload-types.ts @@ -70,6 +70,7 @@ export interface Config { 'disable-publish': DisablePublish; posts: Post; 'autosave-posts': AutosavePost; + 'autosave-with-draft-button-posts': AutosaveWithDraftButtonPost; 'autosave-with-validate-posts': AutosaveWithValidatePost; 'draft-posts': DraftPost; 'draft-with-max-posts': DraftWithMaxPost; @@ -91,6 +92,7 @@ export interface Config { 'disable-publish': DisablePublishSelect | DisablePublishSelect; posts: PostsSelect | PostsSelect; 'autosave-posts': AutosavePostsSelect | AutosavePostsSelect; + 'autosave-with-draft-button-posts': AutosaveWithDraftButtonPostsSelect | AutosaveWithDraftButtonPostsSelect; 'autosave-with-validate-posts': AutosaveWithValidatePostsSelect | AutosaveWithValidatePostsSelect; 'draft-posts': DraftPostsSelect | DraftPostsSelect; 'draft-with-max-posts': DraftWithMaxPostsSelect | DraftWithMaxPostsSelect; @@ -112,6 +114,7 @@ export interface Config { }; globals: { 'autosave-global': AutosaveGlobal; + 'autosave-with-draft-button-global': AutosaveWithDraftButtonGlobal; 'draft-global': DraftGlobal; 'draft-with-max-global': DraftWithMaxGlobal; 'disable-publish-global': DisablePublishGlobal; @@ -119,6 +122,7 @@ export interface Config { }; globalsSelect: { 'autosave-global': AutosaveGlobalSelect | AutosaveGlobalSelect; + 'autosave-with-draft-button-global': AutosaveWithDraftButtonGlobalSelect | AutosaveWithDraftButtonGlobalSelect; 'draft-global': DraftGlobalSelect | DraftGlobalSelect; 'draft-with-max-global': DraftWithMaxGlobalSelect | DraftWithMaxGlobalSelect; 'disable-publish-global': DisablePublishGlobalSelect | DisablePublishGlobalSelect; @@ -228,6 +232,17 @@ export interface DraftPost { createdAt: string; _status?: ('draft' | 'published') | null; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "autosave-with-draft-button-posts". + */ +export interface AutosaveWithDraftButtonPost { + id: string; + title: string; + updatedAt: string; + createdAt: string; + _status?: ('draft' | 'published') | null; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "autosave-with-validate-posts". @@ -554,6 +569,10 @@ export interface PayloadLockedDocument { relationTo: 'autosave-posts'; value: string | AutosavePost; } | null) + | ({ + relationTo: 'autosave-with-draft-button-posts'; + value: string | AutosaveWithDraftButtonPost; + } | null) | ({ relationTo: 'autosave-with-validate-posts'; value: string | AutosaveWithValidatePost; @@ -676,6 +695,16 @@ export interface AutosavePostsSelect { createdAt?: T; _status?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "autosave-with-draft-button-posts_select". + */ +export interface AutosaveWithDraftButtonPostsSelect { + title?: T; + updatedAt?: T; + createdAt?: T; + _status?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "autosave-with-validate-posts_select". @@ -973,6 +1002,17 @@ export interface AutosaveGlobal { updatedAt?: string | null; createdAt?: string | null; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "autosave-with-draft-button-global". + */ +export interface AutosaveWithDraftButtonGlobal { + id: string; + title: string; + _status?: ('draft' | 'published') | null; + updatedAt?: string | null; + createdAt?: string | null; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "draft-global". @@ -1029,6 +1069,17 @@ export interface AutosaveGlobalSelect { createdAt?: T; globalType?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "autosave-with-draft-button-global_select". + */ +export interface AutosaveWithDraftButtonGlobalSelect { + title?: T; + _status?: T; + updatedAt?: T; + createdAt?: T; + globalType?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "draft-global_select". @@ -1082,10 +1133,15 @@ export interface TaskSchedulePublish { input: { type?: ('publish' | 'unpublish') | null; locale?: string | null; - doc?: { - relationTo: 'draft-posts'; - value: string | DraftPost; - } | null; + doc?: + | ({ + relationTo: 'autosave-posts'; + value: string | AutosavePost; + } | null) + | ({ + relationTo: 'draft-posts'; + value: string | DraftPost; + } | null); global?: 'draft-global' | null; user?: (string | null) | User; }; diff --git a/test/versions/slugs.ts b/test/versions/slugs.ts index 2dccdb8b65..5807c381b5 100644 --- a/test/versions/slugs.ts +++ b/test/versions/slugs.ts @@ -1,5 +1,7 @@ export const autosaveCollectionSlug = 'autosave-posts' +export const autosaveWithDraftButtonSlug = 'autosave-with-draft-button-posts' + export const autosaveWithValidateCollectionSlug = 'autosave-with-validate-posts' export const customIDSlug = 'custom-ids' @@ -33,7 +35,11 @@ export const collectionSlugs = [ ] export const autoSaveGlobalSlug = 'autosave-global' + +export const autosaveWithDraftButtonGlobal = 'autosave-with-draft-button-global' + export const draftGlobalSlug = 'draft-global' + export const draftWithMaxGlobalSlug = 'draft-with-max-global' export const globalSlugs = [autoSaveGlobalSlug, draftGlobalSlug]