fix: various fixes and tweaks (#701)
This commit is contained in:
parent
422b7798da
commit
364612e9ae
@ -515,6 +515,11 @@ collections:
|
||||
widget: markdown
|
||||
pattern: ['# [a-zA-Z0-9]+', 'Must have a header']
|
||||
required: false
|
||||
- name: folder_support
|
||||
label: Folder Support
|
||||
widget: markdown
|
||||
media_library:
|
||||
folder_support: true
|
||||
- name: number
|
||||
label: Number
|
||||
file: _widgets/number.json
|
||||
|
@ -33,7 +33,7 @@
|
||||
"type-check": "tsc --watch"
|
||||
},
|
||||
"main": "dist/static-cms-core.js",
|
||||
"types": "dist/src/index.d.ts",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist/**/*"
|
||||
],
|
||||
|
@ -481,10 +481,11 @@ export function changeDraftFieldValidation(
|
||||
path: string,
|
||||
errors: FieldError[],
|
||||
i18n?: I18nSettings,
|
||||
isMeta?: boolean,
|
||||
) {
|
||||
return {
|
||||
type: DRAFT_VALIDATION_ERRORS,
|
||||
payload: { path, errors, i18n },
|
||||
payload: { path, errors, i18n, isMeta },
|
||||
} as const;
|
||||
}
|
||||
|
||||
|
@ -54,8 +54,6 @@ async function loadAsset(
|
||||
): Promise<AssetProxy> {
|
||||
try {
|
||||
dispatch(loadAssetRequest(resolvedPath));
|
||||
// load asset url from backend
|
||||
// await waitForMediaLibraryToLoad(dispatch, getState());
|
||||
const { url } = await getMediaFile(getState(), resolvedPath);
|
||||
const asset = createAssetProxy({ path: resolvedPath, url });
|
||||
dispatch(addAsset(asset));
|
||||
|
@ -169,7 +169,10 @@ export default class ProxyBackend implements BackendClass {
|
||||
async getMediaFile(path: string): Promise<ImplementationMediaFile> {
|
||||
const file = await this.request<MediaFile>({
|
||||
action: 'getMediaFile',
|
||||
params: { branch: this.branch, path },
|
||||
params: {
|
||||
branch: this.branch,
|
||||
path,
|
||||
},
|
||||
});
|
||||
return deserializeMediaFile(file);
|
||||
}
|
||||
|
@ -1,18 +1,43 @@
|
||||
import React from 'react';
|
||||
|
||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||
|
||||
import type { FC } from 'react';
|
||||
|
||||
export interface CircularProgressProps {
|
||||
className?: 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 (
|
||||
<div role="status" className={className} data-testid={dataTestId}>
|
||||
<svg
|
||||
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"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
@ -13,7 +13,7 @@ const TableCell = ({ columns, children }: TableCellProps) => {
|
||||
return (
|
||||
<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 ">
|
||||
<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>
|
||||
{columns.map((column, index) => (
|
||||
<TableHeaderCell key={index}>{column}</TableHeaderCell>
|
||||
|
@ -82,7 +82,10 @@ const EditorControl = ({
|
||||
|
||||
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 hasErrors = (submitted || dirty) && Boolean(errors.length);
|
||||
@ -103,11 +106,11 @@ const EditorControl = ({
|
||||
|
||||
const validateValue = async () => {
|
||||
const errors = await validate(field, value, widget, t);
|
||||
dispatch(changeDraftFieldValidation(path, errors, i18n));
|
||||
dispatch(changeDraftFieldValidation(path, errors, i18n, isMeta));
|
||||
};
|
||||
|
||||
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(
|
||||
(value: ValueOrNestedValue) => {
|
||||
|
@ -117,7 +117,7 @@ const EditorControlPane = ({
|
||||
<EditorControl
|
||||
key="entry-path"
|
||||
field={pathField}
|
||||
value={nestedFieldPath}
|
||||
value={entry.meta?.path ?? nestedFieldPath}
|
||||
fieldsErrors={fieldsErrors}
|
||||
submitted={submitted}
|
||||
locale={locale}
|
||||
|
@ -26,6 +26,7 @@ import type { FC, KeyboardEvent } from 'react';
|
||||
interface MediaLibraryCardProps<T extends MediaField, EF extends BaseField = UnknownField> {
|
||||
isSelected?: boolean;
|
||||
displayURL: MediaLibraryDisplayURL;
|
||||
path: string;
|
||||
text: string;
|
||||
draftText: 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>({
|
||||
isSelected = false,
|
||||
displayURL,
|
||||
path,
|
||||
text,
|
||||
draftText,
|
||||
type,
|
||||
@ -60,7 +62,7 @@ const MediaLibraryCard = <T extends MediaField, EF extends BaseField = UnknownFi
|
||||
t,
|
||||
}: TranslatedProps<MediaLibraryCardProps<T, EF>>) => {
|
||||
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 url = displayURL.url;
|
||||
|
@ -126,6 +126,7 @@ const CardWrapper = ({
|
||||
isDraft={file.draft}
|
||||
draftText={cardDraftText}
|
||||
displayURL={displayURLs[file.id] ?? (file.url ? { url: file.url } : {})}
|
||||
path={file.path}
|
||||
loadDisplayURL={() => loadDisplayURL(file)}
|
||||
type={file.type}
|
||||
isViewableImage={file.isViewableImage ?? false}
|
||||
|
@ -476,9 +476,9 @@ export abstract class BackendClass {
|
||||
|
||||
abstract getMediaDisplayURL(displayURL: DisplayURL): Promise<string>;
|
||||
abstract getMedia(
|
||||
folder?: string,
|
||||
mediaFolder?: string,
|
||||
folderSupport?: boolean,
|
||||
mediaPath?: string,
|
||||
publicFolder?: string,
|
||||
): Promise<ImplementationMediaFile[]>;
|
||||
abstract getMediaFile(path: string): Promise<ImplementationMediaFile>;
|
||||
|
||||
|
@ -8,11 +8,12 @@ export default function useHasChildErrors(
|
||||
path: string,
|
||||
fieldsErrors: FieldsErrors,
|
||||
i18n: I18nSettings | undefined,
|
||||
isMeta: boolean | undefined,
|
||||
) {
|
||||
return useMemo(() => {
|
||||
const dataPath = getEntryDataPath(i18n);
|
||||
const dataPath = getEntryDataPath(i18n, isMeta);
|
||||
const fullPath = `${dataPath}.${path}`;
|
||||
|
||||
return Boolean(Object.keys(fieldsErrors).find(key => key.startsWith(fullPath)));
|
||||
}, [fieldsErrors, i18n, path]);
|
||||
}, [fieldsErrors, i18n, isMeta, path]);
|
||||
}
|
||||
|
@ -232,7 +232,7 @@ function traverseFields<EF extends BaseField>(
|
||||
export function selectMediaFolder<EF extends BaseField>(
|
||||
config: Config<EF>,
|
||||
collection: Collection<EF> | undefined | null,
|
||||
entryMap: Entry | null | undefined,
|
||||
entryMap: Entry | undefined | null,
|
||||
field: MediaField | undefined,
|
||||
currentFolder?: string,
|
||||
) {
|
||||
@ -257,9 +257,9 @@ export function selectMediaFolder<EF extends BaseField>(
|
||||
|
||||
export function selectMediaFilePublicPath<EF extends BaseField>(
|
||||
config: Config<EF>,
|
||||
collection: Collection<EF> | null,
|
||||
collection: Collection<EF> | undefined | null,
|
||||
mediaPath: string,
|
||||
entryMap: Entry | undefined,
|
||||
entryMap: Entry | undefined | null,
|
||||
field: MediaField | undefined,
|
||||
currentFolder?: string,
|
||||
) {
|
||||
@ -299,7 +299,7 @@ export function selectMediaFilePath(
|
||||
mediaPath: string,
|
||||
field: MediaField | undefined,
|
||||
currentFolder?: string,
|
||||
) {
|
||||
): string {
|
||||
if (isAbsolutePath(mediaPath)) {
|
||||
return mediaPath;
|
||||
}
|
||||
|
@ -185,10 +185,12 @@ function entryDraftReducer(
|
||||
}
|
||||
|
||||
case DRAFT_VALIDATION_ERRORS: {
|
||||
const { path, errors, i18n } = action.payload;
|
||||
const { path, errors, i18n, isMeta } = action.payload;
|
||||
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}`;
|
||||
|
||||
if (errors.length === 0) {
|
||||
|
@ -3,13 +3,16 @@ import { getDataPath } from '@staticcms/core/lib/i18n';
|
||||
import type { I18nSettings } from '@staticcms/core/interface';
|
||||
import type { RootState } from '@staticcms/core/store';
|
||||
|
||||
export const getEntryDataPath = (i18n: I18nSettings | undefined) => {
|
||||
return (i18n && getDataPath(i18n.currentLocale, i18n.defaultLocale)) || ['data'];
|
||||
export const getEntryDataPath = (i18n: I18nSettings | undefined, isMeta: boolean | undefined) => {
|
||||
return isMeta
|
||||
? ['meta']
|
||||
: (i18n && getDataPath(i18n.currentLocale, i18n.defaultLocale)) || ['data'];
|
||||
};
|
||||
|
||||
export const selectFieldErrors =
|
||||
(path: string, i18n: I18nSettings | undefined) => (state: RootState) => {
|
||||
const dataPath = getEntryDataPath(i18n);
|
||||
(path: string, i18n: I18nSettings | undefined, isMeta: boolean | undefined) =>
|
||||
(state: RootState) => {
|
||||
const dataPath = getEntryDataPath(i18n, isMeta);
|
||||
const fullPath = `${dataPath.join('.')}.${path}`;
|
||||
return state.entryDraft.fieldsErrors[fullPath] ?? [];
|
||||
};
|
||||
|
@ -293,7 +293,7 @@ const ListControl: FC<WidgetControlProps<ValueOrNestedValue[], ListField>> = ({
|
||||
[onChange, internalValue, keys],
|
||||
);
|
||||
|
||||
const hasChildErrors = useHasChildErrors(path, fieldsErrors, i18n);
|
||||
const hasChildErrors = useHasChildErrors(path, fieldsErrors, i18n, false);
|
||||
|
||||
if (valueType === null) {
|
||||
return null;
|
||||
|
@ -165,7 +165,7 @@ const ListItem: FC<ListItemProps> = ({
|
||||
}
|
||||
}, [entry, field, index, value, valueType]);
|
||||
|
||||
const hasChildErrors = useHasChildErrors(path, fieldsErrors, i18n);
|
||||
const hasChildErrors = useHasChildErrors(path, fieldsErrors, i18n, false);
|
||||
|
||||
const finalValue = useMemo(() => {
|
||||
if (field.fields && field.fields.length === 1) {
|
||||
|
@ -31,7 +31,7 @@ const ObjectControl: FC<WidgetControlProps<ObjectValue, ObjectField>> = ({
|
||||
|
||||
const fields = useMemo(() => field.fields, [field.fields]);
|
||||
|
||||
const hasChildErrors = useHasChildErrors(path, fieldsErrors, i18n);
|
||||
const hasChildErrors = useHasChildErrors(path, fieldsErrors, i18n, false);
|
||||
|
||||
const renderedField = useMemo(() => {
|
||||
return (
|
||||
|
@ -179,11 +179,15 @@ const RelationControl: FC<WidgetControlProps<string | string[], RelationField>>
|
||||
);
|
||||
|
||||
const [options, setOptions] = useState<HitOption[]>([]);
|
||||
const [entries, setEntries] = useState<Entry[]>([]);
|
||||
const loading = useMemo(() => options.length === 0, [options.length]);
|
||||
const [entries, setEntries] = useState<Entry[] | null>(null);
|
||||
const loading = useMemo(() => !entries, [entries]);
|
||||
|
||||
const filterOptions = useCallback(
|
||||
(inputValue: string) => {
|
||||
if (!entries) {
|
||||
return;
|
||||
}
|
||||
|
||||
const searchFields = field.search_fields;
|
||||
const limit = field.options_length || DEFAULT_OPTIONS_LIMIT;
|
||||
const expandedEntries = expandSearchEntries(entries, searchFields);
|
||||
@ -334,6 +338,7 @@ const RelationControl: FC<WidgetControlProps<string | string[], RelationField>>
|
||||
key="loading-indicator"
|
||||
className="absolute inset-y-0 right-4 flex items-center pr-2"
|
||||
data-testid="relation-loading-indicator"
|
||||
size="small"
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
|
@ -306,6 +306,21 @@ describe(RelationControl.name, () => {
|
||||
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', () => {
|
||||
const field: RelationField = {
|
||||
label: 'Relation',
|
||||
|
@ -515,6 +515,11 @@ collections:
|
||||
widget: markdown
|
||||
pattern: ['# [a-zA-Z0-9]+', 'Must have a header']
|
||||
required: false
|
||||
- name: folder_support
|
||||
label: Folder Support
|
||||
widget: markdown
|
||||
media_library:
|
||||
folder_support: true
|
||||
- name: number
|
||||
label: Number
|
||||
file: _widgets/number.json
|
||||
|
Loading…
x
Reference in New Issue
Block a user