From 09d26f2af0ad32f311847f3bd74721cf8355954a Mon Sep 17 00:00:00 2001 From: DMehaffy Date: Tue, 11 Apr 2023 10:37:30 -0700 Subject: [PATCH 01/39] Remove overrides from Vercel --- vercel.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/vercel.json b/vercel.json index 77f6c2a272..1ab665d8d1 100644 --- a/vercel.json +++ b/vercel.json @@ -1,10 +1,7 @@ { - "buildCommand": "cd packages/core/helper-plugin && yarn build-storybook", "github": { "silent": true, "autoJobCancelation": true }, - "installCommand": "yarn install --immutable", "ignoreCommand": "git diff HEAD^ HEAD --quiet './packages/core/helper-plugin'", - "outputDirectory": "packages/core/helper-plugin/storybook-static" } From e720eba0cf678b1bc4af02a553e4b740643a7e40 Mon Sep 17 00:00:00 2001 From: Ben Irvin Date: Wed, 12 Apr 2023 15:48:44 +0200 Subject: [PATCH 02/39] export files with consistent internal paths --- .../core/data-transfer/src/file/providers/destination/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/data-transfer/src/file/providers/destination/index.ts b/packages/core/data-transfer/src/file/providers/destination/index.ts index aebec13218..1f6ebd7ebd 100644 --- a/packages/core/data-transfer/src/file/providers/destination/index.ts +++ b/packages/core/data-transfer/src/file/providers/destination/index.ts @@ -249,7 +249,8 @@ class LocalFileDestinationProvider implements IDestinationProvider { return new Writable({ objectMode: true, write(data: IAsset, _encoding, callback) { - const entryPath = path.join('assets', 'uploads', data.filename); + // always write tar files with posix paths so we have a standard format for paths regardless of system + const entryPath = path.posix.join('assets', 'uploads', data.filename); const entry = archiveStream.entry({ name: entryPath, From e0e0576330e8a45b74b654c9e3002bec424611e3 Mon Sep 17 00:00:00 2001 From: Ben Irvin Date: Fri, 14 Apr 2023 17:56:50 +0200 Subject: [PATCH 03/39] Add error message when schema can't be loaded --- .../core/data-transfer/src/file/providers/source/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/core/data-transfer/src/file/providers/source/index.ts b/packages/core/data-transfer/src/file/providers/source/index.ts index 6172f39ba0..3e33d5ab69 100644 --- a/packages/core/data-transfer/src/file/providers/source/index.ts +++ b/packages/core/data-transfer/src/file/providers/source/index.ts @@ -93,6 +93,10 @@ class LocalFileSourceProvider implements ISourceProvider { async getSchemas() { const schemas = await collect(this.createSchemasReadStream()); + if (isEmpty(schemas)) { + throw new ProviderInitializationError('Could not load schemas from Strapi data file.'); + } + return keyBy('uid', schemas); } From 87c678b3518956c2367d024d8ade40191fe9cedb Mon Sep 17 00:00:00 2001 From: Ben Irvin Date: Fri, 14 Apr 2023 18:02:01 +0200 Subject: [PATCH 04/39] always export posix paths --- .../data-transfer/src/file/providers/destination/utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/data-transfer/src/file/providers/destination/utils.ts b/packages/core/data-transfer/src/file/providers/destination/utils.ts index 7149e589f6..fe49954b5d 100644 --- a/packages/core/data-transfer/src/file/providers/destination/utils.ts +++ b/packages/core/data-transfer/src/file/providers/destination/utils.ts @@ -1,5 +1,5 @@ import { Writable } from 'stream'; -import { join } from 'path'; +import { posix } from 'path'; import tar from 'tar-stream'; /** @@ -9,7 +9,7 @@ import tar from 'tar-stream'; export const createFilePathFactory = (type: string) => (fileIndex = 0): string => { - return join( + return posix.join( // "{type}" directory type, // "${type}_XXXXX.jsonl" file From 39f180a8143669b2a5b90fc1f071ff59bef04571 Mon Sep 17 00:00:00 2001 From: Ben Irvin Date: Fri, 14 Apr 2023 18:19:08 +0200 Subject: [PATCH 05/39] improve error handling --- .../src/file/providers/source/index.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/core/data-transfer/src/file/providers/source/index.ts b/packages/core/data-transfer/src/file/providers/source/index.ts index 3e33d5ab69..524af347cc 100644 --- a/packages/core/data-transfer/src/file/providers/source/index.ts +++ b/packages/core/data-transfer/src/file/providers/source/index.ts @@ -4,7 +4,7 @@ import fs from 'fs-extra'; import zip from 'zlib'; import tar from 'tar'; import path from 'path'; -import { keyBy } from 'lodash/fp'; +import { isEmpty, keyBy } from 'lodash/fp'; import { chain } from 'stream-chain'; import { pipeline, PassThrough } from 'stream'; import { parser } from 'stream-json/jsonl/Parser'; @@ -73,6 +73,8 @@ class LocalFileSourceProvider implements ISourceProvider { try { // Read the metadata to ensure the file can be parsed this.#metadata = await this.getMetadata(); + + // TODO: we might also need to read the schema.jsonl files & implements a custom stream-check } catch (e) { if (this.options?.encryption?.enabled) { throw new ProviderInitializationError( @@ -81,11 +83,17 @@ class LocalFileSourceProvider implements ISourceProvider { } throw new ProviderInitializationError(`File '${filePath}' is not a valid Strapi data file.`); } + + if (!this.#metadata) { + throw new ProviderInitializationError('Could not load metadata from Strapi data file.'); + } } - getMetadata() { - // TODO: need to read the file & extract the metadata json file - // => we might also need to read the schema.jsonl files & implements a custom stream-check + async getMetadata() { + if (this.#metadata) { + return this.#metadata; + } + const backupStream = this.#getBackupStream(); return this.#parseJSONFile(backupStream, METADATA_FILE_PATH); } From 4ec704416fc0bc94d9c6a278f4a70259d8b77feb Mon Sep 17 00:00:00 2001 From: Ben Irvin Date: Fri, 14 Apr 2023 18:28:46 +0200 Subject: [PATCH 06/39] filesystem agnostic loading of tar files --- .../src/engine/__tests__/engine.test.ts | 6 +-- .../src/file/providers/source/index.ts | 49 ++++++++++++++----- 2 files changed, 40 insertions(+), 15 deletions(-) diff --git a/packages/core/data-transfer/src/engine/__tests__/engine.test.ts b/packages/core/data-transfer/src/engine/__tests__/engine.test.ts index b08a5259f2..60d9cb889b 100644 --- a/packages/core/data-transfer/src/engine/__tests__/engine.test.ts +++ b/packages/core/data-transfer/src/engine/__tests__/engine.test.ts @@ -1,4 +1,4 @@ -import { join } from 'path'; +import { posix, win32 } from 'path'; import { cloneDeep } from 'lodash/fp'; import { Readable, Writable } from 'stream-chain'; import type { Schema } from '@strapi/strapi'; @@ -48,13 +48,13 @@ const getAssetsMockSourceStream = ( data: Iterable = [ { filename: 'foo.jpg', - filepath: join(__dirname, 'foo.jpg'), + filepath: posix.join(__dirname, 'foo.jpg'), // test a file with a posix path stats: { size: 24 }, stream: Readable.from([1, 2, 3]), }, { filename: 'bar.jpg', - filepath: join(__dirname, 'bar.jpg'), + filepath: win32.join(__dirname, 'bar.jpg'), // test a file with a win32 path stats: { size: 48 }, stream: Readable.from([4, 5, 6, 7, 8, 9]), }, diff --git a/packages/core/data-transfer/src/file/providers/source/index.ts b/packages/core/data-transfer/src/file/providers/source/index.ts index 524af347cc..a5981b477d 100644 --- a/packages/core/data-transfer/src/file/providers/source/index.ts +++ b/packages/core/data-transfer/src/file/providers/source/index.ts @@ -45,6 +45,34 @@ export const createLocalFileSourceProvider = (options: ILocalFileSourceProviderO return new LocalFileSourceProvider(options); }; +/** + * Note: in versions of the transfer engine <=4.9.0, exports were generated with windows paths + * on Windows systems, and posix paths on posix systems. + * + * We now store all paths as posix, but need to leave a separator conversion for legacy purposes, and to + * support manually-created tar files coming from Windows systems (ie, if a user creates a + * backup file with a windows tar tool rather than using the `export` command) + * + * Because of this, export/import files may never contain files with a forward slash in the name, even escaped + * */ + +// Check if the directory of a given filePath (which can be either posix or win32) resolves to the same as the given posix-format path posixDirName +const isDirPathEquivalent = (posixDirName: string, filePath: string) => { + // if win32 convert to posix, then get dirname + const normalizedDir = path.posix.dirname(filePath.split(path.win32.sep).join(path.posix.sep)); + + return posixDirName === normalizedDir; +}; + +// Check if two file paths that can be either in posix or win32 format resolves to the same file +const isFilePathEquivalent = (fileA: string, fileB: string) => { + // Check if paths appear to be win32 or posix, and if win32 convert to posix + const normalizedPathA = fileA.split(path.win32.sep).join(path.posix.sep); + const normalizedPathB = fileB.split(path.win32.sep).join(path.posix.sep); + + return !path.posix.relative(normalizedPathB, normalizedPathA).length; +}; + class LocalFileSourceProvider implements ISourceProvider { type: ProviderType = 'source'; @@ -133,15 +161,15 @@ class LocalFileSourceProvider implements ISourceProvider { [ inStream, new tar.Parse({ + // find only files in the assets/uploads folder filter(filePath, entry) { if (entry.type !== 'File') { return false; } - - const parts = filePath.split('/'); - return parts[0] === 'assets' && parts[1] === 'uploads'; + return isDirPathEquivalent('./assets/uploads', filePath); }, onentry(entry) { + // TODO: Check if we need to handle win32 paths here for the assets const { path: filePath, size = 0 } = entry; const file = path.basename(filePath); const asset: IAsset = { @@ -196,14 +224,7 @@ class LocalFileSourceProvider implements ISourceProvider { return false; } - const parts = path.relative('.', filePath).split('/'); - - // TODO: this method is limiting us from having additional subdirectories and is requiring us to remove any "./" prefixes (the path.relative line above) - if (parts.length !== 2) { - return false; - } - - return parts[0] === directory; + return isDirPathEquivalent(directory, filePath); }, async onentry(entry) { @@ -261,7 +282,11 @@ class LocalFileSourceProvider implements ISourceProvider { * Filter the parsed entries to only keep the one that matches the given filepath */ filter(entryPath, entry) { - return !path.relative(filePath, entryPath).length && entry.type === 'File'; + if (entry.type !== 'File') { + return false; + } + + return isFilePathEquivalent(entryPath, filePath); }, async onentry(entry) { From 5af88d5068babbc37fca4079e7185ae7dead24cf Mon Sep 17 00:00:00 2001 From: derrickmehaffy Date: Fri, 14 Apr 2023 11:49:54 -0700 Subject: [PATCH 07/39] remove storybook build command --- vercel.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/vercel.json b/vercel.json index 1ab665d8d1..3d53349522 100644 --- a/vercel.json +++ b/vercel.json @@ -2,6 +2,5 @@ "github": { "silent": true, "autoJobCancelation": true - }, - "ignoreCommand": "git diff HEAD^ HEAD --quiet './packages/core/helper-plugin'", + } } From 2c776ad5aa575d6799b12090eba381d92a42e0ee Mon Sep 17 00:00:00 2001 From: Ben Irvin Date: Mon, 17 Apr 2023 12:10:04 +0200 Subject: [PATCH 08/39] only load metadata during bootstrap --- .../src/file/providers/source/index.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/core/data-transfer/src/file/providers/source/index.ts b/packages/core/data-transfer/src/file/providers/source/index.ts index a5981b477d..2420a5bdf6 100644 --- a/packages/core/data-transfer/src/file/providers/source/index.ts +++ b/packages/core/data-transfer/src/file/providers/source/index.ts @@ -100,8 +100,7 @@ class LocalFileSourceProvider implements ISourceProvider { try { // Read the metadata to ensure the file can be parsed - this.#metadata = await this.getMetadata(); - + await this.#loadMetadata(); // TODO: we might also need to read the schema.jsonl files & implements a custom stream-check } catch (e) { if (this.options?.encryption?.enabled) { @@ -117,13 +116,18 @@ class LocalFileSourceProvider implements ISourceProvider { } } + async #loadMetadata() { + const backupStream = this.#getBackupStream(); + this.#metadata = await this.#parseJSONFile(backupStream, METADATA_FILE_PATH); + return this.#metadata; + } + async getMetadata() { if (this.#metadata) { - return this.#metadata; + await this.#loadMetadata(); } - const backupStream = this.#getBackupStream(); - return this.#parseJSONFile(backupStream, METADATA_FILE_PATH); + return this.#metadata || null; } async getSchemas() { From 4f4107af848e984ca6eb70c2ced366216e234fda Mon Sep 17 00:00:00 2001 From: Ben Irvin Date: Tue, 18 Apr 2023 10:42:56 +0200 Subject: [PATCH 09/39] move and fix path comparison functions --- .../src/file/providers/source/index.ts | 34 +++---------------- .../src/file/providers/source/utils.ts | 29 ++++++++++++++++ 2 files changed, 33 insertions(+), 30 deletions(-) create mode 100644 packages/core/data-transfer/src/file/providers/source/utils.ts diff --git a/packages/core/data-transfer/src/file/providers/source/index.ts b/packages/core/data-transfer/src/file/providers/source/index.ts index 2420a5bdf6..e3af20e3b2 100644 --- a/packages/core/data-transfer/src/file/providers/source/index.ts +++ b/packages/core/data-transfer/src/file/providers/source/index.ts @@ -15,6 +15,7 @@ import type { IAsset, IMetadata, ISourceProvider, ProviderType } from '../../../ import { createDecryptionCipher } from '../../../utils/encryption'; import { collect } from '../../../utils/stream'; import { ProviderInitializationError, ProviderTransferError } from '../../../errors/providers'; +import { isDirPathEquivalent, isPathEquivalent } from './utils'; type StreamItemArray = Parameters[0]; @@ -45,34 +46,6 @@ export const createLocalFileSourceProvider = (options: ILocalFileSourceProviderO return new LocalFileSourceProvider(options); }; -/** - * Note: in versions of the transfer engine <=4.9.0, exports were generated with windows paths - * on Windows systems, and posix paths on posix systems. - * - * We now store all paths as posix, but need to leave a separator conversion for legacy purposes, and to - * support manually-created tar files coming from Windows systems (ie, if a user creates a - * backup file with a windows tar tool rather than using the `export` command) - * - * Because of this, export/import files may never contain files with a forward slash in the name, even escaped - * */ - -// Check if the directory of a given filePath (which can be either posix or win32) resolves to the same as the given posix-format path posixDirName -const isDirPathEquivalent = (posixDirName: string, filePath: string) => { - // if win32 convert to posix, then get dirname - const normalizedDir = path.posix.dirname(filePath.split(path.win32.sep).join(path.posix.sep)); - - return posixDirName === normalizedDir; -}; - -// Check if two file paths that can be either in posix or win32 format resolves to the same file -const isFilePathEquivalent = (fileA: string, fileB: string) => { - // Check if paths appear to be win32 or posix, and if win32 convert to posix - const normalizedPathA = fileA.split(path.win32.sep).join(path.posix.sep); - const normalizedPathB = fileB.split(path.win32.sep).join(path.posix.sep); - - return !path.posix.relative(normalizedPathB, normalizedPathA).length; -}; - class LocalFileSourceProvider implements ISourceProvider { type: ProviderType = 'source'; @@ -170,7 +143,8 @@ class LocalFileSourceProvider implements ISourceProvider { if (entry.type !== 'File') { return false; } - return isDirPathEquivalent('./assets/uploads', filePath); + console.log(`${filePath} is assets?`, isDirPathEquivalent('assets/uploads', filePath)); + return isDirPathEquivalent('assets/uploads', filePath); }, onentry(entry) { // TODO: Check if we need to handle win32 paths here for the assets @@ -290,7 +264,7 @@ class LocalFileSourceProvider implements ISourceProvider { return false; } - return isFilePathEquivalent(entryPath, filePath); + return isPathEquivalent(entryPath, filePath); }, async onentry(entry) { diff --git a/packages/core/data-transfer/src/file/providers/source/utils.ts b/packages/core/data-transfer/src/file/providers/source/utils.ts new file mode 100644 index 0000000000..d4470ad66f --- /dev/null +++ b/packages/core/data-transfer/src/file/providers/source/utils.ts @@ -0,0 +1,29 @@ +import path from 'path'; + +/** + * Note: in versions of the transfer engine <=4.9.0, exports were generated with windows paths + * on Windows systems, and posix paths on posix systems. + * + * We now store all paths as posix, but need to leave a separator conversion for legacy purposes, and to + * support manually-created tar files coming from Windows systems (ie, if a user creates a + * backup file with a windows tar tool rather than using the `export` command) + * + * Because of this, export/import files may never contain files with a forward slash in the name, even escaped + * + * */ + +// Check if the directory of a given filePath (which can be either posix or win32) resolves to the same as the given posix-format path posixDirName +export const isDirPathEquivalent = (posixDirName: string, filePath: string) => { + // if win32 convert to posix, then get dirname + const normalizedDir = path.posix.dirname(filePath.split(path.win32.sep).join(path.posix.sep)); + return isPathEquivalent(posixDirName, normalizedDir); +}; + +// Check if two paths that can be either in posix or win32 format resolves to the same file +export const isPathEquivalent = (fileA: string, fileB: string) => { + // Check if paths appear to be win32 or posix, and if win32 convert to posix + const normalizedPathA = fileA.split(path.win32.sep).join(path.posix.sep); + const normalizedPathB = fileB.split(path.win32.sep).join(path.posix.sep); + + return !path.posix.relative(normalizedPathB, normalizedPathA).length; +}; From 7419f86063f2f15349d2c5ca7f2de344cd47960f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20de=20Juvigny?= Date: Tue, 18 Apr 2023 11:34:49 +0200 Subject: [PATCH 10/39] Validate input size in custom field server registry --- .../__tests__/custom-fields.test.js | 19 +++++++++++++++++++ .../lib/core/registries/custom-fields.js | 19 ++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/packages/core/strapi/lib/core/registries/__tests__/custom-fields.test.js b/packages/core/strapi/lib/core/registries/__tests__/custom-fields.test.js index fd45f4346d..283bcf5b36 100644 --- a/packages/core/strapi/lib/core/registries/__tests__/custom-fields.test.js +++ b/packages/core/strapi/lib/core/registries/__tests__/custom-fields.test.js @@ -90,6 +90,25 @@ describe('Custom fields registry', () => { ); }); + it('validates inputSize', () => { + const mockCF = { + name: 'test', + type: 'text', + }; + + const customFields = customFieldsRegistry(strapi); + + expect(() => customFields.add({ ...mockCF, inputSize: 'small' })).toThrowError( + `inputSize should be an object with 'default' and 'isResizable' keys` + ); + expect(() => + customFields.add({ ...mockCF, inputSize: { default: 99, isResizable: true } }) + ).toThrowError('Custom fields require a valid default input size'); + expect(() => + customFields.add({ ...mockCF, inputSize: { default: 12, isResizable: 'true' } }) + ).toThrowError('Custom fields should specify if their input is resizable'); + }); + it('confirms the custom field does not already exist', () => { const mockCF = { name: 'test', diff --git a/packages/core/strapi/lib/core/registries/custom-fields.js b/packages/core/strapi/lib/core/registries/custom-fields.js index 47ad2eaf08..9b8dc3ab41 100644 --- a/packages/core/strapi/lib/core/registries/custom-fields.js +++ b/packages/core/strapi/lib/core/registries/custom-fields.js @@ -44,7 +44,7 @@ const customFieldsRegistry = (strapi) => { throw new Error(`Custom fields require a 'name' and 'type' key`); } - const { name, plugin, type } = cf; + const { name, plugin, type, inputSize } = cf; if (!ALLOWED_TYPES.includes(type)) { throw new Error( `Custom field type: '${type}' is not a valid Strapi type or it can't be used with a Custom Field` @@ -56,6 +56,23 @@ const customFieldsRegistry = (strapi) => { throw new Error(`Custom field name: '${name}' is not a valid object key`); } + // Validate inputSize when provided + if (inputSize) { + if ( + typeof inputSize !== 'object' || + !has('default', inputSize) || + !has('isResizable', inputSize) + ) { + throw new Error(`inputSize should be an object with 'default' and 'isResizable' keys`); + } + if (![4, 6, 8, 12].includes(inputSize.default)) { + throw new Error('Custom fields require a valid default input size'); + } + if (typeof inputSize.isResizable !== 'boolean') { + throw new Error('Custom fields should specify if their input is resizable'); + } + } + // When no plugin is specified, or it isn't found in Strapi, default to global const uid = strapi.plugin(plugin) ? `plugin::${plugin}.${name}` : `global::${name}`; From e429fb7ee9e9a6a913269c0c9755c59cdcbfe263 Mon Sep 17 00:00:00 2001 From: Ben Irvin Date: Tue, 18 Apr 2023 12:34:54 +0200 Subject: [PATCH 11/39] fix assets output paths and clean up --- .../src/file/providers/source/index.ts | 9 ++++---- .../src/file/providers/source/utils.ts | 22 +++++++++++++++---- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/packages/core/data-transfer/src/file/providers/source/index.ts b/packages/core/data-transfer/src/file/providers/source/index.ts index e3af20e3b2..119d527aee 100644 --- a/packages/core/data-transfer/src/file/providers/source/index.ts +++ b/packages/core/data-transfer/src/file/providers/source/index.ts @@ -15,7 +15,7 @@ import type { IAsset, IMetadata, ISourceProvider, ProviderType } from '../../../ import { createDecryptionCipher } from '../../../utils/encryption'; import { collect } from '../../../utils/stream'; import { ProviderInitializationError, ProviderTransferError } from '../../../errors/providers'; -import { isDirPathEquivalent, isPathEquivalent } from './utils'; +import { isDirPathEquivalent, isPathEquivalent, unknownPathToPosix } from './utils'; type StreamItemArray = Parameters[0]; @@ -143,16 +143,17 @@ class LocalFileSourceProvider implements ISourceProvider { if (entry.type !== 'File') { return false; } - console.log(`${filePath} is assets?`, isDirPathEquivalent('assets/uploads', filePath)); return isDirPathEquivalent('assets/uploads', filePath); }, onentry(entry) { // TODO: Check if we need to handle win32 paths here for the assets const { path: filePath, size = 0 } = entry; - const file = path.basename(filePath); + const normalizedPath = unknownPathToPosix(filePath); + const file = path.basename(normalizedPath); + const asset: IAsset = { filename: file, - filepath: filePath, + filepath: normalizedPath, stats: { size }, stream: entry as unknown as Readable, }; diff --git a/packages/core/data-transfer/src/file/providers/source/utils.ts b/packages/core/data-transfer/src/file/providers/source/utils.ts index d4470ad66f..e1d25aed0c 100644 --- a/packages/core/data-transfer/src/file/providers/source/utils.ts +++ b/packages/core/data-transfer/src/file/providers/source/utils.ts @@ -14,16 +14,30 @@ import path from 'path'; // Check if the directory of a given filePath (which can be either posix or win32) resolves to the same as the given posix-format path posixDirName export const isDirPathEquivalent = (posixDirName: string, filePath: string) => { - // if win32 convert to posix, then get dirname - const normalizedDir = path.posix.dirname(filePath.split(path.win32.sep).join(path.posix.sep)); + const normalizedDir = path.posix.dirname(unknownPathToPosix(filePath)); return isPathEquivalent(posixDirName, normalizedDir); }; // Check if two paths that can be either in posix or win32 format resolves to the same file export const isPathEquivalent = (fileA: string, fileB: string) => { // Check if paths appear to be win32 or posix, and if win32 convert to posix - const normalizedPathA = fileA.split(path.win32.sep).join(path.posix.sep); - const normalizedPathB = fileB.split(path.win32.sep).join(path.posix.sep); + const normalizedPathA = unknownPathToPosix(fileA); + const normalizedPathB = unknownPathToPosix(fileB); return !path.posix.relative(normalizedPathB, normalizedPathA).length; }; + +/** + * + * @param {string} filePath a path that may be either win32 or posix + * + * @returns {string} a posix path + */ +export const unknownPathToPosix = (filePath: string) => { + // if it includes a forward slash, it must be posix already -- we will not support win32 with mixed path separators + if (filePath.includes(path.posix.sep)) { + return filePath; + } + + return path.normalize(filePath).split(path.win32.sep).join(path.posix.sep); +}; From d6965e562c9d28cd078a59106417117790f675e0 Mon Sep 17 00:00:00 2001 From: Ben Irvin Date: Tue, 18 Apr 2023 12:38:30 +0200 Subject: [PATCH 12/39] add comment --- packages/core/data-transfer/src/file/providers/source/utils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/data-transfer/src/file/providers/source/utils.ts b/packages/core/data-transfer/src/file/providers/source/utils.ts index e1d25aed0c..ced1cf0f7d 100644 --- a/packages/core/data-transfer/src/file/providers/source/utils.ts +++ b/packages/core/data-transfer/src/file/providers/source/utils.ts @@ -13,6 +13,7 @@ import path from 'path'; * */ // Check if the directory of a given filePath (which can be either posix or win32) resolves to the same as the given posix-format path posixDirName +// We must be able to assume the first argument is a path, otherwise path.dirname will interpret a path without any slashes as the filename export const isDirPathEquivalent = (posixDirName: string, filePath: string) => { const normalizedDir = path.posix.dirname(unknownPathToPosix(filePath)); return isPathEquivalent(posixDirName, normalizedDir); From d11699c79bce17e3dacbd9f49797d1a3d620bd5c Mon Sep 17 00:00:00 2001 From: Ben Irvin Date: Tue, 18 Apr 2023 17:09:47 +0200 Subject: [PATCH 13/39] Add tests and comments and PR co-author credit Co-Authored-By: Anton Mikaskin Co-Authored-By: vnguyen42 --- .../providers/source/__tests__/index.test.ts | 109 +++++++++++++++++- .../src/file/providers/source/index.ts | 6 +- .../src/file/providers/source/utils.ts | 28 +++-- 3 files changed, 131 insertions(+), 12 deletions(-) diff --git a/packages/core/data-transfer/src/file/providers/source/__tests__/index.test.ts b/packages/core/data-transfer/src/file/providers/source/__tests__/index.test.ts index 7de7b2f551..edcd1c8f72 100644 --- a/packages/core/data-transfer/src/file/providers/source/__tests__/index.test.ts +++ b/packages/core/data-transfer/src/file/providers/source/__tests__/index.test.ts @@ -2,9 +2,10 @@ import { Readable } from 'stream'; import type { ILocalFileSourceProviderOptions } from '..'; import { createLocalFileSourceProvider } from '..'; +import { isFilePathInDirname, isPathEquivalent, unknownPathToPosix } from '../utils'; -describe('Stream assets', () => { - test('returns a stream', () => { +describe('File source provider', () => { + test('returns assets stream', () => { const options: ILocalFileSourceProviderOptions = { file: { path: './test-file', @@ -21,4 +22,108 @@ describe('Stream assets', () => { expect(stream instanceof Readable).toBeTruthy(); }); + + describe('utils', () => { + const unknownConversionCases = [ + ['some/path/on/posix', 'some/path/on/posix'], + ['some/path/on/posix/', 'some/path/on/posix/'], + ['some/path/on/posix.jpg', 'some/path/on/posix.jpg'], + ['file.jpg', 'file.jpg'], + ['noextension', 'noextension'], + ['some\\windows\\filename.jpg', 'some/windows/filename.jpg'], + ['some\\windows\\noendingslash', 'some/windows/noendingslash'], + ['some\\windows\\endingslash\\', 'some/windows/endingslash/'], + ]; + test.each(unknownConversionCases)('unknownPathToPosix: %p -> %p', (input, expected) => { + expect(unknownPathToPosix(input)).toEqual(expected); + }); + + const isFilePathInDirnameCases: [string, string, boolean][] = [ + // posix paths + ['some/path/on/posix', 'some/path/on/posix/file.jpg', true], + ['some/path/on/posix/', 'some/path/on/posix/file.jpg', true], + ['./some/path/on/posix', 'some/path/on/posix/file.jpg', true], + ['some/path/on/posix/', './some/path/on/posix/file.jpg', true], + ['some/path/on/posix/', 'some/path/on/posix/', false], // invalid; second method should include a filename + ['some/path/on/posix', 'some/path/on/posix', false], // 'posix' in second case should be interpreted as a filename + ['', 'file.jpg', true], + ['', './file.jpg', true], + ['./', './file.jpg', true], + ['noextension', 'noextension', false], // second case is a file + ['noextension', './noextension/file.jpg', true], + ['./noextension', './noextension/file.jpg', true], + ['./noextension', 'noextension/file.jpg', true], + ['noextension', 'noextension/noextension', true], + // win32 paths + ['some/path/on/win32', 'some\\path\\on\\win32\\file.jpg', true], + ['some/path/on/win32/', 'some\\path\\on\\win32\\file.jpg', true], + ['some/path/on/win32/', 'some\\path\\on\\win32\\', false], // invalid; second method should include a filename + ['some/path/on/win32', 'some\\path\\on\\win32', false], // 'win32' in second case should be interpreted as a filename + ['', 'file.jpg', true], + ['', '.\\file.jpg', true], + ['./', '.\\file.jpg', true], + ['noextension', 'noextension', false], // second case is a file + ['noextension', '.\\noextension\\file.jpg', true], + ['./noextension', '.\\noextension\\file.jpg', true], + ['./noextension', 'noextension\\file.jpg', true], + ['noextension', 'noextension\\noextension', true], + ]; + test.each(isFilePathInDirnameCases)( + 'isFilePathInDirname: %p : %p -> %p', + (inputA, inputB, expected) => { + expect(isFilePathInDirname(inputA, inputB)).toEqual(expected); + } + ); + + const isPathEquivalentCases: [string, string, boolean][] = [ + // POSITIVES + // root level + ['file.jpg', 'file.jpg', true], + ['file.jpg', '.\\file.jpg', true], + ['file.jpg', './file.jpg', true], + // cwd root level (posix) + ['./file.jpg', 'file.jpg', true], + ['./file.jpg', './file.jpg', true], + ['./file.jpg', '.\\file.jpg', true], + // cwd root level (win32) + ['.\\file.jpg', 'file.jpg', true], + ['.\\file.jpg', './file.jpg', true], + ['.\\file.jpg', '.\\file.jpg', true], + // directory with file (posix) + ['one/two/file.jpg', 'one/two/file.jpg', true], + ['one/two/file.jpg', './one/two/file.jpg', true], + ['one/two/file.jpg', 'one\\two\\file.jpg', true], + ['one/two/file.jpg', '.\\one\\two\\file.jpg', true], + // cwd with file (posix) + ['./one/two/file.jpg', 'one/two/file.jpg', true], + ['./one/two/file.jpg', './one/two/file.jpg', true], + ['./one/two/file.jpg', 'one\\two\\file.jpg', true], + ['./one/two/file.jpg', '.\\one\\two\\file.jpg', true], + // directory with file (win32) + ['one\\two\\file.jpg', 'one/two/file.jpg', true], + ['one\\two\\file.jpg', './one/two/file.jpg', true], + ['one\\two\\file.jpg', '.\\one\\two\\file.jpg', true], + ['one\\two\\file.jpg', 'one\\two\\file.jpg', true], + // cwd with file (win32) + ['.\\one\\two\\file.jpg', 'one/two/file.jpg', true], + ['.\\one\\two\\file.jpg', './one/two/file.jpg', true], + ['.\\one\\two\\file.jpg', '.\\one\\two\\file.jpg', true], + ['.\\one\\two\\file.jpg', 'one\\two\\file.jpg', true], + + // NEGATIVES + ['file.jpg', 'one/file.jpg', false], + ['file.jpg', 'one\\file.jpg', false], + ['file.jpg', '/file.jpg', false], + ['file.jpg', '\\file.jpg', false], + ['one/file.jpg', '\\one\\file.jpg', false], + ['one/file.jpg', '/one/file.jpg', false], + ['one/file.jpg', 'file.jpg', false], + ]; + test.each(isPathEquivalentCases)( + 'isPathEquivalent: %p : %p -> %p', + (inputA, inputB, expected) => { + expect(isPathEquivalent(inputA, inputB)).toEqual(expected); + } + ); + }); }); diff --git a/packages/core/data-transfer/src/file/providers/source/index.ts b/packages/core/data-transfer/src/file/providers/source/index.ts index 119d527aee..53e5f746bb 100644 --- a/packages/core/data-transfer/src/file/providers/source/index.ts +++ b/packages/core/data-transfer/src/file/providers/source/index.ts @@ -15,7 +15,7 @@ import type { IAsset, IMetadata, ISourceProvider, ProviderType } from '../../../ import { createDecryptionCipher } from '../../../utils/encryption'; import { collect } from '../../../utils/stream'; import { ProviderInitializationError, ProviderTransferError } from '../../../errors/providers'; -import { isDirPathEquivalent, isPathEquivalent, unknownPathToPosix } from './utils'; +import { isFilePathInDirname, isPathEquivalent, unknownPathToPosix } from './utils'; type StreamItemArray = Parameters[0]; @@ -143,7 +143,7 @@ class LocalFileSourceProvider implements ISourceProvider { if (entry.type !== 'File') { return false; } - return isDirPathEquivalent('assets/uploads', filePath); + return isFilePathInDirname('assets/uploads', filePath); }, onentry(entry) { // TODO: Check if we need to handle win32 paths here for the assets @@ -203,7 +203,7 @@ class LocalFileSourceProvider implements ISourceProvider { return false; } - return isDirPathEquivalent(directory, filePath); + return isFilePathInDirname(directory, filePath); }, async onentry(entry) { diff --git a/packages/core/data-transfer/src/file/providers/source/utils.ts b/packages/core/data-transfer/src/file/providers/source/utils.ts index ced1cf0f7d..60111f5da3 100644 --- a/packages/core/data-transfer/src/file/providers/source/utils.ts +++ b/packages/core/data-transfer/src/file/providers/source/utils.ts @@ -12,23 +12,37 @@ import path from 'path'; * * */ -// Check if the directory of a given filePath (which can be either posix or win32) resolves to the same as the given posix-format path posixDirName -// We must be able to assume the first argument is a path, otherwise path.dirname will interpret a path without any slashes as the filename -export const isDirPathEquivalent = (posixDirName: string, filePath: string) => { +/** + * Check if the directory of a given filePath (which can be either posix or win32) resolves to the same as the given posix-format path posixDirName + * We must be able to assume the first argument is a path to a directory and the second is a path to a file, otherwise path.dirname will interpret a path without any slashes as the filename + * + * @param {string} posixDirName A posix path pointing to a directory + * @param {string} filePath an unknown filesystem path pointing to a file + * @returns {boolean} is the file located in the given directory + */ +export const isFilePathInDirname = (posixDirName: string, filePath: string) => { const normalizedDir = path.posix.dirname(unknownPathToPosix(filePath)); return isPathEquivalent(posixDirName, normalizedDir); }; -// Check if two paths that can be either in posix or win32 format resolves to the same file -export const isPathEquivalent = (fileA: string, fileB: string) => { +/** + * Check if two paths that can be either in posix or win32 format resolves to the same file + * + * @param {string} pathA a path that may be either win32 or posix + * @param {string} pathB a path that may be either win32 or posix + * + * @returns {boolean} do paths point to the same place + */ +export const isPathEquivalent = (pathA: string, pathB: string) => { // Check if paths appear to be win32 or posix, and if win32 convert to posix - const normalizedPathA = unknownPathToPosix(fileA); - const normalizedPathB = unknownPathToPosix(fileB); + const normalizedPathA = path.posix.normalize(unknownPathToPosix(pathA)); + const normalizedPathB = path.posix.normalize(unknownPathToPosix(pathB)); return !path.posix.relative(normalizedPathB, normalizedPathA).length; }; /** + * Convert an unknown format path (win32 or posix) to a posix path * * @param {string} filePath a path that may be either win32 or posix * From 4c19721b18e230bec4a1a0d4d739ec985249d298 Mon Sep 17 00:00:00 2001 From: Ben Irvin Date: Tue, 18 Apr 2023 17:39:00 +0200 Subject: [PATCH 14/39] test special characters --- .../src/file/providers/source/__tests__/index.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/core/data-transfer/src/file/providers/source/__tests__/index.test.ts b/packages/core/data-transfer/src/file/providers/source/__tests__/index.test.ts index edcd1c8f72..ca1ecadfa7 100644 --- a/packages/core/data-transfer/src/file/providers/source/__tests__/index.test.ts +++ b/packages/core/data-transfer/src/file/providers/source/__tests__/index.test.ts @@ -109,6 +109,9 @@ describe('File source provider', () => { ['.\\one\\two\\file.jpg', './one/two/file.jpg', true], ['.\\one\\two\\file.jpg', '.\\one\\two\\file.jpg', true], ['.\\one\\two\\file.jpg', 'one\\two\\file.jpg', true], + // special characters + [".\\one\\two\\fi ' ^&*() le.jpg", "one/two/fi ' ^&*() le.jpg", true], // valid characters on win32 + ['test/backslash\\file.jpg', 'test/backslash\\file.jpg', true], // backlash is valid on posix but not win32 // NEGATIVES ['file.jpg', 'one/file.jpg', false], @@ -118,6 +121,7 @@ describe('File source provider', () => { ['one/file.jpg', '\\one\\file.jpg', false], ['one/file.jpg', '/one/file.jpg', false], ['one/file.jpg', 'file.jpg', false], + ['test/mixedslash\\file.jpg', 'test/mixedslash/file.jpg', false], // windows path with mixed path separators should fail ]; test.each(isPathEquivalentCases)( 'isPathEquivalent: %p : %p -> %p', From e2f4e7b296a29833e5ccea8afa075cded6d08921 Mon Sep 17 00:00:00 2001 From: Ben Irvin Date: Tue, 18 Apr 2023 17:48:02 +0200 Subject: [PATCH 15/39] add comment --- .../src/file/providers/destination/utils.ts | 1 + .../file/providers/source/__tests__/index.test.ts | 15 +++++++-------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/core/data-transfer/src/file/providers/destination/utils.ts b/packages/core/data-transfer/src/file/providers/destination/utils.ts index fe49954b5d..90eef24750 100644 --- a/packages/core/data-transfer/src/file/providers/destination/utils.ts +++ b/packages/core/data-transfer/src/file/providers/destination/utils.ts @@ -9,6 +9,7 @@ import tar from 'tar-stream'; export const createFilePathFactory = (type: string) => (fileIndex = 0): string => { + // always write tar files with posix paths so we have a standard format for paths regardless of system return posix.join( // "{type}" directory type, diff --git a/packages/core/data-transfer/src/file/providers/source/__tests__/index.test.ts b/packages/core/data-transfer/src/file/providers/source/__tests__/index.test.ts index ca1ecadfa7..ee1ac5e2a5 100644 --- a/packages/core/data-transfer/src/file/providers/source/__tests__/index.test.ts +++ b/packages/core/data-transfer/src/file/providers/source/__tests__/index.test.ts @@ -44,12 +44,10 @@ describe('File source provider', () => { ['some/path/on/posix/', 'some/path/on/posix/file.jpg', true], ['./some/path/on/posix', 'some/path/on/posix/file.jpg', true], ['some/path/on/posix/', './some/path/on/posix/file.jpg', true], - ['some/path/on/posix/', 'some/path/on/posix/', false], // invalid; second method should include a filename - ['some/path/on/posix', 'some/path/on/posix', false], // 'posix' in second case should be interpreted as a filename - ['', 'file.jpg', true], + ['some/path/on/posix/', 'some/path/on/posix/', false], // invalid; second should include a filename + ['some/path/on/posix', 'some/path/on/posix', false], // 'posix' in second should be interpreted as a filename ['', './file.jpg', true], ['./', './file.jpg', true], - ['noextension', 'noextension', false], // second case is a file ['noextension', './noextension/file.jpg', true], ['./noextension', './noextension/file.jpg', true], ['./noextension', 'noextension/file.jpg', true], @@ -57,16 +55,17 @@ describe('File source provider', () => { // win32 paths ['some/path/on/win32', 'some\\path\\on\\win32\\file.jpg', true], ['some/path/on/win32/', 'some\\path\\on\\win32\\file.jpg', true], - ['some/path/on/win32/', 'some\\path\\on\\win32\\', false], // invalid; second method should include a filename - ['some/path/on/win32', 'some\\path\\on\\win32', false], // 'win32' in second case should be interpreted as a filename - ['', 'file.jpg', true], + ['some/path/on/win32/', 'some\\path\\on\\win32\\', false], // invalid; second should include a filename + ['some/path/on/win32', 'some\\path\\on\\win32', false], // 'win32' in second should be interpreted as a filename ['', '.\\file.jpg', true], ['./', '.\\file.jpg', true], - ['noextension', 'noextension', false], // second case is a file ['noextension', '.\\noextension\\file.jpg', true], ['./noextension', '.\\noextension\\file.jpg', true], ['./noextension', 'noextension\\file.jpg', true], ['noextension', 'noextension\\noextension', true], + // no path structure + ['', 'file.jpg', true], + ['noextension', 'noextension', false], // second case is a file ]; test.each(isFilePathInDirnameCases)( 'isFilePathInDirname: %p : %p -> %p', From 568de18120c71c093227c068927cf8f68f305719 Mon Sep 17 00:00:00 2001 From: Ben Irvin Date: Tue, 18 Apr 2023 17:55:33 +0200 Subject: [PATCH 16/39] test mixed path behaviour --- .../src/file/providers/source/__tests__/index.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/data-transfer/src/file/providers/source/__tests__/index.test.ts b/packages/core/data-transfer/src/file/providers/source/__tests__/index.test.ts index ee1ac5e2a5..867bc45b61 100644 --- a/packages/core/data-transfer/src/file/providers/source/__tests__/index.test.ts +++ b/packages/core/data-transfer/src/file/providers/source/__tests__/index.test.ts @@ -33,6 +33,7 @@ describe('File source provider', () => { ['some\\windows\\filename.jpg', 'some/windows/filename.jpg'], ['some\\windows\\noendingslash', 'some/windows/noendingslash'], ['some\\windows\\endingslash\\', 'some/windows/endingslash/'], + ['some\\windows/mixed', 'some\\windows/mixed'], // improper usage resulting in invalid path if provided mixed windows path, but test expected behaviour ]; test.each(unknownConversionCases)('unknownPathToPosix: %p -> %p', (input, expected) => { expect(unknownPathToPosix(input)).toEqual(expected); From b0bc481a4784a9ab9b6280d4163f4a3513dd7bef Mon Sep 17 00:00:00 2001 From: Ben Irvin Date: Tue, 18 Apr 2023 18:01:29 +0200 Subject: [PATCH 17/39] comments --- packages/core/data-transfer/src/file/providers/source/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/data-transfer/src/file/providers/source/index.ts b/packages/core/data-transfer/src/file/providers/source/index.ts index 53e5f746bb..03f29cb5e9 100644 --- a/packages/core/data-transfer/src/file/providers/source/index.ts +++ b/packages/core/data-transfer/src/file/providers/source/index.ts @@ -146,7 +146,6 @@ class LocalFileSourceProvider implements ISourceProvider { return isFilePathInDirname('assets/uploads', filePath); }, onentry(entry) { - // TODO: Check if we need to handle win32 paths here for the assets const { path: filePath, size = 0 } = entry; const normalizedPath = unknownPathToPosix(filePath); const file = path.basename(normalizedPath); @@ -189,6 +188,7 @@ class LocalFileSourceProvider implements ISourceProvider { return chain(streams); } + // `directory` must be posix formatted path #streamJsonlDirectory(directory: string) { const inStream = this.#getBackupStream(); From 7233e0c0027cadd8e9aa385905a716e600745c04 Mon Sep 17 00:00:00 2001 From: Josh <37798644+joshuaellis@users.noreply.github.com> Date: Wed, 19 Apr 2023 11:27:41 +0100 Subject: [PATCH 18/39] fix: use the full key par the index for the type --- .../RepeatableComponent/components/Component.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/core/admin/admin/src/content-manager/components/RepeatableComponent/components/Component.js b/packages/core/admin/admin/src/content-manager/components/RepeatableComponent/components/Component.js index aca74533b5..249db3fcad 100644 --- a/packages/core/admin/admin/src/content-manager/components/RepeatableComponent/components/Component.js +++ b/packages/core/admin/admin/src/content-manager/components/RepeatableComponent/components/Component.js @@ -97,11 +97,17 @@ const DraggedItem = ({ const accordionRef = useRef(null); const { formatMessage } = useIntl(); - const [parentFieldName] = componentFieldName.split('.'); + /** + * The last item in the fieldName array will be the index of this component. + * Drag and drop should be isolated to the parent component so nested repeatable + * components are not affected by the drag and drop of the parent component in + * their own re-ordering context. + */ + const componentKey = componentFieldName.split('.').slice(0, -1).join('.'); const [{ handlerId, isDragging, handleKeyDown }, boxRef, dropRef, dragRef, dragPreviewRef] = useDragAndDrop(!isReadOnly, { - type: `${ItemTypes.COMPONENT}_${parentFieldName}`, + type: `${ItemTypes.COMPONENT}_${componentKey}`, index, item: { displayedValue, From d48dd1c41331012528fe0e2af19ee38e80806641 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20de=20Juvigny?= Date: Wed, 19 Apr 2023 16:53:28 +0200 Subject: [PATCH 19/39] Register custom field field sizes --- .../core/content-manager/server/bootstrap.js | 1 + .../server/services/field-sizes.js | 24 ++++++++++++++++++- .../plugins/color-picker/server/register.js | 4 ++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/core/content-manager/server/bootstrap.js b/packages/core/content-manager/server/bootstrap.js index 5d12cbc871..e6f16ef481 100644 --- a/packages/core/content-manager/server/bootstrap.js +++ b/packages/core/content-manager/server/bootstrap.js @@ -6,4 +6,5 @@ module.exports = async () => { await getService('components').syncConfigurations(); await getService('content-types').syncConfigurations(); await getService('permission').registerPermissions(); + getService('field-sizes').registerCustomFields(); }; diff --git a/packages/core/content-manager/server/services/field-sizes.js b/packages/core/content-manager/server/services/field-sizes.js index bdbeffe83f..4543b8f66f 100644 --- a/packages/core/content-manager/server/services/field-sizes.js +++ b/packages/core/content-manager/server/services/field-sizes.js @@ -44,7 +44,7 @@ const fieldSizes = { uid: defaultSize, }; -module.exports = () => ({ +module.exports = ({ strapi }) => ({ getAllFieldSizes() { return fieldSizes; }, @@ -60,4 +60,26 @@ module.exports = () => ({ return fieldSize; }, + setFieldSize(type, size) { + if (!type) { + throw new Error('The type is required'); + } + + if (!size) { + throw new Error('The size is required'); + } + + fieldSizes[type] = size; + }, + registerCustomFields() { + // Find all custom fields already registered + const customFields = strapi.container.get('custom-fields').getAll(); + + // If they have a custom field size, register it + Object.entries(customFields).forEach(([uid, customField]) => { + if (customField.inputSize) { + this.setFieldSize(uid, customField.inputSize); + } + }); + }, }); diff --git a/packages/plugins/color-picker/server/register.js b/packages/plugins/color-picker/server/register.js index e46cc3feb3..c91de8b385 100644 --- a/packages/plugins/color-picker/server/register.js +++ b/packages/plugins/color-picker/server/register.js @@ -5,5 +5,9 @@ module.exports = ({ strapi }) => { name: 'color', plugin: 'color-picker', type: 'string', + inputSize: { + default: 4, + isResizable: true, + }, }); }; From 5942cce59102f4bf2d7d4286082e568336d93a93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20de=20Juvigny?= Date: Wed, 19 Apr 2023 17:57:32 +0200 Subject: [PATCH 20/39] Update getDefaultFieldSize --- .../services/utils/configuration/layouts.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/core/content-manager/server/services/utils/configuration/layouts.js b/packages/core/content-manager/server/services/utils/configuration/layouts.js index 18dd5f5c6e..983366b162 100644 --- a/packages/core/content-manager/server/services/utils/configuration/layouts.js +++ b/packages/core/content-manager/server/services/utils/configuration/layouts.js @@ -20,9 +20,18 @@ const isAllowedFieldSize = (type, size) => { return size <= MAX_ROW_SIZE; }; -const getDefaultFieldSize = (type) => { +const getDefaultFieldSize = (attribute) => { + // Check if it's a custom field with a custom size + if (attribute.customField) { + const customField = strapi.container.get('custom-fields').get(attribute.customField); + if (customField.inputSize) { + return customField.inputSize; + } + } + + // Get the default size for the field type const { getFieldSize } = getService('field-sizes'); - return getFieldSize(type).default; + return getFieldSize(attribute.type).default; }; async function createDefaultLayouts(schema) { @@ -127,7 +136,7 @@ const appendToEditLayout = (layout = [], keysToAppend, schema) => { for (const key of keysToAppend) { const attribute = schema.attributes[key]; - const attributeSize = getDefaultFieldSize(attribute.type); + const attributeSize = getDefaultFieldSize(attribute); const currenRowSize = rowSize(layout[currentRowIndex]); if (currenRowSize + attributeSize > MAX_ROW_SIZE) { From e725596ca7f6d986a4b00391cce8a0a659930873 Mon Sep 17 00:00:00 2001 From: Marc-Roig Date: Thu, 20 Apr 2023 12:08:15 +0200 Subject: [PATCH 21/39] feat: add indexes to morph tables --- packages/core/database/lib/metadata/relations.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/core/database/lib/metadata/relations.js b/packages/core/database/lib/metadata/relations.js index dbafdb9f45..ee662b2ec6 100644 --- a/packages/core/database/lib/metadata/relations.js +++ b/packages/core/database/lib/metadata/relations.js @@ -244,6 +244,16 @@ const createMorphToMany = (attributeName, attribute, meta, metadata) => { name: `${joinTableName}_fk`, columns: [joinColumnName], }, + { + name: `${joinTableName}_order_index`, + columns: ['order'], + type: null, + }, + { + name: `${joinTableName}_id_column_index`, + columns: [idColumnName], + type: null, + }, ], foreignKeys: [ { From cb3588782cb93e089419c6875e464a9c48da0249 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20de=20Juvigny?= Date: Thu, 20 Apr 2023 12:24:55 +0200 Subject: [PATCH 22/39] Use custom field input size in admin --- .../pages/EditSettingsView/index.js | 14 +++++++++++++- .../pages/EditSettingsView/reducer.js | 10 ++++++++-- .../server/services/utils/configuration/layouts.js | 2 +- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/index.js b/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/index.js index 77c3af9a47..28e08bf15f 100644 --- a/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/index.js +++ b/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/index.js @@ -9,7 +9,13 @@ import flatMap from 'lodash/flatMap'; import isEqual from 'lodash/isEqual'; import get from 'lodash/get'; import set from 'lodash/set'; -import { useNotification, useTracking, ConfirmDialog, Link } from '@strapi/helper-plugin'; +import { + useNotification, + useTracking, + useCustomFields, + ConfirmDialog, + Link, +} from '@strapi/helper-plugin'; import { useHistory } from 'react-router-dom'; import { Main, @@ -52,6 +58,11 @@ const EditSettingsView = ({ mainLayout, components, isContentTypeView, slug, upd const modelName = get(mainLayout, ['info', 'displayName'], ''); const attributes = get(modifiedData, ['attributes'], {}); const fieldSizes = useSelector(selectFieldSizes); + const customFieldsRegistry = useCustomFields(); + console.log('registry', customFieldsRegistry.getAll()); + + // TODO: find out why logs don't show on port 8000 + // TODO: find out why custom fields registry is empty const entryTitleOptions = Object.keys(attributes).filter((attr) => { const type = get(attributes, [attr, 'type'], ''); @@ -322,6 +333,7 @@ const EditSettingsView = ({ mainLayout, components, isContentTypeView, slug, upd type: 'ON_ADD_FIELD', name: field, fieldSizes, + getCustomField: customFieldsRegistry.get, }); }} onRemoveField={(rowId, index) => { diff --git a/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/reducer.js b/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/reducer.js index 6b79791202..d9cad8c5f8 100644 --- a/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/reducer.js +++ b/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/reducer.js @@ -30,8 +30,14 @@ const reducer = (state = initialState, action) => } case 'ON_ADD_FIELD': { const newState = cloneDeep(state); - const type = get(newState, ['modifiedData', 'attributes', action.name, 'type'], ''); - const size = action.fieldSizes[type]?.default ?? DEFAULT_FIELD_SIZE; + const attribute = get(newState, ['modifiedData', 'attributes', action.name], {}); + + // Get the default size, checking custom fields first, then the type and generic defaults + const size = + action.fieldSizes[attribute?.customField]?.default ?? + action.fieldSizes[attribute?.type]?.default ?? + DEFAULT_FIELD_SIZE; + const listSize = get(newState, layoutPathEdit, []).length; const actualRowContentPath = [...layoutPathEdit, listSize - 1, 'rowContent']; const rowContentToSet = get(newState, actualRowContentPath, []); diff --git a/packages/core/content-manager/server/services/utils/configuration/layouts.js b/packages/core/content-manager/server/services/utils/configuration/layouts.js index 983366b162..03ce8d2854 100644 --- a/packages/core/content-manager/server/services/utils/configuration/layouts.js +++ b/packages/core/content-manager/server/services/utils/configuration/layouts.js @@ -25,7 +25,7 @@ const getDefaultFieldSize = (attribute) => { if (attribute.customField) { const customField = strapi.container.get('custom-fields').get(attribute.customField); if (customField.inputSize) { - return customField.inputSize; + return customField.inputSize.default; } } From 0dd4f8f0687ef1b9f4882e9bb5b0298a8ba69ff7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20de=20Juvigny?= Date: Thu, 20 Apr 2023 12:34:09 +0200 Subject: [PATCH 23/39] Remove color picker inputSize --- packages/plugins/color-picker/server/register.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/plugins/color-picker/server/register.js b/packages/plugins/color-picker/server/register.js index c91de8b385..e46cc3feb3 100644 --- a/packages/plugins/color-picker/server/register.js +++ b/packages/plugins/color-picker/server/register.js @@ -5,9 +5,5 @@ module.exports = ({ strapi }) => { name: 'color', plugin: 'color-picker', type: 'string', - inputSize: { - default: 4, - isResizable: true, - }, }); }; From 8253cfdd71f3747959c09a4f4e58ed5471701106 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20de=20Juvigny?= Date: Thu, 20 Apr 2023 12:49:46 +0200 Subject: [PATCH 24/39] Handle isResizable in the admin --- .../pages/EditSettingsView/components/ModalForm.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/components/ModalForm.js b/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/components/ModalForm.js index 3b532666bb..157a11827a 100644 --- a/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/components/ModalForm.js +++ b/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/components/ModalForm.js @@ -102,7 +102,9 @@ const ModalForm = ({ onMetaChange, onSizeChange }) => { ); }); - const { isResizable } = fieldSizes[attributes[selectedField].type]; + // Check for a custom input provided by a custom field, or use the default one for that type + const { type, customField } = attributes[selectedField]; + const { isResizable } = fieldSizes[customField] ?? fieldSizes[type]; const sizeField = ( From b5e1372bd43cbed432d4d18bc61361cb8e6f3209 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20de=20Juvigny?= Date: Thu, 20 Apr 2023 12:55:22 +0200 Subject: [PATCH 25/39] Code cleanup --- .../admin/src/content-manager/pages/EditSettingsView/index.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/index.js b/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/index.js index 28e08bf15f..d52c64a4fd 100644 --- a/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/index.js +++ b/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/index.js @@ -59,10 +59,6 @@ const EditSettingsView = ({ mainLayout, components, isContentTypeView, slug, upd const attributes = get(modifiedData, ['attributes'], {}); const fieldSizes = useSelector(selectFieldSizes); const customFieldsRegistry = useCustomFields(); - console.log('registry', customFieldsRegistry.getAll()); - - // TODO: find out why logs don't show on port 8000 - // TODO: find out why custom fields registry is empty const entryTitleOptions = Object.keys(attributes).filter((attr) => { const type = get(attributes, [attr, 'type'], ''); From 095af63f5ea4a0b3dfa1787e91a8f57dbd08ab32 Mon Sep 17 00:00:00 2001 From: Ben Irvin Date: Fri, 21 Apr 2023 09:23:29 +0200 Subject: [PATCH 26/39] fix getmetadata --- .../core/data-transfer/src/file/providers/source/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/data-transfer/src/file/providers/source/index.ts b/packages/core/data-transfer/src/file/providers/source/index.ts index 03f29cb5e9..0072a88814 100644 --- a/packages/core/data-transfer/src/file/providers/source/index.ts +++ b/packages/core/data-transfer/src/file/providers/source/index.ts @@ -96,11 +96,11 @@ class LocalFileSourceProvider implements ISourceProvider { } async getMetadata() { - if (this.#metadata) { + if (!this.#metadata) { await this.#loadMetadata(); } - return this.#metadata || null; + return this.#metadata ?? null; } async getSchemas() { From cb2133d8ffd36316426b386a97dd7d27ad98a4cc Mon Sep 17 00:00:00 2001 From: Ben Irvin Date: Fri, 21 Apr 2023 09:31:04 +0200 Subject: [PATCH 27/39] remove return value --- packages/core/data-transfer/src/file/providers/source/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/data-transfer/src/file/providers/source/index.ts b/packages/core/data-transfer/src/file/providers/source/index.ts index 0072a88814..80c993f1cc 100644 --- a/packages/core/data-transfer/src/file/providers/source/index.ts +++ b/packages/core/data-transfer/src/file/providers/source/index.ts @@ -92,7 +92,6 @@ class LocalFileSourceProvider implements ISourceProvider { async #loadMetadata() { const backupStream = this.#getBackupStream(); this.#metadata = await this.#parseJSONFile(backupStream, METADATA_FILE_PATH); - return this.#metadata; } async getMetadata() { From 151e92e1a0e0323b90a7baf0fc50090ea91b0944 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20de=20Juvigny?= Date: Fri, 21 Apr 2023 09:55:46 +0200 Subject: [PATCH 28/39] Stricter object check --- .../lib/core/registries/__tests__/custom-fields.test.js | 3 +++ packages/core/strapi/lib/core/registries/custom-fields.js | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/core/strapi/lib/core/registries/__tests__/custom-fields.test.js b/packages/core/strapi/lib/core/registries/__tests__/custom-fields.test.js index 283bcf5b36..938968d129 100644 --- a/packages/core/strapi/lib/core/registries/__tests__/custom-fields.test.js +++ b/packages/core/strapi/lib/core/registries/__tests__/custom-fields.test.js @@ -101,6 +101,9 @@ describe('Custom fields registry', () => { expect(() => customFields.add({ ...mockCF, inputSize: 'small' })).toThrowError( `inputSize should be an object with 'default' and 'isResizable' keys` ); + expect(() => customFields.add({ ...mockCF, inputSize: ['array'] })).toThrowError( + `inputSize should be an object with 'default' and 'isResizable' keys` + ); expect(() => customFields.add({ ...mockCF, inputSize: { default: 99, isResizable: true } }) ).toThrowError('Custom fields require a valid default input size'); diff --git a/packages/core/strapi/lib/core/registries/custom-fields.js b/packages/core/strapi/lib/core/registries/custom-fields.js index 9b8dc3ab41..597b6ddb1b 100644 --- a/packages/core/strapi/lib/core/registries/custom-fields.js +++ b/packages/core/strapi/lib/core/registries/custom-fields.js @@ -1,6 +1,6 @@ 'use strict'; -const { has } = require('lodash/fp'); +const { has, isObject } = require('lodash/fp'); const ALLOWED_TYPES = [ 'biginteger', @@ -59,7 +59,7 @@ const customFieldsRegistry = (strapi) => { // Validate inputSize when provided if (inputSize) { if ( - typeof inputSize !== 'object' || + !isObject(inputSize) || !has('default', inputSize) || !has('isResizable', inputSize) ) { From 70e60071e4c5ed17bdbb1f7390d5f9fb4ba4adc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20de=20Juvigny?= Date: Fri, 21 Apr 2023 10:19:30 +0200 Subject: [PATCH 29/39] Improve and test setCustomFieldInputSizes --- .../core/content-manager/server/bootstrap.js | 2 +- .../services/__tests__/field-sizes.test.js | 46 ++++++++-- .../server/services/field-sizes.js | 83 ++++++++++--------- 3 files changed, 88 insertions(+), 43 deletions(-) diff --git a/packages/core/content-manager/server/bootstrap.js b/packages/core/content-manager/server/bootstrap.js index e6f16ef481..99768e9414 100644 --- a/packages/core/content-manager/server/bootstrap.js +++ b/packages/core/content-manager/server/bootstrap.js @@ -6,5 +6,5 @@ module.exports = async () => { await getService('components').syncConfigurations(); await getService('content-types').syncConfigurations(); await getService('permission').registerPermissions(); - getService('field-sizes').registerCustomFields(); + getService('field-sizes').setCustomFieldInputSizes(); }; diff --git a/packages/core/content-manager/server/services/__tests__/field-sizes.test.js b/packages/core/content-manager/server/services/__tests__/field-sizes.test.js index 43c79cc101..caa8a90eee 100644 --- a/packages/core/content-manager/server/services/__tests__/field-sizes.test.js +++ b/packages/core/content-manager/server/services/__tests__/field-sizes.test.js @@ -1,10 +1,35 @@ 'use strict'; -const fieldSizesService = require('../field-sizes'); +const createFieldSizesService = require('../field-sizes'); + +const strapi = { + container: { + // Mock container.get('custom-fields') + get: jest.fn(() => ({ + // Mock container.get('custom-fields').getAll() + getAll: jest.fn(() => ({ + 'plugin::mycustomfields.color': { + name: 'color', + plugin: 'mycustomfields', + type: 'string', + }, + 'plugin::mycustomfields.smallColor': { + name: 'smallColor', + plugin: 'mycustomfields', + type: 'string', + inputSize: { + default: 4, + isResizable: false, + }, + }, + })), + })), + }, +}; describe('field sizes service', () => { it('should return the correct field sizes', () => { - const { getAllFieldSizes } = fieldSizesService(); + const { getAllFieldSizes } = createFieldSizesService({ strapi }); const fieldSizes = getAllFieldSizes(); Object.values(fieldSizes).forEach((fieldSize) => { expect(typeof fieldSize.isResizable).toBe('boolean'); @@ -13,21 +38,32 @@ describe('field sizes service', () => { }); it('should return the correct field size for a given type', () => { - const { getFieldSize } = fieldSizesService(); + const { getFieldSize } = createFieldSizesService({ strapi }); const fieldSize = getFieldSize('string'); expect(fieldSize.isResizable).toBe(true); expect(fieldSize.default).toBe(6); }); it('should throw an error if the type is not found', () => { - const { getFieldSize } = fieldSizesService(); + const { getFieldSize } = createFieldSizesService({ strapi }); expect(() => getFieldSize('not-found')).toThrowError( 'Could not find field size for type not-found' ); }); it('should throw an error if the type is not provided', () => { - const { getFieldSize } = fieldSizesService(); + const { getFieldSize } = createFieldSizesService({ strapi }); expect(() => getFieldSize()).toThrowError('The type is required'); }); + + it('should set the custom fields input sizes', () => { + const { setCustomFieldInputSizes, getAllFieldSizes } = createFieldSizesService({ strapi }); + setCustomFieldInputSizes(); + const fieldSizes = getAllFieldSizes(); + console.log(fieldSizes); + + expect(fieldSizes).not.toHaveProperty('plugin::mycustomfields.color'); + expect(fieldSizes['plugin::mycustomfields.smallColor'].default).toBe(4); + expect(fieldSizes['plugin::mycustomfields.smallColor'].isResizable).toBe(false); + }); }); diff --git a/packages/core/content-manager/server/services/field-sizes.js b/packages/core/content-manager/server/services/field-sizes.js index 4543b8f66f..2d4487d812 100644 --- a/packages/core/content-manager/server/services/field-sizes.js +++ b/packages/core/content-manager/server/services/field-sizes.js @@ -44,42 +44,51 @@ const fieldSizes = { uid: defaultSize, }; -module.exports = ({ strapi }) => ({ - getAllFieldSizes() { - return fieldSizes; - }, - getFieldSize(type) { - if (!type) { - throw new Error('The type is required'); - } +const createFieldSizesService = ({ strapi }) => { + const fieldSizesService = { + getAllFieldSizes() { + return fieldSizes; + }, - const fieldSize = fieldSizes[type]; - if (!fieldSize) { - throw new Error(`Could not find field size for type ${type}`); - } - - return fieldSize; - }, - setFieldSize(type, size) { - if (!type) { - throw new Error('The type is required'); - } - - if (!size) { - throw new Error('The size is required'); - } - - fieldSizes[type] = size; - }, - registerCustomFields() { - // Find all custom fields already registered - const customFields = strapi.container.get('custom-fields').getAll(); - - // If they have a custom field size, register it - Object.entries(customFields).forEach(([uid, customField]) => { - if (customField.inputSize) { - this.setFieldSize(uid, customField.inputSize); + getFieldSize(type) { + if (!type) { + throw new Error('The type is required'); } - }); - }, -}); + + const fieldSize = fieldSizes[type]; + if (!fieldSize) { + throw new Error(`Could not find field size for type ${type}`); + } + + return fieldSize; + }, + + setFieldSize(type, size) { + if (!type) { + throw new Error('The type is required'); + } + + if (!size) { + throw new Error('The size is required'); + } + + fieldSizes[type] = size; + }, + + setCustomFieldInputSizes() { + // Find all custom fields already registered + const customFields = strapi.container.get('custom-fields').getAll(); + + // If they have a custom field size, register it + Object.entries(customFields).forEach(([uid, customField]) => { + if (customField.inputSize) { + fieldSizesService.setFieldSize(uid, customField.inputSize); + } + }); + }, + }; + + return fieldSizesService; +}; + +module.exports = createFieldSizesService; From 8ae384894e563c33eecdbc51d1100fd58b088536 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20de=20Juvigny?= Date: Fri, 21 Apr 2023 10:24:22 +0200 Subject: [PATCH 30/39] Use ApplicationError for validation --- .../services/__tests__/field-sizes.test.js | 19 +++++++++++++++---- .../server/services/field-sizes.js | 10 ++++++---- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/core/content-manager/server/services/__tests__/field-sizes.test.js b/packages/core/content-manager/server/services/__tests__/field-sizes.test.js index caa8a90eee..0b4e5d3780 100644 --- a/packages/core/content-manager/server/services/__tests__/field-sizes.test.js +++ b/packages/core/content-manager/server/services/__tests__/field-sizes.test.js @@ -1,5 +1,6 @@ 'use strict'; +const { ApplicationError } = require('@strapi/utils').errors; const createFieldSizesService = require('../field-sizes'); const strapi = { @@ -46,14 +47,24 @@ describe('field sizes service', () => { it('should throw an error if the type is not found', () => { const { getFieldSize } = createFieldSizesService({ strapi }); - expect(() => getFieldSize('not-found')).toThrowError( - 'Could not find field size for type not-found' - ); + + try { + getFieldSize('not-found'); + } catch (error) { + expect(error instanceof ApplicationError).toBe(true); + expect(error.message).toBe('Could not find field size for type not-found'); + } }); it('should throw an error if the type is not provided', () => { const { getFieldSize } = createFieldSizesService({ strapi }); - expect(() => getFieldSize()).toThrowError('The type is required'); + + try { + getFieldSize(); + } catch (error) { + expect(error instanceof ApplicationError).toBe(true); + expect(error.message).toBe('The type is required'); + } }); it('should set the custom fields input sizes', () => { diff --git a/packages/core/content-manager/server/services/field-sizes.js b/packages/core/content-manager/server/services/field-sizes.js index 2d4487d812..6f87781f45 100644 --- a/packages/core/content-manager/server/services/field-sizes.js +++ b/packages/core/content-manager/server/services/field-sizes.js @@ -1,5 +1,7 @@ 'use strict'; +const { ApplicationError } = require('@strapi/utils').errors; + const needsFullSize = { default: 12, isResizable: false, @@ -52,12 +54,12 @@ const createFieldSizesService = ({ strapi }) => { getFieldSize(type) { if (!type) { - throw new Error('The type is required'); + throw new ApplicationError('The type is required'); } const fieldSize = fieldSizes[type]; if (!fieldSize) { - throw new Error(`Could not find field size for type ${type}`); + throw new ApplicationError(`Could not find field size for type ${type}`); } return fieldSize; @@ -65,11 +67,11 @@ const createFieldSizesService = ({ strapi }) => { setFieldSize(type, size) { if (!type) { - throw new Error('The type is required'); + throw new ApplicationError('The type is required'); } if (!size) { - throw new Error('The size is required'); + throw new ApplicationError('The size is required'); } fieldSizes[type] = size; From 82db28a4d3417f0f0af979e1171d49d98c2c4e48 Mon Sep 17 00:00:00 2001 From: Mark Kaylor Date: Fri, 21 Apr 2023 10:37:20 +0200 Subject: [PATCH 31/39] Update ds yarn link docs --- .../core/admin/link-strapi-design-system.md | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/docs/docs/core/admin/link-strapi-design-system.md b/docs/docs/core/admin/link-strapi-design-system.md index f08c5f9519..0d9e0bed93 100644 --- a/docs/docs/core/admin/link-strapi-design-system.md +++ b/docs/docs/core/admin/link-strapi-design-system.md @@ -2,24 +2,18 @@ Follow these steps to use a local version of the Strapi design system with the Strapi monorepo -First, run `yarn build` in `strapi-design-system/packages/strapi-design-system` to generate the bundle. +In your copy of the design system run `yarn build` to generate the bundle. -In your copy of Strapi, you can link the design system using either a [relative path](#relative-path) or [yarn link](#yarn-link). - -### Relative path - -Replace the version number in both `strapi/packages/core/admin/package.json` and `strapi/packages/core/helper-plugin/package.json` with the relative path to your copy of the design system: +In the Strapi monorepo link your local copy of the design system with [`yarn link`](https://yarnpkg.com/cli/link#gatsby-focus-wrapper): ``` -"@strapi/design-system": "link:../../../../strapi-design-system/packages/strapi-design-system" +yarn link -r ../ ``` -### Yarn link +Running yarn build in `examples/getstarted` should now use your local version of the design system. -Alternatively, you can use [`yarn link`](https://classic.yarnpkg.com/lang/en/docs/cli/link/) by first running `yarn link` in `strapi-design-system/packages/design-system` and then `yarn link @strapi/design-system` in both `strapi/packages/core/admin` and `strapi/packages/core/helper-plugin`. With this approach, no changes need to be made to the `package.json` - -Once the link is setup, run the following command from the root of the monorepo +To revert back to the released version of the design system use [`yarn unlink`](https://yarnpkg.com/cli/unlink#usage): ``` -yarn clean && yarn setup +yarn unlink ../ ``` From f9564e0b9486fa7b926548c6bcd2678b4ea49c8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20de=20Juvigny?= Date: Fri, 21 Apr 2023 10:59:29 +0200 Subject: [PATCH 32/39] Remove unused action function --- .../content-manager/pages/EditSettingsView/index.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/index.js b/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/index.js index d52c64a4fd..77c3af9a47 100644 --- a/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/index.js +++ b/packages/core/admin/admin/src/content-manager/pages/EditSettingsView/index.js @@ -9,13 +9,7 @@ import flatMap from 'lodash/flatMap'; import isEqual from 'lodash/isEqual'; import get from 'lodash/get'; import set from 'lodash/set'; -import { - useNotification, - useTracking, - useCustomFields, - ConfirmDialog, - Link, -} from '@strapi/helper-plugin'; +import { useNotification, useTracking, ConfirmDialog, Link } from '@strapi/helper-plugin'; import { useHistory } from 'react-router-dom'; import { Main, @@ -58,7 +52,6 @@ const EditSettingsView = ({ mainLayout, components, isContentTypeView, slug, upd const modelName = get(mainLayout, ['info', 'displayName'], ''); const attributes = get(modifiedData, ['attributes'], {}); const fieldSizes = useSelector(selectFieldSizes); - const customFieldsRegistry = useCustomFields(); const entryTitleOptions = Object.keys(attributes).filter((attr) => { const type = get(attributes, [attr, 'type'], ''); @@ -329,7 +322,6 @@ const EditSettingsView = ({ mainLayout, components, isContentTypeView, slug, upd type: 'ON_ADD_FIELD', name: field, fieldSizes, - getCustomField: customFieldsRegistry.get, }); }} onRemoveField={(rowId, index) => { From 98e3a3172172eb6a6d545f36a0a9a6f869d82e34 Mon Sep 17 00:00:00 2001 From: Marc-Roig Date: Fri, 21 Apr 2023 12:25:39 +0200 Subject: [PATCH 33/39] fix: check for base url --- packages/providers/upload-aws-s3/src/index.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/providers/upload-aws-s3/src/index.ts b/packages/providers/upload-aws-s3/src/index.ts index 5cfdd3ecda..5b886b06ea 100644 --- a/packages/providers/upload-aws-s3/src/index.ts +++ b/packages/providers/upload-aws-s3/src/index.ts @@ -27,7 +27,7 @@ interface File { // eslint-disable-next-line @typescript-eslint/no-var-requires require('aws-sdk/lib/maintenance_mode_message').suppress = true; -function assertUrlProtocol(url: string) { +function hasUrlProtocol(url: string) { // Regex to test protocol like "http://", "https://" return /^\w*:\/\//.test(url); } @@ -93,11 +93,13 @@ export = { } // set the bucket file url - if (assertUrlProtocol(data.Location)) { - file.url = baseUrl ? `${baseUrl}/${fileKey}` : data.Location; + if (baseUrl) { + // Construct the url with the baseUrl + file.url = `${baseUrl}/${fileKey}`; } else { - // Default protocol to https protocol - file.url = `https://${data.Location}`; + // Add the protocol if it is missing + // Some providers like DigitalOcean Spaces return the url without the protocol + file.url = hasUrlProtocol(data.Location) ? data.Location : `https://${data.Location}`; } resolve(); }; From 0f29e9367ac2533572d989da7e13bc5f65ab00a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20de=20Juvigny?= Date: Fri, 21 Apr 2023 12:34:56 +0200 Subject: [PATCH 34/39] Check for arrays when validating inputSize --- packages/core/strapi/lib/core/registries/custom-fields.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/strapi/lib/core/registries/custom-fields.js b/packages/core/strapi/lib/core/registries/custom-fields.js index 597b6ddb1b..18207fab1f 100644 --- a/packages/core/strapi/lib/core/registries/custom-fields.js +++ b/packages/core/strapi/lib/core/registries/custom-fields.js @@ -60,6 +60,7 @@ const customFieldsRegistry = (strapi) => { if (inputSize) { if ( !isObject(inputSize) || + Array.isArray(inputSize) || !has('default', inputSize) || !has('isResizable', inputSize) ) { From 4465cb1ef8b85c29ab30e885b9f8c9efd7baae5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20de=20Juvigny?= Date: Fri, 21 Apr 2023 12:38:17 +0200 Subject: [PATCH 35/39] Use isPlainObject from lodash instead --- packages/core/strapi/lib/core/registries/custom-fields.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/core/strapi/lib/core/registries/custom-fields.js b/packages/core/strapi/lib/core/registries/custom-fields.js index 18207fab1f..a911819e75 100644 --- a/packages/core/strapi/lib/core/registries/custom-fields.js +++ b/packages/core/strapi/lib/core/registries/custom-fields.js @@ -1,6 +1,6 @@ 'use strict'; -const { has, isObject } = require('lodash/fp'); +const { has, isPlainObject } = require('lodash/fp'); const ALLOWED_TYPES = [ 'biginteger', @@ -59,8 +59,7 @@ const customFieldsRegistry = (strapi) => { // Validate inputSize when provided if (inputSize) { if ( - !isObject(inputSize) || - Array.isArray(inputSize) || + !isPlainObject(inputSize) || !has('default', inputSize) || !has('isResizable', inputSize) ) { From a203df388d6d9fe9a4e2d5f0ee447e2ee34dd338 Mon Sep 17 00:00:00 2001 From: Convly Date: Fri, 21 Apr 2023 17:23:14 +0200 Subject: [PATCH 36/39] Update the contribution guidelines based on the yarn 3 requirements --- CONTRIBUTING.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 769a89eba1..6450e8561b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -52,6 +52,7 @@ The Strapi core team will review your pull request and either merge it, request **Before submitting your pull request** make sure the following requirements are fulfilled: - Fork the repository and create your new branch from `main`. +- Run `yarn install` in the root of the repository. - Run `yarn setup` in the root of the repository. - If you've fixed a bug or added code that should be tested, please make sure to add tests - Ensure the following test suites are passing: @@ -78,6 +79,7 @@ Go to the root of the repository and run the setup: ```bash cd strapi +# NOTE: If you don't already have node_modules, you will need to run `yarn install` first. yarn setup ``` @@ -183,4 +185,4 @@ Before submitting an issue you need to make sure: - Make sure your application has a clean `node_modules` directory, meaning: - you didn't link any dependencies (e.g., by running `yarn link`) - you haven't made any inline changes to files in the `node_modules` directory - - you don't have any global dependency loops. If you aren't sure, the easiest way to double-check any of the above is to run: `$ rm -rf node_modules && yarn cache clean && yarn setup`. + - you don't have any global dependency loops. If you aren't sure, the easiest way to double-check any of the above is to run: `$ rm -rf node_modules && yarn cache clean && yarn install && yarn setup`. From 975a3ca03d4a65903caa9d082a70f568d55a63bc Mon Sep 17 00:00:00 2001 From: Michael Olund Date: Fri, 21 Apr 2023 12:11:16 -0700 Subject: [PATCH 37/39] Fix knex error when removing existing relations as a batch Added a fallback of 0 after null coalesce --- packages/core/database/lib/entity-manager/regular-relations.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/database/lib/entity-manager/regular-relations.js b/packages/core/database/lib/entity-manager/regular-relations.js index 33f780330d..1f90714b1e 100644 --- a/packages/core/database/lib/entity-manager/regular-relations.js +++ b/packages/core/database/lib/entity-manager/regular-relations.js @@ -152,7 +152,7 @@ const deleteRelations = async ({ .transacting(trx) .execute(); done = batchToDelete.length < batchSize; - lastId = batchToDelete[batchToDelete.length - 1]?.id; + lastId = batchToDelete[batchToDelete.length - 1]?.id || 0; const batchIds = map(inverseJoinColumn.name, batchToDelete); From 86853d812053a995510798c2196ba73a80f8754e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-S=C3=A9bastien=20Herbaux?= Date: Mon, 24 Apr 2023 09:11:27 +0200 Subject: [PATCH 38/39] Replace note with command --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6450e8561b..5381a13472 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -79,7 +79,7 @@ Go to the root of the repository and run the setup: ```bash cd strapi -# NOTE: If you don't already have node_modules, you will need to run `yarn install` first. +yarn install yarn setup ``` From 7f1bb7e73f5be951a5d0c43ed64edf516d18b5a7 Mon Sep 17 00:00:00 2001 From: DMehaffy Date: Mon, 24 Apr 2023 03:33:20 -0700 Subject: [PATCH 39/39] Allow configuration of SSO Cookie Domain (#16471) --- .../admin/ee/server/controllers/authentication/middlewares.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/admin/ee/server/controllers/authentication/middlewares.js b/packages/core/admin/ee/server/controllers/authentication/middlewares.js index deb669844b..1d2e8a7a03 100644 --- a/packages/core/admin/ee/server/controllers/authentication/middlewares.js +++ b/packages/core/admin/ee/server/controllers/authentication/middlewares.js @@ -95,13 +95,14 @@ const redirectWithAuth = (ctx) => { params: { provider }, } = ctx; const redirectUrls = utils.getPrefixedRedirectUrls(); + const domain = strapi.config.get('server.admin.auth.domain'); const { user } = ctx.state; const jwt = getService('token').createJwtToken(user); const isProduction = strapi.config.get('environment') === 'production'; - const cookiesOptions = { httpOnly: false, secure: isProduction, overwrite: true }; + const cookiesOptions = { httpOnly: false, secure: isProduction, overwrite: true, domain }; const sanitizedUser = getService('user').sanitizeUser(user); strapi.eventHub.emit('admin.auth.success', { user: sanitizedUser, provider });