Feat: editorial workflow bitbucket gitlab (#3014)

* refactor: typescript the backends

* feat: support multiple files upload for GitLab and BitBucket

* fix: load entry media files from media folder or UI state

* chore: cleanup log message

* chore: code cleanup

* refactor: typescript the test backend

* refactor: cleanup getEntry unsued variables

* refactor: moved shared backend code to lib util

* chore: rename files to preserve history

* fix: bind readFile method to API classes

* test(e2e): switch to chrome in cypress tests

* refactor: extract common api methods

* refactor: remove most of immutable js usage from backends

* feat(backend-gitlab): initial editorial workflow support

* feat(backend-gitlab): implement missing workflow methods

* chore: fix lint error

* feat(backend-gitlab): support files deletion

* test(e2e): add gitlab cypress tests

* feat(backend-bitbucket): implement missing editorial workflow methods

* test(e2e): add BitBucket backend e2e tests

* build: update node version to 12 on netlify builds

* fix(backend-bitbucket): extract BitBucket avatar url

* test: fix git-gateway AuthenticationPage test

* test(e2e): fix some backend tests

* test(e2e): fix tests

* test(e2e): add git-gateway editorial workflow test

* chore: code cleanup

* test(e2e): revert back to electron

* test(e2e): add non editorial workflow tests

* fix(git-gateway-gitlab): don't call unpublishedEntry in simple workflow

gitlab git-gateway doesn't support editorial workflow APIs yet. This change makes sure not to call them in simple workflow

* refactor(backend-bitbucket): switch to diffstat API instead of raw diff

* chore: fix test

* test(e2e): add more git-gateway tests

* fix: post rebase typescript fixes

* test(e2e): fix tests

* fix: fix parsing of content key and add tests

* refactor: rename test file

* test(unit): add getStatues unit tests

* chore: update cypress

* docs: update beta docs
This commit is contained in:
Erez Rokah
2020-01-15 00:15:14 +02:00
committed by Shawn Erquhart
parent 4ff5bc2ee0
commit 6f221ab3c1
251 changed files with 70910 additions and 15974 deletions

View File

@ -5,7 +5,6 @@ import reducer, {
selectMediaFilePath,
selectMediaFilePublicPath,
} from '../entries';
import { EDITORIAL_WORKFLOW } from '../../constants/publishModes';
const initialState = OrderedMap({
posts: Map({ name: 'posts' }),
@ -73,33 +72,26 @@ describe('entries', () => {
});
describe('selectMediaFolder', () => {
it('should return global media folder when not in editorial workflow', () => {
expect(selectMediaFolder(Map({ media_folder: 'static/media' }))).toEqual('static/media');
});
it("should return global media folder when in editorial workflow and collection doesn't specify media_folder", () => {
it("should return global media folder when collection doesn't specify media_folder", () => {
expect(
selectMediaFolder(
Map({ media_folder: 'static/media', publish_mode: EDITORIAL_WORKFLOW }),
Map({ name: 'posts' }),
),
selectMediaFolder(Map({ media_folder: 'static/media' }), Map({ name: 'posts' })),
).toEqual('static/media');
});
it('should return draft media folder when in editorial workflow, collection specifies media_folder and entry path is null', () => {
it('should return draft media folder when collection specifies media_folder and entry path is null', () => {
expect(
selectMediaFolder(
Map({ media_folder: 'static/media', publish_mode: EDITORIAL_WORKFLOW }),
Map({ media_folder: 'static/media' }),
Map({ name: 'posts', folder: 'posts', media_folder: '' }),
null,
),
).toEqual('posts/DRAFT_MEDIA_FILES');
});
it('should return relative media folder when in editorial workflow, collection specifies media_folder and entry path is not null', () => {
it('should return relative media folder when collection specifies media_folder and entry path is not null', () => {
expect(
selectMediaFolder(
Map({ media_folder: 'static/media', publish_mode: EDITORIAL_WORKFLOW }),
Map({ media_folder: 'static/media' }),
Map({ name: 'posts', folder: 'posts', media_folder: '' }),
'posts/title/index.md',
),
@ -109,7 +101,7 @@ describe('entries', () => {
it('should resolve relative media folder', () => {
expect(
selectMediaFolder(
Map({ media_folder: 'static/media', publish_mode: EDITORIAL_WORKFLOW }),
Map({ media_folder: 'static/media' }),
Map({ name: 'posts', folder: 'posts', media_folder: '../' }),
'posts/title/index.md',
),
@ -126,19 +118,14 @@ describe('entries', () => {
it('should resolve path from global media folder when absolute path', () => {
expect(
selectMediaFilePath(
Map({ media_folder: 'static/media', publish_mode: EDITORIAL_WORKFLOW }),
null,
null,
'/media/image.png',
),
selectMediaFilePath(Map({ media_folder: 'static/media' }), null, null, '/media/image.png'),
).toBe('static/media/image.png');
});
it('should resolve path from global media folder when relative path for collection with no media folder', () => {
expect(
selectMediaFilePath(
Map({ media_folder: 'static/media', publish_mode: EDITORIAL_WORKFLOW }),
Map({ media_folder: 'static/media' }),
Map({ name: 'posts', folder: 'posts' }),
null,
'image.png',
@ -149,7 +136,7 @@ describe('entries', () => {
it('should resolve path from collection media folder when relative path for collection with media folder', () => {
expect(
selectMediaFilePath(
Map({ media_folder: 'static/media', publish_mode: EDITORIAL_WORKFLOW }),
Map({ media_folder: 'static/media' }),
Map({ name: 'posts', folder: 'posts', media_folder: '' }),
null,
'image.png',
@ -160,7 +147,7 @@ describe('entries', () => {
it('should handle relative media_folder', () => {
expect(
selectMediaFilePath(
Map({ media_folder: 'static/media', publish_mode: EDITORIAL_WORKFLOW }),
Map({ media_folder: 'static/media' }),
Map({ name: 'posts', folder: 'posts', media_folder: '../../static/media/' }),
'posts/title/index.md',
'image.png',
@ -176,26 +163,16 @@ describe('entries', () => {
);
});
it('should resolve path from public folder when not in editorial workflow', () => {
it('should resolve path from public folder for collection with no media folder', () => {
expect(
selectMediaFilePublicPath(Map({ public_folder: '/media' }), null, '/media/image.png'),
).toBe('/media/image.png');
});
it('should resolve path from public folder when in editorial workflow for collection with no public folder', () => {
it('should resolve path from collection media folder for collection with public folder', () => {
expect(
selectMediaFilePublicPath(
Map({ public_folder: '/media', publish_mode: EDITORIAL_WORKFLOW }),
Map({ name: 'posts', folder: 'posts' }),
'image.png',
),
).toBe('/media/image.png');
});
it('should resolve path from collection media folder when in editorial workflow for collection with public folder', () => {
expect(
selectMediaFilePublicPath(
Map({ public_folder: '/media', publish_mode: EDITORIAL_WORKFLOW }),
Map({ public_folder: '/media' }),
Map({ name: 'posts', folder: 'posts', public_folder: '' }),
'image.png',
),
@ -205,7 +182,7 @@ describe('entries', () => {
it('should handle relative public_folder', () => {
expect(
selectMediaFilePublicPath(
Map({ public_folder: '/media', publish_mode: EDITORIAL_WORKFLOW }),
Map({ public_folder: '/media' }),
Map({ name: 'posts', folder: 'posts', public_folder: '../../static/media/' }),
'image.png',
),

View File

@ -7,7 +7,7 @@ import mediaLibrary, {
} from '../mediaLibrary';
jest.mock('uuid/v4');
jest.mock('Reducers/editorialWorkflow');
jest.mock('Reducers/entries');
jest.mock('Reducers');
describe('mediaLibrary', () => {
@ -43,10 +43,10 @@ describe('mediaLibrary', () => {
);
});
it('should select draft media files when editing a workflow draft', () => {
const { selectEditingWorkflowDraft } = require('Reducers/editorialWorkflow');
it('should select draft media files when editing a draft', () => {
const { selectEditingDraft } = require('Reducers/entries');
selectEditingWorkflowDraft.mockReturnValue(true);
selectEditingDraft.mockReturnValue(true);
const state = {
entryDraft: fromJS({ entry: { mediaFiles: [{ id: 1 }] } }),
@ -55,10 +55,10 @@ describe('mediaLibrary', () => {
expect(selectMediaFiles(state)).toEqual([{ key: 1, id: 1 }]);
});
it('should select global media files when not editing a workflow draft', () => {
const { selectEditingWorkflowDraft } = require('Reducers/editorialWorkflow');
it('should select global media files when not editing a draft', () => {
const { selectEditingDraft } = require('Reducers/entries');
selectEditingWorkflowDraft.mockReturnValue(false);
selectEditingDraft.mockReturnValue(false);
const state = {
mediaLibrary: Map({ files: [{ id: 1 }] }),
@ -80,9 +80,9 @@ describe('mediaLibrary', () => {
});
it('should return media file by path', () => {
const { selectEditingWorkflowDraft } = require('Reducers/editorialWorkflow');
const { selectEditingDraft } = require('Reducers/entries');
selectEditingWorkflowDraft.mockReturnValue(false);
selectEditingDraft.mockReturnValue(false);
const state = {
mediaLibrary: Map({ files: [{ id: 1, path: 'path' }] }),

View File

@ -36,11 +36,6 @@ const collections = (state = null, action: CollectionsAction) => {
}
};
enum ListMethod {
ENTRIES_BY_FOLDER = 'entriesByFolder',
ENTRIES_BY_FILES = 'entriesByFiles',
}
const selectors = {
[FOLDER]: {
entryExtension(collection: Collection) {
@ -65,9 +60,6 @@ const selectors = {
return slug;
},
listMethod() {
return ListMethod.ENTRIES_BY_FOLDER;
},
allowNewEntries(collection: Collection) {
return collection.get('create');
},
@ -102,16 +94,13 @@ const selectors = {
const files = collection.get('files');
return files && files.find(f => f?.get('file') === path).get('label');
},
listMethod() {
return ListMethod.ENTRIES_BY_FILES;
},
allowNewEntries() {
return false;
},
allowDeletion(collection: Collection) {
return collection.get('delete', false);
},
templateName(collection: Collection, slug: string) {
templateName(_collection: Collection, slug: string) {
return slug;
},
},
@ -127,8 +116,6 @@ export const selectEntryPath = (collection: Collection, slug: string) =>
selectors[collection.get('type')].entryPath(collection, slug);
export const selectEntrySlug = (collection: Collection, path: string) =>
selectors[collection.get('type')].entrySlug(collection, path);
export const selectListMethod = (collection: Collection) =>
selectors[collection.get('type')].listMethod();
export const selectAllowNewEntries = (collection: Collection) =>
selectors[collection.get('type')].allowNewEntries(collection);
export const selectAllowDeletion = (collection: Collection) =>

View File

@ -1,6 +1,7 @@
import { Map } from 'immutable';
import { CONFIG_REQUEST, CONFIG_SUCCESS, CONFIG_FAILURE, CONFIG_MERGE } from '../actions/config';
import { Config, ConfigAction } from '../types/redux';
import { EDITORIAL_WORKFLOW } from '../constants/publishModes';
const config = (state = Map({ isFetching: true }), action: ConfigAction) => {
switch (action.type) {
@ -24,4 +25,7 @@ const config = (state = Map({ isFetching: true }), action: ConfigAction) => {
export const selectLocale = (state: Config) => state.get('locale', 'en') as string;
export const selectUseWorkflow = (state: Config) =>
state.get('publish_mode') === EDITORIAL_WORKFLOW;
export default config;

View File

@ -1,6 +1,6 @@
import { Map, List, fromJS } from 'immutable';
import { startsWith } from 'lodash';
import { EDITORIAL_WORKFLOW } from 'Constants/publishModes';
import { EDITORIAL_WORKFLOW } from '../constants/publishModes';
import {
UNPUBLISHED_ENTRY_REQUEST,
UNPUBLISHED_ENTRY_REDIRECT,
@ -16,32 +16,33 @@ import {
UNPUBLISHED_ENTRY_PUBLISH_SUCCESS,
UNPUBLISHED_ENTRY_PUBLISH_FAILURE,
UNPUBLISHED_ENTRY_DELETE_SUCCESS,
} from 'Actions/editorialWorkflow';
import { CONFIG_SUCCESS } from 'Actions/config';
} from '../actions/editorialWorkflow';
import { CONFIG_SUCCESS } from '../actions/config';
import { EditorialWorkflowAction, EditorialWorkflow, Entities } from '../types/redux';
const unpublishedEntries = (state = Map(), action) => {
const unpublishedEntries = (state = Map(), action: EditorialWorkflowAction) => {
switch (action.type) {
case CONFIG_SUCCESS: {
const publishMode = action.payload && action.payload.get('publish_mode');
if (publishMode === EDITORIAL_WORKFLOW) {
// Editorial workflow state is explicetelly initiated after the config.
// Editorial workflow state is explicitly initiated after the config.
return Map({ entities: Map(), pages: Map() });
}
return state;
}
case UNPUBLISHED_ENTRY_REQUEST:
return state.setIn(
['entities', `${action.payload.collection}.${action.payload.slug}`, 'isFetching'],
['entities', `${action.payload!.collection}.${action.payload!.slug}`, 'isFetching'],
true,
);
case UNPUBLISHED_ENTRY_REDIRECT:
return state.deleteIn(['entities', `${action.payload.collection}.${action.payload.slug}`]);
return state.deleteIn(['entities', `${action.payload!.collection}.${action.payload!.slug}`]);
case UNPUBLISHED_ENTRY_SUCCESS:
return state.setIn(
['entities', `${action.payload.collection}.${action.payload.entry.slug}`],
fromJS(action.payload.entry),
['entities', `${action.payload!.collection}.${action.payload!.entry.slug}`],
fromJS(action.payload!.entry),
);
case UNPUBLISHED_ENTRIES_REQUEST:
@ -49,7 +50,7 @@ const unpublishedEntries = (state = Map(), action) => {
case UNPUBLISHED_ENTRIES_SUCCESS:
return state.withMutations(map => {
action.payload.entries.forEach(entry =>
action.payload!.entries.forEach(entry =>
map.setIn(
['entities', `${entry.collection}.${entry.slug}`],
fromJS(entry).set('isFetching', false),
@ -58,35 +59,38 @@ const unpublishedEntries = (state = Map(), action) => {
map.set(
'pages',
Map({
...action.payload.pages,
ids: List(action.payload.entries.map(entry => entry.slug)),
...action.payload!.pages,
ids: List(action.payload!.entries.map(entry => entry.slug)),
}),
);
});
case UNPUBLISHED_ENTRY_PERSIST_REQUEST:
case UNPUBLISHED_ENTRY_PERSIST_REQUEST: {
// Update Optimistically
return state.withMutations(map => {
map.setIn(
['entities', `${action.payload.collection}.${action.payload.entry.get('slug')}`],
fromJS(action.payload.entry),
['entities', `${action.payload!.collection}.${action.payload!.entry.get('slug')}`],
fromJS(action.payload!.entry),
);
map.setIn(
[
'entities',
`${action.payload.collection}.${action.payload.entry.get('slug')}`,
`${action.payload!.collection}.${action.payload!.entry.get('slug')}`,
'isPersisting',
],
true,
);
map.updateIn(['pages', 'ids'], List(), list => list.push(action.payload.entry.get('slug')));
map.updateIn(['pages', 'ids'], List(), list =>
list.push(action.payload!.entry.get('slug')),
);
});
}
case UNPUBLISHED_ENTRY_PERSIST_SUCCESS:
// Update Optimistically
return state.deleteIn([
'entities',
`${action.payload.collection}.${action.payload.slug}`,
`${action.payload!.collection}.${action.payload!.slug}`,
'isPersisting',
]);
@ -94,11 +98,16 @@ const unpublishedEntries = (state = Map(), action) => {
// Update Optimistically
return state.withMutations(map => {
map.setIn(
['entities', `${action.payload.collection}.${action.payload.slug}`, 'metaData', 'status'],
action.payload.newStatus,
[
'entities',
`${action.payload!.collection}.${action.payload!.slug}`,
'metaData',
'status',
],
action.payload!.newStatus,
);
map.setIn(
['entities', `${action.payload.collection}.${action.payload.slug}`, 'isUpdatingStatus'],
['entities', `${action.payload!.collection}.${action.payload!.slug}`, 'isUpdatingStatus'],
true,
);
});
@ -106,55 +115,49 @@ const unpublishedEntries = (state = Map(), action) => {
case UNPUBLISHED_ENTRY_STATUS_CHANGE_SUCCESS:
case UNPUBLISHED_ENTRY_STATUS_CHANGE_FAILURE:
return state.setIn(
['entities', `${action.payload.collection}.${action.payload.slug}`, 'isUpdatingStatus'],
['entities', `${action.payload!.collection}.${action.payload!.slug}`, 'isUpdatingStatus'],
false,
);
case UNPUBLISHED_ENTRY_PUBLISH_REQUEST:
return state.setIn(
['entities', `${action.payload.collection}.${action.payload.slug}`, 'isPublishing'],
['entities', `${action.payload!.collection}.${action.payload!.slug}`, 'isPublishing'],
true,
);
case UNPUBLISHED_ENTRY_PUBLISH_SUCCESS:
case UNPUBLISHED_ENTRY_PUBLISH_FAILURE:
return state.withMutations(map => {
map.deleteIn(['entities', `${action.payload.collection}.${action.payload.slug}`]);
map.deleteIn(['entities', `${action.payload!.collection}.${action.payload!.slug}`]);
});
case UNPUBLISHED_ENTRY_DELETE_SUCCESS:
return state.deleteIn(['entities', `${action.payload.collection}.${action.payload.slug}`]);
return state.deleteIn(['entities', `${action.payload!.collection}.${action.payload!.slug}`]);
default:
return state;
}
};
export const selectUnpublishedEntry = (state, collection, slug) =>
state && state.getIn(['entities', `${collection}.${slug}`]);
export const selectUnpublishedEntry = (
state: EditorialWorkflow,
collection: string,
slug: string,
) => state && state.getIn(['entities', `${collection}.${slug}`]);
export const selectUnpublishedEntriesByStatus = (state, status) => {
export const selectUnpublishedEntriesByStatus = (state: EditorialWorkflow, status: string) => {
if (!state) return null;
return state
.get('entities')
.filter(entry => entry.getIn(['metaData', 'status']) === status)
.valueSeq();
const entities = state.get('entities') as Entities;
return entities.filter(entry => entry.getIn(['metaData', 'status']) === status).valueSeq();
};
export const selectUnpublishedSlugs = (state, collection) => {
export const selectUnpublishedSlugs = (state: EditorialWorkflow, collection: string) => {
if (!state.get('entities')) return null;
return state
.get('entities')
.filter((v, k) => startsWith(k, `${collection}.`))
const entities = state.get('entities') as Entities;
return entities
.filter((_v, k) => startsWith(k as string, `${collection}.`))
.map(entry => entry.get('slug'))
.valueSeq();
};
export const selectEditingWorkflowDraft = state => {
const entry = state.entryDraft.get('entry');
const useWorkflow = state.config.get('publish_mode') === EDITORIAL_WORKFLOW;
const workflowDraft = entry && !entry.isEmpty() && useWorkflow;
return workflowDraft;
};
export default unpublishedEntries;

View File

@ -23,9 +23,9 @@ import {
EntryFailurePayload,
EntryDeletePayload,
EntriesRequestPayload,
EntryDraft,
} from '../types/redux';
import { isAbsolutePath, basename } from 'netlify-cms-lib-util/src';
import { EDITORIAL_WORKFLOW } from '../constants/publishModes';
let collection: string;
let loadedEntries: EntryObject[];
@ -144,8 +144,7 @@ export const selectMediaFolder = (
) => {
let mediaFolder = config.get('media_folder');
const useWorkflow = config.get('publish_mode') === EDITORIAL_WORKFLOW;
if (useWorkflow && collection && collection.has('media_folder')) {
if (collection && collection.has('media_folder')) {
if (entryPath) {
const entryDir = dirname(entryPath);
mediaFolder = join(entryDir, collection.get('media_folder') as string);
@ -189,12 +188,17 @@ export const selectMediaFilePublicPath = (
let publicFolder = config.get('public_folder');
const useWorkflow = config.get('publish_mode') === EDITORIAL_WORKFLOW;
if (useWorkflow && collection && collection.has('public_folder')) {
if (collection && collection.has('public_folder')) {
publicFolder = collection.get('public_folder') as string;
}
return join(publicFolder, basename(mediaPath));
};
export const selectEditingDraft = (state: EntryDraft) => {
const entry = state.get('entry');
const workflowDraft = entry && !entry.isEmpty();
return workflowDraft;
};
export default entries;

View File

@ -18,11 +18,29 @@ import {
MEDIA_DISPLAY_URL_REQUEST,
MEDIA_DISPLAY_URL_SUCCESS,
MEDIA_DISPLAY_URL_FAILURE,
} from 'Actions/mediaLibrary';
import { selectEditingWorkflowDraft } from 'Reducers/editorialWorkflow';
import { selectIntegration } from 'Reducers';
} from '../actions/mediaLibrary';
import { selectEditingDraft } from './entries';
import { selectIntegration } from './';
import {
State,
MediaLibraryAction,
MediaLibraryInstance,
MediaFile,
MediaFileMap,
DisplayURLState,
} from '../types/redux';
const defaultState = {
const defaultState: {
isVisible: boolean;
showMediaButton: boolean;
controlMedia: Map<string, string>;
displayURLs: Map<string, string>;
externalLibrary?: MediaLibraryInstance;
controlID?: string;
page?: number;
files?: MediaFile[];
config: Map<string, string>;
} = {
isVisible: false,
showMediaButton: true,
controlMedia: Map(),
@ -30,7 +48,7 @@ const defaultState = {
config: Map(),
};
const mediaLibrary = (state = Map(defaultState), action) => {
const mediaLibrary = (state = Map(defaultState), action: MediaLibraryAction) => {
switch (action.type) {
case MEDIA_LIBRARY_CREATE:
return state.withMutations(map => {
@ -104,7 +122,7 @@ const mediaLibrary = (state = Map(defaultState), action) => {
map.set('dynamicSearchQuery', dynamicSearchQuery);
map.set('dynamicSearchActive', !!dynamicSearchQuery);
if (page && page > 1) {
const updatedFiles = map.get('files').concat(filesWithKeys);
const updatedFiles = (map.get('files') as MediaFile[]).concat(filesWithKeys);
map.set('files', updatedFiles);
} else {
map.set('files', filesWithKeys);
@ -128,7 +146,8 @@ const mediaLibrary = (state = Map(defaultState), action) => {
}
return state.withMutations(map => {
const fileWithKey = { ...file, key: uuid() };
const updatedFiles = [fileWithKey, ...map.get('files')];
const files = map.get('files') as MediaFile[];
const updatedFiles = [fileWithKey, ...files];
map.set('files', updatedFiles);
map.set('isPersisting', false);
});
@ -149,9 +168,8 @@ const mediaLibrary = (state = Map(defaultState), action) => {
return state;
}
return state.withMutations(map => {
const updatedFiles = map
.get('files')
.filter(file => (key ? file.key !== key : file.id !== id));
const files = map.get('files') as MediaFile[];
const updatedFiles = files.filter(file => (key ? file.key !== key : file.id !== id));
map.set('files', updatedFiles);
map.deleteIn(['displayURLs', id]);
map.set('isDeleting', false);
@ -191,17 +209,17 @@ const mediaLibrary = (state = Map(defaultState), action) => {
}
};
export function selectMediaFiles(state) {
export function selectMediaFiles(state: State) {
const { mediaLibrary, entryDraft } = state;
const workflowDraft = selectEditingWorkflowDraft(state);
const editingDraft = selectEditingDraft(state.entryDraft);
const integration = selectIntegration(state, null, 'assetStore');
let files;
if (workflowDraft && !integration) {
files = entryDraft
.getIn(['entry', 'mediaFiles'], List())
.toJS()
.map(file => ({ key: file.id, ...file }));
if (editingDraft && !integration) {
const entryFiles = entryDraft
.getIn(['entry', 'mediaFiles'], List<MediaFileMap>())
.toJS() as MediaFile[];
files = entryFiles.map(file => ({ key: file.id, ...file }));
} else {
files = mediaLibrary.get('files') || [];
}
@ -209,14 +227,17 @@ export function selectMediaFiles(state) {
return files;
}
export function selectMediaFileByPath(state, path) {
export function selectMediaFileByPath(state: State, path: string) {
const files = selectMediaFiles(state);
const file = files.find(file => file.path === path);
return file;
}
export function selectMediaDisplayURL(state, id) {
const displayUrlState = state.mediaLibrary.getIn(['displayURLs', id], Map());
export function selectMediaDisplayURL(state: State, id: string) {
const displayUrlState = state.mediaLibrary.getIn(
['displayURLs', id],
(Map() as unknown) as DisplayURLState,
);
return displayUrlState;
}