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:
committed by
Shawn Erquhart
parent
4ff5bc2ee0
commit
6f221ab3c1
@ -78,7 +78,7 @@ describe('mediaLibrary', () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should not persist media in editorial workflow', () => {
|
||||
it('should not persist media when editing draft', () => {
|
||||
const { getBlobSHA } = require('netlify-cms-lib-util');
|
||||
|
||||
getBlobSHA.mockReturnValue('000000000000000');
|
||||
@ -88,7 +88,6 @@ describe('mediaLibrary', () => {
|
||||
|
||||
const store = mockStore({
|
||||
config: Map({
|
||||
publish_mode: 'editorial_workflow',
|
||||
media_folder: 'static/media',
|
||||
}),
|
||||
collections: Map({
|
||||
@ -132,52 +131,7 @@ describe('mediaLibrary', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should persist media when not in editorial workflow', () => {
|
||||
const { sanitizeSlug } = require('../../lib/urlHelper');
|
||||
sanitizeSlug.mockReturnValue('name.png');
|
||||
|
||||
const store = mockStore({
|
||||
config: Map({
|
||||
media_folder: 'static/media',
|
||||
}),
|
||||
collections: Map({
|
||||
posts: Map({ name: 'posts' }),
|
||||
}),
|
||||
integrations: Map(),
|
||||
mediaLibrary: Map({
|
||||
files: List(),
|
||||
}),
|
||||
entryDraft: Map({
|
||||
entry: Map({ isPersisting: false, collection: 'posts' }),
|
||||
}),
|
||||
});
|
||||
|
||||
const file = new File([''], 'name.png');
|
||||
const assetProxy = { path: 'static/media/name.png' };
|
||||
createAssetProxy.mockReturnValue(assetProxy);
|
||||
|
||||
return store.dispatch(persistMedia(file)).then(() => {
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toHaveLength(3);
|
||||
expect(actions[0]).toEqual({ type: 'MEDIA_PERSIST_REQUEST' });
|
||||
expect(actions[1]).toEqual({
|
||||
type: 'ADD_ASSET',
|
||||
payload: { path: 'static/media/name.png' },
|
||||
});
|
||||
expect(actions[2]).toEqual({
|
||||
type: 'MEDIA_PERSIST_SUCCESS',
|
||||
payload: {
|
||||
file: { id: 'id' },
|
||||
},
|
||||
});
|
||||
|
||||
expect(backend.persistMedia).toHaveBeenCalledTimes(1);
|
||||
expect(backend.persistMedia).toHaveBeenCalledWith(store.getState().config, assetProxy);
|
||||
});
|
||||
});
|
||||
|
||||
it('should persist media when draft is empty', () => {
|
||||
it('should persist media when not editing draft', () => {
|
||||
const store = mockStore({
|
||||
config: Map({
|
||||
media_folder: 'static/media',
|
||||
|
@ -7,7 +7,7 @@ import { getIntegrationProvider } from '../integrations';
|
||||
import { selectIntegration, selectPublishedSlugs } from '../reducers';
|
||||
import { selectFields } from '../reducers/collections';
|
||||
import { selectCollectionEntriesCursor } from '../reducers/cursors';
|
||||
import { Cursor } from 'netlify-cms-lib-util';
|
||||
import { Cursor, ImplementationMediaFile } from 'netlify-cms-lib-util';
|
||||
import { createEntry, EntryValue } from '../valueObjects/Entry';
|
||||
import AssetProxy, { createAssetProxy } from '../valueObjects/AssetProxy';
|
||||
import ValidationErrorTypes from '../constants/validationErrorTypes';
|
||||
@ -23,7 +23,7 @@ import {
|
||||
} from '../types/redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { AnyAction, Dispatch } from 'redux';
|
||||
import { waitForMediaLibraryToLoad } from './mediaLibrary';
|
||||
import { waitForMediaLibraryToLoad, loadMedia } from './mediaLibrary';
|
||||
import { waitUntil } from './waitUntil';
|
||||
|
||||
const { notifSend } = notifActions;
|
||||
@ -108,7 +108,7 @@ export function entriesLoaded(
|
||||
collection: Collection,
|
||||
entries: EntryValue[],
|
||||
pagination: number | null,
|
||||
cursor: typeof Cursor,
|
||||
cursor: Cursor,
|
||||
append = true,
|
||||
) {
|
||||
return {
|
||||
@ -261,7 +261,7 @@ export function loadLocalBackup() {
|
||||
};
|
||||
}
|
||||
|
||||
export function addDraftEntryMediaFile(file: MediaFile) {
|
||||
export function addDraftEntryMediaFile(file: ImplementationMediaFile) {
|
||||
return { type: ADD_DRAFT_ENTRY_MEDIA_FILE, payload: file };
|
||||
}
|
||||
|
||||
@ -270,7 +270,7 @@ export function removeDraftEntryMediaFile({ id }: { id: string }) {
|
||||
}
|
||||
|
||||
export function persistLocalBackup(entry: EntryMap, collection: Collection) {
|
||||
return (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
return (_dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
const state = getState();
|
||||
const backend = currentBackend(state.config);
|
||||
|
||||
@ -309,7 +309,7 @@ export function retrieveLocalBackup(collection: Collection, slug: string) {
|
||||
}
|
||||
|
||||
export function deleteLocalBackup(collection: Collection, slug: string) {
|
||||
return (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
return (_dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
const state = getState();
|
||||
const backend = currentBackend(state.config);
|
||||
return backend.deleteLocalDraftBackup(collection, slug);
|
||||
@ -351,7 +351,7 @@ const appendActions = fromJS({
|
||||
['append_next']: { action: 'next', append: true },
|
||||
});
|
||||
|
||||
const addAppendActionsToCursor = (cursor: typeof Cursor) => {
|
||||
const addAppendActionsToCursor = (cursor: Cursor) => {
|
||||
return Cursor.create(cursor).updateStore('actions', (actions: Set<string>) => {
|
||||
return actions.union(
|
||||
appendActions
|
||||
@ -393,11 +393,11 @@ export function loadEntries(collection: Collection, page = 0) {
|
||||
})
|
||||
: Cursor.create(response.cursor),
|
||||
}))
|
||||
.then((response: { cursor: typeof Cursor; pagination: number; entries: EntryValue[] }) =>
|
||||
.then((response: { cursor: Cursor; pagination: number; entries: EntryValue[] }) =>
|
||||
dispatch(
|
||||
entriesLoaded(
|
||||
collection,
|
||||
response.cursor.meta.get('usingOldPaginationAPI')
|
||||
response.cursor.meta!.get('usingOldPaginationAPI')
|
||||
? response.entries.reverse()
|
||||
: response.entries,
|
||||
response.pagination,
|
||||
@ -422,8 +422,8 @@ export function loadEntries(collection: Collection, page = 0) {
|
||||
};
|
||||
}
|
||||
|
||||
function traverseCursor(backend: Backend, cursor: typeof Cursor, action: string) {
|
||||
if (!cursor.actions.has(action)) {
|
||||
function traverseCursor(backend: Backend, cursor: Cursor, action: string) {
|
||||
if (!cursor.actions!.has(action)) {
|
||||
throw new Error(`The current cursor does not support the pagination action "${action}".`);
|
||||
}
|
||||
return backend.traverseCursor(cursor, action);
|
||||
@ -445,8 +445,8 @@ export function traverseCollectionCursor(collection: Collection, action: string)
|
||||
|
||||
// Handle cursors representing pages in the old, integer-based
|
||||
// pagination API
|
||||
if (cursor.meta.get('usingOldPaginationAPI', false)) {
|
||||
return dispatch(loadEntries(collection, cursor.data.get('nextPage')));
|
||||
if (cursor.meta!.get('usingOldPaginationAPI', false)) {
|
||||
return dispatch(loadEntries(collection, cursor.data!.get('nextPage') as number));
|
||||
}
|
||||
|
||||
try {
|
||||
@ -625,6 +625,10 @@ export function persistEntry(collection: Collection) {
|
||||
dismissAfter: 4000,
|
||||
}),
|
||||
);
|
||||
// re-load media library if entry had media files
|
||||
if (assetProxies.length > 0) {
|
||||
dispatch(loadMedia());
|
||||
}
|
||||
dispatch(entryPersisted(collection, serializedEntry, slug));
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import AssetProxy, { createAssetProxy } from '../valueObjects/AssetProxy';
|
||||
import { Collection, State, MediaFile } from '../types/redux';
|
||||
import { Collection, State } from '../types/redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { AnyAction } from 'redux';
|
||||
import { isAbsolutePath } from 'netlify-cms-lib-util';
|
||||
@ -49,7 +49,7 @@ export function getAsset({ collection, entryPath, path }: GetAssetArgs) {
|
||||
} else {
|
||||
// load asset url from backend
|
||||
await waitForMediaLibraryToLoad(dispatch, getState());
|
||||
const file: MediaFile | null = selectMediaFileByPath(state, resolvedPath);
|
||||
const file = selectMediaFileByPath(state, resolvedPath);
|
||||
|
||||
if (file) {
|
||||
const url = await getMediaDisplayURL(dispatch, getState(), file);
|
||||
|
@ -1,20 +1,22 @@
|
||||
import { Map } from 'immutable';
|
||||
import { actions as notifActions } from 'redux-notifications';
|
||||
import { getBlobSHA } from 'netlify-cms-lib-util';
|
||||
import { getBlobSHA, ImplementationMediaFile } from 'netlify-cms-lib-util';
|
||||
import { currentBackend } from '../backend';
|
||||
import AssetProxy, { createAssetProxy } from '../valueObjects/AssetProxy';
|
||||
import { selectIntegration } from '../reducers';
|
||||
import { selectMediaFilePath, selectMediaFilePublicPath } from '../reducers/entries';
|
||||
import {
|
||||
selectMediaFilePath,
|
||||
selectMediaFilePublicPath,
|
||||
selectEditingDraft,
|
||||
} from '../reducers/entries';
|
||||
import { selectMediaDisplayURL, selectMediaFiles } from '../reducers/mediaLibrary';
|
||||
import { getIntegrationProvider } from '../integrations';
|
||||
import { addAsset, removeAsset } from './media';
|
||||
import { addDraftEntryMediaFile, removeDraftEntryMediaFile } from './entries';
|
||||
import { sanitizeSlug } from '../lib/urlHelper';
|
||||
import { State, MediaFile, DisplayURLState } from '../types/redux';
|
||||
import { State, MediaFile, DisplayURLState, MediaLibraryInstance } from '../types/redux';
|
||||
import { AnyAction } from 'redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { MediaLibraryInstance } from '../mediaLibrary';
|
||||
import { selectEditingWorkflowDraft } from '../reducers/editorialWorkflow';
|
||||
import { waitUntilWithTimeout } from './waitUntil';
|
||||
|
||||
const { notifSend } = notifActions;
|
||||
@ -49,7 +51,7 @@ export function createMediaLibrary(instance: MediaLibraryInstance) {
|
||||
}
|
||||
|
||||
export function clearMediaControl(id: string) {
|
||||
return (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
return (_dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
const state = getState();
|
||||
const mediaLibrary = state.mediaLibrary.get('externalLibrary');
|
||||
if (mediaLibrary) {
|
||||
@ -59,7 +61,7 @@ export function clearMediaControl(id: string) {
|
||||
}
|
||||
|
||||
export function removeMediaControl(id: string) {
|
||||
return (dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
return (_dispatch: ThunkDispatch<State, {}, AnyAction>, getState: () => State) => {
|
||||
const state = getState();
|
||||
const mediaLibrary = state.mediaLibrary.get('externalLibrary');
|
||||
if (mediaLibrary) {
|
||||
@ -150,7 +152,7 @@ export function loadMedia(
|
||||
resolve(
|
||||
backend
|
||||
.getMedia()
|
||||
.then((files: MediaFile[]) => dispatch(mediaLoaded(files)))
|
||||
.then(files => dispatch(mediaLoaded(files)))
|
||||
.catch((error: { status?: number }) => {
|
||||
console.error(error);
|
||||
if (error.status === 404) {
|
||||
@ -177,7 +179,7 @@ function createMediaFileFromAsset({
|
||||
file: File;
|
||||
assetProxy: AssetProxy;
|
||||
draft: boolean;
|
||||
}): MediaFile {
|
||||
}): ImplementationMediaFile {
|
||||
const mediaFile = {
|
||||
id,
|
||||
name: file.name,
|
||||
@ -200,7 +202,7 @@ export function persistMedia(file: File, opts: MediaOptions = {}) {
|
||||
const fileName = sanitizeSlug(file.name.toLowerCase(), state.config.get('slug'));
|
||||
const existingFile = files.find(existingFile => existingFile.name.toLowerCase() === fileName);
|
||||
|
||||
const editingDraft = selectEditingWorkflowDraft(state);
|
||||
const editingDraft = selectEditingDraft(state.entryDraft);
|
||||
|
||||
/**
|
||||
* Check for existing files of the same name before persisting. If no asset
|
||||
@ -255,7 +257,7 @@ export function persistMedia(file: File, opts: MediaOptions = {}) {
|
||||
|
||||
dispatch(addAsset(assetProxy));
|
||||
|
||||
let mediaFile: MediaFile;
|
||||
let mediaFile: ImplementationMediaFile;
|
||||
if (integration) {
|
||||
const id = await getBlobSHA(file);
|
||||
// integration assets are persisted immediately, thus draft is false
|
||||
@ -314,7 +316,7 @@ export function deleteMedia(file: MediaFile, opts: MediaOptions = {}) {
|
||||
dispatch(removeAsset(file.path));
|
||||
dispatch(removeDraftEntryMediaFile({ id: file.id }));
|
||||
} else {
|
||||
const editingDraft = selectEditingWorkflowDraft(state);
|
||||
const editingDraft = selectEditingDraft(state.entryDraft);
|
||||
|
||||
dispatch(mediaDeleting());
|
||||
dispatch(removeAsset(file.path));
|
||||
@ -395,7 +397,7 @@ interface MediaOptions {
|
||||
privateUpload?: boolean;
|
||||
}
|
||||
|
||||
export function mediaLoaded(files: MediaFile[], opts: MediaOptions = {}) {
|
||||
export function mediaLoaded(files: ImplementationMediaFile[], opts: MediaOptions = {}) {
|
||||
return {
|
||||
type: MEDIA_LOAD_SUCCESS,
|
||||
payload: { files, ...opts },
|
||||
@ -411,7 +413,7 @@ export function mediaPersisting() {
|
||||
return { type: MEDIA_PERSIST_REQUEST };
|
||||
}
|
||||
|
||||
export function mediaPersisted(file: MediaFile, opts: MediaOptions = {}) {
|
||||
export function mediaPersisted(file: ImplementationMediaFile, opts: MediaOptions = {}) {
|
||||
const { privateUpload } = opts;
|
||||
return {
|
||||
type: MEDIA_PERSIST_SUCCESS,
|
||||
|
@ -3,10 +3,10 @@ import { List } from 'immutable';
|
||||
import { stripIndent } from 'common-tags';
|
||||
import * as fuzzy from 'fuzzy';
|
||||
import { resolveFormat } from './formats/formats';
|
||||
import { selectUseWorkflow } from './reducers/config';
|
||||
import { selectMediaFilePath, selectMediaFolder } from './reducers/entries';
|
||||
import { selectIntegration } from './reducers/integrations';
|
||||
import {
|
||||
selectListMethod,
|
||||
selectEntrySlug,
|
||||
selectEntryPath,
|
||||
selectFileEntryLabel,
|
||||
@ -24,8 +24,16 @@ import {
|
||||
Cursor,
|
||||
CURSOR_COMPATIBILITY_SYMBOL,
|
||||
EditorialWorkflowError,
|
||||
Implementation as BackendImplementation,
|
||||
DisplayURL,
|
||||
ImplementationEntry,
|
||||
ImplementationMediaFile,
|
||||
Credentials,
|
||||
User,
|
||||
getPathDepth,
|
||||
Config as ImplementationConfig,
|
||||
} from 'netlify-cms-lib-util';
|
||||
import { EDITORIAL_WORKFLOW, status } from './constants/publishModes';
|
||||
import { status } from './constants/publishModes';
|
||||
import {
|
||||
SLUG_MISSING_REQUIRED_DATE,
|
||||
compileStringTemplate,
|
||||
@ -38,16 +46,14 @@ import {
|
||||
EntryMap,
|
||||
Config,
|
||||
SlugConfig,
|
||||
DisplayURL,
|
||||
FilterRule,
|
||||
Collections,
|
||||
MediaFile,
|
||||
EntryDraft,
|
||||
CollectionFile,
|
||||
State,
|
||||
} from './types/redux';
|
||||
import AssetProxy from './valueObjects/AssetProxy';
|
||||
import { selectEditingWorkflowDraft } from './reducers/editorialWorkflow';
|
||||
import { FOLDER, FILES } from './constants/collectionTypes';
|
||||
|
||||
export class LocalStorageAuthStore {
|
||||
storageKey = 'netlify-cms-user';
|
||||
@ -153,87 +159,6 @@ function createPreviewUrl(
|
||||
return `${basePath}/${previewPath}`;
|
||||
}
|
||||
|
||||
interface ImplementationInitOptions {
|
||||
useWorkflow: boolean;
|
||||
updateUserCredentials: (credentials: Credentials) => void;
|
||||
initialWorkflowStatus: string;
|
||||
}
|
||||
|
||||
interface ImplementationEntry {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
data: any;
|
||||
file: { path: string; label: string };
|
||||
metaData: { collection: string };
|
||||
isModification?: boolean;
|
||||
slug: string;
|
||||
mediaFiles: MediaFile[];
|
||||
}
|
||||
|
||||
interface Implementation {
|
||||
authComponent: () => void;
|
||||
restoreUser: (user: User) => Promise<User>;
|
||||
init: (config: Config, options: ImplementationInitOptions) => Implementation;
|
||||
authenticate: (credentials: Credentials) => Promise<User>;
|
||||
logout: () => Promise<void>;
|
||||
getToken: () => Promise<string>;
|
||||
unpublishedEntry?: (collection: Collection, slug: string) => Promise<ImplementationEntry>;
|
||||
getEntry: (collection: Collection, slug: string, path: string) => Promise<ImplementationEntry>;
|
||||
allEntriesByFolder?: (
|
||||
collection: Collection,
|
||||
extension: string,
|
||||
) => Promise<ImplementationEntry[]>;
|
||||
traverseCursor: (
|
||||
cursor: typeof Cursor,
|
||||
action: unknown,
|
||||
) => Promise<{ entries: ImplementationEntry[]; cursor: typeof Cursor }>;
|
||||
entriesByFolder: (collection: Collection, extension: string) => Promise<ImplementationEntry[]>;
|
||||
entriesByFiles: (collection: Collection, extension: string) => Promise<ImplementationEntry[]>;
|
||||
unpublishedEntries: () => Promise<ImplementationEntry[]>;
|
||||
getMediaDisplayURL?: (displayURL: DisplayURL) => Promise<string>;
|
||||
getMedia: (folder?: string) => Promise<MediaFile[]>;
|
||||
getMediaFile: (path: string) => Promise<MediaFile>;
|
||||
getDeployPreview: (
|
||||
collection: Collection,
|
||||
slug: string,
|
||||
) => Promise<{ url: string; status: string }>;
|
||||
|
||||
persistEntry: (
|
||||
obj: { path: string; slug: string; raw: string },
|
||||
assetProxies: AssetProxy[],
|
||||
opts: {
|
||||
newEntry: boolean;
|
||||
parsedData: { title: string; description: string };
|
||||
commitMessage: string;
|
||||
collectionName: string;
|
||||
useWorkflow: boolean;
|
||||
unpublished: boolean;
|
||||
status?: string;
|
||||
},
|
||||
) => Promise<void>;
|
||||
persistMedia: (file: AssetProxy, opts: { commitMessage: string }) => Promise<MediaFile>;
|
||||
deleteFile: (
|
||||
path: string,
|
||||
commitMessage: string,
|
||||
opts?: { collection: Collection; slug: string },
|
||||
) => Promise<void>;
|
||||
updateUnpublishedEntryStatus: (
|
||||
collection: string,
|
||||
slug: string,
|
||||
newStatus: string,
|
||||
) => Promise<void>;
|
||||
publishUnpublishedEntry: (collection: string, slug: string) => Promise<void>;
|
||||
deleteUnpublishedEntry: (collection: string, slug: string) => Promise<void>;
|
||||
}
|
||||
|
||||
type Credentials = {};
|
||||
|
||||
interface User {
|
||||
backendName: string;
|
||||
login: string;
|
||||
name: string;
|
||||
useOpenAuthoring: boolean;
|
||||
}
|
||||
|
||||
interface AuthStore {
|
||||
retrieve: () => User;
|
||||
store: (user: User) => void;
|
||||
@ -246,18 +171,10 @@ interface BackendOptions {
|
||||
config?: Config;
|
||||
}
|
||||
|
||||
interface BackupMediaFile extends MediaFile {
|
||||
file?: File;
|
||||
}
|
||||
|
||||
export interface ImplementationMediaFile extends MediaFile {
|
||||
file?: File;
|
||||
}
|
||||
|
||||
interface BackupEntry {
|
||||
raw: string;
|
||||
path: string;
|
||||
mediaFiles: BackupMediaFile[];
|
||||
mediaFiles: ImplementationMediaFile[];
|
||||
}
|
||||
|
||||
interface PersistArgs {
|
||||
@ -270,6 +187,16 @@ interface PersistArgs {
|
||||
status?: string;
|
||||
}
|
||||
|
||||
interface ImplementationInitOptions {
|
||||
useWorkflow: boolean;
|
||||
updateUserCredentials: (credentials: Credentials) => void;
|
||||
initialWorkflowStatus: string;
|
||||
}
|
||||
|
||||
type Implementation = BackendImplementation & {
|
||||
init: (config: ImplementationConfig, options: ImplementationInitOptions) => Implementation;
|
||||
};
|
||||
|
||||
export class Backend {
|
||||
implementation: Implementation;
|
||||
backendName: string;
|
||||
@ -284,8 +211,8 @@ export class Backend {
|
||||
// We can't reliably run this on exit, so we do cleanup on load.
|
||||
this.deleteAnonymousBackup();
|
||||
this.config = config as Config;
|
||||
this.implementation = implementation.init(this.config, {
|
||||
useWorkflow: this.config.get('publish_mode') === EDITORIAL_WORKFLOW,
|
||||
this.implementation = implementation.init(this.config.toJS(), {
|
||||
useWorkflow: selectUseWorkflow(this.config),
|
||||
updateUserCredentials: this.updateUserCredentials,
|
||||
initialWorkflowStatus: status.first(),
|
||||
});
|
||||
@ -300,12 +227,12 @@ export class Backend {
|
||||
if (this.user) {
|
||||
return this.user;
|
||||
}
|
||||
const stored = this.authStore?.retrieve();
|
||||
const stored = this.authStore!.retrieve();
|
||||
if (stored && stored.backendName === this.backendName) {
|
||||
return Promise.resolve(this.implementation.restoreUser(stored)).then(user => {
|
||||
this.user = { ...user, backendName: this.backendName };
|
||||
// return confirmed/rehydrated user object instead of stored
|
||||
this.authStore?.store(this.user);
|
||||
this.authStore!.store(this.user as User);
|
||||
return this.user;
|
||||
});
|
||||
}
|
||||
@ -313,10 +240,10 @@ export class Backend {
|
||||
}
|
||||
|
||||
updateUserCredentials = (updatedCredentials: Credentials) => {
|
||||
const storedUser = this.authStore?.retrieve();
|
||||
const storedUser = this.authStore!.retrieve();
|
||||
if (storedUser && storedUser.backendName === this.backendName) {
|
||||
this.user = { ...storedUser, ...updatedCredentials };
|
||||
this.authStore?.store(this.user as User);
|
||||
this.authStore!.store(this.user as User);
|
||||
return this.user;
|
||||
}
|
||||
};
|
||||
@ -346,10 +273,10 @@ export class Backend {
|
||||
|
||||
getToken = () => this.implementation.getToken();
|
||||
|
||||
async entryExist(collection: Collection, path: string, slug: string) {
|
||||
async entryExist(collection: Collection, path: string, slug: string, useWorkflow: boolean) {
|
||||
const unpublishedEntry =
|
||||
this.implementation.unpublishedEntry &&
|
||||
(await this.implementation.unpublishedEntry(collection, slug).catch(error => {
|
||||
useWorkflow &&
|
||||
(await this.implementation.unpublishedEntry(collection.get('name'), slug).catch(error => {
|
||||
if (error instanceof EditorialWorkflowError && error.notUnderEditorialWorkflow) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
@ -359,7 +286,7 @@ export class Backend {
|
||||
if (unpublishedEntry) return unpublishedEntry;
|
||||
|
||||
const publishedEntry = await this.implementation
|
||||
.getEntry(collection, slug, path)
|
||||
.getEntry(path)
|
||||
.then(({ data }) => data)
|
||||
.catch(() => {
|
||||
return Promise.resolve(false);
|
||||
@ -371,9 +298,10 @@ export class Backend {
|
||||
async generateUniqueSlug(
|
||||
collection: Collection,
|
||||
entryData: EntryMap,
|
||||
slugConfig: SlugConfig,
|
||||
config: Config,
|
||||
usedSlugs: List<string>,
|
||||
) {
|
||||
const slugConfig = config.get('slug');
|
||||
const slug: string = slugFormatter(collection, entryData, slugConfig);
|
||||
let i = 1;
|
||||
let uniqueSlug = slug;
|
||||
@ -385,6 +313,7 @@ export class Backend {
|
||||
collection,
|
||||
selectEntryPath(collection, uniqueSlug) as string,
|
||||
uniqueSlug,
|
||||
selectUseWorkflow(config),
|
||||
))
|
||||
) {
|
||||
uniqueSlug = `${slug}${sanitizeChar(' ', slugConfig)}${i++}`;
|
||||
@ -411,24 +340,42 @@ export class Backend {
|
||||
}
|
||||
|
||||
listEntries(collection: Collection) {
|
||||
const listMethod = this.implementation[selectListMethod(collection)];
|
||||
const extension = selectFolderEntryExtension(collection);
|
||||
return listMethod
|
||||
.call(this.implementation, collection, extension)
|
||||
.then((loadedEntries: ImplementationEntry[]) => ({
|
||||
entries: this.processEntries(loadedEntries, collection),
|
||||
/*
|
||||
let listMethod: () => Promise<ImplementationEntry[]>;
|
||||
const collectionType = collection.get('type');
|
||||
if (collectionType === FOLDER) {
|
||||
listMethod = () =>
|
||||
this.implementation.entriesByFolder(
|
||||
collection.get('folder') as string,
|
||||
extension,
|
||||
getPathDepth(collection.get('path', '') as string),
|
||||
);
|
||||
} else if (collectionType === FILES) {
|
||||
const files = collection
|
||||
.get('files')!
|
||||
.map(collectionFile => ({
|
||||
path: collectionFile!.get('file'),
|
||||
label: collectionFile!.get('label'),
|
||||
}))
|
||||
.toArray();
|
||||
listMethod = () => this.implementation.entriesByFiles(files);
|
||||
} else {
|
||||
throw new Error(`Unknown collection type: ${collectionType}`);
|
||||
}
|
||||
return listMethod().then((loadedEntries: ImplementationEntry[]) => ({
|
||||
entries: this.processEntries(loadedEntries, collection),
|
||||
/*
|
||||
Wrap cursors so we can tell which collection the cursor is
|
||||
from. This is done to prevent traverseCursor from requiring a
|
||||
`collection` argument.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
||||
// @ts-ignore
|
||||
cursor: Cursor.create(loadedEntries[CURSOR_COMPATIBILITY_SYMBOL]).wrapData({
|
||||
cursorType: 'collectionEntries',
|
||||
collection,
|
||||
}),
|
||||
}));
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
|
||||
// @ts-ignore
|
||||
cursor: Cursor.create(loadedEntries[CURSOR_COMPATIBILITY_SYMBOL]).wrapData({
|
||||
cursorType: 'collectionEntries',
|
||||
collection,
|
||||
}),
|
||||
}));
|
||||
}
|
||||
|
||||
// The same as listEntries, except that if a cursor with the "next"
|
||||
@ -440,14 +387,18 @@ export class Backend {
|
||||
if (collection.get('folder') && this.implementation.allEntriesByFolder) {
|
||||
const extension = selectFolderEntryExtension(collection);
|
||||
return this.implementation
|
||||
.allEntriesByFolder(collection, extension)
|
||||
.allEntriesByFolder(
|
||||
collection.get('folder') as string,
|
||||
extension,
|
||||
getPathDepth(collection.get('path', '') as string),
|
||||
)
|
||||
.then(entries => this.processEntries(entries, collection));
|
||||
}
|
||||
|
||||
const response = await this.listEntries(collection);
|
||||
const { entries } = response;
|
||||
let { cursor } = response;
|
||||
while (cursor && cursor.actions.includes('next')) {
|
||||
while (cursor && cursor.actions!.includes('next')) {
|
||||
const { entries: newEntries, cursor: newCursor } = await this.traverseCursor(cursor, 'next');
|
||||
entries.push(...newEntries);
|
||||
cursor = newCursor;
|
||||
@ -513,19 +464,19 @@ export class Backend {
|
||||
return { query: searchTerm, hits };
|
||||
}
|
||||
|
||||
traverseCursor(cursor: typeof Cursor, action: string) {
|
||||
traverseCursor(cursor: Cursor, action: string) {
|
||||
const [data, unwrappedCursor] = cursor.unwrapData();
|
||||
// TODO: stop assuming all cursors are for collections
|
||||
const collection: Collection = data.get('collection');
|
||||
return this.implementation
|
||||
.traverseCursor(unwrappedCursor, action)
|
||||
.then(async ({ entries, cursor: newCursor }) => ({
|
||||
const collection = data.get('collection') as Collection;
|
||||
return this.implementation!.traverseCursor!(unwrappedCursor, action).then(
|
||||
async ({ entries, cursor: newCursor }) => ({
|
||||
entries: this.processEntries(entries, collection),
|
||||
cursor: Cursor.create(newCursor).wrapData({
|
||||
cursorType: 'collectionEntries',
|
||||
collection,
|
||||
}),
|
||||
}));
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async getLocalDraftBackup(collection: Collection, slug: string) {
|
||||
@ -560,14 +511,14 @@ export class Backend {
|
||||
return;
|
||||
}
|
||||
|
||||
const mediaFiles = await Promise.all<BackupMediaFile>(
|
||||
const mediaFiles = await Promise.all<ImplementationMediaFile>(
|
||||
entry
|
||||
.get('mediaFiles')
|
||||
.toJS()
|
||||
.map(async (file: MediaFile) => {
|
||||
.map(async (file: ImplementationMediaFile) => {
|
||||
// make sure to serialize the file
|
||||
if (file.url?.startsWith('blob:')) {
|
||||
const blob = await fetch(file.url).then(res => res.blob());
|
||||
const blob = await fetch(file.url as string).then(res => res.blob());
|
||||
return { ...file, file: new File([blob], file.name) };
|
||||
}
|
||||
return file;
|
||||
@ -598,14 +549,13 @@ export class Backend {
|
||||
const path = selectEntryPath(collection, slug) as string;
|
||||
const label = selectFileEntryLabel(collection, slug);
|
||||
|
||||
const workflowDraft = selectEditingWorkflowDraft(state);
|
||||
const integration = selectIntegration(state.integrations, null, 'assetStore');
|
||||
|
||||
const [loadedEntry, mediaFiles] = await Promise.all([
|
||||
this.implementation.getEntry(collection, slug, path),
|
||||
workflowDraft && !integration
|
||||
this.implementation.getEntry(path),
|
||||
collection.has('media_folder') && !integration
|
||||
? this.implementation.getMedia(selectMediaFolder(state.config, collection, path))
|
||||
: Promise.resolve([]),
|
||||
: Promise.resolve(state.mediaLibrary.get('files') || []),
|
||||
]);
|
||||
|
||||
const entry = createEntry(collection.get('name'), slug, loadedEntry.file.path, {
|
||||
@ -649,17 +599,15 @@ export class Backend {
|
||||
}
|
||||
|
||||
unpublishedEntries(collections: Collections) {
|
||||
return this.implementation
|
||||
.unpublishedEntries()
|
||||
.then(loadedEntries => loadedEntries.filter(entry => entry !== null))
|
||||
return this.implementation.unpublishedEntries!()
|
||||
.then(entries =>
|
||||
entries.map(loadedEntry => {
|
||||
const collectionName = loadedEntry.metaData.collection;
|
||||
const collectionName = loadedEntry.metaData!.collection;
|
||||
const collection = collections.find(c => c.get('name') === collectionName);
|
||||
const entry = createEntry(collectionName, loadedEntry.slug, loadedEntry.file.path, {
|
||||
raw: loadedEntry.data,
|
||||
isModification: loadedEntry.isModification,
|
||||
label: selectFileEntryLabel(collection, loadedEntry.slug),
|
||||
label: selectFileEntryLabel(collection, loadedEntry.slug!),
|
||||
});
|
||||
entry.metaData = loadedEntry.metaData;
|
||||
return entry;
|
||||
@ -678,8 +626,7 @@ export class Backend {
|
||||
}
|
||||
|
||||
unpublishedEntry(collection: Collection, slug: string) {
|
||||
return this.implementation
|
||||
.unpublishedEntry?.(collection, slug)
|
||||
return this.implementation!.unpublishedEntry!(collection.get('name') as string, slug)
|
||||
.then(loadedEntry => {
|
||||
const entry = createEntry(collection.get('name'), loadedEntry.slug, loadedEntry.file.path, {
|
||||
raw: loadedEntry.data,
|
||||
@ -741,7 +688,7 @@ export class Backend {
|
||||
count = 0;
|
||||
while (!deployPreview && count < maxAttempts) {
|
||||
count++;
|
||||
deployPreview = await this.implementation.getDeployPreview(collection, slug);
|
||||
deployPreview = await this.implementation.getDeployPreview(collection.get('name'), slug);
|
||||
if (!deployPreview) {
|
||||
await new Promise(resolve => setTimeout(() => resolve(), interval));
|
||||
}
|
||||
@ -795,7 +742,7 @@ export class Backend {
|
||||
const slug = await this.generateUniqueSlug(
|
||||
collection,
|
||||
entryDraft.getIn(['entry', 'data']),
|
||||
config.get('slug'),
|
||||
config,
|
||||
usedSlugs,
|
||||
);
|
||||
const path = selectEntryPath(collection, slug) as string;
|
||||
@ -836,7 +783,7 @@ export class Backend {
|
||||
user.useOpenAuthoring,
|
||||
);
|
||||
|
||||
const useWorkflow = config.get('publish_mode') === EDITORIAL_WORKFLOW;
|
||||
const useWorkflow = selectUseWorkflow(config);
|
||||
|
||||
const collectionName = collection.get('name');
|
||||
|
||||
@ -892,7 +839,7 @@ export class Backend {
|
||||
},
|
||||
user.useOpenAuthoring,
|
||||
);
|
||||
return this.implementation.deleteFile(path, commitMessage, { collection, slug });
|
||||
return this.implementation.deleteFile(path, commitMessage);
|
||||
}
|
||||
|
||||
async deleteMedia(config: Config, path: string) {
|
||||
@ -917,15 +864,15 @@ export class Backend {
|
||||
}
|
||||
|
||||
updateUnpublishedEntryStatus(collection: string, slug: string, newStatus: string) {
|
||||
return this.implementation.updateUnpublishedEntryStatus(collection, slug, newStatus);
|
||||
return this.implementation.updateUnpublishedEntryStatus!(collection, slug, newStatus);
|
||||
}
|
||||
|
||||
publishUnpublishedEntry(collection: string, slug: string) {
|
||||
return this.implementation.publishUnpublishedEntry(collection, slug);
|
||||
return this.implementation.publishUnpublishedEntry!(collection, slug);
|
||||
}
|
||||
|
||||
deleteUnpublishedEntry(collection: string, slug: string) {
|
||||
return this.implementation.deleteUnpublishedEntry(collection, slug);
|
||||
return this.implementation.deleteUnpublishedEntry!(collection, slug);
|
||||
}
|
||||
|
||||
entryToRaw(collection: Collection, entry: EntryMap): string {
|
||||
@ -939,13 +886,13 @@ export class Backend {
|
||||
if (fields) {
|
||||
return collection
|
||||
.get('fields')
|
||||
.map(f => f?.get('name'))
|
||||
.map(f => f!.get('name'))
|
||||
.toArray();
|
||||
}
|
||||
|
||||
const files = collection.get('files');
|
||||
const file = (files || List<CollectionFile>())
|
||||
.filter(f => f?.get('name') === entry.get('slug'))
|
||||
.filter(f => f!.get('name') === entry.get('slug'))
|
||||
.get(0);
|
||||
|
||||
if (file == null) {
|
||||
@ -953,7 +900,7 @@ export class Backend {
|
||||
}
|
||||
return file
|
||||
.get('fields')
|
||||
.map(f => f?.get('name'))
|
||||
.map(f => f!.get('name'))
|
||||
.toArray();
|
||||
}
|
||||
|
||||
@ -976,10 +923,11 @@ export function resolveBackend(config: Config) {
|
||||
|
||||
const authStore = new LocalStorageAuthStore();
|
||||
|
||||
if (!getBackend(name)) {
|
||||
const backend = getBackend(name);
|
||||
if (!backend) {
|
||||
throw new Error(`Backend not found: ${name}`);
|
||||
} else {
|
||||
return new Backend(getBackend(name), { backendName: name, authStore, config });
|
||||
return new Backend(backend, { backendName: name, authStore, config });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -130,7 +130,7 @@ class App extends React.Component {
|
||||
siteId: this.props.config.getIn(['backend', 'site_domain']),
|
||||
base_url: this.props.config.getIn(['backend', 'base_url'], null),
|
||||
authEndpoint: this.props.config.getIn(['backend', 'auth_endpoint']),
|
||||
config: this.props.config,
|
||||
config: this.props.config.toJS(),
|
||||
clearHash: () => history.replace('/'),
|
||||
t,
|
||||
})}
|
||||
|
@ -6,6 +6,7 @@ import { once } from 'lodash';
|
||||
import { getMediaLibrary } from './lib/registry';
|
||||
import store from './redux';
|
||||
import { createMediaLibrary, insertMedia } from './actions/mediaLibrary';
|
||||
import { MediaLibraryInstance } from './types/redux';
|
||||
|
||||
type MediaLibraryOptions = {};
|
||||
|
||||
@ -16,14 +17,6 @@ interface MediaLibrary {
|
||||
}) => MediaLibraryInstance;
|
||||
}
|
||||
|
||||
export interface MediaLibraryInstance {
|
||||
show?: () => void;
|
||||
hide?: () => void;
|
||||
onClearControl?: (args: { id: string }) => void;
|
||||
onRemoveControl?: (args: { id: string }) => void;
|
||||
enableStandalone?: () => boolean;
|
||||
}
|
||||
|
||||
const initializeMediaLibrary = once(async function initializeMediaLibrary(name, options) {
|
||||
const lib = (getMediaLibrary(name) as unknown) as MediaLibrary;
|
||||
const handleInsert = (url: string) => store.dispatch(insertMedia(url));
|
||||
|
@ -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',
|
||||
),
|
||||
|
@ -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' }] }),
|
||||
|
@ -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) =>
|
||||
|
@ -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;
|
||||
|
@ -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;
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -21,4 +21,11 @@ export interface StaticallyTypedRecord<T> {
|
||||
some<K extends keyof T>(predicate: (value: T[K], key: K, iter: this) => boolean): boolean;
|
||||
mapKeys<K extends keyof T, V>(mapFunc: (key: K, value: StaticallyTypedRecord<T>) => V): V[];
|
||||
find<K extends keyof T>(findFunc: (value: T[K]) => boolean): T[K];
|
||||
filter<K extends keyof T>(
|
||||
predicate: (value: T[K], key: K, iter: this) => boolean,
|
||||
): StaticallyTypedRecord<T>;
|
||||
valueSeq<K extends keyof T>(): T[K][];
|
||||
map<K extends keyof T, V>(
|
||||
mapFunc: (value: T[K]) => V,
|
||||
): StaticallyTypedRecord<{ [key: string]: V }>;
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import { Action } from 'redux';
|
||||
import { StaticallyTypedRecord } from './immutable';
|
||||
import { Map, List } from 'immutable';
|
||||
import AssetProxy from '../valueObjects/AssetProxy';
|
||||
import { ImplementationMediaFile } from 'netlify-cms-lib-util';
|
||||
|
||||
export type SlugConfig = StaticallyTypedRecord<{
|
||||
encoding: string;
|
||||
@ -11,6 +12,17 @@ export type SlugConfig = StaticallyTypedRecord<{
|
||||
|
||||
type BackendObject = {
|
||||
name: string;
|
||||
repo?: string | null;
|
||||
open_authoring?: boolean;
|
||||
branch?: string;
|
||||
api_root?: string;
|
||||
squash_merges?: boolean;
|
||||
use_graphql?: boolean;
|
||||
preview_context?: string;
|
||||
identity_url?: string;
|
||||
gateway_url?: string;
|
||||
large_media_url?: string;
|
||||
use_large_media_transforms_in_media_library?: boolean;
|
||||
};
|
||||
|
||||
type Backend = StaticallyTypedRecord<Backend> & BackendObject;
|
||||
@ -24,6 +36,8 @@ export type Config = StaticallyTypedRecord<{
|
||||
locale?: string;
|
||||
slug: SlugConfig;
|
||||
media_folder_relative?: boolean;
|
||||
base_url?: string;
|
||||
site_id?: string;
|
||||
site_url?: string;
|
||||
show_preview_links?: boolean;
|
||||
}>;
|
||||
@ -36,7 +50,7 @@ type Pages = StaticallyTypedRecord<PagesObject>;
|
||||
|
||||
type EntitiesObject = { [key: string]: EntryMap };
|
||||
|
||||
type Entities = StaticallyTypedRecord<EntitiesObject>;
|
||||
export type Entities = StaticallyTypedRecord<EntitiesObject>;
|
||||
|
||||
export type Entries = StaticallyTypedRecord<{
|
||||
pages: Pages & PagesObject;
|
||||
@ -45,7 +59,10 @@ export type Entries = StaticallyTypedRecord<{
|
||||
|
||||
export type Deploys = StaticallyTypedRecord<{}>;
|
||||
|
||||
export type EditorialWorkflow = StaticallyTypedRecord<{}>;
|
||||
export type EditorialWorkflow = StaticallyTypedRecord<{
|
||||
pages: Pages & PagesObject;
|
||||
entities: Entities & EntitiesObject;
|
||||
}>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type EntryObject = {
|
||||
@ -56,6 +73,7 @@ export type EntryObject = {
|
||||
collection: string;
|
||||
mediaFiles: List<MediaFileMap>;
|
||||
newRecord: boolean;
|
||||
metaData: { status: string };
|
||||
};
|
||||
|
||||
export type EntryMap = StaticallyTypedRecord<EntryObject>;
|
||||
@ -120,7 +138,7 @@ export type Collections = StaticallyTypedRecord<{ [path: string]: Collection & C
|
||||
|
||||
export type Medias = StaticallyTypedRecord<{ [path: string]: AssetProxy | undefined }>;
|
||||
|
||||
interface MediaLibraryInstance {
|
||||
export interface MediaLibraryInstance {
|
||||
show: (args: {
|
||||
id?: string;
|
||||
value?: string;
|
||||
@ -136,23 +154,17 @@ interface MediaLibraryInstance {
|
||||
|
||||
export type DisplayURL = { id: string; path: string } | string;
|
||||
|
||||
export interface MediaFile {
|
||||
name: string;
|
||||
id: string;
|
||||
size?: number;
|
||||
displayURL?: DisplayURL;
|
||||
path: string;
|
||||
draft?: boolean;
|
||||
url?: string;
|
||||
}
|
||||
export type MediaFile = ImplementationMediaFile & { key?: string };
|
||||
|
||||
export type MediaFileMap = StaticallyTypedRecord<MediaFile>;
|
||||
|
||||
export type DisplayURLState = StaticallyTypedRecord<{
|
||||
type DisplayURLStateObject = {
|
||||
isFetching: boolean;
|
||||
url?: string;
|
||||
err?: Error;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type DisplayURLState = StaticallyTypedRecord<DisplayURLStateObject>;
|
||||
|
||||
interface DisplayURLsObject {
|
||||
[id: string]: DisplayURLState;
|
||||
@ -262,3 +274,43 @@ export interface EntriesAction extends Action<string> {
|
||||
export interface CollectionsAction extends Action<string> {
|
||||
payload?: StaticallyTypedRecord<{ collections: List<Collection> }>;
|
||||
}
|
||||
|
||||
export interface EditorialWorkflowAction extends Action<string> {
|
||||
payload?: StaticallyTypedRecord<{ publish_mode: string }> & {
|
||||
collection: string;
|
||||
entry: { slug: string };
|
||||
} & {
|
||||
collection: string;
|
||||
slug: string;
|
||||
} & {
|
||||
pages: [];
|
||||
entries: { collection: string; slug: string }[];
|
||||
} & {
|
||||
collection: string;
|
||||
entry: StaticallyTypedRecord<{ slug: string }>;
|
||||
} & {
|
||||
collection: string;
|
||||
slug: string;
|
||||
newStatus: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MediaLibraryAction extends Action<string> {
|
||||
payload: MediaLibraryInstance & {
|
||||
controlID: string;
|
||||
forImage: boolean;
|
||||
privateUpload: boolean;
|
||||
config: Map<string, string>;
|
||||
} & { mediaPath: string | string[] } & { page: number } & {
|
||||
files: MediaFile[];
|
||||
page: number;
|
||||
canPaginate: boolean;
|
||||
dynamicSearch: boolean;
|
||||
dynamicSearchQuery: boolean;
|
||||
} & {
|
||||
file: MediaFile;
|
||||
privateUpload: boolean;
|
||||
} & {
|
||||
file: { id: string; key: string; privateUpload: boolean };
|
||||
} & { key: string } & { url: string } & { err: Error };
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { isBoolean } from 'lodash';
|
||||
import { ImplementationMediaFile } from '../backend';
|
||||
import { ImplementationMediaFile } from 'netlify-cms-lib-util';
|
||||
|
||||
interface Options {
|
||||
partial?: boolean;
|
||||
|
Reference in New Issue
Block a user