Fix/bugfixes (#80)

* Fix widgetsFor for lists and objects
* Fix input for datetime widget
* Fix search
This commit is contained in:
Daniel Lautzenheiser 2022-11-09 09:52:05 -05:00 committed by GitHub
parent b13653b26d
commit 28a1f7a78a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 270 additions and 178 deletions

View File

@ -628,7 +628,7 @@ collections:
widget: select
options:
- label: One
value: "One"
value: 'One'
- label: Two
value: 2
- label: Three

View File

@ -1,7 +1,7 @@
// Register all the things
CMS.init();
const PostPreview = ({ entry, widgetFor }) => {
const PostPreview = ({ entry, widgetFor, widgetsFor }) => {
return h(
'div',
{},

View File

@ -140,7 +140,7 @@ export function searchEntries(searchTerm: string, searchCollections: string[], p
return dispatch(searchFailure(new Error(`No integration found for name "${integration}"`)));
}
return dispatch(searchSuccess(response.entries, response.pagination));
return dispatch(searchSuccess(response.entries, page));
} catch (error: unknown) {
console.error(error);
if (error instanceof Error) {

View File

@ -261,7 +261,8 @@ interface PersistArgs {
function collectionDepth(collection: Collection) {
let depth;
depth = 'nested' in collection && collection.nested?.depth || getPathDepth(collection.path ?? '');
depth =
('nested' in collection && collection.nested?.depth) || getPathDepth(collection.path ?? '');
if (hasI18n(collection)) {
depth = getI18nFilesDepth(collection, depth);
@ -549,7 +550,7 @@ export class Backend {
}
const hits = entries
.filter(({ score }: fuzzy.FilterResult<Entry>) => score > 5)
.filter(({ score }: fuzzy.FilterResult<Entry>) => score > 3)
.sort(sortByScore)
.map((f: fuzzy.FilterResult<Entry>) => f.original);
return { entries: hits, pagination: 1 };

View File

@ -110,7 +110,14 @@ const CollectionView = ({
}
}
return <EntriesSearch collections={searchCollections} searchTerm={searchTerm} />;
return (
<EntriesSearch
key="search"
collections={searchCollections}
searchTerm={searchTerm}
viewStyle={viewStyle}
/>
);
}
return (
@ -210,11 +217,14 @@ const CollectionView = ({
<CollectionMain>
<>
{isSearchResults ? (
<SearchResultContainer>
<SearchResultHeading>
{t(searchResultKey, { searchTerm, collection: collection.label })}
</SearchResultHeading>
</SearchResultContainer>
<>
<SearchResultContainer>
<SearchResultHeading>
{t(searchResultKey, { searchTerm, collection: collection.label })}
</SearchResultHeading>
</SearchResultContainer>
<CollectionControls viewStyle={viewStyle} onChangeViewStyle={changeViewStyle} t={t} />
</>
) : (
<>
<CollectionTop collection={collection} newEntryUrl={newEntryUrl} />

View File

@ -33,15 +33,15 @@ const CollectionControlsContainer = styled('div')`
interface CollectionControlsProps {
viewStyle: CollectionViewStyle;
onChangeViewStyle: (viewStyle: CollectionViewStyle) => void;
sortableFields: SortableField[];
onSortClick: (key: string, direction?: SortDirection) => Promise<void>;
sort: SortMap | undefined;
filter: Record<string, FilterMap>;
viewFilters: ViewFilter[];
onFilterClick: (filter: ViewFilter) => void;
group: Record<string, GroupMap>;
viewGroups: ViewGroup[];
onGroupClick: (filter: ViewGroup) => void;
sortableFields?: SortableField[];
onSortClick?: (key: string, direction?: SortDirection) => Promise<void>;
sort?: SortMap | undefined;
filter?: Record<string, FilterMap>;
viewFilters?: ViewFilter[];
onFilterClick?: (filter: ViewFilter) => void;
group?: Record<string, GroupMap>;
viewGroups?: ViewGroup[];
onGroupClick?: (filter: ViewGroup) => void;
}
const CollectionControls = ({
@ -61,20 +61,26 @@ const CollectionControls = ({
return (
<CollectionControlsContainer>
<ViewStyleControl viewStyle={viewStyle} onChangeViewStyle={onChangeViewStyle} />
{viewGroups.length > 0 && (
<GroupControl viewGroups={viewGroups} onGroupClick={onGroupClick} t={t} group={group} />
)}
{viewFilters.length > 0 && (
<FilterControl
viewFilters={viewFilters}
onFilterClick={onFilterClick}
t={t}
filter={filter}
/>
)}
{sortableFields.length > 0 && (
<SortControl fields={sortableFields} sort={sort} onSortClick={onSortClick} />
)}
{viewGroups && onGroupClick && group
? viewGroups.length > 0 && (
<GroupControl viewGroups={viewGroups} onGroupClick={onGroupClick} t={t} group={group} />
)
: null}
{viewFilters && onFilterClick && filter
? viewFilters.length > 0 && (
<FilterControl
viewFilters={viewFilters}
onFilterClick={onFilterClick}
t={t}
filter={filter}
/>
)
: null}
{sortableFields && onSortClick && sort
? sortableFields.length > 0 && (
<SortControl fields={sortableFields} sort={sort} onSortClick={onSortClick} />
)
: null}
</CollectionControlsContainer>
);
};

View File

@ -1,37 +1,27 @@
import { styled } from '@mui/material/styles';
import SearchIcon from '@mui/icons-material/Search';
import InputAdornment from '@mui/material/InputAdornment';
import Popover from '@mui/material/Popover';
import { styled } from '@mui/material/styles';
import TextField from '@mui/material/TextField';
import React, { useCallback, useEffect, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { translate } from 'react-polyglot';
import { colors, colorsRaw, lengths } from '../../components/UI/styles';
import { transientOptions } from '../../lib';
import { colors, colorsRaw, lengths, zIndex } from '../../components/UI/styles';
import type { KeyboardEvent, MouseEvent } from 'react';
import type { ChangeEvent, FocusEvent, KeyboardEvent, MouseEvent } from 'react';
import type { Collection, Collections, TranslatedProps } from '../../interface';
const SearchContainer = styled('div')`
position: relative;
`;
const SuggestionsContainer = styled('div')`
position: relative;
width: 100%;
`;
const Suggestions = styled('ul')`
position: absolute;
top: 0px;
left: 0;
right: 0;
padding: 10px 0;
margin: 0;
list-style: none;
background-color: #fff;
border-radius: ${lengths.borderRadius};
border: 1px solid ${colors.textFieldBorder};
z-index: ${zIndex.zIndex1};
width: 240px;
`;
const SuggestionHeader = styled('li')`
@ -66,6 +56,10 @@ const SuggestionDivider = styled('div')`
width: 100%;
`;
const StyledPopover = styled(Popover)`
margin-left: -44px;
`;
interface CollectionSearchProps {
collections: Collections;
collection?: Collection;
@ -74,17 +68,34 @@ interface CollectionSearchProps {
}
const CollectionSearch = ({
collections,
collections: collectionsMap,
collection,
searchTerm,
onSubmit,
t,
}: TranslatedProps<CollectionSearchProps>) => {
const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement>();
const [query, setQuery] = useState(searchTerm);
const [suggestionsVisible, setSuggestionsVisible] = useState(false);
const [anchorEl, setAnchorEl] = useState<HTMLInputElement | HTMLTextAreaElement | null>(null);
const open = Boolean(anchorEl);
const collections = useMemo(() => Object.values(collectionsMap), [collectionsMap]);
const handleClose = useCallback(() => {
setAnchorEl(null);
inputRef.current?.blur();
}, []);
const handleFocus = useCallback((event: FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setAnchorEl(event.currentTarget);
}, []);
const handleBlur = useCallback(() => {
setAnchorEl(null);
}, []);
const getSelectedSelectionBasedOnProps = useCallback(() => {
return collection ? Object.keys(collections).indexOf(collection.name) : -1;
return collection ? collections.findIndex(c => c.name === collection.name) : -1;
}, [collection, collections]);
const [selectedCollectionIdx, setSelectedCollectionIdx] = useState(
@ -92,50 +103,62 @@ const CollectionSearch = ({
);
const [prevCollection, setPrevCollection] = useState(collection);
console.log('selectedCollectionIdx', selectedCollectionIdx);
useEffect(() => {
if (prevCollection !== collection) {
console.log(
'resetting to ',
getSelectedSelectionBasedOnProps(),
'collection',
collection,
'prev',
prevCollection,
);
setSelectedCollectionIdx(getSelectedSelectionBasedOnProps());
}
setPrevCollection(collection);
}, [collection, getSelectedSelectionBasedOnProps, prevCollection]);
const toggleSuggestions = useCallback((visible: boolean) => {
setSuggestionsVisible(visible);
}, []);
const selectNextSuggestion = useCallback(() => {
setSelectedCollectionIdx(
Math.min(selectedCollectionIdx + 1, Object.keys(collections).length - 1),
);
console.log('selectNextSuggestion');
setSelectedCollectionIdx(Math.min(selectedCollectionIdx + 1, collections.length - 1));
}, [collections, selectedCollectionIdx]);
const selectPreviousSuggestion = useCallback(() => {
console.log('selectPreviousSuggestion');
setSelectedCollectionIdx(Math.max(selectedCollectionIdx - 1, -1));
}, [selectedCollectionIdx]);
const resetSelectedSuggestion = useCallback(() => {
console.log('resetSelectedSuggestion');
setSelectedCollectionIdx(-1);
}, []);
const submitSearch = useCallback(() => {
toggleSuggestions(false);
if (selectedCollectionIdx !== -1) {
onSubmit(query, Object.values(collections)[selectedCollectionIdx]?.name);
} else {
onSubmit(query);
}
}, [collections, onSubmit, query, selectedCollectionIdx, toggleSuggestions]);
const submitSearch = useCallback(
(index: number) => {
console.log('searching selectedCollectionIdx', index);
if (index !== -1) {
console.log(collections, index);
onSubmit(query, collections[index]?.name);
} else {
onSubmit(query);
}
handleClose();
},
[collections, handleClose, onSubmit, query],
);
const handleKeyDown = useCallback(
(event: KeyboardEvent) => {
if (event.key === 'Enter') {
submitSearch();
submitSearch(selectedCollectionIdx);
}
if (suggestionsVisible) {
if (open) {
// allow closing of suggestions with escape key
if (event.key === 'Escape') {
toggleSuggestions(false);
handleClose();
}
if (event.key === 'ArrowDown') {
@ -148,30 +171,37 @@ const CollectionSearch = ({
}
},
[
handleClose,
open,
selectNextSuggestion,
selectPreviousSuggestion,
selectedCollectionIdx,
submitSearch,
suggestionsVisible,
toggleSuggestions,
],
);
const handleQueryChange = useCallback(
(newQuery: string) => {
(event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const newQuery = event.target.value;
setQuery(newQuery);
toggleSuggestions(newQuery !== '');
if (newQuery === '') {
resetSelectedSuggestion();
if (newQuery !== '') {
setAnchorEl(event.currentTarget);
return;
}
resetSelectedSuggestion();
handleClose();
},
[resetSelectedSuggestion, toggleSuggestions],
[handleClose, resetSelectedSuggestion],
);
const handleSuggestionClick = useCallback(
(event: MouseEvent, idx: number) => {
setSelectedCollectionIdx(idx);
submitSearch();
event.preventDefault();
console.log('clicked index', idx);
setSelectedCollectionIdx(idx);
submitSearch(idx);
},
[submitSearch],
);
@ -180,16 +210,16 @@ const CollectionSearch = ({
<SearchContainer>
<TextField
onKeyDown={handleKeyDown}
onClick={() => toggleSuggestions(true)}
placeholder={t('collection.sidebar.searchAll')}
onBlur={() => toggleSuggestions(false)}
onFocus={() => toggleSuggestions(query !== '')}
onBlur={handleBlur}
onFocus={handleFocus}
value={query}
onChange={e => handleQueryChange(e.target.value)}
onChange={handleQueryChange}
variant="outlined"
size="small"
fullWidth
InputProps={{
inputRef,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
@ -197,31 +227,42 @@ const CollectionSearch = ({
),
}}
/>
{suggestionsVisible && (
<SuggestionsContainer>
<Suggestions>
<SuggestionHeader>{t('collection.sidebar.searchIn')}</SuggestionHeader>
<StyledPopover
id="search-popover"
open={open}
anchorEl={anchorEl}
onClose={handleClose}
disableAutoFocus
disableEnforceFocus
disableScrollLock
hideBackdrop
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
>
<Suggestions>
<SuggestionHeader>{t('collection.sidebar.searchIn')}</SuggestionHeader>
<SuggestionItem
$isActive={selectedCollectionIdx === -1}
onClick={e => handleSuggestionClick(e, -1)}
onMouseDown={e => e.preventDefault()}
>
{t('collection.sidebar.allCollections')}
</SuggestionItem>
<SuggestionDivider />
{collections.map((collection, idx) => (
<SuggestionItem
$isActive={selectedCollectionIdx === -1}
onClick={e => handleSuggestionClick(e, -1)}
key={idx}
$isActive={idx === selectedCollectionIdx}
onClick={e => handleSuggestionClick(e, idx)}
onMouseDown={e => e.preventDefault()}
>
{t('collection.sidebar.allCollections')}
{collection.label}
</SuggestionItem>
<SuggestionDivider />
{Object.values(collections).map((collection, idx) => (
<SuggestionItem
key={idx}
$isActive={idx === selectedCollectionIdx}
onClick={e => handleSuggestionClick(e, idx)}
onMouseDown={e => e.preventDefault()}
>
{collection.label}
</SuggestionItem>
))}
</Suggestions>
</SuggestionsContainer>
)}
))}
</Suggestions>
</StyledPopover>
</SearchContainer>
);
};

View File

@ -24,7 +24,7 @@ export interface BaseEntriesProps {
isFetching: boolean;
viewStyle: CollectionViewStyle;
cursor: Cursor;
handleCursorActions: (action: string) => void;
handleCursorActions?: (action: string) => void;
}
export interface SingleCollectionEntriesProps extends BaseEntriesProps {

View File

@ -1,5 +1,5 @@
import isEqual from 'lodash/isEqual';
import React, { useCallback, useEffect, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { connect } from 'react-redux';
import {
@ -11,6 +11,7 @@ import { selectSearchedEntries } from '../../../reducers';
import Entries from './Entries';
import type { ConnectedProps } from 'react-redux';
import type { CollectionViewStyle } from '../../../constants/collectionViews';
import type { Collections } from '../../../interface';
import type { RootState } from '../../../store';
@ -20,35 +21,24 @@ const EntriesSearch = ({
isFetching,
page,
searchTerm,
viewStyle,
searchEntries,
collectionNames,
clearSearch,
}: EntriesSearchProps) => {
const collectionNames = useMemo(() => Object.keys(collections), [collections]);
const getCursor = useCallback(() => {
return Cursor.create({
actions: Number.isNaN(page) ? [] : ['append_next'],
});
}, [page]);
const handleCursorActions = useCallback(
(action: string) => {
if (action === 'append_next') {
const nextPage = page + 1;
searchEntries(searchTerm, collectionNames, nextPage);
}
},
[collectionNames, page, searchEntries, searchTerm],
);
useEffect(() => {
searchEntries(searchTerm, collectionNames);
}, [collectionNames, searchEntries, searchTerm]);
useEffect(() => {
return () => {
clearSearch();
};
}, [clearSearch]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const [prevSearch, setPrevSearch] = useState('');
const [prevCollectionNames, setPrevCollectionNames] = useState<string[]>([]);
@ -61,16 +51,19 @@ const EntriesSearch = ({
setPrevSearch(searchTerm);
setPrevCollectionNames(collectionNames);
searchEntries(searchTerm, collectionNames);
}, [collectionNames, prevCollectionNames, prevSearch, searchEntries, searchTerm]);
setTimeout(() => {
searchEntries(searchTerm, collectionNames);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [collectionNames, prevCollectionNames, prevSearch, searchTerm]);
return (
<Entries
cursor={getCursor()}
handleCursorActions={handleCursorActions}
collections={collections}
entries={entries}
isFetching={isFetching}
viewStyle={viewStyle}
/>
);
};
@ -78,16 +71,16 @@ const EntriesSearch = ({
interface EntriesSearchOwnProps {
searchTerm: string;
collections: Collections;
viewStyle: CollectionViewStyle;
}
function mapStateToProps(state: RootState, ownProps: EntriesSearchOwnProps) {
const { searchTerm } = ownProps;
const collections = Object.values(ownProps.collections);
const collectionNames = Object.keys(ownProps.collections);
const { searchTerm, collections, viewStyle } = ownProps;
const collectionNames = Object.keys(collections);
const isFetching = state.search.isFetching;
const page = state.search.page;
const entries = selectSearchedEntries(state, collectionNames);
return { isFetching, page, collections, collectionNames, entries, searchTerm };
return { isFetching, page, collections, viewStyle, entries, searchTerm };
}
const mapDispatchToProps = {

View File

@ -41,7 +41,7 @@ export interface BaseEntryListingProps {
entries: Entry[];
viewStyle: CollectionViewStyle;
cursor?: Cursor;
handleCursorActions: (action: string) => void;
handleCursorActions?: (action: string) => void;
page?: number;
}
@ -65,14 +65,11 @@ const EntryListing = ({
handleCursorActions,
...otherProps
}: EntryListingProps) => {
const hasMore = useMemo(() => {
const hasMore = cursor?.actions?.has('append_next');
return hasMore;
}, [cursor?.actions]);
const hasMore = useMemo(() => cursor?.actions?.has('append_next'), [cursor?.actions]);
const handleLoadMore = useCallback(() => {
if (hasMore) {
handleCursorActions('append_next');
handleCursorActions?.('append_next');
}
}, [handleCursorActions, hasMore]);
@ -138,7 +135,7 @@ const EntryListing = ({
<div>
<CardsGrid $layout={viewStyle}>
{renderedCards}
{hasMore && <Waypoint key={page} onEnter={handleLoadMore} />}
{hasMore && handleLoadMore && <Waypoint key={page} onEnter={handleLoadMore} />}
</CardsGrid>
</div>
);

View File

@ -1,6 +1,5 @@
import React, { useMemo } from 'react';
import React, { memo } from 'react';
import type { ReactNode } from 'react';
import type { TemplatePreviewComponent, TemplatePreviewProps } from '../../../interface';
interface EditorPreviewContentProps {
@ -8,17 +7,16 @@ interface EditorPreviewContentProps {
previewProps: TemplatePreviewProps;
}
const EditorPreviewContent = ({ previewComponent, previewProps }: EditorPreviewContentProps) => {
return useMemo(() => {
let children: ReactNode;
const EditorPreviewContent = memo(
({ previewComponent, previewProps }: EditorPreviewContentProps) => {
if (!previewComponent) {
children = null;
} else {
children = React.createElement(previewComponent, previewProps);
return null;
}
return children;
}, [previewComponent, previewProps]);
};
return React.createElement(previewComponent, previewProps);
},
);
EditorPreviewContent.displayName = 'EditorPreviewContent';
export default EditorPreviewContent;

View File

@ -29,6 +29,7 @@ import type {
Field,
GetAssetFunction,
ListField,
ObjectValue,
RenderedField,
TemplatePreviewProps,
TranslatedProps,
@ -367,36 +368,92 @@ const PreviewPane = (props: TranslatedProps<EditorPreviewPaneProps>) => {
*/
const widgetsFor = useCallback(
(name: string) => {
const field = fields.find(f => f.name === name);
if (!field) {
const cmsConfig = config.config;
if (!cmsConfig) {
return {
data: null,
widgets: {},
};
}
const nestedFields = field && 'fields' in field ? field.fields ?? [] : [];
const value = entry.data?.[field.name] as EntryData | EntryData[];
const field = fields.find(f => f.name === name);
if (!field || !('fields' in field)) {
return {
data: null,
widgets: {},
};
}
if (Array.isArray(value)) {
return value.map(val => {
const widgets = nestedFields.reduce((acc, field, index) => {
acc[field.name] = <div key={index}>{widgetFor(field.name)}</div>;
return acc;
}, {} as Record<string, ReactNode>);
return { data: val, widgets };
});
const value = entry.data?.[field.name];
const nestedFields = field && 'fields' in field ? field.fields ?? [] : [];
if (field.widget === 'list' || Array.isArray(value)) {
let finalValue: ObjectValue[];
if (!value || typeof value !== 'object') {
finalValue = [];
} else if (!Array.isArray(value)) {
finalValue = [value];
} else {
finalValue = value as ObjectValue[];
}
return finalValue
.filter((val: unknown) => typeof val === 'object')
.map((val: ObjectValue) => {
const widgets = nestedFields.reduce((acc, field, index) => {
acc[field.name] = (
<div key={index}>
{getWidgetFor(
cmsConfig,
collection,
field.name,
fields,
entry,
inferedFields,
handleGetAsset,
nestedFields,
val,
index,
)}
</div>
);
return acc;
}, {} as Record<string, ReactNode>);
return { data: val, widgets };
});
}
if (typeof value !== 'object') {
return {
data: {},
widgets: {},
};
}
return {
data: value,
widgets: nestedFields.reduce((acc, field, index) => {
acc[field.name] = <div key={index}>{widgetFor(field.name)}</div>;
acc[field.name] = (
<div key={index}>
{getWidgetFor(
cmsConfig,
collection,
field.name,
fields,
entry,
inferedFields,
handleGetAsset,
nestedFields,
value,
index,
)}
</div>
);
return acc;
}, {} as Record<string, ReactNode>),
};
},
[entry.data, fields, widgetFor],
[collection, config.config, entry, fields, handleGetAsset, inferedFields],
);
const previewStyles = useMemo(

View File

@ -19,7 +19,7 @@ const knownMetaKeys = [
];
function filterUnknownMetaKeys(meta: Record<string, unknown>) {
return Object.keys(meta).reduce((acc, k) => {
return Object.keys(meta ?? {}).reduce((acc, k) => {
if (knownMetaKeys.includes(k)) {
acc[k] = meta[k];
}

View File

@ -13,7 +13,6 @@ import parse from 'date-fns/parse';
import parseISO from 'date-fns/parseISO';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import alert from '../../components/UI/Alert';
import { isNotEmpty } from '../../lib/util/string.util';
import type { MouseEvent } from 'react';
@ -169,16 +168,7 @@ const DateTimeControl = ({
return null;
}
let formattedValue = defaultValue;
try {
formattedValue = formatDate(field.picker_utc ? utcDate : dateValue, inputFormat);
} catch (e) {
alert({
title: 'editor.editorWidgets.datetime.invalidDateTitle',
body: 'editor.editorWidgets.datetime.invalidDateBody',
});
console.error(e);
}
const inputDate = field.picker_utc ? utcDate : dateValue;
if (dateFormat && !timeFormat) {
return (
@ -186,7 +176,7 @@ const DateTimeControl = ({
key="mobile-date-picker"
inputFormat={inputFormat}
label={label}
value={formattedValue}
value={inputDate}
onChange={handleChange}
renderInput={params => (
<TextField
@ -216,7 +206,7 @@ const DateTimeControl = ({
key="time-picker"
label={label}
inputFormat={inputFormat}
value={formattedValue}
value={inputDate}
onChange={handleChange}
renderInput={params => (
<TextField
@ -245,7 +235,7 @@ const DateTimeControl = ({
key="mobile-date-time-picker"
inputFormat={inputFormat}
label={label}
value={formattedValue}
value={inputDate}
onChange={handleChange}
renderInput={params => (
<TextField
@ -270,7 +260,6 @@ const DateTimeControl = ({
}, [
dateFormat,
dateValue,
defaultValue,
field.picker_utc,
handleChange,
hasErrors,