migrate core to emotion

This commit is contained in:
Shawn Erquhart 2018-07-06 18:56:28 -04:00
parent 768fcbaa1d
commit 4931711892
114 changed files with 3414 additions and 78296 deletions

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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/*"

View File

@ -0,0 +1,4 @@
import { init } from '../src/index';
import config from './config.yml';
init({ config });

View File

@ -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({

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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;

View File

@ -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 = {

View File

@ -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 = {

View File

@ -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 = {

View File

@ -12,6 +12,7 @@ import App from 'App/App';
import 'EditorWidgets';
import 'MarkdownPlugins';
import './index.css';
import 'what-input';
const ROOT_ID = 'nc-root';

View File

@ -1,8 +0,0 @@
@import "./NotFoundPage.css";
@import "./Header.css";
.nc-app-main {
min-width: 800px;
max-width: 1440px;
margin: 0 auto;
}

View File

@ -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>
);
}

View File

@ -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);
}

View File

@ -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>
);
}
}

View File

@ -1,3 +0,0 @@
.nc-notFound-container {
margin: var(--pageMargin);
}

View File

@ -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>
);

View File

@ -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;
}

View File

@ -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>
);
}
}

View File

@ -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);
}

View File

@ -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>
);
};

View File

@ -1,2 +0,0 @@
@import "./EntryListing.css";
@import "./EntryCard.css";

View File

@ -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 = ({

View File

@ -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;
}

View File

@ -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>
);
}
}

View File

@ -1,9 +0,0 @@
.nc-entryListing-cardsGrid {
display: flex;
flex-flow: row wrap;
margin-left: -12px;
}
.nc-entryListing-cardsList {
margin-left: -12px;
}

View File

@ -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>
);
}

View File

@ -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;
}
}

View File

@ -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>
);
}
}

View File

@ -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";

View File

@ -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';

View File

@ -1,7 +0,0 @@
.nc-controlPane-control {
margin-top: 16px;
&:first-child {
margin-top: 36px;
}
}

View File

@ -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>
);
}
}

View File

@ -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;
}

View File

@ -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>
);
}
}

View File

@ -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;
}

View File

@ -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>
);
}
}

View File

@ -1,7 +0,0 @@
.nc-previewPane-frame {
width: 100%;
height: 100%;
border: none;
background: #fff;
border-radius: var(--borderRadius);
}

View File

@ -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>
);
}

View File

@ -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);
}

View File

@ -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,

View File

@ -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);
}

View File

@ -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>
);
}
};

View File

@ -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() {

View File

@ -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) {

View File

@ -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'))} />

View File

@ -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);

View File

@ -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 {

View File

@ -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">

View File

@ -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, '\\$&');

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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}
/>
);
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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>

View File

@ -1,8 +0,0 @@
.nc-errorBoundary {
padding: 0 20px;
}
.nc-errorBoundary-heading,
.nc-errorBoundary-link {
color: var(--colorErrorText);
}

View 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>
);
}
}

View File

@ -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;
}

View File

@ -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>
);
}
}

View File

@ -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;

View 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,
};

View File

@ -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);
}

View File

@ -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,
};

View File

@ -1,3 +0,0 @@
@import "./Toast/Toast.css";
@import "./Modal/Modal.css";
@import "./ErrorBoundary/ErrorBoundary.css";

View File

@ -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;
}

View File

@ -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';

View File

@ -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);
}

View File

@ -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>
);
}
}

View File

@ -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);
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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>
);
}
}

View File

@ -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";
/**

File diff suppressed because it is too large Load Diff

View File

@ -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",

File diff suppressed because it is too large Load Diff

View File

@ -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",

View File

@ -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);
})();
;

View File

@ -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);
})();
;

View File

@ -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

View File

@ -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);
})();
;

View File

@ -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);
})();
;

View File

@ -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);
})();
;

File diff suppressed because it is too large Load Diff

View File

@ -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"
},

View 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 };

View File

@ -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;
}

View File

@ -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 };

View 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)``

View File

@ -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