fix: various fixes and tweaks (#701)

This commit is contained in:
Daniel Lautzenheiser 2023-04-14 13:52:11 -04:00 committed by GitHub
parent 422b7798da
commit 364612e9ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 101 additions and 32 deletions

View File

@ -515,6 +515,11 @@ collections:
widget: markdown widget: markdown
pattern: ['# [a-zA-Z0-9]+', 'Must have a header'] pattern: ['# [a-zA-Z0-9]+', 'Must have a header']
required: false required: false
- name: folder_support
label: Folder Support
widget: markdown
media_library:
folder_support: true
- name: number - name: number
label: Number label: Number
file: _widgets/number.json file: _widgets/number.json

View File

@ -33,7 +33,7 @@
"type-check": "tsc --watch" "type-check": "tsc --watch"
}, },
"main": "dist/static-cms-core.js", "main": "dist/static-cms-core.js",
"types": "dist/src/index.d.ts", "types": "dist/index.d.ts",
"files": [ "files": [
"dist/**/*" "dist/**/*"
], ],

View File

@ -481,10 +481,11 @@ export function changeDraftFieldValidation(
path: string, path: string,
errors: FieldError[], errors: FieldError[],
i18n?: I18nSettings, i18n?: I18nSettings,
isMeta?: boolean,
) { ) {
return { return {
type: DRAFT_VALIDATION_ERRORS, type: DRAFT_VALIDATION_ERRORS,
payload: { path, errors, i18n }, payload: { path, errors, i18n, isMeta },
} as const; } as const;
} }

View File

