From 39f113715a3fd5e0e28e9a9eec3b1d9b57983283 Mon Sep 17 00:00:00 2001 From: Vladislav Shkodin Date: Wed, 19 May 2021 14:39:35 +0200 Subject: [PATCH] chore: update prettier (#5412) --- .gitattributes | 1 + .prettierrc | 1 + package.json | 2 +- .../src/implementation.ts | 6 +- .../netlify-cms-backend-github/src/API.ts | 12 +- .../src/AuthenticationPage.js | 6 +- .../src/GraphQLAPI.ts | 4 +- .../src/implementation.tsx | 6 +- .../netlify-cms-backend-gitlab/src/API.ts | 6 +- .../src/__tests__/gitlab.spec.js | 10 +- .../src/implementation.ts | 2 +- .../src/implementation.ts | 4 +- packages/netlify-cms-core/index.d.ts | 9 +- .../src/actions/__tests__/entries.spec.js | 1148 ++++++++--------- .../src/actions/__tests__/media.spec.ts | 5 +- .../actions/__tests__/mediaLibrary.spec.js | 652 +++++----- .../netlify-cms-core/src/actions/config.ts | 8 +- .../src/actions/editorialWorkflow.ts | 2 +- packages/netlify-cms-core/src/backend.ts | 2 +- .../src/components/App/App.js | 2 +- .../src/components/Editor/Editor.js | 23 +- packages/netlify-cms-core/src/formats/yaml.ts | 8 +- .../src/integrations/index.js | 2 +- .../netlify-cms-core/src/lib/formatters.ts | 6 +- .../netlify-cms-core/src/lib/urlHelper.ts | 14 +- packages/netlify-cms-core/src/mediaLibrary.ts | 2 +- .../reducers/__tests__/collections.spec.js | 21 +- .../src/reducers/collections.ts | 27 +- .../netlify-cms-core/src/reducers/entries.ts | 15 +- .../src/reducers/mediaLibrary.ts | 2 +- packages/netlify-cms-core/src/redux/index.ts | 2 +- .../src/redux/middleware/waitUntilAction.ts | 19 +- .../src/routing/__tests__/history.spec.ts | 2 +- .../netlify-cms-core/src/types/immutable.ts | 2 +- .../netlify-cms-lib-auth/src/pkce-oauth.js | 5 +- .../src/__tests__/backendUtil.spec.js | 192 +-- .../src/__tests__/unsentRequest.spec.js | 9 +- .../netlify-cms-lib-util/src/loadScript.js | 2 +- .../netlify-cms-lib-util/src/unsentRequest.js | 8 +- .../src/middlewares/joi/index.spec.ts | 2 +- .../src/middlewares/joi/index.ts | 9 +- .../src/middlewares/localFs/index.ts | 2 +- .../src/middlewares/localGit/index.spec.ts | 2 +- .../src/middlewares/localGit/index.ts | 10 +- .../src/middlewares/utils/entries.ts | 5 +- .../src/BooleanControl.js | 10 +- .../src/ColorControl.js | 11 +- .../src/DateTimeControl.js | 6 +- .../MarkdownControl/__tests__/slate.spec.js | 5 +- .../MarkdownControl/plugins/ForceInsert.js | 10 +- .../src/MarkdownControl/plugins/LineBreak.js | 5 +- .../serializers/__tests__/commonmark.spec.js | 21 +- .../src/serializers/__tests__/index.spec.js | 3 +- .../__tests__/remarkAllowHtmlEntities.spec.js | 5 +- .../remarkEscapeMarkdownEntities.spec.js | 4 +- .../__tests__/remarkPaddedLinks.spec.js | 5 +- .../remarkStripTrailingBreaks.spec.js | 4 +- .../src/serializers/index.js | 8 +- .../src/serializers/slateRemark.js | 7 +- .../src/RelationControl.js | 11 +- .../src/__tests__/relation.spec.js | 958 +++++++------- .../src/TextControl.js | 10 +- website/gatsby-config.js | 2 +- website/src/templates/blog-post.js | 11 +- website/src/templates/doc-page.js | 5 +- yarn.lock | 8 +- 66 files changed, 1625 insertions(+), 1763 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..6313b56c --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.prettierrc b/.prettierrc index 9efa52f4..30e01f34 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,4 +1,5 @@ { + "arrowParens": "avoid", "trailingComma": "all", "singleQuote": true, "printWidth": 100 diff --git a/package.json b/package.json index c8ab32fd..de2e3e22 100644 --- a/package.json +++ b/package.json @@ -148,7 +148,7 @@ "nock": "^13.0.0", "node-fetch": "^2.3.0", "npm-run-all": "^4.1.5", - "prettier": "^1.19.1", + "prettier": "^2.3.0", "react": "^16.12.0", "react-dom": "^16.12.0", "react-test-renderer": "^16.8.4", diff --git a/packages/netlify-cms-backend-bitbucket/src/implementation.ts b/packages/netlify-cms-backend-bitbucket/src/implementation.ts index 1f4a0bd7..aa6667ca 100644 --- a/packages/netlify-cms-backend-bitbucket/src/implementation.ts +++ b/packages/netlify-cms-backend-bitbucket/src/implementation.ts @@ -277,9 +277,9 @@ export default class BitbucketBackend implements Implementation { } apiRequestFunction = async (req: ApiRequest) => { - const token = (this.refreshedTokenPromise - ? await this.refreshedTokenPromise - : this.token) as string; + const token = ( + this.refreshedTokenPromise ? await this.refreshedTokenPromise : this.token + ) as string; const authorizedRequest = unsentRequest.withHeaders({ Authorization: `Bearer ${token}` }, req); const response: Response = await unsentRequest.performRequest(authorizedRequest); diff --git a/packages/netlify-cms-backend-github/src/API.ts b/packages/netlify-cms-backend-github/src/API.ts index f23dde14..27e238f4 100644 --- a/packages/netlify-cms-backend-github/src/API.ts +++ b/packages/netlify-cms-backend-github/src/API.ts @@ -310,10 +310,10 @@ export default class API { let responseStatus = 500; try { - const req = (unsentRequest.fromFetchArguments(url, { + const req = unsentRequest.fromFetchArguments(url, { ...options, headers, - }) as unknown) as ApiRequest; + }) as unknown as ApiRequest; const response = await requestWithBackoff(this, req); responseStatus = response.status; const parsedResponse = await parser(response); @@ -366,8 +366,7 @@ export default class API { .catch(() => { // Meta ref doesn't exist const readme = { - raw: - '# Netlify CMS\n\nThis tree is used by the Netlify CMS to store metadata information for specific files and branches.', + raw: '# Netlify CMS\n\nThis tree is used by the Netlify CMS to store metadata information for specific files and branches.', }; return this.uploadBlob(readme) @@ -808,7 +807,8 @@ export default class API { let branches: string[]; if (this.useOpenAuthoring) { // open authoring branches can exist without a pr - const cmsBranches: Octokit.GitListMatchingRefsResponse = await this.getOpenAuthoringBranches(); + const cmsBranches: Octokit.GitListMatchingRefsResponse = + await this.getOpenAuthoringBranches(); branches = cmsBranches.map(b => b.ref.substring('refs/heads/'.length)); // filter irrelevant branches const branchesWithFilter = await Promise.all( @@ -1036,7 +1036,7 @@ export default class API { author, committer, ); - return (newCommit as unknown) as GitHubCompareCommit; + return newCommit as unknown as GitHubCompareCommit; } else { return commit; } diff --git a/packages/netlify-cms-backend-github/src/AuthenticationPage.js b/packages/netlify-cms-backend-github/src/AuthenticationPage.js index f633e1d4..fba42b85 100644 --- a/packages/netlify-cms-backend-github/src/AuthenticationPage.js +++ b/packages/netlify-cms-backend-github/src/AuthenticationPage.js @@ -75,10 +75,8 @@ export default class GitHubAuthenticationPage extends React.Component { }; const auth = new NetlifyAuthenticator(cfg); - const { - open_authoring: openAuthoring = false, - auth_scope: authScope = '', - } = this.props.config.backend; + const { open_authoring: openAuthoring = false, auth_scope: authScope = '' } = + this.props.config.backend; const scope = authScope || (openAuthoring ? 'public_repo' : 'repo'); auth.authenticate({ provider: 'github', scope }, (err, data) => { diff --git a/packages/netlify-cms-backend-github/src/GraphQLAPI.ts b/packages/netlify-cms-backend-github/src/GraphQLAPI.ts index 42be456f..d429b5b6 100644 --- a/packages/netlify-cms-backend-github/src/GraphQLAPI.ts +++ b/packages/netlify-cms-backend-github/src/GraphQLAPI.ts @@ -300,7 +300,7 @@ export default class GraphQLAPI extends API { const mapped = pullRequests.nodes.map(transformPullRequest); - return ((mapped as unknown) as Octokit.PullsListResponseItem[]).filter( + return (mapped as unknown as Octokit.PullsListResponseItem[]).filter( pr => pr.head.ref.startsWith(`${CMS_BRANCH_PREFIX}/`) && predicate(pr), ); } @@ -673,7 +673,7 @@ export default class GraphQLAPI extends API { }, }); const { pullRequest } = data!.createPullRequest; - return (transformPullRequest(pullRequest) as unknown) as Octokit.PullsCreateResponse; + return transformPullRequest(pullRequest) as unknown as Octokit.PullsCreateResponse; } async getFileSha(path: string, { repoURL = this.repoURL, branch = this.branch } = {}) { diff --git a/packages/netlify-cms-backend-github/src/implementation.tsx b/packages/netlify-cms-backend-github/src/implementation.tsx index a154a57e..177a5837 100644 --- a/packages/netlify-cms-backend-github/src/implementation.tsx +++ b/packages/netlify-cms-backend-github/src/implementation.tsx @@ -537,9 +537,9 @@ export default class GitHub implements Implementation { } const readFile = (path: string, id: string | null | undefined) => - this.api!.readFile(path, id, { repoURL: this.api!.originRepoURL }).catch(() => '') as Promise< - string - >; + this.api!.readFile(path, id, { repoURL: this.api!.originRepoURL }).catch( + () => '', + ) as Promise; const entries = await entriesByFiles( result.files, diff --git a/packages/netlify-cms-backend-gitlab/src/API.ts b/packages/netlify-cms-backend-gitlab/src/API.ts index 399cea11..712169b5 100644 --- a/packages/netlify-cms-backend-gitlab/src/API.ts +++ b/packages/netlify-cms-backend-gitlab/src/API.ts @@ -245,10 +245,8 @@ export default class API { MAINTAINER_ACCESS = 40; hasWriteAccess = async () => { - const { - shared_with_groups: sharedWithGroups, - permissions, - }: GitLabRepo = await this.requestJSON(this.repoURL); + const { shared_with_groups: sharedWithGroups, permissions }: GitLabRepo = + await this.requestJSON(this.repoURL); const { project_access: projectAccess, group_access: groupAccess } = permissions; if (projectAccess && projectAccess.access_level >= this.WRITE_ACCESS) { diff --git a/packages/netlify-cms-backend-gitlab/src/__tests__/gitlab.spec.js b/packages/netlify-cms-backend-gitlab/src/__tests__/gitlab.spec.js index dde73482..597d1c86 100644 --- a/packages/netlify-cms-backend-gitlab/src/__tests__/gitlab.spec.js +++ b/packages/netlify-cms-backend-gitlab/src/__tests__/gitlab.spec.js @@ -287,10 +287,7 @@ describe('gitlab backend', () => { function interceptFiles(backend, path) { const api = mockApi(backend); const url = `${expectedRepoUrl}/repository/files/${encodeURIComponent(path)}/raw`; - api - .get(url) - .query(true) - .reply(200, mockRepo.files[path]); + api.get(url).query(true).reply(200, mockRepo.files[path]); api .get(`${expectedRepoUrl}/repository/commits`) @@ -391,10 +388,7 @@ describe('gitlab backend', () => { it('returns an entry from folder collection', async () => { const entryTree = mockRepo.tree[collectionContentConfig.folder][0]; - const slug = entryTree.path - .split('/') - .pop() - .replace('.md', ''); + const slug = entryTree.path.split('/').pop().replace('.md', ''); interceptFiles(backend, entryTree.path); interceptCollection(backend, collectionContentConfig); diff --git a/packages/netlify-cms-backend-proxy/src/implementation.ts b/packages/netlify-cms-backend-proxy/src/implementation.ts index 57114387..a848f5fb 100644 --- a/packages/netlify-cms-backend-proxy/src/implementation.ts +++ b/packages/netlify-cms-backend-proxy/src/implementation.ts @@ -80,7 +80,7 @@ export default class ProxyBackend implements Implementation { } authenticate() { - return (Promise.resolve() as unknown) as Promise; + return Promise.resolve() as unknown as Promise; } logout() { diff --git a/packages/netlify-cms-backend-test/src/implementation.ts b/packages/netlify-cms-backend-test/src/implementation.ts index 371ba267..704f161a 100644 --- a/packages/netlify-cms-backend-test/src/implementation.ts +++ b/packages/netlify-cms-backend-test/src/implementation.ts @@ -54,7 +54,7 @@ function getFile(path: string, tree: RepoTree) { while (obj && segments.length) { obj = obj[segments.shift() as string] as RepoTree; } - return ((obj as unknown) as RepoFile) || {}; + return (obj as unknown as RepoFile) || {}; } function writeFile(path: string, content: string | AssetProxy, tree: RepoTree) { @@ -146,7 +146,7 @@ export default class TestBackend implements Implementation { } authenticate() { - return (Promise.resolve() as unknown) as Promise; + return Promise.resolve() as unknown as Promise; } logout() { diff --git a/packages/netlify-cms-core/index.d.ts b/packages/netlify-cms-core/index.d.ts index 356755f2..cc7d67f9 100644 --- a/packages/netlify-cms-core/index.d.ts +++ b/packages/netlify-cms-core/index.d.ts @@ -516,9 +516,12 @@ declare module 'netlify-cms-core' { }; } - type GetAssetFunction = ( - asset: string, - ) => { url: string; path: string; field?: any; fileObj: File }; + type GetAssetFunction = (asset: string) => { + url: string; + path: string; + field?: any; + fileObj: File; + }; export type PreviewTemplateComponentProps = { entry: Map; diff --git a/packages/netlify-cms-core/src/actions/__tests__/entries.spec.js b/packages/netlify-cms-core/src/actions/__tests__/entries.spec.js index c74ab054..9ba9f8ef 100644 --- a/packages/netlify-cms-core/src/actions/__tests__/entries.spec.js +++ b/packages/netlify-cms-core/src/actions/__tests__/entries.spec.js @@ -1,574 +1,574 @@ -import { fromJS, Map } from 'immutable'; -import { - createEmptyDraft, - createEmptyDraftData, - retrieveLocalBackup, - persistLocalBackup, - getMediaAssets, - validateMetaField, -} from '../entries'; -import configureMockStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; -import AssetProxy from '../../valueObjects/AssetProxy'; - -jest.mock('coreSrc/backend'); -jest.mock('netlify-cms-lib-util'); -jest.mock('../mediaLibrary'); -jest.mock('../../reducers/entries'); -jest.mock('../../reducers/entryDraft'); - -const middlewares = [thunk]; -const mockStore = configureMockStore(middlewares); - -describe('entries', () => { - describe('createEmptyDraft', () => { - const { currentBackend } = require('coreSrc/backend'); - const backend = { - processEntry: jest.fn((_state, _collection, entry) => Promise.resolve(entry)), - }; - - currentBackend.mockReturnValue(backend); - - beforeEach(() => { - jest.clearAllMocks(); - }); - it('should dispatch draft created action', () => { - const store = mockStore({ mediaLibrary: fromJS({ files: [] }) }); - - const collection = fromJS({ - fields: [{ name: 'title' }], - }); - - return store.dispatch(createEmptyDraft(collection, '')).then(() => { - const actions = store.getActions(); - expect(actions).toHaveLength(1); - - expect(actions[0]).toEqual({ - payload: { - author: '', - collection: undefined, - data: {}, - meta: {}, - i18n: {}, - isModification: null, - label: null, - mediaFiles: [], - partial: false, - path: '', - raw: '', - slug: '', - status: '', - updatedOn: '', - }, - type: 'DRAFT_CREATE_EMPTY', - }); - }); - }); - - it('should populate draft entry from URL param', () => { - const store = mockStore({ mediaLibrary: fromJS({ files: [] }) }); - - const collection = fromJS({ - fields: [{ name: 'title' }, { name: 'boolean' }], - }); - - return store.dispatch(createEmptyDraft(collection, '?title=title&boolean=True')).then(() => { - const actions = store.getActions(); - expect(actions).toHaveLength(1); - - expect(actions[0]).toEqual({ - payload: { - author: '', - collection: undefined, - data: { title: 'title', boolean: true }, - meta: {}, - i18n: {}, - isModification: null, - label: null, - mediaFiles: [], - partial: false, - path: '', - raw: '', - slug: '', - status: '', - updatedOn: '', - }, - type: 'DRAFT_CREATE_EMPTY', - }); - }); - }); - - it('should html escape URL params', () => { - const store = mockStore({ mediaLibrary: fromJS({ files: [] }) }); - - const collection = fromJS({ - fields: [{ name: 'title' }], - }); - - return store - .dispatch(createEmptyDraft(collection, "?title=")) - .then(() => { - const actions = store.getActions(); - expect(actions).toHaveLength(1); - - expect(actions[0]).toEqual({ - payload: { - author: '', - collection: undefined, - data: { title: '<script>alert('hello')</script>' }, - meta: {}, - i18n: {}, - isModification: null, - label: null, - mediaFiles: [], - partial: false, - path: '', - raw: '', - slug: '', - status: '', - updatedOn: '', - }, - type: 'DRAFT_CREATE_EMPTY', - }); - }); - }); - }); - describe('createEmptyDraftData', () => { - it('should allow an empty array as list default for a single field list', () => { - const fields = fromJS([ - { - name: 'images', - widget: 'list', - default: [], - field: { name: 'url', widget: 'text' }, - }, - ]); - expect(createEmptyDraftData(fields)).toEqual({ images: fromJS([]) }); - }); - - it('should allow a complex array as list default for a single field list', () => { - const fields = fromJS([ - { - name: 'images', - widget: 'list', - default: [ - { - url: 'https://image.png', - }, - ], - field: { name: 'url', widget: 'text' }, - }, - ]); - expect(createEmptyDraftData(fields)).toEqual({ - images: fromJS([ - { - url: 'https://image.png', - }, - ]), - }); - }); - - it('should allow an empty array as list default for a fields list', () => { - const fields = fromJS([ - { - name: 'images', - widget: 'list', - default: [], - fields: [ - { name: 'title', widget: 'text' }, - { name: 'url', widget: 'text' }, - ], - }, - ]); - expect(createEmptyDraftData(fields)).toEqual({ images: fromJS([]) }); - }); - - it('should allow a complex array as list default for a fields list', () => { - const fields = fromJS([ - { - name: 'images', - widget: 'list', - default: [ - { - title: 'default image', - url: 'https://image.png', - }, - ], - fields: [ - { name: 'title', widget: 'text' }, - { name: 'url', widget: 'text' }, - ], - }, - ]); - expect(createEmptyDraftData(fields)).toEqual({ - images: fromJS([ - { - title: 'default image', - url: 'https://image.png', - }, - ]), - }); - }); - - it('should use field default when no list default is provided', () => { - const fields = fromJS([ - { - name: 'images', - widget: 'list', - field: { name: 'url', widget: 'text', default: 'https://image.png' }, - }, - ]); - expect(createEmptyDraftData(fields)).toEqual({ images: [{ url: 'https://image.png' }] }); - }); - - it('should use fields default when no list default is provided', () => { - const fields = fromJS([ - { - name: 'images', - widget: 'list', - fields: [ - { name: 'title', widget: 'text', default: 'default image' }, - { name: 'url', widget: 'text', default: 'https://image.png' }, - ], - }, - ]); - expect(createEmptyDraftData(fields)).toEqual({ - images: [{ title: 'default image', url: 'https://image.png' }], - }); - }); - - it('should not set empty value for list fields widget', () => { - const fields = fromJS([ - { - name: 'images', - widget: 'list', - fields: [ - { name: 'title', widget: 'text' }, - { name: 'url', widget: 'text' }, - ], - }, - ]); - expect(createEmptyDraftData(fields)).toEqual({}); - }); - - it('should set default value for object field widget', () => { - const fields = fromJS([ - { - name: 'post', - widget: 'object', - field: { name: 'image', widget: 'text', default: 'https://image.png' }, - }, - ]); - expect(createEmptyDraftData(fields)).toEqual({ post: { image: 'https://image.png' } }); - }); - - it('should set default values for object fields widget', () => { - const fields = fromJS([ - { - name: 'post', - widget: 'object', - fields: [ - { name: 'title', widget: 'text', default: 'default title' }, - { name: 'url', widget: 'text', default: 'https://image.png' }, - ], - }, - ]); - expect(createEmptyDraftData(fields)).toEqual({ - post: { title: 'default title', url: 'https://image.png' }, - }); - }); - - it('should not set empty value for object fields widget', () => { - const fields = fromJS([ - { - name: 'post', - widget: 'object', - fields: [ - { name: 'title', widget: 'text' }, - { name: 'url', widget: 'text' }, - ], - }, - ]); - expect(createEmptyDraftData(fields)).toEqual({}); - }); - - it('should populate nested fields', () => { - const fields = fromJS([ - { - name: 'names', - widget: 'list', - field: { - name: 'object', - widget: 'object', - fields: [ - { name: 'first', widget: 'string', default: 'first' }, - { name: 'second', widget: 'string', default: 'second' }, - ], - }, - }, - ]); - expect(createEmptyDraftData(fields)).toEqual({ - names: [{ object: { first: 'first', second: 'second' } }], - }); - }); - }); - - describe('persistLocalBackup', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('should persist local backup with media files', () => { - const { currentBackend } = require('coreSrc/backend'); - - const backend = { - persistLocalDraftBackup: jest.fn(() => Promise.resolve()), - }; - - const store = mockStore({ - config: Map(), - }); - - currentBackend.mockReturnValue(backend); - - const collection = Map(); - const mediaFiles = [{ path: 'static/media/image.png' }]; - const entry = fromJS({ mediaFiles }); - - return store.dispatch(persistLocalBackup(entry, collection)).then(() => { - const actions = store.getActions(); - expect(actions).toHaveLength(0); - - expect(backend.persistLocalDraftBackup).toHaveBeenCalledTimes(1); - expect(backend.persistLocalDraftBackup).toHaveBeenCalledWith(entry, collection); - }); - }); - }); - - describe('retrieveLocalBackup', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('should retrieve media files with local backup', () => { - const { currentBackend } = require('coreSrc/backend'); - const { createAssetProxy } = require('../../valueObjects/AssetProxy'); - - const backend = { - getLocalDraftBackup: jest.fn((...args) => args), - }; - - const store = mockStore({ - config: Map(), - }); - - currentBackend.mockReturnValue(backend); - - const collection = Map({ - name: 'collection', - }); - const slug = 'slug'; - - const file = new File([], 'image.png'); - const mediaFiles = [{ path: 'static/media/image.png', url: 'url', file }]; - const asset = createAssetProxy(mediaFiles[0]); - const entry = { mediaFiles }; - - backend.getLocalDraftBackup.mockReturnValue({ entry }); - - return store.dispatch(retrieveLocalBackup(collection, slug)).then(() => { - const actions = store.getActions(); - - expect(actions).toHaveLength(2); - - expect(actions[0]).toEqual({ - type: 'ADD_ASSETS', - payload: [asset], - }); - expect(actions[1]).toEqual({ - type: 'DRAFT_LOCAL_BACKUP_RETRIEVED', - payload: { entry }, - }); - }); - }); - }); - - describe('getMediaAssets', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('should map mediaFiles to assets', () => { - const mediaFiles = fromJS([{ path: 'path1' }, { path: 'path2', draft: true }]); - - const entry = Map({ mediaFiles }); - expect(getMediaAssets({ entry })).toEqual([new AssetProxy({ path: 'path2' })]); - }); - }); - - describe('validateMetaField', () => { - const state = { - config: { - slug: { - encoding: 'unicode', - clean_accents: false, - sanitize_replacement: '-', - }, - }, - entries: fromJS([]), - }; - const collection = fromJS({ - folder: 'folder', - type: 'folder_based_collection', - name: 'name', - }); - const t = jest.fn((key, args) => ({ key, args })); - - const { selectCustomPath } = require('../../reducers/entryDraft'); - const { selectEntryByPath } = require('../../reducers/entries'); - - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('should not return error on non meta field', () => { - expect(validateMetaField(null, null, fromJS({}), null, t)).toEqual({ error: false }); - }); - - it('should not return error on meta path field', () => { - expect( - validateMetaField(null, null, fromJS({ meta: true, name: 'other' }), null, t), - ).toEqual({ error: false }); - }); - - it('should return error on empty path', () => { - expect(validateMetaField(null, null, fromJS({ meta: true, name: 'path' }), null, t)).toEqual({ - error: { - message: { - key: 'editor.editorControlPane.widget.invalidPath', - args: { path: null }, - }, - type: 'CUSTOM', - }, - }); - - expect( - validateMetaField(null, null, fromJS({ meta: true, name: 'path' }), undefined, t), - ).toEqual({ - error: { - message: { - key: 'editor.editorControlPane.widget.invalidPath', - args: { path: undefined }, - }, - type: 'CUSTOM', - }, - }); - - expect(validateMetaField(null, null, fromJS({ meta: true, name: 'path' }), '', t)).toEqual({ - error: { - message: { - key: 'editor.editorControlPane.widget.invalidPath', - args: { path: '' }, - }, - type: 'CUSTOM', - }, - }); - }); - - it('should return error on invalid path', () => { - expect( - validateMetaField(state, null, fromJS({ meta: true, name: 'path' }), 'invalid path', t), - ).toEqual({ - error: { - message: { - key: 'editor.editorControlPane.widget.invalidPath', - args: { path: 'invalid path' }, - }, - type: 'CUSTOM', - }, - }); - }); - - it('should return error on existing path', () => { - selectCustomPath.mockReturnValue('existing-path'); - selectEntryByPath.mockReturnValue(fromJS({ path: 'existing-path' })); - expect( - validateMetaField( - { - ...state, - entryDraft: fromJS({ - entry: {}, - }), - }, - collection, - fromJS({ meta: true, name: 'path' }), - 'existing-path', - t, - ), - ).toEqual({ - error: { - message: { - key: 'editor.editorControlPane.widget.pathExists', - args: { path: 'existing-path' }, - }, - type: 'CUSTOM', - }, - }); - - expect(selectCustomPath).toHaveBeenCalledTimes(1); - expect(selectCustomPath).toHaveBeenCalledWith( - collection, - fromJS({ entry: { meta: { path: 'existing-path' } } }), - ); - - expect(selectEntryByPath).toHaveBeenCalledTimes(1); - expect(selectEntryByPath).toHaveBeenCalledWith( - state.entries, - collection.get('name'), - 'existing-path', - ); - }); - - it('should not return error on non existing path for new entry', () => { - selectCustomPath.mockReturnValue('non-existing-path'); - selectEntryByPath.mockReturnValue(undefined); - expect( - validateMetaField( - { - ...state, - entryDraft: fromJS({ - entry: {}, - }), - }, - collection, - fromJS({ meta: true, name: 'path' }), - 'non-existing-path', - t, - ), - ).toEqual({ - error: false, - }); - }); - - it('should not return error when for existing entry', () => { - selectCustomPath.mockReturnValue('existing-path'); - selectEntryByPath.mockReturnValue(fromJS({ path: 'existing-path' })); - expect( - validateMetaField( - { - ...state, - entryDraft: fromJS({ - entry: { path: 'existing-path' }, - }), - }, - collection, - fromJS({ meta: true, name: 'path' }), - 'existing-path', - t, - ), - ).toEqual({ - error: false, - }); - }); - }); -}); +import { fromJS, Map } from 'immutable'; +import { + createEmptyDraft, + createEmptyDraftData, + retrieveLocalBackup, + persistLocalBackup, + getMediaAssets, + validateMetaField, +} from '../entries'; +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import AssetProxy from '../../valueObjects/AssetProxy'; + +jest.mock('coreSrc/backend'); +jest.mock('netlify-cms-lib-util'); +jest.mock('../mediaLibrary'); +jest.mock('../../reducers/entries'); +jest.mock('../../reducers/entryDraft'); + +const middlewares = [thunk]; +const mockStore = configureMockStore(middlewares); + +describe('entries', () => { + describe('createEmptyDraft', () => { + const { currentBackend } = require('coreSrc/backend'); + const backend = { + processEntry: jest.fn((_state, _collection, entry) => Promise.resolve(entry)), + }; + + currentBackend.mockReturnValue(backend); + + beforeEach(() => { + jest.clearAllMocks(); + }); + it('should dispatch draft created action', () => { + const store = mockStore({ mediaLibrary: fromJS({ files: [] }) }); + + const collection = fromJS({ + fields: [{ name: 'title' }], + }); + + return store.dispatch(createEmptyDraft(collection, '')).then(() => { + const actions = store.getActions(); + expect(actions).toHaveLength(1); + + expect(actions[0]).toEqual({ + payload: { + author: '', + collection: undefined, + data: {}, + meta: {}, + i18n: {}, + isModification: null, + label: null, + mediaFiles: [], + partial: false, + path: '', + raw: '', + slug: '', + status: '', + updatedOn: '', + }, + type: 'DRAFT_CREATE_EMPTY', + }); + }); + }); + + it('should populate draft entry from URL param', () => { + const store = mockStore({ mediaLibrary: fromJS({ files: [] }) }); + + const collection = fromJS({ + fields: [{ name: 'title' }, { name: 'boolean' }], + }); + + return store.dispatch(createEmptyDraft(collection, '?title=title&boolean=True')).then(() => { + const actions = store.getActions(); + expect(actions).toHaveLength(1); + + expect(actions[0]).toEqual({ + payload: { + author: '', + collection: undefined, + data: { title: 'title', boolean: true }, + meta: {}, + i18n: {}, + isModification: null, + label: null, + mediaFiles: [], + partial: false, + path: '', + raw: '', + slug: '', + status: '', + updatedOn: '', + }, + type: 'DRAFT_CREATE_EMPTY', + }); + }); + }); + + it('should html escape URL params', () => { + const store = mockStore({ mediaLibrary: fromJS({ files: [] }) }); + + const collection = fromJS({ + fields: [{ name: 'title' }], + }); + + return store + .dispatch(createEmptyDraft(collection, "?title=")) + .then(() => { + const actions = store.getActions(); + expect(actions).toHaveLength(1); + + expect(actions[0]).toEqual({ + payload: { + author: '', + collection: undefined, + data: { title: '<script>alert('hello')</script>' }, + meta: {}, + i18n: {}, + isModification: null, + label: null, + mediaFiles: [], + partial: false, + path: '', + raw: '', + slug: '', + status: '', + updatedOn: '', + }, + type: 'DRAFT_CREATE_EMPTY', + }); + }); + }); + }); + describe('createEmptyDraftData', () => { + it('should allow an empty array as list default for a single field list', () => { + const fields = fromJS([ + { + name: 'images', + widget: 'list', + default: [], + field: { name: 'url', widget: 'text' }, + }, + ]); + expect(createEmptyDraftData(fields)).toEqual({ images: fromJS([]) }); + }); + + it('should allow a complex array as list default for a single field list', () => { + const fields = fromJS([ + { + name: 'images', + widget: 'list', + default: [ + { + url: 'https://image.png', + }, + ], + field: { name: 'url', widget: 'text' }, + }, + ]); + expect(createEmptyDraftData(fields)).toEqual({ + images: fromJS([ + { + url: 'https://image.png', + }, + ]), + }); + }); + + it('should allow an empty array as list default for a fields list', () => { + const fields = fromJS([ + { + name: 'images', + widget: 'list', + default: [], + fields: [ + { name: 'title', widget: 'text' }, + { name: 'url', widget: 'text' }, + ], + }, + ]); + expect(createEmptyDraftData(fields)).toEqual({ images: fromJS([]) }); + }); + + it('should allow a complex array as list default for a fields list', () => { + const fields = fromJS([ + { + name: 'images', + widget: 'list', + default: [ + { + title: 'default image', + url: 'https://image.png', + }, + ], + fields: [ + { name: 'title', widget: 'text' }, + { name: 'url', widget: 'text' }, + ], + }, + ]); + expect(createEmptyDraftData(fields)).toEqual({ + images: fromJS([ + { + title: 'default image', + url: 'https://image.png', + }, + ]), + }); + }); + + it('should use field default when no list default is provided', () => { + const fields = fromJS([ + { + name: 'images', + widget: 'list', + field: { name: 'url', widget: 'text', default: 'https://image.png' }, + }, + ]); + expect(createEmptyDraftData(fields)).toEqual({ images: [{ url: 'https://image.png' }] }); + }); + + it('should use fields default when no list default is provided', () => { + const fields = fromJS([ + { + name: 'images', + widget: 'list', + fields: [ + { name: 'title', widget: 'text', default: 'default image' }, + { name: 'url', widget: 'text', default: 'https://image.png' }, + ], + }, + ]); + expect(createEmptyDraftData(fields)).toEqual({ + images: [{ title: 'default image', url: 'https://image.png' }], + }); + }); + + it('should not set empty value for list fields widget', () => { + const fields = fromJS([ + { + name: 'images', + widget: 'list', + fields: [ + { name: 'title', widget: 'text' }, + { name: 'url', widget: 'text' }, + ], + }, + ]); + expect(createEmptyDraftData(fields)).toEqual({}); + }); + + it('should set default value for object field widget', () => { + const fields = fromJS([ + { + name: 'post', + widget: 'object', + field: { name: 'image', widget: 'text', default: 'https://image.png' }, + }, + ]); + expect(createEmptyDraftData(fields)).toEqual({ post: { image: 'https://image.png' } }); + }); + + it('should set default values for object fields widget', () => { + const fields = fromJS([ + { + name: 'post', + widget: 'object', + fields: [ + { name: 'title', widget: 'text', default: 'default title' }, + { name: 'url', widget: 'text', default: 'https://image.png' }, + ], + }, + ]); + expect(createEmptyDraftData(fields)).toEqual({ + post: { title: 'default title', url: 'https://image.png' }, + }); + }); + + it('should not set empty value for object fields widget', () => { + const fields = fromJS([ + { + name: 'post', + widget: 'object', + fields: [ + { name: 'title', widget: 'text' }, + { name: 'url', widget: 'text' }, + ], + }, + ]); + expect(createEmptyDraftData(fields)).toEqual({}); + }); + + it('should populate nested fields', () => { + const fields = fromJS([ + { + name: 'names', + widget: 'list', + field: { + name: 'object', + widget: 'object', + fields: [ + { name: 'first', widget: 'string', default: 'first' }, + { name: 'second', widget: 'string', default: 'second' }, + ], + }, + }, + ]); + expect(createEmptyDraftData(fields)).toEqual({ + names: [{ object: { first: 'first', second: 'second' } }], + }); + }); + }); + + describe('persistLocalBackup', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should persist local backup with media files', () => { + const { currentBackend } = require('coreSrc/backend'); + + const backend = { + persistLocalDraftBackup: jest.fn(() => Promise.resolve()), + }; + + const store = mockStore({ + config: Map(), + }); + + currentBackend.mockReturnValue(backend); + + const collection = Map(); + const mediaFiles = [{ path: 'static/media/image.png' }]; + const entry = fromJS({ mediaFiles }); + + return store.dispatch(persistLocalBackup(entry, collection)).then(() => { + const actions = store.getActions(); + expect(actions).toHaveLength(0); + + expect(backend.persistLocalDraftBackup).toHaveBeenCalledTimes(1); + expect(backend.persistLocalDraftBackup).toHaveBeenCalledWith(entry, collection); + }); + }); + }); + + describe('retrieveLocalBackup', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should retrieve media files with local backup', () => { + const { currentBackend } = require('coreSrc/backend'); + const { createAssetProxy } = require('../../valueObjects/AssetProxy'); + + const backend = { + getLocalDraftBackup: jest.fn((...args) => args), + }; + + const store = mockStore({ + config: Map(), + }); + + currentBackend.mockReturnValue(backend); + + const collection = Map({ + name: 'collection', + }); + const slug = 'slug'; + + const file = new File([], 'image.png'); + const mediaFiles = [{ path: 'static/media/image.png', url: 'url', file }]; + const asset = createAssetProxy(mediaFiles[0]); + const entry = { mediaFiles }; + + backend.getLocalDraftBackup.mockReturnValue({ entry }); + + return store.dispatch(retrieveLocalBackup(collection, slug)).then(() => { + const actions = store.getActions(); + + expect(actions).toHaveLength(2); + + expect(actions[0]).toEqual({ + type: 'ADD_ASSETS', + payload: [asset], + }); + expect(actions[1]).toEqual({ + type: 'DRAFT_LOCAL_BACKUP_RETRIEVED', + payload: { entry }, + }); + }); + }); + }); + + describe('getMediaAssets', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should map mediaFiles to assets', () => { + const mediaFiles = fromJS([{ path: 'path1' }, { path: 'path2', draft: true }]); + + const entry = Map({ mediaFiles }); + expect(getMediaAssets({ entry })).toEqual([new AssetProxy({ path: 'path2' })]); + }); + }); + + describe('validateMetaField', () => { + const state = { + config: { + slug: { + encoding: 'unicode', + clean_accents: false, + sanitize_replacement: '-', + }, + }, + entries: fromJS([]), + }; + const collection = fromJS({ + folder: 'folder', + type: 'folder_based_collection', + name: 'name', + }); + const t = jest.fn((key, args) => ({ key, args })); + + const { selectCustomPath } = require('../../reducers/entryDraft'); + const { selectEntryByPath } = require('../../reducers/entries'); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should not return error on non meta field', () => { + expect(validateMetaField(null, null, fromJS({}), null, t)).toEqual({ error: false }); + }); + + it('should not return error on meta path field', () => { + expect(validateMetaField(null, null, fromJS({ meta: true, name: 'other' }), null, t)).toEqual( + { error: false }, + ); + }); + + it('should return error on empty path', () => { + expect(validateMetaField(null, null, fromJS({ meta: true, name: 'path' }), null, t)).toEqual({ + error: { + message: { + key: 'editor.editorControlPane.widget.invalidPath', + args: { path: null }, + }, + type: 'CUSTOM', + }, + }); + + expect( + validateMetaField(null, null, fromJS({ meta: true, name: 'path' }), undefined, t), + ).toEqual({ + error: { + message: { + key: 'editor.editorControlPane.widget.invalidPath', + args: { path: undefined }, + }, + type: 'CUSTOM', + }, + }); + + expect(validateMetaField(null, null, fromJS({ meta: true, name: 'path' }), '', t)).toEqual({ + error: { + message: { + key: 'editor.editorControlPane.widget.invalidPath', + args: { path: '' }, + }, + type: 'CUSTOM', + }, + }); + }); + + it('should return error on invalid path', () => { + expect( + validateMetaField(state, null, fromJS({ meta: true, name: 'path' }), 'invalid path', t), + ).toEqual({ + error: { + message: { + key: 'editor.editorControlPane.widget.invalidPath', + args: { path: 'invalid path' }, + }, + type: 'CUSTOM', + }, + }); + }); + + it('should return error on existing path', () => { + selectCustomPath.mockReturnValue('existing-path'); + selectEntryByPath.mockReturnValue(fromJS({ path: 'existing-path' })); + expect( + validateMetaField( + { + ...state, + entryDraft: fromJS({ + entry: {}, + }), + }, + collection, + fromJS({ meta: true, name: 'path' }), + 'existing-path', + t, + ), + ).toEqual({ + error: { + message: { + key: 'editor.editorControlPane.widget.pathExists', + args: { path: 'existing-path' }, + }, + type: 'CUSTOM', + }, + }); + + expect(selectCustomPath).toHaveBeenCalledTimes(1); + expect(selectCustomPath).toHaveBeenCalledWith( + collection, + fromJS({ entry: { meta: { path: 'existing-path' } } }), + ); + + expect(selectEntryByPath).toHaveBeenCalledTimes(1); + expect(selectEntryByPath).toHaveBeenCalledWith( + state.entries, + collection.get('name'), + 'existing-path', + ); + }); + + it('should not return error on non existing path for new entry', () => { + selectCustomPath.mockReturnValue('non-existing-path'); + selectEntryByPath.mockReturnValue(undefined); + expect( + validateMetaField( + { + ...state, + entryDraft: fromJS({ + entry: {}, + }), + }, + collection, + fromJS({ meta: true, name: 'path' }), + 'non-existing-path', + t, + ), + ).toEqual({ + error: false, + }); + }); + + it('should not return error when for existing entry', () => { + selectCustomPath.mockReturnValue('existing-path'); + selectEntryByPath.mockReturnValue(fromJS({ path: 'existing-path' })); + expect( + validateMetaField( + { + ...state, + entryDraft: fromJS({ + entry: { path: 'existing-path' }, + }), + }, + collection, + fromJS({ meta: true, name: 'path' }), + 'existing-path', + t, + ), + ).toEqual({ + error: false, + }); + }); + }); +}); diff --git a/packages/netlify-cms-core/src/actions/__tests__/media.spec.ts b/packages/netlify-cms-core/src/actions/__tests__/media.spec.ts index 9b77e13d..4b1dd2e5 100644 --- a/packages/netlify-cms-core/src/actions/__tests__/media.spec.ts +++ b/packages/netlify-cms-core/src/actions/__tests__/media.spec.ts @@ -9,9 +9,8 @@ import { State } from '../../types/redux'; import AssetProxy from '../../valueObjects/AssetProxy'; const middlewares = [thunk]; -const mockStore = configureMockStore, ThunkDispatch>( - middlewares, -); +const mockStore = + configureMockStore, ThunkDispatch>(middlewares); const mockedSelectMediaFilePath = mocked(selectMediaFilePath); jest.mock('../../reducers/entries'); diff --git a/packages/netlify-cms-core/src/actions/__tests__/mediaLibrary.spec.js b/packages/netlify-cms-core/src/actions/__tests__/mediaLibrary.spec.js index 64fce9f4..2d6e44e7 100644 --- a/packages/netlify-cms-core/src/actions/__tests__/mediaLibrary.spec.js +++ b/packages/netlify-cms-core/src/actions/__tests__/mediaLibrary.spec.js @@ -1,326 +1,326 @@ -import configureMockStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; -import { List, Map } from 'immutable'; -import { insertMedia, persistMedia, deleteMedia } from '../mediaLibrary'; - -jest.mock('../../backend'); -jest.mock('../waitUntil'); -jest.mock('netlify-cms-lib-util', () => { - const lib = jest.requireActual('netlify-cms-lib-util'); - return { - ...lib, - getBlobSHA: jest.fn(), - }; -}); - -const middlewares = [thunk]; -const mockStore = configureMockStore(middlewares); - -describe('mediaLibrary', () => { - describe('insertMedia', () => { - it('should return mediaPath as string when string is given', () => { - const store = mockStore({ - config: { - public_folder: '/media', - }, - collections: Map({ - posts: Map({ name: 'posts' }), - }), - entryDraft: Map({ - entry: Map({ isPersisting: false, collection: 'posts' }), - }), - }); - - store.dispatch(insertMedia('foo.png')); - expect(store.getActions()[0]).toEqual({ - type: 'MEDIA_INSERT', - payload: { mediaPath: '/media/foo.png' }, - }); - }); - - it('should return mediaPath as array of strings when array of strings is given', () => { - const store = mockStore({ - config: { - public_folder: '/media', - }, - collections: Map({ - posts: Map({ name: 'posts' }), - }), - entryDraft: Map({ - entry: Map({ isPersisting: false, collection: 'posts' }), - }), - }); - - store.dispatch(insertMedia(['foo.png'])); - expect(store.getActions()[0]).toEqual({ - type: 'MEDIA_INSERT', - payload: { mediaPath: ['/media/foo.png'] }, - }); - }); - }); - - const { currentBackend } = require('coreSrc/backend'); - - const backend = { - persistMedia: jest.fn(() => ({ id: 'id' })), - deleteMedia: jest.fn(), - }; - - currentBackend.mockReturnValue(backend); - - describe('persistMedia', () => { - global.URL = { createObjectURL: jest.fn().mockReturnValue('displayURL') }; - - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('should not persist media when editing draft', () => { - const { getBlobSHA } = require('netlify-cms-lib-util'); - - getBlobSHA.mockReturnValue('000000000000000'); - - const store = mockStore({ - config: { - media_folder: 'static/media', - slug: { - encoding: 'unicode', - clean_accents: false, - sanitize_replacement: '-', - }, - }, - collections: Map({ - posts: Map({ name: 'posts' }), - }), - integrations: Map(), - mediaLibrary: Map({ - files: List(), - }), - entryDraft: Map({ - entry: Map({ isPersisting: false, collection: 'posts' }), - }), - }); - - const file = new File([''], 'name.png'); - - return store.dispatch(persistMedia(file)).then(() => { - const actions = store.getActions(); - - expect(actions).toHaveLength(2); - expect(actions[0].type).toEqual('ADD_ASSET'); - expect(actions[0].payload).toEqual( - expect.objectContaining({ - path: 'static/media/name.png', - }), - ); - expect(actions[1].type).toEqual('ADD_DRAFT_ENTRY_MEDIA_FILE'); - expect(actions[1].payload).toEqual( - expect.objectContaining({ - draft: true, - id: '000000000000000', - path: 'static/media/name.png', - size: file.size, - name: file.name, - }), - ); - - expect(getBlobSHA).toHaveBeenCalledTimes(1); - expect(getBlobSHA).toHaveBeenCalledWith(file); - expect(backend.persistMedia).toHaveBeenCalledTimes(0); - }); - }); - - it('should persist media when not editing draft', () => { - const store = mockStore({ - config: { - media_folder: 'static/media', - slug: { - encoding: 'unicode', - clean_accents: false, - sanitize_replacement: '-', - }, - }, - collections: Map({ - posts: Map({ name: 'posts' }), - }), - integrations: Map(), - mediaLibrary: Map({ - files: List(), - }), - entryDraft: Map({ - entry: Map(), - }), - }); - - const file = new File([''], 'name.png'); - - return store.dispatch(persistMedia(file)).then(() => { - const actions = store.getActions(); - - expect(actions).toHaveLength(3); - - expect(actions).toHaveLength(3); - expect(actions[0]).toEqual({ type: 'MEDIA_PERSIST_REQUEST' }); - expect(actions[1].type).toEqual('ADD_ASSET'); - expect(actions[1].payload).toEqual( - expect.objectContaining({ - path: 'static/media/name.png', - }), - ); - expect(actions[2]).toEqual({ - type: 'MEDIA_PERSIST_SUCCESS', - payload: { - file: { id: 'id' }, - }, - }); - - expect(backend.persistMedia).toHaveBeenCalledTimes(1); - expect(backend.persistMedia).toHaveBeenCalledWith( - store.getState().config, - expect.objectContaining({ - path: 'static/media/name.png', - }), - ); - }); - }); - - it('should sanitize media name if needed when persisting', () => { - const store = mockStore({ - config: { - media_folder: 'static/media', - slug: { - encoding: 'ascii', - clean_accents: true, - sanitize_replacement: '_', - }, - }, - collections: Map({ - posts: Map({ name: 'posts' }), - }), - integrations: Map(), - mediaLibrary: Map({ - files: List(), - }), - entryDraft: Map({ - entry: Map(), - }), - }); - - const file = new File([''], 'abc DEF éâçÖ $;, .png'); - - return store.dispatch(persistMedia(file)).then(() => { - const actions = store.getActions(); - - expect(actions).toHaveLength(3); - - expect(actions[0]).toEqual({ type: 'MEDIA_PERSIST_REQUEST' }); - - expect(actions[1].type).toEqual('ADD_ASSET'); - expect(actions[1].payload).toEqual( - expect.objectContaining({ - path: 'static/media/abc_def_eaco_.png', - }), - ); - - expect(actions[2]).toEqual({ - type: 'MEDIA_PERSIST_SUCCESS', - payload: { - file: { id: 'id' }, - }, - }); - - expect(backend.persistMedia).toHaveBeenCalledTimes(1); - expect(backend.persistMedia).toHaveBeenCalledWith( - store.getState().config, - expect.objectContaining({ - path: 'static/media/abc_def_eaco_.png', - }), - ); - }); - }); - }); - - describe('deleteMedia', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('should delete non draft file', () => { - const store = mockStore({ - config: { - publish_mode: 'editorial_workflow', - }, - collections: Map(), - integrations: Map(), - mediaLibrary: Map({ - files: List(), - }), - entryDraft: Map({ - entry: Map({ isPersisting: false }), - }), - }); - - const file = { name: 'name.png', id: 'id', path: 'static/media/name.png', draft: false }; - - return store.dispatch(deleteMedia(file)).then(() => { - const actions = store.getActions(); - - expect(actions).toHaveLength(4); - expect(actions[0]).toEqual({ type: 'MEDIA_DELETE_REQUEST' }); - expect(actions[1]).toEqual({ - type: 'REMOVE_ASSET', - payload: 'static/media/name.png', - }); - expect(actions[2]).toEqual({ - type: 'MEDIA_DELETE_SUCCESS', - payload: { file }, - }); - expect(actions[3]).toEqual({ - type: 'REMOVE_DRAFT_ENTRY_MEDIA_FILE', - payload: { id: 'id' }, - }); - - expect(backend.deleteMedia).toHaveBeenCalledTimes(1); - expect(backend.deleteMedia).toHaveBeenCalledWith( - store.getState().config, - 'static/media/name.png', - ); - }); - }); - - it('should not delete a draft file', () => { - const store = mockStore({ - config: { - publish_mode: 'editorial_workflow', - }, - collections: Map(), - integrations: Map(), - mediaLibrary: Map({ - files: List(), - }), - entryDraft: Map({ - entry: Map({ isPersisting: false }), - }), - }); - - const file = { name: 'name.png', id: 'id', path: 'static/media/name.png', draft: true }; - - return store.dispatch(deleteMedia(file)).then(() => { - const actions = store.getActions(); - - expect(actions).toHaveLength(2); - expect(actions[0]).toEqual({ - type: 'REMOVE_ASSET', - payload: 'static/media/name.png', - }); - - expect(actions[1]).toEqual({ - type: 'REMOVE_DRAFT_ENTRY_MEDIA_FILE', - payload: { id: 'id' }, - }); - - expect(backend.deleteMedia).toHaveBeenCalledTimes(0); - }); - }); - }); -}); +import configureMockStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import { List, Map } from 'immutable'; +import { insertMedia, persistMedia, deleteMedia } from '../mediaLibrary'; + +jest.mock('../../backend'); +jest.mock('../waitUntil'); +jest.mock('netlify-cms-lib-util', () => { + const lib = jest.requireActual('netlify-cms-lib-util'); + return { + ...lib, + getBlobSHA: jest.fn(), + }; +}); + +const middlewares = [thunk]; +const mockStore = configureMockStore(middlewares); + +describe('mediaLibrary', () => { + describe('insertMedia', () => { + it('should return mediaPath as string when string is given', () => { + const store = mockStore({ + config: { + public_folder: '/media', + }, + collections: Map({ + posts: Map({ name: 'posts' }), + }), + entryDraft: Map({ + entry: Map({ isPersisting: false, collection: 'posts' }), + }), + }); + + store.dispatch(insertMedia('foo.png')); + expect(store.getActions()[0]).toEqual({ + type: 'MEDIA_INSERT', + payload: { mediaPath: '/media/foo.png' }, + }); + }); + + it('should return mediaPath as array of strings when array of strings is given', () => { + const store = mockStore({ + config: { + public_folder: '/media', + }, + collections: Map({ + posts: Map({ name: 'posts' }), + }), + entryDraft: Map({ + entry: Map({ isPersisting: false, collection: 'posts' }), + }), + }); + + store.dispatch(insertMedia(['foo.png'])); + expect(store.getActions()[0]).toEqual({ + type: 'MEDIA_INSERT', + payload: { mediaPath: ['/media/foo.png'] }, + }); + }); + }); + + const { currentBackend } = require('coreSrc/backend'); + + const backend = { + persistMedia: jest.fn(() => ({ id: 'id' })), + deleteMedia: jest.fn(), + }; + + currentBackend.mockReturnValue(backend); + + describe('persistMedia', () => { + global.URL = { createObjectURL: jest.fn().mockReturnValue('displayURL') }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should not persist media when editing draft', () => { + const { getBlobSHA } = require('netlify-cms-lib-util'); + + getBlobSHA.mockReturnValue('000000000000000'); + + const store = mockStore({ + config: { + media_folder: 'static/media', + slug: { + encoding: 'unicode', + clean_accents: false, + sanitize_replacement: '-', + }, + }, + collections: Map({ + posts: Map({ name: 'posts' }), + }), + integrations: Map(), + mediaLibrary: Map({ + files: List(), + }), + entryDraft: Map({ + entry: Map({ isPersisting: false, collection: 'posts' }), + }), + }); + + const file = new File([''], 'name.png'); + + return store.dispatch(persistMedia(file)).then(() => { + const actions = store.getActions(); + + expect(actions).toHaveLength(2); + expect(actions[0].type).toEqual('ADD_ASSET'); + expect(actions[0].payload).toEqual( + expect.objectContaining({ + path: 'static/media/name.png', + }), + ); + expect(actions[1].type).toEqual('ADD_DRAFT_ENTRY_MEDIA_FILE'); + expect(actions[1].payload).toEqual( + expect.objectContaining({ + draft: true, + id: '000000000000000', + path: 'static/media/name.png', + size: file.size, + name: file.name, + }), + ); + + expect(getBlobSHA).toHaveBeenCalledTimes(1); + expect(getBlobSHA).toHaveBeenCalledWith(file); + expect(backend.persistMedia).toHaveBeenCalledTimes(0); + }); + }); + + it('should persist media when not editing draft', () => { + const store = mockStore({ + config: { + media_folder: 'static/media', + slug: { + encoding: 'unicode', + clean_accents: false, + sanitize_replacement: '-', + }, + }, + collections: Map({ + posts: Map({ name: 'posts' }), + }), + integrations: Map(), + mediaLibrary: Map({ + files: List(), + }), + entryDraft: Map({ + entry: Map(), + }), + }); + + const file = new File([''], 'name.png'); + + return store.dispatch(persistMedia(file)).then(() => { + const actions = store.getActions(); + + expect(actions).toHaveLength(3); + + expect(actions).toHaveLength(3); + expect(actions[0]).toEqual({ type: 'MEDIA_PERSIST_REQUEST' }); + expect(actions[1].type).toEqual('ADD_ASSET'); + expect(actions[1].payload).toEqual( + expect.objectContaining({ + path: 'static/media/name.png', + }), + ); + expect(actions[2]).toEqual({ + type: 'MEDIA_PERSIST_SUCCESS', + payload: { + file: { id: 'id' }, + }, + }); + + expect(backend.persistMedia).toHaveBeenCalledTimes(1); + expect(backend.persistMedia).toHaveBeenCalledWith( + store.getState().config, + expect.objectContaining({ + path: 'static/media/name.png', + }), + ); + }); + }); + + it('should sanitize media name if needed when persisting', () => { + const store = mockStore({ + config: { + media_folder: 'static/media', + slug: { + encoding: 'ascii', + clean_accents: true, + sanitize_replacement: '_', + }, + }, + collections: Map({ + posts: Map({ name: 'posts' }), + }), + integrations: Map(), + mediaLibrary: Map({ + files: List(), + }), + entryDraft: Map({ + entry: Map(), + }), + }); + + const file = new File([''], 'abc DEF éâçÖ $;, .png'); + + return store.dispatch(persistMedia(file)).then(() => { + const actions = store.getActions(); + + expect(actions).toHaveLength(3); + + expect(actions[0]).toEqual({ type: 'MEDIA_PERSIST_REQUEST' }); + + expect(actions[1].type).toEqual('ADD_ASSET'); + expect(actions[1].payload).toEqual( + expect.objectContaining({ + path: 'static/media/abc_def_eaco_.png', + }), + ); + + expect(actions[2]).toEqual({ + type: 'MEDIA_PERSIST_SUCCESS', + payload: { + file: { id: 'id' }, + }, + }); + + expect(backend.persistMedia).toHaveBeenCalledTimes(1); + expect(backend.persistMedia).toHaveBeenCalledWith( + store.getState().config, + expect.objectContaining({ + path: 'static/media/abc_def_eaco_.png', + }), + ); + }); + }); + }); + + describe('deleteMedia', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should delete non draft file', () => { + const store = mockStore({ + config: { + publish_mode: 'editorial_workflow', + }, + collections: Map(), + integrations: Map(), + mediaLibrary: Map({ + files: List(), + }), + entryDraft: Map({ + entry: Map({ isPersisting: false }), + }), + }); + + const file = { name: 'name.png', id: 'id', path: 'static/media/name.png', draft: false }; + + return store.dispatch(deleteMedia(file)).then(() => { + const actions = store.getActions(); + + expect(actions).toHaveLength(4); + expect(actions[0]).toEqual({ type: 'MEDIA_DELETE_REQUEST' }); + expect(actions[1]).toEqual({ + type: 'REMOVE_ASSET', + payload: 'static/media/name.png', + }); + expect(actions[2]).toEqual({ + type: 'MEDIA_DELETE_SUCCESS', + payload: { file }, + }); + expect(actions[3]).toEqual({ + type: 'REMOVE_DRAFT_ENTRY_MEDIA_FILE', + payload: { id: 'id' }, + }); + + expect(backend.deleteMedia).toHaveBeenCalledTimes(1); + expect(backend.deleteMedia).toHaveBeenCalledWith( + store.getState().config, + 'static/media/name.png', + ); + }); + }); + + it('should not delete a draft file', () => { + const store = mockStore({ + config: { + publish_mode: 'editorial_workflow', + }, + collections: Map(), + integrations: Map(), + mediaLibrary: Map({ + files: List(), + }), + entryDraft: Map({ + entry: Map({ isPersisting: false }), + }), + }); + + const file = { name: 'name.png', id: 'id', path: 'static/media/name.png', draft: true }; + + return store.dispatch(deleteMedia(file)).then(() => { + const actions = store.getActions(); + + expect(actions).toHaveLength(2); + expect(actions[0]).toEqual({ + type: 'REMOVE_ASSET', + payload: 'static/media/name.png', + }); + + expect(actions[1]).toEqual({ + type: 'REMOVE_DRAFT_ENTRY_MEDIA_FILE', + payload: { id: 'id' }, + }); + + expect(backend.deleteMedia).toHaveBeenCalledTimes(0); + }); + }); + }); +}); diff --git a/packages/netlify-cms-core/src/actions/config.ts b/packages/netlify-cms-core/src/actions/config.ts index 985edd86..33f2f19e 100644 --- a/packages/netlify-cms-core/src/actions/config.ts +++ b/packages/netlify-cms-core/src/actions/config.ts @@ -474,9 +474,11 @@ export async function handleLocalBackend(originalConfig: CmsConfig) { return originalConfig; } - const { proxyUrl, publish_modes: publishModes, type: backendType } = await detectProxyServer( - originalConfig.local_backend, - ); + const { + proxyUrl, + publish_modes: publishModes, + type: backendType, + } = await detectProxyServer(originalConfig.local_backend); if (!proxyUrl) { return originalConfig; diff --git a/packages/netlify-cms-core/src/actions/editorialWorkflow.ts b/packages/netlify-cms-core/src/actions/editorialWorkflow.ts index 733932a3..4d240865 100644 --- a/packages/netlify-cms-core/src/actions/editorialWorkflow.ts +++ b/packages/netlify-cms-core/src/actions/editorialWorkflow.ts @@ -519,7 +519,7 @@ export function unpublishPublishedEntry(collection: Collection, slug: string) { const state = getState(); const backend = currentBackend(state.config); const entry = selectEntry(state, collection.get('name'), slug); - const entryDraft = (Map().set('entry', entry) as unknown) as EntryDraft; + const entryDraft = Map().set('entry', entry) as unknown as EntryDraft; dispatch(unpublishedEntryPersisting(collection, slug)); return backend .deleteEntry(state, collection, slug) diff --git a/packages/netlify-cms-core/src/backend.ts b/packages/netlify-cms-core/src/backend.ts index 9364e44e..fe8a1014 100644 --- a/packages/netlify-cms-core/src/backend.ts +++ b/packages/netlify-cms-core/src/backend.ts @@ -1322,7 +1322,7 @@ export function resolveBackend(config: CmsConfig) { } } -export const currentBackend = (function() { +export const currentBackend = (function () { let backend: Backend; return (config: CmsConfig) => { diff --git a/packages/netlify-cms-core/src/components/App/App.js b/packages/netlify-cms-core/src/components/App/App.js index 645a0916..622b4b5d 100644 --- a/packages/netlify-cms-core/src/components/App/App.js +++ b/packages/netlify-cms-core/src/components/App/App.js @@ -25,7 +25,7 @@ import Header from './Header'; TopBarProgress.config({ barColors: { - '0': colors.active, + 0: colors.active, '1.0': colors.active, }, shadowBlur: 0, diff --git a/packages/netlify-cms-core/src/components/Editor/Editor.js b/packages/netlify-cms-core/src/components/Editor/Editor.js index 2a2e27ac..3635cb57 100644 --- a/packages/netlify-cms-core/src/components/Editor/Editor.js +++ b/packages/netlify-cms-core/src/components/Editor/Editor.js @@ -192,7 +192,7 @@ export class Editor extends React.Component { window.removeEventListener('beforeunload', this.exitBlocker); } - createBackup = debounce(function(entry, collection) { + createBackup = debounce(function (entry, collection) { this.props.persistLocalBackup(entry, collection); }, 2000); @@ -202,14 +202,8 @@ export class Editor extends React.Component { }; handleChangeStatus = newStatusName => { - const { - entryDraft, - updateUnpublishedEntryStatus, - collection, - slug, - currentStatus, - t, - } = this.props; + const { entryDraft, updateUnpublishedEntryStatus, collection, slug, currentStatus, t } = + this.props; if (entryDraft.get('hasChanged')) { window.alert(t('editor.editor.onUpdatingWithUnsavedChanges')); return; @@ -318,15 +312,8 @@ export class Editor extends React.Component { }; handleDeleteUnpublishedChanges = async () => { - const { - entryDraft, - collection, - slug, - deleteUnpublishedEntry, - loadEntry, - isModification, - t, - } = this.props; + const { entryDraft, collection, slug, deleteUnpublishedEntry, loadEntry, isModification, t } = + this.props; if ( entryDraft.get('hasChanged') && !window.confirm(t('editor.editor.onDeleteUnpublishedChangesWithUnsavedChanges')) diff --git a/packages/netlify-cms-core/src/formats/yaml.ts b/packages/netlify-cms-core/src/formats/yaml.ts index 11e45b60..ec34da61 100644 --- a/packages/netlify-cms-core/src/formats/yaml.ts +++ b/packages/netlify-cms-core/src/formats/yaml.ts @@ -24,10 +24,10 @@ const timestampTag = { tag: '!timestamp', test: RegExp( '^' + - '([0-9]{4})-([0-9]{2})-([0-9]{2})' + // YYYY-MM-DD - 'T' + // T - '([0-9]{2}):([0-9]{2}):([0-9]{2}(\\.[0-9]+)?)' + // HH:MM:SS(.ss)? - 'Z' + // Z + '([0-9]{4})-([0-9]{2})-([0-9]{2})' + // YYYY-MM-DD + 'T' + // T + '([0-9]{2}):([0-9]{2}):([0-9]{2}(\\.[0-9]+)?)' + // HH:MM:SS(.ss)? + 'Z' + // Z '$', ), resolve: (str: string) => new Date(str), diff --git a/packages/netlify-cms-core/src/integrations/index.js b/packages/netlify-cms-core/src/integrations/index.js index c5c51f4e..3546c7af 100644 --- a/packages/netlify-cms-core/src/integrations/index.js +++ b/packages/netlify-cms-core/src/integrations/index.js @@ -20,7 +20,7 @@ export function resolveIntegrations(interationsConfig, getToken) { return integrationInstances; } -export const getIntegrationProvider = (function() { +export const getIntegrationProvider = (function () { let integrations = null; return (interationsConfig, getToken, provider) => { diff --git a/packages/netlify-cms-core/src/lib/formatters.ts b/packages/netlify-cms-core/src/lib/formatters.ts index bbe1ad7f..715b32ab 100644 --- a/packages/netlify-cms-core/src/lib/formatters.ts +++ b/packages/netlify-cms-core/src/lib/formatters.ts @@ -182,7 +182,7 @@ export function previewUrlFormatter( let fields = entry.get('data') as Map; fields = addFileTemplateFields(entry.get('path'), fields, collection.get('folder')); const dateFieldName = getDateField() || selectInferedField(collection, 'date'); - const date = parseDateFromEntry((entry as unknown) as Map, dateFieldName); + const date = parseDateFromEntry(entry as unknown as Map, dateFieldName); // Prepare and sanitize slug variables only, leave the rest of the // `preview_path` template as is. @@ -213,7 +213,7 @@ export function summaryFormatter(summaryTemplate: string, entry: EntryMap, colle let entryData = entry.get('data'); const date = parseDateFromEntry( - (entry as unknown) as Map, + entry as unknown as Map, selectInferedField(collection, 'date'), ) || null; const identifier = entryData.getIn(keyToPathArray(selectIdentifier(collection) as string)); @@ -247,7 +247,7 @@ export function folderFormatter( const date = parseDateFromEntry( - (entry as unknown) as Map, + entry as unknown as Map, selectInferedField(collection, 'date'), ) || null; const identifier = fields.getIn(keyToPathArray(selectIdentifier(collection) as string)); diff --git a/packages/netlify-cms-core/src/lib/urlHelper.ts b/packages/netlify-cms-core/src/lib/urlHelper.ts index 4f30a814..dd9006e8 100644 --- a/packages/netlify-cms-core/src/lib/urlHelper.ts +++ b/packages/netlify-cms-core/src/lib/urlHelper.ts @@ -36,7 +36,8 @@ export function stripProtocol(urlString: string) { * but JS stores strings as UTF-16/UCS-2 internally, so we should not normalize or re-encode. */ const uriChars = /[\w\-.~]/i; -const ucsChars = /[\xA0-\u{D7FF}\u{F900}-\u{FDCF}\u{FDF0}-\u{FFEF}\u{10000}-\u{1FFFD}\u{20000}-\u{2FFFD}\u{30000}-\u{3FFFD}\u{40000}-\u{4FFFD}\u{50000}-\u{5FFFD}\u{60000}-\u{6FFFD}\u{70000}-\u{7FFFD}\u{80000}-\u{8FFFD}\u{90000}-\u{9FFFD}\u{A0000}-\u{AFFFD}\u{B0000}-\u{BFFFD}\u{C0000}-\u{CFFFD}\u{D0000}-\u{DFFFD}\u{E1000}-\u{EFFFD}]/u; +const ucsChars = + /[\xA0-\u{D7FF}\u{F900}-\u{FDCF}\u{FDF0}-\u{FFEF}\u{10000}-\u{1FFFD}\u{20000}-\u{2FFFD}\u{30000}-\u{3FFFD}\u{40000}-\u{4FFFD}\u{50000}-\u{5FFFD}\u{60000}-\u{6FFFD}\u{70000}-\u{7FFFD}\u{80000}-\u{8FFFD}\u{90000}-\u{9FFFD}\u{A0000}-\u{AFFFD}\u{B0000}-\u{BFFFD}\u{C0000}-\u{CFFFD}\u{D0000}-\u{DFFFD}\u{E1000}-\u{EFFFD}]/u; function validURIChar(char: string) { return uriChars.test(char); @@ -80,9 +81,7 @@ export function sanitizeURI( // `Array.from` must be used instead of `String.split` because // `split` converts things like emojis into UTF-16 surrogate pairs. - return Array.from(str) - .map(getCharReplacer(encoding, replacement)) - .join(''); + return Array.from(str).map(getCharReplacer(encoding, replacement)).join(''); } export function sanitizeChar(char: string, options?: CmsSlug) { @@ -95,8 +94,11 @@ export function sanitizeSlug(str: string, options?: CmsSlug) { throw new Error('The input slug must be a string.'); } - const { encoding, clean_accents: stripDiacritics, sanitize_replacement: replacement } = - options || {}; + const { + encoding, + clean_accents: stripDiacritics, + sanitize_replacement: replacement, + } = options || {}; const sanitizedSlug = flow([ ...(stripDiacritics ? [diacritics.remove] : []), diff --git a/packages/netlify-cms-core/src/mediaLibrary.ts b/packages/netlify-cms-core/src/mediaLibrary.ts index 311477fd..2396892e 100644 --- a/packages/netlify-cms-core/src/mediaLibrary.ts +++ b/packages/netlify-cms-core/src/mediaLibrary.ts @@ -25,7 +25,7 @@ function handleInsert(url: string) { } const initializeMediaLibrary = once(async function initializeMediaLibrary(name, options) { - const lib = (getMediaLibrary(name) as unknown) as MediaLibrary | undefined; + const lib = getMediaLibrary(name) as unknown as MediaLibrary | undefined; if (!lib) { const err = new Error( `Missing external media library '${name}'. Please use 'registerMediaLibrary' to register it.`, diff --git a/packages/netlify-cms-core/src/reducers/__tests__/collections.spec.js b/packages/netlify-cms-core/src/reducers/__tests__/collections.spec.js index 93ef2266..9bffd826 100644 --- a/packages/netlify-cms-core/src/reducers/__tests__/collections.spec.js +++ b/packages/netlify-cms-core/src/reducers/__tests__/collections.spec.js @@ -368,30 +368,15 @@ describe('collections', () => { }); expect(selectField(collection, 'en.title')).toBe( - collection - .get('fields') - .get(0) - .get('fields') - .get(0), + collection.get('fields').get(0).get('fields').get(0), ); expect(selectField(collection, 'it.title.subTitle')).toBe( - collection - .get('fields') - .get(2) - .get('field') - .get('fields') - .get(0), + collection.get('fields').get(2).get('field').get('fields').get(0), ); expect(selectField(collection, 'fr.title.variableType')).toBe( - collection - .get('fields') - .get(3) - .get('fields') - .get(0) - .get('types') - .get(0), + collection.get('fields').get(3).get('fields').get(0).get('types').get(0), ); }); }); diff --git a/packages/netlify-cms-core/src/reducers/collections.ts b/packages/netlify-cms-core/src/reducers/collections.ts index b6d6b16c..b1caad03 100644 --- a/packages/netlify-cms-core/src/reducers/collections.ts +++ b/packages/netlify-cms-core/src/reducers/collections.ts @@ -144,10 +144,7 @@ export function selectFieldsWithMediaFolders(collection: Collection, slug: strin const fields = collection.get('fields').toArray(); return getFieldsWithMediaFolders(fields); } else if (collection.has('files')) { - const fields = - getFileFromSlug(collection, slug) - ?.get('fields') - .toArray() || []; + const fields = getFileFromSlug(collection, slug)?.get('fields').toArray() || []; return getFieldsWithMediaFolders(fields); } @@ -317,16 +314,18 @@ export function selectInferedField(collection: Collection, fieldName: string) { if (fieldName === 'title' && collection.get('identifier_field')) { return selectIdentifier(collection); } - const inferableField = (INFERABLE_FIELDS as Record< - string, - { - type: string; - synonyms: string[]; - secondaryTypes: string[]; - fallbackToFirstField: boolean; - showError: boolean; - } - >)[fieldName]; + const inferableField = ( + INFERABLE_FIELDS as Record< + string, + { + type: string; + synonyms: string[]; + secondaryTypes: string[]; + fallbackToFirstField: boolean; + showError: boolean; + } + > + )[fieldName]; const fields = collection.get('fields'); let field; diff --git a/packages/netlify-cms-core/src/reducers/entries.ts b/packages/netlify-cms-core/src/reducers/entries.ts index c348df3e..194904ee 100644 --- a/packages/netlify-cms-core/src/reducers/entries.ts +++ b/packages/netlify-cms-core/src/reducers/entries.ts @@ -105,10 +105,9 @@ function persistSort(sort: Sort | undefined) { const storageSort: StorageSort = {}; sort.keySeq().forEach(key => { const collection = key as string; - const sortObjects = (sort - .get(collection) - .valueSeq() - .toJS() as SortObject[]).map((value, index) => ({ ...value, index })); + const sortObjects = (sort.get(collection).valueSeq().toJS() as SortObject[]).map( + (value, index) => ({ ...value, index }), + ); sortObjects.forEach(value => { set(storageSort, [collection, value.key], value); @@ -333,7 +332,7 @@ function entries( } case CHANGE_VIEW_STYLE: { - const payload = (action.payload as unknown) as ChangeViewStylePayload; + const payload = action.payload as unknown as ChangeViewStylePayload; const { style } = payload; const newState = state.withMutations(map => { map.setIn(['viewStyle'], style); @@ -492,10 +491,8 @@ export function selectGroups(state: Entries, collection: Collection) { return []; } - let groups: Record< - string, - { id: string; label: string; value: string | boolean | undefined } - > = {}; + let groups: Record = + {}; const groupedEntries = groupBy(entries.toArray(), entry => { const group = getGroup(entry, selectedGroup); groups = { ...groups, [group.id]: group }; diff --git a/packages/netlify-cms-core/src/reducers/mediaLibrary.ts b/packages/netlify-cms-core/src/reducers/mediaLibrary.ts index cc4a6fe6..2b3142e9 100644 --- a/packages/netlify-cms-core/src/reducers/mediaLibrary.ts +++ b/packages/netlify-cms-core/src/reducers/mediaLibrary.ts @@ -262,7 +262,7 @@ export function selectMediaFileByPath(state: State, path: string) { export function selectMediaDisplayURL(state: State, id: string) { const displayUrlState = state.mediaLibrary.getIn( ['displayURLs', id], - (Map() as unknown) as DisplayURLState, + Map() as unknown as DisplayURLState, ); return displayUrlState; } diff --git a/packages/netlify-cms-core/src/redux/index.ts b/packages/netlify-cms-core/src/redux/index.ts index d293787a..b4e0d255 100644 --- a/packages/netlify-cms-core/src/redux/index.ts +++ b/packages/netlify-cms-core/src/redux/index.ts @@ -7,7 +7,7 @@ import { State } from '../types/redux'; import { Reducer } from 'react'; const store = createStore( - (createRootReducer() as unknown) as Reducer, + createRootReducer() as unknown as Reducer, composeWithDevTools(applyMiddleware(thunkMiddleware as ThunkMiddleware, waitUntilAction)), ); diff --git a/packages/netlify-cms-core/src/redux/middleware/waitUntilAction.ts b/packages/netlify-cms-core/src/redux/middleware/waitUntilAction.ts index 6645a524..ffa082c4 100644 --- a/packages/netlify-cms-core/src/redux/middleware/waitUntilAction.ts +++ b/packages/netlify-cms-core/src/redux/middleware/waitUntilAction.ts @@ -51,13 +51,14 @@ export const waitUntilAction: Middleware<{}, State, Dispatch> = ({ } } - return (next: Dispatch) => (action: AnyAction): null | AnyAction => { - if (action.type === WAIT_UNTIL_ACTION) { - pending.push(action as WaitAction); - return null; - } - const result = next(action); - checkPending(action); - return result; - }; + return (next: Dispatch) => + (action: AnyAction): null | AnyAction => { + if (action.type === WAIT_UNTIL_ACTION) { + pending.push(action as WaitAction); + return null; + } + const result = next(action); + checkPending(action); + return result; + }; }; diff --git a/packages/netlify-cms-core/src/routing/__tests__/history.spec.ts b/packages/netlify-cms-core/src/routing/__tests__/history.spec.ts index 6622edb8..80a5ee41 100644 --- a/packages/netlify-cms-core/src/routing/__tests__/history.spec.ts +++ b/packages/netlify-cms-core/src/routing/__tests__/history.spec.ts @@ -3,7 +3,7 @@ import { mocked } from 'ts-jest/utils'; jest.mock('history'); -const history = ({ push: jest.fn(), replace: jest.fn() } as unknown) as History; +const history = { push: jest.fn(), replace: jest.fn() } as unknown as History; const mockedCreateHashHistory = mocked(createHashHistory); mockedCreateHashHistory.mockReturnValue(history); diff --git a/packages/netlify-cms-core/src/types/immutable.ts b/packages/netlify-cms-core/src/types/immutable.ts index 7549879b..0afa58cd 100644 --- a/packages/netlify-cms-core/src/types/immutable.ts +++ b/packages/netlify-cms-core/src/types/immutable.ts @@ -11,7 +11,7 @@ export interface StaticallyTypedRecord { K1 extends keyof T, K2 extends keyof T[K1], K3 extends keyof T[K1][K2], - V extends T[K1][K2][K3] + V extends T[K1][K2][K3], >( keys: [K1, K2, K3], defaultValue?: V, diff --git a/packages/netlify-cms-lib-auth/src/pkce-oauth.js b/packages/netlify-cms-lib-auth/src/pkce-oauth.js index 40f0be03..29ad47b6 100644 --- a/packages/netlify-cms-lib-auth/src/pkce-oauth.js +++ b/packages/netlify-cms-lib-auth/src/pkce-oauth.js @@ -26,10 +26,7 @@ function generateVerifierCode() { async function createCodeChallenge(codeVerifier) { const sha = await sha256(codeVerifier); // https://tools.ietf.org/html/rfc7636#appendix-A - return btoa(sha) - .split('=')[0] - .replace(/\+/g, '-') - .replace(/\//g, '_'); + return btoa(sha).split('=')[0].replace(/\+/g, '-').replace(/\//g, '_'); } const CODE_VERIFIER_STORAGE_KEY = 'netlify-cms-pkce-verifier-code'; diff --git a/packages/netlify-cms-lib-util/src/__tests__/backendUtil.spec.js b/packages/netlify-cms-lib-util/src/__tests__/backendUtil.spec.js index f9deeb87..f1ff88c7 100644 --- a/packages/netlify-cms-lib-util/src/__tests__/backendUtil.spec.js +++ b/packages/netlify-cms-lib-util/src/__tests__/backendUtil.spec.js @@ -1,96 +1,96 @@ -import { parseLinkHeader, getAllResponses, getPathDepth, filterByExtension } from '../backendUtil'; -import { oneLine } from 'common-tags'; -import nock from 'nock'; - -describe('parseLinkHeader', () => { - it('should return the right rel urls', () => { - const url = 'https://api.github.com/resource'; - const link = oneLine` - <${url}?page=1>; rel="first", - <${url}?page=2>; rel="prev", - <${url}?page=4>; rel="next", - <${url}?page=5>; rel="last" - `; - const linkHeader = parseLinkHeader(link); - - expect(linkHeader.next).toBe(`${url}?page=4`); - expect(linkHeader.last).toBe(`${url}?page=5`); - expect(linkHeader.first).toBe(`${url}?page=1`); - expect(linkHeader.prev).toBe(`${url}?page=2`); - }); -}); - -describe('getAllResponses', () => { - function generatePulls(length) { - return Array.from({ length }, (_, id) => { - return { id: id + 1, number: `134${id}`, state: 'open' }; - }); - } - - function createLinkHeaders({ page, pageCount }) { - const pageNum = parseInt(page, 10); - const pageCountNum = parseInt(pageCount, 10); - const url = 'https://api.github.com/pulls'; - - function link(linkPage) { - return `<${url}?page=${linkPage}>`; - } - - const linkHeader = oneLine` - ${pageNum === 1 ? '' : `${link(1)}; rel="first",`} - ${pageNum === pageCountNum ? '' : `${link(pageCount)}; rel="last",`} - ${pageNum === 1 ? '' : `${link(pageNum - 1)}; rel="prev",`} - ${pageNum === pageCountNum ? '' : `${link(pageNum + 1)}; rel="next",`} - `.slice(0, -1); - - return { Link: linkHeader }; - } - - function interceptCall({ perPage = 30, repeat = 1, data = [] } = {}) { - nock('https://api.github.com') - .get('/pulls') - .query(true) - .times(repeat) - .reply(uri => { - const searchParams = new URLSearchParams(uri.split('?')[1]); - const page = searchParams.get('page') || 1; - const pageCount = data.length <= perPage ? 1 : Math.ceil(data.length / perPage); - const pageLastIndex = page * perPage; - const pageFirstIndex = pageLastIndex - perPage; - const resp = data.slice(pageFirstIndex, pageLastIndex); - return [200, resp, createLinkHeaders({ page, pageCount })]; - }); - } - - it('should return all paged response', async () => { - interceptCall({ repeat: 3, data: generatePulls(70) }); - const res = await getAllResponses('https://api.github.com/pulls', {}, 'next', url => url); - const pages = await Promise.all(res.map(res => res.json())); - - expect(pages[0]).toHaveLength(30); - expect(pages[1]).toHaveLength(30); - expect(pages[2]).toHaveLength(10); - }); -}); - -describe('getPathDepth', () => { - it('should return 1 for empty string', () => { - expect(getPathDepth('')).toBe(1); - }); - - it('should return 2 for path of one nested folder', () => { - expect(getPathDepth('{{year}}/{{slug}}')).toBe(2); - }); -}); - -describe('filterByExtension', () => { - it('should return true when extension matches', () => { - expect(filterByExtension({ path: 'file.html.md' }, '.html.md')).toBe(true); - expect(filterByExtension({ path: 'file.html.md' }, 'html.md')).toBe(true); - }); - - it("should return false when extension doesn't match", () => { - expect(filterByExtension({ path: 'file.json' }, '.html.md')).toBe(false); - expect(filterByExtension({ path: 'file.json' }, 'html.md')).toBe(false); - }); -}); +import { parseLinkHeader, getAllResponses, getPathDepth, filterByExtension } from '../backendUtil'; +import { oneLine } from 'common-tags'; +import nock from 'nock'; + +describe('parseLinkHeader', () => { + it('should return the right rel urls', () => { + const url = 'https://api.github.com/resource'; + const link = oneLine` + <${url}?page=1>; rel="first", + <${url}?page=2>; rel="prev", + <${url}?page=4>; rel="next", + <${url}?page=5>; rel="last" + `; + const linkHeader = parseLinkHeader(link); + + expect(linkHeader.next).toBe(`${url}?page=4`); + expect(linkHeader.last).toBe(`${url}?page=5`); + expect(linkHeader.first).toBe(`${url}?page=1`); + expect(linkHeader.prev).toBe(`${url}?page=2`); + }); +}); + +describe('getAllResponses', () => { + function generatePulls(length) { + return Array.from({ length }, (_, id) => { + return { id: id + 1, number: `134${id}`, state: 'open' }; + }); + } + + function createLinkHeaders({ page, pageCount }) { + const pageNum = parseInt(page, 10); + const pageCountNum = parseInt(pageCount, 10); + const url = 'https://api.github.com/pulls'; + + function link(linkPage) { + return `<${url}?page=${linkPage}>`; + } + + const linkHeader = oneLine` + ${pageNum === 1 ? '' : `${link(1)}; rel="first",`} + ${pageNum === pageCountNum ? '' : `${link(pageCount)}; rel="last",`} + ${pageNum === 1 ? '' : `${link(pageNum - 1)}; rel="prev",`} + ${pageNum === pageCountNum ? '' : `${link(pageNum + 1)}; rel="next",`} + `.slice(0, -1); + + return { Link: linkHeader }; + } + + function interceptCall({ perPage = 30, repeat = 1, data = [] } = {}) { + nock('https://api.github.com') + .get('/pulls') + .query(true) + .times(repeat) + .reply(uri => { + const searchParams = new URLSearchParams(uri.split('?')[1]); + const page = searchParams.get('page') || 1; + const pageCount = data.length <= perPage ? 1 : Math.ceil(data.length / perPage); + const pageLastIndex = page * perPage; + const pageFirstIndex = pageLastIndex - perPage; + const resp = data.slice(pageFirstIndex, pageLastIndex); + return [200, resp, createLinkHeaders({ page, pageCount })]; + }); + } + + it('should return all paged response', async () => { + interceptCall({ repeat: 3, data: generatePulls(70) }); + const res = await getAllResponses('https://api.github.com/pulls', {}, 'next', url => url); + const pages = await Promise.all(res.map(res => res.json())); + + expect(pages[0]).toHaveLength(30); + expect(pages[1]).toHaveLength(30); + expect(pages[2]).toHaveLength(10); + }); +}); + +describe('getPathDepth', () => { + it('should return 1 for empty string', () => { + expect(getPathDepth('')).toBe(1); + }); + + it('should return 2 for path of one nested folder', () => { + expect(getPathDepth('{{year}}/{{slug}}')).toBe(2); + }); +}); + +describe('filterByExtension', () => { + it('should return true when extension matches', () => { + expect(filterByExtension({ path: 'file.html.md' }, '.html.md')).toBe(true); + expect(filterByExtension({ path: 'file.html.md' }, 'html.md')).toBe(true); + }); + + it("should return false when extension doesn't match", () => { + expect(filterByExtension({ path: 'file.json' }, '.html.md')).toBe(false); + expect(filterByExtension({ path: 'file.json' }, 'html.md')).toBe(false); + }); +}); diff --git a/packages/netlify-cms-lib-util/src/__tests__/unsentRequest.spec.js b/packages/netlify-cms-lib-util/src/__tests__/unsentRequest.spec.js index eb6a5113..91138925 100644 --- a/packages/netlify-cms-lib-util/src/__tests__/unsentRequest.spec.js +++ b/packages/netlify-cms-lib-util/src/__tests__/unsentRequest.spec.js @@ -3,11 +3,10 @@ import unsentRequest from '../unsentRequest'; describe('unsentRequest', () => { describe('withHeaders', () => { it('should create new request with headers', () => { - expect( - unsentRequest - .withHeaders({ Authorization: 'token' })('path') - .toJS(), - ).toEqual({ url: 'path', headers: { Authorization: 'token' } }); + expect(unsentRequest.withHeaders({ Authorization: 'token' })('path').toJS()).toEqual({ + url: 'path', + headers: { Authorization: 'token' }, + }); }); it('should add headers to existing request', () => { diff --git a/packages/netlify-cms-lib-util/src/loadScript.js b/packages/netlify-cms-lib-util/src/loadScript.js index fded8722..95a62355 100644 --- a/packages/netlify-cms-lib-util/src/loadScript.js +++ b/packages/netlify-cms-lib-util/src/loadScript.js @@ -7,7 +7,7 @@ export default function loadScript(url) { const head = document.getElementsByTagName('head')[0]; const script = document.createElement('script'); script.src = url; - script.onload = script.onreadystatechange = function() { + script.onload = script.onreadystatechange = function () { if ( !done && (!this.readyState || this.readyState === 'loaded' || this.readyState === 'complete') diff --git a/packages/netlify-cms-lib-util/src/unsentRequest.js b/packages/netlify-cms-lib-util/src/unsentRequest.js index a877c437..1c077d6f 100644 --- a/packages/netlify-cms-lib-util/src/unsentRequest.js +++ b/packages/netlify-cms-lib-util/src/unsentRequest.js @@ -60,13 +60,7 @@ function toURL(req) { } function toFetchArguments(req) { - return [ - toURL(req), - req - .remove('url') - .remove('params') - .toJS(), - ]; + return [toURL(req), req.remove('url').remove('params').toJS()]; } function maybeRequestArg(req) { diff --git a/packages/netlify-cms-proxy-server/src/middlewares/joi/index.spec.ts b/packages/netlify-cms-proxy-server/src/middlewares/joi/index.spec.ts index 8b6e050a..4df54a0e 100644 --- a/packages/netlify-cms-proxy-server/src/middlewares/joi/index.spec.ts +++ b/packages/netlify-cms-proxy-server/src/middlewares/joi/index.spec.ts @@ -601,7 +601,7 @@ describe('joi', () => { } as express.Request; const json = jest.fn(); const status = jest.fn(() => ({ json })); - const res: express.Response = ({ status } as unknown) as express.Response; + const res: express.Response = { status } as unknown as express.Response; joi(defaultSchema())(req, res, next); diff --git a/packages/netlify-cms-proxy-server/src/middlewares/joi/index.ts b/packages/netlify-cms-proxy-server/src/middlewares/joi/index.ts index 90f38140..0b741ea5 100644 --- a/packages/netlify-cms-proxy-server/src/middlewares/joi/index.ts +++ b/packages/netlify-cms-proxy-server/src/middlewares/joi/index.ts @@ -132,9 +132,7 @@ export function defaultSchema({ path = requiredString } = {}) { cmsLabelPrefix: Joi.string().optional(), entry: dataFile, // entry is kept for backwards compatibility dataFiles: Joi.array().items(dataFile), - assets: Joi.array() - .items(asset) - .required(), + assets: Joi.array().items(asset).required(), options: Joi.object({ collectionName: Joi.string(), commitMessage: requiredString, @@ -207,10 +205,7 @@ export function defaultSchema({ path = requiredString } = {}) { is: 'deleteFiles', then: defaultParams .keys({ - paths: Joi.array() - .items(path) - .min(1) - .required(), + paths: Joi.array().items(path).min(1).required(), options: Joi.object({ commitMessage: requiredString, }).required(), diff --git a/packages/netlify-cms-proxy-server/src/middlewares/localFs/index.ts b/packages/netlify-cms-proxy-server/src/middlewares/localFs/index.ts index 7f2f3a9e..ff7ff5b8 100644 --- a/packages/netlify-cms-proxy-server/src/middlewares/localFs/index.ts +++ b/packages/netlify-cms-proxy-server/src/middlewares/localFs/index.ts @@ -24,7 +24,7 @@ type FsOptions = { }; export function localFsMiddleware({ repoPath, logger }: FsOptions) { - return async function(req: express.Request, res: express.Response) { + return async function (req: express.Request, res: express.Response) { try { const { body } = req; diff --git a/packages/netlify-cms-proxy-server/src/middlewares/localGit/index.spec.ts b/packages/netlify-cms-proxy-server/src/middlewares/localGit/index.spec.ts index 3f5701cf..93428d8e 100644 --- a/packages/netlify-cms-proxy-server/src/middlewares/localGit/index.spec.ts +++ b/packages/netlify-cms-proxy-server/src/middlewares/localGit/index.spec.ts @@ -123,7 +123,7 @@ describe('localGitMiddleware', () => { describe('localGitMiddleware', () => { const json = jest.fn(); const status = jest.fn(() => ({ json })); - const res: express.Response = ({ status } as unknown) as express.Response; + const res: express.Response = { status } as unknown as express.Response; const repoPath = '.'; diff --git a/packages/netlify-cms-proxy-server/src/middlewares/localGit/index.ts b/packages/netlify-cms-proxy-server/src/middlewares/localGit/index.ts index a03f7fdc..5d880f9b 100644 --- a/packages/netlify-cms-proxy-server/src/middlewares/localGit/index.ts +++ b/packages/netlify-cms-proxy-server/src/middlewares/localGit/index.ts @@ -173,7 +173,7 @@ export function localGitMiddleware({ repoPath, logger }: GitOptions) { // we can only perform a single git operation at any given time const mutex = withTimeout(new Mutex(), 3000, new Error('Request timed out')); - return async function(req: express.Request, res: express.Response) { + return async function (req: express.Request, res: express.Response) { let release; try { release = await mutex.acquire(); @@ -345,12 +345,8 @@ export function localGitMiddleware({ repoPath, logger }: GitOptions) { break; } case 'updateUnpublishedEntryStatus': { - const { - collection, - slug, - newStatus, - cmsLabelPrefix, - } = body.params as UpdateUnpublishedEntryStatusParams; + const { collection, slug, newStatus, cmsLabelPrefix } = + body.params as UpdateUnpublishedEntryStatusParams; const contentKey = generateContentKey(collection, slug); const cmsBranch = branchFromContentKey(contentKey); const description = statusToLabel(newStatus, cmsLabelPrefix || ''); diff --git a/packages/netlify-cms-proxy-server/src/middlewares/utils/entries.ts b/packages/netlify-cms-proxy-server/src/middlewares/utils/entries.ts index a839dc3c..be31ebc0 100644 --- a/packages/netlify-cms-proxy-server/src/middlewares/utils/entries.ts +++ b/packages/netlify-cms-proxy-server/src/middlewares/utils/entries.ts @@ -3,10 +3,7 @@ import path from 'path'; import { promises as fs } from 'fs'; function sha256(buffer: Buffer) { - return crypto - .createHash('sha256') - .update(buffer) - .digest('hex'); + return crypto.createHash('sha256').update(buffer).digest('hex'); } // normalize windows os path format diff --git a/packages/netlify-cms-widget-boolean/src/BooleanControl.js b/packages/netlify-cms-widget-boolean/src/BooleanControl.js index 67d9a12e..ed6e2e6d 100644 --- a/packages/netlify-cms-widget-boolean/src/BooleanControl.js +++ b/packages/netlify-cms-widget-boolean/src/BooleanControl.js @@ -17,14 +17,8 @@ function BooleanBackground({ isActive, ...props }) { export default class BooleanControl extends React.Component { render() { - const { - value, - forID, - onChange, - classNameWrapper, - setActiveStyle, - setInactiveStyle, - } = this.props; + const { value, forID, onChange, classNameWrapper, setActiveStyle, setInactiveStyle } = + this.props; return (