Merge branch 'master' into feature/storybook
|
@ -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
|
||||||
|
|
46
.github/CONTRIBUTING.md
vendored
|
@ -1,16 +1,16 @@
|
||||||
# Contributing to the p5.js Web Editor
|
# Contributing to the p5.js Web Editor
|
||||||
|
|
||||||
Hello! We welcome community contributions to the p5.js Web Editor. Contributing takes many forms and doesn't have to be **writing code**, it can be **report bugs**, **proposing new features**, **creating UI/UX designs**, and **updating documentation**.
|
Hello! We welcome community contributions to the p5.js Web Editor. Contributing takes many forms and doesn't have to be **writing code**, it can be **reporting bugs**, **proposing new features**, **creating UI/UX designs**, and **updating documentation**.
|
||||||
|
|
||||||
Here are links to all the sections in this document:
|
|
||||||
|
|
||||||
<!-- If you change any of the headings in this document, remember to update the table of contents. -->
|
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
- [Contributing to the p5.js Web Editor](#contributing-to-the-p5js-web-editor)
|
- [Contributing to the p5.js Web Editor](#contributing-to-the-p5js-web-editor)
|
||||||
|
- [Table of Contents](#table-of-contents)
|
||||||
- [Code of Conduct](#code-of-conduct)
|
- [Code of Conduct](#code-of-conduct)
|
||||||
- [How Can I Contribute?](#how-can-i-contribute)
|
- [How Can I Contribute?](#how-can-i-contribute)
|
||||||
- [First Timers](#first-timers)
|
- [First Steps](#first-steps)
|
||||||
- [Milestones](#milestones)
|
- [Good First Issues](#good-first-issues)
|
||||||
|
- [Good Medium Issues](#good-medium-issues)
|
||||||
|
- [Project Board](#project-board)
|
||||||
- [Project Ideas](#project-ideas)
|
- [Project Ideas](#project-ideas)
|
||||||
- [Issue Search and Tagging](#issue-search-and-tagging)
|
- [Issue Search and Tagging](#issue-search-and-tagging)
|
||||||
- [Beginning Work](#beginning-work)
|
- [Beginning Work](#beginning-work)
|
||||||
|
@ -20,15 +20,35 @@ Here are links to all the sections in this document:
|
||||||
|
|
||||||
## Code of Conduct
|
## Code of Conduct
|
||||||
|
|
||||||
Please follow the guidelines mentioned at [CODE OF CONDUCT.md](https://github.com/processing/p5.js-web-editor/blob/master/.github/CODE_OF_CONDUCT.md).
|
Please follow the guidelines in the [Code of Conduct](https://github.com/processing/p5.js-web-editor/blob/master/.github/CODE_OF_CONDUCT.md).
|
||||||
|
|
||||||
## How Can I Contribute?
|
## How Can I Contribute?
|
||||||
|
If you're new to open source, [read about how to contribute to open source](https://opensource.guide/how-to-contribute/).
|
||||||
|
|
||||||
### First Timers
|
### First Steps
|
||||||
For first-time contributors or those who want to start with a small task: [check out our list of good first bugs](https://github.com/processing/p5.js-web-editor/labels/good%20first%20issue). First read the github discussion on that issue and find out if there's currently a person working on that or not. If no one is working on it or if there has was one claimed to but has not been active for a while, ask if it is up for grabs. It's okay to not know how to fix an issue and feel free to ask questions about to approach the problem! We are all just here to learn and make something awesome. Someone from the community would help you out and these are great issues for learning about the web editor, its file structure and its development process.
|
Don't know where to begin? Here are some suggestions to get started:
|
||||||
|
* Think about what you're hoping to learn by working on open source. The web editor is a full-stack web application, therefore there's tons of different areas to focus on:
|
||||||
|
- UI/UX design
|
||||||
|
- Project management: Organizing tickets, pull requests, tasks
|
||||||
|
- Front end: React/Redux, CSS/Sass, CodeMirror
|
||||||
|
- Back end: Node, Express, MongoDB, Jest, AWS
|
||||||
|
- DevOps: Travis CI, Jest, Docker, Kubernetes, AWS
|
||||||
|
- Documentation
|
||||||
|
- Translations: Application and documentation
|
||||||
|
* Use the [p5.js Web Editor](https://editor.p5js.org)! Find a bug? Think of something you think would add to the project? Open an issue.
|
||||||
|
* Expand an existing issue. Sometimes issues are missing steps to reproduce, or need suggestions for potential solutions. Sometimes they need another voice saying, "this is really important!"
|
||||||
|
* Try getting the project running locally on your computer by following the [installation steps](./../developer_docs/installation.md).
|
||||||
|
* Look through the documentation in the [developer docs](../developer_docs/). Is there anything that could be expanded? Is there anything missing?
|
||||||
|
* Look at the [development guide](./../developer_docs/development.md).
|
||||||
|
|
||||||
### Milestones
|
### Good First Issues
|
||||||
A good place to check for tickets to work on is [milestones](https://github.com/processing/p5.js-web-editor/milestones), as miletones have a due date, and will give you a sense of tickets the tickets that maintainers would like to be completed sooner rather than later.
|
For first-time contributors or those who want to start with a small task, [check out the list of good first issues](https://github.com/processing/p5.js-web-editor/labels/good%20first%20issue), or [issues that need documentation of steps to reproduce](https://github.com/processing/p5.js-web-editor/issues?q=is%3Aissue+is%3Aopen+label%3A%22needs+steps+to+reproduce%22). If the issue has not been assigned to anyone, then you can work on it! It's okay to not know how to fix an issue, and feel free to ask questions about to approach the problem! We are all here to learn and make something awesome. Someone from the community would help you out and these are great issues for learning about the web editor, its file structure and its development process.
|
||||||
|
|
||||||
|
### Good Medium Issues
|
||||||
|
If you're looking for a bigger project to take on, look through the issues tagged [good medium issue](https://github.com/processing/p5.js-web-editor/labels/good%20medium%20issue). These issues are self-contained projects that may take longer to work on, but are great if you're looking to get more deeply involved in contributing!
|
||||||
|
|
||||||
|
### Project Board
|
||||||
|
Many issues are related to each other and fall under bigger projects. To get a bigger picture, look at the [All Projects](https://github.com/processing/p5.js-web-editor/projects/4) board.
|
||||||
|
|
||||||
### Project Ideas
|
### Project Ideas
|
||||||
If you're looking for inspiration for Google Summer of Code or a bigger project, there's a [project list](https://github.com/processing/processing/wiki/Project-List#p5js-web-editor) maintained on the Processing wiki.
|
If you're looking for inspiration for Google Summer of Code or a bigger project, there's a [project list](https://github.com/processing/processing/wiki/Project-List#p5js-web-editor) maintained on the Processing wiki.
|
||||||
|
@ -40,7 +60,7 @@ If you feel like an issue is tagged incorrectly (e.g. it's low priority and you
|
||||||
|
|
||||||
### Beginning Work
|
### Beginning Work
|
||||||
|
|
||||||
If you'd like to work on an issue, please comment on it to let the maintainers know. If someone else has already commented and taken up that issue, please refrain from working on it and submitting a PR without asking the maintainers as it leads to unnecessary duplication of effort.
|
If you'd like to work on an issue, please comment on it to let the maintainers know, so that they can assign it to you. If someone else has already commented and taken up that issue, please refrain from working on it and submitting a PR without asking the maintainers as it leads to unnecessary duplication of effort.
|
||||||
|
|
||||||
Then, follow the [installation guide](https://github.com/processing/p5.js-web-editor/blob/master/developer_docs/installation.md) to get the project building and working on your computer.
|
Then, follow the [installation guide](https://github.com/processing/p5.js-web-editor/blob/master/developer_docs/installation.md) to get the project building and working on your computer.
|
||||||
|
|
||||||
|
|
1
.github/FUNDING.yml
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
custom: https://processingfoundation.org/support
|
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:
|
||||||
- "10.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
|
||||||
|
|
33
README.md
|
@ -1,29 +1,42 @@
|
||||||
# [p5.js Web Editor](https://editor.p5js.org)
|
# [p5.js Web Editor](https://editor.p5js.org)
|
||||||
|
|
||||||
|
|
||||||
|
Documentation is also available in the following languages:
|
||||||
|
|
||||||
|
[한국어](https://github.com/processing/p5.js-web-editor/blob/master/translations/ko)
|
||||||
|
|
||||||
|
## Welcome! 👋👋🏿👋🏽👋🏻👋🏾👋🏼
|
||||||
|
|
||||||
The p5.js Web Editor is a platform for creative coding, with a focus on making coding accessible for as many people as possible, including artists, designers, educators, beginners, and anyone who wants to learn. Simply by opening the website you can get started writing p5.js sketches without downloading or configuring anything. The editor is designed with simplicity in mind by limiting features and frills. We strive to listen to the community to drive the editor’s development, and to be intentional with every change. The editor is free and open-source.
|
The p5.js Web Editor is a platform for creative coding, with a focus on making coding accessible for as many people as possible, including artists, designers, educators, beginners, and anyone who wants to learn. Simply by opening the website you can get started writing p5.js sketches without downloading or configuring anything. The editor is designed with simplicity in mind by limiting features and frills. We strive to listen to the community to drive the editor’s development, and to be intentional with every change. The editor is free and open-source.
|
||||||
|
|
||||||
We also strive to give the community as much ownership and control as possible. You can download your sketches so that you can edit them locally or host them elsewhere. You can also host your own version of the editor, giving you control over its data.
|
We also strive to give the community as much ownership and control as possible. You can download your sketches so that you can edit them locally or host them elsewhere. You can also host your own version of the editor, giving you control over its data.
|
||||||
|
|
||||||
The p5.js Web Editor is currently in active development, and looking for contributions of any type! Please check out the [contribution guide](https://github.com/processing/p5.js-web-editor/blob/master/.github/CONTRIBUTING.md) for more details.
|
## Community
|
||||||
|
|
||||||
If you have found a bug in the p5.js Web Editor, you can file it under the ["issues" tab](https://github.com/processing/p5.js-web-editor/issues).
|
New to the p5.js community? Read our [community statement](https://p5js.org/community/).
|
||||||
|
|
||||||
|
## Code of Conduct
|
||||||
|
|
||||||
|
All contributors to the p5.js Web Editor are expected to follow the [Code of Conduct](./.github/CODE_OF_CONDUCT.md). We strive to create a friendly and safe community!
|
||||||
|
|
||||||
|
## Get Involved
|
||||||
|
|
||||||
|
The p5.js Web Editor is a collaborative project created by many individuals, and you are invited to help. All types of involvement are welcome! Please check out the [contribution guide](./.github/CONTRIBUTING.md) for more details.
|
||||||
|
|
||||||
|
Developers, check the [developer docs](https://github.com/processing/p5.js-web-editor/blob/master/developer_docs/) details about contributing code, bug fixes, and documentation. To start writing code, a great place to start is the [development guide](https://github.com/processing/p5.js-web-editor/blob/master/developer_docs/development.md).
|
||||||
|
|
||||||
## Issues
|
## Issues
|
||||||
|
|
||||||
|
If you have found a bug in the p5.js Web Editor, you can file it under the ["issues" tab](https://github.com/processing/p5.js-web-editor/issues).
|
||||||
|
|
||||||
Please post bugs and feature requests in the correct repository:
|
Please post bugs and feature requests in the correct repository:
|
||||||
|
|
||||||
* p5.js general and p5.dom: [https://github.com/processing/p5.js/issues](https://github.com/processing/p5.js/issues)
|
* p5.js library and p5.dom: [https://github.com/processing/p5.js/issues](https://github.com/processing/p5.js/issues)
|
||||||
* p5.accessibility: [https://github.com/processing/p5.accessibility/issues](https://github.com/processing/p5.accessibility/issues)
|
* p5.accessibility: [https://github.com/processing/p5.accessibility/issues](https://github.com/processing/p5.accessibility/issues)
|
||||||
* p5.sound: [https://github.com/processing/p5.js-sound/issues](https://github.com/processing/p5.js-sound/issues)
|
* p5.sound: [https://github.com/processing/p5.js-sound/issues](https://github.com/processing/p5.js-sound/issues)
|
||||||
* p5.js website: [https://github.com/processing/p5.js-website/issues](https://github.com/processing/p5.js-website/issues)
|
* p5.js website: [https://github.com/processing/p5.js-website/issues](https://github.com/processing/p5.js-website/issues)
|
||||||
|
|
||||||
## Get Involved
|
## Acknowledgements
|
||||||
|
|
||||||
The p5.js Web Editor is a collaborative project created by many individuals, and you are invited to help. All types of involvement are welcome. You can start with the [p5.js community section](https://p5js.org/community), which also applies to this project.
|
|
||||||
|
|
||||||
Developers, check the [developer docs](https://github.com/processing/p5.js-web-editor/blob/master/developer_docs/) details about contributing code, bug fixes, and documentation. To start writing code, a great place to start is the [development guide](https://github.com/processing/p5.js-web-editor/blob/master/developer_docs/development.md).
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
Support for this project has come from [Processing Foundation](https://processingfoundation.org/), [NYU ITP](https://tisch.nyu.edu/itp), and [CS4All, NYC DOE](http://cs4all.nyc/).
|
Support for this project has come from [Processing Foundation](https://processingfoundation.org/), [NYU ITP](https://tisch.nyu.edu/itp), and [CS4All, NYC DOE](http://cs4all.nyc/).
|
||||||
|
|
||||||
|
|
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;
|
|
@ -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();
|
||||||
|
@ -180,7 +182,8 @@ class Nav extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleShare() {
|
handleShare() {
|
||||||
this.props.showShareModal();
|
const { username } = this.props.params;
|
||||||
|
this.props.showShareModal(this.props.project.id, this.props.project.name, username);
|
||||||
this.setDropdown('none');
|
this.setDropdown('none');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -222,32 +225,27 @@ 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 = {
|
|
||||||
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 (
|
return (
|
||||||
<nav className="nav" title="main-navigation" ref={(node) => { this.node = node; }}>
|
<ul className="nav__items-left">
|
||||||
<ul className="nav__items-left" title="project-menu">
|
<li className="nav__item-logo">
|
||||||
|
<InlineSVG src={logoUrl} alt="p5.js logo" className="svg__logo" />
|
||||||
|
</li>
|
||||||
|
<li className="nav__item nav__item--no-icon">
|
||||||
|
<Link to="/" className="nav__back-link">
|
||||||
|
<InlineSVG src={caretLeft} className="nav__back-icon" />
|
||||||
|
<span className="nav__item-header">
|
||||||
|
Back to Editor
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderProjectMenu(navDropdownState) {
|
||||||
|
return (
|
||||||
|
<ul className="nav__items-left">
|
||||||
<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" />
|
||||||
</li>
|
</li>
|
||||||
|
@ -327,6 +325,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 +534,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 +588,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 +631,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.
|
||||||
|
@ -631,6 +716,7 @@ Nav.propTypes = {
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
project: PropTypes.shape({
|
project: PropTypes.shape({
|
||||||
id: PropTypes.string,
|
id: PropTypes.string,
|
||||||
|
name: PropTypes.string,
|
||||||
owner: PropTypes.shape({
|
owner: PropTypes.shape({
|
||||||
id: PropTypes.string
|
id: PropTypes.string
|
||||||
})
|
})
|
||||||
|
@ -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,9 +739,13 @@ 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,
|
||||||
|
params: PropTypes.shape({
|
||||||
|
username: PropTypes.string
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
Nav.defaultProps = {
|
Nav.defaultProps = {
|
||||||
|
@ -663,7 +753,12 @@ Nav.defaultProps = {
|
||||||
id: undefined,
|
id: undefined,
|
||||||
owner: undefined
|
owner: undefined
|
||||||
},
|
},
|
||||||
cmController: {}
|
cmController: {},
|
||||||
|
layout: 'project',
|
||||||
|
warnIfUnsavedChanges: undefined,
|
||||||
|
params: {
|
||||||
|
username: 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">
|
||||||
|
<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;
|
71
client/components/__test__/FileNode.test.jsx
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import { FileNode } from '../../modules/IDE/components/FileNode';
|
||||||
|
|
||||||
|
beforeAll(() => {});
|
||||||
|
describe('<FileNode />', () => {
|
||||||
|
let component;
|
||||||
|
let props = {};
|
||||||
|
|
||||||
|
describe('with valid props', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
props = {
|
||||||
|
...props,
|
||||||
|
id: '0',
|
||||||
|
children: [],
|
||||||
|
name: 'test.jsx',
|
||||||
|
fileType: 'dunno',
|
||||||
|
setSelectedFile: jest.fn(),
|
||||||
|
deleteFile: jest.fn(),
|
||||||
|
updateFileName: jest.fn(),
|
||||||
|
resetSelectedFile: jest.fn(),
|
||||||
|
newFile: jest.fn(),
|
||||||
|
newFolder: jest.fn(),
|
||||||
|
showFolderChildren: jest.fn(),
|
||||||
|
hideFolderChildren: jest.fn(),
|
||||||
|
canEdit: true,
|
||||||
|
authenticated: false
|
||||||
|
};
|
||||||
|
component = shallow(<FileNode {...props} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when changing name', () => {
|
||||||
|
let input;
|
||||||
|
let renameTriggerButton;
|
||||||
|
const changeName = (newFileName) => {
|
||||||
|
renameTriggerButton.simulate('click');
|
||||||
|
input.simulate('change', { target: { value: newFileName } });
|
||||||
|
input.simulate('blur');
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
input = component.find('.sidebar__file-item-input');
|
||||||
|
renameTriggerButton = component
|
||||||
|
.find('.sidebar__file-item-option')
|
||||||
|
.first();
|
||||||
|
component.setState({ isEditing: true });
|
||||||
|
});
|
||||||
|
it('should render', () => expect(component).toBeDefined());
|
||||||
|
|
||||||
|
// it('should debug', () => console.log(component.debug()));
|
||||||
|
|
||||||
|
describe('to a valid filename', () => {
|
||||||
|
const newName = 'newname.jsx';
|
||||||
|
beforeEach(() => changeName(newName));
|
||||||
|
|
||||||
|
it('should save the name', () => {
|
||||||
|
expect(props.updateFileName).toBeCalledWith(props.id, newName);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('to an empty filename', () => {
|
||||||
|
const newName = '';
|
||||||
|
beforeEach(() => changeName(newName));
|
||||||
|
|
||||||
|
it('should not save the name', () => {
|
||||||
|
expect(props.updateFileName).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -7,7 +7,6 @@ exports[`Nav renders correctly 1`] = `
|
||||||
>
|
>
|
||||||
<ul
|
<ul
|
||||||
className="nav__items-left"
|
className="nav__items-left"
|
||||||
title="project-menu"
|
|
||||||
>
|
>
|
||||||
<li
|
<li
|
||||||
className="nav__item-logo"
|
className="nav__item-logo"
|
||||||
|
|
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
|
||||||
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
176
client/modules/IDE/actions/collections.js
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
import { browserHistory } from 'react-router';
|
||||||
|
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 newCollection = response.data;
|
||||||
|
dispatch(setToastText(`Created "${newCollection.name}"`));
|
||||||
|
dispatch(showToast(TOAST_DISPLAY_TIME_MS));
|
||||||
|
|
||||||
|
const pathname = `/${newCollection.owner.username}/collections/${newCollection.id}`;
|
||||||
|
const location = { pathname, state: { skipSavingPath: true } };
|
||||||
|
|
||||||
|
browserHistory.push(location);
|
||||||
|
})
|
||||||
|
.catch((response) => {
|
||||||
|
console.error('Error creating collection', response.data);
|
||||||
|
dispatch({
|
||||||
|
type: ActionTypes.ERROR,
|
||||||
|
error: response.data
|
||||||
|
});
|
||||||
|
dispatch(stopLoader());
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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,18 +130,19 @@ export function createFolder(formProps) {
|
||||||
fileType: 'folder',
|
fileType: 'folder',
|
||||||
children: []
|
children: []
|
||||||
});
|
});
|
||||||
dispatch({
|
dispatch(closeNewFolderModal());
|
||||||
type: ActionTypes.CLOSE_NEW_FOLDER_MODAL
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateFileName(id, name) {
|
export function updateFileName(id, name) {
|
||||||
return {
|
return (dispatch) => {
|
||||||
|
dispatch(setUnsavedChanges(true));
|
||||||
|
dispatch({
|
||||||
type: ActionTypes.UPDATE_FILE_NAME,
|
type: ActionTypes.UPDATE_FILE_NAME,
|
||||||
id,
|
id,
|
||||||
name
|
name
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -133,6 +133,7 @@ export function saveProject(selectedFile = null, autosave = false) {
|
||||||
}
|
}
|
||||||
const formParams = Object.assign({}, state.project);
|
const formParams = Object.assign({}, state.project);
|
||||||
formParams.files = [...state.files];
|
formParams.files = [...state.files];
|
||||||
|
|
||||||
if (selectedFile) {
|
if (selectedFile) {
|
||||||
const fileToUpdate = formParams.files.find(file => file.id === selectedFile.id);
|
const fileToUpdate = formParams.files.find(file => file.id === selectedFile.id);
|
||||||
fileToUpdate.content = selectedFile.content;
|
fileToUpdate.content = selectedFile.content;
|
||||||
|
|
|
@ -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;
|
|
@ -269,8 +269,8 @@ class Editor extends React.Component {
|
||||||
indent_size: INDENTATION_AMOUNT,
|
indent_size: INDENTATION_AMOUNT,
|
||||||
indent_with_tabs: IS_TAB_INDENT
|
indent_with_tabs: IS_TAB_INDENT
|
||||||
};
|
};
|
||||||
|
|
||||||
const mode = this._cm.getOption('mode');
|
const mode = this._cm.getOption('mode');
|
||||||
|
const currentPosition = this._cm.doc.getCursor();
|
||||||
if (mode === 'javascript') {
|
if (mode === 'javascript') {
|
||||||
this._cm.doc.setValue(beautifyJS(this._cm.doc.getValue(), beautifyOptions));
|
this._cm.doc.setValue(beautifyJS(this._cm.doc.getValue(), beautifyOptions));
|
||||||
} else if (mode === 'css') {
|
} else if (mode === 'css') {
|
||||||
|
@ -278,6 +278,10 @@ class Editor extends React.Component {
|
||||||
} else if (mode === 'htmlmixed') {
|
} else if (mode === 'htmlmixed') {
|
||||||
this._cm.doc.setValue(beautifyHTML(this._cm.doc.getValue(), beautifyOptions));
|
this._cm.doc.setValue(beautifyHTML(this._cm.doc.getValue(), beautifyOptions));
|
||||||
}
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
this._cm.focus();
|
||||||
|
this._cm.doc.setCursor({ line: currentPosition.line, ch: currentPosition.ch + INDENTATION_AMOUNT });
|
||||||
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeDocuments(files) {
|
initializeDocuments(files) {
|
||||||
|
|
|
@ -6,39 +6,29 @@ import InlineSVG from 'react-inlinesvg';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import * as IDEActions from '../actions/ide';
|
import * as IDEActions from '../actions/ide';
|
||||||
import * as FileActions from '../actions/files';
|
import * as FileActions from '../actions/files';
|
||||||
|
import downArrowUrl from '../../../images/down-filled-triangle.svg';
|
||||||
const downArrowUrl = require('../../../images/down-filled-triangle.svg');
|
import folderRightUrl from '../../../images/triangle-arrow-right.svg';
|
||||||
const folderRightUrl = require('../../../images/triangle-arrow-right.svg');
|
import folderDownUrl from '../../../images/triangle-arrow-down.svg';
|
||||||
const folderDownUrl = require('../../../images/triangle-arrow-down.svg');
|
import fileUrl from '../../../images/file.svg';
|
||||||
const fileUrl = require('../../../images/file.svg');
|
|
||||||
|
|
||||||
export class FileNode extends React.Component {
|
export class FileNode extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.renderChild = this.renderChild.bind(this);
|
|
||||||
this.handleKeyPress = this.handleKeyPress.bind(this);
|
|
||||||
this.handleFileNameChange = this.handleFileNameChange.bind(this);
|
|
||||||
this.validateFileName = this.validateFileName.bind(this);
|
|
||||||
this.handleFileClick = this.handleFileClick.bind(this);
|
|
||||||
this.toggleFileOptions = this.toggleFileOptions.bind(this);
|
|
||||||
this.hideFileOptions = this.hideFileOptions.bind(this);
|
|
||||||
this.showEditFileName = this.showEditFileName.bind(this);
|
|
||||||
this.hideEditFileName = this.hideEditFileName.bind(this);
|
|
||||||
this.onBlurComponent = this.onBlurComponent.bind(this);
|
|
||||||
this.onFocusComponent = this.onFocusComponent.bind(this);
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isOptionsOpen: false,
|
isOptionsOpen: false,
|
||||||
isEditingName: false,
|
isEditingName: false,
|
||||||
isFocused: false,
|
isFocused: false,
|
||||||
|
isDeleting: false,
|
||||||
|
updatedName: this.props.name
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onFocusComponent() {
|
onFocusComponent = () => {
|
||||||
this.setState({ isFocused: true });
|
this.setState({ isFocused: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
onBlurComponent() {
|
onBlurComponent = () => {
|
||||||
this.setState({ isFocused: false });
|
this.setState({ isFocused: false });
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!this.state.isFocused) {
|
if (!this.state.isFocused) {
|
||||||
|
@ -47,41 +37,96 @@ export class FileNode extends React.Component {
|
||||||
}, 200);
|
}, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFileClick(e) {
|
|
||||||
e.stopPropagation();
|
setUpdatedName = (updatedName) => {
|
||||||
if (this.props.name !== 'root' && !this.isDeleting) {
|
this.setState({ updatedName });
|
||||||
this.props.setSelectedFile(this.props.id);
|
}
|
||||||
|
|
||||||
|
saveUpdatedFileName = () => {
|
||||||
|
const { updatedName } = this.state;
|
||||||
|
const { name, updateFileName, id } = this.props;
|
||||||
|
|
||||||
|
if (updatedName !== name) {
|
||||||
|
updateFileName(id, updatedName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFileNameChange(event) {
|
handleFileClick = (event) => {
|
||||||
this.props.updateFileName(this.props.id, event.target.value);
|
event.stopPropagation();
|
||||||
|
const { isDeleting } = this.state;
|
||||||
|
const { id, setSelectedFile, name } = this.props;
|
||||||
|
if (name !== 'root' && !isDeleting) {
|
||||||
|
setSelectedFile(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyPress(event) {
|
handleFileNameChange = (event) => {
|
||||||
|
const newName = event.target.value;
|
||||||
|
this.setUpdatedName(newName);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFileNameBlur = () => {
|
||||||
|
this.validateFileName();
|
||||||
|
this.hideEditFileName();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClickRename = () => {
|
||||||
|
this.setUpdatedName(this.props.name);
|
||||||
|
this.showEditFileName();
|
||||||
|
setTimeout(() => this.fileNameInput.focus(), 0);
|
||||||
|
setTimeout(() => this.hideFileOptions(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClickAddFile = () => {
|
||||||
|
this.props.newFile(this.props.id);
|
||||||
|
setTimeout(() => this.hideFileOptions(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClickAddFolder = () => {
|
||||||
|
this.props.newFolder(this.props.id);
|
||||||
|
setTimeout(() => this.hideFileOptions(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClickUploadFile = () => {
|
||||||
|
this.props.openUploadFileModal(this.props.id);
|
||||||
|
setTimeout(this.hideFileOptions, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClickDelete = () => {
|
||||||
|
if (window.confirm(`Are you sure you want to delete ${this.props.name}?`)) {
|
||||||
|
this.setState({ isDeleting: true });
|
||||||
|
this.props.resetSelectedFile(this.props.id);
|
||||||
|
setTimeout(() => this.props.deleteFile(this.props.id, this.props.parentId), 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyPress = (event) => {
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
this.hideEditFileName();
|
this.hideEditFileName();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
validateFileName() {
|
validateFileName = () => {
|
||||||
const oldFileExtension = this.originalFileName.match(/\.[0-9a-z]+$/i);
|
const currentName = this.props.name;
|
||||||
const newFileExtension = this.props.name.match(/\.[0-9a-z]+$/i);
|
const { updatedName } = this.state;
|
||||||
const hasPeriod = this.props.name.match(/\.+/);
|
const oldFileExtension = currentName.match(/\.[0-9a-z]+$/i);
|
||||||
const newFileName = this.props.name;
|
const newFileExtension = updatedName.match(/\.[0-9a-z]+$/i);
|
||||||
|
const hasPeriod = updatedName.match(/\.+/);
|
||||||
const hasNoExtension = oldFileExtension && !newFileExtension;
|
const hasNoExtension = oldFileExtension && !newFileExtension;
|
||||||
const hasExtensionIfFolder = this.props.fileType === 'folder' && hasPeriod;
|
const hasExtensionIfFolder = this.props.fileType === 'folder' && hasPeriod;
|
||||||
const notSameExtension = oldFileExtension && newFileExtension
|
const notSameExtension = oldFileExtension && newFileExtension
|
||||||
&& oldFileExtension[0].toLowerCase() !== newFileExtension[0].toLowerCase();
|
&& oldFileExtension[0].toLowerCase() !== newFileExtension[0].toLowerCase();
|
||||||
const hasEmptyFilename = newFileName === '';
|
const hasEmptyFilename = updatedName.trim() === '';
|
||||||
const hasOnlyExtension = newFileExtension && newFileName === newFileExtension[0];
|
const hasOnlyExtension = newFileExtension && updatedName.trim() === newFileExtension[0];
|
||||||
if (hasEmptyFilename || hasNoExtension || notSameExtension || hasOnlyExtension || hasExtensionIfFolder) {
|
if (hasEmptyFilename || hasNoExtension || notSameExtension || hasOnlyExtension || hasExtensionIfFolder) {
|
||||||
this.props.updateFileName(this.props.id, this.originalFileName);
|
this.setUpdatedName(currentName);
|
||||||
|
} else {
|
||||||
|
this.saveUpdatedFileName();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleFileOptions(e) {
|
toggleFileOptions = (event) => {
|
||||||
e.preventDefault();
|
event.preventDefault();
|
||||||
if (!this.props.canEdit) {
|
if (!this.props.canEdit) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -93,25 +138,31 @@ export class FileNode extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hideFileOptions() {
|
hideFileOptions = () => {
|
||||||
this.setState({ isOptionsOpen: false });
|
this.setState({ isOptionsOpen: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
showEditFileName() {
|
showEditFileName = () => {
|
||||||
this.setState({ isEditingName: true });
|
this.setState({ isEditingName: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
hideEditFileName() {
|
hideEditFileName = () => {
|
||||||
this.setState({ isEditingName: false });
|
this.setState({ isEditingName: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
renderChild(childId) {
|
showFolderChildren = () => {
|
||||||
return (
|
this.props.showFolderChildren(this.props.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
hideFolderChildren = () => {
|
||||||
|
this.props.hideFolderChildren(this.props.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderChild = childId => (
|
||||||
<li key={childId}>
|
<li key={childId}>
|
||||||
<ConnectedFileNode id={childId} parentId={this.props.id} canEdit={this.props.canEdit} />
|
<ConnectedFileNode id={childId} parentId={this.props.id} canEdit={this.props.canEdit} />
|
||||||
</li>
|
</li>
|
||||||
);
|
)
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const itemClass = classNames({
|
const itemClass = classNames({
|
||||||
|
@ -123,49 +174,50 @@ export class FileNode extends React.Component {
|
||||||
'sidebar__file-item--closed': this.props.isFolderClosed
|
'sidebar__file-item--closed': this.props.isFolderClosed
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isFile = this.props.fileType === 'file';
|
||||||
|
const isFolder = this.props.fileType === 'folder';
|
||||||
|
const isRoot = this.props.name === 'root';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={itemClass}>
|
<div className={itemClass}>
|
||||||
{(() => { // eslint-disable-line
|
{ !isRoot &&
|
||||||
if (this.props.name !== 'root') {
|
|
||||||
return (
|
|
||||||
<div className="file-item__content" onContextMenu={this.toggleFileOptions}>
|
<div className="file-item__content" onContextMenu={this.toggleFileOptions}>
|
||||||
<span className="file-item__spacer"></span>
|
<span className="file-item__spacer"></span>
|
||||||
{(() => { // eslint-disable-line
|
{ isFile &&
|
||||||
if (this.props.fileType === 'file') {
|
|
||||||
return (
|
|
||||||
<span className="sidebar__file-item-icon">
|
<span className="sidebar__file-item-icon">
|
||||||
<InlineSVG src={fileUrl} />
|
<InlineSVG src={fileUrl} />
|
||||||
</span>
|
</span>
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return (
|
{ isFolder &&
|
||||||
<div className="sidebar__file-item--folder">
|
<div className="sidebar__file-item--folder">
|
||||||
<button
|
<button
|
||||||
className="sidebar__file-item-closed"
|
className="sidebar__file-item-closed"
|
||||||
onClick={() => this.props.showFolderChildren(this.props.id)}
|
onClick={this.showFolderChildren}
|
||||||
>
|
>
|
||||||
<InlineSVG className="folder-right" src={folderRightUrl} />
|
<InlineSVG className="folder-right" src={folderRightUrl} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="sidebar__file-item-open"
|
className="sidebar__file-item-open"
|
||||||
onClick={() => this.props.hideFolderChildren(this.props.id)}
|
onClick={this.hideFolderChildren}
|
||||||
>
|
>
|
||||||
<InlineSVG className="folder-down" src={folderDownUrl} />
|
<InlineSVG className="folder-down" src={folderDownUrl} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
}
|
||||||
})()}
|
<button
|
||||||
<button className="sidebar__file-item-name" onClick={this.handleFileClick}>{this.props.name}</button>
|
className="sidebar__file-item-name"
|
||||||
|
onClick={this.handleFileClick}
|
||||||
|
>
|
||||||
|
{this.state.updatedName}
|
||||||
|
</button>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="sidebar__file-item-input"
|
className="sidebar__file-item-input"
|
||||||
value={this.props.name}
|
value={this.state.updatedName}
|
||||||
|
maxLength="128"
|
||||||
onChange={this.handleFileNameChange}
|
onChange={this.handleFileNameChange}
|
||||||
ref={(element) => { this.fileNameInput = element; }}
|
ref={(element) => { this.fileNameInput = element; }}
|
||||||
onBlur={() => {
|
onBlur={this.handleFileNameBlur}
|
||||||
this.validateFileName();
|
|
||||||
this.hideEditFileName();
|
|
||||||
}}
|
|
||||||
onKeyPress={this.handleKeyPress}
|
onKeyPress={this.handleKeyPress}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
|
@ -181,54 +233,47 @@ export class FileNode extends React.Component {
|
||||||
</button>
|
</button>
|
||||||
<div className="sidebar__file-item-options">
|
<div className="sidebar__file-item-options">
|
||||||
<ul title="file options">
|
<ul title="file options">
|
||||||
{(() => { // eslint-disable-line
|
{ isFolder &&
|
||||||
if (this.props.fileType === 'folder') {
|
<React.Fragment>
|
||||||
return (
|
|
||||||
<li>
|
|
||||||
<button
|
|
||||||
aria-label="add file"
|
|
||||||
onClick={() => {
|
|
||||||
this.props.newFile(this.props.id);
|
|
||||||
setTimeout(() => this.hideFileOptions(), 0);
|
|
||||||
}}
|
|
||||||
onBlur={this.onBlurComponent}
|
|
||||||
onFocus={this.onFocusComponent}
|
|
||||||
className="sidebar__file-item-option"
|
|
||||||
>
|
|
||||||
Add File
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})()}
|
|
||||||
{(() => { // eslint-disable-line
|
|
||||||
if (this.props.fileType === 'folder') {
|
|
||||||
return (
|
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
aria-label="add folder"
|
aria-label="add folder"
|
||||||
onClick={() => {
|
onClick={this.handleClickAddFolder}
|
||||||
this.props.newFolder(this.props.id);
|
|
||||||
setTimeout(() => this.hideFileOptions(), 0);
|
|
||||||
}}
|
|
||||||
onBlur={this.onBlurComponent}
|
onBlur={this.onBlurComponent}
|
||||||
onFocus={this.onFocusComponent}
|
onFocus={this.onFocusComponent}
|
||||||
className="sidebar__file-item-option"
|
className="sidebar__file-item-option"
|
||||||
>
|
>
|
||||||
Add Folder
|
Create folder
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
);
|
|
||||||
}
|
|
||||||
})()}
|
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
aria-label="add file"
|
||||||
this.originalFileName = this.props.name;
|
onClick={this.handleClickAddFile}
|
||||||
this.showEditFileName();
|
onBlur={this.onBlurComponent}
|
||||||
setTimeout(() => this.fileNameInput.focus(), 0);
|
onFocus={this.onFocusComponent}
|
||||||
setTimeout(() => this.hideFileOptions(), 0);
|
className="sidebar__file-item-option"
|
||||||
}}
|
>
|
||||||
|
Create file
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{ this.props.authenticated &&
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
aria-label="upload file"
|
||||||
|
onClick={this.handleClickUploadFile}
|
||||||
|
onBlur={this.onBlurComponent}
|
||||||
|
onFocus={this.onFocusComponent}
|
||||||
|
>
|
||||||
|
Upload file
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</React.Fragment>
|
||||||
|
}
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
onClick={this.handleClickRename}
|
||||||
onBlur={this.onBlurComponent}
|
onBlur={this.onBlurComponent}
|
||||||
onFocus={this.onFocusComponent}
|
onFocus={this.onFocusComponent}
|
||||||
className="sidebar__file-item-option"
|
className="sidebar__file-item-option"
|
||||||
|
@ -238,13 +283,7 @@ export class FileNode extends React.Component {
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={this.handleClickDelete}
|
||||||
if (window.confirm(`Are you sure you want to delete ${this.props.name}?`)) {
|
|
||||||
this.isDeleting = true;
|
|
||||||
this.props.resetSelectedFile(this.props.id);
|
|
||||||
setTimeout(() => this.props.deleteFile(this.props.id, this.props.parentId), 100);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onBlur={this.onBlurComponent}
|
onBlur={this.onBlurComponent}
|
||||||
onFocus={this.onFocusComponent}
|
onFocus={this.onFocusComponent}
|
||||||
className="sidebar__file-item-option"
|
className="sidebar__file-item-option"
|
||||||
|
@ -255,18 +294,12 @@ export class FileNode extends React.Component {
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
|
||||||
}
|
}
|
||||||
})()}
|
{ this.props.children &&
|
||||||
{(() => { // eslint-disable-line
|
|
||||||
if (this.props.children) {
|
|
||||||
return (
|
|
||||||
<ul className="file-item__children">
|
<ul className="file-item__children">
|
||||||
{this.props.children.map(this.renderChild)}
|
{this.props.children.map(this.renderChild)}
|
||||||
</ul>
|
</ul>
|
||||||
);
|
|
||||||
}
|
}
|
||||||
})()}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -288,18 +321,21 @@ FileNode.propTypes = {
|
||||||
newFolder: PropTypes.func.isRequired,
|
newFolder: PropTypes.func.isRequired,
|
||||||
showFolderChildren: PropTypes.func.isRequired,
|
showFolderChildren: PropTypes.func.isRequired,
|
||||||
hideFolderChildren: PropTypes.func.isRequired,
|
hideFolderChildren: PropTypes.func.isRequired,
|
||||||
canEdit: PropTypes.bool.isRequired
|
canEdit: PropTypes.bool.isRequired,
|
||||||
|
openUploadFileModal: PropTypes.func.isRequired,
|
||||||
|
authenticated: PropTypes.bool.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
FileNode.defaultProps = {
|
FileNode.defaultProps = {
|
||||||
parentId: '0',
|
parentId: '0',
|
||||||
isSelectedFile: false,
|
isSelectedFile: false,
|
||||||
isFolderClosed: false
|
isFolderClosed: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
function mapStateToProps(state, ownProps) {
|
function mapStateToProps(state, ownProps) {
|
||||||
// this is a hack, state is updated before ownProps
|
// this is a hack, state is updated before ownProps
|
||||||
return state.files.find(file => file.id === ownProps.id) || { name: 'test', fileType: 'file' };
|
const fileNode = state.files.find(file => file.id === ownProps.id) || { name: 'test', fileType: 'file' };
|
||||||
|
return Object.assign({}, fileNode, { authenticated: state.user.authenticated });
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch) {
|
function mapDispatchToProps(dispatch) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -74,6 +74,18 @@ function KeyboardShortcutModal() {
|
||||||
</span>
|
</span>
|
||||||
<span>Turn off Accessible Output</span>
|
<span>Turn off Accessible Output</span>
|
||||||
</li>
|
</li>
|
||||||
|
<li className="keyboard-shortcut-item">
|
||||||
|
<span className="keyboard-shortcut__command">
|
||||||
|
{metaKeyName} + B
|
||||||
|
</span>
|
||||||
|
<span>Toggle Sidebar</span>
|
||||||
|
</li>
|
||||||
|
<li className="keyboard-shortcut-item">
|
||||||
|
<span className="keyboard-shortcut__command">
|
||||||
|
Ctrl + `
|
||||||
|
</span>
|
||||||
|
<span>Toggle Console</span>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
72
client/modules/IDE/components/QuickAddList/QuickAddList.jsx
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
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.stopPropagation();
|
||||||
|
event.currentTarget.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) {
|
||||||
|
@ -29,12 +26,13 @@ class Searchbar extends React.Component {
|
||||||
|
|
||||||
handleSearchEnter = (e) => {
|
handleSearchEnter = (e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
this.props.setSearchTerm(this.state.searchValue);
|
this.searchChange();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
searchChange = (value) => {
|
searchChange = () => {
|
||||||
this.props.setSearchTerm(this.state.searchValue);
|
if (this.state.searchValue.trim().length === 0) return;
|
||||||
|
this.props.setSearchTerm(this.state.searchValue.trim());
|
||||||
};
|
};
|
||||||
|
|
||||||
handleSearchChange = (e) => {
|
handleSearchChange = (e) => {
|
||||||
|
@ -46,19 +44,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 +69,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,9 +110,25 @@ class Sidebar extends React.Component {
|
||||||
onBlur={this.onBlurComponent}
|
onBlur={this.onBlurComponent}
|
||||||
onFocus={this.onFocusComponent}
|
onFocus={this.onFocusComponent}
|
||||||
>
|
>
|
||||||
Add file
|
Create file
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
{
|
||||||
|
this.props.user.authenticated &&
|
||||||
|
<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>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -137,6 +153,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,14 +244,63 @@ 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>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SketchListRowBase.propTypes = {
|
SketchListRowBase.propTypes = {
|
||||||
sketch: PropTypes.shape({
|
sketch: PropTypes.shape({
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
name: PropTypes.string.isRequired
|
name: PropTypes.string.isRequired,
|
||||||
|
createdAt: PropTypes.string.isRequired,
|
||||||
|
updatedAt: PropTypes.string.isRequired
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
username: PropTypes.string.isRequired,
|
username: PropTypes.string.isRequired,
|
||||||
user: PropTypes.shape({
|
user: PropTypes.shape({
|
||||||
|
@ -249,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) {
|
||||||
|
@ -264,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() {
|
||||||
|
@ -274,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;
|
||||||
|
@ -336,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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -364,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
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -391,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) {
|
||||||
|
@ -156,6 +156,20 @@ class IDEView extends React.Component {
|
||||||
} else if (e.keyCode === 49 && ((e.metaKey && this.isMac) || (e.ctrlKey && !this.isMac)) && e.shiftKey) {
|
} else if (e.keyCode === 49 && ((e.metaKey && this.isMac) || (e.ctrlKey && !this.isMac)) && e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.props.setAllAccessibleOutput(true);
|
this.props.setAllAccessibleOutput(true);
|
||||||
|
} else if (e.keyCode === 66 && ((e.metaKey && this.isMac) || (e.ctrlKey && !this.isMac))) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!this.props.ide.sidebarIsExpanded) {
|
||||||
|
this.props.expandSidebar();
|
||||||
|
} else {
|
||||||
|
this.props.collapseSidebar();
|
||||||
|
}
|
||||||
|
} else if (e.keyCode === 192 && e.ctrlKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (this.props.ide.consoleIsExpanded) {
|
||||||
|
this.props.collapseConsole();
|
||||||
|
} else {
|
||||||
|
this.props.expandConsole();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -239,6 +253,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 +367,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 +375,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 +398,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 +493,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 +551,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 +563,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 +594,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;
|
|
@ -11,7 +11,7 @@ function draw() {
|
||||||
|
|
||||||
const defaultHTML =
|
const defaultHTML =
|
||||||
`<!DOCTYPE html>
|
`<!DOCTYPE html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.10.2/p5.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.10.2/p5.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.10.2/addons/p5.sound.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.10.2/addons/p5.sound.min.js"></script>
|
||||||
|
|
|
@ -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;
|
431
client/modules/User/components/Collection.jsx
Normal file
|
@ -0,0 +1,431 @@
|
||||||
|
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-content">
|
||||||
|
<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>
|
||||||
|
</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,
|
||||||
|
name: PropTypes.string,
|
||||||
|
slug: PropTypes.string,
|
||||||
|
description: PropTypes.string,
|
||||||
|
owner: PropTypes.shape({
|
||||||
|
username: PropTypes.string,
|
||||||
|
}).isRequired,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.shape({})),
|
||||||
|
}),
|
||||||
|
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,
|
||||||
|
collection: {
|
||||||
|
id: undefined,
|
||||||
|
items: [],
|
||||||
|
owner: {
|
||||||
|
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);
|
110
client/modules/User/components/CollectionCreate.jsx
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
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 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
|
||||||
|
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({
|
||||||
|
|
92
client/modules/User/pages/CollectionView.jsx
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
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) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
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,7 +83,11 @@
|
||||||
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;
|
||||||
&:enabled:hover {
|
& g {
|
||||||
|
fill: getThemifyVariable('button-color');
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
&:not(disabled):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');
|
||||||
color: getThemifyVariable('button-hover-color');
|
color: getThemifyVariable('button-hover-color');
|
||||||
|
@ -91,7 +95,7 @@
|
||||||
fill: getThemifyVariable('button-hover-color');
|
fill: getThemifyVariable('button-hover-color');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&:enabled:active {
|
&:not(disabled):active {
|
||||||
border-color: getThemifyVariable('button-background-active-color');
|
border-color: getThemifyVariable('button-background-active-color');
|
||||||
background-color: getThemifyVariable('button-background-active-color');
|
background-color: getThemifyVariable('button-background-active-color');
|
||||||
color: getThemifyVariable('button-active-color');
|
color: getThemifyVariable('button-active-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,
|
||||||
|
@ -127,7 +186,7 @@ $themes: (
|
||||||
modal-button-color: #333,
|
modal-button-color: #333,
|
||||||
heading-text-color: #e1e1e1,
|
heading-text-color: #e1e1e1,
|
||||||
secondary-text-color: #e1e1e1,
|
secondary-text-color: #e1e1e1,
|
||||||
inactive-text-color: #c1c1c1,
|
inactive-text-color: #f2f2f2,
|
||||||
background-color: #333,
|
background-color: #333,
|
||||||
button-background-color: $white,
|
button-background-color: $white,
|
||||||
button-color: $black,
|
button-color: $black,
|
||||||
|
@ -135,21 +194,21 @@ $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,
|
||||||
modal-background-color: #444,
|
modal-background-color: #444,
|
||||||
modal-button-background-color: #C1C1C1,
|
modal-button-background-color: #C1C1C1,
|
||||||
modal-border-color: #949494,
|
modal-border-color: #949494,
|
||||||
icon-color: #a9a9a9,
|
icon-color: #d9d9d9,
|
||||||
icon-hover-color: $yellow,
|
icon-hover-color: $yellow,
|
||||||
icon-toast-hover-color: $yellow,
|
icon-toast-hover-color: $yellow,
|
||||||
shadow-color: rgba(0, 0, 0, 0.16),
|
shadow-color: rgba(0, 0, 0, 0.16),
|
||||||
console-background-color: #4f4f4f,
|
console-background-color: #4f4f4f,
|
||||||
console-color: $black,
|
console-color: $black,
|
||||||
console-header-background-color: #3f3f3f,
|
console-header-background-color: #3f3f3f,
|
||||||
console-header-color: #b5b5b5,
|
console-header-color: #d9d9d9,
|
||||||
console-info-background-color: $lightsteelblue,
|
console-info-background-color: $lightsteelblue,
|
||||||
console-warn-background-color: $orange,
|
console-warn-background-color: $orange,
|
||||||
console-debug-background-color: $dodgerblue,
|
console-debug-background-color: $dodgerblue,
|
||||||
|
@ -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%;
|
||||||
|
}
|
190
client/styles/components/_collection.scss
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
.collection-container {
|
||||||
|
padding: #{24 / $base-font-size}rem #{66 / $base-font-size}rem;
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction:column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-metadata {
|
||||||
|
max-width: #{1012 / $base-font-size}rem;
|
||||||
|
width: 100%;
|
||||||
|
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-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: #{1012 / $base-font-size}rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
@include themify() {
|
||||||
|
border: 1px solid getThemifyVariable('modal-border-color');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-table-wrapper {
|
||||||
|
overflow-y: auto;
|
||||||
|
max-width: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// maybe don't need this?
|
||||||
|
[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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|