refactor: monorepo setup with lerna (#243)
This commit is contained in:
committed by
GitHub
parent
dac29fbf3c
commit
504d95c34f
46
packages/core/src/reducers/auth.ts
Normal file
46
packages/core/src/reducers/auth.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { produce } from 'immer';
|
||||
|
||||
import {
|
||||
AUTH_REQUEST,
|
||||
AUTH_SUCCESS,
|
||||
AUTH_FAILURE,
|
||||
AUTH_REQUEST_DONE,
|
||||
LOGOUT,
|
||||
} from '../actions/auth';
|
||||
|
||||
import type { User } from '../interface';
|
||||
import type { AuthAction } from '../actions/auth';
|
||||
|
||||
export type AuthState = {
|
||||
isFetching: boolean;
|
||||
user: User | undefined;
|
||||
error: string | undefined;
|
||||
};
|
||||
|
||||
export const defaultState: AuthState = {
|
||||
isFetching: false,
|
||||
user: undefined,
|
||||
error: undefined,
|
||||
};
|
||||
|
||||
const auth = produce((state: AuthState, action: AuthAction) => {
|
||||
switch (action.type) {
|
||||
case AUTH_REQUEST:
|
||||
state.isFetching = true;
|
||||
break;
|
||||
case AUTH_SUCCESS:
|
||||
state.user = action.payload;
|
||||
break;
|
||||
case AUTH_FAILURE:
|
||||
state.error = action.payload && action.payload.toString();
|
||||
break;
|
||||
case AUTH_REQUEST_DONE:
|
||||
state.isFetching = false;
|
||||
break;
|
||||
case LOGOUT:
|
||||
state.user = undefined;
|
||||
state.isFetching = false;
|
||||
}
|
||||
}, defaultState);
|
||||
|
||||
export default auth;
|
32
packages/core/src/reducers/collections.ts
Normal file
32
packages/core/src/reducers/collections.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { CONFIG_SUCCESS } from '../actions/config';
|
||||
|
||||
import type { ConfigAction } from '../actions/config';
|
||||
import type { Collection, Collections } from '../interface';
|
||||
import type { RootState } from '../store';
|
||||
|
||||
export type CollectionsState = Collections;
|
||||
|
||||
const defaultState: CollectionsState = {};
|
||||
|
||||
function collections(
|
||||
state: CollectionsState = defaultState,
|
||||
action: ConfigAction,
|
||||
): CollectionsState {
|
||||
switch (action.type) {
|
||||
case CONFIG_SUCCESS: {
|
||||
const collections = action.payload.collections;
|
||||
return collections.reduce((acc, collection) => {
|
||||
acc[collection.name] = collection as Collection;
|
||||
return acc;
|
||||
}, {} as Record<string, Collection>);
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default collections;
|
||||
|
||||
export const selectCollection = (collectionName: string) => (state: RootState) => {
|
||||
return Object.values(state.collections).find(collection => collection.name === collectionName);
|
||||
};
|
13
packages/core/src/reducers/combinedReducer.ts
Normal file
13
packages/core/src/reducers/combinedReducer.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { combineReducers } from 'redux';
|
||||
|
||||
import snackbarReducer from '../store/slices/snackbars';
|
||||
import reducers from './index';
|
||||
|
||||
function createRootReducer() {
|
||||
return combineReducers({
|
||||
...reducers,
|
||||
snackbar: snackbarReducer,
|
||||
});
|
||||
}
|
||||
|
||||
export default createRootReducer;
|
39
packages/core/src/reducers/config.ts
Normal file
39
packages/core/src/reducers/config.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { produce } from 'immer';
|
||||
|
||||
import { CONFIG_FAILURE, CONFIG_REQUEST, CONFIG_SUCCESS } from '../actions/config';
|
||||
|
||||
import type { ConfigAction } from '../actions/config';
|
||||
import type { Config } from '../interface';
|
||||
|
||||
export interface ConfigState {
|
||||
config?: Config;
|
||||
isFetching: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const defaultState: ConfigState = {
|
||||
isFetching: true,
|
||||
};
|
||||
|
||||
const config = produce((state: ConfigState, action: ConfigAction) => {
|
||||
switch (action.type) {
|
||||
case CONFIG_REQUEST:
|
||||
state.isFetching = true;
|
||||
break;
|
||||
case CONFIG_SUCCESS:
|
||||
return {
|
||||
config: action.payload,
|
||||
isFetching: false,
|
||||
error: undefined,
|
||||
};
|
||||
case CONFIG_FAILURE:
|
||||
state.isFetching = false;
|
||||
state.error = action.payload.toString();
|
||||
}
|
||||
}, defaultState);
|
||||
|
||||
export function selectLocale(state?: Config) {
|
||||
return state?.locale || 'en';
|
||||
}
|
||||
|
||||
export default config;
|
60
packages/core/src/reducers/cursors.ts
Normal file
60
packages/core/src/reducers/cursors.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import {
|
||||
ENTRIES_SUCCESS,
|
||||
FILTER_ENTRIES_SUCCESS,
|
||||
GROUP_ENTRIES_SUCCESS,
|
||||
SORT_ENTRIES_SUCCESS,
|
||||
} from '../actions/entries';
|
||||
import { Cursor } from '../lib/util';
|
||||
|
||||
import type { EntriesAction } from '../actions/entries';
|
||||
import type { CursorStore } from '../lib/util/Cursor';
|
||||
|
||||
export interface CursorsState {
|
||||
cursorsByType: {
|
||||
collectionEntries: Record<string, CursorStore>;
|
||||
};
|
||||
}
|
||||
|
||||
function cursors(
|
||||
state: CursorsState = { cursorsByType: { collectionEntries: {} } },
|
||||
action: EntriesAction,
|
||||
): CursorsState {
|
||||
switch (action.type) {
|
||||
case ENTRIES_SUCCESS: {
|
||||
return {
|
||||
cursorsByType: {
|
||||
collectionEntries: {
|
||||
...state.cursorsByType.collectionEntries,
|
||||
[action.payload.collection]: Cursor.create(action.payload.cursor).store,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
case FILTER_ENTRIES_SUCCESS:
|
||||
case GROUP_ENTRIES_SUCCESS:
|
||||
case SORT_ENTRIES_SUCCESS: {
|
||||
const newCollectionEntries = {
|
||||
...state.cursorsByType.collectionEntries,
|
||||
};
|
||||
|
||||
delete newCollectionEntries[action.payload.collection];
|
||||
|
||||
return {
|
||||
cursorsByType: {
|
||||
collectionEntries: newCollectionEntries,
|
||||
},
|
||||
};
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
// Since pagination can be used for a variety of views (collections
|
||||
// and searches are the most common examples), we namespace cursors by
|
||||
// their type before storing them in the state.
|
||||
export function selectCollectionEntriesCursor(state: CursorsState, collectionName: string) {
|
||||
return new Cursor(state.cursorsByType.collectionEntries[collectionName]);
|
||||
}
|
||||
|
||||
export default cursors;
|
717
packages/core/src/reducers/entries.ts
Normal file
717
packages/core/src/reducers/entries.ts
Normal file
@ -0,0 +1,717 @@
|
||||
import get from 'lodash/get';
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import once from 'lodash/once';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
|
||||
import {
|
||||
CHANGE_VIEW_STYLE,
|
||||
ENTRIES_FAILURE,
|
||||
ENTRIES_REQUEST,
|
||||
ENTRIES_SUCCESS,
|
||||
ENTRY_DELETE_SUCCESS,
|
||||
ENTRY_FAILURE,
|
||||
ENTRY_REQUEST,
|
||||
ENTRY_SUCCESS,
|
||||
FILTER_ENTRIES_FAILURE,
|
||||
FILTER_ENTRIES_REQUEST,
|
||||
FILTER_ENTRIES_SUCCESS,
|
||||
GROUP_ENTRIES_FAILURE,
|
||||
GROUP_ENTRIES_REQUEST,
|
||||
GROUP_ENTRIES_SUCCESS,
|
||||
SORT_ENTRIES_FAILURE,
|
||||
SORT_ENTRIES_REQUEST,
|
||||
SORT_ENTRIES_SUCCESS,
|
||||
} from '../actions/entries';
|
||||
import { SEARCH_ENTRIES_SUCCESS } from '../actions/search';
|
||||
import { SORT_DIRECTION_ASCENDING, SORT_DIRECTION_NONE } from '../constants';
|
||||
import { VIEW_STYLE_LIST } from '../constants/collectionViews';
|
||||
import { set } from '../lib/util/object.util';
|
||||
import { selectSortDataPath } from '../lib/util/sort.util';
|
||||
|
||||
import type { EntriesAction } from '../actions/entries';
|
||||
import type { SearchAction } from '../actions/search';
|
||||
import type { CollectionViewStyle } from '../constants/collectionViews';
|
||||
import type {
|
||||
Collection,
|
||||
Entities,
|
||||
Entry,
|
||||
Filter,
|
||||
FilterMap,
|
||||
Group,
|
||||
GroupMap,
|
||||
GroupOfEntries,
|
||||
Pages,
|
||||
Sort,
|
||||
SortMap,
|
||||
SortObject,
|
||||
} from '../interface';
|
||||
import type { EntryDraftState } from './entryDraft';
|
||||
|
||||
const storageSortKey = '../netlify-cms.entries.sort';
|
||||
const viewStyleKey = '../netlify-cms.entries.viewStyle';
|
||||
type StorageSortObject = SortObject & { index: number };
|
||||
type StorageSort = { [collection: string]: { [key: string]: StorageSortObject } };
|
||||
|
||||
const loadSort = once(() => {
|
||||
const sortString = localStorage.getItem(storageSortKey);
|
||||
if (sortString) {
|
||||
try {
|
||||
const sort: StorageSort = JSON.parse(sortString);
|
||||
const map: Sort = {};
|
||||
Object.entries(sort).forEach(([collection, sort]) => {
|
||||
const orderedMap: SortMap = {};
|
||||
sortBy(Object.values(sort), ['index']).forEach(value => {
|
||||
const { key, direction } = value;
|
||||
orderedMap[key] = { key, direction };
|
||||
});
|
||||
map[collection] = orderedMap;
|
||||
});
|
||||
return map;
|
||||
} catch (e: unknown) {
|
||||
return {} as Sort;
|
||||
}
|
||||
}
|
||||
return {} as Sort;
|
||||
});
|
||||
|
||||
function clearSort() {
|
||||
localStorage.removeItem(storageSortKey);
|
||||
}
|
||||
|
||||
function persistSort(sort: Sort | undefined) {
|
||||
if (sort) {
|
||||
const storageSort: StorageSort = {};
|
||||
Object.keys(sort).forEach(key => {
|
||||
const collection = key as string;
|
||||
const sortObjects = (
|
||||
(sort[collection] ? Object.values(sort[collection]) : []) as SortObject[]
|
||||
).map((value, index) => ({ ...value, index }));
|
||||
|
||||
sortObjects.forEach(value => {
|
||||
set(storageSort, `${collection}.${value.key}`, value);
|
||||
});
|
||||
});
|
||||
localStorage.setItem(storageSortKey, JSON.stringify(storageSort));
|
||||
} else {
|
||||
clearSort();
|
||||
}
|
||||
}
|
||||
|
||||
const loadViewStyle = once(() => {
|
||||
const viewStyle = localStorage.getItem(viewStyleKey) as CollectionViewStyle;
|
||||
if (viewStyle) {
|
||||
return viewStyle;
|
||||
}
|
||||
|
||||
localStorage.setItem(viewStyleKey, VIEW_STYLE_LIST);
|
||||
return VIEW_STYLE_LIST;
|
||||
});
|
||||
|
||||
function clearViewStyle() {
|
||||
localStorage.removeItem(viewStyleKey);
|
||||
}
|
||||
|
||||
function persistViewStyle(viewStyle: string | undefined) {
|
||||
if (viewStyle) {
|
||||
localStorage.setItem(viewStyleKey, viewStyle);
|
||||
} else {
|
||||
clearViewStyle();
|
||||
}
|
||||
}
|
||||
|
||||
export type EntriesState = {
|
||||
pages: Pages;
|
||||
entities: Entities;
|
||||
sort: Sort;
|
||||
filter?: Filter;
|
||||
group?: Group;
|
||||
viewStyle: CollectionViewStyle;
|
||||
};
|
||||
|
||||
function entries(
|
||||
state: EntriesState = { entities: {}, pages: {}, sort: loadSort(), viewStyle: loadViewStyle() },
|
||||
action: EntriesAction | SearchAction,
|
||||
): EntriesState {
|
||||
switch (action.type) {
|
||||
case ENTRY_REQUEST: {
|
||||
const payload = action.payload;
|
||||
|
||||
const key = `${payload.collection}.${payload.slug}`;
|
||||
const newEntity: Entry = {
|
||||
...(state.entities[key] ?? {}),
|
||||
};
|
||||
|
||||
newEntity.isFetching = true;
|
||||
|
||||
return {
|
||||
...state,
|
||||
entities: {
|
||||
...state.entities,
|
||||
[key]: newEntity,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case ENTRY_SUCCESS: {
|
||||
const payload = action.payload;
|
||||
|
||||
return {
|
||||
...state,
|
||||
entities: {
|
||||
...state.entities,
|
||||
[`${payload.collection}.${payload.entry.slug}`]: payload.entry,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case ENTRIES_REQUEST: {
|
||||
const payload = action.payload;
|
||||
|
||||
const pages = {
|
||||
...state.pages,
|
||||
};
|
||||
|
||||
if (payload.collection in pages) {
|
||||
const newCollection = {
|
||||
...(pages[payload.collection] ?? {}),
|
||||
};
|
||||
|
||||
newCollection.isFetching = true;
|
||||
|
||||
pages[payload.collection] = newCollection;
|
||||
}
|
||||
|
||||
return { ...state, pages };
|
||||
}
|
||||
|
||||
case ENTRIES_SUCCESS: {
|
||||
const payload = action.payload;
|
||||
const loadedEntries = payload.entries;
|
||||
const page = payload.page;
|
||||
|
||||
const entities = {
|
||||
...state.entities,
|
||||
};
|
||||
|
||||
loadedEntries.forEach(entry => {
|
||||
entities[`${payload.collection}.${entry.slug}`] = { ...entry, isFetching: false };
|
||||
});
|
||||
|
||||
const pages = {
|
||||
...state.pages,
|
||||
};
|
||||
|
||||
pages[payload.collection] = {
|
||||
page: page ?? undefined,
|
||||
ids: [...(pages[payload.collection]?.ids ?? []), ...loadedEntries.map(entry => entry.slug)],
|
||||
isFetching: false,
|
||||
};
|
||||
|
||||
return { ...state, entities, pages };
|
||||
}
|
||||
|
||||
case ENTRIES_FAILURE: {
|
||||
const pages = {
|
||||
...state.pages,
|
||||
};
|
||||
|
||||
if (action.meta.collection in pages) {
|
||||
const newCollection = {
|
||||
...(pages[action.meta.collection] ?? {}),
|
||||
};
|
||||
|
||||
newCollection.isFetching = false;
|
||||
|
||||
pages[action.meta.collection] = newCollection;
|
||||
}
|
||||
|
||||
return { ...state, pages };
|
||||
}
|
||||
|
||||
case ENTRY_FAILURE: {
|
||||
const payload = action.payload;
|
||||
const key = `${payload.collection}.${payload.slug}`;
|
||||
|
||||
return {
|
||||
...state,
|
||||
entities: {
|
||||
...state.entities,
|
||||
[key]: {
|
||||
...(state.entities[key] ?? {}),
|
||||
isFetching: false,
|
||||
error: payload.error.message,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case SEARCH_ENTRIES_SUCCESS: {
|
||||
const payload = action.payload;
|
||||
const loadedEntries = payload.entries;
|
||||
|
||||
const entities = {
|
||||
...state.entities,
|
||||
};
|
||||
|
||||
loadedEntries.forEach(entry => {
|
||||
entities[`${entry.collection}.${entry.slug}`] = {
|
||||
...entry,
|
||||
isFetching: false,
|
||||
};
|
||||
});
|
||||
|
||||
return { ...state, entities };
|
||||
}
|
||||
|
||||
case ENTRY_DELETE_SUCCESS: {
|
||||
const payload = action.payload;
|
||||
const collection = payload.collectionName;
|
||||
const slug = payload.entrySlug;
|
||||
|
||||
const entities = {
|
||||
...state.entities,
|
||||
};
|
||||
|
||||
delete entities[`${collection}.${slug}`];
|
||||
|
||||
const pages = {
|
||||
...state.pages,
|
||||
};
|
||||
|
||||
const newPagesCollection = {
|
||||
...(pages[collection] ?? {}),
|
||||
};
|
||||
|
||||
if (!newPagesCollection.ids) {
|
||||
newPagesCollection.ids = [];
|
||||
}
|
||||
|
||||
newPagesCollection.ids = newPagesCollection.ids.filter(
|
||||
(id: string) => id !== payload.entrySlug,
|
||||
);
|
||||
|
||||
pages[collection] = newPagesCollection;
|
||||
|
||||
return {
|
||||
...state,
|
||||
entities,
|
||||
pages,
|
||||
};
|
||||
}
|
||||
|
||||
case SORT_ENTRIES_REQUEST: {
|
||||
const payload = action.payload;
|
||||
const { collection, key, direction } = payload;
|
||||
|
||||
const sort = {
|
||||
...state.sort,
|
||||
};
|
||||
|
||||
sort[collection] = { [key]: { key, direction } } as SortMap;
|
||||
|
||||
const pages = {
|
||||
...state.pages,
|
||||
};
|
||||
|
||||
const newPagesCollection = {
|
||||
...(pages[collection] ?? {}),
|
||||
};
|
||||
|
||||
newPagesCollection.isFetching = true;
|
||||
delete newPagesCollection.page;
|
||||
|
||||
pages[collection] = newPagesCollection;
|
||||
|
||||
persistSort(sort);
|
||||
|
||||
return {
|
||||
...state,
|
||||
sort,
|
||||
pages,
|
||||
};
|
||||
}
|
||||
|
||||
case GROUP_ENTRIES_SUCCESS:
|
||||
case FILTER_ENTRIES_SUCCESS:
|
||||
case SORT_ENTRIES_SUCCESS: {
|
||||
const payload = action.payload as { collection: string; entries: Entry[] };
|
||||
const { collection, entries } = payload;
|
||||
|
||||
const entities = {
|
||||
...state.entities,
|
||||
};
|
||||
|
||||
entries.forEach(entry => {
|
||||
entities[`${entry.collection}.${entry.slug}`] = {
|
||||
...entry,
|
||||
isFetching: false,
|
||||
};
|
||||
});
|
||||
|
||||
const pages = {
|
||||
...state.pages,
|
||||
};
|
||||
|
||||
const ids = entries.map(entry => entry.slug);
|
||||
|
||||
pages[collection] = {
|
||||
page: 1,
|
||||
ids,
|
||||
isFetching: false,
|
||||
};
|
||||
|
||||
return {
|
||||
...state,
|
||||
entities,
|
||||
pages,
|
||||
};
|
||||
}
|
||||
|
||||
case SORT_ENTRIES_FAILURE: {
|
||||
const payload = action.payload;
|
||||
const { collection, key } = payload;
|
||||
|
||||
const sort = {
|
||||
...state.sort,
|
||||
};
|
||||
|
||||
const newSortCollection = {
|
||||
...(sort[collection] ?? {}),
|
||||
};
|
||||
|
||||
delete newSortCollection[key];
|
||||
|
||||
sort[collection] = newSortCollection;
|
||||
|
||||
const pages = {
|
||||
...state.pages,
|
||||
};
|
||||
|
||||
const newPagesCollection = {
|
||||
...(pages[collection] ?? {}),
|
||||
};
|
||||
|
||||
newPagesCollection.isFetching = false;
|
||||
delete newPagesCollection.page;
|
||||
|
||||
pages[collection] = newPagesCollection;
|
||||
|
||||
persistSort(sort);
|
||||
|
||||
return {
|
||||
...state,
|
||||
sort,
|
||||
pages,
|
||||
};
|
||||
}
|
||||
|
||||
case FILTER_ENTRIES_REQUEST: {
|
||||
const payload = action.payload;
|
||||
const { collection, filter: viewFilter } = payload;
|
||||
|
||||
const filter = {
|
||||
...state.filter,
|
||||
};
|
||||
|
||||
const newFilterCollection = {
|
||||
...(filter[collection] ?? {}),
|
||||
};
|
||||
|
||||
let newFilter: FilterMap;
|
||||
if (viewFilter.id in newFilterCollection) {
|
||||
newFilter = { ...newFilterCollection[viewFilter.id] };
|
||||
} else {
|
||||
newFilter = { ...viewFilter };
|
||||
}
|
||||
|
||||
newFilter.active = !newFilter.active;
|
||||
newFilterCollection[viewFilter.id] = newFilter;
|
||||
filter[collection] = newFilterCollection;
|
||||
|
||||
return {
|
||||
...state,
|
||||
filter,
|
||||
};
|
||||
}
|
||||
|
||||
case FILTER_ENTRIES_FAILURE: {
|
||||
const payload = action.payload;
|
||||
const { collection, filter: viewFilter } = payload;
|
||||
|
||||
const filter = {
|
||||
...state.filter,
|
||||
};
|
||||
|
||||
const newFilterCollection = {
|
||||
...(filter[collection] ?? {}),
|
||||
};
|
||||
|
||||
delete newFilterCollection[viewFilter.id];
|
||||
filter[collection] = newFilterCollection;
|
||||
|
||||
const pages = {
|
||||
...state.pages,
|
||||
};
|
||||
|
||||
const newPagesCollection = {
|
||||
...(pages[collection] ?? {}),
|
||||
};
|
||||
|
||||
newPagesCollection.isFetching = false;
|
||||
|
||||
pages[collection] = newPagesCollection;
|
||||
|
||||
return {
|
||||
...state,
|
||||
filter,
|
||||
pages,
|
||||
};
|
||||
}
|
||||
|
||||
case GROUP_ENTRIES_REQUEST: {
|
||||
const payload = action.payload;
|
||||
const { collection, group: groupBy } = payload;
|
||||
|
||||
const group = {
|
||||
...state.group,
|
||||
};
|
||||
|
||||
let newGroup: GroupMap;
|
||||
if (group[collection] && groupBy.id in group[collection]) {
|
||||
newGroup = { ...group[collection][groupBy.id] };
|
||||
} else {
|
||||
newGroup = { ...groupBy };
|
||||
}
|
||||
|
||||
newGroup.active = !newGroup.active;
|
||||
group[collection] = {
|
||||
[groupBy.id]: newGroup,
|
||||
};
|
||||
|
||||
return {
|
||||
...state,
|
||||
group,
|
||||
};
|
||||
}
|
||||
|
||||
case GROUP_ENTRIES_FAILURE: {
|
||||
const payload = action.payload;
|
||||
const { collection, group: groupBy } = payload;
|
||||
|
||||
const group = {
|
||||
...state.group,
|
||||
};
|
||||
|
||||
const newGroupCollection = {
|
||||
...(group[collection] ?? {}),
|
||||
};
|
||||
|
||||
delete newGroupCollection[groupBy.id];
|
||||
|
||||
group[collection] = newGroupCollection;
|
||||
|
||||
const pages = {
|
||||
...state.pages,
|
||||
};
|
||||
|
||||
const newPagesCollection = {
|
||||
...(pages[collection] ?? {}),
|
||||
};
|
||||
|
||||
newPagesCollection.isFetching = false;
|
||||
|
||||
pages[collection] = newPagesCollection;
|
||||
|
||||
return {
|
||||
...state,
|
||||
group,
|
||||
pages,
|
||||
};
|
||||
}
|
||||
|
||||
case CHANGE_VIEW_STYLE: {
|
||||
const payload = action.payload;
|
||||
const { style } = payload;
|
||||
persistViewStyle(style);
|
||||
return {
|
||||
...state,
|
||||
viewStyle: style,
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export function selectEntriesSort(entries: EntriesState, collection: string) {
|
||||
const sort = entries.sort as Sort | undefined;
|
||||
return sort?.[collection];
|
||||
}
|
||||
|
||||
export function selectEntriesFilter(entries: EntriesState, collection: string) {
|
||||
const filter = entries.filter as Filter | undefined;
|
||||
return filter?.[collection] || {};
|
||||
}
|
||||
|
||||
export function selectEntriesGroup(entries: EntriesState, collection: string) {
|
||||
const group = entries.group as Group | undefined;
|
||||
return group?.[collection] || {};
|
||||
}
|
||||
|
||||
export function selectEntriesGroupField(entries: EntriesState, collection: string) {
|
||||
const groups = selectEntriesGroup(entries, collection);
|
||||
const value = Object.values(groups ?? {}).find(v => v?.active === true);
|
||||
return value;
|
||||
}
|
||||
|
||||
export function selectEntriesSortFields(entries: EntriesState, collection: string) {
|
||||
const sort = selectEntriesSort(entries, collection);
|
||||
const values = Object.values(sort ?? {}).filter(v => v?.direction !== SORT_DIRECTION_NONE) || [];
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
export function selectEntriesFilterFields(entries: EntriesState, collection: string) {
|
||||
const filter = selectEntriesFilter(entries, collection);
|
||||
const values = Object.values(filter ?? {}).filter(v => v?.active === true) || [];
|
||||
return values;
|
||||
}
|
||||
|
||||
export function selectViewStyle(entries: EntriesState): CollectionViewStyle {
|
||||
return entries.viewStyle;
|
||||
}
|
||||
|
||||
export function selectEntry(state: EntriesState, collection: string, slug: string) {
|
||||
return state.entities[`${collection}.${slug}`];
|
||||
}
|
||||
|
||||
export function selectPublishedSlugs(state: EntriesState, collection: string) {
|
||||
return state.pages[collection]?.ids ?? [];
|
||||
}
|
||||
|
||||
function getPublishedEntries(state: EntriesState, collectionName: string) {
|
||||
const slugs = selectPublishedSlugs(state, collectionName);
|
||||
const entries =
|
||||
slugs && (slugs.map(slug => selectEntry(state, collectionName, slug as string)) as Entry[]);
|
||||
return entries;
|
||||
}
|
||||
|
||||
export function selectEntries(state: EntriesState, collection: Collection) {
|
||||
const collectionName = collection.name;
|
||||
let entries = getPublishedEntries(state, collectionName);
|
||||
|
||||
const sortFields = selectEntriesSortFields(state, collectionName);
|
||||
if (sortFields && sortFields.length > 0) {
|
||||
const keys = sortFields.map(v => selectSortDataPath(collection, v.key));
|
||||
const orders = sortFields.map(v => (v.direction === SORT_DIRECTION_ASCENDING ? 'asc' : 'desc'));
|
||||
entries = orderBy(entries, keys, orders);
|
||||
}
|
||||
|
||||
const filters = selectEntriesFilterFields(state, collectionName);
|
||||
if (filters && filters.length > 0) {
|
||||
entries = entries.filter(e => {
|
||||
const allMatched = filters.every(f => {
|
||||
const pattern = f.pattern;
|
||||
const field = f.field;
|
||||
const data = e!.data || {};
|
||||
const toMatch = get(data, field);
|
||||
const matched = toMatch !== undefined && new RegExp(String(pattern)).test(String(toMatch));
|
||||
return matched;
|
||||
});
|
||||
return allMatched;
|
||||
});
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
function getGroup(entry: Entry, selectedGroup: GroupMap) {
|
||||
const label = selectedGroup.label;
|
||||
const field = selectedGroup.field;
|
||||
|
||||
const fieldData = get(entry.data, field);
|
||||
if (fieldData === undefined) {
|
||||
return {
|
||||
id: 'missing_value',
|
||||
label,
|
||||
value: fieldData,
|
||||
};
|
||||
}
|
||||
|
||||
const dataAsString = String(fieldData);
|
||||
if (selectedGroup.pattern) {
|
||||
const pattern = selectedGroup.pattern;
|
||||
let value = '';
|
||||
try {
|
||||
const regex = new RegExp(pattern);
|
||||
const matched = dataAsString.match(regex);
|
||||
if (matched) {
|
||||
value = matched[0];
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
console.warn(`Invalid view group pattern '${pattern}' for field '${field}'`, e);
|
||||
}
|
||||
return {
|
||||
id: `${label}${value}`,
|
||||
label,
|
||||
value,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: `${label}${fieldData}`,
|
||||
label,
|
||||
value: typeof fieldData === 'boolean' ? fieldData : dataAsString,
|
||||
};
|
||||
}
|
||||
|
||||
export function selectGroups(state: EntriesState, collection: Collection) {
|
||||
const collectionName = collection.name;
|
||||
const entries = getPublishedEntries(state, collectionName);
|
||||
|
||||
const selectedGroup = selectEntriesGroupField(state, collectionName);
|
||||
if (selectedGroup === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let groups: Record<string, { id: string; label: string; value: string | boolean | undefined }> =
|
||||
{};
|
||||
const groupedEntries = groupBy(entries, entry => {
|
||||
const group = getGroup(entry, selectedGroup);
|
||||
groups = { ...groups, [group.id]: group };
|
||||
return group.id;
|
||||
});
|
||||
|
||||
const groupsArray: GroupOfEntries[] = Object.entries(groupedEntries).map(([id, entries]) => {
|
||||
return {
|
||||
...groups[id],
|
||||
paths: new Set(entries.map(entry => entry.path)),
|
||||
};
|
||||
});
|
||||
|
||||
return groupsArray;
|
||||
}
|
||||
|
||||
export function selectEntryByPath(state: EntriesState, collection: string, path: string) {
|
||||
const slugs = selectPublishedSlugs(state, collection);
|
||||
const entries =
|
||||
slugs && (slugs.map(slug => selectEntry(state, collection, slug as string)) as Entry[]);
|
||||
|
||||
return entries && entries.find(e => e?.path === path);
|
||||
}
|
||||
|
||||
export function selectEntriesLoaded(state: EntriesState, collection: string) {
|
||||
return !!state.pages[collection];
|
||||
}
|
||||
|
||||
export function selectIsFetching(state: EntriesState, collection: string) {
|
||||
return state.pages[collection]?.isFetching ?? false;
|
||||
}
|
||||
|
||||
export function selectEditingDraft(state: EntryDraftState) {
|
||||
return state.entry;
|
||||
}
|
||||
|
||||
export default entries;
|
307
packages/core/src/reducers/entryDraft.ts
Normal file
307
packages/core/src/reducers/entryDraft.ts
Normal file
@ -0,0 +1,307 @@
|
||||
import get from 'lodash/get';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import {
|
||||
ADD_DRAFT_ENTRY_MEDIA_FILE,
|
||||
DRAFT_CHANGE_FIELD,
|
||||
DRAFT_CREATE_DUPLICATE_FROM_ENTRY,
|
||||
DRAFT_CREATE_EMPTY,
|
||||
DRAFT_CREATE_FROM_ENTRY,
|
||||
DRAFT_CREATE_FROM_LOCAL_BACKUP,
|
||||
DRAFT_DISCARD,
|
||||
DRAFT_LOCAL_BACKUP_DELETE,
|
||||
DRAFT_LOCAL_BACKUP_RETRIEVED,
|
||||
DRAFT_VALIDATION_ERRORS,
|
||||
ENTRY_DELETE_SUCCESS,
|
||||
ENTRY_PERSIST_FAILURE,
|
||||
ENTRY_PERSIST_REQUEST,
|
||||
ENTRY_PERSIST_SUCCESS,
|
||||
REMOVE_DRAFT_ENTRY_MEDIA_FILE,
|
||||
} from '../actions/entries';
|
||||
import { duplicateI18nFields, getDataPath } from '../lib/i18n';
|
||||
import { set } from '../lib/util/object.util';
|
||||
|
||||
import type { EntriesAction } from '../actions/entries';
|
||||
import type { Entry, FieldsErrors } from '../interface';
|
||||
import type { RootState } from '../store';
|
||||
|
||||
export interface EntryDraftState {
|
||||
original?: Entry;
|
||||
entry?: Entry;
|
||||
fieldsErrors: FieldsErrors;
|
||||
hasChanged: boolean;
|
||||
key: string;
|
||||
localBackup?: {
|
||||
entry: Entry;
|
||||
};
|
||||
}
|
||||
|
||||
const initialState: EntryDraftState = {
|
||||
fieldsErrors: {},
|
||||
hasChanged: false,
|
||||
key: '',
|
||||
};
|
||||
|
||||
function entryDraftReducer(
|
||||
state: EntryDraftState = initialState,
|
||||
action: EntriesAction,
|
||||
): EntryDraftState {
|
||||
switch (action.type) {
|
||||
case DRAFT_CREATE_FROM_ENTRY: {
|
||||
const newState = { ...state };
|
||||
|
||||
const entry: Entry = {
|
||||
...action.payload.entry,
|
||||
newRecord: false,
|
||||
};
|
||||
|
||||
// Existing Entry
|
||||
return {
|
||||
...newState,
|
||||
entry,
|
||||
original: entry,
|
||||
fieldsErrors: {},
|
||||
hasChanged: false,
|
||||
key: uuid(),
|
||||
};
|
||||
}
|
||||
case DRAFT_CREATE_EMPTY: {
|
||||
const newState = { ...state };
|
||||
delete newState.localBackup;
|
||||
|
||||
const entry: Entry = {
|
||||
...action.payload,
|
||||
newRecord: true,
|
||||
};
|
||||
|
||||
// New Entry
|
||||
return {
|
||||
...newState,
|
||||
entry,
|
||||
original: entry,
|
||||
fieldsErrors: {},
|
||||
hasChanged: false,
|
||||
key: uuid(),
|
||||
};
|
||||
}
|
||||
case DRAFT_CREATE_FROM_LOCAL_BACKUP: {
|
||||
const backupDraftEntry = state.localBackup;
|
||||
if (!backupDraftEntry) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const backupEntry = backupDraftEntry?.['entry'];
|
||||
|
||||
const newState = { ...state };
|
||||
delete newState.localBackup;
|
||||
|
||||
const entry: Entry = {
|
||||
...backupEntry,
|
||||
newRecord: !backupEntry?.path,
|
||||
};
|
||||
|
||||
// Local Backup
|
||||
return {
|
||||
...state,
|
||||
entry,
|
||||
original: entry,
|
||||
fieldsErrors: {},
|
||||
hasChanged: true,
|
||||
key: uuid(),
|
||||
};
|
||||
}
|
||||
|
||||
case DRAFT_CREATE_DUPLICATE_FROM_ENTRY: {
|
||||
const newState = { ...state };
|
||||
delete newState.localBackup;
|
||||
|
||||
const entry: Entry = {
|
||||
...action.payload,
|
||||
newRecord: true,
|
||||
};
|
||||
|
||||
// Duplicate Entry
|
||||
return {
|
||||
...newState,
|
||||
entry,
|
||||
original: entry,
|
||||
fieldsErrors: {},
|
||||
hasChanged: true,
|
||||
};
|
||||
}
|
||||
case DRAFT_DISCARD:
|
||||
return initialState;
|
||||
case DRAFT_LOCAL_BACKUP_RETRIEVED: {
|
||||
const { entry } = action.payload;
|
||||
const newState = {
|
||||
entry,
|
||||
};
|
||||
return {
|
||||
...state,
|
||||
localBackup: newState,
|
||||
};
|
||||
}
|
||||
|
||||
case DRAFT_LOCAL_BACKUP_DELETE: {
|
||||
const newState = { ...state };
|
||||
delete newState.localBackup;
|
||||
return newState;
|
||||
}
|
||||
|
||||
case DRAFT_CHANGE_FIELD: {
|
||||
let newState = { ...state };
|
||||
if (!newState.entry) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const { path, field, value, i18n } = action.payload;
|
||||
const dataPath = (i18n && getDataPath(i18n.currentLocale, i18n.defaultLocale)) || ['data'];
|
||||
|
||||
newState = {
|
||||
...newState,
|
||||
entry: set(newState.entry, `${dataPath.join('.')}.${path}`, value),
|
||||
};
|
||||
|
||||
if (i18n) {
|
||||
newState = duplicateI18nFields(newState, field, i18n.locales, i18n.defaultLocale);
|
||||
}
|
||||
|
||||
const newData = get(newState.entry, dataPath) ?? {};
|
||||
|
||||
return {
|
||||
...newState,
|
||||
hasChanged: !newState.original || !isEqual(newData, get(newState.original, dataPath)),
|
||||
};
|
||||
}
|
||||
|
||||
case DRAFT_VALIDATION_ERRORS: {
|
||||
const { path, errors } = action.payload;
|
||||
const fieldsErrors = { ...state.fieldsErrors };
|
||||
if (errors.length === 0) {
|
||||
delete fieldsErrors[path];
|
||||
} else {
|
||||
fieldsErrors[path] = action.payload.errors;
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
fieldsErrors,
|
||||
};
|
||||
}
|
||||
|
||||
case ENTRY_PERSIST_REQUEST: {
|
||||
if (!state.entry) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
entry: {
|
||||
...state.entry,
|
||||
isPersisting: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case ENTRY_PERSIST_FAILURE: {
|
||||
if (!state.entry) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
entry: {
|
||||
...state.entry,
|
||||
isPersisting: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case ENTRY_PERSIST_SUCCESS: {
|
||||
if (!state.entry) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const newState = { ...state };
|
||||
delete newState.localBackup;
|
||||
|
||||
const entry: Entry = {
|
||||
...state.entry,
|
||||
slug: action.payload.slug,
|
||||
isPersisting: false,
|
||||
};
|
||||
|
||||
return {
|
||||
...newState,
|
||||
hasChanged: false,
|
||||
entry,
|
||||
original: entry,
|
||||
};
|
||||
}
|
||||
|
||||
case ENTRY_DELETE_SUCCESS: {
|
||||
if (!state.entry) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const newState = { ...state };
|
||||
delete newState.localBackup;
|
||||
|
||||
const entry: Entry = {
|
||||
...state.entry,
|
||||
isPersisting: false,
|
||||
};
|
||||
|
||||
return {
|
||||
...newState,
|
||||
hasChanged: false,
|
||||
entry,
|
||||
original: entry,
|
||||
};
|
||||
}
|
||||
|
||||
case ADD_DRAFT_ENTRY_MEDIA_FILE: {
|
||||
if (!state.entry) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const mediaFiles = state.entry.mediaFiles.filter(file => file.id !== action.payload.id);
|
||||
mediaFiles.unshift(action.payload);
|
||||
|
||||
return {
|
||||
...state,
|
||||
hasChanged: true,
|
||||
entry: {
|
||||
...state.entry,
|
||||
mediaFiles,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case REMOVE_DRAFT_ENTRY_MEDIA_FILE: {
|
||||
if (!state.entry) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const mediaFiles = state.entry.mediaFiles.filter(file => file.id !== action.payload.id);
|
||||
|
||||
return {
|
||||
...state,
|
||||
hasChanged: true,
|
||||
entry: {
|
||||
...state.entry,
|
||||
mediaFiles,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default entryDraftReducer;
|
||||
|
||||
export const selectFieldErrors = (path: string) => (state: RootState) => {
|
||||
return state.entryDraft.fieldsErrors[path] ?? [];
|
||||
};
|
29
packages/core/src/reducers/globalUI.ts
Normal file
29
packages/core/src/reducers/globalUI.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import type { AnyAction } from 'redux';
|
||||
|
||||
export type GlobalUIState = {
|
||||
isFetching: boolean;
|
||||
};
|
||||
|
||||
const defaultState: GlobalUIState = {
|
||||
isFetching: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Reducer for some global UI state that we want to share between components
|
||||
*/
|
||||
const globalUI = (state: GlobalUIState = defaultState, action: AnyAction): GlobalUIState => {
|
||||
// Generic, global loading indicator
|
||||
if (action.type.includes('REQUEST')) {
|
||||
return {
|
||||
isFetching: true,
|
||||
};
|
||||
} else if (action.type.includes('SUCCESS') || action.type.includes('FAILURE')) {
|
||||
return {
|
||||
isFetching: false,
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
export default globalUI;
|
54
packages/core/src/reducers/index.ts
Normal file
54
packages/core/src/reducers/index.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import auth from './auth';
|
||||
import collections from './collections';
|
||||
import config from './config';
|
||||
import cursors from './cursors';
|
||||
import entries, * as fromEntries from './entries';
|
||||
import entryDraft from './entryDraft';
|
||||
import globalUI from './globalUI';
|
||||
import mediaLibrary from './mediaLibrary';
|
||||
import medias from './medias';
|
||||
import scroll from './scroll';
|
||||
import search from './search';
|
||||
import status from './status';
|
||||
|
||||
import type { Collection } from '../interface';
|
||||
import type { RootState } from '../store';
|
||||
|
||||
const reducers = {
|
||||
auth,
|
||||
collections,
|
||||
config,
|
||||
cursors,
|
||||
entries,
|
||||
entryDraft,
|
||||
globalUI,
|
||||
mediaLibrary,
|
||||
medias,
|
||||
scroll,
|
||||
search,
|
||||
status,
|
||||
};
|
||||
|
||||
export default reducers;
|
||||
|
||||
/*
|
||||
* Selectors
|
||||
*/
|
||||
export function selectEntry(state: RootState, collection: string, slug: string) {
|
||||
return fromEntries.selectEntry(state.entries, collection, slug);
|
||||
}
|
||||
|
||||
export function selectEntries(state: RootState, collection: Collection) {
|
||||
return fromEntries.selectEntries(state.entries, collection);
|
||||
}
|
||||
|
||||
export function selectPublishedSlugs(state: RootState, collection: string) {
|
||||
return fromEntries.selectPublishedSlugs(state.entries, collection);
|
||||
}
|
||||
|
||||
export function selectSearchedEntries(state: RootState, availableCollections: string[]) {
|
||||
// only return search results for actually available collections
|
||||
return state.search.entryIds
|
||||
.filter(entryId => availableCollections.indexOf(entryId!.collection) !== -1)
|
||||
.map(entryId => fromEntries.selectEntry(state.entries, entryId!.collection, entryId!.slug));
|
||||
}
|
327
packages/core/src/reducers/mediaLibrary.ts
Normal file
327
packages/core/src/reducers/mediaLibrary.ts
Normal file
@ -0,0 +1,327 @@
|
||||
import get from 'lodash/get';
|
||||
import { dirname } from 'path';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import {
|
||||
MEDIA_DELETE_FAILURE,
|
||||
MEDIA_DELETE_REQUEST,
|
||||
MEDIA_DELETE_SUCCESS,
|
||||
MEDIA_DISPLAY_URL_FAILURE,
|
||||
MEDIA_DISPLAY_URL_REQUEST,
|
||||
MEDIA_DISPLAY_URL_SUCCESS,
|
||||
MEDIA_INSERT,
|
||||
MEDIA_LIBRARY_CLOSE,
|
||||
MEDIA_LIBRARY_CREATE,
|
||||
MEDIA_LIBRARY_OPEN,
|
||||
MEDIA_LOAD_FAILURE,
|
||||
MEDIA_LOAD_REQUEST,
|
||||
MEDIA_LOAD_SUCCESS,
|
||||
MEDIA_PERSIST_FAILURE,
|
||||
MEDIA_PERSIST_REQUEST,
|
||||
MEDIA_PERSIST_SUCCESS,
|
||||
MEDIA_REMOVE_INSERTED,
|
||||
} from '../actions/mediaLibrary';
|
||||
import { selectMediaFolder } from '../lib/util/media.util';
|
||||
import { selectEditingDraft } from './entries';
|
||||
|
||||
import type { MediaLibraryAction } from '../actions/mediaLibrary';
|
||||
import type { DisplayURLState, Field, MediaFile, MediaLibraryInstance } from '../interface';
|
||||
import type { RootState } from '../store';
|
||||
|
||||
export interface MediaLibraryDisplayURL {
|
||||
url?: string;
|
||||
isFetching: boolean;
|
||||
err?: unknown;
|
||||
}
|
||||
|
||||
export type MediaLibraryState = {
|
||||
isVisible: boolean;
|
||||
showMediaButton: boolean;
|
||||
controlMedia: Record<string, string | string[]>;
|
||||
displayURLs: Record<string, MediaLibraryDisplayURL>;
|
||||
externalLibrary?: MediaLibraryInstance;
|
||||
controlID?: string;
|
||||
page?: number;
|
||||
files?: MediaFile[];
|
||||
config: Record<string, unknown>;
|
||||
field?: Field;
|
||||
value?: string | string[];
|
||||
replaceIndex?: number;
|
||||
canInsert?: boolean;
|
||||
isLoading?: boolean;
|
||||
dynamicSearch?: boolean;
|
||||
dynamicSearchActive?: boolean;
|
||||
dynamicSearchQuery?: string;
|
||||
forImage?: boolean;
|
||||
isPersisting?: boolean;
|
||||
isDeleting?: boolean;
|
||||
hasNextPage?: boolean;
|
||||
isPaginating?: boolean;
|
||||
};
|
||||
|
||||
const defaultState: MediaLibraryState = {
|
||||
isVisible: false,
|
||||
showMediaButton: true,
|
||||
controlMedia: {},
|
||||
displayURLs: {},
|
||||
config: {},
|
||||
};
|
||||
|
||||
function mediaLibrary(
|
||||
state: MediaLibraryState = defaultState,
|
||||
action: MediaLibraryAction,
|
||||
): MediaLibraryState {
|
||||
switch (action.type) {
|
||||
case MEDIA_LIBRARY_CREATE:
|
||||
return {
|
||||
...state,
|
||||
externalLibrary: action.payload,
|
||||
showMediaButton: action.payload.enableStandalone(),
|
||||
};
|
||||
|
||||
case MEDIA_LIBRARY_OPEN: {
|
||||
const { controlID, forImage, config, field, value, replaceIndex } = action.payload;
|
||||
const libConfig = config || {};
|
||||
|
||||
return {
|
||||
...state,
|
||||
isVisible: true,
|
||||
forImage: Boolean(forImage),
|
||||
controlID,
|
||||
canInsert: !!controlID,
|
||||
config: libConfig,
|
||||
field,
|
||||
value,
|
||||
replaceIndex,
|
||||
};
|
||||
}
|
||||
|
||||
case MEDIA_LIBRARY_CLOSE:
|
||||
return {
|
||||
...state,
|
||||
isVisible: false,
|
||||
};
|
||||
|
||||
case MEDIA_INSERT: {
|
||||
const { mediaPath } = action.payload;
|
||||
const controlID = state.controlID;
|
||||
if (!controlID) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const value = state.value;
|
||||
|
||||
if (!Array.isArray(value)) {
|
||||
return {
|
||||
...state,
|
||||
controlMedia: {
|
||||
...state.controlMedia,
|
||||
[controlID]: mediaPath,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const replaceIndex = state.replaceIndex;
|
||||
const mediaArray = Array.isArray(mediaPath) ? mediaPath : [mediaPath];
|
||||
const valueArray = value as string[];
|
||||
if (typeof replaceIndex == 'number') {
|
||||
valueArray[replaceIndex] = mediaArray[0];
|
||||
} else {
|
||||
valueArray.push(...mediaArray);
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
controlMedia: {
|
||||
...state.controlMedia,
|
||||
[controlID]: valueArray,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case MEDIA_REMOVE_INSERTED: {
|
||||
const controlID = action.payload.controlID;
|
||||
|
||||
return {
|
||||
...state,
|
||||
controlMedia: {
|
||||
...state.controlMedia,
|
||||
[controlID]: '',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case MEDIA_LOAD_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
isLoading: true,
|
||||
isPaginating: action.payload.page > 1,
|
||||
};
|
||||
|
||||
case MEDIA_LOAD_SUCCESS: {
|
||||
const { files = [], page, canPaginate, dynamicSearch, dynamicSearchQuery } = action.payload;
|
||||
|
||||
const filesWithKeys = files.map(file => ({ ...file, key: uuid() }));
|
||||
return {
|
||||
...state,
|
||||
isLoading: false,
|
||||
isPaginating: false,
|
||||
page: page ?? 1,
|
||||
hasNextPage: Boolean(canPaginate && files.length > 0),
|
||||
dynamicSearch: Boolean(dynamicSearch),
|
||||
dynamicSearchQuery: dynamicSearchQuery ?? '',
|
||||
dynamicSearchActive: !!dynamicSearchQuery,
|
||||
files:
|
||||
page && page > 1 ? (state.files as MediaFile[]).concat(filesWithKeys) : filesWithKeys,
|
||||
};
|
||||
}
|
||||
|
||||
case MEDIA_LOAD_FAILURE: {
|
||||
return {
|
||||
...state,
|
||||
isLoading: false,
|
||||
};
|
||||
}
|
||||
|
||||
case MEDIA_PERSIST_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
isPersisting: true,
|
||||
};
|
||||
|
||||
case MEDIA_PERSIST_SUCCESS: {
|
||||
const { file } = action.payload;
|
||||
const fileWithKey = { ...file, key: uuid() };
|
||||
const files = state.files as MediaFile[];
|
||||
const updatedFiles = [fileWithKey, ...files];
|
||||
return {
|
||||
...state,
|
||||
files: updatedFiles,
|
||||
isPersisting: false,
|
||||
};
|
||||
}
|
||||
|
||||
case MEDIA_PERSIST_FAILURE: {
|
||||
return {
|
||||
...state,
|
||||
isPersisting: false,
|
||||
};
|
||||
}
|
||||
|
||||
case MEDIA_DELETE_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
isDeleting: true,
|
||||
};
|
||||
|
||||
case MEDIA_DELETE_SUCCESS: {
|
||||
const { file } = action.payload;
|
||||
const { key, id } = file;
|
||||
|
||||
const files = state.files as MediaFile[];
|
||||
const updatedFiles = files.filter(file => (key ? file.key !== key : file.id !== id));
|
||||
|
||||
const displayURLs = {
|
||||
...state.displayURLs,
|
||||
};
|
||||
|
||||
delete displayURLs[id];
|
||||
|
||||
return {
|
||||
...state,
|
||||
files: updatedFiles,
|
||||
displayURLs,
|
||||
isDeleting: false,
|
||||
};
|
||||
}
|
||||
|
||||
case MEDIA_DELETE_FAILURE: {
|
||||
return {
|
||||
...state,
|
||||
isDeleting: false,
|
||||
};
|
||||
}
|
||||
|
||||
case MEDIA_DISPLAY_URL_REQUEST:
|
||||
return {
|
||||
...state,
|
||||
displayURLs: {
|
||||
...state.displayURLs,
|
||||
[action.payload.key]: {
|
||||
...state.displayURLs[action.payload.key],
|
||||
isFetching: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
case MEDIA_DISPLAY_URL_SUCCESS: {
|
||||
return {
|
||||
...state,
|
||||
displayURLs: {
|
||||
...state.displayURLs,
|
||||
[action.payload.key]: {
|
||||
url: action.payload.url,
|
||||
isFetching: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case MEDIA_DISPLAY_URL_FAILURE: {
|
||||
const displayUrl = { ...state.displayURLs[action.payload.key] };
|
||||
delete displayUrl.url;
|
||||
displayUrl.isFetching = false;
|
||||
displayUrl.err = action.payload.err ?? true;
|
||||
|
||||
return {
|
||||
...state,
|
||||
displayURLs: {
|
||||
...state.displayURLs,
|
||||
[action.payload.key]: displayUrl,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export function selectMediaFiles(state: RootState, field?: Field): MediaFile[] {
|
||||
const { mediaLibrary, entryDraft } = state;
|
||||
const editingDraft = selectEditingDraft(entryDraft);
|
||||
|
||||
let files: MediaFile[] = [];
|
||||
if (editingDraft) {
|
||||
const entryFiles = entryDraft?.entry?.mediaFiles ?? [];
|
||||
const entry = entryDraft['entry'];
|
||||
const collection = entry?.collection ? state.collections[entry.collection] : null;
|
||||
if (state.config.config) {
|
||||
const mediaFolder = selectMediaFolder(state.config.config, collection, entry, field);
|
||||
files = entryFiles
|
||||
.filter(f => dirname(f.path) === mediaFolder)
|
||||
.map(file => ({ key: file.id, ...file }));
|
||||
}
|
||||
} else {
|
||||
files = mediaLibrary.files || [];
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
export function selectMediaFileByPath(state: RootState, path: string) {
|
||||
const files = selectMediaFiles(state);
|
||||
const file = files.find(file => file.path === path);
|
||||
return file;
|
||||
}
|
||||
|
||||
export function selectMediaDisplayURL(state: RootState, id: string) {
|
||||
const displayUrlState = (get(state.mediaLibrary, ['displayURLs', id]) ?? {}) as DisplayURLState;
|
||||
return displayUrlState;
|
||||
}
|
||||
|
||||
export const selectMediaPath = (controlID: string) => (state: RootState) => {
|
||||
return state.mediaLibrary.controlMedia[controlID];
|
||||
};
|
||||
|
||||
export default mediaLibrary;
|
66
packages/core/src/reducers/medias.ts
Normal file
66
packages/core/src/reducers/medias.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { produce } from 'immer';
|
||||
|
||||
import {
|
||||
ADD_ASSETS,
|
||||
ADD_ASSET,
|
||||
REMOVE_ASSET,
|
||||
LOAD_ASSET_REQUEST,
|
||||
LOAD_ASSET_SUCCESS,
|
||||
LOAD_ASSET_FAILURE,
|
||||
} from '../actions/media';
|
||||
|
||||
import type { MediasAction } from '../actions/media';
|
||||
import type AssetProxy from '../valueObjects/AssetProxy';
|
||||
|
||||
export interface MediasState {
|
||||
[path: string]: { asset: AssetProxy | undefined; isLoading: boolean; error: Error | null };
|
||||
}
|
||||
|
||||
const defaultState: MediasState = {};
|
||||
|
||||
const medias = produce((state: MediasState, action: MediasAction) => {
|
||||
switch (action.type) {
|
||||
case ADD_ASSETS: {
|
||||
const assets = action.payload;
|
||||
assets.forEach(asset => {
|
||||
state[asset.path] = { asset, isLoading: false, error: null };
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ADD_ASSET: {
|
||||
const asset = action.payload;
|
||||
state[asset.path] = { asset, isLoading: false, error: null };
|
||||
break;
|
||||
}
|
||||
case REMOVE_ASSET: {
|
||||
const path = action.payload;
|
||||
delete state[path];
|
||||
break;
|
||||
}
|
||||
case LOAD_ASSET_REQUEST: {
|
||||
const { path } = action.payload;
|
||||
state[path] = state[path] || {};
|
||||
state[path].isLoading = true;
|
||||
break;
|
||||
}
|
||||
case LOAD_ASSET_SUCCESS: {
|
||||
const { path } = action.payload;
|
||||
state[path] = state[path] || {};
|
||||
state[path].isLoading = false;
|
||||
state[path].error = null;
|
||||
break;
|
||||
}
|
||||
case LOAD_ASSET_FAILURE: {
|
||||
const { path, error } = action.payload;
|
||||
state[path] = state[path] || {};
|
||||
state[path].isLoading = false;
|
||||
state[path].error = error;
|
||||
}
|
||||
}
|
||||
}, defaultState);
|
||||
|
||||
export function selectIsLoadingAsset(state: MediasState) {
|
||||
return Object.values(state).some(state => state.isLoading);
|
||||
}
|
||||
|
||||
export default medias;
|
28
packages/core/src/reducers/scroll.ts
Normal file
28
packages/core/src/reducers/scroll.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { produce } from 'immer';
|
||||
|
||||
import { SCROLL_SYNC_ENABLED, SET_SCROLL, TOGGLE_SCROLL } from '../actions/scroll';
|
||||
|
||||
import type { ScrollAction } from '../actions/scroll';
|
||||
|
||||
export interface ScrollState {
|
||||
isScrolling: boolean;
|
||||
}
|
||||
|
||||
const defaultState: ScrollState = {
|
||||
isScrolling: true,
|
||||
};
|
||||
|
||||
const status = produce((state: ScrollState, action: ScrollAction) => {
|
||||
switch (action.type) {
|
||||
case TOGGLE_SCROLL:
|
||||
state.isScrolling = !state.isScrolling;
|
||||
localStorage.setItem(SCROLL_SYNC_ENABLED, `${state.isScrolling}`);
|
||||
break;
|
||||
case SET_SCROLL:
|
||||
state.isScrolling = action.payload;
|
||||
localStorage.setItem(SCROLL_SYNC_ENABLED, `${state.isScrolling}`);
|
||||
break;
|
||||
}
|
||||
}, defaultState);
|
||||
|
||||
export default status;
|
88
packages/core/src/reducers/search.ts
Normal file
88
packages/core/src/reducers/search.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import {
|
||||
QUERY_FAILURE,
|
||||
QUERY_REQUEST,
|
||||
QUERY_SUCCESS,
|
||||
SEARCH_CLEAR,
|
||||
SEARCH_ENTRIES_FAILURE,
|
||||
SEARCH_ENTRIES_REQUEST,
|
||||
SEARCH_ENTRIES_SUCCESS,
|
||||
} from '../actions/search';
|
||||
|
||||
import type { SearchAction } from '../actions/search';
|
||||
|
||||
export interface SearchState {
|
||||
isFetching: boolean;
|
||||
term: string;
|
||||
collections: string[];
|
||||
page: number;
|
||||
entryIds: { collection: string; slug: string }[];
|
||||
error: Error | undefined;
|
||||
}
|
||||
|
||||
const defaultState: SearchState = {
|
||||
isFetching: false,
|
||||
term: '',
|
||||
collections: [],
|
||||
page: 0,
|
||||
entryIds: [],
|
||||
error: undefined,
|
||||
};
|
||||
|
||||
const search = (state: SearchState = defaultState, action: SearchAction): SearchState => {
|
||||
switch (action.type) {
|
||||
case SEARCH_CLEAR:
|
||||
return defaultState;
|
||||
|
||||
case SEARCH_ENTRIES_REQUEST: {
|
||||
const { page, searchTerm, searchCollections } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
isFetching: true,
|
||||
term: searchTerm,
|
||||
collections: searchCollections,
|
||||
page,
|
||||
};
|
||||
}
|
||||
|
||||
case SEARCH_ENTRIES_SUCCESS: {
|
||||
const { entries, page } = action.payload;
|
||||
const entryIds = entries.map(entry => ({ collection: entry.collection, slug: entry.slug }));
|
||||
return {
|
||||
...state,
|
||||
isFetching: false,
|
||||
page,
|
||||
entryIds: !page || isNaN(page) || page === 0 ? entryIds : state.entryIds.concat(entryIds),
|
||||
};
|
||||
}
|
||||
|
||||
case QUERY_FAILURE:
|
||||
case SEARCH_ENTRIES_FAILURE: {
|
||||
const { error } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
isFetching: false,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
case QUERY_REQUEST: {
|
||||
const { searchTerm } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
isFetching: true,
|
||||
term: searchTerm,
|
||||
};
|
||||
}
|
||||
|
||||
case QUERY_SUCCESS: {
|
||||
return {
|
||||
...state,
|
||||
isFetching: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
export default search;
|
40
packages/core/src/reducers/status.ts
Normal file
40
packages/core/src/reducers/status.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { produce } from 'immer';
|
||||
|
||||
import { STATUS_REQUEST, STATUS_SUCCESS, STATUS_FAILURE } from '../actions/status';
|
||||
|
||||
import type { StatusAction } from '../actions/status';
|
||||
|
||||
export interface StatusState {
|
||||
isFetching: boolean;
|
||||
status: {
|
||||
auth: { status: boolean };
|
||||
api: { status: boolean; statusPage: string };
|
||||
};
|
||||
error: Error | undefined;
|
||||
}
|
||||
|
||||
const defaultState: StatusState = {
|
||||
isFetching: false,
|
||||
status: {
|
||||
auth: { status: true },
|
||||
api: { status: true, statusPage: '' },
|
||||
},
|
||||
error: undefined,
|
||||
};
|
||||
|
||||
const status = produce((state: StatusState, action: StatusAction) => {
|
||||
switch (action.type) {
|
||||
case STATUS_REQUEST:
|
||||
state.isFetching = true;
|
||||
break;
|
||||
case STATUS_SUCCESS:
|
||||
state.isFetching = false;
|
||||
state.status = action.payload.status;
|
||||
break;
|
||||
case STATUS_FAILURE:
|
||||
state.isFetching = false;
|
||||
state.error = action.payload.error;
|
||||
}
|
||||
}, defaultState);
|
||||
|
||||
export default status;
|
Reference in New Issue
Block a user