Merge branch 'master' into markitup-react
This commit is contained in:
commit
899681ee84
20
.editorconfig
Normal file
20
.editorconfig
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# http://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.js]
|
||||||
|
quote_type = single
|
||||||
|
spaces_around_operators = true
|
||||||
|
|
||||||
|
[*.css]
|
||||||
|
quote_type = single
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
126
.eslintrc
126
.eslintrc
@ -1,114 +1,12 @@
|
|||||||
env:
|
{
|
||||||
browser: true
|
"extends": [
|
||||||
es6: true
|
"eslint-config-netlify"
|
||||||
jest: true
|
],
|
||||||
|
"settings": {
|
||||||
parser: babel-eslint
|
"import/resolver": {
|
||||||
plugins: [
|
"webpack": {
|
||||||
"react",
|
"config": "webpack.dev.js"
|
||||||
"class-property"
|
}
|
||||||
]
|
}
|
||||||
|
}
|
||||||
rules:
|
}
|
||||||
# Possible Errors
|
|
||||||
# https://github.com/eslint/eslint/tree/master/docs/rules#possible-errors
|
|
||||||
no-control-regex: 2
|
|
||||||
no-debugger: 2
|
|
||||||
no-dupe-args: 2
|
|
||||||
no-dupe-keys: 2
|
|
||||||
no-duplicate-case: 2
|
|
||||||
no-empty-character-class: 2
|
|
||||||
no-ex-assign: 2
|
|
||||||
no-extra-boolean-cast : 2
|
|
||||||
no-extra-semi: 2
|
|
||||||
no-invalid-regexp: 2
|
|
||||||
no-irregular-whitespace: 2
|
|
||||||
no-proto: 2
|
|
||||||
no-unexpected-multiline: 2
|
|
||||||
no-unreachable: 2
|
|
||||||
valid-typeof: 2
|
|
||||||
|
|
||||||
# Best Practices
|
|
||||||
# https://github.com/eslint/eslint/tree/master/docs/rules#best-practices
|
|
||||||
no-fallthrough: 2
|
|
||||||
no-redeclare: 2
|
|
||||||
no-constant-condition: 2
|
|
||||||
|
|
||||||
# Stylistic Issues
|
|
||||||
# https://github.com/eslint/eslint/tree/master/docs/rules#stylistic-issues
|
|
||||||
no-alert: 2
|
|
||||||
no-console: [2, { allow: ["warn", "error"] }]
|
|
||||||
comma-spacing: 2
|
|
||||||
eol-last: 2
|
|
||||||
indent: [2, 2, {SwitchCase: 1}]
|
|
||||||
max-len: [2, 160, 2]
|
|
||||||
new-parens: 2
|
|
||||||
no-mixed-spaces-and-tabs: 2
|
|
||||||
no-multiple-empty-lines: [2, {max: 2}]
|
|
||||||
no-trailing-spaces: 2
|
|
||||||
object-curly-spacing: [1, "always"]
|
|
||||||
quotes: [2, "single", "avoid-escape"]
|
|
||||||
semi: 2
|
|
||||||
keyword-spacing: 2
|
|
||||||
space-before-blocks: [2, "always"]
|
|
||||||
space-before-function-paren: [2, "never"]
|
|
||||||
space-in-parens: [2, "never"]
|
|
||||||
space-infix-ops: 2
|
|
||||||
space-unary-ops: 2
|
|
||||||
|
|
||||||
# ECMAScript 6
|
|
||||||
# http://eslint.org/docs/rules/#ecmascript-6
|
|
||||||
arrow-spacing: [2, {"before": true, "after": true}]
|
|
||||||
no-confusing-arrow: 2
|
|
||||||
prefer-const: 2
|
|
||||||
|
|
||||||
# Strict Mode
|
|
||||||
# https://github.com/eslint/eslint/tree/master/docs/rules#strict-mode
|
|
||||||
strict: [2, "global"]
|
|
||||||
|
|
||||||
# Variables
|
|
||||||
# https://github.com/eslint/eslint/tree/master/docs/rules#variables
|
|
||||||
no-undef: 2
|
|
||||||
no-unused-vars: [2, {"args": "none"}]
|
|
||||||
|
|
||||||
|
|
||||||
react/prop-types: 1
|
|
||||||
react/forbid-prop-types: 1
|
|
||||||
react/jsx-boolean-value: 1
|
|
||||||
react/jsx-closing-bracket-location: 1
|
|
||||||
react/jsx-curly-spacing: 1
|
|
||||||
react/jsx-equals-spacing: 1
|
|
||||||
react/jsx-handler-names: 1
|
|
||||||
react/jsx-indent-props: [2, 2]
|
|
||||||
react/jsx-indent: [2, 2]
|
|
||||||
react/jsx-no-bind: 1
|
|
||||||
react/jsx-no-duplicate-props: 1
|
|
||||||
react/jsx-no-undef: 1
|
|
||||||
react/jsx-pascal-case: 1
|
|
||||||
react/jsx-uses-react: 1
|
|
||||||
react/jsx-uses-vars: 1
|
|
||||||
react/no-danger: 1
|
|
||||||
react/no-deprecated: 1
|
|
||||||
react/no-did-mount-set-state: 1
|
|
||||||
react/no-did-update-set-state: 1
|
|
||||||
react/no-direct-mutation-state: 1
|
|
||||||
react/no-is-mounted: 1
|
|
||||||
react/no-multi-comp: 1
|
|
||||||
react/no-string-refs: 1
|
|
||||||
react/no-unknown-property: 1
|
|
||||||
react/prefer-es6-class: 1
|
|
||||||
react/prefer-stateless-function: 1
|
|
||||||
react/react-in-jsx-scope: 1
|
|
||||||
react/require-extension: 1
|
|
||||||
react/self-closing-comp: 1
|
|
||||||
react/sort-comp: 1
|
|
||||||
|
|
||||||
class-property/class-property-semicolon: 2
|
|
||||||
|
|
||||||
# Global scoped method and vars
|
|
||||||
globals:
|
|
||||||
netlify: true
|
|
||||||
require: true
|
|
||||||
process: true
|
|
||||||
module: true
|
|
||||||
CMS_ENV: true
|
|
||||||
|
270
.stylelintrc
Normal file
270
.stylelintrc
Normal file
@ -0,0 +1,270 @@
|
|||||||
|
{
|
||||||
|
"extends": [
|
||||||
|
"stylelint-config-standard",
|
||||||
|
"stylelint-config-css-modules"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"stylelint-declaration-block-order",
|
||||||
|
"stylelint-declaration-use-variable"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"at-rule-no-vendor-prefix": true,
|
||||||
|
"comment-empty-line-before": "never",
|
||||||
|
"declaration-no-important": true,
|
||||||
|
"function-url-no-scheme-relative": true,
|
||||||
|
"function-url-quotes": "always",
|
||||||
|
"max-nesting-depth": 1,
|
||||||
|
"media-feature-name-no-vendor-prefix": true,
|
||||||
|
"number-leading-zero": "never",
|
||||||
|
"number-max-precision": 3,
|
||||||
|
"property-no-vendor-prefix": true,
|
||||||
|
"selector-attribute-quotes": "always",
|
||||||
|
"selector-no-attribute": true,
|
||||||
|
"selector-no-qualifying-type": true,
|
||||||
|
"selector-no-id": true,
|
||||||
|
"selector-no-type": true,
|
||||||
|
"selector-no-universal": true,
|
||||||
|
"selector-no-vendor-prefix": true,
|
||||||
|
"selector-pseudo-element-colon-notation": "double",
|
||||||
|
"selector-pseudo-element-no-unknown": true,
|
||||||
|
"selector-root-no-composition": true,
|
||||||
|
"selector-type-no-unknown": true,
|
||||||
|
"string-quotes": "single",
|
||||||
|
"value-no-vendor-prefix": true,
|
||||||
|
"stylelint-disable-reason": "always-after",
|
||||||
|
|
||||||
|
"declaration-block-properties-order": [
|
||||||
|
[
|
||||||
|
"composes",
|
||||||
|
"position",
|
||||||
|
"top",
|
||||||
|
"right",
|
||||||
|
"bottom",
|
||||||
|
"left",
|
||||||
|
"z-index",
|
||||||
|
"display",
|
||||||
|
"visibility",
|
||||||
|
"flex",
|
||||||
|
"flex-grow",
|
||||||
|
"flex-shrink",
|
||||||
|
"flex-basis",
|
||||||
|
"flex-direction",
|
||||||
|
"flex-flow",
|
||||||
|
"flex-wrap",
|
||||||
|
"align-content",
|
||||||
|
"align-items",
|
||||||
|
"align-self",
|
||||||
|
"justify-content",
|
||||||
|
"order",
|
||||||
|
"float",
|
||||||
|
"clear",
|
||||||
|
"overflow",
|
||||||
|
"overflow-x",
|
||||||
|
"overflow-y",
|
||||||
|
"-webkit-overflow-scrolling",
|
||||||
|
"clip",
|
||||||
|
"box-sizing",
|
||||||
|
"margin",
|
||||||
|
"margin-top",
|
||||||
|
"margin-right",
|
||||||
|
"margin-bottom",
|
||||||
|
"margin-left",
|
||||||
|
"padding",
|
||||||
|
"padding-top",
|
||||||
|
"padding-right",
|
||||||
|
"padding-bottom",
|
||||||
|
"padding-left",
|
||||||
|
"min-width",
|
||||||
|
"min-height",
|
||||||
|
"max-width",
|
||||||
|
"max-height",
|
||||||
|
"width",
|
||||||
|
"height",
|
||||||
|
"outline",
|
||||||
|
"outline-width",
|
||||||
|
"outline-style",
|
||||||
|
"outline-color",
|
||||||
|
"outline-offset",
|
||||||
|
"border",
|
||||||
|
"border-spacing",
|
||||||
|
"border-collapse",
|
||||||
|
"border-width",
|
||||||
|
"border-style",
|
||||||
|
"border-color",
|
||||||
|
"border-top",
|
||||||
|
"border-top-width",
|
||||||
|
"border-top-style",
|
||||||
|
"border-top-color",
|
||||||
|
"border-right",
|
||||||
|
"border-right-width",
|
||||||
|
"border-right-style",
|
||||||
|
"border-right-color",
|
||||||
|
"border-bottom",
|
||||||
|
"border-bottom-width",
|
||||||
|
"border-bottom-style",
|
||||||
|
"border-bottom-color",
|
||||||
|
"border-left",
|
||||||
|
"border-left-width",
|
||||||
|
"border-left-style",
|
||||||
|
"border-left-color",
|
||||||
|
"border-radius",
|
||||||
|
"border-top-left-radius",
|
||||||
|
"border-top-right-radius",
|
||||||
|
"border-bottom-right-radius",
|
||||||
|
"border-bottom-left-radius",
|
||||||
|
"border-image",
|
||||||
|
"border-image-source",
|
||||||
|
"border-image-slice",
|
||||||
|
"border-image-width",
|
||||||
|
"border-image-outset",
|
||||||
|
"border-image-repeat",
|
||||||
|
"border-top-image",
|
||||||
|
"border-right-image",
|
||||||
|
"border-bottom-image",
|
||||||
|
"border-left-image",
|
||||||
|
"border-corner-image",
|
||||||
|
"border-top-left-image",
|
||||||
|
"border-top-right-image",
|
||||||
|
"border-bottom-right-image",
|
||||||
|
"border-bottom-left-image",
|
||||||
|
"background",
|
||||||
|
"background-color",
|
||||||
|
"background-image",
|
||||||
|
"background-attachment",
|
||||||
|
"background-position",
|
||||||
|
"background-position-x",
|
||||||
|
"background-position-y",
|
||||||
|
"background-clip",
|
||||||
|
"background-origin",
|
||||||
|
"background-size",
|
||||||
|
"background-repeat",
|
||||||
|
"box-decoration-break",
|
||||||
|
"box-shadow",
|
||||||
|
"color",
|
||||||
|
"table-layout",
|
||||||
|
"caption-side",
|
||||||
|
"empty-cells",
|
||||||
|
"list-style",
|
||||||
|
"list-style-position",
|
||||||
|
"list-style-type",
|
||||||
|
"list-style-image",
|
||||||
|
"quotes",
|
||||||
|
"content",
|
||||||
|
"counter-increment",
|
||||||
|
"counter-reset",
|
||||||
|
"-ms-writing-mode",
|
||||||
|
"vertical-align",
|
||||||
|
"text-align",
|
||||||
|
"text-align-last",
|
||||||
|
"text-decoration",
|
||||||
|
"text-emphasis",
|
||||||
|
"text-emphasis-position",
|
||||||
|
"text-emphasis-style",
|
||||||
|
"text-emphasis-color",
|
||||||
|
"text-indent",
|
||||||
|
"text-justify",
|
||||||
|
"text-outline",
|
||||||
|
"text-transform",
|
||||||
|
"text-wrap",
|
||||||
|
"text-overflow",
|
||||||
|
"text-overflow-ellipsis",
|
||||||
|
"text-overflow-mode",
|
||||||
|
"text-shadow",
|
||||||
|
"text-rendering",
|
||||||
|
"white-space",
|
||||||
|
"word-spacing",
|
||||||
|
"word-wrap",
|
||||||
|
"word-break",
|
||||||
|
"tab-size",
|
||||||
|
"hyphens",
|
||||||
|
"letter-spacing",
|
||||||
|
"font",
|
||||||
|
"font-weight",
|
||||||
|
"font-style",
|
||||||
|
"font-variant",
|
||||||
|
"font-size-adjust",
|
||||||
|
"font-stretch",
|
||||||
|
"font-size",
|
||||||
|
"font-family",
|
||||||
|
"font-feature-settings",
|
||||||
|
"-webkit-font-smoothing",
|
||||||
|
"-moz-osx-font-smoothing",
|
||||||
|
"src",
|
||||||
|
"line-height",
|
||||||
|
"opacity",
|
||||||
|
"filter",
|
||||||
|
"resize",
|
||||||
|
"cursor",
|
||||||
|
"nav-index",
|
||||||
|
"nav-up",
|
||||||
|
"nav-right",
|
||||||
|
"nav-down",
|
||||||
|
"nav-left",
|
||||||
|
"transition",
|
||||||
|
"transition-delay",
|
||||||
|
"transition-timing-function",
|
||||||
|
"transition-duration",
|
||||||
|
"transition-property",
|
||||||
|
"transform",
|
||||||
|
"transform-origin",
|
||||||
|
"animation",
|
||||||
|
"animation-name",
|
||||||
|
"animation-duration",
|
||||||
|
"animation-play-state",
|
||||||
|
"animation-timing-function",
|
||||||
|
"animation-delay",
|
||||||
|
"animation-iteration-count",
|
||||||
|
"animation-direction",
|
||||||
|
"animation-fill-mode",
|
||||||
|
"pointer-events",
|
||||||
|
"unicode-bidi",
|
||||||
|
"direction",
|
||||||
|
"columns",
|
||||||
|
"column-span",
|
||||||
|
"column-width",
|
||||||
|
"column-count",
|
||||||
|
"column-fill",
|
||||||
|
"column-gap",
|
||||||
|
"column-rule",
|
||||||
|
"column-rule-width",
|
||||||
|
"column-rule-style",
|
||||||
|
"column-rule-color",
|
||||||
|
"break-before",
|
||||||
|
"break-inside",
|
||||||
|
"break-after",
|
||||||
|
"page-break-before",
|
||||||
|
"page-break-inside",
|
||||||
|
"page-break-after",
|
||||||
|
"orphans",
|
||||||
|
"widows",
|
||||||
|
"zoom",
|
||||||
|
"max-zoom",
|
||||||
|
"min-zoom",
|
||||||
|
"user-zoom",
|
||||||
|
"orientation",
|
||||||
|
"user-select",
|
||||||
|
"fill",
|
||||||
|
"stroke"
|
||||||
|
],
|
||||||
|
{ "unspecified": "bottomAlphabetical" }
|
||||||
|
],
|
||||||
|
|
||||||
|
"plugin/declaration-block-order": [
|
||||||
|
"custom-properties",
|
||||||
|
"dollar-variables",
|
||||||
|
"declarations",
|
||||||
|
"rules",
|
||||||
|
"at-rules"
|
||||||
|
],
|
||||||
|
|
||||||
|
"sh-waqar/declaration-use-variable": [
|
||||||
|
[
|
||||||
|
"/color/",
|
||||||
|
"z-index",
|
||||||
|
"font-size",
|
||||||
|
"border-radius",
|
||||||
|
"/background/"
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
3
.travis.yml
Normal file
3
.travis.yml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
language: node_js
|
||||||
|
node_js:
|
||||||
|
- "6.0"
|
104
package.json
104
package.json
@ -5,19 +5,29 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "webpack-dev-server --config webpack.dev.js",
|
"start": "webpack-dev-server --config webpack.dev.js",
|
||||||
"test": "NODE_ENV=test jest",
|
"test": "NODE_ENV=test npm run lint && jest",
|
||||||
"test:watch": "npm test -- --watch",
|
"test:watch": "NODE_ENV=test jest --watch",
|
||||||
"build": "webpack --config webpack.config.js",
|
"build": "webpack --config webpack.config.js",
|
||||||
"storybook": "start-storybook -p 9001",
|
"storybook": "start-storybook -p 9001",
|
||||||
"storybook-build": "build-storybook -o dist",
|
"storybook-build": "build-storybook -o dist",
|
||||||
"lint": "eslint .",
|
"lint": "npm run lint:js & npm run lint:css",
|
||||||
"lint:fix": "npm run lint -- --fix",
|
"lint:js": "eslint .",
|
||||||
"lint:staged": "lint-staged"
|
"lint:js:fix": "npm run lint:js -- --fix",
|
||||||
|
"lint:css": "stylelint 'src/**/*.css'",
|
||||||
|
"lint:css:fix": "stylefmt --recursive src/",
|
||||||
|
"lint:staged": "lint-staged",
|
||||||
|
"deps": "npm-check -s",
|
||||||
|
"deps:update": "npm-check -u"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.@(js|jsx)": [
|
"*.js": [
|
||||||
"eslint --fix",
|
"eslint --fix",
|
||||||
"git add"
|
"git add"
|
||||||
|
],
|
||||||
|
"*.css": [
|
||||||
|
"stylefmt",
|
||||||
|
"stylelint",
|
||||||
|
"git add"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"pre-commit": "lint:staged",
|
"pre-commit": "lint:staged",
|
||||||
@ -29,9 +39,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kadira/storybook": "^1.36.0",
|
"@kadira/storybook": "^1.36.0",
|
||||||
"autoprefixer": "^6.3.3",
|
|
||||||
"babel-core": "^6.5.1",
|
"babel-core": "^6.5.1",
|
||||||
"babel-eslint": "^6.1.2",
|
|
||||||
"babel-jest": "^15.0.0",
|
"babel-jest": "^15.0.0",
|
||||||
"babel-loader": "^6.2.2",
|
"babel-loader": "^6.2.2",
|
||||||
"babel-plugin-lodash": "^3.2.0",
|
"babel-plugin-lodash": "^3.2.0",
|
||||||
@ -43,28 +51,61 @@
|
|||||||
"babel-runtime": "^6.5.0",
|
"babel-runtime": "^6.5.0",
|
||||||
"css-loader": "^0.23.1",
|
"css-loader": "^0.23.1",
|
||||||
"enzyme": "^2.4.1",
|
"enzyme": "^2.4.1",
|
||||||
"eslint": "^3.5.0",
|
"eslint": "^3.7.1",
|
||||||
"eslint-plugin-class-property": "^1.0.1",
|
"eslint-config-netlify": "github:netlify/eslint-config-netlify",
|
||||||
"eslint-plugin-react": "^5.1.1",
|
|
||||||
"expect": "^1.20.2",
|
"expect": "^1.20.2",
|
||||||
"exports-loader": "^0.6.3",
|
"exports-loader": "^0.6.3",
|
||||||
"file-loader": "^0.8.5",
|
"file-loader": "^0.8.5",
|
||||||
"immutable": "^3.7.6",
|
|
||||||
"imports-loader": "^0.6.5",
|
"imports-loader": "^0.6.5",
|
||||||
"jest-cli": "^15.1.1",
|
"jest-cli": "^15.1.1",
|
||||||
"js-yaml": "^3.5.3",
|
|
||||||
"lint-staged": "^3.0.3",
|
"lint-staged": "^3.0.3",
|
||||||
"moment": "^2.11.2",
|
|
||||||
"node-sass": "^3.10.0",
|
"node-sass": "^3.10.0",
|
||||||
"normalizr": "^2.0.0",
|
"npm-check": "^5.2.3",
|
||||||
"postcss-cssnext": "^2.7.0",
|
"postcss-cssnext": "^2.7.0",
|
||||||
"postcss-import": "^8.1.2",
|
"postcss-import": "^8.1.2",
|
||||||
"postcss-loader": "^0.9.1",
|
"postcss-loader": "^0.9.1",
|
||||||
"pre-commit": "^1.1.3",
|
"pre-commit": "^1.1.3",
|
||||||
|
"sass-loader": "^4.0.2",
|
||||||
|
"style-loader": "^0.13.0",
|
||||||
|
"stylefmt": "^4.3.1",
|
||||||
|
"stylelint": "^7.3.1",
|
||||||
|
"stylelint-config-css-modules": "^0.1.0",
|
||||||
|
"stylelint-config-standard": "^13.0.2",
|
||||||
|
"stylelint-declaration-block-order": "^0.1.0",
|
||||||
|
"stylelint-declaration-use-variable": "^1.6.0",
|
||||||
|
"url-loader": "^0.5.7",
|
||||||
|
"webpack": "^1.13.2",
|
||||||
|
"webpack-dev-server": "^1.15.1",
|
||||||
|
"webpack-merge": "^0.14.1",
|
||||||
|
"webpack-postcss-tools": "^1.1.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"autoprefixer": "^6.3.3",
|
||||||
|
"bricks.js": "^1.7.0",
|
||||||
|
"dateformat": "^1.0.12",
|
||||||
|
"fuzzy": "^0.1.1",
|
||||||
|
"immutable": "^3.7.6",
|
||||||
|
"immutability-helper": "^2.0.0",
|
||||||
|
"js-base64": "^2.1.9",
|
||||||
|
"js-yaml": "^3.5.3",
|
||||||
|
"json-loader": "^0.5.4",
|
||||||
|
"localforage": "^1.4.2",
|
||||||
|
"lodash": "^4.13.1",
|
||||||
|
"markup-it": "git+https://github.com/cassiozen/markup-it.git",
|
||||||
|
"material-design-icons": "^3.0.1",
|
||||||
|
"moment": "^2.11.2",
|
||||||
|
"normalize.css": "^4.2.0",
|
||||||
|
"pluralize": "^3.0.0",
|
||||||
|
"prismjs": "^1.5.1",
|
||||||
"react": "^15.1.0",
|
"react": "^15.1.0",
|
||||||
"react-addons-test-utils": "^15.3.2",
|
|
||||||
"react-dom": "^15.1.0",
|
"react-dom": "^15.1.0",
|
||||||
"react-hot-loader": "^3.0.0-beta.2",
|
"react-hot-loader": "^3.0.0-beta.2",
|
||||||
|
"react-addons-css-transition-group": "^15.3.1",
|
||||||
|
"react-datetime": "^2.6.0",
|
||||||
|
"react-portal": "^2.2.1",
|
||||||
|
"react-simple-dnd": "^0.1.2",
|
||||||
|
"react-toolbox": "^1.2.1",
|
||||||
|
"react-waypoint": "^3.1.3",
|
||||||
"react-immutable-proptypes": "^1.6.0",
|
"react-immutable-proptypes": "^1.6.0",
|
||||||
"react-lazy-load": "^3.0.3",
|
"react-lazy-load": "^3.0.3",
|
||||||
"react-pure-render": "^1.0.2",
|
"react-pure-render": "^1.0.2",
|
||||||
@ -73,37 +114,10 @@
|
|||||||
"react-router-redux": "^4.0.5",
|
"react-router-redux": "^4.0.5",
|
||||||
"redux": "^3.3.1",
|
"redux": "^3.3.1",
|
||||||
"redux-thunk": "^1.0.3",
|
"redux-thunk": "^1.0.3",
|
||||||
"sass-loader": "^4.0.2",
|
|
||||||
"style-loader": "^0.13.0",
|
|
||||||
"url-loader": "^0.5.7",
|
|
||||||
"webpack": "^1.13.2",
|
|
||||||
"webpack-dev-server": "^1.15.1",
|
|
||||||
"webpack-merge": "^0.14.1",
|
|
||||||
"webpack-postcss-tools": "^1.1.1",
|
|
||||||
"whatwg-fetch": "^1.0.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"bricks.js": "^1.7.0",
|
|
||||||
"dateformat": "^1.0.12",
|
|
||||||
"fuzzy": "^0.1.1",
|
|
||||||
"immutability-helper": "^2.0.0",
|
|
||||||
"js-base64": "^2.1.9",
|
|
||||||
"json-loader": "^0.5.4",
|
|
||||||
"localforage": "^1.4.2",
|
|
||||||
"lodash": "^4.13.1",
|
|
||||||
"markup-it": "git+https://github.com/cassiozen/markup-it.git",
|
|
||||||
"material-design-icons": "^3.0.1",
|
|
||||||
"normalize.css": "^4.2.0",
|
|
||||||
"pluralize": "^3.0.0",
|
|
||||||
"prismjs": "^1.5.1",
|
|
||||||
"react-addons-css-transition-group": "^15.3.1",
|
|
||||||
"react-datetime": "^2.6.0",
|
|
||||||
"react-portal": "^2.2.1",
|
|
||||||
"react-simple-dnd": "^0.1.2",
|
|
||||||
"react-toolbox": "^1.2.1",
|
|
||||||
"selection-position": "^1.0.0",
|
"selection-position": "^1.0.0",
|
||||||
"semaphore": "^1.0.5",
|
"semaphore": "^1.0.5",
|
||||||
"slate": "^0.14.14",
|
"slate": "^0.14.14",
|
||||||
"slate-drop-or-paste-images": "^0.2.0"
|
"slate-drop-or-paste-images": "^0.2.0",
|
||||||
|
"whatwg-fetch": "^1.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
import _ from 'lodash';
|
|
||||||
import { currentBackend } from '../backends/backend';
|
import { currentBackend } from '../backends/backend';
|
||||||
import { authenticate } from '../actions/auth';
|
import { authenticate } from '../actions/auth';
|
||||||
import * as publishModes from '../constants/publishModes';
|
|
||||||
import * as MediaProxy from '../valueObjects/MediaProxy';
|
import * as MediaProxy from '../valueObjects/MediaProxy';
|
||||||
|
|
||||||
export const CONFIG_REQUEST = 'CONFIG_REQUEST';
|
export const CONFIG_REQUEST = 'CONFIG_REQUEST';
|
||||||
@ -72,19 +70,5 @@ function parseConfig(data) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!('publish_mode' in config) || _.values(publishModes).indexOf(config.publish_mode) === -1) {
|
|
||||||
// Make sure there is a publish workflow mode set
|
|
||||||
config.publish_mode = publishModes.SIMPLE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!('public_folder' in config)) {
|
|
||||||
// Make sure there is a public folder
|
|
||||||
config.public_folder = config.media_folder;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.public_folder.charAt(0) !== '/') {
|
|
||||||
config.public_folder = '/' + config.public_folder;
|
|
||||||
}
|
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { currentBackend } from '../backends/backend';
|
import { currentBackend } from '../backends/backend';
|
||||||
import { getMedia } from '../reducers';
|
import { getIntegrationProvider } from '../integrations';
|
||||||
|
import { getMedia, selectIntegration } from '../reducers';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Contant Declarations
|
* Contant Declarations
|
||||||
@ -21,6 +22,9 @@ export const ENTRY_PERSIST_REQUEST = 'ENTRY_PERSIST_REQUEST';
|
|||||||
export const ENTRY_PERSIST_SUCCESS = 'ENTRY_PERSIST_SUCCESS';
|
export const ENTRY_PERSIST_SUCCESS = 'ENTRY_PERSIST_SUCCESS';
|
||||||
export const ENTRY_PERSIST_FAILURE = 'ENTRY_PERSIST_FAILURE';
|
export const ENTRY_PERSIST_FAILURE = 'ENTRY_PERSIST_FAILURE';
|
||||||
|
|
||||||
|
export const SEARCH_ENTRIES_REQUEST = 'SEARCH_ENTRIES_REQUEST';
|
||||||
|
export const SEARCH_ENTRIES_SUCCESS = 'SEARCH_ENTRIES_SUCCESS';
|
||||||
|
export const SEARCH_ENTRIES_FAILURE = 'SEARCH_ENTRIES_FAILURE';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Simple Action Creators (Internal)
|
* Simple Action Creators (Internal)
|
||||||
@ -61,7 +65,7 @@ export function entriesLoaded(collection, entries, pagination) {
|
|||||||
payload: {
|
payload: {
|
||||||
collection: collection.get('name'),
|
collection: collection.get('name'),
|
||||||
entries: entries,
|
entries: entries,
|
||||||
pages: pagination
|
page: pagination
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -110,6 +114,34 @@ export function emmptyDraftCreated(entry) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function searchingEntries(searchTerm) {
|
||||||
|
return {
|
||||||
|
type: SEARCH_ENTRIES_REQUEST,
|
||||||
|
payload: { searchTerm }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SearchSuccess(searchTerm, entries, page) {
|
||||||
|
return {
|
||||||
|
type: SEARCH_ENTRIES_SUCCESS,
|
||||||
|
payload: {
|
||||||
|
searchTerm,
|
||||||
|
entries,
|
||||||
|
page
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SearchFailure(searchTerm, error) {
|
||||||
|
return {
|
||||||
|
type: SEARCH_ENTRIES_FAILURE,
|
||||||
|
payload: {
|
||||||
|
searchTerm,
|
||||||
|
error
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Exported simple Action Creators
|
* Exported simple Action Creators
|
||||||
*/
|
*/
|
||||||
@ -136,25 +168,30 @@ export function changeDraft(entry) {
|
|||||||
/*
|
/*
|
||||||
* Exported Thunk Action Creators
|
* Exported Thunk Action Creators
|
||||||
*/
|
*/
|
||||||
export function loadEntry(collection, slug) {
|
|
||||||
|
export function loadEntry(entry, collection, slug) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const backend = currentBackend(state.config);
|
const backend = currentBackend(state.config);
|
||||||
|
|
||||||
dispatch(entryLoading(collection, slug));
|
dispatch(entryLoading(collection, slug));
|
||||||
backend.entry(collection, slug)
|
let getPromise;
|
||||||
.then((entry) => dispatch(entryLoaded(collection, entry)));
|
if (entry && entry.get('path')) {
|
||||||
|
getPromise = backend.getEntry(entry.get('collection'), entry.get('slug'), entry.get('path'));
|
||||||
|
} else {
|
||||||
|
getPromise = backend.lookupEntry(collection, slug);
|
||||||
|
}
|
||||||
|
return getPromise.then((loadedEntry) => dispatch(entryLoaded(collection, loadedEntry)));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadEntries(collection) {
|
export function loadEntries(collection, page = 0) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
if (collection.get('isFetching')) { return; }
|
if (collection.get('isFetching')) { return; }
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const backend = currentBackend(state.config);
|
const integration = selectIntegration(state, collection.get('name'), 'listEntries');
|
||||||
|
const provider = integration ? getIntegrationProvider(state.integrations, integration) : currentBackend(state.config);
|
||||||
dispatch(entriesLoading(collection));
|
dispatch(entriesLoading(collection));
|
||||||
backend.entries(collection).then(
|
provider.listEntries(collection, page).then(
|
||||||
(response) => dispatch(entriesLoaded(collection, response.entries, response.pagination)),
|
(response) => dispatch(entriesLoaded(collection, response.entries, response.pagination)),
|
||||||
(error) => dispatch(entriesFailed(collection, error))
|
(error) => dispatch(entriesFailed(collection, error))
|
||||||
);
|
);
|
||||||
@ -184,3 +221,19 @@ export function persistEntry(collection, entry) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function searchEntries(searchTerm, page = 0) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const state = getState();
|
||||||
|
let collections = state.collections.keySeq().toArray();
|
||||||
|
collections = collections.filter(collection => selectIntegration(state, collection, 'search'));
|
||||||
|
const integration = selectIntegration(state, collections[0], 'search');
|
||||||
|
if (!integration) console.warn('There isn\'t a search integration configured.');
|
||||||
|
const provider = integration ? getIntegrationProvider(state.integrations, integration) : currentBackend(state.config);
|
||||||
|
dispatch(searchingEntries(searchTerm));
|
||||||
|
provider.search(collections, searchTerm, page).then(
|
||||||
|
(response) => dispatch(SearchSuccess(searchTerm, response.entries, response.pagination)),
|
||||||
|
(error) => dispatch(SearchFailure(searchTerm, error))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -31,7 +31,7 @@ export function runCommand(commandName, payload) {
|
|||||||
window.alert('Find Bar Help (PLACEHOLDER)\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit.');
|
window.alert('Find Bar Help (PLACEHOLDER)\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit.');
|
||||||
break;
|
break;
|
||||||
case SEARCH:
|
case SEARCH:
|
||||||
history.push('/search');
|
history.push(`/search/${payload.searchTerm}`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
dispatch(run(commandName, payload));
|
dispatch(run(commandName, payload));
|
||||||
|
@ -17,6 +17,25 @@ class LocalStorageAuthStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const slugFormatter = (template, entryData) => {
|
||||||
|
var date = new Date();
|
||||||
|
return template.replace(/\{\{([^\}]+)\}\}/g, function(_, name) {
|
||||||
|
switch (name) {
|
||||||
|
case 'year':
|
||||||
|
return date.getFullYear();
|
||||||
|
case 'month':
|
||||||
|
return ('0' + (date.getMonth() + 1)).slice(-2);
|
||||||
|
case 'day':
|
||||||
|
return ('0' + date.getDate()).slice(-2);
|
||||||
|
case 'slug':
|
||||||
|
const identifier = entryData.get('title', entryData.get('path'));
|
||||||
|
return identifier.trim().toLowerCase().replace(/[^a-z0-9\.\-\_]+/gi, '-');
|
||||||
|
default:
|
||||||
|
return entryData.get(name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
class Backend {
|
class Backend {
|
||||||
constructor(implementation, authStore = null) {
|
constructor(implementation, authStore = null) {
|
||||||
this.implementation = implementation;
|
this.implementation = implementation;
|
||||||
@ -46,7 +65,7 @@ class Backend {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
entries(collection, page, perPage) {
|
listEntries(collection, page, perPage) {
|
||||||
return this.implementation.entries(collection, page, perPage).then((response) => {
|
return this.implementation.entries(collection, page, perPage).then((response) => {
|
||||||
return {
|
return {
|
||||||
pagination: response.pagination,
|
pagination: response.pagination,
|
||||||
@ -55,8 +74,15 @@ class Backend {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
entry(collection, slug) {
|
// We have the file path. Fetch and parse the file.
|
||||||
return this.implementation.entry(collection, slug).then(this.entryWithFormat(collection));
|
getEntry(collection, slug, path) {
|
||||||
|
return this.implementation.getEntry(collection, slug, path).then(this.entryWithFormat(collection));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Will fetch the whole list of files from GitHub and load each file, then looks up for entry.
|
||||||
|
// (Files are persisted in local storage - only expensive on the first run for each file).
|
||||||
|
lookupEntry(collection, slug) {
|
||||||
|
return this.implementation.lookupEntry(collection, slug).then(this.entryWithFormat(collection));
|
||||||
}
|
}
|
||||||
|
|
||||||
newEntry(collection) {
|
newEntry(collection) {
|
||||||
@ -87,24 +113,6 @@ class Backend {
|
|||||||
return this.implementation.unpublishedEntry(collection, slug).then(this.entryWithFormat(collection));
|
return this.implementation.unpublishedEntry(collection, slug).then(this.entryWithFormat(collection));
|
||||||
}
|
}
|
||||||
|
|
||||||
slugFormatter(template, entry) {
|
|
||||||
var date = new Date();
|
|
||||||
return template.replace(/\{\{([^\}]+)\}\}/g, function(_, name) {
|
|
||||||
switch (name) {
|
|
||||||
case 'year':
|
|
||||||
return date.getFullYear();
|
|
||||||
case 'month':
|
|
||||||
return ('0' + (date.getMonth() + 1)).slice(-2);
|
|
||||||
case 'day':
|
|
||||||
return ('0' + date.getDate()).slice(-2);
|
|
||||||
case 'slug':
|
|
||||||
return entry.getIn(['data', 'title']).trim().toLowerCase().replace(/[^a-z0-9\.\-\_]+/gi, '-');
|
|
||||||
default:
|
|
||||||
return entry.getIn(['data', name]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
persistEntry(config, collection, entryDraft, MediaFiles, options) {
|
persistEntry(config, collection, entryDraft, MediaFiles, options) {
|
||||||
const newEntry = entryDraft.getIn(['entry', 'newRecord']) || false;
|
const newEntry = entryDraft.getIn(['entry', 'newRecord']) || false;
|
||||||
|
|
||||||
@ -116,7 +124,7 @@ class Backend {
|
|||||||
const entryData = entryDraft.getIn(['entry', 'data']).toJS();
|
const entryData = entryDraft.getIn(['entry', 'data']).toJS();
|
||||||
let entryObj;
|
let entryObj;
|
||||||
if (newEntry) {
|
if (newEntry) {
|
||||||
const slug = this.slugFormatter(collection.get('slug'), entryDraft.get('entry'));
|
const slug = slugFormatter(collection.get('slug'), entryDraft.getIn(['entry', 'data']));
|
||||||
entryObj = {
|
entryObj = {
|
||||||
path: `${collection.get('folder')}/${slug}.md`,
|
path: `${collection.get('folder')}/${slug}.md`,
|
||||||
slug: slug,
|
slug: slug,
|
||||||
@ -172,11 +180,11 @@ export function resolveBackend(config) {
|
|||||||
|
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case 'test-repo':
|
case 'test-repo':
|
||||||
return new Backend(new TestRepoBackend(config), authStore);
|
return new Backend(new TestRepoBackend(config, slugFormatter), authStore);
|
||||||
case 'github':
|
case 'github':
|
||||||
return new Backend(new GitHubBackend(config), authStore);
|
return new Backend(new GitHubBackend(config, slugFormatter), authStore);
|
||||||
case 'netlify-git':
|
case 'netlify-git':
|
||||||
return new Backend(new NetlifyGitBackend(config), authStore);
|
return new Backend(new NetlifyGitBackend(config, slugFormatter), authStore);
|
||||||
default:
|
default:
|
||||||
throw `Backend not found: ${name}`;
|
throw `Backend not found: ${name}`;
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ export default class API {
|
|||||||
this.token = token;
|
this.token = token;
|
||||||
this.repo = repo;
|
this.repo = repo;
|
||||||
this.branch = branch;
|
this.branch = branch;
|
||||||
this.repoURL = `/repos/${this.repo}`;
|
this.repoURL = `/repos/${ this.repo }`;
|
||||||
}
|
}
|
||||||
|
|
||||||
user() {
|
user() {
|
||||||
@ -20,9 +20,9 @@ export default class API {
|
|||||||
|
|
||||||
requestHeaders(headers = {}) {
|
requestHeaders(headers = {}) {
|
||||||
return {
|
return {
|
||||||
Authorization: `token ${this.token}`,
|
Authorization: `token ${ this.token }`,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...headers
|
...headers,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,11 +40,11 @@ export default class API {
|
|||||||
const params = [];
|
const params = [];
|
||||||
if (options.params) {
|
if (options.params) {
|
||||||
for (const key in 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) {
|
if (params.length) {
|
||||||
path += `?${params.join('&')}`;
|
path += `?${ params.join('&') }`;
|
||||||
}
|
}
|
||||||
return API_ROOT + path;
|
return API_ROOT + path;
|
||||||
}
|
}
|
||||||
@ -52,7 +52,7 @@ export default class API {
|
|||||||
request(path, options = {}) {
|
request(path, options = {}) {
|
||||||
const headers = this.requestHeaders(options.headers || {});
|
const headers = this.requestHeaders(options.headers || {});
|
||||||
const url = this.urlFor(path, options);
|
const url = this.urlFor(path, options);
|
||||||
return fetch(url, { ...options, headers: headers }).then((response) => {
|
return fetch(url, { ...options, headers }).then((response) => {
|
||||||
const contentType = response.headers.get('Content-Type');
|
const contentType = response.headers.get('Content-Type');
|
||||||
if (contentType && contentType.match(/json/)) {
|
if (contentType && contentType.match(/json/)) {
|
||||||
return this.parseJsonResponse(response);
|
return this.parseJsonResponse(response);
|
||||||
@ -63,20 +63,20 @@ export default class API {
|
|||||||
}
|
}
|
||||||
|
|
||||||
checkMetadataRef() {
|
checkMetadataRef() {
|
||||||
return this.request(`${this.repoURL}/git/refs/meta/_netlify_cms?${Date.now()}`, {
|
return this.request(`${ this.repoURL }/git/refs/meta/_netlify_cms?${ Date.now() }`, {
|
||||||
cache: 'no-store',
|
cache: 'no-store',
|
||||||
})
|
})
|
||||||
.then(response => response.object)
|
.then(response => response.object)
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
// Meta ref doesn't exist
|
// Meta ref doesn't exist
|
||||||
const readme = {
|
const readme = {
|
||||||
raw: '# Netlify CMS\n\nThis tree is used by the Netlify CMS to store metadata information for specific files and branches.'
|
raw: '# Netlify CMS\n\nThis tree is used by the Netlify CMS to store metadata information for specific files and branches.',
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.uploadBlob(readme)
|
return this.uploadBlob(readme)
|
||||||
.then(item => this.request(`${this.repoURL}/git/trees`, {
|
.then(item => this.request(`${ this.repoURL }/git/trees`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ tree: [{ path: 'README.md', mode: '100644', type: 'blob', sha: item.sha }] })
|
body: JSON.stringify({ tree: [{ path: 'README.md', mode: '100644', type: 'blob', sha: item.sha }] }),
|
||||||
}))
|
}))
|
||||||
.then(tree => this.commit('First Commit', tree))
|
.then(tree => this.commit('First Commit', tree))
|
||||||
.then(response => this.createRef('meta', '_netlify_cms', response.sha))
|
.then(response => this.createRef('meta', '_netlify_cms', response.sha))
|
||||||
@ -88,32 +88,32 @@ export default class API {
|
|||||||
return this.checkMetadataRef()
|
return this.checkMetadataRef()
|
||||||
.then((branchData) => {
|
.then((branchData) => {
|
||||||
const fileTree = {
|
const fileTree = {
|
||||||
[`${key}.json`]: {
|
[`${ key }.json`]: {
|
||||||
path: `${key}.json`,
|
path: `${ key }.json`,
|
||||||
raw: JSON.stringify(data),
|
raw: JSON.stringify(data),
|
||||||
file: true
|
file: true,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.uploadBlob(fileTree[`${key}.json`])
|
return this.uploadBlob(fileTree[`${ key }.json`])
|
||||||
.then(item => this.updateTree(branchData.sha, '/', fileTree))
|
.then(item => this.updateTree(branchData.sha, '/', fileTree))
|
||||||
.then(changeTree => this.commit(`Updating “${key}” metadata`, changeTree))
|
.then(changeTree => this.commit(`Updating “${ key }” metadata`, changeTree))
|
||||||
.then(response => this.patchRef('meta', '_netlify_cms', response.sha))
|
.then(response => this.patchRef('meta', '_netlify_cms', response.sha))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
LocalForage.setItem(`gh.meta.${key}`, {
|
LocalForage.setItem(`gh.meta.${ key }`, {
|
||||||
expires: Date.now() + 300000, // In 5 minutes
|
expires: Date.now() + 300000, // In 5 minutes
|
||||||
data
|
data,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
retrieveMetadata(key) {
|
retrieveMetadata(key) {
|
||||||
const cache = LocalForage.getItem(`gh.meta.${key}`);
|
const cache = LocalForage.getItem(`gh.meta.${ key }`);
|
||||||
return cache.then((cached) => {
|
return cache.then((cached) => {
|
||||||
if (cached && cached.expires > Date.now()) { return cached.data; }
|
if (cached && cached.expires > Date.now()) { return cached.data; }
|
||||||
|
|
||||||
return this.request(`${this.repoURL}/contents/${key}.json`, {
|
return this.request(`${ this.repoURL }/contents/${ key }.json`, {
|
||||||
params: { ref: 'refs/meta/_netlify_cms' },
|
params: { ref: 'refs/meta/_netlify_cms' },
|
||||||
headers: { Accept: 'application/vnd.github.VERSION.raw' },
|
headers: { Accept: 'application/vnd.github.VERSION.raw' },
|
||||||
cache: 'no-store',
|
cache: 'no-store',
|
||||||
@ -123,17 +123,17 @@ export default class API {
|
|||||||
}
|
}
|
||||||
|
|
||||||
readFile(path, sha, branch = this.branch) {
|
readFile(path, sha, branch = this.branch) {
|
||||||
const cache = sha ? LocalForage.getItem(`gh.${sha}`) : Promise.resolve(null);
|
const cache = sha ? LocalForage.getItem(`gh.${ sha }`) : Promise.resolve(null);
|
||||||
return cache.then((cached) => {
|
return cache.then((cached) => {
|
||||||
if (cached) { return cached; }
|
if (cached) { return cached; }
|
||||||
|
|
||||||
return this.request(`${this.repoURL}/contents/${path}`, {
|
return this.request(`${ this.repoURL }/contents/${ path }`, {
|
||||||
headers: { Accept: 'application/vnd.github.VERSION.raw' },
|
headers: { Accept: 'application/vnd.github.VERSION.raw' },
|
||||||
params: { ref: branch },
|
params: { ref: branch },
|
||||||
cache: false
|
cache: false,
|
||||||
}).then((result) => {
|
}).then((result) => {
|
||||||
if (sha) {
|
if (sha) {
|
||||||
LocalForage.setItem(`gh.${sha}`, result);
|
LocalForage.setItem(`gh.${ sha }`, result);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
@ -141,25 +141,27 @@ export default class API {
|
|||||||
}
|
}
|
||||||
|
|
||||||
listFiles(path) {
|
listFiles(path) {
|
||||||
return this.request(`${this.repoURL}/contents/${path}`, {
|
return this.request(`${ this.repoURL }/contents/${ path }`, {
|
||||||
params: { ref: this.branch }
|
params: { ref: this.branch },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
readUnpublishedBranchFile(contentKey) {
|
readUnpublishedBranchFile(contentKey) {
|
||||||
let metaData;
|
let metaData;
|
||||||
return this.retrieveMetadata(contentKey)
|
const unpublishedPromise = this.retrieveMetadata(contentKey)
|
||||||
.then(data => {
|
.then((data) => {
|
||||||
metaData = data;
|
metaData = data;
|
||||||
return this.readFile(data.objects.entry, null, data.branch);
|
return this.readFile(data.objects.entry, null, data.branch);
|
||||||
})
|
})
|
||||||
.then(file => {
|
.then(file => ({ metaData, file }))
|
||||||
return { metaData, file };
|
.catch((error) => {
|
||||||
|
return null;
|
||||||
});
|
});
|
||||||
|
return unpublishedPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
listUnpublishedBranches() {
|
listUnpublishedBranches() {
|
||||||
return this.request(`${this.repoURL}/git/refs/heads/cms`);
|
return this.request(`${ this.repoURL }/git/refs/heads/cms`);
|
||||||
}
|
}
|
||||||
|
|
||||||
persistFiles(entry, mediaFiles, options) {
|
persistFiles(entry, mediaFiles, options) {
|
||||||
@ -172,7 +174,7 @@ export default class API {
|
|||||||
files.forEach((file) => {
|
files.forEach((file) => {
|
||||||
if (file.uploaded) { return; }
|
if (file.uploaded) { return; }
|
||||||
uploadPromises.push(this.uploadBlob(file));
|
uploadPromises.push(this.uploadBlob(file));
|
||||||
parts = file.path.split('/').filter((part) => part);
|
parts = file.path.split('/').filter(part => part);
|
||||||
filename = parts.pop();
|
filename = parts.pop();
|
||||||
subtree = fileTree;
|
subtree = fileTree;
|
||||||
while (part = parts.shift()) {
|
while (part = parts.shift()) {
|
||||||
@ -196,14 +198,14 @@ export default class API {
|
|||||||
}
|
}
|
||||||
|
|
||||||
editorialWorkflowGit(fileTree, entry, filesList, options) {
|
editorialWorkflowGit(fileTree, entry, filesList, options) {
|
||||||
const contentKey = options.collectionName ? `${options.collectionName}-${entry.slug}` : entry.slug;
|
const contentKey = options.collectionName ? `${ options.collectionName }-${ entry.slug }` : entry.slug;
|
||||||
const branchName = `cms/${contentKey}`;
|
const branchName = `cms/${ contentKey }`;
|
||||||
const unpublished = options.unpublished || false;
|
const unpublished = options.unpublished || false;
|
||||||
|
|
||||||
if (!unpublished) {
|
if (!unpublished) {
|
||||||
// Open new editorial review workflow for this entry - Create new metadata and commit to new branch
|
// Open new editorial review workflow for this entry - Create new metadata and commit to new branch
|
||||||
const contentKey = options.collectionName ? `${options.collectionName}-${entry.slug}` : entry.slug;
|
const contentKey = options.collectionName ? `${ options.collectionName }-${ entry.slug }` : entry.slug;
|
||||||
const branchName = `cms/${contentKey}`;
|
const branchName = `cms/${ contentKey }`;
|
||||||
|
|
||||||
return this.getBranch()
|
return this.getBranch()
|
||||||
.then(branchData => this.updateTree(branchData.commit.sha, '/', fileTree))
|
.then(branchData => this.updateTree(branchData.commit.sha, '/', fileTree))
|
||||||
@ -211,14 +213,14 @@ export default class API {
|
|||||||
.then(commitResponse => this.createBranch(branchName, commitResponse.sha))
|
.then(commitResponse => this.createBranch(branchName, commitResponse.sha))
|
||||||
.then(branchResponse => this.createPR(options.commitMessage, branchName))
|
.then(branchResponse => this.createPR(options.commitMessage, branchName))
|
||||||
.then((prResponse) => {
|
.then((prResponse) => {
|
||||||
return this.user().then(user => {
|
return this.user().then((user) => {
|
||||||
return user.name ? user.name : user.login;
|
return user.name ? user.name : user.login;
|
||||||
})
|
})
|
||||||
.then(username => this.storeMetadata(contentKey, {
|
.then(username => this.storeMetadata(contentKey, {
|
||||||
type: 'PR',
|
type: 'PR',
|
||||||
pr: {
|
pr: {
|
||||||
number: prResponse.number,
|
number: prResponse.number,
|
||||||
head: prResponse.head && prResponse.head.sha
|
head: prResponse.head && prResponse.head.sha,
|
||||||
},
|
},
|
||||||
user: username,
|
user: username,
|
||||||
status: status.first(),
|
status: status.first(),
|
||||||
@ -228,9 +230,9 @@ export default class API {
|
|||||||
description: options.parsedData && options.parsedData.description,
|
description: options.parsedData && options.parsedData.description,
|
||||||
objects: {
|
objects: {
|
||||||
entry: entry.path,
|
entry: entry.path,
|
||||||
files: filesList
|
files: filesList,
|
||||||
},
|
},
|
||||||
timeStamp: new Date().toISOString()
|
timeStamp: new Date().toISOString(),
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -239,13 +241,13 @@ export default class API {
|
|||||||
.then(branchData => this.updateTree(branchData.commit.sha, '/', fileTree))
|
.then(branchData => this.updateTree(branchData.commit.sha, '/', fileTree))
|
||||||
.then(changeTree => this.commit(options.commitMessage, changeTree))
|
.then(changeTree => this.commit(options.commitMessage, changeTree))
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
const contentKey = options.collectionName ? `${options.collectionName}-${entry.slug}` : entry.slug;
|
const contentKey = options.collectionName ? `${ options.collectionName }-${ entry.slug }` : entry.slug;
|
||||||
const branchName = `cms/${contentKey}`;
|
const branchName = `cms/${ contentKey }`;
|
||||||
return this.user().then(user => {
|
return this.user().then((user) => {
|
||||||
return user.name ? user.name : user.login;
|
return user.name ? user.name : user.login;
|
||||||
})
|
})
|
||||||
.then(username => this.retrieveMetadata(contentKey))
|
.then(username => this.retrieveMetadata(contentKey))
|
||||||
.then(metadata => {
|
.then((metadata) => {
|
||||||
let files = metadata.objects && metadata.objects.files || [];
|
let files = metadata.objects && metadata.objects.files || [];
|
||||||
files = files.concat(filesList);
|
files = files.concat(filesList);
|
||||||
|
|
||||||
@ -255,9 +257,9 @@ export default class API {
|
|||||||
description: options.parsedData && options.parsedData.description,
|
description: options.parsedData && options.parsedData.description,
|
||||||
objects: {
|
objects: {
|
||||||
entry: entry.path,
|
entry: entry.path,
|
||||||
files: _.uniq(files)
|
files: _.uniq(files),
|
||||||
},
|
},
|
||||||
timeStamp: new Date().toISOString()
|
timeStamp: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.then(updatedMetadata => this.storeMetadata(contentKey, updatedMetadata))
|
.then(updatedMetadata => this.storeMetadata(contentKey, updatedMetadata))
|
||||||
@ -267,50 +269,50 @@ export default class API {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateUnpublishedEntryStatus(collection, slug, status) {
|
updateUnpublishedEntryStatus(collection, slug, status) {
|
||||||
const contentKey = collection ? `${collection}-${slug}` : slug;
|
const contentKey = collection ? `${ collection }-${ slug }` : slug;
|
||||||
return this.retrieveMetadata(contentKey)
|
return this.retrieveMetadata(contentKey)
|
||||||
.then(metadata => {
|
.then((metadata) => {
|
||||||
return {
|
return {
|
||||||
...metadata,
|
...metadata,
|
||||||
status
|
status,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.then(updatedMetadata => this.storeMetadata(contentKey, updatedMetadata));
|
.then(updatedMetadata => this.storeMetadata(contentKey, updatedMetadata));
|
||||||
}
|
}
|
||||||
|
|
||||||
publishUnpublishedEntry(collection, slug, status) {
|
publishUnpublishedEntry(collection, slug, status) {
|
||||||
const contentKey = collection ? `${collection}-${slug}` : slug;
|
const contentKey = collection ? `${ collection }-${ slug }` : slug;
|
||||||
return this.retrieveMetadata(contentKey)
|
return this.retrieveMetadata(contentKey)
|
||||||
.then(metadata => {
|
.then((metadata) => {
|
||||||
const headSha = metadata.pr && metadata.pr.head;
|
const headSha = metadata.pr && metadata.pr.head;
|
||||||
const number = metadata.pr && metadata.pr.number;
|
const number = metadata.pr && metadata.pr.number;
|
||||||
return this.mergePR(headSha, number);
|
return this.mergePR(headSha, number);
|
||||||
})
|
})
|
||||||
.then(() => this.deleteBranch(`cms/${contentKey}`));
|
.then(() => this.deleteBranch(`cms/${ contentKey }`));
|
||||||
}
|
}
|
||||||
|
|
||||||
createRef(type, name, sha) {
|
createRef(type, name, sha) {
|
||||||
return this.request(`${this.repoURL}/git/refs`, {
|
return this.request(`${ this.repoURL }/git/refs`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ ref: `refs/${type}/${name}`, sha }),
|
body: JSON.stringify({ ref: `refs/${ type }/${ name }`, sha }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
patchRef(type, name, sha) {
|
patchRef(type, name, sha) {
|
||||||
return this.request(`${this.repoURL}/git/refs/${type}/${name}`, {
|
return this.request(`${ this.repoURL }/git/refs/${ type }/${ name }`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
body: JSON.stringify({ sha })
|
body: JSON.stringify({ sha }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteRef(type, name, sha) {
|
deleteRef(type, name, sha) {
|
||||||
return this.request(`${this.repoURL}/git/refs/${type}/${name}`, {
|
return this.request(`${ this.repoURL }/git/refs/${ type }/${ name }`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getBranch(branch = this.branch) {
|
getBranch(branch = this.branch) {
|
||||||
return this.request(`${this.repoURL}/branches/${branch}`);
|
return this.request(`${ this.repoURL }/branches/${ branch }`);
|
||||||
}
|
}
|
||||||
|
|
||||||
createBranch(branchName, sha) {
|
createBranch(branchName, sha) {
|
||||||
@ -327,24 +329,24 @@ export default class API {
|
|||||||
|
|
||||||
createPR(title, head, base = 'master') {
|
createPR(title, head, base = 'master') {
|
||||||
const body = 'Automatically generated by Netlify CMS';
|
const body = 'Automatically generated by Netlify CMS';
|
||||||
return this.request(`${this.repoURL}/pulls`, {
|
return this.request(`${ this.repoURL }/pulls`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ title, body, head, base }),
|
body: JSON.stringify({ title, body, head, base }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
mergePR(headSha, number) {
|
mergePR(headSha, number) {
|
||||||
return this.request(`${this.repoURL}/pulls/${number}/merge`, {
|
return this.request(`${ this.repoURL }/pulls/${ number }/merge`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
commit_message: 'Automatically generated. Merged on Netlify CMS.',
|
commit_message: 'Automatically generated. Merged on Netlify CMS.',
|
||||||
sha: headSha
|
sha: headSha,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getTree(sha) {
|
getTree(sha) {
|
||||||
return sha ? this.request(`${this.repoURL}/git/trees/${sha}`) : Promise.resolve({ tree: [] });
|
return sha ? this.request(`${ this.repoURL }/git/trees/${ sha }`) : Promise.resolve({ tree: [] });
|
||||||
}
|
}
|
||||||
|
|
||||||
toBase64(str) {
|
toBase64(str) {
|
||||||
@ -357,12 +359,12 @@ export default class API {
|
|||||||
const content = item instanceof MediaProxy ? item.toBase64() : this.toBase64(item.raw);
|
const content = item instanceof MediaProxy ? item.toBase64() : this.toBase64(item.raw);
|
||||||
|
|
||||||
return content.then((contentBase64) => {
|
return content.then((contentBase64) => {
|
||||||
return this.request(`${this.repoURL}/git/blobs`, {
|
return this.request(`${ this.repoURL }/git/blobs`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
content: contentBase64,
|
content: contentBase64,
|
||||||
encoding: 'base64'
|
encoding: 'base64',
|
||||||
})
|
}),
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
item.sha = response.sha;
|
item.sha = response.sha;
|
||||||
item.uploaded = true;
|
item.uploaded = true;
|
||||||
@ -374,11 +376,11 @@ export default class API {
|
|||||||
updateTree(sha, path, fileTree) {
|
updateTree(sha, path, fileTree) {
|
||||||
return this.getTree(sha)
|
return this.getTree(sha)
|
||||||
.then((tree) => {
|
.then((tree) => {
|
||||||
var obj, filename, fileOrDir;
|
let obj, filename, fileOrDir;
|
||||||
var updates = [];
|
const updates = [];
|
||||||
var added = {};
|
const added = {};
|
||||||
|
|
||||||
for (var i = 0, len = tree.tree.length; i < len; i++) {
|
for (let i = 0, len = tree.tree.length; i < len; i++) {
|
||||||
obj = tree.tree[i];
|
obj = tree.tree[i];
|
||||||
if (fileOrDir = fileTree[obj.path]) {
|
if (fileOrDir = fileTree[obj.path]) {
|
||||||
added[obj.path] = true;
|
added[obj.path] = true;
|
||||||
@ -400,12 +402,12 @@ export default class API {
|
|||||||
}
|
}
|
||||||
return Promise.all(updates)
|
return Promise.all(updates)
|
||||||
.then((updates) => {
|
.then((updates) => {
|
||||||
return this.request(`${this.repoURL}/git/trees`, {
|
return this.request(`${ this.repoURL }/git/trees`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ base_tree: sha, tree: updates })
|
body: JSON.stringify({ base_tree: sha, tree: updates }),
|
||||||
});
|
});
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
return { path: path, mode: '040000', type: 'tree', sha: response.sha, parentSha: sha };
|
return { path, mode: '040000', type: 'tree', sha: response.sha, parentSha: sha };
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -413,9 +415,9 @@ export default class API {
|
|||||||
commit(message, changeTree) {
|
commit(message, changeTree) {
|
||||||
const tree = changeTree.sha;
|
const tree = changeTree.sha;
|
||||||
const parents = changeTree.parentSha ? [changeTree.parentSha] : [];
|
const parents = changeTree.parentSha ? [changeTree.parentSha] : [];
|
||||||
return this.request(`${this.repoURL}/git/commits`, {
|
return this.request(`${ this.repoURL }/git/commits`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ message, tree, parents })
|
body: JSON.stringify({ message, tree, parents }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ export default class GitHub {
|
|||||||
files.map((file) => {
|
files.map((file) => {
|
||||||
promises.push(new Promise((resolve, reject) => {
|
promises.push(new Promise((resolve, reject) => {
|
||||||
return sem.take(() => this.api.readFile(file.path, file.sha).then((data) => {
|
return sem.take(() => this.api.readFile(file.path, file.sha).then((data) => {
|
||||||
resolve(createEntry(file.path, file.path.split('/').pop().replace(/\.[^\.]+$/, ''), data));
|
resolve(createEntry(collection.get('name'), file.path.split('/').pop().replace(/\.[^\.]+$/, ''), file.path, { raw: data }));
|
||||||
sem.leave();
|
sem.leave();
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
sem.leave();
|
sem.leave();
|
||||||
@ -47,18 +47,25 @@ export default class GitHub {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
return Promise.all(promises);
|
return Promise.all(promises);
|
||||||
}).then((entries) => ({
|
}).then(entries => ({
|
||||||
pagination: {},
|
pagination: 0,
|
||||||
entries
|
entries,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
entry(collection, slug) {
|
|
||||||
return this.entries(collection).then((response) => (
|
// Will fetch the entire list of entries from github.
|
||||||
response.entries.filter((entry) => entry.slug === slug)[0]
|
lookupEntry(collection, slug) {
|
||||||
|
return this.entries(collection).then(response => (
|
||||||
|
response.entries.filter(entry => entry.slug === slug)[0]
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetches a single entry.
|
||||||
|
getEntry(collection, slug, path) {
|
||||||
|
return this.api.readFile(path).then(data => createEntry(collection, slug, path, { raw: data }));
|
||||||
|
}
|
||||||
|
|
||||||
persistEntry(entry, mediaFiles = [], options = {}) {
|
persistEntry(entry, mediaFiles = [], options = {}) {
|
||||||
return this.api.persistFiles(entry, mediaFiles, options);
|
return this.api.persistFiles(entry, mediaFiles, options);
|
||||||
}
|
}
|
||||||
@ -71,11 +78,16 @@ export default class GitHub {
|
|||||||
promises.push(new Promise((resolve, reject) => {
|
promises.push(new Promise((resolve, reject) => {
|
||||||
const contentKey = branch.ref.split('refs/heads/cms/').pop();
|
const contentKey = branch.ref.split('refs/heads/cms/').pop();
|
||||||
return sem.take(() => this.api.readUnpublishedBranchFile(contentKey).then((data) => {
|
return sem.take(() => this.api.readUnpublishedBranchFile(contentKey).then((data) => {
|
||||||
const entryPath = data.metaData.objects.entry;
|
if (data === null || data === undefined) {
|
||||||
const entry = createEntry(entryPath, entryPath.split('/').pop().replace(/\.[^\.]+$/, ''), data.file);
|
resolve(null);
|
||||||
entry.metaData = data.metaData;
|
sem.leave();
|
||||||
resolve(entry);
|
} else {
|
||||||
sem.leave();
|
const entryPath = data.metaData.objects.entry;
|
||||||
|
const entry = createEntry('draft', entryPath.split('/').pop().replace(/\.[^\.]+$/, ''), entryPath, { raw: data.file });
|
||||||
|
entry.metaData = data.metaData;
|
||||||
|
resolve(entry);
|
||||||
|
sem.leave();
|
||||||
|
}
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
sem.leave();
|
sem.leave();
|
||||||
reject(err);
|
reject(err);
|
||||||
@ -84,16 +96,17 @@ export default class GitHub {
|
|||||||
});
|
});
|
||||||
return Promise.all(promises);
|
return Promise.all(promises);
|
||||||
}).then((entries) => {
|
}).then((entries) => {
|
||||||
|
const filteredEntries = entries.filter(entry => entry !== null);
|
||||||
return {
|
return {
|
||||||
pagination: {},
|
pagination: 0,
|
||||||
entries
|
entries: filteredEntries,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
unpublishedEntry(collection, slug) {
|
unpublishedEntry(collection, slug) {
|
||||||
return this.unpublishedEntries().then((response) => (
|
return this.unpublishedEntries().then(response => (
|
||||||
response.entries.filter((entry) => (
|
response.entries.filter(entry => (
|
||||||
entry.metaData && entry.metaData.collection === collection.get('name') && entry.slug === slug
|
entry.metaData && entry.metaData.collection === collection.get('name') && entry.slug === slug
|
||||||
))[0]
|
))[0]
|
||||||
));
|
));
|
||||||
|
@ -19,7 +19,6 @@ export default class AuthenticationPage extends React.Component {
|
|||||||
'Authorization': 'Basic ' + btoa(`${email}:${password}`)
|
'Authorization': 'Basic ' + btoa(`${email}:${password}`)
|
||||||
}
|
}
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
console.log(response);
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
return response.json().then((data) => {
|
return response.json().then((data) => {
|
||||||
this.props.onLogin(Object.assign({ email }, data));
|
this.props.onLogin(Object.assign({ email }, data));
|
||||||
|
@ -35,7 +35,7 @@ export default class NetlifyGit {
|
|||||||
files.map((file) => {
|
files.map((file) => {
|
||||||
promises.push(new Promise((resolve, reject) => {
|
promises.push(new Promise((resolve, reject) => {
|
||||||
return sem.take(() => this.api.readFile(file.path, file.sha).then((data) => {
|
return sem.take(() => this.api.readFile(file.path, file.sha).then((data) => {
|
||||||
resolve(createEntry(file.path, file.path.split('/').pop().replace(/\.[^\.]+$/, ''), data));
|
resolve(createEntry(collection.get('name'), file.path.split('/').pop().replace(/\.[^\.]+$/, ''), file.path, { raw: data }));
|
||||||
sem.leave();
|
sem.leave();
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
sem.leave();
|
sem.leave();
|
||||||
@ -50,7 +50,7 @@ export default class NetlifyGit {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
entry(collection, slug) {
|
lookupEntry(collection, slug) {
|
||||||
return this.entries(collection).then((response) => (
|
return this.entries(collection).then((response) => (
|
||||||
response.entries.filter((entry) => entry.slug === slug)[0]
|
response.entries.filter((entry) => entry.slug === slug)[0]
|
||||||
));
|
));
|
||||||
|
@ -29,7 +29,7 @@ export default class TestRepo {
|
|||||||
const folder = collection.get('folder');
|
const folder = collection.get('folder');
|
||||||
if (folder) {
|
if (folder) {
|
||||||
for (var path in window.repoFiles[folder]) {
|
for (var path in window.repoFiles[folder]) {
|
||||||
entries.push(createEntry(folder + '/' + path, getSlug(path), window.repoFiles[folder][path].content));
|
entries.push(createEntry(collection.get('name'), getSlug(path), folder + '/' + path, { raw: window.repoFiles[folder][path].content }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,7 +39,7 @@ export default class TestRepo {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
entry(collection, slug) {
|
lookupEntry(collection, slug) {
|
||||||
return this.entries(collection).then((response) => (
|
return this.entries(collection).then((response) => (
|
||||||
response.entries.filter((entry) => entry.slug === slug)[0]
|
response.entries.filter((entry) => entry.slug === slug)[0]
|
||||||
));
|
));
|
||||||
|
@ -8,17 +8,20 @@ export default class ControlPane extends Component {
|
|||||||
controlFor(field) {
|
controlFor(field) {
|
||||||
const { entry, getMedia, onChange, onAddMedia, onRemoveMedia } = this.props;
|
const { entry, getMedia, onChange, onAddMedia, onRemoveMedia } = this.props;
|
||||||
const widget = resolveWidget(field.get('widget'));
|
const widget = resolveWidget(field.get('widget'));
|
||||||
|
const fieldName = field.get('name');
|
||||||
|
const value = entry.getIn(['data', fieldName]);
|
||||||
|
if (!value) return null;
|
||||||
return (
|
return (
|
||||||
<div className={styles.control}>
|
<div className={styles.control}>
|
||||||
<label className={styles.label}>{field.get('label')}</label>
|
<label className={styles.label} htmlFor={fieldName}>{field.get('label')}</label>
|
||||||
{
|
{
|
||||||
React.createElement(widget.control, {
|
React.createElement(widget.control, {
|
||||||
field: field,
|
field,
|
||||||
value: entry.getIn(['data', field.get('name')]),
|
value,
|
||||||
onChange: (value) => onChange(entry.setIn(['data', field.get('name')], value)),
|
onChange: val => onChange(entry.setIn(['data', fieldName], val)),
|
||||||
onAddMedia: onAddMedia,
|
onAddMedia,
|
||||||
onRemoveMedia: onRemoveMedia,
|
onRemoveMedia,
|
||||||
getMedia: getMedia
|
getMedia,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import React from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
import { Map } from 'immutable';
|
||||||
import Bricks from 'bricks.js';
|
import Bricks from 'bricks.js';
|
||||||
|
import Waypoint from 'react-waypoint';
|
||||||
import history from '../routing/history';
|
import history from '../routing/history';
|
||||||
import Cards from './Cards';
|
import Cards from './Cards';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
@ -23,6 +25,7 @@ export default class EntryListing extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.updateBricks = _.throttle(this.updateBricks.bind(this), 30);
|
this.updateBricks = _.throttle(this.updateBricks.bind(this), 30);
|
||||||
|
this.handleLoadMore = this.handleLoadMore.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -58,7 +61,6 @@ export default class EntryListing extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cardFor(collection, entry, link) {
|
cardFor(collection, entry, link) {
|
||||||
//const { entry, getMedia, onChange, onAddMedia, onRemoveMedia } = this.props;
|
|
||||||
const cartType = collection.getIn(['card', 'type']) || 'alltype';
|
const cartType = collection.getIn(['card', 'type']) || 'alltype';
|
||||||
const card = Cards[cartType] || Cards._unknown;
|
const card = Cards[cartType] || Cards._unknown;
|
||||||
return React.createElement(card, {
|
return React.createElement(card, {
|
||||||
@ -72,23 +74,47 @@ export default class EntryListing extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
handleLoadMore() {
|
||||||
const { collection, entries } = this.props;
|
this.props.onPaginate(this.props.page + 1);
|
||||||
const name = collection.get('name');
|
}
|
||||||
|
|
||||||
|
renderCards = () => {
|
||||||
|
const { collections, entries } = this.props;
|
||||||
|
if (Map.isMap(collections)) {
|
||||||
|
const collectionName = collections.get('name');
|
||||||
|
return entries.map((entry) => {
|
||||||
|
const path = `/collections/${collectionName}/entries/${entry.get('slug')}`;
|
||||||
|
return this.cardFor(collections, entry, path);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return entries.map((entry) => {
|
||||||
|
const collection = collections.filter(collection => collection.get('name') === entry.get('collection')).first();
|
||||||
|
const path = `/collections/${collection.get('name')}/entries/${entry.get('slug')}`;
|
||||||
|
return this.cardFor(collection, entry, path);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { children } = this.props;
|
||||||
|
const cards = this.renderCards();
|
||||||
return <div>
|
return <div>
|
||||||
<h1>Listing {name}</h1>
|
<h1>{children}</h1>
|
||||||
<div ref={(c) => this._entries = c}>
|
<div ref={(c) => this._entries = c}>
|
||||||
{entries.map((entry) => {
|
{cards}
|
||||||
const path = `/collections/${name}/entries/${entry.get('slug')}`;
|
<Waypoint onEnter={this.handleLoadMore} />
|
||||||
return this.cardFor(collection, entry, path);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
EntryListing.propTypes = {
|
EntryListing.propTypes = {
|
||||||
collection: ImmutablePropTypes.map.isRequired,
|
children: PropTypes.node.isRequired,
|
||||||
|
collections: PropTypes.oneOfType([
|
||||||
|
ImmutablePropTypes.map,
|
||||||
|
ImmutablePropTypes.iterable
|
||||||
|
]).isRequired,
|
||||||
entries: ImmutablePropTypes.list,
|
entries: ImmutablePropTypes.list,
|
||||||
|
onPaginate: PropTypes.func.isRequired,
|
||||||
|
page: PropTypes.number,
|
||||||
};
|
};
|
||||||
|
@ -16,6 +16,9 @@ class UnpublishedListing extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
requestPublish = (collection, slug, ownStatus) => {
|
requestPublish = (collection, slug, ownStatus) => {
|
||||||
|
console.log('HERE');
|
||||||
|
console.log(ownStatus);
|
||||||
|
console.log(status.last());
|
||||||
if (ownStatus !== status.last()) return;
|
if (ownStatus !== status.last()) return;
|
||||||
if (window.confirm('Are you sure you want to publish this entry?')) {
|
if (window.confirm('Are you sure you want to publish this entry?')) {
|
||||||
this.props.handlePublish(collection, slug, ownStatus);
|
this.props.handlePublish(collection, slug, ownStatus);
|
||||||
@ -39,12 +42,12 @@ class UnpublishedListing extends React.Component {
|
|||||||
/* eslint-enable */
|
/* eslint-enable */
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
return <div>
|
return (<div>
|
||||||
{entries.map(entry => {
|
{entries.map((entry) => {
|
||||||
// Look for an "author" field. Fallback to username on backend implementation;
|
// Look for an "author" field. Fallback to username on backend implementation;
|
||||||
const author = entry.getIn(['data', 'author'], entry.getIn(['metaData', 'user']));
|
const author = entry.getIn(['data', 'author'], entry.getIn(['metaData', 'user']));
|
||||||
const timeStamp = moment(entry.getIn(['metaData', 'timeStamp'])).format('llll');
|
const timeStamp = moment(entry.getIn(['metaData', 'timeStamp'])).format('llll');
|
||||||
const link = `/editorialworkflow/${entry.getIn(['metaData', 'collection'])}/${entry.getIn(['metaData', 'status'])}/${entry.get('slug')}`;
|
const link = `/editorialworkflow/${ entry.getIn(['metaData', 'collection']) }/${ entry.getIn(['metaData', 'status']) }/${ entry.get('slug') }`;
|
||||||
const slug = entry.get('slug');
|
const slug = entry.get('slug');
|
||||||
const ownStatus = entry.getIn(['metaData', 'status']);
|
const ownStatus = entry.getIn(['metaData', 'status']);
|
||||||
const collection = entry.getIn(['metaData', 'collection']);
|
const collection = entry.getIn(['metaData', 'collection']);
|
||||||
@ -56,7 +59,7 @@ class UnpublishedListing extends React.Component {
|
|||||||
<span className={styles.cardHeading}><Link to={link}>{entry.getIn(['data', 'title'])}</Link> <small>by {author}</small></span>
|
<span className={styles.cardHeading}><Link to={link}>{entry.getIn(['data', 'title'])}</Link> <small>by {author}</small></span>
|
||||||
<p className={styles.cardText}>Last updated: {timeStamp} by {entry.getIn(['metaData', 'user'])}</p>
|
<p className={styles.cardText}>Last updated: {timeStamp} by {entry.getIn(['metaData', 'user'])}</p>
|
||||||
{(ownStatus === status.last()) &&
|
{(ownStatus === status.last()) &&
|
||||||
<button className={styles.button} onClick={this.requestPublish.bind(this, collection, slug, status)}>Publish now</button>
|
<button className={styles.button} onClick={this.requestPublish.bind(this, collection, slug, ownStatus)}>Publish now</button>
|
||||||
}
|
}
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
@ -65,7 +68,7 @@ class UnpublishedListing extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
</div>;
|
</div>);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -81,7 +84,7 @@ class UnpublishedListing extends React.Component {
|
|||||||
<div className={styles.clear}>
|
<div className={styles.clear}>
|
||||||
<h1>Editorial Workflow</h1>
|
<h1>Editorial Workflow</h1>
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
{columns}
|
{columns}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -83,7 +83,6 @@ export default class RawEditor extends React.Component {
|
|||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const content = props.value ? Plain.deserialize(props.value) : Plain.deserialize('');
|
const content = props.value ? Plain.deserialize(props.value) : Plain.deserialize('');
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
@ -17,7 +17,7 @@ const EditorComponent = Record({
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
class Plugin extends Component {
|
class Plugin extends Component { // eslint-disable-line
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
children: PropTypes.element.isRequired
|
children: PropTypes.element.isRequired
|
||||||
};
|
};
|
||||||
|
@ -9,10 +9,12 @@ import styles from './CollectionPage.css';
|
|||||||
import CollectionPageHOC from './editorialWorkflow/CollectionPageHOC';
|
import CollectionPageHOC from './editorialWorkflow/CollectionPageHOC';
|
||||||
|
|
||||||
class DashboardPage extends React.Component {
|
class DashboardPage extends React.Component {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
collection: ImmutablePropTypes.map.isRequired,
|
collection: ImmutablePropTypes.map.isRequired,
|
||||||
collections: ImmutablePropTypes.orderedMap.isRequired,
|
collections: ImmutablePropTypes.orderedMap.isRequired,
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
page: PropTypes.number,
|
||||||
entries: ImmutablePropTypes.list,
|
entries: ImmutablePropTypes.list,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -30,20 +32,30 @@ class DashboardPage extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleLoadMore = (page) => {
|
||||||
|
const { collection, dispatch } = this.props;
|
||||||
|
dispatch(loadEntries(collection, page));
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { collections, collection, entries } = this.props;
|
const { collections, collection, page, entries } = this.props;
|
||||||
if (collections == null) {
|
if (collections == null) {
|
||||||
return <h1>No collections defined in your config.yml</h1>;
|
return <h1>No collections defined in your config.yml</h1>;
|
||||||
}
|
}
|
||||||
|
return (<div className={styles.root}>
|
||||||
|
|
||||||
return <div className={styles.root}>
|
|
||||||
{entries ?
|
{entries ?
|
||||||
<EntryListing collection={collection} entries={entries}/>
|
<EntryListing
|
||||||
|
collections={collection}
|
||||||
|
entries={entries}
|
||||||
|
page={page}
|
||||||
|
onPaginate={this.handleLoadMore}
|
||||||
|
>
|
||||||
|
{collection.get('name')}
|
||||||
|
</EntryListing>
|
||||||
:
|
:
|
||||||
<Loader active>{['Loading Entries', 'Caching Entries', 'This might take several minutes']}</Loader>
|
<Loader active>{['Loading Entries', 'Caching Entries', 'This might take several minutes']}</Loader>
|
||||||
}
|
}
|
||||||
</div>;
|
</div>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,16 +63,18 @@ class DashboardPage extends React.Component {
|
|||||||
* Instead of checking the publish mode everywhere to dispatch & render the additional editorial workflow stuff,
|
* Instead of checking the publish mode everywhere to dispatch & render the additional editorial workflow stuff,
|
||||||
* We delegate it to a Higher Order Component
|
* We delegate it to a Higher Order Component
|
||||||
*/
|
*/
|
||||||
DashboardPage = CollectionPageHOC(DashboardPage);
|
DashboardPage = CollectionPageHOC(DashboardPage); // eslint-disable-line
|
||||||
|
|
||||||
|
|
||||||
function mapStateToProps(state, ownProps) {
|
function mapStateToProps(state, ownProps) {
|
||||||
const { collections } = state;
|
const { collections } = state;
|
||||||
const { name, slug } = ownProps.params;
|
const { name, slug } = ownProps.params;
|
||||||
const collection = name ? collections.get(name) : collections.first();
|
const collection = name ? collections.get(name) : collections.first();
|
||||||
|
const page = state.entries.getIn(['pages', collection.get('name'), 'page']);
|
||||||
|
|
||||||
const entries = selectEntries(state, collection.get('name'));
|
const entries = selectEntries(state, collection.get('name'));
|
||||||
|
|
||||||
return { slug, collection, collections, entries };
|
return { slug, collection, collections, page, entries };
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps)(DashboardPage);
|
export default connect(mapStateToProps)(DashboardPage);
|
||||||
|
@ -33,18 +33,18 @@ class EntryPage extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (!this.props.newEntry) {
|
const { entry, collection, slug } = this.props;
|
||||||
this.props.loadEntry(this.props.collection, this.props.slug);
|
|
||||||
|
|
||||||
this.createDraft(this.props.entry);
|
if (this.props.newEntry) {
|
||||||
} else {
|
|
||||||
this.props.createEmptyDraft(this.props.collection);
|
this.props.createEmptyDraft(this.props.collection);
|
||||||
|
} else {
|
||||||
|
this.props.loadEntry(entry, collection, slug);
|
||||||
|
this.createDraft(entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
if (this.props.entry === nextProps.entry) return;
|
if (this.props.entry === nextProps.entry) return;
|
||||||
|
|
||||||
if (nextProps.entry && !nextProps.entry.get('isFetching')) {
|
if (nextProps.entry && !nextProps.entry.get('isFetching')) {
|
||||||
this.createDraft(nextProps.entry);
|
this.createDraft(nextProps.entry);
|
||||||
} else if (nextProps.newEntry) {
|
} else if (nextProps.newEntry) {
|
||||||
@ -86,6 +86,13 @@ class EntryPage extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Instead of checking the publish mode everywhere to dispatch & render the additional editorial workflow stuff,
|
||||||
|
* We delegate it to a Higher Order Component
|
||||||
|
*/
|
||||||
|
EntryPage = EntryPageHOC(EntryPage);
|
||||||
|
|
||||||
function mapStateToProps(state, ownProps) {
|
function mapStateToProps(state, ownProps) {
|
||||||
const { collections, entryDraft } = state;
|
const { collections, entryDraft } = state;
|
||||||
const collection = collections.get(ownProps.params.name);
|
const collection = collections.get(ownProps.params.name);
|
||||||
@ -96,12 +103,6 @@ function mapStateToProps(state, ownProps) {
|
|||||||
return { collection, collections, newEntry, entryDraft, boundGetMedia, slug, entry };
|
return { collection, collections, newEntry, entryDraft, boundGetMedia, slug, entry };
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Instead of checking the publish mode everywhere to dispatch & render the additional editorial workflow stuff,
|
|
||||||
* We delegate it to a Higher Order Component
|
|
||||||
*/
|
|
||||||
EntryPage = EntryPageHOC(EntryPage);
|
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
mapStateToProps,
|
mapStateToProps,
|
||||||
{
|
{
|
||||||
|
@ -1,12 +1,64 @@
|
|||||||
import React from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import { selectSearchedEntries } from '../reducers';
|
||||||
|
import { searchEntries } from '../actions/entries';
|
||||||
|
import { Loader } from '../components/UI';
|
||||||
|
import EntryListing from '../components/EntryListing';
|
||||||
|
import styles from './CollectionPage.css';
|
||||||
|
|
||||||
class SearchPage extends React.Component {
|
class SearchPage extends React.Component {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
isFetching: PropTypes.bool,
|
||||||
|
searchEntries: PropTypes.func.isRequired,
|
||||||
|
searchTerm: PropTypes.string.isRequired,
|
||||||
|
entries: ImmutablePropTypes.list
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const { searchTerm, searchEntries } = this.props;
|
||||||
|
searchEntries(searchTerm);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
if (this.props.searchTerm === nextProps.searchTerm) return;
|
||||||
|
const { searchEntries } = this.props;
|
||||||
|
searchEntries(nextProps.searchTerm);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLoadMore = (page) => {
|
||||||
|
const { searchTerm, searchEntries } = this.props;
|
||||||
|
searchEntries(searchTerm, page);
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <div>
|
const { collections, searchTerm, entries, isFetching, page } = this.props;
|
||||||
<h1>Search</h1>
|
return <div className={styles.root}>
|
||||||
|
{(isFetching === true || !entries) ?
|
||||||
|
<Loader active>{['Loading Entries', 'Caching Entries', 'This might take several minutes']}</Loader>
|
||||||
|
:
|
||||||
|
<EntryListing collections={collections} entries={entries} page={page} onPaginate={this.handleLoadMore}>
|
||||||
|
Results for “{searchTerm}”
|
||||||
|
</EntryListing>
|
||||||
|
}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect()(SearchPage);
|
|
||||||
|
function mapStateToProps(state, ownProps) {
|
||||||
|
const isFetching = state.entries.getIn(['search', 'isFetching']);
|
||||||
|
const page = state.entries.getIn(['search', 'page']);
|
||||||
|
const entries = selectSearchedEntries(state);
|
||||||
|
const collections = state.collections.toIndexedSeq();
|
||||||
|
const searchTerm = ownProps.params && ownProps.params.searchTerm;
|
||||||
|
|
||||||
|
return { isFetching, page, collections, entries, searchTerm };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
{ searchEntries }
|
||||||
|
)(SearchPage);
|
||||||
|
28
src/integrations/index.js
Normal file
28
src/integrations/index.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import Algolia from './providers/algolia/implementation';
|
||||||
|
import { Map } from 'immutable';
|
||||||
|
|
||||||
|
export function resolveIntegrations(interationsConfig) {
|
||||||
|
let integrationInstances = Map({});
|
||||||
|
interationsConfig.get('providers').forEach((providerData, providerName) => {
|
||||||
|
switch (providerName) {
|
||||||
|
case 'algolia':
|
||||||
|
integrationInstances = integrationInstances.set('algolia', new Algolia(providerData));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return integrationInstances;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const getIntegrationProvider = (function() {
|
||||||
|
let integrations = null;
|
||||||
|
|
||||||
|
return (interationsConfig, provider) => {
|
||||||
|
if (integrations) {
|
||||||
|
return integrations.get(provider);
|
||||||
|
} else {
|
||||||
|
integrations = resolveIntegrations(interationsConfig);
|
||||||
|
return integrations.get(provider);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
123
src/integrations/providers/algolia/implementation.js
Normal file
123
src/integrations/providers/algolia/implementation.js
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import { createEntry } from '../../../valueObjects/Entry';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
function getSlug(path) {
|
||||||
|
const m = path.match(/([^\/]+?)(\.[^\/\.]+)?$/);
|
||||||
|
return m && m[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Algolia {
|
||||||
|
constructor(config) {
|
||||||
|
this.config = config;
|
||||||
|
if (config.get('applicationID') == null ||
|
||||||
|
config.get('apiKey') == null) {
|
||||||
|
throw 'The Algolia search integration needs the credentials (applicationID and apiKey) in the integration configuration.';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.applicationID = config.get('applicationID');
|
||||||
|
this.apiKey = config.get('apiKey');
|
||||||
|
this.searchURL = `https://${this.applicationID}-dsn.algolia.net/1`;
|
||||||
|
|
||||||
|
this.entriesCache = {
|
||||||
|
collection: null,
|
||||||
|
page: null,
|
||||||
|
entries: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
requestHeaders(headers = {}) {
|
||||||
|
return {
|
||||||
|
'X-Algolia-API-Key': this.apiKey,
|
||||||
|
'X-Algolia-Application-Id': this.applicationID,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...headers
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
parseJsonResponse(response) {
|
||||||
|
return response.json().then((json) => {
|
||||||
|
if (!response.ok) {
|
||||||
|
return Promise.reject(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
return json;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
urlFor(path, options) {
|
||||||
|
const params = [];
|
||||||
|
if (options.params) {
|
||||||
|
for (const key in options.params) {
|
||||||
|
params.push(`${key}=${encodeURIComponent(options.params[key])}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (params.length) {
|
||||||
|
path += `?${params.join('&')}`;
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
request(path, options = {}) {
|
||||||
|
const headers = this.requestHeaders(options.headers || {});
|
||||||
|
const url = this.urlFor(path, options);
|
||||||
|
return fetch(url, { ...options, headers: headers }).then((response) => {
|
||||||
|
const contentType = response.headers.get('Content-Type');
|
||||||
|
if (contentType && contentType.match(/json/)) {
|
||||||
|
return this.parseJsonResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.text();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
search(collections, searchTerm, page) {
|
||||||
|
const searchCollections = collections.map(collection => (
|
||||||
|
{ indexName: collection, params: `query=${searchTerm}&page=${page}` }
|
||||||
|
));
|
||||||
|
|
||||||
|
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 = hit.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/${collection}`, {
|
||||||
|
params: {
|
||||||
|
restrictSearchableAttributes: field,
|
||||||
|
query
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
listEntries(collection, page) {
|
||||||
|
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/${collection.get('name')}`, {
|
||||||
|
params: { page }
|
||||||
|
}).then(response => {
|
||||||
|
const entries = response.hits.map(hit => {
|
||||||
|
const slug = hit.slug || getSlug(hit.path);
|
||||||
|
return createEntry(collection.get('name'), slug, hit.path, { data: hit.data, partial: true });
|
||||||
|
});
|
||||||
|
this.entriesCache = { collection, page, entries };
|
||||||
|
return { entries, pagination: response.page };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getEntry(collection, slug) {
|
||||||
|
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 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +1,28 @@
|
|||||||
import Immutable from 'immutable';
|
import Immutable from 'immutable';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import * as publishModes from '../constants/publishModes';
|
||||||
import { CONFIG_REQUEST, CONFIG_SUCCESS, CONFIG_FAILURE } from '../actions/config';
|
import { CONFIG_REQUEST, CONFIG_SUCCESS, CONFIG_FAILURE } from '../actions/config';
|
||||||
|
|
||||||
|
const defaults = {
|
||||||
|
publish_mode: publishModes.SIMPLE
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyDefaults = (config) => {
|
||||||
|
// Make sure there is a public folder
|
||||||
|
_.set(defaults,
|
||||||
|
'public_folder',
|
||||||
|
config.media_folder.charAt(0) === '/' ? config.media_folder : '/' + config.media_folder);
|
||||||
|
|
||||||
|
return _.defaultsDeep(config, defaults);
|
||||||
|
};
|
||||||
|
|
||||||
const config = (state = null, action) => {
|
const config = (state = null, action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case CONFIG_REQUEST:
|
case CONFIG_REQUEST:
|
||||||
return Immutable.Map({ isFetching: true });
|
return Immutable.Map({ isFetching: true });
|
||||||
case CONFIG_SUCCESS:
|
case CONFIG_SUCCESS:
|
||||||
return Immutable.fromJS(action.payload);
|
const config = applyDefaults(action.payload);
|
||||||
|
return Immutable.fromJS(config);
|
||||||
case CONFIG_FAILURE:
|
case CONFIG_FAILURE:
|
||||||
return Immutable.Map({ error: action.payload.toString() });
|
return Immutable.Map({ error: action.payload.toString() });
|
||||||
default:
|
default:
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import { Map, List, fromJS } from 'immutable';
|
import { Map, List, fromJS } from 'immutable';
|
||||||
import {
|
import {
|
||||||
ENTRY_REQUEST, ENTRY_SUCCESS, ENTRIES_REQUEST, ENTRIES_SUCCESS
|
ENTRY_REQUEST, ENTRY_SUCCESS, ENTRIES_REQUEST, ENTRIES_SUCCESS, SEARCH_ENTRIES_REQUEST, SEARCH_ENTRIES_SUCCESS
|
||||||
} from '../actions/entries';
|
} from '../actions/entries';
|
||||||
|
|
||||||
|
let collection, loadedEntries, page, searchTerm;
|
||||||
|
|
||||||
const entries = (state = Map({ entities: Map(), pages: Map() }), action) => {
|
const entries = (state = Map({ entities: Map(), pages: Map() }), action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ENTRY_REQUEST:
|
case ENTRY_REQUEST:
|
||||||
@ -18,14 +20,45 @@ const entries = (state = Map({ entities: Map(), pages: Map() }), action) => {
|
|||||||
return state.setIn(['pages', action.payload.collection, 'isFetching'], true);
|
return state.setIn(['pages', action.payload.collection, 'isFetching'], true);
|
||||||
|
|
||||||
case ENTRIES_SUCCESS:
|
case ENTRIES_SUCCESS:
|
||||||
const { collection, entries, pages } = action.payload;
|
collection = action.payload.collection;
|
||||||
|
loadedEntries = action.payload.entries;
|
||||||
|
page = action.payload.page;
|
||||||
return state.withMutations((map) => {
|
return state.withMutations((map) => {
|
||||||
entries.forEach((entry) => (
|
loadedEntries.forEach((entry) => (
|
||||||
map.setIn(['entities', `${collection}.${entry.slug}`], fromJS(entry).set('isFetching', false))
|
map.setIn(['entities', `${collection}.${entry.slug}`], fromJS(entry).set('isFetching', false))
|
||||||
));
|
));
|
||||||
|
|
||||||
|
const ids = List(loadedEntries.map((entry) => entry.slug));
|
||||||
|
|
||||||
map.setIn(['pages', collection], Map({
|
map.setIn(['pages', collection], Map({
|
||||||
...pages,
|
page: page,
|
||||||
ids: List(entries.map((entry) => entry.slug))
|
ids: page === 0 ? ids : map.getIn(['pages', collection, 'ids'], List()).concat(ids)
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
case SEARCH_ENTRIES_REQUEST:
|
||||||
|
if (action.payload.searchTerm !== state.getIn(['search', 'term'])) {
|
||||||
|
return state.withMutations((map) => {
|
||||||
|
map.setIn(['search', 'isFetching'], true);
|
||||||
|
map.setIn(['search', 'term'], action.payload.searchTerm);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
case SEARCH_ENTRIES_SUCCESS:
|
||||||
|
loadedEntries = action.payload.entries;
|
||||||
|
page = action.payload.page;
|
||||||
|
searchTerm = action.payload.searchTerm;
|
||||||
|
return state.withMutations((map) => {
|
||||||
|
loadedEntries.forEach((entry) => (
|
||||||
|
map.setIn(['entities', `${entry.collection}.${entry.slug}`], fromJS(entry).set('isFetching', false))
|
||||||
|
));
|
||||||
|
const ids = List(loadedEntries.map(entry => ({ collection: entry.collection, slug: entry.slug })));
|
||||||
|
map.set('search', Map({
|
||||||
|
page: page,
|
||||||
|
term: searchTerm,
|
||||||
|
ids: page === 0 ? ids : map.getIn(['search', 'ids'], List()).concat(ids)
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -43,4 +76,9 @@ export const selectEntries = (state, collection) => {
|
|||||||
return slugs && slugs.map((slug) => selectEntry(state, collection, slug));
|
return slugs && slugs.map((slug) => selectEntry(state, collection, slug));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const selectSearchedEntries = (state) => {
|
||||||
|
const searchItems = state.getIn(['search', 'ids']);
|
||||||
|
return searchItems && searchItems.map(({ collection, slug }) => selectEntry(state, collection, slug));
|
||||||
|
};
|
||||||
|
|
||||||
export default entries;
|
export default entries;
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import auth from './auth';
|
import auth from './auth';
|
||||||
import config from './config';
|
import config from './config';
|
||||||
import editor from './editor';
|
import editor from './editor';
|
||||||
|
import integrations, * as fromIntegrations from './integrations';
|
||||||
import entries, * as fromEntries from './entries';
|
import entries, * as fromEntries from './entries';
|
||||||
import editorialWorkflow, * as fromEditorialWorkflow from './editorialWorkflow';
|
import editorialWorkflow, * as fromEditorialWorkflow from './editorialWorkflow';
|
||||||
import entryDraft from './entryDraft';
|
import entryDraft from './entryDraft';
|
||||||
import collections from './collections';
|
import collections from './collections';
|
||||||
import medias, * as fromMedias from './medias';
|
import medias, * as fromMedias from './medias';
|
||||||
@ -11,6 +12,7 @@ const reducers = {
|
|||||||
auth,
|
auth,
|
||||||
config,
|
config,
|
||||||
collections,
|
collections,
|
||||||
|
integrations,
|
||||||
editor,
|
editor,
|
||||||
entries,
|
entries,
|
||||||
editorialWorkflow,
|
editorialWorkflow,
|
||||||
@ -29,11 +31,17 @@ export const selectEntry = (state, collection, slug) =>
|
|||||||
export const selectEntries = (state, collection) =>
|
export const selectEntries = (state, collection) =>
|
||||||
fromEntries.selectEntries(state.entries, collection);
|
fromEntries.selectEntries(state.entries, collection);
|
||||||
|
|
||||||
|
export const selectSearchedEntries = (state) =>
|
||||||
|
fromEntries.selectSearchedEntries(state.entries);
|
||||||
|
|
||||||
export const selectUnpublishedEntry = (state, status, slug) =>
|
export const selectUnpublishedEntry = (state, status, slug) =>
|
||||||
fromEditorialWorkflow.selectUnpublishedEntry(state.editorialWorkflow, status, slug);
|
fromEditorialWorkflow.selectUnpublishedEntry(state.editorialWorkflow, status, slug);
|
||||||
|
|
||||||
export const selectUnpublishedEntries = (state, status) =>
|
export const selectUnpublishedEntries = (state, status) =>
|
||||||
fromEditorialWorkflow.selectUnpublishedEntries(state.editorialWorkflow, status);
|
fromEditorialWorkflow.selectUnpublishedEntries(state.editorialWorkflow, status);
|
||||||
|
|
||||||
|
export const selectIntegration = (state, collection, hook) =>
|
||||||
|
fromIntegrations.selectIntegration(state.integrations, collection, hook);
|
||||||
|
|
||||||
export const getMedia = (state, path) =>
|
export const getMedia = (state, path) =>
|
||||||
fromMedias.getMedia(state.medias, path);
|
fromMedias.getMedia(state.medias, path);
|
||||||
|
29
src/reducers/integrations.js
Normal file
29
src/reducers/integrations.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { fromJS } from 'immutable';
|
||||||
|
import { CONFIG_SUCCESS } from '../actions/config';
|
||||||
|
|
||||||
|
const integrations = (state = null, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case CONFIG_SUCCESS:
|
||||||
|
const integrations = action.payload.integrations || [];
|
||||||
|
const newState = integrations.reduce((acc, integration) => {
|
||||||
|
const { hooks, collections, provider, ...providerData } = integration;
|
||||||
|
acc.providers[provider] = { ...providerData };
|
||||||
|
collections.forEach(collection => {
|
||||||
|
hooks.forEach(hook => {
|
||||||
|
acc.hooks[collection] ? acc.hooks[collection][hook] = provider : acc.hooks[collection] = { [hook]: provider };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return acc;
|
||||||
|
}, { providers:{}, hooks: {} });
|
||||||
|
return fromJS(newState);
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const selectIntegration = (state, collection, hook) => {
|
||||||
|
return state.getIn(['hooks', collection, hook], false);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default integrations;
|
@ -13,7 +13,7 @@ export default (
|
|||||||
<Route path="/collections/:name/entries/new" component={EntryPage} newRecord />
|
<Route path="/collections/:name/entries/new" component={EntryPage} newRecord />
|
||||||
<Route path="/collections/:name/entries/:slug" component={EntryPage} />
|
<Route path="/collections/:name/entries/:slug" component={EntryPage} />
|
||||||
<Route path="/editorialworkflow/:name/:status/:slug" component={EntryPage} unpublishedEntry />
|
<Route path="/editorialworkflow/:name/:status/:slug" component={EntryPage} unpublishedEntry />
|
||||||
<Route path="/search" component={SearchPage}/>
|
<Route path="/search/:searchTerm" component={SearchPage}/>
|
||||||
<Route path="*" component={NotFoundPage}/>
|
<Route path="*" component={NotFoundPage}/>
|
||||||
</Route>
|
</Route>
|
||||||
);
|
);
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
export function createEntry(path = '', slug = '', raw = '') {
|
export function createEntry(collection, slug = '', path = '', options = {}) {
|
||||||
const returnObj = {};
|
const returnObj = {};
|
||||||
returnObj.path = path;
|
returnObj.collection = collection;
|
||||||
returnObj.slug = slug;
|
returnObj.slug = slug;
|
||||||
returnObj.raw = raw;
|
returnObj.path = path;
|
||||||
returnObj.data = {};
|
returnObj.partial = options.partial || false;
|
||||||
returnObj.metaData = {};
|
returnObj.raw = options.raw || '';
|
||||||
|
returnObj.data = options.data || {};
|
||||||
|
returnObj.metaData = options.metaData || null;
|
||||||
return returnObj;
|
return returnObj;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user