feat: singleton array list widget (#336)
This commit is contained in:
committed by
GitHub
parent
a60d53b4ec
commit
c5e94ed16d
77
packages/core/src/reducers/__tests__/entryDraft.spec.ts
Normal file
77
packages/core/src/reducers/__tests__/entryDraft.spec.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import { DRAFT_CHANGE_FIELD, DRAFT_CREATE_EMPTY } from '@staticcms/core/constants';
|
||||
import mockEntry from '@staticcms/core/lib/test-utils/mock-data/MockEntry';
|
||||
import entryDraftReducer from '../entryDraft';
|
||||
|
||||
import type { EntryDraftState } from '../entryDraft';
|
||||
|
||||
describe('entryDraft', () => {
|
||||
describe('reducer', () => {
|
||||
describe('DRAFT_CHANGE_FIELD', () => {
|
||||
let startState: EntryDraftState;
|
||||
|
||||
beforeEach(() => {
|
||||
startState = entryDraftReducer(undefined, {
|
||||
type: DRAFT_CREATE_EMPTY,
|
||||
payload: mockEntry,
|
||||
});
|
||||
});
|
||||
|
||||
it('should update path with value', () => {
|
||||
const state = entryDraftReducer(startState, {
|
||||
type: DRAFT_CHANGE_FIELD,
|
||||
payload: {
|
||||
path: 'path1.path2',
|
||||
field: {
|
||||
widget: 'string',
|
||||
name: 'stringInput',
|
||||
},
|
||||
value: 'newValue',
|
||||
i18n: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
expect(state.entry?.data).toEqual({
|
||||
path1: {
|
||||
path2: 'newValue',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should update path with value for singleton list', () => {
|
||||
let state = entryDraftReducer(startState, {
|
||||
type: DRAFT_CHANGE_FIELD,
|
||||
payload: {
|
||||
path: 'path1',
|
||||
field: {
|
||||
widget: 'string',
|
||||
name: 'stringInput',
|
||||
},
|
||||
value: ['newValue1', 'newValue2', 'newValue3'],
|
||||
i18n: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
expect(state.entry?.data).toEqual({
|
||||
path1: ['newValue1', 'newValue2', 'newValue3'],
|
||||
});
|
||||
|
||||
state = entryDraftReducer(state, {
|
||||
type: DRAFT_CHANGE_FIELD,
|
||||
payload: {
|
||||
path: 'path1.1',
|
||||
field: {
|
||||
widget: 'string',
|
||||
name: 'stringInput',
|
||||
},
|
||||
value: 'newValue2Updated',
|
||||
i18n: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
expect(state.entry?.data).toEqual({
|
||||
path1: ['newValue1', 'newValue2Updated', 'newValue3'],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -1,15 +1,9 @@
|
||||
import { produce } from 'immer';
|
||||
|
||||
import {
|
||||
AUTH_REQUEST,
|
||||
AUTH_SUCCESS,
|
||||
AUTH_FAILURE,
|
||||
AUTH_REQUEST_DONE,
|
||||
LOGOUT,
|
||||
} from '../actions/auth';
|
||||
import { AUTH_FAILURE, AUTH_REQUEST, AUTH_REQUEST_DONE, AUTH_SUCCESS, LOGOUT } from '../constants';
|
||||
|
||||
import type { User } from '../interface';
|
||||
import type { AuthAction } from '../actions/auth';
|
||||
import type { User } from '../interface';
|
||||
|
||||
export type AuthState = {
|
||||
isFetching: boolean;
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { CONFIG_SUCCESS } from '../actions/config';
|
||||
import { CONFIG_SUCCESS } from '../constants';
|
||||
|
||||
import type { ConfigAction } from '../actions/config';
|
||||
import type { Collection, Collections } from '../interface';
|
||||
import type { RootState } from '../store';
|
||||
|
||||
export type CollectionsState = Collections;
|
||||
|
||||
@ -26,7 +25,3 @@ function collections(
|
||||
}
|
||||
|
||||
export default collections;
|
||||
|
||||
export const selectCollection = (collectionName: string) => (state: RootState) => {
|
||||
return Object.values(state.collections).find(collection => collection.name === collectionName);
|
||||
};
|
||||
|
@ -1,6 +1,4 @@
|
||||
import { produce } from 'immer';
|
||||
|
||||
import { CONFIG_FAILURE, CONFIG_REQUEST, CONFIG_SUCCESS } from '../actions/config';
|
||||
import { CONFIG_FAILURE, CONFIG_REQUEST, CONFIG_SUCCESS } from '../constants';
|
||||
|
||||
import type { ConfigAction } from '../actions/config';
|
||||
import type { Config } from '../interface';
|
||||
@ -15,11 +13,13 @@ const defaultState: ConfigState = {
|
||||
isFetching: true,
|
||||
};
|
||||
|
||||
const config = produce((state: ConfigState, action: ConfigAction) => {
|
||||
const config = (state: ConfigState = defaultState, action: ConfigAction) => {
|
||||
switch (action.type) {
|
||||
case CONFIG_REQUEST:
|
||||
state.isFetching = true;
|
||||
break;
|
||||
return {
|
||||
...state,
|
||||
isFetching: true,
|
||||
};
|
||||
case CONFIG_SUCCESS:
|
||||
return {
|
||||
config: action.payload,
|
||||
@ -27,13 +27,15 @@ const config = produce((state: ConfigState, action: ConfigAction) => {
|
||||
error: undefined,
|
||||
};
|
||||
case CONFIG_FAILURE:
|
||||
state.isFetching = false;
|
||||
state.error = action.payload.toString();
|
||||
}
|
||||
}, defaultState);
|
||||
return {
|
||||
...state,
|
||||
isFetching: false,
|
||||
error: action.payload.toString(),
|
||||
};
|
||||
|
||||
export function selectLocale(state?: Config) {
|
||||
return state?.locale || 'en';
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
@ -3,7 +3,7 @@ import {
|
||||
FILTER_ENTRIES_SUCCESS,
|
||||
GROUP_ENTRIES_SUCCESS,
|
||||
SORT_ENTRIES_SUCCESS,
|
||||
} from '../actions/entries';
|
||||
} from '../constants';
|
||||
import { Cursor } from '../lib/util';
|
||||
|
||||
import type { EntriesAction } from '../actions/entries';
|
||||
@ -50,11 +50,4 @@ function cursors(
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
@ -1,7 +1,4 @@
|
||||
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 {
|
||||
@ -19,34 +16,29 @@ import {
|
||||
GROUP_ENTRIES_FAILURE,
|
||||
GROUP_ENTRIES_REQUEST,
|
||||
GROUP_ENTRIES_SUCCESS,
|
||||
SEARCH_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';
|
||||
} 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';
|
||||
@ -545,173 +537,4 @@ function entries(
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
@ -18,13 +18,12 @@ import {
|
||||
ENTRY_PERSIST_REQUEST,
|
||||
ENTRY_PERSIST_SUCCESS,
|
||||
REMOVE_DRAFT_ENTRY_MEDIA_FILE,
|
||||
} from '../actions/entries';
|
||||
} from '../constants';
|
||||
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;
|
||||
@ -301,7 +300,3 @@ function entryDraftReducer(
|
||||
}
|
||||
|
||||
export default entryDraftReducer;
|
||||
|
||||
export const selectFieldErrors = (path: string) => (state: RootState) => {
|
||||
return state.entryDraft.fieldsErrors[path] ?? [];
|
||||
};
|
||||
|
@ -2,7 +2,7 @@ import auth from './auth';
|
||||
import collections from './collections';
|
||||
import config from './config';
|
||||
import cursors from './cursors';
|
||||
import entries, * as fromEntries from './entries';
|
||||
import entries from './entries';
|
||||
import entryDraft from './entryDraft';
|
||||
import globalUI from './globalUI';
|
||||
import mediaLibrary from './mediaLibrary';
|
||||
@ -11,9 +11,6 @@ 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,
|
||||
@ -30,25 +27,3 @@ const reducers = {
|
||||
};
|
||||
|
||||
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));
|
||||
}
|
||||
|
@ -1,5 +1,3 @@
|
||||
import get from 'lodash/get';
|
||||
import { dirname } from 'path';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import {
|
||||
@ -20,13 +18,10 @@ import {
|
||||
MEDIA_PERSIST_REQUEST,
|
||||
MEDIA_PERSIST_SUCCESS,
|
||||
MEDIA_REMOVE_INSERTED,
|
||||
} from '../actions/mediaLibrary';
|
||||
import { selectMediaFolder } from '../lib/util/media.util';
|
||||
import { selectEditingDraft } from './entries';
|
||||
} from '../constants';
|
||||
|
||||
import type { MediaLibraryAction } from '../actions/mediaLibrary';
|
||||
import type { DisplayURLState, Field, MediaFile, MediaLibraryInstance } from '../interface';
|
||||
import type { RootState } from '../store';
|
||||
import type { Field, MediaFile, MediaLibraryInstance } from '../interface';
|
||||
|
||||
export interface MediaLibraryDisplayURL {
|
||||
url?: string;
|
||||
@ -287,41 +282,4 @@ function mediaLibrary(
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
@ -1,66 +1,74 @@
|
||||
import { produce } from 'immer';
|
||||
|
||||
import {
|
||||
ADD_ASSETS,
|
||||
ADD_ASSET,
|
||||
REMOVE_ASSET,
|
||||
ADD_ASSETS,
|
||||
LOAD_ASSET_FAILURE,
|
||||
LOAD_ASSET_REQUEST,
|
||||
LOAD_ASSET_SUCCESS,
|
||||
LOAD_ASSET_FAILURE,
|
||||
} from '../actions/media';
|
||||
REMOVE_ASSET,
|
||||
} from '../constants';
|
||||
|
||||
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 };
|
||||
}
|
||||
export type MediasState = Record<
|
||||
string,
|
||||
{ asset: AssetProxy | undefined; isLoading: boolean; error: Error | null }
|
||||
>;
|
||||
|
||||
const defaultState: MediasState = {};
|
||||
|
||||
const medias = produce((state: MediasState, action: MediasAction) => {
|
||||
const medias = (state: MediasState = defaultState, action: MediasAction) => {
|
||||
switch (action.type) {
|
||||
case ADD_ASSETS: {
|
||||
const assets = action.payload;
|
||||
const newState = {
|
||||
...state,
|
||||
};
|
||||
assets.forEach(asset => {
|
||||
state[asset.path] = { asset, isLoading: false, error: null };
|
||||
newState[asset.path] = { asset, isLoading: false, error: null };
|
||||
});
|
||||
break;
|
||||
return newState;
|
||||
}
|
||||
case ADD_ASSET: {
|
||||
const asset = action.payload;
|
||||
state[asset.path] = { asset, isLoading: false, error: null };
|
||||
break;
|
||||
return {
|
||||
...state,
|
||||
[asset.path]: { asset, isLoading: false, error: null },
|
||||
};
|
||||
}
|
||||
case REMOVE_ASSET: {
|
||||
const path = action.payload;
|
||||
delete state[path];
|
||||
break;
|
||||
const newState = {
|
||||
...state,
|
||||
};
|
||||
delete newState[path];
|
||||
return newState;
|
||||
}
|
||||
case LOAD_ASSET_REQUEST: {
|
||||
const { path } = action.payload;
|
||||
state[path] = state[path] || {};
|
||||
state[path].isLoading = true;
|
||||
break;
|
||||
return {
|
||||
...state,
|
||||
[path]: { ...state[path], isLoading: true },
|
||||
};
|
||||
}
|
||||
case LOAD_ASSET_SUCCESS: {
|
||||
const { path } = action.payload;
|
||||
state[path] = state[path] || {};
|
||||
state[path].isLoading = false;
|
||||
state[path].error = null;
|
||||
break;
|
||||
return {
|
||||
...state,
|
||||
[path]: { ...state[path], isLoading: false, error: null },
|
||||
};
|
||||
}
|
||||
case LOAD_ASSET_FAILURE: {
|
||||
const { path, error } = action.payload;
|
||||
state[path] = state[path] || {};
|
||||
state[path].isLoading = false;
|
||||
state[path].error = error;
|
||||
return {
|
||||
...state,
|
||||
[path]: { ...state[path], isLoading: false, error },
|
||||
};
|
||||
}
|
||||
}
|
||||
}, defaultState);
|
||||
|
||||
export function selectIsLoadingAsset(state: MediasState) {
|
||||
return Object.values(state).some(state => state.isLoading);
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default medias;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { produce } from 'immer';
|
||||
|
||||
import { SCROLL_SYNC_ENABLED, SET_SCROLL, TOGGLE_SCROLL } from '../actions/scroll';
|
||||
import { SCROLL_SYNC_ENABLED, SET_SCROLL, TOGGLE_SCROLL } from '../constants';
|
||||
|
||||
import type { ScrollAction } from '../actions/scroll';
|
||||
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
SEARCH_ENTRIES_FAILURE,
|
||||
SEARCH_ENTRIES_REQUEST,
|
||||
SEARCH_ENTRIES_SUCCESS,
|
||||
} from '../actions/search';
|
||||
} from '../constants';
|
||||
|
||||
import type { SearchAction } from '../actions/search';
|
||||
|
||||
|
6
packages/core/src/reducers/selectors/collections.ts
Normal file
6
packages/core/src/reducers/selectors/collections.ts
Normal file
@ -0,0 +1,6 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import type { RootState } from '@staticcms/core/store';
|
||||
|
||||
export const selectCollection = (collectionName: string) => (state: RootState) => {
|
||||
return Object.values(state.collections).find(collection => collection.name === collectionName);
|
||||
};
|
7
packages/core/src/reducers/selectors/config.ts
Normal file
7
packages/core/src/reducers/selectors/config.ts
Normal file
@ -0,0 +1,7 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
|
||||
import type { Config } from '@staticcms/core/interface';
|
||||
|
||||
export function selectLocale(config?: Config) {
|
||||
return config?.locale || 'en';
|
||||
}
|
12
packages/core/src/reducers/selectors/cursors.ts
Normal file
12
packages/core/src/reducers/selectors/cursors.ts
Normal file
@ -0,0 +1,12 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
|
||||
import Cursor from '@staticcms/core/lib/util/Cursor';
|
||||
|
||||
import type { RootState } from '@staticcms/core/store';
|
||||
|
||||
// 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: RootState, collectionName: string) {
|
||||
return new Cursor(state.cursors.cursorsByType.collectionEntries[collectionName]);
|
||||
}
|
190
packages/core/src/reducers/selectors/entries.ts
Normal file
190
packages/core/src/reducers/selectors/entries.ts
Normal file
@ -0,0 +1,190 @@
|
||||
import get from 'lodash/get';
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
|
||||
import { SORT_DIRECTION_ASCENDING, SORT_DIRECTION_NONE } from '@staticcms/core/constants';
|
||||
import { selectSortDataPath } from '@staticcms/core/lib/util/sort.util';
|
||||
|
||||
import type { CollectionViewStyle } from '@staticcms/core/constants/collectionViews';
|
||||
import type {
|
||||
Collection,
|
||||
Entry,
|
||||
Filter,
|
||||
Group,
|
||||
GroupMap,
|
||||
GroupOfEntries,
|
||||
Sort,
|
||||
} from '@staticcms/core/interface';
|
||||
import type { RootState } from '@staticcms/core/store';
|
||||
|
||||
export function selectEntriesSort(entries: RootState, collection: string) {
|
||||
const sort = entries.entries.sort as Sort | undefined;
|
||||
return sort?.[collection];
|
||||
}
|
||||
|
||||
export function selectEntriesFilter(entries: RootState, collection: string) {
|
||||
const filter = entries.entries.filter as Filter | undefined;
|
||||
return filter?.[collection] || {};
|
||||
}
|
||||
|
||||
export function selectEntriesGroup(entries: RootState, collection: string) {
|
||||
const group = entries.entries.group as Group | undefined;
|
||||
return group?.[collection] || {};
|
||||
}
|
||||
|
||||
export function selectEntriesGroupField(entries: RootState, collection: string) {
|
||||
const groups = selectEntriesGroup(entries, collection);
|
||||
const value = Object.values(groups ?? {}).find(v => v?.active === true);
|
||||
return value;
|
||||
}
|
||||
|
||||
export function selectEntriesSortFields(entries: RootState, 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: RootState, collection: string) {
|
||||
const filter = selectEntriesFilter(entries, collection);
|
||||
const values = Object.values(filter ?? {}).filter(v => v?.active === true) || [];
|
||||
return values;
|
||||
}
|
||||
|
||||
export function selectViewStyle(entries: RootState): CollectionViewStyle {
|
||||
return entries.entries.viewStyle;
|
||||
}
|
||||
|
||||
export function selectEntry(state: RootState, collection: string, slug: string) {
|
||||
return state.entries.entities[`${collection}.${slug}`];
|
||||
}
|
||||
|
||||
export function selectPublishedSlugs(state: RootState, collection: string) {
|
||||
return state.entries.pages[collection]?.ids ?? [];
|
||||
}
|
||||
|
||||
function getPublishedEntries(state: RootState, 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: RootState, 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: RootState, 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: RootState, 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: RootState, collection: string) {
|
||||
return !!state.entries.pages[collection];
|
||||
}
|
||||
|
||||
export function selectIsFetching(state: RootState, collection: string) {
|
||||
return state.entries.pages[collection]?.isFetching ?? false;
|
||||
}
|
||||
|
||||
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 => selectEntry(state, entryId!.collection, entryId!.slug));
|
||||
}
|
10
packages/core/src/reducers/selectors/entryDraft.ts
Normal file
10
packages/core/src/reducers/selectors/entryDraft.ts
Normal file
@ -0,0 +1,10 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import type { RootState } from '@staticcms/core/store';
|
||||
|
||||
export const selectFieldErrors = (path: string) => (state: RootState) => {
|
||||
return state.entryDraft.fieldsErrors[path] ?? [];
|
||||
};
|
||||
|
||||
export function selectEditingDraft(state: RootState) {
|
||||
return state.entryDraft.entry;
|
||||
}
|
44
packages/core/src/reducers/selectors/mediaLibrary.ts
Normal file
44
packages/core/src/reducers/selectors/mediaLibrary.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import get from 'lodash/get';
|
||||
import { dirname } from 'path';
|
||||
|
||||
import { selectMediaFolder } from '@staticcms/core/lib/util/media.util';
|
||||
import { selectEditingDraft } from './entryDraft';
|
||||
|
||||
import type { DisplayURLState, Field, MediaFile } from '@staticcms/core/interface';
|
||||
import type { RootState } from '@staticcms/core/store';
|
||||
|
||||
export function selectMediaFiles(state: RootState, field?: Field): MediaFile[] {
|
||||
const { mediaLibrary, entryDraft } = state;
|
||||
const editingDraft = selectEditingDraft(state);
|
||||
|
||||
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) {
|
||||
return (get(state.mediaLibrary, ['displayURLs', id]) ?? {}) as DisplayURLState;
|
||||
}
|
||||
|
||||
export const selectMediaPath = (controlID: string) => (state: RootState) => {
|
||||
return state.mediaLibrary.controlMedia[controlID];
|
||||
};
|
7
packages/core/src/reducers/selectors/medias.ts
Normal file
7
packages/core/src/reducers/selectors/medias.ts
Normal file
@ -0,0 +1,7 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
|
||||
import type { RootState } from '@staticcms/core/store';
|
||||
|
||||
export function selectIsLoadingAsset(state: RootState) {
|
||||
return Object.values(state.medias).some(state => state.isLoading);
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { produce } from 'immer';
|
||||
|
||||
import { STATUS_REQUEST, STATUS_SUCCESS, STATUS_FAILURE } from '../actions/status';
|
||||
import { STATUS_REQUEST, STATUS_SUCCESS, STATUS_FAILURE } from '../constants';
|
||||
|
||||
import type { StatusAction } from '../actions/status';
|
||||
|
||||
|
Reference in New Issue
Block a user