Merge pull request #1835 from payloadcms/fix/1801
fix: upload field filterOptions
This commit is contained in:
@@ -67,7 +67,8 @@ const DrawerContent: React.FC<ListDrawerProps & {
|
|||||||
collectionSlugs,
|
collectionSlugs,
|
||||||
uploads,
|
uploads,
|
||||||
selectedCollection,
|
selectedCollection,
|
||||||
enabledCollectionConfigs
|
enabledCollectionConfigs,
|
||||||
|
filterOptions,
|
||||||
}) => {
|
}) => {
|
||||||
const { t, i18n } = useTranslation(['upload', 'general']);
|
const { t, i18n } = useTranslation(['upload', 'general']);
|
||||||
const { permissions } = useAuth();
|
const { permissions } = useAuth();
|
||||||
@@ -165,13 +166,20 @@ const DrawerContent: React.FC<ListDrawerProps & {
|
|||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
if (page) params.page = page;
|
if (page) params.page = page;
|
||||||
if (where) params.where = where;
|
|
||||||
|
params.where = {
|
||||||
|
...where ? { ...where } : {},
|
||||||
|
...filterOptions?.[selectedCollectionConfig.slug] ? {
|
||||||
|
...filterOptions[selectedCollectionConfig.slug],
|
||||||
|
} : {},
|
||||||
|
};
|
||||||
|
|
||||||
if (sort) params.sort = sort;
|
if (sort) params.sort = sort;
|
||||||
if (limit) params.limit = limit;
|
if (limit) params.limit = limit;
|
||||||
if (cacheBust) params.cacheBust = cacheBust;
|
if (cacheBust) params.cacheBust = cacheBust;
|
||||||
|
|
||||||
setParams(params);
|
setParams(params);
|
||||||
}, [setParams, page, sort, where, limit, cacheBust]);
|
}, [setParams, page, sort, where, limit, cacheBust, filterOptions, selectedCollectionConfig]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const syncColumnsFromPrefs = async () => {
|
const syncColumnsFromPrefs = async () => {
|
||||||
@@ -324,7 +332,7 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = (props) => {
|
|||||||
|
|
||||||
const [enabledCollectionConfigs] = useState(() => collections.filter((coll) => shouldIncludeCollection({ coll, uploads, collectionSlugs })));
|
const [enabledCollectionConfigs] = useState(() => collections.filter((coll) => shouldIncludeCollection({ coll, uploads, collectionSlugs })));
|
||||||
|
|
||||||
if (enabledCollectionConfigs.length === 0){
|
if (enabledCollectionConfigs.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,5 +341,5 @@ export const ListDrawerContent: React.FC<ListDrawerProps> = (props) => {
|
|||||||
{...props}
|
{...props}
|
||||||
enabledCollectionConfigs={enabledCollectionConfigs}
|
enabledCollectionConfigs={enabledCollectionConfigs}
|
||||||
/>
|
/>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -54,7 +54,12 @@ export const ListDrawer: React.FC<ListDrawerProps> = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useListDrawer: UseListDrawer = ({ collectionSlugs, uploads, selectedCollection }) => {
|
export const useListDrawer: UseListDrawer = ({
|
||||||
|
collectionSlugs,
|
||||||
|
uploads,
|
||||||
|
selectedCollection,
|
||||||
|
filterOptions,
|
||||||
|
}) => {
|
||||||
const drawerDepth = useEditDepth();
|
const drawerDepth = useEditDepth();
|
||||||
const uuid = useId();
|
const uuid = useId();
|
||||||
const { modalState, toggleModal, closeModal, openModal } = useModal();
|
const { modalState, toggleModal, closeModal, openModal } = useModal();
|
||||||
@@ -90,9 +95,10 @@ export const useListDrawer: UseListDrawer = ({ collectionSlugs, uploads, selecte
|
|||||||
closeDrawer={closeDrawer}
|
closeDrawer={closeDrawer}
|
||||||
key={drawerSlug}
|
key={drawerSlug}
|
||||||
selectedCollection={selectedCollection}
|
selectedCollection={selectedCollection}
|
||||||
|
filterOptions={filterOptions}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
}, [drawerSlug, collectionSlugs, uploads, closeDrawer, selectedCollection]);
|
}, [drawerSlug, collectionSlugs, uploads, closeDrawer, selectedCollection, filterOptions]);
|
||||||
|
|
||||||
const MemoizedDrawerToggler = useMemo(() => {
|
const MemoizedDrawerToggler = useMemo(() => {
|
||||||
return ((props) => (
|
return ((props) => (
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { HTMLAttributes } from 'react';
|
import React, { HTMLAttributes } from 'react';
|
||||||
import { SanitizedCollectionConfig } from '../../../../collections/config/types';
|
import { SanitizedCollectionConfig } from '../../../../collections/config/types';
|
||||||
|
import { FilterOptionsResult } from '../../forms/field-types/Relationship/types';
|
||||||
|
|
||||||
export type ListDrawerProps = {
|
export type ListDrawerProps = {
|
||||||
onSelect?: (args: {
|
onSelect?: (args: {
|
||||||
@@ -11,6 +12,7 @@ export type ListDrawerProps = {
|
|||||||
collectionSlugs?: string[]
|
collectionSlugs?: string[]
|
||||||
uploads?: boolean
|
uploads?: boolean
|
||||||
selectedCollection?: string
|
selectedCollection?: string
|
||||||
|
filterOptions?: FilterOptionsResult
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ListTogglerProps = HTMLAttributes<HTMLButtonElement> & {
|
export type ListTogglerProps = HTMLAttributes<HTMLButtonElement> & {
|
||||||
@@ -24,6 +26,7 @@ export type UseListDrawer = (args: {
|
|||||||
collectionSlugs?: string[]
|
collectionSlugs?: string[]
|
||||||
selectedCollection?: string
|
selectedCollection?: string
|
||||||
uploads?: boolean // finds all collections with upload: true
|
uploads?: boolean // finds all collections with upload: true
|
||||||
|
filterOptions?: FilterOptionsResult
|
||||||
}) => [
|
}) => [
|
||||||
React.FC<Omit<ListDrawerProps, 'collectionSlug' | 'id'>>, // drawer
|
React.FC<Omit<ListDrawerProps, 'collectionSlug' | 'id'>>, // drawer
|
||||||
React.FC<Omit<ListTogglerProps, 'collectionSlug' | 'id'>>, // toggler
|
React.FC<Omit<ListTogglerProps, 'collectionSlug' | 'id'>>, // toggler
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { useDebouncedCallback } from '../../../../hooks/useDebouncedCallback';
|
|||||||
import wordBoundariesRegex from '../../../../../utilities/wordBoundariesRegex';
|
import wordBoundariesRegex from '../../../../../utilities/wordBoundariesRegex';
|
||||||
import { AddNewRelation } from './AddNew';
|
import { AddNewRelation } from './AddNew';
|
||||||
import { findOptionsByValue } from './findOptionsByValue';
|
import { findOptionsByValue } from './findOptionsByValue';
|
||||||
import { GetFilterOptions } from './GetFilterOptions';
|
import { GetFilterOptions } from '../../../utilities/GetFilterOptions';
|
||||||
import { SingleValue } from './select-components/SingleValue';
|
import { SingleValue } from './select-components/SingleValue';
|
||||||
import { MultiValueLabel } from './select-components/MultiValueLabel';
|
import { MultiValueLabel } from './select-components/MultiValueLabel';
|
||||||
import { DocumentDrawerProps } from '../../../elements/DocumentDrawer/types';
|
import { DocumentDrawerProps } from '../../../elements/DocumentDrawer/types';
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import { useListDrawer } from '../../../elements/ListDrawer';
|
|||||||
import Button from '../../../elements/Button';
|
import Button from '../../../elements/Button';
|
||||||
import { DocumentDrawerProps } from '../../../elements/DocumentDrawer/types';
|
import { DocumentDrawerProps } from '../../../elements/DocumentDrawer/types';
|
||||||
import { ListDrawerProps } from '../../../elements/ListDrawer/types';
|
import { ListDrawerProps } from '../../../elements/ListDrawer/types';
|
||||||
|
import { GetFilterOptions } from '../../../utilities/GetFilterOptions';
|
||||||
|
import { FilterOptionsResult } from '../Relationship/types';
|
||||||
|
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
|
||||||
@@ -57,6 +59,7 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
|
|||||||
api = '/api',
|
api = '/api',
|
||||||
collection,
|
collection,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
|
filterOptions,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const { t, i18n } = useTranslation('fields');
|
const { t, i18n } = useTranslation('fields');
|
||||||
@@ -64,6 +67,7 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
|
|||||||
const [file, setFile] = useState(undefined);
|
const [file, setFile] = useState(undefined);
|
||||||
const [missingFile, setMissingFile] = useState(false);
|
const [missingFile, setMissingFile] = useState(false);
|
||||||
const [collectionSlugs] = useState([collection?.slug]);
|
const [collectionSlugs] = useState([collection?.slug]);
|
||||||
|
const [filterOptionsResult, setFilterOptionsResult] = useState<FilterOptionsResult>();
|
||||||
|
|
||||||
const [
|
const [
|
||||||
DocumentDrawer,
|
DocumentDrawer,
|
||||||
@@ -83,6 +87,7 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
|
|||||||
},
|
},
|
||||||
] = useListDrawer({
|
] = useListDrawer({
|
||||||
collectionSlugs,
|
collectionSlugs,
|
||||||
|
filterOptions: filterOptionsResult,
|
||||||
});
|
});
|
||||||
|
|
||||||
const classes = [
|
const classes = [
|
||||||
@@ -145,6 +150,15 @@ const UploadInput: React.FC<UploadInputProps> = (props) => {
|
|||||||
width,
|
width,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<GetFilterOptions
|
||||||
|
{...{
|
||||||
|
filterOptionsResult,
|
||||||
|
setFilterOptionsResult,
|
||||||
|
filterOptions,
|
||||||
|
path,
|
||||||
|
relationTo,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Error
|
<Error
|
||||||
showError={showError}
|
showError={showError}
|
||||||
message={errorMessage}
|
message={errorMessage}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import equal from 'deep-equal';
|
import equal from 'deep-equal';
|
||||||
import { FilterOptions } from '../../../../../../fields/config/types';
|
import { FilterOptions } from '../../../../fields/config/types';
|
||||||
import { useAuth } from '../../../../utilities/Auth';
|
import { useAuth } from '../Auth';
|
||||||
import { useDocumentInfo } from '../../../../utilities/DocumentInfo';
|
import { useDocumentInfo } from '../DocumentInfo';
|
||||||
import { useAllFormFields } from '../../../Form/context';
|
import { useAllFormFields } from '../../forms/Form/context';
|
||||||
import { getFilterOptionsQuery } from '../../getFilterOptionsQuery';
|
import { getFilterOptionsQuery } from '../../forms/field-types/getFilterOptionsQuery';
|
||||||
import { FilterOptionsResult } from '../types';
|
import { FilterOptionsResult } from '../../forms/field-types/Relationship/types';
|
||||||
import reduceFieldsToValues from '../../../Form/reduceFieldsToValues';
|
import reduceFieldsToValues from '../../forms/Form/reduceFieldsToValues';
|
||||||
import getSiblingData from '../../../Form/getSiblingData';
|
import getSiblingData from '../../forms/Form/getSiblingData';
|
||||||
|
|
||||||
type Args = {
|
type Args = {
|
||||||
filterOptions: FilterOptions
|
filterOptions: FilterOptions
|
||||||
@@ -15,6 +15,11 @@ const Uploads: CollectionConfig = {
|
|||||||
type: 'upload',
|
type: 'upload',
|
||||||
name: 'media',
|
name: 'media',
|
||||||
relationTo: 'uploads',
|
relationTo: 'uploads',
|
||||||
|
filterOptions: {
|
||||||
|
mimeType: {
|
||||||
|
equals: 'image/png',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -89,10 +89,20 @@ export default buildConfig({
|
|||||||
|
|
||||||
if (fs.existsSync(uploadsDir)) fs.readdirSync(uploadsDir).forEach((f) => fs.rmSync(`${uploadsDir}/${f}`));
|
if (fs.existsSync(uploadsDir)) fs.readdirSync(uploadsDir).forEach((f) => fs.rmSync(`${uploadsDir}/${f}`));
|
||||||
|
|
||||||
const filePath = path.resolve(__dirname, './collections/Upload/payload.jpg');
|
const pngPath = path.resolve(__dirname, './uploads/payload.png');
|
||||||
const file = await getFileByPath(filePath);
|
const pngFile = await getFileByPath(pngPath);
|
||||||
|
const createdPNGDoc = await payload.create({ collection: 'uploads', data: {}, file: pngFile });
|
||||||
|
|
||||||
const createdUploadDoc = await payload.create({ collection: 'uploads', data: uploadsDoc, file });
|
const jpgPath = path.resolve(__dirname, './collections/Upload/payload.jpg');
|
||||||
|
const jpgFile = await getFileByPath(jpgPath);
|
||||||
|
const createdJPGDoc = await payload.create({
|
||||||
|
collection: 'uploads',
|
||||||
|
data: {
|
||||||
|
...uploadsDoc,
|
||||||
|
media: createdPNGDoc.id,
|
||||||
|
},
|
||||||
|
file: jpgFile
|
||||||
|
});
|
||||||
|
|
||||||
const richTextDocWithRelId = JSON.parse(JSON.stringify(richTextDoc).replace('{{ARRAY_DOC_ID}}', createdArrayDoc.id));
|
const richTextDocWithRelId = JSON.parse(JSON.stringify(richTextDoc).replace('{{ARRAY_DOC_ID}}', createdArrayDoc.id));
|
||||||
const richTextDocWithRelationship = { ...richTextDocWithRelId };
|
const richTextDocWithRelationship = { ...richTextDocWithRelId };
|
||||||
@@ -101,8 +111,8 @@ export default buildConfig({
|
|||||||
richTextDocWithRelationship.richText[richTextRelationshipIndex].value = { id: createdTextDoc.id };
|
richTextDocWithRelationship.richText[richTextRelationshipIndex].value = { id: createdTextDoc.id };
|
||||||
|
|
||||||
const richTextUploadIndex = richTextDocWithRelationship.richText.findIndex(({ type }) => type === 'upload');
|
const richTextUploadIndex = richTextDocWithRelationship.richText.findIndex(({ type }) => type === 'upload');
|
||||||
richTextDocWithRelationship.richText[richTextUploadIndex].value = { id: createdUploadDoc.id };
|
richTextDocWithRelationship.richText[richTextUploadIndex].value = { id: createdJPGDoc.id };
|
||||||
richTextDocWithRelationship.richTextReadOnly[richTextUploadIndex].value = { id: createdUploadDoc.id };
|
richTextDocWithRelationship.richTextReadOnly[richTextUploadIndex].value = { id: createdJPGDoc.id };
|
||||||
|
|
||||||
await payload.create({ collection: 'rich-text-fields', data: richTextBulletsDoc });
|
await payload.create({ collection: 'rich-text-fields', data: richTextBulletsDoc });
|
||||||
await payload.create({ collection: 'rich-text-fields', data: richTextDocWithRelationship });
|
await payload.create({ collection: 'rich-text-fields', data: richTextDocWithRelationship });
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Page } from '@playwright/test';
|
import type { Page } from '@playwright/test';
|
||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
import path from 'path';
|
||||||
import { AdminUrlUtil } from '../helpers/adminUrlUtil';
|
import { AdminUrlUtil } from '../helpers/adminUrlUtil';
|
||||||
import { initPayloadE2E } from '../helpers/configHelpers';
|
import { initPayloadE2E } from '../helpers/configHelpers';
|
||||||
import { login, saveDocAndAssert } from '../helpers';
|
import { login, saveDocAndAssert } from '../helpers';
|
||||||
@@ -500,4 +501,55 @@ describe('fields', () => {
|
|||||||
await expect(page.locator('.Toastify')).toContainText('successfully');
|
await expect(page.locator('.Toastify')).toContainText('successfully');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('upload', () => {
|
||||||
|
let url: AdminUrlUtil;
|
||||||
|
beforeAll(() => {
|
||||||
|
url = new AdminUrlUtil(serverURL, 'uploads');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should upload files', async () => {
|
||||||
|
await page.goto(url.create);
|
||||||
|
|
||||||
|
// create a jpg upload
|
||||||
|
await page.locator('.file-field__upload input[type="file"]').setInputFiles(path.resolve(__dirname, './collections/Upload/payload.jpg'));
|
||||||
|
await expect(page.locator('.file-field .file-field__filename')).toContainText('payload.jpg');
|
||||||
|
await page.locator('#action-save').click();
|
||||||
|
await wait(200);
|
||||||
|
await expect(page.locator('.Toastify')).toContainText('successfully');
|
||||||
|
});
|
||||||
|
|
||||||
|
// test that the image renders
|
||||||
|
test('should render uploaded image', async () => {
|
||||||
|
await expect(page.locator('.file-field .file-details img')).toHaveAttribute('src', '/uploads/payload-1.jpg');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should upload using the document drawer', async () => {
|
||||||
|
// Open the media drawer and create a png upload
|
||||||
|
await page.locator('.field-type.upload .upload__toggler.doc-drawer__toggler').click();
|
||||||
|
await page.locator('[id^=doc-drawer_uploads_1_] .file-field__upload input[type="file"]').setInputFiles(path.resolve(__dirname, './uploads/payload.png'));
|
||||||
|
await page.locator('[id^=doc-drawer_uploads_1_] #action-save').click();
|
||||||
|
await wait(200);
|
||||||
|
await expect(page.locator('.Toastify')).toContainText('successfully');
|
||||||
|
|
||||||
|
// Assert that the media field has the png upload
|
||||||
|
await expect(page.locator('.field-type.upload .file-details .file-meta__url a')).toHaveAttribute('href', '/uploads/payload-1.png');
|
||||||
|
await expect(page.locator('.field-type.upload .file-details .file-meta__url a')).toContainText('payload-1.png');
|
||||||
|
await expect(page.locator('.field-type.upload .file-details img')).toHaveAttribute('src', '/uploads/payload-1.png');
|
||||||
|
await page.locator('#action-save').click();
|
||||||
|
await wait(200);
|
||||||
|
await expect(page.locator('.Toastify')).toContainText('successfully');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should clear selected upload', async () => {
|
||||||
|
await page.locator('.field-type.upload .file-details__remove').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should select using the list drawer and restrict mimetype based on filterOptions', async () => {
|
||||||
|
await page.locator('.field-type.upload .upload__toggler.list-drawer__toggler').click();
|
||||||
|
await wait(200);
|
||||||
|
const jpgImages = await page.locator('[id^=list-drawer_1_] .upload-gallery img[src$=".jpg"]');
|
||||||
|
expect(await jpgImages.count()).toEqual(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 0 B After Width: | Height: | Size: 341 KiB |
Reference in New Issue
Block a user