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

@ -22,15 +22,13 @@ import {
selectEntriesGroup,
selectEntriesSort,
selectViewStyle,
} from '@staticcms/core/reducers/entries';
} from '@staticcms/core/reducers/selectors/entries';
import CollectionControls from './CollectionControls';
import CollectionTop from './CollectionTop';
import EntriesCollection from './Entries/EntriesCollection';
import EntriesSearch from './Entries/EntriesSearch';
import Sidebar from './Sidebar';
import type { ComponentType } from 'react';
import type { ConnectedProps } from 'react-redux';
import type {
Collection,
SortDirection,
@ -39,6 +37,8 @@ import type {
ViewGroup,
} from '@staticcms/core/interface';
import type { RootState } from '@staticcms/core/store';
import type { ComponentType } from 'react';
import type { ConnectedProps } from 'react-redux';
const CollectionMain = styled('main')`
width: 100%;
@ -271,13 +271,13 @@ function mapStateToProps(state: RootState, ownProps: TranslatedProps<CollectionV
t,
} = ownProps;
const collection: Collection = name ? collections[name] : collections[0];
const sort = selectEntriesSort(state.entries, collection.name);
const sort = selectEntriesSort(state, collection.name);
const sortableFields = selectSortableFields(collection, t);
const viewFilters = selectViewFilters(collection);
const viewGroups = selectViewGroups(collection);
const filter = selectEntriesFilter(state.entries, collection.name);
const group = selectEntriesGroup(state.entries, collection.name);
const viewStyle = selectViewStyle(state.entries);
const filter = selectEntriesFilter(state, collection.name);
const group = selectEntriesGroup(state, collection.name);
const viewStyle = selectViewStyle(state);
return {
isSearchResults,

View File

@ -9,21 +9,21 @@ import {
} from '@staticcms/core/actions/entries';
import { colors } from '@staticcms/core/components/UI/styles';
import { Cursor } from '@staticcms/core/lib/util';
import { selectCollectionEntriesCursor } from '@staticcms/core/reducers/cursors';
import { selectCollectionEntriesCursor } from '@staticcms/core/reducers/selectors/cursors';
import {
selectEntries,
selectEntriesLoaded,
selectGroups,
selectIsFetching,
} from '@staticcms/core/reducers/entries';
} from '@staticcms/core/reducers/selectors/entries';
import Entries from './Entries';
import type { ComponentType } from 'react';
import type { t } from 'react-polyglot';
import type { ConnectedProps } from 'react-redux';
import type { CollectionViewStyle } from '@staticcms/core/constants/collectionViews';
import type { Collection, Entry, GroupOfEntries, TranslatedProps } from '@staticcms/core/interface';
import type { RootState } from '@staticcms/core/store';
import type { ComponentType } from 'react';
import type { t } from 'react-polyglot';
import type { ConnectedProps } from 'react-redux';
const GroupHeading = styled('h2')`
font-size: 23px;
@ -163,18 +163,18 @@ function mapStateToProps(state: RootState, ownProps: EntriesCollectionOwnProps)
const { collection, viewStyle, filterTerm } = ownProps;
const page = state.entries.pages[collection.name]?.page;
let entries = selectEntries(state.entries, collection);
const groups = selectGroups(state.entries, collection);
let entries = selectEntries(state, collection);
const groups = selectGroups(state, collection);
if ('nested' in collection) {
const collectionFolder = collection.folder ?? '';
entries = filterNestedEntries(filterTerm || '', collectionFolder, entries);
}
const entriesLoaded = selectEntriesLoaded(state.entries, collection.name);
const isFetching = selectIsFetching(state.entries, collection.name);
const entriesLoaded = selectEntriesLoaded(state, collection.name);
const isFetching = selectIsFetching(state, collection.name);
const rawCursor = selectCollectionEntriesCursor(state.cursors, collection.name);
const rawCursor = selectCollectionEntriesCursor(state, collection.name);
const cursor = Cursor.create(rawCursor).clearData();
return { ...ownProps, page, entries, groups, entriesLoaded, isFetching, viewStyle, cursor };

View File

@ -7,13 +7,13 @@ import {
searchEntries as searchEntriesAction,
} from '@staticcms/core/actions/search';
import { Cursor } from '@staticcms/core/lib/util';
import { selectSearchedEntries } from '@staticcms/core/reducers';
import { selectSearchedEntries } from '@staticcms/core/reducers/selectors/entries';
import Entries from './Entries';
import type { ConnectedProps } from 'react-redux';
import type { CollectionViewStyle } from '@staticcms/core/constants/collectionViews';
import type { Collections } from '@staticcms/core/interface';
import type { RootState } from '@staticcms/core/store';
import type { ConnectedProps } from 'react-redux';
const EntriesSearch = ({
collections,

View File

@ -11,7 +11,7 @@ import { getAsset as getAssetAction } from '@staticcms/core/actions/media';
import { VIEW_STYLE_GRID, VIEW_STYLE_LIST } from '@staticcms/core/constants/collectionViews';
import useMediaAsset from '@staticcms/core/lib/hooks/useMediaAsset';
import { selectEntryCollectionTitle } from '@staticcms/core/lib/util/collection.util';
import { selectIsLoadingAsset } from '@staticcms/core/reducers/medias';
import { selectIsLoadingAsset } from '@staticcms/core/reducers/selectors/medias';
import type { CollectionViewStyle } from '@staticcms/core/constants/collectionViews';
import type { Collection, Entry, Field } from '@staticcms/core/interface';
@ -78,7 +78,7 @@ function mapStateToProps(state: RootState, ownProps: EntryCardOwnProps) {
image = encodeURI(image.trim());
}
const isLoadingAsset = selectIsLoadingAsset(state.medias);
const isLoadingAsset = selectIsLoadingAsset(state);
return {
...ownProps,

View File

@ -10,7 +10,7 @@ import { colors, components } from '@staticcms/core/components/UI/styles';
import { transientOptions } from '@staticcms/core/lib';
import { selectEntryCollectionTitle } from '@staticcms/core/lib/util/collection.util';
import { stringTemplate } from '@staticcms/core/lib/widgets';
import { selectEntries } from '@staticcms/core/reducers/entries';
import { selectEntries } from '@staticcms/core/reducers/selectors/entries';
import type { Collection, Entry } from '@staticcms/core/interface';
import type { RootState } from '@staticcms/core/store';
@ -344,7 +344,7 @@ interface NestedCollectionOwnProps {
function mapStateToProps(state: RootState, ownProps: NestedCollectionOwnProps) {
const { collection } = ownProps;
const entries = selectEntries(state.entries, collection) ?? [];
const entries = selectEntries(state, collection) ?? [];
return { ...ownProps, entries };
}

View File

@ -24,15 +24,12 @@ import {
} from '@staticcms/core/actions/scroll';
import { selectFields } from '@staticcms/core/lib/util/collection.util';
import { useWindowEvent } from '@staticcms/core/lib/util/window.util';
import { selectEntry } from '@staticcms/core/reducers';
import { selectEntry } from '@staticcms/core/reducers/selectors/entries';
import { history, navigateToCollection, navigateToNewEntry } from '@staticcms/core/routing/history';
import confirm from '../UI/Confirm';
import Loader from '../UI/Loader';
import EditorInterface from './EditorInterface';
import type { Blocker } from 'history';
import type { ComponentType } from 'react';
import type { ConnectedProps } from 'react-redux';
import type {
Collection,
EditorPersistOptions,
@ -40,6 +37,9 @@ import type {
TranslatedProps,
} from '@staticcms/core/interface';
import type { RootState } from '@staticcms/core/store';
import type { Blocker } from 'history';
import type { ComponentType } from 'react';
import type { ConnectedProps } from 'react-redux';
const Editor = ({
entry,

View File

@ -25,8 +25,8 @@ import { resolveWidget } from '@staticcms/core/lib/registry';
import { getFieldLabel } from '@staticcms/core/lib/util/field.util';
import { isNotNullish } from '@staticcms/core/lib/util/null.util';
import { validate } from '@staticcms/core/lib/util/validation.util';
import { selectFieldErrors } from '@staticcms/core/reducers/entryDraft';
import { selectIsLoadingAsset } from '@staticcms/core/reducers/medias';
import { selectFieldErrors } from '@staticcms/core/reducers/selectors/entryDraft';
import { selectIsLoadingAsset } from '@staticcms/core/reducers/selectors/medias';
import { useAppDispatch, useAppSelector } from '@staticcms/core/store/hooks';
import type {
@ -160,6 +160,7 @@ const EditorControl = ({
forList = false,
changeDraftField,
i18n,
fieldName,
}: TranslatedProps<EditorControlProps>) => {
const dispatch = useAppDispatch();
@ -170,8 +171,9 @@ const EditorControl = ({
const fieldHint = field.hint;
const path = useMemo(
() => (parentPath.length > 0 ? `${parentPath}.${field.name}` : field.name),
[field.name, parentPath],
() =>
parentPath.length > 0 ? `${parentPath}.${fieldName ?? field.name}` : fieldName ?? field.name,
[field.name, fieldName, parentPath],
);
const [dirty, setDirty] = useState(!isEmpty(value));
@ -336,13 +338,14 @@ interface EditorControlOwnProps {
value: ValueOrNestedValue;
forList?: boolean;
i18n: I18nSettings | undefined;
fieldName?: string;
}
function mapStateToProps(state: RootState, ownProps: EditorControlOwnProps) {
const { collections, entryDraft } = state;
const entry = entryDraft.entry;
const collection = entryDraft.entry ? collections[entryDraft.entry.collection] : null;
const isLoadingAsset = selectIsLoadingAsset(state.medias);
const isLoadingAsset = selectIsLoadingAsset(state);
return {
...ownProps,

View File

@ -12,7 +12,7 @@ import { lengths } from '@staticcms/core/components/UI/styles';
import { getPreviewStyles, getPreviewTemplate, resolveWidget } from '@staticcms/core/lib/registry';
import { selectTemplateName, useInferedFields } from '@staticcms/core/lib/util/collection.util';
import { selectField } from '@staticcms/core/lib/util/field.util';
import { selectIsLoadingAsset } from '@staticcms/core/reducers/medias';
import { selectIsLoadingAsset } from '@staticcms/core/reducers/selectors/medias';
import { getTypedFieldForValue } from '@staticcms/list/typedListHelpers';
import EditorPreview from './EditorPreview';
import EditorPreviewContent from './EditorPreviewContent';
@ -106,17 +106,12 @@ function getWidgetFor(
}
const value = values?.[field.name];
let fieldWithWidgets = Object.entries(field).reduce((acc, [key, fieldValue]) => {
if (!['fields', 'fields'].includes(key)) {
acc[key] = fieldValue;
}
return acc;
}, {} as Record<string, unknown>) as RenderedField;
let fieldWithWidgets = field as RenderedField;
if ('fields' in field && field.fields) {
fieldWithWidgets = {
...fieldWithWidgets,
fields: getNestedWidgets(
renderedFields: getNestedWidgets(
config,
collection,
fields,
@ -130,7 +125,7 @@ function getWidgetFor(
} else if ('types' in field && field.types) {
fieldWithWidgets = {
...fieldWithWidgets,
fields: getTypedNestedWidgets(
renderedFields: getTypedNestedWidgets(
config,
collection,
field,
@ -594,7 +589,7 @@ export interface EditorPreviewPaneOwnProps {
}
function mapStateToProps(state: RootState, ownProps: EditorPreviewPaneOwnProps) {
const isLoadingAsset = selectIsLoadingAsset(state.medias);
const isLoadingAsset = selectIsLoadingAsset(state);
return { ...ownProps, isLoadingAsset, config: state.config };
}

View File

@ -12,15 +12,15 @@ import {
persistMedia as persistMediaAction,
} from '@staticcms/core/actions/mediaLibrary';
import { fileExtension } from '@staticcms/core/lib/util';
import { selectMediaFiles } from '@staticcms/core/reducers/mediaLibrary';
import { selectMediaFiles } from '@staticcms/core/reducers/selectors/mediaLibrary';
import alert from '../UI/Alert';
import confirm from '../UI/Confirm';
import MediaLibraryModal from './MediaLibraryModal';
import type { ChangeEvent, KeyboardEvent } from 'react';
import type { ConnectedProps } from 'react-redux';
import type { MediaFile, TranslatedProps } from '@staticcms/core/interface';
import type { RootState } from '@staticcms/core/store';
import type { ChangeEvent, KeyboardEvent } from 'react';
import type { ConnectedProps } from 'react-redux';
/**
* Extensions used to determine which files to show when the media library is

View File

@ -117,12 +117,12 @@ const ListItemTopBar = ({
/>
</IconButton>
) : null}
<StyledTitle key="title" onClick={onCollapseToggle}>
<StyledTitle key="title" onClick={onCollapseToggle} data-testid="list-item-title">
{title}
</StyledTitle>
{listeners ? <DragHandle listeners={listeners} /> : null}
{onRemove ? (
<TopBarButton onClick={onRemove}>
<TopBarButton data-testid="remove-button" onClick={onRemove}>
<CloseIcon />
</TopBarButton>
) : null}

View File

@ -53,6 +53,7 @@ export interface ObjectWidgetTopBarProps {
heading: ReactNode;
label?: string;
hasError?: boolean;
testId?: string;
}
const ObjectWidgetTopBar = ({
@ -66,6 +67,7 @@ const ObjectWidgetTopBar = ({
label,
hasError = false,
t,
testId,
}: TranslatedProps<ObjectWidgetTopBarProps>) => {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
@ -129,6 +131,7 @@ const ObjectWidgetTopBar = ({
endIcon={<AddIcon fontSize="small" />}
size="small"
variant="outlined"
data-testid="add-button"
>
{t('editor.editorWidgets.list.add', { item: label })}
</Button>
@ -147,7 +150,7 @@ const ObjectWidgetTopBar = ({
}, [allowAdd, types, renderTypesDropdown, renderAddButton]);
return (
<TopBarContainer>
<TopBarContainer data-testid={testId}>
<ExpandButtonContainer $hasError={hasError}>
<IconButton onClick={onCollapseToggle} data-testid="expand-button">
<ExpandMoreIcon