diff --git a/.editorconfig b/.editorconfig index 1fe32959..446b628c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,7 +4,6 @@ root = true [*] indent_style = space indent_size = 2 -end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 00000000..f134e4d0 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,22 @@ +{ + "parser": "babel-eslint", + "extends": [ + "eslint:recommended", + "plugin:react/recommended" + ], + "env": { + "es6": true, + "browser": true, + "node": true, + "jest": true + }, + "globals": { + "NETLIFY_CMS_VERSION": false, + "NETLIFY_CMS_CORE_VERSION": false, + "CMS_ENV": false + }, + "rules": { + "no-console": [0], + "react/prop-types": [1] + } +} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..2cfd2d58 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +dist/ +bin/ +CHANGELOG.md \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..9efa52f4 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "trailingComma": "all", + "singleQuote": true, + "printWidth": 100 +} diff --git a/.stylelintrc b/.stylelintrc new file mode 100644 index 00000000..f461fdd6 --- /dev/null +++ b/.stylelintrc @@ -0,0 +1,14 @@ +{ + "processors": ["stylelint-processor-styled-components"], + "extends": [ + "stylelint-config-recommended", + "stylelint-config-styled-components" + ], + "rules": { + "block-no-empty": null, + "no-duplicate-selectors": null, + "selector-type-no-unknown": [true, { + "ignoreTypes": ["$dummyValue"] + }] + } +} \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index c828c2af..0a073ee3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,10 +6,12 @@ cache: - $HOME/.yarn-cache - node_modules node_js: - - "8" - - "10" + - '8' + - '10' install: - yarn bootstrap +script: + - yarn test-ci notifications: email: false git: diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 2867c8a7..73b70570 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -14,21 +14,21 @@ orientation. Examples of behavior that contributes to creating a positive environment include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members Examples of unacceptable behavior by participants include: -* The use of sexualized language or imagery and unwelcome sexual attention or -advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic +- The use of sexualized language or imagery and unwelcome sexual attention or + advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a +- Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a2f33148..e15a2c72 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -95,6 +95,16 @@ Runs all the CMS package tests. yarn test ``` +### `format` + +Formats code and docs according to our style guidelines. + +#### Usage + +```sh +yarn format +``` + ## Pull Requests We actively welcome your pull requests. @@ -102,8 +112,8 @@ We actively welcome your pull requests. 1. Fork the repo and create your branch from `master`. 2. If you've added code that should be tested, add tests. 3. If you've changed APIs, update the documentation. -4. Ensure the test suite passes. -5. Make sure your code lints. +4. Run `yarn test` and ensure the test suite passes. +5. Use `yarn format` to format and lint your code. 6. PR's must be rebased before merge (feel free to ask for help) 7. PR should be reviewed by two maintainers (@erquhart, @Benaiah, @tech4him1) prior to merging. diff --git a/README.md b/README.md index 8902d0c9..ac3030b5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Netlify CMS + [![All Contributors](https://img.shields.io/badge/all_contributors-112-orange.svg)](#contributors) [![Open Source Helpers](https://www.codetriage.com/netlify/netlify-cms/badges/users.svg)](https://www.codetriage.com/netlify/netlify-cms) [![](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/netlify/netlifycms) @@ -24,9 +25,9 @@ Read more about Netlify CMS [Core Concepts](https://www.netlifycms.org/docs/intr The Netlify CMS can be used in two different ways. -* A Quick and easy install, that just requires you to create a single HTML file and a configuration file. All the CMS Javascript and CSS are loaded from a CDN. -To learn more about this installation method, refer to the [Quick Start Guide](https://www.netlifycms.org/docs/quick-start/) -* A complete, more complex install, that gives you more flexibility but requires that you use a static site builder with a build system that supports npm packages. +- A Quick and easy install, that just requires you to create a single HTML file and a configuration file. All the CMS Javascript and CSS are loaded from a CDN. + To learn more about this installation method, refer to the [Quick Start Guide](https://www.netlifycms.org/docs/quick-start/) +- A complete, more complex install, that gives you more flexibility but requires that you use a static site builder with a build system that supports npm packages. # Community @@ -45,7 +46,9 @@ Please make sure you understand its [implications and guarantees](https://writin # Thanks ## Services + These services support Netlify CMS development by providing free infrastructure. +

@@ -57,6 +60,7 @@ These services support Netlify CMS development by providing free infrastructure.

## Contributors + These wonderful folks are responsible for developing and maintaining Netlify CMS. ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)) @@ -79,6 +83,7 @@ These wonderful folks are responsible for developing and maintaining Netlify CMS | [
David Ko](https://github.com/daveyko)
[💻](https://github.com/netlify/netlify-cms/commits?author=daveyko "Code") | [
Iñaki García](http://www.txorua.com)
[🎨](#design-igarbla "Design") | [
Sam](https://github.com/gazebosx3)
[💻](https://github.com/netlify/netlify-cms/commits?author=gazebosx3 "Code") | [
Josh Dzielak](https://dzello.com)
[📖](https://github.com/netlify/netlify-cms/commits?author=dzello "Documentation") | [
Jeremy Bise](http://thosegeeks.com)
[📖](https://github.com/netlify/netlify-cms/commits?author=jeremybise "Documentation") | [
terrierscript](https://terrierscript.com)
[💻](https://github.com/netlify/netlify-cms/commits?author=terrierscript "Code") | [
Christopher Geary](https://twitter.com/crgeary)
[🔌](#plugin-crgeary "Plugin/utility libraries") | | [
Brian Macdonald](https://github.com/brianlmacdonald)
[💻](https://github.com/netlify/netlify-cms/commits?author=brianlmacdonald "Code") | [
John Vandenberg](https://jayvdb.github.io/)
[📖](https://github.com/netlify/netlify-cms/commits?author=jayvdb "Documentation") | [
MarkZither](https://github.com/MarkZither)
[📖](https://github.com/netlify/netlify-cms/commits?author=MarkZither "Documentation") | [
Rob Phoenix](https://www.robphoenix.com)
[📖](https://github.com/netlify/netlify-cms/commits?author=robphoenix "Documentation") | [
Steve Lathrop](https://www.SteLa.io)
[💻](https://github.com/netlify/netlify-cms/commits?author=slathrop "Code") [📖](https://github.com/netlify/netlify-cms/commits?author=slathrop "Documentation") [💡](#example-slathrop "Examples") | [
Maciej Matuszewski](https://github.com/maciejmatu)
[💻](https://github.com/netlify/netlify-cms/commits?author=maciejmatu "Code") | [
Eko Eryanto](https://github.com/ekoeryanto)
[🔌](#plugin-ekoeryanto "Plugin/utility libraries") | | [
Taylor D. Edmiston](http://blog.tedmiston.com/)
[📖](https://github.com/netlify/netlify-cms/commits?author=tedmiston "Documentation") | [
Daniel Mahon](https://www.mahonstudios.com)
[💻](https://github.com/netlify/netlify-cms/commits?author=danielmahon "Code") | [
Evan Hennessy](https://www.hennessyevan.com)
[🔌](#plugin-hennessyevan "Plugin/utility libraries") | [
Hasan Azizul Haque](https://hasanavi.me)
[💻](https://github.com/netlify/netlify-cms/commits?author=hasanavi "Code") [📖](https://github.com/netlify/netlify-cms/commits?author=hasanavi "Documentation") [🤔](#ideas-hasanavi "Ideas, Planning, & Feedback") | [
Robert Karlsson](https://github.com/robertkarlsson)
[🐛](https://github.com/netlify/netlify-cms/issues?q=author%3Arobertkarlsson "Bug reports") | [
Gil Greenberg](http://gilgreenberg.com)
[💻](https://github.com/netlify/netlify-cms/commits?author=gil-- "Code") | [
Tyler Ipson](http://loremipson.com)
[📖](https://github.com/netlify/netlify-cms/commits?author=loremipson "Documentation") | + This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome! diff --git a/babel.config.js b/babel.config.js index 7f15a2ed..8a5a35ea 100644 --- a/babel.config.js +++ b/babel.config.js @@ -3,25 +3,28 @@ const isTest = process.env.NODE_ENV === 'test'; const presets = () => { if (isTest) { - return [ - '@babel/preset-react', - '@babel/preset-env', - ]; + return ['@babel/preset-react', '@babel/preset-env']; } return [ '@babel/preset-react', - ['@babel/preset-env', { - modules: false, - }], + [ + '@babel/preset-env', + { + modules: false, + }, + ], ]; }; const plugins = () => { const defaultPlugins = [ 'lodash', - ['babel-plugin-transform-builtin-extend', { - globals: ['Error'] - }], + [ + 'babel-plugin-transform-builtin-extend', + { + globals: ['Error'], + }, + ], 'transform-export-extensions', '@babel/plugin-proposal-class-properties', '@babel/plugin-proposal-object-rest-spread', @@ -29,35 +32,39 @@ const plugins = () => { ]; if (isProduction) { - return [ - ...defaultPlugins, - ['emotion', { hoist: true }], - ]; + return [...defaultPlugins, ['emotion', { hoist: true }]]; } if (isTest) { return [ ...defaultPlugins, - ['inline-svg', { - svgo: { - plugins: [ - {removeViewBox: false}, - ], + [ + 'inline-svg', + { + svgo: { + plugins: [{ removeViewBox: false }], + }, }, - }], - ['emotion', { - sourceMap: true, - autoLabel: true, - }], + ], + [ + 'emotion', + { + sourceMap: true, + autoLabel: true, + }, + ], ]; } return [ ...defaultPlugins, - ['emotion', { - sourceMap: true, - autoLabel: true, - }], + [ + 'emotion', + { + sourceMap: true, + autoLabel: true, + }, + ], ]; }; diff --git a/custom-preprocessor.js b/custom-preprocessor.js index eb66c39e..43a27e2a 100644 --- a/custom-preprocessor.js +++ b/custom-preprocessor.js @@ -1,4 +1,4 @@ -const babelJest = require('babel-jest') +const babelJest = require('babel-jest'); const babelConfig = require('./packages/netlify-cms-core/babel.config.js'); -module.exports = babelJest.createTransformer(babelConfig) +module.exports = babelJest.createTransformer(babelConfig); diff --git a/jest.config.js b/jest.config.js index 837b2d48..fe43e915 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,11 +1,11 @@ module.exports = { - setupTestFrameworkScriptFile: "/setupTestFramework.js", + setupTestFrameworkScriptFile: '/setupTestFramework.js', transform: { - "\\.js$": "/custom-preprocessor.js", + '\\.js$': '/custom-preprocessor.js', }, moduleNameMapper: { - "netlify-cms-lib-util": "/packages/netlify-cms-lib-util/src/index.js", - "netlify-cms-ui-default": "/packages/netlify-cms-ui-default/src/index.js", + 'netlify-cms-lib-util': '/packages/netlify-cms-lib-util/src/index.js', + 'netlify-cms-ui-default': '/packages/netlify-cms-ui-default/src/index.js', }, - testEnvironment: "node", + testEnvironment: 'node', }; diff --git a/lerna.json b/lerna.json index 0253d3dc..017e9dc2 100644 --- a/lerna.json +++ b/lerna.json @@ -1,8 +1,6 @@ { "lerna": "2.11.0", - "packages": [ - "packages/*" - ], + "packages": ["packages/*"], "version": "independent", "npmClient": "yarn", "useWorkspaces": true, @@ -11,9 +9,7 @@ "publish": { "allowBranch": "master", "conventionalCommits": true, - "ignoreChanges": [ - "CHANGELOG.md" - ] + "ignoreChanges": ["CHANGELOG.md"] } } } diff --git a/package.json b/package.json index 5977e7c6..f46eb792 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "reset": "npm run clean && lerna clean --yes", "cache-ci": "node scripts/cache.js", "test": "run-s jest e2e", - "test-ci": "run-s cache-ci jest e2e-ci", + "test-ci": "run-s cache-ci jest e2e-ci lint-quiet", "jest": "cross-env NODE_ENV=test jest --no-cache", "e2e-prep": "npm run build && cp -r packages/netlify-cms/dist dev-test/", "e2e-serve": "http-server dev-test", @@ -24,6 +24,13 @@ "e2e-dev": "start-test develop 8080 e2e-exec-dev", "dryrun": "lerna publish --skip-npm --skip-git", "publish": "run-s bootstrap dryrun build && git checkout . && lerna publish", + "format": "run-s \"lint:css -- --fix --quiet\" \"lint:js -- --fix --quiet\" \"format:prettier -- --write\"", + "format:prettier": "prettier \"{{packages,scripts,website}/**/,}*.{js,css,md,json,yml,yaml}\"", + "lint": "run-p -c --aggregate-output lint:*", + "lint-quiet": "run-p -c --aggregate-output \"lint:* -- --quiet\"", + "lint:css": "stylelint --ignore-path .gitignore \"{packages/**/*.{css,js},website/**/*.css}\"", + "lint:js": "eslint --ignore-path .gitignore \"{{packages,scripts,website}/**/,}*.js\"", + "lint:format": "npm run format:prettier -- --list-different", "add-contributor": "all-contributors add" }, "browserslist": [ @@ -45,6 +52,7 @@ "@babel/preset-react": "^7.0.0-beta.54", "all-contributors-cli": "^4.4.0", "babel-core": "^7.0.0-bridge.0", + "babel-eslint": "^9.0.0-beta.3", "babel-jest": "^23.4.0", "babel-loader": "^8.0.0-beta", "babel-plugin-emotion": "^9.2.4", @@ -60,6 +68,8 @@ "deep-equal": "^1.0.1", "enzyme": "^3.1.0", "enzyme-adapter-react-16": "^1.0.2", + "eslint": "^5.3.0", + "eslint-plugin-react": "^7.10.0", "friendly-errors-webpack-plugin": "^1.7.0", "http-server": "^0.11.1", "jest": "^23.4.0", @@ -67,11 +77,16 @@ "jest-emotion": "^9.2.7", "lerna": "^2.11.0", "npm-run-all": "^4.1.3", + "prettier": "1.14.0", "raf": "^3.4.0", "react-test-renderer": "^16.0.0", "rimraf": "^2.6.2", "start-server-and-test": "^1.7.0", "style-loader": "^0.21.0", + "stylelint": "^9.4.0", + "stylelint-config-recommended": "^2.1.0", + "stylelint-config-styled-components": "^0.1.1", + "stylelint-processor-styled-components": "^1.3.1", "svg-inline-loader": "^0.8.0" }, "workspaces": [ diff --git a/packages/netlify-cms-backend-bitbucket/src/API.js b/packages/netlify-cms-backend-bitbucket/src/API.js index a039ef6a..1fecaa67 100644 --- a/packages/netlify-cms-backend-bitbucket/src/API.js +++ b/packages/netlify-cms-backend-bitbucket/src/API.js @@ -1,4 +1,4 @@ -import { flow, get } from "lodash"; +import { flow, get } from 'lodash'; import { localForage, unsentRequest, @@ -6,48 +6,49 @@ import { then, basename, Cursor, - APIError -} from "netlify-cms-lib-util"; + APIError, +} from 'netlify-cms-lib-util'; export default class API { constructor(config) { - this.api_root = config.api_root || "https://api.bitbucket.org/2.0"; - this.branch = config.branch || "master"; - this.repo = config.repo || ""; + this.api_root = config.api_root || 'https://api.bitbucket.org/2.0'; + this.branch = config.branch || 'master'; + this.repo = config.repo || ''; this.requestFunction = config.requestFunction || unsentRequest.performRequest; // Allow overriding this.hasWriteAccess this.hasWriteAccess = config.hasWriteAccess || this.hasWriteAccess; - this.repoURL = this.repo ? `/repositories/${ this.repo }` : ""; + this.repoURL = this.repo ? `/repositories/${this.repo}` : ''; } - buildRequest = req => flow([ - unsentRequest.withRoot(this.api_root), - unsentRequest.withTimestamp, - ])(req); + buildRequest = req => + flow([unsentRequest.withRoot(this.api_root), unsentRequest.withTimestamp])(req); - request = req => flow([ - this.buildRequest, - this.requestFunction, - p => p.catch(err => Promise.reject(new APIError(err.message, null, "BitBucket"))), - ])(req); + request = req => + flow([ + this.buildRequest, + this.requestFunction, + p => p.catch(err => Promise.reject(new APIError(err.message, null, 'BitBucket'))), + ])(req); - requestJSON = req => flow([ - unsentRequest.withDefaultHeaders({ "Content-Type": "application/json" }), - this.request, - then(responseParser({ format: "json" })), - p => p.catch(err => Promise.reject(new APIError(err.message, null, "BitBucket"))), - ])(req); - requestText = req => flow([ - unsentRequest.withDefaultHeaders({ "Content-Type": "text/plain" }), - this.request, - then(responseParser({ format: "text" })), - p => p.catch(err => Promise.reject(new APIError(err.message, null, "BitBucket"))), - ])(req); + requestJSON = req => + flow([ + unsentRequest.withDefaultHeaders({ 'Content-Type': 'application/json' }), + this.request, + then(responseParser({ format: 'json' })), + p => p.catch(err => Promise.reject(new APIError(err.message, null, 'BitBucket'))), + ])(req); + requestText = req => + flow([ + unsentRequest.withDefaultHeaders({ 'Content-Type': 'text/plain' }), + this.request, + then(responseParser({ format: 'text' })), + p => p.catch(err => Promise.reject(new APIError(err.message, null, 'BitBucket'))), + ])(req); - user = () => this.request("/user"); - hasWriteAccess = user => this.request(this.repoURL).then(res => res.ok); + user = () => this.request('/user'); + hasWriteAccess = () => this.request(this.repoURL).then(res => res.ok); - isFile = ({ type }) => type === "commit_file"; + isFile = ({ type }) => type === 'commit_file'; processFile = file => ({ ...file, name: basename(file.path), @@ -59,59 +60,75 @@ export default class API { // that will help with caching (though not as well as a normal // SHA, since it will change even if the individual file itself // doesn't.) - ...(file.commit && file.commit.hash - ? { id: `${ file.commit.hash }/${ file.path }` } - : {}), + ...(file.commit && file.commit.hash ? { id: `${file.commit.hash}/${file.path}` } : {}), }); processFiles = files => files.filter(this.isFile).map(this.processFile); readFile = async (path, sha, { ref = this.branch, parseText = true } = {}) => { - const cacheKey = parseText ? `bb.${ sha }` : `bb.${ sha }.blob`; + const cacheKey = parseText ? `bb.${sha}` : `bb.${sha}.blob`; const cachedFile = sha ? await localForage.getItem(cacheKey) : null; - if (cachedFile) { return cachedFile; } + if (cachedFile) { + return cachedFile; + } const result = await this.request({ - url: `${ this.repoURL }/src/${ ref }/${ path }`, - cache: "no-store", - }).then(parseText ? responseParser({ format: "text" }) : responseParser({ format: "blob" })); - if (sha) { localForage.setItem(cacheKey, result); } + url: `${this.repoURL}/src/${ref}/${path}`, + cache: 'no-store', + }).then(parseText ? responseParser({ format: 'text' }) : responseParser({ format: 'blob' })); + if (sha) { + localForage.setItem(cacheKey, result); + } return result; - } + }; getEntriesAndCursor = jsonResponse => { - const { size: count, page: index, pagelen: pageSize, next, previous: prev, values: entries } = jsonResponse; - const pageCount = (pageSize && count) ? Math.ceil(count / pageSize) : undefined; + const { + size: count, + page: index, + pagelen: pageSize, + next, + previous: prev, + values: entries, + } = jsonResponse; + const pageCount = pageSize && count ? Math.ceil(count / pageSize) : undefined; return { entries, cursor: Cursor.create({ - actions: [...(next ? ["next"] : []), ...(prev ? ["prev"] : [])], + actions: [...(next ? ['next'] : []), ...(prev ? ['prev'] : [])], meta: { index, count, pageSize, pageCount }, data: { links: { next, prev } }, }), }; - } + }; listFiles = async path => { const { entries, cursor } = await flow([ // sort files by filename ascending - unsentRequest.withParams({ sort: "-path" }), + unsentRequest.withParams({ sort: '-path' }), this.requestJSON, then(this.getEntriesAndCursor), - ])(`${ this.repoURL }/src/${ this.branch }/${ path }`); + ])(`${this.repoURL}/src/${this.branch}/${path}`); return { entries: this.processFiles(entries), cursor }; - } + }; - traverseCursor = async (cursor, action) => flow([ - this.requestJSON, - then(this.getEntriesAndCursor), - then(({ cursor: newCursor, entries }) => ({ cursor: newCursor, entries: this.processFiles(entries) })), - ])(cursor.data.getIn(["links", action])); + traverseCursor = async (cursor, action) => + flow([ + this.requestJSON, + then(this.getEntriesAndCursor), + then(({ cursor: newCursor, entries }) => ({ + cursor: newCursor, + entries: this.processFiles(entries), + })), + ])(cursor.data.getIn(['links', action])); listAllFiles = async path => { const { cursor: initialCursor, entries: initialEntries } = await this.listFiles(path); const entries = [...initialEntries]; let currentCursor = initialCursor; - while (currentCursor && currentCursor.actions.has("next")) { - const { cursor: newCursor, entries: newEntries } = await this.traverseCursor(currentCursor, "next"); + while (currentCursor && currentCursor.actions.has('next')) { + const { cursor: newCursor, entries: newEntries } = await this.traverseCursor( + currentCursor, + 'next', + ); entries.push(...newEntries); currentCursor = newCursor; } @@ -125,40 +142,41 @@ export default class API { formData.append(item.path, contentBlob, basename(item.path)); formData.append('branch', branch); if (commitMessage) { - formData.append("message", commitMessage); + formData.append('message', commitMessage); } if (this.commitAuthor) { const { name, email } = this.commitAuthor; - formData.append("author", `${name} <${email}>`); + formData.append('author', `${name} <${email}>`); } return flow([ - unsentRequest.withMethod("POST"), + unsentRequest.withMethod('POST'), unsentRequest.withBody(formData), this.request, then(() => ({ ...item, uploaded: true })), - ])(`${ this.repoURL }/src`); + ])(`${this.repoURL}/src`); }; - persistFiles = (files, { commitMessage }) => Promise.all( - files.filter(({ uploaded }) => !uploaded).map(file => this.uploadBlob(file, { commitMessage })) - ); + persistFiles = (files, { commitMessage }) => + Promise.all( + files + .filter(({ uploaded }) => !uploaded) + .map(file => this.uploadBlob(file, { commitMessage })), + ); deleteFile = (path, message, { branch = this.branch } = {}) => { const body = new FormData(); body.append('files', path); body.append('branch', branch); if (message) { - body.append("message", message); + body.append('message', message); } if (this.commitAuthor) { const { name, email } = this.commitAuthor; - body.append("author", `${name} <${email}>`); + body.append('author', `${name} <${email}>`); } - return flow([ - unsentRequest.withMethod("POST"), - unsentRequest.withBody(body), - this.request, - ])(`${ this.repoURL }/src`); + return flow([unsentRequest.withMethod('POST'), unsentRequest.withBody(body), this.request])( + `${this.repoURL}/src`, + ); }; } diff --git a/packages/netlify-cms-backend-bitbucket/src/AuthenticationPage.js b/packages/netlify-cms-backend-bitbucket/src/AuthenticationPage.js index 87679022..d14d5113 100644 --- a/packages/netlify-cms-backend-bitbucket/src/AuthenticationPage.js +++ b/packages/netlify-cms-backend-bitbucket/src/AuthenticationPage.js @@ -6,7 +6,7 @@ import { AuthenticationPage, Icon } from 'netlify-cms-ui-default'; const LoginButtonIcon = styled(Icon)` margin-right: 18px; -` +`; export default class BitbucketAuthenticationPage extends React.Component { static propTypes = { @@ -16,11 +16,14 @@ export default class BitbucketAuthenticationPage extends React.Component { state = {}; - handleLogin = (e) => { + handleLogin = e => { e.preventDefault(); const cfg = { base_url: this.props.base_url, - site_id: (document.location.host.split(':')[0] === 'localhost') ? 'cms.netlify.com' : this.props.site_id, + site_id: + document.location.host.split(':')[0] === 'localhost' + ? 'cms.netlify.com' + : this.props.site_id, auth_endpoint: this.props.authEndpoint, }; const auth = new NetlifyAuthenticator(cfg); @@ -44,8 +47,8 @@ export default class BitbucketAuthenticationPage extends React.Component { loginErrorMessage={this.state.loginError} renderButtonContent={() => ( - - {inProgress ? "Logging in..." : "Login with Bitbucket"} + + {inProgress ? 'Logging in...' : 'Login with Bitbucket'} )} /> diff --git a/packages/netlify-cms-backend-bitbucket/src/implementation.js b/packages/netlify-cms-backend-bitbucket/src/implementation.js index 57d84266..80068f6d 100644 --- a/packages/netlify-cms-backend-bitbucket/src/implementation.js +++ b/packages/netlify-cms-backend-bitbucket/src/implementation.js @@ -1,21 +1,21 @@ -import semaphore from "semaphore"; -import { flow, trimStart } from "lodash"; +import semaphore from 'semaphore'; +import { flow, trimStart } from 'lodash'; import { CURSOR_COMPATIBILITY_SYMBOL, filterByPropExtension, resolvePromiseProperties, then, unsentRequest, -} from "netlify-cms-lib-util"; +} from 'netlify-cms-lib-util'; import { NetlifyAuthenticator } from 'netlify-cms-lib-auth'; -import AuthenticationPage from "./AuthenticationPage"; -import API from "./API"; +import AuthenticationPage from './AuthenticationPage'; +import API from './API'; const MAX_CONCURRENT_DOWNLOADS = 10; // Implementation wrapper class export default class Bitbucket { - constructor(config, options={}) { + constructor(config, options = {}) { this.config = config; this.options = { proxied: false, @@ -25,23 +25,23 @@ export default class Bitbucket { }; if (this.options.useWorkflow) { - throw new Error("The BitBucket backend does not support the Editorial Workflow."); + throw new Error('The BitBucket backend does not support the Editorial Workflow.'); } - if (!this.options.proxied && !config.getIn(["backend", "repo"], false)) { - throw new Error("The BitBucket backend needs a \"repo\" in the backend configuration."); + if (!this.options.proxied && !config.getIn(['backend', 'repo'], false)) { + throw new Error('The BitBucket backend needs a "repo" in the backend configuration.'); } this.api = this.options.API || null; this.updateUserCredentials = this.options.updateUserCredentials; - this.repo = config.getIn(["backend", "repo"], ""); - this.branch = config.getIn(["backend", "branch"], "master"); - this.api_root = config.getIn(["backend", "api_root"], "https://api.bitbucket.org/2.0"); - this.base_url = config.get("base_url"); - this.site_id = config.get("site_id"); - this.token = ""; + this.repo = config.getIn(['backend', 'repo'], ''); + this.branch = config.getIn(['backend', 'branch'], 'master'); + this.api_root = config.getIn(['backend', 'api_root'], 'https://api.bitbucket.org/2.0'); + this.base_url = config.get('base_url'); + this.site_id = config.get('site_id'); + this.token = ''; } authComponent() { @@ -50,7 +50,11 @@ export default class Bitbucket { setUser(user) { this.token = user.token; - this.api = new API({ requestFunction: this.apiRequestFunction, branch: this.branch, repo: this.repo }); + this.api = new API({ + requestFunction: this.apiRequestFunction, + branch: this.branch, + repo: this.repo, + }); } restoreUser(user) { @@ -60,13 +64,19 @@ export default class Bitbucket { authenticate(state) { this.token = state.token; this.refreshToken = state.refresh_token; - this.api = new API({ requestFunction: this.apiRequestFunction, branch: this.branch, repo: this.repo, api_root: this.api_root }); + this.api = new API({ + requestFunction: this.apiRequestFunction, + branch: this.branch, + repo: this.repo, + api_root: this.api_root, + }); return this.api.user().then(user => this.api.hasWriteAccess(user).then(isCollab => { - if (!isCollab) throw new Error("Your BitBucker user account does not have access to this repo."); + if (!isCollab) + throw new Error('Your BitBucker user account does not have access to this repo.'); return Object.assign({}, user, { token: state.token, refresh_token: state.refresh_token }); - }) + }), ); } @@ -84,7 +94,8 @@ export default class Bitbucket { this.authenticator = new NetlifyAuthenticator(cfg); } - this.refreshedTokenPromise = this.authenticator.refresh({ provider: "bitbucket", refresh_token: this.refreshToken }) + this.refreshedTokenPromise = this.authenticator + .refresh({ provider: 'bitbucket', refresh_token: this.refreshToken }) .then(({ token, refresh_token }) => { this.token = token; this.refreshToken = refresh_token; @@ -112,14 +123,17 @@ export default class Bitbucket { apiRequestFunction = async req => { const token = this.refreshedTokenPromise ? await this.refreshedTokenPromise : this.token; return flow([ - unsentRequest.withHeaders({ Authorization: `Bearer ${ token }` }), + unsentRequest.withHeaders({ Authorization: `Bearer ${token}` }), unsentRequest.performRequest, then(async res => { if (res.status === 401) { - const json = (await res.json().catch(() => null)); - if (json && json.type === "error" && /^access token expired/i.test(json.error.message)) { + const json = await res.json().catch(() => null); + if (json && json.type === 'error' && /^access token expired/i.test(json.error.message)) { const newToken = await this.getRefreshedAccessToken(); - const reqWithNewToken = unsentRequest.withHeaders({ Authorization: `Bearer ${ newToken }` }, req); + const reqWithNewToken = unsentRequest.withHeaders( + { Authorization: `Bearer ${newToken}` }, + req, + ); return unsentRequest.performRequest(reqWithNewToken); } } @@ -129,11 +143,11 @@ export default class Bitbucket { }; entriesByFolder(collection, extension) { - const listPromise = this.api.listFiles(collection.get("folder")); + const listPromise = this.api.listFiles(collection.get('folder')); return resolvePromiseProperties({ files: listPromise .then(({ entries }) => entries) - .then(filterByPropExtension(extension, "path")) + .then(filterByPropExtension(extension, 'path')) .then(this.fetchFiles), cursor: listPromise.then(({ cursor }) => cursor), }).then(({ files, cursor }) => { @@ -143,37 +157,46 @@ export default class Bitbucket { } allEntriesByFolder(collection, extension) { - return this.api.listAllFiles(collection.get("folder")) - .then(filterByPropExtension(extension, "path")) + return this.api + .listAllFiles(collection.get('folder')) + .then(filterByPropExtension(extension, 'path')) .then(this.fetchFiles); } entriesByFiles(collection) { - const files = collection.get("files").map(collectionFile => ({ - path: collectionFile.get("file"), - label: collectionFile.get("label"), + const files = collection.get('files').map(collectionFile => ({ + path: collectionFile.get('file'), + label: collectionFile.get('label'), })); return this.fetchFiles(files); } - fetchFiles = (files) => { + fetchFiles = files => { const sem = semaphore(MAX_CONCURRENT_DOWNLOADS); const promises = []; - files.forEach((file) => { - promises.push(new Promise(resolve => ( - sem.take(() => this.api.readFile(file.path, file.id).then((data) => { - resolve({ file, data }); - sem.leave(); - }).catch((error = true) => { - sem.leave(); - console.error(`failed to load file from BitBucket: ${ file.path }`); - resolve({ error }); - })) - ))); + files.forEach(file => { + promises.push( + new Promise(resolve => + sem.take(() => + this.api + .readFile(file.path, file.id) + .then(data => { + resolve({ file, data }); + sem.leave(); + }) + .catch((error = true) => { + sem.leave(); + console.error(`failed to load file from BitBucket: ${file.path}`); + resolve({ error }); + }), + ), + ), + ); }); - return Promise.all(promises) - .then(loadedEntries => loadedEntries.filter(loadedEntry => !loadedEntry.error)); - } + return Promise.all(promises).then(loadedEntries => + loadedEntries.filter(loadedEntry => !loadedEntry.error), + ); + }; getEntry(collection, slug, path) { return this.api.readFile(path).then(data => ({ @@ -185,18 +208,21 @@ export default class Bitbucket { getMedia() { const sem = semaphore(MAX_CONCURRENT_DOWNLOADS); - return this.api.listAllFiles(this.config.get("media_folder")) - .then(files => files.map(({ id, name, download_url, path }) => { - const getBlobPromise = () => new Promise((resolve, reject) => - sem.take(() => - this.api.readFile(path, id, { parseText: false }) - .then(resolve, reject) - .finally(() => sem.leave()) - ) - ); + return this.api.listAllFiles(this.config.get('media_folder')).then(files => + files.map(({ id, name, download_url, path }) => { + const getBlobPromise = () => + new Promise((resolve, reject) => + sem.take(() => + this.api + .readFile(path, id, { parseText: false }) + .then(resolve, reject) + .finally(() => sem.leave()), + ), + ); return { id, name, getBlobPromise, url: download_url, path }; - })); + }), + ); } persistEntry(entry, mediaFiles, options = {}) { @@ -215,10 +241,11 @@ export default class Bitbucket { } traverseCursor(cursor, action) { - return this.api.traverseCursor(cursor, action) - .then(async ({ entries, cursor: newCursor }) => ({ - entries: await Promise.all(entries.map(file => this.api.readFile(file.path, file.id).then(data => ({ file, data })))), - cursor: newCursor, - })); + return this.api.traverseCursor(cursor, action).then(async ({ entries, cursor: newCursor }) => ({ + entries: await Promise.all( + entries.map(file => this.api.readFile(file.path, file.id).then(data => ({ file, data }))), + ), + cursor: newCursor, + })); } } diff --git a/packages/netlify-cms-backend-bitbucket/src/index.js b/packages/netlify-cms-backend-bitbucket/src/index.js index fdad0123..d5bebfa2 100644 --- a/packages/netlify-cms-backend-bitbucket/src/index.js +++ b/packages/netlify-cms-backend-bitbucket/src/index.js @@ -1,4 +1,3 @@ export BitbucketBackend from './implementation'; export API from './API'; export AuthenticationPage from './AuthenticationPage'; - diff --git a/packages/netlify-cms-backend-git-gateway/src/AuthenticationPage.js b/packages/netlify-cms-backend-git-gateway/src/AuthenticationPage.js index 2ec2b4de..be967c6f 100644 --- a/packages/netlify-cms-backend-git-gateway/src/AuthenticationPage.js +++ b/packages/netlify-cms-backend-git-gateway/src/AuthenticationPage.js @@ -8,7 +8,7 @@ import { shadows, colors, colorsRaw, - lengths + lengths, } from 'netlify-cms-ui-default'; const LoginButton = styled.button` @@ -21,12 +21,12 @@ const LoginButton = styled.button` display: block; margin-top: 20px; margin-left: auto; -` +`; const AuthForm = styled.form` width: 350px; margin-top: -80px; -` +`; const AuthInput = styled.input` background-color: ${colorsRaw.white}; @@ -44,16 +44,16 @@ const AuthInput = styled.input` outline: none; box-shadow: inset 0 0 0 2px ${colors.active}; } -` +`; const ErrorMessage = styled.p` color: ${colors.errorText}; -` +`; let component = null; if (window.netlifyIdentity) { - window.netlifyIdentity.on('login', (user) => { + window.netlifyIdentity.on('login', user => { component && component.handleIdentityLogin(user); }); window.netlifyIdentity.on('logout', () => { @@ -78,14 +78,14 @@ export default class GitGatewayAuthenticationPage extends React.Component { component = null; } - handleIdentityLogin = (user) => { + handleIdentityLogin = user => { this.props.onLogin(user); window.netlifyIdentity.close(); - } + }; handleIdentityLogout = () => { window.netlifyIdentity.open(); - } + }; handleIdentity = () => { const user = window.netlifyIdentity.currentUser(); @@ -94,20 +94,20 @@ export default class GitGatewayAuthenticationPage extends React.Component { } else { window.netlifyIdentity.open(); } - } + }; static propTypes = { onLogin: PropTypes.func.isRequired, inProgress: PropTypes.bool.isRequired, }; - state = { email: "", password: "", errors: {} }; + state = { email: '', password: '', errors: {} }; handleChange = (name, e) => { this.setState({ ...this.state, [name]: e.target.value }); }; - handleLogin = (e) => { + handleLogin = e => { e.preventDefault(); const { email, password } = this.state; @@ -124,13 +124,17 @@ export default class GitGatewayAuthenticationPage extends React.Component { return; } - AuthenticationPage.authClient.login(this.state.email, this.state.password, true) - .then((user) => { - this.props.onLogin(user); - }) - .catch((error) => { - this.setState({ errors: { server: error.description || error.msg || error }, loggingIn: false }); - }); + AuthenticationPage.authClient + .login(this.state.email, this.state.password, true) + .then(user => { + this.props.onLogin(user); + }) + .catch(error => { + this.setState({ + errors: { server: error.description || error.msg || error }, + loggingIn: false, + }); + }); }; render() { @@ -147,29 +151,33 @@ export default class GitGatewayAuthenticationPage extends React.Component { } return ( - ( - - {!error ? null : {error}} - {!errors.server ? null : {errors.server}} - {errors.email || null} - - {errors.password || null} - - {inProgress ? 'Logging in...' : 'Login'} - - )}/> + ( + + {!error ? null : {error}} + {!errors.server ? null : {errors.server}} + {errors.email || null} + + {errors.password || null} + + + {inProgress ? 'Logging in...' : 'Login'} + + + )} + /> ); } } diff --git a/packages/netlify-cms-backend-git-gateway/src/GitHubAPI.js b/packages/netlify-cms-backend-git-gateway/src/GitHubAPI.js index a1f93e06..eeb5b405 100644 --- a/packages/netlify-cms-backend-git-gateway/src/GitHubAPI.js +++ b/packages/netlify-cms-backend-git-gateway/src/GitHubAPI.js @@ -1,5 +1,5 @@ -import { API as GithubAPI } from "netlify-cms-backend-github"; -import { APIError } from "netlify-cms-lib-util"; +import { API as GithubAPI } from 'netlify-cms-backend-github'; +import { APIError } from 'netlify-cms-lib-util'; export default class API extends GithubAPI { constructor(config) { @@ -7,7 +7,7 @@ export default class API extends GithubAPI { this.api_root = config.api_root; this.tokenPromise = config.tokenPromise; this.commitAuthor = config.commitAuthor; - this.repoURL = ""; + this.repoURL = ''; } hasWriteAccess() { @@ -15,26 +15,36 @@ export default class API extends GithubAPI { .then(() => true) .catch(error => { if (error.status === 401) { - if (error.message === "Bad credentials") { - throw new APIError("Git Gateway Error: Please ask your site administrator to reissue the Git Gateway token.", error.status, 'Git Gateway'); + if (error.message === 'Bad credentials') { + throw new APIError( + 'Git Gateway Error: Please ask your site administrator to reissue the Git Gateway token.', + error.status, + 'Git Gateway', + ); } else { return false; } - } else if (error.status === 404 && (error.message === undefined || error.message === "Unable to locate site configuration")) { - throw new APIError(`Git Gateway Error: Please make sure Git Gateway is enabled on your site.`, error.status, 'Git Gateway'); + } else if ( + error.status === 404 && + (error.message === undefined || error.message === 'Unable to locate site configuration') + ) { + throw new APIError( + `Git Gateway Error: Please make sure Git Gateway is enabled on your site.`, + error.status, + 'Git Gateway', + ); } else { - console.error("Problem fetching repo data from Git Gateway"); + console.error('Problem fetching repo data from Git Gateway'); throw error; } }); } getRequestHeaders(headers = {}) { - return this.tokenPromise() - .then((jwtToken) => { + return this.tokenPromise().then(jwtToken => { const baseHeader = { - "Authorization": `Bearer ${ jwtToken }`, - "Content-Type": "application/json", + Authorization: `Bearer ${jwtToken}`, + 'Content-Type': 'application/json', ...headers, }; @@ -42,17 +52,16 @@ export default class API extends GithubAPI { }); } - urlFor(path, options) { const cacheBuster = new Date().getTime(); - const params = [`ts=${ cacheBuster }`]; + const params = [`ts=${cacheBuster}`]; if (options.params) { for (const key in options.params) { - params.push(`${ key }=${ encodeURIComponent(options.params[key]) }`); + params.push(`${key}=${encodeURIComponent(options.params[key])}`); } } if (params.length) { - path += `?${ params.join("&") }`; + path += `?${params.join('&')}`; } return this.api_root + path; } @@ -65,22 +74,22 @@ export default class API extends GithubAPI { const url = this.urlFor(path, options); let responseStatus; return this.getRequestHeaders(options.headers || {}) - .then(headers => fetch(url, { ...options, headers })) - .then((response) => { - responseStatus = response.status; - const contentType = response.headers.get("Content-Type"); - if (contentType && contentType.match(/json/)) { - return this.parseJsonResponse(response); - } - const text = response.text(); - if (!response.ok) { - return Promise.reject(text); - } - return text; - }) - .catch(error => { - throw new APIError((error.message || error.msg), responseStatus, 'Git Gateway'); - }); + .then(headers => fetch(url, { ...options, headers })) + .then(response => { + responseStatus = response.status; + const contentType = response.headers.get('Content-Type'); + if (contentType && contentType.match(/json/)) { + return this.parseJsonResponse(response); + } + const text = response.text(); + if (!response.ok) { + return Promise.reject(text); + } + return text; + }) + .catch(error => { + throw new APIError(error.message || error.msg, responseStatus, 'Git Gateway'); + }); } commit(message, changeTree) { @@ -97,10 +106,9 @@ export default class API extends GithubAPI { }; } - return this.request("/git/commits", { - method: "POST", + return this.request('/git/commits', { + method: 'POST', body: JSON.stringify(commitParams), }); } - } diff --git a/packages/netlify-cms-backend-git-gateway/src/GitLabAPI.js b/packages/netlify-cms-backend-git-gateway/src/GitLabAPI.js index 64978645..d521f4ef 100644 --- a/packages/netlify-cms-backend-git-gateway/src/GitLabAPI.js +++ b/packages/netlify-cms-backend-git-gateway/src/GitLabAPI.js @@ -1,24 +1,25 @@ -import { flow } from "lodash"; -import { API as GitlabAPI } from "netlify-cms-backend-gitlab"; -import { unsentRequest, then } from "netlify-cms-lib-util"; +import { flow } from 'lodash'; +import { API as GitlabAPI } from 'netlify-cms-backend-gitlab'; +import { unsentRequest, then } from 'netlify-cms-lib-util'; export default class API extends GitlabAPI { constructor(config) { super(config); this.tokenPromise = config.tokenPromise; this.commitAuthor = config.commitAuthor; - this.repoURL = ""; + this.repoURL = ''; } - authenticateRequest = async req => unsentRequest.withHeaders({ - Authorization: `Bearer ${ await this.tokenPromise() }`, - }, req); + authenticateRequest = async req => + unsentRequest.withHeaders( + { + Authorization: `Bearer ${await this.tokenPromise()}`, + }, + req, + ); - request = async req => flow([ - this.buildRequest, - this.authenticateRequest, - then(unsentRequest.performRequest), - ])(req); + request = async req => + flow([this.buildRequest, this.authenticateRequest, then(unsentRequest.performRequest)])(req); - hasWriteAccess = () => Promise.resolve(true) + hasWriteAccess = () => Promise.resolve(true); } diff --git a/packages/netlify-cms-backend-git-gateway/src/implementation.js b/packages/netlify-cms-backend-git-gateway/src/implementation.js index 7266b6da..b41073c7 100644 --- a/packages/netlify-cms-backend-git-gateway/src/implementation.js +++ b/packages/netlify-cms-backend-git-gateway/src/implementation.js @@ -1,13 +1,13 @@ -import GoTrue from "gotrue-js"; +import GoTrue from 'gotrue-js'; import jwtDecode from 'jwt-decode'; -import { get, pick, intersection } from "lodash"; -import { unsentRequest } from "netlify-cms-lib-util"; -import { GitHubBackend } from "netlify-cms-backend-github"; -import { GitLabBackend } from "netlify-cms-backend-gitlab"; -import { BitBucketBackend, API as BitBucketAPI } from "netlify-cms-backend-bitbucket"; -import GitHubAPI from "./GitHubAPI"; -import GitLabAPI from "./GitLabAPI"; -import AuthenticationPage from "./AuthenticationPage"; +import { get, pick, intersection } from 'lodash'; +import { unsentRequest } from 'netlify-cms-lib-util'; +import { GitHubBackend } from 'netlify-cms-backend-github'; +import { GitLabBackend } from 'netlify-cms-backend-gitlab'; +import { BitBucketBackend, API as BitBucketAPI } from 'netlify-cms-backend-bitbucket'; +import GitHubAPI from './GitHubAPI'; +import GitLabAPI from './GitLabAPI'; +import AuthenticationPage from './AuthenticationPage'; const localHosts = { localhost: true, @@ -20,14 +20,20 @@ const defaults = { }; function getEndpoint(endpoint, netlifySiteURL) { - if (localHosts[document.location.host.split(":").shift()] && netlifySiteURL && endpoint.match(/^\/\.netlify\//)) { + if ( + localHosts[document.location.host.split(':').shift()] && + netlifySiteURL && + endpoint.match(/^\/\.netlify\//) + ) { const parts = []; if (netlifySiteURL) { parts.push(netlifySiteURL); - if (!netlifySiteURL.match(/\/$/)) { parts.push("/"); } + if (!netlifySiteURL.match(/\/$/)) { + parts.push('/'); + } } parts.push(endpoint.replace(/^\//, '')); - return parts.join(""); + return parts.join(''); } return endpoint; } @@ -40,46 +46,58 @@ export default class GitGateway { ...options, }; this.config = config; - this.branch = config.getIn(["backend", "branch"], "master").trim(); - this.squash_merges = config.getIn(["backend", "squash_merges"]); + this.branch = config.getIn(['backend', 'branch'], 'master').trim(); + this.squash_merges = config.getIn(['backend', 'squash_merges']); - const netlifySiteURL = localStorage.getItem("netlifySiteURL"); - const APIUrl = getEndpoint(config.getIn(["backend", "identity_url"], defaults.identity), netlifySiteURL); - this.gatewayUrl = getEndpoint(config.getIn(["backend", "gateway_url"], defaults.gateway), netlifySiteURL); + const netlifySiteURL = localStorage.getItem('netlifySiteURL'); + const APIUrl = getEndpoint( + config.getIn(['backend', 'identity_url'], defaults.identity), + netlifySiteURL, + ); + this.gatewayUrl = getEndpoint( + config.getIn(['backend', 'gateway_url'], defaults.gateway), + netlifySiteURL, + ); const backendTypeRegex = /\/(github|gitlab|bitbucket)\/?$/; const backendTypeMatches = this.gatewayUrl.match(backendTypeRegex); if (backendTypeMatches) { this.backendType = backendTypeMatches[1]; - this.gatewayUrl = this.gatewayUrl.replace(backendTypeRegex, "/"); + this.gatewayUrl = this.gatewayUrl.replace(backendTypeRegex, '/'); } else { this.backendType = null; } - this.authClient = window.netlifyIdentity ? window.netlifyIdentity.gotrue : new GoTrue({ APIUrl }); + this.authClient = window.netlifyIdentity + ? window.netlifyIdentity.gotrue + : new GoTrue({ APIUrl }); AuthenticationPage.authClient = this.authClient; this.backend = null; } - requestFunction = req => this.tokenPromise() - .then(token => unsentRequest.withHeaders({ Authorization: `Bearer ${ token }` }, req)) - .then(unsentRequest.performRequest); + requestFunction = req => + this.tokenPromise() + .then(token => unsentRequest.withHeaders({ Authorization: `Bearer ${token}` }, req)) + .then(unsentRequest.performRequest); authenticate(user) { this.tokenPromise = user.jwt.bind(user); return this.tokenPromise().then(async token => { if (!this.backendType) { - const { github_enabled, gitlab_enabled, bitbucket_enabled, roles } = await fetch(`${ this.gatewayUrl }/settings`, { - headers: { Authorization: `Bearer ${ token }` }, - }).then(res => res.json()); + const { github_enabled, gitlab_enabled, bitbucket_enabled, roles } = await fetch( + `${this.gatewayUrl}/settings`, + { + headers: { Authorization: `Bearer ${token}` }, + }, + ).then(res => res.json()); this.acceptRoles = roles; if (github_enabled) { - this.backendType = "github"; + this.backendType = 'github'; } else if (gitlab_enabled) { - this.backendType = "gitlab"; + this.backendType = 'gitlab'; } else if (bitbucket_enabled) { - this.backendType = "bitbucket"; + this.backendType = 'bitbucket'; } } @@ -98,21 +116,21 @@ export default class GitGateway { metadata: user.user_metadata, }; const apiConfig = { - api_root: `${ this.gatewayUrl }/${ this.backendType }`, + api_root: `${this.gatewayUrl}/${this.backendType}`, branch: this.branch, tokenPromise: this.tokenPromise, - commitAuthor: pick(userData, ["name", "email"]), + commitAuthor: pick(userData, ['name', 'email']), squash_merges: this.squash_merges, initialWorkflowStatus: this.options.initialWorkflowStatus, }; - if (this.backendType === "github") { + if (this.backendType === 'github') { this.api = new GitHubAPI(apiConfig); this.backend = new GitHubBackend(this.config, { ...this.options, API: this.api }); - } else if (this.backendType === "gitlab") { + } else if (this.backendType === 'gitlab') { this.api = new GitLabAPI(apiConfig); this.backend = new GitLabBackend(this.config, { ...this.options, API: this.api }); - } else if (this.backendType === "bitbucket") { + } else if (this.backendType === 'bitbucket') { this.api = new BitBucketAPI({ ...apiConfig, requestFunction: this.requestFunction, @@ -145,18 +163,46 @@ export default class GitGateway { return this.tokenPromise(); } - entriesByFolder(collection, extension) { return this.backend.entriesByFolder(collection, extension); } - entriesByFiles(collection) { return this.backend.entriesByFiles(collection); } - fetchFiles(files) { return this.backend.fetchFiles(files); } - getEntry(collection, slug, path) { return this.backend.getEntry(collection, slug, path); } - getMedia() { return this.backend.getMedia(); } - persistEntry(entry, mediaFiles, options) { return this.backend.persistEntry(entry, mediaFiles, options); } - persistMedia(mediaFile, options) { return this.backend.persistMedia(mediaFile, options); } - deleteFile(path, commitMessage, options) { return this.backend.deleteFile(path, commitMessage, options); } - unpublishedEntries() { return this.backend.unpublishedEntries(); } - unpublishedEntry(collection, slug) { return this.backend.unpublishedEntry(collection, slug); } - updateUnpublishedEntryStatus(collection, slug, newStatus) { return this.backend.updateUnpublishedEntryStatus(collection, slug, newStatus); } - deleteUnpublishedEntry(collection, slug) { return this.backend.deleteUnpublishedEntry(collection, slug); } - publishUnpublishedEntry(collection, slug) { return this.backend.publishUnpublishedEntry(collection, slug); } - traverseCursor(cursor, action) { return this.backend.traverseCursor(cursor, action); } + entriesByFolder(collection, extension) { + return this.backend.entriesByFolder(collection, extension); + } + entriesByFiles(collection) { + return this.backend.entriesByFiles(collection); + } + fetchFiles(files) { + return this.backend.fetchFiles(files); + } + getEntry(collection, slug, path) { + return this.backend.getEntry(collection, slug, path); + } + getMedia() { + return this.backend.getMedia(); + } + persistEntry(entry, mediaFiles, options) { + return this.backend.persistEntry(entry, mediaFiles, options); + } + persistMedia(mediaFile, options) { + return this.backend.persistMedia(mediaFile, options); + } + deleteFile(path, commitMessage, options) { + return this.backend.deleteFile(path, commitMessage, options); + } + unpublishedEntries() { + return this.backend.unpublishedEntries(); + } + unpublishedEntry(collection, slug) { + return this.backend.unpublishedEntry(collection, slug); + } + updateUnpublishedEntryStatus(collection, slug, newStatus) { + return this.backend.updateUnpublishedEntryStatus(collection, slug, newStatus); + } + deleteUnpublishedEntry(collection, slug) { + return this.backend.deleteUnpublishedEntry(collection, slug); + } + publishUnpublishedEntry(collection, slug) { + return this.backend.publishUnpublishedEntry(collection, slug); + } + traverseCursor(cursor, action) { + return this.backend.traverseCursor(cursor, action); + } } diff --git a/packages/netlify-cms-backend-git-gateway/src/index.js b/packages/netlify-cms-backend-git-gateway/src/index.js index b7ace893..59b7f092 100644 --- a/packages/netlify-cms-backend-git-gateway/src/index.js +++ b/packages/netlify-cms-backend-git-gateway/src/index.js @@ -1,3 +1,2 @@ export GitGatewayBackend from './implementation'; export AuthenticationPage from './AuthenticationPage'; - diff --git a/packages/netlify-cms-backend-github/src/API.js b/packages/netlify-cms-backend-github/src/API.js index e4d88c4c..5fc084b6 100644 --- a/packages/netlify-cms-backend-github/src/API.js +++ b/packages/netlify-cms-backend-github/src/API.js @@ -1,43 +1,43 @@ -import { localForage } from "netlify-cms-lib-util"; -import { Base64 } from "js-base64"; -import { uniq, initial, last, get, find, hasIn, partial, result } from "lodash"; -import { filterPromises, resolvePromiseProperties } from "netlify-cms-lib-util"; -import { APIError, EditorialWorkflowError } from "netlify-cms-lib-util"; +import { localForage } from 'netlify-cms-lib-util'; +import { Base64 } from 'js-base64'; +import { uniq, initial, last, get, find, hasIn, partial, result } from 'lodash'; +import { filterPromises, resolvePromiseProperties } from 'netlify-cms-lib-util'; +import { APIError, EditorialWorkflowError } from 'netlify-cms-lib-util'; const CMS_BRANCH_PREFIX = 'cms/'; export default class API { constructor(config) { - this.api_root = config.api_root || "https://api.github.com"; + this.api_root = config.api_root || 'https://api.github.com'; this.token = config.token || false; - this.branch = config.branch || "master"; - this.repo = config.repo || ""; - this.repoURL = `/repos/${ this.repo }`; - this.merge_method = config.squash_merges ? "squash" : "merge"; + this.branch = config.branch || 'master'; + this.repo = config.repo || ''; + this.repoURL = `/repos/${this.repo}`; + this.merge_method = config.squash_merges ? 'squash' : 'merge'; this.initialWorkflowStatus = config.initialWorkflowStatus; } user() { - return this.request("/user"); + return this.request('/user'); } hasWriteAccess() { return this.request(this.repoURL) .then(repo => repo.permissions.push) .catch(error => { - console.error("Problem fetching repo data from GitHub"); + console.error('Problem fetching repo data from GitHub'); throw error; }); } requestHeaders(headers = {}) { const baseHeader = { - "Content-Type": "application/json", + 'Content-Type': 'application/json', ...headers, }; if (this.token) { - baseHeader.Authorization = `token ${ this.token }`; + baseHeader.Authorization = `token ${this.token}`; return baseHeader; } @@ -45,7 +45,7 @@ export default class API { } parseJsonResponse(response) { - return response.json().then((json) => { + return response.json().then(json => { if (!response.ok) { return Promise.reject(json); } @@ -59,11 +59,11 @@ export default class API { const params = [`ts=${cacheBuster}`]; if (options.params) { for (const key in options.params) { - params.push(`${ key }=${ encodeURIComponent(options.params[key]) }`); + params.push(`${key}=${encodeURIComponent(options.params[key])}`); } } if (params.length) { - path += `?${ params.join("&") }`; + path += `?${params.join('&')}`; } return this.api_root + path; } @@ -72,21 +72,22 @@ export default class API { const headers = this.requestHeaders(options.headers || {}); const url = this.urlFor(path, options); let responseStatus; - return fetch(url, { ...options, headers }).then((response) => { - responseStatus = response.status; - const contentType = response.headers.get("Content-Type"); - if (contentType && contentType.match(/json/)) { - return this.parseJsonResponse(response); - } - const text = response.text(); - if (!response.ok) { - return Promise.reject(text); - } - return text; - }) - .catch((error) => { - throw new APIError(error.message, responseStatus, 'GitHub'); - }); + return fetch(url, { ...options, headers }) + .then(response => { + responseStatus = response.status; + const contentType = response.headers.get('Content-Type'); + if (contentType && contentType.match(/json/)) { + return this.parseJsonResponse(response); + } + const text = response.text(); + if (!response.ok) { + return Promise.reject(text); + } + return text; + }) + .catch(error => { + throw new APIError(error.message, responseStatus, 'GitHub'); + }); } generateBranchName(basename) { @@ -94,63 +95,78 @@ export default class API { } checkMetadataRef() { - return this.request(`${ this.repoURL }/git/refs/meta/_netlify_cms?${ Date.now() }`, { - cache: "no-store", + return this.request(`${this.repoURL}/git/refs/meta/_netlify_cms?${Date.now()}`, { + cache: 'no-store', }) - .then(response => response.object) - .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.", - }; + .then(response => response.object) + .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.', + }; - return this.uploadBlob(readme) - .then(item => this.request(`${ this.repoURL }/git/trees`, { - method: "POST", - body: JSON.stringify({ tree: [{ path: "README.md", mode: "100644", type: "blob", sha: item.sha }] }), - })) - .then(tree => this.commit("First Commit", tree)) - .then(response => this.createRef("meta", "_netlify_cms", response.sha)) - .then(response => response.object); - }); + return this.uploadBlob(readme) + .then(item => + this.request(`${this.repoURL}/git/trees`, { + method: 'POST', + body: JSON.stringify({ + tree: [{ path: 'README.md', mode: '100644', type: 'blob', sha: item.sha }], + }), + }), + ) + .then(tree => this.commit('First Commit', tree)) + .then(response => this.createRef('meta', '_netlify_cms', response.sha)) + .then(response => response.object); + }); } storeMetadata(key, data) { - return this.checkMetadataRef() - .then((branchData) => { + return this.checkMetadataRef().then(branchData => { const fileTree = { - [`${ key }.json`]: { - path: `${ key }.json`, + [`${key}.json`]: { + path: `${key}.json`, raw: JSON.stringify(data), file: true, }, }; - return this.uploadBlob(fileTree[`${ key }.json`]) - .then(() => this.updateTree(branchData.sha, "/", fileTree)) - .then(changeTree => this.commit(`Updating “${ key }” metadata`, changeTree)) - .then(response => this.patchRef("meta", "_netlify_cms", response.sha)) - .then(() => { - localForage.setItem(`gh.meta.${ key }`, { - expires: Date.now() + 300000, // In 5 minutes - data, + return this.uploadBlob(fileTree[`${key}.json`]) + .then(() => this.updateTree(branchData.sha, '/', fileTree)) + .then(changeTree => this.commit(`Updating “${key}” metadata`, changeTree)) + .then(response => this.patchRef('meta', '_netlify_cms', response.sha)) + .then(() => { + localForage.setItem(`gh.meta.${key}`, { + expires: Date.now() + 300000, // In 5 minutes + data, + }); }); - }); }); } retrieveMetadata(key) { - const cache = localForage.getItem(`gh.meta.${ key }`); - return cache.then((cached) => { - if (cached && cached.expires > Date.now()) { return cached.data; } - console.log("%c Checking for MetaData files", "line-height: 30px;text-align: center;font-weight: bold"); - return this.request(`${ this.repoURL }/contents/${ key }.json`, { - params: { ref: "refs/meta/_netlify_cms" }, - headers: { Accept: "application/vnd.github.VERSION.raw" }, - cache: "no-store", + const cache = localForage.getItem(`gh.meta.${key}`); + return cache.then(cached => { + if (cached && cached.expires > Date.now()) { + return cached.data; + } + console.log( + '%c Checking for MetaData files', + 'line-height: 30px;text-align: center;font-weight: bold', + ); + return this.request(`${this.repoURL}/contents/${key}.json`, { + params: { ref: 'refs/meta/_netlify_cms' }, + headers: { Accept: 'application/vnd.github.VERSION.raw' }, + cache: 'no-store', }) - .then(response => JSON.parse(response)) - .catch(() => console.log("%c %s does not have metadata", "line-height: 30px;text-align: center;font-weight: bold", key)); + .then(response => JSON.parse(response)) + .catch(() => + console.log( + '%c %s does not have metadata', + 'line-height: 30px;text-align: center;font-weight: bold', + key, + ), + ); }); } @@ -158,13 +174,16 @@ export default class API { if (sha) { return this.getBlob(sha); } else { - return this.request(`${ this.repoURL }/contents/${ path }`, { - headers: { Accept: "application/vnd.github.VERSION.raw" }, + return this.request(`${this.repoURL}/contents/${path}`, { + headers: { Accept: 'application/vnd.github.VERSION.raw' }, params: { ref: branch }, - cache: "no-store", + cache: 'no-store', }).catch(error => { - if (hasIn(error, 'message.errors') && find(error.message.errors, { code: "too_large" })) { - const dir = path.split('/').slice(0, -1).join('/'); + if (hasIn(error, 'message.errors') && find(error.message.errors, { code: 'too_large' })) { + const dir = path + .split('/') + .slice(0, -1) + .join('/'); return this.listFiles(dir) .then(files => files.find(file => file.path === path)) .then(file => this.getBlob(file.sha)); @@ -176,10 +195,12 @@ export default class API { getBlob(sha) { return localForage.getItem(`gh.${sha}`).then(cached => { - if (cached) { return cached; } + if (cached) { + return cached; + } return this.request(`${this.repoURL}/git/blobs/${sha}`, { - headers: { Accept: "application/vnd.github.VERSION.raw" }, + headers: { Accept: 'application/vnd.github.VERSION.raw' }, }).then(result => { localForage.setItem(`gh.${sha}`, result); return result; @@ -188,66 +209,75 @@ export default class API { } listFiles(path) { - return this.request(`${ this.repoURL }/contents/${ path.replace(/\/$/, '') }`, { + return this.request(`${this.repoURL}/contents/${path.replace(/\/$/, '')}`, { params: { ref: this.branch }, }) - .then(files => { - if (!Array.isArray(files)) { - throw new Error(`Cannot list files, path ${path} is not a directory but a ${files.type}`); - } - return files; - }) - .then(files => files.filter(file => file.type === "file")); + .then(files => { + if (!Array.isArray(files)) { + throw new Error(`Cannot list files, path ${path} is not a directory but a ${files.type}`); + } + return files; + }) + .then(files => files.filter(file => file.type === 'file')); } readUnpublishedBranchFile(contentKey) { - const metaDataPromise = this.retrieveMetadata(contentKey) - .then(data => (data.objects.entry.path ? data : Promise.reject(null))); + const metaDataPromise = this.retrieveMetadata(contentKey).then( + data => (data.objects.entry.path ? data : Promise.reject(null)), + ); return resolvePromiseProperties({ metaData: metaDataPromise, - fileData: metaDataPromise.then( - data => this.readFile(data.objects.entry.path, null, data.branch)), - isModification: metaDataPromise.then( - data => this.isUnpublishedEntryModification(data.objects.entry.path, this.branch)), - }) - .catch(() => { + fileData: metaDataPromise.then(data => + this.readFile(data.objects.entry.path, null, data.branch), + ), + isModification: metaDataPromise.then(data => + this.isUnpublishedEntryModification(data.objects.entry.path, this.branch), + ), + }).catch(() => { throw new EditorialWorkflowError('content is not under editorial workflow', true); }); } isUnpublishedEntryModification(path, branch) { return this.readFile(path, null, branch) - .then(() => true) - .catch((err) => { - if (err.message && err.message === "Not Found") { - return false; - } - throw err; - }); + .then(() => true) + .catch(err => { + if (err.message && err.message === 'Not Found') { + return false; + } + throw err; + }); } listUnpublishedBranches() { - console.log("%c Checking for Unpublished entries", "line-height: 30px;text-align: center;font-weight: bold"); - return this.request(`${ this.repoURL }/git/refs/heads/cms`) - .then(branches => filterPromises(branches, (branch) => { - const branchName = branch.ref.substring("/refs/heads/".length - 1); + console.log( + '%c Checking for Unpublished entries', + 'line-height: 30px;text-align: center;font-weight: bold', + ); + return this.request(`${this.repoURL}/git/refs/heads/cms`) + .then(branches => + filterPromises(branches, branch => { + const branchName = branch.ref.substring('/refs/heads/'.length - 1); - // Get PRs with a `head` of `branchName`. Note that this is a - // substring match, so we need to check that the `head.ref` of - // at least one of the returned objects matches `branchName`. - return this.request(`${ this.repoURL }/pulls`, { - params: { - head: branchName, - state: 'open', - base: this.branch, - }, - }) - .then(prs => prs.some(pr => pr.head.ref === branchName)); - })) - .catch((error) => { - console.log("%c No Unpublished entries", "line-height: 30px;text-align: center;font-weight: bold"); - throw error; - }); + // Get PRs with a `head` of `branchName`. Note that this is a + // substring match, so we need to check that the `head.ref` of + // at least one of the returned objects matches `branchName`. + return this.request(`${this.repoURL}/pulls`, { + params: { + head: branchName, + state: 'open', + base: this.branch, + }, + }).then(prs => prs.some(pr => pr.head.ref === branchName)); + }), + ) + .catch(error => { + console.log( + '%c No Unpublished entries', + 'line-height: 30px;text-align: center;font-weight: bold', + ); + throw error; + }); } composeFileTree(files) { @@ -257,12 +287,15 @@ export default class API { let subtree; const fileTree = {}; - files.forEach((file) => { - if (file.uploaded) { return; } - parts = file.path.split("/").filter(part => part); + files.forEach(file => { + if (file.uploaded) { + return; + } + parts = file.path.split('/').filter(part => part); filename = parts.pop(); subtree = fileTree; - while (part = parts.shift()) { // eslint-disable-line no-cond-assign + while ((part = parts.shift())) { + // eslint-disable-line no-cond-assign subtree[part] = subtree[part] || {}; subtree = subtree[part]; } @@ -277,8 +310,10 @@ export default class API { const uploadPromises = []; const files = entry ? mediaFiles.concat(entry) : mediaFiles; - files.forEach((file) => { - if (file.uploaded) { return; } + files.forEach(file => { + if (file.uploaded) { + return; + } uploadPromises.push(this.uploadBlob(file)); }); @@ -287,10 +322,9 @@ export default class API { return Promise.all(uploadPromises).then(() => { if (!options.useWorkflow) { return this.getBranch() - .then(branchData => this.updateTree(branchData.commit.sha, "/", fileTree)) - .then(changeTree => this.commit(options.commitMessage, changeTree)) - .then(response => this.patchBranch(this.branch, response.sha)); - + .then(branchData => this.updateTree(branchData.commit.sha, '/', fileTree)) + .then(changeTree => this.commit(options.commitMessage, changeTree)) + .then(response => this.patchBranch(this.branch, response.sha)); } else { const mediaFilesList = mediaFiles.map(file => ({ path: file.path, sha: file.sha })); return this.editorialWorkflowGit(fileTree, entry, mediaFilesList, options); @@ -298,32 +332,31 @@ export default class API { }); } - deleteFile(path, message, options={}) { + deleteFile(path, message, options = {}) { const branch = options.branch || this.branch; const pathArray = path.split('/'); const filename = last(pathArray); const directory = initial(pathArray).join('/'); const fileDataPath = encodeURIComponent(directory); const fileDataURL = `${this.repoURL}/git/trees/${branch}:${fileDataPath}`; - const fileURL = `${ this.repoURL }/contents/${ path }`; + const fileURL = `${this.repoURL}/contents/${path}`; /** * We need to request the tree first to get the SHA. We use extended SHA-1 * syntax (:) to get a blob from a tree without having to recurse * through the tree. */ - return this.request(fileDataURL, { cache: 'no-store' }) - .then(resp => { - const { sha } = resp.tree.find(file => file.path === filename); - const opts = { method: 'DELETE', params: { sha, message, branch } }; - if (this.commitAuthor) { - opts.params.author = { - ...this.commitAuthor, - date: new Date().toISOString(), - }; - } - return this.request(fileURL, opts); - }); + return this.request(fileDataURL, { cache: 'no-store' }).then(resp => { + const { sha } = resp.tree.find(file => file.path === filename); + const opts = { method: 'DELETE', params: { sha, message, branch } }; + if (this.commitAuthor) { + opts.params.author = { + ...this.commitAuthor, + date: new Date().toISOString(), + }; + } + return this.request(fileURL, opts); + }); } editorialWorkflowGit(fileTree, entry, filesList, options) { @@ -335,42 +368,42 @@ export default class API { let prResponse; return this.getBranch() - .then(branchData => this.updateTree(branchData.commit.sha, "/", fileTree)) - .then(changeTree => this.commit(options.commitMessage, changeTree)) - .then(commitResponse => this.createBranch(branchName, commitResponse.sha)) - .then(() => this.createPR(options.commitMessage, branchName)) - .then(pr => { - prResponse = pr; - return this.user(); - }) - .then(user => { - return this.storeMetadata(contentKey, { - type: "PR", - pr: { - number: prResponse.number, - head: prResponse.head && prResponse.head.sha, - }, - user: user.name || user.login, - status: this.initialWorkflowStatus, - branch: branchName, - collection: options.collectionName, - title: options.parsedData && options.parsedData.title, - description: options.parsedData && options.parsedData.description, - objects: { - entry: { - path: entry.path, - sha: entry.sha, + .then(branchData => this.updateTree(branchData.commit.sha, '/', fileTree)) + .then(changeTree => this.commit(options.commitMessage, changeTree)) + .then(commitResponse => this.createBranch(branchName, commitResponse.sha)) + .then(() => this.createPR(options.commitMessage, branchName)) + .then(pr => { + prResponse = pr; + return this.user(); + }) + .then(user => { + return this.storeMetadata(contentKey, { + type: 'PR', + pr: { + number: prResponse.number, + head: prResponse.head && prResponse.head.sha, }, - files: filesList, - }, - timeStamp: new Date().toISOString(), + user: user.name || user.login, + status: this.initialWorkflowStatus, + branch: branchName, + collection: options.collectionName, + title: options.parsedData && options.parsedData.title, + description: options.parsedData && options.parsedData.description, + objects: { + entry: { + path: entry.path, + sha: entry.sha, + }, + files: filesList, + }, + timeStamp: new Date().toISOString(), + }); }); - }); } else { // Entry is already on editorial review workflow - just update metadata and commit to existing branch let newHead; return this.getBranch(branchName) - .then(branchData => this.updateTree(branchData.commit.sha, "/", fileTree)) + .then(branchData => this.updateTree(branchData.commit.sha, '/', fileTree)) .then(changeTree => this.commit(options.commitMessage, changeTree)) .then(commit => { newHead = commit; @@ -379,7 +412,7 @@ export default class API { .then(metadata => { const { title, description } = options.parsedData || {}; const metadataFiles = get(metadata.objects, 'files', []); - const files = [ ...metadataFiles, ...filesList ]; + const files = [...metadataFiles, ...filesList]; const pr = { ...metadata.pr, head: newHead.sha }; const objects = { entry: { path: entry.path, sha: entry.sha }, @@ -392,8 +425,9 @@ export default class API { * can just finish the persist operation here. */ if (options.hasAssetStore) { - return this.storeMetadata(contentKey, updatedMetadata) - .then(() => this.patchBranch(branchName, newHead.sha)); + return this.storeMetadata(contentKey, updatedMetadata).then(() => + this.patchBranch(branchName, newHead.sha), + ); } /** @@ -440,8 +474,7 @@ export default class API { const updatedMetadata = { ...metadata, pr, timeStamp }; await this.storeMetadata(contentKey, updatedMetadata); return this.patchBranch(branchName, rebasedHead.sha, { force: true }); - } - catch(error) { + } catch (error) { console.error(error); throw error; } @@ -496,39 +529,40 @@ export default class API { /** * Set the base commit as the parent. */ - const parent = [ baseCommit.sha ]; + const parent = [baseCommit.sha]; /** * Get the blob data by path. */ - return this.getBlobInTree(commit.tree.sha, pathToBlob) + return ( + this.getBlobInTree(commit.tree.sha, pathToBlob) - /** - * Create a new tree consisting of the base tree and the single updated - * blob. Use the full path to indicate nesting, GitHub will take care of - * subtree creation. - */ - .then(blob => this.createTree(baseCommit.tree.sha, [{ ...blob, path: pathToBlob }])) + /** + * Create a new tree consisting of the base tree and the single updated + * blob. Use the full path to indicate nesting, GitHub will take care of + * subtree creation. + */ + .then(blob => this.createTree(baseCommit.tree.sha, [{ ...blob, path: pathToBlob }])) - /** - * Create a new commit with the updated tree and original commit metadata. - */ - .then(tree => this.createCommit(message, tree.sha, parent, author, committer)); + /** + * Create a new commit with the updated tree and original commit metadata. + */ + .then(tree => this.createCommit(message, tree.sha, parent, author, committer)) + ); } - /** * Get a pull request by PR number. */ getPullRequest(prNumber) { - return this.request(`${ this.repoURL }/pulls/${prNumber} }`); + return this.request(`${this.repoURL}/pulls/${prNumber} }`); } /** * Get the list of commits for a given pull request. */ - getPullRequestCommits (prNumber) { - return this.request(`${ this.repoURL }/pulls/${prNumber}/commits`); + getPullRequestCommits(prNumber) { + return this.request(`${this.repoURL}/pulls/${prNumber}/commits`); } /** @@ -552,66 +586,67 @@ export default class API { updateUnpublishedEntryStatus(collection, slug, status) { const contentKey = slug; return this.retrieveMetadata(contentKey) - .then(metadata => ({ - ...metadata, - status, - })) - .then(updatedMetadata => this.storeMetadata(contentKey, updatedMetadata)); + .then(metadata => ({ + ...metadata, + status, + })) + .then(updatedMetadata => this.storeMetadata(contentKey, updatedMetadata)); } deleteUnpublishedEntry(collection, slug) { const contentKey = slug; const branchName = this.generateBranchName(contentKey); - return this.retrieveMetadata(contentKey) - .then(metadata => this.closePR(metadata.pr)) - .then(() => this.deleteBranch(branchName)) - // If the PR doesn't exist, then this has already been deleted - - // deletion should be idempotent, so we can consider this a - // success. - .catch((err) => { - if (err.message === "Reference does not exist") { - return Promise.resolve(); - } - return Promise.reject(err); - }); + return ( + this.retrieveMetadata(contentKey) + .then(metadata => this.closePR(metadata.pr)) + .then(() => this.deleteBranch(branchName)) + // If the PR doesn't exist, then this has already been deleted - + // deletion should be idempotent, so we can consider this a + // success. + .catch(err => { + if (err.message === 'Reference does not exist') { + return Promise.resolve(); + } + return Promise.reject(err); + }) + ); } publishUnpublishedEntry(collection, slug) { const contentKey = slug; const branchName = this.generateBranchName(contentKey); return this.retrieveMetadata(contentKey) - .then(metadata => this.mergePR(metadata.pr, metadata.objects)) - .then(() => this.deleteBranch(branchName)); + .then(metadata => this.mergePR(metadata.pr, metadata.objects)) + .then(() => this.deleteBranch(branchName)); } - createRef(type, name, sha) { - return this.request(`${ this.repoURL }/git/refs`, { - method: "POST", - body: JSON.stringify({ ref: `refs/${ type }/${ name }`, sha }), + return this.request(`${this.repoURL}/git/refs`, { + method: 'POST', + body: JSON.stringify({ ref: `refs/${type}/${name}`, sha }), }); } patchRef(type, name, sha, opts = {}) { const force = opts.force || false; - return this.request(`${ this.repoURL }/git/refs/${ type }/${ encodeURIComponent(name) }`, { - method: "PATCH", + return this.request(`${this.repoURL}/git/refs/${type}/${encodeURIComponent(name)}`, { + method: 'PATCH', body: JSON.stringify({ sha, force }), }); } deleteRef(type, name) { - return this.request(`${ this.repoURL }/git/refs/${ type }/${ encodeURIComponent(name) }`, { + return this.request(`${this.repoURL}/git/refs/${type}/${encodeURIComponent(name)}`, { method: 'DELETE', }); } getBranch(branch = this.branch) { - return this.request(`${ this.repoURL }/branches/${ encodeURIComponent(branch) }`); + return this.request(`${this.repoURL}/branches/${encodeURIComponent(branch)}`); } createBranch(branchName, sha) { - return this.createRef("heads", branchName, sha); + return this.createRef('heads', branchName, sha); } assertCmsBranch(branchName) { @@ -623,26 +658,26 @@ export default class API { if (force && !this.assertCmsBranch(branchName)) { throw Error(`Only CMS branches can be force updated, cannot force update ${branchName}`); } - return this.patchRef("heads", branchName, sha, { force }); + return this.patchRef('heads', branchName, sha, { force }); } deleteBranch(branchName) { - return this.deleteRef("heads", branchName); + return this.deleteRef('heads', branchName); } createPR(title, head, base = this.branch) { - const body = "Automatically generated by Netlify CMS"; - return this.request(`${ this.repoURL }/pulls`, { - method: "POST", + const body = 'Automatically generated by Netlify CMS'; + return this.request(`${this.repoURL}/pulls`, { + method: 'POST', body: JSON.stringify({ title, body, head, base }), }); } closePR(pullrequest) { const prNumber = pullrequest.number; - console.log("%c Deleting PR", "line-height: 30px;text-align: center;font-weight: bold"); - return this.request(`${ this.repoURL }/pulls/${ prNumber }`, { - method: "PATCH", + console.log('%c Deleting PR', 'line-height: 30px;text-align: center;font-weight: bold'); + return this.request(`${this.repoURL}/pulls/${prNumber}`, { + method: 'PATCH', body: JSON.stringify({ state: closed, }), @@ -652,16 +687,15 @@ export default class API { mergePR(pullrequest, objects) { const headSha = pullrequest.head; const prNumber = pullrequest.number; - console.log("%c Merging PR", "line-height: 30px;text-align: center;font-weight: bold"); - return this.request(`${ this.repoURL }/pulls/${ prNumber }/merge`, { - method: "PUT", + console.log('%c Merging PR', 'line-height: 30px;text-align: center;font-weight: bold'); + return this.request(`${this.repoURL}/pulls/${prNumber}/merge`, { + method: 'PUT', body: JSON.stringify({ - commit_message: "Automatically generated. Merged on Netlify CMS.", + commit_message: 'Automatically generated. Merged on Netlify CMS.', sha: headSha, merge_method: this.merge_method, }), - }) - .catch((error) => { + }).catch(error => { if (error instanceof APIError && error.status === 405) { return this.forceMergePR(pullrequest, objects); } else { @@ -673,15 +707,18 @@ export default class API { forceMergePR(pullrequest, objects) { const files = objects.files.concat(objects.entry); const fileTree = this.composeFileTree(files); - let commitMessage = "Automatically generated. Merged on Netlify CMS\n\nForce merge of:"; - files.forEach((file) => { - commitMessage += `\n* "${ file.path }"`; + let commitMessage = 'Automatically generated. Merged on Netlify CMS\n\nForce merge of:'; + files.forEach(file => { + commitMessage += `\n* "${file.path}"`; }); - console.log("%c Automatic merge not possible - Forcing merge.", "line-height: 30px;text-align: center;font-weight: bold"); + console.log( + '%c Automatic merge not possible - Forcing merge.', + 'line-height: 30px;text-align: center;font-weight: bold', + ); return this.getBranch() - .then(branchData => this.updateTree(branchData.commit.sha, "/", fileTree)) - .then(changeTree => this.commit(commitMessage, changeTree)) - .then(response => this.patchBranch(this.branch, response.sha)); + .then(branchData => this.updateTree(branchData.commit.sha, '/', fileTree)) + .then(changeTree => this.commit(commitMessage, changeTree)) + .then(response => this.patchBranch(this.branch, response.sha)); } getTree(sha) { @@ -701,7 +738,7 @@ export default class API { const filename = pathSegments.slice(-1)[0]; const baseTree = this.getTree(treeSha); const subTreePromise = directories.reduce((treePromise, segment) => { - return treePromise.then(tree => { + return treePromise.then(tree => { const subTreeSha = find(tree.tree, { path: segment }).sha; return this.getTree(subTreeSha); }); @@ -710,65 +747,73 @@ export default class API { } toBase64(str) { - return Promise.resolve( - Base64.encode(str) - ); + return Promise.resolve(Base64.encode(str)); } uploadBlob(item) { const content = result(item, 'toBase64', partial(this.toBase64, item.raw)); - return content.then(contentBase64 => this.request(`${ this.repoURL }/git/blobs`, { - method: "POST", - body: JSON.stringify({ - content: contentBase64, - encoding: "base64", + return content.then(contentBase64 => + this.request(`${this.repoURL}/git/blobs`, { + method: 'POST', + body: JSON.stringify({ + content: contentBase64, + encoding: 'base64', + }), + }).then(response => { + item.sha = response.sha; + item.uploaded = true; + return item; }), - }).then((response) => { - item.sha = response.sha; - item.uploaded = true; - return item; - })); + ); } updateTree(sha, path, fileTree) { - return this.getTree(sha) - .then((tree) => { - let obj; - let filename; - let fileOrDir; - const updates = []; - const added = {}; + return this.getTree(sha).then(tree => { + let obj; + let filename; + let fileOrDir; + const updates = []; + const added = {}; - for (let i = 0, len = tree.tree.length; i < len; i++) { - obj = tree.tree[i]; - if (fileOrDir = fileTree[obj.path]) { // eslint-disable-line no-cond-assign - added[obj.path] = true; - if (fileOrDir.file) { - updates.push({ path: obj.path, mode: obj.mode, type: obj.type, sha: fileOrDir.sha }); - } else { - updates.push(this.updateTree(obj.sha, obj.path, fileOrDir)); - } + for (let i = 0, len = tree.tree.length; i < len; i++) { + obj = tree.tree[i]; + if ((fileOrDir = fileTree[obj.path])) { + // eslint-disable-line no-cond-assign + added[obj.path] = true; + if (fileOrDir.file) { + updates.push({ path: obj.path, mode: obj.mode, type: obj.type, sha: fileOrDir.sha }); + } else { + updates.push(this.updateTree(obj.sha, obj.path, fileOrDir)); } } - for (filename in fileTree) { - fileOrDir = fileTree[filename]; - if (added[filename]) { continue; } - updates.push( - fileOrDir.file ? - { path: filename, mode: "100644", type: "blob", sha: fileOrDir.sha } : - this.updateTree(null, filename, fileOrDir) - ); + } + for (filename in fileTree) { + fileOrDir = fileTree[filename]; + if (added[filename]) { + continue; } - return Promise.all(updates) - .then(tree => this.createTree(sha, tree)) - .then(response => ({ path, mode: "040000", type: "tree", sha: response.sha, parentSha: sha })); - }); + updates.push( + fileOrDir.file + ? { path: filename, mode: '100644', type: 'blob', sha: fileOrDir.sha } + : this.updateTree(null, filename, fileOrDir), + ); + } + return Promise.all(updates) + .then(tree => this.createTree(sha, tree)) + .then(response => ({ + path, + mode: '040000', + type: 'tree', + sha: response.sha, + parentSha: sha, + })); + }); } createTree(baseSha, tree) { - return this.request(`${ this.repoURL }/git/trees`, { - method: "POST", + return this.request(`${this.repoURL}/git/trees`, { + method: 'POST', body: JSON.stringify({ base_tree: baseSha, tree }), }); } @@ -792,8 +837,8 @@ export default class API { } createCommit(message, treeSha, parents, author, committer) { - return this.request(`${ this.repoURL }/git/commits`, { - method: "POST", + return this.request(`${this.repoURL}/git/commits`, { + method: 'POST', body: JSON.stringify({ message, tree: treeSha, parents, author, committer }), }); } diff --git a/packages/netlify-cms-backend-github/src/AuthenticationPage.js b/packages/netlify-cms-backend-github/src/AuthenticationPage.js index 20511c2e..e8ff0577 100644 --- a/packages/netlify-cms-backend-github/src/AuthenticationPage.js +++ b/packages/netlify-cms-backend-github/src/AuthenticationPage.js @@ -6,7 +6,7 @@ import { AuthenticationPage, Icon } from 'netlify-cms-ui-default'; const LoginButtonIcon = styled(Icon)` margin-right: 18px; -` +`; export default class GitHubAuthenticationPage extends React.Component { static propTypes = { @@ -19,11 +19,14 @@ export default class GitHubAuthenticationPage extends React.Component { state = {}; - handleLogin = (e) => { + handleLogin = e => { e.preventDefault(); const cfg = { base_url: this.props.base_url, - site_id: (document.location.host.split(':')[0] === 'localhost') ? 'cms.netlify.com' : this.props.siteId, + site_id: + document.location.host.split(':')[0] === 'localhost' + ? 'cms.netlify.com' + : this.props.siteId, auth_endpoint: this.props.authEndpoint, }; const auth = new NetlifyAuthenticator(cfg); @@ -46,7 +49,7 @@ export default class GitHubAuthenticationPage extends React.Component { loginErrorMessage={this.state.loginError} renderButtonContent={() => ( - {inProgress ? "Logging in..." : "Login with GitHub"} + {inProgress ? 'Logging in...' : 'Login with GitHub'} )} /> diff --git a/packages/netlify-cms-backend-github/src/__tests__/API.spec.js b/packages/netlify-cms-backend-github/src/__tests__/API.spec.js index 8a027623..b7edb3bc 100644 --- a/packages/netlify-cms-backend-github/src/__tests__/API.spec.js +++ b/packages/netlify-cms-backend-github/src/__tests__/API.spec.js @@ -1,4 +1,4 @@ -import API from "../API"; +import API from '../API'; describe('github API', () => { const mockAPI = (api, responses) => { @@ -7,9 +7,9 @@ describe('github API', () => { const response = responses[normalizedPath]; return typeof response === 'function' ? Promise.resolve(response(options)) - : Promise.reject(new Error(`No response for path '${normalizedPath}'`)) + : Promise.reject(new Error(`No response for path '${normalizedPath}'`)); }; - } + }; it('should create PR with correct base branch name when publishing with editorial workflow', () => { let prBaseBranch = null; @@ -20,19 +20,20 @@ describe('github API', () => { '/repos/my-repo/git/trees': () => ({}), '/repos/my-repo/git/commits': () => ({}), '/repos/my-repo/git/refs': () => ({}), - '/repos/my-repo/pulls': (pullRequest) => { + '/repos/my-repo/pulls': pullRequest => { prBaseBranch = JSON.parse(pullRequest.body).base; return { head: { sha: 'cbd' } }; }, '/user': () => ({}), '/repos/my-repo/git/blobs': () => ({}), - '/repos/my-repo/git/refs/meta/_netlify_cms': () => ({ 'object': {} }) + '/repos/my-repo/git/refs/meta/_netlify_cms': () => ({ object: {} }), }; mockAPI(api, responses); return expect( - api.editorialWorkflowGit(null, { slug: 'entry', sha: 'abc' }, null, {}) - .then(() => prBaseBranch) - ).resolves.toEqual('gh-pages') + api + .editorialWorkflowGit(null, { slug: 'entry', sha: 'abc' }, null, {}) + .then(() => prBaseBranch), + ).resolves.toEqual('gh-pages'); }); }); diff --git a/packages/netlify-cms-backend-github/src/implementation.js b/packages/netlify-cms-backend-github/src/implementation.js index bf61214a..ff0719ca 100644 --- a/packages/netlify-cms-backend-github/src/implementation.js +++ b/packages/netlify-cms-backend-github/src/implementation.js @@ -1,12 +1,12 @@ import trimStart from 'lodash/trimStart'; -import semaphore from "semaphore"; -import AuthenticationPage from "./AuthenticationPage"; -import API from "./API"; +import semaphore from 'semaphore'; +import AuthenticationPage from './AuthenticationPage'; +import API from './API'; const MAX_CONCURRENT_DOWNLOADS = 10; export default class GitHub { - constructor(config, options={}) { + constructor(config, options = {}) { this.config = config; this.options = { proxied: false, @@ -14,17 +14,17 @@ export default class GitHub { ...options, }; - if (!this.options.proxied && config.getIn(["backend", "repo"]) == null) { - throw new Error("The GitHub backend needs a \"repo\" in the backend configuration."); + if (!this.options.proxied && config.getIn(['backend', 'repo']) == null) { + throw new Error('The GitHub backend needs a "repo" in the backend configuration.'); } this.api = this.options.API || null; - this.repo = config.getIn(["backend", "repo"], ""); - this.branch = config.getIn(["backend", "branch"], "master").trim(); - this.api_root = config.getIn(["backend", "api_root"], "https://api.github.com"); + this.repo = config.getIn(['backend', 'repo'], ''); + this.branch = config.getIn(['backend', 'branch'], 'master').trim(); + this.api_root = config.getIn(['backend', 'api_root'], 'https://api.github.com'); this.token = ''; - this.squash_merges = config.getIn(["backend", "squash_merges"]); + this.squash_merges = config.getIn(['backend', 'squash_merges']); } authComponent() { @@ -46,13 +46,14 @@ export default class GitHub { initialWorkflowStatus: this.options.initialWorkflowStatus, }); return this.api.user().then(user => - this.api.hasWriteAccess().then((isCollab) => { + this.api.hasWriteAccess().then(isCollab => { // Unauthorized user - if (!isCollab) throw new Error("Your GitHub user account does not have access to this repo."); + if (!isCollab) + throw new Error('Your GitHub user account does not have access to this repo.'); // Authorized user user.token = state.token; return user; - }) + }), ); } @@ -66,36 +67,45 @@ export default class GitHub { } entriesByFolder(collection, extension) { - return this.api.listFiles(collection.get("folder")) - .then(files => files.filter(file => file.name.endsWith('.' + extension))) - .then(this.fetchFiles); + return this.api + .listFiles(collection.get('folder')) + .then(files => files.filter(file => file.name.endsWith('.' + extension))) + .then(this.fetchFiles); } entriesByFiles(collection) { - const files = collection.get("files").map(collectionFile => ({ - path: collectionFile.get("file"), - label: collectionFile.get("label"), + const files = collection.get('files').map(collectionFile => ({ + path: collectionFile.get('file'), + label: collectionFile.get('label'), })); return this.fetchFiles(files); } - fetchFiles = (files) => { + fetchFiles = files => { const sem = semaphore(MAX_CONCURRENT_DOWNLOADS); const promises = []; - files.forEach((file) => { - promises.push(new Promise(resolve => ( - sem.take(() => this.api.readFile(file.path, file.sha).then((data) => { - resolve({ file, data }); - sem.leave(); - }).catch((err = true) => { - sem.leave(); - console.error(`failed to load file from GitHub: ${file.path}`); - resolve({ error: err }); - })) - ))); + files.forEach(file => { + promises.push( + new Promise(resolve => + sem.take(() => + this.api + .readFile(file.path, file.sha) + .then(data => { + resolve({ file, data }); + sem.leave(); + }) + .catch((err = true) => { + sem.leave(); + console.error(`failed to load file from GitHub: ${file.path}`); + resolve({ error: err }); + }), + ), + ), + ); }); - return Promise.all(promises) - .then(loadedEntries => loadedEntries.filter(loadedEntry => !loadedEntry.error)); + return Promise.all(promises).then(loadedEntries => + loadedEntries.filter(loadedEntry => !loadedEntry.error), + ); }; // Fetches a single entry. @@ -107,14 +117,15 @@ export default class GitHub { } getMedia() { - return this.api.listFiles(this.config.get('media_folder')) - .then(files => files.map(({ sha, name, size, download_url, path }) => { + return this.api.listFiles(this.config.get('media_folder')).then(files => + files.map(({ sha, name, size, download_url, path }) => { const url = new URL(download_url); if (url.pathname.match(/.svg$/)) { url.search += (url.search.slice(1) === '' ? '?' : '&') + 'sanitize=true'; } return { id: sha, name, size, url: url.href, path }; - })); + }), + ); } persistEntry(entry, mediaFiles = [], options = {}) { @@ -124,12 +135,11 @@ export default class GitHub { async persistMedia(mediaFile, options = {}) { try { await this.api.persistFiles(null, [mediaFile], options); - + const { sha, value, path, fileObj } = mediaFile; const url = URL.createObjectURL(fileObj); return { id: sha, name: value, size: fileObj.size, url, path: trimStart(path, '/') }; - } - catch(error) { + } catch (error) { console.error(error); throw error; } @@ -140,46 +150,54 @@ export default class GitHub { } unpublishedEntries() { - return this.api.listUnpublishedBranches().then((branches) => { - const sem = semaphore(MAX_CONCURRENT_DOWNLOADS); - const promises = []; - branches.map((branch) => { - promises.push(new Promise(resolve => { - const slug = branch.ref.split("refs/heads/cms/").pop(); - return sem.take(() => this.api.readUnpublishedBranchFile(slug).then((data) => { - if (data === null || data === undefined) { - resolve(null); - sem.leave(); - } else { - const path = data.metaData.objects.entry.path; - resolve({ - slug, - file: { path }, - data: data.fileData, - metaData: data.metaData, - isModification: data.isModification, - }); - sem.leave(); - } - }).catch(() => { - sem.leave(); - resolve(null); - })); - })); + return this.api + .listUnpublishedBranches() + .then(branches => { + const sem = semaphore(MAX_CONCURRENT_DOWNLOADS); + const promises = []; + branches.map(branch => { + promises.push( + new Promise(resolve => { + const slug = branch.ref.split('refs/heads/cms/').pop(); + return sem.take(() => + this.api + .readUnpublishedBranchFile(slug) + .then(data => { + if (data === null || data === undefined) { + resolve(null); + sem.leave(); + } else { + const path = data.metaData.objects.entry.path; + resolve({ + slug, + file: { path }, + data: data.fileData, + metaData: data.metaData, + isModification: data.isModification, + }); + sem.leave(); + } + }) + .catch(() => { + sem.leave(); + resolve(null); + }), + ); + }), + ); + }); + return Promise.all(promises); + }) + .catch(error => { + if (error.message === 'Not Found') { + return Promise.resolve([]); + } + return error; }); - return Promise.all(promises); - }) - .catch((error) => { - if (error.message === "Not Found") { - return Promise.resolve([]); - } - return error; - }); } unpublishedEntry(collection, slug) { - return this.api.readUnpublishedBranchFile(slug) - .then((data) => { + return this.api.readUnpublishedBranchFile(slug).then(data => { if (!data) return null; return { slug, diff --git a/packages/netlify-cms-backend-github/src/index.js b/packages/netlify-cms-backend-github/src/index.js index 8c20cced..6ccfbd0f 100644 --- a/packages/netlify-cms-backend-github/src/index.js +++ b/packages/netlify-cms-backend-github/src/index.js @@ -1,4 +1,3 @@ export GitHubBackend from './implementation'; export API from './API'; export AuthenticationPage from './AuthenticationPage'; - diff --git a/packages/netlify-cms-backend-gitlab/src/API.js b/packages/netlify-cms-backend-gitlab/src/API.js index 0ccf901e..1690a57a 100644 --- a/packages/netlify-cms-backend-gitlab/src/API.js +++ b/packages/netlify-cms-backend-gitlab/src/API.js @@ -1,105 +1,119 @@ -import { localForage, unsentRequest, then, APIError, Cursor } from "netlify-cms-lib-util"; -import { Base64 } from "js-base64"; -import { List, Map } from "immutable"; -import { flow, partial, pick, get, result } from "lodash"; +import { localForage, unsentRequest, then, APIError, Cursor } from 'netlify-cms-lib-util'; +import { Base64 } from 'js-base64'; +import { List, Map } from 'immutable'; +import { flow, partial, result } from 'lodash'; export default class API { constructor(config) { - this.api_root = config.api_root || "https://gitlab.com/api/v4"; + this.api_root = config.api_root || 'https://gitlab.com/api/v4'; this.token = config.token || false; - this.branch = config.branch || "master"; - this.repo = config.repo || ""; - this.repoURL = `/projects/${ encodeURIComponent(this.repo) }`; + this.branch = config.branch || 'master'; + this.repo = config.repo || ''; + this.repoURL = `/projects/${encodeURIComponent(this.repo)}`; } - withAuthorizationHeaders = req => ( - unsentRequest.withHeaders(this.token ? { Authorization: `Bearer ${ this.token }` } : {}, req) - ); + withAuthorizationHeaders = req => + unsentRequest.withHeaders(this.token ? { Authorization: `Bearer ${this.token}` } : {}, req); - buildRequest = req => flow([ - unsentRequest.withRoot(this.api_root), - this.withAuthorizationHeaders, - unsentRequest.withTimestamp, - ])(req); + buildRequest = req => + flow([ + unsentRequest.withRoot(this.api_root), + this.withAuthorizationHeaders, + unsentRequest.withTimestamp, + ])(req); - request = async req => flow([ - this.buildRequest, - unsentRequest.performRequest, - p => p.catch(err => Promise.reject(new APIError(err.message, null, "GitLab"))), - ])(req); + request = async req => + flow([ + this.buildRequest, + unsentRequest.performRequest, + p => p.catch(err => Promise.reject(new APIError(err.message, null, 'GitLab'))), + ])(req); - parseResponse = async (res, { expectingOk=true, expectingFormat=false }) => { - const contentType = res.headers.get("Content-Type"); - const isJSON = contentType === "application/json"; + parseResponse = async (res, { expectingOk = true, expectingFormat = false }) => { + const contentType = res.headers.get('Content-Type'); + const isJSON = contentType === 'application/json'; let body; try { - body = await ((expectingFormat === "json" || isJSON) ? res.json() : res.text()); + body = await (expectingFormat === 'json' || isJSON ? res.json() : res.text()); } catch (err) { - throw new APIError(err.message, res.status, "GitLab"); + throw new APIError(err.message, res.status, 'GitLab'); } if (expectingOk && !res.ok) { - throw new APIError((isJSON && body.message) ? body.message : body, res.status, "GitLab"); + throw new APIError(isJSON && body.message ? body.message : body, res.status, 'GitLab'); } return body; }; - responseToJSON = res => this.parseResponse(res, { expectingFormat: "json" }); - responseToText = res => this.parseResponse(res, { expectingFormat: "text" }); + responseToJSON = res => this.parseResponse(res, { expectingFormat: 'json' }); + responseToText = res => this.parseResponse(res, { expectingFormat: 'text' }); requestJSON = req => this.request(req).then(this.responseToJSON); requestText = req => this.request(req).then(this.responseToText); - user = () => this.requestJSON("/user"); + user = () => this.requestJSON('/user'); WRITE_ACCESS = 30; - hasWriteAccess = () => this.requestJSON(this.repoURL).then(({ permissions }) => { - const { project_access, group_access } = permissions; - if (project_access && (project_access.access_level >= this.WRITE_ACCESS)) { - return true; - } - if (group_access && (group_access.access_level >= this.WRITE_ACCESS)) { - return true; - } - return false; - }); - - readFile = async (path, sha, ref=this.branch) => { - const cachedFile = sha ? await localForage.getItem(`gl.${ sha }`) : null; - if (cachedFile) { return cachedFile; } - const result = await this.requestText({ - url: `${ this.repoURL }/repository/files/${ encodeURIComponent(path) }/raw`, - params: { ref }, - cache: "no-store", + hasWriteAccess = () => + this.requestJSON(this.repoURL).then(({ permissions }) => { + const { project_access, group_access } = permissions; + if (project_access && project_access.access_level >= this.WRITE_ACCESS) { + return true; + } + if (group_access && group_access.access_level >= this.WRITE_ACCESS) { + return true; + } + return false; }); - if (sha) { localForage.setItem(`gl.${ sha }`, result) } + + readFile = async (path, sha, ref = this.branch) => { + const cachedFile = sha ? await localForage.getItem(`gl.${sha}`) : null; + if (cachedFile) { + return cachedFile; + } + const result = await this.requestText({ + url: `${this.repoURL}/repository/files/${encodeURIComponent(path)}/raw`, + params: { ref }, + cache: 'no-store', + }); + if (sha) { + localForage.setItem(`gl.${sha}`, result); + } return result; }; - fileDownloadURL = (path, ref=this.branch) => unsentRequest.toURL(this.buildRequest({ - url: `${ this.repoURL }/repository/files/${ encodeURIComponent(path) }/raw`, - params: { ref }, - })); + fileDownloadURL = (path, ref = this.branch) => + unsentRequest.toURL( + this.buildRequest({ + url: `${this.repoURL}/repository/files/${encodeURIComponent(path)}/raw`, + params: { ref }, + }), + ); getCursorFromHeaders = headers => { // indices and page counts are assumed to be zero-based, but the // indices and page counts returned from GitLab are one-based - const index = parseInt(headers.get("X-Page"), 10) - 1; - const pageCount = parseInt(headers.get("X-Total-Pages"), 10) - 1; - const pageSize = parseInt(headers.get("X-Per-Page"), 10); - const count = parseInt(headers.get("X-Total"), 10); - const linksRaw = headers.get("Link"); - const links = List(linksRaw.split(",")) - .map(str => str.trim().split(";")) + const index = parseInt(headers.get('X-Page'), 10) - 1; + const pageCount = parseInt(headers.get('X-Total-Pages'), 10) - 1; + const pageSize = parseInt(headers.get('X-Per-Page'), 10); + const count = parseInt(headers.get('X-Total'), 10); + const linksRaw = headers.get('Link'); + const links = List(linksRaw.split(',')) + .map(str => str.trim().split(';')) .map(([linkStr, keyStr]) => [ keyStr.match(/rel="(.*?)"/)[1], unsentRequest.fromURL(linkStr.trim().match(/<(.*?)>/)[1]), ]) .update(list => Map(list)); - const actions = links.keySeq().flatMap(key => ( - (key === "prev" && index > 0) || - (key === "next" && index < pageCount) || - (key === "first" && index > 0) || - (key === "last" && index < pageCount) - ) ? [key] : []); + const actions = links + .keySeq() + .flatMap( + key => + (key === 'prev' && index > 0) || + (key === 'next' && index < pageCount) || + (key === 'first' && index > 0) || + (key === 'last' && index < pageCount) + ? [key] + : [], + ); return Cursor.create({ actions, meta: { index, count, pageSize, pageCount }, @@ -111,36 +125,42 @@ export default class API { // Gets a cursor without retrieving the entries by using a HEAD // request - fetchCursor = req => flow([unsentRequest.withMethod("HEAD"), this.request, then(this.getCursor)])(req); - fetchCursorAndEntries = req => flow([ - unsentRequest.withMethod("GET"), - this.request, - p => Promise.all([p.then(this.getCursor), p.then(this.responseToJSON)]), - then(([cursor, entries]) => ({ cursor, entries })), - ])(req); + fetchCursor = req => + flow([unsentRequest.withMethod('HEAD'), this.request, then(this.getCursor)])(req); + fetchCursorAndEntries = req => + flow([ + unsentRequest.withMethod('GET'), + this.request, + p => Promise.all([p.then(this.getCursor), p.then(this.responseToJSON)]), + then(([cursor, entries]) => ({ cursor, entries })), + ])(req); fetchRelativeCursor = async (cursor, action) => this.fetchCursor(cursor.data.links[action]); reversableActions = Map({ - first: "last", - last: "first", - next: "prev", - prev: "next", + first: 'last', + last: 'first', + next: 'prev', + prev: 'next', }); reverseCursor = cursor => { - const pageCount = cursor.meta.get("pageCount", 0); - const currentIndex = cursor.meta.get("index", 0); + const pageCount = cursor.meta.get('pageCount', 0); + const currentIndex = cursor.meta.get('index', 0); const newIndex = pageCount - currentIndex; - const links = cursor.data.get("links", Map()); + const links = cursor.data.get('links', Map()); const reversedLinks = links.mapEntries(([k, v]) => [this.reversableActions.get(k) || k, v]); - const reversedActions = cursor.actions.map(action => this.reversableActions.get(action) || action); + const reversedActions = cursor.actions.map( + action => this.reversableActions.get(action) || action, + ); - return cursor.updateStore(store => store - .setIn(["meta", "index"], newIndex) - .setIn(["data", "links"], reversedLinks) - .set("actions", reversedActions)); + return cursor.updateStore(store => + store + .setIn(['meta', 'index'], newIndex) + .setIn(['data', 'links'], reversedLinks) + .set('actions', reversedActions), + ); }; // The exported listFiles and traverseCursor reverse the direction @@ -151,16 +171,19 @@ export default class API { // refactored. listFiles = async path => { const firstPageCursor = await this.fetchCursor({ - url: `${ this.repoURL }/repository/tree`, + url: `${this.repoURL}/repository/tree`, params: { path, ref: this.branch }, }); - const lastPageLink = firstPageCursor.data.getIn(["links", "last"]); + const lastPageLink = firstPageCursor.data.getIn(['links', 'last']); const { entries, cursor } = await this.fetchCursorAndEntries(lastPageLink); - return { files: entries.filter(({ type }) => type === "blob").reverse(), cursor: this.reverseCursor(cursor) }; + return { + files: entries.filter(({ type }) => type === 'blob').reverse(), + cursor: this.reverseCursor(cursor), + }; }; traverseCursor = async (cursor, action) => { - const link = cursor.data.getIn(["links", action]); + const link = cursor.data.getIn(['links', action]); const { entries, cursor: newCursor } = await this.fetchCursorAndEntries(link); return { entries: entries.reverse(), cursor: this.reverseCursor(newCursor) }; }; @@ -168,28 +191,31 @@ export default class API { listAllFiles = async path => { const entries = []; let { cursor, entries: initialEntries } = await this.fetchCursorAndEntries({ - url: `${ this.repoURL }/repository/tree`, + url: `${this.repoURL}/repository/tree`, // Get the maximum number of entries per page params: { path, ref: this.branch, per_page: 100 }, }); entries.push(...initialEntries); - while (cursor && cursor.actions.has("next")) { - const link = cursor.data.getIn(["links", "next"]); + while (cursor && cursor.actions.has('next')) { + const link = cursor.data.getIn(['links', 'next']); const { cursor: newCursor, entries: newEntries } = await this.fetchCursorAndEntries(link); entries.push(...newEntries); cursor = newCursor; } - return entries.filter(({ type }) => type === "blob"); + return entries.filter(({ type }) => type === 'blob'); }; toBase64 = str => Promise.resolve(Base64.encode(str)); fromBase64 = str => Base64.decode(str); - uploadAndCommit = async (item, { commitMessage, updateFile = false, branch = this.branch, author = this.commitAuthor }) => { + uploadAndCommit = async ( + item, + { commitMessage, updateFile = false, branch = this.branch, author = this.commitAuthor }, + ) => { const content = await result(item, 'toBase64', partial(this.toBase64, item.raw)); - const file_path = item.path.replace(/^\//, ""); - const action = (updateFile ? "update" : "create"); - const encoding = "base64"; - + const file_path = item.path.replace(/^\//, ''); + const action = updateFile ? 'update' : 'create'; + const encoding = 'base64'; + const commitParams = { branch, commit_message: commitMessage, @@ -202,9 +228,9 @@ export default class API { } await this.request({ - url: `${ this.repoURL }/repository/commits`, - method: "POST", - headers: { "Content-Type": "application/json" }, + url: `${this.repoURL}/repository/commits`, + method: 'POST', + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(commitParams), }); @@ -212,7 +238,11 @@ export default class API { }; persistFiles = (files, { commitMessage, newEntry }) => - Promise.all(files.map(file => this.uploadAndCommit(file, { commitMessage, updateFile: newEntry === false }))); + Promise.all( + files.map(file => + this.uploadAndCommit(file, { commitMessage, updateFile: newEntry === false }), + ), + ); deleteFile = (path, commit_message, options = {}) => { const branch = options.branch || this.branch; @@ -223,10 +253,10 @@ export default class API { commitParams.author_email = email; } return flow([ - unsentRequest.withMethod("DELETE"), + unsentRequest.withMethod('DELETE'), // TODO: only send author params if they are defined. unsentRequest.withParams(commitParams), this.request, - ])(`${ this.repoURL }/repository/files/${ encodeURIComponent(path) }`); + ])(`${this.repoURL}/repository/files/${encodeURIComponent(path)}`); }; } diff --git a/packages/netlify-cms-backend-gitlab/src/AuthenticationPage.js b/packages/netlify-cms-backend-gitlab/src/AuthenticationPage.js index 924fed65..b4a4761f 100644 --- a/packages/netlify-cms-backend-gitlab/src/AuthenticationPage.js +++ b/packages/netlify-cms-backend-gitlab/src/AuthenticationPage.js @@ -6,7 +6,7 @@ import { AuthenticationPage, Icon } from 'netlify-cms-ui-default'; const LoginButtonIcon = styled(Icon)` margin-right: 18px; -` +`; export default class GitLabAuthenticationPage extends React.Component { static propTypes = { @@ -18,9 +18,9 @@ export default class GitLabAuthenticationPage extends React.Component { componentDidMount() { const authType = this.props.config.getIn(['backend', 'auth_type']); - if (authType === "implicit") { + if (authType === 'implicit') { this.auth = new ImplicitAuthenticator({ - base_url: this.props.config.getIn(['backend', 'base_url'], "https://gitlab.com"), + base_url: this.props.config.getIn(['backend', 'base_url'], 'https://gitlab.com'), auth_endpoint: this.props.config.getIn(['backend', 'auth_endpoint'], 'oauth/authorize'), app_id: this.props.config.getIn(['backend', 'app_id']), clearHash: this.props.clearHash, @@ -36,13 +36,16 @@ export default class GitLabAuthenticationPage extends React.Component { } else { this.auth = new NetlifyAuthenticator({ base_url: this.props.base_url, - site_id: (document.location.host.split(':')[0] === 'localhost') ? 'cms.netlify.com' : this.props.siteId, + site_id: + document.location.host.split(':')[0] === 'localhost' + ? 'cms.netlify.com' + : this.props.siteId, auth_endpoint: this.props.authEndpoint, }); } } - handleLogin = (e) => { + handleLogin = e => { e.preventDefault(); this.auth.authenticate({ provider: 'gitlab', scope: 'api' }, (err, data) => { if (err) { @@ -62,7 +65,7 @@ export default class GitLabAuthenticationPage extends React.Component { loginErrorMessage={this.state.loginError} renderButtonContent={() => ( - {inProgress ? "Logging in..." : "Login with GitLab"} + {inProgress ? 'Logging in...' : 'Login with GitLab'} )} /> diff --git a/packages/netlify-cms-backend-gitlab/src/implementation.js b/packages/netlify-cms-backend-gitlab/src/implementation.js index 63de76eb..aee54215 100644 --- a/packages/netlify-cms-backend-gitlab/src/implementation.js +++ b/packages/netlify-cms-backend-gitlab/src/implementation.js @@ -1,8 +1,8 @@ import trimStart from 'lodash/trimStart'; -import semaphore from "semaphore"; +import semaphore from 'semaphore'; import { CURSOR_COMPATIBILITY_SYMBOL } from 'netlify-cms-lib-util'; -import AuthenticationPage from "./AuthenticationPage"; -import API from "./API"; +import AuthenticationPage from './AuthenticationPage'; +import API from './API'; const MAX_CONCURRENT_DOWNLOADS = 10; @@ -16,18 +16,18 @@ export default class GitLab { }; if (this.options.useWorkflow) { - throw new Error("The GitLab backend does not support the Editorial Workflow.") + throw new Error('The GitLab backend does not support the Editorial Workflow.'); } - if (!this.options.proxied && config.getIn(["backend", "repo"]) == null) { - throw new Error("The GitLab backend needs a \"repo\" in the backend configuration."); + if (!this.options.proxied && config.getIn(['backend', 'repo']) == null) { + throw new Error('The GitLab backend needs a "repo" in the backend configuration.'); } this.api = this.options.API || null; - this.repo = config.getIn(["backend", "repo"], ""); - this.branch = config.getIn(["backend", "branch"], "master"); - this.api_root = config.getIn(["backend", "api_root"], "https://gitlab.com/api/v4"); + this.repo = config.getIn(['backend', 'repo'], ''); + this.branch = config.getIn(['backend', 'branch'], 'master'); + this.api_root = config.getIn(['backend', 'api_root'], 'https://gitlab.com/api/v4'); this.token = ''; } @@ -41,14 +41,20 @@ export default class GitLab { authenticate(state) { this.token = state.token; - this.api = new API({ token: this.token, branch: this.branch, repo: this.repo, api_root: this.api_root }); + this.api = new API({ + token: this.token, + branch: this.branch, + repo: this.repo, + api_root: this.api_root, + }); return this.api.user().then(user => - this.api.hasWriteAccess(user).then((isCollab) => { + this.api.hasWriteAccess(user).then(isCollab => { // Unauthorized user - if (!isCollab) throw new Error("Your GitLab user account does not have access to this repo."); + if (!isCollab) + throw new Error('Your GitLab user account does not have access to this repo.'); // Authorized user return Object.assign({}, user, { token: state.token }); - }) + }), ); } @@ -62,32 +68,27 @@ export default class GitLab { } entriesByFolder(collection, extension) { - return this.api.listFiles(collection.get("folder")) - .then(({ files, cursor }) => - this.fetchFiles( - files.filter(file => file.name.endsWith('.' + extension)) - ) - .then(fetchedFiles => { - const returnedFiles = fetchedFiles; - returnedFiles[CURSOR_COMPATIBILITY_SYMBOL] = cursor; - return returnedFiles; - }) + return this.api.listFiles(collection.get('folder')).then(({ files, cursor }) => + this.fetchFiles(files.filter(file => file.name.endsWith('.' + extension))).then( + fetchedFiles => { + const returnedFiles = fetchedFiles; + returnedFiles[CURSOR_COMPATIBILITY_SYMBOL] = cursor; + return returnedFiles; + }, + ), ); } allEntriesByFolder(collection, extension) { - return this.api.listAllFiles(collection.get("folder")) - .then(files => - this.fetchFiles( - files.filter(file => file.name.endsWith('.' + extension)) - ) - ); + return this.api + .listAllFiles(collection.get('folder')) + .then(files => this.fetchFiles(files.filter(file => file.name.endsWith('.' + extension)))); } entriesByFiles(collection) { - const files = collection.get("files").map(collectionFile => ({ - path: collectionFile.get("file"), - label: collectionFile.get("label"), + const files = collection.get('files').map(collectionFile => ({ + path: collectionFile.get('file'), + label: collectionFile.get('label'), })); return this.fetchFiles(files).then(fetchedFiles => { const returnedFiles = fetchedFiles; @@ -95,23 +96,31 @@ export default class GitLab { }); } - fetchFiles = (files) => { + fetchFiles = files => { const sem = semaphore(MAX_CONCURRENT_DOWNLOADS); const promises = []; - files.forEach((file) => { - promises.push(new Promise(resolve => ( - sem.take(() => this.api.readFile(file.path, file.id).then((data) => { - resolve({ file, data }); - sem.leave(); - }).catch((error = true) => { - sem.leave(); - console.error(`failed to load file from GitLab: ${ file.path }`); - resolve({ error }); - })) - ))); + files.forEach(file => { + promises.push( + new Promise(resolve => + sem.take(() => + this.api + .readFile(file.path, file.id) + .then(data => { + resolve({ file, data }); + sem.leave(); + }) + .catch((error = true) => { + sem.leave(); + console.error(`failed to load file from GitLab: ${file.path}`); + resolve({ error }); + }), + ), + ), + ); }); - return Promise.all(promises) - .then(loadedEntries => loadedEntries.filter(loadedEntry => !loadedEntry.error)); + return Promise.all(promises).then(loadedEntries => + loadedEntries.filter(loadedEntry => !loadedEntry.error), + ); }; // Fetches a single entry. @@ -123,17 +132,17 @@ export default class GitLab { } getMedia() { - return this.api.listAllFiles(this.config.get('media_folder')) - .then(files => files.map(({ id, name, path }) => { + return this.api.listAllFiles(this.config.get('media_folder')).then(files => + files.map(({ id, name, path }) => { const url = new URL(this.api.fileDownloadURL(path)); if (url.pathname.match(/.svg$/)) { url.search += (url.search.slice(1) === '' ? '?' : '&') + 'sanitize=true'; } return { id, name, url: url.href, path }; - })); + }), + ); } - async persistEntry(entry, mediaFiles, options = {}) { return this.api.persistFiles([entry], options); } @@ -150,10 +159,11 @@ export default class GitLab { } traverseCursor(cursor, action) { - return this.api.traverseCursor(cursor, action) - .then(async ({ entries, cursor: newCursor }) => ({ - entries: await Promise.all(entries.map(file => this.api.readFile(file.path, file.id).then(data => ({ file, data })))), - cursor: newCursor, - })); + return this.api.traverseCursor(cursor, action).then(async ({ entries, cursor: newCursor }) => ({ + entries: await Promise.all( + entries.map(file => this.api.readFile(file.path, file.id).then(data => ({ file, data }))), + ), + cursor: newCursor, + })); } } diff --git a/packages/netlify-cms-backend-gitlab/src/index.js b/packages/netlify-cms-backend-gitlab/src/index.js index 25130502..a87fc489 100644 --- a/packages/netlify-cms-backend-gitlab/src/index.js +++ b/packages/netlify-cms-backend-gitlab/src/index.js @@ -1,4 +1,3 @@ export GitLabBackend from './implementation'; export API from './API'; export AuthenticationPage from './AuthenticationPage'; - diff --git a/packages/netlify-cms-backend-test/src/AuthenticationPage.js b/packages/netlify-cms-backend-test/src/AuthenticationPage.js index 7f97dab7..7b0fed5c 100644 --- a/packages/netlify-cms-backend-test/src/AuthenticationPage.js +++ b/packages/netlify-cms-backend-test/src/AuthenticationPage.js @@ -10,12 +10,12 @@ const StyledAuthenticationPage = styled.section` align-items: center; justify-content: center; height: 100vh; -` +`; const PageLogoIcon = styled(Icon)` color: #c4c6d2; margin-top: -300px; -` +`; const LoginButton = styled.button` ${buttons.button}; @@ -32,7 +32,7 @@ const LoginButton = styled.button` ${Icon} { margin-right: 18px; } -` +`; export default class AuthenticationPage extends React.Component { static propTypes = { @@ -51,7 +51,7 @@ export default class AuthenticationPage extends React.Component { } } - handleLogin = (e) => { + handleLogin = e => { e.preventDefault(); this.props.onLogin(this.state); }; @@ -61,9 +61,9 @@ export default class AuthenticationPage extends React.Component { return ( - + - {inProgress ? "Logging in..." : "Login"} + {inProgress ? 'Logging in...' : 'Login'} ); diff --git a/packages/netlify-cms-backend-test/src/implementation.js b/packages/netlify-cms-backend-test/src/implementation.js index 1651802f..0e54a383 100644 --- a/packages/netlify-cms-backend-test/src/implementation.js +++ b/packages/netlify-cms-backend-test/src/implementation.js @@ -23,8 +23,8 @@ const getCursor = (collection, extension, entries, index) => { const pageCount = Math.floor(count / pageSize); return Cursor.create({ actions: [ - ...(index < pageCount ? ["next", "last"] : []), - ...(index > 0 ? ["prev", "first"] : []), + ...(index < pageCount ? ['next', 'last'] : []), + ...(index > 0 ? ['prev', 'first'] : []), ], meta: { index, count, pageSize, pageCount }, data: { collection, extension, index, pageCount }, @@ -33,9 +33,9 @@ const getCursor = (collection, extension, entries, index) => { const getFolderEntries = (folder, extension) => { return Object.keys(window.repoFiles[folder] || {}) - .filter(path => path.endsWith(`.${ extension }`)) + .filter(path => path.endsWith(`.${extension}`)) .map(path => ({ - file: { path: `${ folder }/${ path }` }, + file: { path: `${folder}/${path}` }, data: window.repoFiles[folder][path].content, })) .reverse(); @@ -71,14 +71,22 @@ export default class TestRepo { traverseCursor(cursor, action) { const { collection, extension, index, pageCount } = cursor.data.toObject(); const newIndex = (() => { - if (action === "next") { return index + 1; } - if (action === "prev") { return index - 1; } - if (action === "first") { return 0; } - if (action === "last") { return pageCount; } + if (action === 'next') { + return index + 1; + } + if (action === 'prev') { + return index - 1; + } + if (action === 'first') { + return 0; + } + if (action === 'last') { + return pageCount; + } })(); // TODO: stop assuming cursors are for collections const allEntries = getFolderEntries(collection.get('folder'), extension); - const entries = allEntries.slice(newIndex * pageSize, (newIndex * pageSize) + pageSize); + const entries = allEntries.slice(newIndex * pageSize, newIndex * pageSize + pageSize); const newCursor = getCursor(collection, extension, allEntries, newIndex); return Promise.resolve({ entries, cursor: newCursor }); } @@ -97,10 +105,12 @@ export default class TestRepo { path: collectionFile.get('file'), label: collectionFile.get('label'), })); - return Promise.all(files.map(file => ({ - file, - data: getFile(file.path).content, - }))); + return Promise.all( + files.map(file => ({ + file, + data: getFile(file.path).content, + })), + ); } getEntry(collection, slug, path) { @@ -115,20 +125,22 @@ export default class TestRepo { } unpublishedEntry(collection, slug) { - const entry = window.repoFilesUnpublished.find(e => ( - e.metaData.collection === collection.get('name') && e.slug === slug - )); + const entry = window.repoFilesUnpublished.find( + e => e.metaData.collection === collection.get('name') && e.slug === slug, + ); if (!entry) { - return Promise.reject(new EditorialWorkflowError('content is not under editorial workflow', true)); + return Promise.reject( + new EditorialWorkflowError('content is not under editorial workflow', true), + ); } return Promise.resolve(entry); } deleteUnpublishedEntry(collection, slug) { const unpubStore = window.repoFilesUnpublished; - const existingEntryIndex = unpubStore.findIndex(e => ( - e.metaData.collection === collection && e.slug === slug - )); + const existingEntryIndex = unpubStore.findIndex( + e => e.metaData.collection === collection && e.slug === slug, + ); unpubStore.splice(existingEntryIndex, 1); return Promise.resolve(); } @@ -176,18 +188,18 @@ export default class TestRepo { updateUnpublishedEntryStatus(collection, slug, newStatus) { const unpubStore = window.repoFilesUnpublished; - const entryIndex = unpubStore.findIndex(e => ( - e.metaData.collection === collection && e.slug === slug - )); + const entryIndex = unpubStore.findIndex( + e => e.metaData.collection === collection && e.slug === slug, + ); unpubStore[entryIndex].metaData.status = newStatus; return Promise.resolve(); } publishUnpublishedEntry(collection, slug) { const unpubStore = window.repoFilesUnpublished; - const unpubEntryIndex = unpubStore.findIndex(e => ( - e.metaData.collection === collection && e.slug === slug - )); + const unpubEntryIndex = unpubStore.findIndex( + e => e.metaData.collection === collection && e.slug === slug, + ); const unpubEntry = unpubStore[unpubEntryIndex]; const entry = { raw: unpubEntry.data, slug: unpubEntry.slug, path: unpubEntry.file.path }; unpubStore.splice(unpubEntryIndex, 1); @@ -211,9 +223,7 @@ export default class TestRepo { const assetIndex = this.assets.findIndex(asset => asset.path === path); if (assetIndex > -1) { this.assets.splice(assetIndex, 1); - } - - else { + } else { const folder = path.substring(0, path.lastIndexOf('/')); const fileName = path.substring(path.lastIndexOf('/') + 1); delete window.repoFiles[folder][fileName]; diff --git a/packages/netlify-cms-core/babel.config.js b/packages/netlify-cms-core/babel.config.js index 1ce2617c..bf48e647 100644 --- a/packages/netlify-cms-core/babel.config.js +++ b/packages/netlify-cms-core/babel.config.js @@ -6,20 +6,23 @@ module.exports = { plugins: [ ...babelConfig.plugins, 'react-hot-loader/babel', - ['module-resolver', { - root: path.join(__dirname, 'src/components'), - alias: { - src: path.join(__dirname, 'src'), - Actions: path.join(__dirname, 'src/actions/'), - Constants: path.join(__dirname, 'src/constants/'), - Formats: path.join(__dirname, 'src/formats/'), - Integrations: path.join(__dirname, 'src/integrations/'), - Lib: path.join(__dirname, 'src/lib/'), - Reducers: path.join(__dirname, 'src/reducers/'), - Redux: path.join(__dirname, 'src/redux/'), - Routing: path.join(__dirname, 'src/routing/'), - ValueObjects: path.join(__dirname, 'src/valueObjects/'), - } - }], + [ + 'module-resolver', + { + root: path.join(__dirname, 'src/components'), + alias: { + src: path.join(__dirname, 'src'), + Actions: path.join(__dirname, 'src/actions/'), + Constants: path.join(__dirname, 'src/constants/'), + Formats: path.join(__dirname, 'src/formats/'), + Integrations: path.join(__dirname, 'src/integrations/'), + Lib: path.join(__dirname, 'src/lib/'), + Reducers: path.join(__dirname, 'src/reducers/'), + Redux: path.join(__dirname, 'src/redux/'), + Routing: path.join(__dirname, 'src/routing/'), + ValueObjects: path.join(__dirname, 'src/valueObjects/'), + }, + }, + ], ], }; diff --git a/packages/netlify-cms-core/src/actions/__tests__/config.spec.js b/packages/netlify-cms-core/src/actions/__tests__/config.spec.js index cc406139..fa55fd17 100644 --- a/packages/netlify-cms-core/src/actions/__tests__/config.spec.js +++ b/packages/netlify-cms-core/src/actions/__tests__/config.spec.js @@ -9,11 +9,7 @@ describe('config', () => { media_folder: 'path/to/media', public_folder: '/path/to/media', }); - expect( - applyDefaults(config) - ).toEqual( - config.set('publish_mode', 'simple') - ); + expect(applyDefaults(config)).toEqual(config.set('publish_mode', 'simple')); }); it('should set publish_mode from config', () => { @@ -23,36 +19,44 @@ describe('config', () => { media_folder: 'path/to/media', public_folder: '/path/to/media', }); - expect( - applyDefaults(config) - ).toEqual( - config - ); + expect(applyDefaults(config)).toEqual(config); }); it('should set public_folder based on media_folder if not set', () => { - expect(applyDefaults(fromJS({ - foo: 'bar', - media_folder: 'path/to/media', - }))).toEqual(fromJS({ - foo: 'bar', - publish_mode: 'simple', - media_folder: 'path/to/media', - public_folder: '/path/to/media', - })); + expect( + applyDefaults( + fromJS({ + foo: 'bar', + media_folder: 'path/to/media', + }), + ), + ).toEqual( + fromJS({ + foo: 'bar', + publish_mode: 'simple', + media_folder: 'path/to/media', + public_folder: '/path/to/media', + }), + ); }); it('should not overwrite public_folder if set', () => { - expect(applyDefaults(fromJS({ - foo: 'bar', - media_folder: 'path/to/media', - public_folder: '/publib/path', - }))).toEqual(fromJS({ - foo: 'bar', - publish_mode: 'simple', - media_folder: 'path/to/media', - public_folder: '/publib/path', - })); + expect( + applyDefaults( + fromJS({ + foo: 'bar', + media_folder: 'path/to/media', + public_folder: '/publib/path', + }), + ), + ).toEqual( + fromJS({ + foo: 'bar', + publish_mode: 'simple', + media_folder: 'path/to/media', + public_folder: '/publib/path', + }), + ); }); }); -}); \ No newline at end of file +}); diff --git a/packages/netlify-cms-core/src/actions/auth.js b/packages/netlify-cms-core/src/actions/auth.js index 62bd8d11..6392b167 100644 --- a/packages/netlify-cms-core/src/actions/auth.js +++ b/packages/netlify-cms-core/src/actions/auth.js @@ -48,15 +48,16 @@ export function authenticateUser() { const state = getState(); const backend = currentBackend(state.config); dispatch(authenticating()); - return backend.currentUser() - .then((user) => { + return backend + .currentUser() + .then(user => { if (user) { dispatch(authenticate(user)); } else { dispatch(doneAuthenticating()); } }) - .catch((error) => { + .catch(error => { dispatch(authError(error)); dispatch(logoutUser()); }); @@ -69,16 +70,19 @@ export function loginUser(credentials) { const backend = currentBackend(state.config); dispatch(authenticating()); - return backend.authenticate(credentials) - .then((user) => { + return backend + .authenticate(credentials) + .then(user => { dispatch(authenticate(user)); }) - .catch((error) => { - dispatch(notifSend({ - message: `${ error.message }`, - kind: 'warning', - dismissAfter: 8000, - })); + .catch(error => { + dispatch( + notifSend({ + message: `${error.message}`, + kind: 'warning', + dismissAfter: 8000, + }), + ); dispatch(authError(error)); }); }; diff --git a/packages/netlify-cms-core/src/actions/config.js b/packages/netlify-cms-core/src/actions/config.js index 344220f5..874e38e0 100644 --- a/packages/netlify-cms-core/src/actions/config.js +++ b/packages/netlify-cms-core/src/actions/config.js @@ -1,15 +1,14 @@ -import yaml from "js-yaml"; -import { Map, fromJS } from "immutable"; -import { trimStart, flow, get } from "lodash"; -import { authenticateUser } from "Actions/auth"; -import * as publishModes from "Constants/publishModes"; +import yaml from 'js-yaml'; +import { Map, fromJS } from 'immutable'; +import { trimStart, get } from 'lodash'; +import { authenticateUser } from 'Actions/auth'; +import * as publishModes from 'Constants/publishModes'; import { validateConfig } from 'Constants/configSchema'; -export const CONFIG_REQUEST = "CONFIG_REQUEST"; -export const CONFIG_SUCCESS = "CONFIG_SUCCESS"; -export const CONFIG_FAILURE = "CONFIG_FAILURE"; -export const CONFIG_MERGE = "CONFIG_MERGE"; - +export const CONFIG_REQUEST = 'CONFIG_REQUEST'; +export const CONFIG_SUCCESS = 'CONFIG_SUCCESS'; +export const CONFIG_FAILURE = 'CONFIG_FAILURE'; +export const CONFIG_MERGE = 'CONFIG_MERGE'; const getConfigUrl = () => { const validTypes = { 'text/yaml': 'yaml', 'application/x-yaml': 'yaml' }; @@ -21,7 +20,7 @@ const getConfigUrl = () => { return link; } return 'config.yml'; -} +}; const defaults = { publish_mode: publishModes.SIMPLE, @@ -48,8 +47,8 @@ function mergePreloadedConfig(preloadedConfig, loadedConfig) { function parseConfig(data) { const config = yaml.safeLoad(data); - if (typeof CMS_ENV === "string" && config[CMS_ENV]) { - Object.keys(config[CMS_ENV]).forEach((key) => { + if (typeof CMS_ENV === 'string' && config[CMS_ENV]) { + Object.keys(config[CMS_ENV]).forEach(key => { config[key] = config[CMS_ENV][key]; }); } @@ -60,12 +59,12 @@ async function getConfig(file, isPreloaded) { const response = await fetch(file, { credentials: 'same-origin' }); if (response.status !== 200) { if (isPreloaded) return parseConfig(''); - throw new Error(`Failed to load config.yml (${ response.status })`); + throw new Error(`Failed to load config.yml (${response.status})`); } const contentType = response.headers.get('Content-Type') || 'Not-Found'; const isYaml = contentType.indexOf('yaml') !== -1; if (!isYaml) { - console.log(`Response for ${ file } was not yaml. (Content-Type: ${ contentType })`); + console.log(`Response for ${file} was not yaml. (Content-Type: ${contentType})`); if (isPreloaded) return parseConfig(''); } return parseConfig(await response.text()); @@ -87,13 +86,13 @@ export function configLoading() { export function configFailed(err) { return { type: CONFIG_FAILURE, - error: "Error loading config", + error: 'Error loading config', payload: err, }; } export function configDidLoad(config) { - return (dispatch) => { + return dispatch => { dispatch(configLoaded(config)); }; } @@ -124,10 +123,9 @@ export function loadConfig() { dispatch(configDidLoad(config)); dispatch(authenticateUser()); - } - catch(err) { + } catch (err) { dispatch(configFailed(err)); - throw(err) + throw err; } }; } diff --git a/packages/netlify-cms-core/src/actions/editorialWorkflow.js b/packages/netlify-cms-core/src/actions/editorialWorkflow.js index ab256116..495d7a4c 100644 --- a/packages/netlify-cms-core/src/actions/editorialWorkflow.js +++ b/packages/netlify-cms-core/src/actions/editorialWorkflow.js @@ -56,7 +56,7 @@ function unpublishedEntryLoading(collection, slug) { function unpublishedEntryLoaded(collection, entry) { return { type: UNPUBLISHED_ENTRY_SUCCESS, - payload: { + payload: { collection: collection.get('name'), entry, }, @@ -66,7 +66,7 @@ function unpublishedEntryLoaded(collection, entry) { function unpublishedEntryRedirected(collection, slug) { return { type: UNPUBLISHED_ENTRY_REDIRECT, - payload: { + payload: { collection: collection.get('name'), slug, }, @@ -97,7 +97,6 @@ function unpublishedEntriesFailed(error) { }; } - function unpublishedEntryPersisting(collection, entry, transactionID) { return { type: UNPUBLISHED_ENTRY_PERSIST_REQUEST, @@ -112,7 +111,7 @@ function unpublishedEntryPersisting(collection, entry, transactionID) { function unpublishedEntryPersisted(collection, entry, transactionID, slug) { return { type: UNPUBLISHED_ENTRY_PERSIST_SUCCESS, - payload: { + payload: { collection: collection.get('name'), entry, slug, @@ -130,10 +129,16 @@ function unpublishedEntryPersistedFail(error, transactionID) { }; } -function unpublishedEntryStatusChangeRequest(collection, slug, oldStatus, newStatus, transactionID) { +function unpublishedEntryStatusChangeRequest( + collection, + slug, + oldStatus, + newStatus, + transactionID, +) { return { type: UNPUBLISHED_ENTRY_STATUS_CHANGE_REQUEST, - payload: { + payload: { collection, slug, oldStatus, @@ -143,10 +148,16 @@ function unpublishedEntryStatusChangeRequest(collection, slug, oldStatus, newSta }; } -function unpublishedEntryStatusChangePersisted(collection, slug, oldStatus, newStatus, transactionID) { +function unpublishedEntryStatusChangePersisted( + collection, + slug, + oldStatus, + newStatus, + transactionID, +) { return { type: UNPUBLISHED_ENTRY_STATUS_CHANGE_SUCCESS, - payload: { + payload: { collection, slug, oldStatus, @@ -223,20 +234,23 @@ export function loadUnpublishedEntry(collection, slug) { const state = getState(); const backend = currentBackend(state.config); dispatch(unpublishedEntryLoading(collection, slug)); - backend.unpublishedEntry(collection, slug) - .then(entry => dispatch(unpublishedEntryLoaded(collection, entry))) - .catch((error) => { - if (error instanceof EditorialWorkflowError && error.notUnderEditorialWorkflow) { - dispatch(unpublishedEntryRedirected(collection, slug)); - dispatch(loadEntry(collection, slug)); - } else { - dispatch(notifSend({ - message: `Error loading entry: ${ error }`, - kind: 'danger', - dismissAfter: 8000, - })); - } - }); + backend + .unpublishedEntry(collection, slug) + .then(entry => dispatch(unpublishedEntryLoaded(collection, entry))) + .catch(error => { + if (error instanceof EditorialWorkflowError && error.notUnderEditorialWorkflow) { + dispatch(unpublishedEntryRedirected(collection, slug)); + dispatch(loadEntry(collection, slug)); + } else { + dispatch( + notifSend({ + message: `Error loading entry: ${error}`, + kind: 'danger', + dismissAfter: 8000, + }), + ); + } + }); }; } @@ -246,16 +260,19 @@ export function loadUnpublishedEntries(collections) { if (state.config.get('publish_mode') !== EDITORIAL_WORKFLOW) return; const backend = currentBackend(state.config); dispatch(unpublishedEntriesLoading()); - backend.unpublishedEntries(collections) + backend + .unpublishedEntries(collections) .then(response => dispatch(unpublishedEntriesLoaded(response.entries, response.pagination))) .catch(error => { - dispatch(notifSend({ - message: `Error loading entries: ${ error }`, - kind: 'danger', - dismissAfter: 8000, - })); + dispatch( + notifSend({ + message: `Error loading entries: ${error}`, + kind: 'danger', + dismissAfter: 8000, + }), + ); dispatch(unpublishedEntriesFailed(error)); - Promise.reject(error) + Promise.reject(error); }); }; } @@ -268,17 +285,20 @@ export function persistUnpublishedEntry(collection, existingUnpublishedEntry) { // Early return if draft contains validation errors if (!fieldsErrors.isEmpty()) { - const hasPresenceErrors = fieldsErrors - .some(errors => errors.some(error => error.type && error.type === ValidationErrorTypes.PRESENCE)); + const hasPresenceErrors = fieldsErrors.some(errors => + errors.some(error => error.type && error.type === ValidationErrorTypes.PRESENCE), + ); if (hasPresenceErrors) { - dispatch(notifSend({ - message: 'Oops, you\'ve missed a required field. Please complete before saving.', - kind: 'danger', - dismissAfter: 8000, - })); + dispatch( + notifSend({ + message: "Oops, you've missed a required field. Please complete before saving.", + kind: 'danger', + dismissAfter: 8000, + }), + ); } - return Promise.reject() + return Promise.reject(); } const backend = currentBackend(state.config); @@ -296,7 +316,9 @@ export function persistUnpublishedEntry(collection, existingUnpublishedEntry) { const serializedEntryDraft = entryDraft.set('entry', serializedEntry); dispatch(unpublishedEntryPersisting(collection, serializedEntry, transactionID)); - const persistAction = existingUnpublishedEntry ? backend.persistUnpublishedEntry : backend.persistEntry; + const persistAction = existingUnpublishedEntry + ? backend.persistUnpublishedEntry + : backend.persistEntry; const persistCallArgs = [ backend, state.config, @@ -308,19 +330,22 @@ export function persistUnpublishedEntry(collection, existingUnpublishedEntry) { try { const newSlug = await persistAction.call(...persistCallArgs); - dispatch(notifSend({ - message: 'Entry saved', - kind: 'success', - dismissAfter: 4000, - })); + dispatch( + notifSend({ + message: 'Entry saved', + kind: 'success', + dismissAfter: 4000, + }), + ); dispatch(unpublishedEntryPersisted(collection, serializedEntry, transactionID, newSlug)); - } - catch(error) { - dispatch(notifSend({ - message: `Failed to persist entry: ${ error }`, - kind: 'danger', - dismissAfter: 8000, - })); + } catch (error) { + dispatch( + notifSend({ + message: `Failed to persist entry: ${error}`, + kind: 'danger', + dismissAfter: 8000, + }), + ); return Promise.reject(dispatch(unpublishedEntryPersistedFail(error, transactionID))); } }; @@ -331,24 +356,39 @@ export function updateUnpublishedEntryStatus(collection, slug, oldStatus, newSta const state = getState(); const backend = currentBackend(state.config); const transactionID = uuid(); - dispatch(unpublishedEntryStatusChangeRequest(collection, slug, oldStatus, newStatus, transactionID)); - backend.updateUnpublishedEntryStatus(collection, slug, newStatus) - .then(() => { - dispatch(notifSend({ - message: 'Entry status updated', - kind: 'success', - dismissAfter: 4000, - })); - dispatch(unpublishedEntryStatusChangePersisted(collection, slug, oldStatus, newStatus, transactionID)); - }) - .catch((error) => { - dispatch(notifSend({ - message: `Failed to update status: ${ error }`, - kind: 'danger', - dismissAfter: 8000, - })); - dispatch(unpublishedEntryStatusChangeError(collection, slug, transactionID)); - }); + dispatch( + unpublishedEntryStatusChangeRequest(collection, slug, oldStatus, newStatus, transactionID), + ); + backend + .updateUnpublishedEntryStatus(collection, slug, newStatus) + .then(() => { + dispatch( + notifSend({ + message: 'Entry status updated', + kind: 'success', + dismissAfter: 4000, + }), + ); + dispatch( + unpublishedEntryStatusChangePersisted( + collection, + slug, + oldStatus, + newStatus, + transactionID, + ), + ); + }) + .catch(error => { + dispatch( + notifSend({ + message: `Failed to update status: ${error}`, + kind: 'danger', + dismissAfter: 8000, + }), + ); + dispatch(unpublishedEntryStatusChangeError(collection, slug, transactionID)); + }); }; } @@ -358,23 +398,28 @@ export function deleteUnpublishedEntry(collection, slug) { const backend = currentBackend(state.config); const transactionID = uuid(); dispatch(unpublishedEntryDeleteRequest(collection, slug, transactionID)); - return backend.deleteUnpublishedEntry(collection, slug) - .then(() => { - dispatch(notifSend({ - message: 'Unpublished changes deleted', - kind: 'success', - dismissAfter: 4000, - })); - dispatch(unpublishedEntryDeleted(collection, slug, transactionID)); - }) - .catch((error) => { - dispatch(notifSend({ - message: `Failed to delete unpublished changes: ${ error }`, - kind: 'danger', - dismissAfter: 8000, - })); - dispatch(unpublishedEntryDeleteError(collection, slug, transactionID)); - }); + return backend + .deleteUnpublishedEntry(collection, slug) + .then(() => { + dispatch( + notifSend({ + message: 'Unpublished changes deleted', + kind: 'success', + dismissAfter: 4000, + }), + ); + dispatch(unpublishedEntryDeleted(collection, slug, transactionID)); + }) + .catch(error => { + dispatch( + notifSend({ + message: `Failed to delete unpublished changes: ${error}`, + kind: 'danger', + dismissAfter: 8000, + }), + ); + dispatch(unpublishedEntryDeleteError(collection, slug, transactionID)); + }); }; } @@ -384,22 +429,27 @@ export function publishUnpublishedEntry(collection, slug) { const backend = currentBackend(state.config); const transactionID = uuid(); dispatch(unpublishedEntryPublishRequest(collection, slug, transactionID)); - return backend.publishUnpublishedEntry(collection, slug) - .then(() => { - dispatch(notifSend({ - message: 'Entry published', - kind: 'success', - dismissAfter: 4000, - })); - dispatch(unpublishedEntryPublished(collection, slug, transactionID)); - }) - .catch((error) => { - dispatch(notifSend({ - message: `Failed to publish: ${ error }`, - kind: 'danger', - dismissAfter: 8000, - })); - dispatch(unpublishedEntryPublishError(collection, slug, transactionID)); - }); + return backend + .publishUnpublishedEntry(collection, slug) + .then(() => { + dispatch( + notifSend({ + message: 'Entry published', + kind: 'success', + dismissAfter: 4000, + }), + ); + dispatch(unpublishedEntryPublished(collection, slug, transactionID)); + }) + .catch(error => { + dispatch( + notifSend({ + message: `Failed to publish: ${error}`, + kind: 'danger', + dismissAfter: 8000, + }), + ); + dispatch(unpublishedEntryPublishError(collection, slug, transactionID)); + }); }; } diff --git a/packages/netlify-cms-core/src/actions/entries.js b/packages/netlify-cms-core/src/actions/entries.js index 61b48bcb..12c6c835 100644 --- a/packages/netlify-cms-core/src/actions/entries.js +++ b/packages/netlify-cms-core/src/actions/entries.js @@ -188,7 +188,6 @@ export function createDraftFromEntry(entry, metadata) { }; } - export function discardDraft() { return { type: DRAFT_DISCARD, @@ -216,7 +215,6 @@ export function changeDraftFieldValidation(field, errors) { }; } - /* * Exported Thunk Action Creators */ @@ -226,31 +224,33 @@ export function loadEntry(collection, slug) { const state = getState(); const backend = currentBackend(state.config); dispatch(entryLoading(collection, slug)); - return backend.getEntry(collection, slug) + return backend + .getEntry(collection, slug) .then(loadedEntry => { - return dispatch(entryLoaded(collection, loadedEntry)) + return dispatch(entryLoaded(collection, loadedEntry)); }) - .catch((error) => { + .catch(error => { console.error(error); - dispatch(notifSend({ - message: `Failed to load entry: ${ error.message }`, - kind: 'danger', - dismissAfter: 8000, - })); + dispatch( + notifSend({ + message: `Failed to load entry: ${error.message}`, + kind: 'danger', + dismissAfter: 8000, + }), + ); dispatch(entryLoadError(error, collection, slug)); }); }; } const appendActions = fromJS({ - ["append_next"]: { action: "next", append: true }, + ['append_next']: { action: 'next', append: true }, }); -const addAppendActionsToCursor = cursor => Cursor - .create(cursor) - .updateStore("actions", actions => actions.union( - appendActions.filter(v => actions.has(v.get("action"))).keySeq() - )); +const addAppendActionsToCursor = cursor => + Cursor.create(cursor).updateStore('actions', actions => + actions.union(appendActions.filter(v => actions.has(v.get('action'))).keySeq()), + ); export function loadEntries(collection, page = 0) { return (dispatch, getState) => { @@ -260,46 +260,59 @@ export function loadEntries(collection, page = 0) { const state = getState(); const backend = currentBackend(state.config); const integration = selectIntegration(state, collection.get('name'), 'listEntries'); - const provider = integration ? getIntegrationProvider(state.integrations, backend.getToken, integration) : backend; + const provider = integration + ? getIntegrationProvider(state.integrations, backend.getToken, integration) + : backend; const append = !!(page && !isNaN(page) && page > 0); dispatch(entriesLoading(collection)); - provider.listEntries(collection, page) - .then(response => ({ - ...response, + provider + .listEntries(collection, page) + .then(response => ({ + ...response, - // The only existing backend using the pagination system is the - // Algolia integration, which is also the only integration used - // to list entries. Thus, this checking for an integration can - // determine whether or not this is using the old integer-based - // pagination API. Other backends will simply store an empty - // cursor, which behaves identically to no cursor at all. - cursor: integration - ? Cursor.create({ actions: ["next"], meta: { usingOldPaginationAPI: true }, data: { nextPage: page + 1 } }) - : Cursor.create(response.cursor), - })) - .then(response => dispatch(entriesLoaded( - collection, - response.cursor.meta.get('usingOldPaginationAPI') - ? response.entries.reverse() - : response.entries, - response.pagination, - addAppendActionsToCursor(response.cursor), - append, - ))) - .catch(err => { - dispatch(notifSend({ - message: `Failed to load entries: ${ err }`, - kind: 'danger', - dismissAfter: 8000, - })); - return Promise.reject(dispatch(entriesFailed(collection, err))); - }); + // The only existing backend using the pagination system is the + // Algolia integration, which is also the only integration used + // to list entries. Thus, this checking for an integration can + // determine whether or not this is using the old integer-based + // pagination API. Other backends will simply store an empty + // cursor, which behaves identically to no cursor at all. + cursor: integration + ? Cursor.create({ + actions: ['next'], + meta: { usingOldPaginationAPI: true }, + data: { nextPage: page + 1 }, + }) + : Cursor.create(response.cursor), + })) + .then(response => + dispatch( + entriesLoaded( + collection, + response.cursor.meta.get('usingOldPaginationAPI') + ? response.entries.reverse() + : response.entries, + response.pagination, + addAppendActionsToCursor(response.cursor), + append, + ), + ), + ) + .catch(err => { + dispatch( + notifSend({ + message: `Failed to load entries: ${err}`, + kind: 'danger', + dismissAfter: 8000, + }), + ); + return Promise.reject(dispatch(entriesFailed(collection, err))); + }); }; } function traverseCursor(backend, cursor, action) { if (!cursor.actions.has(action)) { - throw new Error(`The current cursor does not support the pagination action "${ action }".`); + throw new Error(`The current cursor does not support the pagination action "${action}".`); } return backend.traverseCursor(cursor, action); } @@ -307,7 +320,7 @@ function traverseCursor(backend, cursor, action) { export function traverseCollectionCursor(collection, action) { return async (dispatch, getState) => { const state = getState(); - if (state.entries.getIn(['pages', `${ collection.get('name') }`, 'isFetching',])) { + if (state.entries.getIn(['pages', `${collection.get('name')}`, 'isFetching'])) { return; } const backend = currentBackend(state.config); @@ -319,8 +332,8 @@ export function traverseCollectionCursor(collection, action) { // Handle cursors representing pages in the old, integer-based // pagination API - if (cursor.meta.get("usingOldPaginationAPI", false)) { - return dispatch(loadEntries(collection, cursor.data.get("nextPage"))); + if (cursor.meta.get('usingOldPaginationAPI', false)) { + return dispatch(loadEntries(collection, cursor.data.get('nextPage'))); } try { @@ -328,23 +341,27 @@ export function traverseCollectionCursor(collection, action) { const { entries, cursor: newCursor } = await traverseCursor(backend, cursor, realAction); // Pass null for the old pagination argument - this will // eventually be removed. - return dispatch(entriesLoaded(collection, entries, null, addAppendActionsToCursor(newCursor), append)); + return dispatch( + entriesLoaded(collection, entries, null, addAppendActionsToCursor(newCursor), append), + ); } catch (err) { console.error(err); - dispatch(notifSend({ - message: `Failed to persist entry: ${ err }`, - kind: 'danger', - dismissAfter: 8000, - })); + dispatch( + notifSend({ + message: `Failed to persist entry: ${err}`, + kind: 'danger', + dismissAfter: 8000, + }), + ); return Promise.reject(dispatch(entriesFailed(collection, err))); } - } + }; } export function createEmptyDraft(collection) { - return (dispatch) => { + return dispatch => { const dataFields = {}; - collection.get('fields', List()).forEach((field) => { + collection.get('fields', List()).forEach(field => { dataFields[field.get('name')] = field.get('default'); }); const newEntry = createEntry(collection.get('name'), '', '', { data: dataFields }); @@ -360,15 +377,18 @@ export function persistEntry(collection) { // Early return if draft contains validation errors if (!fieldsErrors.isEmpty()) { - const hasPresenceErrors = fieldsErrors - .some(errors => errors.some(error => error.type && error.type === ValidationErrorTypes.PRESENCE)); + const hasPresenceErrors = fieldsErrors.some(errors => + errors.some(error => error.type && error.type === ValidationErrorTypes.PRESENCE), + ); if (hasPresenceErrors) { - dispatch(notifSend({ - message: 'Oops, you\'ve missed a required field. Please complete before saving.', - kind: 'danger', - dismissAfter: 8000, - })); + dispatch( + notifSend({ + message: "Oops, you've missed a required field. Please complete before saving.", + kind: 'danger', + dismissAfter: 8000, + }), + ); } return Promise.reject(); @@ -390,20 +410,24 @@ export function persistEntry(collection) { return backend .persistEntry(state.config, collection, serializedEntryDraft, assetProxies.toJS()) .then(slug => { - dispatch(notifSend({ - message: 'Entry saved', - kind: 'success', - dismissAfter: 4000, - })); - dispatch(entryPersisted(collection, serializedEntry, slug)) + dispatch( + notifSend({ + message: 'Entry saved', + kind: 'success', + dismissAfter: 4000, + }), + ); + dispatch(entryPersisted(collection, serializedEntry, slug)); }) - .catch((error) => { + .catch(error => { console.error(error); - dispatch(notifSend({ - message: `Failed to persist entry: ${ error }`, - kind: 'danger', - dismissAfter: 8000, - })); + dispatch( + notifSend({ + message: `Failed to persist entry: ${error}`, + kind: 'danger', + dismissAfter: 8000, + }), + ); return Promise.reject(dispatch(entryPersistFail(collection, serializedEntry, error))); }); }; @@ -415,18 +439,21 @@ export function deleteEntry(collection, slug) { const backend = currentBackend(state.config); dispatch(entryDeleting(collection, slug)); - return backend.deleteEntry(state.config, collection, slug) - .then(() => { - return dispatch(entryDeleted(collection, slug)); - }) - .catch((error) => { - dispatch(notifSend({ - message: `Failed to delete entry: ${ error }`, - kind: 'danger', - dismissAfter: 8000, - })); - console.error(error); - return Promise.reject(dispatch(entryDeleteFail(collection, slug, error))); - }); + return backend + .deleteEntry(state.config, collection, slug) + .then(() => { + return dispatch(entryDeleted(collection, slug)); + }) + .catch(error => { + dispatch( + notifSend({ + message: `Failed to delete entry: ${error}`, + kind: 'danger', + dismissAfter: 8000, + }), + ); + console.error(error); + return Promise.reject(dispatch(entryDeleteFail(collection, slug, error))); + }); }; } diff --git a/packages/netlify-cms-core/src/actions/mediaLibrary.js b/packages/netlify-cms-core/src/actions/mediaLibrary.js index 9dba2e3f..2cab5d03 100644 --- a/packages/netlify-cms-core/src/actions/mediaLibrary.js +++ b/packages/netlify-cms-core/src/actions/mediaLibrary.js @@ -4,7 +4,7 @@ import { createAssetProxy } from 'ValueObjects/AssetProxy'; import { selectIntegration } from 'Reducers'; import { getIntegrationProvider } from 'Integrations'; import { addAsset } from './media'; -import { sanitizeSlug } from "Lib/urlHelper"; +import { sanitizeSlug } from 'Lib/urlHelper'; const { notifSend } = notifActions; @@ -57,18 +57,20 @@ export function loadMedia(opts = {}) { privateUpload, }; return dispatch(mediaLoaded(files, mediaLoadedOpts)); - } - catch(error) { + } catch (error) { return dispatch(mediaLoadFailed({ privateUpload })); } } dispatch(mediaLoading(page)); return new Promise(resolve => { - setTimeout(() => resolve( - backend.getMedia() - .then(files => dispatch(mediaLoaded(files))) - .catch((error) => dispatch(error.status === 404 ? mediaLoaded() : mediaLoadFailed())) - )); + setTimeout(() => + resolve( + backend + .getMedia() + .then(files => dispatch(mediaLoaded(files))) + .catch(error => dispatch(error.status === 404 ? mediaLoaded() : mediaLoadFailed())), + ), + ); }, delay); }; } @@ -107,14 +109,15 @@ export function persistMedia(file, opts = {}) { return dispatch(mediaPersisted(asset)); } return dispatch(mediaPersisted(assetProxy.asset, { privateUpload })); - } - catch(error) { + } catch (error) { console.error(error); - dispatch(notifSend({ - message: `Failed to persist media: ${ error }`, - kind: 'danger', - dismissAfter: 8000, - })); + dispatch( + notifSend({ + message: `Failed to persist media: ${error}`, + kind: 'danger', + dismissAfter: 8000, + }), + ); return dispatch(mediaPersistFailed({ privateUpload })); } }; @@ -129,32 +132,38 @@ export function deleteMedia(file, opts = {}) { if (integration) { const provider = getIntegrationProvider(state.integrations, backend.getToken, integration); dispatch(mediaDeleting()); - return provider.delete(file.id) + return provider + .delete(file.id) .then(() => { return dispatch(mediaDeleted(file, { privateUpload })); }) .catch(error => { console.error(error); - dispatch(notifSend({ - message: `Failed to delete media: ${ error.message }`, - kind: 'danger', - dismissAfter: 8000, - })); + dispatch( + notifSend({ + message: `Failed to delete media: ${error.message}`, + kind: 'danger', + dismissAfter: 8000, + }), + ); return dispatch(mediaDeleteFailed({ privateUpload })); }); } dispatch(mediaDeleting()); - return backend.deleteMedia(state.config, file.path) + return backend + .deleteMedia(state.config, file.path) .then(() => { return dispatch(mediaDeleted(file)); }) .catch(error => { console.error(error); - dispatch(notifSend({ - message: `Failed to delete media: ${ error.message }`, - kind: 'danger', - dismissAfter: 8000, - })); + dispatch( + notifSend({ + message: `Failed to delete media: ${error.message}`, + kind: 'danger', + dismissAfter: 8000, + }), + ); return dispatch(mediaDeleteFailed()); }); }; @@ -164,13 +173,13 @@ export function mediaLoading(page) { return { type: MEDIA_LOAD_REQUEST, payload: { page }, - } + }; } export function mediaLoaded(files, opts = {}) { return { type: MEDIA_LOAD_SUCCESS, - payload: { files, ...opts } + payload: { files, ...opts }, }; } diff --git a/packages/netlify-cms-core/src/actions/search.js b/packages/netlify-cms-core/src/actions/search.js index 5e04db26..eacb0905 100644 --- a/packages/netlify-cms-core/src/actions/search.js +++ b/packages/netlify-cms-core/src/actions/search.js @@ -93,7 +93,6 @@ export function clearSearch() { return { type: SEARCH_CLEAR }; } - /* * Exported Thunk Action Creators */ @@ -106,16 +105,22 @@ export function searchEntries(searchTerm, page = 0) { const state = getState(); const backend = currentBackend(state.config); const allCollections = state.collections.keySeq().toArray(); - const collections = allCollections.filter(collection => selectIntegration(state, collection, 'search')); + const collections = allCollections.filter(collection => + selectIntegration(state, collection, 'search'), + ); const integration = selectIntegration(state, collections[0], 'search'); const searchPromise = integration - ? getIntegrationProvider(state.integrations, backend.getToken, integration).search(collections, searchTerm, page) + ? getIntegrationProvider(state.integrations, backend.getToken, integration).search( + collections, + searchTerm, + page, + ) : backend.search(state.collections.valueSeq().toArray(), searchTerm); return searchPromise.then( response => dispatch(searchSuccess(searchTerm, response.entries, response.pagination)), - error => dispatch(searchFailure(searchTerm, error)) + error => dispatch(searchFailure(searchTerm, error)), ); }; } @@ -129,16 +134,22 @@ export function query(namespace, collectionName, searchFields, searchTerm) { const state = getState(); const backend = currentBackend(state.config); const integration = selectIntegration(state, collectionName, 'search'); - const collection = state.collections.find(collection => collection.get('name') === collectionName); + const collection = state.collections.find( + collection => collection.get('name') === collectionName, + ); const queryPromise = integration - ? getIntegrationProvider(state.integrations, backend.getToken, integration) - .searchBy(searchFields.map(f => `data.${ f }`), collectionName, searchTerm) + ? getIntegrationProvider(state.integrations, backend.getToken, integration).searchBy( + searchFields.map(f => `data.${f}`), + collectionName, + searchTerm, + ) : backend.query(collection, searchFields, searchTerm); return queryPromise.then( - response => dispatch(querySuccess(namespace, collectionName, searchFields, searchTerm, response)), - error => dispatch(queryFailure(namespace, collectionName, searchFields, searchTerm, error)) + response => + dispatch(querySuccess(namespace, collectionName, searchFields, searchTerm, response)), + error => dispatch(queryFailure(namespace, collectionName, searchFields, searchTerm, error)), ); }; } diff --git a/packages/netlify-cms-core/src/backend.js b/packages/netlify-cms-core/src/backend.js index 1b7a3d49..7e2a700e 100644 --- a/packages/netlify-cms-core/src/backend.js +++ b/packages/netlify-cms-core/src/backend.js @@ -1,7 +1,7 @@ import { attempt, flatten, isError } from 'lodash'; import { Map } from 'immutable'; import fuzzy from 'fuzzy'; -import { resolveFormat } from "Formats/formats"; +import { resolveFormat } from 'Formats/formats'; import { selectIntegration } from 'Reducers/integrations'; import { selectListMethod, @@ -12,15 +12,15 @@ import { selectFolderEntryExtension, selectIdentifier, selectInferedField, -} from "Reducers/collections"; -import { createEntry } from "ValueObjects/Entry"; -import { sanitizeSlug } from "Lib/urlHelper"; +} from 'Reducers/collections'; +import { createEntry } from 'ValueObjects/Entry'; +import { sanitizeSlug } from 'Lib/urlHelper'; import { getBackend } from 'Lib/registry'; import { Cursor, CURSOR_COMPATIBILITY_SYMBOL } from 'netlify-cms-lib-util'; import { EDITORIAL_WORKFLOW, status } from 'Constants/publishModes'; class LocalStorageAuthStore { - storageKey = "netlify-cms-user"; + storageKey = 'netlify-cms-user'; retrieve() { const data = window.localStorage.getItem(this.storageKey); @@ -37,39 +37,40 @@ class LocalStorageAuthStore { } const slugFormatter = (collection, entryData, slugConfig) => { - const template = collection.get('slug') || "{{slug}}"; + const template = collection.get('slug') || '{{slug}}'; const date = new Date(); const identifier = entryData.get(selectIdentifier(collection)); if (!identifier) { - throw new Error("Collection must have a field name that is a valid entry identifier"); + throw new Error('Collection must have a field name that is a valid entry identifier'); } - const slug = template.replace(/\{\{([^}]+)\}\}/g, (_, field) => { - switch (field) { - case "year": - return date.getFullYear(); - case "month": - return (`0${ date.getMonth() + 1 }`).slice(-2); - case "day": - return (`0${ date.getDate() }`).slice(-2); - case "hour": - return (`0${ date.getHours() }`).slice(-2); - case "minute": - return (`0${ date.getMinutes() }`).slice(-2); - case "second": - return (`0${ date.getSeconds() }`).slice(-2); - case "slug": - return identifier.trim(); - default: - return entryData.get(field, "").trim(); - } - }) - // Convert slug to lower-case - .toLocaleLowerCase() + const slug = template + .replace(/\{\{([^}]+)\}\}/g, (_, field) => { + switch (field) { + case 'year': + return date.getFullYear(); + case 'month': + return `0${date.getMonth() + 1}`.slice(-2); + case 'day': + return `0${date.getDate()}`.slice(-2); + case 'hour': + return `0${date.getHours()}`.slice(-2); + case 'minute': + return `0${date.getMinutes()}`.slice(-2); + case 'second': + return `0${date.getSeconds()}`.slice(-2); + case 'slug': + return identifier.trim(); + default: + return entryData.get(field, '').trim(); + } + }) + // Convert slug to lower-case + .toLocaleLowerCase() - // Replace periods with dashes. - .replace(/[.]/g, '-'); + // Replace periods with dashes. + .replace(/[.]/g, '-'); return sanitizeSlug(slug, slugConfig); }; @@ -79,11 +80,13 @@ const commitMessageTemplates = Map({ update: 'Update {{collection}} “{{slug}}”', delete: 'Delete {{collection}} “{{slug}}”', uploadMedia: 'Upload “{{path}}”', - deleteMedia: 'Delete “{{path}}”' + deleteMedia: 'Delete “{{path}}”', }); const commitMessageFormatter = (type, config, { slug, path, collection }) => { - const templates = commitMessageTemplates.merge(config.getIn(['backend', 'commit_messages'], Map())); + const templates = commitMessageTemplates.merge( + config.getIn(['backend', 'commit_messages'], Map()), + ); const messageTemplate = templates.get(type); return messageTemplate.replace(/\{\{([^}]+)\}\}/g, (_, variable) => { switch (variable) { @@ -94,16 +97,17 @@ const commitMessageFormatter = (type, config, { slug, path, collection }) => { case 'collection': return collection.get('label'); default: - console.warn(`Ignoring unknown variable “${ variable }” in commit message template.`); + console.warn(`Ignoring unknown variable “${variable}” in commit message template.`); return ''; } }); -} +}; -const extractSearchFields = searchFields => entry => searchFields.reduce((acc, field) => { - const f = entry.data[field]; - return f ? `${acc} ${f}` : acc; -}, ""); +const extractSearchFields = searchFields => entry => + searchFields.reduce((acc, field) => { + const f = entry.data[field]; + return f ? `${acc} ${f}` : acc; + }, ''); const sortByScore = (a, b) => { if (a.score > b.score) return -1; @@ -114,23 +118,25 @@ const sortByScore = (a, b) => { class Backend { constructor(implementation, { backendName, authStore = null, config } = {}) { this.implementation = implementation.init(config, { - useWorkflow: config.getIn(["publish_mode"]) === EDITORIAL_WORKFLOW, + useWorkflow: config.getIn(['publish_mode']) === EDITORIAL_WORKFLOW, updateUserCredentials: this.updateUserCredentials, initialWorkflowStatus: status.first(), }); this.backendName = backendName; this.authStore = authStore; if (this.implementation === null) { - throw new Error("Cannot instantiate a Backend with no implementation"); + throw new Error('Cannot instantiate a Backend with no implementation'); } } currentUser() { - if (this.user) { return this.user; } + if (this.user) { + return this.user; + } const stored = this.authStore && this.authStore.retrieve(); if (stored && stored.backendName === this.backendName) { - return Promise.resolve(this.implementation.restoreUser(stored)).then((user) => { - const newUser = {...user, backendName: this.backendName}; + return Promise.resolve(this.implementation.restoreUser(stored)).then(user => { + const newUser = { ...user, backendName: this.backendName }; // return confirmed/rehydrated user object instead of stored this.authStore.store(newUser); return newUser; @@ -153,9 +159,11 @@ class Backend { } authenticate(credentials) { - return this.implementation.authenticate(credentials).then((user) => { - const newUser = {...user, backendName: this.backendName}; - if (this.authStore) { this.authStore.store(newUser); } + return this.implementation.authenticate(credentials).then(user => { + const newUser = { ...user, backendName: this.backendName }; + if (this.authStore) { + this.authStore.store(newUser); + } return newUser; }); } @@ -172,12 +180,14 @@ class Backend { processEntries(loadedEntries, collection) { const collectionFilter = collection.get('filter'); - const entries = loadedEntries.map(loadedEntry => createEntry( - collection.get("name"), - selectEntrySlug(collection, loadedEntry.file.path), - loadedEntry.file.path, - { raw: loadedEntry.data || '', label: loadedEntry.file.label } - )); + const entries = loadedEntries.map(loadedEntry => + createEntry( + collection.get('name'), + selectEntrySlug(collection, loadedEntry.file.path), + loadedEntry.file.path, + { raw: loadedEntry.data || '', label: loadedEntry.file.label }, + ), + ); const formattedEntries = entries.map(this.entryWithFormat(collection)); // If this collection has a "filter" property, filter entries accordingly const filteredEntries = collectionFilter @@ -186,23 +196,21 @@ class Backend { return filteredEntries; } - listEntries(collection) { const listMethod = this.implementation[selectListMethod(collection)]; const extension = selectFolderEntryExtension(collection); - return listMethod.call(this.implementation, collection, extension) - .then(loadedEntries => ({ - entries: this.processEntries(loadedEntries, collection), - /* + return listMethod.call(this.implementation, collection, extension).then(loadedEntries => ({ + entries: this.processEntries(loadedEntries, collection), + /* Wrap cursors so we can tell which collection the cursor is from. This is done to prevent traverseCursor from requiring a `collection` argument. */ - cursor: Cursor.create(loadedEntries[CURSOR_COMPATIBILITY_SYMBOL]).wrapData({ - cursorType: "collectionEntries", - collection, - }), - })); + cursor: Cursor.create(loadedEntries[CURSOR_COMPATIBILITY_SYMBOL]).wrapData({ + cursorType: 'collectionEntries', + collection, + }), + })); } // The same as listEntries, except that if a cursor with the "next" @@ -211,17 +219,18 @@ class Backend { // returns all the collected entries. Used to retrieve all entries // for local searches and queries. async listAllEntries(collection) { - if (collection.get("folder") && this.implementation.allEntriesByFolder) { + if (collection.get('folder') && this.implementation.allEntriesByFolder) { const extension = selectFolderEntryExtension(collection); - return this.implementation.allEntriesByFolder(collection, extension) - .then(entries => this.processEntries(entries, collection)); + return this.implementation + .allEntriesByFolder(collection, extension) + .then(entries => this.processEntries(entries, collection)); } const response = await this.listEntries(collection); const { entries } = response; let { cursor } = response; - while (cursor && cursor.actions.includes("next")) { - const { entries: newEntries, cursor: newCursor } = await this.traverseCursor(cursor, "next"); + while (cursor && cursor.actions.includes('next')) { + const { entries: newEntries, cursor: newCursor } = await this.traverseCursor(cursor, 'next'); entries.push(...newEntries); cursor = newCursor; } @@ -233,31 +242,37 @@ class Backend { // collection, load it, search, and call onCollectionResults with // its results. const errors = []; - const collectionEntriesRequests = collections.map(async collection => { - // TODO: pass search fields in as an argument - const searchFields = [ - selectInferedField(collection, 'title'), - selectInferedField(collection, 'shortTitle'), - selectInferedField(collection, 'author'), - ]; - const collectionEntries = await this.listAllEntries(collection); - return fuzzy.filter(searchTerm, collectionEntries, { - extract: extractSearchFields(searchFields), - }); - }).map(p => p.catch(err => errors.push(err) && [])); + const collectionEntriesRequests = collections + .map(async collection => { + // TODO: pass search fields in as an argument + const searchFields = [ + selectInferedField(collection, 'title'), + selectInferedField(collection, 'shortTitle'), + selectInferedField(collection, 'author'), + ]; + const collectionEntries = await this.listAllEntries(collection); + return fuzzy.filter(searchTerm, collectionEntries, { + extract: extractSearchFields(searchFields), + }); + }) + .map(p => p.catch(err => errors.push(err) && [])); const entries = await Promise.all(collectionEntriesRequests).then(arrs => flatten(arrs)); if (errors.length > 0) { - throw new Error({ message: "Errors ocurred while searching entries locally!", errors }); + throw new Error({ message: 'Errors ocurred while searching entries locally!', errors }); } - const hits = entries.filter(({ score }) => score > 5).sort(sortByScore).map(f => f.original); + const hits = entries + .filter(({ score }) => score > 5) + .sort(sortByScore) + .map(f => f.original); return { entries: hits }; } async query(collection, searchFields, searchTerm) { const entries = await this.listAllEntries(collection); - const hits = fuzzy.filter(searchTerm, entries, { extract: extractSearchFields(searchFields) }) + const hits = fuzzy + .filter(searchTerm, entries, { extract: extractSearchFields(searchFields) }) .filter(entry => entry.score > 5) .sort(sortByScore) .map(f => f.original); @@ -267,26 +282,29 @@ class Backend { traverseCursor(cursor, action) { const [data, unwrappedCursor] = cursor.unwrapData(); // TODO: stop assuming all cursors are for collections - const collection = data.get("collection"); - return this.implementation.traverseCursor(unwrappedCursor, action) + const collection = data.get('collection'); + return this.implementation + .traverseCursor(unwrappedCursor, action) .then(async ({ entries, cursor: newCursor }) => ({ entries: this.processEntries(entries, collection), cursor: Cursor.create(newCursor).wrapData({ - cursorType: "collectionEntries", + cursorType: 'collectionEntries', collection, }), })); } getEntry(collection, slug) { - return this.implementation.getEntry(collection, slug, selectEntryPath(collection, slug)) - .then(loadedEntry => this.entryWithFormat(collection, slug)(createEntry( - collection.get("name"), - slug, - loadedEntry.file.path, - { raw: loadedEntry.data, label: loadedEntry.file.label } - )) - ); + return this.implementation + .getEntry(collection, slug, selectEntryPath(collection, slug)) + .then(loadedEntry => + this.entryWithFormat(collection, slug)( + createEntry(collection.get('name'), slug, loadedEntry.file.path, { + raw: loadedEntry.data, + label: loadedEntry.file.label, + }), + ), + ); } getMedia() { @@ -294,7 +312,7 @@ class Backend { } entryWithFormat(collectionOrEntity) { - return (entry) => { + return entry => { const format = resolveFormat(collectionOrEntity, entry); if (entry && entry.raw !== undefined) { const data = (format && attempt(format.fromFile.bind(format, entry.raw))) || {}; @@ -306,87 +324,93 @@ class Backend { } unpublishedEntries(collections) { - return this.implementation.unpublishedEntries() - .then(loadedEntries => loadedEntries.filter(entry => entry !== null)) - .then(entries => ( - entries.map((loadedEntry) => { - const entry = createEntry( - loadedEntry.metaData.collection, - loadedEntry.slug, - loadedEntry.file.path, - { - raw: loadedEntry.data, - isModification: loadedEntry.isModification, + return this.implementation + .unpublishedEntries() + .then(loadedEntries => loadedEntries.filter(entry => entry !== null)) + .then(entries => + entries.map(loadedEntry => { + const entry = createEntry( + loadedEntry.metaData.collection, + loadedEntry.slug, + loadedEntry.file.path, + { + raw: loadedEntry.data, + isModification: loadedEntry.isModification, + }, + ); + entry.metaData = loadedEntry.metaData; + return entry; + }), + ) + .then(entries => ({ + pagination: 0, + entries: entries.reduce((acc, entry) => { + const collection = collections.get(entry.collection); + if (collection) { + acc.push(this.entryWithFormat(collection)(entry)); } - ); - entry.metaData = loadedEntry.metaData; - return entry; - }) - )) - .then(entries => ({ - pagination: 0, - entries: entries.reduce((acc, entry) => { - const collection = collections.get(entry.collection); - if (collection) { - acc.push(this.entryWithFormat(collection)(entry)); - } - return acc; - }, []), - })); + return acc; + }, []), + })); } unpublishedEntry(collection, slug) { - return this.implementation.unpublishedEntry(collection, slug) - .then((loadedEntry) => { - const entry = createEntry( - "draft", - loadedEntry.slug, - loadedEntry.file.path, - { + return this.implementation + .unpublishedEntry(collection, slug) + .then(loadedEntry => { + const entry = createEntry('draft', loadedEntry.slug, loadedEntry.file.path, { raw: loadedEntry.data, isModification: loadedEntry.isModification, }); - entry.metaData = loadedEntry.metaData; - return entry; - }) - .then(this.entryWithFormat(collection, slug)); + entry.metaData = loadedEntry.metaData; + return entry; + }) + .then(this.entryWithFormat(collection, slug)); } persistEntry(config, collection, entryDraft, MediaFiles, integrations, options = {}) { - const newEntry = entryDraft.getIn(["entry", "newRecord"]) || false; + const newEntry = entryDraft.getIn(['entry', 'newRecord']) || false; const parsedData = { - title: entryDraft.getIn(["entry", "data", "title"], "No Title"), - description: entryDraft.getIn(["entry", "data", "description"], "No Description!"), + title: entryDraft.getIn(['entry', 'data', 'title'], 'No Title'), + description: entryDraft.getIn(['entry', 'data', 'description'], 'No Description!'), }; let entryObj; if (newEntry) { if (!selectAllowNewEntries(collection)) { - throw (new Error("Not allowed to create new entries in this collection")); + throw new Error('Not allowed to create new entries in this collection'); } - const slug = slugFormatter(collection, entryDraft.getIn(["entry", "data"]), config.get("slug")); + const slug = slugFormatter( + collection, + entryDraft.getIn(['entry', 'data']), + config.get('slug'), + ); const path = selectEntryPath(collection, slug); entryObj = { path, slug, - raw: this.entryToRaw(collection, entryDraft.get("entry")), + raw: this.entryToRaw(collection, entryDraft.get('entry')), }; } else { - const path = entryDraft.getIn(["entry", "path"]); - const slug = entryDraft.getIn(["entry", "slug"]); + const path = entryDraft.getIn(['entry', 'path']); + const slug = entryDraft.getIn(['entry', 'slug']); entryObj = { path, slug, - raw: this.entryToRaw(collection, entryDraft.get("entry")), + raw: this.entryToRaw(collection, entryDraft.get('entry')), }; } - const commitMessage = commitMessageFormatter(newEntry ? 'create' : 'update', config, { collection, slug: entryObj.slug, path: entryObj.path }); + const commitMessage = commitMessageFormatter(newEntry ? 'create' : 'update', config, { + collection, + slug: entryObj.slug, + path: entryObj.path, + }); - const useWorkflow = config.getIn(["publish_mode"]) === EDITORIAL_WORKFLOW; + const useWorkflow = config.getIn(['publish_mode']) === EDITORIAL_WORKFLOW; - const collectionName = collection.get("name"); + const collectionName = collection.get('name'); /** * Determine whether an asset store integration is in use. @@ -399,11 +423,10 @@ class Backend { commitMessage, collectionName, useWorkflow, - ...updatedOptions + ...updatedOptions, }; - return this.implementation.persistEntry(entryObj, MediaFiles, opts) - .then(() => entryObj.slug); + return this.implementation.persistEntry(entryObj, MediaFiles, opts).then(() => entryObj.slug); } persistMedia(config, file) { @@ -417,7 +440,7 @@ class Backend { const path = selectEntryPath(collection, slug); if (!selectAllowDeletion(collection)) { - throw (new Error("Not allowed to delete entries in this collection")); + throw new Error('Not allowed to delete entries in this collection'); } const commitMessage = commitMessageFormatter('delete', config, { collection, slug, path }); @@ -448,52 +471,60 @@ class Backend { entryToRaw(collection, entry) { const format = resolveFormat(collection, entry.toJS()); const fieldsOrder = this.fieldsOrder(collection, entry); - return format && format.toFile(entry.get("data").toJS(), fieldsOrder); + return format && format.toFile(entry.get('data').toJS(), fieldsOrder); } fieldsOrder(collection, entry) { const fields = collection.get('fields'); if (fields) { - return collection.get('fields').map(f => f.get('name')).toArray(); + return collection + .get('fields') + .map(f => f.get('name')) + .toArray(); } const files = collection.get('files'); - const file = (files || []).filter(f => f.get("name") === entry.get("slug")).get(0); + const file = (files || []).filter(f => f.get('name') === entry.get('slug')).get(0); if (file == null) { - throw new Error(`No file found for ${ entry.get("slug") } in ${ collection.get('name') }`); + throw new Error(`No file found for ${entry.get('slug')} in ${collection.get('name')}`); } - return file.get('fields').map(f => f.get('name')).toArray(); + return file + .get('fields') + .map(f => f.get('name')) + .toArray(); } filterEntries(collection, filterRule) { - return collection.entries.filter(entry => ( - entry.data[filterRule.get('field')] === filterRule.get('value') - )); + return collection.entries.filter( + entry => entry.data[filterRule.get('field')] === filterRule.get('value'), + ); } } export function resolveBackend(config) { - const name = config.getIn(["backend", "name"]); + const name = config.getIn(['backend', 'name']); if (name == null) { - throw new Error("No backend defined in configuration"); + throw new Error('No backend defined in configuration'); } const authStore = new LocalStorageAuthStore(); if (!getBackend(name)) { - throw new Error(`Backend not found: ${ name }`); + throw new Error(`Backend not found: ${name}`); } else { return new Backend(getBackend(name), { backendName: name, authStore, config }); } } -export const currentBackend = (function () { +export const currentBackend = (function() { let backend = null; - return (config) => { - if (backend) { return backend; } - if (config.get("backend")) { - return backend = resolveBackend(config); + return config => { + if (backend) { + return backend; + } + if (config.get('backend')) { + return (backend = resolveBackend(config)); } }; -}()); +})(); diff --git a/packages/netlify-cms-core/src/bootstrap.js b/packages/netlify-cms-core/src/bootstrap.js index 75a3c608..9ba62d97 100644 --- a/packages/netlify-cms-core/src/bootstrap.js +++ b/packages/netlify-cms-core/src/bootstrap.js @@ -7,7 +7,7 @@ import history from 'Routing/history'; import configureStore from 'Redux/configureStore'; import { mergeConfig } from 'Actions/config'; import { setStore } from 'ValueObjects/AssetProxy'; -import { ErrorBoundary } from 'UI' +import { ErrorBoundary } from 'UI'; import App from 'App/App'; import 'EditorWidgets'; import 'what-input'; @@ -73,7 +73,7 @@ function bootstrap(opts = {}) { - + diff --git a/packages/netlify-cms-core/src/components/App/App.js b/packages/netlify-cms-core/src/components/App/App.js index 32d758a5..ee702482 100644 --- a/packages/netlify-cms-core/src/components/App/App.js +++ b/packages/netlify-cms-core/src/components/App/App.js @@ -36,20 +36,19 @@ const AppMainContainer = styled.div` min-width: 800px; max-width: 1440px; margin: 0 auto; -` +`; const ErrorContainer = styled.div` margin: 20px; -` +`; const ErrorCodeBlock = styled.pre` margin-left: 20px; font-size: 15px; line-height: 1.5; -` +`; class App extends React.Component { - static propTypes = { auth: ImmutablePropTypes.map, config: ImmutablePropTypes.map, @@ -89,24 +88,26 @@ class App extends React.Component { const backend = currentBackend(this.props.config); if (backend == null) { - return

Waiting for backend...

; + return ( +
+

Waiting for backend...

+
+ ); } return (
- { - React.createElement(backend.authComponent(), { - onLogin: this.handleLogin.bind(this), - error: auth && auth.get('error'), - isFetching: auth && auth.get('isFetching'), - siteId: this.props.config.getIn(["backend", "site_domain"]), - base_url: this.props.config.getIn(["backend", "base_url"], null), - authEndpoint: this.props.config.getIn(["backend", "auth_endpoint"]), - config: this.props.config, - clearHash: () => history.replace('/'), - }) - } + {React.createElement(backend.authComponent(), { + onLogin: this.handleLogin.bind(this), + error: auth && auth.get('error'), + isFetching: auth && auth.get('isFetching'), + siteId: this.props.config.getIn(['backend', 'site_domain']), + base_url: this.props.config.getIn(['backend', 'base_url'], null), + authEndpoint: this.props.config.getIn(['backend', 'auth_endpoint']), + config: this.props.config, + clearHash: () => history.replace('/'), + })}
); } @@ -159,19 +160,25 @@ class App extends React.Component { displayUrl={config.get('display_url')} /> - { isFetching && } + {isFetching && }
- { hasWorkflow ? : null } + {hasWorkflow ? : null} - } /> + } + /> - } /> + } + /> - +
@@ -200,5 +207,8 @@ function mapDispatchToProps(dispatch) { } export default hot(module)( - connect(mapStateToProps, mapDispatchToProps)(App) + connect( + mapStateToProps, + mapDispatchToProps, + )(App), ); diff --git a/packages/netlify-cms-core/src/components/App/Header.js b/packages/netlify-cms-core/src/components/App/Header.js index 39664634..42c54191 100644 --- a/packages/netlify-cms-core/src/components/App/Header.js +++ b/packages/netlify-cms-core/src/components/App/Header.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; -import React from "react"; -import ImmutablePropTypes from "react-immutable-proptypes"; +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; import styled, { css } from 'react-emotion'; import { NavLink } from 'react-router-dom'; import { @@ -33,7 +33,7 @@ const AppHeader = styled.div` background-color: ${colors.foreground}; z-index: 300; height: ${lengths.topBarHeight}; -` +`; const AppHeaderContent = styled.div` display: flex; @@ -69,15 +69,15 @@ const AppHeaderButton = styled.button` ${styles.buttonActive}; } } - `} -` + `}; +`; const AppHeaderNavLink = AppHeaderButton.withComponent(NavLink); const AppHeaderActions = styled.div` display: inline-flex; align-items: center; -` +`; const AppHeaderQuickNewButton = styled(StyledDropdownButton)` ${buttons.button}; @@ -88,7 +88,7 @@ const AppHeaderQuickNewButton = styled(StyledDropdownButton)` &:after { top: 11px; } -` +`; export default class Header extends React.Component { static propTypes = { @@ -99,7 +99,7 @@ export default class Header extends React.Component { displayUrl: PropTypes.string, }; - handleCreatePostClick = (collectionName) => { + handleCreatePostClick = collectionName => { const { onCreateEntryClick } = this.props; if (onCreateEntryClick) { onCreateEntryClick(collectionName); @@ -126,19 +126,17 @@ export default class Header extends React.Component { activeClassName="header-link-active" isActive={(match, location) => location.pathname.startsWith('/collections/')} > - + Content - { - hasWorkflow - ? - - Workflow - - : null - } + {hasWorkflow ? ( + + + Workflow + + ) : null} - + Media @@ -149,15 +147,16 @@ export default class Header extends React.Component { dropdownWidth="160px" dropdownPosition="left" > - { - collections.filter(collection => collection.get('create')).toList().map(collection => + {collections + .filter(collection => collection.get('create')) + .toList() + .map(collection => ( this.handleCreatePostClick(collection.get('name'))} /> - ) - } + ))} { const { name, collection } = this.props; - return + return ( + + ); }; renderEntriesSearch = () => { const { searchTerm, collections } = this.props; - return + return ; }; - handleChangeViewStyle = (viewStyle) => { + handleChangeViewStyle = viewStyle => { if (this.state.viewStyle !== viewStyle) { this.setState({ viewStyle }); } - } + }; render() { const { collection, collections, collectionName, isSearchResults, searchTerm } = this.props; const newEntryUrl = collection.get('create') ? getNewEntryUrl(collectionName) : ''; return ( - + - { - isSearchResults - ? null - : - } - { isSearchResults ? this.renderEntriesSearch() : this.renderEntriesCollection() } + {isSearchResults ? null : ( + + )} + {isSearchResults ? this.renderEntriesSearch() : this.renderEntriesCollection()} ); diff --git a/packages/netlify-cms-core/src/components/Collection/CollectionTop.js b/packages/netlify-cms-core/src/components/Collection/CollectionTop.js index 98e6a1ca..71893271 100644 --- a/packages/netlify-cms-core/src/components/Collection/CollectionTop.js +++ b/packages/netlify-cms-core/src/components/Collection/CollectionTop.js @@ -7,18 +7,18 @@ import { VIEW_STYLE_LIST, VIEW_STYLE_GRID } from 'Constants/collectionViews'; const CollectionTopContainer = styled.div` ${components.cardTop}; -` +`; const CollectionTopRow = styled.div` display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; -` +`; const CollectionTopHeading = styled.h1` ${components.cardTopHeading}; -` +`; const CollectionTopNewButton = styled(Link)` ${buttons.button}; @@ -27,28 +27,28 @@ const CollectionTopNewButton = styled(Link)` ${buttons.gray}; padding: 0 30px; -` +`; const CollectionTopDescription = styled.p` ${components.cardTopDescription}; -` +`; const ViewControls = styled.div` display: flex; align-items: center; justify-content: flex-end; margin-top: 24px; -` +`; const ViewControlsText = styled.span` font-size: 14px; color: ${colors.text}; margin-right: 12px; -` +`; const ViewControlsButton = styled.button` ${buttons.button}; - color: ${props => props.isActive ? colors.active : '#b3b9c4'}; + color: ${props => (props.isActive ? colors.active : '#b3b9c4')}; background-color: transparent; display: block; padding: 0; @@ -61,7 +61,7 @@ const ViewControlsButton = styled.button` ${Icon} { display: block; } -` +`; const CollectionTop = ({ collectionLabel, @@ -75,32 +75,28 @@ const CollectionTop = ({ {collectionLabel} - { - newEntryUrl - ? - {`New ${collectionLabelSingular || collectionLabel}`} - - : null - } + {newEntryUrl ? ( + + {`New ${collectionLabelSingular || collectionLabel}`} + + ) : null} - { - collectionDescription - ? {collectionDescription} - : null - } + {collectionDescription ? ( + {collectionDescription} + ) : null} View as: onChangeViewStyle(VIEW_STYLE_LIST)} > - + onChangeViewStyle(VIEW_STYLE_GRID)} > - + @@ -110,7 +106,7 @@ const CollectionTop = ({ CollectionTop.propTypes = { collectionLabel: PropTypes.string.isRequired, collectionDescription: PropTypes.string, - newEntryUrl: PropTypes.string + newEntryUrl: PropTypes.string, }; export default CollectionTop; diff --git a/packages/netlify-cms-core/src/components/Collection/Entries/Entries.js b/packages/netlify-cms-core/src/components/Collection/Entries/Entries.js index cf0e7daf..b3188099 100644 --- a/packages/netlify-cms-core/src/components/Collection/Entries/Entries.js +++ b/packages/netlify-cms-core/src/components/Collection/Entries/Entries.js @@ -13,11 +13,7 @@ const Entries = ({ cursor, handleCursorActions, }) => { - const loadingMessages = [ - 'Loading Entries', - 'Caching Entries', - 'This might take several minutes', - ]; + const loadingMessages = ['Loading Entries', 'Caching Entries', 'This might take several minutes']; if (entries) { return ( @@ -37,7 +33,7 @@ const Entries = ({ } return
No Entries
; -} +}; Entries.propTypes = { collections: ImmutablePropTypes.map.isRequired, diff --git a/packages/netlify-cms-core/src/components/Collection/Entries/EntriesCollection.js b/packages/netlify-cms-core/src/components/Collection/Entries/EntriesCollection.js index 38b649d5..2a923ab1 100644 --- a/packages/netlify-cms-core/src/components/Collection/Entries/EntriesCollection.js +++ b/packages/netlify-cms-core/src/components/Collection/Entries/EntriesCollection.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { connect } from 'react-redux'; import { partial } from 'lodash'; -import { Cursor } from 'netlify-cms-lib-util' +import { Cursor } from 'netlify-cms-lib-util'; import { loadEntries as actionLoadEntries, traverseCollectionCursor as actionTraverseCollectionCursor, @@ -43,7 +43,7 @@ class EntriesCollection extends React.Component { traverseCollectionCursor(collection, action); }; - render () { + render() { const { collection, entries, publicFolder, isFetching, viewStyle, cursor } = this.props; return ( @@ -70,7 +70,7 @@ function mapStateToProps(state, ownProps) { const entries = selectEntries(state, collection.get('name')); const isFetching = state.entries.getIn(['pages', collection.get('name'), 'isFetching'], false); - const rawCursor = selectCollectionEntriesCursor(state.cursors, collection.get("name")); + const rawCursor = selectCollectionEntriesCursor(state.cursors, collection.get('name')); const cursor = Cursor.create(rawCursor).clearData(); return { publicFolder, collection, page, entries, isFetching, viewStyle, cursor }; @@ -81,4 +81,7 @@ const mapDispatchToProps = { traverseCollectionCursor: actionTraverseCollectionCursor, }; -export default connect(mapStateToProps, mapDispatchToProps)(EntriesCollection); +export default connect( + mapStateToProps, + mapDispatchToProps, +)(EntriesCollection); diff --git a/packages/netlify-cms-core/src/components/Collection/Entries/EntriesSearch.js b/packages/netlify-cms-core/src/components/Collection/Entries/EntriesSearch.js index 924e7f65..674d29b0 100644 --- a/packages/netlify-cms-core/src/components/Collection/Entries/EntriesSearch.js +++ b/packages/netlify-cms-core/src/components/Collection/Entries/EntriesSearch.js @@ -6,7 +6,7 @@ import { Cursor } from 'netlify-cms-lib-util'; import { selectSearchedEntries } from 'Reducers'; import { searchEntries as actionSearchEntries, - clearSearch as actionClearSearch + clearSearch as actionClearSearch, } from 'Actions/search'; import Entries from './Entries'; @@ -40,19 +40,19 @@ class EntriesSearch extends React.Component { getCursor = () => { const { page } = this.props; return Cursor.create({ - actions: isNaN(page) ? [] : ["append_next"], + actions: isNaN(page) ? [] : ['append_next'], }); }; - handleCursorActions = (action) => { + handleCursorActions = action => { const { page, searchTerm, searchEntries } = this.props; - if (action === "append_next") { + if (action === 'append_next') { const nextPage = page + 1; searchEntries(searchTerm, nextPage); } }; - render () { + render() { const { collections, entries, publicFolder, isFetching } = this.props; return ( props.url}); @@ -77,7 +78,7 @@ const CardImage = styled.div` background-size: cover; background-repeat: no-repeat; height: 150px; -` +`; const EntryCard = ({ collection, @@ -92,7 +93,7 @@ const EntryCard = ({ const path = `/collections/${collection.get('name')}/entries/${entry.get('slug')}`; let image = entry.getIn(['data', inferedFields.imageField]); image = resolvePath(image, publicFolder); - if(image) { + if (image) { image = encodeURI(image); } @@ -100,8 +101,8 @@ const EntryCard = ({ return ( - { collectionLabel ? {collectionLabel} : null } - { title } + {collectionLabel ? {collectionLabel} : null} + {title} ); @@ -112,14 +113,14 @@ const EntryCard = ({ - { collectionLabel ? {collectionLabel} : null } + {collectionLabel ? {collectionLabel} : null} {title} - { image ? : null } + {image ? : null} ); } -} +}; export default EntryCard; diff --git a/packages/netlify-cms-core/src/components/Collection/Entries/EntryListing.js b/packages/netlify-cms-core/src/components/Collection/Entries/EntryListing.js index c6bb131b..1eeb3fd8 100644 --- a/packages/netlify-cms-core/src/components/Collection/Entries/EntryListing.js +++ b/packages/netlify-cms-core/src/components/Collection/Entries/EntryListing.js @@ -13,23 +13,21 @@ const CardsGrid = styled.ul` flex-flow: row wrap; list-style-type: none; margin-left: -12px; -` +`; export default class EntryListing extends React.Component { static propTypes = { publicFolder: PropTypes.string.isRequired, - collections: PropTypes.oneOfType([ - ImmutablePropTypes.map, - ImmutablePropTypes.iterable, - ]).isRequired, + collections: PropTypes.oneOfType([ImmutablePropTypes.map, ImmutablePropTypes.iterable]) + .isRequired, entries: ImmutablePropTypes.list, viewStyle: PropTypes.string, }; handleLoadMore = () => { const { cursor, handleCursorActions } = this.props; - if (Cursor.create(cursor).actions.has("append_next")) { - handleCursorActions("append_next"); + if (Cursor.create(cursor).actions.has('append_next')) { + handleCursorActions('append_next'); } }; @@ -39,7 +37,8 @@ export default class EntryListing extends React.Component { const imageField = selectInferedField(collection, 'image'); const fields = selectFields(collection); const inferedFields = [titleField, descriptionField, imageField]; - const remainingFields = fields && fields.filter(f => inferedFields.indexOf(f.get('name')) === -1); + const remainingFields = + fields && fields.filter(f => inferedFields.indexOf(f.get('name')) === -1); return { titleField, descriptionField, imageField, remainingFields }; }; @@ -68,11 +67,9 @@ export default class EntryListing extends React.Component { return (
- { - Map.isMap(collections) - ? this.renderCardsForSingleCollection() - : this.renderCardsForMultipleCollections() - } + {Map.isMap(collections) + ? this.renderCardsForSingleCollection() + : this.renderCardsForMultipleCollections()}
diff --git a/packages/netlify-cms-core/src/components/Collection/Sidebar.js b/packages/netlify-cms-core/src/components/Collection/Sidebar.js index 64931101..ff3b8bc1 100644 --- a/packages/netlify-cms-core/src/components/Collection/Sidebar.js +++ b/packages/netlify-cms-core/src/components/Collection/Sidebar.js @@ -20,7 +20,7 @@ const SidebarContainer = styled.aside` position: fixed; max-height: calc(100vh - 112px); overflow: auto; -` +`; const SidebarHeading = styled.h2` font-size: 23px; @@ -28,7 +28,7 @@ const SidebarHeading = styled.h2` padding: 0; margin: 18px 12px 12px; color: ${colors.textLead}; -` +`; const SearchContainer = styled.div` display: flex; @@ -46,7 +46,7 @@ const SearchContainer = styled.div` align-items: center; pointer-events: none; } -` +`; const SearchInput = styled.input` background-color: #eff0f4; @@ -61,7 +61,7 @@ const SearchInput = styled.input` outline: none; box-shadow: inset 0 0 0 2px ${colorsRaw.blue}; } -` +`; const SidebarNavLink = styled(NavLink)` display: flex; @@ -77,19 +77,16 @@ const SidebarNavLink = styled(NavLink)` &.${props.activeClassName} { ${styles.sidebarNavLinkActive}; } - `} - - &:first-of-type { + `} &:first-of-type { margin-top: 16px; } ${Icon} { margin-right: 8px; } -` +`; export default class Sidebar extends React.Component { - static propTypes = { collections: ImmutablePropTypes.orderedMap.isRequired, }; @@ -104,13 +101,12 @@ export default class Sidebar extends React.Component { to={`/collections/${collectionName}`} activeClassName="sidebar-active" > - + {collection.get('label')} ); }; - render() { const { collections } = this.props; const { query } = this.state; @@ -119,7 +115,7 @@ export default class Sidebar extends React.Component { Collections - + this.setState({ query: e.target.value })} onKeyDown={e => e.key === 'Enter' && searchCollections(query)} diff --git a/packages/netlify-cms-core/src/components/Editor/Editor.js b/packages/netlify-cms-core/src/components/Editor/Editor.js index fbfaea88..e4c5bca9 100644 --- a/packages/netlify-cms-core/src/components/Editor/Editor.js +++ b/packages/netlify-cms-core/src/components/Editor/Editor.js @@ -19,7 +19,7 @@ import { import { updateUnpublishedEntryStatus, publishUnpublishedEntry, - deleteUnpublishedEntry + deleteUnpublishedEntry, } from 'Actions/editorialWorkflow'; import { deserializeValues } from 'Lib/serializeEntryValues'; import { selectEntry, selectUnpublishedEntry, getAsset } from 'Reducers'; @@ -29,10 +29,11 @@ import { EDITORIAL_WORKFLOW } from 'Constants/publishModes'; import EditorInterface from './EditorInterface'; import withWorkflow from './withWorkflow'; -const navigateCollection = (collectionPath) => history.push(`/collections/${collectionPath}`); +const navigateCollection = collectionPath => history.push(`/collections/${collectionPath}`); const navigateToCollection = collectionName => navigateCollection(collectionName); const navigateToNewEntry = collectionName => navigateCollection(`${collectionName}/new`); -const navigateToEntry = (collectionName, slug) => navigateCollection(`${collectionName}/entries/${slug}`); +const navigateToEntry = (collectionName, slug) => + navigateCollection(`${collectionName}/entries/${slug}`); class Editor extends React.Component { static propTypes = { @@ -83,7 +84,7 @@ class Editor extends React.Component { const leaveMessage = 'Are you sure you want to leave this page?'; - this.exitBlocker = (event) => { + this.exitBlocker = event => { if (this.props.entryDraft.get('hasChanged')) { // This message is ignored in most browsers, but its presence // triggers the confirmation dialog @@ -100,14 +101,18 @@ class Editor extends React.Component { const isPersisting = this.props.entryDraft.getIn(['entry', 'isPersisting']); const newRecord = this.props.entryDraft.getIn(['entry', 'newRecord']); const newEntryPath = `/collections/${collection.get('name')}/new`; - if (isPersisting && newRecord && this.props.location.pathname === newEntryPath && action === 'PUSH') { + if ( + isPersisting && + newRecord && + this.props.location.pathname === newEntryPath && + action === 'PUSH' + ) { return; } if (this.props.hasChanged) { return leaveMessage; } - }; const unblock = history.block(navigationBlocker); @@ -119,7 +124,10 @@ class Editor extends React.Component { const newEntryPath = `/collections/${collection.get('name')}/new`; const entriesPath = `/collections/${collection.get('name')}/entries/`; const { pathname } = location; - if (pathname.startsWith(newEntryPath) || pathname.startsWith(entriesPath) && action === 'PUSH') { + if ( + pathname.startsWith(newEntryPath) || + (pathname.startsWith(entriesPath) && action === 'PUSH') + ) { return; } unblock(); @@ -147,7 +155,6 @@ class Editor extends React.Component { const { entry, newEntry, fields, collection } = this.props; if (entry && !entry.get('isFetching') && !entry.get('error')) { - /** * Deserialize entry values for widgets with registered serializers before * creating the entry draft. @@ -170,34 +177,54 @@ class Editor extends React.Component { if (entry) this.props.createDraftFromEntry(entry, metadata); }; - handleChangeStatus = (newStatusName) => { - const { entryDraft, updateUnpublishedEntryStatus, collection, slug, currentStatus } = this.props; + handleChangeStatus = newStatusName => { + const { + entryDraft, + updateUnpublishedEntryStatus, + collection, + slug, + currentStatus, + } = this.props; if (entryDraft.get('hasChanged')) { window.alert('You have unsaved changes, please save before updating status.'); return; } const newStatus = status.get(newStatusName); updateUnpublishedEntryStatus(collection.get('name'), slug, currentStatus, newStatus); - } + }; handlePersistEntry = async (opts = {}) => { const { createNew = false } = opts; - const { persistEntry, collection, currentStatus, hasWorkflow, loadEntry, slug, createEmptyDraft } = this.props; + const { + persistEntry, + collection, + currentStatus, + hasWorkflow, + loadEntry, + slug, + createEmptyDraft, + } = this.props; - await persistEntry(collection) + await persistEntry(collection); if (createNew) { navigateToNewEntry(collection.get('name')); createEmptyDraft(collection); - } - else if (slug && hasWorkflow && !currentStatus) { + } else if (slug && hasWorkflow && !currentStatus) { loadEntry(collection, slug); } }; handlePublishEntry = async (opts = {}) => { const { createNew = false } = opts; - const { publishUnpublishedEntry, entryDraft, collection, slug, currentStatus, loadEntry } = this.props; + const { + publishUnpublishedEntry, + entryDraft, + collection, + slug, + currentStatus, + loadEntry, + } = this.props; if (currentStatus !== status.last()) { window.alert('Please update status to "Ready" before publishing.'); return; @@ -212,8 +239,7 @@ class Editor extends React.Component { if (createNew) { navigateToNewEntry(collection.get('name')); - } - else { + } else { loadEntry(collection, slug); } }; @@ -221,7 +247,11 @@ class Editor extends React.Component { handleDeleteEntry = () => { const { entryDraft, newEntry, collection, deleteEntry, slug } = this.props; if (entryDraft.get('hasChanged')) { - if (!window.confirm('Are you sure you want to delete this published entry, as well as your unsaved changes from the current session?')) { + if ( + !window.confirm( + 'Are you sure you want to delete this published entry, as well as your unsaved changes from the current session?', + ) + ) { return; } } else if (!window.confirm('Are you sure you want to delete this published entry?')) { @@ -238,10 +268,26 @@ class Editor extends React.Component { }; handleDeleteUnpublishedChanges = async () => { - const { entryDraft, collection, slug, deleteUnpublishedEntry, loadEntry, isModification } = this.props; - if (entryDraft.get('hasChanged') && !window.confirm('This will delete all unpublished changes to this entry, as well as your unsaved changes from the current session. Do you still want to delete?')) { + const { + entryDraft, + collection, + slug, + deleteUnpublishedEntry, + loadEntry, + isModification, + } = this.props; + if ( + entryDraft.get('hasChanged') && + !window.confirm( + 'This will delete all unpublished changes to this entry, as well as your unsaved changes from the current session. Do you still want to delete?', + ) + ) { return; - } else if (!window.confirm('All unpublished changes to this entry will be deleted. Do you still want to delete?')) { + } else if ( + !window.confirm( + 'All unpublished changes to this entry will be deleted. Do you still want to delete?', + ) + ) { return; } await deleteUnpublishedEntry(collection.get('name'), slug); @@ -274,10 +320,16 @@ class Editor extends React.Component { } = this.props; if (entry && entry.get('error')) { - return

{ entry.get('error') }

; - } else if (entryDraft == null - || entryDraft.get('entry') === undefined - || (entry && entry.get('isFetching'))) { + return ( +
+

{entry.get('error')}

+
+ ); + } else if ( + entryDraft == null || + entryDraft.get('entry') === undefined || + (entry && entry.get('isFetching')) + ) { return Loading entry...; } @@ -325,7 +377,7 @@ function mapStateToProps(state, ownProps) { const displayUrl = config.get('display_url'); const hasWorkflow = config.get('publish_mode') === EDITORIAL_WORKFLOW; const isModification = entryDraft.getIn(['entry', 'isModification']); - const collectionEntriesLoaded = !!entries.getIn(['entities', collectionName]) + const collectionEntriesLoaded = !!entries.getIn(['entities', collectionName]); const unpublishedEntry = selectUnpublishedEntry(state, collectionName, slug); const currentStatus = unpublishedEntry && unpublishedEntry.getIn(['metaData', 'status']); return { @@ -363,5 +415,5 @@ export default connect( publishUnpublishedEntry, deleteUnpublishedEntry, logoutUser, - } + }, )(withWorkflow(Editor)); diff --git a/packages/netlify-cms-core/src/components/Editor/EditorControlPane/EditorControl.js b/packages/netlify-cms-core/src/components/Editor/EditorControlPane/EditorControl.js index 800f5d50..f9d99a92 100644 --- a/packages/netlify-cms-core/src/components/Editor/EditorControlPane/EditorControl.js +++ b/packages/netlify-cms-core/src/components/Editor/EditorControlPane/EditorControl.js @@ -89,7 +89,7 @@ const ControlContainer = styled.div` &:first-child { margin-top: 36px; } -` +`; const ControlErrorsList = styled.ul` list-style-type: none; @@ -101,9 +101,7 @@ const ControlErrorsList = styled.ul` position: relative; font-weight: 600; top: 20px; -` - - +`; class EditorControl extends React.Component { state = { @@ -138,13 +136,14 @@ class EditorControl extends React.Component { return ( - { - errors && errors.map(error => - error.message && - typeof error.message === 'string' && -
  • {error.message}
  • - ) - } + {errors && + errors.map( + error => + error.message && + typeof error.message === 'string' && ( +
  • {error.message}
  • + ), + )}

    {"There's been an error - please "} - report it! + + report it + + !

    {errorMessage}

    diff --git a/packages/netlify-cms-core/src/components/UI/Modal.js b/packages/netlify-cms-core/src/components/UI/Modal.js index ba0dab95..d776975d 100644 --- a/packages/netlify-cms-core/src/components/UI/Modal.js +++ b/packages/netlify-cms-core/src/components/UI/Modal.js @@ -46,8 +46,7 @@ const styles = { background-color: rgba(0, 0, 0, 0); opacity: 0; `, -} - +}; export class Modal extends React.Component { static propTypes = { @@ -55,7 +54,7 @@ export class Modal extends React.Component { isOpen: PropTypes.bool.isRequired, className: PropTypes.string, onClose: PropTypes.func.isRequired, - } + }; componentDidMount() { ReactModal.setAppElement('#nc-root'); diff --git a/packages/netlify-cms-core/src/components/UI/SettingsDropdown.js b/packages/netlify-cms-core/src/components/UI/SettingsDropdown.js index b9197dde..ef4da684 100644 --- a/packages/netlify-cms-core/src/components/UI/SettingsDropdown.js +++ b/packages/netlify-cms-core/src/components/UI/SettingsDropdown.js @@ -16,54 +16,52 @@ const AppHeaderAvatar = styled.button` cursor: pointer; color: #1e2532; background-color: transparent; -` +`; const AvatarImage = styled.img` ${styles.avatarImage}; -` +`; const AvatarPlaceholderIcon = styled(Icon)` ${styles.avatarImage}; height: 32px; color: #1e2532; background-color: ${colors.textFieldBorder}; -` +`; const AppHeaderSiteLink = styled.a` font-size: 14px; font-weight: 400; color: #7b8290; padding: 10px 16px; -` +`; const Avatar = ({ imageUrl }) => ( - {imageUrl ? : } + {imageUrl ? : } ); const SettingsDropdown = ({ displayUrl, imageUrl, onLogoutClick }) => ( - { - displayUrl - ? - {stripProtocol(displayUrl)} - - : null - } + {displayUrl ? ( + + {stripProtocol(displayUrl)} + + ) : null} ( - + )} > - + -) +); export default SettingsDropdown; diff --git a/packages/netlify-cms-core/src/components/UI/Toast.js b/packages/netlify-cms-core/src/components/UI/Toast.js index d64a7395..b2b6247c 100644 --- a/packages/netlify-cms-core/src/components/UI/Toast.js +++ b/packages/netlify-cms-core/src/components/UI/Toast.js @@ -40,10 +40,9 @@ const styles = { `, }; -export const Toast = ({ kind, message }) => -
    - {message} -
    ; +export const Toast = ({ kind, message }) => ( +
    {message}
    +); Toast.propTypes = { kind: PropTypes.oneOf(['info', 'success', 'warning', 'danger']).isRequired, diff --git a/packages/netlify-cms-core/src/components/Workflow/Workflow.js b/packages/netlify-cms-core/src/components/Workflow/Workflow.js index 056bf70d..2f60e653 100644 --- a/packages/netlify-cms-core/src/components/Workflow/Workflow.js +++ b/packages/netlify-cms-core/src/components/Workflow/Workflow.js @@ -18,7 +18,7 @@ import { loadUnpublishedEntries, updateUnpublishedEntryStatus, publishUnpublishedEntry, - deleteUnpublishedEntry + deleteUnpublishedEntry, } from 'Actions/editorialWorkflow'; import { selectUnpublishedEntriesByStatus } from 'Reducers'; import { EDITORIAL_WORKFLOW, status } from 'Constants/publishModes'; @@ -27,28 +27,28 @@ import WorkflowList from './WorkflowList'; const WorkflowContainer = styled.div` padding: ${lengths.pageMargin} 0; height: 100vh; -` +`; const WorkflowTop = styled.div` ${components.cardTop}; -` +`; const WorkflowTopRow = styled.div` display: flex; justify-content: space-between; - span[role="button"] { + span[role='button'] { ${shadows.dropDeep}; } -` +`; const WorkflowTopHeading = styled.h1` ${components.cardTopHeading}; -` +`; const WorkflowTopDescription = styled.p` ${components.cardTopDescription}; -` +`; class Workflow extends Component { static propTypes = { @@ -96,19 +96,21 @@ class Workflow extends Component { dropdownTopOverlap="40px" renderButton={() => New Post} > - { - collections.filter(collection => collection.get('create')).toList().map(collection => + {collections + .filter(collection => collection.get('create')) + .toList() + .map(collection => ( createNewEntry(collection.get('name'))} /> - ) - } + ))} - {reviewCount} {reviewCount === 1 ? 'entry' : 'entries'} waiting for review, {readyCount} ready to go live. + {reviewCount} {reviewCount === 1 ? 'entry' : 'entries'} waiting for review, {readyCount}{' '} + ready to go live. { const entries = selectUnpublishedEntriesByStatus(state, currStatus); - return acc.set(currStatus, entries) + return acc.set(currStatus, entries); }, OrderedMap()); } return returnObj; } -export default connect(mapStateToProps, { - loadUnpublishedEntries, - updateUnpublishedEntryStatus, - publishUnpublishedEntry, - deleteUnpublishedEntry, -})(Workflow); +export default connect( + mapStateToProps, + { + loadUnpublishedEntries, + updateUnpublishedEntryStatus, + publishUnpublishedEntry, + deleteUnpublishedEntry, + }, +)(Workflow); diff --git a/packages/netlify-cms-core/src/components/Workflow/WorkflowCard.js b/packages/netlify-cms-core/src/components/Workflow/WorkflowCard.js index ad7fc760..756031b7 100644 --- a/packages/netlify-cms-core/src/components/Workflow/WorkflowCard.js +++ b/packages/netlify-cms-core/src/components/Workflow/WorkflowCard.js @@ -23,7 +23,7 @@ const WorkflowLink = styled(Link)` padding: 0 18px 18px; height: 200px; overflow: hidden; -` +`; const CardCollection = styled.div` font-size: 14px; @@ -33,16 +33,16 @@ const CardCollection = styled.div` text-overflow: ellipsis; overflow: hidden; white-space: nowrap; -` +`; const CardTitle = styled.h2` margin: 28px 0 0; color: ${colors.textLead}; -` +`; const CardDate = styled.div` ${styles.text}; -` +`; const CardBody = styled.p` ${styles.text}; @@ -51,7 +51,7 @@ const CardBody = styled.p` overflow-wrap: break-word; word-break: break-word; hyphens: auto; -` +`; const CardButtonContainer = styled.div` background-color: ${colors.foreground}; @@ -63,14 +63,14 @@ const CardButtonContainer = styled.div` opacity: 0; transition: opacity ${transitions.main}; cursor: pointer; -` +`; const DeleteButton = styled.button` ${styles.button}; background-color: ${colorsRaw.redLight}; color: ${colorsRaw.red}; margin-right: 6px; -` +`; const PublishButton = styled.button` ${styles.button}; @@ -82,7 +82,7 @@ const PublishButton = styled.button` background-color: ${colorsRaw.grayLight}; color: ${colorsRaw.gray}; } -` +`; const WorkflowCardContainer = styled.div` ${components.card}; @@ -93,7 +93,7 @@ const WorkflowCardContainer = styled.div` &:hover ${CardButtonContainer} { opacity: 1; } -` +`; const WorkflowCard = ({ collectionName, @@ -111,7 +111,9 @@ const WorkflowCard = ({ {collectionName} {title} - {timestamp} by {authorLastChange} + + {timestamp} by {authorLastChange} + {body} diff --git a/packages/netlify-cms-core/src/components/Workflow/WorkflowList.js b/packages/netlify-cms-core/src/components/Workflow/WorkflowList.js index a6543926..42af5bc5 100644 --- a/packages/netlify-cms-core/src/components/Workflow/WorkflowList.js +++ b/packages/netlify-cms-core/src/components/Workflow/WorkflowList.js @@ -5,19 +5,19 @@ import styled, { css, cx } from 'react-emotion'; import moment from 'moment'; import { colors, lengths } from 'netlify-cms-ui-default'; import { status } from 'Constants/publishModes'; -import { DragSource, DropTarget, HTML5DragDrop } from 'UI' +import { DragSource, DropTarget, HTML5DragDrop } from 'UI'; import WorkflowCard from './WorkflowCard'; const WorkflowListContainer = styled.div` min-height: 60%; display: grid; grid-template-columns: 33.3% 33.3% 33.3%; -` +`; const styles = { column: css` margin: 0 20px; - transition: background-color .5s ease; + transition: background-color 0.5s ease; border: 2px dashed transparent; border-radius: 4px; position: relative; @@ -39,7 +39,7 @@ const styles = { width: 2px; height: 80%; top: 76px; - background-color: ${colors.textFieldBorder} + background-color: ${colors.textFieldBorder}; } &:before { @@ -63,21 +63,27 @@ const ColumnHeader = styled.h2` border-radius: ${lengths.borderRadius}; margin-bottom: 28px; - ${props => props.name === 'draft' && css` - background-color: ${colors.statusDraftBackground}; - color: ${colors.statusDraftText}; - `} + ${props => + props.name === 'draft' && + css` + background-color: ${colors.statusDraftBackground}; + color: ${colors.statusDraftText}; + `} - ${props => props.name === 'pending_review' && css` - background-color: ${colors.statusReviewBackground}; - color: ${colors.statusReviewText}; - `} + ${props => + props.name === 'pending_review' && + css` + background-color: ${colors.statusReviewBackground}; + color: ${colors.statusReviewText}; + `} - ${props => props.name === 'pending_publish' && css` - background-color: ${colors.statusReadyBackground}; - color: ${colors.statusReadyText}; - `} -` + ${props => + props.name === 'pending_publish' && + css` + background-color: ${colors.statusReadyBackground}; + color: ${colors.statusReadyText}; + `} +`; const ColumnCount = styled.p` font-size: 13px; @@ -85,18 +91,21 @@ const ColumnCount = styled.p` color: ${colors.text}; text-transform: uppercase; margin-bottom: 6px; -` +`; // This is a namespace so that we can only drop these elements on a DropTarget with the same const DNDNamespace = 'cms-workflow'; const getColumnHeaderText = columnName => { switch (columnName) { - case 'draft': return 'Drafts'; - case 'pending_review': return 'In Review'; - case 'pending_publish': return 'Ready'; + case 'draft': + return 'Drafts'; + case 'pending_review': + return 'In Review'; + case 'pending_publish': + return 'Ready'; } -} +}; class WorkflowList extends React.Component { static propTypes = { @@ -122,9 +131,9 @@ class WorkflowList extends React.Component { requestPublish = (collection, slug, ownStatus) => { if (ownStatus !== status.last()) { window.alert( -`Only items with a "Ready" status can be published. + `Only items with a "Ready" status can be published. -Please drag the card to the "Ready" column to enable publishing.` +Please drag the card to the "Ready" column to enable publishing.`, ); return; } else if (!window.confirm('Are you sure you want to publish this entry?')) { @@ -143,66 +152,69 @@ Please drag the card to the "Ready" column to enable publishing.` key={currColumn} onDrop={this.handleChangeStatus.bind(this, currColumn)} > - {(connect, { isHovered }) => connect( -
    - {getColumnHeaderText(currColumn)} - - {currEntries.size} {currEntries.size === 1 ? 'entry' : 'entries'} - - {this.renderColumns(currEntries, currColumn)} -
    - )} + {(connect, { isHovered }) => + connect( +
    + {getColumnHeaderText(currColumn)} + + {currEntries.size} {currEntries.size === 1 ? 'entry' : 'entries'} + + {this.renderColumns(currEntries, currColumn)} +
    , + ) + } )); } return (
    - { - entries.map((entry) => { - const timestamp = moment(entry.getIn(['metaData', 'timeStamp'])).format('MMMM D'); - const editLink = `collections/${ entry.getIn(['metaData', 'collection']) }/entries/${ entry.get('slug') }`; - const slug = entry.get('slug'); - const ownStatus = entry.getIn(['metaData', 'status']); - const collection = entry.getIn(['metaData', 'collection']); - const isModification = entry.get('isModification'); - const canPublish = ownStatus === status.last() && !entry.get('isPersisting', false); - return ( - - {connect => connect( -
    - -
    - )} -
    - ); - }) - } + {entries.map(entry => { + const timestamp = moment(entry.getIn(['metaData', 'timeStamp'])).format('MMMM D'); + const editLink = `collections/${entry.getIn([ + 'metaData', + 'collection', + ])}/entries/${entry.get('slug')}`; + const slug = entry.get('slug'); + const ownStatus = entry.getIn(['metaData', 'status']); + const collection = entry.getIn(['metaData', 'collection']); + const isModification = entry.get('isModification'); + const canPublish = ownStatus === status.last() && !entry.get('isPersisting', false); + return ( + + {connect => + connect( +
    + +
    , + ) + } +
    + ); + })}
    ); }; render() { const columns = this.renderColumns(this.props.entries); - return ( - {columns} - ); + return {columns}; } } diff --git a/packages/netlify-cms-core/src/constants/__tests__/configSchema.spec.js b/packages/netlify-cms-core/src/constants/__tests__/configSchema.spec.js index 3cccf8b7..15b8109b 100644 --- a/packages/netlify-cms-core/src/constants/__tests__/configSchema.spec.js +++ b/packages/netlify-cms-core/src/constants/__tests__/configSchema.spec.js @@ -6,8 +6,8 @@ describe('config', () => { * log test failures and associated errors as expected. */ beforeEach(() => { - spyOn(console, 'error') - }) + jest.spyOn(console, 'error'); + }); describe('validateConfig', () => { it('should not throw if no errors', () => { @@ -15,12 +15,14 @@ describe('config', () => { foo: 'bar', backend: { name: 'bar' }, media_folder: 'baz', - collections: [{ - name: 'posts', - label: 'Posts', - folder: '_posts', - fields: [{ name: 'title', label: 'title', widget: 'string' }], - }], + collections: [ + { + name: 'posts', + label: 'Posts', + folder: '_posts', + fields: [{ name: 'title', label: 'title', widget: 'string' }], + }, + ], }; expect(() => { validateConfig(config); @@ -41,7 +43,7 @@ describe('config', () => { it('should throw if backend name is not a string in config', () => { expect(() => { - validateConfig({ foo: 'bar', backend: { name: { } } }); + validateConfig({ foo: 'bar', backend: { name: {} } }); }).toThrowError("'backend.name' should be string"); }); @@ -65,20 +67,35 @@ describe('config', () => { it('should throw if collections not an array in config', () => { expect(() => { - validateConfig({ foo: 'bar', backend: { name: 'bar' }, media_folder: 'baz', collections: {} }); + validateConfig({ + foo: 'bar', + backend: { name: 'bar' }, + media_folder: 'baz', + collections: {}, + }); }).toThrowError("'collections' should be array"); }); it('should throw if collections is an empty array in config', () => { expect(() => { - validateConfig({ foo: 'bar', backend: { name: 'bar' }, media_folder: 'baz', collections: [] }); + validateConfig({ + foo: 'bar', + backend: { name: 'bar' }, + media_folder: 'baz', + collections: [], + }); }).toThrowError("'collections' should NOT have less than 1 items"); }); it('should throw if collections is an array with a single null element in config', () => { expect(() => { - validateConfig({ foo: 'bar', backend: { name: 'bar' }, media_folder: 'baz', collections: [null] }); + validateConfig({ + foo: 'bar', + backend: { name: 'bar' }, + media_folder: 'baz', + collections: [null], + }); }).toThrowError("'collections[0]' should be object"); }); }); -}); \ No newline at end of file +}); diff --git a/packages/netlify-cms-core/src/constants/configSchema.js b/packages/netlify-cms-core/src/constants/configSchema.js index 66a9244c..329c4e9b 100644 --- a/packages/netlify-cms-core/src/constants/configSchema.js +++ b/packages/netlify-cms-core/src/constants/configSchema.js @@ -1,28 +1,24 @@ import AJV from 'ajv'; import ajvErrors from 'ajv-errors'; -import { - formatExtensions, - frontmatterFormats, - extensionFormatters, -} from "Formats/formats"; -import { IDENTIFIER_FIELDS } from "Constants/fieldInference"; +import { formatExtensions, frontmatterFormats, extensionFormatters } from 'Formats/formats'; +import { IDENTIFIER_FIELDS } from 'Constants/fieldInference'; /** * Config for fields in both file and folder collections. */ const fieldsConfig = { - type: "array", + type: 'array', minItems: 1, items: { // ------- Each field: ------- - type: "object", + type: 'object', properties: { - name: { type: "string" }, - label: { type: "string" }, - widget: { type: "string" }, - required: { type: "boolean" }, + name: { type: 'string' }, + label: { type: 'string' }, + widget: { type: 'string' }, + required: { type: 'boolean' }, }, - required: ["name"], + required: ['name'], }, }; @@ -32,72 +28,72 @@ const fieldsConfig = { * where the imports get resolved asyncronously. */ const getConfigSchema = () => ({ - type: "object", + type: 'object', properties: { backend: { - type: "object", - properties: { name: { type: "string", examples: ["test-repo"] } }, - required: ["name"], + type: 'object', + properties: { name: { type: 'string', examples: ['test-repo'] } }, + required: ['name'], }, - display_url: { type: "string", examples: ["https://example.com"] }, - media_folder: { type: "string", examples: ["assets/uploads"] }, - public_folder: { type: "string", examples: ["/uploads"] }, + display_url: { type: 'string', examples: ['https://example.com'] }, + media_folder: { type: 'string', examples: ['assets/uploads'] }, + public_folder: { type: 'string', examples: ['/uploads'] }, publish_mode: { - type: "string", - enum: ["editorial_workflow"], - examples: ["editorial_workflow"], + type: 'string', + enum: ['editorial_workflow'], + examples: ['editorial_workflow'], }, slug: { - type: "object", + type: 'object', properties: { - encoding: { type: "string", enum: ["unicode", "ascii"] }, - clean_accents: { type: "boolean" }, + encoding: { type: 'string', enum: ['unicode', 'ascii'] }, + clean_accents: { type: 'boolean' }, }, }, collections: { - type: "array", + type: 'array', minItems: 1, items: { // ------- Each collection: ------- - type: "object", + type: 'object', properties: { - name: { type: "string" }, - label: { type: "string" }, - label_singular: { type: "string" }, - description: { type: "string" }, - folder: { type: "string" }, + name: { type: 'string' }, + label: { type: 'string' }, + label_singular: { type: 'string' }, + description: { type: 'string' }, + folder: { type: 'string' }, files: { - type: "array", + type: 'array', items: { // ------- Each file: ------- - type: "object", + type: 'object', properties: { - name: { type: "string" }, - label: { type: "string" }, - label_singular: { type: "string" }, - description: { type: "string" }, - file: { type: "string" }, + name: { type: 'string' }, + label: { type: 'string' }, + label_singular: { type: 'string' }, + description: { type: 'string' }, + file: { type: 'string' }, fields: fieldsConfig, }, - required: ["name", "label", "file", "fields"], + required: ['name', 'label', 'file', 'fields'], }, }, - slug: { type: "string" }, - create: { type: "boolean" }, + slug: { type: 'string' }, + create: { type: 'boolean' }, editor: { - type: "object", + type: 'object', properties: { - preview: { type: "boolean" }, + preview: { type: 'boolean' }, }, }, - format: { type: "string", enum: Object.keys(formatExtensions) }, - extension: { type: "string" }, - frontmatter_delimiter: { type: "string" }, + format: { type: 'string', enum: Object.keys(formatExtensions) }, + extension: { type: 'string' }, + frontmatter_delimiter: { type: 'string' }, fields: fieldsConfig, }, - required: ["name", "label"], - oneOf: [{ required: ["files"] }, { required: ["folder", "fields"] }], - if: { required: ["extension"] }, + required: ['name', 'label'], + oneOf: [{ required: ['files'] }, { required: ['folder', 'fields'] }], + if: { required: ['extension'] }, then: { // Cannot infer format from extension. if: { @@ -105,14 +101,14 @@ const getConfigSchema = () => ({ extension: { enum: Object.keys(extensionFormatters) }, }, }, - else: { required: ["format"] }, + else: { required: ['format'] }, }, dependencies: { frontmatter_delimiter: { properties: { format: { enum: frontmatterFormats }, }, - required: ["format"], + required: ['format'], }, folder: { errorMessage: { @@ -132,7 +128,7 @@ const getConfigSchema = () => ({ }, }, }, - required: ["backend", "media_folder", "collections"], + required: ['backend', 'media_folder', 'collections'], }); class ConfigError extends Error { @@ -141,13 +137,13 @@ class ConfigError extends Error { .map(({ message, dataPath }) => { const dotPath = dataPath .slice(1) - .split("/") + .split('/') .map(seg => (seg.match(/^\d+$/) ? `[${seg}]` : `.${seg}`)) - .join("") + .join('') .slice(1); - return `${dotPath ? `'${dotPath}'` : "config"} ${message}`; + return `${dotPath ? `'${dotPath}'` : 'config'} ${message}`; }) - .join("\n"); + .join('\n'); super(message, ...args); this.errors = errors; @@ -161,7 +157,7 @@ class ConfigError extends Error { /** * `validateConfig` is a pure function. It does not mutate - * the config that is passed in. + * the config that is passed in. */ export function validateConfig(config) { const ajv = new AJV({ allErrors: true, jsonPointers: true }); diff --git a/packages/netlify-cms-core/src/constants/fieldInference.js b/packages/netlify-cms-core/src/constants/fieldInference.js index 9f765937..7f3c6680 100644 --- a/packages/netlify-cms-core/src/constants/fieldInference.js +++ b/packages/netlify-cms-core/src/constants/fieldInference.js @@ -7,7 +7,7 @@ export const INFERABLE_FIELDS = { type: 'string', secondaryTypes: [], synonyms: ['title', 'name', 'label', 'headline', 'header'], - defaultPreview: value =>

    { value }

    , // eslint-disable-line react/display-name + defaultPreview: value =>

    {value}

    , // eslint-disable-line react/display-name fallbackToFirstField: true, showError: true, }, @@ -15,7 +15,7 @@ export const INFERABLE_FIELDS = { type: 'string', secondaryTypes: [], synonyms: ['short_title', 'shortTitle', 'short'], - defaultPreview: value =>

    { value }

    , // eslint-disable-line react/display-name + defaultPreview: value =>

    {value}

    , // eslint-disable-line react/display-name fallbackToFirstField: false, showError: false, }, @@ -23,14 +23,26 @@ export const INFERABLE_FIELDS = { type: 'string', secondaryTypes: [], synonyms: ['author', 'name', 'by', 'byline', 'owner'], - defaultPreview: value => { value }, // eslint-disable-line react/display-name + defaultPreview: value => {value}, // eslint-disable-line react/display-name fallbackToFirstField: false, showError: false, }, description: { type: 'string', secondaryTypes: ['text', 'markdown'], - synonyms: ['shortDescription', 'short_description', 'shortdescription', 'description', 'intro', 'introduction', 'brief', 'content', 'biography', 'bio', 'summary'], + synonyms: [ + 'shortDescription', + 'short_description', + 'shortdescription', + 'description', + 'intro', + 'introduction', + 'brief', + 'content', + 'biography', + 'bio', + 'summary', + ], defaultPreview: value => value, fallbackToFirstField: false, showError: false, diff --git a/packages/netlify-cms-core/src/formats/__tests__/frontmatter.spec.js b/packages/netlify-cms-core/src/formats/__tests__/frontmatter.spec.js index 3c12116b..1c04964b 100644 --- a/packages/netlify-cms-core/src/formats/__tests__/frontmatter.spec.js +++ b/packages/netlify-cms-core/src/formats/__tests__/frontmatter.spec.js @@ -1,166 +1,167 @@ -import { FrontmatterInfer, frontmatterJSON, frontmatterTOML, frontmatterYAML } from '../frontmatter'; +import { + FrontmatterInfer, + frontmatterJSON, + frontmatterTOML, + frontmatterYAML, +} from '../frontmatter'; -jest.mock("../../valueObjects/AssetProxy.js"); +jest.mock('../../valueObjects/AssetProxy.js'); describe('Frontmatter', () => { it('should parse YAML with --- delimiters', () => { expect( - FrontmatterInfer.fromFile('---\ntitle: YAML\ndescription: Something longer\n---\nContent') - ).toEqual( - { - title: 'YAML', - description: 'Something longer', - body: 'Content', - } - ); + FrontmatterInfer.fromFile('---\ntitle: YAML\ndescription: Something longer\n---\nContent'), + ).toEqual({ + title: 'YAML', + description: 'Something longer', + body: 'Content', + }); }); it('should parse YAML with --- delimiters when it is explicitly set as the format without a custom delimiter', () => { expect( - frontmatterYAML().fromFile('---\ntitle: YAML\ndescription: Something longer\n---\nContent') - ).toEqual( - { - title: 'YAML', - description: 'Something longer', - body: 'Content', - } - ); + frontmatterYAML().fromFile('---\ntitle: YAML\ndescription: Something longer\n---\nContent'), + ).toEqual({ + title: 'YAML', + description: 'Something longer', + body: 'Content', + }); }); it('should parse YAML with custom delimiters when it is explicitly set as the format with a custom delimiter', () => { expect( - frontmatterYAML("~~~").fromFile('~~~\ntitle: YAML\ndescription: Something longer\n~~~\nContent') - ).toEqual( - { - title: 'YAML', - description: 'Something longer', - body: 'Content', - } - ); + frontmatterYAML('~~~').fromFile( + '~~~\ntitle: YAML\ndescription: Something longer\n~~~\nContent', + ), + ).toEqual({ + title: 'YAML', + description: 'Something longer', + body: 'Content', + }); }); it('should parse YAML with custom delimiters when it is explicitly set as the format with different custom delimiters', () => { expect( - frontmatterYAML(["~~~", "^^^"]).fromFile('~~~\ntitle: YAML\ndescription: Something longer\n^^^\nContent') - ).toEqual( - { - title: 'YAML', - description: 'Something longer', - body: 'Content', - } - ); + frontmatterYAML(['~~~', '^^^']).fromFile( + '~~~\ntitle: YAML\ndescription: Something longer\n^^^\nContent', + ), + ).toEqual({ + title: 'YAML', + description: 'Something longer', + body: 'Content', + }); }); it('should parse YAML with ---yaml delimiters', () => { expect( - FrontmatterInfer.fromFile('---yaml\ntitle: YAML\ndescription: Something longer\n---\nContent') - ).toEqual( - { - title: 'YAML', - description: 'Something longer', - body: 'Content', - } - ); + FrontmatterInfer.fromFile( + '---yaml\ntitle: YAML\ndescription: Something longer\n---\nContent', + ), + ).toEqual({ + title: 'YAML', + description: 'Something longer', + body: 'Content', + }); }); it('should overwrite any body param in the front matter', () => { expect( - FrontmatterInfer.fromFile('---\ntitle: The Title\nbody: Something longer\n---\nContent') - ).toEqual( - { - title: 'The Title', - body: 'Content', - } - ); + FrontmatterInfer.fromFile('---\ntitle: The Title\nbody: Something longer\n---\nContent'), + ).toEqual({ + title: 'The Title', + body: 'Content', + }); }); it('should parse TOML with +++ delimiters', () => { expect( - FrontmatterInfer.fromFile('+++\ntitle = "TOML"\ndescription = "Front matter"\n+++\nContent') - ).toEqual( - { - title: 'TOML', - description: 'Front matter', - body: 'Content', - } - ); + FrontmatterInfer.fromFile('+++\ntitle = "TOML"\ndescription = "Front matter"\n+++\nContent'), + ).toEqual({ + title: 'TOML', + description: 'Front matter', + body: 'Content', + }); }); it('should parse TOML with +++ delimiters when it is explicitly set as the format without a custom delimiter', () => { expect( - frontmatterTOML("~~~").fromFile('~~~\ntitle = "TOML"\ndescription = "Front matter"\n~~~\nContent') - ).toEqual( - { - title: 'TOML', - description: 'Front matter', - body: 'Content', - } - ); + frontmatterTOML('~~~').fromFile( + '~~~\ntitle = "TOML"\ndescription = "Front matter"\n~~~\nContent', + ), + ).toEqual({ + title: 'TOML', + description: 'Front matter', + body: 'Content', + }); }); it('should parse TOML with ---toml delimiters', () => { expect( - FrontmatterInfer.fromFile('---toml\ntitle = "TOML"\ndescription = "Something longer"\n---\nContent') - ).toEqual( - { - title: 'TOML', - description: 'Something longer', - body: 'Content', - } - ); + FrontmatterInfer.fromFile( + '---toml\ntitle = "TOML"\ndescription = "Something longer"\n---\nContent', + ), + ).toEqual({ + title: 'TOML', + description: 'Something longer', + body: 'Content', + }); }); it('should parse JSON with { } delimiters', () => { expect( - FrontmatterInfer.fromFile('{\n"title": "The Title",\n"description": "Something longer"\n}\nContent') - ).toEqual( - { - title: 'The Title', - description: 'Something longer', - body: 'Content', - } - ); + FrontmatterInfer.fromFile( + '{\n"title": "The Title",\n"description": "Something longer"\n}\nContent', + ), + ).toEqual({ + title: 'The Title', + description: 'Something longer', + body: 'Content', + }); }); it('should parse JSON with { } delimiters when it is explicitly set as the format without a custom delimiter', () => { expect( - frontmatterJSON().fromFile('{\n"title": "The Title",\n"description": "Something longer"\n}\nContent') - ).toEqual( - { - title: 'The Title', - description: 'Something longer', - body: 'Content', - } - ); + frontmatterJSON().fromFile( + '{\n"title": "The Title",\n"description": "Something longer"\n}\nContent', + ), + ).toEqual({ + title: 'The Title', + description: 'Something longer', + body: 'Content', + }); }); it('should parse JSON with { } delimiters when it is explicitly set as the format with a custom delimiter', () => { expect( - frontmatterJSON("~~~").fromFile('~~~\n"title": "The Title",\n"description": "Something longer"\n~~~\nContent') - ).toEqual( - { - title: 'The Title', - description: 'Something longer', - body: 'Content', - } - ); + frontmatterJSON('~~~').fromFile( + '~~~\n"title": "The Title",\n"description": "Something longer"\n~~~\nContent', + ), + ).toEqual({ + title: 'The Title', + description: 'Something longer', + body: 'Content', + }); }); it('should parse JSON with ---json delimiters', () => { expect( - FrontmatterInfer.fromFile('---json\n{\n"title": "The Title",\n"description": "Something longer"\n}\n---\nContent') - ).toEqual( - { - title: 'The Title', - description: 'Something longer', - body: 'Content', - } - ); + FrontmatterInfer.fromFile( + '---json\n{\n"title": "The Title",\n"description": "Something longer"\n}\n---\nContent', + ), + ).toEqual({ + title: 'The Title', + description: 'Something longer', + body: 'Content', + }); }); it('should stringify YAML with --- delimiters', () => { expect( - FrontmatterInfer.toFile({ body: 'Some content\nOn another line', tags: ['front matter', 'yaml'], title: 'YAML' }) + FrontmatterInfer.toFile({ + body: 'Some content\nOn another line', + tags: ['front matter', 'yaml'], + title: 'YAML', + }), ).toEqual( [ '---', @@ -171,31 +172,23 @@ describe('Frontmatter', () => { '---', 'Some content', 'On another line\n', - ].join('\n') + ].join('\n'), ); }); it('should stringify YAML with missing body', () => { - expect( - FrontmatterInfer.toFile({ tags: ['front matter', 'yaml'], title: 'YAML' }) - ).toEqual( - [ - '---', - 'tags:', - ' - front matter', - ' - yaml', - 'title: YAML', - '---', - '', - '', - ].join('\n') + expect(FrontmatterInfer.toFile({ tags: ['front matter', 'yaml'], title: 'YAML' })).toEqual( + ['---', 'tags:', ' - front matter', ' - yaml', 'title: YAML', '---', '', ''].join('\n'), ); }); - it('should stringify YAML with --- delimiters when it is explicitly set as the format without a custom delimiter', - () => { + it('should stringify YAML with --- delimiters when it is explicitly set as the format without a custom delimiter', () => { expect( - frontmatterYAML().toFile({ body: 'Some content\nOn another line', tags: ['front matter', 'yaml'], title: 'YAML' }) + frontmatterYAML().toFile({ + body: 'Some content\nOn another line', + tags: ['front matter', 'yaml'], + title: 'YAML', + }), ).toEqual( [ '---', @@ -206,14 +199,17 @@ describe('Frontmatter', () => { '---', 'Some content', 'On another line\n', - ].join('\n') - ); + ].join('\n'), + ); }); - it('should stringify YAML with --- delimiters when it is explicitly set as the format with a custom delimiter', - () => { + it('should stringify YAML with --- delimiters when it is explicitly set as the format with a custom delimiter', () => { expect( - frontmatterYAML("~~~").toFile({ body: 'Some content\nOn another line', tags: ['front matter', 'yaml'], title: 'YAML' }) + frontmatterYAML('~~~').toFile({ + body: 'Some content\nOn another line', + tags: ['front matter', 'yaml'], + title: 'YAML', + }), ).toEqual( [ '~~~', @@ -224,14 +220,17 @@ describe('Frontmatter', () => { '~~~', 'Some content', 'On another line\n', - ].join('\n') - ); + ].join('\n'), + ); }); - it('should stringify YAML with --- delimiters when it is explicitly set as the format with different custom delimiters', - () => { + it('should stringify YAML with --- delimiters when it is explicitly set as the format with different custom delimiters', () => { expect( - frontmatterYAML(["~~~", "^^^"]).toFile({ body: 'Some content\nOn another line', tags: ['front matter', 'yaml'], title: 'YAML' }) + frontmatterYAML(['~~~', '^^^']).toFile({ + body: 'Some content\nOn another line', + tags: ['front matter', 'yaml'], + title: 'YAML', + }), ).toEqual( [ '~~~', @@ -242,14 +241,17 @@ describe('Frontmatter', () => { '^^^', 'Some content', 'On another line\n', - ].join('\n') - ); + ].join('\n'), + ); }); - it('should stringify TOML with +++ delimiters when it is explicitly set as the format without a custom delimiter', - () => { + it('should stringify TOML with +++ delimiters when it is explicitly set as the format without a custom delimiter', () => { expect( - frontmatterTOML().toFile({ body: 'Some content\nOn another line', tags: ['front matter', 'toml'], title: 'TOML' }) + frontmatterTOML().toFile({ + body: 'Some content\nOn another line', + tags: ['front matter', 'toml'], + title: 'TOML', + }), ).toEqual( [ '+++', @@ -258,14 +260,17 @@ describe('Frontmatter', () => { '+++', 'Some content', 'On another line\n', - ].join('\n') - ); + ].join('\n'), + ); }); - it('should stringify TOML with +++ delimiters when it is explicitly set as the format with a custom delimiter', - () => { + it('should stringify TOML with +++ delimiters when it is explicitly set as the format with a custom delimiter', () => { expect( - frontmatterTOML("~~~").toFile({ body: 'Some content\nOn another line', tags: ['front matter', 'toml'], title: 'TOML' }) + frontmatterTOML('~~~').toFile({ + body: 'Some content\nOn another line', + tags: ['front matter', 'toml'], + title: 'TOML', + }), ).toEqual( [ '~~~', @@ -274,14 +279,17 @@ describe('Frontmatter', () => { '~~~', 'Some content', 'On another line\n', - ].join('\n') - ); + ].join('\n'), + ); }); - it('should stringify JSON with { } delimiters when it is explicitly set as the format without a custom delimiter', - () => { + it('should stringify JSON with { } delimiters when it is explicitly set as the format without a custom delimiter', () => { expect( - frontmatterJSON().toFile({ body: 'Some content\nOn another line', tags: ['front matter', 'json'], title: 'JSON' }) + frontmatterJSON().toFile({ + body: 'Some content\nOn another line', + tags: ['front matter', 'json'], + title: 'JSON', + }), ).toEqual( [ '{', @@ -293,14 +301,17 @@ describe('Frontmatter', () => { '}', 'Some content', 'On another line\n', - ].join('\n') - ); + ].join('\n'), + ); }); - it('should stringify JSON with { } delimiters when it is explicitly set as the format with a custom delimiter', - () => { + it('should stringify JSON with { } delimiters when it is explicitly set as the format with a custom delimiter', () => { expect( - frontmatterJSON("~~~").toFile({ body: 'Some content\nOn another line', tags: ['front matter', 'json'], title: 'JSON' }) + frontmatterJSON('~~~').toFile({ + body: 'Some content\nOn another line', + tags: ['front matter', 'json'], + title: 'JSON', + }), ).toEqual( [ '~~~', @@ -312,7 +323,7 @@ describe('Frontmatter', () => { '~~~', 'Some content', 'On another line\n', - ].join('\n') - ); + ].join('\n'), + ); }); }); diff --git a/packages/netlify-cms-core/src/formats/__tests__/tomlFormatter.spec.js b/packages/netlify-cms-core/src/formats/__tests__/tomlFormatter.spec.js index 54d39896..7b89b6f1 100644 --- a/packages/netlify-cms-core/src/formats/__tests__/tomlFormatter.spec.js +++ b/packages/netlify-cms-core/src/formats/__tests__/tomlFormatter.spec.js @@ -2,14 +2,8 @@ import tomlFormatter from '../toml'; describe('tomlFormatter', () => { it('should output TOML integer values without decimals', () => { - expect( - tomlFormatter.toFile({ testFloat: 123.456, testInteger: 789, title: 'TOML' }) - ).toEqual( - [ - 'testFloat = 123.456', - 'testInteger = 789', - 'title = "TOML"' - ].join('\n') - ); + expect(tomlFormatter.toFile({ testFloat: 123.456, testInteger: 789, title: 'TOML' })).toEqual( + ['testFloat = 123.456', 'testInteger = 789', 'title = "TOML"'].join('\n'), + ); }); }); diff --git a/packages/netlify-cms-core/src/formats/formats.js b/packages/netlify-cms-core/src/formats/formats.js index 21a55d8d..4fc0729c 100644 --- a/packages/netlify-cms-core/src/formats/formats.js +++ b/packages/netlify-cms-core/src/formats/formats.js @@ -5,7 +5,7 @@ import tomlFormatter from './toml'; import jsonFormatter from './json'; import { FrontmatterInfer, frontmatterJSON, frontmatterTOML, frontmatterYAML } from './frontmatter'; -export const frontmatterFormats = ['yaml-frontmatter','toml-frontmatter','json-frontmatter'] +export const frontmatterFormats = ['yaml-frontmatter', 'toml-frontmatter', 'json-frontmatter']; export const formatExtensions = { yml: 'yml', @@ -28,21 +28,24 @@ export const extensionFormatters = { html: FrontmatterInfer, }; -const formatByName = (name, customDelimiter) => ({ - yml: yamlFormatter, - yaml: yamlFormatter, - toml: tomlFormatter, - json: jsonFormatter, - frontmatter: FrontmatterInfer, - 'json-frontmatter': frontmatterJSON(customDelimiter), - 'toml-frontmatter': frontmatterTOML(customDelimiter), - 'yaml-frontmatter': frontmatterYAML(customDelimiter), -}[name]); +const formatByName = (name, customDelimiter) => + ({ + yml: yamlFormatter, + yaml: yamlFormatter, + toml: tomlFormatter, + json: jsonFormatter, + frontmatter: FrontmatterInfer, + 'json-frontmatter': frontmatterJSON(customDelimiter), + 'toml-frontmatter': frontmatterTOML(customDelimiter), + 'yaml-frontmatter': frontmatterYAML(customDelimiter), + }[name]); export function resolveFormat(collectionOrEntity, entry) { // Check for custom delimiter const frontmatter_delimiter = collectionOrEntity.get('frontmatter_delimiter'); - const customDelimiter = List.isList(frontmatter_delimiter) ? frontmatter_delimiter.toArray() : frontmatter_delimiter; + const customDelimiter = List.isList(frontmatter_delimiter) + ? frontmatter_delimiter.toArray() + : frontmatter_delimiter; // If the format is specified in the collection, use that format. const formatSpecification = collectionOrEntity.get('format'); diff --git a/packages/netlify-cms-core/src/formats/frontmatter.js b/packages/netlify-cms-core/src/formats/frontmatter.js index 804d9ac6..3f116505 100644 --- a/packages/netlify-cms-core/src/formats/frontmatter.js +++ b/packages/netlify-cms-core/src/formats/frontmatter.js @@ -33,31 +33,32 @@ const parsers = { parse: input => yamlFormatter.fromFile(input), stringify: (metadata, { sortedKeys }) => yamlFormatter.toFile(metadata, sortedKeys), }, -} +}; function inferFrontmatterFormat(str) { const firstLine = str.substr(0, str.indexOf('\n')).trim(); - if ((firstLine.length > 3) && (firstLine.substr(0, 3) === "---")) { + if (firstLine.length > 3 && firstLine.substr(0, 3) === '---') { // No need to infer, `gray-matter` will handle things like `---toml` for us. return; } switch (firstLine) { - case "---": + case '---': return getFormatOpts('yaml'); - case "+++": + case '+++': return getFormatOpts('toml'); - case "{": + case '{': return getFormatOpts('json'); default: - throw "Unrecognized front-matter format."; + throw 'Unrecognized front-matter format.'; } } -export const getFormatOpts = format => ({ - yaml: { language: "yaml", delimiters: "---" }, - toml: { language: "toml", delimiters: "+++" }, - json: { language: "json", delimiters: ["{", "}"] }, -}[format]); +export const getFormatOpts = format => + ({ + yaml: { language: 'yaml', delimiters: '---' }, + toml: { language: 'toml', delimiters: '+++' }, + json: { language: 'json', delimiters: ['{', '}'] }, + }[format]); class FrontmatterFormatter { constructor(format, customDelimiter) { diff --git a/packages/netlify-cms-core/src/formats/json.js b/packages/netlify-cms-core/src/formats/json.js index f7579b9a..2de169f7 100644 --- a/packages/netlify-cms-core/src/formats/json.js +++ b/packages/netlify-cms-core/src/formats/json.js @@ -5,5 +5,5 @@ export default { toFile(data) { return JSON.stringify(data, null, 2); - } -} + }, +}; diff --git a/packages/netlify-cms-core/src/formats/toml.js b/packages/netlify-cms-core/src/formats/toml.js index 82a532c4..7741bb4b 100644 --- a/packages/netlify-cms-core/src/formats/toml.js +++ b/packages/netlify-cms-core/src/formats/toml.js @@ -9,7 +9,7 @@ const outputReplacer = (key, value) => { return value.format(value._f); } if (value instanceof AssetProxy) { - return `${ value.path }`; + return `${value.path}`; } if (Number.isInteger(value)) { // Return the string representation of integers so tomlify won't render with tenths (".0") @@ -26,5 +26,5 @@ export default { toFile(data, sortedKeys = []) { return tomlify.toToml(data, { replace: outputReplacer, sort: sortKeys(sortedKeys) }); - } -} + }, +}; diff --git a/packages/netlify-cms-core/src/formats/yaml.js b/packages/netlify-cms-core/src/formats/yaml.js index 040d5182..18d26a64 100644 --- a/packages/netlify-cms-core/src/formats/yaml.js +++ b/packages/netlify-cms-core/src/formats/yaml.js @@ -20,7 +20,7 @@ const ImageType = new yaml.Type('image', { kind: 'scalar', instanceOf: AssetProxy, represent(value) { - return `${ value.path }`; + return `${value.path}`; }, resolve(value) { if (value === null) return false; @@ -29,7 +29,6 @@ const ImageType = new yaml.Type('image', { }, }); - const OutputSchema = new yaml.Schema({ include: yaml.DEFAULT_SAFE_SCHEMA.include, implicit: [MomentType, ImageType].concat(yaml.DEFAULT_SAFE_SCHEMA.implicit), @@ -43,5 +42,5 @@ export default { toFile(data, sortedKeys = []) { return yaml.safeDump(data, { schema: OutputSchema, sortKeys: sortKeys(sortedKeys) }); - } -} + }, +}; diff --git a/packages/netlify-cms-core/src/integrations/index.js b/packages/netlify-cms-core/src/integrations/index.js index 3cd7ad2d..c5c51f4e 100644 --- a/packages/netlify-cms-core/src/integrations/index.js +++ b/packages/netlify-cms-core/src/integrations/index.js @@ -10,14 +10,16 @@ export function resolveIntegrations(interationsConfig, getToken) { integrationInstances = integrationInstances.set('algolia', new Algolia(providerData)); break; case 'assetStore': - integrationInstances = integrationInstances.set('assetStore', new AssetStore(providerData, getToken)); + integrationInstances = integrationInstances.set( + 'assetStore', + new AssetStore(providerData, getToken), + ); break; } }); return integrationInstances; } - export const getIntegrationProvider = (function() { let integrations = null; @@ -29,4 +31,4 @@ export const getIntegrationProvider = (function() { return integrations.get(provider); } }; -}()); +})(); diff --git a/packages/netlify-cms-core/src/integrations/providers/algolia/implementation.js b/packages/netlify-cms-core/src/integrations/providers/algolia/implementation.js index bd5d26f8..0e2da9ab 100644 --- a/packages/netlify-cms-core/src/integrations/providers/algolia/implementation.js +++ b/packages/netlify-cms-core/src/integrations/providers/algolia/implementation.js @@ -3,14 +3,16 @@ import { createEntry } from 'ValueObjects/Entry'; import { selectEntrySlug } from 'Reducers/collections'; function getSlug(path) { - return path.split('/').pop().replace(/\.[^.]+$/, ''); + return path + .split('/') + .pop() + .replace(/\.[^.]+$/, ''); } export default class Algolia { constructor(config) { this.config = config; - if (config.get('applicationID') == null || - config.get('apiKey') == null) { + if (config.get('applicationID') == null || config.get('apiKey') == null) { throw 'The Algolia search integration needs the credentials (applicationID and apiKey) in the integration configuration.'; } @@ -18,9 +20,9 @@ export default class Algolia { this.apiKey = config.get('apiKey'); const prefix = config.get('indexPrefix'); - this.indexPrefix = prefix ? `${ prefix }-` : ''; + this.indexPrefix = prefix ? `${prefix}-` : ''; - this.searchURL = `https://${ this.applicationID }-dsn.algolia.net/1`; + this.searchURL = `https://${this.applicationID}-dsn.algolia.net/1`; this.entriesCache = { collection: null, @@ -39,7 +41,7 @@ export default class Algolia { } parseJsonResponse(response) { - return response.json().then((json) => { + return response.json().then(json => { if (!response.ok) { return Promise.reject(json); } @@ -52,11 +54,11 @@ export default class Algolia { const params = []; if (options.params) { for (const key in options.params) { - params.push(`${ key }=${ encodeURIComponent(options.params[key]) }`); + params.push(`${key}=${encodeURIComponent(options.params[key])}`); } } if (params.length) { - path += `?${ params.join('&') }`; + path += `?${params.join('&')}`; } return path; } @@ -64,7 +66,7 @@ export default class Algolia { request(path, options = {}) { const headers = this.requestHeaders(options.headers || {}); const url = this.urlFor(path, options); - return fetch(url, { ...options, headers }).then((response) => { + return fetch(url, { ...options, headers }).then(response => { const contentType = response.headers.get('Content-Type'); if (contentType && contentType.match(/json/)) { return this.parseJsonResponse(response); @@ -75,25 +77,28 @@ export default class Algolia { } search(collections, searchTerm, page) { - const searchCollections = collections.map(collection => ( - { indexName: `${ this.indexPrefix }${ collection }`, params: `query=${ searchTerm }&page=${ page }` } - )); + const searchCollections = collections.map(collection => ({ + indexName: `${this.indexPrefix}${collection}`, + params: `query=${searchTerm}&page=${page}`, + })); - return this.request(`${ this.searchURL }/indexes/*/queries`, { + return this.request(`${this.searchURL}/indexes/*/queries`, { method: 'POST', body: JSON.stringify({ requests: searchCollections }), - }).then((response) => { - const entries = response.results.map((result, index) => result.hits.map((hit) => { - const slug = getSlug(hit.path); - return createEntry(collections[index], slug, hit.path, { data: hit.data, partial: true }); - })); + }).then(response => { + const entries = response.results.map((result, index) => + result.hits.map(hit => { + const slug = getSlug(hit.path); + return createEntry(collections[index], slug, hit.path, { data: hit.data, partial: true }); + }), + ); return { entries: _.flatten(entries), pagination: page }; }); } searchBy(field, collection, query) { - return this.request(`${ this.searchURL }/indexes/${ this.indexPrefix }${ collection }`, { + return this.request(`${this.searchURL}/indexes/${this.indexPrefix}${collection}`, { params: { restrictSearchableAttributes: field, query, @@ -105,12 +110,18 @@ export default class Algolia { if (this.entriesCache.collection === collection && this.entriesCache.page === page) { return Promise.resolve({ page: this.entriesCache.page, entries: this.entriesCache.entries }); } else { - return this.request(`${ this.searchURL }/indexes/${ this.indexPrefix }${ collection.get('name') }`, { - params: { page }, - }).then((response) => { - const entries = response.hits.map((hit) => { + return this.request( + `${this.searchURL}/indexes/${this.indexPrefix}${collection.get('name')}`, + { + params: { page }, + }, + ).then(response => { + const entries = response.hits.map(hit => { const slug = selectEntrySlug(collection, hit.path); - return createEntry(collection.get('name'), slug, hit.path, { data: hit.data, partial: true }); + return createEntry(collection.get('name'), slug, hit.path, { + data: hit.data, + partial: true, + }); }); this.entriesCache = { collection, pagination: response.page, entries }; return { entries, pagination: response.page }; @@ -119,9 +130,12 @@ export default class Algolia { } getEntry(collection, slug) { - return this.searchBy('slug', collection.get('name'), slug).then((response) => { + return this.searchBy('slug', collection.get('name'), slug).then(response => { const entry = response.hits.filter(hit => hit.slug === slug)[0]; - return createEntry(collection.get('name'), slug, entry.path, { data: entry.data, partial: true }); + return createEntry(collection.get('name'), slug, entry.path, { + data: entry.data, + partial: true, + }); }); } } diff --git a/packages/netlify-cms-core/src/integrations/providers/assetStore/implementation.js b/packages/netlify-cms-core/src/integrations/providers/assetStore/implementation.js index 0210819f..eb200976 100644 --- a/packages/netlify-cms-core/src/integrations/providers/assetStore/implementation.js +++ b/packages/netlify-cms-core/src/integrations/providers/assetStore/implementation.js @@ -14,7 +14,7 @@ export default class AssetStore { } parseJsonResponse(response) { - return response.json().then((json) => { + return response.json().then(json => { if (!response.ok) { return Promise.reject(json); } @@ -27,16 +27,15 @@ export default class AssetStore { const params = []; if (options.params) { for (const key in options.params) { - params.push(`${ key }=${ encodeURIComponent(options.params[key]) }`); + params.push(`${key}=${encodeURIComponent(options.params[key])}`); } } if (params.length) { - path += `?${ params.join('&') }`; + path += `?${params.join('&')}`; } return path; } - requestHeaders(headers = {}) { return { ...headers, @@ -44,15 +43,16 @@ export default class AssetStore { } confirmRequest(assetID) { - this.getToken() - .then(token => this.request(`${ this.getSignedFormURL }/${ assetID }`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${ token }`, - }, - body: JSON.stringify({ state: 'uploaded' }), - })); + this.getToken().then(token => + this.request(`${this.getSignedFormURL}/${assetID}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ state: 'uploaded' }), + }), + ); } async request(path, options = {}) { @@ -66,12 +66,15 @@ export default class AssetStore { } async retrieve(query, page, privateUpload) { - const params = pickBy({ search: query, page, filter: privateUpload ? 'private' : 'public' }, val => !!val); + const params = pickBy( + { search: query, page, filter: privateUpload ? 'private' : 'public' }, + val => !!val, + ); const url = addParams(this.getSignedFormURL, params); const token = await this.getToken(); const headers = { 'Content-Type': 'application/json', - 'Authorization': `Bearer ${ token }`, + Authorization: `Bearer ${token}`, }; const response = await this.request(url, { headers }); const files = response.map(({ id, name, size, url }) => { @@ -81,21 +84,22 @@ export default class AssetStore { } delete(assetID) { - const url = `${ this.getSignedFormURL }/${ assetID }` - return this.getToken() - .then(token => this.request(url, { + const url = `${this.getSignedFormURL}/${assetID}`; + return this.getToken().then(token => + this.request(url, { method: 'DELETE', headers: { 'Content-Type': 'application/json', - 'Authorization': `Bearer ${ token }`, + Authorization: `Bearer ${token}`, }, - })); + }), + ); } async upload(file, privateUpload = false) { const fileData = { name: file.name, - size: file.size + size: file.size, }; if (file.type) { fileData.content_type = file.type; @@ -111,7 +115,7 @@ export default class AssetStore { method: 'POST', headers: { 'Content-Type': 'application/json', - 'Authorization': `Bearer ${ token }`, + Authorization: `Bearer ${token}`, }, body: JSON.stringify(fileData), }); @@ -131,8 +135,7 @@ export default class AssetStore { const asset = { id, name, size, url, urlIsPublicPath: true }; return { success: true, url, asset }; - } - catch(error) { + } catch (error) { console.error(error); } } diff --git a/packages/netlify-cms-core/src/lib/__tests__/urlHelper.spec.js b/packages/netlify-cms-core/src/lib/__tests__/urlHelper.spec.js index de27c31c..f5d23d3b 100644 --- a/packages/netlify-cms-core/src/lib/__tests__/urlHelper.spec.js +++ b/packages/netlify-cms-core/src/lib/__tests__/urlHelper.spec.js @@ -4,111 +4,101 @@ import { sanitizeURI, sanitizeSlug } from '../urlHelper'; describe('sanitizeURI', () => { // `sanitizeURI` tests from RFC 3987 it('should keep valid URI chars (letters digits _ - . ~)', () => { - expect( - sanitizeURI("This, that-one_or.the~other 123!") - ).toEqual('Thisthat-one_or.the~other123'); + expect(sanitizeURI('This, that-one_or.the~other 123!')).toEqual('Thisthat-one_or.the~other123'); }); - + it('should not remove accents', () => { - expect( - sanitizeURI("ěščřžý") - ).toEqual('ěščřžý'); + expect(sanitizeURI('ěščřžý')).toEqual('ěščřžý'); }); - + it('should keep valid non-latin chars (ucschars in RFC 3987)', () => { - expect( - sanitizeURI("日本語のタイトル") - ).toEqual('日本語のタイトル'); + expect(sanitizeURI('日本語のタイトル')).toEqual('日本語のタイトル'); }); it('should not keep valid non-latin chars (ucschars in RFC 3987) if set to ASCII mode', () => { - expect( - sanitizeURI("ěščřžý日本語のタイトル", { encoding: 'ascii' }) - ).toEqual(''); + expect(sanitizeURI('ěščřžý日本語のタイトル', { encoding: 'ascii' })).toEqual(''); }); it('should not normalize Unicode strings', () => { - expect( - sanitizeURI('\u017F\u0323\u0307') - ).toEqual('\u017F\u0323\u0307'); - expect( - sanitizeURI('\u017F\u0323\u0307') - ).not.toEqual('\u1E9B\u0323'); + expect(sanitizeURI('\u017F\u0323\u0307')).toEqual('\u017F\u0323\u0307'); + expect(sanitizeURI('\u017F\u0323\u0307')).not.toEqual('\u1E9B\u0323'); }); - + it('should allow a custom replacement character', () => { - expect( - sanitizeURI("duck\\goose.elephant", { replacement: '-' }) - ).toEqual('duck-goose.elephant'); + expect(sanitizeURI('duck\\goose.elephant', { replacement: '-' })).toEqual( + 'duck-goose.elephant', + ); }); - + it('should not allow an improper replacement character', () => { expect(() => { - sanitizeURI("I! like! dollars!", { replacement: '$' }); - }).toThrow(); + sanitizeURI('I! like! dollars!', { replacement: '$' }); + }).toThrow(); }); - + it('should not actually URI-encode the characters', () => { - expect( - sanitizeURI("🎉") - ).toEqual('🎉'); - expect( - sanitizeURI("🎉") - ).not.toEqual("%F0%9F%8E%89"); + expect(sanitizeURI('🎉')).toEqual('🎉'); + expect(sanitizeURI('🎉')).not.toEqual('%F0%9F%8E%89'); }); }); - -describe('sanitizeSlug', ()=> { - +describe('sanitizeSlug', () => { it('throws an error for non-strings', () => { - expect(() => sanitizeSlug({})).toThrowError("The input slug must be a string."); - expect(() => sanitizeSlug([])).toThrowError("The input slug must be a string."); - expect(() => sanitizeSlug(false)).toThrowError("The input slug must be a string."); - expect(() => sanitizeSlug(null)).toThrowError("The input slug must be a string."); - expect(() => sanitizeSlug(11234)).toThrowError("The input slug must be a string."); - expect(() => sanitizeSlug(undefined)).toThrowError("The input slug must be a string."); - expect(() => sanitizeSlug(()=>{})).toThrowError("The input slug must be a string."); + expect(() => sanitizeSlug({})).toThrowError('The input slug must be a string.'); + expect(() => sanitizeSlug([])).toThrowError('The input slug must be a string.'); + expect(() => sanitizeSlug(false)).toThrowError('The input slug must be a string.'); + expect(() => sanitizeSlug(null)).toThrowError('The input slug must be a string.'); + expect(() => sanitizeSlug(11234)).toThrowError('The input slug must be a string.'); + expect(() => sanitizeSlug(undefined)).toThrowError('The input slug must be a string.'); + expect(() => sanitizeSlug(() => {})).toThrowError('The input slug must be a string.'); }); it('throws an error for non-string replacements', () => { - expect(() => sanitizeSlug('test', Map({ sanitize_replacement: {} }))).toThrowError("`options.replacement` must be a string."); - expect(() => sanitizeSlug('test', Map({ sanitize_replacement: [] }))).toThrowError("`options.replacement` must be a string."); - expect(() => sanitizeSlug('test', Map({ sanitize_replacement: false }))).toThrowError("`options.replacement` must be a string."); - expect(() => sanitizeSlug('test', Map({ sanitize_replacement: null } ))).toThrowError("`options.replacement` must be a string."); - expect(() => sanitizeSlug('test', Map({ sanitize_replacement: 11232 }))).toThrowError("`options.replacement` must be a string."); - // do not test undefined for this variant since a default is set in the cosntructor. + expect(() => sanitizeSlug('test', Map({ sanitize_replacement: {} }))).toThrowError( + '`options.replacement` must be a string.', + ); + expect(() => sanitizeSlug('test', Map({ sanitize_replacement: [] }))).toThrowError( + '`options.replacement` must be a string.', + ); + expect(() => sanitizeSlug('test', Map({ sanitize_replacement: false }))).toThrowError( + '`options.replacement` must be a string.', + ); + expect(() => sanitizeSlug('test', Map({ sanitize_replacement: null }))).toThrowError( + '`options.replacement` must be a string.', + ); + expect(() => sanitizeSlug('test', Map({ sanitize_replacement: 11232 }))).toThrowError( + '`options.replacement` must be a string.', + ); + // do not test undefined for this variant since a default is set in the cosntructor. //expect(() => sanitizeSlug('test', { sanitize_replacement: undefined })).toThrowError("`options.replacement` must be a string."); - expect(() => sanitizeSlug('test', Map({ sanitize_replacement: ()=>{} }))).toThrowError("`options.replacement` must be a string."); + expect(() => sanitizeSlug('test', Map({ sanitize_replacement: () => {} }))).toThrowError( + '`options.replacement` must be a string.', + ); }); it('should keep valid URI chars (letters digits _ - . ~)', () => { - expect( - sanitizeSlug("This, that-one_or.the~other 123!") - ).toEqual('This-that-one_or.the~other-123'); + expect(sanitizeSlug('This, that-one_or.the~other 123!')).toEqual( + 'This-that-one_or.the~other-123', + ); }); it('should remove accents with `clean_accents` set', () => { - expect( - sanitizeSlug("ěščřžý", Map({ clean_accents: true })) - ).toEqual('escrzy'); + expect(sanitizeSlug('ěščřžý', Map({ clean_accents: true }))).toEqual('escrzy'); }); it('should remove non-latin chars in "ascii" mode', () => { - expect( - sanitizeSlug("ěščřžý日本語のタイトル", Map({ encoding: 'ascii' })) - ).toEqual(''); + expect(sanitizeSlug('ěščřžý日本語のタイトル', Map({ encoding: 'ascii' }))).toEqual(''); }); it('should clean accents and strip non-latin chars in "ascii" mode with `clean_accents` set', () => { expect( - sanitizeSlug("ěščřžý日本語のタイトル", Map({ encoding: 'ascii', clean_accents: true })) + sanitizeSlug('ěščřžý日本語のタイトル', Map({ encoding: 'ascii', clean_accents: true })), ).toEqual('escrzy'); }); it('removes double replacements', () => { - expect(sanitizeSlug('test--test')).toEqual('test-test'); - expect(sanitizeSlug('test test')).toEqual('test-test'); + expect(sanitizeSlug('test--test')).toEqual('test-test'); + expect(sanitizeSlug('test test')).toEqual('test-test'); }); it('removes trailing replacemenets', () => { @@ -118,5 +108,4 @@ describe('sanitizeSlug', ()=> { it('uses alternate replacements', () => { expect(sanitizeSlug('test test ', Map({ sanitize_replacement: '_' }))).toEqual('test_test'); }); - }); diff --git a/packages/netlify-cms-core/src/lib/consoleError.js b/packages/netlify-cms-core/src/lib/consoleError.js index 17561a6e..95415cc4 100644 --- a/packages/netlify-cms-core/src/lib/consoleError.js +++ b/packages/netlify-cms-core/src/lib/consoleError.js @@ -1,7 +1,7 @@ export default function consoleError(title, description) { console.error( - `%c ⛔ ${ title }\n` + `%c${ description }\n\n`, + `%c ⛔ ${title}\n` + `%c${description}\n\n`, 'color: black; font-weight: bold; font-size: 16px; line-height: 50px;', - 'color: black;' + 'color: black;', ); } diff --git a/packages/netlify-cms-core/src/lib/registry.js b/packages/netlify-cms-core/src/lib/registry.js index a7e096ba..e82de6b3 100644 --- a/packages/netlify-cms-core/src/lib/registry.js +++ b/packages/netlify-cms-core/src/lib/registry.js @@ -1,11 +1,11 @@ import { Map } from 'immutable'; -import EditorComponent from 'ValueObjects/EditorComponent' +import EditorComponent from 'ValueObjects/EditorComponent'; /** * Global Registry Object */ const registry = { - backends: { }, + backends: {}, templates: {}, previewStyles: [], widgets: {}, @@ -29,7 +29,6 @@ export default { getBackend, }; - /** * Preview Styles * @@ -43,7 +42,6 @@ export function getPreviewStyles() { return registry.previewStyles; } - /** * Preview Templates */ @@ -54,7 +52,6 @@ export function getPreviewTemplate(name) { return registry.templates[name]; } - /** * Editor Widgets */ @@ -71,7 +68,6 @@ export function resolveWidget(name) { return getWidget(name || 'string') || getWidget('unknown'); } - /** * Markdown Editor Custom Components */ @@ -83,7 +79,6 @@ export function getEditorComponents() { return registry.editorComponents; } - /** * Widget Serializers */ @@ -99,9 +94,11 @@ export function getWidgetValueSerializer(widgetName) { */ export function registerBackend(name, BackendClass) { if (!name || !BackendClass) { - console.error("Backend parameters invalid. example: CMS.registerBackend('myBackend', BackendClass)"); + console.error( + "Backend parameters invalid. example: CMS.registerBackend('myBackend', BackendClass)", + ); } else if (registry.backends[name]) { - console.error(`Backend [${ name }] already registered. Please choose a different name.`); + console.error(`Backend [${name}] already registered. Please choose a different name.`); } else { registry.backends[name] = { init: (...args) => new BackendClass(...args), @@ -112,4 +109,3 @@ export function registerBackend(name, BackendClass) { export function getBackend(name) { return registry.backends[name]; } - diff --git a/packages/netlify-cms-core/src/lib/serializeEntryValues.js b/packages/netlify-cms-core/src/lib/serializeEntryValues.js index 10f2d9b0..1b1b4070 100644 --- a/packages/netlify-cms-core/src/lib/serializeEntryValues.js +++ b/packages/netlify-cms-core/src/lib/serializeEntryValues.js @@ -21,7 +21,6 @@ import { getWidgetValueSerializer } from './registry'; * handlers run on persist. */ const runSerializer = (values, fields, method) => { - /** * Reduce the list of fields to a map where keys are field names and values * are field values, serializing the values of fields whose widgets have diff --git a/packages/netlify-cms-core/src/lib/textHelper.js b/packages/netlify-cms-core/src/lib/textHelper.js index 07d24ff7..eff09bc2 100644 --- a/packages/netlify-cms-core/src/lib/textHelper.js +++ b/packages/netlify-cms-core/src/lib/textHelper.js @@ -1,13 +1,11 @@ export function stringToRGB(str) { - if (!str) return "000000"; + if (!str) return '000000'; let hash = 0; for (let i = 0; i < str.length; i++) { hash = str.charCodeAt(i) + ((hash << 5) - hash); } - const c = (hash & 0x00FFFFFF) - .toString(16) - .toUpperCase(); + const c = (hash & 0x00ffffff).toString(16).toUpperCase(); - return "00000".substring(0, 6 - c.length) + c; + return '00000'.substring(0, 6 - c.length) + c; } diff --git a/packages/netlify-cms-core/src/lib/urlHelper.js b/packages/netlify-cms-core/src/lib/urlHelper.js index 4ef79b8d..d851e4ef 100644 --- a/packages/netlify-cms-core/src/lib/urlHelper.js +++ b/packages/netlify-cms-core/src/lib/urlHelper.js @@ -5,15 +5,15 @@ import { isString, escapeRegExp, flow, partialRight } from 'lodash'; import { Map } from 'immutable'; function getUrl(urlString, direct) { - return `${ direct ? '/#' : '' }${ urlString }`; + return `${direct ? '/#' : ''}${urlString}`; } export function getCollectionUrl(collectionName, direct) { - return getUrl(`/collections/${ collectionName }`, direct); + return getUrl(`/collections/${collectionName}`, direct); } export function getNewEntryUrl(collectionName, direct) { - return getUrl(`/collections/${ collectionName }/new`, direct); + return getUrl(`/collections/${collectionName}/new`, direct); } export function addParams(urlString, params) { @@ -39,18 +39,18 @@ const ucsChars = /[\xA0-\u{D7FF}\u{F900}-\u{FDCF}\u{FDF0}-\u{FFEF}\u{10000}-\u{1 const validURIChar = char => uriChars.test(char); const validIRIChar = char => uriChars.test(char) || ucsChars.test(char); // `sanitizeURI` does not actually URI-encode the chars (that is the browser's and server's job), just removes the ones that are not allowed. -export function sanitizeURI(str, { replacement = "", encoding = "unicode" } = {}) { +export function sanitizeURI(str, { replacement = '', encoding = 'unicode' } = {}) { if (!isString(str)) { - throw new Error("The input slug must be a string."); + throw new Error('The input slug must be a string.'); } if (!isString(replacement)) { - throw new Error("`options.replacement` must be a string."); + throw new Error('`options.replacement` must be a string.'); } - + let validChar; - if (encoding === "unicode") { + if (encoding === 'unicode') { validChar = validIRIChar; - } else if (encoding === "ascii") { + } else if (encoding === 'ascii') { validChar = validURIChar; } else { throw new Error('`options.encoding` must be "unicode" or "ascii".'); @@ -58,12 +58,14 @@ export function sanitizeURI(str, { replacement = "", encoding = "unicode" } = {} // Check and make sure the replacement character is actually a safe char itself. if (!Array.from(replacement).every(validChar)) { - throw new Error("The replacement character(s) (options.replacement) is itself unsafe."); + throw new Error('The replacement character(s) (options.replacement) is itself unsafe.'); } // `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(char => (validChar(char) ? char : replacement)).join(''); + return Array.from(str) + .map(char => (validChar(char) ? char : replacement)) + .join(''); } export function sanitizeSlug(str, options = Map()) { @@ -71,8 +73,10 @@ export function sanitizeSlug(str, options = Map()) { const stripDiacritics = options.get('clean_accents', false); const replacement = options.get('sanitize_replacement', '-'); - if (!isString(str)) { throw new Error("The input slug must be a string."); } - + if (!isString(str)) { + throw new Error('The input slug must be a string.'); + } + const sanitizedSlug = flow([ ...(stripDiacritics ? [diacritics.remove] : []), partialRight(sanitizeURI, { replacement, encoding }), @@ -80,8 +84,8 @@ export function sanitizeSlug(str, options = Map()) { ])(str); // Remove any doubled or trailing replacement characters (that were added in the sanitizers). - const doubleReplacement = new RegExp(`(?:${ escapeRegExp(replacement) })+`, 'g'); - const trailingReplacment = new RegExp(`${ escapeRegExp(replacement) }$`); + const doubleReplacement = new RegExp(`(?:${escapeRegExp(replacement)})+`, 'g'); + const trailingReplacment = new RegExp(`${escapeRegExp(replacement)}$`); const normalizedSlug = sanitizedSlug .replace(doubleReplacement, replacement) .replace(trailingReplacment, ''); diff --git a/packages/netlify-cms-core/src/reducers/__tests__/auth.spec.js b/packages/netlify-cms-core/src/reducers/__tests__/auth.spec.js index 738213e0..452494d9 100644 --- a/packages/netlify-cms-core/src/reducers/__tests__/auth.spec.js +++ b/packages/netlify-cms-core/src/reducers/__tests__/auth.spec.js @@ -4,36 +4,24 @@ import auth from '../auth'; describe('auth', () => { it('should handle an empty state', () => { - expect( - auth(undefined, {}) - ).toEqual( - null - ); + expect(auth(undefined, {})).toEqual(null); }); it('should handle an authentication request', () => { - expect( - auth(undefined, authenticating()) - ).toEqual( - Immutable.Map({ isFetching: true }) - ); + expect(auth(undefined, authenticating())).toEqual(Immutable.Map({ isFetching: true })); }); it('should handle authentication', () => { - expect( - auth(undefined, authenticate({ email: 'joe@example.com' })) - ).toEqual( - Immutable.fromJS({ user: { email: 'joe@example.com' } }) + expect(auth(undefined, authenticate({ email: 'joe@example.com' }))).toEqual( + Immutable.fromJS({ user: { email: 'joe@example.com' } }), ); }); it('should handle an authentication error', () => { - expect( - auth(undefined, authError(new Error('Bad credentials'))) - ).toEqual( + expect(auth(undefined, authError(new Error('Bad credentials')))).toEqual( Immutable.Map({ error: 'Error: Bad credentials', - }) + }), ); }); 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 5919aa15..151f5d72 100644 --- a/packages/netlify-cms-core/src/reducers/__tests__/collections.spec.js +++ b/packages/netlify-cms-core/src/reducers/__tests__/collections.spec.js @@ -4,24 +4,25 @@ import collections from '../collections'; describe('collections', () => { it('should handle an empty state', () => { - expect( - collections(undefined, {}) - ).toEqual( - null - ); + expect(collections(undefined, {})).toEqual(null); }); it('should load the collections from the config', () => { expect( - collections(undefined, configLoaded(fromJS({ - collections: [ - { - name: 'posts', - folder: '_posts', - fields: [{ name: 'title', widget: 'string' }], - }, - ], - }))) + collections( + undefined, + configLoaded( + fromJS({ + collections: [ + { + name: 'posts', + folder: '_posts', + fields: [{ name: 'title', widget: 'string' }], + }, + ], + }), + ), + ), ).toEqual( OrderedMap({ posts: fromJS({ @@ -30,7 +31,7 @@ describe('collections', () => { fields: [{ name: 'title', widget: 'string' }], type: 'folder_based_collection', }), - }) + }), ); }); }); diff --git a/packages/netlify-cms-core/src/reducers/__tests__/config.spec.js b/packages/netlify-cms-core/src/reducers/__tests__/config.spec.js index 7aa4d87b..25052a9e 100644 --- a/packages/netlify-cms-core/src/reducers/__tests__/config.spec.js +++ b/packages/netlify-cms-core/src/reducers/__tests__/config.spec.js @@ -4,34 +4,22 @@ import config from 'Reducers/config'; describe('config', () => { it('should handle an empty state', () => { - expect( - config(undefined, {}) - ).toEqual( - Map({ isFetching: true }) - ); + expect(config(undefined, {})).toEqual(Map({ isFetching: true })); }); it('should handle an update', () => { - expect( - config(Map({ a: 'b', c: 'd' }), configLoaded(Map({ a: 'changed', e: 'new' }))) - ).toEqual( - Map({ a: 'changed', e: 'new' }) + expect(config(Map({ a: 'b', c: 'd' }), configLoaded(Map({ a: 'changed', e: 'new' })))).toEqual( + Map({ a: 'changed', e: 'new' }), ); }); it('should mark the config as loading', () => { - expect( - config(undefined, configLoading()) - ).toEqual( - Map({ isFetching: true }) - ); + expect(config(undefined, configLoading())).toEqual(Map({ isFetching: true })); }); it('should handle an error', () => { - expect( - config(Map(), configFailed(new Error('Config could not be loaded'))) - ).toEqual( - Map({ error: 'Error: Config could not be loaded' }) + expect(config(Map(), configFailed(new Error('Config could not be loaded')))).toEqual( + Map({ error: 'Error: Config could not be loaded' }), ); }); }); diff --git a/packages/netlify-cms-core/src/reducers/__tests__/entries.spec.js b/packages/netlify-cms-core/src/reducers/__tests__/entries.spec.js index 628d2d46..b2211c37 100644 --- a/packages/netlify-cms-core/src/reducers/__tests__/entries.spec.js +++ b/packages/netlify-cms-core/src/reducers/__tests__/entries.spec.js @@ -8,25 +8,25 @@ const initialState = OrderedMap({ describe('entries', () => { it('should mark entries as fetching', () => { - expect( - reducer(initialState, actions.entriesLoading(Map({ name: 'posts' }))) - ).toEqual( - OrderedMap(fromJS({ - posts: { name: 'posts' }, - pages: { - posts: { isFetching: true }, - }, - })) + expect(reducer(initialState, actions.entriesLoading(Map({ name: 'posts' })))).toEqual( + OrderedMap( + fromJS({ + posts: { name: 'posts' }, + pages: { + posts: { isFetching: true }, + }, + }), + ), ); }); it('should handle loaded entries', () => { const entries = [{ slug: 'a', path: '' }, { slug: 'b', title: 'B' }]; expect( - reducer(initialState, actions.entriesLoaded(Map({ name: 'posts' }), entries, 0)) + reducer(initialState, actions.entriesLoaded(Map({ name: 'posts' }), entries, 0)), ).toEqual( - OrderedMap(fromJS( - { + OrderedMap( + fromJS({ posts: { name: 'posts' }, entities: { 'posts.a': { slug: 'a', path: '', isFetching: false }, @@ -38,8 +38,8 @@ describe('entries', () => { ids: ['a', 'b'], }, }, - } - )) + }), + ), ); }); }); diff --git a/packages/netlify-cms-core/src/reducers/__tests__/entryDraft.spec.js b/packages/netlify-cms-core/src/reducers/__tests__/entryDraft.spec.js index c9ffc510..c1fb9f9d 100644 --- a/packages/netlify-cms-core/src/reducers/__tests__/entryDraft.spec.js +++ b/packages/netlify-cms-core/src/reducers/__tests__/entryDraft.spec.js @@ -23,12 +23,7 @@ const entry = { describe('entryDraft reducer', () => { describe('DRAFT_CREATE_FROM_ENTRY', () => { it('should create draft from the entry', () => { - expect( - reducer( - initialState, - actions.createDraftFromEntry(fromJS(entry)) - ) - ).toEqual( + expect(reducer(initialState, actions.createDraftFromEntry(fromJS(entry)))).toEqual( fromJS({ entry: { ...entry, @@ -38,19 +33,14 @@ describe('entryDraft reducer', () => { fieldsMetaData: Map(), fieldsErrors: Map(), hasChanged: false, - }) + }), ); }); }); describe('DRAFT_CREATE_EMPTY', () => { it('should create a new draft ', () => { - expect( - reducer( - initialState, - actions.emptyDraftCreated(fromJS(entry)) - ) - ).toEqual( + expect(reducer(initialState, actions.emptyDraftCreated(fromJS(entry)))).toEqual( fromJS({ entry: { ...entry, @@ -60,15 +50,14 @@ describe('entryDraft reducer', () => { fieldsMetaData: Map(), fieldsErrors: Map(), hasChanged: false, - }) + }), ); }); }); describe('DRAFT_DISCARD', () => { it('should discard the draft and return initial state', () => { - expect(reducer(initialState, actions.discardDraft())) - .toEqual(initialState); + expect(reducer(initialState, actions.discardDraft())).toEqual(initialState); }); }); @@ -78,15 +67,16 @@ describe('entryDraft reducer', () => { ...entry, raw: 'updated', }; - expect(reducer(initialState, actions.changeDraft(newEntry))) - .toEqual(fromJS({ + expect(reducer(initialState, actions.changeDraft(newEntry))).toEqual( + fromJS({ entry: { ...entry, raw: 'updated', }, mediaFiles: [], hasChanged: true, - })); + }), + ); }); }); @@ -111,27 +101,31 @@ describe('entryDraft reducer', () => { it('should handle persisting request', () => { const newState = reducer( initialState, - actions.entryPersisting(Map({ name: 'posts' }), Map({ slug: 'slug' })) + actions.entryPersisting(Map({ name: 'posts' }), Map({ slug: 'slug' })), ); expect(newState.getIn(['entry', 'isPersisting'])).toBe(true); }); it('should handle persisting success', () => { - let newState = reducer(initialState, - actions.entryPersisting(Map({ name: 'posts' }), Map({ slug: 'slug' })) + let newState = reducer( + initialState, + actions.entryPersisting(Map({ name: 'posts' }), Map({ slug: 'slug' })), ); - newState = reducer(newState, - actions.entryPersisted(Map({ name: 'posts' }), Map({ slug: 'slug' })) + newState = reducer( + newState, + actions.entryPersisted(Map({ name: 'posts' }), Map({ slug: 'slug' })), ); expect(newState.getIn(['entry', 'isPersisting'])).toBeUndefined(); }); it('should handle persisting error', () => { - let newState = reducer(initialState, - actions.entryPersisting(Map({ name: 'posts' }), Map({ slug: 'slug' })) + let newState = reducer( + initialState, + actions.entryPersisting(Map({ name: 'posts' }), Map({ slug: 'slug' })), ); - newState = reducer(newState, - actions.entryPersistFail(Map({ name: 'posts' }), Map({ slug: 'slug' }), 'Error message') + newState = reducer( + newState, + actions.entryPersistFail(Map({ name: 'posts' }), Map({ slug: 'slug' }), 'Error message'), ); expect(newState.getIn(['entry', 'isPersisting'])).toBeUndefined(); }); diff --git a/packages/netlify-cms-core/src/reducers/collections.js b/packages/netlify-cms-core/src/reducers/collections.js index d6bb31ff..c873729f 100644 --- a/packages/netlify-cms-core/src/reducers/collections.js +++ b/packages/netlify-cms-core/src/reducers/collections.js @@ -30,16 +30,24 @@ const collections = (state = null, action) => { const selectors = { [FOLDER]: { entryExtension(collection) { - return (collection.get('extension') || get(formatExtensions, (collection.get('format') || 'frontmatter'))).replace(/^\./, ''); + return ( + collection.get('extension') || + get(formatExtensions, collection.get('format') || 'frontmatter') + ).replace(/^\./, ''); }, fields(collection) { return collection.get('fields'); }, entryPath(collection, slug) { - return `${ collection.get('folder').replace(/\/$/, '') }/${ slug }.${ this.entryExtension(collection) }`; + return `${collection.get('folder').replace(/\/$/, '')}/${slug}.${this.entryExtension( + collection, + )}`; }, entrySlug(collection, path) { - return path.split('/').pop().replace(new RegExp(`\\.${ escapeRegExp(this.entryExtension(collection)) }$`), ''); + return path + .split('/') + .pop() + .replace(new RegExp(`\\.${escapeRegExp(this.entryExtension(collection))}$`), ''); }, listMethod() { return 'entriesByFolder'; @@ -68,7 +76,10 @@ const selectors = { return file && file.get('file'); }, entrySlug(collection, path) { - const file = collection.get('files').filter(f => f.get('file') === path).get(0); + const file = collection + .get('files') + .filter(f => f.get('file') === path) + .get(0); return file && file.get('name'); }, listMethod() { @@ -86,14 +97,21 @@ const selectors = { }, }; -export const selectFields = (collection, slug) => selectors[collection.get('type')].fields(collection, slug); -export const selectFolderEntryExtension = (collection) => selectors[FOLDER].entryExtension(collection); -export const selectEntryPath = (collection, slug) => selectors[collection.get('type')].entryPath(collection, slug); -export const selectEntrySlug = (collection, path) => selectors[collection.get('type')].entrySlug(collection, path); +export const selectFields = (collection, slug) => + selectors[collection.get('type')].fields(collection, slug); +export const selectFolderEntryExtension = collection => + selectors[FOLDER].entryExtension(collection); +export const selectEntryPath = (collection, slug) => + selectors[collection.get('type')].entryPath(collection, slug); +export const selectEntrySlug = (collection, path) => + selectors[collection.get('type')].entrySlug(collection, path); export const selectListMethod = collection => selectors[collection.get('type')].listMethod(); -export const selectAllowNewEntries = collection => selectors[collection.get('type')].allowNewEntries(collection); -export const selectAllowDeletion = collection => selectors[collection.get('type')].allowDeletion(collection); -export const selectTemplateName = (collection, slug) => selectors[collection.get('type')].templateName(collection, slug); +export const selectAllowNewEntries = collection => + selectors[collection.get('type')].allowNewEntries(collection); +export const selectAllowDeletion = collection => + selectors[collection.get('type')].allowDeletion(collection); +export const selectTemplateName = (collection, slug) => + selectors[collection.get('type')].templateName(collection, slug); export const selectIdentifier = collection => { const fieldNames = collection.get('fields').map(field => field.get('name')); return IDENTIFIER_FIELDS.find(id => fieldNames.find(name => name.toLowerCase().trim() === id)); @@ -107,12 +125,16 @@ export const selectInferedField = (collection, fieldName) => { // If colllection has no fields or fieldName is not defined within inferables list, return null if (!fields || !inferableField) return null; // Try to return a field of the specified type with one of the synonyms - const mainTypeFields = fields.filter(f => f.get('widget', 'string') === inferableField.type).map(f => f.get('name')); + const mainTypeFields = fields + .filter(f => f.get('widget', 'string') === inferableField.type) + .map(f => f.get('name')); field = mainTypeFields.filter(f => inferableField.synonyms.indexOf(f) !== -1); if (field && field.size > 0) return field.first(); // Try to return a field for each of the specified secondary types - const secondaryTypeFields = fields.filter(f => inferableField.secondaryTypes.indexOf(f.get('widget', 'string')) !== -1).map(f => f.get('name')); + const secondaryTypeFields = fields + .filter(f => inferableField.secondaryTypes.indexOf(f.get('widget', 'string')) !== -1) + .map(f => f.get('name')); field = secondaryTypeFields.filter(f => inferableField.synonyms.indexOf(f) !== -1); if (field && field.size > 0) return field.first(); @@ -122,8 +144,10 @@ export const selectInferedField = (collection, fieldName) => { // Coundn't infer the field. Show error and return null. if (inferableField.showError) { consoleError( - `The Field ${ fieldName } is missing for the collection “${ collection.get('name') }”`, - `Netlify CMS tries to infer the entry ${ fieldName } automatically, but one couldn't be found for entries of the collection “${ collection.get('name') }”. Please check your site configuration.` + `The Field ${fieldName} is missing for the collection “${collection.get('name')}”`, + `Netlify CMS tries to infer the entry ${fieldName} automatically, but one couldn't be found for entries of the collection “${collection.get( + 'name', + )}”. Please check your site configuration.`, ); } diff --git a/packages/netlify-cms-core/src/reducers/combinedReducer.js b/packages/netlify-cms-core/src/reducers/combinedReducer.js index 3c247402..23be34b6 100644 --- a/packages/netlify-cms-core/src/reducers/combinedReducer.js +++ b/packages/netlify-cms-core/src/reducers/combinedReducer.js @@ -4,8 +4,10 @@ import { reducer as notifReducer } from 'redux-notifications'; import optimist from 'redux-optimist'; import reducers from '.'; -export default optimist(combineReducers({ - ...reducers, - notifs: notifReducer, - routing: routerReducer, -})); +export default optimist( + combineReducers({ + ...reducers, + notifs: notifReducer, + routing: routerReducer, + }), +); diff --git a/packages/netlify-cms-core/src/reducers/cursors.js b/packages/netlify-cms-core/src/reducers/cursors.js index 3a04ea3b..f8d44d96 100644 --- a/packages/netlify-cms-core/src/reducers/cursors.js +++ b/packages/netlify-cms-core/src/reducers/cursors.js @@ -1,21 +1,19 @@ import { fromJS } from 'immutable'; import { Cursor } from 'netlify-cms-lib-util'; -import { - ENTRIES_SUCCESS, -} from 'Actions/entries'; +import { ENTRIES_SUCCESS } from 'Actions/entries'; // Since pagination can be used for a variety of views (collections // and searches are the most common examples), we namespace cursors by // their type before storing them in the state. export const selectCollectionEntriesCursor = (state, collectionName) => - new Cursor(state.getIn(["cursorsByType", "collectionEntries", collectionName])); + new Cursor(state.getIn(['cursorsByType', 'collectionEntries', collectionName])); const cursors = (state = fromJS({ cursorsByType: { collectionEntries: {} } }), action) => { switch (action.type) { case ENTRIES_SUCCESS: { return state.setIn( - ["cursorsByType", "collectionEntries", action.payload.collection], - Cursor.create(action.payload.cursor).store + ['cursorsByType', 'collectionEntries', action.payload.collection], + Cursor.create(action.payload.cursor).store, ); } diff --git a/packages/netlify-cms-core/src/reducers/editorialWorkflow.js b/packages/netlify-cms-core/src/reducers/editorialWorkflow.js index 3d921306..3788d593 100644 --- a/packages/netlify-cms-core/src/reducers/editorialWorkflow.js +++ b/packages/netlify-cms-core/src/reducers/editorialWorkflow.js @@ -29,77 +29,115 @@ const unpublishedEntries = (state = Map(), action) => { return state; } case UNPUBLISHED_ENTRY_REQUEST: - return state.setIn(['entities', `${ action.payload.collection }.${ action.payload.slug }`, 'isFetching'], true); + return state.setIn( + ['entities', `${action.payload.collection}.${action.payload.slug}`, 'isFetching'], + true, + ); case UNPUBLISHED_ENTRY_REDIRECT: - return state.deleteIn(['entities', `${ action.payload.collection }.${ action.payload.slug }`]); + return state.deleteIn(['entities', `${action.payload.collection}.${action.payload.slug}`]); case UNPUBLISHED_ENTRY_SUCCESS: return state.setIn( - ['entities', `${ action.payload.collection }.${ action.payload.entry.slug }`], - fromJS(action.payload.entry) + ['entities', `${action.payload.collection}.${action.payload.entry.slug}`], + fromJS(action.payload.entry), ); case UNPUBLISHED_ENTRIES_REQUEST: return state.setIn(['pages', 'isFetching'], true); case UNPUBLISHED_ENTRIES_SUCCESS: - return state.withMutations((map) => { - action.payload.entries.forEach(entry => ( - map.setIn(['entities', `${ entry.collection }.${ entry.slug }`], fromJS(entry).set('isFetching', false)) - )); - map.set('pages', Map({ - ...action.payload.pages, - ids: List(action.payload.entries.map(entry => entry.slug)), - })); + return state.withMutations(map => { + action.payload.entries.forEach(entry => + map.setIn( + ['entities', `${entry.collection}.${entry.slug}`], + fromJS(entry).set('isFetching', false), + ), + ); + map.set( + 'pages', + Map({ + ...action.payload.pages, + ids: List(action.payload.entries.map(entry => entry.slug)), + }), + ); }); case UNPUBLISHED_ENTRY_PERSIST_REQUEST: // Update Optimistically - return state.withMutations((map) => { - map.setIn(['entities', `${ action.payload.collection }.${ action.payload.entry.get('slug') }`], fromJS(action.payload.entry)); - map.setIn(['entities', `${ action.payload.collection }.${ action.payload.entry.get('slug') }`, 'isPersisting'], true); + return state.withMutations(map => { + map.setIn( + ['entities', `${action.payload.collection}.${action.payload.entry.get('slug')}`], + fromJS(action.payload.entry), + ); + map.setIn( + [ + 'entities', + `${action.payload.collection}.${action.payload.entry.get('slug')}`, + 'isPersisting', + ], + true, + ); map.updateIn(['pages', 'ids'], List(), list => list.push(action.payload.entry.get('slug'))); }); case UNPUBLISHED_ENTRY_PERSIST_SUCCESS: // Update Optimistically - return state.deleteIn(['entities', `${ action.payload.collection }.${ action.payload.entry.get('slug') }`, 'isPersisting']); + return state.deleteIn([ + 'entities', + `${action.payload.collection}.${action.payload.entry.get('slug')}`, + 'isPersisting', + ]); case UNPUBLISHED_ENTRY_STATUS_CHANGE_REQUEST: // Update Optimistically - return state.withMutations((map) => { - map.setIn(['entities', `${ action.payload.collection }.${ action.payload.slug }`, 'metaData', 'status'], action.payload.newStatus); - map.setIn(['entities', `${ action.payload.collection }.${ action.payload.slug }`, 'isUpdatingStatus'], true); + return state.withMutations(map => { + map.setIn( + ['entities', `${action.payload.collection}.${action.payload.slug}`, 'metaData', 'status'], + action.payload.newStatus, + ); + map.setIn( + ['entities', `${action.payload.collection}.${action.payload.slug}`, 'isUpdatingStatus'], + true, + ); }); case UNPUBLISHED_ENTRY_STATUS_CHANGE_SUCCESS: case UNPUBLISHED_ENTRY_STATUS_CHANGE_FAILURE: - return state.setIn(['entities', `${ action.payload.collection }.${ action.payload.slug }`, 'isUpdatingStatus'], false); + return state.setIn( + ['entities', `${action.payload.collection}.${action.payload.slug}`, 'isUpdatingStatus'], + false, + ); case UNPUBLISHED_ENTRY_PUBLISH_REQUEST: - return state.setIn(['entities', `${ action.payload.collection }.${ action.payload.slug }`, 'isPublishing'], true); + return state.setIn( + ['entities', `${action.payload.collection}.${action.payload.slug}`, 'isPublishing'], + true, + ); case UNPUBLISHED_ENTRY_PUBLISH_SUCCESS: case UNPUBLISHED_ENTRY_PUBLISH_FAILURE: return state.withMutations(map => { - map.deleteIn(['entities', `${ action.payload.collection }.${ action.payload.slug }`]); + map.deleteIn(['entities', `${action.payload.collection}.${action.payload.slug}`]); }); case UNPUBLISHED_ENTRY_DELETE_SUCCESS: - return state.deleteIn(['entities', `${ action.payload.collection }.${ action.payload.slug }`]); + return state.deleteIn(['entities', `${action.payload.collection}.${action.payload.slug}`]); default: return state; } }; -export const selectUnpublishedEntry = (state, collection, slug) => state && state.getIn(['entities', `${ collection }.${ slug }`]); +export const selectUnpublishedEntry = (state, collection, slug) => + state && state.getIn(['entities', `${collection}.${slug}`]); export const selectUnpublishedEntriesByStatus = (state, status) => { if (!state) return null; - return state.get('entities').filter(entry => entry.getIn(['metaData', 'status']) === status).valueSeq(); + return state + .get('entities') + .filter(entry => entry.getIn(['metaData', 'status']) === status) + .valueSeq(); }; - export default unpublishedEntries; diff --git a/packages/netlify-cms-core/src/reducers/entries.js b/packages/netlify-cms-core/src/reducers/entries.js index 8670ac1b..ed4795ed 100644 --- a/packages/netlify-cms-core/src/reducers/entries.js +++ b/packages/netlify-cms-core/src/reducers/entries.js @@ -19,12 +19,15 @@ let page; const entries = (state = Map({ entities: Map(), pages: Map() }), action) => { switch (action.type) { case ENTRY_REQUEST: - return state.setIn(['entities', `${ action.payload.collection }.${ action.payload.slug }`, 'isFetching'], true); + return state.setIn( + ['entities', `${action.payload.collection}.${action.payload.slug}`, 'isFetching'], + true, + ); case ENTRY_SUCCESS: return state.setIn( - ['entities', `${ action.payload.collection }.${ action.payload.entry.slug }`], - fromJS(action.payload.entry) + ['entities', `${action.payload.collection}.${action.payload.entry.slug}`], + fromJS(action.payload.entry), ); case ENTRIES_REQUEST: @@ -35,42 +38,56 @@ const entries = (state = Map({ entities: Map(), pages: Map() }), action) => { loadedEntries = action.payload.entries; append = action.payload.append; page = action.payload.page; - return state.withMutations((map) => { - loadedEntries.forEach(entry => ( - map.setIn(['entities', `${ collection }.${ entry.slug }`], fromJS(entry).set('isFetching', false)) - )); + return state.withMutations(map => { + loadedEntries.forEach(entry => + map.setIn( + ['entities', `${collection}.${entry.slug}`], + fromJS(entry).set('isFetching', false), + ), + ); const ids = List(loadedEntries.map(entry => entry.slug)); - map.setIn(['pages', collection], Map({ - page, - ids: append - ? map.getIn(['pages', collection, 'ids'], List()).concat(ids) - : ids, - })); + map.setIn( + ['pages', collection], + Map({ + page, + ids: append ? map.getIn(['pages', collection, 'ids'], List()).concat(ids) : ids, + }), + ); }); case ENTRIES_FAILURE: return state.setIn(['pages', action.meta.collection, 'isFetching'], false); case ENTRY_FAILURE: - return state.withMutations((map) => { - map.setIn(['entities', `${ action.payload.collection }.${ action.payload.slug }`, 'isFetching'], false); - map.setIn(['entities', `${ action.payload.collection }.${ action.payload.slug }`, 'error'], action.payload.error.message); + return state.withMutations(map => { + map.setIn( + ['entities', `${action.payload.collection}.${action.payload.slug}`, 'isFetching'], + false, + ); + map.setIn( + ['entities', `${action.payload.collection}.${action.payload.slug}`, 'error'], + action.payload.error.message, + ); }); case SEARCH_ENTRIES_SUCCESS: loadedEntries = action.payload.entries; - return state.withMutations((map) => { - loadedEntries.forEach(entry => ( - map.setIn(['entities', `${ entry.collection }.${ entry.slug }`], fromJS(entry).set('isFetching', false)) - )); + return state.withMutations(map => { + loadedEntries.forEach(entry => + map.setIn( + ['entities', `${entry.collection}.${entry.slug}`], + fromJS(entry).set('isFetching', false), + ), + ); }); case ENTRY_DELETE_SUCCESS: - return state.withMutations((map) => { - map.deleteIn(['entities', `${ action.payload.collectionName }.${ action.payload.entrySlug }`]); - map.updateIn(['pages', action.payload.collectionName, 'ids'], - ids => ids.filter(id => id !== action.payload.entrySlug)); + return state.withMutations(map => { + map.deleteIn(['entities', `${action.payload.collectionName}.${action.payload.entrySlug}`]); + map.updateIn(['pages', action.payload.collectionName, 'ids'], ids => + ids.filter(id => id !== action.payload.entrySlug), + ); }); default: @@ -78,9 +95,8 @@ const entries = (state = Map({ entities: Map(), pages: Map() }), action) => { } }; -export const selectEntry = (state, collection, slug) => ( - state.getIn(['entities', `${ collection }.${ slug }`]) -); +export const selectEntry = (state, collection, slug) => + state.getIn(['entities', `${collection}.${slug}`]); export const selectEntries = (state, collection) => { const slugs = state.getIn(['pages', collection, 'ids']); diff --git a/packages/netlify-cms-core/src/reducers/entryDraft.js b/packages/netlify-cms-core/src/reducers/entryDraft.js index 936462a3..bbb83e0f 100644 --- a/packages/netlify-cms-core/src/reducers/entryDraft.js +++ b/packages/netlify-cms-core/src/reducers/entryDraft.js @@ -15,10 +15,7 @@ import { UNPUBLISHED_ENTRY_PERSIST_SUCCESS, UNPUBLISHED_ENTRY_PERSIST_FAILURE, } from 'Actions/editorialWorkflow'; -import { - ADD_ASSET, - REMOVE_ASSET, -} from 'Actions/media'; +import { ADD_ASSET, REMOVE_ASSET } from 'Actions/media'; const initialState = Map({ entry: Map(), @@ -32,7 +29,7 @@ const entryDraftReducer = (state = Map(), action) => { switch (action.type) { case DRAFT_CREATE_FROM_ENTRY: // Existing Entry - return state.withMutations((state) => { + return state.withMutations(state => { state.set('entry', action.payload.entry); state.setIn(['entry', 'newRecord'], false); state.set('mediaFiles', List()); @@ -45,7 +42,7 @@ const entryDraftReducer = (state = Map(), action) => { }); case DRAFT_CREATE_EMPTY: // New Entry - return state.withMutations((state) => { + return state.withMutations(state => { state.set('entry', fromJS(action.payload)); state.setIn(['entry', 'newRecord'], true); state.set('mediaFiles', List()); @@ -56,7 +53,7 @@ const entryDraftReducer = (state = Map(), action) => { case DRAFT_DISCARD: return initialState; case DRAFT_CHANGE_FIELD: - return state.withMutations((state) => { + return state.withMutations(state => { state.setIn(['entry', 'data', action.payload.field], action.payload.value); state.mergeDeepIn(['fieldsMetaData'], fromJS(action.payload.metadata)); state.set('hasChanged', true); @@ -81,7 +78,7 @@ const entryDraftReducer = (state = Map(), action) => { case ENTRY_PERSIST_SUCCESS: case UNPUBLISHED_ENTRY_PERSIST_SUCCESS: - return state.withMutations((state) => { + return state.withMutations(state => { state.deleteIn(['entry', 'isPersisting']); state.set('hasChanged', false); if (!state.getIn(['entry', 'slug'])) { @@ -90,7 +87,7 @@ const entryDraftReducer = (state = Map(), action) => { }); case ENTRY_DELETE_SUCCESS: - return state.withMutations((state) => { + return state.withMutations(state => { state.deleteIn(['entry', 'isPersisting']); state.set('hasChanged', false); }); diff --git a/packages/netlify-cms-core/src/reducers/globalUI.js b/packages/netlify-cms-core/src/reducers/globalUI.js index 68a4a371..4362bd0e 100644 --- a/packages/netlify-cms-core/src/reducers/globalUI.js +++ b/packages/netlify-cms-core/src/reducers/globalUI.js @@ -4,12 +4,9 @@ import { Map } from 'immutable'; * */ const globalUI = (state = Map({ isFetching: false }), action) => { // Generic, global loading indicator - if ((action.type.indexOf('REQUEST') > -1)) { + if (action.type.indexOf('REQUEST') > -1) { return state.set('isFetching', true); - } else if ( - (action.type.indexOf('SUCCESS') > -1) || - (action.type.indexOf('FAILURE') > -1) - ) { + } else if (action.type.indexOf('SUCCESS') > -1 || action.type.indexOf('FAILURE') > -1) { return state.set('isFetching', false); } return state; diff --git a/packages/netlify-cms-core/src/reducers/index.js b/packages/netlify-cms-core/src/reducers/index.js index 3c2c4ba7..c7bd2725 100644 --- a/packages/netlify-cms-core/src/reducers/index.js +++ b/packages/netlify-cms-core/src/reducers/index.js @@ -37,9 +37,14 @@ export const selectEntry = (state, collection, slug) => export const selectEntries = (state, collection) => fromEntries.selectEntries(state.entries, collection); -export const selectSearchedEntries = (state) => { +export const selectSearchedEntries = state => { const searchItems = state.search.get('entryIds'); - return searchItems && searchItems.map(({ collection, slug }) => fromEntries.selectEntry(state.entries, collection, slug)); + return ( + searchItems && + searchItems.map(({ collection, slug }) => + fromEntries.selectEntry(state.entries, collection, slug), + ) + ); }; export const selectUnpublishedEntry = (state, collection, slug) => diff --git a/packages/netlify-cms-core/src/reducers/integrations.js b/packages/netlify-cms-core/src/reducers/integrations.js index 776304df..93a5d6f8 100644 --- a/packages/netlify-cms-core/src/reducers/integrations.js +++ b/packages/netlify-cms-core/src/reducers/integrations.js @@ -5,23 +5,31 @@ const integrations = (state = null, action) => { switch (action.type) { case CONFIG_SUCCESS: { const integrations = action.payload.get('integrations', List()).toJS() || []; - const newState = integrations.reduce((acc, integration) => { - const { hooks, collections, provider, ...providerData } = integration; - acc.providers[provider] = { ...providerData }; - if (!collections) { - hooks.forEach((hook) => { - acc.hooks[hook] = provider; + const newState = integrations.reduce( + (acc, integration) => { + const { hooks, collections, provider, ...providerData } = integration; + acc.providers[provider] = { ...providerData }; + if (!collections) { + hooks.forEach(hook => { + acc.hooks[hook] = provider; + }); + return acc; + } + const integrationCollections = + collections === '*' + ? action.payload.collections.map(collection => collection.name) + : collections; + integrationCollections.forEach(collection => { + hooks.forEach(hook => { + acc.hooks[collection] + ? (acc.hooks[collection][hook] = provider) + : (acc.hooks[collection] = { [hook]: provider }); + }); }); return acc; - } - const integrationCollections = collections === "*" ? action.payload.collections.map(collection => collection.name) : collections; - integrationCollections.forEach((collection) => { - hooks.forEach((hook) => { - acc.hooks[collection] ? acc.hooks[collection][hook] = provider : acc.hooks[collection] = { [hook]: provider }; - }); - }); - return acc; - }, { providers:{}, hooks: {} }); + }, + { providers: {}, hooks: {} }, + ); return fromJS(newState); } default: @@ -29,9 +37,9 @@ const integrations = (state = null, action) => { } }; -export const selectIntegration = (state, collection, hook) => ( - collection? state.getIn(['hooks', collection, hook], false) : state.getIn(['hooks', hook], false) -); - +export const selectIntegration = (state, collection, hook) => + collection + ? state.getIn(['hooks', collection, hook], false) + : state.getIn(['hooks', hook], false); export default integrations; diff --git a/packages/netlify-cms-core/src/reducers/mediaLibrary.js b/packages/netlify-cms-core/src/reducers/mediaLibrary.js index 97fa0df8..328b7fe8 100644 --- a/packages/netlify-cms-core/src/reducers/mediaLibrary.js +++ b/packages/netlify-cms-core/src/reducers/mediaLibrary.js @@ -18,7 +18,8 @@ import { } from 'Actions/mediaLibrary'; const mediaLibrary = (state = Map({ isVisible: false, controlMedia: Map() }), action) => { - const privateUploadChanged = state.get('privateUpload') !== get(action, ['payload', 'privateUpload']); + const privateUploadChanged = + state.get('privateUpload') !== get(action, ['payload', 'privateUpload']); switch (action.type) { case MEDIA_LIBRARY_OPEN: { const { controlID, forImage, privateUpload } = action.payload || {}; diff --git a/packages/netlify-cms-core/src/reducers/search.js b/packages/netlify-cms-core/src/reducers/search.js index d7e8b4b6..37a9893e 100644 --- a/packages/netlify-cms-core/src/reducers/search.js +++ b/packages/netlify-cms-core/src/reducers/search.js @@ -13,7 +13,13 @@ let response; let page; let searchTerm; -const defaultState = Map({ isFetching: false, term: null, page: 0, entryIds: List([]), queryHits: Map({}) }); +const defaultState = Map({ + isFetching: false, + term: null, + page: 0, + entryIds: List([]), + queryHits: Map({}), +}); const entries = (state = defaultState, action) => { switch (action.type) { @@ -22,7 +28,7 @@ const entries = (state = defaultState, action) => { case SEARCH_ENTRIES_REQUEST: if (action.payload.searchTerm !== state.get('term')) { - return state.withMutations((map) => { + return state.withMutations(map => { map.set('isFetching', true); map.set('term', action.payload.searchTerm); }); @@ -33,20 +39,27 @@ const entries = (state = defaultState, action) => { loadedEntries = action.payload.entries; page = action.payload.page; searchTerm = action.payload.searchTerm; - return state.withMutations((map) => { - const entryIds = List(loadedEntries.map(entry => ({ collection: entry.collection, slug: entry.slug }))); + return state.withMutations(map => { + const entryIds = List( + loadedEntries.map(entry => ({ collection: entry.collection, slug: entry.slug })), + ); map.set('isFetching', false); map.set('fetchID', null); map.set('page', page); map.set('term', searchTerm); - map.set('entryIds', (!page || isNaN(page) || page === 0) ? entryIds : map.get('entryIds', List()).concat(entryIds)); + map.set( + 'entryIds', + !page || isNaN(page) || page === 0 + ? entryIds + : map.get('entryIds', List()).concat(entryIds), + ); }); case QUERY_REQUEST: if (action.payload.searchTerm !== state.get('term')) { - return state.withMutations((map) => { + return state.withMutations(map => { map.set('isFetching', action.payload.namespace ? true : false); - map.set('fetchID', action.payload.namespace) + map.set('fetchID', action.payload.namespace); map.set('term', action.payload.searchTerm); }); } @@ -55,7 +68,7 @@ const entries = (state = defaultState, action) => { case QUERY_SUCCESS: searchTerm = action.payload.searchTerm; response = action.payload.response; - return state.withMutations((map) => { + return state.withMutations(map => { map.set('isFetching', false); map.set('fetchID', null); map.set('term', searchTerm); diff --git a/packages/netlify-cms-core/src/redux/configureStore.js b/packages/netlify-cms-core/src/redux/configureStore.js index 769a03af..27453407 100644 --- a/packages/netlify-cms-core/src/redux/configureStore.js +++ b/packages/netlify-cms-core/src/redux/configureStore.js @@ -4,10 +4,14 @@ import waitUntilAction from './middleware/waitUntilAction'; import reducer from 'Reducers/combinedReducer'; export default function configureStore(initialState) { - const store = createStore(reducer, initialState, compose( - applyMiddleware(thunkMiddleware, waitUntilAction), - window.devToolsExtension ? window.devToolsExtension() : f => f - )); + const store = createStore( + reducer, + initialState, + compose( + applyMiddleware(thunkMiddleware, waitUntilAction), + window.devToolsExtension ? window.devToolsExtension() : f => f, + ), + ); return store; } diff --git a/packages/netlify-cms-core/src/redux/middleware/waitUntilAction.js b/packages/netlify-cms-core/src/redux/middleware/waitUntilAction.js index 33ae41c8..19112084 100644 --- a/packages/netlify-cms-core/src/redux/middleware/waitUntilAction.js +++ b/packages/netlify-cms-core/src/redux/middleware/waitUntilAction.js @@ -1,8 +1,8 @@ -// Based on wait-service by Mozilla: +// Based on wait-service by Mozilla: // https://github.com/mozilla/gecko-dev/blob/master/devtools/client/shared/redux/middleware/wait-service.js - + /** - * A middleware that provides the ability for actions to install a + * A middleware that provides the ability for actions to install a * function to be run once when a specific condition is met by an * action coming through the system. Think of it as a thunk that * blocks until the condition is met. @@ -35,7 +35,7 @@ export default function waitUntilAction({ dispatch, getState }) { } } - return next => (action) => { + return next => action => { if (action.type === WAIT_UNTIL_ACTION) { pending.push(action); return null; diff --git a/packages/netlify-cms-core/src/valueObjects/AssetProxy.js b/packages/netlify-cms-core/src/valueObjects/AssetProxy.js index 40a8aa37..49cdeefa 100644 --- a/packages/netlify-cms-core/src/valueObjects/AssetProxy.js +++ b/packages/netlify-cms-core/src/valueObjects/AssetProxy.js @@ -4,7 +4,7 @@ import { getIntegrationProvider } from 'Integrations'; import { selectIntegration } from 'Reducers'; let store; -export const setStore = (storeObj) => { +export const setStore = storeObj => { store = storeObj; }; @@ -14,12 +14,15 @@ export default function AssetProxy(value, fileObj, uploaded = false, asset) { this.fileObj = fileObj; this.uploaded = uploaded; this.sha = null; - this.path = config.get('media_folder') && !uploaded ? resolvePath(value, config.get('media_folder')) : value; + this.path = + config.get('media_folder') && !uploaded + ? resolvePath(value, config.get('media_folder')) + : value; this.public_path = !uploaded ? resolvePath(value, config.get('public_folder')) : value; this.asset = asset; } -AssetProxy.prototype.toString = function () { +AssetProxy.prototype.toString = function() { // Use the deployed image path if we do not have a locally cached copy. if (this.uploaded && !this.fileObj) return this.public_path; try { @@ -29,10 +32,10 @@ AssetProxy.prototype.toString = function () { } }; -AssetProxy.prototype.toBase64 = function () { +AssetProxy.prototype.toBase64 = function() { return new Promise(resolve => { const fr = new FileReader(); - fr.onload = (readerEvt) => { + fr.onload = readerEvt => { const binaryString = readerEvt.target.result; resolve(binaryString.split('base64,')[1]); @@ -45,16 +48,23 @@ export function createAssetProxy(value, fileObj, uploaded = false, privateUpload const state = store.getState(); const integration = selectIntegration(state, null, 'assetStore'); if (integration && !uploaded) { - const provider = integration && getIntegrationProvider(state.integrations, currentBackend(state.config).getToken, integration); - return provider.upload(fileObj, privateUpload).then( - response => ( - new AssetProxy(response.asset.url.replace(/^(https?):/, ''), null, true, response.asset) - ), - () => new AssetProxy(value, fileObj, false) - ); + const provider = + integration && + getIntegrationProvider( + state.integrations, + currentBackend(state.config).getToken, + integration, + ); + return provider + .upload(fileObj, privateUpload) + .then( + response => + new AssetProxy(response.asset.url.replace(/^(https?):/, ''), null, true, response.asset), + () => new AssetProxy(value, fileObj, false), + ); } else if (privateUpload) { throw new Error('The Private Upload option is only avaible for Asset Store Integration'); } - + return Promise.resolve(new AssetProxy(value, fileObj, uploaded)); } diff --git a/packages/netlify-cms-core/src/valueObjects/EditorComponent.js b/packages/netlify-cms-core/src/valueObjects/EditorComponent.js index 8d26fc99..eb5406b3 100644 --- a/packages/netlify-cms-core/src/valueObjects/EditorComponent.js +++ b/packages/netlify-cms-core/src/valueObjects/EditorComponent.js @@ -9,24 +9,31 @@ const EditorComponent = Record({ icon: 'exclamation-triangle', fields: [], pattern: catchesNothing, - fromBlock(match) { return {}; }, - toBlock(attributes) { return 'Plugin'; }, - toPreview(attributes) { return 'Plugin'; }, + fromBlock(match) { + return {}; + }, + toBlock(attributes) { + return 'Plugin'; + }, + toPreview(attributes) { + return 'Plugin'; + }, }); /* eslint-enable */ export default function createEditorComponent(config) { const configObj = new EditorComponent({ - id: config.id || config.label.replace(/[^A-Z0-9]+/ig, '_'), + id: config.id || config.label.replace(/[^A-Z0-9]+/gi, '_'), label: config.label, icon: config.icon, fields: fromJS(config.fields), pattern: config.pattern, fromBlock: isFunction(config.fromBlock) ? config.fromBlock.bind(null) : null, toBlock: isFunction(config.toBlock) ? config.toBlock.bind(null) : null, - toPreview: isFunction(config.toPreview) ? config.toPreview.bind(null) : config.toBlock.bind(null), + toPreview: isFunction(config.toPreview) + ? config.toPreview.bind(null) + : config.toBlock.bind(null), }); - return configObj; } diff --git a/packages/netlify-cms-core/src/valueObjects/Entry.js b/packages/netlify-cms-core/src/valueObjects/Entry.js index 56960b1e..4efa515c 100644 --- a/packages/netlify-cms-core/src/valueObjects/Entry.js +++ b/packages/netlify-cms-core/src/valueObjects/Entry.js @@ -1,4 +1,4 @@ -import { isBoolean } from "lodash"; +import { isBoolean } from 'lodash'; export function createEntry(collection, slug = '', path = '', options = {}) { const returnObj = {}; @@ -10,8 +10,6 @@ export function createEntry(collection, slug = '', path = '', options = {}) { returnObj.data = options.data || {}; returnObj.label = options.label || null; returnObj.metaData = options.metaData || null; - returnObj.isModification = isBoolean(options.isModification) - ? options.isModification - : null; + returnObj.isModification = isBoolean(options.isModification) ? options.isModification : null; return returnObj; } diff --git a/packages/netlify-cms-core/webpack.config.js b/packages/netlify-cms-core/webpack.config.js index 73e96e31..cf981587 100644 --- a/packages/netlify-cms-core/webpack.config.js +++ b/packages/netlify-cms-core/webpack.config.js @@ -14,8 +14,8 @@ module.exports = { module: { rules: [ ...Object.entries(rules) - .filter(([ key ]) => key !== 'js') - .map(([ , rule ]) => rule()), + .filter(([key]) => key !== 'js') + .map(([, rule]) => rule()), { test: /\.js$/, exclude: /node_modules/, diff --git a/packages/netlify-cms-editor-component-image/src/index.js b/packages/netlify-cms-editor-component-image/src/index.js index 843b0fac..a24ff072 100644 --- a/packages/netlify-cms-editor-component-image/src/index.js +++ b/packages/netlify-cms-editor-component-image/src/index.js @@ -3,21 +3,26 @@ import React from 'react'; const image = { label: 'Image', id: 'image', - fromBlock: match => match && { - image: match[2], - alt: match[1], - }, - toBlock: data => `![${ data.alt || '' }](${ data.image || '' })`, - toPreview: (data, getAsset) => {data.alt, // eslint-disable-line react/display-name + fromBlock: match => + match && { + image: match[2], + alt: match[1], + }, + toBlock: data => `![${data.alt || ''}](${data.image || ''})`, + // eslint-disable-next-line react/display-name + toPreview: (data, getAsset) => {data.alt, pattern: /^!\[(.*)\]\((.*)\)$/, - fields: [{ - label: 'Image', - name: 'image', - widget: 'image', - }, { - label: 'Alt Text', - name: 'alt', - }], + fields: [ + { + label: 'Image', + name: 'image', + widget: 'image', + }, + { + label: 'Alt Text', + name: 'alt', + }, + ], }; export default image; diff --git a/packages/netlify-cms-lib-auth/src/implicit-oauth.js b/packages/netlify-cms-lib-auth/src/implicit-oauth.js index 1c845c20..e3f473d0 100644 --- a/packages/netlify-cms-lib-auth/src/implicit-oauth.js +++ b/packages/netlify-cms-lib-auth/src/implicit-oauth.js @@ -5,34 +5,34 @@ import uuid from 'uuid/v4'; function createNonce() { const nonce = uuid(); - window.sessionStorage.setItem("netlify-cms-auth", JSON.stringify({ nonce })); + window.sessionStorage.setItem('netlify-cms-auth', JSON.stringify({ nonce })); return nonce; } function validateNonce(check) { - const auth = window.sessionStorage.getItem("netlify-cms-auth"); + const auth = window.sessionStorage.getItem('netlify-cms-auth'); const valid = auth && JSON.parse(auth).nonce; - window.localStorage.removeItem("netlify-cms-auth"); - return (check === valid); + window.localStorage.removeItem('netlify-cms-auth'); + return check === valid; } export default class ImplicitAuthenticator { constructor(config = {}) { const baseURL = trimEnd(config.base_url, '/'); const authEndpoint = trim(config.auth_endpoint, '/'); - this.auth_url = `${ baseURL }/${ authEndpoint }`; + this.auth_url = `${baseURL}/${authEndpoint}`; this.appID = config.app_id; this.clearHash = config.clearHash; } authenticate(options, cb) { if ( - document.location.protocol !== "https:" - // TODO: Is insecure localhost a bad idea as well? I don't think it is, since you are not actually - // sending the token over the internet in this case, assuming the auth URL is secure. - && (document.location.hostname !== "localhost" && document.location.hostname !== "127.0.0.1") - ) { - return cb(new Error("Cannot authenticate over insecure protocol!")); + document.location.protocol !== 'https:' && + // TODO: Is insecure localhost a bad idea as well? I don't think it is, since you are not actually + // sending the token over the internet in this case, assuming the auth URL is secure. + (document.location.hostname !== 'localhost' && document.location.hostname !== '127.0.0.1') + ) { + return cb(new Error('Cannot authenticate over insecure protocol!')); } const authURL = new URL(this.auth_url); @@ -50,7 +50,7 @@ export default class ImplicitAuthenticator { */ completeAuth(cb) { const hashParams = new URLSearchParams(document.location.hash.replace(/^#?\/?/, '')); - if (!hashParams.has("access_token") && !hashParams.has("error")) { + if (!hashParams.has('access_token') && !hashParams.has('error')) { return; } // Remove tokens from hash so that token does not remain in browser history. @@ -60,11 +60,11 @@ export default class ImplicitAuthenticator { const validNonce = validateNonce(params.get('state')); if (!validNonce) { - return cb(new Error("Invalid nonce")); + return cb(new Error('Invalid nonce')); } if (params.has('error')) { - return cb(new Error(`${ params.get('error') }: ${ params.get('error_description') }`)); + return cb(new Error(`${params.get('error')}: ${params.get('error_description')}`)); } if (params.has('access_token')) { diff --git a/packages/netlify-cms-lib-auth/src/netlify-auth.js b/packages/netlify-cms-lib-auth/src/netlify-auth.js index 64000445..5dbc3faa 100644 --- a/packages/netlify-cms-lib-auth/src/netlify-auth.js +++ b/packages/netlify-cms-lib-auth/src/netlify-auth.js @@ -16,20 +16,20 @@ class NetlifyError { const PROVIDERS = { github: { width: 960, - height: 600 + height: 600, }, gitlab: { width: 960, - height: 600 + height: 600, }, bitbucket: { width: 960, - height: 500 + height: 500, }, email: { width: 500, - height: 400 - } + height: 400, + }, }; class Authenticator { @@ -40,8 +40,8 @@ class Authenticator { } handshakeCallback(options, cb) { - const fn = (e) => { - if (e.data === ('authorizing:' + options.provider) && e.origin === this.base_url) { + const fn = e => { + if (e.data === 'authorizing:' + options.provider && e.origin === this.base_url) { window.removeEventListener('message', fn, false); window.addEventListener('message', this.authorizeCallback(options, cb), false); return this.authWindow.postMessage(e.data, e.origin); @@ -51,17 +51,23 @@ class Authenticator { } authorizeCallback(options, cb) { - const fn = (e) => { - if (e.origin !== this.base_url) { return; } + const fn = e => { + if (e.origin !== this.base_url) { + return; + } if (e.data.indexOf('authorization:' + options.provider + ':success:') === 0) { - const data = JSON.parse(e.data.match(new RegExp('^authorization:' + options.provider + ':success:(.+)$'))[1]); + const data = JSON.parse( + e.data.match(new RegExp('^authorization:' + options.provider + ':success:(.+)$'))[1], + ); window.removeEventListener('message', fn, false); this.authWindow.close(); cb(null, data); } if (e.data.indexOf('authorization:' + options.provider + ':error:') === 0) { - const err = JSON.parse(e.data.match(new RegExp('^authorization:' + options.provider + ':error:(.+)$'))[1]); + const err = JSON.parse( + e.data.match(new RegExp('^authorization:' + options.provider + ':error:(.+)$'))[1], + ); window.removeEventListener('message', fn, false); this.authWindow.close(); cb(new NetlifyError(err)); @@ -83,21 +89,28 @@ class Authenticator { const siteID = this.getSiteID(); if (!provider) { - return cb(new NetlifyError({ - message: 'You must specify a provider when calling netlify.authenticate', - })); + return cb( + new NetlifyError({ + message: 'You must specify a provider when calling netlify.authenticate', + }), + ); } if (!siteID) { - return cb(new NetlifyError({ - message: 'You must set a site_id with netlify.configure({site_id: \'your-site-id\'}) to make authentication work from localhost', - })); + return cb( + new NetlifyError({ + message: + "You must set a site_id with netlify.configure({site_id: 'your-site-id'}) to make authentication work from localhost", + }), + ); } const conf = PROVIDERS[provider] || PROVIDERS.github; - const left = (screen.width / 2) - (conf.width / 2); - const top = (screen.height / 2) - (conf.height / 2); + const left = screen.width / 2 - conf.width / 2; + const top = screen.height / 2 - conf.height / 2; window.addEventListener('message', this.handshakeCallback(options, cb), false); - let url = `${ this.base_url }/${ this.auth_endpoint }?provider=${ options.provider }&site_id=${ siteID }`; + let url = `${this.base_url}/${this.auth_endpoint}?provider=${ + options.provider + }&site_id=${siteID}`; if (options.scope) { url += '&scope=' + options.scope; } @@ -114,7 +127,15 @@ class Authenticator { url, 'Netlify Authorization', 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, ' + - ('width=' + conf.width + ', height=' + conf.height + ', top=' + top + ', left=' + left + ');') + ('width=' + + conf.width + + ', height=' + + conf.height + + ', top=' + + top + + ', left=' + + left + + ');'), ); this.authWindow.focus(); } @@ -125,17 +146,24 @@ class Authenticator { const onError = cb || Promise.reject.bind(Promise); if (!provider || !refresh_token) { - return onError(new NetlifyError({ - message: 'You must specify a provider and refresh token when calling netlify.refresh', - })); + return onError( + new NetlifyError({ + message: 'You must specify a provider and refresh token when calling netlify.refresh', + }), + ); } if (!siteID) { - return onError(new NetlifyError({ - message: 'You must set a site_id with netlify.configure({site_id: \'your-site-id\'}) to make token refresh work from localhost', - })); + return onError( + new NetlifyError({ + message: + "You must set a site_id with netlify.configure({site_id: 'your-site-id'}) to make token refresh work from localhost", + }), + ); } - const url = `${ this.base_url }/${ this.auth_endpoint }/refresh?provider=${ provider }&site_id=${ siteID }&refresh_token=${ refresh_token }`; - const refreshPromise = fetch(url, { method: "POST", body: "" }).then(res => res.json()); + const url = `${this.base_url}/${ + this.auth_endpoint + }/refresh?provider=${provider}&site_id=${siteID}&refresh_token=${refresh_token}`; + const refreshPromise = fetch(url, { method: 'POST', body: '' }).then(res => res.json()); // Return a promise if a callback wasn't provided if (!cb) { diff --git a/packages/netlify-cms-lib-util/src/APIError.js b/packages/netlify-cms-lib-util/src/APIError.js index fc45bea2..027836ec 100644 --- a/packages/netlify-cms-lib-util/src/APIError.js +++ b/packages/netlify-cms-lib-util/src/APIError.js @@ -1,7 +1,7 @@ export const API_ERROR = 'API_ERROR'; export default class APIError extends Error { - constructor(message, status, api, meta={}) { + constructor(message, status, api, meta = {}) { super(message); this.message = message; this.status = status; diff --git a/packages/netlify-cms-lib-util/src/Cursor.js b/packages/netlify-cms-lib-util/src/Cursor.js index 8ff9c374..f7b66396 100644 --- a/packages/netlify-cms-lib-util/src/Cursor.js +++ b/packages/netlify-cms-lib-util/src/Cursor.js @@ -1,4 +1,4 @@ -import { fromJS, Map, Set } from "immutable"; +import { fromJS, Map, Set } from 'immutable'; const jsToMap = obj => { if (obj === undefined) { @@ -6,12 +6,12 @@ const jsToMap = obj => { } const immutableObj = fromJS(obj); if (!Map.isMap(immutableObj)) { - throw new Error("Object must be equivalent to a Map."); + throw new Error('Object must be equivalent to a Map.'); } return immutableObj; }; -const knownMetaKeys = Set(["index", "count", "pageSize", "pageCount", "usingOldPaginationAPI"]); +const knownMetaKeys = Set(['index', 'count', 'pageSize', 'pageCount', 'usingOldPaginationAPI']); const filterUnknownMetaKeys = meta => meta.filter((v, k) => knownMetaKeys.has(k)); /* @@ -21,9 +21,10 @@ const filterUnknownMetaKeys = meta => meta.filter((v, k) => knownMetaKeys.has(k) - (actions: , data: , meta: ) -> cursor */ const createCursorMap = (...args) => { - const { actions, data, meta } = args.length === 1 - ? jsToMap(args[0]).toObject() - : { actions: args[0], data: args[1], meta: args[2] }; + const { actions, data, meta } = + args.length === 1 + ? jsToMap(args[0]).toObject() + : { actions: args[0], data: args[1], meta: args[2] }; return Map({ // actions are a Set, rather than a List, to ensure an efficient .has actions: Set(actions), @@ -34,10 +35,13 @@ const createCursorMap = (...args) => { }); }; -const hasAction = (cursorMap, action) => cursorMap.hasIn(["actions", action]); +const hasAction = (cursorMap, action) => cursorMap.hasIn(['actions', action]); const getActionHandlers = (cursorMap, handler) => - cursorMap.get("actions", Set()).toMap().map(action => handler(action)); + cursorMap + .get('actions', Set()) + .toMap() + .map(action => handler(action)); // The cursor logic is entirely functional, so this class simply // provides a chainable interface @@ -52,9 +56,9 @@ export default class Cursor { } this.store = createCursorMap(...args); - this.actions = this.store.get("actions"); - this.data = this.store.get("data"); - this.meta = this.store.get("meta"); + this.actions = this.store.get('actions'); + this.data = this.store.get('data'); + this.meta = this.store.get('meta'); } updateStore(...args) { @@ -68,42 +72,45 @@ export default class Cursor { return hasAction(this.store, action); } addAction(action) { - return this.updateStore("actions", actions => actions.add(action)); + return this.updateStore('actions', actions => actions.add(action)); } removeAction(action) { - return this.updateStore("actions", actions => actions.delete(action)); + return this.updateStore('actions', actions => actions.delete(action)); } setActions(actions) { - return this.updateStore(store => store.set("actions", Set(actions))); + return this.updateStore(store => store.set('actions', Set(actions))); } mergeActions(actions) { - return this.updateStore("actions", oldActions => oldActions.union(actions)); + return this.updateStore('actions', oldActions => oldActions.union(actions)); } getActionHandlers(handler) { return getActionHandlers(this.store, handler); } setData(data) { - return new Cursor(this.store.set("data", jsToMap(data))); + return new Cursor(this.store.set('data', jsToMap(data))); } mergeData(data) { - return new Cursor(this.store.mergeIn(["data"], jsToMap(data))); + return new Cursor(this.store.mergeIn(['data'], jsToMap(data))); } wrapData(data) { - return this.updateStore("data", oldData => jsToMap(data).set("wrapped_cursor_data", oldData)); + return this.updateStore('data', oldData => jsToMap(data).set('wrapped_cursor_data', oldData)); } unwrapData() { - return [this.store.get("data").delete("wrapped_cursor_data"), this.updateStore("data", data => data.get("wrapped_cursor_data"))]; + return [ + this.store.get('data').delete('wrapped_cursor_data'), + this.updateStore('data', data => data.get('wrapped_cursor_data')), + ]; } clearData() { - return this.updateStore("data", () => Map()); + return this.updateStore('data', () => Map()); } setMeta(meta) { - return this.updateStore(store => store.set("meta", jsToMap(meta))); + return this.updateStore(store => store.set('meta', jsToMap(meta))); } mergeMeta(meta) { - return this.updateStore(store => store.update("meta", oldMeta => oldMeta.merge(jsToMap(meta)))) + return this.updateStore(store => store.update('meta', oldMeta => oldMeta.merge(jsToMap(meta)))); } } @@ -112,4 +119,4 @@ export default class Cursor { // backends at all. This should be removed in favor of wrapping old // backends with a compatibility layer, as part of the backend API // refactor. -export const CURSOR_COMPATIBILITY_SYMBOL = Symbol("cursor key for compatibility with old backends"); +export const CURSOR_COMPATIBILITY_SYMBOL = Symbol('cursor key for compatibility with old backends'); diff --git a/packages/netlify-cms-lib-util/src/__tests__/path.spec.js b/packages/netlify-cms-lib-util/src/__tests__/path.spec.js index 9c5f5e48..0d296541 100644 --- a/packages/netlify-cms-lib-util/src/__tests__/path.spec.js +++ b/packages/netlify-cms-lib-util/src/__tests__/path.spec.js @@ -2,100 +2,52 @@ import { fileExtensionWithSeparator, fileExtension } from '../path'; describe('fileExtensionWithSeparator', () => { it('should return the extension of a file', () => { - expect( - fileExtensionWithSeparator('index.html') - ).toEqual( - '.html' - ); + expect(fileExtensionWithSeparator('index.html')).toEqual('.html'); }); it('should return the extension of a file path', () => { - expect( - fileExtensionWithSeparator('/src/main/index.html') - ).toEqual( - '.html' - ); + expect(fileExtensionWithSeparator('/src/main/index.html')).toEqual('.html'); }); it('should return the extension of a file path with trailing slash', () => { - expect( - fileExtensionWithSeparator('/src/main/index.html/') - ).toEqual( - '.html' - ); + expect(fileExtensionWithSeparator('/src/main/index.html/')).toEqual('.html'); }); it('should return the extension for an extension with two ..', () => { - expect( - fileExtensionWithSeparator('/src/main/index..html') - ).toEqual( - '.html' - ); + expect(fileExtensionWithSeparator('/src/main/index..html')).toEqual('.html'); }); it('should return an empty string for the parent path ..', () => { - expect( - fileExtensionWithSeparator('..') - ).toEqual( - '' - ); + expect(fileExtensionWithSeparator('..')).toEqual(''); }); it('should return an empty string if the file has no extension', () => { - expect( - fileExtensionWithSeparator('/src/main/index') - ).toEqual( - '' - ); + expect(fileExtensionWithSeparator('/src/main/index')).toEqual(''); }); }); describe('fileExtension', () => { it('should return the extension of a file', () => { - expect( - fileExtension('index.html') - ).toEqual( - 'html' - ); + expect(fileExtension('index.html')).toEqual('html'); }); it('should return the extension of a file path', () => { - expect( - fileExtension('/src/main/index.html') - ).toEqual( - 'html' - ); + expect(fileExtension('/src/main/index.html')).toEqual('html'); }); it('should return the extension of a file path with trailing slash', () => { - expect( - fileExtension('/src/main/index.html/') - ).toEqual( - 'html' - ); + expect(fileExtension('/src/main/index.html/')).toEqual('html'); }); it('should return the extension for an extension with two ..', () => { - expect( - fileExtension('/src/main/index..html') - ).toEqual( - 'html' - ); + expect(fileExtension('/src/main/index..html')).toEqual('html'); }); it('should return an empty string for the parent path ..', () => { - expect( - fileExtension('..') - ).toEqual( - '' - ); + expect(fileExtension('..')).toEqual(''); }); it('should return an empty string if the file has no extension', () => { - expect( - fileExtension('/src/main/index') - ).toEqual( - '' - ); + expect(fileExtension('/src/main/index')).toEqual(''); }); }); diff --git a/packages/netlify-cms-lib-util/src/backendUtil.js b/packages/netlify-cms-lib-util/src/backendUtil.js index ff6f2018..80817762 100644 --- a/packages/netlify-cms-lib-util/src/backendUtil.js +++ b/packages/netlify-cms-lib-util/src/backendUtil.js @@ -1,6 +1,6 @@ -import { get } from "lodash"; -import { fromJS } from "immutable"; -import { fileExtension } from "./path"; +import { get } from 'lodash'; +import { fromJS } from 'immutable'; +import { fileExtension } from './path'; export const filterByPropExtension = (extension, propName) => arr => arr.filter(el => fileExtension(get(el, propName)) === extension); @@ -9,31 +9,31 @@ const catchFormatErrors = (format, formatter) => res => { try { return formatter(res); } catch (err) { - throw new Error(`Response cannot be parsed into the expected format (${ format }): ${ err.message }`); + throw new Error( + `Response cannot be parsed into the expected format (${format}): ${err.message}`, + ); } }; const responseFormatters = fromJS({ json: async res => { - const contentType = res.headers.get("Content-Type"); - if (!contentType.startsWith("application/json") && !contentType.startsWith("text/json")) { - throw new Error(`${ contentType } is not a valid JSON Content-Type`); + const contentType = res.headers.get('Content-Type'); + if (!contentType.startsWith('application/json') && !contentType.startsWith('text/json')) { + throw new Error(`${contentType} is not a valid JSON Content-Type`); } return res.json(); }, text: async res => res.text(), blob: async res => res.blob(), -}).mapEntries( - ([format, formatter]) => [format, catchFormatErrors(format, formatter)] -); +}).mapEntries(([format, formatter]) => [format, catchFormatErrors(format, formatter)]); -export const parseResponse = async (res, { expectingOk = true, format = "text" } = {}) => { +export const parseResponse = async (res, { expectingOk = true, format = 'text' } = {}) => { if (expectingOk && !res.ok) { - throw new Error(`Expected an ok response, but received an error status: ${ res.status }.`); + throw new Error(`Expected an ok response, but received an error status: ${res.status}.`); } const formatter = responseFormatters.get(format, false); if (!formatter) { - throw new Error(`${ format } is not a supported response format.`); + throw new Error(`${format} is not a supported response format.`); } const body = await formatter(res); return body; diff --git a/packages/netlify-cms-lib-util/src/localForage.js b/packages/netlify-cms-lib-util/src/localForage.js index 87519938..1cdb47cf 100644 --- a/packages/netlify-cms-lib-util/src/localForage.js +++ b/packages/netlify-cms-lib-util/src/localForage.js @@ -1,16 +1,19 @@ -import localForage from "localforage"; +import localForage from 'localforage'; function localForageTest() { const testKey = 'localForageTest'; - localForage.setItem(testKey, {expires: Date.now() + 300000}).then(() => { - localForage.removeItem(testKey); - }).catch((err) => { - if (err.code === 22) { - const message = `Unable to set localStorage key. Quota exceeded! Full disk?`; - return alert(`${message}\n\n${err}`); - } - console.log(err); - }) + localForage + .setItem(testKey, { expires: Date.now() + 300000 }) + .then(() => { + localForage.removeItem(testKey); + }) + .catch(err => { + if (err.code === 22) { + const message = `Unable to set localStorage key. Quota exceeded! Full disk?`; + return alert(`${message}\n\n${err}`); + } + console.log(err); + }); } localForageTest(); diff --git a/packages/netlify-cms-lib-util/src/path.js b/packages/netlify-cms-lib-util/src/path.js index 46799360..11e0ccb7 100644 --- a/packages/netlify-cms-lib-util/src/path.js +++ b/packages/netlify-cms-lib-util/src/path.js @@ -10,11 +10,11 @@ export function resolvePath(path, basePath) { if (path.indexOf('/') === -1) { // It's a single file name, no directories. Prepend public folder - return normalizePath(`/${ basePath }/${ path }`); + return normalizePath(`/${basePath}/${path}`); } // It's a relative path. Prepend a forward slash. - return normalizePath(`/${ path }`); + return normalizePath(`/${path}`); } /** @@ -28,7 +28,7 @@ export function resolvePath(path, basePath) { * // returns * 'quux' */ -export function basename(p, ext = "") { +export function basename(p, ext = '') { // Special case: Normalize will modify this to '.' if (p === '') { return p; diff --git a/packages/netlify-cms-lib-util/src/promise.js b/packages/netlify-cms-lib-util/src/promise.js index e3f86bf8..ad9bae8c 100644 --- a/packages/netlify-cms-lib-util/src/promise.js +++ b/packages/netlify-cms-lib-util/src/promise.js @@ -1,22 +1,20 @@ import zipObject from 'lodash/zipObject'; export const filterPromises = (arr, filter) => - Promise.all(arr.map(entry => filter(entry))) - .then(bits => arr.filter(() => bits.shift())); + Promise.all(arr.map(entry => filter(entry))).then(bits => arr.filter(() => bits.shift())); -export const resolvePromiseProperties = (obj) => { +export const resolvePromiseProperties = obj => { // Get the keys which represent promises - const promiseKeys = Object.keys(obj).filter( - key => typeof obj[key].then === "function"); + const promiseKeys = Object.keys(obj).filter(key => typeof obj[key].then === 'function'); const promises = promiseKeys.map(key => obj[key]); // Resolve all promises - return Promise.all(promises) - .then(resolvedPromises => + return Promise.all(promises).then(resolvedPromises => // Return a copy of obj with promises overwritten by their // resolved values - Object.assign({}, obj, zipObject(promiseKeys, resolvedPromises))); + Object.assign({}, obj, zipObject(promiseKeys, resolvedPromises)), + ); }; export const then = fn => p => Promise.resolve(p).then(fn); diff --git a/packages/netlify-cms-lib-util/src/unsentRequest.js b/packages/netlify-cms-lib-util/src/unsentRequest.js index 5161e64f..2f735725 100644 --- a/packages/netlify-cms-lib-util/src/unsentRequest.js +++ b/packages/netlify-cms-lib-util/src/unsentRequest.js @@ -3,26 +3,40 @@ import curry from 'lodash/curry'; import flow from 'lodash/flow'; import isString from 'lodash/isString'; -const decodeParams = paramsString => List(paramsString.split("&")) - .map(s => List(s.split("=")).map(decodeURIComponent)) - .update(Map); +const decodeParams = paramsString => + List(paramsString.split('&')) + .map(s => List(s.split('=')).map(decodeURIComponent)) + .update(Map); const fromURL = wholeURL => { - const [url, allParamsString] = wholeURL.split("?"); + const [url, allParamsString] = wholeURL.split('?'); return Map({ url, ...(allParamsString ? { params: decodeParams(allParamsString) } : {}) }); }; -const encodeParams = params => params.entrySeq() - .map(([k, v]) => `${ encodeURIComponent(k) }=${ encodeURIComponent(v) }`) - .join("&"); +const encodeParams = params => + params + .entrySeq() + .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`) + .join('&'); -const toURL = req => `${ req.get("url") }${ req.get("params") ? `?${ encodeParams(req.get("params")) }` : "" }`; +const toURL = req => + `${req.get('url')}${req.get('params') ? `?${encodeParams(req.get('params'))}` : ''}`; -const toFetchArguments = req => [toURL(req), req.delete("url").delete("params").toJS()]; +const toFetchArguments = req => [ + toURL(req), + req + .delete('url') + .delete('params') + .toJS(), +]; const maybeRequestArg = req => { - if (isString(req)) { return fromURL(req); } - if (req) { return fromJS(req); } + if (isString(req)) { + return fromURL(req); + } + if (req) { + return fromJS(req); + } return Map(); }; const ensureRequestArg = func => req => func(maybeRequestArg(req)); @@ -41,23 +55,27 @@ const getPropSetFunctions = path => [ getCurriedRequestProcessor((val, req) => (req.getIn(path) ? req : req.setIn(path, val))), ]; const getPropMergeFunctions = path => [ - getCurriedRequestProcessor((obj, req) => req.updateIn(path, (p=Map()) => p.merge(obj))), - getCurriedRequestProcessor((obj, req) => req.updateIn(path, (p=Map()) => Map(obj).merge(p))), + getCurriedRequestProcessor((obj, req) => req.updateIn(path, (p = Map()) => p.merge(obj))), + getCurriedRequestProcessor((obj, req) => req.updateIn(path, (p = Map()) => Map(obj).merge(p))), ]; -const [withMethod, withDefaultMethod] = getPropSetFunctions(["method"]); -const [withBody, withDefaultBody] = getPropSetFunctions(["body"]); -const [withParams, withDefaultParams] = getPropMergeFunctions(["params"]); -const [withHeaders, withDefaultHeaders] = getPropMergeFunctions(["headers"]); +const [withMethod, withDefaultMethod] = getPropSetFunctions(['method']); +const [withBody, withDefaultBody] = getPropSetFunctions(['body']); +const [withParams, withDefaultParams] = getPropMergeFunctions(['params']); +const [withHeaders, withDefaultHeaders] = getPropMergeFunctions(['headers']); // withRoot sets a root URL, unless the URL is already absolute const absolutePath = new RegExp('^(?:[a-z]+:)?//', 'i'); -const withRoot = getCurriedRequestProcessor((root, req) => req.update("url", p => { - if (absolutePath.test(p)) { return p; } - return (root && p && p[0] !== "/" && root[root.length - 1] !== "/") - ? `${ root }/${ p }` - : `${ root }${ p }`; -})); +const withRoot = getCurriedRequestProcessor((root, req) => + req.update('url', p => { + if (absolutePath.test(p)) { + return p; + } + return root && p && p[0] !== '/' && root[root.length - 1] !== '/' + ? `${root}/${p}` + : `${root}${p}`; + }), +); // withTimestamp needs no argument and has to run as late as possible, // so it calls `withParams` only when it's actually called with a diff --git a/packages/netlify-cms-ui-default/src/AuthenticationPage.js b/packages/netlify-cms-ui-default/src/AuthenticationPage.js index 223599ac..9b3446f1 100644 --- a/packages/netlify-cms-ui-default/src/AuthenticationPage.js +++ b/packages/netlify-cms-ui-default/src/AuthenticationPage.js @@ -9,12 +9,12 @@ const StyledAuthenticationPage = styled.section` align-items: center; justify-content: center; height: 100vh; -` +`; const PageLogoIcon = styled(Icon)` color: #c4c6d2; margin-top: -300px; -` +`; const LoginButton = styled.button` ${buttons.button}; @@ -27,7 +27,7 @@ const LoginButton = styled.button` display: flex; align-items: center; position: relative; -` +`; const AuthenticationPage = ({ onLogin, @@ -38,14 +38,14 @@ const AuthenticationPage = ({ }) => { return ( - + {loginErrorMessage ?

    {loginErrorMessage}

    : null} {!renderPageContent ? null : renderPageContent()} - {!renderButtonContent ? null : + {!renderButtonContent ? null : ( {renderButtonContent()} - } + )}
    ); }; diff --git a/packages/netlify-cms-ui-default/src/Dropdown.js b/packages/netlify-cms-ui-default/src/Dropdown.js index a536a0f4..a97c016f 100644 --- a/packages/netlify-cms-ui-default/src/Dropdown.js +++ b/packages/netlify-cms-ui-default/src/Dropdown.js @@ -8,7 +8,7 @@ const StyledWrapper = styled(Wrapper)` position: relative; font-size: 14px; user-select: none; -` +`; const StyledDropdownButton = styled(DropdownButton)` ${buttons.button}; @@ -26,7 +26,7 @@ const StyledDropdownButton = styled(DropdownButton)` right: 16px; color: currentColor; } -` +`; const DropdownList = styled.ul` ${components.dropdownList}; @@ -42,19 +42,19 @@ const DropdownList = styled.ul` top: ${props.top}; left: ${props.position === 'left' ? 0 : 'auto'}; right: ${props.position === 'right' ? 0 : 'auto'}; - `} -` + `}; +`; const StyledMenuItem = styled(MenuItem)` ${components.dropdownItem}; -` +`; const MenuItemIconContainer = styled.div` flex: 1 0 32px; text-align: right; position: relative; top: 2px; -` +`; const Dropdown = ({ renderButton, @@ -79,13 +79,11 @@ const Dropdown = ({ const DropdownItem = ({ label, icon, iconDirection, onClick, className }) => ( {label} - { - icon - ? - - - : null - } + {icon ? ( + + + + ) : null} ); diff --git a/packages/netlify-cms-ui-default/src/Icon.js b/packages/netlify-cms-ui-default/src/Icon.js index c69d621a..e893dab1 100644 --- a/packages/netlify-cms-ui-default/src/Icon.js +++ b/packages/netlify-cms-ui-default/src/Icon.js @@ -24,7 +24,7 @@ const IconWrapper = styled.span` width: 100%; height: 100%; } -` +`; /** * Calculates rotation for icons that have a `direction` property configured @@ -41,7 +41,7 @@ const getRotation = (iconDirection, newDirection) => { const rotations = { right: 90, down: 180, left: 270, up: 360 }; const degrees = rotations[newDirection] - rotations[iconDirection]; return `${degrees}deg`; -} +}; const sizes = { xsmall: '12px', @@ -59,4 +59,4 @@ const Icon = ({ type, direction, size = 'medium', className }) => ( /> ); -export default styled(Icon)`` +export default styled(Icon)``; diff --git a/packages/netlify-cms-ui-default/src/Icon/icons.js b/packages/netlify-cms-ui-default/src/Icon/icons.js index 573c3664..b6fd0a78 100644 --- a/packages/netlify-cms-ui-default/src/Icon/icons.js +++ b/packages/netlify-cms-ui-default/src/Icon/icons.js @@ -19,15 +19,15 @@ import images from './images/_index'; * Configuration for individual icons. */ const config = { - 'arrow': { + arrow: { direction: 'left', }, - 'chevron': { + chevron: { direction: 'down', }, 'chevron-double': { direction: 'down', - } + }, }; /** diff --git a/packages/netlify-cms-ui-default/src/Icon/images/_index.js b/packages/netlify-cms-ui-default/src/Icon/images/_index.js index c342784e..2bc93499 100644 --- a/packages/netlify-cms-ui-default/src/Icon/images/_index.js +++ b/packages/netlify-cms-ui-default/src/Icon/images/_index.js @@ -44,48 +44,48 @@ import iconWrite from './write.svg'; const iconix = iconAdd; const images = { - 'add': iconix, + add: iconix, 'add-with': iconAddWith, - 'arrow': iconArrow, - 'bitbucket': iconBitbucket, - 'bold': iconBold, - 'check': iconCheck, - 'chevron': iconChevron, + arrow: iconArrow, + bitbucket: iconBitbucket, + bold: iconBold, + check: iconCheck, + chevron: iconChevron, 'chevron-double': iconChevronDouble, - 'circle': iconCircle, - 'close': iconClose, - 'code': iconCode, + circle: iconCircle, + close: iconClose, + code: iconCode, 'code-block': iconCodeBlock, 'drag-handle': iconDragHandle, - 'eye': iconEye, - 'folder': iconFolder, - 'github': iconGithub, - 'gitlab': iconGitlab, - 'grid': iconGrid, - 'h1': iconH1, - 'h2': iconH2, - 'home': iconHome, - 'image': iconImage, - 'italic': iconItalic, - 'link': iconLink, - 'list': iconList, + eye: iconEye, + folder: iconFolder, + github: iconGithub, + gitlab: iconGitlab, + grid: iconGrid, + h1: iconH1, + h2: iconH2, + home: iconHome, + image: iconImage, + italic: iconItalic, + link: iconLink, + list: iconList, 'list-bulleted': iconListBulleted, 'list-numbered': iconListNumbered, - 'markdown': iconMarkdown, - 'media': iconMedia, + markdown: iconMarkdown, + media: iconMedia, 'media-alt': iconMediaAlt, - 'netlify': iconNetlify, + netlify: iconNetlify, 'netlify-cms': iconNetlifyCms, - 'page': iconPage, - 'pages': iconPages, + page: iconPage, + pages: iconPages, 'pages-alt': iconPagesAlt, - 'quote': iconQuote, - 'scroll': iconScroll, - 'search': iconSearch, - 'settings': iconSettings, - 'user': iconUser, - 'workflow': iconWorkflow, - 'write': iconWrite, + quote: iconQuote, + scroll: iconScroll, + search: iconSearch, + settings: iconSettings, + user: iconUser, + workflow: iconWorkflow, + write: iconWrite, }; export default images; diff --git a/packages/netlify-cms-ui-default/src/ListItemTopBar.js b/packages/netlify-cms-ui-default/src/ListItemTopBar.js index 19ad8593..358731cc 100644 --- a/packages/netlify-cms-ui-default/src/ListItemTopBar.js +++ b/packages/netlify-cms-ui-default/src/ListItemTopBar.js @@ -9,7 +9,7 @@ const TopBar = styled.div` height: 26px; border-radius: ${lengths.borderRadius} ${lengths.borderRadius} 0 0; position: relative; -` +`; const TopBarButton = styled.button` ${buttons.button}; @@ -24,38 +24,32 @@ const TopBarButton = styled.button` display: flex; justify-content: center; align-items: center; -` +`; const TopBarButtonSpan = TopBarButton.withComponent('span'); const DragIcon = styled(TopBarButtonSpan)` width: 100%; cursor: move; -` +`; const ListItemTopBar = ({ className, collapsed, onCollapseToggle, onRemove, dragHandleHOC }) => ( - { - onCollapseToggle - ? - - - : null - } - { - dragHandleHOC - ? - - - : null - } - { - onRemove - ? - - - : null - } + {onCollapseToggle ? ( + + + + ) : null} + {dragHandleHOC ? ( + + + + ) : null} + {onRemove ? ( + + + + ) : null} ); @@ -65,6 +59,6 @@ const StyledListItemTopBar = styled(ListItemTopBar)` height: 26px; border-radius: ${lengths.borderRadius} ${lengths.borderRadius} 0 0; position: relative; -` +`; export default StyledListItemTopBar; diff --git a/packages/netlify-cms-ui-default/src/Loader.js b/packages/netlify-cms-ui-default/src/Loader.js index 3610a077..b531fcb6 100644 --- a/packages/netlify-cms-ui-default/src/Loader.js +++ b/packages/netlify-cms-ui-default/src/Loader.js @@ -45,13 +45,13 @@ const LoaderText = styled.div` color: #767676; margin-top: 55px; line-height: 35px; -` +`; const LoaderItem = styled.div` position: absolute; white-space: nowrap; transform: translateX(-50%); -` +`; export class Loader extends React.Component { state = { @@ -69,7 +69,8 @@ export class Loader extends React.Component { const { children } = this.props; this.interval = setInterval(() => { - const nextItem = (this.state.currentItem === children.length - 1) ? 0 : this.state.currentItem + 1; + const nextItem = + this.state.currentItem === children.length - 1 ? 0 : this.state.currentItem + 1; this.setState({ currentItem: nextItem }); }, 5000); }; @@ -108,7 +109,7 @@ export class Loader extends React.Component { } const StyledLoader = styled(Loader)` - display: ${props => props.active ? 'block' : 'none'}; + display: ${props => (props.active ? 'block' : 'none')}; position: absolute; top: 50%; left: 50%; @@ -143,6 +144,6 @@ const StyledLoader = styled(Loader)` border-color: ${colors.active} transparent transparent; box-shadow: 0px 0px 0px 1px transparent; } -` +`; export default StyledLoader; diff --git a/packages/netlify-cms-ui-default/src/ObjectWidgetTopBar.js b/packages/netlify-cms-ui-default/src/ObjectWidgetTopBar.js index b26ec1cb..081098d3 100644 --- a/packages/netlify-cms-ui-default/src/ObjectWidgetTopBar.js +++ b/packages/netlify-cms-ui-default/src/ObjectWidgetTopBar.js @@ -1,5 +1,5 @@ import React from 'react'; -import styled, { css }from 'react-emotion'; +import styled, { css } from 'react-emotion'; import Icon from './Icon'; import { colors, buttons } from './styles'; @@ -10,17 +10,19 @@ const TopBarContainer = styled.div` justify-content: space-between; margin: 0 -14px; padding: 13px; -` +`; const ExpandButtonContainer = styled.div` - ${props => props.hasHeading && css` - display: flex; - align-items: center; - font-size: 14px; - font-weight: 500; - line-height: 1; - `} -` + ${props => + props.hasHeading && + css` + display: flex; + align-items: center; + font-size: 14px; + font-weight: 500; + line-height: 1; + `}; +`; const ExpandButton = styled.button` ${buttons.button}; @@ -31,7 +33,7 @@ const ExpandButton = styled.button` &:last-of-type { margin-right: 4px; } -` +`; const AddButton = styled.button` ${buttons.button}; @@ -46,9 +48,16 @@ const AddButton = styled.button` ${Icon} { margin-left: 6px; } -` +`; -const ObjectWidgetTopBar = ({ allowAdd, onAdd, onCollapseToggle, collapsed, heading = null, label }) => ( +const ObjectWidgetTopBar = ({ + allowAdd, + onAdd, + onCollapseToggle, + collapsed, + heading = null, + label, +}) => ( @@ -56,11 +65,11 @@ const ObjectWidgetTopBar = ({ allowAdd, onAdd, onCollapseToggle, collapsed, head {heading} - {!allowAdd ? null : + {!allowAdd ? null : ( Add {label} - } + )} ); diff --git a/packages/netlify-cms-ui-default/src/Toggle.js b/packages/netlify-cms-ui-default/src/Toggle.js index 1147ee23..be534ff0 100644 --- a/packages/netlify-cms-ui-default/src/Toggle.js +++ b/packages/netlify-cms-ui-default/src/Toggle.js @@ -1,4 +1,4 @@ -import React from 'react' +import React from 'react'; import styled, { css } from 'react-emotion'; import ReactToggled from 'react-toggled'; import { colors, colorsRaw, shadows, transitions } from './styles'; @@ -11,7 +11,7 @@ const ToggleContainer = styled.span` width: 40px; height: 20px; cursor: pointer; -` +`; const ToggleHandle = styled.span` ${shadows.dropDeep}; @@ -24,17 +24,19 @@ const ToggleHandle = styled.span` background-color: ${colorsRaw.white}; transition: transform ${transitions.main}; - ${props => props.isActive && css` - transform: translateX(20px); - `} -` + ${props => + props.isActive && + css` + transform: translateX(20px); + `}; +`; const ToggleBackground = styled.span` width: 34px; height: 14px; border-radius: 10px; background-color: ${colors.active}; -` +`; const Toggle = ({ active, @@ -47,7 +49,7 @@ const Toggle = ({ Handle = ToggleHandle, }) => ( - {({on, getElementTogglerProps}) => ( + {({ on, getElementTogglerProps }) => ( - - + + )} diff --git a/packages/netlify-cms-ui-default/src/styles.js b/packages/netlify-cms-ui-default/src/styles.js index ef947670..ce09561a 100644 --- a/packages/netlify-cms-ui-default/src/styles.js +++ b/packages/netlify-cms-ui-default/src/styles.js @@ -1,16 +1,6 @@ import { css, injectGlobal } from 'react-emotion'; -export { - fonts, - colorsRaw, - colors, - lengths, - components, - buttons, - shadows, - borders, - transitions, -}; +export { fonts, colorsRaw, colors, lengths, components, buttons, shadows, borders, transitions }; /** * Font Stacks @@ -110,19 +100,16 @@ const transitions = { const shadows = { drop: css` - box-shadow: 0 2px 4px 0 rgba(19, 39, 48, .12); + box-shadow: 0 2px 4px 0 rgba(19, 39, 48, 0.12); `, dropMain: css` - box-shadow: 0 2px 6px 0 rgba(68, 74, 87, 0.05), - 0 1px 3px 0 rgba(68, 74, 87, 0.10); + box-shadow: 0 2px 6px 0 rgba(68, 74, 87, 0.05), 0 1px 3px 0 rgba(68, 74, 87, 0.1); `, dropMiddle: css` - box-shadow: 0 2px 6px 0 rgba(68, 74, 87, 0.15), - 0 1px 3px 0 rgba(68, 74, 87, 0.30); + box-shadow: 0 2px 6px 0 rgba(68, 74, 87, 0.15), 0 1px 3px 0 rgba(68, 74, 87, 0.3); `, dropDeep: css` - box-shadow: 0 4px 12px 0 rgba(68, 74, 87, 0.15), - 0 1px 3px 0 rgba(68, 74, 87, 0.25); + box-shadow: 0 4px 12px 0 rgba(68, 74, 87, 0.15), 0 1px 3px 0 rgba(68, 74, 87, 0.25); `, }; @@ -285,7 +272,7 @@ const components = { background-color: ${colors.activeBackground}; } `, -} +}; injectGlobal` *, *:before, *:after { diff --git a/packages/netlify-cms-widget-boolean/src/BooleanControl.js b/packages/netlify-cms-widget-boolean/src/BooleanControl.js index 1fd2e086..78b7b032 100644 --- a/packages/netlify-cms-widget-boolean/src/BooleanControl.js +++ b/packages/netlify-cms-widget-boolean/src/BooleanControl.js @@ -6,8 +6,8 @@ import { isBoolean } from 'lodash'; import { Toggle, ToggleBackground, colors } from 'netlify-cms-ui-default'; const BooleanBackground = styled(ToggleBackground)` - background-color: ${props => props.isActive ? colors.active : colors.textFieldBorder}; -` + background-color: ${props => (props.isActive ? colors.active : colors.textFieldBorder)}; +`; export default class BooleanControl extends React.Component { render() { @@ -18,7 +18,7 @@ export default class BooleanControl extends React.Component { onChange, classNameWrapper, setActiveStyle, - setInactiveStyle + setInactiveStyle, } = this.props; return (
    diff --git a/packages/netlify-cms-widget-date/src/DateControl.js b/packages/netlify-cms-widget-date/src/DateControl.js index afd10d69..63c978a0 100644 --- a/packages/netlify-cms-widget-date/src/DateControl.js +++ b/packages/netlify-cms-widget-date/src/DateControl.js @@ -7,7 +7,7 @@ import moment from 'moment'; injectGlobal` ${dateTimeStyles} -` +`; export default class DateControl extends React.Component { static propTypes = { @@ -16,10 +16,7 @@ export default class DateControl extends React.Component { classNameWrapper: PropTypes.string.isRequired, setActiveStyle: PropTypes.func.isRequired, setInactiveStyle: PropTypes.func.isRequired, - value: PropTypes.oneOfType([ - PropTypes.object, - PropTypes.string, - ]), + value: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), includeTime: PropTypes.bool, }; @@ -39,7 +36,8 @@ export default class DateControl extends React.Component { // Date is valid if datetime is a moment or Date object otherwise it's a string. // Handle the empty case, if the user wants to empty the field. - isValidDate = datetime => (moment.isMoment(datetime) || datetime instanceof Date || datetime === ''); + isValidDate = datetime => + moment.isMoment(datetime) || datetime instanceof Date || datetime === ''; handleChange = datetime => { const { onChange } = this.props; diff --git a/packages/netlify-cms-widget-file/src/FilePreview.js b/packages/netlify-cms-widget-file/src/FilePreview.js index c2c2743f..28917900 100644 --- a/packages/netlify-cms-widget-file/src/FilePreview.js +++ b/packages/netlify-cms-widget-file/src/FilePreview.js @@ -4,7 +4,7 @@ import { WidgetPreviewContainer } from 'netlify-cms-ui-default'; const FilePreview = ({ value, getAsset }) => ( - { value ? { value } : null} + {value ? {value} : null} ); diff --git a/packages/netlify-cms-widget-file/src/withFileControl.js b/packages/netlify-cms-widget-file/src/withFileControl.js index 3f881e23..f1f133b0 100644 --- a/packages/netlify-cms-widget-file/src/withFileControl.js +++ b/packages/netlify-cms-widget-file/src/withFileControl.js @@ -9,44 +9,44 @@ const MAX_DISPLAY_LENGTH = 50; const FileContent = styled.div` display: flex; -` +`; const ImageWrapper = styled.div` width: 155px; height: 100px; margin-right: 20px; -` +`; const Image = styled.img` width: 100%; height: 100%; object-fit: cover; border-radius: ${lengths.borderRadius}; -` +`; const FileInfo = styled.div` button:not(:first-child) { margin-top: 12px; } -` +`; const FileName = styled.span` display: block; font-size: 16px; margin-bottom: 20px; -` +`; const FileWidgetButton = styled.button` ${buttons.button}; ${components.textBadge}; display: block; -` +`; const FileWidgetButtonRemove = styled.button` ${buttons.button}; ${components.textBadgeDanger}; display: block; -` +`; export default function withFileControl({ forImage } = {}) { return class FileControl extends React.Component { @@ -84,7 +84,7 @@ export default function withFileControl({ forImage } = {}) { * path is different than the value in `nextProps`, update. */ const mediaPath = nextProps.mediaPaths.get(this.controlID); - if (mediaPath && (nextProps.value !== mediaPath)) { + if (mediaPath && nextProps.value !== mediaPath) { return true; } @@ -122,15 +122,22 @@ export default function withFileControl({ forImage } = {}) { if (!value || value.length <= size) { return value; } - return `${ value.substring(0, size / 2) }\u2026${ value.substring(value.length - size / 2 + 1, value.length) }`; + return `${value.substring(0, size / 2)}\u2026${value.substring( + value.length - size / 2 + 1, + value.length, + )}`; }; - renderSelection = (subject) => { + renderSelection = subject => { const fileName = this.renderFileName(); const { getAsset, value } = this.props; return ( - { forImage ? : null } + {forImage ? ( + + + + ) : null} {fileName} @@ -158,10 +165,10 @@ export default function withFileControl({ forImage } = {}) { return (
    - { value ? this.renderSelection(subject) : this.renderNoSelection(subject, article) } + {value ? this.renderSelection(subject) : this.renderNoSelection(subject, article)}
    ); } - } + }; } diff --git a/packages/netlify-cms-widget-image/src/ImagePreview.js b/packages/netlify-cms-widget-image/src/ImagePreview.js index f588f47d..021ee011 100644 --- a/packages/netlify-cms-widget-image/src/ImagePreview.js +++ b/packages/netlify-cms-widget-image/src/ImagePreview.js @@ -6,11 +6,11 @@ import { WidgetPreviewContainer } from 'netlify-cms-ui-default'; const Image = styled.img` max-width: 100%; height: auto; -` +`; const ImagePreview = ({ value, getAsset }) => ( - { value ? : null} + {value ? : null} ); diff --git a/packages/netlify-cms-widget-list/src/ListControl.js b/packages/netlify-cms-widget-list/src/ListControl.js index 6d117fef..607c86e6 100644 --- a/packages/netlify-cms-widget-list/src/ListControl.js +++ b/packages/netlify-cms-widget-list/src/ListControl.js @@ -24,15 +24,15 @@ const SortableListItem = SortableElement(ListItem); const StyledListItemTopBar = styled(ListItemTopBar)` background-color: ${colors.textFieldBorder}; -` +`; const NestedObjectLabel = styled.div` - display: ${props => props.collapsed ? 'block' : 'none'}; + display: ${props => (props.collapsed ? 'block' : 'none')}; border-top: 0; background-color: ${colors.textFieldBorder}; padding: 13px; border-radius: 0 0 ${lengths.borderRadius} ${lengths.borderRadius}; -` +`; const styles = { collapsedObjectControl: css` @@ -101,7 +101,7 @@ export default class ListControl extends React.Component { } else { return null; } - } + }; /** * Always update so that each nested widget has the option to update. This is @@ -113,7 +113,7 @@ export default class ListControl extends React.Component { return true; } - handleChange = (e) => { + handleChange = e => { const { onChange } = this.props; const oldValue = this.state.value; const newValue = e.target.value; @@ -129,21 +129,24 @@ export default class ListControl extends React.Component { handleFocus = () => { this.props.setActiveStyle(); - } + }; - handleBlur = (e) => { - const listValue = e.target.value.split(',').map(el => el.trim()).filter(el => el); + handleBlur = e => { + const listValue = e.target.value + .split(',') + .map(el => el.trim()) + .filter(el => el); this.setState({ value: valueToString(listValue) }); this.props.setInactiveStyle(); - } + }; - handleAdd = (e) => { + handleAdd = e => { e.preventDefault(); const { value, onChange } = this.props; - const parsedValue = (this.getValueType() === valueTypes.SINGLE) ? null : Map(); + const parsedValue = this.getValueType() === valueTypes.SINGLE ? null : Map(); this.setState({ itemsCollapsed: this.state.itemsCollapsed.push(false) }); onChange((value || List()).push(parsedValue)); - } + }; /** * In case the `onChangeObject` function is frozen by a child widget implementation, @@ -157,9 +160,13 @@ export default class ListControl extends React.Component { const { value, metadata, onChange, field } = this.props; const collectionName = field.get('name'); const newObjectValue = this.getObjectValue(index).set(fieldName, newValue); - const parsedValue = (this.getValueType() === valueTypes.SINGLE) ? newObjectValue.first() : newObjectValue; + const parsedValue = + this.getValueType() === valueTypes.SINGLE ? newObjectValue.first() : newObjectValue; const parsedMetadata = { - [collectionName]: Object.assign(metadata ? metadata.toJS() : {}, newMetadata ? newMetadata[collectionName] : {}), + [collectionName]: Object.assign( + metadata ? metadata.toJS() : {}, + newMetadata ? newMetadata[collectionName] : {}, + ), }; onChange(value.set(index, parsedValue), parsedMetadata); }; @@ -170,7 +177,9 @@ export default class ListControl extends React.Component { const { itemsCollapsed } = this.state; const { value, metadata, onChange, field } = this.props; const collectionName = field.get('name'); - const parsedMetadata = metadata && { [collectionName]: metadata.removeIn(value.get(index).valueSeq()) }; + const parsedMetadata = metadata && { + [collectionName]: metadata.removeIn(value.get(index).valueSeq()), + }; this.setState({ itemsCollapsed: itemsCollapsed.delete(index) }); @@ -184,7 +193,7 @@ export default class ListControl extends React.Component { this.setState({ itemsCollapsed: itemsCollapsed.set(index, !collapsed) }); }; - handleCollapseAllToggle = (e) => { + handleCollapseAllToggle = e => { e.preventDefault(); const { value } = this.props; const { itemsCollapsed } = this.state; @@ -197,8 +206,10 @@ export default class ListControl extends React.Component { const multiFields = field.get('fields'); const singleField = field.get('field'); const labelField = (multiFields && multiFields.first()) || singleField; - const value = multiFields ? item.get(multiFields.first().get('name')) : singleField.get('label'); - return (value || `No ${ labelField.get('name') }`).toString(); + const value = multiFields + ? item.get(multiFields.first().get('name')) + : singleField.get('label'); + return (value || `No ${labelField.get('name')}`).toString(); } onSortEnd = ({ oldIndex, newIndex }) => { @@ -217,12 +228,7 @@ export default class ListControl extends React.Component { }; renderItem = (item, index) => { - const { - field, - classNameWrapper, - editorControl, - resolveWidget, - } = this.props; + const { field, classNameWrapper, editorControl, resolveWidget } = this.props; const { itemsCollapsed } = this.state; const collapsed = itemsCollapsed.get(index); @@ -230,7 +236,7 @@ export default class ListControl extends React.Component { {this.objectLabel(item)} ); + return ( + + ); } } diff --git a/packages/netlify-cms-widget-markdown/src/MarkdownControl/RawEditor.js b/packages/netlify-cms-widget-markdown/src/MarkdownControl/RawEditor.js index 7996448d..eb9099ca 100644 --- a/packages/netlify-cms-widget-markdown/src/MarkdownControl/RawEditor.js +++ b/packages/netlify-cms-widget-markdown/src/MarkdownControl/RawEditor.js @@ -99,5 +99,5 @@ RawEditor.propTypes = { onMode: PropTypes.func.isRequired, className: PropTypes.string.isRequired, value: PropTypes.string, - field: ImmutablePropTypes.map + field: ImmutablePropTypes.map, }; diff --git a/packages/netlify-cms-widget-markdown/src/MarkdownControl/Shortcode.js b/packages/netlify-cms-widget-markdown/src/MarkdownControl/Shortcode.js index 52b0d081..e38b4d53 100644 --- a/packages/netlify-cms-widget-markdown/src/MarkdownControl/Shortcode.js +++ b/packages/netlify-cms-widget-markdown/src/MarkdownControl/Shortcode.js @@ -12,22 +12,24 @@ const ShortcodeContainer = styled.div` margin: 12px 0; padding: 14px; - ${props => props.collapsed && css` - background-color: ${colors.textFieldBorder}; - cursor: pointer; - `} -` + ${props => + props.collapsed && + css` + background-color: ${colors.textFieldBorder}; + cursor: pointer; + `}; +`; const ShortcodeTopBar = styled(ListItemTopBar)` background-color: ${colors.textFieldBorder}; margin: -14px -14px 0; border-radius: 0; -` +`; const ShortcodeTitle = styled.div` padding: 8px; color: ${colors.controlLabel}; -` +`; export default class Shortcode extends React.Component { constructor(props) { @@ -51,16 +53,14 @@ export default class Shortcode extends React.Component { handleCollapseToggle = () => { this.setState({ collapsed: !this.state.collapsed }); - } + }; handleRemove = () => { const { editor, node } = this.props; editor.change(change => { - change - .removeNodeByKey(node.key) - .focus(); + change.removeNodeByKey(node.key).focus(); }); - } + }; handleClick = event => { /** @@ -75,18 +75,18 @@ export default class Shortcode extends React.Component { if (this.state.collapsed) { this.handleCollapseToggle(); } - } + }; renderControl = (shortcodeData, field) => { if (field.get('widget') === 'hidden') return null; const value = shortcodeData.get(field.get('name')); - const key = `field-${ field.get('name') }`; + const key = `field-${field.get('name')}`; const Control = getEditorControl(); const controlProps = { field, value, onChange: this.handleChange }; return (
    - +
    ); }; @@ -104,11 +104,11 @@ export default class Shortcode extends React.Component { onCollapseToggle={this.handleCollapseToggle} onRemove={this.handleRemove} /> - { - collapsed - ? {capitalize(pluginId)} - : plugin.get('fields').map(partial(this.renderControl, shortcodeData)) - } + {collapsed ? ( + {capitalize(pluginId)} + ) : ( + plugin.get('fields').map(partial(this.renderControl, shortcodeData)) + )} ); } diff --git a/packages/netlify-cms-widget-markdown/src/MarkdownControl/Toolbar.js b/packages/netlify-cms-widget-markdown/src/MarkdownControl/Toolbar.js index dc72913a..4549bf8a 100644 --- a/packages/netlify-cms-widget-markdown/src/MarkdownControl/Toolbar.js +++ b/packages/netlify-cms-widget-markdown/src/MarkdownControl/Toolbar.js @@ -27,7 +27,7 @@ const ToolbarContainer = styled.div` const ToolbarDropdownWrapper = styled.div` display: inline-block; position: relative; -` +`; const ToolbarToggle = styled.div` flex-shrink: 0; @@ -35,7 +35,7 @@ const ToolbarToggle = styled.div` align-items: center; font-size: 14px; margin: 0 10px; -` +`; const StyledToggle = ToolbarToggle.withComponent(Toggle); @@ -44,13 +44,15 @@ const ToolbarToggleLabel = styled.span` text-align: center; white-space: nowrap; line-height: 20px; - width: ${props => props.offPosition ? '62px' : '70px'}; + width: ${props => (props.offPosition ? '62px' : '70px')}; - ${props => props.isActive && css` - font-weight: 600; - color: ${colors.active}; - `} -` + ${props => + props.isActive && + css` + font-weight: 600; + color: ${colors.active}; + `}; +`; export default class Toolbar extends React.Component { static propTypes = { @@ -67,7 +69,7 @@ export default class Toolbar extends React.Component { isHidden = button => { const { buttons } = this.props; return List.isList(buttons) ? !buttons.includes(button) : false; - } + }; render() { const { @@ -191,15 +193,24 @@ export default class Toolbar extends React.Component { )} > - {plugins && plugins.toList().map((plugin, idx) => ( - onSubmit(plugin.get('id'))} /> - ))} + {plugins && + plugins + .toList() + .map((plugin, idx) => ( + onSubmit(plugin.get('id'))} + /> + ))}
    - Rich Text - + + Rich Text + + Markdown diff --git a/packages/netlify-cms-widget-markdown/src/MarkdownControl/ToolbarButton.js b/packages/netlify-cms-widget-markdown/src/MarkdownControl/ToolbarButton.js index 379d4c57..f6673072 100644 --- a/packages/netlify-cms-widget-markdown/src/MarkdownControl/ToolbarButton.js +++ b/packages/netlify-cms-widget-markdown/src/MarkdownControl/ToolbarButton.js @@ -10,7 +10,7 @@ const StyledToolbarButton = styled.button` border: none; background-color: transparent; font-size: 16px; - color: ${props => props.isActive ? '#1e2532' : 'inherit'}; + color: ${props => (props.isActive ? '#1e2532' : 'inherit')}; cursor: pointer; &:disabled { @@ -21,7 +21,7 @@ const StyledToolbarButton = styled.button` ${Icon} { display: block; } -` +`; const ToolbarButton = ({ type, label, icon, onClick, isActive, isHidden, disabled }) => { if (isHidden) { @@ -35,7 +35,7 @@ const ToolbarButton = ({ type, label, icon, onClick, isActive, isHidden, disable title={label} disabled={disabled} > - { icon ? : label } + {icon ? : label} ); }; diff --git a/packages/netlify-cms-widget-markdown/src/MarkdownControl/VisualEditor.js b/packages/netlify-cms-widget-markdown/src/MarkdownControl/VisualEditor.js index 5f2737fa..608d1b9a 100644 --- a/packages/netlify-cms-widget-markdown/src/MarkdownControl/VisualEditor.js +++ b/packages/netlify-cms-widget-markdown/src/MarkdownControl/VisualEditor.js @@ -17,20 +17,20 @@ import { EditorControlBar } from '../styles'; const VisualEditorContainer = styled.div` position: relative; -` +`; const createEmptyRawDoc = () => { const emptyText = Text.create(''); - const emptyBlock = Block.create({ kind: 'block', type: 'paragraph', nodes: [ emptyText ] }); + const emptyBlock = Block.create({ kind: 'block', type: 'paragraph', nodes: [emptyText] }); return { nodes: [emptyBlock] }; }; -const createSlateValue = (rawValue) => { +const createSlateValue = rawValue => { const rawDoc = rawValue && markdownToSlate(rawValue); - const rawDocHasNodes = !isEmpty(get(rawDoc, 'nodes')) + const rawDocHasNodes = !isEmpty(get(rawDoc, 'nodes')); const document = Document.fromJSON(rawDocHasNodes ? rawDoc : createEmptyRawDoc()); return Value.create({ document }); -} +}; export default class Editor extends React.Component { static propTypes = { @@ -40,7 +40,7 @@ export default class Editor extends React.Component { onMode: PropTypes.func.isRequired, className: PropTypes.string.isRequired, value: PropTypes.string, - field: ImmutablePropTypes.map + field: ImmutablePropTypes.map, }; constructor(props) { @@ -61,14 +61,17 @@ export default class Editor extends React.Component { const ast = htmlToSlate(data.html); const doc = Document.fromJSON(ast); return change.insertFragment(doc); - } + }; selectionHasMark = type => this.state.value.activeMarks.some(mark => mark.type === type); selectionHasBlock = type => this.state.value.blocks.some(node => node.type === type); handleMarkClick = (event, type) => { event.preventDefault(); - const resolvedChange = this.state.value.change().focus().toggleMark(type); + const resolvedChange = this.state.value + .change() + .focus() + .toggleMark(type); this.ref.onChange(resolvedChange); this.setState({ value: resolvedChange.value }); }; @@ -119,9 +122,7 @@ export default class Editor extends React.Component { // should simply unlink them. if (this.hasLinks()) { change = change.unwrapInline('link'); - } - - else { + } else { const url = window.prompt('Enter the URL of the link'); // If nothing is entered in the URL prompt, do nothing. @@ -129,14 +130,10 @@ export default class Editor extends React.Component { // If no text is selected, use the entered URL as text. if (change.value.isCollapsed) { - change = change - .insertText(url) - .extend(0 - url.length); + change = change.insertText(url).extend(0 - url.length); } - change = change - .wrapInline({ type: 'link', data: { url } }) - .collapseToEnd(); + change = change.wrapInline({ type: 'link', data: { url } }).collapseToEnd(); } this.ref.onChange(change); @@ -155,7 +152,7 @@ export default class Editor extends React.Component { shortcodeData: Map(), }, isVoid: true, - nodes + nodes, }; let change = value.change(); const { focusBlock } = change.value; @@ -176,7 +173,6 @@ export default class Editor extends React.Component { this.props.onMode('raw'); }; - handleDocumentChange = debounce(change => { const { onChange } = this.props; const raw = change.value.document.toJSON(); @@ -193,7 +189,7 @@ export default class Editor extends React.Component { processRef = ref => { this.ref = ref; - } + }; render() { const { onAddAsset, getAsset, className, field, getEditorComponents } = this.props; diff --git a/packages/netlify-cms-widget-markdown/src/MarkdownControl/__tests__/parser.spec.js b/packages/netlify-cms-widget-markdown/src/MarkdownControl/__tests__/parser.spec.js index 097afb46..4b7cf40a 100644 --- a/packages/netlify-cms-widget-markdown/src/MarkdownControl/__tests__/parser.spec.js +++ b/packages/netlify-cms-widget-markdown/src/MarkdownControl/__tests__/parser.spec.js @@ -2,8 +2,8 @@ import { markdownToSlate } from '../../serializers'; const parser = markdownToSlate; -describe("Compile markdown to Slate Raw AST", () => { - it("should compile simple markdown", () => { +describe('Compile markdown to Slate Raw AST', () => { + it('should compile simple markdown', () => { const value = ` # H1 @@ -12,7 +12,7 @@ sweet body expect(parser(value)).toMatchSnapshot(); }); - it("should compile a markdown ordered list", () => { + it('should compile a markdown ordered list', () => { const value = ` # H1 @@ -23,7 +23,7 @@ sweet body expect(parser(value)).toMatchSnapshot(); }); - it("should compile bulleted lists", () => { + it('should compile bulleted lists', () => { const value = ` # H1 @@ -34,7 +34,7 @@ sweet body expect(parser(value)).toMatchSnapshot(); }); - it("should compile multiple header levels", () => { + it('should compile multiple header levels', () => { const value = ` # H1 @@ -45,7 +45,7 @@ sweet body expect(parser(value)).toMatchSnapshot(); }); - it("should compile horizontal rules", () => { + it('should compile horizontal rules', () => { const value = ` # H1 @@ -56,7 +56,7 @@ blue moon expect(parser(value)).toMatchSnapshot(); }); - it("should compile horizontal rules", () => { + it('should compile horizontal rules', () => { const value = ` # H1 @@ -67,7 +67,7 @@ blue moon expect(parser(value)).toMatchSnapshot(); }); - it("should compile soft breaks (double space)", () => { + it('should compile soft breaks (double space)', () => { const value = ` blue moon footballs @@ -75,14 +75,14 @@ footballs expect(parser(value)).toMatchSnapshot(); }); - it("should compile images", () => { + it('should compile images', () => { const value = ` ![super](duper.jpg) `; expect(parser(value)).toMatchSnapshot(); }); - it("should compile code blocks", () => { + it('should compile code blocks', () => { const value = ` \`\`\`javascript var a = 1; @@ -91,7 +91,7 @@ var a = 1; expect(parser(value)).toMatchSnapshot(); }); - it("should compile nested inline markup", () => { + it('should compile nested inline markup', () => { const value = ` # Word @@ -102,7 +102,7 @@ perhaps **scalding** even expect(parser(value)).toMatchSnapshot(); }); - it("should compile inline code", () => { + it('should compile inline code', () => { const value = ` # Word @@ -111,7 +111,7 @@ This is some sweet \`inline code\` yo! expect(parser(value)).toMatchSnapshot(); }); - it("should compile links", () => { + it('should compile links', () => { const value = ` # Word @@ -120,7 +120,7 @@ How far is it to [Google](https://google.com) land? expect(parser(value)).toMatchSnapshot(); }); - it("should compile plugins", () => { + it('should compile plugins', () => { const value = ` ![test](test.png) @@ -129,7 +129,7 @@ How far is it to [Google](https://google.com) land? expect(parser(value)).toMatchSnapshot(); }); - it("should compile kitchen sink example", () => { + it('should compile kitchen sink example', () => { const value = ` # An exhibit of Markdown diff --git a/packages/netlify-cms-widget-markdown/src/MarkdownControl/index.js b/packages/netlify-cms-widget-markdown/src/MarkdownControl/index.js index 27e99ddb..f4d0e27a 100644 --- a/packages/netlify-cms-widget-markdown/src/MarkdownControl/index.js +++ b/packages/netlify-cms-widget-markdown/src/MarkdownControl/index.js @@ -32,12 +32,12 @@ export default class MarkdownControl extends React.Component { this.state = { mode: localStorage.getItem(MODE_STORAGE_KEY) || 'visual' }; } - handleMode = (mode) => { + handleMode = mode => { this.setState({ mode }); localStorage.setItem(MODE_STORAGE_KEY, mode); }; - processRef = ref => this.ref = ref; + processRef = ref => (this.ref = ref); render() { const { diff --git a/packages/netlify-cms-widget-markdown/src/MarkdownControl/keys.js b/packages/netlify-cms-widget-markdown/src/MarkdownControl/keys.js index 305f5d3c..d1494f39 100644 --- a/packages/netlify-cms-widget-markdown/src/MarkdownControl/keys.js +++ b/packages/netlify-cms-widget-markdown/src/MarkdownControl/keys.js @@ -38,14 +38,9 @@ function onKeyDown(event, change) { .collapseToStartOf(newBlock); } - const marks = [ - [ 'b', 'bold' ], - [ 'i', 'italic' ], - [ 's', 'strikethrough' ], - [ '`', 'code' ], - ]; + const marks = [['b', 'bold'], ['i', 'italic'], ['s', 'strikethrough'], ['`', 'code']]; - const [ , markName ] = marks.find(([ key ]) => isHotkey(`mod+${key}`, event)) || []; + const [, markName] = marks.find(([key]) => isHotkey(`mod+${key}`, event)) || []; if (markName) { event.preventDefault(); diff --git a/packages/netlify-cms-widget-markdown/src/MarkdownControl/plugins.js b/packages/netlify-cms-widget-markdown/src/MarkdownControl/plugins.js index 3f2f6cf7..ecc24fd3 100644 --- a/packages/netlify-cms-widget-markdown/src/MarkdownControl/plugins.js +++ b/packages/netlify-cms-widget-markdown/src/MarkdownControl/plugins.js @@ -15,18 +15,16 @@ const SoftBreak = (options = {}) => ({ const shouldClose = text.endsWith('\n'); if (shouldClose) { - return change - .deleteBackward(1) - .insertBlock(defaultBlock); + return change.deleteBackward(1).insertBlock(defaultBlock); } const textNode = Text.create('\n'); - const breakNode = Inline.create({ type: 'break', nodes: [ textNode ] }); + const breakNode = Inline.create({ type: 'break', nodes: [textNode] }); return change .insertInline(breakNode) .insertText('') .collapseToStartOfNextText(); - } + }, }); const SoftBreakOpts = { @@ -44,11 +42,18 @@ const BreakToDefaultBlock = ({ onlyIn = [], defaultBlock = 'paragraph' }) => ({ if (onlyIn.includes(value.startBlock.type)) { return change.insertBlock(defaultBlock); } - } + }, }); const BreakToDefaultBlockOpts = { - onlyIn: ['heading-one', 'heading-two', 'heading-three', 'heading-four', 'heading-five', 'heading-six'], + onlyIn: [ + 'heading-one', + 'heading-two', + 'heading-three', + 'heading-four', + 'heading-five', + 'heading-six', + ], }; export const BreakToDefaultBlockConfigured = BreakToDefaultBlock(BreakToDefaultBlockOpts); @@ -67,7 +72,7 @@ const BackspaceCloseBlock = (options = {}) => ({ if (startBlock.text === '') { return change.setBlock(defaultBlock).focus(); } - } + }, }); const BackspaceCloseBlockOpts = { diff --git a/packages/netlify-cms-widget-markdown/src/MarkdownControl/renderers.js b/packages/netlify-cms-widget-markdown/src/MarkdownControl/renderers.js index aa2bd364..e2410368 100644 --- a/packages/netlify-cms-widget-markdown/src/MarkdownControl/renderers.js +++ b/packages/netlify-cms-widget-markdown/src/MarkdownControl/renderers.js @@ -21,30 +21,46 @@ const Code = props => {props.children}; const Paragraph = props =>

    {props.children}

    ; const ListItem = props =>
  • {props.children}
  • ; const Quote = props =>
    {props.children}
    ; -const CodeBlock = props =>
    {props.children}
    ; +const CodeBlock = props => ( +
    +    {props.children}
    +  
    +); const HeadingOne = props =>

    {props.children}

    ; const HeadingTwo = props =>

    {props.children}

    ; const HeadingThree = props =>

    {props.children}

    ; const HeadingFour = props =>

    {props.children}

    ; const HeadingFive = props =>
    {props.children}
    ; const HeadingSix = props =>
    {props.children}
    ; -const Table = props => {props.children}
    ; +const Table = props => ( + + {props.children} +
    +); const TableRow = props => {props.children}; const TableCell = props => {props.children}; -const ThematicBreak = props =>
    ; +const ThematicBreak = props =>
    ; const BulletedList = props =>
      {props.children}
    ; const NumberedList = props => ( -
      {props.children}
    +
      + {props.children} +
    ); const Link = props => { const data = props.node.get('data'); const marks = data.get('marks'); const url = data.get('url'); const title = data.get('title'); - const link = {props.children}; - const result = !marks ? link : marks.reduce((acc, mark) => { - return renderMark({ mark, children: acc }); - }, link); + const link = ( + + {props.children} + + ); + const result = !marks + ? link + : marks.reduce((acc, mark) => { + return renderMark({ mark, children: acc }); + }, link); return result; }; const Image = props => { @@ -53,42 +69,67 @@ const Image = props => { const url = data.get('url'); const title = data.get('title'); const alt = data.get('alt'); - const image = {alt}; - const result = !marks ? image : marks.reduce((acc, mark) => { - return renderMark({ mark, children: acc }); - }, image); + const image = {alt}; + const result = !marks + ? image + : marks.reduce((acc, mark) => { + return renderMark({ mark, children: acc }); + }, image); return result; }; export const renderMark = props => { switch (props.mark.type) { - case 'bold': return ; - case 'italic': return ; - case 'strikethrough': return ; - case 'code': return ; + case 'bold': + return ; + case 'italic': + return ; + case 'strikethrough': + return ; + case 'code': + return ; } }; export const renderNode = props => { switch (props.node.type) { - case 'paragraph': return ; - case 'list-item': return ; - case 'quote': return ; - case 'code': return ; - case 'heading-one': return ; - case 'heading-two': return ; - case 'heading-three': return ; - case 'heading-four': return ; - case 'heading-five': return ; - case 'heading-six': return ; - case 'table': return ; - case 'table-row': return ; - case 'table-cell': return ; - case 'thematic-break': return ; - case 'bulleted-list': return ; - case 'numbered-list': return ; - case 'link': return ; - case 'image': return ; - case 'shortcode': return ; + case 'paragraph': + return ; + case 'list-item': + return ; + case 'quote': + return ; + case 'code': + return ; + case 'heading-one': + return ; + case 'heading-two': + return ; + case 'heading-three': + return ; + case 'heading-four': + return ; + case 'heading-five': + return ; + case 'heading-six': + return ; + case 'table': + return
    ; + case 'table-row': + return ; + case 'table-cell': + return ; + case 'thematic-break': + return ; + case 'bulleted-list': + return ; + case 'numbered-list': + return ; + case 'link': + return ; + case 'image': + return ; + case 'shortcode': + return ; } }; diff --git a/packages/netlify-cms-widget-markdown/src/MarkdownControl/validators.js b/packages/netlify-cms-widget-markdown/src/MarkdownControl/validators.js index 629dc3f0..ece167c9 100644 --- a/packages/netlify-cms-widget-markdown/src/MarkdownControl/validators.js +++ b/packages/netlify-cms-widget-markdown/src/MarkdownControl/validators.js @@ -66,24 +66,24 @@ export function validateNode(node) { if (trailingShortcode) { return change => { const text = Text.create(''); - const block = Block.create({ type: 'paragraph', nodes: [ text ] }); + const block = Block.create({ type: 'paragraph', nodes: [text] }); return change.insertNodeByKey(doc.key, doc.get('nodes').size, block); }; } } - /** * Ensure that code blocks contain no marks. */ if (node.type === 'code') { const invalidChild = node.getTexts().find(text => !text.getMarks().isEmpty()); if (invalidChild) { - return change => ( - invalidChild.getMarks().forEach(mark => ( - change.removeMarkByKey(invalidChild.key, 0, invalidChild.get('characters').size, mark) - )) - ); + return change => + invalidChild + .getMarks() + .forEach(mark => + change.removeMarkByKey(invalidChild.key, 0, invalidChild.get('characters').size, mark), + ); } } } diff --git a/packages/netlify-cms-widget-markdown/src/MarkdownControl/visualEditorStyles.js b/packages/netlify-cms-widget-markdown/src/MarkdownControl/visualEditorStyles.js index 69a85f68..0e736073 100644 --- a/packages/netlify-cms-widget-markdown/src/MarkdownControl/visualEditorStyles.js +++ b/packages/netlify-cms-widget-markdown/src/MarkdownControl/visualEditorStyles.js @@ -39,12 +39,21 @@ export default css` margin-top: 8px; } - h1, h2, h3, h4, h5, h6 { + h1, + h2, + h3, + h4, + h5, + h6 { font-weight: 700; line-height: 1; } - p, pre, blockquote, ul, ol { + p, + pre, + blockquote, + ul, + ol { margin-top: 16px; margin-bottom: 16px; } @@ -62,7 +71,8 @@ export default css` margin: 0; } - ul, ol { + ul, + ol { padding-left: 30px; } @@ -98,7 +108,8 @@ export default css` border-collapse: collapse; } - td, th { + td, + th { border: 2px solid black; padding: 8px; text-align: left; diff --git a/packages/netlify-cms-widget-markdown/src/MarkdownPreview.js b/packages/netlify-cms-widget-markdown/src/MarkdownPreview.js index 31e81126..f3818746 100644 --- a/packages/netlify-cms-widget-markdown/src/MarkdownPreview.js +++ b/packages/netlify-cms-widget-markdown/src/MarkdownPreview.js @@ -8,7 +8,7 @@ const MarkdownPreview = ({ value, getAsset }) => { return null; } const html = markdownToHtml(value, getAsset); - return + return ; }; MarkdownPreview.propTypes = { diff --git a/packages/netlify-cms-widget-markdown/src/__tests__/renderer.spec.js b/packages/netlify-cms-widget-markdown/src/__tests__/renderer.spec.js index dce899f0..65b75b50 100644 --- a/packages/netlify-cms-widget-markdown/src/__tests__/renderer.spec.js +++ b/packages/netlify-cms-widget-markdown/src/__tests__/renderer.spec.js @@ -35,18 +35,18 @@ Text with **bold** & _em_ elements ###### H6 `; expect( - renderer.create().toJSON() - ).toMatchSnapshot() + renderer.create().toJSON(), + ).toMatchSnapshot(); }); }); describe('Headings', () => { for (const heading of [...Array(6).keys()]) { - it(`should render Heading ${ heading + 1 }`, () => { + it(`should render Heading ${heading + 1}`, () => { const value = padStart(' Title', heading + 7, '#'); expect( - renderer.create().toJSON() - ).toMatchSnapshot() + renderer.create().toJSON(), + ).toMatchSnapshot(); }); } }); @@ -65,8 +65,8 @@ Text with **bold** & _em_ elements 1. ol item 3 `; expect( - renderer.create().toJSON() - ).toMatchSnapshot() + renderer.create().toJSON(), + ).toMatchSnapshot(); }); }); @@ -80,8 +80,8 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] [3]: http://search.msn.com/ "MSN Search" `; expect( - renderer.create().toJSON() - ).toMatchSnapshot() + renderer.create().toJSON(), + ).toMatchSnapshot(); }); }); @@ -89,15 +89,15 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] it('should render code', () => { const value = 'Use the `printf()` function.'; expect( - renderer.create().toJSON() - ).toMatchSnapshot() + renderer.create().toJSON(), + ).toMatchSnapshot(); }); it('should render code 2', () => { const value = '``There is a literal backtick (`) here.``'; expect( - renderer.create().toJSON() - ).toMatchSnapshot() + renderer.create().toJSON(), + ).toMatchSnapshot(); }); }); @@ -119,8 +119,8 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3]

    Test

    `; expect( - renderer.create().toJSON() - ).toMatchSnapshot() + renderer.create().toJSON(), + ).toMatchSnapshot(); }); }); }); @@ -129,8 +129,8 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3] it('should render HTML', () => { const value = '

    Paragraph with inline element

    '; expect( - renderer.create().toJSON() - ).toMatchSnapshot() + renderer.create().toJSON(), + ).toMatchSnapshot(); }); }); }); diff --git a/packages/netlify-cms-widget-markdown/src/index.js b/packages/netlify-cms-widget-markdown/src/index.js index 3e51a532..df1c2ad9 100644 --- a/packages/netlify-cms-widget-markdown/src/index.js +++ b/packages/netlify-cms-widget-markdown/src/index.js @@ -1,2 +1,2 @@ -export MarkdownControl from './MarkdownControl' -export MarkdownPreview from './MarkdownPreview' +export MarkdownControl from './MarkdownControl'; +export MarkdownPreview from './MarkdownPreview'; diff --git a/packages/netlify-cms-widget-markdown/src/regexHelper.js b/packages/netlify-cms-widget-markdown/src/regexHelper.js index febded3d..50dd1ec7 100644 --- a/packages/netlify-cms-widget-markdown/src/regexHelper.js +++ b/packages/netlify-cms-widget-markdown/src/regexHelper.js @@ -8,7 +8,6 @@ export function joinPatternSegments(patterns) { return patterns.map(p => p.source).join(''); } - /** * Combines an array of regular expressions into a single expression, wrapping * each in a non-capturing group and interposing alternation characters (|) so @@ -18,7 +17,6 @@ export function combinePatterns(patterns) { return patterns.map(p => `(?:${p.source})`).join('|'); } - /** * Modify substrings within a string if they match a (global) pattern. Can be * inverted to only modify non-matches. @@ -63,34 +61,28 @@ export function replaceWhen(matchPattern, replaceFn, text, invertMatchPattern) { */ if (match.index === 0) { addSubstring(acc, 0, match[0], true); - } - - /** - * If there are no entries in the accumulator, convert the substring before - * the match to a data object (without the `match` flag set to true) and - * push to the accumulator, followed by a data object for the matching - * substring. - */ - else if (!lastEntry) { + } else if (!lastEntry) { + /** + * If there are no entries in the accumulator, convert the substring before + * the match to a data object (without the `match` flag set to true) and + * push to the accumulator, followed by a data object for the matching + * substring. + */ addSubstring(acc, 0, match.input.slice(0, match.index)); addSubstring(acc, match.index, match[0], true); - } - - /** - * If the last entry in the accumulator immediately preceded the current - * matched substring in the original string, just add the data object for - * the matching substring to the accumulator. - */ - else if (match.index === lastEntry.index + lastEntry.text.length) { + } else if (match.index === lastEntry.index + lastEntry.text.length) { + /** + * If the last entry in the accumulator immediately preceded the current + * matched substring in the original string, just add the data object for + * the matching substring to the accumulator. + */ addSubstring(acc, match.index, match[0], true); - } - - /** - * Convert the substring before the match to a data object (without the - * `match` flag set to true), followed by a data object for the matching - * substring. - */ - else { + } else { + /** + * Convert the substring before the match to a data object (without the + * `match` flag set to true), followed by a data object for the matching + * substring. + */ const nextIndex = lastEntry.index + lastEntry.text.length; const nextText = match.input.slice(nextIndex, match.index); addSubstring(acc, nextIndex, nextText); diff --git a/packages/netlify-cms-widget-markdown/src/serializers/__tests__/remarkAllowHtmlEntities.spec.js b/packages/netlify-cms-widget-markdown/src/serializers/__tests__/remarkAllowHtmlEntities.spec.js index a7eed4c4..18c6722f 100644 --- a/packages/netlify-cms-widget-markdown/src/serializers/__tests__/remarkAllowHtmlEntities.spec.js +++ b/packages/netlify-cms-widget-markdown/src/serializers/__tests__/remarkAllowHtmlEntities.spec.js @@ -3,7 +3,10 @@ import markdownToRemark from 'remark-parse'; import remarkAllowHtmlEntities from '../remarkAllowHtmlEntities'; const process = markdown => { - const mdast = unified().use(markdownToRemark).use(remarkAllowHtmlEntities).parse(markdown); + const mdast = unified() + .use(markdownToRemark) + .use(remarkAllowHtmlEntities) + .parse(markdown); /** * The MDAST will look like: diff --git a/packages/netlify-cms-widget-markdown/src/serializers/__tests__/remarkAssertParents.spec.js b/packages/netlify-cms-widget-markdown/src/serializers/__tests__/remarkAssertParents.spec.js index afccd2ed..3ab404dd 100644 --- a/packages/netlify-cms-widget-markdown/src/serializers/__tests__/remarkAssertParents.spec.js +++ b/packages/netlify-cms-widget-markdown/src/serializers/__tests__/remarkAssertParents.spec.js @@ -7,23 +7,23 @@ describe('remarkAssertParents', () => { it('should unnest invalidly nested blocks', () => { const input = u('root', [ u('paragraph', [ - u('paragraph', [ u('text', 'Paragraph text.') ]), - u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]), + u('paragraph', [u('text', 'Paragraph text.')]), + u('heading', { depth: 1 }, [u('text', 'Heading text.')]), u('code', 'someCode()'), - u('blockquote', [ u('text', 'Quote text.') ]), - u('list', [ u('listItem', [ u('text', 'A list item.') ]) ]), - u('table', [ u('tableRow', [ u('tableCell', [ u('text', 'Text in a table cell.') ]) ]) ]), + u('blockquote', [u('text', 'Quote text.')]), + u('list', [u('listItem', [u('text', 'A list item.')])]), + u('table', [u('tableRow', [u('tableCell', [u('text', 'Text in a table cell.')])])]), u('thematicBreak'), ]), ]); const output = u('root', [ - u('paragraph', [ u('text', 'Paragraph text.') ]), - u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]), + u('paragraph', [u('text', 'Paragraph text.')]), + u('heading', { depth: 1 }, [u('text', 'Heading text.')]), u('code', 'someCode()'), - u('blockquote', [ u('text', 'Quote text.') ]), - u('list', [ u('listItem', [ u('text', 'A list item.') ]) ]), - u('table', [ u('tableRow', [ u('tableCell', [ u('text', 'Text in a table cell.') ]) ]) ]), + u('blockquote', [u('text', 'Quote text.')]), + u('list', [u('listItem', [u('text', 'A list item.')])]), + u('table', [u('tableRow', [u('tableCell', [u('text', 'Text in a table cell.')])])]), u('thematicBreak'), ]); @@ -35,20 +35,14 @@ describe('remarkAssertParents', () => { u('paragraph', [ u('paragraph', [ u('paragraph', [ - u('paragraph', [ u('text', 'Paragraph text.') ]), - u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]), + u('paragraph', [u('text', 'Paragraph text.')]), + u('heading', { depth: 1 }, [u('text', 'Heading text.')]), u('code', 'someCode()'), u('blockquote', [ - u('paragraph', [ - u('strong', [ - u('heading', [ - u('text', 'Quote text.'), - ]), - ]), - ]), + u('paragraph', [u('strong', [u('heading', [u('text', 'Quote text.')])])]), ]), - u('list', [ u('listItem', [ u('text', 'A list item.') ]) ]), - u('table', [ u('tableRow', [ u('tableCell', [ u('text', 'Text in a table cell.') ]) ]) ]), + u('list', [u('listItem', [u('text', 'A list item.')])]), + u('table', [u('tableRow', [u('tableCell', [u('text', 'Text in a table cell.')])])]), u('thematicBreak'), ]), ]), @@ -56,16 +50,12 @@ describe('remarkAssertParents', () => { ]); const output = u('root', [ - u('paragraph', [ u('text', 'Paragraph text.') ]), - u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]), + u('paragraph', [u('text', 'Paragraph text.')]), + u('heading', { depth: 1 }, [u('text', 'Heading text.')]), u('code', 'someCode()'), - u('blockquote', [ - u('heading', [ - u('text', 'Quote text.'), - ]), - ]), - u('list', [ u('listItem', [ u('text', 'A list item.') ]) ]), - u('table', [ u('tableRow', [ u('tableCell', [ u('text', 'Text in a table cell.') ]) ]) ]), + u('blockquote', [u('heading', [u('text', 'Quote text.')])]), + u('list', [u('listItem', [u('text', 'A list item.')])]), + u('table', [u('tableRow', [u('tableCell', [u('text', 'Text in a table cell.')])])]), u('thematicBreak'), ]); @@ -74,42 +64,30 @@ describe('remarkAssertParents', () => { it('should remove blocks that are emptied as a result of denesting', () => { const input = u('root', [ - u('paragraph', [ - u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]), - ]), + u('paragraph', [u('heading', { depth: 1 }, [u('text', 'Heading text.')])]), ]); - const output = u('root', [ - u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]), - ]); + const output = u('root', [u('heading', { depth: 1 }, [u('text', 'Heading text.')])]); expect(transform(input)).toEqual(output); }); it('should remove blocks that are emptied as a result of denesting', () => { const input = u('root', [ - u('paragraph', [ - u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]), - ]), + u('paragraph', [u('heading', { depth: 1 }, [u('text', 'Heading text.')])]), ]); - const output = u('root', [ - u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]), - ]); + const output = u('root', [u('heading', { depth: 1 }, [u('text', 'Heading text.')])]); expect(transform(input)).toEqual(output); }); it('should handle assymetrical splits', () => { const input = u('root', [ - u('paragraph', [ - u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]), - ]), + u('paragraph', [u('heading', { depth: 1 }, [u('text', 'Heading text.')])]), ]); - const output = u('root', [ - u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]), - ]); + const output = u('root', [u('heading', { depth: 1 }, [u('text', 'Heading text.')])]); expect(transform(input)).toEqual(output); }); @@ -117,18 +95,12 @@ describe('remarkAssertParents', () => { it('should nest invalidly nested blocks in the nearest valid ancestor', () => { const input = u('root', [ u('paragraph', [ - u('blockquote', [ - u('strong', [ - u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]), - ]), - ]), + u('blockquote', [u('strong', [u('heading', { depth: 1 }, [u('text', 'Heading text.')])])]), ]), ]); const output = u('root', [ - u('blockquote', [ - u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]), - ]), + u('blockquote', [u('heading', { depth: 1 }, [u('text', 'Heading text.')])]), ]); expect(transform(input)).toEqual(output); @@ -140,7 +112,7 @@ describe('remarkAssertParents', () => { u('blockquote', [ u('strong', [ u('text', 'Deep validly nested text a.'), - u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]), + u('heading', { depth: 1 }, [u('text', 'Heading text.')]), u('text', 'Deep validly nested text b.'), ]), ]), @@ -150,17 +122,11 @@ describe('remarkAssertParents', () => { const output = u('root', [ u('blockquote', [ - u('strong', [ - u('text', 'Deep validly nested text a.'), - ]), - u('heading', { depth: 1 }, [ u('text', 'Heading text.') ]), - u('strong', [ - u('text', 'Deep validly nested text b.'), - ]), - ]), - u('paragraph', [ - u('text', 'Validly nested text.'), + u('strong', [u('text', 'Deep validly nested text a.')]), + u('heading', { depth: 1 }, [u('text', 'Heading text.')]), + u('strong', [u('text', 'Deep validly nested text b.')]), ]), + u('paragraph', [u('text', 'Validly nested text.')]), ]); expect(transform(input)).toEqual(output); @@ -174,7 +140,7 @@ describe('remarkAssertParents', () => { u('table', [ u('tableRow', [ u('tableCell', [ - u('heading', { depth: 1 }, [ u('text', 'Validly nested heading text.') ]), + u('heading', { depth: 1 }, [u('text', 'Validly nested heading text.')]), ]), ]), ]), @@ -190,7 +156,7 @@ describe('remarkAssertParents', () => { u('table', [ u('tableRow', [ u('tableCell', [ - u('heading', { depth: 1 }, [ u('text', 'Validly nested heading text.') ]), + u('heading', { depth: 1 }, [u('text', 'Validly nested heading text.')]), ]), ]), ]), diff --git a/packages/netlify-cms-widget-markdown/src/serializers/__tests__/remarkEscapeMarkdownEntities.spec.js b/packages/netlify-cms-widget-markdown/src/serializers/__tests__/remarkEscapeMarkdownEntities.spec.js index 09af85db..86372ea4 100644 --- a/packages/netlify-cms-widget-markdown/src/serializers/__tests__/remarkEscapeMarkdownEntities.spec.js +++ b/packages/netlify-cms-widget-markdown/src/serializers/__tests__/remarkEscapeMarkdownEntities.spec.js @@ -3,7 +3,7 @@ import u from 'unist-builder'; import remarkEscapeMarkdownEntities from '../remarkEscapeMarkdownEntities'; const process = text => { - const tree = u('root', [ u('text', text) ]); + const tree = u('root', [u('text', text)]); const escapedMdast = unified() .use(remarkEscapeMarkdownEntities) .runSync(tree); @@ -22,8 +22,9 @@ describe('remarkEscapeMarkdownEntities', () => { expect(process('[]')).toEqual('\\[]'); expect(process('[]()')).toEqual('\\[]()'); expect(process('[a](b)')).toEqual('\\[a](b)'); - expect(process('[Test sentence.](https://www.example.com)')) - .toEqual('\\[Test sentence.](https://www.example.com)'); + expect(process('[Test sentence.](https://www.example.com)')).toEqual( + '\\[Test sentence.](https://www.example.com)', + ); expect(process('![a](b)')).toEqual('!\\[a](b)'); }); diff --git a/packages/netlify-cms-widget-markdown/src/serializers/__tests__/remarkPaddedLinks.spec.js b/packages/netlify-cms-widget-markdown/src/serializers/__tests__/remarkPaddedLinks.spec.js index 40471ec1..1ec68d16 100644 --- a/packages/netlify-cms-widget-markdown/src/serializers/__tests__/remarkPaddedLinks.spec.js +++ b/packages/netlify-cms-widget-markdown/src/serializers/__tests__/remarkPaddedLinks.spec.js @@ -8,15 +8,13 @@ const input = markdown => .use(markdownToRemark) .use(remarkPaddedLinks) .use(remarkToMarkdown) - .processSync(markdown) - .contents; + .processSync(markdown).contents; const output = markdown => unified() .use(markdownToRemark) .use(remarkToMarkdown) - .processSync(markdown) - .contents; + .processSync(markdown).contents; describe('remarkPaddedLinks', () => { it('should move leading and trailing spaces outside of a link', () => { diff --git a/packages/netlify-cms-widget-markdown/src/serializers/index.js b/packages/netlify-cms-widget-markdown/src/serializers/index.js index a0b42403..d01dd72c 100644 --- a/packages/netlify-cms-widget-markdown/src/serializers/index.js +++ b/packages/netlify-cms-widget-markdown/src/serializers/index.js @@ -56,7 +56,6 @@ import { getEditorComponents } from '../MarkdownControl'; * for serialization to/from Slate's Raw AST and MDAST. */ - /** * Deserialize a Markdown string to an MDAST. */ @@ -82,17 +81,16 @@ export const markdownToRemark = markdown => { return result; }; - /** * Remove named tokenizers from the parser, effectively deactivating them. */ function markdownToRemarkRemoveTokenizers({ inlineTokenizers }) { - inlineTokenizers && inlineTokenizers.forEach(tokenizer => { - delete this.Parser.prototype.inlineTokenizers[tokenizer]; - }); + inlineTokenizers && + inlineTokenizers.forEach(tokenizer => { + delete this.Parser.prototype.inlineTokenizers[tokenizer]; + }); } - /** * Serialize an MDAST to a Markdown string. */ @@ -146,7 +144,6 @@ export const remarkToMarkdown = obj => { return trimEnd(markdown); }; - /** * Convert Markdown to HTML. */ @@ -163,8 +160,7 @@ export const markdownToHtml = (markdown, getAsset) => { .stringify(hast); return html; -} - +}; /** * Deserialize an HTML string to Slate's Raw AST. Currently used for HTML @@ -192,7 +188,6 @@ export const htmlToSlate = html => { return slateRaw; }; - /** * Convert Markdown to Slate's Raw AST. */ @@ -207,7 +202,6 @@ export const markdownToSlate = markdown => { return slateRaw; }; - /** * Convert a Slate Raw AST to Markdown. * diff --git a/packages/netlify-cms-widget-markdown/src/serializers/remarkEscapeMarkdownEntities.js b/packages/netlify-cms-widget-markdown/src/serializers/remarkEscapeMarkdownEntities.js index 4e066ea5..7763b770 100644 --- a/packages/netlify-cms-widget-markdown/src/serializers/remarkEscapeMarkdownEntities.js +++ b/packages/netlify-cms-widget-markdown/src/serializers/remarkEscapeMarkdownEntities.js @@ -13,7 +13,6 @@ const patternSegments = { htmlOpeningTagEnd: /(?: *\w+=(?:(?:"[^"]*")|(?:'[^']*')))* *>/, }; - /** * Patterns matching substrings that should not be escaped. Array values must be * joined before use. @@ -37,7 +36,6 @@ const nonEscapePatterns = { patternSegments.htmlOpeningTagEnd, ], - /** * Preformatted HTML Blocks * @@ -72,7 +70,6 @@ const nonEscapePatterns = { ], }; - /** * Escape patterns * @@ -137,7 +134,6 @@ const escapePatterns = [ /(\[)[^\]]*]/g, ]; - /** * Generate new non-escape expression. The non-escape expression matches * substrings whose contents should not be processed for escaping. @@ -147,14 +143,12 @@ const joinedNonEscapePatterns = map(nonEscapePatterns, pattern => { }); const nonEscapePattern = combinePatterns(joinedNonEscapePatterns); - /** * Create chain of successive escape functions for various markdown entities. */ const escapeFunctions = escapePatterns.map(pattern => partial(escapeDelimiters, pattern)); const escapeAll = flow(escapeFunctions); - /** * Executes both the `escapeCommonChars` and `escapeLeadingChars` functions. */ @@ -163,7 +157,6 @@ function escapeAllChars(text) { return escapeLeadingChars(partiallyEscapedMarkdown); } - /** * escapeLeadingChars * @@ -178,7 +171,6 @@ function escapeLeadingChars(text) { return text.replace(/^\s*([-#*>=|]| {4,}|`{3,})/, '$`\\$1'); } - /** * escapeCommonChars * @@ -199,7 +191,6 @@ function escapeCommonChars(text) { return replaceWhen(nonEscapeExpression, escapeAll, text, true); } - /** * escapeDelimiters * @@ -216,7 +207,6 @@ function escapeDelimiters(pattern, text) { }); } - /** * escape * @@ -231,7 +221,6 @@ function escape(delim) { return result; } - /** * A Remark plugin for escaping markdown entities. * @@ -261,7 +250,6 @@ export default function remarkEscapeMarkdownEntities() { * text in html nodes to keep Remark from escaping html entities. */ if (['text', 'html'].includes(node.type)) { - /** * Escape all characters if this is the first child node, otherwise only * common characters. @@ -273,7 +261,7 @@ export default function remarkEscapeMarkdownEntities() { /** * Always return nodes with recursively mapped children. */ - return {...node, children }; + return { ...node, children }; }; return transform; diff --git a/packages/netlify-cms-widget-markdown/src/serializers/remarkImagesToText.js b/packages/netlify-cms-widget-markdown/src/serializers/remarkImagesToText.js index 568e8a06..3e36cd2d 100644 --- a/packages/netlify-cms-widget-markdown/src/serializers/remarkImagesToText.js +++ b/packages/netlify-cms-widget-markdown/src/serializers/remarkImagesToText.js @@ -11,9 +11,9 @@ export default function remarkImagesToText() { function transform(node) { const children = node.children.map(child => { if ( - child.type === 'paragraph' - && child.children.length === 1 - && child.children[0].type === 'image' + child.type === 'paragraph' && + child.children.length === 1 && + child.children[0].type === 'image' ) { const { alt = '', url = '', title = '' } = child.children[0]; const value = `![${alt}](${url}${title ? ' title' : ''})`; diff --git a/packages/netlify-cms-widget-markdown/src/serializers/remarkPaddedLinks.js b/packages/netlify-cms-widget-markdown/src/serializers/remarkPaddedLinks.js index 7bc789e2..96ac4a5d 100644 --- a/packages/netlify-cms-widget-markdown/src/serializers/remarkPaddedLinks.js +++ b/packages/netlify-cms-widget-markdown/src/serializers/remarkPaddedLinks.js @@ -1,12 +1,4 @@ -import { - find, - findLast, - startsWith, - endsWith, - trimStart, - trimEnd, - flatMap -} from 'lodash'; +import { find, findLast, startsWith, endsWith, trimStart, trimEnd, flatMap } from 'lodash'; import u from 'unist-builder'; import toString from 'mdast-util-to-string'; @@ -20,9 +12,7 @@ import toString from 'mdast-util-to-string'; * children one at a time. */ export default function remarkPaddedLinks() { - function transform(node) { - /** * Because we're operating on link nodes and their children at once, we can * exit if the current node has no children. @@ -40,7 +30,9 @@ export default function remarkPaddedLinks() { * this seems unlikely to produce a noticeable perf gain. */ const hasLinkChild = node.children.some(child => child.type === 'link'); - const processedChildren = hasLinkChild ? flatMap(node.children, transformChildren) : node.children; + const processedChildren = hasLinkChild + ? flatMap(node.children, transformChildren) + : node.children; /** * Run all children through the transform recursively. @@ -83,7 +75,7 @@ export default function remarkPaddedLinks() { const nodes = [ leadingWhitespaceNode && u('text', ' '), node, - trailingWhitespaceNode && u('text', ' ') + trailingWhitespaceNode && u('text', ' '), ]; return nodes.filter(val => val); @@ -95,14 +87,17 @@ export default function remarkPaddedLinks() { */ function getEdgeTextChild(node, end) { /** - * This was changed from a ternary to a long form if due to issues with istanbul's instrumentation and babel's code - * generation. + * This was changed from a ternary to a long form if due to issues with istanbul's instrumentation and babel's code + * generation. * TODO: watch https://github.com/istanbuljs/babel-plugin-istanbul/issues/95 * when it is resolved then revert to ```const findFn = end ? findLast : find;``` */ let findFn; - if (end) { findFn = findLast } - else { findFn = find } + if (end) { + findFn = findLast; + } else { + findFn = find; + } let edgeChildWithValue; setEdgeChildWithValue(node); diff --git a/packages/netlify-cms-widget-markdown/src/serializers/remarkRehypeShortcodes.js b/packages/netlify-cms-widget-markdown/src/serializers/remarkRehypeShortcodes.js index 7e6507a7..9dc0fe1a 100644 --- a/packages/netlify-cms-widget-markdown/src/serializers/remarkRehypeShortcodes.js +++ b/packages/netlify-cms-widget-markdown/src/serializers/remarkRehypeShortcodes.js @@ -44,7 +44,7 @@ export default function remarkToRehypeShortcodes({ plugins, getAsset }) { * Return a new 'html' type node containing the shortcode preview markup. */ const textNode = u('html', valueHtml); - const children = [ textNode ]; + const children = [textNode]; return { ...node, children }; } } diff --git a/packages/netlify-cms-widget-markdown/src/serializers/remarkSlate.js b/packages/netlify-cms-widget-markdown/src/serializers/remarkSlate.js index 6c34dc9a..439b40e0 100644 --- a/packages/netlify-cms-widget-markdown/src/serializers/remarkSlate.js +++ b/packages/netlify-cms-widget-markdown/src/serializers/remarkSlate.js @@ -9,7 +9,6 @@ export default function remarkToSlate() { } function transform(node) { - /** * Call `transform` recursively on child nodes. * @@ -17,9 +16,10 @@ function transform(node) { * translate from MDAST to Slate, such as definitions for link/image * references or footnotes. */ - const children = !['strong', 'emphasis', 'delete'].includes(node.type) - && !isEmpty(node.children) - && flatMap(node.children, transform).filter(val => val); + const children = + !['strong', 'emphasis', 'delete'].includes(node.type) && + !isEmpty(node.children) && + flatMap(node.children, transform).filter(val => val); /** * Run individual nodes through the conversion factory. @@ -45,7 +45,6 @@ const typeMap = { shortcode: 'shortcode', }; - /** * Map of MDAST node types to Slate mark types. */ @@ -56,7 +55,6 @@ const markMap = { inlineCode: 'code', }; - /** * Add nodes to a parent node only if `nodes` is truthy. */ @@ -64,7 +62,6 @@ function addNodes(parent, nodes) { return nodes ? { ...parent, nodes } : parent; } - /** * Create a Slate Inline node. */ @@ -78,7 +75,6 @@ function createBlock(type, nodes, props = {}) { return addNodes(node, nodes); } - /** * Create a Slate Block node. */ @@ -87,7 +83,6 @@ function createInline(type, props = {}, nodes) { return addNodes(node, nodes); } - /** * Create a Slate Raw text node. */ @@ -122,7 +117,7 @@ function processMarkNode(node, parentMarks = []) { * first add the inline code mark to the marks array. */ case 'inlineCode': { - const childMarks = [ ...marks, { type: markMap['inlineCode'] } ]; + const childMarks = [...marks, { type: markMap['inlineCode'] }]; return { text: childNode.value, marks: childMarks }; } @@ -156,11 +151,9 @@ function convertMarkNode(node) { const lastConvertedNode = last(acc); if (node.text && lastConvertedNode && lastConvertedNode.leaves) { lastConvertedNode.leaves.push(node); - } - else if (node.text) { + } else if (node.text) { acc.push(createText([node])); - } - else { + } else { acc.push(transform(node)); } @@ -176,7 +169,6 @@ function convertMarkNode(node) { * transformer. */ function convertNode(node, nodes) { - /** * Unified/Remark processors use mutable operations, so we don't want to * change a node's type directly for conversion purposes, as that tends to @@ -185,7 +177,6 @@ function convertNode(node, nodes) { const type = get(node, ['data', 'shortcode']) ? 'shortcode' : node.type; switch (type) { - /** * General * @@ -201,7 +192,6 @@ function convertNode(node, nodes) { return createBlock(typeMap[type], nodes); } - /** * Shortcodes * @@ -211,7 +201,7 @@ function convertNode(node, nodes) { */ case 'shortcode': { const { data } = node; - const nodes = [ createText('') ]; + const nodes = [createText('')]; return createBlock(typeMap[type], nodes, { data, isVoid: true }); } @@ -240,7 +230,7 @@ function convertNode(node, nodes) { text: node.value, marks: [{ type: 'code' }], }; - return createText([ leaf ]); + return createText([leaf]); } /** @@ -308,7 +298,7 @@ function convertNode(node, nodes) { */ case 'break': { const textNode = createText('\n'); - return createInline('break', {}, [ textNode ]); + return createInline('break', {}, [textNode]); } /** @@ -345,7 +335,6 @@ function convertNode(node, nodes) { return createInline(typeMap[type], { isVoid: true, data: newData }); } - /** * Tables * diff --git a/packages/netlify-cms-widget-markdown/src/serializers/remarkSquashReferences.js b/packages/netlify-cms-widget-markdown/src/serializers/remarkSquashReferences.js index 5e49d2e9..72dd4fb9 100644 --- a/packages/netlify-cms-widget-markdown/src/serializers/remarkSquashReferences.js +++ b/packages/netlify-cms-widget-markdown/src/serializers/remarkSquashReferences.js @@ -32,7 +32,6 @@ export default function remarkSquashReferences() { } function transform(getDefinition, node) { - /** * Bind the `getDefinition` function to `transform` and recursively map all * nodes. @@ -55,15 +54,15 @@ export default function remarkSquashReferences() { const pre = u('text', node.type === 'imageReference' ? '![' : '['); const post = u('text', ']'); - const nodes = children || [ u('text', node.alt) ]; - return [ pre, ...nodes, post]; + const nodes = children || [u('text', node.alt)]; + return [pre, ...nodes, post]; } /** * Remove definition nodes and filter the resulting null values from the * filtered children array. */ - if(node.type === 'definition') { + if (node.type === 'definition') { return null; } diff --git a/packages/netlify-cms-widget-markdown/src/serializers/remarkStripTrailingBreaks.js b/packages/netlify-cms-widget-markdown/src/serializers/remarkStripTrailingBreaks.js index 9c7468e5..50e1bf0e 100644 --- a/packages/netlify-cms-widget-markdown/src/serializers/remarkStripTrailingBreaks.js +++ b/packages/netlify-cms-widget-markdown/src/serializers/remarkStripTrailingBreaks.js @@ -14,7 +14,6 @@ export default function remarkStripTrailingBreaks() { if (node.children) { node.children = node.children .map((child, idx, children) => { - /** * Only touch break nodes. Convert all subsequent nodes to their text * value and exclude the break node if no non-whitespace characters diff --git a/packages/netlify-cms-widget-markdown/src/serializers/remarkWrapHtml.js b/packages/netlify-cms-widget-markdown/src/serializers/remarkWrapHtml.js index baee06bb..6131faaa 100644 --- a/packages/netlify-cms-widget-markdown/src/serializers/remarkWrapHtml.js +++ b/packages/netlify-cms-widget-markdown/src/serializers/remarkWrapHtml.js @@ -5,7 +5,6 @@ import u from 'unist-builder'; * are used for text nodes that we don't want Remark or Rehype to parse. */ export default function remarkWrapHtml() { - function transform(tree) { tree.children = tree.children.map(node => { if (node.type === 'html') { diff --git a/packages/netlify-cms-widget-markdown/src/serializers/slateRemark.js b/packages/netlify-cms-widget-markdown/src/serializers/slateRemark.js index ae3b5fc3..7f7ddd6d 100644 --- a/packages/netlify-cms-widget-markdown/src/serializers/slateRemark.js +++ b/packages/netlify-cms-widget-markdown/src/serializers/slateRemark.js @@ -5,29 +5,28 @@ import u from 'unist-builder'; * Map of Slate node types to MDAST/Remark node types. */ const typeMap = { - 'root': 'root', - 'paragraph': 'paragraph', + root: 'root', + paragraph: 'paragraph', 'heading-one': 'heading', 'heading-two': 'heading', 'heading-three': 'heading', 'heading-four': 'heading', 'heading-five': 'heading', 'heading-six': 'heading', - 'quote': 'blockquote', - 'code': 'code', + quote: 'blockquote', + code: 'code', 'numbered-list': 'list', 'bulleted-list': 'list', 'list-item': 'listItem', - 'table': 'table', + table: 'table', 'table-row': 'tableRow', 'table-cell': 'tableCell', - 'break': 'break', + break: 'break', 'thematic-break': 'thematicBreak', - 'link': 'link', - 'image': 'image', + link: 'link', + image: 'image', }; - /** * Map of Slate mark types to MDAST/Remark node types. */ @@ -55,7 +54,6 @@ export default function slateToRemark(raw, opts) { return transform(raw); } - /** * The transform function mimics the approach of a Remark plugin for * conformity with the other serialization functions. This function converts @@ -83,7 +81,6 @@ function transform(node) { : convertNode(node, children, shortcodePlugins); } - /** * Includes inline nodes as leaves in adjacent text nodes where appropriate, so * that mark node combining logic can apply to both text and inline nodes. This @@ -140,7 +137,6 @@ function combineTextAndInline(nodes) { }, []); } - /** * Slate treats inline code decoration as a standard mark, but MDAST does * not allow inline code nodes to contain children, only a single text @@ -232,7 +228,6 @@ function convertTextNode(node) { return u('html', node.text); } - /** * Process Slate node leaves in preparation for MDAST transformation. */ @@ -256,7 +251,6 @@ function processLeaves(leaf) { return { node: leaf.node, marks: markTypes }; } - /** * Slate's AST doesn't group adjacent text nodes with the same marks - a * change in marks from letter to letter, even if some are in common, results @@ -311,47 +305,51 @@ function condenseNodesReducer(acc, node, idx, nodes) { * children. */ const children = nodes.slice(idx, newNextIndex); - const denestedChildren = children.map(child => ({ ...child, marks: without(child.marks, parentType) })); - const mdastChildren = denestedChildren.reduce(condenseNodesReducer, { nodes: [], parentType }).nodes; + const denestedChildren = children.map(child => ({ + ...child, + marks: without(child.marks, parentType), + })); + const mdastChildren = denestedChildren.reduce(condenseNodesReducer, { nodes: [], parentType }) + .nodes; const mdastNode = u(parentType, mdastChildren); - return { ...acc, nodes: [ ...acc.nodes, mdastNode ], nextIndex: newNextIndex }; + return { ...acc, nodes: [...acc.nodes, mdastNode], nextIndex: newNextIndex }; } /** * Create the base text node, and pass in the array of mark types as data * (helpful when optimizing/condensing the final structure). */ - const baseNode = typeof node.text === 'string' - ? u(node.textNodeType, { marks: node.marks }, node.text) - : transform(node.node); + const baseNode = + typeof node.text === 'string' + ? u(node.textNodeType, { marks: node.marks }, node.text) + : transform(node.node); /** * Recursively wrap the base text node in the individual mark nodes, if * any exist. */ - return { ...acc, nodes: [ ...acc.nodes, baseNode ] }; + return { ...acc, nodes: [...acc.nodes, baseNode] }; } - /** * Get the number of consecutive Slate nodes containing a given mark beginning * from the first received node. */ function getMarkLength(markType, nodes) { let length = 0; - while(nodes[length] && nodes[length].marks.includes(markType)) { ++length; } + while (nodes[length] && nodes[length].marks.includes(markType)) { + ++length; + } return { markType, length }; } - /** * Convert a single Slate Raw node to an MDAST node. Uses the unist-builder `u` * function to create MDAST nodes and parses shortcodes. */ function convertNode(node, children, shortcodePlugins) { switch (node.type) { - /** * General * @@ -390,7 +388,7 @@ function convertNode(node, children, shortcodePlugins) { const plugin = shortcodePlugins.get(data.shortcode); const text = plugin.toBlock(data.shortcodeData); const textNode = u('html', text); - return u('paragraph', { data }, [ textNode ]); + return u('paragraph', { data }, [textNode]); } /** @@ -480,7 +478,6 @@ function convertNode(node, children, shortcodePlugins) { return u(typeMap[node.type], { url, title, alt, data }); } - /** * No default case is supplied because an unhandled case should never * occur. In the event that it does, let the error throw (for now). diff --git a/packages/netlify-cms-widget-markdown/src/styles.js b/packages/netlify-cms-widget-markdown/src/styles.js index 258b33b9..2ab8f4f1 100644 --- a/packages/netlify-cms-widget-markdown/src/styles.js +++ b/packages/netlify-cms-widget-markdown/src/styles.js @@ -9,4 +9,4 @@ export const EditorControlBar = styled.div` position: sticky; top: 0; margin-bottom: ${editorStyleVars.stickyDistanceBottom}; -` +`; diff --git a/packages/netlify-cms-widget-number/src/NumberControl.js b/packages/netlify-cms-widget-number/src/NumberControl.js index f88251f2..1a721c82 100644 --- a/packages/netlify-cms-widget-number/src/NumberControl.js +++ b/packages/netlify-cms-widget-number/src/NumberControl.js @@ -19,12 +19,12 @@ export default class NumberControl extends React.Component { value: '', }; - handleChange = (e) => { + handleChange = e => { const valueType = this.props.field.get('valueType'); const { onChange } = this.props; - if(valueType === 'int') { + if (valueType === 'int') { onChange(parseInt(e.target.value, 10)); - } else if(valueType === 'float') { + } else if (valueType === 'float') { onChange(parseFloat(e.target.value)); } else { onChange(e.target.value); @@ -36,17 +36,19 @@ export default class NumberControl extends React.Component { const min = field.get('min', ''); const max = field.get('max', ''); const step = field.get('step', field.get('valueType') === 'int' ? 1 : ''); - return ; + return ( + + ); } } diff --git a/packages/netlify-cms-widget-number/src/NumberPreview.js b/packages/netlify-cms-widget-number/src/NumberPreview.js index 300a045f..b4bb4466 100644 --- a/packages/netlify-cms-widget-number/src/NumberPreview.js +++ b/packages/netlify-cms-widget-number/src/NumberPreview.js @@ -2,9 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { WidgetPreviewContainer } from 'netlify-cms-ui-default'; -const NumberPreview = ({ value }) => ( - { value } -); +const NumberPreview = ({ value }) => {value}; NumberPreview.propTypes = { value: PropTypes.node, diff --git a/packages/netlify-cms-widget-object/src/ObjectControl.js b/packages/netlify-cms-widget-object/src/ObjectControl.js index a8fd925b..ff3fa58a 100644 --- a/packages/netlify-cms-widget-object/src/ObjectControl.js +++ b/packages/netlify-cms-widget-object/src/ObjectControl.js @@ -16,11 +16,7 @@ const styles = { export default class ObjectControl extends Component { static propTypes = { onChangeObject: PropTypes.func.isRequired, - value: PropTypes.oneOfType([ - PropTypes.node, - PropTypes.object, - PropTypes.bool, - ]), + value: PropTypes.oneOfType([PropTypes.node, PropTypes.object, PropTypes.bool]), field: PropTypes.object, forID: PropTypes.string, classNameWrapper: PropTypes.string.isRequired, @@ -59,14 +55,12 @@ export default class ObjectControl extends Component { const fieldName = field.get('name'); const fieldValue = value && Map.isMap(value) ? value.get(fieldName) : value; - return ( - - ); + return ; } handleCollapseToggle = () => { this.setState({ collapsed: !this.state.collapsed }); - } + }; render() { const { field, forID, classNameWrapper, forList } = this.props; @@ -76,14 +70,18 @@ export default class ObjectControl extends Component { if (multiFields) { return ( -
    - {forList ? null : - - } +
    + {forList ? null : ( + + )} {collapsed ? null : multiFields.map((f, idx) => this.controlFor(f, idx))}
    ); diff --git a/packages/netlify-cms-widget-relation/src/RelationControl.js b/packages/netlify-cms-widget-relation/src/RelationControl.js index e96e37ea..f2956a65 100644 --- a/packages/netlify-cms-widget-relation/src/RelationControl.js +++ b/packages/netlify-cms-widget-relation/src/RelationControl.js @@ -43,7 +43,7 @@ injectGlobal` .react-autosuggest__suggestion--focused { background-color: #ddd; } -` +`; export default class RelationControl extends React.Component { static propTypes = { @@ -55,10 +55,7 @@ export default class RelationControl extends React.Component { fetchID: PropTypes.string, query: PropTypes.func.isRequired, clearSearch: PropTypes.func.isRequired, - queryHits: PropTypes.oneOfType([ - PropTypes.array, - PropTypes.object, - ]), + queryHits: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), classNameWrapper: PropTypes.string.isRequired, setActiveStyle: PropTypes.func.isRequired, setInactiveStyle: PropTypes.func.isRequired, @@ -88,12 +85,18 @@ export default class RelationControl extends React.Component { * Load extra post data into the store after first query. */ if (this.didInitialSearch) return; - if (this.props.queryHits !== prevProps.queryHits && this.props.queryHits.get && this.props.queryHits.get(this.controlID)) { + if ( + this.props.queryHits !== prevProps.queryHits && + this.props.queryHits.get && + this.props.queryHits.get(this.controlID) + ) { this.didInitialSearch = true; const suggestion = this.props.queryHits.get(this.controlID); if (suggestion && suggestion.length === 1) { const val = this.getSuggestionValue(suggestion[0]); - this.props.onChange(val, { [this.props.field.get('collection')]: { [val]: suggestion[0].data } }); + this.props.onChange(val, { + [this.props.field.get('collection')]: { [val]: suggestion[0].data }, + }); } } } @@ -104,7 +107,9 @@ export default class RelationControl extends React.Component { onSuggestionSelected = (event, { suggestion }) => { const value = this.getSuggestionValue(suggestion); - this.props.onChange(value, { [this.props.field.get('collection')]: { [value]: suggestion.data } }); + this.props.onChange(value, { + [this.props.field.get('collection')]: { [value]: suggestion.data }, + }); }; onSuggestionsFetchRequested = debounce(({ value }) => { @@ -119,19 +124,21 @@ export default class RelationControl extends React.Component { this.props.clearSearch(); }; - getSuggestionValue = (suggestion) => { + getSuggestionValue = suggestion => { const { field } = this.props; const valueField = field.get('valueField'); return suggestion.data[valueField]; }; - renderSuggestion = (suggestion) => { + renderSuggestion = suggestion => { const { field } = this.props; const valueField = field.get('displayFields') || field.get('valueField'); if (List.isList(valueField)) { return ( - {valueField.toJS().map(key => {new String(suggestion.data[key])}{' '})} + {valueField.toJS().map(key => ( + {new String(suggestion.data[key])} + ))} ); } @@ -147,7 +154,7 @@ export default class RelationControl extends React.Component { queryHits, classNameWrapper, setActiveStyle, - setInactiveStyle + setInactiveStyle, } = this.props; const inputProps = { @@ -160,7 +167,7 @@ export default class RelationControl extends React.Component { onBlur: setInactiveStyle, }; - const suggestions = (queryHits.get) ? queryHits.get(this.controlID, []) : []; + const suggestions = queryHits.get ? queryHits.get(this.controlID, []) : []; return (
    diff --git a/packages/netlify-cms-widget-relation/src/RelationPreview.js b/packages/netlify-cms-widget-relation/src/RelationPreview.js index 916cec6b..4b763bf8 100644 --- a/packages/netlify-cms-widget-relation/src/RelationPreview.js +++ b/packages/netlify-cms-widget-relation/src/RelationPreview.js @@ -2,9 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { WidgetPreviewContainer } from 'netlify-cms-ui-default'; -const RelationPreview = ({ value }) => ( - { value } -) +const RelationPreview = ({ value }) => {value}; RelationPreview.propTypes = { value: PropTypes.node, diff --git a/packages/netlify-cms-widget-select/src/SelectControl.js b/packages/netlify-cms-widget-select/src/SelectControl.js index fbbe5bfd..45679d4d 100644 --- a/packages/netlify-cms-widget-select/src/SelectControl.js +++ b/packages/netlify-cms-widget-select/src/SelectControl.js @@ -12,13 +12,15 @@ export default class SelectControl extends React.Component { setActiveStyle: PropTypes.func.isRequired, setInactiveStyle: PropTypes.func.isRequired, field: ImmutablePropTypes.contains({ - options: ImmutablePropTypes.listOf(PropTypes.oneOfType([ - PropTypes.string, - ImmutablePropTypes.contains({ - label: PropTypes.string.isRequired, - value: PropTypes.string.isRequired, - }), - ])).isRequired, + options: ImmutablePropTypes.listOf( + PropTypes.oneOfType([ + PropTypes.string, + ImmutablePropTypes.contains({ + label: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + }), + ]), + ).isRequired, }), }; @@ -26,7 +28,7 @@ export default class SelectControl extends React.Component { value: '', }; - handleChange = (e) => { + handleChange = e => { this.props.onChange(e.target.value); }; @@ -40,7 +42,7 @@ export default class SelectControl extends React.Component { const options = [ ...(field.get('default', false) ? [] : [{ label: '', value: '' }]), - ...fieldOptions.map((option) => { + ...fieldOptions.map(option => { if (typeof option === 'string') { return { label: option, value: option }; } @@ -57,11 +59,11 @@ export default class SelectControl extends React.Component { onFocus={setActiveStyle} onBlur={setInactiveStyle} > - { - options.map( - (option, idx) => - ) - } + {options.map((option, idx) => ( + + ))} ); } diff --git a/packages/netlify-cms-widget-select/src/SelectPreview.js b/packages/netlify-cms-widget-select/src/SelectPreview.js index c4b5ab2f..7daf92ca 100644 --- a/packages/netlify-cms-widget-select/src/SelectPreview.js +++ b/packages/netlify-cms-widget-select/src/SelectPreview.js @@ -4,7 +4,7 @@ import { WidgetPreviewContainer } from 'netlify-cms-ui-default'; const SelectPreview = ({ value }) => ( {value ? value.toString() : null} -) +); SelectPreview.propTypes = { value: PropTypes.string, diff --git a/packages/netlify-cms-widget-string/src/StringControl.js b/packages/netlify-cms-widget-string/src/StringControl.js index e9c512fe..3f7d3212 100644 --- a/packages/netlify-cms-widget-string/src/StringControl.js +++ b/packages/netlify-cms-widget-string/src/StringControl.js @@ -22,7 +22,7 @@ export default class StringControl extends React.Component { onChange, classNameWrapper, setActiveStyle, - setInactiveStyle + setInactiveStyle, } = this.props; return ( diff --git a/packages/netlify-cms-widget-string/src/StringPreview.js b/packages/netlify-cms-widget-string/src/StringPreview.js index dbd5998f..3c82bcad 100644 --- a/packages/netlify-cms-widget-string/src/StringPreview.js +++ b/packages/netlify-cms-widget-string/src/StringPreview.js @@ -2,9 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { WidgetPreviewContainer } from 'netlify-cms-ui-default'; -const StringPreview = ({ value }) => ( - { value } -); +const StringPreview = ({ value }) => {value}; StringPreview.propTypes = { value: PropTypes.node, diff --git a/packages/netlify-cms-widget-text/src/TextPreview.js b/packages/netlify-cms-widget-text/src/TextPreview.js index 5e47b371..f3417216 100644 --- a/packages/netlify-cms-widget-text/src/TextPreview.js +++ b/packages/netlify-cms-widget-text/src/TextPreview.js @@ -2,9 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { WidgetPreviewContainer } from 'netlify-cms-ui-default'; -const TextPreview = ({ value }) => ( - { value } -) +const TextPreview = ({ value }) => {value}; TextPreview.propTypes = { value: PropTypes.node, diff --git a/packages/netlify-cms/README.md b/packages/netlify-cms/README.md index 5cdb470d..92504005 100644 --- a/packages/netlify-cms/README.md +++ b/packages/netlify-cms/README.md @@ -1,4 +1,5 @@ # Netlify CMS + [![All Contributors](https://img.shields.io/badge/all_contributors-110-orange.svg)](#contributors) [![Open Source Helpers](https://www.codetriage.com/netlify/netlify-cms/badges/users.svg)](https://www.codetriage.com/netlify/netlify-cms) [![](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/netlify/netlifycms) @@ -24,9 +25,9 @@ Read more about Netlify CMS [Core Concepts](https://www.netlifycms.org/docs/intr The Netlify CMS can be used in two different ways. -* A Quick and easy install, that just requires you to create a single HTML file and a configuration file. All the CMS Javascript and CSS are loaded from a CDN. -To learn more about this installation method, refer to the [Quick Start Guide](https://www.netlifycms.org/docs/quick-start/) -* A complete, more complex install, that gives you more flexibility but requires that you use a static site builder with a build system that supports npm packages. +- A Quick and easy install, that just requires you to create a single HTML file and a configuration file. All the CMS Javascript and CSS are loaded from a CDN. + To learn more about this installation method, refer to the [Quick Start Guide](https://www.netlifycms.org/docs/quick-start/) +- A complete, more complex install, that gives you more flexibility but requires that you use a static site builder with a build system that supports npm packages. # Community @@ -45,7 +46,9 @@ Please make sure you understand its [implications and guarantees](https://writin # Thanks ## Services + These services support Netlify CMS development by providing free infrastructure. +

    @@ -57,6 +60,7 @@ These services support Netlify CMS development by providing free infrastructure.

    ## Contributors + These wonderful folks are responsible for developing and maintaining Netlify CMS. ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)) @@ -79,6 +83,7 @@ These wonderful folks are responsible for developing and maintaining Netlify CMS | [
    David Ko](https://github.com/daveyko)
    [💻](https://github.com/netlify/netlify-cms/commits?author=daveyko "Code") | [
    Iñaki García](http://www.txorua.com)
    [🎨](#design-igarbla "Design") | [
    Sam](https://github.com/gazebosx3)
    [💻](https://github.com/netlify/netlify-cms/commits?author=gazebosx3 "Code") | [
    Josh Dzielak](https://dzello.com)
    [📖](https://github.com/netlify/netlify-cms/commits?author=dzello "Documentation") | [
    Jeremy Bise](http://thosegeeks.com)
    [📖](https://github.com/netlify/netlify-cms/commits?author=jeremybise "Documentation") | [
    terrierscript](https://terrierscript.com)
    [💻](https://github.com/netlify/netlify-cms/commits?author=terrierscript "Code") | [
    Christopher Geary](https://twitter.com/crgeary)
    [🔌](#plugin-crgeary "Plugin/utility libraries") | | [
    Brian Macdonald](https://github.com/brianlmacdonald)
    [💻](https://github.com/netlify/netlify-cms/commits?author=brianlmacdonald "Code") | [
    John Vandenberg](https://jayvdb.github.io/)
    [📖](https://github.com/netlify/netlify-cms/commits?author=jayvdb "Documentation") | [
    MarkZither](https://github.com/MarkZither)
    [📖](https://github.com/netlify/netlify-cms/commits?author=MarkZither "Documentation") | [
    Rob Phoenix](https://www.robphoenix.com)
    [📖](https://github.com/netlify/netlify-cms/commits?author=robphoenix "Documentation") | [
    Steve Lathrop](https://www.SteLa.io)
    [💻](https://github.com/netlify/netlify-cms/commits?author=slathrop "Code") [📖](https://github.com/netlify/netlify-cms/commits?author=slathrop "Documentation") [💡](#example-slathrop "Examples") | [
    Maciej Matuszewski](https://github.com/maciejmatu)
    [💻](https://github.com/netlify/netlify-cms/commits?author=maciejmatu "Code") | [
    Eko Eryanto](https://github.com/ekoeryanto)
    [🔌](#plugin-ekoeryanto "Plugin/utility libraries") | | [
    Taylor D. Edmiston](http://blog.tedmiston.com/)
    [📖](https://github.com/netlify/netlify-cms/commits?author=tedmiston "Documentation") | [
    Daniel Mahon](https://www.mahonstudios.com)
    [💻](https://github.com/netlify/netlify-cms/commits?author=danielmahon "Code") | [
    Evan Hennessy](https://www.hennessyevan.com)
    [🔌](#plugin-hennessyevan "Plugin/utility libraries") | [
    Hasan Azizul Haque](https://hasanavi.me)
    [💻](https://github.com/netlify/netlify-cms/commits?author=hasanavi "Code") [📖](https://github.com/netlify/netlify-cms/commits?author=hasanavi "Documentation") [🤔](#ideas-hasanavi "Ideas, Planning, & Feedback") | [
    Robert Karlsson](https://github.com/robertkarlsson)
    [🐛](https://github.com/netlify/netlify-cms/issues?q=author%3Arobertkarlsson "Bug reports") | [
    Gil Greenberg](http://gilgreenberg.com)
    [💻](https://github.com/netlify/netlify-cms/commits?author=gil-- "Code") | + This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome! diff --git a/packages/netlify-cms/scripts/deprecate-old-dist.js b/packages/netlify-cms/scripts/deprecate-old-dist.js index 0d755c26..ed130bca 100644 --- a/packages/netlify-cms/scripts/deprecate-old-dist.js +++ b/packages/netlify-cms/scripts/deprecate-old-dist.js @@ -1 +1,3 @@ -console.warn('You seem to be loading Netlify CMS by fetching `dist/cms.js` from a CDN. That file is deprecated and will be removed in the next major release. Please use `dist/netlify-cms.js` instead.') +console.warn( + 'You seem to be loading Netlify CMS by fetching `dist/cms.js` from a CDN. That file is deprecated and will be removed in the next major release. Please use `dist/netlify-cms.js` instead.', +); diff --git a/packages/netlify-cms/webpack.config.js b/packages/netlify-cms/webpack.config.js index 4cb063f0..aeb15ca7 100644 --- a/packages/netlify-cms/webpack.config.js +++ b/packages/netlify-cms/webpack.config.js @@ -13,8 +13,8 @@ const baseConfig = { entry: './index.js', plugins: [ ...Object.entries(plugins) - .filter(([ key ]) => key !== 'friendlyErrors') - .map(([ , plugin ]) => plugin()), + .filter(([key]) => key !== 'friendlyErrors') + .map(([, plugin]) => plugin()), new webpack.DefinePlugin({ NETLIFY_CMS_VERSION: JSON.stringify(`${pkg.version}${isProduction ? '' : '-dev'}`), NETLIFY_CMS_CORE_VERSION: null, @@ -44,10 +44,7 @@ if (isProduction) { */ { ...baseConfig, - entry: [ - path.join(__dirname, 'scripts/deprecate-old-dist.js'), - baseConfig.entry, - ], + entry: [path.join(__dirname, 'scripts/deprecate-old-dist.js'), baseConfig.entry], output: { ...baseConfig.output, filename: 'dist/cms.js', diff --git a/scripts/cache.js b/scripts/cache.js index 0f2ac5fa..21736808 100644 --- a/scripts/cache.js +++ b/scripts/cache.js @@ -1,6 +1,6 @@ -const os = require('os') -const path = require('path') -const cache = require('cache-me-outside') +const os = require('os'); +const path = require('path'); +const cache = require('cache-me-outside'); cache({ cacheFolder: path.join('/', 'opt', 'build', 'cache', 'fast-cache'), @@ -12,4 +12,4 @@ cache({ }, ], ignoreIfFolderExists: false, -}) +}); diff --git a/setupTestFramework.js b/setupTestFramework.js index 00614510..7babb39e 100644 --- a/setupTestFramework.js +++ b/setupTestFramework.js @@ -1,4 +1,4 @@ -import * as emotion from 'emotion' -import { createSerializer } from 'jest-emotion' +import * as emotion from 'emotion'; +import { createSerializer } from 'jest-emotion'; -expect.addSnapshotSerializer(createSerializer(emotion)) +expect.addSnapshotSerializer(createSerializer(emotion)); diff --git a/website/.stylelintrc b/website/.stylelintrc new file mode 100644 index 00000000..4f7a87c3 --- /dev/null +++ b/website/.stylelintrc @@ -0,0 +1,13 @@ +{ + "extends": [ + "stylelint-config-recommended" + ], + "rules": { + "at-rule-no-unknown": [true, { + "ignoreAtRules": ["/^neat/"] + }], + "no-descending-specificity": [true, { + "severity": "warning" + }] + } +} \ No newline at end of file diff --git a/website/README.md b/website/README.md index 3c175453..e3e89443 100755 --- a/website/README.md +++ b/website/README.md @@ -4,7 +4,7 @@ This directory builds netlifycms.org. If you'd like to propose changes to the si ## Local development -The site is built with [GatsbyJS](https://gatsbyjs.org/). +The site is built with [GatsbyJS](https://gatsbyjs.org/). To run the site locally, you'll need to have [Node](https://nodejs.org) and [Yarn](https://yarnpkg.com/en/) installed on your computer. diff --git a/website/content/blog/netlify-cms-2-0-launches-with-bitbucket-support-and-a-new-monorepo-architecture.md b/website/content/blog/netlify-cms-2-0-launches-with-bitbucket-support-and-a-new-monorepo-architecture.md index 62be4d37..5fa5ce2b 100644 --- a/website/content/blog/netlify-cms-2-0-launches-with-bitbucket-support-and-a-new-monorepo-architecture.md +++ b/website/content/blog/netlify-cms-2-0-launches-with-bitbucket-support-and-a-new-monorepo-architecture.md @@ -9,19 +9,20 @@ description: >- of features. date: '2018-07-26' --- -Today we’re releasing Netlify CMS 2.0, which adds support for using Bitbucket as a backend. -With this release, [Netlify CMS](https://www.netlifycms.org/) now supports all major Git collaboration providers, adding Bitbucket to the list of supported providers which already includes GitLab and GitHub. +Today we’re releasing Netlify CMS 2.0, which adds support for using Bitbucket as a backend. -While you could already use Netlify CMS with most static site generators, our long-term vision is to be tool-agnostic so you can use whatever tool helps you work best. The latest release brings us one step closer by giving the option of an open source, Git-centric CMS to tens of thousands of businesses that depend on Bitbucket, including 60 of the Fortune 100. +With this release, [Netlify CMS](https://www.netlifycms.org/) now supports all major Git collaboration providers, adding Bitbucket to the list of supported providers which already includes GitLab and GitHub. + +While you could already use Netlify CMS with most static site generators, our long-term vision is to be tool-agnostic so you can use whatever tool helps you work best. The latest release brings us one step closer by giving the option of an open source, Git-centric CMS to tens of thousands of businesses that depend on Bitbucket, including 60 of the Fortune 100. ## Becoming a Monorepo The other big change with 2.0 is the migration from a single codebase to a collection of interdependent packages called a “monorepo”. Netlify CMS still lives in a [single repository on GitHub](https://github.com/netlify/netlify-cms), but the many extensions that were kept within Netlify CMS itself are now completely separate from the application core. This brings a few benefits: -* Extension authors can easily copy an existing extension from the Netlify CMS repo and create a custom version. -* Your custom extensions can now do anything the “official” extensions can do (because official extensions are no longer taking advantage of privileged internal code). -* The monorepo approach provides a foundation that will encourage a more modular CMS, with shared parts that make extension authoring easier. +- Extension authors can easily copy an existing extension from the Netlify CMS repo and create a custom version. +- Your custom extensions can now do anything the “official” extensions can do (because official extensions are no longer taking advantage of privileged internal code). +- The monorepo approach provides a foundation that will encourage a more modular CMS, with shared parts that make extension authoring easier. ## What’s next diff --git a/website/content/blog/netlify-cms-now-supports-gitlab-as-a-backend.md b/website/content/blog/netlify-cms-now-supports-gitlab-as-a-backend.md index 3a0e49ad..519acb75 100644 --- a/website/content/blog/netlify-cms-now-supports-gitlab-as-a-backend.md +++ b/website/content/blog/netlify-cms-now-supports-gitlab-as-a-backend.md @@ -4,24 +4,25 @@ author: Benaiah Mischenko description: >- Netlify CMS, the open source, headless CMS that provides a user-friendly UI around your Git repository, can now be used with GitLab in addition to - GitHub. + GitHub. date: '2018-06-13' --- -Netlify CMS is releasing support for GitLab as a backend, creating the world's first completely open source stack for Git-based content editing. + +Netlify CMS is releasing support for GitLab as a backend, creating the world's first completely open source stack for Git-based content editing. -We heard [you](https://github.com/netlify/netlify-cms/pull/517#issuecomment-383283557) (and [you](https://github.com/netlify/netlify-cms/pull/517#issuecomment-355386542), and [you](https://github.com/netlify/netlify-cms/pull/517#issuecomment-343569725), and [you](https://github.com/netlify/netlify-cms/pull/517#issuecomment-333629637))! While you want to use Netlify CMS as the headless content management system for your JAMstack projects, all of your code lives in GitLab. Our long-term vision is to be tool-agnostic so you can use whatever tool helps you work best. But while you can already use Netlify CMS with most static site generators, backend support was limited to GitHub. +We heard [you](https://github.com/netlify/netlify-cms/pull/517#issuecomment-383283557) (and [you](https://github.com/netlify/netlify-cms/pull/517#issuecomment-355386542), and [you](https://github.com/netlify/netlify-cms/pull/517#issuecomment-343569725), and [you](https://github.com/netlify/netlify-cms/pull/517#issuecomment-333629637))! While you want to use Netlify CMS as the headless content management system for your JAMstack projects, all of your code lives in GitLab. Our long-term vision is to be tool-agnostic so you can use whatever tool helps you work best. But while you can already use Netlify CMS with most static site generators, backend support was limited to GitHub. -Immediately after the December release of Netlify CMS 1.0, contributors got to work on improving the API for backend integrations. At the urging of the community, we prioritized support for GitLab. With today’s release of Netlify CMS 1.9.0, you can now use GitLab as the backend for Netlify CMS. +Immediately after the December release of Netlify CMS 1.0, contributors got to work on improving the API for backend integrations. At the urging of the community, we prioritized support for GitLab. With today’s release of Netlify CMS 1.9.0, you can now use GitLab as the backend for Netlify CMS. -Adding support for GitLab means that millions more developers can now use Netlify CMS with their projects. Seriously — millions. GitLab is used by more than 100,000 organizations like Ticketmaster, Intel, Red Hat, and CERN. +Adding support for GitLab means that millions more developers can now use Netlify CMS with their projects. Seriously — millions. GitLab is used by more than 100,000 organizations like Ticketmaster, Intel, Red Hat, and CERN. ## How it works Netlify CMS is an open source content management system for your Git workflow that enables you to provide editors with a friendly UI and intuitive workflow. You can use it with any static site generator to create faster, more flexible web projects. Content is stored in your GitLab repository alongside your code for easier versioning, multi-channel publishing, and the option to handle content updates directly in Git. -In case you want an even easier way to get started, or just want to poke around in the code, you can use the button below to automatically deploy a starter site that uses the Hugo static site generator along with Netlify CMS. +In case you want an even easier way to get started, or just want to poke around in the code, you can use the button below to automatically deploy a starter site that uses the Hugo static site generator along with Netlify CMS.
    Deploy to Netlify diff --git a/website/content/docs/add-to-your-site.md b/website/content/docs/add-to-your-site.md index c41df6f7..df8cec91 100755 --- a/website/content/docs/add-to-your-site.md +++ b/website/content/docs/add-to-your-site.md @@ -60,19 +60,19 @@ npm install netlify-cms --save Then import it (assuming your project has tooling for imports): ```js -import CMS from 'netlify-cms' +import CMS from 'netlify-cms'; // Now the registry is available via the CMS object. -CMS.registerPreviewTemplate('my-template', MyTemplate) +CMS.registerPreviewTemplate('my-template', MyTemplate); ``` ## Configuration -Configuration will be different for every site, so we'll break it down into parts. All code snippets in this section will be added to your `admin/config.yml` file. +Configuration will be different for every site, so we'll break it down into parts. All code snippets in this section will be added to your `admin/config.yml` file. ### Backend -We're using [Netlify](https://www.netlify.com) for our hosting and authentication in this tutorial, so backend configuration is fairly straightforward. +We're using [Netlify](https://www.netlify.com) for our hosting and authentication in this tutorial, so backend configuration is fairly straightforward. For GitHub and GitLab repositories, you can start your Netlify CMS `config.yml` file with these lines: @@ -103,7 +103,7 @@ Netlify CMS allows users to upload images directly within the editor. For this t ```yaml # This line should *not* be indented -media_folder: "images/uploads" # Media files will be stored in the repo under images/uploads +media_folder: 'images/uploads' # Media files will be stored in the repo under images/uploads ``` If you're creating a new folder for uploaded media, you'll need to know where your static site generator expects static files. You can refer to the paths outlined above in [App File Structure](#app-file-structure), and put your media folder in the same location where you put the `admin` folder. @@ -112,14 +112,13 @@ Note that the`media_folder` file path is relative to the project root, so the ex ```yaml # These lines should *not* be indented -media_folder: "static/images/uploads" # Media files will be stored in the repo under static/images/uploads -public_folder: "/images/uploads" # The src attribute for uploaded media will begin with /images/uploads +media_folder: 'static/images/uploads' # Media files will be stored in the repo under static/images/uploads +public_folder: '/images/uploads' # The src attribute for uploaded media will begin with /images/uploads ``` The configuration above adds a new setting, `public_folder`. While `media_folder` specifies where uploaded files will be saved in the repo, `public_folder` indicates where they can be found in the published site. This path is used in image `src` attributes and is relative to the file where it's called. For this reason, we usually start the path at the site root, using the opening `/`. -*If `public_folder` is not set, Netlify CMS will default to the same value as `media_folder`, adding an opening `/` if one is not included.* - +_If `public_folder` is not set, Netlify CMS will default to the same value as `media_folder`, adding an opening `/` if one is not included._ ### Collections @@ -128,14 +127,12 @@ Collections define the structure for the different content types on your static Let's say your site has a blog, with the posts stored in `_posts/blog`, and files saved in a date-title format, like `1999-12-31-lets-party.md`. Each post begins with settings in yaml-formatted front matter, like so: ```yaml ---- layout: blog title: "Let's Party" date: 1999-12-31 11:59:59 -0800 -thumbnail: "/images/prince.jpg" +thumbnail: '/images/prince.jpg' rating: 5 ---- - +... This is the post body, where I write about our last chance to party before the Y2K bug destroys us all. ``` @@ -143,18 +140,18 @@ Given this example, our `collections` settings would look like this: ```yaml collections: - - name: "blog" # Used in routes, e.g., /admin/collections/blog - label: "Blog" # Used in the UI - folder: "_posts/blog" # The path to the folder where the documents are stored + - name: 'blog' # Used in routes, e.g., /admin/collections/blog + label: 'Blog' # Used in the UI + folder: '_posts/blog' # The path to the folder where the documents are stored create: true # Allow users to create new documents in this collection - slug: "{{year}}-{{month}}-{{day}}-{{slug}}" # Filename template, e.g., YYYY-MM-DD-title.md + slug: '{{year}}-{{month}}-{{day}}-{{slug}}' # Filename template, e.g., YYYY-MM-DD-title.md fields: # The fields for each document, usually in front matter - - {label: "Layout", name: "layout", widget: "hidden", default: "blog"} - - {label: "Title", name: "title", widget: "string"} - - {label: "Publish Date", name: "date", widget: "datetime"} - - {label: "Featured Image", name: "thumbnail", widget: "image"} - - {label: "Rating (scale of 1-5)", name: "rating", widget: "number"} - - {label: "Body", name: "body", widget: "markdown"} + - { label: 'Layout', name: 'layout', widget: 'hidden', default: 'blog' } + - { label: 'Title', name: 'title', widget: 'string' } + - { label: 'Publish Date', name: 'date', widget: 'datetime' } + - { label: 'Featured Image', name: 'thumbnail', widget: 'image' } + - { label: 'Rating (scale of 1-5)', name: 'rating', widget: 'number' } + - { label: 'Body', name: 'body', widget: 'markdown' } ``` Let's break that down: @@ -205,14 +202,14 @@ The entries for any collection can be filtered based on the value of a single fi ```yaml collections: - - name: "posts" - label: "Post" - folder: "_posts" + - name: 'posts' + label: 'Post' + folder: '_posts' filter: field: language value: en fields: - - {label: "Language", name: "language"} + - { label: 'Language', name: 'language' } ``` ## Authentication diff --git a/website/content/docs/architecture.md b/website/content/docs/architecture.md index c0cea65a..05715d32 100755 --- a/website/content/docs/architecture.md +++ b/website/content/docs/architecture.md @@ -14,9 +14,10 @@ The structure of an entry is defined as a series of fields, each with a `name`, The `widget` determines the UI widget that the content editor will use when editing this field of an entry, as well as how the content of the field is presented in the editing preview. -Entries are loaded and persisted through a `backend` that will typically represent a `git` repository. +Entries are loaded and persisted through a `backend` that will typically represent a `git` repository. ## State shape / reducers + **Auth:** Keeps track of the logged state and the current user. **Config:** Holds the environment configuration (backend type, available collections and fields). @@ -28,6 +29,7 @@ Entries are loaded and persisted through a `backend` that will typically represe **EntryDraft:** Reused for each entry that is edited or created. It holds the entry's temporary data until it's persisted on the backend. ## Selectors + Selectors are functions defined within reducers used to compute derived data from the Redux store. The available selectors are: **selectEntry:** Selects a single entry, given the collection and a slug. @@ -37,6 +39,7 @@ Selectors are functions defined within reducers used to compute derived data fro **getAsset:** Selects a single AssetProxy object for the given URI. ## Value Objects + **AssetProxy:** AssetProxy is a Value Object that holds information regarding an asset file (such as an image, for example), whether it's persisted online or held locally in cache. For a file persisted online, the AssetProxy only keeps information about its URI. For local files, the AssetProxy will keep a reference to the actual File object while generating the expected final URIs and on-demand blobs for local preview. @@ -44,26 +47,28 @@ For a file persisted online, the AssetProxy only keeps information about its URI The AssetProxy object can be used directly inside a media tag (such as ``), as it will always return something that can be used by the media tag to render correctly (either the URI for the online file or a single-use blob). ## Components structure and Workflows + Components are separated into two main categories: Container components and Presentational components. ### Entry Editing + For either updating an existing entry or creating a new one, the `EntryEditor` is used and the flow is the same: -* When mounted, the `EntryPage` container component dispatches the `createDraft` action, setting the `entryDraft` state to a blank state (in case of a new entry) or to a copy of the selected entry (in case of an edit). -* The `EntryPage` will also render widgets for each field type in the given entry. -* Widgets are used for editing entry fields. There are different widgets for different field types, and they are always defined in a pair containing a `control` and a `preview` component. The control component is responsible for presenting the user with the appropriate interface for manipulating the current field value, while the preview component is responsible for displaying the value with the appropriate styling. +- When mounted, the `EntryPage` container component dispatches the `createDraft` action, setting the `entryDraft` state to a blank state (in case of a new entry) or to a copy of the selected entry (in case of an edit). +- The `EntryPage` will also render widgets for each field type in the given entry. +- Widgets are used for editing entry fields. There are different widgets for different field types, and they are always defined in a pair containing a `control` and a `preview` component. The control component is responsible for presenting the user with the appropriate interface for manipulating the current field value, while the preview component is responsible for displaying the value with the appropriate styling. #### Widget components implementation + The control component receives one (1) callback as a prop: `onChange`. -* onChange (required): Should be called when the users changes the current value. It will ultimately end up updating the EntryDraft object in the Redux Store, thus updating the preview component. -* onAddAsset & onRemoveAsset (optionals): If the field accepts file uploads for media (images, for example), these callbacks should be invoked with a `AssetProxy` value object. `onAddAsset` will get the current media stored in the Redux state tree while `onRemoveAsset` will remove it. AssetProxy objects are stored in the `Medias` object and referenced in the `EntryDraft` object on the state tree. +- onChange (required): Should be called when the users changes the current value. It will ultimately end up updating the EntryDraft object in the Redux Store, thus updating the preview component. +- onAddAsset & onRemoveAsset (optionals): If the field accepts file uploads for media (images, for example), these callbacks should be invoked with a `AssetProxy` value object. `onAddAsset` will get the current media stored in the Redux state tree while `onRemoveAsset` will remove it. AssetProxy objects are stored in the `Medias` object and referenced in the `EntryDraft` object on the state tree. Both control and preview widgets receive a `getAsset` selector via props. Displaying the media (or its URI) for the user should always be done via `getAsset`, as it returns an AssetProxy that can return the correct value for both medias already persisted on the server and cached media not yet uploaded. The actual persistence of the content and medias inserted into the control component is delegated to the backend implementation. The backend will be called with the updated values and a list of assetProxy objects for each field of the entry, and should return a promise that can resolve into the persisted entry object and the list of the persisted media URIs. - ## Editorial Workflow implementation Instead of adding logic to `CollectionPage` and `EntryPage`, the Editorial Workflow is implemented as Higher Order Components, adding UI and dispatching additional actions. diff --git a/website/content/docs/authentication-backends.md b/website/content/docs/authentication-backends.md index a4b1eb77..f44fc8f0 100644 --- a/website/content/docs/authentication-backends.md +++ b/website/content/docs/authentication-backends.md @@ -18,13 +18,13 @@ To use it in your own project stored on GitHub or GitLab, follow these steps: steps to get started. 2. Add the following lines to your Netlify CMS `config.yml` file: - ```yaml - backend: - name: git-gateway - accept_roles: #optional - accepts all users if left out - - admin - - editor - ``` + ```yaml + backend: + name: git-gateway + accept_roles: #optional - accepts all users if left out + - admin + - editor + ``` 3. Optionally, you can assign roles to users in your Netlify dashboard, and then limit which roles can access the CMS by defining the `accept_roles` field as shown in the example above. @@ -50,11 +50,11 @@ To enable it: docs](https://www.netlify.com/docs/authentication-providers/#using-an-authentication-provider). 2. Add the following lines to your Netlify CMS `config.yml` file: - ```yaml - backend: - name: github - repo: owner-name/repo-name # Path to your GitHub repository - ``` + ```yaml + backend: + name: github + repo: owner-name/repo-name # Path to your GitHub repository + ``` If you prefer to run your own authentication server, check out the section on [external OAuth clients](#external-oauth-clients). @@ -72,14 +72,14 @@ To enable it: 1. Follow the [GitLab docs](https://docs.gitlab.com/ee/integration/oauth_provider.html#adding-an-application-through-the-profile) to add your Netlify CMS instance as an OAuth application. For the **Redirect URI**, enter `https://api.netlify.com/auth/done`, and check the box for `api` scope. 2. Follow the [Netlify - docs](https://www.netlify.com/docs/authentication-providers/#using-an-authentication-provider) to add your new GitLab Application ID and Secret to your Netlify site dashboard. -2. In your repository, add the following lines to your Netlify CMS `config.yml` file: + docs](https://www.netlify.com/docs/authentication-providers/#using-an-authentication-provider) to add your new GitLab Application ID and Secret to your Netlify site dashboard. +3. In your repository, add the following lines to your Netlify CMS `config.yml` file: - ```yaml - backend: - name: gitlab - repo: owner-name/repo-name # Path to your GitLab repository - ``` + ```yaml + backend: + name: gitlab + repo: owner-name/repo-name # Path to your GitLab repository + ``` ### Client-Side Implicit Grant @@ -88,26 +88,26 @@ With GitLab's Implicit Grant, users can authenticate with GitLab directly from t 1. Follow the [GitLab docs](https://docs.gitlab.com/ee/integration/oauth_provider.html#adding-an-application-through-the-profile) to add your Netlify CMS instance as an OAuth application. For the **Redirect URI**, enter the address where you access Netlify CMS, for example, `https://www.mysite.com/admin/`. For scope, select `api`. 2. GitLab will give you an **Application ID**. Copy this and enter it in your Netlify CMS `config.yml` file, along with the following settings: - ```yaml - backend: - name: gitlab - repo: owner-name/repo-name # Path to your GitLab repository - auth_type: implicit # Required for implicit grant - app_id: your-app-id # Application ID from your GitLab settings - ``` + ```yaml + backend: + name: gitlab + repo: owner-name/repo-name # Path to your GitLab repository + auth_type: implicit # Required for implicit grant + app_id: your-app-id # Application ID from your GitLab settings + ``` - You can also use Implicit Grant with a self-hosted GitLab instance. This requires adding `api_root`, `base_url`, and `auth_endpoint` fields: + You can also use Implicit Grant with a self-hosted GitLab instance. This requires adding `api_root`, `base_url`, and `auth_endpoint` fields: - ```yaml - backend: - name: gitlab - repo: owner-name/repo-name # Path to your GitLab repository - auth_type: implicit # Required for implicit grant - app_id: your-app-id # Application ID from your GitLab settings - api_root: https://my-hosted-gitlab-instance.com/api/v4 - base_url: https://my-hosted-gitlab-instance.com - auth_endpoint: oauth/authorize - ``` + ```yaml + backend: + name: gitlab + repo: owner-name/repo-name # Path to your GitLab repository + auth_type: implicit # Required for implicit grant + app_id: your-app-id # Application ID from your GitLab settings + api_root: https://my-hosted-gitlab-instance.com/api/v4 + base_url: https://my-hosted-gitlab-instance.com + auth_endpoint: oauth/authorize + ``` Note that in both cases, GitLab will also provide you with a client secret. You should _never_ store this in your repo or reveal it in the client. @@ -121,11 +121,11 @@ To enable it: docs](https://www.netlify.com/docs/authentication-providers/#using-an-authentication-provider). 2. Add the following lines to your Netlify CMS `config.yml` file: - ```yaml - backend: - name: bitbucket - repo: owner-name/repo-name # Path to your Bitbucket repository - ``` + ```yaml + backend: + name: bitbucket + repo: owner-name/repo-name # Path to your Bitbucket repository + ``` ## External OAuth Clients @@ -145,12 +145,12 @@ Check each project's documentation for instructions on how to configure it. Netlify CMS backends allow some additional fields for certain use cases. A full reference is below. Note that these are properties of the `backend` field, and should be nested under that field. -| Field | Default | Description | -| --------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -| `repo` | none | **Required** for `github`, `gitlab`, and `bitbucket` backends; ignored by `git-gateway`. Follows the pattern `[org-or-username]/[repo-name]`. | -| `accept_roles` | none | `git-gateway` only. Limits CMS access to your defined array of user roles. Omitting this field gives access to all registered users. | -| `branch` | `master` | The branch where published content is stored. All CMS commits and PRs are made to this branch. | -| `api_root` | `https://api.github.com` (GitHub), `https://gitlab.com/api/v4` (GitLab), or `https://api.bitbucket.org/2.0` (Bitbucket) | The API endpoint. Only necessary in certain cases, like with GitHub Enterprise or self-hosted GitLab. | -| `site_domain` | `location.hostname` (or `cms.netlify.com` when on `localhost`) | Sets the `site_id` query param sent to the API endpoint. Non-Netlify auth setups will often need to set this for local development to work properly. | -| `base_url` | `https://api.netlify.com` (GitHub, Bitbucket) or `https://gitlab.com` (GitLab) | OAuth client URL. **Required** when using an external OAuth server or self-hosted GitLab. | -| `auth_endpoint` | `auth` (GitHub, Bitbucket) or `oauth/authorize` (GitLab) | Path to append to `base_url` for authentication requests. Optional. | +| Field | Default | Description | +| --------------- | ----------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| `repo` | none | **Required** for `github`, `gitlab`, and `bitbucket` backends; ignored by `git-gateway`. Follows the pattern `[org-or-username]/[repo-name]`. | +| `accept_roles` | none | `git-gateway` only. Limits CMS access to your defined array of user roles. Omitting this field gives access to all registered users. | +| `branch` | `master` | The branch where published content is stored. All CMS commits and PRs are made to this branch. | +| `api_root` | `https://api.github.com` (GitHub), `https://gitlab.com/api/v4` (GitLab), or `https://api.bitbucket.org/2.0` (Bitbucket) | The API endpoint. Only necessary in certain cases, like with GitHub Enterprise or self-hosted GitLab. | +| `site_domain` | `location.hostname` (or `cms.netlify.com` when on `localhost`) | Sets the `site_id` query param sent to the API endpoint. Non-Netlify auth setups will often need to set this for local development to work properly. | +| `base_url` | `https://api.netlify.com` (GitHub, Bitbucket) or `https://gitlab.com` (GitLab) | OAuth client URL. **Required** when using an external OAuth server or self-hosted GitLab. | +| `auth_endpoint` | `auth` (GitHub, Bitbucket) or `oauth/authorize` (GitLab) | Path to append to `base_url` for authentication requests. Optional. | diff --git a/website/content/docs/beta-features.md b/website/content/docs/beta-features.md index 8b276448..3775dde8 100644 --- a/website/content/docs/beta-features.md +++ b/website/content/docs/beta-features.md @@ -9,6 +9,7 @@ We run new functionality in an open beta format from time to time. That means th **Use these features at your own risk.** ## Custom Mount Element + Netlify CMS always creates it's own DOM element for mounting the application, which means it always takes over the entire page, and is generally inflexible if you're trying to do something creative, like injecting it into a shared context. @@ -18,6 +19,7 @@ as `nc-root`. If Netlify CMS finds an element with this ID during initialization within that element instead of creating it's own. ## Manual Initialization + Netlify CMS can now be manually initialized, rather than automatically loading up the moment you import it. The whole point of this at the moment is to inject configuration into Netlify CMS before it loads, bypassing need for an actual Netlify CMS `config.yml`. This is important, for example, when creating tight integrations with static site generators. Injecting config is technically already possible by setting `window.CMS_CONFIG` before importing/requiring/running Netlify CMS, but most projects are modular and don't want to use globals, plus `window.CMS_CONFIG` is an internal, not technically supported, and provides no validation. @@ -65,6 +67,7 @@ CMS.registerPreviewTemplate(...); ``` ## Raw CSS in `registerPreviewStyle` + `registerPreviewStyle` can now accept a CSS string, in addition to accepting a url. The feature is activated by passing in an object as the second argument, with `raw` set to a truthy value.This is critical for integrating with modern build tooling. Here's an example using webpack: ```js @@ -73,12 +76,13 @@ CMS.registerPreviewTemplate(...); * Takes advantage of the `toString` method in the return value of `css-loader`. */ import CMS from 'netlify-cms'; -import styles from '!css-loader!sass-loader!../main.scss' +import styles from '!css-loader!sass-loader!../main.scss'; -CMS.registerPreviewStyle(styles.toString(), { raw: true }) +CMS.registerPreviewStyle(styles.toString(), { raw: true }); ``` ## Squash merge GitHub pull requests + When using the [Editorial Workflow](/docs/configuration-options/#publish-mode) with the `github` or GitHub-connected `git-gateway` backends, Netlify CMS creates a pull request for each unpublished entry. Every time the unpublished entry is changed and saved, a new commit is added to the pull request. When the entry is published, the pull request is merged, and all of those commits are added to your project commit history in a merge commit. The squash merge option causes all commits to be "squashed" into a single commit when the pull request is merged, and the resulting commit is rebased onto the target branch, avoiding the merge commit altogether. @@ -91,6 +95,7 @@ backend: ``` ## Commit Message Templates + You can customize the templates used by Netlify CMS to generate commit messages by setting the `commit_messages` option under `backend` in your Netlify CMS `config.yml`. Template tags wrapped in curly braces will be expanded to include information about the file changed by the commit. For example, `{{path}}` will include the full path to the file changed. @@ -109,13 +114,13 @@ backend: Netlify CMS generates the following commit types: -Commit type | When is it triggered? | Available template tags ---------------|------------------------------|----------------------------- -`create` | A new entry is created | `slug`, `path`, `collection` -`update` | An existing entry is changed | `slug`, `path`, `collection` -`delete` | An exising entry is deleted | `slug`, `path`, `collection` -`uploadMedia` | A media file is uploaded | `path` -`deleteMedia` | A media file is deleted | `path` +| Commit type | When is it triggered? | Available template tags | +| ------------- | ---------------------------- | ---------------------------- | +| `create` | A new entry is created | `slug`, `path`, `collection` | +| `update` | An existing entry is changed | `slug`, `path`, `collection` | +| `delete` | An exising entry is deleted | `slug`, `path`, `collection` | +| `uploadMedia` | A media file is uploaded | `path` | +| `deleteMedia` | A media file is deleted | `path` | Template tags produce the following output: diff --git a/website/content/docs/collection-types.md b/website/content/docs/collection-types.md index d1db9c9b..8df4bdfd 100644 --- a/website/content/docs/collection-types.md +++ b/website/content/docs/collection-types.md @@ -19,15 +19,15 @@ Unlike file collections, folder collections have the option to allow editors to Example: ```yaml -- label: "Blog" - name: "blog" - folder: "_posts/blog" +- label: 'Blog' + name: 'blog' + folder: '_posts/blog' create: true fields: - - {label: "Title", name: "title", widget: "string"} - - {label: "Publish Date", name: "date", widget: "datetime"} - - {label: "Featured Image", name: "thumbnail", widget: "image"} - - {label: "Body", name: "body", widget: "markdown"} + - { label: 'Title', name: 'title', widget: 'string' } + - { label: 'Publish Date', name: 'date', widget: 'datetime' } + - { label: 'Featured Image', name: 'thumbnail', widget: 'image' } + - { label: 'Body', name: 'body', widget: 'markdown' } ``` ### Filtered folder collections @@ -36,29 +36,29 @@ The entries for any folder collection can be filtered based on the value of a si The `filter` option requires two fields: -* `field`: the name of the collection field to filter on -* `value`: the desired field value +- `field`: the name of the collection field to filter on +- `value`: the desired field value The example below creates two collections in the same folder, filtered by the `language` field. The first collection includes posts with `language: en`, and the second, with `language: es`. ```yaml collections: - - label: "Blog in English" - name: "english_posts" - folder: "_posts" - filter: {field: "language", value: "en"} + - label: 'Blog in English' + name: 'english_posts' + folder: '_posts' + filter: { field: 'language', value: 'en' } fields: - - {label: "Language", name: "language", widget: "select", options: ["en", "es"]} - - {label: "Title", name: "title", widget: "string"} - - {label: "Content", name: "body", widget: "markdown"} - - label: "Blog en Español" - name: "spanish_posts" - folder: "_posts" - filter: {field: "language", value: "es"} + - { label: 'Language', name: 'language', widget: 'select', options: ['en', 'es'] } + - { label: 'Title', name: 'title', widget: 'string' } + - { label: 'Content', name: 'body', widget: 'markdown' } + - label: 'Blog en Español' + name: 'spanish_posts' + folder: '_posts' + filter: { field: 'language', value: 'es' } fields: - - {label: "Lenguaje", name: "language", widget: "select", options: ["en", "es"]} - - {label: "Titulo", name: "title", widget: "string"} - - {label: "Contenido", name: "body", widget: "markdown"} + - { label: 'Lenguaje', name: 'language', widget: 'select', options: ['en', 'es'] } + - { label: 'Titulo', name: 'title', widget: 'string' } + - { label: 'Contenido', name: 'body', widget: 'markdown' } ``` ## File collections @@ -72,32 +72,32 @@ When configuring a `files` collection, each file in the collection is configured Example: ```yaml -- label: "Pages" - name: "pages" +- label: 'Pages' + name: 'pages' files: - - label: "About Page" - name: "about" - file: "site/content/about.yml" + - label: 'About Page' + name: 'about' + file: 'site/content/about.yml' fields: - - {label: Title, name: title, widget: string} - - {label: Intro, name: intro, widget: markdown} + - { label: Title, name: title, widget: string } + - { label: Intro, name: intro, widget: markdown } - label: Team name: team widget: list fields: - - {label: Name, name: name, widget: string} - - {label: Position, name: position, widget: string} - - {label: Photo, name: photo, widget: image} - - label: "Locations Page" - name: "locations" - file: "site/content/locations.yml" + - { label: Name, name: name, widget: string } + - { label: Position, name: position, widget: string } + - { label: Photo, name: photo, widget: image } + - label: 'Locations Page' + name: 'locations' + file: 'site/content/locations.yml' fields: - - {label: Title, name: title, widget: string} - - {label: Intro, name: intro, widget: markdown} + - { label: Title, name: title, widget: string } + - { label: Intro, name: intro, widget: markdown } - label: Locations name: locations widget: list fields: - - {label: Name, name: name, widget: string} - - {label: Address, name: address, widget: string} + - { label: Name, name: name, widget: string } + - { label: Address, name: address, widget: string } ``` diff --git a/website/content/docs/configuration-options.md b/website/content/docs/configuration-options.md index a912871c..d5dcdca0 100644 --- a/website/content/docs/configuration-options.md +++ b/website/content/docs/configuration-options.md @@ -18,14 +18,12 @@ To see working configuration examples, you can [start from a template](https://w You can find details about all configuration options below. Note that [YAML syntax](https://en.wikipedia.org/wiki/YAML#Basic_components) allows lists and objects to be written in block or inline style, and the code samples below include a mix of both. - ## Backend -*This setting is required.* +_This setting is required._ The `backend` option specifies how to access the content for your site, including authentication. Full details and code samples can be found in [Authentication & Backends](https://www.netlifycms.org/docs/authentication-backends). - ## Publish Mode By default, all entries created or edited in the Netlify CMS are committed directly into the main repository branch. @@ -42,16 +40,15 @@ publish_mode: editorial_workflow From a technical perspective, the workflow translates editor UI actions into common Git commands: -Actions in Netlify UI ... | Perform these Git actions ---- | --- -Save draft | Commits to a new branch (named according to the pattern `cms/collectionName-entrySlug`), and opens a pull request -Edit draft | Pushes another commit to the draft branch/pull request -Approve and publish draft | Merges pull request and deletes branch - +| Actions in Netlify UI ... | Perform these Git actions | +| ------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| Save draft | Commits to a new branch (named according to the pattern `cms/collectionName-entrySlug`), and opens a pull request | +| Edit draft | Pushes another commit to the draft branch/pull request | +| Approve and publish draft | Merges pull request and deletes branch | ## Media and Public Folders -*This setting is required.* +_This setting is required._ Netlify CMS users can upload files to your repository using the Media Gallery. The following settings specify where these files are saved, and where they can be accessed on your built site. @@ -62,9 +59,9 @@ Netlify CMS users can upload files to your repository using the Media Gallery. T **Example** -``` yaml -media_folder: "static/images/uploads" -public_folder: "/images/uploads" +```yaml +media_folder: 'static/images/uploads' +public_folder: '/images/uploads' ``` Based on the settings above, if a user used an image widget field called `avatar` to upload and select an image called `philosoraptor.png`, the image would be saved to the repository at `/static/images/uploads/philosoraptor.png`, and the `avatar` field for the file would be set to `/images/uploads/philosoraptor.png`. @@ -92,15 +89,15 @@ The `slug` option allows you to change how filenames for entries are created and **Example** -``` yaml +```yaml slug: - encoding: "ascii" + encoding: 'ascii' clean_accents: true ``` ## Collections -*This setting is required.* +_This setting is required._ The `collections` setting is the heart of your Netlify CMS configuration, as it determines how content types and editor fields in the UI generate files and content in your repository. Each collection you configure displays in the left sidebar of the Content page of the editor UI, in the order they are entered into your Netlify CMS `config.yml` file. @@ -136,13 +133,12 @@ You may also specify a custom `extension` not included in the list above, as lon - `toml`: parses and saves files as TOML-formatted data files; saves with `toml` extension by default - `json`: parses and saves files as JSON-formatted data files; saves with `json` extension by default - `frontmatter`: parses files and saves files with data frontmatter followed by an unparsed body text (edited using a `body` field); saves with `md` extension by default; default for collections that can't be inferred. Collections with `frontmatter` format (either inferred or explicitly set) can parse files with frontmatter in YAML, TOML, or JSON format. However, they will be saved with YAML frontmatter. -- `yaml-frontmatter`: same as the `frontmatter` format above, except frontmatter will be both parsed and saved only as YAML, followed by unparsed body text. The default delimiter for this option is `---`. -- `toml-frontmatter`: same as the `frontmatter` format above, except frontmatter will be both parsed and saved only as TOML, followed by unparsed body text. The default delimiter for this option is `+++`. -- `json-frontmatter`: same as the `frontmatter` format above, except frontmatter will be both parsed and saved as JSON, followed by unparsed body text. The default delimiter for this option is `{` `}`. +- `yaml-frontmatter`: same as the `frontmatter` format above, except frontmatter will be both parsed and saved only as YAML, followed by unparsed body text. The default delimiter for this option is `---`. +- `toml-frontmatter`: same as the `frontmatter` format above, except frontmatter will be both parsed and saved only as TOML, followed by unparsed body text. The default delimiter for this option is `+++`. +- `json-frontmatter`: same as the `frontmatter` format above, except frontmatter will be both parsed and saved as JSON, followed by unparsed body text. The default delimiter for this option is `{` `}`. `frontmatter_delimiter`: if you have an explicit frontmatter format declared, this option allows you to specify a custom delimiter like `~~~`. If you need different beginning and ending delimiters, you can use an array like `["(", ")"]`. - ### `slug` For folder collections where users can create new items, the `slug` option specifies a template for generating new filenames based on a file's creation date and `title` field. (This means that all collections with `create: true` must have a `title` field.) @@ -160,7 +156,7 @@ For folder collections where users can create new items, the `slug` option speci **Example:** ```yaml -slug: "{{year}}-{{month}}-{{day}}_{{slug}}" +slug: '{{year}}-{{month}}-{{day}}_{{slug}}' ``` ### `fields` @@ -182,13 +178,13 @@ In files with frontmatter, one field should be named `body`. This special field ```yaml fields: - - label: "Title" - name: "title" - widget: "string" - pattern: ['.{20,}', "Must have at least 20 characters"] - - {label: "Layout", name: "layout", widget: "hidden", default: "blog"} - - {label: "Featured Image", name: "thumbnail", widget: "image", required: false} - - {label: "Body", name: "body", widget: "markdown"} + - label: 'Title' + name: 'title' + widget: 'string' + pattern: ['.{20,}', 'Must have at least 20 characters'] + - { label: 'Layout', name: 'layout', widget: 'hidden', default: 'blog' } + - { label: 'Featured Image', name: 'thumbnail', widget: 'image', required: false } + - { label: 'Body', name: 'body', widget: 'markdown' } ``` ### `editor` @@ -200,6 +196,6 @@ This setting changes options for the editor view of the collection. It has one o **Example:** ```yaml - editor: - preview: false +editor: + preview: false ``` diff --git a/website/content/docs/contributor-guide.md b/website/content/docs/contributor-guide.md index 56f54851..6e076c3e 100644 --- a/website/content/docs/contributor-guide.md +++ b/website/content/docs/contributor-guide.md @@ -8,8 +8,8 @@ We're hoping that Netlify CMS will do for the [JAMstack](https://www.jamstack.or While we work on building this page (and you can help!), here are some links with more information about getting involved: -* [Setup instructions and Contribution Guidelines](https://github.com/netlify/netlify-cms/blob/master/CONTRIBUTING.md) -* [Join us on Gitter](https://gitter.im/netlify/NetlifyCMS) -* [Code of Conduct](https://github.com/netlify/netlify-cms/blob/master/CODE_OF_CONDUCT.md) -* [Project Milestones](https://github.com/netlify/netlify-cms/milestones) -* [Good First Issues](https://github.com/netlify/netlify-cms/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22+-label%3Aclaimed) +- [Setup instructions and Contribution Guidelines](https://github.com/netlify/netlify-cms/blob/master/CONTRIBUTING.md) +- [Join us on Gitter](https://gitter.im/netlify/NetlifyCMS) +- [Code of Conduct](https://github.com/netlify/netlify-cms/blob/master/CODE_OF_CONDUCT.md) +- [Project Milestones](https://github.com/netlify/netlify-cms/milestones) +- [Good First Issues](https://github.com/netlify/netlify-cms/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22good+first+issue%22+-label%3Aclaimed) diff --git a/website/content/docs/custom-widgets.md b/website/content/docs/custom-widgets.md index b13ebeea..8e4507a7 100644 --- a/website/content/docs/custom-widgets.md +++ b/website/content/docs/custom-widgets.md @@ -6,8 +6,8 @@ group: guides The NetlifyCMS exposes a `window.CMS` global object that you can use to register custom widgets, previews, and editor plugins. The same object is also the default export if you import Netify CMS as an npm module. The available widget extension methods are: -* **registerWidget:** lets you register a custom widget. -* **registerEditorComponent:** lets you add a block component to the Markdown editor. +- **registerWidget:** lets you register a custom widget. +- **registerEditorComponent:** lets you add a block component to the Markdown editor. ### Writing React Components inline @@ -30,17 +30,17 @@ CMS.registerWidget(name, control, [preview]); **Params:** -| Param | Type | Description | -| ----------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `name` | `string` | Widget name, allows this widget to be used via the field `widget` property in config | -| `control` | `React.Component` or `string`|
    • React component that renders the control, receives the following props:
      • **value:** Current field value
      • **onChange:** Callback function to update the field value
    • Name of a registered widget whose control should be used (includes built in widgets).
    | -| [`preview`] | `React.Component`, optional | Renders the widget preview, receives the following props:
    • **value:** Current preview value
    • **field:** Immutable map of current field configuration
    • **metadata:** Immutable map of any available metadata for the current field
    • **getAsset:** Function for retrieving an asset url for image/file fields
    • **entry:** Immutable Map of all entry data
    • **fieldsMetaData:** Immutable map of metadata from all fields.
    | +| Param | Type | Description | +| ----------- | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `name` | `string` | Widget name, allows this widget to be used via the field `widget` property in config | +| `control` | `React.Component` or `string` |
    • React component that renders the control, receives the following props:
      • **value:** Current field value
      • **onChange:** Callback function to update the field value
    • Name of a registered widget whose control should be used (includes built in widgets).
    | +| [`preview`] | `React.Component`, optional | Renders the widget preview, receives the following props:
    • **value:** Current preview value
    • **field:** Immutable map of current field configuration
    • **metadata:** Immutable map of any available metadata for the current field
    • **getAsset:** Function for retrieving an asset url for image/file fields
    • **entry:** Immutable Map of all entry data
    • **fieldsMetaData:** Immutable map of metadata from all fields.
    | -* **field:** The field type that this widget will be used for. -* **control:** A React component that renders the editing interface for this field. Two props will be passed: - * **value:** The current value for this field. - * **onChange:** Callback function to update the field value. -* **preview (optional):** A React component that renders the preview of how the content will look. A `value` prop will be passed to this component. +- **field:** The field type that this widget will be used for. +- **control:** A React component that renders the editing interface for this field. Two props will be passed: + - **value:** The current value for this field. + - **onChange:** Callback function to update the field value. +- **preview (optional):** A React component that renders the preview of how the content will look. A `value` prop will be passed to this component. **Example:** @@ -77,12 +77,12 @@ CMS.registerWidget('categories', CategoriesControl, CategoriesPreview); Register a block level component for the Markdown editor: ```js -CMS.registerEditorComponent(definition) +CMS.registerEditorComponent(definition); ``` **Params** -* **definition:** The component definition; must specify: id, label, fields, patterns, fromBlock, toBlock, toPreview +- **definition:** The component definition; must specify: id, label, fields, patterns, fromBlock, toBlock, toPreview **Example:** @@ -133,38 +133,38 @@ With custom widgets, the widget control can also optionally implement an `isVali No errors: ```javascript - isValid = () => { - // Do internal validation - return true; - }; +isValid = () => { + // Do internal validation + return true; +}; ``` Existing error: ```javascript - isValid = () => { - // Do internal validation - return false; - }; +isValid = () => { + // Do internal validation + return false; +}; ``` **Object with `error` (useful for returning custom error messages)** Existing error: ```javascript - isValid = () => { - // Do internal validation - return { error: 'Your error message.' }; - }; +isValid = () => { + // Do internal validation + return { error: 'Your error message.' }; +}; ``` **Promise** You can also return a promise from `isValid`. While the promise is pending, the widget will be marked as "in error". When the promise resolves, the error is automatically cleared. ```javascript - isValid = () => { - return this.existingPromise; - }; +isValid = () => { + return this.existingPromise; +}; ``` Note: Do not create a promise inside `isValid` - `isValid` is called right before trying to persist. This means that even if a previous promise was already resolved, when the user hits 'save', `isValid` will be called again. If it returns a new promise, it will be immediately marked as "in error" until the new promise resolves. diff --git a/website/content/docs/customization.md b/website/content/docs/customization.md index 0d7a1176..6cb87e1d 100644 --- a/website/content/docs/customization.md +++ b/website/content/docs/customization.md @@ -6,8 +6,8 @@ group: guides The NetlifyCMS exposes a `window.CMS` global object that you can use to register custom widgets, previews and editor plugins. The available customization methods are: -* **registerPreviewStyle:** Register a custom stylesheet to use on the preview pane. -* **registerPreviewTemplate:** Registers a template for a collection. +- **registerPreviewStyle:** Register a custom stylesheet to use on the preview pane. +- **registerPreviewTemplate:** Registers a template for a collection. Explore the [NetlifyCMS GitHub example](https://github.com/netlify/netlify-cms/blob/9ced3f16c8bcc3d1a36773b126842d023c589eaf/example/index.html#L90-L91), a working example you can review on GitHub. @@ -25,7 +25,7 @@ CMS.registerPreviewStyle(file); **Params:** -* **file:** css file path +- **file:** css file path **Example:** @@ -60,12 +60,13 @@ Registers a template for a collection. **Params:** -* collection: The name of the collection which this preview component will be used for. -* react_component: A React component that renders the collection data. Four props will be passed to your component during render: - * entry: Immutable collection containing the entry data. - * widgetFor: Returns the appropriate widget preview component for a given field. - * [widgetsFor](#lists-and-objects): Returns an array of objects with widgets and associated field data. For use with list and object type entries. - * getAsset: Returns the correct filePath or in-memory preview for uploaded images. +- collection: The name of the collection which this preview component will be used for. +- react_component: A React component that renders the collection data. Four props will be passed to your component during render: + + - entry: Immutable collection containing the entry data. + - widgetFor: Returns the appropriate widget preview component for a given field. + - [widgetsFor](#lists-and-objects): Returns an array of objects with widgets and associated field data. For use with list and object type entries. + - getAsset: Returns the correct filePath or in-memory preview for uploaded images. **Example:** ```html @@ -87,7 +88,9 @@ Registers a template for a collection. CMS.registerPreviewTemplate("posts", PostPreview); ``` + ### Lists and Objects + The API for accessing the individual fields of list- and object-type entries is similar to the API for accessing fields in standard entries, but there are a few key differences. Access to these nested fields is facilitated through the `widgetsFor` function, which is passed to the preview @@ -96,6 +99,7 @@ Registers a template for a collection. Immutable.js. If some of the methods that we use are unfamiliar, such as `getIn`, check out [their docs](https://facebook.github.io/immutable-js/docs/#/) to get a better understanding. **List Example:** + ```html ``` + **Object Example:** + ```html ``` + ### Accessing Metadata - Preview Components also receive an additional prop: `fieldsMetaData`. It contains aditional information (besides the plain plain textual value of each field) that can be useful for preview purposes. + + Preview Components also receive an additional prop: `fieldsMetaData`. It contains aditional information (besides the plain plain textual value of each field) that can be useful for preview purposes. For example, the Relation widget passes the whole selected relation data in `fieldsMetaData`. + ```js export default class ArticlePreview extends React.Component { render() { - const {entry, fieldsMetaData} = this.props; + const { entry, fieldsMetaData } = this.props; const author = fieldsMetaData.getIn(['authors', data.author]); - return

    { entry.getIn(['data', 'title']) }

    - {author &&} -
    + return ( +
    +

    {entry.getIn(['data', 'title'])}

    + {author && } +
    + ); } } ``` diff --git a/website/content/docs/examples.md b/website/content/docs/examples.md index bf47c32f..9a05dceb 100644 --- a/website/content/docs/examples.md +++ b/website/content/docs/examples.md @@ -6,10 +6,10 @@ group: start Do you have a great, open source example? Submit a pull request to this page! -Example | Tools | Type | Source | More info | ---- | --- | --- | --- | --- -[This Developing Journey](https://briandouglas.me) | middleman | blog | [bdougie/blog](https://github.com/bdougie/blog) | [blog post](https://www.netlify.com/blog/2017/04/20/creating-a-blog-with-middleman-and-netlify-cms/) -[JAMstack Recipes](https://jamstack-cms.netlify.com) | Hugo, Azure | demo | [hlaueriksson/jamstack-cms](https://github.com/hlaueriksson/jamstack-cms) | [blog post](http://conductofcode.io/post/managing-content-for-a-jamstack-site-with-netlify-cms/) -[The Ragasirtahk Blog](https://www.ragasirtahk.tk/) | Hugo | blog | [ragasirtahk/the-ragasirtahk-blog](https://github.com/ragasirtahk/the-ragasirtahk-blog) | [blog post](https://www.ragasirtahk.tk/2018/01/setting-up-netlify-cms-on-hugo/) -[Forest Garden Wales](https://www.forestgarden.wales/) | Hugo | blog | [forestgardenwales/forestgarden.wales](https://github.com/forestgardenwales/forestgarden.wales) | [blog post](https://www.forestgarden.wales/blog/now-using-netlify-cms/) -[Cup of Data](https://www.cupofdata.com/blog) | Gatsby | blog | [cupofdata/cupofdata.com](https://github.com/cupofdata/cupofdata.com) | - +| Example | Tools | Type | Source | More info | +| ------------------------------------------------------ | ----------- | ---- | ----------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| [This Developing Journey](https://briandouglas.me) | middleman | blog | [bdougie/blog](https://github.com/bdougie/blog) | [blog post](https://www.netlify.com/blog/2017/04/20/creating-a-blog-with-middleman-and-netlify-cms/) | +| [JAMstack Recipes](https://jamstack-cms.netlify.com) | Hugo, Azure | demo | [hlaueriksson/jamstack-cms](https://github.com/hlaueriksson/jamstack-cms) | [blog post](http://conductofcode.io/post/managing-content-for-a-jamstack-site-with-netlify-cms/) | +| [The Ragasirtahk Blog](https://www.ragasirtahk.tk/) | Hugo | blog | [ragasirtahk/the-ragasirtahk-blog](https://github.com/ragasirtahk/the-ragasirtahk-blog) | [blog post](https://www.ragasirtahk.tk/2018/01/setting-up-netlify-cms-on-hugo/) | +| [Forest Garden Wales](https://www.forestgarden.wales/) | Hugo | blog | [forestgardenwales/forestgarden.wales](https://github.com/forestgardenwales/forestgarden.wales) | [blog post](https://www.forestgarden.wales/blog/now-using-netlify-cms/) | +| [Cup of Data](https://www.cupofdata.com/blog) | Gatsby | blog | [cupofdata/cupofdata.com](https://github.com/cupofdata/cupofdata.com) | - | diff --git a/website/content/docs/intro.md b/website/content/docs/intro.md index e6efc807..e9bc8068 100755 --- a/website/content/docs/intro.md +++ b/website/content/docs/intro.md @@ -8,13 +8,12 @@ Netlify CMS is an open source content management system for your Git workflow th At its core, Netlify CMS is an open-source React app that acts as a wrapper for the Git workflow, using the GitHub, GitLab, or Bitbucket API. This provides many advantages, including: -* **Fast, web-based UI:** with rich-text editing, real-time preview, and drag-and-drop media uploads. -* **Platform agnostic:** works with most static site generators. -* **Easy installation:** add two files to your site and hook up the backend by including in your build process or linking to our CDN. -* **Modern authentication:** using GitHub, GitLab, or Bitbucket and JSON web tokens. -* **Flexible content types:** specify an unlimited number of content types with custom fields. -* **Fully extensible:** create custom-styled previews, UI widgets, and editor plugins. - +- **Fast, web-based UI:** with rich-text editing, real-time preview, and drag-and-drop media uploads. +- **Platform agnostic:** works with most static site generators. +- **Easy installation:** add two files to your site and hook up the backend by including in your build process or linking to our CDN. +- **Modern authentication:** using GitHub, GitLab, or Bitbucket and JSON web tokens. +- **Flexible content types:** specify an unlimited number of content types with custom fields. +- **Fully extensible:** create custom-styled previews, UI widgets, and editor plugins. ## Find out more diff --git a/website/content/docs/start-with-a-template.md b/website/content/docs/start-with-a-template.md index 7f971d33..f172b304 100644 --- a/website/content/docs/start-with-a-template.md +++ b/website/content/docs/start-with-a-template.md @@ -25,11 +25,11 @@ After clicking one of those buttons, you’ll authenticate with GitHub or GitLab 1. The template deploy process will send you an invitation to your new site, sent from `no-reply@netlify.com`. - ![Sample email subject line: You've been invited to join radiologist-amanda-53841.netlify.com](https://www.netlifycms.org/img/email-subject.png?raw=true) + ![Sample email subject line: You've been invited to join radiologist-amanda-53841.netlify.com](https://www.netlifycms.org/img/email-subject.png?raw=true) 2. Click the link to accept the invite, and you’ll be directed to your site, with a prompt to create a password. - !["Complete your signup" modal on the Kaldi coffee site](https://www.netlifycms.org/img/create-password.png?raw=true) + !["Complete your signup" modal on the Kaldi coffee site](https://www.netlifycms.org/img/create-password.png?raw=true) 3. Enter a password, sign in, and you’ll be directed straight to the CMS. (For future visits, you can go straight to `/admin/`.) diff --git a/website/content/docs/update-the-cms-version.md b/website/content/docs/update-the-cms-version.md index b6650ae9..cd35917c 100644 --- a/website/content/docs/update-the-cms-version.md +++ b/website/content/docs/update-the-cms-version.md @@ -15,11 +15,12 @@ If you are using a package manager like Yarn or NPM, you will use their standard If you are using the CMS through a CDN like Unpkg, then that depends on the version tag you are using. You can find the version tag you are using in the `/admin/index.html` file of your site. - (Recommended) If you use `^2.0.0`, the CMS will do all updates except major versions automatically. - - It will upgrade to `2.0.1`, `2.1.0`, `2.1.2`. - - It will not upgrade to `3.0.0` or higher. - - It will not upgrade to beta versions. + + - It will upgrade to `2.0.1`, `2.1.0`, `2.1.2`. + - It will not upgrade to `3.0.0` or higher. + - It will not upgrade to beta versions. - If you use `~2.0.0`, the CMS will do only patch updates automatically. - - It will upgrade `2.0.1`, `2.0.2`. - - It will not upgrade to `2.1.0` or higher. - - It will not upgrade beta versions. + - It will upgrade `2.0.1`, `2.0.2`. + - It will not upgrade to `2.1.0` or higher. + - It will not upgrade beta versions. diff --git a/website/content/docs/widgets/boolean.md b/website/content/docs/widgets/boolean.md index 083cee69..420c7a94 100644 --- a/website/content/docs/widgets/boolean.md +++ b/website/content/docs/widgets/boolean.md @@ -1,5 +1,5 @@ --- -label: "Boolean" +label: 'Boolean' target: boolean --- @@ -13,5 +13,5 @@ The boolean widget translates a toggle switch input to a true/false value. - **Example:** ```yaml - - {label: "Draft", name: "draft", widget: "boolean", default: true} + - { label: 'Draft', name: 'draft', widget: 'boolean', default: true } ``` diff --git a/website/content/docs/widgets/date.md b/website/content/docs/widgets/date.md index 42e6b273..f2c44257 100644 --- a/website/content/docs/widgets/date.md +++ b/website/content/docs/widgets/date.md @@ -1,5 +1,5 @@ --- -label: "Date" +label: 'Date' target: date --- @@ -14,9 +14,9 @@ The date widget translates a date picker input to a date string. For saving date - **Example:** ```yaml - - label: "Birthdate" - name: "birthdate" - widget: "date" - default: "" - format: "MMM Do YY" + - label: 'Birthdate' + name: 'birthdate' + widget: 'date' + default: '' + format: 'MMM Do YY' ``` diff --git a/website/content/docs/widgets/datetime.md b/website/content/docs/widgets/datetime.md index 65f2700b..c01ebe4d 100644 --- a/website/content/docs/widgets/datetime.md +++ b/website/content/docs/widgets/datetime.md @@ -1,5 +1,5 @@ --- -label: "DateTime" +label: 'DateTime' target: datetime --- @@ -14,9 +14,9 @@ The datetime widget translates a datetime picker to a datetime string. For savin - **Example:** ```yaml - - label: "Start time" - name: "start" - widget: "datetime" - default: "" - format: "LLL" + - label: 'Start time' + name: 'start' + widget: 'datetime' + default: '' + format: 'LLL' ``` diff --git a/website/content/docs/widgets/file.md b/website/content/docs/widgets/file.md index dbe3186c..11392cd2 100644 --- a/website/content/docs/widgets/file.md +++ b/website/content/docs/widgets/file.md @@ -1,5 +1,5 @@ --- -label: "File" +label: 'File' target: file --- @@ -13,8 +13,8 @@ The file widget allows editors to upload a file or select an existing one from t - **Example:** ```yaml - - label: "Manual PDF" - name: "manual_pdf" - widget: "file" - default: "/uploads/general-manual.pdf" + - label: 'Manual PDF' + name: 'manual_pdf' + widget: 'file' + default: '/uploads/general-manual.pdf' ``` diff --git a/website/content/docs/widgets/hidden.md b/website/content/docs/widgets/hidden.md index 4e5fffeb..336049db 100644 --- a/website/content/docs/widgets/hidden.md +++ b/website/content/docs/widgets/hidden.md @@ -1,5 +1,5 @@ --- -label: "Hidden" +label: 'Hidden' target: hidden --- @@ -8,10 +8,10 @@ Hidden widgets do not display in the UI. In folder collections that allow users - **Name:** `hidden` - **UI:** none - **Data type:** any valid data type -- **Options:** +- **Options:** - `default`: accepts any valid data type; recommended for collections that allow adding new items - **Example:** ```yaml - - {label: "Layout", name: "layout", widget: "hidden", default: "blog"} + - { label: 'Layout', name: 'layout', widget: 'hidden', default: 'blog' } ``` diff --git a/website/content/docs/widgets/image.md b/website/content/docs/widgets/image.md index 4af9f8bf..f0d0b5c8 100644 --- a/website/content/docs/widgets/image.md +++ b/website/content/docs/widgets/image.md @@ -1,5 +1,5 @@ --- -label: "Image" +label: 'Image' target: image --- @@ -13,8 +13,8 @@ The image widget allows editors to upload an image or select an existing one fro - **Example:** ```yaml - - label: "Featured Image" - name: "thumbnail" - widget: "image" - default: "/uploads/chocolate-dogecoin.jpg" + - label: 'Featured Image' + name: 'thumbnail' + widget: 'image' + default: '/uploads/chocolate-dogecoin.jpg' ``` diff --git a/website/content/docs/widgets/index.md b/website/content/docs/widgets/index.md index ede4a16f..2270f758 100644 --- a/website/content/docs/widgets/index.md +++ b/website/content/docs/widgets/index.md @@ -10,20 +10,20 @@ Widgets are specified as collection fields in the Netlify CMS `config.yml` file. To see working examples of all of the built-in widgets, try making a 'Kitchen Sink' collection item on the [CMS demo site](https://cms-demo.netlify.com). (No login required: click the login button and the CMS will open.) You can refer to the demo [configuration code](https://github.com/netlify/netlify-cms/blob/master/packages/netlify-cms/example/config.yml) to see how each field was configured. - ## Common widget options The following options are available on all fields: - `required`: specify as `false` to make a field optional; defaults to `true` - `pattern`: add field validation by specifying a list with a [regex pattern](https://regexr.com/) and an error message; more extensive validation can be achieved with [custom widgets](https://www.netlifycms.org/docs/custom-widgets/#advanced-field-validation) + - **Example:** ```yaml - - label: "Title" - name: "title" - widget: "string" - pattern: [".{12,}", "Must have at least 12 characters"] + - label: 'Title' + name: 'title' + widget: 'string' + pattern: ['.{12,}', 'Must have at least 12 characters'] ``` ## Default widgets diff --git a/website/content/docs/widgets/list.md b/website/content/docs/widgets/list.md index 41181f29..6d733e17 100644 --- a/website/content/docs/widgets/list.md +++ b/website/content/docs/widgets/list.md @@ -1,5 +1,5 @@ --- -label: "List" +label: 'List' target: list --- @@ -16,44 +16,44 @@ The list widget allows you to create a repeatable item in the UI which saves as - **Example** (`field`/`fields` not specified): ```yaml - - label: "Tags" - name: "tags" - widget: "list" - default: ["news"] + - label: 'Tags' + name: 'tags' + widget: 'list' + default: ['news'] ``` - **Example** (`allow_add` marked `false`): ```yaml - - label: "Tags" - name: "tags" - widget: "list" + - label: 'Tags' + name: 'tags' + widget: 'list' allow_add: false - default: ["news"] + default: ['news'] ``` - **Example** (with `field`): ```yaml - - label: "Gallery" - name: "galleryImages" - widget: "list" + - label: 'Gallery' + name: 'galleryImages' + widget: 'list' field: - - {label: Image, name: image, widget: image} + - { label: Image, name: image, widget: image } ``` - **Example** (with `fields`): ```yaml - - label: "Testimonials" - name: "testimonials" - widget: "list" + - label: 'Testimonials' + name: 'testimonials' + widget: 'list' fields: - - {label: Quote, name: quote, widget: string, default: "Everything is awesome!"} + - { label: Quote, name: quote, widget: string, default: 'Everything is awesome!' } - label: Author name: author widget: object fields: - - {label: Name, name: name, widget: string, default: "Emmet"} - - {label: Avatar, name: avatar, widget: image, default: "/img/emmet.jpg"} + - { label: Name, name: name, widget: string, default: 'Emmet' } + - { label: Avatar, name: avatar, widget: image, default: '/img/emmet.jpg' } ``` diff --git a/website/content/docs/widgets/markdown.md b/website/content/docs/widgets/markdown.md index 2df28322..c5b52ffb 100644 --- a/website/content/docs/widgets/markdown.md +++ b/website/content/docs/widgets/markdown.md @@ -1,11 +1,11 @@ --- -label: "Markdown" +label: 'Markdown' target: markdown --- The markdown widget provides a full fledged text editor - which is based on [slate](https://github.com/ianstormtaylor/slate) - that allows users to format text with features such as headings and blockquotes. Users are also allowed to write in markdown by simply flipping a switch. -*Please note:* in case you want to use your markdown editor to fill a markdown's file content after the frontmatter, you'll have name the field as `body` so then the CMS can recognize it and save the file accordingly. +_Please note:_ in case you want to use your markdown editor to fill a markdown's file content after the frontmatter, you'll have name the field as `body` so then the CMS can recognize it and save the file accordingly. - **Name:** `markdown` - **UI:** full text editor @@ -16,10 +16,9 @@ The markdown widget provides a full fledged text editor - which is based on [sla - **Example:** ```yaml - - {label: "Blog post content", name: "body", widget: "markdown"} + - { label: 'Blog post content', name: 'body', widget: 'markdown' } ``` This would render as: ![Markdown widget example](/img/widgets-markdown.png) - diff --git a/website/content/docs/widgets/number.md b/website/content/docs/widgets/number.md index 383f35f4..aaa3f5ba 100644 --- a/website/content/docs/widgets/number.md +++ b/website/content/docs/widgets/number.md @@ -1,5 +1,5 @@ --- -label: "Number" +label: 'Number' target: number --- @@ -16,11 +16,11 @@ The number widget uses an HTML number input, saving the value as a string, integ - **Example:** ```yaml - - label: "Puppy Count" - name: "puppies" - widget: "number" + - label: 'Puppy Count' + name: 'puppies' + widget: 'number' default: 2 - valueType: "int" + valueType: 'int' min: 1 max: 101 ``` diff --git a/website/content/docs/widgets/object.md b/website/content/docs/widgets/object.md index fb3eb6c8..fbb1b715 100644 --- a/website/content/docs/widgets/object.md +++ b/website/content/docs/widgets/object.md @@ -1,5 +1,5 @@ --- -label: "Object" +label: 'Object' target: object --- @@ -14,22 +14,22 @@ The object widget allows you to group multiple widgets together, nested under a - **Example:** ```yaml - - label: "Profile" - name: "profile" - widget: "object" + - label: 'Profile' + name: 'profile' + widget: 'object' fields: - - {label: "Public", name: "public", widget: "boolean", default: true} - - {label: "Name", name: "name", widget: "string"} - - label: "Birthdate" - name: "birthdate" - widget: "date" - default: "" - format: "MM/DD/YYYY" - - label: "Address" - name: "address" - widget: "object" - fields: - - {label: "Street Address", name: "street", widget: "string"} - - {label: "City", name: "city", widget: "string"} - - {label: "Postal Code", name: "post-code", widget: "string"} + - { label: 'Public', name: 'public', widget: 'boolean', default: true } + - { label: 'Name', name: 'name', widget: 'string' } + - label: 'Birthdate' + name: 'birthdate' + widget: 'date' + default: '' + format: 'MM/DD/YYYY' + - label: 'Address' + name: 'address' + widget: 'object' + fields: + - { label: 'Street Address', name: 'street', widget: 'string' } + - { label: 'City', name: 'city', widget: 'string' } + - { label: 'Postal Code', name: 'post-code', widget: 'string' } ``` diff --git a/website/content/docs/widgets/relation.md b/website/content/docs/widgets/relation.md index 6d5af6f4..e411453c 100644 --- a/website/content/docs/widgets/relation.md +++ b/website/content/docs/widgets/relation.md @@ -1,5 +1,5 @@ --- -label: "Relation" +label: 'Relation' target: relation --- @@ -17,12 +17,13 @@ The relation widget allows you to reference items from another collection. It pr - **Example** (assuming a separate "authors" collection with "name" and "twitterHandle" fields): ```yaml - - label: "Post Author" - name: "author" - widget: "relation" - collection: "authors" - searchFields: ["name", "twitterHandle"] - valueField: "name" - displayFields: ["twitterHandle", "followerCount"] + - label: 'Post Author' + name: 'author' + widget: 'relation' + collection: 'authors' + searchFields: ['name', 'twitterHandle'] + valueField: 'name' + displayFields: ['twitterHandle', 'followerCount'] ``` + The generated UI input will search the authors collection by name and twitterHandle, and display each author's handle and follower count. On selection, the author name will be saved for the field. diff --git a/website/content/docs/widgets/select.md b/website/content/docs/widgets/select.md index 2cf37ed7..46220d02 100644 --- a/website/content/docs/widgets/select.md +++ b/website/content/docs/widgets/select.md @@ -1,5 +1,5 @@ --- -label: "Select" +label: 'Select' target: select --- @@ -11,25 +11,25 @@ The select widget allows you to pick a single string value from a dropdown menu. - **Options:** - `default`: accepts a string; defaults to an empty string - `options`: (**required**) a list of options for the dropdown menu; can be listed in two ways: - - string values: the label displayed in the dropdown is the value saved in the file - - object with `label` and `value` fields: the label displays in the dropdown; the value is saved in the file + - string values: the label displayed in the dropdown is the value saved in the file + - object with `label` and `value` fields: the label displays in the dropdown; the value is saved in the file - **Example** (options as strings): ```yaml - - label: "Align Content" - name: "align" - widget: "select" - options: ["left", "center", "right"] + - label: 'Align Content' + name: 'align' + widget: 'select' + options: ['left', 'center', 'right'] ``` + - **Example** (options as objects): ```yaml - - label: "City" - name: "airport-code" - widget: "select" + - label: 'City' + name: 'airport-code' + widget: 'select' options: - - { label: "Chicago", value: "ORD" } - - { label: "Paris", value: "CDG" } - - { label: "Tokyo", value: "HND" } + - { label: 'Chicago', value: 'ORD' } + - { label: 'Paris', value: 'CDG' } + - { label: 'Tokyo', value: 'HND' } ``` - diff --git a/website/content/docs/widgets/string.md b/website/content/docs/widgets/string.md index bb5e8afb..346c8d00 100644 --- a/website/content/docs/widgets/string.md +++ b/website/content/docs/widgets/string.md @@ -1,5 +1,5 @@ --- -label: "String" +label: 'String' target: string --- @@ -13,5 +13,5 @@ The string widget translates a basic text input to a string value. For larger te - **Example:** ```yaml - - {label: "Title", name: "title", widget: "string"} + - { label: 'Title', name: 'title', widget: 'string' } ``` diff --git a/website/content/docs/widgets/text.md b/website/content/docs/widgets/text.md index f1e0392d..25332ef5 100644 --- a/website/content/docs/widgets/text.md +++ b/website/content/docs/widgets/text.md @@ -1,5 +1,5 @@ --- -label: "Text" +label: 'Text' target: text --- @@ -13,6 +13,5 @@ The text widget takes a multiline text field and saves it as a string. For short - **Example:** ```yaml - - {label: "Description", name: "description", widget: "text"} + - { label: 'Description', name: 'description', widget: 'text' } ``` - diff --git a/website/content/pages/community.md b/website/content/pages/community.md index 3561904b..1d75a9f8 100644 --- a/website/content/pages/community.md +++ b/website/content/pages/community.md @@ -2,13 +2,16 @@ title: Community headline: Be a part of building the CMS of the future. -subhead: We’re serious about being community driven, so everyone is welcome to join the [community chat](https://gitter.im/netlify/NetlifyCMS), and to be a part of our bi-weekly planning sessions (details below). -primarycta: "[Register on Eventbrite →](https://www.eventbrite.com/e/netlify-cms-planning-session-bi-weekly-tickets-35794058994)" +subhead: + We’re serious about being community driven, so everyone is welcome to join the [community chat](https://gitter.im/netlify/NetlifyCMS), and to be a part of our bi-weekly planning sessions (details below). +primarycta: + '[Register on Eventbrite →](https://www.eventbrite.com/e/netlify-cms-planning-session-bi-weekly-tickets-35794058994)' upcomingevent: hook: The next development planning session is on -howitworks: Every other week we have public development planning sessions. They're web based, last about an hour, and are geared toward contributors and those interested in contributing. Sessions currently take place every other Wednesday, 9am - 10am PT. +howitworks: + Every other week we have public development planning sessions. They're web based, last about an hour, and are geared toward contributors and those interested in contributing. Sessions currently take place every other Wednesday, 9am - 10am PT. howtojoin: | **On the web:** diff --git a/website/data/global.yaml b/website/data/global.yaml index e5addedc..9ce2adc4 100644 --- a/website/data/global.yaml +++ b/website/data/global.yaml @@ -1,6 +1,6 @@ footer: buttons: - - name: "Twitter" - url: "https://twitter.com/netlifycms" - - name: "GitHub" - url: "https://github.com/netlify/netlify-cms" + - name: 'Twitter' + url: 'https://twitter.com/netlifycms' + - name: 'GitHub' + url: 'https://github.com/netlify/netlify-cms' diff --git a/website/data/landing.yaml b/website/data/landing.yaml index 460899c2..8bcac2ca 100644 --- a/website/data/landing.yaml +++ b/website/data/landing.yaml @@ -1,41 +1,52 @@ hero: - headline: "Open source content management for your Git workflow" - subhead: "Use Netlify CMS with any static site generator for a faster and more flexible web project" + headline: 'Open source content management for your Git workflow' + subhead: + 'Use Netlify CMS with any static site generator for a faster and more flexible web project' devfeatures: - - feature: "Static + content management = ♥" - description: "Get the speed, security, and scalability of a static site, while still providing a convenient editing interface for content." - - feature: "An integrated part of your Git workflow" - description: "Content is stored in your Git repository alongside your code for easier versioning, multi-channel publishing, and the option to handle content updates directly in Git." - - feature: "An extensible CMS built on React" - description: "Netlify CMS is built as a single-page React app. Create custom-styled previews, UI widgets, and editor plugins or add backends to support different Git platform APIs." + - feature: 'Static + content management = ♥' + description: + 'Get the speed, security, and scalability of a static site, while still providing a convenient editing interface for content.' + - feature: 'An integrated part of your Git workflow' + description: + 'Content is stored in your Git repository alongside your code for easier versioning, multi-channel publishing, and the option to handle content updates directly in Git.' + - feature: 'An extensible CMS built on React' + description: + 'Netlify CMS is built as a single-page React app. Create custom-styled previews, UI widgets, and editor plugins or add backends to support different Git platform APIs.' cta: - primaryhook: "Getting started is simple and free." - primary: "Choose a template that’s pre-configured with a static site generator and deploys to a global CDN in one click." - button: "[Get started](/docs/start-with-a-template/)" + primaryhook: 'Getting started is simple and free.' + primary: + 'Choose a template that’s pre-configured with a static site generator and deploys to a global CDN in one click.' + button: '[Get started](/docs/start-with-a-template/)' editors: - hook: "A CMS that developers and content editors can agree on" - intro: "You get to implement modern front end tools to deliver a faster, safer, and more scalable site. Editors get a friendly UI and intuitive workflow that meets their content management requirements." + hook: 'A CMS that developers and content editors can agree on' + intro: + 'You get to implement modern front end tools to deliver a faster, safer, and more scalable site. Editors get a friendly UI and intuitive workflow that meets their content management requirements.' features: - - feature: "Editor-friendly user interface" - description: "The web-based app includes rich-text editing, real-time preview, and drag-and-drop media uploads." - imgpath: "feature-editor.svg" - - feature: "Intuitive workflow for content teams" - description: "Writers and editors can easily manage content from draft to review to publish across any number of custom content types." - imgpath: "feature-workflow.svg" - - feature: "Instant access without GitHub account" - description: "With [Git Gateway](/docs/authentication-backends/#git-gateway-with-netlify-identity), you can add CMS access for any team member — even if they don’t have a GitHub account. " - imgpath: "feature-access.svg" + - feature: 'Editor-friendly user interface' + description: + 'The web-based app includes rich-text editing, real-time preview, and drag-and-drop media uploads.' + imgpath: 'feature-editor.svg' + - feature: 'Intuitive workflow for content teams' + description: + 'Writers and editors can easily manage content from draft to review to publish across any number of custom content types.' + imgpath: 'feature-workflow.svg' + - feature: 'Instant access without GitHub account' + description: + 'With [Git Gateway](/docs/authentication-backends/#git-gateway-with-netlify-identity), you can add CMS access for any team member — even if they don’t have a GitHub account. ' + imgpath: 'feature-access.svg' community: - hook: "Supported by a growing community" + hook: 'Supported by a growing community' features: - - feature: "Built on the JAMstack" - description: "Netlify CMS is based on client-side JavaScript, reusable APIs and prebuilt Markup. Compared to server-side CMS like WordPress, this means better performance, higher security, lower cost of scaling, and a better developer experience. You can learn more about the JAMstack on [jamstack.org](https://jamstack.org)." - - feature: "Support when you need it" - description: "Get up and running with comprehensive [documentation](/docs) and templates or work through difficult problems with help from the community on [Gitter](https://gitter.im/netlify/NetlifyCMS)." - - feature: "A community-driven project you can help evolve" - description: "Netlify CMS is built by a community of more than 100 contributors — and you can help. Join our [bi-weekly planning sessions](/community) or read the [contributing guide](/docs/contributor-guide) to join in." - contributors: "Made possible by awesome contributors" - + - feature: 'Built on the JAMstack' + description: + 'Netlify CMS is based on client-side JavaScript, reusable APIs and prebuilt Markup. Compared to server-side CMS like WordPress, this means better performance, higher security, lower cost of scaling, and a better developer experience. You can learn more about the JAMstack on [jamstack.org](https://jamstack.org).' + - feature: 'Support when you need it' + description: + 'Get up and running with comprehensive [documentation](/docs) and templates or work through difficult problems with help from the community on [Gitter](https://gitter.im/netlify/NetlifyCMS).' + - feature: 'A community-driven project you can help evolve' + description: + 'Netlify CMS is built by a community of more than 100 contributors — and you can help. Join our [bi-weekly planning sessions](/community) or read the [contributing guide](/docs/contributor-guide) to join in.' + contributors: 'Made possible by awesome contributors' diff --git a/website/data/notifications.yml b/website/data/notifications.yml index 8b54ffb9..3811d56e 100644 --- a/website/data/notifications.yml +++ b/website/data/notifications.yml @@ -1,28 +1,29 @@ notifications: -- loud: true - message: >- - Register to join us online for our next community dev meeting, every other - Wednesday at 9am-10am PT! - published: false - title: Netlify CMS Development Planning Sessions Promo - url: >- - https://www.eventbrite.com/e/netlify-cms-planning-session-bi-weekly-tickets-35794058994 -- loud: true - message: >- - We have a community on Gitter - join now to ask questions and discuss the - project with other devs! - published: false - title: Gitter shoutout - url: 'https://gitter.im/netlify/netlifycms' -- loud: true - message: >- - Netlify CMS now supports GitLab! - published: false - title: GitLab announcement - url: '/blog/2018/06/netlify-cms-now-supports-gitlab-as-a-backend/' -- loud: true - message: >- - Announcing 2.0 - Bitbucket support and monorepo architecture! - published: true - title: 2.0 announcement - url: '/blog/2018/07/netlify-cms-2-0-launches-with-bitbucket-support-and-a-new-monorepo-architecture' + - loud: true + message: >- + Register to join us online for our next community dev meeting, every other + Wednesday at 9am-10am PT! + published: false + title: Netlify CMS Development Planning Sessions Promo + url: >- + https://www.eventbrite.com/e/netlify-cms-planning-session-bi-weekly-tickets-35794058994 + - loud: true + message: >- + We have a community on Gitter - join now to ask questions and discuss the + project with other devs! + published: false + title: Gitter shoutout + url: 'https://gitter.im/netlify/netlifycms' + - loud: true + message: >- + Netlify CMS now supports GitLab! + published: false + title: GitLab announcement + url: '/blog/2018/06/netlify-cms-now-supports-gitlab-as-a-backend/' + - loud: true + message: >- + Announcing 2.0 - Bitbucket support and monorepo architecture! + published: true + title: 2.0 announcement + url: + '/blog/2018/07/netlify-cms-2-0-launches-with-bitbucket-support-and-a-new-monorepo-architecture' diff --git a/website/gatsby-browser.js b/website/gatsby-browser.js index 16395bdf..48e480dd 100644 --- a/website/gatsby-browser.js +++ b/website/gatsby-browser.js @@ -5,6 +5,6 @@ exports.onClientEntry = () => { new SmoothScroll('a[href*="#"]', { offset() { return document.querySelector('#header').offsetHeight; - } + }, }); }; diff --git a/website/gatsby-config.js b/website/gatsby-config.js index 4af8061b..4f57c791 100644 --- a/website/gatsby-config.js +++ b/website/gatsby-config.js @@ -15,7 +15,7 @@ const postCssPlugins = [ colorfunctions(), hdBackgrounds(), cssextend(), - cssvars({ variables: styleVariables }) + cssvars({ variables: styleVariables }), ]; module.exports = { @@ -27,30 +27,30 @@ module.exports = { docs: [ { name: 'start', - title: 'Quick Start' + title: 'Quick Start', }, { name: 'guides', - title: 'Guides' + title: 'Guides', }, { name: 'reference', - title: 'Reference' + title: 'Reference', }, { name: 'contributing', - title: 'Contributing' - } - ] - } + title: 'Contributing', + }, + ], + }, }, plugins: [ { resolve: 'gatsby-source-filesystem', options: { path: `${__dirname}/content`, - name: 'content' - } + name: 'content', + }, }, 'gatsby-transformer-yaml', 'gatsby-transformer-json', @@ -58,8 +58,8 @@ module.exports = { resolve: 'gatsby-source-filesystem', options: { path: `${__dirname}/data`, - name: 'data' - } + name: 'data', + }, }, { resolve: 'gatsby-transformer-remark', @@ -69,13 +69,13 @@ module.exports = { 'gatsby-remark-autolink-headers', 'gatsby-remark-prismjs' ] - } + }, }, { resolve: 'gatsby-plugin-postcss-sass', options: { - postCssPlugins - } + postCssPlugins, + }, }, 'gatsby-plugin-react-helmet', 'gatsby-plugin-react-next', @@ -89,8 +89,8 @@ module.exports = { background_color: '#ffffff', theme_color: '#ffffff', display: 'standalone', - icon: 'static/img/favicon/icon-512x512.png' - } - } - ] + icon: 'static/img/favicon/icon-512x512.png', + }, + }, + ], }; diff --git a/website/gatsby-node.js b/website/gatsby-node.js index 7b6b8442..c7b4e484 100644 --- a/website/gatsby-node.js +++ b/website/gatsby-node.js @@ -43,8 +43,8 @@ exports.createPages = async ({ graphql, boundActionCreators }) => { path: slug, component: template, context: { - slug - } + slug, + }, }); }); }; @@ -70,7 +70,7 @@ exports.onCreateNode = ({ node, boundActionCreators, getNode }) => { createNodeField({ node, name: 'date', - value: date.toJSON() + value: date.toJSON(), }); } @@ -78,14 +78,14 @@ exports.onCreateNode = ({ node, boundActionCreators, getNode }) => { createNodeField({ node, name: 'slug', - value: slug + value: slug, }); // used to create GitHub edit link createNodeField({ node, name: 'path', - value: relativePath + value: relativePath, }); } }; diff --git a/website/src/components/docs-nav.js b/website/src/components/docs-nav.js index 4b0b1b5e..ba3ce3f6 100644 --- a/website/src/components/docs-nav.js +++ b/website/src/components/docs-nav.js @@ -9,7 +9,7 @@ import Link from 'gatsby-link'; */ class TableOfContents extends Component { state = { - headings: [] + headings: [], }; componentDidMount() { @@ -19,12 +19,12 @@ class TableOfContents extends Component { contentHeadings.forEach(h => { headings.push({ id: h.id, - text: h.innerText + text: h.innerText, }); }); this.setState({ - headings + headings, }); } @@ -52,10 +52,7 @@ const DocsNav = ({ items, location }) => (