Merge master
|
@ -1,4 +1,4 @@
|
||||||
API_URL=/api
|
API_URL=/editor
|
||||||
AWS_ACCESS_KEY=<your-aws-access-key>
|
AWS_ACCESS_KEY=<your-aws-access-key>
|
||||||
AWS_REGION=<your-aws-region>
|
AWS_REGION=<your-aws-region>
|
||||||
AWS_SECRET_KEY=<your-aws-secret-key>
|
AWS_SECRET_KEY=<your-aws-secret-key>
|
||||||
|
@ -23,3 +23,5 @@ PORT=8000
|
||||||
S3_BUCKET=<your-s3-bucket>
|
S3_BUCKET=<your-s3-bucket>
|
||||||
S3_BUCKET_URL_BASE=<alt-for-s3-url>
|
S3_BUCKET_URL_BASE=<alt-for-s3-url>
|
||||||
SESSION_SECRET=whatever_you_want_this_to_be_it_only_matters_for_production
|
SESSION_SECRET=whatever_you_want_this_to_be_it_only_matters_for_production
|
||||||
|
UI_ACCESS_TOKEN_ENABLED=false
|
||||||
|
UPLOAD_LIMIT=250000000
|
||||||
|
|
2
.github/config.yml
vendored
|
@ -10,7 +10,7 @@ newIssueWelcomeComment: >
|
||||||
|
|
||||||
# Comment to be posted to on PRs from first time contributors in your repository
|
# Comment to be posted to on PRs from first time contributors in your repository
|
||||||
newPRWelcomeComment: >
|
newPRWelcomeComment: >
|
||||||
🎉 Thanks for opening this pull request! Please check out our [contributing guidelines](https://github.com/processing/p5.js-web-editor/blob/master/CONTRIBUTING.md) if you haven't already.
|
🎉 Thanks for opening this pull request! Please check out our [contributing guidelines](https://github.com/processing/p5.js-web-editor/blob/master/.github/CONTRIBUTING.md) if you haven't already.
|
||||||
|
|
||||||
# Configuration for first-pr-merge - https://github.com/behaviorbot/first-pr-merge
|
# Configuration for first-pr-merge - https://github.com/behaviorbot/first-pr-merge
|
||||||
|
|
||||||
|
|
1
.nvmrc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
12.16.1
|
|
@ -1,7 +1,7 @@
|
||||||
sudo: required
|
sudo: required
|
||||||
language: node_js
|
language: node_js
|
||||||
node_js:
|
node_js:
|
||||||
- "11.15.0"
|
- "12.16.1"
|
||||||
|
|
||||||
cache:
|
cache:
|
||||||
directories:
|
directories:
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM node:10.15.0 as base
|
FROM node:12.16.1 as base
|
||||||
ENV APP_HOME=/usr/src/app \
|
ENV APP_HOME=/usr/src/app \
|
||||||
TERM=xterm
|
TERM=xterm
|
||||||
RUN mkdir -p $APP_HOME
|
RUN mkdir -p $APP_HOME
|
||||||
|
|
2
app.json
|
@ -16,7 +16,7 @@
|
||||||
],
|
],
|
||||||
"env": {
|
"env": {
|
||||||
"API_URL": {
|
"API_URL": {
|
||||||
"value": "/api"
|
"value": "/editor"
|
||||||
},
|
},
|
||||||
"AWS_ACCESS_KEY": {
|
"AWS_ACCESS_KEY": {
|
||||||
"description": "AWS Access Key",
|
"description": "AWS Access Key",
|
||||||
|
|
24
client/components/AddRemoveButton.jsx
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import InlineSVG from 'react-inlinesvg';
|
||||||
|
|
||||||
|
const addIcon = require('../images/plus.svg');
|
||||||
|
const removeIcon = require('../images/minus.svg');
|
||||||
|
|
||||||
|
const AddRemoveButton = ({ type, onClick }) => {
|
||||||
|
const alt = type === 'add' ? 'add to collection' : 'remove from collection';
|
||||||
|
const icon = type === 'add' ? addIcon : removeIcon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button className="overlay__close-button" onClick={onClick}>
|
||||||
|
<InlineSVG src={icon} alt={alt} />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
AddRemoveButton.propTypes = {
|
||||||
|
type: PropTypes.oneOf(['add', 'remove']).isRequired,
|
||||||
|
onClick: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddRemoveButton;
|
|
@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
import { Link } from 'react-router';
|
import { Link, browserHistory } from 'react-router';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
import InlineSVG from 'react-inlinesvg';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import * as IDEActions from '../modules/IDE/actions/ide';
|
import * as IDEActions from '../modules/IDE/actions/ide';
|
||||||
|
@ -12,6 +12,7 @@ import { setAllAccessibleOutput } from '../modules/IDE/actions/preferences';
|
||||||
import { logoutUser } from '../modules/User/actions';
|
import { logoutUser } from '../modules/User/actions';
|
||||||
|
|
||||||
import { metaKeyName, } from '../utils/metaKey';
|
import { metaKeyName, } from '../utils/metaKey';
|
||||||
|
import caretLeft from '../images/left-arrow.svg';
|
||||||
|
|
||||||
const triangleUrl = require('../images/down-filled-triangle.svg');
|
const triangleUrl = require('../images/down-filled-triangle.svg');
|
||||||
const logoUrl = require('../images/p5js-logo-small.svg');
|
const logoUrl = require('../images/p5js-logo-small.svg');
|
||||||
|
@ -92,11 +93,12 @@ class Nav extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleNew() {
|
handleNew() {
|
||||||
if (!this.props.unsavedChanges) {
|
const { unsavedChanges, warnIfUnsavedChanges } = this.props;
|
||||||
|
if (!unsavedChanges) {
|
||||||
this.props.showToast(1500);
|
this.props.showToast(1500);
|
||||||
this.props.setToastText('Opened new sketch.');
|
this.props.setToastText('Opened new sketch.');
|
||||||
this.props.newProject();
|
this.props.newProject();
|
||||||
} else if (this.props.warnIfUnsavedChanges()) {
|
} else if (warnIfUnsavedChanges && warnIfUnsavedChanges()) {
|
||||||
this.props.showToast(1500);
|
this.props.showToast(1500);
|
||||||
this.props.setToastText('Opened new sketch.');
|
this.props.setToastText('Opened new sketch.');
|
||||||
this.props.newProject();
|
this.props.newProject();
|
||||||
|
@ -165,6 +167,8 @@ class Nav extends React.PureComponent {
|
||||||
|
|
||||||
handleLogout() {
|
handleLogout() {
|
||||||
this.props.logoutUser();
|
this.props.logoutUser();
|
||||||
|
// if you're on the settings page, probably.
|
||||||
|
browserHistory.push('/');
|
||||||
this.setDropdown('none');
|
this.setDropdown('none');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -222,31 +226,26 @@ class Nav extends React.PureComponent {
|
||||||
this.timer = setTimeout(this.setDropdown.bind(this, 'none'), 10);
|
this.timer = setTimeout(this.setDropdown.bind(this, 'none'), 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
renderDashboardMenu(navDropdownState) {
|
||||||
const navDropdownState = {
|
return (
|
||||||
file: classNames({
|
<ul className="nav__items-left" title="project-menu">
|
||||||
'nav__item': true,
|
<li className="nav__item-logo">
|
||||||
'nav__item--open': this.state.dropdownOpen === 'file'
|
<InlineSVG src={logoUrl} alt="p5.js logo" className="svg__logo" />
|
||||||
}),
|
</li>
|
||||||
edit: classNames({
|
<li className="nav__item nav__item--no-icon">
|
||||||
'nav__item': true,
|
<Link to="/" className="nav__back-link">
|
||||||
'nav__item--open': this.state.dropdownOpen === 'edit'
|
<InlineSVG src={caretLeft} className="nav__back-icon" />
|
||||||
}),
|
<span className="nav__item-header">
|
||||||
sketch: classNames({
|
Back to Editor
|
||||||
'nav__item': true,
|
</span>
|
||||||
'nav__item--open': this.state.dropdownOpen === 'sketch'
|
</Link>
|
||||||
}),
|
</li>
|
||||||
help: classNames({
|
</ul>
|
||||||
'nav__item': true,
|
);
|
||||||
'nav__item--open': this.state.dropdownOpen === 'help'
|
}
|
||||||
}),
|
|
||||||
account: classNames({
|
renderProjectMenu(navDropdownState) {
|
||||||
'nav__item': true,
|
|
||||||
'nav__item--open': this.state.dropdownOpen === 'account'
|
|
||||||
})
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<nav className="nav" title="main-navigation" ref={(node) => { this.node = node; }}>
|
|
||||||
<ul className="nav__items-left" title="project-menu">
|
<ul className="nav__items-left" title="project-menu">
|
||||||
<li className="nav__item-logo">
|
<li className="nav__item-logo">
|
||||||
<InlineSVG src={logoUrl} alt="p5.js logo" className="svg__logo" />
|
<InlineSVG src={logoUrl} alt="p5.js logo" className="svg__logo" />
|
||||||
|
@ -327,6 +326,19 @@ class Nav extends React.PureComponent {
|
||||||
Open
|
Open
|
||||||
</Link>
|
</Link>
|
||||||
</li> }
|
</li> }
|
||||||
|
{__process.env.UI_COLLECTIONS_ENABLED &&
|
||||||
|
this.props.user.authenticated &&
|
||||||
|
this.props.project.id &&
|
||||||
|
<li className="nav__dropdown-item">
|
||||||
|
<Link
|
||||||
|
to={`/${this.props.user.username}/sketches/${this.props.project.id}/add-to-collection`}
|
||||||
|
onFocus={this.handleFocusForFile}
|
||||||
|
onBlur={this.handleBlur}
|
||||||
|
onClick={this.setDropdownForNone}
|
||||||
|
>
|
||||||
|
Add to Collection
|
||||||
|
</Link>
|
||||||
|
</li>}
|
||||||
{ __process.env.EXAMPLES_ENABLED &&
|
{ __process.env.EXAMPLES_ENABLED &&
|
||||||
<li className="nav__dropdown-item">
|
<li className="nav__dropdown-item">
|
||||||
<Link
|
<Link
|
||||||
|
@ -523,21 +535,29 @@ class Nav extends React.PureComponent {
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
{ __process.env.LOGIN_ENABLED && !this.props.user.authenticated &&
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderUnauthenticatedUserMenu(navDropdownState) {
|
||||||
|
return (
|
||||||
<ul className="nav__items-right" title="user-menu">
|
<ul className="nav__items-right" title="user-menu">
|
||||||
<li>
|
<li className="nav__item">
|
||||||
<Link to="/login">
|
<Link to="/login">
|
||||||
<span className="nav__item-header">Log in</span>
|
<span className="nav__item-header">Log in</span>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<span className="nav__item-spacer">or</span>
|
<span className="nav__item-spacer">or</span>
|
||||||
<li>
|
<li className="nav__item">
|
||||||
<Link to="/signup">
|
<Link to="/signup">
|
||||||
<span className="nav__item-header">Sign up</span>
|
<span className="nav__item-header">Sign up</span>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
</ul>}
|
</ul>
|
||||||
{ __process.env.LOGIN_ENABLED && this.props.user.authenticated &&
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderAuthenticatedUserMenu(navDropdownState) {
|
||||||
|
return (
|
||||||
<ul className="nav__items-right" title="user-menu">
|
<ul className="nav__items-right" title="user-menu">
|
||||||
<li className="nav__item">
|
<li className="nav__item">
|
||||||
<span>Hello, {this.props.user.username}!</span>
|
<span>Hello, {this.props.user.username}!</span>
|
||||||
|
@ -569,9 +589,21 @@ class Nav extends React.PureComponent {
|
||||||
My sketches
|
My sketches
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
|
{__process.env.UI_COLLECTIONS_ENABLED &&
|
||||||
<li className="nav__dropdown-item">
|
<li className="nav__dropdown-item">
|
||||||
<Link
|
<Link
|
||||||
to="/assets"
|
to={`/${this.props.user.username}/collections`}
|
||||||
|
onFocus={this.handleFocusForAccount}
|
||||||
|
onBlur={this.handleBlur}
|
||||||
|
onClick={this.setDropdownForNone}
|
||||||
|
>
|
||||||
|
My collections
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
<li className="nav__dropdown-item">
|
||||||
|
<Link
|
||||||
|
to={`/${this.props.user.username}/assets`}
|
||||||
onFocus={this.handleFocusForAccount}
|
onFocus={this.handleFocusForAccount}
|
||||||
onBlur={this.handleBlur}
|
onBlur={this.handleBlur}
|
||||||
onClick={this.setDropdownForNone}
|
onClick={this.setDropdownForNone}
|
||||||
|
@ -600,7 +632,61 @@ class Nav extends React.PureComponent {
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul> }
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderUserMenu(navDropdownState) {
|
||||||
|
const isLoginEnabled = __process.env.LOGIN_ENABLED;
|
||||||
|
const isAuthenticated = this.props.user.authenticated;
|
||||||
|
|
||||||
|
if (isLoginEnabled && isAuthenticated) {
|
||||||
|
return this.renderAuthenticatedUserMenu(navDropdownState);
|
||||||
|
} else if (isLoginEnabled && !isAuthenticated) {
|
||||||
|
return this.renderUnauthenticatedUserMenu(navDropdownState);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderLeftLayout(navDropdownState) {
|
||||||
|
switch (this.props.layout) {
|
||||||
|
case 'dashboard':
|
||||||
|
return this.renderDashboardMenu(navDropdownState);
|
||||||
|
case 'project':
|
||||||
|
default:
|
||||||
|
return this.renderProjectMenu(navDropdownState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const navDropdownState = {
|
||||||
|
file: classNames({
|
||||||
|
'nav__item': true,
|
||||||
|
'nav__item--open': this.state.dropdownOpen === 'file'
|
||||||
|
}),
|
||||||
|
edit: classNames({
|
||||||
|
'nav__item': true,
|
||||||
|
'nav__item--open': this.state.dropdownOpen === 'edit'
|
||||||
|
}),
|
||||||
|
sketch: classNames({
|
||||||
|
'nav__item': true,
|
||||||
|
'nav__item--open': this.state.dropdownOpen === 'sketch'
|
||||||
|
}),
|
||||||
|
help: classNames({
|
||||||
|
'nav__item': true,
|
||||||
|
'nav__item--open': this.state.dropdownOpen === 'help'
|
||||||
|
}),
|
||||||
|
account: classNames({
|
||||||
|
'nav__item': true,
|
||||||
|
'nav__item--open': this.state.dropdownOpen === 'account'
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="nav" title="main-navigation" ref={(node) => { this.node = node; }}>
|
||||||
|
{this.renderLeftLayout(navDropdownState)}
|
||||||
|
{this.renderUserMenu(navDropdownState)}
|
||||||
{/*
|
{/*
|
||||||
<div className="nav__announce">
|
<div className="nav__announce">
|
||||||
This is a preview version of the editor, that has not yet been officially released.
|
This is a preview version of the editor, that has not yet been officially released.
|
||||||
|
@ -639,7 +725,7 @@ Nav.propTypes = {
|
||||||
showShareModal: PropTypes.func.isRequired,
|
showShareModal: PropTypes.func.isRequired,
|
||||||
showErrorModal: PropTypes.func.isRequired,
|
showErrorModal: PropTypes.func.isRequired,
|
||||||
unsavedChanges: PropTypes.bool.isRequired,
|
unsavedChanges: PropTypes.bool.isRequired,
|
||||||
warnIfUnsavedChanges: PropTypes.func.isRequired,
|
warnIfUnsavedChanges: PropTypes.func,
|
||||||
showKeyboardShortcutModal: PropTypes.func.isRequired,
|
showKeyboardShortcutModal: PropTypes.func.isRequired,
|
||||||
cmController: PropTypes.shape({
|
cmController: PropTypes.shape({
|
||||||
tidyCode: PropTypes.func,
|
tidyCode: PropTypes.func,
|
||||||
|
@ -653,6 +739,7 @@ Nav.propTypes = {
|
||||||
setAllAccessibleOutput: PropTypes.func.isRequired,
|
setAllAccessibleOutput: PropTypes.func.isRequired,
|
||||||
newFile: PropTypes.func.isRequired,
|
newFile: PropTypes.func.isRequired,
|
||||||
newFolder: PropTypes.func.isRequired,
|
newFolder: PropTypes.func.isRequired,
|
||||||
|
layout: PropTypes.oneOf(['dashboard', 'project']),
|
||||||
rootFile: PropTypes.shape({
|
rootFile: PropTypes.shape({
|
||||||
id: PropTypes.string.isRequired
|
id: PropTypes.string.isRequired
|
||||||
}).isRequired
|
}).isRequired
|
||||||
|
@ -663,7 +750,9 @@ Nav.defaultProps = {
|
||||||
id: undefined,
|
id: undefined,
|
||||||
owner: undefined
|
owner: undefined
|
||||||
},
|
},
|
||||||
cmController: {}
|
cmController: {},
|
||||||
|
layout: 'project',
|
||||||
|
warnIfUnsavedChanges: undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
function mapStateToProps(state) {
|
function mapStateToProps(state) {
|
||||||
|
|
40
client/components/NavBasic.jsx
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import InlineSVG from 'react-inlinesvg';
|
||||||
|
|
||||||
|
const logoUrl = require('../images/p5js-logo-small.svg');
|
||||||
|
const arrowUrl = require('../images/triangle-arrow-left.svg');
|
||||||
|
|
||||||
|
class NavBasic extends React.PureComponent {
|
||||||
|
static defaultProps = {
|
||||||
|
onBack: null
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<nav className="nav" title="main-navigation" ref={(node) => { this.node = node; }}>
|
||||||
|
<ul className="nav__items-left" title="project-menu">
|
||||||
|
<li className="nav__item-logo">
|
||||||
|
<InlineSVG src={logoUrl} alt="p5.js logo" className="svg__logo" />
|
||||||
|
</li>
|
||||||
|
{ this.props.onBack && (
|
||||||
|
<li className="nav__item">
|
||||||
|
<button onClick={this.props.onBack}>
|
||||||
|
<span className="nav__item-header">
|
||||||
|
<InlineSVG src={arrowUrl} alt="Left arrow" />
|
||||||
|
</span>
|
||||||
|
Back to the editor
|
||||||
|
</button>
|
||||||
|
</li>)
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NavBasic.propTypes = {
|
||||||
|
onBack: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NavBasic;
|
27
client/components/createRedirectWithUsername.jsx
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { browserHistory } from 'react-router';
|
||||||
|
|
||||||
|
const RedirectToUser = ({ username, url = '/:username/sketches' }) => {
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (username == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
browserHistory.replace(url.replace(':username', username));
|
||||||
|
}, [username]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
username: state.user ? state.user.username : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConnectedRedirectToUser = connect(mapStateToProps)(RedirectToUser);
|
||||||
|
|
||||||
|
const createRedirectWithUsername = url => props => <ConnectedRedirectToUser {...props} url={url} />;
|
||||||
|
|
||||||
|
export default createRedirectWithUsername;
|
|
@ -20,6 +20,9 @@ export const AUTH_ERROR = 'AUTH_ERROR';
|
||||||
|
|
||||||
export const SETTINGS_UPDATED = 'SETTINGS_UPDATED';
|
export const SETTINGS_UPDATED = 'SETTINGS_UPDATED';
|
||||||
|
|
||||||
|
export const API_KEY_CREATED = 'API_KEY_CREATED';
|
||||||
|
export const API_KEY_REMOVED = 'API_KEY_REMOVED';
|
||||||
|
|
||||||
export const SET_PROJECT_NAME = 'SET_PROJECT_NAME';
|
export const SET_PROJECT_NAME = 'SET_PROJECT_NAME';
|
||||||
export const RENAME_PROJECT = 'RENAME_PROJECT';
|
export const RENAME_PROJECT = 'RENAME_PROJECT';
|
||||||
|
|
||||||
|
@ -33,6 +36,14 @@ export const HIDE_EDIT_PROJECT_NAME = 'HIDE_EDIT_PROJECT_NAME';
|
||||||
export const SET_PROJECT = 'SET_PROJECT';
|
export const SET_PROJECT = 'SET_PROJECT';
|
||||||
export const SET_PROJECTS = 'SET_PROJECTS';
|
export const SET_PROJECTS = 'SET_PROJECTS';
|
||||||
|
|
||||||
|
export const SET_COLLECTIONS = 'SET_COLLECTIONS';
|
||||||
|
export const CREATE_COLLECTION = 'CREATED_COLLECTION';
|
||||||
|
export const DELETE_COLLECTION = 'DELETE_COLLECTION';
|
||||||
|
|
||||||
|
export const ADD_TO_COLLECTION = 'ADD_TO_COLLECTION';
|
||||||
|
export const REMOVE_FROM_COLLECTION = 'REMOVE_FROM_COLLECTION';
|
||||||
|
export const EDIT_COLLECTION = 'EDIT_COLLECTION';
|
||||||
|
|
||||||
export const DELETE_PROJECT = 'DELETE_PROJECT';
|
export const DELETE_PROJECT = 'DELETE_PROJECT';
|
||||||
|
|
||||||
export const SET_SELECTED_FILE = 'SET_SELECTED_FILE';
|
export const SET_SELECTED_FILE = 'SET_SELECTED_FILE';
|
||||||
|
@ -69,6 +80,8 @@ export const SHOW_NEW_FOLDER_MODAL = 'SHOW_NEW_FOLDER_MODAL';
|
||||||
export const CLOSE_NEW_FOLDER_MODAL = 'CLOSE_NEW_FOLDER_MODAL';
|
export const CLOSE_NEW_FOLDER_MODAL = 'CLOSE_NEW_FOLDER_MODAL';
|
||||||
export const SHOW_FOLDER_CHILDREN = 'SHOW_FOLDER_CHILDREN';
|
export const SHOW_FOLDER_CHILDREN = 'SHOW_FOLDER_CHILDREN';
|
||||||
export const HIDE_FOLDER_CHILDREN = 'HIDE_FOLDER_CHILDREN';
|
export const HIDE_FOLDER_CHILDREN = 'HIDE_FOLDER_CHILDREN';
|
||||||
|
export const OPEN_UPLOAD_FILE_MODAL = 'OPEN_UPLOAD_FILE_MODAL';
|
||||||
|
export const CLOSE_UPLOAD_FILE_MODAL = 'CLOSE_UPLOAD_FILE_MODAL';
|
||||||
|
|
||||||
export const SHOW_SHARE_MODAL = 'SHOW_SHARE_MODAL';
|
export const SHOW_SHARE_MODAL = 'SHOW_SHARE_MODAL';
|
||||||
export const CLOSE_SHARE_MODAL = 'CLOSE_SHARE_MODAL';
|
export const CLOSE_SHARE_MODAL = 'CLOSE_SHARE_MODAL';
|
||||||
|
@ -116,6 +129,7 @@ export const CLEAR_PERSISTED_STATE = 'CLEAR_PERSISTED_STATE';
|
||||||
export const HIDE_RUNTIME_ERROR_WARNING = 'HIDE_RUNTIME_ERROR_WARNING';
|
export const HIDE_RUNTIME_ERROR_WARNING = 'HIDE_RUNTIME_ERROR_WARNING';
|
||||||
export const SHOW_RUNTIME_ERROR_WARNING = 'SHOW_RUNTIME_ERROR_WARNING';
|
export const SHOW_RUNTIME_ERROR_WARNING = 'SHOW_RUNTIME_ERROR_WARNING';
|
||||||
export const SET_ASSETS = 'SET_ASSETS';
|
export const SET_ASSETS = 'SET_ASSETS';
|
||||||
|
export const DELETE_ASSET = 'DELETE_ASSET';
|
||||||
|
|
||||||
export const TOGGLE_DIRECTION = 'TOGGLE_DIRECTION';
|
export const TOGGLE_DIRECTION = 'TOGGLE_DIRECTION';
|
||||||
export const SET_SORTING = 'SET_SORTING';
|
export const SET_SORTING = 'SET_SORTING';
|
||||||
|
|
6
client/images/check.svg
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
<svg width="81px" height="65px" viewBox="0 0 81 65" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<path d="M45.437888,42.4740871 L45.437888,-12.5259129 L62.437888,-12.5259129 L62.437888,42.4740871 L62.437888,59.4740871 L18.437888,59.4740871 L18.437888,42.4740871 L45.437888,42.4740871 Z" fill="#D8D8D8" fill-rule="nonzero" transform="translate(40.437888, 23.474087) rotate(42.000000) translate(-40.437888, -23.474087) "></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 583 B |
11
client/images/check_encircled.svg
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 22.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="check-encircled" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px"
|
||||||
|
y="0px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#FFFFFF;}
|
||||||
|
</style>
|
||||||
|
<path id="circle" d="M50,26.5c-14.4,0-26,11.5-25.9,25.9c0,14.5,11.6,26,26,26c14.5,0,25.9-11.5,25.9-26C76,38,64.5,26.5,50,26.5z
|
||||||
|
M47.6,66.6L34,53.4l5.6-5.5l7.1,6.8l14-15.4l6.1,5.5L47.6,66.6z"/>
|
||||||
|
<polygon id="check" class="st0 counter-form" points="46.7,54.7 39.6,47.9 34,53.4 47.6,66.6 66.8,44.8 60.7,39.3 "/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 727 B |
12
client/images/close.svg
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 22.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="close" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
|
||||||
|
<style type="text/css">
|
||||||
|
.st0{fill:#FFFFFF;}
|
||||||
|
</style>
|
||||||
|
<path id="circle" d="M50,26.5c-14.4,0-26,11.5-25.9,25.9c0,14.5,11.6,26,26,26c14.5,0,25.9-11.5,25.9-26C76,38,64.5,26.5,50,26.5z
|
||||||
|
M63.4,60.2L58,65.6l-7.9-8L42,65.7l-5.4-5.4l8.1-8l-8.1-8l5.4-5.4l8,8.1l8-8l5.4,5.4l-8,7.8L63.4,60.2z"/>
|
||||||
|
<polygon id="x" class="st0 counter-form" points="58,39 50,47 42,38.9 36.6,44.3 44.7,52.3 36.6,60.3 42,65.7 50.1,57.6 58,65.6 63.4,60.2
|
||||||
|
55.4,52.2 63.4,44.4 "/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 801 B |
|
@ -1,7 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<svg width="14px" height="9px" viewBox="0 0 14 9" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
<svg width="14px" height="9px" viewBox="0 0 14 9" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
<!-- Generator: Sketch 39 (31667) - http://www.bohemiancoding.com/sketch -->
|
<!-- Generator: Sketch 39 (31667) - http://www.bohemiancoding.com/sketch -->
|
||||||
<title>arrow shape copy 2</title>
|
|
||||||
<!-- <desc>Created with Sketch.</desc> -->
|
<!-- <desc>Created with Sketch.</desc> -->
|
||||||
<defs></defs>
|
<defs></defs>
|
||||||
<g id="IDEs" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="0.539999962">
|
<g id="IDEs" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="0.539999962">
|
||||||
|
|
Before Width: | Height: | Size: 1,017 B After Width: | Height: | Size: 979 B |
|
@ -1,7 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<svg width="10px" height="15px" viewBox="0 0 10 15" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
<svg width="10px" height="15px" viewBox="0 0 10 15" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
<!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
|
<!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
|
||||||
<title>arrow shape copy</title>
|
|
||||||
<!-- <desc>Created with Sketch.</desc> -->
|
<!-- <desc>Created with Sketch.</desc> -->
|
||||||
<defs></defs>
|
<defs></defs>
|
||||||
<g id="IDEs" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="0.539999962">
|
<g id="IDEs" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="0.539999962">
|
||||||
|
|
Before Width: | Height: | Size: 925 B After Width: | Height: | Size: 889 B |
|
@ -1,7 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<svg width="10px" height="15px" viewBox="0 0 10 15" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
<svg width="10px" height="15px" viewBox="0 0 10 15" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
<!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
|
<!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
|
||||||
<title>arrow shape copy</title>
|
|
||||||
<!-- <desc>Created with Sketch.</desc> -->
|
<!-- <desc>Created with Sketch.</desc> -->
|
||||||
<defs></defs>
|
<defs></defs>
|
||||||
<g id="IDEs" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="0.539999962">
|
<g id="IDEs" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="0.539999962">
|
||||||
|
|
Before Width: | Height: | Size: 924 B After Width: | Height: | Size: 888 B |
14
client/images/triangle-arrow-left.svg
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<svg width="10px" height="10px" viewBox="0 0 5 5" version="1.1"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<title>Left Arrow</title>
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<defs></defs>
|
||||||
|
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="environment" transform="translate(-26.000000, -42.000000)" fill="#B5B5B5">
|
||||||
|
<g id="libraries" transform="translate(21.000000, 32.000000)">
|
||||||
|
<polygon id="Triangle-3" transform="translate(7.500000, 12.500000) rotate(270.000000) translate(-7.500000, -12.500000) " points="7.5 10 10 15 5 15"></polygon>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 665 B |
|
@ -1,7 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<svg width="14px" height="9px" viewBox="0 0 14 9" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
<svg width="14px" height="9px" viewBox="0 0 14 9" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
<!-- Generator: Sketch 39 (31667) - http://www.bohemiancoding.com/sketch -->
|
<!-- Generator: Sketch 39 (31667) - http://www.bohemiancoding.com/sketch -->
|
||||||
<title>arrow shape copy</title>
|
|
||||||
<!-- <desc>Created with Sketch.</desc> -->
|
<!-- <desc>Created with Sketch.</desc> -->
|
||||||
<defs></defs>
|
<defs></defs>
|
||||||
<g id="IDEs" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="0.539999962">
|
<g id="IDEs" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="0.539999962">
|
||||||
|
|
Before Width: | Height: | Size: 909 B After Width: | Height: | Size: 873 B |
|
@ -18,7 +18,10 @@ class App extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
if (nextProps.location !== this.props.location) {
|
const locationWillChange = nextProps.location !== this.props.location;
|
||||||
|
const shouldSkipRemembering = nextProps.location.state && nextProps.location.state.skipSavingPath === true;
|
||||||
|
|
||||||
|
if (locationWillChange && !shouldSkipRemembering) {
|
||||||
this.props.setPreviousPath(this.props.location.pathname);
|
this.props.setPreviousPath(this.props.location.pathname);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,7 +45,10 @@ class App extends React.Component {
|
||||||
App.propTypes = {
|
App.propTypes = {
|
||||||
children: PropTypes.element,
|
children: PropTypes.element,
|
||||||
location: PropTypes.shape({
|
location: PropTypes.shape({
|
||||||
pathname: PropTypes.string
|
pathname: PropTypes.string,
|
||||||
|
state: PropTypes.shape({
|
||||||
|
skipSavingPath: PropTypes.bool,
|
||||||
|
}),
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
setPreviousPath: PropTypes.func.isRequired,
|
setPreviousPath: PropTypes.func.isRequired,
|
||||||
theme: PropTypes.string,
|
theme: PropTypes.string,
|
||||||
|
|
|
@ -64,10 +64,12 @@ class Overlay extends React.Component {
|
||||||
const {
|
const {
|
||||||
ariaLabel,
|
ariaLabel,
|
||||||
title,
|
title,
|
||||||
children
|
children,
|
||||||
|
actions,
|
||||||
|
isFixedHeight,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
return (
|
return (
|
||||||
<div className="overlay">
|
<div className={`overlay ${isFixedHeight ? 'overlay--is-fixed-height' : ''}`}>
|
||||||
<div className="overlay__content">
|
<div className="overlay__content">
|
||||||
<section
|
<section
|
||||||
role="main"
|
role="main"
|
||||||
|
@ -77,9 +79,12 @@ class Overlay extends React.Component {
|
||||||
>
|
>
|
||||||
<header className="overlay__header">
|
<header className="overlay__header">
|
||||||
<h2 className="overlay__title">{title}</h2>
|
<h2 className="overlay__title">{title}</h2>
|
||||||
|
<div className="overlay__actions">
|
||||||
|
{actions}
|
||||||
<button className="overlay__close-button" onClick={this.close} >
|
<button className="overlay__close-button" onClick={this.close} >
|
||||||
<InlineSVG src={exitUrl} alt="close overlay" />
|
<InlineSVG src={exitUrl} alt="close overlay" />
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
{children}
|
{children}
|
||||||
</section>
|
</section>
|
||||||
|
@ -91,18 +96,22 @@ class Overlay extends React.Component {
|
||||||
|
|
||||||
Overlay.propTypes = {
|
Overlay.propTypes = {
|
||||||
children: PropTypes.element,
|
children: PropTypes.element,
|
||||||
|
actions: PropTypes.element,
|
||||||
closeOverlay: PropTypes.func,
|
closeOverlay: PropTypes.func,
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
ariaLabel: PropTypes.string,
|
ariaLabel: PropTypes.string,
|
||||||
previousPath: PropTypes.string
|
previousPath: PropTypes.string,
|
||||||
|
isFixedHeight: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
Overlay.defaultProps = {
|
Overlay.defaultProps = {
|
||||||
children: null,
|
children: null,
|
||||||
|
actions: null,
|
||||||
title: 'Modal',
|
title: 'Modal',
|
||||||
closeOverlay: null,
|
closeOverlay: null,
|
||||||
ariaLabel: 'modal',
|
ariaLabel: 'modal',
|
||||||
previousPath: '/'
|
previousPath: '/',
|
||||||
|
isFixedHeight: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Overlay;
|
export default Overlay;
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
const Loader = () => (
|
const Loader = () => (
|
||||||
|
<div className="loader-container">
|
||||||
<div className="loader">
|
<div className="loader">
|
||||||
<div className="loader__circle1" />
|
<div className="loader__circle1" />
|
||||||
<div className="loader__circle2" />
|
<div className="loader__circle2" />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
export default Loader;
|
export default Loader;
|
||||||
|
|
|
@ -30,8 +30,23 @@ export function getAssets() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteAsset(assetKey, userId) {
|
export function deleteAsset(assetKey) {
|
||||||
return {
|
return {
|
||||||
type: 'PLACEHOLDER'
|
type: ActionTypes.DELETE_ASSET,
|
||||||
|
key: assetKey
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteAssetRequest(assetKey) {
|
||||||
|
return (dispatch) => {
|
||||||
|
axios.delete(`${ROOT_URL}/S3/${assetKey}`, { withCredentials: true })
|
||||||
|
.then((response) => {
|
||||||
|
dispatch(deleteAsset(assetKey));
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
dispatch({
|
||||||
|
type: ActionTypes.ERROR
|
||||||
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
173
client/modules/IDE/actions/collections.js
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
import * as ActionTypes from '../../../constants';
|
||||||
|
import { startLoader, stopLoader } from './loader';
|
||||||
|
import { setToastText, showToast } from './toast';
|
||||||
|
|
||||||
|
const __process = (typeof global !== 'undefined' ? global : window).process;
|
||||||
|
const ROOT_URL = __process.env.API_URL;
|
||||||
|
|
||||||
|
const TOAST_DISPLAY_TIME_MS = 1500;
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
export function getCollections(username) {
|
||||||
|
return (dispatch) => {
|
||||||
|
dispatch(startLoader());
|
||||||
|
let url;
|
||||||
|
if (username) {
|
||||||
|
url = `${ROOT_URL}/${username}/collections`;
|
||||||
|
} else {
|
||||||
|
url = `${ROOT_URL}/collections`;
|
||||||
|
}
|
||||||
|
axios.get(url, { withCredentials: true })
|
||||||
|
.then((response) => {
|
||||||
|
dispatch({
|
||||||
|
type: ActionTypes.SET_COLLECTIONS,
|
||||||
|
collections: response.data
|
||||||
|
});
|
||||||
|
dispatch(stopLoader());
|
||||||
|
})
|
||||||
|
.catch((response) => {
|
||||||
|
dispatch({
|
||||||
|
type: ActionTypes.ERROR,
|
||||||
|
error: response.data
|
||||||
|
});
|
||||||
|
dispatch(stopLoader());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createCollection(collection) {
|
||||||
|
return (dispatch) => {
|
||||||
|
dispatch(startLoader());
|
||||||
|
const url = `${ROOT_URL}/collections`;
|
||||||
|
return axios.post(url, collection, { withCredentials: true })
|
||||||
|
.then((response) => {
|
||||||
|
dispatch({
|
||||||
|
type: ActionTypes.CREATE_COLLECTION
|
||||||
|
});
|
||||||
|
dispatch(stopLoader());
|
||||||
|
|
||||||
|
const collectionName = response.data.name;
|
||||||
|
dispatch(setToastText(`Created "${collectionName}"`));
|
||||||
|
dispatch(showToast(TOAST_DISPLAY_TIME_MS));
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
})
|
||||||
|
.catch((response) => {
|
||||||
|
dispatch({
|
||||||
|
type: ActionTypes.ERROR,
|
||||||
|
error: response.data
|
||||||
|
});
|
||||||
|
dispatch(stopLoader());
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addToCollection(collectionId, projectId) {
|
||||||
|
return (dispatch) => {
|
||||||
|
dispatch(startLoader());
|
||||||
|
const url = `${ROOT_URL}/collections/${collectionId}/${projectId}`;
|
||||||
|
return axios.post(url, { withCredentials: true })
|
||||||
|
.then((response) => {
|
||||||
|
dispatch({
|
||||||
|
type: ActionTypes.ADD_TO_COLLECTION,
|
||||||
|
payload: response.data
|
||||||
|
});
|
||||||
|
dispatch(stopLoader());
|
||||||
|
|
||||||
|
const collectionName = response.data.name;
|
||||||
|
|
||||||
|
dispatch(setToastText(`Added to "${collectionName}`));
|
||||||
|
dispatch(showToast(TOAST_DISPLAY_TIME_MS));
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
})
|
||||||
|
.catch((response) => {
|
||||||
|
dispatch({
|
||||||
|
type: ActionTypes.ERROR,
|
||||||
|
error: response.data
|
||||||
|
});
|
||||||
|
dispatch(stopLoader());
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeFromCollection(collectionId, projectId) {
|
||||||
|
return (dispatch) => {
|
||||||
|
dispatch(startLoader());
|
||||||
|
const url = `${ROOT_URL}/collections/${collectionId}/${projectId}`;
|
||||||
|
return axios.delete(url, { withCredentials: true })
|
||||||
|
.then((response) => {
|
||||||
|
dispatch({
|
||||||
|
type: ActionTypes.REMOVE_FROM_COLLECTION,
|
||||||
|
payload: response.data
|
||||||
|
});
|
||||||
|
dispatch(stopLoader());
|
||||||
|
|
||||||
|
const collectionName = response.data.name;
|
||||||
|
|
||||||
|
dispatch(setToastText(`Removed from "${collectionName}`));
|
||||||
|
dispatch(showToast(TOAST_DISPLAY_TIME_MS));
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
})
|
||||||
|
.catch((response) => {
|
||||||
|
dispatch({
|
||||||
|
type: ActionTypes.ERROR,
|
||||||
|
error: response.data
|
||||||
|
});
|
||||||
|
dispatch(stopLoader());
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function editCollection(collectionId, { name, description }) {
|
||||||
|
return (dispatch) => {
|
||||||
|
const url = `${ROOT_URL}/collections/${collectionId}`;
|
||||||
|
return axios.patch(url, { name, description }, { withCredentials: true })
|
||||||
|
.then((response) => {
|
||||||
|
dispatch({
|
||||||
|
type: ActionTypes.EDIT_COLLECTION,
|
||||||
|
payload: response.data
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
})
|
||||||
|
.catch((response) => {
|
||||||
|
dispatch({
|
||||||
|
type: ActionTypes.ERROR,
|
||||||
|
error: response.data
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteCollection(collectionId) {
|
||||||
|
return (dispatch) => {
|
||||||
|
const url = `${ROOT_URL}/collections/${collectionId}`;
|
||||||
|
return axios.delete(url, { withCredentials: true })
|
||||||
|
.then((response) => {
|
||||||
|
dispatch({
|
||||||
|
type: ActionTypes.DELETE_COLLECTION,
|
||||||
|
payload: response.data,
|
||||||
|
collectionId,
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
})
|
||||||
|
.catch((response) => {
|
||||||
|
dispatch({
|
||||||
|
type: ActionTypes.ERROR,
|
||||||
|
error: response.data
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
|
@ -3,7 +3,7 @@ import objectID from 'bson-objectid';
|
||||||
import blobUtil from 'blob-util';
|
import blobUtil from 'blob-util';
|
||||||
import { reset } from 'redux-form';
|
import { reset } from 'redux-form';
|
||||||
import * as ActionTypes from '../../../constants';
|
import * as ActionTypes from '../../../constants';
|
||||||
import { setUnsavedChanges } from './ide';
|
import { setUnsavedChanges, closeNewFolderModal, closeNewFileModal } from './ide';
|
||||||
import { setProjectSavedTime } from './project';
|
import { setProjectSavedTime } from './project';
|
||||||
|
|
||||||
const __process = (typeof global !== 'undefined' ? global : window).process;
|
const __process = (typeof global !== 'undefined' ? global : window).process;
|
||||||
|
@ -58,6 +58,7 @@ export function createFile(formProps) {
|
||||||
parentId
|
parentId
|
||||||
});
|
});
|
||||||
dispatch(setProjectSavedTime(response.data.project.updatedAt));
|
dispatch(setProjectSavedTime(response.data.project.updatedAt));
|
||||||
|
dispatch(closeNewFileModal());
|
||||||
dispatch(reset('new-file'));
|
dispatch(reset('new-file'));
|
||||||
// dispatch({
|
// dispatch({
|
||||||
// type: ActionTypes.HIDE_MODAL
|
// type: ActionTypes.HIDE_MODAL
|
||||||
|
@ -85,6 +86,7 @@ export function createFile(formProps) {
|
||||||
// type: ActionTypes.HIDE_MODAL
|
// type: ActionTypes.HIDE_MODAL
|
||||||
// });
|
// });
|
||||||
dispatch(setUnsavedChanges(true));
|
dispatch(setUnsavedChanges(true));
|
||||||
|
dispatch(closeNewFileModal());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -109,9 +111,7 @@ export function createFolder(formProps) {
|
||||||
parentId
|
parentId
|
||||||
});
|
});
|
||||||
dispatch(setProjectSavedTime(response.data.project.updatedAt));
|
dispatch(setProjectSavedTime(response.data.project.updatedAt));
|
||||||
dispatch({
|
dispatch(closeNewFolderModal());
|
||||||
type: ActionTypes.CLOSE_NEW_FOLDER_MODAL
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.catch(response => dispatch({
|
.catch(response => dispatch({
|
||||||
type: ActionTypes.ERROR,
|
type: ActionTypes.ERROR,
|
||||||
|
@ -130,9 +130,7 @@ export function createFolder(formProps) {
|
||||||
fileType: 'folder',
|
fileType: 'folder',
|
||||||
children: []
|
children: []
|
||||||
});
|
});
|
||||||
dispatch({
|
dispatch(closeNewFolderModal());
|
||||||
type: ActionTypes.CLOSE_NEW_FOLDER_MODAL
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,6 +75,19 @@ export function closeNewFileModal() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function openUploadFileModal(parentId) {
|
||||||
|
return {
|
||||||
|
type: ActionTypes.OPEN_UPLOAD_FILE_MODAL,
|
||||||
|
parentId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeUploadFileModal() {
|
||||||
|
return {
|
||||||
|
type: ActionTypes.CLOSE_UPLOAD_FILE_MODAL
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function expandSidebar() {
|
export function expandSidebar() {
|
||||||
return {
|
return {
|
||||||
type: ActionTypes.EXPAND_SIDEBAR
|
type: ActionTypes.EXPAND_SIDEBAR
|
||||||
|
|
|
@ -26,13 +26,14 @@ export function toggleDirectionForField(field) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setSearchTerm(searchTerm) {
|
export function setSearchTerm(scope, searchTerm) {
|
||||||
return {
|
return {
|
||||||
type: ActionTypes.SET_SEARCH_TERM,
|
type: ActionTypes.SET_SEARCH_TERM,
|
||||||
query: searchTerm
|
query: searchTerm,
|
||||||
|
scope,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resetSearchTerm() {
|
export function resetSearchTerm(scope) {
|
||||||
return setSearchTerm('');
|
return setSearchTerm(scope, '');
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,10 +67,10 @@ export function dropzoneAcceptCallback(userId, file, done) {
|
||||||
})
|
})
|
||||||
.catch((response) => {
|
.catch((response) => {
|
||||||
file.custom_status = 'rejected'; // eslint-disable-line
|
file.custom_status = 'rejected'; // eslint-disable-line
|
||||||
if (response.data.responseText && response.data.responseText.message) {
|
if (response.data && response.data.responseText && response.data.responseText.message) {
|
||||||
done(response.data.responseText.message);
|
done(response.data.responseText.message);
|
||||||
}
|
}
|
||||||
done('error preparing the upload');
|
done('Error: Reached upload limit.');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
165
client/modules/IDE/components/AddToCollectionList.jsx
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
|
||||||
|
import * as ProjectActions from '../actions/project';
|
||||||
|
import * as ProjectsActions from '../actions/projects';
|
||||||
|
import * as CollectionsActions from '../actions/collections';
|
||||||
|
import * as ToastActions from '../actions/toast';
|
||||||
|
import * as SortingActions from '../actions/sorting';
|
||||||
|
import getSortedCollections from '../selectors/collections';
|
||||||
|
import Loader from '../../App/components/loader';
|
||||||
|
import QuickAddList from './QuickAddList';
|
||||||
|
|
||||||
|
const projectInCollection = (project, collection) =>
|
||||||
|
collection.items.find(item => item.project.id === project.id) != null;
|
||||||
|
|
||||||
|
class CollectionList extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
if (props.projectId) {
|
||||||
|
props.getProject(props.projectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.getCollections(this.props.username);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
hasLoadedData: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (prevProps.loading === true && this.props.loading === false) {
|
||||||
|
// eslint-disable-next-line react/no-did-update-set-state
|
||||||
|
this.setState({
|
||||||
|
hasLoadedData: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getTitle() {
|
||||||
|
if (this.props.username === this.props.user.username) {
|
||||||
|
return 'p5.js Web Editor | My collections';
|
||||||
|
}
|
||||||
|
return `p5.js Web Editor | ${this.props.username}'s collections`;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCollectionAdd = (collection) => {
|
||||||
|
this.props.addToCollection(collection.id, this.props.project.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCollectionRemove = (collection) => {
|
||||||
|
this.props.removeFromCollection(collection.id, this.props.project.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { collections, project } = this.props;
|
||||||
|
const hasCollections = collections.length > 0;
|
||||||
|
const collectionWithSketchStatus = collections.map(collection => ({
|
||||||
|
...collection,
|
||||||
|
url: `/${collection.owner.username}/collections/${collection.id}`,
|
||||||
|
isAdded: projectInCollection(project, collection),
|
||||||
|
}));
|
||||||
|
|
||||||
|
let content = null;
|
||||||
|
|
||||||
|
if (this.props.loading && !this.state.hasLoadedData) {
|
||||||
|
content = <Loader />;
|
||||||
|
} else if (hasCollections) {
|
||||||
|
content = (
|
||||||
|
<QuickAddList
|
||||||
|
items={collectionWithSketchStatus}
|
||||||
|
onAdd={this.handleCollectionAdd}
|
||||||
|
onRemove={this.handleCollectionRemove}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
content = 'No collections';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="quick-add-wrapper">
|
||||||
|
<Helmet>
|
||||||
|
<title>{this.getTitle()}</title>
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProjectShape = PropTypes.shape({
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
createdAt: PropTypes.string.isRequired,
|
||||||
|
updatedAt: PropTypes.string.isRequired,
|
||||||
|
user: PropTypes.shape({
|
||||||
|
username: PropTypes.string.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
});
|
||||||
|
|
||||||
|
const ItemsShape = PropTypes.shape({
|
||||||
|
createdAt: PropTypes.string.isRequired,
|
||||||
|
updatedAt: PropTypes.string.isRequired,
|
||||||
|
project: ProjectShape
|
||||||
|
});
|
||||||
|
|
||||||
|
CollectionList.propTypes = {
|
||||||
|
user: PropTypes.shape({
|
||||||
|
username: PropTypes.string,
|
||||||
|
authenticated: PropTypes.bool.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
projectId: PropTypes.string.isRequired,
|
||||||
|
getCollections: PropTypes.func.isRequired,
|
||||||
|
getProject: PropTypes.func.isRequired,
|
||||||
|
addToCollection: PropTypes.func.isRequired,
|
||||||
|
removeFromCollection: PropTypes.func.isRequired,
|
||||||
|
collections: PropTypes.arrayOf(PropTypes.shape({
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
description: PropTypes.string,
|
||||||
|
createdAt: PropTypes.string.isRequired,
|
||||||
|
updatedAt: PropTypes.string.isRequired,
|
||||||
|
items: PropTypes.arrayOf(ItemsShape),
|
||||||
|
})).isRequired,
|
||||||
|
username: PropTypes.string,
|
||||||
|
loading: PropTypes.bool.isRequired,
|
||||||
|
project: PropTypes.shape({
|
||||||
|
id: PropTypes.string,
|
||||||
|
owner: PropTypes.shape({
|
||||||
|
id: PropTypes.string
|
||||||
|
})
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
CollectionList.defaultProps = {
|
||||||
|
project: {
|
||||||
|
id: undefined,
|
||||||
|
owner: undefined
|
||||||
|
},
|
||||||
|
username: undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
function mapStateToProps(state, ownProps) {
|
||||||
|
return {
|
||||||
|
user: state.user,
|
||||||
|
collections: getSortedCollections(state),
|
||||||
|
sorting: state.sorting,
|
||||||
|
loading: state.loading,
|
||||||
|
project: ownProps.project || state.project,
|
||||||
|
projectId: ownProps && ownProps.params ? ownProps.prams.project_id : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return bindActionCreators(
|
||||||
|
Object.assign({}, CollectionsActions, ProjectsActions, ProjectActions, ToastActions, SortingActions),
|
||||||
|
dispatch
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(CollectionList);
|
137
client/modules/IDE/components/AddToCollectionSketchList.jsx
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
// import find from 'lodash/find';
|
||||||
|
import * as ProjectsActions from '../actions/projects';
|
||||||
|
import * as CollectionsActions from '../actions/collections';
|
||||||
|
import * as ToastActions from '../actions/toast';
|
||||||
|
import * as SortingActions from '../actions/sorting';
|
||||||
|
import getSortedSketches from '../selectors/projects';
|
||||||
|
import Loader from '../../App/components/loader';
|
||||||
|
import QuickAddList from './QuickAddList';
|
||||||
|
|
||||||
|
class SketchList extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.props.getProjects(this.props.username);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isInitialDataLoad: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
if (this.props.sketches !== nextProps.sketches && Array.isArray(nextProps.sketches)) {
|
||||||
|
this.setState({
|
||||||
|
isInitialDataLoad: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getSketchesTitle() {
|
||||||
|
if (this.props.username === this.props.user.username) {
|
||||||
|
return 'p5.js Web Editor | My sketches';
|
||||||
|
}
|
||||||
|
return `p5.js Web Editor | ${this.props.username}'s sketches`;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCollectionAdd = (sketch) => {
|
||||||
|
this.props.addToCollection(this.props.collection.id, sketch.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCollectionRemove = (sketch) => {
|
||||||
|
this.props.removeFromCollection(this.props.collection.id, sketch.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
inCollection = sketch => this.props.collection.items.find(item => item.project.id === sketch.id) != null
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const hasSketches = this.props.sketches.length > 0;
|
||||||
|
const sketchesWithAddedStatus = this.props.sketches.map(sketch => ({
|
||||||
|
...sketch,
|
||||||
|
isAdded: this.inCollection(sketch),
|
||||||
|
url: `/${this.props.username}/sketches/${sketch.id}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
let content = null;
|
||||||
|
|
||||||
|
if (this.props.loading && this.state.isInitialDataLoad) {
|
||||||
|
content = <Loader />;
|
||||||
|
} else if (hasSketches) {
|
||||||
|
content = (
|
||||||
|
<QuickAddList
|
||||||
|
items={sketchesWithAddedStatus}
|
||||||
|
onAdd={this.handleCollectionAdd}
|
||||||
|
onRemove={this.handleCollectionRemove}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
content = 'No collections';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="quick-add-wrapper">
|
||||||
|
<Helmet>
|
||||||
|
<title>{this.getSketchesTitle()}</title>
|
||||||
|
</Helmet>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SketchList.propTypes = {
|
||||||
|
user: PropTypes.shape({
|
||||||
|
username: PropTypes.string,
|
||||||
|
authenticated: PropTypes.bool.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
getProjects: PropTypes.func.isRequired,
|
||||||
|
sketches: PropTypes.arrayOf(PropTypes.shape({
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
createdAt: PropTypes.string.isRequired,
|
||||||
|
updatedAt: PropTypes.string.isRequired
|
||||||
|
})).isRequired,
|
||||||
|
collection: PropTypes.shape({
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.shape({
|
||||||
|
project: PropTypes.shape({
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
}).isRequired,
|
||||||
|
username: PropTypes.string,
|
||||||
|
loading: PropTypes.bool.isRequired,
|
||||||
|
sorting: PropTypes.shape({
|
||||||
|
field: PropTypes.string.isRequired,
|
||||||
|
direction: PropTypes.string.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
addToCollection: PropTypes.func.isRequired,
|
||||||
|
removeFromCollection: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
SketchList.defaultProps = {
|
||||||
|
username: undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
user: state.user,
|
||||||
|
sketches: getSortedSketches(state),
|
||||||
|
sorting: state.sorting,
|
||||||
|
loading: state.loading,
|
||||||
|
project: state.project
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return bindActionCreators(
|
||||||
|
Object.assign({}, ProjectsActions, CollectionsActions, ToastActions, SortingActions),
|
||||||
|
dispatch
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(SketchList);
|
|
@ -5,9 +5,146 @@ import { bindActionCreators } from 'redux';
|
||||||
import { Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import prettyBytes from 'pretty-bytes';
|
import prettyBytes from 'pretty-bytes';
|
||||||
|
import InlineSVG from 'react-inlinesvg';
|
||||||
|
|
||||||
import Loader from '../../App/components/loader';
|
import Loader from '../../App/components/loader';
|
||||||
import * as AssetActions from '../actions/assets';
|
import * as AssetActions from '../actions/assets';
|
||||||
|
import downFilledTriangle from '../../../images/down-filled-triangle.svg';
|
||||||
|
|
||||||
|
class AssetListRowBase extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
isFocused: false,
|
||||||
|
optionsOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onFocusComponent = () => {
|
||||||
|
this.setState({ isFocused: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onBlurComponent = () => {
|
||||||
|
this.setState({ isFocused: false });
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.state.isFocused) {
|
||||||
|
this.closeOptions();
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
openOptions = () => {
|
||||||
|
this.setState({
|
||||||
|
optionsOpen: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
closeOptions = () => {
|
||||||
|
this.setState({
|
||||||
|
optionsOpen: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleOptions = () => {
|
||||||
|
if (this.state.optionsOpen) {
|
||||||
|
this.closeOptions();
|
||||||
|
} else {
|
||||||
|
this.openOptions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDropdownOpen = () => {
|
||||||
|
this.closeOptions();
|
||||||
|
this.openOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAssetDelete = () => {
|
||||||
|
const { key, name } = this.props.asset;
|
||||||
|
this.closeOptions();
|
||||||
|
if (window.confirm(`Are you sure you want to delete "${name}"?`)) {
|
||||||
|
this.props.deleteAssetRequest(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { asset, username } = this.props;
|
||||||
|
const { optionsOpen } = this.state;
|
||||||
|
return (
|
||||||
|
<tr className="asset-table__row" key={asset.key}>
|
||||||
|
<th scope="row">
|
||||||
|
<Link to={asset.url} target="_blank">
|
||||||
|
{asset.name}
|
||||||
|
</Link>
|
||||||
|
</th>
|
||||||
|
<td>{prettyBytes(asset.size)}</td>
|
||||||
|
<td>
|
||||||
|
{ asset.sketchId && <Link to={`/${username}/sketches/${asset.sketchId}`}>{asset.sketchName}</Link> }
|
||||||
|
</td>
|
||||||
|
<td className="asset-table__dropdown-column">
|
||||||
|
<button
|
||||||
|
className="asset-table__dropdown-button"
|
||||||
|
onClick={this.toggleOptions}
|
||||||
|
onBlur={this.onBlurComponent}
|
||||||
|
onFocus={this.onFocusComponent}
|
||||||
|
>
|
||||||
|
<InlineSVG src={downFilledTriangle} alt="Menu" />
|
||||||
|
</button>
|
||||||
|
{optionsOpen &&
|
||||||
|
<ul
|
||||||
|
className="asset-table__action-dialogue"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
className="asset-table__action-option"
|
||||||
|
onClick={this.handleAssetDelete}
|
||||||
|
onBlur={this.onBlurComponent}
|
||||||
|
onFocus={this.onFocusComponent}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
to={asset.url}
|
||||||
|
target="_blank"
|
||||||
|
onBlur={this.onBlurComponent}
|
||||||
|
onFocus={this.onFocusComponent}
|
||||||
|
className="asset-table__action-option"
|
||||||
|
>
|
||||||
|
Open in New Tab
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
</ul>}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AssetListRowBase.propTypes = {
|
||||||
|
asset: PropTypes.shape({
|
||||||
|
key: PropTypes.string.isRequired,
|
||||||
|
url: PropTypes.string.isRequired,
|
||||||
|
sketchId: PropTypes.string,
|
||||||
|
sketchName: PropTypes.string,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
size: PropTypes.number.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
deleteAssetRequest: PropTypes.func.isRequired,
|
||||||
|
username: PropTypes.string.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
function mapStateToPropsAssetListRow(state) {
|
||||||
|
return {
|
||||||
|
username: state.user.username
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToPropsAssetListRow(dispatch) {
|
||||||
|
return bindActionCreators(AssetActions, dispatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
const AssetListRow = connect(mapStateToPropsAssetListRow, mapDispatchToPropsAssetListRow)(AssetListRowBase);
|
||||||
|
|
||||||
class AssetList extends React.Component {
|
class AssetList extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -16,11 +153,8 @@ class AssetList extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
getAssetsTitle() {
|
getAssetsTitle() {
|
||||||
if (!this.props.username || this.props.username === this.props.user.username) {
|
|
||||||
return 'p5.js Web Editor | My assets';
|
return 'p5.js Web Editor | My assets';
|
||||||
}
|
}
|
||||||
return `p5.js Web Editor | ${this.props.username}'s assets`;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasAssets() {
|
hasAssets() {
|
||||||
return !this.props.loading && this.props.assetList.length > 0;
|
return !this.props.loading && this.props.assetList.length > 0;
|
||||||
|
@ -39,14 +173,9 @@ class AssetList extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const username = this.props.username !== undefined ? this.props.username : this.props.user.username;
|
const { assetList } = this.props;
|
||||||
const { assetList, totalSize } = this.props;
|
|
||||||
return (
|
return (
|
||||||
<div className="asset-table-container">
|
<div className="asset-table-container">
|
||||||
{/* Eventually, this copy should be Total / 250 MB Used */}
|
|
||||||
{this.hasAssets() &&
|
|
||||||
<p className="asset-table__total">{`${prettyBytes(totalSize)} Total`}</p>
|
|
||||||
}
|
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{this.getAssetsTitle()}</title>
|
<title>{this.getAssetsTitle()}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
|
@ -58,20 +187,12 @@ class AssetList extends React.Component {
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Size</th>
|
<th>Size</th>
|
||||||
<th>View</th>
|
|
||||||
<th>Sketch</th>
|
<th>Sketch</th>
|
||||||
|
<th scope="col"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{assetList.map(asset =>
|
{assetList.map(asset => <AssetListRow asset={asset} key={asset.key} />)}
|
||||||
(
|
|
||||||
<tr className="asset-table__row" key={asset.key}>
|
|
||||||
<td>{asset.name}</td>
|
|
||||||
<td>{prettyBytes(asset.size)}</td>
|
|
||||||
<td><Link to={asset.url} target="_blank">View</Link></td>
|
|
||||||
<td><Link to={`/${username}/sketches/${asset.sketchId}`}>{asset.sketchName}</Link></td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>}
|
</table>}
|
||||||
</div>
|
</div>
|
||||||
|
@ -83,15 +204,13 @@ AssetList.propTypes = {
|
||||||
user: PropTypes.shape({
|
user: PropTypes.shape({
|
||||||
username: PropTypes.string
|
username: PropTypes.string
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
username: PropTypes.string.isRequired,
|
|
||||||
assetList: PropTypes.arrayOf(PropTypes.shape({
|
assetList: PropTypes.arrayOf(PropTypes.shape({
|
||||||
key: PropTypes.string.isRequired,
|
key: PropTypes.string.isRequired,
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
url: PropTypes.string.isRequired,
|
url: PropTypes.string.isRequired,
|
||||||
sketchName: PropTypes.string.isRequired,
|
sketchName: PropTypes.string,
|
||||||
sketchId: PropTypes.string.isRequired
|
sketchId: PropTypes.string
|
||||||
})).isRequired,
|
})).isRequired,
|
||||||
totalSize: PropTypes.number.isRequired,
|
|
||||||
getAssets: PropTypes.func.isRequired,
|
getAssets: PropTypes.func.isRequired,
|
||||||
loading: PropTypes.bool.isRequired
|
loading: PropTypes.bool.isRequired
|
||||||
};
|
};
|
||||||
|
@ -100,7 +219,6 @@ function mapStateToProps(state) {
|
||||||
return {
|
return {
|
||||||
user: state.user,
|
user: state.user,
|
||||||
assetList: state.assets.list,
|
assetList: state.assets.list,
|
||||||
totalSize: state.assets.totalSize,
|
|
||||||
loading: state.loading
|
loading: state.loading
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
51
client/modules/IDE/components/AssetSize.jsx
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import prettyBytes from 'pretty-bytes';
|
||||||
|
|
||||||
|
const __process = (typeof global !== 'undefined' ? global : window).process;
|
||||||
|
const limit = __process.env.UPLOAD_LIMIT || 250000000;
|
||||||
|
const MAX_SIZE_B = limit;
|
||||||
|
|
||||||
|
const formatPercent = (percent) => {
|
||||||
|
const percentUsed = percent * 100;
|
||||||
|
if (percentUsed < 1) {
|
||||||
|
return '0%';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${Math.round(percentUsed)}%`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Eventually, this copy should be Total / 250 MB Used */
|
||||||
|
const AssetSize = ({ totalSize }) => {
|
||||||
|
if (totalSize === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentSize = prettyBytes(totalSize);
|
||||||
|
const sizeLimit = prettyBytes(MAX_SIZE_B);
|
||||||
|
const percentValue = totalSize / MAX_SIZE_B;
|
||||||
|
const percent = formatPercent(percentValue);
|
||||||
|
const percentSize = percentValue < 1 ? percentValue : 1;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="asset-size" style={{ '--percent': percentSize }}>
|
||||||
|
<div className="asset-size-bar" />
|
||||||
|
<p className="asset-current">{currentSize} ({percent})</p>
|
||||||
|
<p className="asset-max">Max: {sizeLimit}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
AssetSize.propTypes = {
|
||||||
|
totalSize: PropTypes.number.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
user: state.user,
|
||||||
|
totalSize: state.user.totalSize || state.assets.totalSize,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(AssetSize);
|
221
client/modules/IDE/components/CollectionList/CollectionList.jsx
Normal file
|
@ -0,0 +1,221 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
import InlineSVG from 'react-inlinesvg';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import find from 'lodash/find';
|
||||||
|
import * as ProjectActions from '../../actions/project';
|
||||||
|
import * as ProjectsActions from '../../actions/projects';
|
||||||
|
import * as CollectionsActions from '../../actions/collections';
|
||||||
|
import * as ToastActions from '../../actions/toast';
|
||||||
|
import * as SortingActions from '../../actions/sorting';
|
||||||
|
import getSortedCollections from '../../selectors/collections';
|
||||||
|
import Loader from '../../../App/components/loader';
|
||||||
|
import Overlay from '../../../App/components/Overlay';
|
||||||
|
import AddToCollectionSketchList from '../AddToCollectionSketchList';
|
||||||
|
import { SketchSearchbar } from '../Searchbar';
|
||||||
|
|
||||||
|
import CollectionListRow from './CollectionListRow';
|
||||||
|
|
||||||
|
const arrowUp = require('../../../../images/sort-arrow-up.svg');
|
||||||
|
const arrowDown = require('../../../../images/sort-arrow-down.svg');
|
||||||
|
|
||||||
|
class CollectionList extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
if (props.projectId) {
|
||||||
|
props.getProject(props.projectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.getCollections(this.props.username);
|
||||||
|
this.props.resetSorting();
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
hasLoadedData: false,
|
||||||
|
addingSketchesToCollectionId: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps, prevState) {
|
||||||
|
if (prevProps.loading === true && this.props.loading === false) {
|
||||||
|
// eslint-disable-next-line react/no-did-update-set-state
|
||||||
|
this.setState({
|
||||||
|
hasLoadedData: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getTitle() {
|
||||||
|
if (this.props.username === this.props.user.username) {
|
||||||
|
return 'p5.js Web Editor | My collections';
|
||||||
|
}
|
||||||
|
return `p5.js Web Editor | ${this.props.username}'s collections`;
|
||||||
|
}
|
||||||
|
|
||||||
|
showAddSketches = (collectionId) => {
|
||||||
|
this.setState({
|
||||||
|
addingSketchesToCollectionId: collectionId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
hideAddSketches = () => {
|
||||||
|
this.setState({
|
||||||
|
addingSketchesToCollectionId: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
hasCollections() {
|
||||||
|
return (!this.props.loading || this.state.hasLoadedData) && this.props.collections.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderLoader() {
|
||||||
|
if (this.props.loading && !this.state.hasLoadedData) return <Loader />;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderEmptyTable() {
|
||||||
|
if (!this.props.loading && this.props.collections.length === 0) {
|
||||||
|
return (<p className="sketches-table__empty">No collections.</p>);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderFieldHeader = (fieldName, displayName) => {
|
||||||
|
const { field, direction } = this.props.sorting;
|
||||||
|
const headerClass = classNames({
|
||||||
|
'sketches-table__header': true,
|
||||||
|
'sketches-table__header--selected': field === fieldName
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<th scope="col">
|
||||||
|
<button className="sketch-list__sort-button" onClick={() => this.props.toggleDirectionForField(fieldName)}>
|
||||||
|
<span className={headerClass}>{displayName}</span>
|
||||||
|
{field === fieldName && direction === SortingActions.DIRECTION.ASC &&
|
||||||
|
<InlineSVG src={arrowUp} />
|
||||||
|
}
|
||||||
|
{field === fieldName && direction === SortingActions.DIRECTION.DESC &&
|
||||||
|
<InlineSVG src={arrowDown} />
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</th>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const username = this.props.username !== undefined ? this.props.username : this.props.user.username;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="sketches-table-container">
|
||||||
|
<Helmet>
|
||||||
|
<title>{this.getTitle()}</title>
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
|
{this._renderLoader()}
|
||||||
|
{this._renderEmptyTable()}
|
||||||
|
{this.hasCollections() &&
|
||||||
|
<table className="sketches-table" summary="table containing all collections">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{this._renderFieldHeader('name', 'Name')}
|
||||||
|
{this._renderFieldHeader('createdAt', 'Date Created')}
|
||||||
|
{this._renderFieldHeader('updatedAt', 'Date Updated')}
|
||||||
|
{this._renderFieldHeader('numItems', '# sketches')}
|
||||||
|
<th scope="col"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{this.props.collections.map(collection =>
|
||||||
|
(<CollectionListRow
|
||||||
|
key={collection.id}
|
||||||
|
collection={collection}
|
||||||
|
user={this.props.user}
|
||||||
|
username={username}
|
||||||
|
project={this.props.project}
|
||||||
|
onAddSketches={() => this.showAddSketches(collection.id)}
|
||||||
|
/>))}
|
||||||
|
</tbody>
|
||||||
|
</table>}
|
||||||
|
{
|
||||||
|
this.state.addingSketchesToCollectionId && (
|
||||||
|
<Overlay
|
||||||
|
title="Add sketch"
|
||||||
|
actions={<SketchSearchbar />}
|
||||||
|
closeOverlay={this.hideAddSketches}
|
||||||
|
isFixedHeight
|
||||||
|
>
|
||||||
|
<div className="collection-add-sketch">
|
||||||
|
<AddToCollectionSketchList
|
||||||
|
username={this.props.username}
|
||||||
|
collection={find(this.props.collections, { id: this.state.addingSketchesToCollectionId })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Overlay>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectionList.propTypes = {
|
||||||
|
user: PropTypes.shape({
|
||||||
|
username: PropTypes.string,
|
||||||
|
authenticated: PropTypes.bool.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
projectId: PropTypes.string,
|
||||||
|
getCollections: PropTypes.func.isRequired,
|
||||||
|
getProject: PropTypes.func.isRequired,
|
||||||
|
collections: PropTypes.arrayOf(PropTypes.shape({
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
description: PropTypes.string,
|
||||||
|
createdAt: PropTypes.string.isRequired,
|
||||||
|
updatedAt: PropTypes.string.isRequired,
|
||||||
|
})).isRequired,
|
||||||
|
username: PropTypes.string,
|
||||||
|
loading: PropTypes.bool.isRequired,
|
||||||
|
toggleDirectionForField: PropTypes.func.isRequired,
|
||||||
|
resetSorting: PropTypes.func.isRequired,
|
||||||
|
sorting: PropTypes.shape({
|
||||||
|
field: PropTypes.string.isRequired,
|
||||||
|
direction: PropTypes.string.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
project: PropTypes.shape({
|
||||||
|
id: PropTypes.string,
|
||||||
|
owner: PropTypes.shape({
|
||||||
|
id: PropTypes.string
|
||||||
|
})
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
CollectionList.defaultProps = {
|
||||||
|
projectId: undefined,
|
||||||
|
project: {
|
||||||
|
id: undefined,
|
||||||
|
owner: undefined
|
||||||
|
},
|
||||||
|
username: undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
function mapStateToProps(state, ownProps) {
|
||||||
|
return {
|
||||||
|
user: state.user,
|
||||||
|
collections: getSortedCollections(state),
|
||||||
|
sorting: state.sorting,
|
||||||
|
loading: state.loading,
|
||||||
|
project: state.project,
|
||||||
|
projectId: ownProps && ownProps.params ? ownProps.params.project_id : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return bindActionCreators(
|
||||||
|
Object.assign({}, CollectionsActions, ProjectsActions, ProjectActions, ToastActions, SortingActions),
|
||||||
|
dispatch
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(CollectionList);
|
|
@ -0,0 +1,254 @@
|
||||||
|
import format from 'date-fns/format';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import InlineSVG from 'react-inlinesvg';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { Link } from 'react-router';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import * as ProjectActions from '../../actions/project';
|
||||||
|
import * as CollectionsActions from '../../actions/collections';
|
||||||
|
import * as IdeActions from '../../actions/ide';
|
||||||
|
import * as ToastActions from '../../actions/toast';
|
||||||
|
|
||||||
|
const downFilledTriangle = require('../../../../images/down-filled-triangle.svg');
|
||||||
|
|
||||||
|
class CollectionListRowBase extends React.Component {
|
||||||
|
static projectInCollection(project, collection) {
|
||||||
|
return collection.items.find(item => item.project.id === project.id) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
optionsOpen: false,
|
||||||
|
isFocused: false,
|
||||||
|
renameOpen: false,
|
||||||
|
renameValue: '',
|
||||||
|
};
|
||||||
|
this.renameInput = React.createRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
onFocusComponent = () => {
|
||||||
|
this.setState({ isFocused: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onBlurComponent = () => {
|
||||||
|
this.setState({ isFocused: false });
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.state.isFocused) {
|
||||||
|
this.closeAll();
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
openOptions = () => {
|
||||||
|
this.setState({
|
||||||
|
optionsOpen: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
closeOptions = () => {
|
||||||
|
this.setState({
|
||||||
|
optionsOpen: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleOptions = () => {
|
||||||
|
if (this.state.optionsOpen) {
|
||||||
|
this.closeOptions();
|
||||||
|
} else {
|
||||||
|
this.openOptions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closeAll = () => {
|
||||||
|
this.setState({
|
||||||
|
optionsOpen: false,
|
||||||
|
renameOpen: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAddSketches = () => {
|
||||||
|
this.closeAll();
|
||||||
|
this.props.onAddSketches();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDropdownOpen = () => {
|
||||||
|
this.closeAll();
|
||||||
|
this.openOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCollectionDelete = () => {
|
||||||
|
this.closeAll();
|
||||||
|
if (window.confirm(`Are you sure you want to delete "${this.props.collection.name}"?`)) {
|
||||||
|
this.props.deleteCollection(this.props.collection.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRenameOpen = () => {
|
||||||
|
this.closeAll();
|
||||||
|
this.setState({
|
||||||
|
renameOpen: true,
|
||||||
|
renameValue: this.props.collection.name,
|
||||||
|
}, () => this.renameInput.current.focus());
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRenameChange = (e) => {
|
||||||
|
this.setState({
|
||||||
|
renameValue: e.target.value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRenameEnter = (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
this.updateName();
|
||||||
|
this.closeAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRenameBlur = () => {
|
||||||
|
this.updateName();
|
||||||
|
this.closeAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateName = () => {
|
||||||
|
const isValid = this.state.renameValue.trim().length !== 0;
|
||||||
|
if (isValid) {
|
||||||
|
this.props.editCollection(this.props.collection.id, { name: this.state.renameValue.trim() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderActions = () => {
|
||||||
|
const { optionsOpen } = this.state;
|
||||||
|
const userIsOwner = this.props.user.username === this.props.username;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<button
|
||||||
|
className="sketch-list__dropdown-button"
|
||||||
|
onClick={this.toggleOptions}
|
||||||
|
onBlur={this.onBlurComponent}
|
||||||
|
onFocus={this.onFocusComponent}
|
||||||
|
>
|
||||||
|
<InlineSVG src={downFilledTriangle} alt="Menu" />
|
||||||
|
</button>
|
||||||
|
{optionsOpen &&
|
||||||
|
<ul
|
||||||
|
className="sketch-list__action-dialogue"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
className="sketch-list__action-option"
|
||||||
|
onClick={this.handleAddSketches}
|
||||||
|
onBlur={this.onBlurComponent}
|
||||||
|
onFocus={this.onFocusComponent}
|
||||||
|
>
|
||||||
|
Add sketch
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{userIsOwner &&
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
className="sketch-list__action-option"
|
||||||
|
onClick={this.handleCollectionDelete}
|
||||||
|
onBlur={this.onBlurComponent}
|
||||||
|
onFocus={this.onFocusComponent}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</li>}
|
||||||
|
{userIsOwner &&
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
className="sketch-list__action-option"
|
||||||
|
onClick={this.handleRenameOpen}
|
||||||
|
onBlur={this.onBlurComponent}
|
||||||
|
onFocus={this.onFocusComponent}
|
||||||
|
>
|
||||||
|
Rename
|
||||||
|
</button>
|
||||||
|
</li>}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCollectionName = () => {
|
||||||
|
const { collection, username } = this.props;
|
||||||
|
const { renameOpen, renameValue } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<Link to={{ pathname: `/${username}/collections/${collection.id}`, state: { skipSavingPath: true } }}>
|
||||||
|
{renameOpen ? '' : collection.name}
|
||||||
|
</Link>
|
||||||
|
{renameOpen
|
||||||
|
&&
|
||||||
|
<input
|
||||||
|
value={renameValue}
|
||||||
|
onChange={this.handleRenameChange}
|
||||||
|
onKeyUp={this.handleRenameEnter}
|
||||||
|
onBlur={this.handleRenameBlur}
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
ref={this.renameInput}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { collection } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
className="sketches-table__row"
|
||||||
|
key={collection.id}
|
||||||
|
>
|
||||||
|
<th scope="row">
|
||||||
|
<span className="sketches-table__name">
|
||||||
|
{this.renderCollectionName()}
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
<td>{format(new Date(collection.createdAt), 'MMM D, YYYY')}</td>
|
||||||
|
<td>{format(new Date(collection.updatedAt), 'MMM D, YYYY')}</td>
|
||||||
|
<td>{(collection.items || []).length}</td>
|
||||||
|
<td className="sketch-list__dropdown-column">
|
||||||
|
{this.renderActions()}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectionListRowBase.propTypes = {
|
||||||
|
collection: PropTypes.shape({
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
owner: PropTypes.shape({
|
||||||
|
username: PropTypes.string.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
createdAt: PropTypes.string.isRequired,
|
||||||
|
updatedAt: PropTypes.string.isRequired,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.shape({
|
||||||
|
project: PropTypes.shape({
|
||||||
|
id: PropTypes.string.isRequired
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
}).isRequired,
|
||||||
|
username: PropTypes.string.isRequired,
|
||||||
|
user: PropTypes.shape({
|
||||||
|
username: PropTypes.string,
|
||||||
|
authenticated: PropTypes.bool.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
deleteCollection: PropTypes.func.isRequired,
|
||||||
|
editCollection: PropTypes.func.isRequired,
|
||||||
|
onAddSketches: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
function mapDispatchToPropsSketchListRow(dispatch) {
|
||||||
|
return bindActionCreators(Object.assign({}, CollectionsActions, ProjectActions, IdeActions, ToastActions), dispatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(null, mapDispatchToPropsSketchListRow)(CollectionListRowBase);
|
1
client/modules/IDE/components/CollectionList/index.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './CollectionList';
|
93
client/modules/IDE/components/EditableInput.jsx
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import InlineSVG from 'react-inlinesvg';
|
||||||
|
|
||||||
|
const editIconUrl = require('../../../images/pencil.svg');
|
||||||
|
|
||||||
|
function EditIcon() {
|
||||||
|
return <InlineSVG className="editable-input__icon" src={editIconUrl} alt="Edit" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EditableInput({
|
||||||
|
validate, value, emptyPlaceholder, InputComponent, inputProps, onChange
|
||||||
|
}) {
|
||||||
|
const [isEditing, setIsEditing] = React.useState(false);
|
||||||
|
const [currentValue, setCurrentValue] = React.useState(value || '');
|
||||||
|
const displayValue = currentValue || emptyPlaceholder;
|
||||||
|
const hasValue = currentValue !== '';
|
||||||
|
const classes = `editable-input editable-input--${isEditing ? 'is-editing' : 'is-not-editing'} editable-input--${hasValue ? 'has-value' : 'has-placeholder'}`;
|
||||||
|
const inputRef = React.createRef();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (isEditing) {
|
||||||
|
inputRef.current.focus();
|
||||||
|
}
|
||||||
|
}, [isEditing]);
|
||||||
|
|
||||||
|
function beginEditing() {
|
||||||
|
setIsEditing(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function doneEditing() {
|
||||||
|
setIsEditing(false);
|
||||||
|
|
||||||
|
const isValid = typeof validate === 'function' && validate(currentValue);
|
||||||
|
|
||||||
|
if (isValid) {
|
||||||
|
onChange(currentValue);
|
||||||
|
} else {
|
||||||
|
setCurrentValue(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateValue(event) {
|
||||||
|
setCurrentValue(event.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkForKeyAction(event) {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
doneEditing();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={classes}>
|
||||||
|
<button className="editable-input__label" onClick={beginEditing}>
|
||||||
|
<span>{displayValue}</span>
|
||||||
|
<EditIcon />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<InputComponent
|
||||||
|
className="editable-input__input"
|
||||||
|
type="text"
|
||||||
|
{...inputProps}
|
||||||
|
disabled={!isEditing}
|
||||||
|
onBlur={doneEditing}
|
||||||
|
onChange={updateValue}
|
||||||
|
onKeyPress={checkForKeyAction}
|
||||||
|
ref={inputRef}
|
||||||
|
value={currentValue}
|
||||||
|
/>
|
||||||
|
</span >
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
EditableInput.defaultProps = {
|
||||||
|
emptyPlaceholder: 'No value',
|
||||||
|
InputComponent: 'input',
|
||||||
|
inputProps: {},
|
||||||
|
validate: () => true,
|
||||||
|
value: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
EditableInput.propTypes = {
|
||||||
|
emptyPlaceholder: PropTypes.string,
|
||||||
|
InputComponent: PropTypes.elementType,
|
||||||
|
// eslint-disable-next-line react/forbid-prop-types
|
||||||
|
inputProps: PropTypes.object,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
validate: PropTypes.func,
|
||||||
|
value: PropTypes.string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditableInput;
|
|
@ -160,6 +160,7 @@ export class FileNode extends React.Component {
|
||||||
type="text"
|
type="text"
|
||||||
className="sidebar__file-item-input"
|
className="sidebar__file-item-input"
|
||||||
value={this.props.name}
|
value={this.props.name}
|
||||||
|
maxLength="128"
|
||||||
onChange={this.handleFileNameChange}
|
onChange={this.handleFileNameChange}
|
||||||
ref={(element) => { this.fileNameInput = element; }}
|
ref={(element) => { this.fileNameInput = element; }}
|
||||||
onBlur={() => {
|
onBlur={() => {
|
||||||
|
|
|
@ -30,7 +30,7 @@ class FileUploader extends React.Component {
|
||||||
thumbnailWidth: 200,
|
thumbnailWidth: 200,
|
||||||
thumbnailHeight: 200,
|
thumbnailHeight: 200,
|
||||||
acceptedFiles: fileExtensionsAndMimeTypes,
|
acceptedFiles: fileExtensionsAndMimeTypes,
|
||||||
dictDefaultMessage: 'Drop files here to upload or click to use the file browser',
|
dictDefaultMessage: 'Drop files here or click to use the file browser',
|
||||||
accept: this.props.dropzoneAcceptCallback.bind(this, userId),
|
accept: this.props.dropzoneAcceptCallback.bind(this, userId),
|
||||||
sending: this.props.dropzoneSendingCallback,
|
sending: this.props.dropzoneSendingCallback,
|
||||||
complete: this.props.dropzoneCompleteCallback
|
complete: this.props.dropzoneCompleteCallback
|
||||||
|
|
|
@ -22,16 +22,19 @@ class NewFileForm extends React.Component {
|
||||||
handleSubmit(this.createFile)(data);
|
handleSubmit(this.createFile)(data);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<div className="new-file-form__input-wrapper">
|
||||||
<label className="new-file-form__name-label" htmlFor="name">Name:</label>
|
<label className="new-file-form__name-label" htmlFor="name">Name:</label>
|
||||||
<input
|
<input
|
||||||
className="new-file-form__name-input"
|
className="new-file-form__name-input"
|
||||||
id="name"
|
id="name"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Name"
|
placeholder="Name"
|
||||||
|
maxLength="128"
|
||||||
{...domOnlyProps(name)}
|
{...domOnlyProps(name)}
|
||||||
ref={(element) => { this.fileName = element; }}
|
ref={(element) => { this.fileName = element; }}
|
||||||
/>
|
/>
|
||||||
<input type="submit" value="Add File" aria-label="add file" />
|
<input type="submit" value="Add File" aria-label="add file" />
|
||||||
|
</div>
|
||||||
{name.touched && name.error && <span className="form-error">{name.error}</span>}
|
{name.touched && name.error && <span className="form-error">{name.error}</span>}
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { bindActionCreators, compose } from 'redux';
|
||||||
import { reduxForm } from 'redux-form';
|
import { reduxForm } from 'redux-form';
|
||||||
import classNames from 'classnames';
|
|
||||||
import InlineSVG from 'react-inlinesvg';
|
import InlineSVG from 'react-inlinesvg';
|
||||||
import NewFileForm from './NewFileForm';
|
import NewFileForm from './NewFileForm';
|
||||||
import FileUploader from './FileUploader';
|
import { closeNewFileModal } from '../actions/ide';
|
||||||
|
import { createFile } from '../actions/files';
|
||||||
import { CREATE_FILE_REGEX } from '../../../../server/utils/fileUtils';
|
import { CREATE_FILE_REGEX } from '../../../../server/utils/fileUtils';
|
||||||
|
|
||||||
const exitUrl = require('../../../images/exit.svg');
|
const exitUrl = require('../../../images/exit.svg');
|
||||||
|
@ -28,16 +30,12 @@ class NewFileModal extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const modalClass = classNames({
|
|
||||||
'modal': true,
|
|
||||||
'modal--reduced': !this.props.canUploadMedia
|
|
||||||
});
|
|
||||||
return (
|
return (
|
||||||
<section className={modalClass} ref={(element) => { this.modal = element; }}>
|
<section className="modal" ref={(element) => { this.modal = element; }}>
|
||||||
<div className="modal-content">
|
<div className="modal-content">
|
||||||
<div className="modal__header">
|
<div className="modal__header">
|
||||||
<h2 className="modal__title">Add File</h2>
|
<h2 className="modal__title">Create File</h2>
|
||||||
<button className="modal__exit-button" onClick={this.props.closeModal}>
|
<button className="modal__exit-button" onClick={this.props.closeNewFileModal}>
|
||||||
<InlineSVG src={exitUrl} alt="Close New File Modal" />
|
<InlineSVG src={exitUrl} alt="Close New File Modal" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -45,17 +43,6 @@ class NewFileModal extends React.Component {
|
||||||
focusOnModal={this.focusOnModal}
|
focusOnModal={this.focusOnModal}
|
||||||
{...this.props}
|
{...this.props}
|
||||||
/>
|
/>
|
||||||
{(() => {
|
|
||||||
if (this.props.canUploadMedia) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<p className="modal__divider">OR</p>
|
|
||||||
<FileUploader />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
})()}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
@ -63,8 +50,8 @@ class NewFileModal extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
NewFileModal.propTypes = {
|
NewFileModal.propTypes = {
|
||||||
closeModal: PropTypes.func.isRequired,
|
createFile: PropTypes.func.isRequired,
|
||||||
canUploadMedia: PropTypes.bool.isRequired
|
closeNewFileModal: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
function validate(formProps) {
|
function validate(formProps) {
|
||||||
|
@ -79,9 +66,19 @@ function validate(formProps) {
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function mapStateToProps() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
export default reduxForm({
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return bindActionCreators({ createFile, closeNewFileModal }, dispatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
connect(mapStateToProps, mapDispatchToProps),
|
||||||
|
reduxForm({
|
||||||
form: 'new-file',
|
form: 'new-file',
|
||||||
fields: ['name'],
|
fields: ['name'],
|
||||||
validate
|
validate
|
||||||
})(NewFileModal);
|
})
|
||||||
|
)(NewFileModal);
|
||||||
|
|
|
@ -20,21 +20,22 @@ class NewFolderForm extends React.Component {
|
||||||
<form
|
<form
|
||||||
className="new-folder-form"
|
className="new-folder-form"
|
||||||
onSubmit={(data) => {
|
onSubmit={(data) => {
|
||||||
if (handleSubmit(this.createFolder)(data)) {
|
handleSubmit(this.createFolder)(data);
|
||||||
this.props.closeModal();
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<div className="new-folder-form__input-wrapper">
|
||||||
<label className="new-folder-form__name-label" htmlFor="name">Name:</label>
|
<label className="new-folder-form__name-label" htmlFor="name">Name:</label>
|
||||||
<input
|
<input
|
||||||
className="new-folder-form__name-input"
|
className="new-folder-form__name-input"
|
||||||
id="name"
|
id="name"
|
||||||
type="text"
|
type="text"
|
||||||
|
maxLength="128"
|
||||||
placeholder="Name"
|
placeholder="Name"
|
||||||
ref={(element) => { this.fileName = element; }}
|
ref={(element) => { this.fileName = element; }}
|
||||||
{...domOnlyProps(name)}
|
{...domOnlyProps(name)}
|
||||||
/>
|
/>
|
||||||
<input type="submit" value="Add Folder" aria-label="add folder" />
|
<input type="submit" value="Add Folder" aria-label="add folder" />
|
||||||
|
</div>
|
||||||
{name.touched && name.error && <span className="form-error">{name.error}</span>}
|
{name.touched && name.error && <span className="form-error">{name.error}</span>}
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|
|
@ -16,7 +16,7 @@ class NewFolderModal extends React.Component {
|
||||||
<section className="modal" ref={(element) => { this.newFolderModal = element; }} >
|
<section className="modal" ref={(element) => { this.newFolderModal = element; }} >
|
||||||
<div className="modal-content-folder">
|
<div className="modal-content-folder">
|
||||||
<div className="modal__header">
|
<div className="modal__header">
|
||||||
<h2 className="modal__title">Add Folder</h2>
|
<h2 className="modal__title">Create Folder</h2>
|
||||||
<button className="modal__exit-button" onClick={this.props.closeModal}>
|
<button className="modal__exit-button" onClick={this.props.closeModal}>
|
||||||
<InlineSVG src={exitUrl} alt="Close New Folder Modal" />
|
<InlineSVG src={exitUrl} alt="Close New Folder Modal" />
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -98,9 +98,9 @@ class Preferences extends React.Component {
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<TabList>
|
<TabList>
|
||||||
<div className="preference__subheadings">
|
<div className="tabs__titles">
|
||||||
<Tab><h4 className="preference__subheading">General Settings</h4></Tab>
|
<Tab><h4 className="tabs__title">General Settings</h4></Tab>
|
||||||
<Tab><h4 className="preference__subheading">Accessibility</h4></Tab>
|
<Tab><h4 className="tabs__title">Accessibility</h4></Tab>
|
||||||
</div>
|
</div>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
|
|
27
client/modules/IDE/components/QuickAddList/Icons.jsx
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import InlineSVG from 'react-inlinesvg';
|
||||||
|
|
||||||
|
const check = require('../../../../images/check_encircled.svg');
|
||||||
|
const close = require('../../../../images/close.svg');
|
||||||
|
|
||||||
|
const Icons = ({ isAdded }) => {
|
||||||
|
const classes = [
|
||||||
|
'quick-add__icon',
|
||||||
|
isAdded ? 'quick-add__icon--in-collection' : 'quick-add__icon--not-in-collection'
|
||||||
|
].join(' ');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes}>
|
||||||
|
<InlineSVG className="quick-add__remove-icon" src={close} alt="Remove from collection" />
|
||||||
|
<InlineSVG className="quick-add__in-icon" src={check} alt="In collection" />
|
||||||
|
<InlineSVG className="quick-add__add-icon" src={close} alt="Add to collection" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Icons.propTypes = {
|
||||||
|
isAdded: PropTypes.bool.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Icons;
|
71
client/modules/IDE/components/QuickAddList/QuickAddList.jsx
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Link } from 'react-router';
|
||||||
|
|
||||||
|
import Icons from './Icons';
|
||||||
|
|
||||||
|
const Item = ({
|
||||||
|
isAdded, onSelect, name, url
|
||||||
|
}) => (
|
||||||
|
<li className="quick-add__item" onClick={onSelect}> { /* eslint-disable-line */ }
|
||||||
|
<button className="quick-add__item-toggle" onClick={onSelect}>
|
||||||
|
<Icons isAdded={isAdded} />
|
||||||
|
</button>
|
||||||
|
<span className="quick-add__item-name">{name}</span>
|
||||||
|
<Link
|
||||||
|
className="quick-add__item-view"
|
||||||
|
to={url}
|
||||||
|
target="_blank"
|
||||||
|
onClick={e => e.stopPropogation()}
|
||||||
|
>
|
||||||
|
View
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ItemType = PropTypes.shape({
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
url: PropTypes.string.isRequired,
|
||||||
|
isAdded: PropTypes.bool.isRequired,
|
||||||
|
});
|
||||||
|
|
||||||
|
Item.propTypes = {
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
url: PropTypes.string.isRequired,
|
||||||
|
isAdded: PropTypes.bool.isRequired,
|
||||||
|
onSelect: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
const QuickAddList = ({
|
||||||
|
items, onAdd, onRemove
|
||||||
|
}) => {
|
||||||
|
const handleAction = (item) => {
|
||||||
|
if (item.isAdded) {
|
||||||
|
onRemove(item);
|
||||||
|
} else {
|
||||||
|
onAdd(item);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className="quick-add">{items.map(item => (<Item
|
||||||
|
key={item.id}
|
||||||
|
{...item}
|
||||||
|
onSelect={
|
||||||
|
(event) => {
|
||||||
|
event.target.blur();
|
||||||
|
handleAction(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
QuickAddList.propTypes = {
|
||||||
|
items: PropTypes.arrayOf(ItemType).isRequired,
|
||||||
|
onAdd: PropTypes.func.isRequired,
|
||||||
|
onRemove: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QuickAddList;
|
1
client/modules/IDE/components/QuickAddList/index.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export { default } from './QuickAddList.jsx';
|
24
client/modules/IDE/components/Searchbar/Collection.jsx
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import * as SortingActions from '../../actions/sorting';
|
||||||
|
|
||||||
|
import Searchbar from './Searchbar';
|
||||||
|
|
||||||
|
const scope = 'collection';
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
searchLabel: 'Search collections...',
|
||||||
|
searchTerm: state.search[`${scope}SearchTerm`],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
const actions = {
|
||||||
|
setSearchTerm: term => SortingActions.setSearchTerm(scope, term),
|
||||||
|
resetSearchTerm: () => SortingActions.resetSearchTerm(scope),
|
||||||
|
};
|
||||||
|
return bindActionCreators(Object.assign({}, actions), dispatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(Searchbar);
|
|
@ -1,12 +1,9 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
import InlineSVG from 'react-inlinesvg';
|
||||||
import { bindActionCreators } from 'redux';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { throttle } from 'lodash';
|
import { throttle } from 'lodash';
|
||||||
import * as SortingActions from '../actions/sorting';
|
|
||||||
|
|
||||||
const searchIcon = require('../../../images/magnifyingglass.svg');
|
const searchIcon = require('../../../../images/magnifyingglass.svg');
|
||||||
|
|
||||||
class Searchbar extends React.Component {
|
class Searchbar extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -46,19 +43,15 @@ class Searchbar extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
const { searchValue } = this.state;
|
const { searchValue } = this.state;
|
||||||
return (
|
return (
|
||||||
<div className="searchbar">
|
<div className={`searchbar ${searchValue === '' ? 'searchbar--is-empty' : ''}`}>
|
||||||
<button
|
<div className="searchbar__button">
|
||||||
type="submit"
|
|
||||||
className="searchbar__button"
|
|
||||||
onClick={this.handleSearchEnter}
|
|
||||||
>
|
|
||||||
<InlineSVG className="searchbar__icon" src={searchIcon} />
|
<InlineSVG className="searchbar__icon" src={searchIcon} />
|
||||||
</button>
|
</div>
|
||||||
<input
|
<input
|
||||||
className="searchbar__input"
|
className="searchbar__input"
|
||||||
type="text"
|
type="text"
|
||||||
value={searchValue}
|
value={searchValue}
|
||||||
placeholder="Search files..."
|
placeholder={this.props.searchLabel}
|
||||||
onChange={this.handleSearchChange}
|
onChange={this.handleSearchChange}
|
||||||
onKeyUp={this.handleSearchEnter}
|
onKeyUp={this.handleSearchEnter}
|
||||||
/>
|
/>
|
||||||
|
@ -75,17 +68,12 @@ class Searchbar extends React.Component {
|
||||||
Searchbar.propTypes = {
|
Searchbar.propTypes = {
|
||||||
searchTerm: PropTypes.string.isRequired,
|
searchTerm: PropTypes.string.isRequired,
|
||||||
setSearchTerm: PropTypes.func.isRequired,
|
setSearchTerm: PropTypes.func.isRequired,
|
||||||
resetSearchTerm: PropTypes.func.isRequired
|
resetSearchTerm: PropTypes.func.isRequired,
|
||||||
|
searchLabel: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
function mapStateToProps(state) {
|
Searchbar.defaultProps = {
|
||||||
return {
|
searchLabel: 'Search sketches...',
|
||||||
searchTerm: state.search.searchTerm
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch) {
|
export default Searchbar;
|
||||||
return bindActionCreators(Object.assign({}, SortingActions), dispatch);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(Searchbar);
|
|
23
client/modules/IDE/components/Searchbar/Sketch.jsx
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import * as SortingActions from '../../actions/sorting';
|
||||||
|
|
||||||
|
import Searchbar from './Searchbar';
|
||||||
|
|
||||||
|
const scope = 'sketch';
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
searchTerm: state.search[`${scope}SearchTerm`],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
const actions = {
|
||||||
|
setSearchTerm: term => SortingActions.setSearchTerm(scope, term),
|
||||||
|
resetSearchTerm: () => SortingActions.resetSearchTerm(scope),
|
||||||
|
};
|
||||||
|
return bindActionCreators(Object.assign({}, actions), dispatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(Searchbar);
|
2
client/modules/IDE/components/Searchbar/index.js
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export { default as CollectionSearchbar } from './Collection.jsx';
|
||||||
|
export { default as SketchSearchbar } from './Sketch.jsx';
|
|
@ -97,7 +97,7 @@ class Sidebar extends React.Component {
|
||||||
onBlur={this.onBlurComponent}
|
onBlur={this.onBlurComponent}
|
||||||
onFocus={this.onFocusComponent}
|
onFocus={this.onFocusComponent}
|
||||||
>
|
>
|
||||||
Add folder
|
Create folder
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
|
@ -110,7 +110,20 @@ class Sidebar extends React.Component {
|
||||||
onBlur={this.onBlurComponent}
|
onBlur={this.onBlurComponent}
|
||||||
onFocus={this.onFocusComponent}
|
onFocus={this.onFocusComponent}
|
||||||
>
|
>
|
||||||
Add file
|
Create file
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
aria-label="upload file"
|
||||||
|
onClick={() => {
|
||||||
|
this.props.openUploadFileModal(rootFile.id);
|
||||||
|
setTimeout(this.props.closeProjectOptions, 0);
|
||||||
|
}}
|
||||||
|
onBlur={this.onBlurComponent}
|
||||||
|
onFocus={this.onFocusComponent}
|
||||||
|
>
|
||||||
|
Upload file
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -137,6 +150,7 @@ Sidebar.propTypes = {
|
||||||
openProjectOptions: PropTypes.func.isRequired,
|
openProjectOptions: PropTypes.func.isRequired,
|
||||||
closeProjectOptions: PropTypes.func.isRequired,
|
closeProjectOptions: PropTypes.func.isRequired,
|
||||||
newFolder: PropTypes.func.isRequired,
|
newFolder: PropTypes.func.isRequired,
|
||||||
|
openUploadFileModal: PropTypes.func.isRequired,
|
||||||
owner: PropTypes.shape({
|
owner: PropTypes.shape({
|
||||||
id: PropTypes.string
|
id: PropTypes.string
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -10,11 +10,14 @@ import classNames from 'classnames';
|
||||||
import slugify from 'slugify';
|
import slugify from 'slugify';
|
||||||
import * as ProjectActions from '../actions/project';
|
import * as ProjectActions from '../actions/project';
|
||||||
import * as ProjectsActions from '../actions/projects';
|
import * as ProjectsActions from '../actions/projects';
|
||||||
|
import * as CollectionsActions from '../actions/collections';
|
||||||
import * as ToastActions from '../actions/toast';
|
import * as ToastActions from '../actions/toast';
|
||||||
import * as SortingActions from '../actions/sorting';
|
import * as SortingActions from '../actions/sorting';
|
||||||
import * as IdeActions from '../actions/ide';
|
import * as IdeActions from '../actions/ide';
|
||||||
import getSortedSketches from '../selectors/projects';
|
import getSortedSketches from '../selectors/projects';
|
||||||
import Loader from '../../App/components/loader';
|
import Loader from '../../App/components/loader';
|
||||||
|
import Overlay from '../../App/components/Overlay';
|
||||||
|
import AddToCollectionList from './AddToCollectionList';
|
||||||
|
|
||||||
const arrowUp = require('../../../images/sort-arrow-up.svg');
|
const arrowUp = require('../../../images/sort-arrow-up.svg');
|
||||||
const arrowDown = require('../../../images/sort-arrow-down.svg');
|
const arrowDown = require('../../../images/sort-arrow-down.svg');
|
||||||
|
@ -27,9 +30,11 @@ class SketchListRowBase extends React.Component {
|
||||||
optionsOpen: false,
|
optionsOpen: false,
|
||||||
renameOpen: false,
|
renameOpen: false,
|
||||||
renameValue: props.sketch.name,
|
renameValue: props.sketch.name,
|
||||||
isFocused: false
|
isFocused: false,
|
||||||
};
|
};
|
||||||
|
this.renameInput = React.createRef();
|
||||||
}
|
}
|
||||||
|
|
||||||
onFocusComponent = () => {
|
onFocusComponent = () => {
|
||||||
this.setState({ isFocused: true });
|
this.setState({ isFocused: true });
|
||||||
}
|
}
|
||||||
|
@ -65,8 +70,9 @@ class SketchListRowBase extends React.Component {
|
||||||
|
|
||||||
openRename = () => {
|
openRename = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
renameOpen: true
|
renameOpen: true,
|
||||||
});
|
renameValue: this.props.sketch.name
|
||||||
|
}, () => this.renameInput.current.focus());
|
||||||
}
|
}
|
||||||
|
|
||||||
closeRename = () => {
|
closeRename = () => {
|
||||||
|
@ -90,15 +96,27 @@ class SketchListRowBase extends React.Component {
|
||||||
|
|
||||||
handleRenameEnter = (e) => {
|
handleRenameEnter = (e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
// TODO pass this func
|
this.updateName();
|
||||||
this.props.changeProjectName(this.props.sketch.id, this.state.renameValue);
|
|
||||||
this.closeAll();
|
this.closeAll();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleRenameBlur = () => {
|
||||||
|
this.updateName();
|
||||||
|
this.closeAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateName = () => {
|
||||||
|
const isValid = this.state.renameValue.trim().length !== 0;
|
||||||
|
if (isValid) {
|
||||||
|
this.props.changeProjectName(this.props.sketch.id, this.state.renameValue.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
resetSketchName = () => {
|
resetSketchName = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
renameValue: this.props.sketch.name
|
renameValue: this.props.sketch.name,
|
||||||
|
renameOpen: false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,36 +151,17 @@ class SketchListRowBase extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
renderViewButton = sketchURL => (
|
||||||
const { sketch, username } = this.props;
|
<td className="sketch-list__dropdown-column">
|
||||||
const { renameOpen, optionsOpen, renameValue } = this.state;
|
<Link to={sketchURL}>View</Link>
|
||||||
|
</td>
|
||||||
|
)
|
||||||
|
|
||||||
|
renderDropdown = () => {
|
||||||
|
const { optionsOpen } = this.state;
|
||||||
const userIsOwner = this.props.user.username === this.props.username;
|
const userIsOwner = this.props.user.username === this.props.username;
|
||||||
let url = `/${username}/sketches/${sketch.id}`;
|
|
||||||
if (username === 'p5') {
|
|
||||||
url = `/${username}/sketches/${slugify(sketch.name, '_')}`;
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<tr
|
|
||||||
className="sketches-table__row"
|
|
||||||
key={sketch.id}
|
|
||||||
>
|
|
||||||
<th scope="row">
|
|
||||||
<Link to={url}>
|
|
||||||
{renameOpen ? '' : sketch.name}
|
|
||||||
</Link>
|
|
||||||
{renameOpen
|
|
||||||
&&
|
|
||||||
<input
|
|
||||||
value={renameValue}
|
|
||||||
onChange={this.handleRenameChange}
|
|
||||||
onKeyUp={this.handleRenameEnter}
|
|
||||||
onBlur={this.resetSketchName}
|
|
||||||
onClick={e => e.stopPropagation()}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</th>
|
|
||||||
<td>{format(new Date(sketch.createdAt), 'MMM D, YYYY h:mm A')}</td>
|
|
||||||
<td>{format(new Date(sketch.updatedAt), 'MMM D, YYYY h:mm A')}</td>
|
|
||||||
<td className="sketch-list__dropdown-column">
|
<td className="sketch-list__dropdown-column">
|
||||||
<button
|
<button
|
||||||
className="sketch-list__dropdown-button"
|
className="sketch-list__dropdown-button"
|
||||||
|
@ -208,6 +207,20 @@ class SketchListRowBase extends React.Component {
|
||||||
Duplicate
|
Duplicate
|
||||||
</button>
|
</button>
|
||||||
</li>}
|
</li>}
|
||||||
|
{this.props.user.authenticated &&
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
className="sketch-list__action-option"
|
||||||
|
onClick={() => {
|
||||||
|
this.props.onAddToCollection();
|
||||||
|
this.closeAll();
|
||||||
|
}}
|
||||||
|
onBlur={this.onBlurComponent}
|
||||||
|
onFocus={this.onFocusComponent}
|
||||||
|
>
|
||||||
|
Add to collection
|
||||||
|
</button>
|
||||||
|
</li>}
|
||||||
{ /* <li>
|
{ /* <li>
|
||||||
<button
|
<button
|
||||||
className="sketch-list__action-option"
|
className="sketch-list__action-option"
|
||||||
|
@ -231,7 +244,54 @@ class SketchListRowBase extends React.Component {
|
||||||
</li>}
|
</li>}
|
||||||
</ul>}
|
</ul>}
|
||||||
</td>
|
</td>
|
||||||
</tr>);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
sketch,
|
||||||
|
username,
|
||||||
|
} = this.props;
|
||||||
|
const { renameOpen, renameValue } = this.state;
|
||||||
|
let url = `/${username}/sketches/${sketch.id}`;
|
||||||
|
if (username === 'p5') {
|
||||||
|
url = `/${username}/sketches/${slugify(sketch.name, '_')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = (
|
||||||
|
<React.Fragment>
|
||||||
|
<Link to={url}>
|
||||||
|
{renameOpen ? '' : sketch.name}
|
||||||
|
</Link>
|
||||||
|
{renameOpen
|
||||||
|
&&
|
||||||
|
<input
|
||||||
|
value={renameValue}
|
||||||
|
onChange={this.handleRenameChange}
|
||||||
|
onKeyUp={this.handleRenameEnter}
|
||||||
|
onBlur={this.handleRenameBlur}
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
ref={this.renameInput}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<tr
|
||||||
|
className="sketches-table__row"
|
||||||
|
key={sketch.id}
|
||||||
|
onClick={this.handleRowClick}
|
||||||
|
>
|
||||||
|
<th scope="row">
|
||||||
|
{name}
|
||||||
|
</th>
|
||||||
|
<td>{format(new Date(sketch.createdAt), 'MMM D, YYYY h:mm A')}</td>
|
||||||
|
<td>{format(new Date(sketch.updatedAt), 'MMM D, YYYY h:mm A')}</td>
|
||||||
|
{this.renderDropdown()}
|
||||||
|
</tr>
|
||||||
|
</React.Fragment>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -251,7 +311,8 @@ SketchListRowBase.propTypes = {
|
||||||
showShareModal: PropTypes.func.isRequired,
|
showShareModal: PropTypes.func.isRequired,
|
||||||
cloneProject: PropTypes.func.isRequired,
|
cloneProject: PropTypes.func.isRequired,
|
||||||
exportProjectAsZip: PropTypes.func.isRequired,
|
exportProjectAsZip: PropTypes.func.isRequired,
|
||||||
changeProjectName: PropTypes.func.isRequired
|
changeProjectName: PropTypes.func.isRequired,
|
||||||
|
onAddToCollection: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
function mapDispatchToPropsSketchListRow(dispatch) {
|
function mapDispatchToPropsSketchListRow(dispatch) {
|
||||||
|
@ -266,6 +327,18 @@ class SketchList extends React.Component {
|
||||||
this.props.getProjects(this.props.username);
|
this.props.getProjects(this.props.username);
|
||||||
this.props.resetSorting();
|
this.props.resetSorting();
|
||||||
this._renderFieldHeader = this._renderFieldHeader.bind(this);
|
this._renderFieldHeader = this._renderFieldHeader.bind(this);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isInitialDataLoad: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
if (this.props.sketches !== nextProps.sketches && Array.isArray(nextProps.sketches)) {
|
||||||
|
this.setState({
|
||||||
|
isInitialDataLoad: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getSketchesTitle() {
|
getSketchesTitle() {
|
||||||
|
@ -276,16 +349,20 @@ class SketchList extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
hasSketches() {
|
hasSketches() {
|
||||||
return !this.props.loading && this.props.sketches.length > 0;
|
return !this.isLoading() && this.props.sketches.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading() {
|
||||||
|
return this.props.loading && this.state.isInitialDataLoad;
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderLoader() {
|
_renderLoader() {
|
||||||
if (this.props.loading) return <Loader />;
|
if (this.isLoading()) return <Loader />;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderEmptyTable() {
|
_renderEmptyTable() {
|
||||||
if (!this.props.loading && this.props.sketches.length === 0) {
|
if (!this.isLoading() && this.props.sketches.length === 0) {
|
||||||
return (<p className="sketches-table__empty">No sketches.</p>);
|
return (<p className="sketches-table__empty">No sketches.</p>);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -338,9 +415,26 @@ class SketchList extends React.Component {
|
||||||
sketch={sketch}
|
sketch={sketch}
|
||||||
user={this.props.user}
|
user={this.props.user}
|
||||||
username={username}
|
username={username}
|
||||||
|
onAddToCollection={() => {
|
||||||
|
this.setState({ sketchToAddToCollection: sketch });
|
||||||
|
}}
|
||||||
/>))}
|
/>))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>}
|
</table>}
|
||||||
|
{
|
||||||
|
this.state.sketchToAddToCollection &&
|
||||||
|
<Overlay
|
||||||
|
isFixedHeight
|
||||||
|
title="Add to collection"
|
||||||
|
closeOverlay={() => this.setState({ sketchToAddToCollection: null })}
|
||||||
|
>
|
||||||
|
<AddToCollectionList
|
||||||
|
project={this.state.sketchToAddToCollection}
|
||||||
|
username={this.props.username}
|
||||||
|
user={this.props.user}
|
||||||
|
/>
|
||||||
|
</Overlay>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -366,19 +460,9 @@ SketchList.propTypes = {
|
||||||
field: PropTypes.string.isRequired,
|
field: PropTypes.string.isRequired,
|
||||||
direction: PropTypes.string.isRequired
|
direction: PropTypes.string.isRequired
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
project: PropTypes.shape({
|
|
||||||
id: PropTypes.string,
|
|
||||||
owner: PropTypes.shape({
|
|
||||||
id: PropTypes.string
|
|
||||||
})
|
|
||||||
})
|
|
||||||
};
|
};
|
||||||
|
|
||||||
SketchList.defaultProps = {
|
SketchList.defaultProps = {
|
||||||
project: {
|
|
||||||
id: undefined,
|
|
||||||
owner: undefined
|
|
||||||
},
|
|
||||||
username: undefined
|
username: undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -393,7 +477,10 @@ function mapStateToProps(state) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch) {
|
function mapDispatchToProps(dispatch) {
|
||||||
return bindActionCreators(Object.assign({}, ProjectsActions, ToastActions, SortingActions), dispatch);
|
return bindActionCreators(
|
||||||
|
Object.assign({}, ProjectsActions, CollectionsActions, ToastActions, SortingActions),
|
||||||
|
dispatch
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(SketchList);
|
export default connect(mapStateToProps, mapDispatchToProps)(SketchList);
|
||||||
|
|
|
@ -32,7 +32,7 @@ class Toolbar extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
validateProjectName() {
|
validateProjectName() {
|
||||||
if (this.props.project.name === '') {
|
if ((this.props.project.name.trim()).length === 0) {
|
||||||
this.props.setProjectName(this.originalProjectName);
|
this.props.setProjectName(this.originalProjectName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
68
client/modules/IDE/components/UploadFileModal.jsx
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { Link } from 'react-router';
|
||||||
|
import InlineSVG from 'react-inlinesvg';
|
||||||
|
import prettyBytes from 'pretty-bytes';
|
||||||
|
import FileUploader from './FileUploader';
|
||||||
|
import { getreachedTotalSizeLimit } from '../selectors/users';
|
||||||
|
import exitUrl from '../../../images/exit.svg';
|
||||||
|
|
||||||
|
const __process = (typeof global !== 'undefined' ? global : window).process;
|
||||||
|
const limit = __process.env.UPLOAD_LIMIT || 250000000;
|
||||||
|
const limitText = prettyBytes(limit);
|
||||||
|
|
||||||
|
class UploadFileModal extends React.Component {
|
||||||
|
propTypes = {
|
||||||
|
reachedTotalSizeLimit: PropTypes.bool.isRequired,
|
||||||
|
closeModal: PropTypes.func.isRequired
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.focusOnModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
focusOnModal = () => {
|
||||||
|
this.modal.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<section className="modal" ref={(element) => { this.modal = element; }}>
|
||||||
|
<div className="modal-content">
|
||||||
|
<div className="modal__header">
|
||||||
|
<h2 className="modal__title">Upload File</h2>
|
||||||
|
<button className="modal__exit-button" onClick={this.props.closeModal}>
|
||||||
|
<InlineSVG src={exitUrl} alt="Close New File Modal" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{ this.props.reachedTotalSizeLimit &&
|
||||||
|
<p>
|
||||||
|
{
|
||||||
|
`Error: You cannot upload any more files. You have reached the total size limit of ${limitText}.
|
||||||
|
If you would like to upload more, please remove the ones you aren't using anymore by
|
||||||
|
in your `
|
||||||
|
}
|
||||||
|
<Link to="/assets" onClick={this.props.closeModal}>assets</Link>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
{ !this.props.reachedTotalSizeLimit &&
|
||||||
|
<div>
|
||||||
|
<FileUploader />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
reachedTotalSizeLimit: getreachedTotalSizeLimit(state)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(UploadFileModal);
|
|
@ -12,6 +12,7 @@ import Toolbar from '../components/Toolbar';
|
||||||
import Preferences from '../components/Preferences';
|
import Preferences from '../components/Preferences';
|
||||||
import NewFileModal from '../components/NewFileModal';
|
import NewFileModal from '../components/NewFileModal';
|
||||||
import NewFolderModal from '../components/NewFolderModal';
|
import NewFolderModal from '../components/NewFolderModal';
|
||||||
|
import UploadFileModal from '../components/UploadFileModal';
|
||||||
import ShareModal from '../components/ShareModal';
|
import ShareModal from '../components/ShareModal';
|
||||||
import KeyboardShortcutModal from '../components/KeyboardShortcutModal';
|
import KeyboardShortcutModal from '../components/KeyboardShortcutModal';
|
||||||
import ErrorModal from '../components/ErrorModal';
|
import ErrorModal from '../components/ErrorModal';
|
||||||
|
@ -28,11 +29,10 @@ import * as ToastActions from '../actions/toast';
|
||||||
import * as ConsoleActions from '../actions/console';
|
import * as ConsoleActions from '../actions/console';
|
||||||
import { getHTMLFile } from '../reducers/files';
|
import { getHTMLFile } from '../reducers/files';
|
||||||
import Overlay from '../../App/components/Overlay';
|
import Overlay from '../../App/components/Overlay';
|
||||||
import SketchList from '../components/SketchList';
|
|
||||||
import Searchbar from '../components/Searchbar';
|
|
||||||
import AssetList from '../components/AssetList';
|
|
||||||
import About from '../components/About';
|
import About from '../components/About';
|
||||||
|
import AddToCollectionList from '../components/AddToCollectionList';
|
||||||
import Feedback from '../components/Feedback';
|
import Feedback from '../components/Feedback';
|
||||||
|
import { CollectionSearchbar } from '../components/Searchbar';
|
||||||
|
|
||||||
class IDEView extends React.Component {
|
class IDEView extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -239,6 +239,8 @@ class IDEView extends React.Component {
|
||||||
newFolder={this.props.newFolder}
|
newFolder={this.props.newFolder}
|
||||||
user={this.props.user}
|
user={this.props.user}
|
||||||
owner={this.props.project.owner}
|
owner={this.props.project.owner}
|
||||||
|
openUploadFileModal={this.props.openUploadFileModal}
|
||||||
|
closeUploadFileModal={this.props.closeUploadFileModal}
|
||||||
/>
|
/>
|
||||||
<SplitPane
|
<SplitPane
|
||||||
split="vertical"
|
split="vertical"
|
||||||
|
@ -351,11 +353,7 @@ class IDEView extends React.Component {
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
</div>
|
</div>
|
||||||
{ this.props.ide.modalIsVisible &&
|
{ this.props.ide.modalIsVisible &&
|
||||||
<NewFileModal
|
<NewFileModal />
|
||||||
canUploadMedia={this.props.user.authenticated}
|
|
||||||
closeModal={this.props.closeNewFileModal}
|
|
||||||
createFile={this.props.createFile}
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
{this.props.ide.newFolderModalVisible &&
|
{this.props.ide.newFolderModalVisible &&
|
||||||
<NewFolderModal
|
<NewFolderModal
|
||||||
|
@ -363,30 +361,10 @@ class IDEView extends React.Component {
|
||||||
createFolder={this.props.createFolder}
|
createFolder={this.props.createFolder}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
{ this.props.location.pathname.match(/sketches$/) &&
|
{this.props.ide.uploadFileModalVisible &&
|
||||||
<Overlay
|
<UploadFileModal
|
||||||
ariaLabel="project list"
|
closeModal={this.props.closeUploadFileModal}
|
||||||
title="Open a Sketch"
|
|
||||||
previousPath={this.props.ide.previousPath}
|
|
||||||
>
|
|
||||||
<Searchbar />
|
|
||||||
<SketchList
|
|
||||||
username={this.props.params.username}
|
|
||||||
user={this.props.user}
|
|
||||||
/>
|
/>
|
||||||
</Overlay>
|
|
||||||
}
|
|
||||||
{ this.props.location.pathname.match(/assets$/) &&
|
|
||||||
<Overlay
|
|
||||||
title="Assets"
|
|
||||||
ariaLabel="asset list"
|
|
||||||
previousPath={this.props.ide.previousPath}
|
|
||||||
>
|
|
||||||
<AssetList
|
|
||||||
username={this.props.params.username}
|
|
||||||
user={this.props.user}
|
|
||||||
/>
|
|
||||||
</Overlay>
|
|
||||||
}
|
}
|
||||||
{ this.props.location.pathname === '/about' &&
|
{ this.props.location.pathname === '/about' &&
|
||||||
<Overlay
|
<Overlay
|
||||||
|
@ -406,6 +384,21 @@ class IDEView extends React.Component {
|
||||||
<Feedback previousPath={this.props.ide.previousPath} />
|
<Feedback previousPath={this.props.ide.previousPath} />
|
||||||
</Overlay>
|
</Overlay>
|
||||||
}
|
}
|
||||||
|
{this.props.location.pathname.match(/add-to-collection$/) &&
|
||||||
|
<Overlay
|
||||||
|
ariaLabel="add to collection"
|
||||||
|
title="Add to collection"
|
||||||
|
previousPath={this.props.ide.previousPath}
|
||||||
|
actions={<CollectionSearchbar />}
|
||||||
|
isFixedHeight
|
||||||
|
>
|
||||||
|
<AddToCollectionList
|
||||||
|
projectId={this.props.params.project_id}
|
||||||
|
username={this.props.params.username}
|
||||||
|
user={this.props.user}
|
||||||
|
/>
|
||||||
|
</Overlay>
|
||||||
|
}
|
||||||
{this.props.ide.shareModalVisible &&
|
{this.props.ide.shareModalVisible &&
|
||||||
<Overlay
|
<Overlay
|
||||||
title="Share"
|
title="Share"
|
||||||
|
@ -486,6 +479,7 @@ IDEView.propTypes = {
|
||||||
justOpenedProject: PropTypes.bool.isRequired,
|
justOpenedProject: PropTypes.bool.isRequired,
|
||||||
errorType: PropTypes.string,
|
errorType: PropTypes.string,
|
||||||
runtimeErrorWarningVisible: PropTypes.bool.isRequired,
|
runtimeErrorWarningVisible: PropTypes.bool.isRequired,
|
||||||
|
uploadFileModalVisible: PropTypes.bool.isRequired
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
stopSketch: PropTypes.func.isRequired,
|
stopSketch: PropTypes.func.isRequired,
|
||||||
project: PropTypes.shape({
|
project: PropTypes.shape({
|
||||||
|
@ -543,7 +537,6 @@ IDEView.propTypes = {
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
dispatchConsoleEvent: PropTypes.func.isRequired,
|
dispatchConsoleEvent: PropTypes.func.isRequired,
|
||||||
newFile: PropTypes.func.isRequired,
|
newFile: PropTypes.func.isRequired,
|
||||||
closeNewFileModal: PropTypes.func.isRequired,
|
|
||||||
expandSidebar: PropTypes.func.isRequired,
|
expandSidebar: PropTypes.func.isRequired,
|
||||||
collapseSidebar: PropTypes.func.isRequired,
|
collapseSidebar: PropTypes.func.isRequired,
|
||||||
cloneProject: PropTypes.func.isRequired,
|
cloneProject: PropTypes.func.isRequired,
|
||||||
|
@ -556,7 +549,6 @@ IDEView.propTypes = {
|
||||||
newFolder: PropTypes.func.isRequired,
|
newFolder: PropTypes.func.isRequired,
|
||||||
closeNewFolderModal: PropTypes.func.isRequired,
|
closeNewFolderModal: PropTypes.func.isRequired,
|
||||||
createFolder: PropTypes.func.isRequired,
|
createFolder: PropTypes.func.isRequired,
|
||||||
createFile: PropTypes.func.isRequired,
|
|
||||||
closeShareModal: PropTypes.func.isRequired,
|
closeShareModal: PropTypes.func.isRequired,
|
||||||
showEditorOptions: PropTypes.func.isRequired,
|
showEditorOptions: PropTypes.func.isRequired,
|
||||||
closeEditorOptions: PropTypes.func.isRequired,
|
closeEditorOptions: PropTypes.func.isRequired,
|
||||||
|
@ -588,6 +580,8 @@ IDEView.propTypes = {
|
||||||
showRuntimeErrorWarning: PropTypes.func.isRequired,
|
showRuntimeErrorWarning: PropTypes.func.isRequired,
|
||||||
hideRuntimeErrorWarning: PropTypes.func.isRequired,
|
hideRuntimeErrorWarning: PropTypes.func.isRequired,
|
||||||
startSketch: PropTypes.func.isRequired,
|
startSketch: PropTypes.func.isRequired,
|
||||||
|
openUploadFileModal: PropTypes.func.isRequired,
|
||||||
|
closeUploadFileModal: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
function mapStateToProps(state) {
|
function mapStateToProps(state) {
|
||||||
|
|
|
@ -10,6 +10,8 @@ const assets = (state = initialState, action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ActionTypes.SET_ASSETS:
|
case ActionTypes.SET_ASSETS:
|
||||||
return { list: action.assets, totalSize: action.totalSize };
|
return { list: action.assets, totalSize: action.totalSize };
|
||||||
|
case ActionTypes.DELETE_ASSET:
|
||||||
|
return { list: state.list.filter(asset => asset.key !== action.key) };
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
28
client/modules/IDE/reducers/collections.js
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import * as ActionTypes from '../../../constants';
|
||||||
|
|
||||||
|
const sketches = (state = [], action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case ActionTypes.SET_COLLECTIONS:
|
||||||
|
return action.collections;
|
||||||
|
|
||||||
|
case ActionTypes.DELETE_COLLECTION:
|
||||||
|
return state.filter(({ id }) => action.collectionId !== id);
|
||||||
|
|
||||||
|
// The API returns the complete new edited collection
|
||||||
|
// with any items added or removed
|
||||||
|
case ActionTypes.EDIT_COLLECTION:
|
||||||
|
case ActionTypes.ADD_TO_COLLECTION:
|
||||||
|
case ActionTypes.REMOVE_FROM_COLLECTION:
|
||||||
|
return state.map((collection) => {
|
||||||
|
if (collection.id === action.payload.id) {
|
||||||
|
return action.payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
return collection;
|
||||||
|
});
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default sketches;
|
|
@ -9,11 +9,11 @@ const initialState = {
|
||||||
preferencesIsVisible: false,
|
preferencesIsVisible: false,
|
||||||
projectOptionsVisible: false,
|
projectOptionsVisible: false,
|
||||||
newFolderModalVisible: false,
|
newFolderModalVisible: false,
|
||||||
|
uploadFileModalVisible: false,
|
||||||
shareModalVisible: false,
|
shareModalVisible: false,
|
||||||
shareModalProjectId: 'abcd',
|
shareModalProjectId: 'abcd',
|
||||||
shareModalProjectName: 'My Cute Sketch',
|
shareModalProjectName: 'My Cute Sketch',
|
||||||
shareModalProjectUsername: 'p5_user',
|
shareModalProjectUsername: 'p5_user',
|
||||||
sketchlistModalVisible: false,
|
|
||||||
editorOptionsVisible: false,
|
editorOptionsVisible: false,
|
||||||
keyboardShortcutVisible: false,
|
keyboardShortcutVisible: false,
|
||||||
unsavedChanges: false,
|
unsavedChanges: false,
|
||||||
|
@ -106,6 +106,10 @@ const ide = (state = initialState, action) => {
|
||||||
return Object.assign({}, state, { runtimeErrorWarningVisible: false });
|
return Object.assign({}, state, { runtimeErrorWarningVisible: false });
|
||||||
case ActionTypes.SHOW_RUNTIME_ERROR_WARNING:
|
case ActionTypes.SHOW_RUNTIME_ERROR_WARNING:
|
||||||
return Object.assign({}, state, { runtimeErrorWarningVisible: true });
|
return Object.assign({}, state, { runtimeErrorWarningVisible: true });
|
||||||
|
case ActionTypes.OPEN_UPLOAD_FILE_MODAL:
|
||||||
|
return Object.assign({}, state, { uploadFileModalVisible: true, parentId: action.parentId });
|
||||||
|
case ActionTypes.CLOSE_UPLOAD_FILE_MODAL:
|
||||||
|
return Object.assign({}, state, { uploadFileModalVisible: false });
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,8 @@
|
||||||
import friendlyWords from 'friendly-words';
|
|
||||||
import * as ActionTypes from '../../../constants';
|
import * as ActionTypes from '../../../constants';
|
||||||
|
import { generateProjectName } from '../../../utils/generateRandomName';
|
||||||
const generateRandomName = () => {
|
|
||||||
const adj = friendlyWords.predicates[Math.floor(Math.random() * friendlyWords.predicates.length)];
|
|
||||||
const obj = friendlyWords.objects[Math.floor(Math.random() * friendlyWords.objects.length)];
|
|
||||||
return `${adj} ${obj}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const initialState = () => {
|
const initialState = () => {
|
||||||
const generatedString = generateRandomName();
|
const generatedString = generateProjectName();
|
||||||
const generatedName = generatedString.charAt(0).toUpperCase() + generatedString.slice(1);
|
const generatedName = generatedString.charAt(0).toUpperCase() + generatedString.slice(1);
|
||||||
return {
|
return {
|
||||||
name: generatedName,
|
name: generatedName,
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import * as ActionTypes from '../../../constants';
|
import * as ActionTypes from '../../../constants';
|
||||||
|
|
||||||
const initialState = {
|
const initialState = {
|
||||||
searchTerm: ''
|
collectionSearchTerm: '',
|
||||||
|
sketchSearchTerm: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
export default (state = initialState, action) => {
|
export default (state = initialState, action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ActionTypes.SET_SEARCH_TERM:
|
case ActionTypes.SET_SEARCH_TERM:
|
||||||
return { ...state, searchTerm: action.query };
|
return { ...state, [`${action.scope}SearchTerm`]: action.query };
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
56
client/modules/IDE/selectors/collections.js
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import differenceInMilliseconds from 'date-fns/difference_in_milliseconds';
|
||||||
|
import find from 'lodash/find';
|
||||||
|
import orderBy from 'lodash/orderBy';
|
||||||
|
import { DIRECTION } from '../actions/sorting';
|
||||||
|
|
||||||
|
const getCollections = state => state.collections;
|
||||||
|
const getField = state => state.sorting.field;
|
||||||
|
const getDirection = state => state.sorting.direction;
|
||||||
|
const getSearchTerm = state => state.search.collectionSearchTerm;
|
||||||
|
|
||||||
|
const getFilteredCollections = createSelector(
|
||||||
|
getCollections,
|
||||||
|
getSearchTerm,
|
||||||
|
(collections, search) => {
|
||||||
|
if (search) {
|
||||||
|
const searchStrings = collections.map((collection) => {
|
||||||
|
const smallCollection = {
|
||||||
|
name: collection.name
|
||||||
|
};
|
||||||
|
return { ...collection, searchString: Object.values(smallCollection).join(' ').toLowerCase() };
|
||||||
|
});
|
||||||
|
return searchStrings.filter(collection => collection.searchString.includes(search.toLowerCase()));
|
||||||
|
}
|
||||||
|
return collections;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
const getSortedCollections = createSelector(
|
||||||
|
getFilteredCollections,
|
||||||
|
getField,
|
||||||
|
getDirection,
|
||||||
|
(collections, field, direction) => {
|
||||||
|
if (field === 'name') {
|
||||||
|
if (direction === DIRECTION.DESC) {
|
||||||
|
return orderBy(collections, 'name', 'desc');
|
||||||
|
}
|
||||||
|
return orderBy(collections, 'name', 'asc');
|
||||||
|
}
|
||||||
|
const sortedCollections = [...collections].sort((a, b) => {
|
||||||
|
const result =
|
||||||
|
direction === DIRECTION.ASC
|
||||||
|
? differenceInMilliseconds(new Date(a[field]), new Date(b[field]))
|
||||||
|
: differenceInMilliseconds(new Date(b[field]), new Date(a[field]));
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
return sortedCollections;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export function getCollection(state, id) {
|
||||||
|
return find(getCollections(state), { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getSortedCollections;
|
|
@ -6,7 +6,7 @@ import { DIRECTION } from '../actions/sorting';
|
||||||
const getSketches = state => state.sketches;
|
const getSketches = state => state.sketches;
|
||||||
const getField = state => state.sorting.field;
|
const getField = state => state.sorting.field;
|
||||||
const getDirection = state => state.sorting.direction;
|
const getDirection = state => state.sorting.direction;
|
||||||
const getSearchTerm = state => state.search.searchTerm;
|
const getSearchTerm = state => state.search.sketchSearchTerm;
|
||||||
|
|
||||||
const getFilteredSketches = createSelector(
|
const getFilteredSketches = createSelector(
|
||||||
getSketches,
|
getSketches,
|
||||||
|
|
30
client/modules/IDE/selectors/users.js
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
const __process = (typeof global !== 'undefined' ? global : window).process;
|
||||||
|
const getAuthenticated = state => state.user.authenticated;
|
||||||
|
const getTotalSize = state => state.user.totalSize;
|
||||||
|
const getAssetsTotalSize = state => state.assets.totalSize;
|
||||||
|
const limit = __process.env.UPLOAD_LIMIT || 250000000;
|
||||||
|
|
||||||
|
export const getCanUploadMedia = createSelector(
|
||||||
|
getAuthenticated,
|
||||||
|
getTotalSize,
|
||||||
|
(authenticated, totalSize) => {
|
||||||
|
if (!authenticated) return false;
|
||||||
|
// eventually do the same thing for verified when
|
||||||
|
// email verification actually works
|
||||||
|
if (totalSize > limit) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getreachedTotalSizeLimit = createSelector(
|
||||||
|
getTotalSize,
|
||||||
|
getAssetsTotalSize,
|
||||||
|
(totalSize, assetsTotalSize) => {
|
||||||
|
const currentSize = totalSize || assetsTotalSize;
|
||||||
|
if (currentSize && currentSize > limit) return true;
|
||||||
|
// if (totalSize > 1000) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
);
|
|
@ -218,3 +218,31 @@ export function updateSettings(formValues) {
|
||||||
})
|
})
|
||||||
.catch(response => Promise.reject(new Error(response.data.error)));
|
.catch(response => Promise.reject(new Error(response.data.error)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createApiKeySuccess(user) {
|
||||||
|
return {
|
||||||
|
type: ActionTypes.API_KEY_CREATED,
|
||||||
|
user
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createApiKey(label) {
|
||||||
|
return dispatch =>
|
||||||
|
axios.post(`${ROOT_URL}/account/api-keys`, { label }, { withCredentials: true })
|
||||||
|
.then((response) => {
|
||||||
|
dispatch(createApiKeySuccess(response.data));
|
||||||
|
})
|
||||||
|
.catch(response => Promise.reject(new Error(response.data.error)));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeApiKey(keyId) {
|
||||||
|
return dispatch =>
|
||||||
|
axios.delete(`${ROOT_URL}/account/api-keys/${keyId}`, { withCredentials: true })
|
||||||
|
.then((response) => {
|
||||||
|
dispatch({
|
||||||
|
type: ActionTypes.API_KEY_REMOVED,
|
||||||
|
user: response.data
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(response => Promise.reject(new Error(response.data.error)));
|
||||||
|
}
|
||||||
|
|
123
client/modules/User/components/APIKeyForm.jsx
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import InlineSVG from 'react-inlinesvg';
|
||||||
|
|
||||||
|
import CopyableInput from '../../IDE/components/CopyableInput';
|
||||||
|
|
||||||
|
import APIKeyList from './APIKeyList';
|
||||||
|
|
||||||
|
const plusIcon = require('../../../images/plus-icon.svg');
|
||||||
|
|
||||||
|
export const APIKeyPropType = PropTypes.shape({
|
||||||
|
id: PropTypes.object.isRequired,
|
||||||
|
token: PropTypes.object,
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
createdAt: PropTypes.string.isRequired,
|
||||||
|
lastUsedAt: PropTypes.string,
|
||||||
|
});
|
||||||
|
|
||||||
|
class APIKeyForm extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = { keyLabel: '' };
|
||||||
|
|
||||||
|
this.addKey = this.addKey.bind(this);
|
||||||
|
this.removeKey = this.removeKey.bind(this);
|
||||||
|
this.renderApiKeys = this.renderApiKeys.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
addKey(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
const { keyLabel } = this.state;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
keyLabel: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
this.props.createApiKey(keyLabel);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
removeKey(key) {
|
||||||
|
const message = `Are you sure you want to delete "${key.label}"?`;
|
||||||
|
|
||||||
|
if (window.confirm(message)) {
|
||||||
|
this.props.removeApiKey(key.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderApiKeys() {
|
||||||
|
const hasApiKeys = this.props.apiKeys && this.props.apiKeys.length > 0;
|
||||||
|
|
||||||
|
if (hasApiKeys) {
|
||||||
|
return (
|
||||||
|
<APIKeyList apiKeys={this.props.apiKeys} onRemove={this.removeKey} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <p>You have no exsiting tokens.</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const keyWithToken = this.props.apiKeys.find(k => !!k.token);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="api-key-form">
|
||||||
|
<p className="api-key-form__summary">
|
||||||
|
Personal Access Tokens act like your password to allow automated
|
||||||
|
scripts to access the Editor API. Create a token for each script
|
||||||
|
that needs access.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="api-key-form__section">
|
||||||
|
<h3 className="api-key-form__title">Create new token</h3>
|
||||||
|
<form className="form form--inline" onSubmit={this.addKey}>
|
||||||
|
<label htmlFor="keyLabel" className="form__label form__label--hidden ">What is this token for?</label>
|
||||||
|
<input
|
||||||
|
className="form__input"
|
||||||
|
id="keyLabel"
|
||||||
|
onChange={(event) => { this.setState({ keyLabel: event.target.value }); }}
|
||||||
|
placeholder="What is this token for? e.g. Example import script"
|
||||||
|
type="text"
|
||||||
|
value={this.state.keyLabel}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="api-key-form__create-button"
|
||||||
|
disabled={this.state.keyLabel === ''}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
<InlineSVG src={plusIcon} className="api-key-form__create-icon" />
|
||||||
|
Create
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{
|
||||||
|
keyWithToken && (
|
||||||
|
<div className="api-key-form__new-token">
|
||||||
|
<h4 className="api-key-form__new-token__title">Your new access token</h4>
|
||||||
|
<p className="api-key-form__new-token__info">
|
||||||
|
Make sure to copy your new personal access token now.
|
||||||
|
You won’t be able to see it again!
|
||||||
|
</p>
|
||||||
|
<CopyableInput label={keyWithToken.label} value={keyWithToken.token} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="api-key-form__section">
|
||||||
|
<h3 className="api-key-form__title">Existing tokens</h3>
|
||||||
|
{this.renderApiKeys()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
APIKeyForm.propTypes = {
|
||||||
|
apiKeys: PropTypes.arrayOf(PropTypes.shape(APIKeyPropType)).isRequired,
|
||||||
|
createApiKey: PropTypes.func.isRequired,
|
||||||
|
removeApiKey: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default APIKeyForm;
|
52
client/modules/User/components/APIKeyList.jsx
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import InlineSVG from 'react-inlinesvg';
|
||||||
|
import format from 'date-fns/format';
|
||||||
|
import distanceInWordsToNow from 'date-fns/distance_in_words_to_now';
|
||||||
|
import orderBy from 'lodash/orderBy';
|
||||||
|
|
||||||
|
import { APIKeyPropType } from './APIKeyForm';
|
||||||
|
|
||||||
|
const trashCan = require('../../../images/trash-can.svg');
|
||||||
|
|
||||||
|
function APIKeyList({ apiKeys, onRemove }) {
|
||||||
|
return (
|
||||||
|
<table className="api-key-list">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Created on</th>
|
||||||
|
<th>Last used</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{orderBy(apiKeys, ['createdAt'], ['desc']).map((key) => {
|
||||||
|
const lastUsed = key.lastUsedAt ?
|
||||||
|
distanceInWordsToNow(new Date(key.lastUsedAt), { addSuffix: true }) :
|
||||||
|
'Never';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={key.id}>
|
||||||
|
<td>{key.label}</td>
|
||||||
|
<td>{format(new Date(key.createdAt), 'MMM D, YYYY h:mm A')}</td>
|
||||||
|
<td>{lastUsed}</td>
|
||||||
|
<td className="api-key-list__action">
|
||||||
|
<button className="api-key-list__delete-button" onClick={() => onRemove(key)}>
|
||||||
|
<InlineSVG src={trashCan} alt="Delete Key" />
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
APIKeyList.propTypes = {
|
||||||
|
apiKeys: PropTypes.arrayOf(PropTypes.shape(APIKeyPropType)).isRequired,
|
||||||
|
onRemove: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default APIKeyList;
|
422
client/modules/User/components/Collection.jsx
Normal file
|
@ -0,0 +1,422 @@
|
||||||
|
import format from 'date-fns/format';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
import InlineSVG from 'react-inlinesvg';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { Link } from 'react-router';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import * as ProjectActions from '../../IDE/actions/project';
|
||||||
|
import * as ProjectsActions from '../../IDE/actions/projects';
|
||||||
|
import * as CollectionsActions from '../../IDE/actions/collections';
|
||||||
|
import * as ToastActions from '../../IDE/actions/toast';
|
||||||
|
import * as SortingActions from '../../IDE/actions/sorting';
|
||||||
|
import * as IdeActions from '../../IDE/actions/ide';
|
||||||
|
import { getCollection } from '../../IDE/selectors/collections';
|
||||||
|
import Loader from '../../App/components/loader';
|
||||||
|
import EditableInput from '../../IDE/components/EditableInput';
|
||||||
|
import Overlay from '../../App/components/Overlay';
|
||||||
|
import AddToCollectionSketchList from '../../IDE/components/AddToCollectionSketchList';
|
||||||
|
import CopyableInput from '../../IDE/components/CopyableInput';
|
||||||
|
import { SketchSearchbar } from '../../IDE/components/Searchbar';
|
||||||
|
import dropdownArrow from '../../../images/down-arrow.svg';
|
||||||
|
|
||||||
|
const arrowUp = require('../../../images/sort-arrow-up.svg');
|
||||||
|
const arrowDown = require('../../../images/sort-arrow-down.svg');
|
||||||
|
const removeIcon = require('../../../images/close.svg');
|
||||||
|
|
||||||
|
const ShareURL = ({ value }) => {
|
||||||
|
const [showURL, setShowURL] = useState(false);
|
||||||
|
const node = useRef();
|
||||||
|
|
||||||
|
const handleClickOutside = (e) => {
|
||||||
|
if (node.current.contains(e.target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setShowURL(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (showURL) {
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
} else {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
};
|
||||||
|
}, [showURL]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="collection-share" ref={node}>
|
||||||
|
<button
|
||||||
|
className="collection-share__button"
|
||||||
|
onClick={() => setShowURL(!showURL)}
|
||||||
|
>
|
||||||
|
<span>Share</span>
|
||||||
|
<InlineSVG className="collection-share__arrow" src={dropdownArrow} />
|
||||||
|
</button>
|
||||||
|
{ showURL &&
|
||||||
|
<div className="collection__share-dropdown">
|
||||||
|
<CopyableInput value={value} label="Link to Collection" />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ShareURL.propTypes = {
|
||||||
|
value: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
class CollectionItemRowBase extends React.Component {
|
||||||
|
handleSketchRemove = () => {
|
||||||
|
if (window.confirm(`Are you sure you want to remove "${this.props.item.project.name}" from this collection?`)) {
|
||||||
|
this.props.removeFromCollection(this.props.collection.id, this.props.item.project.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { item } = this.props;
|
||||||
|
const sketchOwnerUsername = item.project.user.username;
|
||||||
|
const sketchUrl = `/${item.project.user.username}/sketches/${item.project.id}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
className="sketches-table__row"
|
||||||
|
>
|
||||||
|
<th scope="row">
|
||||||
|
<Link to={sketchUrl}>
|
||||||
|
{item.project.name}
|
||||||
|
</Link>
|
||||||
|
</th>
|
||||||
|
<td>{format(new Date(item.createdAt), 'MMM D, YYYY h:mm A')}</td>
|
||||||
|
<td>{sketchOwnerUsername}</td>
|
||||||
|
<td className="collection-row__action-column ">
|
||||||
|
<button
|
||||||
|
className="collection-row__remove-button"
|
||||||
|
onClick={this.handleSketchRemove}
|
||||||
|
>
|
||||||
|
<InlineSVG src={removeIcon} alt="Remove" />
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectionItemRowBase.propTypes = {
|
||||||
|
collection: PropTypes.shape({
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
item: PropTypes.shape({
|
||||||
|
createdAt: PropTypes.string.isRequired,
|
||||||
|
project: PropTypes.shape({
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
user: PropTypes.shape({
|
||||||
|
username: PropTypes.string.isRequired
|
||||||
|
})
|
||||||
|
}).isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
user: PropTypes.shape({
|
||||||
|
username: PropTypes.string,
|
||||||
|
authenticated: PropTypes.bool.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
removeFromCollection: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
function mapDispatchToPropsSketchListRow(dispatch) {
|
||||||
|
return bindActionCreators(Object.assign({}, CollectionsActions, ProjectActions, IdeActions), dispatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
const CollectionItemRow = connect(null, mapDispatchToPropsSketchListRow)(CollectionItemRowBase);
|
||||||
|
|
||||||
|
class Collection extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.props.getCollections(this.props.username);
|
||||||
|
this.props.resetSorting();
|
||||||
|
this._renderFieldHeader = this._renderFieldHeader.bind(this);
|
||||||
|
this.showAddSketches = this.showAddSketches.bind(this);
|
||||||
|
this.hideAddSketches = this.hideAddSketches.bind(this);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isAddingSketches: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getTitle() {
|
||||||
|
if (this.props.username === this.props.user.username) {
|
||||||
|
return 'p5.js Web Editor | My collections';
|
||||||
|
}
|
||||||
|
return `p5.js Web Editor | ${this.props.username}'s collections`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getUsername() {
|
||||||
|
return this.props.username !== undefined ? this.props.username : this.props.user.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCollectionName() {
|
||||||
|
return this.props.collection.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
isOwner() {
|
||||||
|
let isOwner = false;
|
||||||
|
|
||||||
|
if (this.props.user != null &&
|
||||||
|
this.props.user.username &&
|
||||||
|
this.props.collection.owner.username === this.props.user.username) {
|
||||||
|
isOwner = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isOwner;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasCollection() {
|
||||||
|
return !this.props.loading && this.props.collection != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasCollectionItems() {
|
||||||
|
return this.hasCollection() && this.props.collection.items.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderLoader() {
|
||||||
|
if (this.props.loading) return <Loader />;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderCollectionMetadata() {
|
||||||
|
const {
|
||||||
|
id, name, description, items, owner
|
||||||
|
} = this.props.collection;
|
||||||
|
|
||||||
|
const hostname = window.location.origin;
|
||||||
|
const { username } = this.props;
|
||||||
|
|
||||||
|
const baseURL = `${hostname}/${username}/collections/`;
|
||||||
|
|
||||||
|
const handleEditCollectionName = (value) => {
|
||||||
|
if (value === name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.editCollection(id, { name: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditCollectionDescription = (value) => {
|
||||||
|
if (value === description) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.editCollection(id, { description: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// TODO: Implement UI for editing slug
|
||||||
|
//
|
||||||
|
// const handleEditCollectionSlug = (value) => {
|
||||||
|
// if (value === slug) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// this.props.editCollection(id, { slug: value });
|
||||||
|
// };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`collection-metadata ${this.isOwner() ? 'collection-metadata--is-owner' : ''}`}>
|
||||||
|
<div className="collection-metadata__columns">
|
||||||
|
<div className="collection-metadata__column--left">
|
||||||
|
<h2 className="collection-metadata__name">
|
||||||
|
{
|
||||||
|
this.isOwner() ?
|
||||||
|
<EditableInput value={name} onChange={handleEditCollectionName} validate={value => value !== ''} /> :
|
||||||
|
name
|
||||||
|
}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p className="collection-metadata__description">
|
||||||
|
{
|
||||||
|
this.isOwner() ?
|
||||||
|
<EditableInput
|
||||||
|
InputComponent="textarea"
|
||||||
|
value={description}
|
||||||
|
onChange={handleEditCollectionDescription}
|
||||||
|
emptyPlaceholder="Add description"
|
||||||
|
/> :
|
||||||
|
description
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="collection-metadata__user">Collection by{' '}
|
||||||
|
<Link to={`${hostname}/${username}/sketches`}>{owner.username}</Link>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="collection-metadata__user">{items.length} sketch{items.length === 1 ? '' : 'es'}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="collection-metadata__column--right">
|
||||||
|
<p className="collection-metadata__share">
|
||||||
|
<ShareURL value={`${baseURL}${id}`} />
|
||||||
|
</p>
|
||||||
|
{
|
||||||
|
this.isOwner() &&
|
||||||
|
<button className="collection-metadata__add-button" onClick={this.showAddSketches}>
|
||||||
|
Add Sketch
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
showAddSketches() {
|
||||||
|
this.setState({
|
||||||
|
isAddingSketches: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
hideAddSketches() {
|
||||||
|
this.setState({
|
||||||
|
isAddingSketches: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderEmptyTable() {
|
||||||
|
const isLoading = this.props.loading;
|
||||||
|
const hasCollectionItems = this.props.collection != null &&
|
||||||
|
this.props.collection.items.length > 0;
|
||||||
|
|
||||||
|
if (!isLoading && !hasCollectionItems) {
|
||||||
|
return (<p className="collection-empty-message">No sketches in collection</p>);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderFieldHeader(fieldName, displayName) {
|
||||||
|
const { field, direction } = this.props.sorting;
|
||||||
|
const headerClass = classNames({
|
||||||
|
'sketches-table__header': true,
|
||||||
|
'sketches-table__header--selected': field === fieldName
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<th scope="col">
|
||||||
|
<button className="sketch-list__sort-button" onClick={() => this.props.toggleDirectionForField(fieldName)}>
|
||||||
|
<span className={headerClass}>{displayName}</span>
|
||||||
|
{field === fieldName && direction === SortingActions.DIRECTION.ASC &&
|
||||||
|
<InlineSVG src={arrowUp} />
|
||||||
|
}
|
||||||
|
{field === fieldName && direction === SortingActions.DIRECTION.DESC &&
|
||||||
|
<InlineSVG src={arrowDown} />
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</th>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const title = this.hasCollection() ? this.getCollectionName() : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="collection-container" data-has-items={this.hasCollectionItems() ? 'true' : 'false'}>
|
||||||
|
<Helmet>
|
||||||
|
<title>{this.getTitle()}</title>
|
||||||
|
</Helmet>
|
||||||
|
{this._renderLoader()}
|
||||||
|
{this.hasCollection() && this._renderCollectionMetadata()}
|
||||||
|
<div className="collection-table-wrapper">
|
||||||
|
{this._renderEmptyTable()}
|
||||||
|
{this.hasCollectionItems() &&
|
||||||
|
<table className="sketches-table" summary="table containing all collections">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{this._renderFieldHeader('name', 'Name')}
|
||||||
|
{this._renderFieldHeader('createdAt', 'Date Added')}
|
||||||
|
{this._renderFieldHeader('user', 'Owner')}
|
||||||
|
<th scope="col"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{this.props.collection.items.map(item =>
|
||||||
|
(<CollectionItemRow
|
||||||
|
key={item.id}
|
||||||
|
item={item}
|
||||||
|
user={this.props.user}
|
||||||
|
username={this.getUsername()}
|
||||||
|
collection={this.props.collection}
|
||||||
|
/>))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
this.state.isAddingSketches && (
|
||||||
|
<Overlay
|
||||||
|
title="Add sketch"
|
||||||
|
actions={<SketchSearchbar />}
|
||||||
|
closeOverlay={this.hideAddSketches}
|
||||||
|
isFixedHeight
|
||||||
|
>
|
||||||
|
<div className="collection-add-sketch">
|
||||||
|
<AddToCollectionSketchList username={this.props.username} collection={this.props.collection} />
|
||||||
|
</div>
|
||||||
|
</Overlay>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Collection.propTypes = {
|
||||||
|
user: PropTypes.shape({
|
||||||
|
username: PropTypes.string,
|
||||||
|
authenticated: PropTypes.bool.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
getCollections: PropTypes.func.isRequired,
|
||||||
|
collection: PropTypes.shape({
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
slug: PropTypes.string,
|
||||||
|
description: PropTypes.string,
|
||||||
|
owner: PropTypes.shape({
|
||||||
|
username: PropTypes.string.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.shape({})),
|
||||||
|
}).isRequired,
|
||||||
|
username: PropTypes.string,
|
||||||
|
loading: PropTypes.bool.isRequired,
|
||||||
|
toggleDirectionForField: PropTypes.func.isRequired,
|
||||||
|
editCollection: PropTypes.func.isRequired,
|
||||||
|
resetSorting: PropTypes.func.isRequired,
|
||||||
|
sorting: PropTypes.shape({
|
||||||
|
field: PropTypes.string.isRequired,
|
||||||
|
direction: PropTypes.string.isRequired
|
||||||
|
}).isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
Collection.defaultProps = {
|
||||||
|
username: undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
function mapStateToProps(state, ownProps) {
|
||||||
|
return {
|
||||||
|
user: state.user,
|
||||||
|
collection: getCollection(state, ownProps.collectionId),
|
||||||
|
sorting: state.sorting,
|
||||||
|
loading: state.loading,
|
||||||
|
project: state.project
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return bindActionCreators(
|
||||||
|
Object.assign({}, CollectionsActions, ProjectsActions, ToastActions, SortingActions),
|
||||||
|
dispatch
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(Collection);
|
128
client/modules/User/components/CollectionCreate.jsx
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import { Helmet } from 'react-helmet';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { browserHistory } from 'react-router';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import * as CollectionsActions from '../../IDE/actions/collections';
|
||||||
|
|
||||||
|
import { generateCollectionName } from '../../../utils/generateRandomName';
|
||||||
|
|
||||||
|
class CollectionCreate extends React.Component {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
const name = generateCollectionName();
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
generatedCollectionName: name,
|
||||||
|
collection: {
|
||||||
|
name,
|
||||||
|
description: ''
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getTitle() {
|
||||||
|
return 'p5.js Web Editor | Create collection';
|
||||||
|
}
|
||||||
|
|
||||||
|
handleTextChange = field => (evt) => {
|
||||||
|
this.setState({
|
||||||
|
collection: {
|
||||||
|
...this.state.collection,
|
||||||
|
[field]: evt.target.value,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCreateCollection = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
this.props.createCollection(this.state.collection)
|
||||||
|
.then(({ id, owner }) => {
|
||||||
|
const pathname = `/${owner.username}/collections/${id}`;
|
||||||
|
const location = { pathname, state: { skipSavingPath: true } };
|
||||||
|
|
||||||
|
browserHistory.replace(location);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Error creating collection', error);
|
||||||
|
this.setState({
|
||||||
|
creationError: error,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { generatedCollectionName, creationError } = this.state;
|
||||||
|
const { name, description } = this.state.collection;
|
||||||
|
|
||||||
|
const invalid = name === '' || name == null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="collection-create">
|
||||||
|
<Helmet>
|
||||||
|
<title>{this.getTitle()}</title>
|
||||||
|
</Helmet>
|
||||||
|
<div className="sketches-table-container">
|
||||||
|
<form className="form" onSubmit={this.handleCreateCollection}>
|
||||||
|
{creationError && <span className="form-error">Couldn't create collection</span>}
|
||||||
|
<p className="form__field">
|
||||||
|
<label htmlFor="name" className="form__label">Collection name</label>
|
||||||
|
<input
|
||||||
|
className="form__input"
|
||||||
|
aria-label="name"
|
||||||
|
type="text"
|
||||||
|
id="name"
|
||||||
|
value={name}
|
||||||
|
placeholder={generatedCollectionName}
|
||||||
|
onChange={this.handleTextChange('name')}
|
||||||
|
/>
|
||||||
|
{invalid && <span className="form-error">Collection name is required</span>}
|
||||||
|
</p>
|
||||||
|
<p className="form__field">
|
||||||
|
<label htmlFor="description" className="form__label">Description (optional)</label>
|
||||||
|
<textarea
|
||||||
|
className="form__input form__input-flexible-height"
|
||||||
|
aria-label="description"
|
||||||
|
type="text"
|
||||||
|
id="description"
|
||||||
|
value={description}
|
||||||
|
onChange={this.handleTextChange('description')}
|
||||||
|
placeholder="My fave sketches"
|
||||||
|
rows="4"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<input type="submit" disabled={invalid} value="Create collection" aria-label="create collection" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectionCreate.propTypes = {
|
||||||
|
user: PropTypes.shape({
|
||||||
|
username: PropTypes.string,
|
||||||
|
authenticated: PropTypes.bool.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
createCollection: PropTypes.func.isRequired,
|
||||||
|
collection: PropTypes.shape({}).isRequired, // TODO
|
||||||
|
sorting: PropTypes.shape({
|
||||||
|
field: PropTypes.string.isRequired,
|
||||||
|
direction: PropTypes.string.isRequired
|
||||||
|
}).isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
function mapStateToProps(state, ownProps) {
|
||||||
|
return {
|
||||||
|
user: state.user,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return bindActionCreators(Object.assign({}, CollectionsActions), dispatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(CollectionCreate);
|
47
client/modules/User/components/DashboardTabSwitcher.jsx
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import { Link } from 'react-router';
|
||||||
|
|
||||||
|
const TabKey = {
|
||||||
|
assets: 'assets',
|
||||||
|
collections: 'collections',
|
||||||
|
sketches: 'sketches',
|
||||||
|
};
|
||||||
|
|
||||||
|
const Tab = ({ children, isSelected, to }) => {
|
||||||
|
const selectedClassName = 'dashboard-header__tab--selected';
|
||||||
|
|
||||||
|
const location = { pathname: to, state: { skipSavingPath: true } };
|
||||||
|
const content = isSelected ? <span>{children}</span> : <Link to={location}>{children}</Link>;
|
||||||
|
return (
|
||||||
|
<li className={`dashboard-header__tab ${isSelected && selectedClassName}`}>
|
||||||
|
<h4 className="dashboard-header__tab__title">
|
||||||
|
{content}
|
||||||
|
</h4>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Tab.propTypes = {
|
||||||
|
children: PropTypes.string.isRequired,
|
||||||
|
isSelected: PropTypes.bool.isRequired,
|
||||||
|
to: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
const DashboardTabSwitcher = ({ currentTab, isOwner, username }) => (
|
||||||
|
<ul className="dashboard-header__switcher">
|
||||||
|
<div className="dashboard-header__tabs">
|
||||||
|
<Tab to={`/${username}/sketches`} isSelected={currentTab === TabKey.sketches}>Sketches</Tab>
|
||||||
|
<Tab to={`/${username}/collections`} isSelected={currentTab === TabKey.collections}>Collections</Tab>
|
||||||
|
{isOwner && <Tab to={`/${username}/assets`} isSelected={currentTab === TabKey.assets}>Assets</Tab>}
|
||||||
|
</div>
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
|
||||||
|
DashboardTabSwitcher.propTypes = {
|
||||||
|
currentTab: PropTypes.string.isRequired,
|
||||||
|
isOwner: PropTypes.bool.isRequired,
|
||||||
|
username: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { DashboardTabSwitcher as default, TabKey };
|
|
@ -2,54 +2,72 @@ import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { reduxForm } from 'redux-form';
|
import { reduxForm } from 'redux-form';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { browserHistory } from 'react-router';
|
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { updateSettings, initiateVerification } from '../actions';
|
import { updateSettings, initiateVerification, createApiKey, removeApiKey } from '../actions';
|
||||||
import AccountForm from '../components/AccountForm';
|
import AccountForm from '../components/AccountForm';
|
||||||
import { validateSettings } from '../../../utils/reduxFormUtils';
|
import { validateSettings } from '../../../utils/reduxFormUtils';
|
||||||
import GithubButton from '../components/GithubButton';
|
import GithubButton from '../components/GithubButton';
|
||||||
|
import GoogleButton from '../components/GoogleButton';
|
||||||
|
import APIKeyForm from '../components/APIKeyForm';
|
||||||
|
import Nav from '../../../components/Nav';
|
||||||
|
|
||||||
const exitUrl = require('../../../images/exit.svg');
|
const __process = (typeof global !== 'undefined' ? global : window).process;
|
||||||
const logoUrl = require('../../../images/p5js-logo.svg');
|
const ROOT_URL = __process.env.API_URL;
|
||||||
|
|
||||||
|
function SocialLoginPanel(props) {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<AccountForm {...props} />
|
||||||
|
<h2 className="form-container__divider">Social Login</h2>
|
||||||
|
<p className="account__social-text">
|
||||||
|
Use your GitHub or Google account to log into the p5.js Web Editor.
|
||||||
|
</p>
|
||||||
|
<GithubButton buttonText="Login with GitHub" />
|
||||||
|
<GoogleButton buttonText="Login with Google" />
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
class AccountView extends React.Component {
|
class AccountView extends React.Component {
|
||||||
constructor(props) {
|
componentDidMount() {
|
||||||
super(props);
|
document.body.className = this.props.theme;
|
||||||
this.closeAccountPage = this.closeAccountPage.bind(this);
|
|
||||||
this.gotoHomePage = this.gotoHomePage.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
closeAccountPage() {
|
|
||||||
browserHistory.push(this.props.previousPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
gotoHomePage() {
|
|
||||||
browserHistory.push('/');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const accessTokensUIEnabled = window.process.env.UI_ACCESS_TOKEN_ENABLED;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="form-container">
|
<div className="account-settings__container">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>p5.js Web Editor | Account</title>
|
<title>p5.js Web Editor | Account Settings</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="form-container__header">
|
|
||||||
<button className="form-container__logo-button" onClick={this.gotoHomePage}>
|
<Nav layout="dashboard" />
|
||||||
<InlineSVG src={logoUrl} alt="p5js Logo" />
|
|
||||||
</button>
|
<section className="account-settings">
|
||||||
<button className="form-container__exit-button" onClick={this.closeAccountPage}>
|
<header className="account-settings__header">
|
||||||
<InlineSVG src={exitUrl} alt="Close Account Page" />
|
<h1 className="account-settings__title">Account Settings</h1>
|
||||||
</button>
|
</header>
|
||||||
</div>
|
{accessTokensUIEnabled &&
|
||||||
<div className="form-container__content">
|
<Tabs className="account__tabs">
|
||||||
<h2 className="form-container__title">My Account</h2>
|
<TabList>
|
||||||
<AccountForm {...this.props} />
|
<div className="tabs__titles">
|
||||||
<h2 className="form-container__divider">Or</h2>
|
<Tab><h4 className="tabs__title">Account</h4></Tab>
|
||||||
<GithubButton buttonText="Login with Github" />
|
{accessTokensUIEnabled && <Tab><h4 className="tabs__title">Access Tokens</h4></Tab>}
|
||||||
</div>
|
</div>
|
||||||
|
</TabList>
|
||||||
|
<TabPanel>
|
||||||
|
<SocialLoginPanel {...this.props} />
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel>
|
||||||
|
<APIKeyForm {...this.props} />
|
||||||
|
</TabPanel>
|
||||||
|
</Tabs>
|
||||||
|
}
|
||||||
|
{ !accessTokensUIEnabled && <SocialLoginPanel {...this.props} /> }
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -58,13 +76,17 @@ class AccountView extends React.Component {
|
||||||
function mapStateToProps(state) {
|
function mapStateToProps(state) {
|
||||||
return {
|
return {
|
||||||
initialValues: state.user, // <- initialValues for reduxForm
|
initialValues: state.user, // <- initialValues for reduxForm
|
||||||
|
previousPath: state.ide.previousPath,
|
||||||
user: state.user,
|
user: state.user,
|
||||||
previousPath: state.ide.previousPath
|
apiKeys: state.user.apiKeys,
|
||||||
|
theme: state.preferences.theme
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch) {
|
function mapDispatchToProps(dispatch) {
|
||||||
return bindActionCreators({ updateSettings, initiateVerification }, dispatch);
|
return bindActionCreators({
|
||||||
|
updateSettings, initiateVerification, createApiKey, removeApiKey
|
||||||
|
}, dispatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
function asyncValidate(formProps, dispatch, props) {
|
function asyncValidate(formProps, dispatch, props) {
|
||||||
|
@ -73,7 +95,7 @@ function asyncValidate(formProps, dispatch, props) {
|
||||||
const queryParams = {};
|
const queryParams = {};
|
||||||
queryParams[fieldToValidate] = formProps[fieldToValidate];
|
queryParams[fieldToValidate] = formProps[fieldToValidate];
|
||||||
queryParams.check_type = fieldToValidate;
|
queryParams.check_type = fieldToValidate;
|
||||||
return axios.get('/api/signup/duplicate_check', { params: queryParams })
|
return axios.get(`${ROOT_URL}/signup/duplicate_check`, { params: queryParams })
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.data.exists) {
|
if (response.data.exists) {
|
||||||
const error = {};
|
const error = {};
|
||||||
|
@ -87,6 +109,7 @@ function asyncValidate(formProps, dispatch, props) {
|
||||||
|
|
||||||
AccountView.propTypes = {
|
AccountView.propTypes = {
|
||||||
previousPath: PropTypes.string.isRequired,
|
previousPath: PropTypes.string.isRequired,
|
||||||
|
theme: PropTypes.string.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default reduxForm({
|
export default reduxForm({
|
||||||
|
|
91
client/modules/User/pages/CollectionView.jsx
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import Nav from '../../../components/Nav';
|
||||||
|
|
||||||
|
import CollectionCreate from '../components/CollectionCreate';
|
||||||
|
import Collection from '../components/Collection';
|
||||||
|
|
||||||
|
class CollectionView extends React.Component {
|
||||||
|
static defaultProps = {
|
||||||
|
user: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
document.body.className = this.props.theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
ownerName() {
|
||||||
|
if (this.props.params.username) {
|
||||||
|
return this.props.params.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.user.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
pageTitle() {
|
||||||
|
if (this.isCreatePage()) {
|
||||||
|
return 'Create collection';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'collection';
|
||||||
|
}
|
||||||
|
|
||||||
|
isOwner() {
|
||||||
|
return this.props.user.username === this.props.params.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
isCreatePage() {
|
||||||
|
const path = this.props.location.pathname;
|
||||||
|
return /create$/.test(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderContent() {
|
||||||
|
if (this.isCreatePage() && this.isOwner()) {
|
||||||
|
return <CollectionCreate />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Collection
|
||||||
|
collectionId={this.props.params.collection_id}
|
||||||
|
username={this.props.params.username}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="dashboard">
|
||||||
|
<Nav layout="dashboard" />
|
||||||
|
|
||||||
|
{this.renderContent()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
user: state.user,
|
||||||
|
theme: state.preferences.theme
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
}
|
||||||
|
|
||||||
|
CollectionView.propTypes = {
|
||||||
|
location: PropTypes.shape({
|
||||||
|
pathname: PropTypes.string.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
params: PropTypes.shape({
|
||||||
|
collection_id: PropTypes.string.isRequired,
|
||||||
|
username: PropTypes.string.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
theme: PropTypes.string.isRequired,
|
||||||
|
user: PropTypes.shape({
|
||||||
|
username: PropTypes.string.isRequired,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(CollectionView);
|
178
client/modules/User/pages/DashboardView.jsx
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { bindActionCreators } from 'redux';
|
||||||
|
import { browserHistory, Link } from 'react-router';
|
||||||
|
import { updateSettings, initiateVerification, createApiKey, removeApiKey } from '../actions';
|
||||||
|
import Nav from '../../../components/Nav';
|
||||||
|
import Overlay from '../../App/components/Overlay';
|
||||||
|
|
||||||
|
import AssetList from '../../IDE/components/AssetList';
|
||||||
|
import AssetSize from '../../IDE/components/AssetSize';
|
||||||
|
import CollectionList from '../../IDE/components/CollectionList';
|
||||||
|
import SketchList from '../../IDE/components/SketchList';
|
||||||
|
import { CollectionSearchbar, SketchSearchbar } from '../../IDE/components/Searchbar';
|
||||||
|
|
||||||
|
import CollectionCreate from '../components/CollectionCreate';
|
||||||
|
import DashboardTabSwitcher, { TabKey } from '../components/DashboardTabSwitcher';
|
||||||
|
|
||||||
|
class DashboardView extends React.Component {
|
||||||
|
static defaultProps = {
|
||||||
|
user: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.closeAccountPage = this.closeAccountPage.bind(this);
|
||||||
|
this.gotoHomePage = this.gotoHomePage.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
document.body.className = this.props.theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
closeAccountPage() {
|
||||||
|
browserHistory.push(this.props.previousPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
gotoHomePage() {
|
||||||
|
browserHistory.push('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedTabKey() {
|
||||||
|
const path = this.props.location.pathname;
|
||||||
|
|
||||||
|
if (/assets/.test(path)) {
|
||||||
|
return TabKey.assets;
|
||||||
|
} else if (/collections/.test(path)) {
|
||||||
|
return TabKey.collections;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TabKey.sketches;
|
||||||
|
}
|
||||||
|
|
||||||
|
ownerName() {
|
||||||
|
if (this.props.params.username) {
|
||||||
|
return this.props.params.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.user.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
isOwner() {
|
||||||
|
return this.props.user.username === this.props.params.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
isCollectionCreate() {
|
||||||
|
const path = this.props.location.pathname;
|
||||||
|
return /collections\/create$/.test(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
returnToDashboard = () => {
|
||||||
|
browserHistory.push(`/${this.ownerName()}/collections`);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderActionButton(tabKey, username) {
|
||||||
|
switch (tabKey) {
|
||||||
|
case TabKey.assets:
|
||||||
|
return this.isOwner() && <AssetSize />;
|
||||||
|
case TabKey.collections:
|
||||||
|
return this.isOwner() && (
|
||||||
|
<React.Fragment>
|
||||||
|
<Link className="dashboard__action-button" to={`/${username}/collections/create`}>
|
||||||
|
Create collection
|
||||||
|
</Link>
|
||||||
|
<CollectionSearchbar />
|
||||||
|
</React.Fragment>);
|
||||||
|
case TabKey.sketches:
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
{this.isOwner() && <Link className="dashboard__action-button" to="/">New sketch</Link>}
|
||||||
|
<SketchSearchbar />
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderContent(tabKey, username) {
|
||||||
|
switch (tabKey) {
|
||||||
|
case TabKey.assets:
|
||||||
|
return <AssetList username={username} />;
|
||||||
|
case TabKey.collections:
|
||||||
|
return <CollectionList username={username} />;
|
||||||
|
case TabKey.sketches:
|
||||||
|
default:
|
||||||
|
return <SketchList username={username} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const currentTab = this.selectedTabKey();
|
||||||
|
const isOwner = this.isOwner();
|
||||||
|
const { username } = this.props.params;
|
||||||
|
const actions = this.renderActionButton(currentTab, username);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="dashboard">
|
||||||
|
<Nav layout="dashboard" />
|
||||||
|
|
||||||
|
<section className="dashboard-header">
|
||||||
|
<div className="dashboard-header__header">
|
||||||
|
<h2 className="dashboard-header__header__title">{this.ownerName()}</h2>
|
||||||
|
<div className="dashboard-header__nav">
|
||||||
|
<DashboardTabSwitcher currentTab={currentTab} isOwner={isOwner} username={username} />
|
||||||
|
{actions &&
|
||||||
|
<div className="dashboard-header__actions">
|
||||||
|
{actions}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="dashboard-content">
|
||||||
|
{this.renderContent(currentTab, username)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{this.isCollectionCreate() &&
|
||||||
|
<Overlay
|
||||||
|
title="Create collection"
|
||||||
|
closeOverlay={this.returnToDashboard}
|
||||||
|
>
|
||||||
|
<CollectionCreate />
|
||||||
|
</Overlay>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
previousPath: state.ide.previousPath,
|
||||||
|
user: state.user,
|
||||||
|
theme: state.preferences.theme,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch) {
|
||||||
|
return bindActionCreators({
|
||||||
|
updateSettings, initiateVerification, createApiKey, removeApiKey
|
||||||
|
}, dispatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
DashboardView.propTypes = {
|
||||||
|
location: PropTypes.shape({
|
||||||
|
pathname: PropTypes.string.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
params: PropTypes.shape({
|
||||||
|
username: PropTypes.string.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
previousPath: PropTypes.string.isRequired,
|
||||||
|
theme: PropTypes.string.isRequired,
|
||||||
|
user: PropTypes.shape({
|
||||||
|
username: PropTypes.string,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(DashboardView);
|
|
@ -3,13 +3,10 @@ import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { browserHistory } from 'react-router';
|
import { browserHistory } from 'react-router';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { verifyEmailConfirmation } from '../actions';
|
import { verifyEmailConfirmation } from '../actions';
|
||||||
|
import Nav from '../../../components/Nav';
|
||||||
const exitUrl = require('../../../images/exit.svg');
|
|
||||||
const logoUrl = require('../../../images/p5js-logo.svg');
|
|
||||||
|
|
||||||
|
|
||||||
class EmailVerificationView extends React.Component {
|
class EmailVerificationView extends React.Component {
|
||||||
|
@ -17,12 +14,6 @@ class EmailVerificationView extends React.Component {
|
||||||
emailVerificationTokenState: null,
|
emailVerificationTokenState: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.closeLoginPage = this.closeLoginPage.bind(this);
|
|
||||||
this.gotoHomePage = this.gotoHomePage.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
const verificationToken = this.verificationToken();
|
const verificationToken = this.verificationToken();
|
||||||
if (verificationToken != null) {
|
if (verificationToken != null) {
|
||||||
|
@ -32,14 +23,6 @@ class EmailVerificationView extends React.Component {
|
||||||
|
|
||||||
verificationToken = () => get(this.props, 'location.query.t', null);
|
verificationToken = () => get(this.props, 'location.query.t', null);
|
||||||
|
|
||||||
closeLoginPage() {
|
|
||||||
browserHistory.push(this.props.previousPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
gotoHomePage() {
|
|
||||||
browserHistory.push('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let status = null;
|
let status = null;
|
||||||
const {
|
const {
|
||||||
|
@ -48,7 +31,7 @@ class EmailVerificationView extends React.Component {
|
||||||
|
|
||||||
if (this.verificationToken() == null) {
|
if (this.verificationToken() == null) {
|
||||||
status = (
|
status = (
|
||||||
<p>That link is invalid</p>
|
<p>That link is invalid.</p>
|
||||||
);
|
);
|
||||||
} else if (emailVerificationTokenState === 'checking') {
|
} else if (emailVerificationTokenState === 'checking') {
|
||||||
status = (
|
status = (
|
||||||
|
@ -58,6 +41,7 @@ class EmailVerificationView extends React.Component {
|
||||||
status = (
|
status = (
|
||||||
<p>All done, your email address has been verified.</p>
|
<p>All done, your email address has been verified.</p>
|
||||||
);
|
);
|
||||||
|
setTimeout(() => browserHistory.push('/'), 1000);
|
||||||
} else if (emailVerificationTokenState === 'invalid') {
|
} else if (emailVerificationTokenState === 'invalid') {
|
||||||
status = (
|
status = (
|
||||||
<p>Something went wrong.</p>
|
<p>Something went wrong.</p>
|
||||||
|
@ -65,23 +49,18 @@ class EmailVerificationView extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="email-verification">
|
||||||
|
<Nav layout="dashboard" />
|
||||||
<div className="form-container">
|
<div className="form-container">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>p5.js Web Editor | Email Verification</title>
|
<title>p5.js Web Editor | Email Verification</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="form-container__header">
|
|
||||||
<button className="form-container__logo-button" onClick={this.gotoHomePage}>
|
|
||||||
<InlineSVG src={logoUrl} alt="p5js Logo" />
|
|
||||||
</button>
|
|
||||||
<button className="form-container__exit-button" onClick={this.closeLoginPage}>
|
|
||||||
<InlineSVG src={exitUrl} alt="Close Login Page" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="form-container__content">
|
<div className="form-container__content">
|
||||||
<h2 className="form-container__title">Verify your email</h2>
|
<h2 className="form-container__title">Verify your email</h2>
|
||||||
{status}
|
{status}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -89,7 +68,6 @@ class EmailVerificationView extends React.Component {
|
||||||
function mapStateToProps(state) {
|
function mapStateToProps(state) {
|
||||||
return {
|
return {
|
||||||
emailVerificationTokenState: state.user.emailVerificationTokenState,
|
emailVerificationTokenState: state.user.emailVerificationTokenState,
|
||||||
previousPath: state.ide.previousPath
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,7 +79,6 @@ function mapDispatchToProps(dispatch) {
|
||||||
|
|
||||||
|
|
||||||
EmailVerificationView.propTypes = {
|
EmailVerificationView.propTypes = {
|
||||||
previousPath: PropTypes.string.isRequired,
|
|
||||||
emailVerificationTokenState: PropTypes.oneOf([
|
emailVerificationTokenState: PropTypes.oneOf([
|
||||||
'checking', 'verified', 'invalid'
|
'checking', 'verified', 'invalid'
|
||||||
]),
|
]),
|
||||||
|
|
|
@ -2,16 +2,13 @@ import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { reduxForm } from 'redux-form';
|
import { reduxForm } from 'redux-form';
|
||||||
import { Link, browserHistory } from 'react-router';
|
import { Link, browserHistory } from 'react-router';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { validateAndLoginUser } from '../actions';
|
import { validateAndLoginUser } from '../actions';
|
||||||
import LoginForm from '../components/LoginForm';
|
import LoginForm from '../components/LoginForm';
|
||||||
import { validateLogin } from '../../../utils/reduxFormUtils';
|
import { validateLogin } from '../../../utils/reduxFormUtils';
|
||||||
import GithubButton from '../components/GithubButton';
|
import GithubButton from '../components/GithubButton';
|
||||||
import GoogleButton from '../components/GoogleButton';
|
import GoogleButton from '../components/GoogleButton';
|
||||||
|
import Nav from '../../../components/Nav';
|
||||||
const exitUrl = require('../../../images/exit.svg');
|
|
||||||
const logoUrl = require('../../../images/p5js-logo.svg');
|
|
||||||
|
|
||||||
class LoginView extends React.Component {
|
class LoginView extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -34,18 +31,12 @@ class LoginView extends React.Component {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
<div className="login">
|
||||||
|
<Nav layout="dashboard" />
|
||||||
<div className="form-container">
|
<div className="form-container">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>p5.js Web Editor | Login</title>
|
<title>p5.js Web Editor | Login</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="form-container__header">
|
|
||||||
<button className="form-container__logo-button" onClick={this.gotoHomePage}>
|
|
||||||
<InlineSVG src={logoUrl} alt="p5js Logo" />
|
|
||||||
</button>
|
|
||||||
<button className="form-container__exit-button" onClick={this.closeLoginPage}>
|
|
||||||
<InlineSVG src={exitUrl} alt="Close Login Page" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="form-container__content">
|
<div className="form-container__content">
|
||||||
<h2 className="form-container__title">Log In</h2>
|
<h2 className="form-container__title">Log In</h2>
|
||||||
<LoginForm {...this.props} />
|
<LoginForm {...this.props} />
|
||||||
|
@ -62,6 +53,7 @@ class LoginView extends React.Component {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,61 +2,37 @@ import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { reduxForm } from 'redux-form';
|
import { reduxForm } from 'redux-form';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { browserHistory } from 'react-router';
|
|
||||||
import InlineSVG from 'react-inlinesvg';
|
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import NewPasswordForm from '../components/NewPasswordForm';
|
import NewPasswordForm from '../components/NewPasswordForm';
|
||||||
import * as UserActions from '../actions';
|
import * as UserActions from '../actions';
|
||||||
|
import Nav from '../../../components/Nav';
|
||||||
|
|
||||||
const exitUrl = require('../../../images/exit.svg');
|
function NewPasswordView(props) {
|
||||||
const logoUrl = require('../../../images/p5js-logo.svg');
|
|
||||||
|
|
||||||
class NewPasswordView extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.gotoHomePage = this.gotoHomePage.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
// need to check if this is a valid token
|
|
||||||
this.props.validateResetPasswordToken(this.props.params.reset_password_token);
|
|
||||||
}
|
|
||||||
|
|
||||||
gotoHomePage() {
|
|
||||||
browserHistory.push('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const newPasswordClass = classNames({
|
const newPasswordClass = classNames({
|
||||||
'new-password': true,
|
'new-password': true,
|
||||||
'new-password--invalid': this.props.user.resetPasswordInvalid,
|
'new-password--invalid': props.user.resetPasswordInvalid,
|
||||||
'form-container': true
|
'form-container': true,
|
||||||
|
'user': true
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
|
<div className="new-password-container">
|
||||||
|
<Nav layout="dashboard" />
|
||||||
<div className={newPasswordClass}>
|
<div className={newPasswordClass}>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>p5.js Web Editor | New Password</title>
|
<title>p5.js Web Editor | New Password</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="form-container__header">
|
|
||||||
<button className="form-container__logo-button" onClick={this.gotoHomePage}>
|
|
||||||
<InlineSVG src={logoUrl} alt="p5js Logo" />
|
|
||||||
</button>
|
|
||||||
<button className="form-container__exit-button" onClick={this.gotoHomePage}>
|
|
||||||
<InlineSVG src={exitUrl} alt="Close NewPassword Page" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="form-container__content">
|
<div className="form-container__content">
|
||||||
<h2 className="form-container__title">Set a New Password</h2>
|
<h2 className="form-container__title">Set a New Password</h2>
|
||||||
<NewPasswordForm {...this.props} />
|
<NewPasswordForm {...props} />
|
||||||
<p className="new-password__invalid">
|
<p className="new-password__invalid">
|
||||||
The password reset token is invalid or has expired.
|
The password reset token is invalid or has expired.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
NewPasswordView.propTypes = {
|
NewPasswordView.propTypes = {
|
||||||
params: PropTypes.shape({
|
params: PropTypes.shape({
|
||||||
|
|
|
@ -1,55 +1,33 @@
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link, browserHistory } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import { reduxForm } from 'redux-form';
|
import { reduxForm } from 'redux-form';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import * as UserActions from '../actions';
|
import * as UserActions from '../actions';
|
||||||
import ResetPasswordForm from '../components/ResetPasswordForm';
|
import ResetPasswordForm from '../components/ResetPasswordForm';
|
||||||
import { validateResetPassword } from '../../../utils/reduxFormUtils';
|
import { validateResetPassword } from '../../../utils/reduxFormUtils';
|
||||||
|
import Nav from '../../../components/Nav';
|
||||||
|
|
||||||
const exitUrl = require('../../../images/exit.svg');
|
function ResetPasswordView(props) {
|
||||||
const logoUrl = require('../../../images/p5js-logo.svg');
|
|
||||||
|
|
||||||
class ResetPasswordView extends React.Component {
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.gotoHomePage = this.gotoHomePage.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillMount() {
|
|
||||||
this.props.resetPasswordReset();
|
|
||||||
}
|
|
||||||
|
|
||||||
gotoHomePage() {
|
|
||||||
browserHistory.push('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const resetPasswordClass = classNames({
|
const resetPasswordClass = classNames({
|
||||||
'reset-password': true,
|
'reset-password': true,
|
||||||
'reset-password--submitted': this.props.user.resetPasswordInitiate,
|
'reset-password--submitted': props.user.resetPasswordInitiate,
|
||||||
'form-container': true
|
'form-container': true,
|
||||||
|
'user': true
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
|
<div className="reset-password-container">
|
||||||
|
<Nav layout="dashboard" />
|
||||||
<div className={resetPasswordClass}>
|
<div className={resetPasswordClass}>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>p5.js Web Editor | Reset Password</title>
|
<title>p5.js Web Editor | Reset Password</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="form-container__header">
|
|
||||||
<button className="form-container__logo-button" onClick={this.gotoHomePage}>
|
|
||||||
<InlineSVG src={logoUrl} alt="p5js Logo" />
|
|
||||||
</button>
|
|
||||||
<button className="form-container__exit-button" onClick={this.gotoHomePage}>
|
|
||||||
<InlineSVG src={exitUrl} alt="Close ResetPassword Page" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="form-container__content">
|
<div className="form-container__content">
|
||||||
<h2 className="form-container__title">Reset Your Password</h2>
|
<h2 className="form-container__title">Reset Your Password</h2>
|
||||||
<ResetPasswordForm {...this.props} />
|
<ResetPasswordForm {...props} />
|
||||||
<p className="reset-password__submitted">
|
<p className="reset-password__submitted">
|
||||||
Your password reset email should arrive shortly. If you don't see it, check
|
Your password reset email should arrive shortly. If you don't see it, check
|
||||||
in your spam folder as sometimes it can end up there.
|
in your spam folder as sometimes it can end up there.
|
||||||
|
@ -61,9 +39,9 @@ class ResetPasswordView extends React.Component {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
ResetPasswordView.propTypes = {
|
ResetPasswordView.propTypes = {
|
||||||
resetPasswordReset: PropTypes.func.isRequired,
|
resetPasswordReset: PropTypes.func.isRequired,
|
||||||
|
|
|
@ -4,27 +4,17 @@ import { bindActionCreators } from 'redux';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { Link, browserHistory } from 'react-router';
|
import { Link, browserHistory } from 'react-router';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
|
||||||
import { reduxForm } from 'redux-form';
|
import { reduxForm } from 'redux-form';
|
||||||
import * as UserActions from '../actions';
|
import * as UserActions from '../actions';
|
||||||
import SignupForm from '../components/SignupForm';
|
import SignupForm from '../components/SignupForm';
|
||||||
import { validateSignup } from '../../../utils/reduxFormUtils';
|
import { validateSignup } from '../../../utils/reduxFormUtils';
|
||||||
|
import Nav from '../../../components/Nav';
|
||||||
|
|
||||||
const exitUrl = require('../../../images/exit.svg');
|
const __process = (typeof global !== 'undefined' ? global : window).process;
|
||||||
const logoUrl = require('../../../images/p5js-logo.svg');
|
const ROOT_URL = __process.env.API_URL;
|
||||||
|
|
||||||
class SignupView extends React.Component {
|
class SignupView extends React.Component {
|
||||||
constructor(props) {
|
gotoHomePage = () => {
|
||||||
super(props);
|
|
||||||
this.closeSignupPage = this.closeSignupPage.bind(this);
|
|
||||||
this.gotoHomePage = this.gotoHomePage.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
closeSignupPage() {
|
|
||||||
browserHistory.push(this.props.previousPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
gotoHomePage() {
|
|
||||||
browserHistory.push('/');
|
browserHistory.push('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,18 +24,12 @@ class SignupView extends React.Component {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
<div className="signup">
|
||||||
|
<Nav layout="dashboard" />
|
||||||
<div className="form-container">
|
<div className="form-container">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>p5.js Web Editor | Signup</title>
|
<title>p5.js Web Editor | Signup</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="form-container__header">
|
|
||||||
<button className="form-container__logo-button" onClick={this.gotoHomePage}>
|
|
||||||
<InlineSVG src={logoUrl} alt="p5js Logo" />
|
|
||||||
</button>
|
|
||||||
<button className="form-container__exit-button" onClick={this.closeSignupPage}>
|
|
||||||
<InlineSVG src={exitUrl} alt="Close Signup Page" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="form-container__content">
|
<div className="form-container__content">
|
||||||
<h2 className="form-container__title">Sign Up</h2>
|
<h2 className="form-container__title">Sign Up</h2>
|
||||||
<SignupForm {...this.props} />
|
<SignupForm {...this.props} />
|
||||||
|
@ -55,6 +39,7 @@ class SignupView extends React.Component {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -95,7 +80,7 @@ function asyncValidate(formProps, dispatch, props) {
|
||||||
const queryParams = {};
|
const queryParams = {};
|
||||||
queryParams[fieldToValidate] = formProps[fieldToValidate];
|
queryParams[fieldToValidate] = formProps[fieldToValidate];
|
||||||
queryParams.check_type = fieldToValidate;
|
queryParams.check_type = fieldToValidate;
|
||||||
return axios.get('/api/signup/duplicate_check', { params: queryParams })
|
return axios.get(`${ROOT_URL}/signup/duplicate_check`, { params: queryParams })
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.data.exists) {
|
if (response.data.exists) {
|
||||||
errors[fieldToValidate] = response.data.message;
|
errors[fieldToValidate] = response.data.message;
|
||||||
|
@ -118,9 +103,9 @@ function onSubmitFail(errors) {
|
||||||
|
|
||||||
SignupView.propTypes = {
|
SignupView.propTypes = {
|
||||||
previousPath: PropTypes.string.isRequired,
|
previousPath: PropTypes.string.isRequired,
|
||||||
user: {
|
user: PropTypes.shape({
|
||||||
authenticated: PropTypes.bool
|
authenticated: PropTypes.bool
|
||||||
}
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
SignupView.defaultProps = {
|
SignupView.defaultProps = {
|
||||||
|
|
|
@ -31,6 +31,10 @@ const user = (state = { authenticated: false }, action) => {
|
||||||
return Object.assign({}, state, { emailVerificationTokenState: 'invalid' });
|
return Object.assign({}, state, { emailVerificationTokenState: 'invalid' });
|
||||||
case ActionTypes.SETTINGS_UPDATED:
|
case ActionTypes.SETTINGS_UPDATED:
|
||||||
return { ...state, ...action.user };
|
return { ...state, ...action.user };
|
||||||
|
case ActionTypes.API_KEY_REMOVED:
|
||||||
|
return { ...state, ...action.user };
|
||||||
|
case ActionTypes.API_KEY_CREATED:
|
||||||
|
return { ...state, ...action.user };
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import assets from './modules/IDE/reducers/assets';
|
||||||
import search from './modules/IDE/reducers/search';
|
import search from './modules/IDE/reducers/search';
|
||||||
import sorting from './modules/IDE/reducers/sorting';
|
import sorting from './modules/IDE/reducers/sorting';
|
||||||
import loading from './modules/IDE/reducers/loading';
|
import loading from './modules/IDE/reducers/loading';
|
||||||
|
import collections from './modules/IDE/reducers/collections';
|
||||||
|
|
||||||
const rootReducer = combineReducers({
|
const rootReducer = combineReducers({
|
||||||
form,
|
form,
|
||||||
|
@ -28,7 +29,8 @@ const rootReducer = combineReducers({
|
||||||
toast,
|
toast,
|
||||||
console,
|
console,
|
||||||
assets,
|
assets,
|
||||||
loading
|
loading,
|
||||||
|
collections
|
||||||
});
|
});
|
||||||
|
|
||||||
export default rootReducer;
|
export default rootReducer;
|
||||||
|
|
|
@ -9,9 +9,12 @@ import ResetPasswordView from './modules/User/pages/ResetPasswordView';
|
||||||
import EmailVerificationView from './modules/User/pages/EmailVerificationView';
|
import EmailVerificationView from './modules/User/pages/EmailVerificationView';
|
||||||
import NewPasswordView from './modules/User/pages/NewPasswordView';
|
import NewPasswordView from './modules/User/pages/NewPasswordView';
|
||||||
import AccountView from './modules/User/pages/AccountView';
|
import AccountView from './modules/User/pages/AccountView';
|
||||||
// import SketchListView from './modules/Sketch/pages/SketchListView';
|
import CollectionView from './modules/User/pages/CollectionView';
|
||||||
|
import DashboardView from './modules/User/pages/DashboardView';
|
||||||
|
import createRedirectWithUsername from './components/createRedirectWithUsername';
|
||||||
import { getUser } from './modules/User/actions';
|
import { getUser } from './modules/User/actions';
|
||||||
import { stopSketch } from './modules/IDE/actions/ide';
|
import { stopSketch } from './modules/IDE/actions/ide';
|
||||||
|
import { userIsAuthenticated, userIsNotAuthenticated, userIsAuthorized } from './utils/auth';
|
||||||
|
|
||||||
const checkAuth = (store) => {
|
const checkAuth = (store) => {
|
||||||
store.dispatch(getUser());
|
store.dispatch(getUser());
|
||||||
|
@ -24,9 +27,9 @@ const onRouteChange = (store) => {
|
||||||
const routes = store => (
|
const routes = store => (
|
||||||
<Route path="/" component={App} onChange={() => { onRouteChange(store); }}>
|
<Route path="/" component={App} onChange={() => { onRouteChange(store); }}>
|
||||||
<IndexRoute component={IDEView} onEnter={checkAuth(store)} />
|
<IndexRoute component={IDEView} onEnter={checkAuth(store)} />
|
||||||
<Route path="/login" component={LoginView} />
|
<Route path="/login" component={userIsNotAuthenticated(LoginView)} />
|
||||||
<Route path="/signup" component={SignupView} />
|
<Route path="/signup" component={userIsNotAuthenticated(SignupView)} />
|
||||||
<Route path="/reset-password" component={ResetPasswordView} />
|
<Route path="/reset-password" component={userIsNotAuthenticated(ResetPasswordView)} />
|
||||||
<Route path="/verify" component={EmailVerificationView} />
|
<Route path="/verify" component={EmailVerificationView} />
|
||||||
<Route
|
<Route
|
||||||
path="/reset-password/:reset_password_token"
|
path="/reset-password/:reset_password_token"
|
||||||
|
@ -35,11 +38,16 @@ const routes = store => (
|
||||||
<Route path="/projects/:project_id" component={IDEView} />
|
<Route path="/projects/:project_id" component={IDEView} />
|
||||||
<Route path="/:username/full/:project_id" component={FullView} />
|
<Route path="/:username/full/:project_id" component={FullView} />
|
||||||
<Route path="/full/:project_id" component={FullView} />
|
<Route path="/full/:project_id" component={FullView} />
|
||||||
<Route path="/sketches" component={IDEView} />
|
<Route path="/sketches" component={createRedirectWithUsername('/:username/sketches')} />
|
||||||
<Route path="/assets" component={IDEView} />
|
<Route path="/:username/assets" component={userIsAuthenticated(userIsAuthorized(DashboardView))} />
|
||||||
<Route path="/account" component={AccountView} />
|
<Route path="/assets" component={createRedirectWithUsername('/:username/assets')} />
|
||||||
|
<Route path="/account" component={userIsAuthenticated(AccountView)} />
|
||||||
<Route path="/:username/sketches/:project_id" component={IDEView} />
|
<Route path="/:username/sketches/:project_id" component={IDEView} />
|
||||||
<Route path="/:username/sketches" component={IDEView} />
|
<Route path="/:username/sketches/:project_id/add-to-collection" component={IDEView} />
|
||||||
|
<Route path="/:username/sketches" component={DashboardView} />
|
||||||
|
<Route path="/:username/collections" component={DashboardView} />
|
||||||
|
<Route path="/:username/collections/create" component={DashboardView} />
|
||||||
|
<Route path="/:username/collections/:collection_id" component={CollectionView} />
|
||||||
<Route path="/about" component={IDEView} />
|
<Route path="/about" component={IDEView} />
|
||||||
</Route>
|
</Route>
|
||||||
);
|
);
|
||||||
|
|
|
@ -83,6 +83,10 @@
|
||||||
border: 2px solid getThemifyVariable('button-border-color');
|
border: 2px solid getThemifyVariable('button-border-color');
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
padding: #{10 / $base-font-size}rem #{30 / $base-font-size}rem;
|
padding: #{10 / $base-font-size}rem #{30 / $base-font-size}rem;
|
||||||
|
& g {
|
||||||
|
fill: getThemifyVariable('button-color');
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
&:enabled:hover {
|
&:enabled:hover {
|
||||||
border-color: getThemifyVariable('button-background-hover-color');
|
border-color: getThemifyVariable('button-background-hover-color');
|
||||||
background-color: getThemifyVariable('button-background-hover-color');
|
background-color: getThemifyVariable('button-background-hover-color');
|
||||||
|
@ -102,27 +106,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
%forms-button {
|
|
||||||
background-color: $form-button-background-color;
|
|
||||||
color: $form-button-color;
|
|
||||||
cursor: pointer;
|
|
||||||
border: 2px solid $form-button-color;
|
|
||||||
border-radius: 2px;
|
|
||||||
padding: #{8 / $base-font-size}rem #{25 / $base-font-size}rem;
|
|
||||||
line-height: 1;
|
|
||||||
margin: #{39 / $base-font-size}rem 0 #{24 / $base-font-size}rem 0;
|
|
||||||
&:enabled:hover {
|
|
||||||
border-color: $form-button-background-hover-color;
|
|
||||||
background-color: $form-button-background-hover-color;
|
|
||||||
color: $form-button-hover-color;
|
|
||||||
}
|
|
||||||
&:enabled:active {
|
|
||||||
border-color: $form-button-background-active-color;
|
|
||||||
background-color: $form-button-background-active-color;
|
|
||||||
color: $form-button-active-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
%preferences-button {
|
%preferences-button {
|
||||||
@extend %toolbar-button;
|
@extend %toolbar-button;
|
||||||
@include themify() {
|
@include themify() {
|
||||||
|
|
|
@ -9,15 +9,22 @@ $orange: #ffa500;
|
||||||
$red: #ff0000;
|
$red: #ff0000;
|
||||||
$lightsteelblue: #B0C4DE;
|
$lightsteelblue: #B0C4DE;
|
||||||
$dodgerblue: #1E90FF;
|
$dodgerblue: #1E90FF;
|
||||||
$primary-text-color: #333;
|
|
||||||
$icon-color: #8b8b8b;
|
$icon-color: #8b8b8b;
|
||||||
$icon-hover-color: #333;
|
$icon-hover-color: #333;
|
||||||
$p5-contrast-pink: #FFA9D9;
|
$p5-contrast-pink: #FFA9D9;
|
||||||
|
|
||||||
|
// Grays
|
||||||
|
$dark: #333;
|
||||||
|
$middleGray: #7d7d7d;
|
||||||
|
$middleLight: #a6a6a6;
|
||||||
|
|
||||||
|
// Abstracts
|
||||||
|
$primary-text-color: $dark;
|
||||||
|
|
||||||
$themes: (
|
$themes: (
|
||||||
light: (
|
light: (
|
||||||
logo-color: $p5js-pink,
|
logo-color: $p5js-pink,
|
||||||
primary-text-color: #333,
|
primary-text-color: $primary-text-color,
|
||||||
dropzone-text-color: #333,
|
dropzone-text-color: #333,
|
||||||
modal-button-color: #333,
|
modal-button-color: #333,
|
||||||
heading-text-color: #333,
|
heading-text-color: #333,
|
||||||
|
@ -65,8 +72,35 @@ $themes: (
|
||||||
keyboard-shortcut-color: #757575,
|
keyboard-shortcut-color: #757575,
|
||||||
nav-hover-color: $p5js-pink,
|
nav-hover-color: $p5js-pink,
|
||||||
error-color: $p5js-pink,
|
error-color: $p5js-pink,
|
||||||
|
table-row-stripe-color: #d6d6d6,
|
||||||
codefold-icon-open: url(../images/triangle-arrow-down.svg),
|
codefold-icon-open: url(../images/triangle-arrow-down.svg),
|
||||||
codefold-icon-closed: url(../images/triangle-arrow-right.svg)
|
codefold-icon-closed: url(../images/triangle-arrow-right.svg),
|
||||||
|
|
||||||
|
primary-button-color: #fff,
|
||||||
|
primary-button-background-color: $p5js-pink,
|
||||||
|
|
||||||
|
table-button-color: $white,
|
||||||
|
table-button-background-color: #979797,
|
||||||
|
table-button-active-color: $white,
|
||||||
|
table-button-background-active-color: #00A1D3,
|
||||||
|
table-button-hover-color: $white,
|
||||||
|
table-button-background-hover-color: $p5js-pink,
|
||||||
|
|
||||||
|
progress-bar-background-color: #979797,
|
||||||
|
progress-bar-active-color: #f10046,
|
||||||
|
|
||||||
|
form-title-color: rgba(51, 51, 51, 0.87),
|
||||||
|
form-secondary-title-color: $middleGray,
|
||||||
|
form-input-text-color: $dark,
|
||||||
|
form-input-placeholder-text-color: $middleLight,
|
||||||
|
form-border-color: #b5b5b5,
|
||||||
|
form-button-background-color: $white,
|
||||||
|
form-button-color: #f10046,
|
||||||
|
form-button-background-hover-color: $p5js-pink,
|
||||||
|
form-button-background-active-color: #f10046,
|
||||||
|
form-button-hover-color: $white,
|
||||||
|
form-button-active-color: $white,
|
||||||
|
form-navigation-options-color: #999999
|
||||||
),
|
),
|
||||||
dark: (
|
dark: (
|
||||||
logo-color: $p5js-pink,
|
logo-color: $p5js-pink,
|
||||||
|
@ -117,8 +151,33 @@ $themes: (
|
||||||
keyboard-shortcut-color: #B5B5B5,
|
keyboard-shortcut-color: #B5B5B5,
|
||||||
nav-hover-color: $p5js-pink,
|
nav-hover-color: $p5js-pink,
|
||||||
error-color: $p5js-pink,
|
error-color: $p5js-pink,
|
||||||
|
table-row-stripe-color: #3f3f3f,
|
||||||
codefold-icon-open: url(../images/triangle-arrow-down-white.svg),
|
codefold-icon-open: url(../images/triangle-arrow-down-white.svg),
|
||||||
codefold-icon-closed: url(../images/triangle-arrow-right-white.svg)
|
codefold-icon-closed: url(../images/triangle-arrow-right-white.svg),
|
||||||
|
|
||||||
|
primary-button-color: #fff,
|
||||||
|
primary-button-background-color: $p5js-pink,
|
||||||
|
|
||||||
|
table-button-color: $white,
|
||||||
|
table-button-background-color: #979797,
|
||||||
|
table-button-active-color: $white,
|
||||||
|
table-button-background-active-color: #00A1D3,
|
||||||
|
table-button-hover-color: $white,
|
||||||
|
table-button-background-hover-color: $p5js-pink,
|
||||||
|
|
||||||
|
progress-bar-background-color: #979797,
|
||||||
|
progress-bar-active-color: #f10046,
|
||||||
|
|
||||||
|
form-title-color: $white,
|
||||||
|
form-secondary-title-color: #b5b5b5,
|
||||||
|
form-border-color: #b5b5b5,
|
||||||
|
form-button-background-color: $black,
|
||||||
|
form-button-color: #f10046,
|
||||||
|
form-button-background-hover-color: $p5js-pink,
|
||||||
|
form-button-background-active-color: #f10046,
|
||||||
|
form-button-hover-color: $white,
|
||||||
|
form-button-active-color: $white,
|
||||||
|
form-navigation-options-color: #999999
|
||||||
),
|
),
|
||||||
contrast: (
|
contrast: (
|
||||||
logo-color: $yellow,
|
logo-color: $yellow,
|
||||||
|
@ -135,7 +194,7 @@ $themes: (
|
||||||
toolbar-button-color: #333333,
|
toolbar-button-color: #333333,
|
||||||
toolbar-button-background-color: #C1C1C1,
|
toolbar-button-background-color: #C1C1C1,
|
||||||
button-background-hover-color: $yellow,
|
button-background-hover-color: $yellow,
|
||||||
button-background-active-color: #f10046,
|
button-background-active-color: $yellow,
|
||||||
button-nav-inactive-color: #a0a0a0,
|
button-nav-inactive-color: #a0a0a0,
|
||||||
button-hover-color: #333333,
|
button-hover-color: #333333,
|
||||||
button-active-color: #333333,
|
button-active-color: #333333,
|
||||||
|
@ -168,8 +227,33 @@ $themes: (
|
||||||
keyboard-shortcut-color: #e1e1e1,
|
keyboard-shortcut-color: #e1e1e1,
|
||||||
nav-hover-color: $yellow,
|
nav-hover-color: $yellow,
|
||||||
error-color: $p5-contrast-pink,
|
error-color: $p5-contrast-pink,
|
||||||
|
table-row-stripe-color: #3f3f3f,
|
||||||
codefold-icon-open: url(../images/triangle-arrow-down-white.svg),
|
codefold-icon-open: url(../images/triangle-arrow-down-white.svg),
|
||||||
codefold-icon-closed: url(../images/triangle-arrow-right-white.svg)
|
codefold-icon-closed: url(../images/triangle-arrow-right-white.svg),
|
||||||
|
|
||||||
|
primary-button-color: #fff,
|
||||||
|
primary-button-background-color: $p5js-pink,
|
||||||
|
|
||||||
|
table-button-color: #333,
|
||||||
|
table-button-background-color: #C1C1C1,
|
||||||
|
table-button-active-color: #333,
|
||||||
|
table-button-background-active-color: #00FFFF,
|
||||||
|
table-button-hover-color: #333,
|
||||||
|
table-button-background-hover-color: $yellow,
|
||||||
|
|
||||||
|
progress-bar-background-color: #979797,
|
||||||
|
progress-bar-active-color: #f10046,
|
||||||
|
|
||||||
|
form-title-color: $white,
|
||||||
|
form-secondary-title-color: #b5b5b5,
|
||||||
|
form-border-color: #b5b5b5,
|
||||||
|
form-button-background-color: $black,
|
||||||
|
form-button-color: #f10046,
|
||||||
|
form-button-background-hover-color: $p5-contrast-pink,
|
||||||
|
form-button-background-active-color: #f10046,
|
||||||
|
form-button-hover-color: $white,
|
||||||
|
form-button-active-color: $white,
|
||||||
|
form-navigation-options-color: #999999
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -179,15 +263,5 @@ $console-error-color: #ff5f52;
|
||||||
$toast-background-color: #4A4A4A;
|
$toast-background-color: #4A4A4A;
|
||||||
$toast-text-color: $white;
|
$toast-text-color: $white;
|
||||||
|
|
||||||
$form-title-color: rgba(51, 51, 51, 0.87);
|
|
||||||
$secondary-form-title-color: #b5b5b5;
|
|
||||||
$form-button-background-color: $white;
|
|
||||||
$form-button-color: #f10046;
|
|
||||||
$form-button-background-hover-color: $p5js-pink;
|
|
||||||
$form-button-background-active-color: #f10046;
|
|
||||||
$form-button-hover-color: $white;
|
|
||||||
$form-button-active-color: $white;
|
|
||||||
$form-navigation-options-color: #999999;
|
|
||||||
|
|
||||||
$about-play-background-color: rgba(255, 255, 255, 0.7);
|
$about-play-background-color: rgba(255, 255, 255, 0.7);
|
||||||
$about-button-border-color: rgba(151, 151, 151, 0.7);
|
$about-button-border-color: rgba(151, 151, 151, 0.7);
|
||||||
|
|
|
@ -6,13 +6,13 @@ html, body {
|
||||||
font-size: #{$base-font-size}px;
|
font-size: #{$base-font-size}px;
|
||||||
}
|
}
|
||||||
|
|
||||||
body, input {
|
body, input, textarea {
|
||||||
@include themify() {
|
@include themify() {
|
||||||
color: getThemifyVariable('primary-text-color');
|
color: getThemifyVariable('primary-text-color');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body, input, button {
|
body, input, textarea, button {
|
||||||
font-family: Montserrat, sans-serif;
|
font-family: Montserrat, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,7 +31,8 @@ input, button {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input,
|
||||||
|
textarea {
|
||||||
padding: #{5 / $base-font-size}rem;
|
padding: #{5 / $base-font-size}rem;
|
||||||
border: 1px solid ;
|
border: 1px solid ;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
@ -42,12 +43,18 @@ input {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button[type="submit"],
|
||||||
input[type="submit"] {
|
input[type="submit"] {
|
||||||
@include themify() {
|
@include themify() {
|
||||||
@extend %button;
|
@extend %button;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button[type="submit"]:disabled,
|
||||||
|
input[type="submit"]:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
@include themify() {
|
@include themify() {
|
||||||
@extend %link;
|
@extend %link;
|
||||||
|
@ -56,6 +63,10 @@ button {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: #{21 / $base-font-size}em;
|
||||||
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
font-size: #{21 / $base-font-size}em;
|
font-size: #{21 / $base-font-size}em;
|
||||||
}
|
}
|
||||||
|
@ -68,6 +79,7 @@ h4 {
|
||||||
}
|
}
|
||||||
h6 {
|
h6 {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
|
font-size: #{12 / $base-font-size}rem;
|
||||||
}
|
}
|
||||||
thead {
|
thead {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
|
@ -46,7 +46,9 @@
|
||||||
padding-left: #{20 / $base-font-size}rem;
|
padding-left: #{20 / $base-font-size}rem;
|
||||||
width: #{720 / $base-font-size}rem;
|
width: #{720 / $base-font-size}rem;
|
||||||
& a {
|
& a {
|
||||||
color: $form-navigation-options-color;
|
@include themify() {
|
||||||
|
color: getThemifyVariable('form-navigation-options-color');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
22
client/styles/components/_account.scss
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
.account-settings__container {
|
||||||
|
@include themify() {
|
||||||
|
color: getThemifyVariable('primary-text-color');
|
||||||
|
background-color: getThemifyVariable('background-color');
|
||||||
|
}
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-settings {
|
||||||
|
max-width: #{700 / $base-font-size}rem;
|
||||||
|
align-self: center;
|
||||||
|
padding: 0 #{10 / $base-font-size}rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account__tabs {
|
||||||
|
padding-top: #{20 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account__social-text {
|
||||||
|
padding-bottom: #{15 / $base-font-size}rem;
|
||||||
|
}
|
109
client/styles/components/_api-key.scss
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
.api-key-form__summary {
|
||||||
|
padding-top: #{25 / $base-font-size}rem;
|
||||||
|
@include themify() {
|
||||||
|
color: getThemifyVariable('heading-text-color');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-key-form__section {
|
||||||
|
padding-bottom: #{15 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-key-form__title {
|
||||||
|
padding: #{15 / $base-font-size}rem 0;
|
||||||
|
font-size: #{21 / $base-font-size}rem;
|
||||||
|
font-weight: bold;
|
||||||
|
@include themify() {
|
||||||
|
color: getThemifyVariable('heading-text-color');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-key-form__create-button {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-key-form__create-icon {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-key-form__create-button .isvg {
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-key-list {
|
||||||
|
display: block;
|
||||||
|
max-width: 900px;
|
||||||
|
border-collapse: collapse;
|
||||||
|
table-layout: fixed;
|
||||||
|
|
||||||
|
thead tr th {
|
||||||
|
width: 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead tr th:last-child {
|
||||||
|
width: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
padding: #{5 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: #{15 / $base-font-size}rem #{5 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr:nth-child(odd) {
|
||||||
|
@include themify() {
|
||||||
|
background: getThemifyVariable('table-row-stripe-color');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-key-list__action {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-key-list__delete-button {
|
||||||
|
width:#{20 / $base-font-size}rem;
|
||||||
|
height:#{20 / $base-font-size}rem;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
@include themify() {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
position: initial;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
& g {
|
||||||
|
opacity: 1;
|
||||||
|
fill: getThemifyVariable('icon-color');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-key-list__delete-button:hover {
|
||||||
|
@include themify() {
|
||||||
|
& g {
|
||||||
|
opacity: 1;
|
||||||
|
fill: getThemifyVariable('icon-hover-color');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-key-form__new-token__title {
|
||||||
|
margin-bottom: #{10 / $base-font-size}rem;
|
||||||
|
font-size: #{18 / $base-font-size}rem;
|
||||||
|
font-weight: bold;
|
||||||
|
@include themify() {
|
||||||
|
color: getThemifyVariable('heading-text-color');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-key-form__new-token__info {
|
||||||
|
padding: #{10 / $base-font-size}rem 0;
|
||||||
|
}
|
|
@ -1,60 +1,99 @@
|
||||||
.asset-table-container {
|
.asset-table-container {
|
||||||
// flex: 1 1 0%;
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
width: #{1000 / $base-font-size}rem;
|
min-height: 100%;
|
||||||
min-height: #{400 / $base-font-size}rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.asset-table {
|
.asset-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: #{10 / $base-font-size}rem #{20 / $base-font-size}rem;
|
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
& .asset-list__delete-column {
|
position: relative;
|
||||||
width: #{23 / $base-font-size}rem;
|
& .asset-table__dropdown-column {
|
||||||
}
|
width: #{60 / $base-font-size}rem;
|
||||||
|
position: relative;
|
||||||
& thead {
|
|
||||||
font-size: #{12 / $base-font-size}rem;
|
|
||||||
@include themify() {
|
|
||||||
color: getThemifyVariable('inactive-text-color')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& th {
|
.asset-table thead th {
|
||||||
height: #{32 / $base-font-size}rem;
|
height: #{32 / $base-font-size}rem;
|
||||||
font-weight: normal;
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
@include themify() {
|
||||||
|
background-color: getThemifyVariable('background-color');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.asset-table thead th:nth-child(1){
|
||||||
|
padding-left: #{12 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
||||||
.asset-table__row {
|
.asset-table__row {
|
||||||
margin: #{10 / $base-font-size}rem;
|
margin: #{10 / $base-font-size}rem;
|
||||||
height: #{72 / $base-font-size}rem;
|
height: #{72 / $base-font-size}rem;
|
||||||
font-size: #{16 / $base-font-size}rem;
|
font-size: #{16 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
||||||
&:nth-child(odd) {
|
.asset-table__row:nth-child(odd) {
|
||||||
@include themify() {
|
@include themify() {
|
||||||
background: getThemifyVariable('console-header-background-color');
|
background: getThemifyVariable('table-row-stripe-color');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& a {
|
.asset-table__row > th:nth-child(1) {
|
||||||
|
padding-left: #{12 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-table__row a {
|
||||||
@include themify() {
|
@include themify() {
|
||||||
color: getThemifyVariable('primary-text-color');
|
color: getThemifyVariable('primary-text-color');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& td:first-child {
|
.asset-table thead {
|
||||||
padding-left: #{10 / $base-font-size}rem;
|
font-size: #{12 / $base-font-size}rem;
|
||||||
|
@include themify() {
|
||||||
|
color: getThemifyVariable('inactive-text-color')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.asset-table th {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
.asset-table__empty {
|
.asset-table__empty {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: #{16 / $base-font-size}rem;
|
font-size: #{16 / $base-font-size}rem;
|
||||||
|
padding: #{42 / $base-font-size}rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.asset-table__total {
|
.asset-table__total {
|
||||||
padding: 0 #{20 / $base-font-size}rem;
|
padding: 0 #{20 / $base-font-size}rem;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
@include themify() {
|
||||||
|
background-color: getThemifyVariable('background-color');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-table__dropdown-button {
|
||||||
|
width:#{25 / $base-font-size}rem;
|
||||||
|
height:#{25 / $base-font-size}rem;
|
||||||
|
|
||||||
|
@include themify() {
|
||||||
|
& polygon {
|
||||||
|
fill: getThemifyVariable('dropdown-color');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-table__action-dialogue {
|
||||||
|
@extend %dropdown-open-right;
|
||||||
|
top: 63%;
|
||||||
|
right: calc(100% - 26px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-table__action-option {
|
||||||
|
font-size: #{12 / $base-font-size}rem;
|
||||||
}
|
}
|
||||||
|
|
48
client/styles/components/_asset-size.scss
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
.asset-size {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
margin-bottom: #{18 / $base-font-size}rem;
|
||||||
|
font-size: #{14 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-size-bar {
|
||||||
|
position: relative;
|
||||||
|
content: ' ';
|
||||||
|
display: block;
|
||||||
|
width: 200px;
|
||||||
|
height: 20px;
|
||||||
|
|
||||||
|
border-radius: #{3 / $base-font-size}rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
@include themify() {
|
||||||
|
background-color: getThemifyVariable('progress-bar-background-color');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-size-bar::before {
|
||||||
|
content: ' ';
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: calc(var(--percent) * 100%);
|
||||||
|
|
||||||
|
@include themify() {
|
||||||
|
background-color: getThemifyVariable('progress-bar-active-color');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-current {
|
||||||
|
position: absolute;
|
||||||
|
top: #{28 / $base-font-size}rem;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asset-max {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: #{210 / $base-font-size}rem;
|
||||||
|
}
|
3
client/styles/components/_collection-create.scss
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.collection-create {
|
||||||
|
padding: #{24 / $base-font-size}rem;
|
||||||
|
}
|
95
client/styles/components/_collection-popover.scss
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
.collection-popover {
|
||||||
|
position: absolute;
|
||||||
|
height: auto;
|
||||||
|
width: #{400 / $base-font-size}rem;
|
||||||
|
top: 63%;
|
||||||
|
right: calc(100% - 26px);
|
||||||
|
|
||||||
|
z-index: 9999;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
border-radius: #{6 / $base-font-size}rem;
|
||||||
|
|
||||||
|
@include themify() {
|
||||||
|
background-color: map-get($theme-map, 'modal-background-color');
|
||||||
|
border: 1px solid map-get($theme-map, 'modal-border-color');
|
||||||
|
box-shadow: 0 0 18px 0 getThemifyVariable('shadow-color');
|
||||||
|
color: getThemifyVariable('dropdown-color');
|
||||||
|
}
|
||||||
|
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-popover__header {
|
||||||
|
display: flex;
|
||||||
|
margin-left: #{17 / $base-font-size}rem;
|
||||||
|
margin-right: #{17 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-popover__filter {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: #{8 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-popover__exit-button {
|
||||||
|
@include icon();
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-popover__items {
|
||||||
|
height: #{70 * 4 / $base-font-size}rem;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.collection-popover__item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
height: #{60 / $base-font-size}rem;
|
||||||
|
margin: 5px;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-popover__item:nth-child(odd) {
|
||||||
|
@include themify() {
|
||||||
|
background: getThemifyVariable('table-row-stripe-color');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-popover__item__info {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-popover__item__info button,
|
||||||
|
.collection-popover__item__info button:hover {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
text-align: left;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-popover__item__view {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.collection-popover__item__view-button {
|
||||||
|
@extend %button;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-popover__empty {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
174
client/styles/components/_collection.scss
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
.collection-container {
|
||||||
|
padding: #{24 / $base-font-size}rem #{66 / $base-font-size}rem;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-metadata {
|
||||||
|
width: #{1012 / $base-font-size}rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
margin-bottom: #{24 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-metadata__columns {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-metadata__column--left,
|
||||||
|
.collection-metadata__column--right {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-metadata__column--right {
|
||||||
|
align-self: flex-end;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-metadata__column--right > * {
|
||||||
|
margin-left: #{10 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-metadata__name {
|
||||||
|
// padding: #{8 / $base-font-size}rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-metadata__name .editable-input__label span {
|
||||||
|
padding: 0.83333rem 0;
|
||||||
|
|
||||||
|
@include themify() {
|
||||||
|
color: getThemifyVariable('primary-text-color');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-metadata__name,
|
||||||
|
.collection-metadata__name .editable-input__input {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-metadata__user {
|
||||||
|
padding-top: #{8 / $base-font-size}rem;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-metadata--is-owner .collection-metadata__user {
|
||||||
|
padding-left: #{8 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-metadata__description {
|
||||||
|
margin-top: #{8 / $base-font-size}rem;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-metadata__description .editable-input__label {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-metadata__description .editable-input--has-value .editable-input__label {
|
||||||
|
@include themify() {
|
||||||
|
color: getThemifyVariable('primary-text-color');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-metadata__description .editable-input__input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-add-sketch {
|
||||||
|
min-width: #{600 / $base-font-size}rem;
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-share {
|
||||||
|
text-align: right;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-share__button {
|
||||||
|
@extend %button;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-share__arrow {
|
||||||
|
margin-left: #{5 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-share .copyable-input {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection__share-dropdown {
|
||||||
|
@extend %dropdown-open-right;
|
||||||
|
padding: #{20 / $base-font-size}rem;
|
||||||
|
width: #{350 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-metadata__add-button {
|
||||||
|
@extend %button;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-table-wrapper {
|
||||||
|
width: #{1012 / $base-font-size}rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
flex: 1;
|
||||||
|
@include themify() {
|
||||||
|
border: 1px solid getThemifyVariable('modal-border-color');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-has-items=false] .collection-table-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-empty-message {
|
||||||
|
text-align: center;
|
||||||
|
font-size: #{16 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-row__action-column {
|
||||||
|
width: #{60 / $base-font-size}rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-row__remove-button {
|
||||||
|
display: inline-block;
|
||||||
|
width:#{35 / $base-font-size}rem;
|
||||||
|
height:#{35 / $base-font-size}rem;
|
||||||
|
@include icon();
|
||||||
|
@include themify() {
|
||||||
|
// icon graphic
|
||||||
|
polygon {
|
||||||
|
fill: getThemifyVariable('table-button-color');
|
||||||
|
}
|
||||||
|
|
||||||
|
// icon background circle
|
||||||
|
path {
|
||||||
|
fill: getThemifyVariable('table-button-background-color');
|
||||||
|
}
|
||||||
|
|
||||||
|
& svg {
|
||||||
|
width:#{35 / $base-font-size}rem;
|
||||||
|
height:#{35 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
polygon {
|
||||||
|
fill: getThemifyVariable('table-button-hover-color');
|
||||||
|
}
|
||||||
|
|
||||||
|
path {
|
||||||
|
fill: getThemifyVariable('table-button-background-hover-color');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
90
client/styles/components/_dashboard-header.scss
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
.dashboard-header {
|
||||||
|
padding: #{24 / $base-font-size}rem #{66 / $base-font-size}rem;
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction:column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header__header {
|
||||||
|
max-width: #{1012 / $base-font-size}rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header--no-vertical-padding {
|
||||||
|
padding: 0 66px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header--no-vertical-padding {
|
||||||
|
padding: 0 66px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header__switcher {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header__tabs {
|
||||||
|
display: flex;
|
||||||
|
padding-top: #{24 / $base-font-size}rem;
|
||||||
|
|
||||||
|
@include themify() {
|
||||||
|
border-bottom: 1px solid getThemifyVariable('inactive-text-color');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header__tab {
|
||||||
|
@include themify() {
|
||||||
|
color: getThemifyVariable('inactive-text-color');
|
||||||
|
border-bottom: #{4 / $base-font-size}rem solid transparent;
|
||||||
|
|
||||||
|
padding: 0;
|
||||||
|
margin-right: #{26 / $base-font-size}rem;
|
||||||
|
|
||||||
|
&:hover, &:focus, &.dashboard-header__tab--selected {
|
||||||
|
color: getThemifyVariable('primary-text-color');
|
||||||
|
border-bottom-color: getThemifyVariable('nav-hover-color');
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
font-size: #{12 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header__tab--selected {
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header__tab a {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header__tab__title {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header__tab__title > * {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0 #{5 /$base-font-size}rem #{5 /$base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header__nav {
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header__actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: #{24 / $base-font-size}rem 0;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-header__actions > *:not(:first-child) {
|
||||||
|
margin-left: #{15 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard__action-button {
|
||||||
|
flex-grow: 0;
|
||||||
|
@extend %button;
|
||||||
|
}
|
53
client/styles/components/_editable-input.scss
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
.editable-input {
|
||||||
|
height: 70%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editable-input__label {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
@include themify() {
|
||||||
|
color: getThemifyVariable('inactive-text-color');
|
||||||
|
&:hover {
|
||||||
|
color: getThemifyVariable('primary-text-color');
|
||||||
|
& .editable-input__icon path {
|
||||||
|
fill: getThemifyVariable('primary-text-color');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: #{18 / $base-font-size}rem;
|
||||||
|
|
||||||
|
font-size: unset;
|
||||||
|
font-weight: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editable-input__icon svg {
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
|
||||||
|
@include themify() {
|
||||||
|
path {
|
||||||
|
fill: getThemifyVariable('inactive-text-color');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.editable-input:hover {
|
||||||
|
@include themify() {
|
||||||
|
.editable-input__icon path {
|
||||||
|
fill: getThemifyVariable('primary-text-color');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.editable-input--is-not-editing .editable-input__input,
|
||||||
|
.editable-input--is-editing .editable-input__label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editable-input--is-editing .editable-input__input,
|
||||||
|
.editable-input--is-not-editing .editable-input__label {
|
||||||
|
display: block;
|
||||||
|
}
|
|
@ -6,6 +6,14 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-container--align-left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container--align-top {
|
||||||
|
height: unset;
|
||||||
|
}
|
||||||
|
|
||||||
.form-container__header {
|
.form-container__header {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: #{15 / $base-font-size}rem #{34 / $base-font-size}rem;
|
padding: #{15 / $base-font-size}rem #{34 / $base-font-size}rem;
|
||||||
|
@ -21,9 +29,21 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-container--align-left .form-container__content {
|
||||||
|
align-items: left;
|
||||||
|
}
|
||||||
|
|
||||||
.form-container__title {
|
.form-container__title {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
color: $form-title-color;
|
@include themify() {
|
||||||
|
color: getThemifyVariable('form-title-color')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container__context {
|
||||||
|
@include themify() {
|
||||||
|
color: getThemifyVariable('secondary-text-color')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-container__divider {
|
.form-container__divider {
|
||||||
|
@ -31,9 +51,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-container__logo-button {
|
.form-container__logo-button {
|
||||||
@extend %none-themify-icon;
|
@include icon();
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-container__exit-button {
|
.form-container__exit-button {
|
||||||
@extend %none-themify-icon-with-hover;
|
@include icon();
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,10 +5,15 @@
|
||||||
font-size: #{9 / $base-font-size}rem;
|
font-size: #{9 / $base-font-size}rem;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@include themify() {
|
@include themify() {
|
||||||
color: getThemifyVariable('error-color')
|
color: getThemifyVariable('error-color');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form--inline {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.form__cancel-button {
|
.form__cancel-button {
|
||||||
margin-top: #{10 / $base-font-size}rem;
|
margin-top: #{10 / $base-font-size}rem;
|
||||||
font-size: #{12 / $base-font-size}rem;
|
font-size: #{12 / $base-font-size}rem;
|
||||||
|
@ -17,22 +22,48 @@
|
||||||
.form__navigation-options {
|
.form__navigation-options {
|
||||||
margin-top: #{16 / $base-font-size}rem;
|
margin-top: #{16 / $base-font-size}rem;
|
||||||
font-size: #{12 / $base-font-size}rem;
|
font-size: #{12 / $base-font-size}rem;
|
||||||
color: $form-navigation-options-color;
|
@include themify() {
|
||||||
|
color: getThemifyVariable('form-navigation-options-color');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form__legend{
|
||||||
|
font-size: #{21 / $base-font-size}rem;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form__label {
|
.form__label {
|
||||||
color: $secondary-form-title-color;
|
|
||||||
font-size: #{12 / $base-font-size}rem;
|
font-size: #{12 / $base-font-size}rem;
|
||||||
margin-top: #{25 / $base-font-size}rem;
|
margin-top: #{25 / $base-font-size}rem;
|
||||||
margin-bottom: #{7 / $base-font-size}rem;
|
margin-bottom: #{7 / $base-font-size}rem;
|
||||||
display: block;
|
display: block;
|
||||||
|
@include themify() {
|
||||||
|
color: getThemifyVariable('form-secondary-title-color');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form__label--hidden {
|
||||||
|
@extend %hidden-element;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form__input {
|
.form__input {
|
||||||
width: #{360 / $base-font-size}rem;
|
width: 100%;
|
||||||
|
min-width: #{360 / $base-font-size}rem;
|
||||||
height: #{40 / $base-font-size}rem;
|
height: #{40 / $base-font-size}rem;
|
||||||
color: $icon-hover-color;
|
font-size: #{16 / $base-font-size}rem;
|
||||||
border-color: $secondary-form-title-color;
|
@include themify() {
|
||||||
|
color: getThemifyVariable('form-input-text-color');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form__input-flexible-height {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form__input::placeholder {
|
||||||
|
@include themify() {
|
||||||
|
color: getThemifyVariable('form-input-placeholder-text-color');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.form__context {
|
.form__context {
|
||||||
|
@ -41,13 +72,26 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.form__status {
|
.form__status {
|
||||||
color: $form-navigation-options-color;
|
@include themify() {
|
||||||
|
color: getThemifyVariable('form-navigation-options-color');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.form input[type="submit"] {
|
.form [type="submit"] {
|
||||||
@extend %forms-button;
|
@extend %button;
|
||||||
|
padding: #{8 / $base-font-size}rem #{25 / $base-font-size}rem;
|
||||||
|
margin: #{39 / $base-font-size}rem 0 #{24 / $base-font-size}rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form input[type="submit"]:disabled {
|
.form [type="submit"][disabled] {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form--inline [type="submit"] {
|
||||||
|
margin: 0 0 0 #{24 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form [type="submit"][disabled],
|
||||||
|
.form--inline [type="submit"][disabled] {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +1,4 @@
|
||||||
.github-button {
|
.github-button,
|
||||||
@include themify() {
|
|
||||||
@extend %button;
|
|
||||||
& path {
|
|
||||||
color: getThemifyVariable('primary-text-color');
|
|
||||||
}
|
|
||||||
&:hover path, &:active path {
|
|
||||||
fill: $white;
|
|
||||||
}
|
|
||||||
&:hover, &:active {
|
|
||||||
background-color: getThemifyVariable('secondary-text-color');
|
|
||||||
border-color: getThemifyVariable('secondary-text-color');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
width: #{300 / $base-font-size}rem;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.github-icon {
|
|
||||||
margin-right: #{10 / $base-font-size}rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.google-button {
|
.google-button {
|
||||||
@include themify() {
|
@include themify() {
|
||||||
@extend %button;
|
@extend %button;
|
||||||
|
@ -33,16 +9,23 @@
|
||||||
fill: $white;
|
fill: $white;
|
||||||
}
|
}
|
||||||
&:hover, &:active {
|
&:hover, &:active {
|
||||||
background-color: getThemifyVariable('secondary-text-color');
|
color: getThemifyVariable('button-hover-color');
|
||||||
border-color: getThemifyVariable('secondary-text-color');
|
background-color: getThemifyVariable('button-background-hover-color');
|
||||||
|
border-color: getThemifyVariable('button-background-hover-color');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
margin-top: #{4 / $base-font-size}rem;
|
width: #{300 / $base-font-size}rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: #{300 / $base-font-size}rem;
|
|
||||||
|
& + & {
|
||||||
|
margin-top: #{10 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.github-icon {
|
||||||
|
margin-right: #{10 / $base-font-size}rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.google-icon {
|
.google-icon {
|
||||||
|
|
|
@ -1,9 +1,17 @@
|
||||||
|
.loader-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.loader {
|
.loader {
|
||||||
width: #{80 / $base-font-size }rem;
|
width: #{80 / $base-font-size }rem;
|
||||||
height: #{80 / $base-font-size}rem;
|
height: #{80 / $base-font-size}rem;
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: #{100 / $base-font-size}rem auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.loader__circle1,
|
.loader__circle1,
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
.modal-content {
|
.modal-content {
|
||||||
@extend %modal;
|
@extend %modal;
|
||||||
min-height: #{150 / $base-font-size}rem;
|
min-height: #{150 / $base-font-size}rem;
|
||||||
width: #{400 / $base-font-size}rem;
|
width: #{500 / $base-font-size}rem;
|
||||||
padding: #{20 / $base-font-size}rem;
|
padding: #{20 / $base-font-size}rem;
|
||||||
.modal--reduced & {
|
.modal--reduced & {
|
||||||
//min-height: #{150 / $base-font-size}rem;
|
//min-height: #{150 / $base-font-size}rem;
|
||||||
|
@ -32,9 +32,8 @@
|
||||||
margin-bottom: #{20 / $base-font-size}rem;
|
margin-bottom: #{20 / $base-font-size}rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.new-file-form, .new-file-folder {
|
.new-folder-form__input-wrapper, .new-file-form__input-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.new-file-form__name-label, .new-folder-form__name-label {
|
.new-file-form__name-label, .new-folder-form__name-label {
|
||||||
|
@ -43,6 +42,7 @@
|
||||||
|
|
||||||
.new-file-form__name-input, .new-folder-form__name-input {
|
.new-file-form__name-input, .new-folder-form__name-input {
|
||||||
margin-right: #{10 / $base-font-size}rem;
|
margin-right: #{10 / $base-font-size}rem;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal__divider {
|
.modal__divider {
|
||||||
|
|
|
@ -43,7 +43,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav__item:first-child {
|
.nav__item:first-child,
|
||||||
|
.nav__item--no-icon {
|
||||||
padding-left: #{15 / $base-font-size}rem;
|
padding-left: #{15 / $base-font-size}rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,6 +59,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& g, & path {
|
||||||
|
@include themify() {
|
||||||
|
fill: getThemifyVariable('nav-hover-color');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.nav__item-header-triangle polygon {
|
.nav__item-header-triangle polygon {
|
||||||
@include themify() {
|
@include themify() {
|
||||||
fill: getThemifyVariable('nav-hover-color');
|
fill: getThemifyVariable('nav-hover-color');
|
||||||
|
@ -69,6 +76,21 @@
|
||||||
@include themify() {
|
@include themify() {
|
||||||
color: getThemifyVariable('nav-hover-color');
|
color: getThemifyVariable('nav-hover-color');
|
||||||
}
|
}
|
||||||
|
& g, & path {
|
||||||
|
@include themify() {
|
||||||
|
fill: getThemifyVariable('nav-hover-color');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__item-header-triangle {
|
||||||
|
margin-left: #{5 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__dropdown {
|
||||||
|
@include themify() {
|
||||||
|
color: getThemifyVariable('nav-hover-color');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav__item-header-triangle {
|
.nav__item-header-triangle {
|
||||||
|
@ -87,7 +109,7 @@
|
||||||
padding-right: #{20 / $base-font-size}rem;
|
padding-right: #{20 / $base-font-size}rem;
|
||||||
|
|
||||||
& .nav__dropdown {
|
& .nav__dropdown {
|
||||||
width: #{121 / $base-font-size}rem;
|
width: #{122 / $base-font-size}rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,3 +182,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav__back-icon {
|
||||||
|
& g, & path {
|
||||||
|
opacity: 1;
|
||||||
|
@include themify() {
|
||||||
|
fill: getThemifyVariable('inactive-text-color');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
margin-right: #{5 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav__back-link {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
|
@ -10,6 +10,8 @@
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: #{40 / $base-font-size}rem;
|
margin-top: #{40 / $base-font-size}rem;
|
||||||
margin-bottom: #{80 / $base-font-size}rem;
|
margin-bottom: #{80 / $base-font-size}rem;
|
||||||
color: $form-navigation-options-color;
|
@include themify() {
|
||||||
|
color: getThemifyVariable('form-navigation-options-color');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|