@ -54,8 +54,6 @@ async function loadAsset(
): Promise<AssetProxy> { ): Promise<AssetProxy> {
try { try {
dispatch(loadAssetRequest(resolvedPath)); dispatch(loadAssetRequest(resolvedPath));
// load asset url from backend
// await waitForMediaLibraryToLoad(dispatch, getState());
const { url } = await getMediaFile(getState(), resolvedPath); const { url } = await getMediaFile(getState(), resolvedPath);
const asset = createAssetProxy({ path: resolvedPath, url }); const asset = createAssetProxy({ path: resolvedPath, url });
dispatch(addAsset(asset)); dispatch(addAsset(asset));

View File

@ -169,7 +169,10 @@ export default class ProxyBackend implements BackendClass {
async getMediaFile(path: string): Promise<ImplementationMediaFile> { async getMediaFile(path: string): Promise<ImplementationMediaFile> {
const file = await this.request<MediaFile>({ const file = await this.request<MediaFile>({
action: 'getMediaFile', action: 'getMediaFile',
params: { branch: this.branch, path }, params: {
branch: this.branch,
path,
},
}); });
return deserializeMediaFile(file); return deserializeMediaFile(file);
} }

View File

@ -1,18 +1,43 @@
import React from 'react'; import React from 'react';
import classNames from '@staticcms/core/lib/util/classNames.util';
import type { FC } from 'react'; import type { FC } from 'react';
export interface CircularProgressProps { export interface CircularProgressProps {
className?: string; className?: string;
'data-testid'?: string; 'data-testid'?: string;
size?: 'small' | 'medium';
} }
const CircularProgress: FC<CircularProgressProps> = ({ className, 'data-testid': dataTestId }) => { const CircularProgress: FC<CircularProgressProps> = ({
className,
'data-testid': dataTestId,
size = 'medium',
}) => {
return ( return (
<div role="status" className={className} data-testid={dataTestId}> <div role="status" className={className} data-testid={dataTestId}>
<svg <svg
aria-hidden="true" aria-hidden="true"
className="w-8 h-8 mr-2 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600" className={classNames(
`
mr-2
text-gray-200
animate-spin
dark:text-gray-600
fill-blue-600
`,
size === 'medium' &&
`
w-8
h-8
`,
size === 'small' &&
`
w-5
h-5
`,
)}
viewBox="0 0 100 101" viewBox="0 0 100 101"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"

View File

@ -13,7 +13,7 @@ const TableCell = ({ columns, children }: TableCellProps) => {
return ( return (
<div className="relative overflow-x-auto shadow-md sm:rounded-lg border border-slate-200 dark:border-gray-700"> <div className="relative overflow-x-auto shadow-md sm:rounded-lg border border-slate-200 dark:border-gray-700">
<table className="w-full text-sm text-left text-gray-500 dark:text-gray-300 "> <table className="w-full text-sm text-left text-gray-500 dark:text-gray-300 ">
<thead className="text-xs text-gray-700 bg-slate-50 dark:bg-slate-700 dark:text-gray-300"> <thead className="text-xs text-gray-700 bg-gray-100 dark:bg-slate-700 dark:text-gray-300">
<tr> <tr>
{columns.map((column, index) => ( {columns.map((column, index) => (
<TableHeaderCell key={index}>{column}</TableHeaderCell> <TableHeaderCell key={index}>{column}</TableHeaderCell>

View File

@ -82,7 +82,10 @@ const EditorControl = ({
const [dirty, setDirty] = useState(!isEmpty(value)); const [dirty, setDirty] = useState(!isEmpty(value));
const fieldErrorsSelector = useMemo(() => selectFieldErrors(path, i18n), [i18n, path]); const fieldErrorsSelector = useMemo(
() => selectFieldErrors(path, i18n, isMeta),
[i18n, isMeta, path],
);
const errors = useAppSelector(fieldErrorsSelector); const errors = useAppSelector(fieldErrorsSelector);
const hasErrors = (submitted || dirty) && Boolean(errors.length); const hasErrors = (submitted || dirty) && Boolean(errors.length);
@ -103,11 +106,11 @@ const EditorControl = ({
const validateValue = async () => { const validateValue = async () => {
const errors = await validate(field, value, widget, t); const errors = await validate(field, value, widget, t);
dispatch(changeDraftFieldValidation(path, errors, i18n)); dispatch(changeDraftFieldValidation(path, errors, i18n, isMeta));
}; };
validateValue(); validateValue();
}, [dirty, dispatch, field, i18n, hidden, path, submitted, t, value, widget, disabled]); }, [dirty, dispatch, field, i18n, hidden, path, submitted, t, value, widget, disabled, isMeta]);
const handleChangeDraftField = useCallback( const handleChangeDraftField = useCallback(
(value: ValueOrNestedValue) => { (value: ValueOrNestedValue) => {

View File

@ -117,7 +117,7 @@ const EditorControlPane = ({
<EditorControl <EditorControl
key="entry-path" key="entry-path"
field={pathField} field={pathField}
value={nestedFieldPath} value={entry.meta?.path ?? nestedFieldPath}
fieldsErrors={fieldsErrors} fieldsErrors={fieldsErrors}
submitted={submitted} submitted={submitted}
locale={locale} locale={locale}

View File

@ -26,6 +26,7 @@ import type { FC, KeyboardEvent } from 'react';
interface MediaLibraryCardProps<T extends MediaField, EF extends BaseField = UnknownField> { interface MediaLibraryCardProps<T extends MediaField, EF extends BaseField = UnknownField> {
isSelected?: boolean; isSelected?: boolean;
displayURL: MediaLibraryDisplayURL; displayURL: MediaLibraryDisplayURL;
path: string;
text: string; text: string;
draftText: string; draftText: string;
type?: string; type?: string;
@ -44,6 +45,7 @@ interface MediaLibraryCardProps<T extends MediaField, EF extends BaseField = Unk
const MediaLibraryCard = <T extends MediaField, EF extends BaseField = UnknownField>({ const MediaLibraryCard = <T extends MediaField, EF extends BaseField = UnknownField>({
isSelected = false, isSelected = false,
displayURL, displayURL,
path,
text, text,
draftText, draftText,
type, type,
@ -60,7 +62,7 @@ const MediaLibraryCard = <T extends MediaField, EF extends BaseField = UnknownFi
t, t,
}: TranslatedProps<MediaLibraryCardProps<T, EF>>) => { }: TranslatedProps<MediaLibraryCardProps<T, EF>>) => {
const entry = useAppSelector(selectEditingDraft); const entry = useAppSelector(selectEditingDraft);
const url = useMediaAsset(displayURL.url, collection, field, entry, currentFolder); const url = useMediaAsset(path, collection, field, entry, currentFolder);
const handleDownload = useCallback(() => { const handleDownload = useCallback(() => {
const url = displayURL.url; const url = displayURL.url;

View File

@ -126,6 +126,7 @@ const CardWrapper = ({
isDraft={file.draft} isDraft={file.draft}
draftText={cardDraftText} draftText={cardDraftText}
displayURL={displayURLs[file.id] ?? (file.url ? { url: file.url } : {})} displayURL={displayURLs[file.id] ?? (file.url ? { url: file.url } : {})}
path={file.path}
loadDisplayURL={() => loadDisplayURL(file)} loadDisplayURL={() => loadDisplayURL(file)}
type={file.type} type={file.type}
isViewableImage={file.isViewableImage ?? false} isViewableImage={file.isViewableImage ?? false}

View File

@ -476,9 +476,9 @@ export abstract class BackendClass {
abstract getMediaDisplayURL(displayURL: DisplayURL): Promise<string>; abstract getMediaDisplayURL(displayURL: DisplayURL): Promise<string>;
abstract getMedia( abstract getMedia(
folder?: string, mediaFolder?: string,
folderSupport?: boolean, folderSupport?: boolean,
mediaPath?: string, publicFolder?: string,
): Promise<ImplementationMediaFile[]>; ): Promise<ImplementationMediaFile[]>;
abstract getMediaFile(path: string): Promise<ImplementationMediaFile>; abstract getMediaFile(path: string): Promise<ImplementationMediaFile>;

View File

@ -8,11 +8,12 @@ export default function useHasChildErrors(
path: string, path: string,
fieldsErrors: FieldsErrors, fieldsErrors: FieldsErrors,
i18n: I18nSettings | undefined, i18n: I18nSettings | undefined,
isMeta: boolean | undefined,
) { ) {
return useMemo(() => { return useMemo(() => {
const dataPath = getEntryDataPath(i18n); const dataPath = getEntryDataPath(i18n, isMeta);
const fullPath = `${dataPath}.${path}`; const fullPath = `${dataPath}.${path}`;
return Boolean(Object.keys(fieldsErrors).find(key => key.startsWith(fullPath))); return Boolean(Object.keys(fieldsErrors).find(key => key.startsWith(fullPath)));
}, [fieldsErrors, i18n, path]); }, [fieldsErrors, i18n, isMeta, path]);
} }

View File

@ -232,7 +232,7 @@ function traverseFields<EF extends BaseField>(
export function selectMediaFolder<EF extends BaseField>( export function selectMediaFolder<EF extends BaseField>(
config: Config<EF>, config: Config<EF>,
collection: Collection<EF> | undefined | null, collection: Collection<EF> | undefined | null,
entryMap: Entry | null | undefined, entryMap: Entry | undefined | null,
field: MediaField | undefined, field: MediaField | undefined,
currentFolder?: string, currentFolder?: string,
) { ) {
@ -257,9 +257,9 @@ export function selectMediaFolder<EF extends BaseField>(
export function selectMediaFilePublicPath<EF extends BaseField>( export function selectMediaFilePublicPath<EF extends BaseField>(
config: Config<EF>, config: Config<EF>,
collection: Collection<EF> | null, collection: Collection<EF> | undefined | null,
mediaPath: string, mediaPath: string,
entryMap: Entry | undefined, entryMap: Entry | undefined | null,
field: MediaField | undefined, field: MediaField | undefined,
currentFolder?: string, currentFolder?: string,
) { ) {
@ -299,7 +299,7 @@ export function selectMediaFilePath(
mediaPath: string, mediaPath: string,
field: MediaField | undefined, field: MediaField | undefined,
currentFolder?: string, currentFolder?: string,
) { ): string {
if (isAbsolutePath(mediaPath)) { if (isAbsolutePath(mediaPath)) {
return mediaPath; return mediaPath;
} }

View File

@ -185,10 +185,12 @@ function entryDraftReducer(
} }
case DRAFT_VALIDATION_ERRORS: { case DRAFT_VALIDATION_ERRORS: {
const { path, errors, i18n } = action.payload; const { path, errors, i18n, isMeta } = action.payload;
const fieldsErrors = { ...state.fieldsErrors }; const fieldsErrors = { ...state.fieldsErrors };
const dataPath = (i18n && getDataPath(i18n.currentLocale, i18n.defaultLocale)) || ['data']; const dataPath = isMeta
? ['meta']
: (i18n && getDataPath(i18n.currentLocale, i18n.defaultLocale)) || ['data'];
const fullPath = `${dataPath.join('.')}.${path}`; const fullPath = `${dataPath.join('.')}.${path}`;
if (errors.length === 0) { if (errors.length === 0) {

View File

@ -3,13 +3,16 @@ import { getDataPath } from '@staticcms/core/lib/i18n';
import type { I18nSettings } from '@staticcms/core/interface'; import type { I18nSettings } from '@staticcms/core/interface';
import type { RootState } from '@staticcms/core/store'; import type { RootState } from '@staticcms/core/store';
export const getEntryDataPath = (i18n: I18nSettings | undefined) => { export const getEntryDataPath = (i18n: I18nSettings | undefined, isMeta: boolean | undefined) => {
return (i18n && getDataPath(i18n.currentLocale, i18n.defaultLocale)) || ['data']; return isMeta
? ['meta']
: (i18n && getDataPath(i18n.currentLocale, i18n.defaultLocale)) || ['data'];
}; };
export const selectFieldErrors = export const selectFieldErrors =
(path: string, i18n: I18nSettings | undefined) => (state: RootState) => { (path: string, i18n: I18nSettings | undefined, isMeta: boolean | undefined) =>
const dataPath = getEntryDataPath(i18n); (state: RootState) => {
const dataPath = getEntryDataPath(i18n, isMeta);
const fullPath = `${dataPath.join('.')}.${path}`; const fullPath = `${dataPath.join('.')}.${path}`;
return state.entryDraft.fieldsErrors[fullPath] ?? []; return state.entryDraft.fieldsErrors[fullPath] ?? [];
}; };

View File

@ -293,7 +293,7 @@ const ListControl: FC<WidgetControlProps<ValueOrNestedValue[], ListField>> = ({
[onChange, internalValue, keys], [onChange, internalValue, keys],
); );
const hasChildErrors = useHasChildErrors(path, fieldsErrors, i18n); const hasChildErrors = useHasChildErrors(path, fieldsErrors, i18n, false);
if (valueType === null) { if (valueType === null) {
return null; return null;

View File

@ -165,7 +165,7 @@ const ListItem: FC<ListItemProps> = ({
} }
}, [entry, field, index, value, valueType]); }, [entry, field, index, value, valueType]);
const hasChildErrors = useHasChildErrors(path, fieldsErrors, i18n); const hasChildErrors = useHasChildErrors(path, fieldsErrors, i18n, false);
const finalValue = useMemo(() => { const finalValue = useMemo(() => {
if (field.fields && field.fields.length === 1) { if (field.fields && field.fields.length === 1) {

View File

@ -31,7 +31,7 @@ const ObjectControl: FC<WidgetControlProps<ObjectValue, ObjectField>> = ({
const fields = useMemo(() => field.fields, [field.fields]); const fields = useMemo(() => field.fields, [field.fields]);
const hasChildErrors = useHasChildErrors(path, fieldsErrors, i18n); const hasChildErrors = useHasChildErrors(path, fieldsErrors, i18n, false);
const renderedField = useMemo(() => { const renderedField = useMemo(() => {
return ( return (

View File

@ -179,11 +179,15 @@ const RelationControl: FC<WidgetControlProps<string | string[], RelationField>>
); );
const [options, setOptions] = useState<HitOption[]>([]); const [options, setOptions] = useState<HitOption[]>([]);
const [entries, setEntries] = useState<Entry[]>([]); const [entries, setEntries] = useState<Entry[] | null>(null);
const loading = useMemo(() => options.length === 0, [options.length]); const loading = useMemo(() => !entries, [entries]);
const filterOptions = useCallback( const filterOptions = useCallback(
(inputValue: string) => { (inputValue: string) => {
if (!entries) {
return;
}
const searchFields = field.search_fields; const searchFields = field.search_fields;
const limit = field.options_length || DEFAULT_OPTIONS_LIMIT; const limit = field.options_length || DEFAULT_OPTIONS_LIMIT;
const expandedEntries = expandSearchEntries(entries, searchFields); const expandedEntries = expandSearchEntries(entries, searchFields);
@ -334,6 +338,7 @@ const RelationControl: FC<WidgetControlProps<string | string[], RelationField>>
key="loading-indicator" key="loading-indicator"
className="absolute inset-y-0 right-4 flex items-center pr-2" className="absolute inset-y-0 right-4 flex items-center pr-2"
data-testid="relation-loading-indicator" data-testid="relation-loading-indicator"
size="small"
/> />
) : null} ) : null}
</> </>

View File

@ -306,6 +306,21 @@ describe(RelationControl.name, () => {
expect(queryByTestId('relation-loading-indicator')).not.toBeInTheDocument(); expect(queryByTestId('relation-loading-indicator')).not.toBeInTheDocument();
}); });
it('should stop showing loading indicator if no entries found', async () => {
mockListAllEntries.mockReturnValue([]);
const { getByTestId, queryByTestId } = renderControl({ value: 'Post 1' });
const input = getByTestId('autocomplete-input');
expect(input).toHaveValue('');
getByTestId('relation-loading-indicator');
await waitFor(() =>
expect(queryByTestId('relation-loading-indicator')).not.toBeInTheDocument(),
);
});
it('should not try to load entiries if search collection does not exist', () => { it('should not try to load entiries if search collection does not exist', () => {
const field: RelationField = { const field: RelationField = {
label: 'Relation', label: 'Relation',

View File

@ -515,6 +515,11 @@ collections:
widget: markdown widget: markdown
pattern: ['# [a-zA-Z0-9]+', 'Must have a header'] pattern: ['# [a-zA-Z0-9]+', 'Must have a header']
required: false required: false
- name: folder_support
label: Folder Support
widget: markdown
media_library:
folder_support: true
- name: number - name: number
label: Number label: Number
file: _widgets/number.json file: _widgets/number.json