Merge branch 'master' of github.com:payloadcms/payload
This commit is contained in:
@@ -32,9 +32,11 @@ const Duplicate: React.FC<Props> = ({ slug, collection, id }) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const create = async (locale?: string): Promise<string | null> => {
|
||||
const localeParam = locale ? `locale=${locale}` : '';
|
||||
const response = await requests.get(`${serverURL}${api}/${slug}/${id}?${localeParam}`);
|
||||
const create = async (locale = ''): Promise<string | null> => {
|
||||
const response = await requests.get(`${serverURL}${api}/${slug}/${id}`, {
|
||||
locale,
|
||||
depth: 0,
|
||||
});
|
||||
const data = await response.json();
|
||||
const result = await requests.post(`${serverURL}${api}/${slug}`, {
|
||||
headers: {
|
||||
@@ -59,7 +61,10 @@ const Duplicate: React.FC<Props> = ({ slug, collection, id }) => {
|
||||
.filter((locale) => locale !== localization.defaultLocale)
|
||||
.forEach(async (locale) => {
|
||||
if (!abort) {
|
||||
const res = await requests.get(`${serverURL}${api}/${slug}/${id}?locale=${locale}`);
|
||||
const res = await requests.get(`${serverURL}${api}/${slug}/${id}`, {
|
||||
locale,
|
||||
depth: 0,
|
||||
});
|
||||
const localizedDoc = await res.json();
|
||||
const patchResult = await requests.patch(`${serverURL}${api}/${slug}/${duplicateID}?locale=${locale}`, {
|
||||
headers: {
|
||||
|
||||
@@ -88,7 +88,7 @@ const RenderFields: React.FC<Props> = (props) => {
|
||||
DefaultComponent={FieldComponent}
|
||||
componentProps={{
|
||||
...field,
|
||||
path: field.path || (isFieldAffectingData ? field.name : undefined),
|
||||
path: field.path || (isFieldAffectingData ? field.name : ''),
|
||||
fieldTypes,
|
||||
admin: {
|
||||
...(field.admin || {}),
|
||||
|
||||
@@ -6,7 +6,7 @@ export type Props = {
|
||||
className?: string
|
||||
readOnly?: boolean
|
||||
forceRender?: boolean
|
||||
permissions?: {
|
||||
permissions?: FieldPermissions | {
|
||||
[field: string]: FieldPermissions
|
||||
}
|
||||
filter?: (field: Field) => boolean
|
||||
|
||||
@@ -75,7 +75,7 @@ const CollapsibleField: React.FC<Props> = (props) => {
|
||||
<RenderFields
|
||||
forceRender
|
||||
readOnly={readOnly}
|
||||
permissions={permissions?.fields}
|
||||
permissions={permissions}
|
||||
fieldTypes={fieldTypes}
|
||||
fieldSchema={fields.map((field) => ({
|
||||
...field,
|
||||
|
||||
@@ -28,7 +28,7 @@ const Row: React.FC<Props> = (props) => {
|
||||
<RenderFields
|
||||
readOnly={readOnly}
|
||||
className={classes}
|
||||
permissions={permissions?.fields}
|
||||
permissions={permissions}
|
||||
fieldTypes={fieldTypes}
|
||||
fieldSchema={fields.map((field) => ({
|
||||
...field,
|
||||
|
||||
@@ -71,7 +71,7 @@ const TabsField: React.FC<Props> = (props) => {
|
||||
key={String(activeTab.label)}
|
||||
forceRender
|
||||
readOnly={readOnly}
|
||||
permissions={permissions?.fields}
|
||||
permissions={tabHasName(activeTab) ? permissions[activeTab.name].fields : permissions}
|
||||
fieldTypes={fieldTypes}
|
||||
fieldSchema={activeTab.fields.map((field) => ({
|
||||
...field,
|
||||
|
||||
@@ -28,7 +28,7 @@ const withCondition = <P extends Record<string, unknown>>(Field: React.Component
|
||||
path?: string
|
||||
};
|
||||
|
||||
const path = pathFromProps || name;
|
||||
const path = typeof pathFromProps === 'string' ? pathFromProps : name;
|
||||
|
||||
const { getData, getSiblingData, getField, dispatchFields } = useWatchForm();
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ const Dashboard: React.FC<Props> = (props) => {
|
||||
|
||||
if (type === EntityType.global) {
|
||||
title = entity.label;
|
||||
onClick = () => push({ pathname: `${admin}/globals/${global.slug}` });
|
||||
onClick = () => push({ pathname: `${admin}/globals/${entity.slug}` });
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { PayloadRequest } from '../../express/types';
|
||||
import { Permissions } from '../types';
|
||||
import { adminInit as adminInitTelemetry } from '../../utilities/telemetry/events/adminInit';
|
||||
import { tabHasName } from '../../fields/config/types';
|
||||
|
||||
const allOperations = ['create', 'read', 'update', 'delete'];
|
||||
|
||||
@@ -66,7 +67,12 @@ async function accessOperation(args: Arguments): Promise<Permissions> {
|
||||
executeFieldPolicies(updatedObj, field.fields, operation);
|
||||
} else if (field.type === 'tabs') {
|
||||
field.tabs.forEach((tab) => {
|
||||
executeFieldPolicies(updatedObj, tab.fields, operation);
|
||||
if (tabHasName(tab)) {
|
||||
if (!updatedObj[tab.name]) updatedObj[tab.name] = { fields: {} };
|
||||
executeFieldPolicies(updatedObj[tab.name].fields, tab.fields, operation);
|
||||
} else {
|
||||
executeFieldPolicies(updatedObj, tab.fields, operation);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -226,7 +226,7 @@ export type CollectionConfig = {
|
||||
/**
|
||||
* Custom rest api endpoints
|
||||
*/
|
||||
endpoints?: Endpoint[]
|
||||
endpoints?: Omit<Endpoint, 'root'>[]
|
||||
/**
|
||||
* Access control
|
||||
*/
|
||||
|
||||
@@ -119,7 +119,7 @@ function initCollectionsGraphQL(payload: Payload): void {
|
||||
singularLabel,
|
||||
);
|
||||
|
||||
if (collection.config.auth) {
|
||||
if (collection.config.auth && !collection.config.auth.disableLocalStrategy) {
|
||||
fields.push({
|
||||
name: 'password',
|
||||
label: 'Password',
|
||||
@@ -274,21 +274,23 @@ function initCollectionsGraphQL(payload: Payload): void {
|
||||
}
|
||||
|
||||
if (collection.config.auth) {
|
||||
const authFields: Field[] = collection.config.auth.disableLocalStrategy ? [] : [{
|
||||
name: 'email',
|
||||
type: 'email',
|
||||
required: true,
|
||||
}]
|
||||
collection.graphQL.JWT = buildObjectType({
|
||||
payload,
|
||||
name: formatName(`${slug}JWT`),
|
||||
fields: collection.config.fields.filter((field) => fieldAffectsData(field) && field.saveToJWT).concat([
|
||||
{
|
||||
name: 'email',
|
||||
type: 'email',
|
||||
required: true,
|
||||
},
|
||||
fields: [
|
||||
...collection.config.fields.filter((field) => fieldAffectsData(field) && field.saveToJWT),
|
||||
...authFields,
|
||||
{
|
||||
name: 'collection',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
]),
|
||||
}
|
||||
],
|
||||
parentName: formatName(`${slug}JWT`),
|
||||
});
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ export default function registerCollections(ctx: Payload): void {
|
||||
}
|
||||
|
||||
const endpoints = buildEndpoints(collection);
|
||||
mountEndpoints(router, endpoints);
|
||||
mountEndpoints(ctx.express, router, endpoints);
|
||||
|
||||
ctx.router.use(`/${slug}`, router);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ const component = joi.alternatives().try(
|
||||
export const endpointsSchema = joi.array().items(joi.object({
|
||||
path: joi.string(),
|
||||
method: joi.string().valid('get', 'head', 'post', 'put', 'patch', 'delete', 'connect', 'options'),
|
||||
root: joi.bool(),
|
||||
handler: joi.alternatives().try(
|
||||
joi.array().items(joi.func()),
|
||||
joi.func(),
|
||||
|
||||
@@ -79,16 +79,19 @@ export type AccessResult = boolean | Where;
|
||||
*/
|
||||
export type Access = (args?: any) => AccessResult | Promise<AccessResult>;
|
||||
|
||||
export interface PayloadHandler {(
|
||||
export interface PayloadHandler {
|
||||
(
|
||||
req: PayloadRequest,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
): void }
|
||||
): void
|
||||
}
|
||||
|
||||
export type Endpoint = {
|
||||
path: string
|
||||
method: 'get' | 'head' | 'post' | 'put' | 'patch' | 'delete' | 'connect' | 'options' | string
|
||||
handler: PayloadHandler | PayloadHandler[]
|
||||
root?: boolean
|
||||
}
|
||||
|
||||
export type AdminView = React.ComponentType<{ user: User, canAccessAdmin: boolean }>
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { Router } from 'express';
|
||||
import { Express, Router } from 'express';
|
||||
import { Endpoint } from '../config/types';
|
||||
|
||||
function mountEndpoints(router: Router, endpoints: Endpoint[]): void {
|
||||
function mountEndpoints(express: Express, router: Router, endpoints: Endpoint[]): void {
|
||||
endpoints.forEach((endpoint) => {
|
||||
router[endpoint.method](endpoint.path, endpoint.handler);
|
||||
if (!endpoint.root) {
|
||||
router[endpoint.method](endpoint.path, endpoint.handler);
|
||||
} else {
|
||||
express[endpoint.method](endpoint.path, endpoint.handler);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ export type GlobalConfig = {
|
||||
beforeRead?: BeforeReadHook[]
|
||||
afterRead?: AfterReadHook[]
|
||||
}
|
||||
endpoints?: Endpoint[],
|
||||
endpoints?: Omit<Endpoint, 'root'>[],
|
||||
access?: {
|
||||
read?: Access;
|
||||
readDrafts?: Access;
|
||||
|
||||
@@ -48,7 +48,7 @@ export default function initGlobals(ctx: Payload): void {
|
||||
const { slug } = global;
|
||||
|
||||
const endpoints = buildEndpoints(global);
|
||||
mountEndpoints(router, endpoints);
|
||||
mountEndpoints(ctx.express, router, endpoints);
|
||||
|
||||
ctx.router.use(`/globals/${slug}`, router);
|
||||
});
|
||||
|
||||
@@ -14,8 +14,8 @@ const buildPaginatedListType = (name, docType) => new GraphQLObjectType({
|
||||
pagingCounter: { type: GraphQLInt },
|
||||
hasPrevPage: { type: GraphQLBoolean },
|
||||
hasNextPage: { type: GraphQLBoolean },
|
||||
prevPage: { type: GraphQLBoolean },
|
||||
nextPage: { type: GraphQLBoolean },
|
||||
prevPage: { type: GraphQLInt },
|
||||
nextPage: { type: GraphQLInt },
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ export const init = (payload: Payload, options: InitOptions): void => {
|
||||
initGraphQLPlayground(payload);
|
||||
}
|
||||
|
||||
mountEndpoints(payload.router, payload.config.endpoints);
|
||||
mountEndpoints(options.express, payload.router, payload.config.endpoints);
|
||||
|
||||
// Bind router to API
|
||||
payload.express.use(payload.config.routes.api, payload.router);
|
||||
|
||||
3
src/uploads/canResizeImage.ts
Normal file
3
src/uploads/canResizeImage.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function canResizeImage(mimeType: string): boolean {
|
||||
return ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].indexOf(mimeType) > -1;
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import fs from 'fs';
|
||||
import sharp from 'sharp';
|
||||
import sanitize from 'sanitize-filename';
|
||||
import { fromBuffer } from 'file-type';
|
||||
import { ProbedImageSize } from './getImageSize';
|
||||
import fileExists from './fileExists';
|
||||
import fs from 'fs';
|
||||
import sanitize from 'sanitize-filename';
|
||||
import sharp from 'sharp';
|
||||
import { SanitizedCollectionConfig } from '../collections/config/types';
|
||||
import { FileSizes, ImageSize } from './types';
|
||||
import { PayloadRequest } from '../express/types';
|
||||
import fileExists from './fileExists';
|
||||
import { ProbedImageSize } from './getImageSize';
|
||||
import { FileSizes, ImageSize } from './types';
|
||||
|
||||
type Args = {
|
||||
req: PayloadRequest
|
||||
@@ -50,7 +50,7 @@ export default async function resizeAndSave({
|
||||
const sizes = imageSizes
|
||||
.filter((desiredSize) => needsResize(desiredSize, dimensions))
|
||||
.map(async (desiredSize) => {
|
||||
let resized = await sharp(file).resize(desiredSize);
|
||||
let resized = sharp(file).resize(desiredSize);
|
||||
|
||||
if (desiredSize.formatOptions) {
|
||||
resized = resized.toFormat(desiredSize.formatOptions.format, desiredSize.formatOptions.options);
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { fromBuffer } from 'file-type';
|
||||
import mkdirp from 'mkdirp';
|
||||
import path from 'path';
|
||||
import sharp, { Sharp } from 'sharp';
|
||||
import { fromBuffer } from 'file-type';
|
||||
import sanitize from 'sanitize-filename';
|
||||
import { SanitizedConfig } from '../config/types';
|
||||
import sharp, { Sharp } from 'sharp';
|
||||
import { Collection } from '../collections/config/types';
|
||||
import { SanitizedConfig } from '../config/types';
|
||||
import { FileUploadError, MissingFile } from '../errors';
|
||||
import { PayloadRequest } from '../express/types';
|
||||
import { FileData } from './types';
|
||||
import saveBufferToFile from './saveBufferToFile';
|
||||
import getImageSize, { ProbedImageSize } from './getImageSize';
|
||||
import getSafeFileName from './getSafeFilename';
|
||||
import getImageSize from './getImageSize';
|
||||
import resizeAndSave from './imageResizer';
|
||||
import isImage from './isImage';
|
||||
import saveBufferToFile from './saveBufferToFile';
|
||||
import { FileData } from './types';
|
||||
import canResizeImage from './canResizeImage';
|
||||
|
||||
type Args = {
|
||||
config: SanitizedConfig,
|
||||
@@ -59,54 +59,53 @@ const uploadFile = async ({
|
||||
|
||||
if (file) {
|
||||
try {
|
||||
const shouldResize = canResizeImage(file.mimetype);
|
||||
let fsSafeName: string;
|
||||
let fileBuffer: Buffer;
|
||||
let mimeType: string;
|
||||
let fileSize: number;
|
||||
|
||||
if (!disableLocalStorage) {
|
||||
let resized: Sharp | undefined;
|
||||
let resized: Sharp | undefined;
|
||||
let dimensions: ProbedImageSize;
|
||||
if (shouldResize) {
|
||||
if (resizeOptions) {
|
||||
resized = sharp(file.data).resize(resizeOptions);
|
||||
resized = sharp(file.data)
|
||||
.resize(resizeOptions);
|
||||
}
|
||||
if (formatOptions) {
|
||||
resized = (resized ?? sharp(file.data)).toFormat(formatOptions.format, formatOptions.options);
|
||||
}
|
||||
fileBuffer = resized ? (await resized.toBuffer()) : file.data;
|
||||
const { mime, ext } = await fromBuffer(fileBuffer);
|
||||
mimeType = mime;
|
||||
fileSize = fileBuffer.length;
|
||||
const baseFilename = sanitize(file.name.substring(0, file.name.lastIndexOf('.')) || file.name);
|
||||
fsSafeName = `${baseFilename}.${ext}`;
|
||||
dimensions = await getImageSize(file);
|
||||
fileData.width = dimensions.width;
|
||||
fileData.height = dimensions.height;
|
||||
}
|
||||
|
||||
if (!overwriteExistingFiles) {
|
||||
fsSafeName = await getSafeFileName(Model, staticPath, fsSafeName);
|
||||
}
|
||||
const fileBuffer = resized ? (await resized.toBuffer()) : file.data;
|
||||
|
||||
const { mime, ext } = await fromBuffer(fileBuffer) ?? { mime: file.mimetype, ext: file.name.split('.').pop() };
|
||||
const fileSize = fileBuffer.length;
|
||||
const baseFilename = sanitize(file.name.substring(0, file.name.lastIndexOf('.')) || file.name);
|
||||
fsSafeName = `${baseFilename}.${ext}`;
|
||||
|
||||
if (!overwriteExistingFiles) {
|
||||
fsSafeName = await getSafeFileName(Model, staticPath, fsSafeName);
|
||||
}
|
||||
|
||||
if (!disableLocalStorage) {
|
||||
await saveBufferToFile(fileBuffer, `${staticPath}/${fsSafeName}`);
|
||||
}
|
||||
|
||||
fileData.filename = fsSafeName || (!overwriteExistingFiles ? await getSafeFileName(Model, staticPath, file.name) : file.name);
|
||||
fileData.filesize = fileSize || file.size;
|
||||
fileData.mimeType = mimeType || (await fromBuffer(file.data)).mime;
|
||||
fileData.mimeType = mime || (await fromBuffer(file.data)).mime;
|
||||
|
||||
if (isImage(file.mimetype)) {
|
||||
const dimensions = await getImageSize(file);
|
||||
fileData.width = dimensions.width;
|
||||
fileData.height = dimensions.height;
|
||||
|
||||
if (Array.isArray(imageSizes) && file.mimetype !== 'image/svg+xml') {
|
||||
req.payloadUploadSizes = {};
|
||||
fileData.sizes = await resizeAndSave({
|
||||
req,
|
||||
file: file.data,
|
||||
dimensions,
|
||||
staticPath,
|
||||
config: collectionConfig,
|
||||
savedFilename: fsSafeName,
|
||||
mimeType: fileData.mimeType,
|
||||
});
|
||||
}
|
||||
if (Array.isArray(imageSizes) && shouldResize) {
|
||||
req.payloadUploadSizes = {};
|
||||
fileData.sizes = await resizeAndSave({
|
||||
req,
|
||||
file: file.data,
|
||||
dimensions,
|
||||
staticPath,
|
||||
config: collectionConfig,
|
||||
savedFilename: fsSafeName || file.name,
|
||||
mimeType: fileData.mimeType,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
Reference in New Issue
Block a user