fix: various fixes and tweaks (#701)
This commit is contained in:
parent
422b7798da
commit
364612e9ae
@ -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
|
||||||
|
@ -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/**/*"
|
||||||
],
|
],
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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));
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
@ -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>
|
||||||
|
@ -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) => {
|
||||||
|
@ -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}
|
||||||
|
@ -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;
|
||||||
|
@ -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}
|
||||||
|
@ -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>;
|
||||||
|
|
||||||
|
@ -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]);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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] ?? [];
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
|
@ -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) {
|
||||||
|
@ -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 (
|
||||||
|
@ -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}
|
||||||
</>
|
</>
|
||||||
|
@ -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',
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user