migrate core to emotion
This commit is contained in:
parent
768fcbaa1d
commit
4931711892
25
.babelrc
25
.babelrc
@ -1,9 +1,17 @@
|
||||
{
|
||||
"presets": [
|
||||
"react",
|
||||
"env",
|
||||
["env", {
|
||||
"modules": false,
|
||||
}],
|
||||
],
|
||||
"plugins": [
|
||||
"transform-object-rest-spread",
|
||||
"transform-class-properties",
|
||||
["transform-runtime", {
|
||||
"useBuiltIns": true,
|
||||
"useESModules": true,
|
||||
}],
|
||||
["module-resolver", {
|
||||
"root": [
|
||||
"./src/components"
|
||||
@ -22,8 +30,17 @@
|
||||
}
|
||||
}],
|
||||
"inline-svg",
|
||||
["inline-import", {
|
||||
"extensions": ["yml"],
|
||||
}],
|
||||
],
|
||||
"env": {
|
||||
"production": {
|
||||
"plugins": [
|
||||
["emotion", {"hoist": true}],
|
||||
],
|
||||
},
|
||||
"development": {
|
||||
"plugins": [
|
||||
["emotion", {"sourceMap": true, "autoLabel": true }],
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
22100
package-lock.json
generated
22100
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
35
package.json
35
package.json
@ -15,6 +15,11 @@
|
||||
"dist/"
|
||||
],
|
||||
"pre-commit": "lint:staged",
|
||||
"scripts": {
|
||||
"start": "lerna run --parallel watch",
|
||||
"clean": "cd packages && rimraf */.cache */*.js */*.css */*.svg */*.map",
|
||||
"reset": "yarn clean && lerna clean --yes && lerna bootstrap && cd packages/netlify-cms-core && yarn link netlify-cms-lib-auth netlify-cms-lib-util netlify-cms-ui-default react-split-pane"
|
||||
},
|
||||
"jest": {
|
||||
"moduleNameMapper": {
|
||||
"^.+\\.(png|eot|woff|woff2|ttf|svg|gif)$": "<rootDir>/__mocks__/fileLoaderMock.js",
|
||||
@ -53,49 +58,29 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"all-contributors-cli": "^4.4.0",
|
||||
"babel-plugin-emotion": "^9.2.4",
|
||||
"babel-plugin-inline-import": "^3.0.0",
|
||||
"babel-plugin-inline-svg": "^1.0.0",
|
||||
"babel-plugin-module-resolver": "^3.0.0",
|
||||
"babel-plugin-transform-class-properties": "^6.24.1",
|
||||
"babel-plugin-transform-inline-environment-variables": "^0.4.3",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"babel-preset-env": "^1.6.0",
|
||||
"babel-preset-react": "^6.23.0",
|
||||
"cross-env": "^5.1.4",
|
||||
"css-loader": "^0.28.11",
|
||||
"cssnano": "^v4.0.0-rc.2",
|
||||
"deep-equal": "^1.0.1",
|
||||
"enzyme": "^3.1.0",
|
||||
"enzyme-adapter-react-16": "^1.0.2",
|
||||
"eslint": "^3.7.1",
|
||||
"eslint-config-netlify": "github:netlify/eslint-config-netlify",
|
||||
"eslint-import-resolver-babel-module": "^4.0.0",
|
||||
"exports-loader": "^0.7.0",
|
||||
"file-loader": "^1.1.11",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"imports-loader": "^0.8.0",
|
||||
"jest": "^22.0.0",
|
||||
"jest-cli": "^22.0.0",
|
||||
"lerna": "^2.11.0",
|
||||
"lint-staged": "^3.3.1",
|
||||
"mini-css-extract-plugin": "^0.4.0",
|
||||
"npm-check": "^5.2.3",
|
||||
"parcel-plugin-inlinesvg": "^0.0.14",
|
||||
"postcss-cssnext": "^3.0.2",
|
||||
"postcss-import": "^11.0.0",
|
||||
"postcss-loader": "^2.1.3",
|
||||
"raf": "^3.4.0",
|
||||
"react-test-renderer": "^16.0.0",
|
||||
"style-loader": "^0.20.3",
|
||||
"stylefmt": "^4.3.1",
|
||||
"stylelint": "^7.9.0",
|
||||
"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",
|
||||
"svg-inline-loader": "^0.8.0",
|
||||
"webpack": "^4.4.1",
|
||||
"webpack-cli": "^2.0.13",
|
||||
"webpack-dev-server": "^3.1.3",
|
||||
"write-file-webpack-plugin": "^4.2.0"
|
||||
"rimraf": "^2.6.2"
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
|
4
packages/netlify-cms-core/example/cms-test.js
Normal file
4
packages/netlify-cms-core/example/cms-test.js
Normal file
@ -0,0 +1,4 @@
|
||||
import { init } from '../src/index';
|
||||
import config from './config.yml';
|
||||
|
||||
init({ config });
|
@ -3,19 +3,12 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>This is an example</title>
|
||||
<title>Netlify CMS Development Test</title>
|
||||
|
||||
<link href='https://fonts.googleapis.com/css?family=Roboto:300,400,500' rel='stylesheet' type='text/css'>
|
||||
<link rel="stylesheet" href="../src/index.css"/>
|
||||
<!--
|
||||
Netlify CMS will automatically look for a "config.yml" file in the same
|
||||
directory as the CMS HTML file (like this one), but you can override this by
|
||||
providing a link tag like the one below.
|
||||
-->
|
||||
|
||||
<link href="./config.yml" type="text/yaml" rel="cms-config-url">
|
||||
|
||||
<script>
|
||||
window.CMS_MANUAL_INIT = true;
|
||||
window.repoFiles = {
|
||||
_posts: {
|
||||
"2015-02-14-this-is-a-post.md": {
|
||||
@ -89,7 +82,7 @@
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<script src='../src/index.js'></script>
|
||||
<script src='cms-test.js'></script>
|
||||
<script>
|
||||
var PostPreview = createClass({
|
||||
render: function() {
|
||||
@ -167,10 +160,34 @@
|
||||
}
|
||||
});
|
||||
|
||||
const previewStyles = `
|
||||
html,
|
||||
body {
|
||||
color: #444;
|
||||
font-size: 14px;
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-top: 20px;
|
||||
color: #666;
|
||||
font-weight: bold;
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
CMS.registerPreviewTemplate("posts", PostPreview);
|
||||
CMS.registerPreviewTemplate("general", GeneralPreview);
|
||||
CMS.registerPreviewTemplate("authors", AuthorsPreview);
|
||||
CMS.registerPreviewStyle("/example.css");
|
||||
CMS.registerPreviewStyle(previewStyles, { raw: true });
|
||||
// Pass the name of a registered control to reuse with a new widget preview.
|
||||
CMS.registerWidget("relationKitchenSinkPost", "relation", RelationKitchenSinkPostPreview);
|
||||
CMS.registerEditorComponent({
|
||||
|
27009
packages/netlify-cms-core/package-lock.json
generated
27009
packages/netlify-cms-core/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -15,8 +15,8 @@
|
||||
"dist/"
|
||||
],
|
||||
"scripts": {
|
||||
"start": "npm run dev",
|
||||
"dev": "parcel example/index.html --open"
|
||||
"watch": "cross-env NETLIFY_CMS_VERSION=$npm_package_version parcel example/index.html --no-cache --open",
|
||||
"build": "cross-env NETLIFY_CMS_VERSION=$npm_package_version parcel build example/index.html --no-cache"
|
||||
},
|
||||
"keywords": [
|
||||
"netlify",
|
||||
@ -30,6 +30,7 @@
|
||||
"classnames": "^2.2.5",
|
||||
"create-react-class": "^15.6.0",
|
||||
"diacritics": "^1.3.0",
|
||||
"emotion": "^9.1.3",
|
||||
"fuzzy": "^0.1.1",
|
||||
"gotrue-js": "^0.9.15",
|
||||
"gray-matter": "^3.0.6",
|
||||
@ -49,13 +50,14 @@
|
||||
"netlify-cms-lib-util": "file:../netlify-cms-lib-util",
|
||||
"netlify-cms-ui-default": "file:../netlify-cms-ui-default",
|
||||
"prop-types": "^15.5.10",
|
||||
"react": "^16.0.0",
|
||||
"react": "15.x || 16.x",
|
||||
"react-aria-menubutton": "^5.1.0",
|
||||
"react-autosuggest": "^9.3.2",
|
||||
"react-datetime": "^2.11.0",
|
||||
"react-dnd": "^2.5.4",
|
||||
"react-dnd-html5-backend": "^2.5.4",
|
||||
"react-dom": "^16.0.0",
|
||||
"react-emotion": "^9.2.5",
|
||||
"react-frame-component": "^2.0.0",
|
||||
"react-hot-loader": "^4.0.0",
|
||||
"react-immutable-proptypes": "^2.1.0",
|
||||
@ -72,6 +74,7 @@
|
||||
"react-topbar-progress-indicator": "^2.0.0",
|
||||
"react-transition-group": "^2.2.1",
|
||||
"react-waypoint": "^7.1.0",
|
||||
"recompose": "^0.27.1",
|
||||
"redux": "^3.3.1",
|
||||
"redux-notifications": "^4.0.1",
|
||||
"redux-optimist": "^0.0.2",
|
||||
|
@ -1,7 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from "react";
|
||||
import { partial } from 'lodash';
|
||||
import { Icon } from 'netlify-cms-ui-default';
|
||||
import Icon from 'netlify-cms-ui-default/Icon';
|
||||
|
||||
let component = null;
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Authenticator from 'netlify-cms-lib-auth/netlify-auth';
|
||||
import { Icon } from 'netlify-cms-ui-default';
|
||||
import Icon from 'netlify-cms-ui-default/Icon';
|
||||
|
||||
export default class AuthenticationPage extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import NetlifyAuthenticator from 'netlify-cms-lib-auth/netlify-auth';
|
||||
import ImplicitAuthenticator from 'netlify-cms-lib-auth/implicit-oauth';
|
||||
import { Icon } from 'netlify-cms-ui-default';
|
||||
import Icon from 'netlify-cms-ui-default/Icon';
|
||||
|
||||
export default class AuthenticationPage extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import React from 'react';
|
||||
import { Icon } from 'netlify-cms-ui-default';
|
||||
import Icon from 'netlify-cms-ui-default/Icon';
|
||||
|
||||
export default class AuthenticationPage extends React.Component {
|
||||
static propTypes = {
|
||||
|
1
packages/netlify-cms-core/src/bootstrap.js
vendored
1
packages/netlify-cms-core/src/bootstrap.js
vendored
@ -12,6 +12,7 @@ import App from 'App/App';
|
||||
import 'EditorWidgets';
|
||||
import 'MarkdownPlugins';
|
||||
import './index.css';
|
||||
import 'what-input';
|
||||
|
||||
const ROOT_ID = 'nc-root';
|
||||
|
||||
|
@ -1,8 +0,0 @@
|
||||
@import "./NotFoundPage.css";
|
||||
@import "./Header.css";
|
||||
|
||||
.nc-app-main {
|
||||
min-width: 800px;
|
||||
max-width: 1440px;
|
||||
margin: 0 auto;
|
||||
}
|
@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { hot } from 'react-hot-loader';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import styled from 'react-emotion';
|
||||
import { connect } from 'react-redux';
|
||||
import { Route, Switch, Link, Redirect } from 'react-router-dom';
|
||||
import { Notifs } from 'redux-notifications';
|
||||
@ -13,7 +14,8 @@ import { showCollection, createNewEntry } from 'Actions/collections';
|
||||
import { openMediaLibrary as actionOpenMediaLibrary } from 'Actions/mediaLibrary';
|
||||
import MediaLibrary from 'MediaLibrary/MediaLibrary';
|
||||
import { Toast } from 'UI';
|
||||
import { Loader } from 'netlify-cms-ui-default';
|
||||
import Loader from 'netlify-cms-ui-default/Loader';
|
||||
import { colors } from 'netlify-cms-ui-default/styles';
|
||||
import history from 'Routing/history';
|
||||
import { getCollectionUrl, getNewEntryUrl } from 'Lib/urlHelper';
|
||||
import { SIMPLE, EDITORIAL_WORKFLOW } from 'Constants/publishModes';
|
||||
@ -25,16 +27,19 @@ import Header from './Header';
|
||||
|
||||
TopBarProgress.config({
|
||||
barColors: {
|
||||
/**
|
||||
* Uses value from CSS --colorActive.
|
||||
*/
|
||||
"0": '#3a69c8',
|
||||
'1.0': '#3a69c8',
|
||||
'0': colors.active,
|
||||
'1.0': colors.active,
|
||||
},
|
||||
shadowBlur: 0,
|
||||
barThickness: 2,
|
||||
});
|
||||
|
||||
const AppMainContainer = styled.div`
|
||||
min-width: 800px;
|
||||
max-width: 1440px;
|
||||
margin: 0 auto;
|
||||
`
|
||||
|
||||
class App extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
@ -132,7 +137,7 @@ class App extends React.Component {
|
||||
const hasWorkflow = publishMode === EDITORIAL_WORKFLOW;
|
||||
|
||||
return (
|
||||
<div className="nc-app-container">
|
||||
<div>
|
||||
<Notifs CustomComponent={Toast} />
|
||||
<Header
|
||||
user={user}
|
||||
@ -143,7 +148,7 @@ class App extends React.Component {
|
||||
hasWorkflow={hasWorkflow}
|
||||
displayUrl={config.get('display_url')}
|
||||
/>
|
||||
<div className="nc-app-main">
|
||||
<AppMainContainer>
|
||||
{ isFetching && <TopBarProgress /> }
|
||||
<div>
|
||||
<Switch>
|
||||
@ -158,7 +163,7 @@ class App extends React.Component {
|
||||
</Switch>
|
||||
<MediaLibrary/>
|
||||
</div>
|
||||
</div>
|
||||
</AppMainContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,91 +0,0 @@
|
||||
.nc-appHeader-container {
|
||||
z-index: 300;
|
||||
}
|
||||
|
||||
.nc-appHeader-main {
|
||||
@apply(--dropShadowMain);
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
background-color: var(--colorForeground);
|
||||
z-index: 300;
|
||||
height: var(--topBarHeight);
|
||||
}
|
||||
|
||||
.nc-appHeader-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
min-width: 800px;
|
||||
max-width: 1440px;
|
||||
padding: 0 12px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.nc-appHeader-button {
|
||||
background-color: transparent;
|
||||
color: #7b8290;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
display: inline-flex;
|
||||
padding: 16px 20px;
|
||||
align-items: center;
|
||||
|
||||
& .nc-icon {
|
||||
margin-right: 4px;
|
||||
color: #b3b9c4;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus,
|
||||
&.nc-appHeader-button-active {
|
||||
background-color: white;
|
||||
color: var(--colorActive);
|
||||
|
||||
& .nc-icon {
|
||||
color: var(--colorActive);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nc-appHeader-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nc-appHeader-siteLink {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #7b8290;
|
||||
padding: 10px 16px;
|
||||
}
|
||||
|
||||
.nc-appHeader-quickNew {
|
||||
@apply(--buttonMedium);
|
||||
@apply(--buttonGray);
|
||||
margin-right: 8px;
|
||||
|
||||
&:after {
|
||||
top: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
.nc-appHeader-avatar {
|
||||
border: 0;
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
color: #1e2532;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.nc-appHeader-avatar-image,
|
||||
.nc-appHeader-avatar-placeholder {
|
||||
width: 32px;
|
||||
border-radius: 32px;
|
||||
}
|
||||
|
||||
.nc-appHeader-avatar-placeholder {
|
||||
height: 32px;
|
||||
color: #1e2532;
|
||||
background-color: var(--textFieldBorderColor);
|
||||
}
|
@ -1,9 +1,90 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from "react";
|
||||
import ImmutablePropTypes from "react-immutable-proptypes";
|
||||
import styled, { css } from 'react-emotion';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { Icon, Dropdown, DropdownItem } from 'netlify-cms-ui-default';
|
||||
import { stripProtocol } from 'Lib/urlHelper';
|
||||
import uuid from 'uuid/v4';
|
||||
import Icon from 'netlify-cms-ui-default/Icon';
|
||||
import Dropdown, { DropdownItem, StyledDropdownButton } from 'netlify-cms-ui-default/Dropdown'
|
||||
import { colors, colorsRaw, lengths, shadows, buttons } from 'netlify-cms-ui-default/styles'
|
||||
import SettingsDropdown from 'UI/SettingsDropdown';
|
||||
|
||||
const styles = {
|
||||
buttonActive: css`
|
||||
color: ${colors.active};
|
||||
`,
|
||||
};
|
||||
|
||||
const AppHeaderContainer = styled.div`
|
||||
z-index: 300;
|
||||
`;
|
||||
|
||||
const AppHeader = styled.div`
|
||||
${shadows.dropMain};
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
background-color: ${colors.foreground};
|
||||
z-index: 300;
|
||||
height: ${lengths.topBarHeight};
|
||||
`
|
||||
|
||||
const AppHeaderContent = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
min-width: 800px;
|
||||
max-width: 1440px;
|
||||
padding: 0 12px;
|
||||
margin: 0 auto;
|
||||
`;
|
||||
|
||||
const AppHeaderButton = styled.button`
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
color: #7b8290;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
display: inline-flex;
|
||||
padding: 16px 20px;
|
||||
align-items: center;
|
||||
|
||||
${Icon} {
|
||||
margin-right: 4px;
|
||||
color: #b3b9c4;
|
||||
}
|
||||
|
||||
${props => css`
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus,
|
||||
&.${props.activeClassName} {
|
||||
${styles.buttonActive};
|
||||
|
||||
${Icon} {
|
||||
${styles.buttonActive};
|
||||
}
|
||||
}
|
||||
`}
|
||||
`
|
||||
|
||||
const AppHeaderNavLink = AppHeaderButton.withComponent(NavLink);
|
||||
|
||||
const AppHeaderActions = styled.div`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const AppHeaderQuickNewButton = styled(StyledDropdownButton)`
|
||||
${buttons.button};
|
||||
${buttons.medium};
|
||||
${buttons.gray};
|
||||
margin-right: 8px;
|
||||
|
||||
&:after {
|
||||
top: 11px;
|
||||
}
|
||||
`
|
||||
|
||||
export default class Header extends React.Component {
|
||||
static propTypes = {
|
||||
@ -14,6 +95,8 @@ export default class Header extends React.Component {
|
||||
displayUrl: PropTypes.string,
|
||||
};
|
||||
|
||||
static activeClassName = `${uuid()}-active`;
|
||||
|
||||
handleCreatePostClick = (collectionName) => {
|
||||
const { onCreateEntryClick } = this.props;
|
||||
if (onCreateEntryClick) {
|
||||
@ -35,36 +118,34 @@ export default class Header extends React.Component {
|
||||
const avatarUrl = user.get('avatar_url');
|
||||
|
||||
return (
|
||||
<div className="nc-appHeader-container">
|
||||
<div className="nc-appHeader-main">
|
||||
<div className="nc-appHeader-content">
|
||||
<AppHeaderContainer>
|
||||
<AppHeader>
|
||||
<AppHeaderContent>
|
||||
<nav>
|
||||
<NavLink
|
||||
<AppHeaderNavLink
|
||||
to="/"
|
||||
className="nc-appHeader-button"
|
||||
activeClassName="nc-appHeader-button-active"
|
||||
activeClassName={Header.activeClassName}
|
||||
isActive={(match, location) => location.pathname.startsWith('/collections/')}
|
||||
>
|
||||
<Icon type="page"/>
|
||||
Content
|
||||
</NavLink>
|
||||
</AppHeaderNavLink>
|
||||
{
|
||||
hasWorkflow
|
||||
? <NavLink to="/workflow" className="nc-appHeader-button" activeClassName="nc-appHeader-button-active">
|
||||
? <AppHeaderNavLink to="/workflow" activeClassName={this.activeClassName}>
|
||||
<Icon type="workflow"/>
|
||||
Workflow
|
||||
</NavLink>
|
||||
</AppHeaderNavLink>
|
||||
: null
|
||||
}
|
||||
<button onClick={openMediaLibrary} className="nc-appHeader-button">
|
||||
<AppHeaderButton onClick={openMediaLibrary}>
|
||||
<Icon type="media-alt"/>
|
||||
Media
|
||||
</button>
|
||||
</AppHeaderButton>
|
||||
</nav>
|
||||
<div className="nc-appHeader-actions">
|
||||
<AppHeaderActions>
|
||||
<Dropdown
|
||||
classNameButton="nc-appHeader-button nc-appHeader-quickNew"
|
||||
label="Quick add"
|
||||
renderButton={() => <AppHeaderQuickNewButton>Quick add</AppHeaderQuickNewButton>}
|
||||
dropdownTopOverlap="30px"
|
||||
dropdownWidth="160px"
|
||||
dropdownPosition="left"
|
||||
@ -79,37 +160,15 @@ export default class Header extends React.Component {
|
||||
)
|
||||
}
|
||||
</Dropdown>
|
||||
{
|
||||
displayUrl
|
||||
? <a
|
||||
className="nc-appHeader-siteLink"
|
||||
href={displayUrl}
|
||||
target="_blank"
|
||||
>
|
||||
{stripProtocol(displayUrl)}
|
||||
</a>
|
||||
: null
|
||||
}
|
||||
<Dropdown
|
||||
dropdownTopOverlap="50px"
|
||||
dropdownWidth="100px"
|
||||
dropdownPosition="right"
|
||||
button={
|
||||
<button className="nc-appHeader-avatar">
|
||||
{
|
||||
avatarUrl
|
||||
? <img className="nc-appHeader-avatar-image" src={user.get('avatar_url')}/>
|
||||
: <Icon className="nc-appHeader-avatar-placeholder" type="user" size="large"/>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<DropdownItem label="Log Out" onClick={onLogoutClick}/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SettingsDropdown
|
||||
displayUrl={displayUrl}
|
||||
imageUrl={user.get('avatar_url')}
|
||||
onLogoutClick={onLogoutClick}
|
||||
/>
|
||||
</AppHeaderActions>
|
||||
</AppHeaderContent>
|
||||
</AppHeader>
|
||||
</AppHeaderContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +0,0 @@
|
||||
.nc-notFound-container {
|
||||
margin: var(--pageMargin);
|
||||
}
|
@ -1,7 +1,14 @@
|
||||
import React from 'react';
|
||||
import styled from 'react-emotion';
|
||||
import { lengths } from 'netlify-cms-ui-default/styles';
|
||||
|
||||
|
||||
const NotFoundContainer = styled.div`
|
||||
margin: ${lengths.pageMargin};
|
||||
`;
|
||||
|
||||
export default () => (
|
||||
<div className="nc-notFound-container">
|
||||
<NotFoundContainer>
|
||||
<h2>Not Found</h2>
|
||||
</div>
|
||||
</NotFoundContainer>
|
||||
);
|
||||
|
@ -1,11 +0,0 @@
|
||||
@import "./Sidebar.css";
|
||||
@import "./CollectionTop.css";
|
||||
@import "./Entries/Entries.css";
|
||||
|
||||
.nc-collectionPage-container {
|
||||
margin: var(--pageMargin);
|
||||
}
|
||||
|
||||
.nc-collectionPage-main {
|
||||
padding-left: 280px;
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import styled from 'react-emotion';
|
||||
import { connect } from 'react-redux';
|
||||
import { lengths } from 'netlify-cms-ui-default/styles';
|
||||
import { getNewEntryUrl } from 'Lib/urlHelper';
|
||||
import Sidebar from './Sidebar';
|
||||
import CollectionTop from './CollectionTop';
|
||||
@ -8,6 +10,14 @@ import EntriesCollection from './Entries/EntriesCollection';
|
||||
import EntriesSearch from './Entries/EntriesSearch';
|
||||
import { VIEW_STYLE_LIST, VIEW_STYLE_GRID } from 'Constants/collectionViews';
|
||||
|
||||
const CollectionContainer = styled.div`
|
||||
margin: ${lengths.pageMargin};
|
||||
`
|
||||
|
||||
const CollectionMain = styled.div`
|
||||
padding-left: 280px;
|
||||
`
|
||||
|
||||
class Collection extends React.Component {
|
||||
static propTypes = {
|
||||
collection: ImmutablePropTypes.map.isRequired,
|
||||
@ -38,9 +48,9 @@ class Collection extends React.Component {
|
||||
const { collection, collections, collectionName, isSearchResults, searchTerm } = this.props;
|
||||
const newEntryUrl = collection.get('create') ? getNewEntryUrl(collectionName) : '';
|
||||
return (
|
||||
<div className="nc-collectionPage-container">
|
||||
<CollectionContainer>
|
||||
<Sidebar collections={collections} searchTerm={searchTerm}/>
|
||||
<div className="nc-collectionPage-main">
|
||||
<CollectionMain>
|
||||
{
|
||||
isSearchResults
|
||||
? null
|
||||
@ -54,8 +64,8 @@ class Collection extends React.Component {
|
||||
/>
|
||||
}
|
||||
{ isSearchResults ? this.renderEntriesSearch() : this.renderEntriesCollection() }
|
||||
</div>
|
||||
</div>
|
||||
</CollectionMain>
|
||||
</CollectionContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,64 +0,0 @@
|
||||
.nc-collectionPage-top {
|
||||
@apply(--cardTop);
|
||||
}
|
||||
|
||||
.nc-collectionPage-top-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.nc-collectionPage-top-description {
|
||||
@apply(--cardTopDescription)
|
||||
}
|
||||
|
||||
.nc-collectionPage-topHeading {
|
||||
@apply(--cardTopHeading)
|
||||
}
|
||||
|
||||
.nc-collectionPage-topNewButton {
|
||||
@apply(--button);
|
||||
@apply(--dropShadowDeep);
|
||||
@apply(--buttonDefault);
|
||||
@apply(--buttonGray);
|
||||
|
||||
padding: 0 30px;
|
||||
}
|
||||
|
||||
.nc-collectionPage-top-viewControls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.nc-collectionPage-top-viewControls {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.nc-collectionPage-top-viewControls-text {
|
||||
font-size: 14px;
|
||||
color: var(--colorText);
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.nc-collectionPage-top-viewControls-button {
|
||||
color: #b3b9c4;
|
||||
background-color: transparent;
|
||||
display: block;
|
||||
padding: 0;
|
||||
margin: 0 4px;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
& .nc-icon {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.nc-collectionPage-top-viewControls-buttonActive {
|
||||
color: var(--colorActive);
|
||||
}
|
@ -1,10 +1,69 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import c from 'classnames';
|
||||
import styled from 'react-emotion';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Icon } from 'netlify-cms-ui-default';
|
||||
import Icon from 'netlify-cms-ui-default/Icon';
|
||||
import { components, buttons, shadows, colors } from 'netlify-cms-ui-default/styles';
|
||||
import { VIEW_STYLE_LIST, VIEW_STYLE_GRID } from 'Constants/collectionViews';
|
||||
|
||||
const CollectionTopContainer = styled.div`
|
||||
${components.cardTop};
|
||||
`
|
||||
|
||||
const CollectionTopRow = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
`
|
||||
|
||||
const CollectionTopHeading = styled.h1`
|
||||
${components.cardTopHeading};
|
||||
`
|
||||
|
||||
const CollectionTopNewButton = styled(Link)`
|
||||
${buttons.button};
|
||||
${shadows.dropDeep};
|
||||
${buttons.default};
|
||||
${buttons.gray};
|
||||
|
||||
padding: 0 30px;
|
||||
`
|
||||
|
||||
const CollectionTopDescription = styled.p`
|
||||
${components.cardTopDescription};
|
||||
`
|
||||
|
||||
const ViewControls = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-top: 24px;
|
||||
`
|
||||
|
||||
const ViewControlsText = styled.span`
|
||||
font-size: 14px;
|
||||
color: ${colors.text};
|
||||
margin-right: 12px;
|
||||
`
|
||||
|
||||
const ViewControlsButton = styled.button`
|
||||
${buttons.button};
|
||||
color: ${props => props.isActive ? colors.active : '#b3b9c4'};
|
||||
background-color: transparent;
|
||||
display: block;
|
||||
padding: 0;
|
||||
margin: 0 4px;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
${Icon} {
|
||||
display: block;
|
||||
}
|
||||
`
|
||||
|
||||
const CollectionTop = ({
|
||||
collectionLabel,
|
||||
collectionLabelSingular,
|
||||
@ -14,44 +73,38 @@ const CollectionTop = ({
|
||||
newEntryUrl,
|
||||
}) => {
|
||||
return (
|
||||
<div className="nc-collectionPage-top">
|
||||
<div className="nc-collectionPage-top-row">
|
||||
<h1 className="nc-collectionPage-topHeading">{collectionLabel}</h1>
|
||||
<CollectionTopContainer>
|
||||
<CollectionTopRow>
|
||||
<CollectionTopHeading>{collectionLabel}</CollectionTopHeading>
|
||||
{
|
||||
newEntryUrl
|
||||
? <Link className="nc-collectionPage-topNewButton" to={newEntryUrl}>
|
||||
? <CollectionTopNewButton to={newEntryUrl}>
|
||||
{`New ${collectionLabelSingular || collectionLabel}`}
|
||||
</Link>
|
||||
</CollectionTopNewButton>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
</CollectionTopRow>
|
||||
{
|
||||
collectionDescription
|
||||
? <p className="nc-collectionPage-top-description">{collectionDescription}</p>
|
||||
? <CollectionTopDescription>{collectionDescription}</CollectionTopDescription>
|
||||
: null
|
||||
}
|
||||
<div className={c('nc-collectionPage-top-viewControls', {
|
||||
'nc-collectionPage-top-viewControls-noDescription': !collectionDescription,
|
||||
})}>
|
||||
<span className="nc-collectionPage-top-viewControls-text">View as:</span>
|
||||
<button
|
||||
className={c('nc-collectionPage-top-viewControls-button', {
|
||||
'nc-collectionPage-top-viewControls-buttonActive': viewStyle === VIEW_STYLE_LIST,
|
||||
})}
|
||||
<ViewControls>
|
||||
<ViewControlsText>View as:</ViewControlsText>
|
||||
<ViewControlsButton
|
||||
isActive={viewStyle === VIEW_STYLE_LIST}
|
||||
onClick={() => onChangeViewStyle(VIEW_STYLE_LIST)}
|
||||
>
|
||||
<Icon type="list"/>
|
||||
</button>
|
||||
<button
|
||||
className={c('nc-collectionPage-top-viewControls-button', {
|
||||
'nc-collectionPage-top-viewControls-buttonActive': viewStyle === VIEW_STYLE_GRID,
|
||||
})}
|
||||
</ViewControlsButton>
|
||||
<ViewControlsButton
|
||||
isActive={viewStyle === VIEW_STYLE_GRID}
|
||||
onClick={() => onChangeViewStyle(VIEW_STYLE_GRID)}
|
||||
>
|
||||
<Icon type="grid"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ViewControlsButton>
|
||||
</ViewControls>
|
||||
</CollectionTopContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,2 +0,0 @@
|
||||
@import "./EntryListing.css";
|
||||
@import "./EntryCard.css";
|
@ -1,7 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { Loader } from 'netlify-cms-ui-default';
|
||||
import Loader from 'netlify-cms-ui-default/Loader';
|
||||
import EntryListing from './EntryListing';
|
||||
|
||||
const Entries = ({
|
||||
|
@ -1,76 +0,0 @@
|
||||
.nc-entryListing-gridCard {
|
||||
@apply(--card);
|
||||
flex: 0 0 335px;
|
||||
height: 240px;
|
||||
background-color: var(--colorForeground);
|
||||
color: var(--colorText);
|
||||
overflow: hidden;
|
||||
margin-bottom: 16px;
|
||||
margin-left: 12px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--colorForeground);
|
||||
color: var(--colorText);
|
||||
}
|
||||
}
|
||||
|
||||
.nc-entryListing-cardImage {
|
||||
background-position: center center;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.nc-entryListing-cardBody {
|
||||
padding: 16px 22px;
|
||||
height: 90px;
|
||||
position: relative;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
display: block;
|
||||
z-index: 1;
|
||||
bottom: 0;
|
||||
left: -20%;
|
||||
height: 140%;
|
||||
width: 140%;
|
||||
box-shadow: inset 0 -15px 24px #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.nc-entryListing-listCard {
|
||||
@apply(--card);
|
||||
width: var(--topCardWidth);
|
||||
max-width: 100%;
|
||||
padding: 16px 22px;
|
||||
margin-left: 12px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--colorForeground);
|
||||
}
|
||||
}
|
||||
|
||||
.nc-entryListing-listCard-title {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.nc-entryListing-listCard-collection-label {
|
||||
font-size: 12px;
|
||||
color: var(--colorTextLead);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.nc-entryListing-cardBody-full {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.nc-entryListing-cardHeading {
|
||||
margin: 0 0 2px;
|
||||
}
|
||||
|
||||
.nc-entryListing-cardListLabel {
|
||||
white-space: nowrap;
|
||||
font-weight: bold;
|
||||
}
|
@ -1,14 +1,81 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import styled from 'react-emotion';
|
||||
import { Link } from 'react-router-dom';
|
||||
import c from 'classnames';
|
||||
import history from 'Routing/history';
|
||||
import { resolvePath } from 'netlify-cms-lib-util/path';
|
||||
import { VIEW_STYLE_LIST, VIEW_STYLE_GRID } from 'Constants/collectionViews';
|
||||
import { colors, colorsRaw, components, lengths } from 'netlify-cms-ui-default/styles';
|
||||
|
||||
const CollectionLabel = ({ label }) =>
|
||||
<h2 className="nc-entryListing-listCard-collection-label">{label}</h2>;
|
||||
const ListCardLink = styled(Link)`
|
||||
${components.card};
|
||||
width: ${lengths.topCardWidth};
|
||||
max-width: 100%;
|
||||
padding: 16px 22px;
|
||||
margin-left: 12px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
&:hover {
|
||||
background-color: ${colors.foreground};
|
||||
}
|
||||
`
|
||||
|
||||
const GridCardLink = styled(Link)`
|
||||
${components.card};
|
||||
flex: 0 0 335px;
|
||||
height: 240px;
|
||||
overflow: hidden;
|
||||
margin-left: 12px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
&, &:hover {
|
||||
background-color: ${colors.foreground};
|
||||
color: ${colors.text};
|
||||
}
|
||||
`
|
||||
|
||||
const CollectionLabel = styled.h2`
|
||||
font-size: 12px;
|
||||
color: ${colors.textLead};
|
||||
text-transform: uppercase;
|
||||
`
|
||||
|
||||
const ListCardTitle = styled.h2`
|
||||
margin-bottom: 0;
|
||||
`
|
||||
|
||||
const CardHeading = styled.h2`
|
||||
margin: 0 0 2px;
|
||||
`
|
||||
|
||||
const CardBody = styled.div`
|
||||
padding: 16px 22px;
|
||||
height: 90px;
|
||||
position: relative;
|
||||
margin-bottom: ${props => props.hasImage && 0};
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
display: block;
|
||||
z-index: 1;
|
||||
bottom: 0;
|
||||
left: -20%;
|
||||
height: 140%;
|
||||
width: 140%;
|
||||
box-shadow: inset 0 -15px 24px ${colorsRaw.white};
|
||||
}
|
||||
`
|
||||
|
||||
const CardImage = styled.div`
|
||||
background-image: url(${props => props.url});
|
||||
background-position: center center;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
height: 150px;
|
||||
`
|
||||
|
||||
const EntryCard = ({
|
||||
collection,
|
||||
@ -29,29 +96,22 @@ const EntryCard = ({
|
||||
|
||||
if (viewStyle === VIEW_STYLE_LIST) {
|
||||
return (
|
||||
<Link to={path} className="nc-entryListing-listCard">
|
||||
{ collectionLabel ? <CollectionLabel label={collectionLabel}/> : null }
|
||||
<h2 className="nc-entryListing-listCard-title">{ title }</h2>
|
||||
</Link>
|
||||
<ListCardLink to={path}>
|
||||
{ collectionLabel ? <CollectionLabel>{collectionLabel}</CollectionLabel> : null }
|
||||
<ListCardTitle>{ title }</ListCardTitle>
|
||||
</ListCardLink>
|
||||
);
|
||||
}
|
||||
|
||||
if (viewStyle === VIEW_STYLE_GRID) {
|
||||
return (
|
||||
<Link to={path} className="nc-entryListing-gridCard">
|
||||
<div className={c('nc-entryListing-cardBody', { 'nc-entryListing-cardBody-full': !image })}>
|
||||
{ collectionLabel ? <CollectionLabel label={collectionLabel}/> : null }
|
||||
<h2 className="nc-entryListing-cardHeading">{title}</h2>
|
||||
</div>
|
||||
{
|
||||
image
|
||||
? <div
|
||||
className="nc-entryListing-cardImage"
|
||||
style={{ backgroundImage: `url(${ image })` }}
|
||||
/>
|
||||
: null
|
||||
}
|
||||
</Link>
|
||||
<GridCardLink to={path}>
|
||||
<CardBody hasImage={image}>
|
||||
{ collectionLabel ? <CollectionLabel>{collectionLabel}</CollectionLabel> : null }
|
||||
<CardHeading>{title}</CardHeading>
|
||||
</CardBody>
|
||||
{ image ? <CardImage url={image}/> : null }
|
||||
</GridCardLink>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +0,0 @@
|
||||
.nc-entryListing-cardsGrid {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
margin-left: -12px;
|
||||
}
|
||||
|
||||
.nc-entryListing-cardsList {
|
||||
margin-left: -12px;
|
||||
}
|
@ -1,12 +1,19 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import styled from 'react-emotion';
|
||||
import Waypoint from 'react-waypoint';
|
||||
import { Map } from 'immutable';
|
||||
import { selectFields, selectInferedField } from 'Reducers/collections';
|
||||
import EntryCard from './EntryCard';
|
||||
import Cursor from 'ValueObjects/Cursor';
|
||||
|
||||
const CardsGrid = styled.div`
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
margin-left: -12px;
|
||||
`
|
||||
|
||||
export default class EntryListing extends React.Component {
|
||||
static propTypes = {
|
||||
publicFolder: PropTypes.string.isRequired,
|
||||
@ -59,14 +66,14 @@ export default class EntryListing extends React.Component {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="nc-entryListing-cardsGrid">
|
||||
<CardsGrid>
|
||||
{
|
||||
Map.isMap(collections)
|
||||
? this.renderCardsForSingleCollection()
|
||||
: this.renderCardsForMultipleCollections()
|
||||
}
|
||||
<Waypoint onEnter={this.handleLoadMore} />
|
||||
</div>
|
||||
</CardsGrid>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,74 +0,0 @@
|
||||
.nc-collectionPage-sidebar {
|
||||
@apply(--card);
|
||||
width: 250px;
|
||||
padding: 8px 0 12px;
|
||||
position: fixed;
|
||||
max-height: calc(100vh - 112px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.nc-collectionPage-sidebarHeading {
|
||||
font-size: 23px;
|
||||
font-weight: 600;
|
||||
padding: 0;
|
||||
margin: 18px 12px 12px;
|
||||
color: var(--colorTextLead);
|
||||
}
|
||||
|
||||
.nc-collectionPage-sidebarSearch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 8px;
|
||||
position: relative;
|
||||
|
||||
& input {
|
||||
background-color: #eff0f4;
|
||||
border-radius: var(--borderRadius);
|
||||
font-size: 14px;
|
||||
padding: 10px 6px 10px 32px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: inset 0 0 0 2px var(--colorBlue);
|
||||
}
|
||||
}
|
||||
|
||||
& .nc-icon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 6px;
|
||||
z-index: 2;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.nc-collectionPage-sidebarLink {
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
border-left: 2px solid #fff;
|
||||
|
||||
& .nc-icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&.nc-collectionPage-sidebarLink-active {
|
||||
color: var(--colorActive);
|
||||
background-color: var(--colorActiveBackground);
|
||||
border-left-color: #4863c6;
|
||||
}
|
||||
|
||||
&:first-of-type {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
@ -1,31 +1,118 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import styled, { css } from 'react-emotion';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import uuid from 'uuid/v4';
|
||||
import Icon from 'netlify-cms-ui-default/Icon';
|
||||
import { components, colors, colorsRaw, lengths } from 'netlify-cms-ui-default/styles';
|
||||
import { searchCollections } from 'Actions/collections';
|
||||
import { getCollectionUrl } from 'Lib/urlHelper';
|
||||
import { Icon } from 'netlify-cms-ui-default';
|
||||
|
||||
export default class Collection extends React.Component {
|
||||
const styles = {
|
||||
sidebarNavLinkActive: css`
|
||||
color: ${colors.active};
|
||||
background-color: ${colors.activeBackground};
|
||||
border-left-color: #4863c6;
|
||||
`,
|
||||
};
|
||||
|
||||
const SidebarContainer = styled.div`
|
||||
${components.card};
|
||||
width: 250px;
|
||||
padding: 8px 0 12px;
|
||||
position: fixed;
|
||||
max-height: calc(100vh - 112px);
|
||||
overflow: auto;
|
||||
`
|
||||
|
||||
const SidebarHeading = styled.h1`
|
||||
font-size: 23px;
|
||||
font-weight: 600;
|
||||
padding: 0;
|
||||
margin: 18px 12px 12px;
|
||||
color: ${colors.textLead};
|
||||
`
|
||||
|
||||
const SearchContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 8px;
|
||||
position: relative;
|
||||
|
||||
${Icon} {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 6px;
|
||||
z-index: 2;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
`
|
||||
|
||||
const SearchInput = styled.input`
|
||||
background-color: #eff0f4;
|
||||
border-radius: ${lengths.borderRadius};
|
||||
font-size: 14px;
|
||||
padding: 10px 6px 10px 32px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: inset 0 0 0 2px ${colorsRaw.blue};
|
||||
}
|
||||
`
|
||||
|
||||
const SidebarNavLink = styled(NavLink)`
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
border-left: 2px solid #fff;
|
||||
|
||||
${props => css`
|
||||
&:hover,
|
||||
&:active,
|
||||
&.${props.activeClassName} {
|
||||
${styles.sidebarNavLinkActive};
|
||||
}
|
||||
`}
|
||||
|
||||
&:first-of-type {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
${Icon} {
|
||||
margin-right: 8px;
|
||||
}
|
||||
`
|
||||
|
||||
export default class Sidebar extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
collections: ImmutablePropTypes.orderedMap.isRequired,
|
||||
};
|
||||
|
||||
static sidebarLinkActiveClassName = `${uuid()}-sidebar-active`;
|
||||
|
||||
state = { query: this.props.searchTerm || '' };
|
||||
|
||||
renderLink = collection => {
|
||||
const collectionName = collection.get('name');
|
||||
return (
|
||||
<NavLink
|
||||
<SidebarNavLink
|
||||
key={collectionName}
|
||||
to={`/collections/${collectionName}`}
|
||||
className="nc-collectionPage-sidebarLink"
|
||||
activeClassName="nc-collectionPage-sidebarLink-active"
|
||||
activeClassName={Sidebar.sidebarLinkActiveClassName}
|
||||
>
|
||||
<Icon type="write"/>
|
||||
{collection.get('label')}
|
||||
</NavLink>
|
||||
</SidebarNavLink>
|
||||
);
|
||||
};
|
||||
|
||||
@ -35,19 +122,19 @@ export default class Collection extends React.Component {
|
||||
const { query } = this.state;
|
||||
|
||||
return (
|
||||
<div className="nc-collectionPage-sidebar">
|
||||
<h1 className="nc-collectionPage-sidebarHeading">Collections</h1>
|
||||
<div className="nc-collectionPage-sidebarSearch">
|
||||
<Icon type="search" size="small"/>
|
||||
<input
|
||||
onChange={e => this.setState({ query: e.target.value })}
|
||||
onKeyDown={e => e.key === 'Enter' && searchCollections(query)}
|
||||
placeholder="Search all"
|
||||
value={query}
|
||||
/>
|
||||
</div>
|
||||
{collections.toList().map(this.renderLink)}
|
||||
</div>
|
||||
<SidebarContainer>
|
||||
<SidebarHeading>Collections</SidebarHeading>
|
||||
<SearchContainer>
|
||||
<Icon type="search" size="small"/>
|
||||
<SearchInput
|
||||
onChange={e => this.setState({ query: e.target.value })}
|
||||
onKeyDown={e => e.key === 'Enter' && searchCollections(query)}
|
||||
placeholder="Search all"
|
||||
value={query}
|
||||
/>
|
||||
</SearchContainer>
|
||||
{collections.toList().map(this.renderLink)}
|
||||
</SidebarContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +0,0 @@
|
||||
@import "./EditorInterface.css";
|
||||
@import "./EditorToolbar.css";
|
||||
@import "./EditorToggle.css";
|
||||
@import "./EditorControlPane/EditorControlPane.css";
|
||||
@import "./EditorControlPane/EditorControl.css";
|
||||
@import "./EditorPreviewPane/EditorPreviewPane.css";
|
@ -4,6 +4,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { Map } from 'immutable';
|
||||
import { get } from 'lodash';
|
||||
import { connect } from 'react-redux';
|
||||
import Loader from 'netlify-cms-ui-default/Loader';
|
||||
import history from 'Routing/history';
|
||||
import { logoutUser } from 'Actions/auth';
|
||||
import {
|
||||
@ -27,7 +28,6 @@ import { addAsset } from 'Actions/media';
|
||||
import { openMediaLibrary, removeInsertedMedia } from 'Actions/mediaLibrary';
|
||||
import { selectEntry, selectUnpublishedEntry, getAsset } from 'Reducers';
|
||||
import { selectFields } from 'Reducers/collections';
|
||||
import { Loader } from 'netlify-cms-ui-default';
|
||||
import { status } from 'Constants/publishModes';
|
||||
import { EDITORIAL_WORKFLOW } from 'Constants/publishModes';
|
||||
import EditorInterface from './EditorInterface';
|
||||
|
@ -1,7 +0,0 @@
|
||||
.nc-controlPane-control {
|
||||
margin-top: 16px;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 36px;
|
||||
}
|
||||
}
|
@ -1,9 +1,105 @@
|
||||
import React from 'react';
|
||||
import styled, { css, cx } from 'react-emotion';
|
||||
import { partial, uniqueId } from 'lodash';
|
||||
import c from 'classnames';
|
||||
import { colors, colorsRaw, transitions, lengths, borders } from 'netlify-cms-ui-default/styles';
|
||||
import { resolveWidget } from 'Lib/registry';
|
||||
import Widget from './Widget';
|
||||
|
||||
const styles = {
|
||||
label: css`
|
||||
color: ${colors.controlLabel};
|
||||
background-color: ${colors.textFieldBorder};
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
border: 0;
|
||||
border-radius: 3px 3px 0 0;
|
||||
padding: 3px 6px 2px;
|
||||
margin: 0;
|
||||
transition: all ${transitions.main};
|
||||
position: relative;
|
||||
|
||||
/**
|
||||
* Faux outside curve into top of input
|
||||
*/
|
||||
&:before,
|
||||
&:after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -4px;
|
||||
height: 100%;
|
||||
width: 4px;
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
&:after {
|
||||
border-bottom-left-radius: 3px;
|
||||
background-color: #fff;
|
||||
}
|
||||
`,
|
||||
labelActive: css`
|
||||
background-color: ${colors.active};
|
||||
color: ${colors.textLight};
|
||||
`,
|
||||
labelError: css`
|
||||
background-color: ${colors.errorText};
|
||||
color: ${colorsRaw.white};
|
||||
`,
|
||||
widget: css`
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: ${lengths.inputPadding};
|
||||
margin: 0;
|
||||
border: ${borders.textFieldBorder};
|
||||
border-radius: ${lengths.borderRadius};
|
||||
border-top-left-radius: 0;
|
||||
outline: 0;
|
||||
box-shadow: none;
|
||||
background-color: ${colors.inputBackground};
|
||||
color: #444a57;
|
||||
transition: border-color ${transitions.main};
|
||||
position: relative;
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
|
||||
select& {
|
||||
text-indent: 14px;
|
||||
height: 58px;
|
||||
}
|
||||
`,
|
||||
widgetActive: css`
|
||||
border-color: ${colors.active};
|
||||
`,
|
||||
widgetError: css`
|
||||
border-color: ${colors.errorText};
|
||||
`,
|
||||
};
|
||||
|
||||
const ControlContainer = styled.div`
|
||||
margin-top: 16px;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 36px;
|
||||
}
|
||||
`
|
||||
|
||||
const ControlErrorsList = styled.ul`
|
||||
list-style-type: none;
|
||||
font-size: 12px;
|
||||
color: ${colors.errorText};
|
||||
margin-bottom: 5px;
|
||||
text-align: right;
|
||||
text-transform: uppercase;
|
||||
position: relative;
|
||||
font-weight: 600;
|
||||
top: 20px;
|
||||
`
|
||||
|
||||
|
||||
|
||||
export default class EditorControl extends React.Component {
|
||||
state = {
|
||||
activeLabel: false,
|
||||
@ -31,8 +127,8 @@ export default class EditorControl extends React.Component {
|
||||
const metadata = fieldsMetaData && fieldsMetaData.get(fieldName);
|
||||
const errors = fieldsErrors && fieldsErrors.get(fieldName);
|
||||
return (
|
||||
<div className="nc-controlPane-control">
|
||||
<ul className="nc-controlPane-errors">
|
||||
<ControlContainer>
|
||||
<ControlErrorsList>
|
||||
{
|
||||
errors && errors.map(error =>
|
||||
error.message &&
|
||||
@ -40,27 +136,27 @@ export default class EditorControl extends React.Component {
|
||||
<li key={error.message.trim().replace(/[^a-z0-9]+/gi, '-')}>{error.message}</li>
|
||||
)
|
||||
}
|
||||
</ul>
|
||||
</ControlErrorsList>
|
||||
<label
|
||||
className={c({
|
||||
'nc-controlPane-label': true,
|
||||
'nc-controlPane-labelActive': this.state.styleActive,
|
||||
'nc-controlPane-labelWithError': !!errors,
|
||||
})}
|
||||
className={cx(
|
||||
styles.label,
|
||||
{ [styles.labelActive]: this.state.styleActive },
|
||||
{ [styles.labelError]: !!errors },
|
||||
)}
|
||||
htmlFor={fieldName + uniqueFieldId}
|
||||
>
|
||||
{field.get('label')}
|
||||
</label>
|
||||
<Widget
|
||||
classNameWrapper={c({
|
||||
'nc-controlPane-widget': true,
|
||||
'nc-controlPane-widgetActive': this.state.styleActive,
|
||||
'nc-controlPane-widgetError': !!errors,
|
||||
})}
|
||||
classNameWidget="nc-controlPane-widget"
|
||||
classNameWidgetActive="nc-controlPane-widgetNestable"
|
||||
classNameLabel="nc-controlPane-label"
|
||||
classNameLabelActive="nc-controlPane-labelActive"
|
||||
classNameWrapper={cx(
|
||||
styles.widget,
|
||||
{ [styles.widgetActive]: this.state.styleActive },
|
||||
{ [styles.widgetError]: !!errors },
|
||||
)}
|
||||
classNameWidget={styles.widget}
|
||||
classNameWidgetActive={styles.widgetActive}
|
||||
classNameLabel={styles.label}
|
||||
classNameLabelActive={styles.labelActive}
|
||||
controlComponent={widget.control}
|
||||
field={field}
|
||||
uniqueFieldId={uniqueFieldId}
|
||||
@ -79,7 +175,7 @@ export default class EditorControl extends React.Component {
|
||||
ref={processControlRef && partial(processControlRef, fieldName)}
|
||||
editorControl={EditorControl}
|
||||
/>
|
||||
</div>
|
||||
</ControlContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,107 +0,0 @@
|
||||
:root {
|
||||
--controlPaneLabel: {
|
||||
display: inline-block;
|
||||
color: var(--controlLabelColor);
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
background-color: var(--textFieldBorderColor);
|
||||
border: 0;
|
||||
border-radius: 3px 3px 0 0;
|
||||
padding: 3px 6px 2px;
|
||||
margin: 0;
|
||||
transition: all var(--transition);
|
||||
position: relative;
|
||||
|
||||
/**
|
||||
* Faux outside curve into top of input
|
||||
*/
|
||||
&:before,
|
||||
&:after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -4px;
|
||||
height: 100%;
|
||||
width: 4px;
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
&:after {
|
||||
border-bottom-left-radius: 3px;
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
--controlPaneWidget: {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: var(--inputPadding);
|
||||
margin: 0;
|
||||
border: var(--textFieldBorder);
|
||||
border-radius: var(--borderRadius);
|
||||
border-top-left-radius: 0;
|
||||
outline: 0;
|
||||
box-shadow: none;
|
||||
background-color: var(--colorInputBackground);
|
||||
color: #444a57;
|
||||
transition: border-color var(--transition);
|
||||
position: relative;
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
.nc-controlPane-root {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding-bottom: 16px;
|
||||
|
||||
& p {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.nc-controlPane-label {
|
||||
@apply(--controlPaneLabel);
|
||||
}
|
||||
|
||||
.nc-controlPane-labelActive {
|
||||
background-color: var(--colorActive);
|
||||
color: var(--colorTextLight);
|
||||
}
|
||||
|
||||
.nc-controlPane-widget {
|
||||
@apply(--controlPaneWidget);
|
||||
|
||||
&.nc-controlPane-widgetActive {
|
||||
border-color: var(--colorActive);
|
||||
}
|
||||
}
|
||||
|
||||
select.nc-controlPane-widget {
|
||||
text-indent: 14px;
|
||||
height: 58px;
|
||||
}
|
||||
|
||||
.nc-controlPane-labelWithError {
|
||||
background-color: var(--colorErrorText);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.nc-controlPane-widgetError {
|
||||
border-color: var(--colorErrorText);
|
||||
}
|
||||
|
||||
.nc-controlPane-errors {
|
||||
list-style-type: none;
|
||||
font-size: 12px;
|
||||
color: var(--colorErrorText);
|
||||
margin-bottom: 5px;
|
||||
text-align: right;
|
||||
text-transform: uppercase;
|
||||
position: relative;
|
||||
font-weight: 600;
|
||||
top: 20px;
|
||||
}
|
@ -1,8 +1,19 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import styled from 'react-emotion';
|
||||
import EditorControl from './EditorControl';
|
||||
|
||||
const ControlPaneContainer = styled.div`
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding-bottom: 16px;
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
}
|
||||
`
|
||||
|
||||
export default class ControlPane extends React.Component {
|
||||
componentValidate = {};
|
||||
|
||||
@ -43,7 +54,7 @@ export default class ControlPane extends React.Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="nc-controlPane-root">
|
||||
<ControlPaneContainer>
|
||||
{fields.map((field, i) => field.get('widget') === 'hidden' ? null :
|
||||
<EditorControl
|
||||
key={i}
|
||||
@ -61,7 +72,7 @@ export default class ControlPane extends React.Component {
|
||||
processControlRef={this.processControlRef}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ControlPaneContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,78 +0,0 @@
|
||||
/**
|
||||
* React Split Pane
|
||||
*/
|
||||
.Resizer.vertical {
|
||||
width: 21px;
|
||||
cursor: col-resize;
|
||||
position: relative;
|
||||
transition: background-color var(--transition);
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
left: 10px;
|
||||
background-color: var(--textFieldBorderColor);
|
||||
display: block;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
background-color: var(--colorGrayLight);
|
||||
}
|
||||
}
|
||||
|
||||
/* Quick fix for preview pane not fully displaying in Safari */
|
||||
.SplitPane .Pane {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.SplitPane,
|
||||
.nc-entryEditor-noPreviewEditorContainer {
|
||||
@apply(--card);
|
||||
border-radius: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.nc-entryEditor-containerOuter {
|
||||
width: 100%;
|
||||
min-width: 800px;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
padding-top: 66px;
|
||||
background-color: var(--colorBackground);
|
||||
}
|
||||
|
||||
.nc-entryEditor-container {
|
||||
max-width: 1600px;
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nc-entryEditor-controlPane,
|
||||
.nc-entryEditor-previewPane {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.nc-entryEditor-controlPane {
|
||||
padding: 0 16px;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.nc-entryEditor-viewControls {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
z-index: 299;
|
||||
}
|
||||
|
||||
.nc-entryEditor-blocker > * {
|
||||
pointer-events: none;
|
||||
}
|
@ -1,10 +1,12 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import styled, { css, injectGlobal } from 'react-emotion';
|
||||
import SplitPane from 'react-split-pane';
|
||||
import classnames from 'classnames';
|
||||
import { ScrollSync, ScrollSyncPane } from './EditorScrollSync';
|
||||
import { Icon } from 'netlify-cms-ui-default';
|
||||
import Icon from 'netlify-cms-ui-default/Icon';
|
||||
import { colors, colorsRaw, components, transitions } from 'netlify-cms-ui-default/styles';
|
||||
import EditorControlPane from './EditorControlPane/EditorControlPane';
|
||||
import EditorPreviewPane from './EditorPreviewPane/EditorPreviewPane';
|
||||
import EditorToolbar from './EditorToolbar';
|
||||
@ -13,6 +15,96 @@ import EditorToggle from './EditorToggle';
|
||||
const PREVIEW_VISIBLE = 'cms.preview-visible';
|
||||
const SCROLL_SYNC_ENABLED = 'cms.scroll-sync-enabled';
|
||||
|
||||
const styles = {
|
||||
noPreviewContainer: css`
|
||||
${components.card};
|
||||
border-radius: 0;
|
||||
height: 100%;
|
||||
`,
|
||||
pane: css`
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
`,
|
||||
}
|
||||
|
||||
injectGlobal`
|
||||
/**
|
||||
* React Split Pane
|
||||
*/
|
||||
.Resizer.vertical {
|
||||
width: 21px;
|
||||
cursor: col-resize;
|
||||
position: relative;
|
||||
transition: background-color ${transitions.main};
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
left: 10px;
|
||||
background-color: ${colors.textFieldBorder};
|
||||
display: block;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
background-color: ${colorsRaw.GrayLight};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Quick fix for preview pane not fully displaying in Safari
|
||||
*/
|
||||
.SplitPane {
|
||||
.Pane {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const NoPreviewContainer = styled.div`
|
||||
${styles.noPreviewContainer};
|
||||
`
|
||||
|
||||
const EditorContainer = styled.div`
|
||||
width: 100%;
|
||||
min-width: 800px;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
overflow: hidden;
|
||||
padding-top: 66px;
|
||||
background-color: ${colors.background};
|
||||
`
|
||||
|
||||
const Editor = styled.div`
|
||||
max-width: 1600px;
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
`
|
||||
|
||||
const PreviewPaneContainer = styled.div`
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
pointer-events: ${props => props.blockEntry ? 'none' : 'auto'};
|
||||
`
|
||||
|
||||
const ControlPaneContainer = styled(PreviewPaneContainer)`
|
||||
padding: 0 16px;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
`
|
||||
|
||||
const ViewControls = styled.div`
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
z-index: 299;
|
||||
`
|
||||
|
||||
class EditorInterface extends Component {
|
||||
state = {
|
||||
showEventBlocker: false,
|
||||
@ -88,7 +180,7 @@ class EditorInterface extends Component {
|
||||
const collectionPreviewEnabled = collection.getIn(['editor', 'preview'], true);
|
||||
|
||||
const editor = (
|
||||
<div className={classnames('nc-entryEditor-controlPane', { 'nc-entryEditor-blocker': showEventBlocker })}>
|
||||
<ControlPaneContainer blockEntry={showEventBlocker}>
|
||||
<EditorControlPane
|
||||
collection={collection}
|
||||
entry={entry}
|
||||
@ -104,7 +196,7 @@ class EditorInterface extends Component {
|
||||
onRemoveInsertedMedia={onRemoveInsertedMedia}
|
||||
ref={c => this.controlPaneRef = c} // eslint-disable-line
|
||||
/>
|
||||
</div>
|
||||
</ControlPaneContainer>
|
||||
);
|
||||
|
||||
const editorWithPreview = (
|
||||
@ -117,7 +209,7 @@ class EditorInterface extends Component {
|
||||
onDragFinished={this.handleSplitPaneDragFinished}
|
||||
>
|
||||
<ScrollSyncPane>{editor}</ScrollSyncPane>
|
||||
<div className={classnames('nc-entryEditor-previewPane', { 'nc-entryEditor-blocker': showEventBlocker })}>
|
||||
<PreviewPaneContainer blockEntry={showEventBlocker}>
|
||||
<EditorPreviewPane
|
||||
collection={collection}
|
||||
entry={entry}
|
||||
@ -125,20 +217,14 @@ class EditorInterface extends Component {
|
||||
fieldsMetaData={fieldsMetaData}
|
||||
getAsset={getAsset}
|
||||
/>
|
||||
</div>
|
||||
</PreviewPaneContainer>
|
||||
</SplitPane>
|
||||
</div>
|
||||
</ScrollSync>
|
||||
);
|
||||
|
||||
const editorWithoutPreview = (
|
||||
<div className="nc-entryEditor-noPreviewEditorContainer">
|
||||
{editor}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="nc-entryEditor-containerOuter">
|
||||
<EditorContainer>
|
||||
<EditorToolbar
|
||||
isPersisting={entry.get('isPersisting')}
|
||||
isPublishing={entry.get('isPublishing')}
|
||||
@ -164,8 +250,8 @@ class EditorInterface extends Component {
|
||||
currentStatus={currentStatus}
|
||||
onLogoutClick={onLogoutClick}
|
||||
/>
|
||||
<div className="nc-entryEditor-container">
|
||||
<div className="nc-entryEditor-viewControls">
|
||||
<Editor>
|
||||
<ViewControls>
|
||||
<EditorToggle
|
||||
enabled={collectionPreviewEnabled}
|
||||
active={previewVisible}
|
||||
@ -178,14 +264,14 @@ class EditorInterface extends Component {
|
||||
onClick={this.handleToggleScrollSync}
|
||||
icon="scroll"
|
||||
/>
|
||||
</div>
|
||||
</ViewControls>
|
||||
{
|
||||
collectionPreviewEnabled && this.state.previewVisible
|
||||
? editorWithPreview
|
||||
: editorWithoutPreview
|
||||
: <NoPreviewContainer>{editor}</NoPreviewContainer>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</Editor>
|
||||
</EditorContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
.nc-previewPane-frame {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background: #fff;
|
||||
border-radius: var(--borderRadius);
|
||||
}
|
@ -1,8 +1,10 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import styled from 'react-emotion';
|
||||
import { List, Map } from 'immutable';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import Frame from 'react-frame-component';
|
||||
import { lengths } from 'netlify-cms-ui-default/styles';
|
||||
import { resolveWidget, getPreviewTemplate, getPreviewStyles } from 'Lib/registry';
|
||||
import { ErrorBoundary } from 'UI';
|
||||
import { selectTemplateName, selectInferedField } from 'Reducers/collections';
|
||||
@ -11,6 +13,14 @@ import EditorPreviewContent from './EditorPreviewContent.js';
|
||||
import PreviewHOC from './PreviewHOC';
|
||||
import EditorPreview from './EditorPreview';
|
||||
|
||||
const PreviewPaneFrame = styled(Frame)`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background: #fff;
|
||||
border-radius: ${lengths.borderRadius};
|
||||
`
|
||||
|
||||
export default class PreviewPane extends React.Component {
|
||||
|
||||
getWidget = (field, value, props) => {
|
||||
@ -146,7 +156,7 @@ export default class PreviewPane extends React.Component {
|
||||
});
|
||||
|
||||
if (!collection) {
|
||||
return <Frame className="nc-previewPane-frame" head={styleEls} />;
|
||||
<PreviewPaneFrame head={styleEls}/>
|
||||
}
|
||||
|
||||
const initialContent = `
|
||||
@ -159,9 +169,9 @@ export default class PreviewPane extends React.Component {
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<Frame className="nc-previewPane-frame" head={styleEls} initialContent={initialContent}>
|
||||
<PreviewPaneFrame head={styleEls} initialContent={initialContent}>
|
||||
<EditorPreviewContent {...{ previewComponent, previewProps }}/>
|
||||
</Frame>
|
||||
</PreviewPaneFrame>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
@ -1,23 +0,0 @@
|
||||
.nc-editor-toggle {
|
||||
@apply(--dropShadowMiddle);
|
||||
background-color: #fff;
|
||||
color: var(--colorInactive);
|
||||
border-radius: 32px;
|
||||
display: block;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
margin-bottom: 12px;
|
||||
|
||||
& .nc-icon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.nc-editor-toggleActive {
|
||||
color: var(--colorActive);
|
||||
}
|
@ -1,12 +1,33 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import c from 'classnames';
|
||||
import { Icon } from 'netlify-cms-ui-default';
|
||||
import styled from 'react-emotion';
|
||||
import Icon from 'netlify-cms-ui-default/Icon';
|
||||
import { colors, colorsRaw, shadows } from 'netlify-cms-ui-default/styles';
|
||||
|
||||
const EditorToggleButton = styled.button`
|
||||
${shadows.dropMiddle};
|
||||
background-color: ${colorsRaw.white};
|
||||
color: ${props => colors[props.active ? `active` : `inactive`]};
|
||||
border-radius: 32px;
|
||||
display: block;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
margin-bottom: 12px;
|
||||
|
||||
${Icon} {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
`
|
||||
|
||||
const EditorToggle = ({ enabled, active, onClick, icon }) => !enabled ? null :
|
||||
<button className={c('nc-editor-toggle', {'nc-editor-toggleActive': active })} onClick={onClick}>
|
||||
<EditorToggleButton onClick={onClick}>
|
||||
<Icon type={icon} size="large"/>
|
||||
</button>;
|
||||
</EditorToggleButton>;
|
||||
|
||||
EditorToggle.propTypes = {
|
||||
enabled: PropTypes.bool,
|
||||
|
@ -1,151 +0,0 @@
|
||||
:root {
|
||||
--editorToolbarButtonMargin: 0 10px;
|
||||
}
|
||||
|
||||
.nc-entryEditor-toolbar {
|
||||
box-shadow: 0 2px 6px 0 rgba(68, 74, 87, 0.05),
|
||||
0 1px 3px 0 rgba(68, 74, 87, 0.10),
|
||||
0 2px 54px rgba(0, 0, 0, 0.1);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
min-width: 800px;
|
||||
z-index: 300;
|
||||
background-color: #fff;
|
||||
height: 66px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.nc-entryEditor-toolbar-mainSection,
|
||||
.nc-entryEditor-toolbar-backSection,
|
||||
.nc-entryEditor-toolbar-metaSection {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
}
|
||||
|
||||
.nc-entryEditor-toolbar-mainSection {
|
||||
flex: 10;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 10px;
|
||||
|
||||
& .nc-entryEditor-toolbar-mainSection-left {
|
||||
display: flex;
|
||||
}
|
||||
& .nc-entryEditor-toolbar-mainSection-right {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.nc-entryEditor-toolbar-backSection,
|
||||
.nc-entryEditor-toolbar-metaSection {
|
||||
border: 0 solid var(--textFieldBorderColor);
|
||||
}
|
||||
|
||||
.nc-entryEditor-toolbar-dropdown {
|
||||
margin: var(--editorToolbarButtonMargin);
|
||||
|
||||
& .nc-icon {
|
||||
color: var(--colorTeal);
|
||||
}
|
||||
}
|
||||
|
||||
.nc-entryEditor-toolbar-publishButton {
|
||||
background-color: var(--colorTeal);
|
||||
}
|
||||
|
||||
.nc-entryEditor-toolbar-statusButton {
|
||||
background-color: var(--colorTealLight);
|
||||
color: var(--colorTeal);
|
||||
}
|
||||
|
||||
.nc-entryEditor-toolbar-backSection {
|
||||
border-right-width: 1px;
|
||||
font-weight: normal;
|
||||
padding: 0 20px;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: #F1F2F4;
|
||||
}
|
||||
}
|
||||
|
||||
.nc-entryEditor-toolbar-metaSection {
|
||||
border-left-width: 1px;
|
||||
padding: 0 7px;
|
||||
}
|
||||
|
||||
.nc-entryEditor-toolbar-backArrow {
|
||||
color: var(--colorTextLead);
|
||||
font-size: 21px;
|
||||
font-weight: 600;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.nc-entryEditor-toolbar-backCollection {
|
||||
color: var(--colorTextLead);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.nc-entryEditor-toolbar-backStatus {
|
||||
@apply(--textBadgeSuccess);
|
||||
|
||||
&::after {
|
||||
height: 12px;
|
||||
width: 15.5px;
|
||||
color: #005614;
|
||||
margin-left: 5px;
|
||||
|
||||
position: relative;
|
||||
top: 1px;
|
||||
|
||||
content: url("data:image/svg+xml; utf8, <svg xmlns='http://www.w3.org/2000/svg' width='15' height='11'><path fill='#005614' fill-rule='nonzero' d='M4.016 11l-.648-.946a6.202 6.202 0 0 0-.157-.22 9.526 9.526 0 0 1-.096-.133l-.511-.7a7.413 7.413 0 0 0-.162-.214l-.102-.134-.265-.346a26.903 26.903 0 0 0-.543-.687l-.11-.136c-.143-.179-.291-.363-.442-.54l-.278-.332a8.854 8.854 0 0 0-.192-.225L.417 6.28l-.283-.324L0 5.805l1.376-1.602c.04.027.186.132.186.132l.377.272.129.095c.08.058.16.115.237.175l.37.28c.192.142.382.292.565.436l.162.126c.27.21.503.398.714.574l.477.393c.078.064.156.127.23.194l.433.375.171-.205A50.865 50.865 0 0 1 8.18 4.023a35.163 35.163 0 0 1 2.382-2.213c.207-.174.42-.349.635-.518l.328-.255.333-.245c.072-.055.146-.107.221-.159l.117-.083c.11-.077.225-.155.341-.23.163-.11.334-.217.503-.32l1.158 1.74a11.908 11.908 0 0 0-.64.55l-.065.06c-.07.062-.139.125-.207.192l-.258.249-.26.265c-.173.176-.345.357-.512.539a32.626 32.626 0 0 0-1.915 2.313 52.115 52.115 0 0 0-2.572 3.746l-.392.642-.19.322-.233.382H4.016z'/></svg>");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.nc-entryEditor-toolbar-backStatus-hasChanged {
|
||||
@apply(--textBadgeDanger);
|
||||
}
|
||||
|
||||
.nc-entryEditor-toolbar-backStatus,
|
||||
.nc-entryEditor-toolbar-backStatus-hasChanged {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.nc-entryEditor-toolbar-deleteButton,
|
||||
.nc-entryEditor-toolbar-saveButton {
|
||||
@apply(--buttonDefault);
|
||||
display: block;
|
||||
margin: var(--editorToolbarButtonMargin);
|
||||
}
|
||||
|
||||
.nc-entryEditor-toolbar-deleteButton {
|
||||
@apply(--buttonLightRed);
|
||||
}
|
||||
|
||||
.nc-entryEditor-toolbar-saveButton {
|
||||
@apply(--buttonLightBlue);
|
||||
}
|
||||
|
||||
.nc-entryEditor-toolbar-statusPublished {
|
||||
margin: var(--editorToolbarButtonMargin);
|
||||
border: 1px solid var(--textFieldBorderColor);
|
||||
border-radius: var(--borderRadius);
|
||||
background-color: var(--colorWhite);
|
||||
color: var(--colorTeal);
|
||||
padding: 0 24px;
|
||||
line-height: 36px;
|
||||
cursor: default;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.nc-entryEditor-toolbar-statusMenu-status .nc-icon {
|
||||
color: var(--colorInfoText);
|
||||
}
|
@ -1,12 +1,156 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import c from 'classnames';
|
||||
import styled, { css } from 'react-emotion';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { status } from 'Constants/publishModes';
|
||||
import { Icon, Dropdown, DropdownItem } from 'netlify-cms-ui-default';
|
||||
import SettingsDropdown from 'UI/SettingsDropdown';
|
||||
import Dropdown, { DropdownItem, StyledDropdownButton } from 'netlify-cms-ui-default/Dropdown';
|
||||
import Icon from 'netlify-cms-ui-default/Icon';
|
||||
import { colorsRaw, colors, components, buttons, lengths } from 'netlify-cms-ui-default/styles';
|
||||
import { stripProtocol } from 'Lib/urlHelper';
|
||||
|
||||
const styles = {
|
||||
buttonMargin: css`
|
||||
margin: 0 10px;
|
||||
`,
|
||||
toolbarSection: css`
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`,
|
||||
}
|
||||
|
||||
const ToolbarContainer = styled.div`
|
||||
box-shadow: 0 2px 6px 0 rgba(68, 74, 87, 0.05),
|
||||
0 1px 3px 0 rgba(68, 74, 87, 0.10),
|
||||
0 2px 54px rgba(0, 0, 0, 0.1);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
min-width: 800px;
|
||||
z-index: 300;
|
||||
background-color: #fff;
|
||||
height: 66px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`
|
||||
|
||||
const ToolbarSectionMain = styled.div`
|
||||
flex: 10;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 10px;
|
||||
`
|
||||
|
||||
const ToolbarSubSectionFirst = styled.div`
|
||||
display: flex;
|
||||
`
|
||||
|
||||
const ToolbarSubSectionLast = styled(ToolbarSubSectionFirst)`
|
||||
justify-content: flex-end;
|
||||
`
|
||||
|
||||
const ToolbarSectionBackLink = styled(Link)`
|
||||
${styles.toolbarSection};
|
||||
border: 0 solid ${colors.textFieldBorder};
|
||||
border-right-width: 1px;
|
||||
font-weight: normal;
|
||||
padding: 0 20px;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: #F1F2F4;
|
||||
}
|
||||
`
|
||||
|
||||
const ToolbarSectionMeta = styled.div`
|
||||
${styles.toolbarSection};
|
||||
border-left-width: 1px;
|
||||
padding: 0 7px;
|
||||
`
|
||||
|
||||
const ToolbarDropdown = styled(Dropdown)`
|
||||
${styles.buttonMargin};
|
||||
|
||||
${Icon} {
|
||||
color: ${colorsRaw.teal};
|
||||
}
|
||||
`
|
||||
|
||||
const BackArrow = styled.div`
|
||||
color: ${colors.textLead};
|
||||
font-size: 21px;
|
||||
font-weight: 600;
|
||||
margin-right: 16px;
|
||||
`
|
||||
|
||||
const BackCollection = styled.div`
|
||||
color: ${colors.textLead};
|
||||
font-size: 14px;
|
||||
`
|
||||
|
||||
const BackStatus = styled.div`
|
||||
margin-top: 6px;
|
||||
`
|
||||
|
||||
const BackStatusUnchanged = styled(BackStatus)`
|
||||
${components.textBadgeSuccess};
|
||||
|
||||
&::after {
|
||||
height: 12px;
|
||||
width: 15.5px;
|
||||
color: ${colors.successText};
|
||||
margin-left: 5px;
|
||||
|
||||
position: relative;
|
||||
top: 1px;
|
||||
|
||||
content: url("data:image/svg+xml; utf8, <svg xmlns='http://www.w3.org/2000/svg' width='15' height='11'><path fill='#005614' fill-rule='nonzero' d='M4.016 11l-.648-.946a6.202 6.202 0 0 0-.157-.22 9.526 9.526 0 0 1-.096-.133l-.511-.7a7.413 7.413 0 0 0-.162-.214l-.102-.134-.265-.346a26.903 26.903 0 0 0-.543-.687l-.11-.136c-.143-.179-.291-.363-.442-.54l-.278-.332a8.854 8.854 0 0 0-.192-.225L.417 6.28l-.283-.324L0 5.805l1.376-1.602c.04.027.186.132.186.132l.377.272.129.095c.08.058.16.115.237.175l.37.28c.192.142.382.292.565.436l.162.126c.27.21.503.398.714.574l.477.393c.078.064.156.127.23.194l.433.375.171-.205A50.865 50.865 0 0 1 8.18 4.023a35.163 35.163 0 0 1 2.382-2.213c.207-.174.42-.349.635-.518l.328-.255.333-.245c.072-.055.146-.107.221-.159l.117-.083c.11-.077.225-.155.341-.23.163-.11.334-.217.503-.32l1.158 1.74a11.908 11.908 0 0 0-.64.55l-.065.06c-.07.062-.139.125-.207.192l-.258.249-.26.265c-.173.176-.345.357-.512.539a32.626 32.626 0 0 0-1.915 2.313 52.115 52.115 0 0 0-2.572 3.746l-.392.642-.19.322-.233.382H4.016z'/></svg>");
|
||||
}
|
||||
`
|
||||
|
||||
const BackStatusChanged = styled(BackStatus)`
|
||||
${components.textBadgeDanger};
|
||||
`
|
||||
|
||||
const ToolbarButton = styled.button`
|
||||
${buttons.default};
|
||||
${styles.buttonMargin};
|
||||
display: block;
|
||||
`
|
||||
|
||||
const DeleteButton = styled(ToolbarButton)`
|
||||
${buttons.lightRed};
|
||||
`
|
||||
|
||||
const SaveButton = styled(ToolbarButton)`
|
||||
${buttons.lightBlue};
|
||||
`
|
||||
|
||||
const StatusPublished = styled.div`
|
||||
${styles.buttonMargin};
|
||||
border: 1px solid ${colors.textFieldBorder};
|
||||
border-radius: ${lengths.borderRadius};
|
||||
background-color: ${colorsRaw.white};
|
||||
color: ${colorsRaw.teal};
|
||||
padding: 0 24px;
|
||||
line-height: 36px;
|
||||
cursor: default;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
`
|
||||
|
||||
const PublishButton = styled(StyledDropdownButton)`
|
||||
background-color: ${colorsRaw.teal};
|
||||
`
|
||||
|
||||
const StatusButton = styled(StyledDropdownButton)`
|
||||
background-color: ${colorsRaw.tealLight};
|
||||
color: ${colorsRaw.teal};
|
||||
`
|
||||
|
||||
export default class EditorToolbar extends React.Component {
|
||||
static propTypes = {
|
||||
isPersisting: PropTypes.bool,
|
||||
@ -38,13 +182,7 @@ export default class EditorToolbar extends React.Component {
|
||||
const { showDelete, onDelete } = this.props;
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
showDelete
|
||||
? <button className="nc-entryEditor-toolbar-deleteButton" onClick={onDelete}>
|
||||
Delete entry
|
||||
</button>
|
||||
: null
|
||||
}
|
||||
{ showDelete ? <DeleteButton onClick={onDelete}>Delete entry</DeleteButton> : null }
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -52,16 +190,16 @@ export default class EditorToolbar extends React.Component {
|
||||
renderSimplePublishControls = () => {
|
||||
const { collection, onPersist, onPersistAndNew, isPersisting, hasChanged, isNewEntry } = this.props;
|
||||
if (!isNewEntry && !hasChanged) {
|
||||
return <div className="nc-entryEditor-toolbar-statusPublished">Published</div>;
|
||||
return <StatusPublished>Published</StatusPublished>;
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<Dropdown
|
||||
className="nc-entryEditor-toolbar-dropdown"
|
||||
classNameButton="nc-entryEditor-toolbar-publishButton"
|
||||
<ToolbarDropdown
|
||||
dropdownTopOverlap="40px"
|
||||
dropdownWidth="150px"
|
||||
label={isPersisting ? 'Publishing...' : 'Publish'}
|
||||
renderButton={() => (
|
||||
<PublishButton>{isPersisting ? 'Publishing...' : 'Publish'}</PublishButton>
|
||||
)}
|
||||
>
|
||||
<DropdownItem label="Publish now" icon="arrow" iconDirection="right" onClick={onPersist}/>
|
||||
{
|
||||
@ -69,7 +207,7 @@ export default class EditorToolbar extends React.Component {
|
||||
? <DropdownItem label="Publish and create new" icon="add" onClick={onPersistAndNew}/>
|
||||
: null
|
||||
}
|
||||
</Dropdown>
|
||||
</ToolbarDropdown>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -92,21 +230,16 @@ export default class EditorToolbar extends React.Component {
|
||||
|| (!hasUnpublishedChanges && !isModification && 'Delete published entry');
|
||||
|
||||
return [
|
||||
<button
|
||||
key="save-button"
|
||||
className="nc-entryEditor-toolbar-saveButton"
|
||||
onClick={() => hasChanged && onPersist()}
|
||||
>
|
||||
{isPersisting ? 'Saving...' : 'Save'}
|
||||
</button>,
|
||||
isNewEntry || !deleteLabel ? null
|
||||
: <button
|
||||
key="delete-button"
|
||||
className="nc-entryEditor-toolbar-deleteButton"
|
||||
onClick={hasUnpublishedChanges ? onDeleteUnpublishedChanges : onDelete}
|
||||
>
|
||||
{isDeleting ? 'Deleting...' : deleteLabel}
|
||||
</button>,
|
||||
<SaveButton key="save-button" onClick={() => hasChanged && onPersist()}>
|
||||
{isPersisting ? 'Saving...' : 'Save'}
|
||||
</SaveButton>,
|
||||
isNewEntry || !deleteLabel ? null
|
||||
: <DeleteButton
|
||||
key="delete-button"
|
||||
onClick={hasUnpublishedChanges ? onDeleteUnpublishedChanges : onDelete}
|
||||
>
|
||||
{isDeleting ? 'Deleting...' : deleteLabel}
|
||||
</DeleteButton>,
|
||||
];
|
||||
};
|
||||
|
||||
@ -125,38 +258,35 @@ export default class EditorToolbar extends React.Component {
|
||||
} = this.props;
|
||||
if (currentStatus) {
|
||||
return [
|
||||
<Dropdown
|
||||
className="nc-entryEditor-toolbar-dropdown"
|
||||
classNameButton="nc-entryEditor-toolbar-statusButton"
|
||||
<ToolbarDropdown
|
||||
dropdownTopOverlap="40px"
|
||||
dropdownWidth="120px"
|
||||
label={isUpdatingStatus ? 'Updating...' : 'Set status'}
|
||||
renderButton={() => (
|
||||
<StatusButton>{isUpdatingStatus ? 'Updating...' : 'Set status'}</StatusButton>
|
||||
)}
|
||||
>
|
||||
<DropdownItem
|
||||
className="nc-entryEditor-toolbar-statusMenu-status"
|
||||
label="Draft"
|
||||
onClick={() => onChangeStatus('DRAFT')}
|
||||
icon={currentStatus === status.get('DRAFT') && 'check'}
|
||||
/>
|
||||
<DropdownItem
|
||||
className="nc-entryEditor-toolbar-statusMenu-status"
|
||||
label="In review"
|
||||
onClick={() => onChangeStatus('PENDING_REVIEW')}
|
||||
icon={currentStatus === status.get('PENDING_REVIEW') && 'check'}
|
||||
/>
|
||||
<DropdownItem
|
||||
className="nc-entryEditor-toolbar-statusMenu-status"
|
||||
label="Ready"
|
||||
onClick={() => onChangeStatus('PENDING_PUBLISH')}
|
||||
icon={currentStatus === status.get('PENDING_PUBLISH') && 'check'}
|
||||
/>
|
||||
</Dropdown>,
|
||||
<Dropdown
|
||||
className="nc-entryEditor-toolbar-dropdown"
|
||||
classNameButton="nc-entryEditor-toolbar-publishButton"
|
||||
</ToolbarDropdown>,
|
||||
<ToolbarDropdown
|
||||
dropdownTopOverlap="40px"
|
||||
dropdownWidth="150px"
|
||||
label={isPublishing ? 'Publishing...' : 'Publish'}
|
||||
renderButton={() => (
|
||||
<PublishButton>{isPersisting ? 'Publishing...' : 'Publish'}</PublishButton>
|
||||
)}
|
||||
>
|
||||
<DropdownItem label="Publish now" icon="arrow" iconDirection="right" onClick={onPublish}/>
|
||||
{
|
||||
@ -164,12 +294,12 @@ export default class EditorToolbar extends React.Component {
|
||||
? <DropdownItem label="Publish and create new" icon="add" onClick={onPublishAndNew}/>
|
||||
: null
|
||||
}
|
||||
</Dropdown>
|
||||
</ToolbarDropdown>
|
||||
];
|
||||
}
|
||||
|
||||
if (!isNewEntry) {
|
||||
return <div className="nc-entryEditor-toolbar-statusPublished">Published</div>;
|
||||
return <StatusPublished>Published</StatusPublished>
|
||||
}
|
||||
};
|
||||
|
||||
@ -195,54 +325,36 @@ export default class EditorToolbar extends React.Component {
|
||||
const avatarUrl = user.get('avatar_url');
|
||||
|
||||
return (
|
||||
<div className="nc-entryEditor-toolbar">
|
||||
<Link to={`/collections/${collection.get('name')}`} className="nc-entryEditor-toolbar-backSection">
|
||||
<div className="nc-entryEditor-toolbar-backArrow">←</div>
|
||||
<ToolbarContainer>
|
||||
<ToolbarSectionBackLink to={`/collections/${collection.get('name')}`}>
|
||||
<BackArrow>←</BackArrow>
|
||||
<div>
|
||||
<div className="nc-entryEditor-toolbar-backCollection">
|
||||
<BackCollection>
|
||||
Writing in <strong>{collection.get('label')}</strong> collection
|
||||
</div>
|
||||
</BackCollection>
|
||||
{
|
||||
hasChanged
|
||||
? <div className="nc-entryEditor-toolbar-backStatus-hasChanged">Unsaved Changes</div>
|
||||
: <div className="nc-entryEditor-toolbar-backStatus">Changes saved</div>
|
||||
? <BackStatusChanged>Unsaved Changes</BackStatusChanged>
|
||||
: <BackStatusUnchanged>Changes saved</BackStatusUnchanged>
|
||||
}
|
||||
</div>
|
||||
</Link>
|
||||
<div className="nc-entryEditor-toolbar-mainSection">
|
||||
<div className="nc-entryEditor-toolbar-mainSection-left">
|
||||
</ToolbarSectionBackLink>
|
||||
<ToolbarSectionMain>
|
||||
<ToolbarSubSectionFirst>
|
||||
{ hasWorkflow ? this.renderWorkflowSaveControls() : this.renderSimpleSaveControls() }
|
||||
</div>
|
||||
<div className="nc-entryEditor-toolbar-mainSection-right">
|
||||
</ToolbarSubSectionFirst>
|
||||
<ToolbarSubSectionLast>
|
||||
{ hasWorkflow ? this.renderWorkflowPublishControls() : this.renderSimplePublishControls() }
|
||||
</div>
|
||||
</div>
|
||||
<div className="nc-entryEditor-toolbar-metaSection">
|
||||
{
|
||||
displayUrl
|
||||
? <a className="nc-appHeader-siteLink" href={displayUrl} target="_blank">
|
||||
{stripProtocol(displayUrl)}
|
||||
</a>
|
||||
: null
|
||||
}
|
||||
<Dropdown
|
||||
dropdownTopOverlap="50px"
|
||||
dropdownWidth="100px"
|
||||
dropdownPosition="right"
|
||||
button={
|
||||
<button className="nc-appHeader-avatar">
|
||||
{
|
||||
avatarUrl
|
||||
? <img className="nc-appHeader-avatar-image" src={user.get('avatar_url')}/>
|
||||
: <Icon className="nc-appHeader-avatar-placeholder" type="user" size="large"/>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<DropdownItem label="Log Out" onClick={onLogoutClick}/>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</ToolbarSubSectionLast>
|
||||
</ToolbarSectionMain>
|
||||
<ToolbarSectionMeta>
|
||||
<SettingsDropdown
|
||||
displayUrl={displayUrl}
|
||||
imageUrl={user.get('avatar_url')}
|
||||
onLogoutClick={onLogoutClick}
|
||||
/>
|
||||
</ToolbarSectionMeta>
|
||||
</ToolbarContainer>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from "react-immutable-proptypes";
|
||||
import { isBoolean } from 'lodash';
|
||||
import { Toggle } from 'netlify-cms-ui-default';
|
||||
import Toggle from 'netlify-cms-ui-default/Toggle';
|
||||
|
||||
export default class BooleanControl extends React.Component {
|
||||
render() {
|
||||
|
@ -5,7 +5,8 @@ import { List, Map } from 'immutable';
|
||||
import { partial } from 'lodash';
|
||||
import c from 'classnames';
|
||||
import { SortableContainer, SortableElement, SortableHandle } from 'react-sortable-hoc';
|
||||
import { Icon, ListItemTopBar } from 'netlify-cms-ui-default';
|
||||
import Icon from 'netlify-cms-ui-default/Icon';
|
||||
import ListItemTopBar from 'netlify-cms-ui-default/ListItemTopBar';
|
||||
import ObjectControl from 'EditorWidgets/Object/ObjectControl';
|
||||
|
||||
function ListItem(props) {
|
||||
|
@ -3,7 +3,9 @@ import React from 'react';
|
||||
import { List } from 'immutable';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import c from 'classnames';
|
||||
import { Dropdown, DropdownItem, Toggle, Icon } from 'netlify-cms-ui-default';
|
||||
import Dropdown, { DropdownItem, DropdownButton } from 'netlify-cms-ui-default/Dropdown';
|
||||
import Toggle from 'netlify-cms-ui-default/Toggle';
|
||||
import Icon from 'netlify-cms-ui-default/Icon';
|
||||
import ToolbarButton from './ToolbarButton';
|
||||
|
||||
export default class Toolbar extends React.Component {
|
||||
@ -158,14 +160,16 @@ export default class Toolbar extends React.Component {
|
||||
<div className="nc-toolbar-dropdown">
|
||||
<Dropdown
|
||||
dropdownTopOverlap="36px"
|
||||
button={
|
||||
<ToolbarButton
|
||||
label="Add Component"
|
||||
icon="add-with"
|
||||
onClick={this.handleComponentsMenuToggle}
|
||||
disabled={disabled}
|
||||
/>
|
||||
}
|
||||
renderButton={() => (
|
||||
<DropdownButton>
|
||||
<ToolbarButton
|
||||
label="Add Component"
|
||||
icon="add-with"
|
||||
onClick={this.handleComponentsMenuToggle}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</DropdownButton>
|
||||
)}
|
||||
>
|
||||
{plugins && plugins.toList().map((plugin, idx) => (
|
||||
<DropdownItem key={idx} label={plugin.get('label')} onClick={() => onSubmit(plugin.get('id'))} />
|
||||
|
@ -1,7 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import c from 'classnames';
|
||||
import { Icon } from 'netlify-cms-ui-default';
|
||||
import Icon from 'netlify-cms-ui-default/Icon';
|
||||
|
||||
const ToolbarButton = ({ type, label, icon, onClick, isActive, isHidden, disabled }) => {
|
||||
const active = isActive && type && isActive(type);
|
||||
|
@ -7,7 +7,7 @@ import { resolveWidget, getEditorComponents } from 'Lib/registry';
|
||||
import { openMediaLibrary, removeInsertedMedia } from 'Actions/mediaLibrary';
|
||||
import { addAsset } from 'Actions/media';
|
||||
import { getAsset } from 'Reducers';
|
||||
import { ListItemTopBar } from 'netlify-cms-ui-default';
|
||||
import ListItemTopBar from 'netlify-cms-ui-default/ListItemTopBar';
|
||||
import { getEditorControl } from '../index';
|
||||
|
||||
class Shortcode extends React.Component {
|
||||
|
@ -5,7 +5,7 @@ import { Map } from 'immutable';
|
||||
import { partial } from 'lodash';
|
||||
import c from 'classnames';
|
||||
import { resolveWidget } from 'Lib/registry';
|
||||
import { Icon } from 'netlify-cms-ui-default';
|
||||
import Icon from 'netlify-cms-ui-default/Icon';
|
||||
|
||||
const TopBar = ({ collapsed, onCollapseToggle }) => (
|
||||
<div className="nc-objectControl-topBar">
|
||||
|
@ -6,7 +6,7 @@ import { List, Map } from 'immutable';
|
||||
import { connect } from 'react-redux';
|
||||
import { debounce } from 'lodash';
|
||||
import { query, clearSearch } from 'Actions/search';
|
||||
import { Loader } from 'netlify-cms-ui-default';
|
||||
import Loader from 'netlify-cms-ui-default/Loader';
|
||||
|
||||
function escapeRegexCharacters(str) {
|
||||
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
|
@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from 'react-emotion';
|
||||
import { colors } from 'netlify-cms-ui-default/styles';
|
||||
|
||||
const EmptyMessageContainer= styled.div`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: ${props => props.isPrivate && colors.textFieldBorder};
|
||||
`
|
||||
|
||||
const EmptyMessage = ({ content, isPrivate }) => (
|
||||
<EmptyMessageContainer isPrivate={isPrivate}>
|
||||
<h1>{content}</h1>
|
||||
</EmptyMessageContainer>
|
||||
);
|
||||
|
||||
EmptyMessage.propTypes = {
|
||||
content: PropTypes.string.isRequired,
|
||||
isPrivate: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default EmptyMessage;
|
@ -1,230 +0,0 @@
|
||||
:root {
|
||||
--mediaLibraryCardWidth: 280px;
|
||||
--mediaLibraryCardMargin: 10px;
|
||||
--mediaLibraryCardOutsideWidth: calc(var(--mediaLibraryCardWidth) + var(--mediaLibraryCardMargin) * 2);
|
||||
}
|
||||
|
||||
.nc-mediaLibrary-dialog {
|
||||
display: grid;
|
||||
grid-template-rows: 120px auto;
|
||||
width: calc(var(--mediaLibraryCardOutsideWidth) + 20px);
|
||||
|
||||
@media (width >= 800px) {
|
||||
width: calc(var(--mediaLibraryCardOutsideWidth) * 2 + 20px);
|
||||
}
|
||||
|
||||
@media (width >= 1120px) {
|
||||
width: calc(var(--mediaLibraryCardOutsideWidth) * 3 + 20px);
|
||||
}
|
||||
|
||||
@media (width >= 1440px) {
|
||||
width: calc(var(--mediaLibraryCardOutsideWidth) * 4 + 20px);
|
||||
}
|
||||
|
||||
@media (width >= 1760px) {
|
||||
width: calc(var(--mediaLibraryCardOutsideWidth) * 5 + 20px);
|
||||
}
|
||||
|
||||
@media (width >= 2080px) {
|
||||
width: calc(var(--mediaLibraryCardOutsideWidth) * 6 + 20px);
|
||||
}
|
||||
}
|
||||
|
||||
.nc-mediaLibrary-title {
|
||||
line-height: 36px;
|
||||
font-size: 22px;
|
||||
text-align: left;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.nc-mediaLibrary-actionContainer {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.nc-mediaLibrary-uploadButton {
|
||||
@apply(--buttonGray);
|
||||
@apply(--dropShadowMain);
|
||||
margin-bottom: 0;
|
||||
|
||||
& span {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
& input {
|
||||
height: .1px;
|
||||
width: .1px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.nc-mediaLibrary-lowerActionContainer {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.nc-mediaLibrary-uploadButton,
|
||||
.nc-mediaLibrary-deleteButton,
|
||||
.nc-mediaLibrary-insertButton {
|
||||
@apply(--button);
|
||||
@apply(--buttonDefault);
|
||||
display: inline-block;
|
||||
margin-left: 15px;
|
||||
|
||||
&[disabled],
|
||||
&.nc-mediaLibrary-uploadButton-disabled {
|
||||
@apply(--buttonDisabled);
|
||||
}
|
||||
}
|
||||
|
||||
.nc-mediaLibrary-deleteButton {
|
||||
@apply(--buttonLightRed);
|
||||
}
|
||||
|
||||
.nc-mediaLibrary-insertButton {
|
||||
@apply(--buttonGreen);
|
||||
}
|
||||
|
||||
|
||||
.nc-mediaLibrary-top {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
& .nc-mediaLibrary-close {
|
||||
@apply(--dropShadowMiddle);
|
||||
|
||||
position: absolute;
|
||||
margin-right: -40px;
|
||||
left: -40px;
|
||||
top: -40px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background-color: white;
|
||||
padding: 0;
|
||||
|
||||
& .nc-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nc-mediaLibrary-search {
|
||||
height: 37px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
width: 400px;
|
||||
|
||||
& input {
|
||||
background-color: #eff0f4;
|
||||
border-radius: var(--borderRadius);
|
||||
|
||||
font-size: 14px;
|
||||
padding: 10px 6px 10px 32px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: inset 0 0 0 2px var(--colorBlue);
|
||||
}
|
||||
}
|
||||
|
||||
& .nc-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 6px;
|
||||
z-index: 2;
|
||||
transform: translate(0, -50%);
|
||||
}
|
||||
}
|
||||
|
||||
.nc-mediaLibrary-emptyMessage {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nc-mediaLibrary-cardGrid-container {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.nc-mediaLibrary-cardGrid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
margin-left: -10px;
|
||||
margin-right: -10px;
|
||||
}
|
||||
|
||||
.nc-mediaLibrary-card {
|
||||
width: var(--mediaLibraryCardWidth);
|
||||
height: 240px;
|
||||
margin: var(--mediaLibraryCardMargin);
|
||||
border: var(--textFieldBorder);
|
||||
border-radius: var(--borderRadius);
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.nc-mediaLibrary-card-selected {
|
||||
border-color: var(--colorActive);
|
||||
}
|
||||
|
||||
.nc-mediaLibrary-cardImage {
|
||||
width: 100%;
|
||||
height: 160px;
|
||||
object-fit: cover;
|
||||
border-radius: 2px 2px 0 0;
|
||||
}
|
||||
|
||||
.nc-mediaLibrary-cardText {
|
||||
color: var(--colorText);
|
||||
padding: 8px;
|
||||
margin-top: 20px;
|
||||
overflow-wrap: break-word;
|
||||
line-height: 1.3 !important;
|
||||
}
|
||||
|
||||
.nc-mediaLibrary-dialogPrivate {
|
||||
background-color: var(--colorGrayDark);
|
||||
|
||||
& .nc-mediaLibrary-title,
|
||||
& .nc-mediaLibrary-emptyMessage,
|
||||
& .nc-mediaLibrary-paginatingMessage,
|
||||
& h1 {
|
||||
color: var(--textFieldBorderColor);
|
||||
}
|
||||
|
||||
& .nc-mediaLibrary-card,
|
||||
& .nc-mediaLibrary-searchInput {
|
||||
background-color: var(--textFieldBorderColor);
|
||||
}
|
||||
|
||||
& button:disabled,
|
||||
& label[disabled] {
|
||||
background-color: rgba(217, 217, 217, 0.15);
|
||||
}
|
||||
}
|
@ -1,10 +1,7 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { orderBy, get, isEmpty, map } from 'lodash';
|
||||
import c from 'classnames';
|
||||
import { orderBy, map } from 'lodash';
|
||||
import fuzzy from 'fuzzy';
|
||||
import Waypoint from 'react-waypoint';
|
||||
import { Modal, FileUploadButton } from 'UI';
|
||||
import { resolvePath, fileExtension } from 'netlify-cms-lib-util/path';
|
||||
import { changeDraftField } from 'Actions/entries';
|
||||
import {
|
||||
@ -14,8 +11,7 @@ import {
|
||||
insertMedia as insertMediaAction,
|
||||
closeMediaLibrary as closeMediaLibraryAction,
|
||||
} from 'Actions/mediaLibrary';
|
||||
import { Icon } from 'netlify-cms-ui-default';
|
||||
|
||||
import MediaLibraryModal from './MediaLibraryModal';
|
||||
|
||||
/**
|
||||
* Extensions used to determine which files to show when the media library is
|
||||
@ -212,7 +208,7 @@ class MediaLibrary extends React.Component {
|
||||
const {
|
||||
isVisible,
|
||||
canInsert,
|
||||
files,
|
||||
files = [],
|
||||
dynamicSearch,
|
||||
dynamicSearchActive,
|
||||
forImage,
|
||||
@ -224,115 +220,37 @@ class MediaLibrary extends React.Component {
|
||||
isPaginating,
|
||||
privateUpload,
|
||||
} = this.props;
|
||||
const { query, selectedFile } = this.state;
|
||||
const filteredFiles = forImage ? this.filterImages(files) : files;
|
||||
const queriedFiles = (!dynamicSearch && query) ? this.queryFilter(query, filteredFiles) : filteredFiles;
|
||||
const tableData = this.toTableData(queriedFiles);
|
||||
const hasFiles = files && !!files.length;
|
||||
const hasFilteredFiles = filteredFiles && !!filteredFiles.length;
|
||||
const hasSearchResults = queriedFiles && !!queriedFiles.length;
|
||||
const hasMedia = hasSearchResults;
|
||||
const shouldShowEmptyMessage = !hasMedia;
|
||||
const emptyMessage = (isLoading && !hasMedia && 'Loading...')
|
||||
|| (dynamicSearchActive && 'No results.')
|
||||
|| (!hasFiles && 'No assets found.')
|
||||
|| (!hasFilteredFiles && 'No images found.')
|
||||
|| (!hasSearchResults && 'No results.');
|
||||
const hasSelection = hasMedia && !isEmpty(selectedFile);
|
||||
const shouldShowButtonLoader = isPersisting || isDeleting;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isVisible}
|
||||
onClose={this.handleClose}
|
||||
className={c('nc-mediaLibrary-dialog', { 'nc-mediaLibrary-dialogPrivate': privateUpload })}
|
||||
>
|
||||
<div className="nc-mediaLibrary-top">
|
||||
<div>
|
||||
<div className="nc-mediaLibrary-header">
|
||||
<button className="nc-mediaLibrary-close" onClick={this.handleClose}>
|
||||
<Icon type="close"/>
|
||||
</button>
|
||||
<h1 className="nc-mediaLibrary-title">
|
||||
{privateUpload ? 'Private ' : null}
|
||||
{forImage ? 'Images' : 'Media assets'}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="nc-mediaLibrary-search">
|
||||
<Icon type="search" size="small"/>
|
||||
<input
|
||||
className=""
|
||||
value={query}
|
||||
onChange={this.handleSearchChange}
|
||||
onKeyDown={event => this.handleSearchKeyDown(event)}
|
||||
placeholder="Search..."
|
||||
disabled={!dynamicSearchActive && !hasFilteredFiles}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="nc-mediaLibrary-actionContainer">
|
||||
<FileUploadButton
|
||||
className={`nc-mediaLibrary-uploadButton ${shouldShowButtonLoader ? 'nc-mediaLibrary-uploadButton-disabled' : ''}`}
|
||||
label={isPersisting ? 'Uploading...' : 'Upload new'}
|
||||
imagesOnly={forImage}
|
||||
onChange={this.handlePersist}
|
||||
disabled={shouldShowButtonLoader}
|
||||
/>
|
||||
<div className="nc-mediaLibrary-lowerActionContainer">
|
||||
<button
|
||||
className="nc-mediaLibrary-deleteButton"
|
||||
onClick={this.handleDelete}
|
||||
disabled={shouldShowButtonLoader || !hasSelection}
|
||||
>
|
||||
{isDeleting ? 'Deleting...' : 'Delete selected'}
|
||||
</button>
|
||||
{ !canInsert ? null :
|
||||
<button
|
||||
onClick={this.handleInsert}
|
||||
disabled={!hasSelection}
|
||||
className="nc-mediaLibrary-insertButton"
|
||||
>
|
||||
Choose selected
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
shouldShowEmptyMessage
|
||||
? <div className="nc-mediaLibrary-emptyMessage"><h1>{emptyMessage}</h1></div>
|
||||
: null
|
||||
}
|
||||
<div className="nc-mediaLibrary-cardGrid-container" ref={ref => (this.scrollContainerRef = ref)}>
|
||||
<div className="nc-mediaLibrary-cardGrid">
|
||||
{
|
||||
tableData.map((file, idx) =>
|
||||
<div
|
||||
key={file.key}
|
||||
className={c('nc-mediaLibrary-card', { 'nc-mediaLibrary-card-selected': selectedFile.key === file.key })}
|
||||
onClick={() => this.handleAssetClick(file)}
|
||||
tabIndex="-1"
|
||||
>
|
||||
<div className="nc-mediaLibrary-cardImage-container">
|
||||
{
|
||||
file.isViewableImage
|
||||
? <img src={file.url} className="nc-mediaLibrary-cardImage"/>
|
||||
: <div className="nc-mediaLibrary-cardImage"/>
|
||||
}
|
||||
</div>
|
||||
<p className="nc-mediaLibrary-cardText">{file.name}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
hasNextPage
|
||||
? <Waypoint onEnter={() => this.handleLoadMore()}/>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
{ isPaginating ? <h1 className="nc-mediaLibrary-paginatingMessage">Loading...</h1> : null }
|
||||
</div>
|
||||
</Modal>
|
||||
<MediaLibraryModal
|
||||
isVisible={isVisible}
|
||||
canInsert={canInsert}
|
||||
files={files}
|
||||
dynamicSearch={dynamicSearch}
|
||||
dynamicSearchActive={dynamicSearchActive}
|
||||
forImage={forImage}
|
||||
isLoading={isLoading}
|
||||
isPersisting={isPersisting}
|
||||
isDeleting={isDeleting}
|
||||
hasNextPage={hasNextPage}
|
||||
page={page}
|
||||
isPaginating={isPaginating}
|
||||
privateUpload={privateUpload}
|
||||
query={this.state.query}
|
||||
selectedFile={this.state.selectedFile}
|
||||
handleFilter={this.filterImages}
|
||||
handleQuery={this.queryFilter}
|
||||
toTableData={this.toTableData}
|
||||
handleClose={this.handleClose}
|
||||
handleSearchChange={this.handleSearchChange}
|
||||
handleSearchKeyDown={this.handleSearchKeyDown}
|
||||
handlePersist={this.handlePersist}
|
||||
handleDelete={this.handleDelete}
|
||||
handleInsert={this.handleInsert}
|
||||
setScrollContainerRef={ref => this.scrollContainerRef = ref}
|
||||
handleAssetClick={this.handleAssetClick}
|
||||
handleLoadMore={this.handleLoadMore}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,114 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled, { css } from 'react-emotion';
|
||||
import { FileUploadButton } from 'UI';
|
||||
import { buttons, shadows } from 'netlify-cms-ui-default/styles';
|
||||
|
||||
const styles = {
|
||||
button: css`
|
||||
${buttons.button};
|
||||
${buttons.default};
|
||||
display: inline-block;
|
||||
margin-left: 15px;
|
||||
margin-right: 2px;
|
||||
|
||||
&[disabled] {
|
||||
${buttons.disabled};
|
||||
cursor: default;
|
||||
}
|
||||
`,
|
||||
}
|
||||
|
||||
const ActionsContainer = styled.div`
|
||||
text-align: right;
|
||||
`
|
||||
|
||||
const StyledUploadButton = styled(FileUploadButton)`
|
||||
${styles.button};
|
||||
${buttons.gray};
|
||||
${shadows.dropMain};
|
||||
margin-bottom: 0;
|
||||
|
||||
span {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
input {
|
||||
height: .1px;
|
||||
width: .1px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
outline: none;
|
||||
}
|
||||
`
|
||||
|
||||
const DeleteButton = styled.button`
|
||||
${styles.button};
|
||||
${buttons.lightRed};
|
||||
`
|
||||
|
||||
const InsertButton = styled.button`
|
||||
${styles.button};
|
||||
${buttons.green};
|
||||
`
|
||||
|
||||
const LowerActionsContainer = styled.div`
|
||||
margin-top: 30px;
|
||||
`
|
||||
|
||||
const MediaLibraryActions = ({
|
||||
uploadButtonLabel,
|
||||
deleteButtonLabel,
|
||||
insertButtonLabel,
|
||||
uploadEnabled,
|
||||
deleteEnabled,
|
||||
insertEnabled,
|
||||
insertVisible,
|
||||
imagesOnly,
|
||||
onPersist,
|
||||
onDelete,
|
||||
onInsert,
|
||||
}) => (
|
||||
<ActionsContainer>
|
||||
<StyledUploadButton
|
||||
label={uploadButtonLabel}
|
||||
imagesOnly={imagesOnly}
|
||||
onChange={onPersist}
|
||||
disabled={!uploadEnabled}
|
||||
/>
|
||||
<LowerActionsContainer>
|
||||
<DeleteButton onClick={onDelete} disabled={!deleteEnabled}>
|
||||
{deleteButtonLabel}
|
||||
</DeleteButton>
|
||||
{ !insertVisible ? null :
|
||||
<InsertButton onClick={onInsert} disabled={!insertEnabled}>
|
||||
{insertButtonLabel}
|
||||
</InsertButton>
|
||||
}
|
||||
</LowerActionsContainer>
|
||||
</ActionsContainer>
|
||||
);
|
||||
|
||||
MediaLibraryActions.propTypes = {
|
||||
uploadButtonLabel: PropTypes.string.isRequired,
|
||||
deleteButtonLabel: PropTypes.string.isRequired,
|
||||
insertButtonLabel: PropTypes.string.isRequired,
|
||||
uploadEnabled: PropTypes.bool,
|
||||
deleteEnabled: PropTypes.bool,
|
||||
insertEnabled: PropTypes.bool,
|
||||
insertVisible: PropTypes.bool,
|
||||
imagesOnly: PropTypes.bool,
|
||||
onPersist: PropTypes.func.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
onInsert: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default MediaLibraryActions;
|
@ -0,0 +1,65 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from 'react-emotion';
|
||||
import { colors, borders, lengths } from 'netlify-cms-ui-default/styles';
|
||||
|
||||
const Card = styled.div`
|
||||
width: ${props => props.width};
|
||||
height: 240px;
|
||||
margin: ${props => props.margin};
|
||||
border: ${borders.textField};
|
||||
border-color: ${props => props.isSelected && colors.active};
|
||||
border-radius: ${lengths.borderRadius};
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
background-color: ${props => props.isPrivate && colors.textFieldBorder};
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
`
|
||||
|
||||
const CardImage = styled.img`
|
||||
width: 100%;
|
||||
height: 160px;
|
||||
object-fit: cover;
|
||||
border-radius: 2px 2px 0 0;
|
||||
`
|
||||
|
||||
const CardImagePlaceholder = CardImage.withComponent(`div`);
|
||||
|
||||
const CardText = styled.p`
|
||||
color: ${colors.text};
|
||||
padding: 8px;
|
||||
margin-top: 20px;
|
||||
overflow-wrap: break-word;
|
||||
line-height: 1.3 !important;
|
||||
`
|
||||
|
||||
const MediaLibraryCard = ({ isSelected, imageUrl, text, onClick, width, margin, isPrivate }) => (
|
||||
<Card
|
||||
isSelected={isSelected}
|
||||
onClick={onClick}
|
||||
width={width}
|
||||
margin={margin}
|
||||
tabIndex="-1"
|
||||
isPrivate={isPrivate}
|
||||
>
|
||||
<div>
|
||||
{ imageUrl ? <CardImage src={imageUrl}/> : <CardImagePlaceholder/> }
|
||||
</div>
|
||||
<CardText>{text}</CardText>
|
||||
</Card>
|
||||
);
|
||||
|
||||
MediaLibraryCard.propTypes = {
|
||||
isSelected: PropTypes.bool,
|
||||
imageUrl: PropTypes.string,
|
||||
text: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
width: PropTypes.string.isRequired,
|
||||
margin: PropTypes.string.isRequired,
|
||||
isPrivate: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default MediaLibraryCard;
|
@ -0,0 +1,81 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from 'react-emotion'
|
||||
import MediaLibraryCard from './MediaLibraryCard';
|
||||
import { colors } from 'netlify-cms-ui-default/styles';
|
||||
|
||||
const CardGridContainer = styled.div`
|
||||
overflow-y: auto;
|
||||
`
|
||||
|
||||
const CardGrid = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
margin-left: -10px;
|
||||
margin-right: -10px;
|
||||
`
|
||||
|
||||
const PaginatingMessage = styled.h1`
|
||||
color: ${props => props.isPrivate && colors.textFieldBorder};
|
||||
`
|
||||
|
||||
const MediaLibraryCardGrid = ({
|
||||
setScrollContainerRef,
|
||||
mediaItems,
|
||||
isSelectedFile,
|
||||
onAssetClick,
|
||||
canLoadMore,
|
||||
onLoadMore,
|
||||
isPaginating,
|
||||
paginatingMessage,
|
||||
cardWidth,
|
||||
cardMargin,
|
||||
isPrivate,
|
||||
}) => (
|
||||
<CardGridContainer innerRef={setScrollContainerRef}>
|
||||
<CardGrid>
|
||||
{
|
||||
mediaItems.map((file, idx) =>
|
||||
<MediaLibraryCard
|
||||
key={file.key}
|
||||
isSelected={isSelectedFile(file)}
|
||||
imageUrl={file.isViewableImage && file.url}
|
||||
text={file.name}
|
||||
onClick={() => onAssetClick(file)}
|
||||
width={cardWidth}
|
||||
margin={cardMargin}
|
||||
isPrivate={isPrivate}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{!canLoadMore ? null : <Waypoint onEnter={onLoadMore}/>}
|
||||
</CardGrid>
|
||||
{
|
||||
!isPaginating ? null :
|
||||
<PaginatingMessage isPrivate={isPrivate}>{paginatingMessage}</PaginatingMessage>
|
||||
}
|
||||
|
||||
</CardGridContainer>
|
||||
);
|
||||
|
||||
MediaLibraryCardGrid.propTypes = {
|
||||
setScrollContainerRef: PropTypes.func.isRequired,
|
||||
mediaItems: PropTypes.arrayOf(PropTypes.shape({
|
||||
key: PropTypes.string.isRequired,
|
||||
isViewableImage: PropTypes.bool,
|
||||
url: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
})).isRequired,
|
||||
isSelectedFile: PropTypes.func.isRequired,
|
||||
onAssetClick: PropTypes.func.isRequired,
|
||||
canLoadMore: PropTypes.bool,
|
||||
onLoadMore: PropTypes.func.isRequired,
|
||||
isPaginating: PropTypes.bool,
|
||||
paginatingMessage: PropTypes.string,
|
||||
cardWidth: PropTypes.string.isRequired,
|
||||
cardMargin: PropTypes.string.isRequired,
|
||||
isPrivate: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default MediaLibraryCardGrid;
|
@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from 'react-emotion';
|
||||
import Icon from 'netlify-cms-ui-default/Icon';
|
||||
import { shadows, colors } from 'netlify-cms-ui-default/styles';
|
||||
|
||||
const CloseButton = styled.button`
|
||||
${shadows.dropMiddle};
|
||||
position: absolute;
|
||||
margin-right: -40px;
|
||||
left: -40px;
|
||||
top: -40px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background-color: white;
|
||||
padding: 0;
|
||||
`
|
||||
|
||||
const LibraryTitle = styled.h1`
|
||||
line-height: 36px;
|
||||
font-size: 22px;
|
||||
text-align: left;
|
||||
margin-bottom: 25px;
|
||||
color: ${props => props.isPrivate && colors.textFieldBorder};
|
||||
`
|
||||
|
||||
const MediaLibraryHeader = ({ onClose, title, isPrivate }) => (
|
||||
<div>
|
||||
<CloseButton onClick={onClose}>
|
||||
<Icon type="close"/>
|
||||
</CloseButton>
|
||||
<LibraryTitle isPrivate={isPrivate}>{title}</LibraryTitle>
|
||||
</div>
|
||||
);
|
||||
|
||||
MediaLibraryHeader.propTypes = {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
isPrivate: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default MediaLibraryHeader;
|
@ -0,0 +1,202 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled, { css } from 'react-emotion';
|
||||
import { isEmpty } from 'lodash';
|
||||
import Waypoint from 'react-waypoint';
|
||||
import { Modal } from 'UI';
|
||||
import MediaLibrarySearch from './MediaLibrarySearch';
|
||||
import MediaLibraryHeader from './MediaLibraryHeader';
|
||||
import MediaLibraryActions from './MediaLibraryActions';
|
||||
import MediaLibraryCardGrid from './MediaLibraryCardGrid';
|
||||
import EmptyMessage from './EmptyMessage';
|
||||
import { buttons, shadows, colors, borders, lengths } from 'netlify-cms-ui-default/styles';
|
||||
|
||||
/**
|
||||
* Responsive styling needs to be overhauled. Current setup requires specifying
|
||||
* widths per breakpoint.
|
||||
*/
|
||||
const cardWidth = `280px`;
|
||||
const cardMargin = `10px`;
|
||||
|
||||
/**
|
||||
* cardWidth + cardMargin * 2 = cardOutsideWidth
|
||||
* (not using calc because this will be nested in other calcs)
|
||||
*/
|
||||
const cardOutsideWidth = `300px`;
|
||||
|
||||
const LibraryTop = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`
|
||||
|
||||
const StyledModal = styled(Modal)`
|
||||
display: grid;
|
||||
grid-template-rows: 120px auto;
|
||||
width: calc(${cardOutsideWidth} + 20px);
|
||||
background-color: ${props => props.isPrivate && colors.grayDark};
|
||||
|
||||
@media (min-width: 800px) {
|
||||
width: calc(${cardOutsideWidth} * 2 + 20px);
|
||||
}
|
||||
|
||||
@media (min-width: 1120px) {
|
||||
width: calc(${cardOutsideWidth} * 3 + 20px);
|
||||
}
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
width: calc(${cardOutsideWidth} * 4 + 20px);
|
||||
}
|
||||
|
||||
@media (min-width: 1760px) {
|
||||
width: calc(${cardOutsideWidth} * 5 + 20px);
|
||||
}
|
||||
|
||||
@media (min-width: 2080px) {
|
||||
width: calc(${cardOutsideWidth} * 6 + 20px);
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: ${props => props.isPrivate && colors.textFieldBorder};
|
||||
}
|
||||
|
||||
button:disabled,
|
||||
label[disabled] {
|
||||
background-color: ${props => props.isPrivate && `rgba(217, 217, 217, 0.15)`};
|
||||
}
|
||||
`
|
||||
|
||||
const MediaLibraryModal = ({
|
||||
isVisible,
|
||||
canInsert,
|
||||
files,
|
||||
dynamicSearch,
|
||||
dynamicSearchActive,
|
||||
forImage,
|
||||
isLoading,
|
||||
isPersisting,
|
||||
isDeleting,
|
||||
hasNextPage,
|
||||
page,
|
||||
isPaginating,
|
||||
privateUpload,
|
||||
query,
|
||||
selectedFile,
|
||||
handleFilter,
|
||||
handleQuery,
|
||||
toTableData,
|
||||
handleClose,
|
||||
handleSearchChange,
|
||||
handleSearchKeyDown,
|
||||
handlePersist,
|
||||
handleDelete,
|
||||
handleInsert,
|
||||
setScrollContainerRef,
|
||||
handleAssetClick,
|
||||
handleLoadMore,
|
||||
}) => {
|
||||
const filteredFiles = forImage ? handleFilter(files) : files;
|
||||
const queriedFiles = (!dynamicSearch && query) ? handleQuery(query, filteredFiles) : filteredFiles;
|
||||
const tableData = toTableData(queriedFiles);
|
||||
const hasFiles = files && !!files.length;
|
||||
const hasFilteredFiles = filteredFiles && !!filteredFiles.length;
|
||||
const hasSearchResults = queriedFiles && !!queriedFiles.length;
|
||||
const hasMedia = hasSearchResults;
|
||||
const shouldShowEmptyMessage = !hasMedia;
|
||||
const emptyMessage = (isLoading && !hasMedia && 'Loading...')
|
||||
|| (dynamicSearchActive && 'No results.')
|
||||
|| (!hasFiles && 'No assets found.')
|
||||
|| (!hasFilteredFiles && 'No images found.')
|
||||
|| (!hasSearchResults && 'No results.');
|
||||
const hasSelection = hasMedia && !isEmpty(selectedFile);
|
||||
const shouldShowButtonLoader = isPersisting || isDeleting;
|
||||
|
||||
return (
|
||||
<StyledModal isOpen={isVisible} onClose={handleClose} isPrivate={privateUpload}>
|
||||
<LibraryTop>
|
||||
<div>
|
||||
<MediaLibraryHeader
|
||||
onClose={handleClose}
|
||||
title={`${privateUpload ? 'Private ' : ''}${forImage ? 'Images' : 'Media assets'}`}
|
||||
isPrivate={privateUpload}
|
||||
/>
|
||||
<MediaLibrarySearch
|
||||
value={query}
|
||||
onChange={handleSearchChange}
|
||||
onKeyDown={handleSearchKeyDown}
|
||||
placeholder="Search..."
|
||||
disabled={!dynamicSearchActive && !hasFilteredFiles}
|
||||
/>
|
||||
</div>
|
||||
<MediaLibraryActions
|
||||
uploadButtonLabel={isPersisting ? 'Uploading...' : 'Upload new'}
|
||||
deleteButtonLabel={isDeleting ? 'Deleting...' : 'Delete selected'}
|
||||
insertButtonLabel="Choose selected"
|
||||
uploadEnabled={!shouldShowButtonLoader}
|
||||
deleteEnabled={!shouldShowButtonLoader && hasSelection}
|
||||
insertEnabled={hasSelection}
|
||||
insertVisible={canInsert}
|
||||
imagesOnly={forImage}
|
||||
onPersist={handlePersist}
|
||||
onDelete={handleDelete}
|
||||
onInsert={handleInsert}
|
||||
/>
|
||||
</LibraryTop>
|
||||
{ !shouldShowEmptyMessage ? null : <EmptyMessage content={emptyMessage} isPrivate={privateUpload}/> }
|
||||
<MediaLibraryCardGrid
|
||||
setScrollContainerRef={setScrollContainerRef}
|
||||
mediaItems={tableData}
|
||||
isSelectedFile={file => selectedFile.key === file.key}
|
||||
onAssetClick={handleAssetClick}
|
||||
canLoadMore={hasNextPage}
|
||||
onLoadMore={handleLoadMore}
|
||||
isPaginating={isPaginating}
|
||||
paginatingMessage="Loading..."
|
||||
cardWidth={cardWidth}
|
||||
cardMargin={cardMargin}
|
||||
isPrivate={privateUpload}
|
||||
/>
|
||||
</StyledModal>
|
||||
);
|
||||
}
|
||||
|
||||
const fileShape = {
|
||||
key: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
size: PropTypes.number.isRequired,
|
||||
queryOrder: PropTypes.number,
|
||||
url: PropTypes.string.isRequired,
|
||||
urlIsPublicPath: PropTypes.bool,
|
||||
};
|
||||
|
||||
MediaLibraryModal.propTypes = {
|
||||
isVisible: PropTypes.bool,
|
||||
canInsert: PropTypes.bool,
|
||||
files: PropTypes.arrayOf(PropTypes.shape(fileShape)).isRequired,
|
||||
dynamicSearch: PropTypes.bool,
|
||||
dynamicSearchActive: PropTypes.bool,
|
||||
forImage: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
isPersisting: PropTypes.bool,
|
||||
isDeleting: PropTypes.bool,
|
||||
hasNextPage: PropTypes.bool,
|
||||
page: PropTypes.number,
|
||||
isPaginating: PropTypes.bool,
|
||||
privateUpload: PropTypes.bool,
|
||||
query: PropTypes.string,
|
||||
selectedFile: PropTypes.oneOfType([PropTypes.shape(fileShape), PropTypes.shape({})]),
|
||||
handleFilter: PropTypes.func.isRequired,
|
||||
handleQuery: PropTypes.func.isRequired,
|
||||
toTableData: PropTypes.func.isRequired,
|
||||
handleClose: PropTypes.func.isRequired,
|
||||
handleSearchChange: PropTypes.func.isRequired,
|
||||
handleSearchKeyDown: PropTypes.func.isRequired,
|
||||
handlePersist: PropTypes.func.isRequired,
|
||||
handleDelete: PropTypes.func.isRequired,
|
||||
handleInsert: PropTypes.func.isRequired,
|
||||
setScrollContainerRef: PropTypes.func.isRequired,
|
||||
handleAssetClick: PropTypes.func.isRequired,
|
||||
handleLoadMore: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default MediaLibraryModal;
|
@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from 'react-emotion';
|
||||
import Icon from 'netlify-cms-ui-default/Icon';
|
||||
import { lengths, colors } from 'netlify-cms-ui-default/styles';
|
||||
|
||||
const SearchContainer = styled.div`
|
||||
height: 37px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
width: 400px;
|
||||
`
|
||||
|
||||
const SearchInput = styled.input`
|
||||
background-color: #eff0f4;
|
||||
border-radius: ${lengths.borderRadius};
|
||||
|
||||
font-size: 14px;
|
||||
padding: 10px 6px 10px 32px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: inset 0 0 0 2px ${colors.active};
|
||||
}
|
||||
`
|
||||
|
||||
const SearchIcon = styled(Icon)`
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 6px;
|
||||
z-index: 2;
|
||||
transform: translate(0, -50%);
|
||||
`
|
||||
|
||||
const MediaLibrarySearch = ({ value, onChange, onKeyDown, placeholder, disabled }) => (
|
||||
<SearchContainer>
|
||||
<SearchIcon type="search" size="small"/>
|
||||
<SearchInput
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</SearchContainer>
|
||||
);
|
||||
|
||||
MediaLibrarySearch.propTypes = {
|
||||
value: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onKeyDown: PropTypes.func.isRequired,
|
||||
placeholder: PropTypes.string.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default MediaLibrarySearch;
|
@ -1,11 +1,22 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { css } from 'react-emotion';
|
||||
import { colors } from 'netlify-cms-ui-default/styles';
|
||||
|
||||
const DefaultErrorComponent = () => {
|
||||
};
|
||||
|
||||
const ISSUE_URL = "https://github.com/netlify/netlify-cms/issues/new";
|
||||
|
||||
const styles = {
|
||||
errorBoundary: css`
|
||||
padding: 0 20px;
|
||||
`,
|
||||
errorText: css`
|
||||
color: ${colors.errorText};
|
||||
`,
|
||||
};
|
||||
|
||||
export class ErrorBoundary extends React.Component {
|
||||
state = {
|
||||
hasError: false,
|
||||
@ -23,11 +34,11 @@ export class ErrorBoundary extends React.Component {
|
||||
return this.props.children;
|
||||
}
|
||||
return (
|
||||
<div className="nc-errorBoundary">
|
||||
<h1 className="nc-errorBoundary-heading">Sorry!</h1>
|
||||
<div className={styles.errorBoundary}>
|
||||
<h1 className={styles.errorBoundaryText}>Sorry!</h1>
|
||||
<p>
|
||||
<span>There's been an error - please </span>
|
||||
<a href={ISSUE_URL} target="_blank" className="nc-errorBoundary-link">report it</a>!
|
||||
<a href={ISSUE_URL} target="_blank" className={styles.errorBoundaryText}>report it</a>!
|
||||
</p>
|
||||
<p>{errorMessage}</p>
|
||||
</div>
|
@ -1,8 +0,0 @@
|
||||
.nc-errorBoundary {
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.nc-errorBoundary-heading,
|
||||
.nc-errorBoundary-link {
|
||||
color: var(--colorErrorText);
|
||||
}
|
86
packages/netlify-cms-core/src/components/UI/Modal.js
Normal file
86
packages/netlify-cms-core/src/components/UI/Modal.js
Normal file
@ -0,0 +1,86 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { css, cx, injectGlobal } from 'react-emotion';
|
||||
import ReactModal from 'react-modal';
|
||||
import { transitions, shadows, lengths } from 'netlify-cms-ui-default/styles';
|
||||
|
||||
injectGlobal`
|
||||
.ReactModal__Body--open {
|
||||
overflow: hidden;
|
||||
}
|
||||
`;
|
||||
|
||||
const styles = {
|
||||
modalBody: css`
|
||||
${shadows.dropDeep};
|
||||
background-color: #fff;
|
||||
border-radius: ${lengths.borderRadius};
|
||||
height: 80%;
|
||||
text-align: center;
|
||||
max-width: 2200px;
|
||||
padding: 20px;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
`,
|
||||
overlay: css`
|
||||
z-index: 99999;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
opacity: 0;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
transition: background-color ${transitions.main}, opacity ${transitions.main};
|
||||
`,
|
||||
overlayAfterOpen: css`
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
opacity: 1;
|
||||
`,
|
||||
overlayBeforeClose: css`
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
opacity: 0;
|
||||
`,
|
||||
}
|
||||
|
||||
|
||||
export class Modal extends React.Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
className: PropTypes.string,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
ReactModal.setAppElement('#nc-root');
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isOpen, children, className, onClose } = this.props;
|
||||
return (
|
||||
<ReactModal
|
||||
isOpen={isOpen}
|
||||
onRequestClose={onClose}
|
||||
closeTimeoutMS={300}
|
||||
className={{
|
||||
base: cx(styles.modalBody, className),
|
||||
afterOpen: '',
|
||||
beforeClose: '',
|
||||
}}
|
||||
overlayClassName={{
|
||||
base: styles.overlay,
|
||||
afterOpen: styles.overlayAfterOpen,
|
||||
beforeClose: styles.overlayBeforeClose,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ReactModal>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
.nc-modal-overlay {
|
||||
z-index: 99999;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
opacity: 0;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
transition: background-color var(--transition), opacity var(--transition);
|
||||
}
|
||||
|
||||
.nc-modal-overlay-afterOpen {
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.nc-modal-overlay-beforeClose {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.nc-modal-body {
|
||||
@apply(--dropShadowDeep);
|
||||
background-color: #fff;
|
||||
border-radius: var(--borderRadius);
|
||||
height: 80%;
|
||||
text-align: center;
|
||||
max-width: 2200px;
|
||||
padding: 20px;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.nc-dialog-body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.nc-dialog-contentWrapper {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.nc-dialog-footer {
|
||||
margin: 24px 0;
|
||||
width: calc(100% - 48px);
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.ReactModal__Body--open {
|
||||
overflow: hidden;
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ReactModal from 'react-modal';
|
||||
|
||||
export class Modal extends React.Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
className: PropTypes.string,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
ReactModal.setAppElement('#nc-root');
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isOpen, children, className, onClose } = this.props;
|
||||
return (
|
||||
<ReactModal
|
||||
isOpen={isOpen}
|
||||
onRequestClose={onClose}
|
||||
closeTimeoutMS={300}
|
||||
className={{
|
||||
base: `nc-modal-body ${className || ''}`,
|
||||
afterOpen: 'nc-modal-body-opening',
|
||||
beforeClose: '',
|
||||
}}
|
||||
overlayClassName={{
|
||||
base: 'nc-modal-overlay',
|
||||
afterOpen: 'nc-modal-overlay-afterOpen',
|
||||
beforeClose: 'nc-modal-overlay-beforeClose',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ReactModal>
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import styled, { css } from 'react-emotion';
|
||||
import Dropdown, { DropdownItem, DropdownButton } from 'netlify-cms-ui-default/Dropdown';
|
||||
import Icon from 'netlify-cms-ui-default/Icon';
|
||||
import { colors } from 'netlify-cms-ui-default/styles';
|
||||
import { stripProtocol } from 'Lib/urlHelper';
|
||||
|
||||
const styles = {
|
||||
avatarImage: css`
|
||||
width: 32px;
|
||||
border-radius: 32px;
|
||||
`,
|
||||
};
|
||||
|
||||
const AppHeaderAvatar = styled.button`
|
||||
border: 0;
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
color: #1e2532;
|
||||
background-color: transparent;
|
||||
`
|
||||
|
||||
const AvatarImage = styled.img`
|
||||
${styles.avatarImage};
|
||||
`
|
||||
|
||||
const AvatarPlaceholderIcon = styled(Icon)`
|
||||
${styles.avatarImage};
|
||||
height: 32px;
|
||||
color: #1e2532;
|
||||
background-color: ${colors.textFieldBorder};
|
||||
`
|
||||
|
||||
const AppHeaderSiteLink = styled.a`
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #7b8290;
|
||||
padding: 10px 16px;
|
||||
`
|
||||
|
||||
const Avatar = ({ imageUrl }) => (
|
||||
<AppHeaderAvatar>
|
||||
{imageUrl ? <AvatarImage src={imageUrl}/> : <AvatarPlaceholderIcon type="user" size="large"/>}
|
||||
</AppHeaderAvatar>
|
||||
);
|
||||
|
||||
const SettingsDropdown = ({ displayUrl, imageUrl, onLogoutClick }) => (
|
||||
<React.Fragment>
|
||||
{
|
||||
displayUrl
|
||||
? <AppHeaderSiteLink href={displayUrl} target="_blank">
|
||||
{stripProtocol(displayUrl)}
|
||||
</AppHeaderSiteLink>
|
||||
: null
|
||||
}
|
||||
<Dropdown
|
||||
dropdownTopOverlap="50px"
|
||||
dropdownWidth="100px"
|
||||
dropdownPosition="right"
|
||||
renderButton={() => (
|
||||
<DropdownButton>
|
||||
<Avatar imageUrl={imageUrl}/>
|
||||
</DropdownButton>
|
||||
)}
|
||||
>
|
||||
<DropdownItem label="Log Out" onClick={onLogoutClick}/>
|
||||
</Dropdown>
|
||||
</React.Fragment>
|
||||
)
|
||||
|
||||
export default SettingsDropdown;
|
49
packages/netlify-cms-core/src/components/UI/Toast.js
Normal file
49
packages/netlify-cms-core/src/components/UI/Toast.js
Normal file
@ -0,0 +1,49 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { css, injectGlobal, cx } from 'react-emotion';
|
||||
import 'redux-notifications/lib/styles.css'; // Import default redux-notifications styles into global scope.
|
||||
import { shadows, colors, lengths } from 'netlify-cms-ui-default/styles';
|
||||
|
||||
injectGlobal`
|
||||
.notif__container {
|
||||
z-index: 10000;
|
||||
}
|
||||
`;
|
||||
|
||||
const styles = {
|
||||
toast: css`
|
||||
${shadows.drop};
|
||||
background-color: ${colors.background};
|
||||
color: ${colors.textLight};
|
||||
border-radius: ${lengths.borderRadius};
|
||||
margin: 10px;
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
`,
|
||||
info: css`
|
||||
background-color: ${colors.infoBackground};
|
||||
color: ${colors.infoText};
|
||||
`,
|
||||
success: css`
|
||||
background-color: ${colors.successBackground};
|
||||
color: ${colors.successText};
|
||||
`,
|
||||
warning: css`
|
||||
background-color: ${colors.warnBackground};
|
||||
color: ${colors.warnText};
|
||||
`,
|
||||
danger: css`
|
||||
background-color: ${colors.errorBackground};
|
||||
color: ${colors.errorText};
|
||||
`,
|
||||
};
|
||||
|
||||
export const Toast = ({ kind, message }) =>
|
||||
<div className={cx(styles.toast, styles[kind])}>
|
||||
{message}
|
||||
</div>;
|
||||
|
||||
Toast.propTypes = {
|
||||
kind: PropTypes.oneOf(['info', 'success', 'warning', 'danger']).isRequired,
|
||||
message: PropTypes.string,
|
||||
};
|
@ -1,34 +0,0 @@
|
||||
/* redux-notifications override */
|
||||
.notif__container {
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.nc-toast {
|
||||
@apply(--dropShadow);
|
||||
background-color: var(--colorBackground);
|
||||
color: var(--colorTextLight);
|
||||
border-radius: var(--borderRadius);
|
||||
margin: 10px;
|
||||
padding: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nc-toast-info {
|
||||
background-color: var(--colorInfoBackground);
|
||||
color: var(--colorInfoText);
|
||||
}
|
||||
|
||||
.nc-toast-success {
|
||||
background-color: var(--colorSuccessBackground);
|
||||
color: var(--colorSuccessText);
|
||||
}
|
||||
|
||||
.nc-toast-warning {
|
||||
background-color: var(--colorWarnBackground);
|
||||
color: var(--colorWarnText);
|
||||
}
|
||||
|
||||
.nc-toast-danger {
|
||||
background-color: var(--colorErrorBackground);
|
||||
color: var(--colorErrorText);
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import 'redux-notifications/lib/styles.css'; // Import default redux-notifications styles into global scope.
|
||||
|
||||
export const Toast = ({ kind, message }) =>
|
||||
<div className={`nc-toast nc-toast-${ kind }`}>
|
||||
{message}
|
||||
</div>;
|
||||
|
||||
Toast.propTypes = {
|
||||
kind: PropTypes.oneOf(['info', 'success', 'warning', 'danger']).isRequired,
|
||||
message: PropTypes.string,
|
||||
};
|
@ -1,3 +0,0 @@
|
||||
@import "./Toast/Toast.css";
|
||||
@import "./Modal/Modal.css";
|
||||
@import "./ErrorBoundary/ErrorBoundary.css";
|
@ -1,337 +0,0 @@
|
||||
:root {
|
||||
/**
|
||||
* Font Stacks
|
||||
*/
|
||||
--fontFamilyPrimary:
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
Roboto,
|
||||
Helvetica,
|
||||
Arial,
|
||||
sans-serif,
|
||||
"Apple Color Emoji",
|
||||
"Segoe UI Emoji",
|
||||
"Segoe UI Symbol";
|
||||
|
||||
--fontFamilyMono:
|
||||
'SFMono-Regular',
|
||||
Consolas,
|
||||
"Liberation Mono",
|
||||
Menlo,
|
||||
Courier,
|
||||
monospace;
|
||||
|
||||
/**
|
||||
* Theme Colors
|
||||
*/
|
||||
--colorWhite: #fff;
|
||||
--colorGrayLight: #eff0f4;
|
||||
--colorGray: #798291;
|
||||
--colorGrayDark: #313d3e;
|
||||
--colorBlue: #3a69c7;
|
||||
--colorBlueLight: #e8f5fe;
|
||||
--colorGreen: #005614;
|
||||
--colorGreenLight: #caef6f;
|
||||
--colorBrown: #754e00;
|
||||
--colorYellow: #ffee9c;
|
||||
--colorRed: #ff003b;
|
||||
--colorRedLight: #fcefea;
|
||||
--colorPurple: #70399f;
|
||||
--colorPurpleLight: #f6d8ff;
|
||||
--colorTeal: #17a2b8;
|
||||
--colorTealLight: #ddf5f9;
|
||||
|
||||
--colorStatusDraftText: var(--colorPurple);
|
||||
--colorStatusDraftBackground: var(--colorPurpleLight);
|
||||
--colorStatusReviewText: var(--colorBrown);
|
||||
--colorStatusReviewBackground: var(--colorYellow);
|
||||
--colorStatusReadyText: var(--colorGreen);
|
||||
--colorStatusReadyBackground: var(--colorGreenLight);
|
||||
|
||||
--colorText: var(--colorGray);
|
||||
--colorTextLight: var(--colorWhite);
|
||||
--colorTextLead: var(--colorGrayDark);
|
||||
--colorBackground: var(--colorGrayLight);
|
||||
--colorForeground: var(--colorWhite);
|
||||
--colorActive: var(--colorBlue);
|
||||
--colorActiveBackground: var(--colorBlueLight);
|
||||
--colorInactive: var(--colorGray);
|
||||
--colorButton: var(--colorGray);
|
||||
--colorButtonText: var(--colorWhite);
|
||||
--colorInputBackground: var(--colorWhite);
|
||||
|
||||
--colorInfoText: var(--colorBlue);
|
||||
--colorInfoBackground: var(--colorBlueLight);
|
||||
--colorSuccessText: var(--colorGreen);
|
||||
--colorSuccessBackground: var(--colorGreenLight);
|
||||
--colorWarnText: var(--colorBrown);
|
||||
--colorWarnBackground: var(--colorYellow);
|
||||
--colorErrorText: var(--colorRed);
|
||||
--colorErrorBackground: var(--colorRedLight);
|
||||
|
||||
--textFieldBorderColor: #dfdfe3;
|
||||
--controlLabelColor: #7a8291;
|
||||
|
||||
--topBarHeight: 56px;
|
||||
--transition: .2s ease;
|
||||
--inputPadding: 16px 20px;
|
||||
--borderRadius: 5px;
|
||||
--richTextEditorMinHeight: 300px;
|
||||
--borderWidth: 2px;
|
||||
--textFieldBorder: solid var(--borderWidth) var(--textFieldBorderColor);
|
||||
--topCardWidth: 682px;
|
||||
--pageMargin: 84px 18px;
|
||||
|
||||
--dropShadow: {
|
||||
box-shadow: 0 2px 4px 0 rgba(19, 39, 48, .12);
|
||||
}
|
||||
|
||||
--dropShadowMain: {
|
||||
box-shadow: 0 2px 6px 0 rgba(68, 74, 87, 0.05),
|
||||
0 1px 3px 0 rgba(68, 74, 87, 0.10);
|
||||
}
|
||||
|
||||
--dropShadowMiddle: {
|
||||
box-shadow: 0 2px 6px 0 rgba(68, 74, 87, 0.15),
|
||||
0 1px 3px 0 rgba(68, 74, 87, 0.30);
|
||||
}
|
||||
|
||||
--dropShadowDeep: {
|
||||
box-shadow: 0 4px 12px 0 rgba(68, 74, 87, 0.15),
|
||||
0 1px 3px 0 rgba(68, 74, 87, 0.25);
|
||||
}
|
||||
|
||||
--caretDown: {
|
||||
color: #fff;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-top: 6px solid currentColor;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
--card: {
|
||||
@apply(--dropShadowMain);
|
||||
border-radius: 5px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
--button: {
|
||||
border: 0;
|
||||
border-radius: var(--borderRadius);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
--buttonDefault {
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
font-weight: 500;
|
||||
padding: 0 15px;
|
||||
background-color: var(--colorGray);
|
||||
color: var(--colorWhite);
|
||||
}
|
||||
|
||||
--buttonMedium {
|
||||
height: 27px;
|
||||
line-height: 27px;
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
|
||||
border-radius: 3px;
|
||||
padding: 0 24px 0 14px;
|
||||
}
|
||||
|
||||
--buttonSmall {
|
||||
height: 23px;
|
||||
line-height: 23px;
|
||||
}
|
||||
|
||||
--buttonGray {
|
||||
background-color: var(--colorButton);
|
||||
color: var(--colorButtonText);
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
color: white;
|
||||
background-color: #555a65;
|
||||
}
|
||||
}
|
||||
|
||||
--buttonGreen {
|
||||
background-color: #AAE31F;
|
||||
color: #005614;
|
||||
}
|
||||
|
||||
--buttonLightRed {
|
||||
background-color: #FCEFEA;
|
||||
color: #FF003B;
|
||||
}
|
||||
|
||||
--buttonLightBlue {
|
||||
background-color: #E8F5FE;
|
||||
color: #3A69C7;
|
||||
}
|
||||
|
||||
--buttonLightTeal {
|
||||
background-color: #DDF5F9;
|
||||
color: #1195AA;
|
||||
}
|
||||
|
||||
--buttonTeal {
|
||||
background-color: #17A2B8;
|
||||
color: white;
|
||||
}
|
||||
|
||||
--buttonDisabled {
|
||||
background-color: var(--colorGrayLight);
|
||||
color: var(--colorGray);
|
||||
}
|
||||
|
||||
--textBadge: {
|
||||
color: #3a69c7;
|
||||
font-size: 13px;
|
||||
background-color: #e8f5fe;
|
||||
border-radius: var(--borderRadius);
|
||||
padding: 4px 10px;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
--textBadgeSuccess: {
|
||||
@apply(--textBadge);
|
||||
color: #005614;
|
||||
background-color: #caef6f;
|
||||
}
|
||||
|
||||
--textBadgeDanger: {
|
||||
@apply(--textBadge);
|
||||
color: #ff003b;
|
||||
background-color: #fbe0d7;
|
||||
}
|
||||
|
||||
--loaderSize: {
|
||||
width: 2.28571429rem;
|
||||
height: 2.28571429rem;
|
||||
}
|
||||
|
||||
--cardTop: {
|
||||
@apply(--card);
|
||||
width: var(--topCardWidth);
|
||||
max-width: 100%;
|
||||
padding: 18px 20px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
--cardTopHeading: {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
line-height: 37px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
--cardTopDescription: {
|
||||
max-width: 480px;
|
||||
color: var(--colorText);
|
||||
font-size: 14px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
*, *:before, *:after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:focus {
|
||||
outline: -webkit-focus-ring-color auto 5px;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't show outlines if the user is utilizing mouse rather than keyboard.
|
||||
*/
|
||||
[data-whatintent="mouse"] *:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
|
||||
input {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--fontFamilyPrimary);
|
||||
font-weight: normal;
|
||||
background-color: var(--colorBackground);
|
||||
color: var(--colorText);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6, p {
|
||||
font-family: var(--fontFamilyPrimary);
|
||||
color: var(--colorTextLead);
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
letter-spacing: 0.4px;
|
||||
color: var(--colorTextLead);
|
||||
}
|
||||
|
||||
a,
|
||||
button {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--colorText);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
button {
|
||||
@apply(--button);
|
||||
|
||||
&.LightBlue {
|
||||
@apply(--buttonLightBlue);
|
||||
}
|
||||
|
||||
&.Teal {
|
||||
@apply(--buttonTeal);
|
||||
}
|
||||
|
||||
&.LightTeal {
|
||||
@apply(--buttonLightTeal);
|
||||
}
|
||||
|
||||
&.LightRed {
|
||||
@apply(--buttonLightRed);
|
||||
}
|
||||
|
||||
&.Green {
|
||||
@apply(--buttonGreen);
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: none;
|
||||
}
|
@ -1,12 +1,5 @@
|
||||
export { DragSource, DropTarget, HTML5DragDrop } from './DragDrop/DragDrop';
|
||||
export { ErrorBoundary } from './ErrorBoundary/ErrorBoundary';
|
||||
export { FileUploadButton } from './FileUploadButton/FileUploadButton';
|
||||
export { Modal } from './Modal/Modal';
|
||||
export { Toast } from './Toast/Toast';
|
||||
|
||||
/**
|
||||
* Utility for determining whether keyboard or mouse is in use. Sets an attribute
|
||||
* on the body that enables related styling.
|
||||
*/
|
||||
import 'what-input';
|
||||
|
||||
export { DragSource, DropTarget, HTML5DragDrop } from './DragDrop';
|
||||
export { ErrorBoundary } from './ErrorBoundary';
|
||||
export { FileUploadButton } from './FileUploadButton';
|
||||
export { Modal } from './Modal';
|
||||
export { Toast } from './Toast';
|
||||
|
@ -1,28 +0,0 @@
|
||||
@import "./WorkflowList.css";
|
||||
@import "./WorkflowCard.css";
|
||||
|
||||
.nc-workflow {
|
||||
padding: var(--pageMargin) 0;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.nc-workflow-top {
|
||||
@apply(--cardTop);
|
||||
}
|
||||
|
||||
.nc-workflow-top-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
& span[role="button"] {
|
||||
@apply(--dropShadowDeep);
|
||||
}
|
||||
}
|
||||
|
||||
.nc-workflow-top-heading {
|
||||
@apply(--cardTopHeading);
|
||||
}
|
||||
|
||||
.nc-workflow-top-description {
|
||||
@apply(--cardTopDescription);
|
||||
}
|
@ -1,8 +1,12 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import styled from 'react-emotion';
|
||||
import { OrderedMap } from 'immutable';
|
||||
import { connect } from 'react-redux';
|
||||
import Dropdown, { DropdownItem, StyledDropdownButton } from 'netlify-cms-ui-default/Dropdown';
|
||||
import Loader from 'netlify-cms-ui-default/Loader';
|
||||
import { lengths, components, shadows } from 'netlify-cms-ui-default/styles';
|
||||
import { createNewEntry } from 'Actions/collections';
|
||||
import {
|
||||
loadUnpublishedEntries,
|
||||
@ -12,9 +16,34 @@ import {
|
||||
} from 'Actions/editorialWorkflow';
|
||||
import { selectUnpublishedEntriesByStatus } from 'Reducers';
|
||||
import { EDITORIAL_WORKFLOW, status } from 'Constants/publishModes';
|
||||
import { Loader, Dropdown, DropdownItem } from 'netlify-cms-ui-default';
|
||||
import WorkflowList from './WorkflowList';
|
||||
|
||||
const WorkflowContainer = styled.div`
|
||||
padding: ${lengths.pageMargin} 0;
|
||||
height: 100vh;
|
||||
`
|
||||
|
||||
const WorkflowTop = styled.div`
|
||||
${components.cardTop};
|
||||
`
|
||||
|
||||
const WorkflowTopRow = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
span[role="button"] {
|
||||
${shadows.dropDeep};
|
||||
}
|
||||
`
|
||||
|
||||
const WorkflowTopHeading = styled.h1`
|
||||
${components.cardTopHeading};
|
||||
`
|
||||
|
||||
const WorkflowTopDescription = styled.p`
|
||||
${components.cardTopDescription};
|
||||
`
|
||||
|
||||
class Workflow extends Component {
|
||||
static propTypes = {
|
||||
collections: ImmutablePropTypes.orderedMap,
|
||||
@ -51,15 +80,15 @@ class Workflow extends Component {
|
||||
const readyCount = unpublishedEntries.get('pending_publish').size;
|
||||
|
||||
return (
|
||||
<div className="nc-workflow">
|
||||
<div className="nc-workflow-top">
|
||||
<div className="nc-workflow-top-row">
|
||||
<h1 className="nc-workflow-top-heading">Editorial Workflow</h1>
|
||||
<WorkflowContainer>
|
||||
<WorkflowTop>
|
||||
<WorkflowTopRow>
|
||||
<WorkflowTopHeading>Editorial Workflow</WorkflowTopHeading>
|
||||
<Dropdown
|
||||
label="New Post"
|
||||
dropdownWidth="160px"
|
||||
dropdownPosition="left"
|
||||
dropdownTopOverlap="40px"
|
||||
renderButton={() => <StyledDropdownButton>New Post</StyledDropdownButton>}
|
||||
>
|
||||
{
|
||||
collections.filter(collection => collection.get('create')).toList().map(collection =>
|
||||
@ -71,18 +100,18 @@ class Workflow extends Component {
|
||||
)
|
||||
}
|
||||
</Dropdown>
|
||||
</div>
|
||||
<p className="nc-workflow-top-description">
|
||||
</WorkflowTopRow>
|
||||
<WorkflowTopDescription>
|
||||
{reviewCount} {reviewCount === 1 ? 'entry' : 'entries'} waiting for review, {readyCount} ready to go live.
|
||||
</p>
|
||||
</div>
|
||||
</WorkflowTopDescription>
|
||||
</WorkflowTop>
|
||||
<WorkflowList
|
||||
entries={unpublishedEntries}
|
||||
handleChangeStatus={updateUnpublishedEntryStatus}
|
||||
handlePublish={publishUnpublishedEntry}
|
||||
handleDelete={deleteUnpublishedEntry}
|
||||
/>
|
||||
</div>
|
||||
</WorkflowContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,87 +0,0 @@
|
||||
.nc-workflow-card {
|
||||
@apply(--card);
|
||||
margin-bottom: 24px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nc-workflow-link {
|
||||
display: block;
|
||||
padding: 0 18px 18px;
|
||||
padding: 0 18px 18px;
|
||||
height: 200px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nc-workflow-card-collection {
|
||||
font-size: 14px;
|
||||
color: var(--colorTextLead);
|
||||
text-transform: uppercase;
|
||||
margin-top: 12px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nc-workflow-card-title {
|
||||
margin: 28px 0 0;
|
||||
color: var(--colorTextLead);
|
||||
}
|
||||
|
||||
.nc-workflow-card-date,
|
||||
.nc-workflow-card-body {
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.nc-workflow-card-body {
|
||||
color: var(--colorText);
|
||||
margin: 24px 0 0;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
.nc-workflow-card-button-container {
|
||||
background-color: var(--colorForeground);
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
padding: 12px 18px;
|
||||
display: flex;
|
||||
opacity: 0;
|
||||
transition: opacity var(--transition);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nc-workflow-card:hover {
|
||||
& .nc-workflow-card-button-container {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.nc-workflow-card-buttonDelete,
|
||||
.nc-workflow-card-buttonPublish {
|
||||
width: auto;
|
||||
flex: 1 0 0;
|
||||
font-size: 13px;
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
.nc-workflow-card-buttonDelete {
|
||||
background-color: var(--colorRedLight);
|
||||
color: var(--colorRed);
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.nc-workflow-card-buttonPublish {
|
||||
background-color: var(--colorTeal);
|
||||
color: var(--colorTextLight);
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.nc-workflow-card-buttonPublishDisabled {
|
||||
background-color: var(--colorGrayLight);
|
||||
color: var(--colorGray);
|
||||
}
|
@ -1,8 +1,100 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import c from 'classnames';
|
||||
import styled, { css } from 'react-emotion';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { components, colors, colorsRaw, transitions, buttons } from 'netlify-cms-ui-default/styles';
|
||||
|
||||
const styles = {
|
||||
text: css`
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
margin-top: 4px;
|
||||
`,
|
||||
button: css`
|
||||
${buttons.button};
|
||||
width: auto;
|
||||
flex: 1 0 0;
|
||||
font-size: 13px;
|
||||
padding: 6px 0;
|
||||
`,
|
||||
};
|
||||
|
||||
const WorkflowLink = styled(Link)`
|
||||
display: block;
|
||||
padding: 0 18px 18px;
|
||||
padding: 0 18px 18px;
|
||||
height: 200px;
|
||||
overflow: hidden;
|
||||
`
|
||||
|
||||
const CardCollection = styled.div`
|
||||
font-size: 14px;
|
||||
color: ${colors.textLead};
|
||||
text-transform: uppercase;
|
||||
margin-top: 12px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
`
|
||||
|
||||
const CardTitle = styled.h2`
|
||||
margin: 28px 0 0;
|
||||
color: ${colors.textLead};
|
||||
`
|
||||
|
||||
const CardDate = styled.div`
|
||||
${styles.text};
|
||||
`
|
||||
|
||||
const CardBody = styled.p`
|
||||
${styles.text};
|
||||
color: ${colors.text};
|
||||
margin: 24px 0 0;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
hyphens: auto;
|
||||
`
|
||||
|
||||
const CardButtonContainer = styled.div`
|
||||
background-color: ${colors.foreground};
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
padding: 12px 18px;
|
||||
display: flex;
|
||||
opacity: 0;
|
||||
transition: opacity ${transitions.main};
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const DeleteButton = styled.button`
|
||||
${styles.button};
|
||||
background-color: ${colorsRaw.redLight};
|
||||
color: ${colorsRaw.red};
|
||||
margin-right: 6px;
|
||||
`
|
||||
|
||||
const PublishButton = styled.button`
|
||||
${styles.button};
|
||||
background-color: ${colorsRaw.teal};
|
||||
color: ${colors.textLight};
|
||||
margin-left: 6px;
|
||||
|
||||
&[disabled] {
|
||||
background-color: ${colorsRaw.grayLight};
|
||||
color: ${colorsRaw.gray};
|
||||
}
|
||||
`
|
||||
|
||||
const WorkflowCardContainer = styled.div`
|
||||
${components.card};
|
||||
margin-bottom: 24px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover ${CardButtonContainer} {
|
||||
opacity: 1;
|
||||
}
|
||||
`
|
||||
|
||||
const WorkflowCard = ({
|
||||
collectionName,
|
||||
@ -17,27 +109,22 @@ const WorkflowCard = ({
|
||||
canPublish,
|
||||
onPublish,
|
||||
}) => (
|
||||
<div className="nc-workflow-card">
|
||||
<Link to={editLink} className="nc-workflow-link">
|
||||
<div className="nc-workflow-card-collection">{collectionName}</div>
|
||||
<h2 className="nc-workflow-card-title">{title}</h2>
|
||||
<div className="nc-workflow-card-date">{timestamp} by {authorLastChange}</div>
|
||||
<p className="nc-workflow-card-body">{body}</p>
|
||||
</Link>
|
||||
<div className="nc-workflow-card-button-container">
|
||||
<button className="nc-workflow-card-buttonDelete" onClick={onDelete}>
|
||||
<WorkflowCardContainer>
|
||||
<WorkflowLink to={editLink}>
|
||||
<CardCollection>{collectionName}</CardCollection>
|
||||
<CardTitle>{title}</CardTitle>
|
||||
<CardDate>{timestamp} by {authorLastChange}</CardDate>
|
||||
<CardBody>{body}</CardBody>
|
||||
</WorkflowLink>
|
||||
<CardButtonContainer>
|
||||
<DeleteButton onClick={onDelete}>
|
||||
{isModification ? 'Delete changes' : 'Delete new entry'}
|
||||
</button>
|
||||
<button
|
||||
className={c('nc-workflow-card-buttonPublish', {
|
||||
'nc-workflow-card-buttonPublishDisabled': !canPublish,
|
||||
})}
|
||||
onClick={onPublish}
|
||||
>
|
||||
</DeleteButton>
|
||||
<PublishButton disabled={!canPublish} onClick={onPublish}>
|
||||
{isModification ? 'Publish changes' : 'Publish new entry'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</PublishButton>
|
||||
</CardButtonContainer>
|
||||
</WorkflowCardContainer>
|
||||
);
|
||||
|
||||
export default WorkflowCard;
|
||||
|
@ -1,83 +0,0 @@
|
||||
.nc-workflow-list-container {
|
||||
min-height: 60%;
|
||||
display: grid;
|
||||
grid-template-columns: 33.3% 33.3% 33.3%;
|
||||
}
|
||||
|
||||
.nc-workflow-list {
|
||||
margin: 0 20px;
|
||||
transition: background-color .5s ease;
|
||||
border: 2px dashed transparent;
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&:not(:first-child):not(:last-child) {
|
||||
&:before,
|
||||
&:after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 2px;
|
||||
height: 80%;
|
||||
top: 76px;
|
||||
background-color: var(--textFieldBorderColor);
|
||||
}
|
||||
|
||||
&:before {
|
||||
left: -23px;
|
||||
}
|
||||
|
||||
&:after {
|
||||
right: -23px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nc-workflow-list-hovered {
|
||||
border-color: var(--colorActive);
|
||||
}
|
||||
|
||||
.nc-workflow-header {
|
||||
font-size: 20px;
|
||||
font-weight: normal;
|
||||
padding: 4px 14px;
|
||||
border-radius: var(--borderRadius);
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.nc-workflow-listDraft {
|
||||
& .nc-workflow-header {
|
||||
background-color: var(--colorStatusDraftBackground);
|
||||
color: var(--colorStatusDraftText);
|
||||
}
|
||||
}
|
||||
|
||||
.nc-workflow-listReview {
|
||||
& .nc-workflow-header {
|
||||
background-color: var(--colorStatusReviewBackground);
|
||||
color: var(--colorStatusReviewText);
|
||||
}
|
||||
}
|
||||
|
||||
.nc-workflow-listReady {
|
||||
& .nc-workflow-header {
|
||||
background-color: var(--colorStatusReadyBackground);
|
||||
color: var(--colorStatusReadyText);
|
||||
}
|
||||
}
|
||||
|
||||
.nc-workflow-list-count {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--colorText);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 6px;
|
||||
}
|
@ -1,24 +1,96 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import styled, { css, cx } from 'react-emotion';
|
||||
import moment from 'moment';
|
||||
import { capitalize } from 'lodash'
|
||||
import c from 'classnames';
|
||||
import { colors, colorsRaw, lengths } from 'netlify-cms-ui-default/styles';
|
||||
import { status } from 'Constants/publishModes';
|
||||
import { DragSource, DropTarget, HTML5DragDrop } from 'UI'
|
||||
import WorkflowCard from './WorkflowCard';
|
||||
|
||||
const WorkflowListContainer = styled.div`
|
||||
min-height: 60%;
|
||||
display: grid;
|
||||
grid-template-columns: 33.3% 33.3% 33.3%;
|
||||
`
|
||||
|
||||
const styles = {
|
||||
column: css`
|
||||
margin: 0 20px;
|
||||
transition: background-color .5s ease;
|
||||
border: 2px dashed transparent;
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&:not(:first-child):not(:last-child) {
|
||||
&:before,
|
||||
&:after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 2px;
|
||||
height: 80%;
|
||||
top: 76px;
|
||||
background-color: ${colors.textFieldBorder}
|
||||
}
|
||||
|
||||
&:before {
|
||||
left: -23px;
|
||||
}
|
||||
|
||||
&:after {
|
||||
right: -23px;
|
||||
}
|
||||
}
|
||||
`,
|
||||
columnHovered: css`
|
||||
border-color: ${colors.active};
|
||||
`,
|
||||
};
|
||||
|
||||
const ColumnHeader = styled.h2`
|
||||
font-size: 20px;
|
||||
font-weight: normal;
|
||||
padding: 4px 14px;
|
||||
border-radius: ${lengths.borderRadius};
|
||||
margin-bottom: 28px;
|
||||
|
||||
${props => props.name === 'draft' && css`
|
||||
background-color: ${colors.statusDraftBackground};
|
||||
color: ${colors.statusDraftText};
|
||||
`}
|
||||
|
||||
${props => props.name === 'pending_review' && css`
|
||||
background-color: ${colors.statusReviewBackground};
|
||||
color: ${colors.statusReviewText};
|
||||
`}
|
||||
|
||||
${props => props.name === 'pending_publish' && css`
|
||||
background-color: ${colors.statusReadyBackground};
|
||||
color: ${colors.statusReadyText};
|
||||
`}
|
||||
`
|
||||
|
||||
const ColumnCount = styled.p`
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: ${colors.text};
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 6px;
|
||||
`
|
||||
|
||||
// This is a namespace so that we can only drop these elements on a DropTarget with the same
|
||||
const DNDNamespace = 'cms-workflow';
|
||||
|
||||
const getColumnClassName = columnName => {
|
||||
switch (columnName) {
|
||||
case 'draft': return 'nc-workflow-listDraft';
|
||||
case 'pending_review': return 'nc-workflow-listReview';
|
||||
case 'pending_publish': return 'nc-workflow-listReady';
|
||||
}
|
||||
}
|
||||
|
||||
const getColumnHeaderText = columnName => {
|
||||
switch (columnName) {
|
||||
case 'draft': return 'Drafts';
|
||||
@ -73,15 +145,11 @@ Please drag the card to the "Ready" column to enable publishing.`
|
||||
onDrop={this.handleChangeStatus.bind(this, currColumn)}
|
||||
>
|
||||
{(connect, { isHovered }) => connect(
|
||||
<div className={c('nc-workflow-list', getColumnClassName(currColumn), {
|
||||
'nc-workflow-list-hovered': isHovered,
|
||||
})}>
|
||||
<h2 className="nc-workflow-header">
|
||||
{getColumnHeaderText(currColumn)}
|
||||
</h2>
|
||||
<p className="nc-workflow-list-count">
|
||||
<div className={cx(styles.column, { [styles.columnHovered]: isHovered })}>
|
||||
<ColumnHeader name={currColumn}>{getColumnHeaderText(currColumn)}</ColumnHeader>
|
||||
<ColumnCount>
|
||||
{currEntries.size} {currEntries.size === 1 ? 'entry' : 'entries'}
|
||||
</p>
|
||||
</ColumnCount>
|
||||
{this.renderColumns(currEntries, currColumn)}
|
||||
</div>
|
||||
)}
|
||||
@ -137,9 +205,7 @@ Please drag the card to the "Ready" column to enable publishing.`
|
||||
render() {
|
||||
const columns = this.renderColumns(this.props.entries);
|
||||
return (
|
||||
<div className="nc-workflow-list-container">
|
||||
{columns}
|
||||
</div>
|
||||
<WorkflowListContainer>{columns}</WorkflowListContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,6 @@
|
||||
/**
|
||||
* Base styles
|
||||
*/
|
||||
@import "./components/UI/base.css";
|
||||
|
||||
/**
|
||||
* Components
|
||||
*/
|
||||
@import "./components/UI/UI.css";
|
||||
@import "./components/App/App.css";
|
||||
@import "./components/Collection/Collection.css";
|
||||
@import "./components/Workflow/Workflow.css";
|
||||
@import "./components/Editor/Editor.css";
|
||||
@import "./components/MediaLibrary/MediaLibrary.css";
|
||||
@import "./components/EditorWidgets/EditorWidgets.css";
|
||||
|
||||
/**
|
||||
|
7007
packages/netlify-cms-lib-auth/package-lock.json
generated
7007
packages/netlify-cms-lib-auth/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -12,7 +12,8 @@
|
||||
"netlify-cms"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "parcel build src --out-dir ."
|
||||
"watch": "parcel watch src/*.js --out-dir . --no-cache",
|
||||
"build": "parcel build src/*.js --out-dir . --no-cache"
|
||||
},
|
||||
"dependencies": {
|
||||
"immutable": "^3.7.6",
|
||||
|
7023
packages/netlify-cms-lib-util/package-lock.json
generated
7023
packages/netlify-cms-lib-util/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -8,7 +8,8 @@
|
||||
"netlify-cms"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "parcel build src --out-dir ."
|
||||
"watch": "parcel watch src/*.js --out-dir . --no-cache",
|
||||
"build": "parcel build src/*.js --out-dir . --no-cache"
|
||||
},
|
||||
"dependencies": {
|
||||
"immutable": "^3.7.6",
|
||||
|
@ -1,98 +0,0 @@
|
||||
(function () {
|
||||
var enterModule = require('react-hot-loader').enterModule;
|
||||
|
||||
enterModule && enterModule(module);
|
||||
})();
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import c from 'classnames';
|
||||
import { Wrapper, Button, Menu, MenuItem } from 'react-aria-menubutton';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
|
||||
var Dropdown = function Dropdown(_ref) {
|
||||
var label = _ref.label,
|
||||
button = _ref.button,
|
||||
className = _ref.className,
|
||||
_ref$classNameButton = _ref.classNameButton,
|
||||
classNameButton = _ref$classNameButton === undefined ? '' : _ref$classNameButton,
|
||||
_ref$dropdownWidth = _ref.dropdownWidth,
|
||||
dropdownWidth = _ref$dropdownWidth === undefined ? 'auto' : _ref$dropdownWidth,
|
||||
_ref$dropdownPosition = _ref.dropdownPosition,
|
||||
dropdownPosition = _ref$dropdownPosition === undefined ? 'left' : _ref$dropdownPosition,
|
||||
_ref$dropdownTopOverl = _ref.dropdownTopOverlap,
|
||||
dropdownTopOverlap = _ref$dropdownTopOverl === undefined ? '0' : _ref$dropdownTopOverl,
|
||||
children = _ref.children;
|
||||
|
||||
var style = {
|
||||
width: dropdownWidth,
|
||||
top: dropdownTopOverlap,
|
||||
left: dropdownPosition === 'left' ? 0 : 'auto',
|
||||
right: dropdownPosition === 'right' ? 0 : 'auto'
|
||||
};
|
||||
return React.createElement(
|
||||
Wrapper,
|
||||
{ className: c('nc-dropdown', className), onSelection: function onSelection(handler) {
|
||||
return handler();
|
||||
} },
|
||||
button ? React.createElement(
|
||||
Button,
|
||||
null,
|
||||
button
|
||||
) : React.createElement(
|
||||
Button,
|
||||
{ className: c('nc-dropdownButton', classNameButton) },
|
||||
label
|
||||
),
|
||||
React.createElement(
|
||||
Menu,
|
||||
null,
|
||||
React.createElement(
|
||||
'ul',
|
||||
{ className: 'nc-dropdownList', style: style },
|
||||
children
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
var DropdownItem = function DropdownItem(_ref2) {
|
||||
var label = _ref2.label,
|
||||
icon = _ref2.icon,
|
||||
iconDirection = _ref2.iconDirection,
|
||||
onClick = _ref2.onClick,
|
||||
className = _ref2.className;
|
||||
return React.createElement(
|
||||
MenuItem,
|
||||
{ className: c('nc-dropdownItem', className), value: onClick },
|
||||
React.createElement(
|
||||
'span',
|
||||
null,
|
||||
label
|
||||
),
|
||||
icon ? React.createElement(
|
||||
'span',
|
||||
{ className: 'nc-dropdownItemIcon' },
|
||||
React.createElement(Icon, { type: icon, direction: iconDirection, size: 'small' })
|
||||
) : null
|
||||
);
|
||||
};
|
||||
|
||||
export { Dropdown, DropdownItem };
|
||||
;
|
||||
|
||||
(function () {
|
||||
var reactHotLoader = require('react-hot-loader').default;
|
||||
|
||||
var leaveModule = require('react-hot-loader').leaveModule;
|
||||
|
||||
if (!reactHotLoader) {
|
||||
return;
|
||||
}
|
||||
|
||||
reactHotLoader.register(Dropdown, 'Dropdown', 'src/Dropdown/Dropdown.js');
|
||||
reactHotLoader.register(DropdownItem, 'DropdownItem', 'src/Dropdown/Dropdown.js');
|
||||
leaveModule(module);
|
||||
})();
|
||||
|
||||
;
|
@ -1,77 +0,0 @@
|
||||
import _extends from 'babel-runtime/helpers/extends';
|
||||
import _objectWithoutProperties from 'babel-runtime/helpers/objectWithoutProperties';
|
||||
|
||||
(function () {
|
||||
var enterModule = require('react-hot-loader').enterModule;
|
||||
|
||||
enterModule && enterModule(module);
|
||||
})();
|
||||
|
||||
import React from 'react';
|
||||
import icons from './icons';
|
||||
|
||||
/**
|
||||
* Calculates rotation for icons that have a `direction` property configured
|
||||
* in the imported icon definition object. If no direction is configured, a
|
||||
* neutral rotation value is returned.
|
||||
*
|
||||
* Returned value is a string of shape `${degrees}deg`, for use in a CSS
|
||||
* transform.
|
||||
*/
|
||||
var getRotation = function getRotation(iconDirection, newDirection) {
|
||||
if (!iconDirection || !newDirection) {
|
||||
return '0deg';
|
||||
}
|
||||
var rotations = { right: 90, down: 180, left: 270, up: 360 };
|
||||
var degrees = rotations[newDirection] - rotations[iconDirection];
|
||||
return degrees + 'deg';
|
||||
};
|
||||
|
||||
var sizes = {
|
||||
xsmall: '12px',
|
||||
small: '18px',
|
||||
medium: '24px',
|
||||
large: '32px'
|
||||
};
|
||||
|
||||
var Icon = function Icon(props) {
|
||||
var type = props.type,
|
||||
direction = props.direction,
|
||||
_props$size = props.size,
|
||||
size = _props$size === undefined ? 'medium' : _props$size,
|
||||
_props$className = props.className,
|
||||
className = _props$className === undefined ? '' : _props$className,
|
||||
width = props.width,
|
||||
height = props.height,
|
||||
remainingProps = _objectWithoutProperties(props, ['type', 'direction', 'size', 'className', 'width', 'height']);
|
||||
|
||||
var icon = icons[type];
|
||||
var rotation = getRotation(icon.direction, direction);
|
||||
var transform = 'rotate(' + rotation + ')';
|
||||
var sizeResolved = sizes[size] || size;
|
||||
var style = { width: sizeResolved, height: sizeResolved, transform: transform };
|
||||
return React.createElement(
|
||||
'span',
|
||||
_extends({ className: 'nc-icon ' + className }, remainingProps),
|
||||
React.createElement('span', { dangerouslySetInnerHTML: { __html: icon.image }, style: style })
|
||||
);
|
||||
};
|
||||
export { Icon };
|
||||
;
|
||||
|
||||
(function () {
|
||||
var reactHotLoader = require('react-hot-loader').default;
|
||||
|
||||
var leaveModule = require('react-hot-loader').leaveModule;
|
||||
|
||||
if (!reactHotLoader) {
|
||||
return;
|
||||
}
|
||||
|
||||
reactHotLoader.register(getRotation, 'getRotation', 'src/Icon/Icon.js');
|
||||
reactHotLoader.register(sizes, 'sizes', 'src/Icon/Icon.js');
|
||||
reactHotLoader.register(Icon, 'Icon', 'src/Icon/Icon.js');
|
||||
leaveModule(module);
|
||||
})();
|
||||
|
||||
;
|
@ -1,70 +0,0 @@
|
||||
import _extends from 'babel-runtime/helpers/extends';
|
||||
|
||||
(function () {
|
||||
var enterModule = require('react-hot-loader').enterModule;
|
||||
|
||||
enterModule && enterModule(module);
|
||||
})();
|
||||
|
||||
import mapValues from 'lodash/mapValues';
|
||||
import images from './images/_index';
|
||||
|
||||
/**
|
||||
* This module outputs icon objects with the following shape:
|
||||
*
|
||||
* {
|
||||
* image: <svg>...</svg>,
|
||||
* ...props
|
||||
* }
|
||||
*
|
||||
* `props` here are config properties defined in this file for specific icons.
|
||||
* For example, an icon may face a specific direction, and the Icon component
|
||||
* accepts a `direction` prop to rotate directional icons, which relies on
|
||||
* defining the default direction here.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Configuration for individual icons.
|
||||
*/
|
||||
var config = {
|
||||
'arrow': {
|
||||
direction: 'left'
|
||||
},
|
||||
'chevron': {
|
||||
direction: 'down'
|
||||
},
|
||||
'chevron-double': {
|
||||
direction: 'down'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Map icon definition objects - imported object of images simply maps the icon
|
||||
* name to the raw svg, so we move that to the `image` property of the
|
||||
* definition object and set any additional configured properties for each icon.
|
||||
*/
|
||||
var icons = mapValues(images, function (image, name) {
|
||||
var props = config[name] || {};
|
||||
return _extends({ image: image }, props);
|
||||
});
|
||||
|
||||
var _default = icons;
|
||||
export default _default;
|
||||
;
|
||||
|
||||
(function () {
|
||||
var reactHotLoader = require('react-hot-loader').default;
|
||||
|
||||
var leaveModule = require('react-hot-loader').leaveModule;
|
||||
|
||||
if (!reactHotLoader) {
|
||||
return;
|
||||
}
|
||||
|
||||
reactHotLoader.register(config, 'config', 'src/Icon/icons.js');
|
||||
reactHotLoader.register(icons, 'icons', 'src/Icon/icons.js');
|
||||
reactHotLoader.register(_default, 'default', 'src/Icon/icons.js');
|
||||
leaveModule(module);
|
||||
})();
|
||||
|
||||
;
|
File diff suppressed because one or more lines are too long
@ -1,57 +0,0 @@
|
||||
(function () {
|
||||
var enterModule = require('react-hot-loader').enterModule;
|
||||
|
||||
enterModule && enterModule(module);
|
||||
})();
|
||||
|
||||
import React from 'react';
|
||||
import c from 'classnames';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
|
||||
export var ListItemTopBar = function ListItemTopBar(_ref) {
|
||||
var collapsed = _ref.collapsed,
|
||||
onCollapseToggle = _ref.onCollapseToggle,
|
||||
onRemove = _ref.onRemove,
|
||||
dragHandleHOC = _ref.dragHandleHOC,
|
||||
className = _ref.className;
|
||||
|
||||
var DragHandle = dragHandleHOC && dragHandleHOC(function () {
|
||||
return React.createElement(
|
||||
'span',
|
||||
{ className: 'nc-listItemTopBar-dragIcon' },
|
||||
React.createElement(Icon, { type: 'drag-handle', size: 'small' })
|
||||
);
|
||||
});
|
||||
|
||||
return React.createElement(
|
||||
'div',
|
||||
{ className: c('nc-listItemTopBar', className) },
|
||||
onCollapseToggle ? React.createElement(
|
||||
'button',
|
||||
{ className: 'nc-listItemTopBar-toggleButton', onClick: onCollapseToggle },
|
||||
React.createElement(Icon, { type: 'chevron', size: 'small', direction: collapsed ? 'right' : 'down' })
|
||||
) : null,
|
||||
dragHandleHOC ? React.createElement(DragHandle, null) : null,
|
||||
onRemove ? React.createElement(
|
||||
'button',
|
||||
{ className: 'nc-listItemTopBar-removeButton', onClick: onRemove },
|
||||
React.createElement(Icon, { type: 'close', size: 'small' })
|
||||
) : null
|
||||
);
|
||||
};
|
||||
;
|
||||
|
||||
(function () {
|
||||
var reactHotLoader = require('react-hot-loader').default;
|
||||
|
||||
var leaveModule = require('react-hot-loader').leaveModule;
|
||||
|
||||
if (!reactHotLoader) {
|
||||
return;
|
||||
}
|
||||
|
||||
reactHotLoader.register(ListItemTopBar, 'ListItemTopBar', 'src/ListItemTopBar/ListItemTopBar.js');
|
||||
leaveModule(module);
|
||||
})();
|
||||
|
||||
;
|
@ -1,128 +0,0 @@
|
||||
import _Object$getPrototypeOf from 'babel-runtime/core-js/object/get-prototype-of';
|
||||
import _classCallCheck from 'babel-runtime/helpers/classCallCheck';
|
||||
import _createClass from 'babel-runtime/helpers/createClass';
|
||||
import _possibleConstructorReturn from 'babel-runtime/helpers/possibleConstructorReturn';
|
||||
import _inherits from 'babel-runtime/helpers/inherits';
|
||||
|
||||
(function () {
|
||||
var enterModule = require('react-hot-loader').enterModule;
|
||||
|
||||
enterModule && enterModule(module);
|
||||
})();
|
||||
|
||||
import React from 'react';
|
||||
import CSSTransition from 'react-transition-group/CSSTransition';
|
||||
import c from 'classnames';
|
||||
|
||||
export var Loader = function (_React$Component) {
|
||||
_inherits(Loader, _React$Component);
|
||||
|
||||
function Loader() {
|
||||
var _ref;
|
||||
|
||||
var _temp, _this, _ret;
|
||||
|
||||
_classCallCheck(this, Loader);
|
||||
|
||||
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
|
||||
args[_key] = arguments[_key];
|
||||
}
|
||||
|
||||
return _ret = (_temp = (_this = _possibleConstructorReturn(this, (_ref = Loader.__proto__ || _Object$getPrototypeOf(Loader)).call.apply(_ref, [this].concat(args))), _this), _this.state = {
|
||||
currentItem: 0
|
||||
}, _this.setAnimation = function () {
|
||||
if (_this.interval) return;
|
||||
var children = _this.props.children;
|
||||
|
||||
|
||||
_this.interval = setInterval(function () {
|
||||
var nextItem = _this.state.currentItem === children.length - 1 ? 0 : _this.state.currentItem + 1;
|
||||
_this.setState({ currentItem: nextItem });
|
||||
}, 5000);
|
||||
}, _this.renderChild = function () {
|
||||
var children = _this.props.children;
|
||||
var currentItem = _this.state.currentItem;
|
||||
|
||||
if (!children) {
|
||||
return null;
|
||||
} else if (typeof children == 'string') {
|
||||
return React.createElement(
|
||||
'div',
|
||||
{ className: 'nc-loader-text' },
|
||||
children
|
||||
);
|
||||
} else if (Array.isArray(children)) {
|
||||
_this.setAnimation();
|
||||
return React.createElement(
|
||||
'div',
|
||||
{ className: 'nc-loader-text' },
|
||||
React.createElement(
|
||||
CSSTransition,
|
||||
{
|
||||
classNames: {
|
||||
enter: 'nc-loader-enter',
|
||||
enterActive: 'nc-loader-enterActive',
|
||||
exit: 'nc-loader-exit',
|
||||
exitActive: 'nc-loader-exitActive'
|
||||
},
|
||||
timeout: 500
|
||||
},
|
||||
React.createElement(
|
||||
'div',
|
||||
{ key: currentItem, className: 'nc-loader-animateItem' },
|
||||
children[currentItem]
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}, _temp), _possibleConstructorReturn(_this, _ret);
|
||||
}
|
||||
|
||||
_createClass(Loader, [{
|
||||
key: 'componentWillUnmount',
|
||||
value: function componentWillUnmount() {
|
||||
if (this.interval) {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
key: 'render',
|
||||
value: function render() {
|
||||
var _props = this.props,
|
||||
active = _props.active,
|
||||
className = _props.className;
|
||||
|
||||
var combinedClassName = c('nc-loader-root', { 'nc-loader-active': active }, className);
|
||||
return React.createElement(
|
||||
'div',
|
||||
{ className: combinedClassName },
|
||||
this.renderChild()
|
||||
);
|
||||
}
|
||||
}, {
|
||||
key: '__reactstandin__regenerateByEval',
|
||||
// @ts-ignore
|
||||
value: function __reactstandin__regenerateByEval(key, code) {
|
||||
// @ts-ignore
|
||||
this[key] = eval(code);
|
||||
}
|
||||
}]);
|
||||
|
||||
return Loader;
|
||||
}(React.Component);
|
||||
;
|
||||
|
||||
(function () {
|
||||
var reactHotLoader = require('react-hot-loader').default;
|
||||
|
||||
var leaveModule = require('react-hot-loader').leaveModule;
|
||||
|
||||
if (!reactHotLoader) {
|
||||
return;
|
||||
}
|
||||
|
||||
reactHotLoader.register(Loader, 'Loader', 'src/Loader/Loader.js');
|
||||
leaveModule(module);
|
||||
})();
|
||||
|
||||
;
|
@ -1,57 +0,0 @@
|
||||
import _extends from 'babel-runtime/helpers/extends';
|
||||
|
||||
(function () {
|
||||
var enterModule = require('react-hot-loader').enterModule;
|
||||
|
||||
enterModule && enterModule(module);
|
||||
})();
|
||||
|
||||
import React from 'react';
|
||||
import ReactToggled from 'react-toggled';
|
||||
import c from 'classnames';
|
||||
|
||||
export var Toggle = function Toggle(_ref) {
|
||||
var active = _ref.active,
|
||||
onChange = _ref.onChange,
|
||||
className = _ref.className,
|
||||
classNameBackground = _ref.classNameBackground,
|
||||
classNameSwitch = _ref.classNameSwitch,
|
||||
onFocus = _ref.onFocus,
|
||||
onBlur = _ref.onBlur;
|
||||
return React.createElement(
|
||||
ReactToggled,
|
||||
{ on: active, onToggle: onChange },
|
||||
function (_ref2) {
|
||||
var on = _ref2.on,
|
||||
getElementTogglerProps = _ref2.getElementTogglerProps;
|
||||
return React.createElement(
|
||||
'span',
|
||||
_extends({
|
||||
className: c('nc-toggle', className, { 'nc-toggle-active': on }),
|
||||
role: 'switch',
|
||||
'aria-checked': on.toString(),
|
||||
onFocus: onFocus,
|
||||
onBlur: onBlur
|
||||
}, getElementTogglerProps()),
|
||||
React.createElement('span', { className: 'nc-toggle-background ' + classNameBackground }),
|
||||
React.createElement('span', { className: 'nc-toggle-switch ' + classNameSwitch })
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
;
|
||||
|
||||
(function () {
|
||||
var reactHotLoader = require('react-hot-loader').default;
|
||||
|
||||
var leaveModule = require('react-hot-loader').leaveModule;
|
||||
|
||||
if (!reactHotLoader) {
|
||||
return;
|
||||
}
|
||||
|
||||
reactHotLoader.register(Toggle, 'Toggle', 'src/Toggle/Toggle.js');
|
||||
leaveModule(module);
|
||||
})();
|
||||
|
||||
;
|
7249
packages/netlify-cms-ui-default/package-lock.json
generated
7249
packages/netlify-cms-ui-default/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -8,14 +8,17 @@
|
||||
"netlify-cms"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "parcel build src --out-dir ."
|
||||
"watch": "parcel watch src/*.js --out-dir . --no-cache",
|
||||
"build": "parcel build src/*.js --out-dir . --no-cache"
|
||||
},
|
||||
"dependencies": {
|
||||
"classnames": "^2.2.5",
|
||||
"emotion": "^9.1.3",
|
||||
"lodash": "^4.13.1",
|
||||
"prop-types": "^15.5.10",
|
||||
"react": "^16.0.0",
|
||||
"react": "15.x || 16.x",
|
||||
"react-aria-menubutton": "^5.1.0",
|
||||
"react-emotion": "^9.2.6",
|
||||
"react-toggled": "^1.1.2",
|
||||
"react-transition-group": "^2.2.1"
|
||||
},
|
||||
|
115
packages/netlify-cms-ui-default/src/Dropdown.js
Normal file
115
packages/netlify-cms-ui-default/src/Dropdown.js
Normal file
@ -0,0 +1,115 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled, { css } from 'react-emotion';
|
||||
import { Wrapper, Button as DropdownButton, Menu, MenuItem } from 'react-aria-menubutton';
|
||||
import { shadows, colors, colorsRaw, lengths, buttons, components } from './styles';
|
||||
import Icon from './Icon';
|
||||
|
||||
const StyledWrapper = styled(Wrapper)`
|
||||
position: relative;
|
||||
font-size: 14px;
|
||||
user-select: none;
|
||||
`
|
||||
|
||||
const StyledDropdownButton = styled(DropdownButton)`
|
||||
${buttons.button};
|
||||
${buttons.default};
|
||||
display: block;
|
||||
padding-left: 20px;
|
||||
padding-right: 40px;
|
||||
|
||||
&:after {
|
||||
${components.caretDown};
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
color: currentColor;
|
||||
}
|
||||
`
|
||||
|
||||
const DropdownList = styled.ul`
|
||||
${shadows.dropDeep};
|
||||
background-color: ${colorsRaw.white};
|
||||
border-radius: ${lengths.borderRadius};
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
min-width: 100%;
|
||||
z-index: 1;
|
||||
|
||||
${props => css`
|
||||
width: ${props.width};
|
||||
top: ${props.top};
|
||||
left: ${props.position === 'left' ? 0 : 'auto'};
|
||||
right: ${props.position === 'right' ? 0 : 'auto'};
|
||||
`}
|
||||
`
|
||||
|
||||
const StyledMenuItem = styled(MenuItem)`
|
||||
${buttons.button};
|
||||
background-color: transparent;
|
||||
border-radius: 0;
|
||||
color: ${colorsRaw.gray};
|
||||
font-weight: 500;
|
||||
border-bottom: 1px solid #eaebf1;
|
||||
padding: 10px 14px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
color: ${colors.active};
|
||||
background-color: ${colors.activeBackground};
|
||||
}
|
||||
`
|
||||
|
||||
const MenuItemIconContainer = styled.div`
|
||||
flex: 1 0 32px;
|
||||
text-align: right;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
`
|
||||
|
||||
const Dropdown = ({
|
||||
renderButton,
|
||||
dropdownWidth = 'auto',
|
||||
dropdownPosition = 'left',
|
||||
dropdownTopOverlap = '0',
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<StyledWrapper onSelection={handler => handler()}>
|
||||
{renderButton()}
|
||||
<Menu>
|
||||
<DropdownList width={dropdownWidth} top={dropdownTopOverlap} position={dropdownPosition}>
|
||||
{children}
|
||||
</DropdownList>
|
||||
</Menu>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const DropdownItem = ({ label, icon, iconDirection, onClick }) => (
|
||||
<StyledMenuItem value={onClick}>
|
||||
<span>{label}</span>
|
||||
{
|
||||
icon
|
||||
? <MenuItemIconContainer>
|
||||
<Icon type={icon} direction={iconDirection} size="small"/>
|
||||
</MenuItemIconContainer>
|
||||
: null
|
||||
}
|
||||
</StyledMenuItem>
|
||||
);
|
||||
|
||||
export { Dropdown as default, DropdownItem, DropdownButton, StyledDropdownButton };
|
@ -1,77 +0,0 @@
|
||||
:root {
|
||||
--dropdownList: {
|
||||
@apply(--dropShadowDeep);
|
||||
background-color: #fff;
|
||||
border-radius: var(--borderRadius);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
--dropdownItem: {
|
||||
@apply --button;
|
||||
background-color: transparent;
|
||||
border-radius: 0;
|
||||
color: var(--colorText);
|
||||
font-weight: 500;
|
||||
border-bottom: 1px solid #eaebf1;
|
||||
padding: 10px 14px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
color: var(--colorBlue);
|
||||
background-color: var(--colorActiveBackground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nc-dropdown {
|
||||
position: relative;
|
||||
font-size: 14px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.nc-dropdownButton {
|
||||
@apply --button;
|
||||
@apply --buttonDefault;
|
||||
display: block;
|
||||
padding-left: 20px;
|
||||
padding-right: 40px;
|
||||
|
||||
&:after {
|
||||
@apply(--caretDown);
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
color: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
.nc-dropdownList {
|
||||
@apply(--dropdownList);
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
min-width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.nc-dropdownItem {
|
||||
@apply(--dropdownItem);
|
||||
}
|
||||
|
||||
.nc-dropdownItemIcon {
|
||||
flex: 1 0 32px;
|
||||
text-align: right;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import c from 'classnames';
|
||||
import { Wrapper, Button, Menu, MenuItem } from 'react-aria-menubutton';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
|
||||
const Dropdown = ({
|
||||
label,
|
||||
button,
|
||||
className,
|
||||
classNameButton = '',
|
||||
dropdownWidth = 'auto',
|
||||
dropdownPosition = 'left',
|
||||
dropdownTopOverlap = '0',
|
||||
children
|
||||
}) => {
|
||||
const style = {
|
||||
width: dropdownWidth,
|
||||
top: dropdownTopOverlap,
|
||||
left: dropdownPosition === 'left' ? 0 : 'auto',
|
||||
right: dropdownPosition === 'right' ? 0 : 'auto',
|
||||
};
|
||||
return (
|
||||
<Wrapper className={c('nc-dropdown', className)} onSelection={handler => handler()}>
|
||||
{
|
||||
button
|
||||
? <Button>{button}</Button>
|
||||
: <Button className={c('nc-dropdownButton', classNameButton)}>{label}</Button>
|
||||
}
|
||||
<Menu>
|
||||
<ul className="nc-dropdownList" style={style}>
|
||||
{children}
|
||||
</ul>
|
||||
</Menu>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const DropdownItem = ({ label, icon, iconDirection, onClick, className }) => (
|
||||
<MenuItem className={c('nc-dropdownItem', className)} value={onClick}>
|
||||
<span>{label}</span>
|
||||
{
|
||||
icon
|
||||
? <span className="nc-dropdownItemIcon">
|
||||
<Icon type={icon} direction={iconDirection} size="small"/>
|
||||
</span>
|
||||
: null
|
||||
}
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
|
||||
export { Dropdown, DropdownItem };
|
62
packages/netlify-cms-ui-default/src/Icon.js
Normal file
62
packages/netlify-cms-ui-default/src/Icon.js
Normal file
@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
import styled from 'react-emotion';
|
||||
import icons from './Icon/icons';
|
||||
|
||||
const IconWrapper = styled.span`
|
||||
display: inline-block;
|
||||
line-height: 0;
|
||||
width: ${props => props.size};
|
||||
height: ${props => props.size};
|
||||
transform: ${props => `rotation(${props.rotation})`};
|
||||
|
||||
& path:not(.no-fill),
|
||||
& circle:not(.no-fill),
|
||||
& polygon:not(.no-fill),
|
||||
& rect:not(.no-fill) {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
& path.clipped {
|
||||
fill: transparent;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
`
|
||||
|
||||
/**
|
||||
* Calculates rotation for icons that have a `direction` property configured
|
||||
* in the imported icon definition object. If no direction is configured, a
|
||||
* neutral rotation value is returned.
|
||||
*
|
||||
* Returned value is a string of shape `${degrees}deg`, for use in a CSS
|
||||
* transform.
|
||||
*/
|
||||
const getRotation = (iconDirection, newDirection) => {
|
||||
if (!iconDirection || !newDirection) {
|
||||
return '0deg';
|
||||
}
|
||||
const rotations = { right: 90, down: 180, left: 270, up: 360 };
|
||||
const degrees = rotations[newDirection] - rotations[iconDirection];
|
||||
return `${degrees}deg`;
|
||||
}
|
||||
|
||||
const sizes = {
|
||||
xsmall: '12px',
|
||||
small: '18px',
|
||||
medium: '24px',
|
||||
large: '32px',
|
||||
};
|
||||
|
||||
const Icon = ({ type, direction, size = 'medium', width, height, className }) => (
|
||||
<IconWrapper
|
||||
className={className}
|
||||
dangerouslySetInnerHTML={{ __html: icons[type].image }}
|
||||
size={sizes[size] || size}
|
||||
rotation={getRotation(icons[type].direction, direction)}
|
||||
/>
|
||||
);
|
||||
|
||||
export default styled(Icon)``
|
@ -1,24 +0,0 @@
|
||||
.nc-icon {
|
||||
display: inline-block;
|
||||
line-height: 0;
|
||||
|
||||
& > span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
& path:not(.no-fill),
|
||||
& circle:not(.no-fill),
|
||||
& polygon:not(.no-fill),
|
||||
& rect:not(.no-fill) {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
& path.clipped {
|
||||
fill: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.nc-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user