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

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