feat: singleton array list widget (#336)

This commit is contained in:
Daniel Lautzenheiser
2023-01-12 14:15:41 -05:00
committed by GitHub
parent a60d53b4ec
commit c5e94ed16d
64 changed files with 1353 additions and 575 deletions

View 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'],
});
});
});
});
});

View File

@ -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;

View File

@ -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);
};

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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] ?? [];
};

View File

@ -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));
}

View File

@ -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;

View File

@ -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;

View File

@ -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';

View File

@ -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';

View 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);
};

View 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';
}

View 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]);
}

View 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));
}

View 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;
}

View 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];
};

View 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);
}

View File

@ -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';