Merge branch 'develop' into p5js-to-1.0.0
2
.babelrc
|
@ -6,6 +6,7 @@
|
|||
"env": {
|
||||
"production": {
|
||||
"plugins": [
|
||||
"babel-plugin-styled-components",
|
||||
"transform-react-remove-prop-types",
|
||||
"@babel/plugin-transform-react-constant-elements",
|
||||
"@babel/plugin-transform-react-inline-elements",
|
||||
|
@ -48,6 +49,7 @@
|
|||
},
|
||||
"development": {
|
||||
"plugins": [
|
||||
"babel-plugin-styled-components",
|
||||
"react-hot-loader/babel"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ API_URL=/editor
|
|||
AWS_ACCESS_KEY=<your-aws-access-key>
|
||||
AWS_REGION=<your-aws-region>
|
||||
AWS_SECRET_KEY=<your-aws-secret-key>
|
||||
CORS_ALLOW_LOCALHOST=true
|
||||
EMAIL_SENDER=<transactional-email-sender>
|
||||
EMAIL_VERIFY_SECRET_TOKEN=whatever_you_want_this_to_be_it_only_matters_for_production
|
||||
EXAMPLE_USER_EMAIL=examples@p5js.org
|
||||
|
|
10
.eslintrc
|
@ -77,5 +77,13 @@
|
|||
"__SERVER__": true,
|
||||
"__DISABLE_SSR__": true,
|
||||
"__DEVTOOLS__": true
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.stories.jsx"],
|
||||
"rules": {
|
||||
"import/no-extraneous-dependencies": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
40
.github/CONTRIBUTING.md
vendored
|
@ -15,8 +15,6 @@ Hello! We welcome community contributions to the p5.js Web Editor. Contributing
|
|||
- [Issue Search and Tagging](#issue-search-and-tagging)
|
||||
- [Beginning Work](#beginning-work)
|
||||
- [Contribution Guides](#contribution-guides)
|
||||
- [Writing Commit Messages](#writing-commit-messages)
|
||||
- [Tips](#tips)
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
|
@ -62,45 +60,9 @@ If you feel like an issue is tagged incorrectly (e.g. it's low priority and you
|
|||
|
||||
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, look at the [development guide](https://github.com/processing/p5.js-web-editor/blob/master/developer_docs/development.md) for instructions on how to install the project locally and follow the right development workflow.
|
||||
|
||||
### Contribution Guides
|
||||
|
||||
* [https://guides.github.com/activities/hello-world/](https://guides.github.com/activities/hello-world/)
|
||||
* [https://guides.github.com/activities/forking/](https://guides.github.com/activities/forking/)
|
||||
|
||||
## Writing Commit Messages
|
||||
|
||||
Good commit messages serve at least three important purposes:
|
||||
|
||||
* They speed up the reviewing process.
|
||||
* They help us write good release notes.
|
||||
* They help future maintainers understand your change and the reasons behind it.
|
||||
|
||||
Structure your commit message like this:
|
||||
|
||||
```
|
||||
Short (50 chars or less) summary of changes ( involving Fixes #Issue-number keyword )
|
||||
|
||||
More detailed explanatory text, if necessary. Wrap it to about 72
|
||||
characters or so. In some contexts, the first line is treated as the
|
||||
subject of an email and the rest of the text as the body. The blank
|
||||
line separating the summary from the body is critical (unless you omit
|
||||
the body entirely); tools like rebase can get confused if you run the
|
||||
two together.
|
||||
|
||||
Further paragraphs come after blank lines.
|
||||
|
||||
- Bullet points are okay, too
|
||||
|
||||
- Typically a hyphen or asterisk is used for the bullet, preceded by a
|
||||
single space, with blank lines in between, but conventions vary here
|
||||
```
|
||||
|
||||
* Write the summary line and description of what you have done in the imperative mode, that is as if you were commanding someone. Start the line with "Fix", "Add", "Change" instead of "Fixed", "Added", "Changed".
|
||||
* Always leave the second line blank.
|
||||
* Be as descriptive as possible in the description. It helps reasoning about the intention of commits and gives more context about why changes happened.
|
||||
|
||||
## Tips
|
||||
|
||||
* If it seems difficult to summarize what your commit does, it may be because it includes several logical changes or bug fixes, and are better split up into several commits using `git add -p`.
|
3
.github/FUNDING.yml
vendored
|
@ -1 +1,2 @@
|
|||
custom: https://processingfoundation.org/support
|
||||
github: processing
|
||||
custom: https://processingfoundation.org/
|
||||
|
|
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
@ -1,3 +1,5 @@
|
|||
Fixes #issue-number
|
||||
|
||||
I have verified that this pull request:
|
||||
|
||||
* [ ] has no linting errors (`npm run lint`)
|
||||
|
|
3
.gitignore
vendored
|
@ -17,3 +17,6 @@ cert_chain.crt
|
|||
localhost.crt
|
||||
localhost.key
|
||||
privkey.pem
|
||||
|
||||
storybook-static
|
||||
duplicates.json
|
||||
|
|
29
.storybook/main.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
stories: ['../client/**/*.stories.(jsx|mdx)'],
|
||||
addons: [
|
||||
'@storybook/addon-actions',
|
||||
'@storybook/addon-docs',
|
||||
'@storybook/addon-knobs',
|
||||
'@storybook/addon-links',
|
||||
'storybook-addon-theme-playground/dist/register'
|
||||
],
|
||||
webpackFinal: async config => {
|
||||
// do mutation to the config
|
||||
|
||||
const rules = config.module.rules;
|
||||
|
||||
// modify storybook's file-loader rule to avoid conflicts with svgr
|
||||
const fileLoaderRule = rules.find(rule => rule.test.test('.svg'));
|
||||
fileLoaderRule.exclude = path.resolve(__dirname, '../client');
|
||||
|
||||
// use svgr for svg files
|
||||
rules.push({
|
||||
test: /\.svg$/,
|
||||
use: ["@svgr/webpack"],
|
||||
})
|
||||
|
||||
return config;
|
||||
},
|
||||
};
|
31
.storybook/preview.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import React from 'react';
|
||||
import { addDecorator, addParameters } from '@storybook/react';
|
||||
import { withKnobs } from "@storybook/addon-knobs";
|
||||
import { withThemePlayground } from 'storybook-addon-theme-playground';
|
||||
import { ThemeProvider } from "styled-components";
|
||||
|
||||
import theme, { Theme } from '../client/theme';
|
||||
|
||||
addDecorator(withKnobs);
|
||||
|
||||
const themeConfigs = Object.values(Theme).map(
|
||||
name => {
|
||||
return { name, theme: theme[name] };
|
||||
}
|
||||
);
|
||||
|
||||
addDecorator(withThemePlayground({
|
||||
theme: themeConfigs,
|
||||
provider: ThemeProvider
|
||||
}));
|
||||
|
||||
addParameters({
|
||||
options: {
|
||||
/**
|
||||
* display the top-level grouping as a "root" in the sidebar
|
||||
*/
|
||||
showRoots: true,
|
||||
},
|
||||
})
|
||||
|
||||
// addDecorator(storyFn => <ThemeProvider theme={theme}>{storyFn()}</ThemeProvider>);
|
|
@ -37,12 +37,13 @@ deploy:
|
|||
script: ./deploy.sh
|
||||
skip_cleanup: true
|
||||
on:
|
||||
branch: master
|
||||
branch: release
|
||||
tags: true
|
||||
- provider: script
|
||||
script: ./deploy_staging.sh
|
||||
skip_cleanup: true
|
||||
on:
|
||||
branch: feature/public-api
|
||||
branch: develop
|
||||
|
||||
env:
|
||||
global:
|
||||
|
|
|
@ -14,6 +14,7 @@ COPY .babelrc index.js nodemon.json ./
|
|||
COPY ./webpack ./webpack
|
||||
COPY client ./client
|
||||
COPY server ./server
|
||||
COPY translations/locales ./translations/locales
|
||||
CMD ["npm", "start"]
|
||||
|
||||
FROM development as build
|
||||
|
|
240
client/common/Button.jsx
Normal file
|
@ -0,0 +1,240 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from 'styled-components';
|
||||
import { Link } from 'react-router';
|
||||
|
||||
import { remSize, prop } from '../theme';
|
||||
|
||||
const kinds = {
|
||||
block: 'block',
|
||||
icon: 'icon',
|
||||
inline: 'inline',
|
||||
};
|
||||
|
||||
// The '&&&' will increase the specificity of the
|
||||
// component's CSS so that it overrides the more
|
||||
// general global styles
|
||||
const StyledButton = styled.button`
|
||||
&&& {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
width: max-content;
|
||||
text-decoration: none;
|
||||
|
||||
color: ${prop('Button.default.foreground')};
|
||||
background-color: ${prop('Button.default.background')};
|
||||
cursor: pointer;
|
||||
border: 2px solid ${prop('Button.default.border')};
|
||||
border-radius: 2px;
|
||||
padding: ${remSize(8)} ${remSize(25)};
|
||||
line-height: 1;
|
||||
|
||||
svg * {
|
||||
fill: ${prop('Button.default.foreground')};
|
||||
}
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
color: ${prop('Button.hover.foreground')};
|
||||
background-color: ${prop('Button.hover.background')};
|
||||
border-color: ${prop('Button.hover.border')};
|
||||
|
||||
svg * {
|
||||
fill: ${prop('Button.hover.foreground')};
|
||||
}
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
color: ${prop('Button.active.foreground')};
|
||||
background-color: ${prop('Button.active.background')};
|
||||
|
||||
svg * {
|
||||
fill: ${prop('Button.active.foreground')};
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: ${prop('Button.disabled.foreground')};
|
||||
background-color: ${prop('Button.disabled.background')};
|
||||
border-color: ${prop('Button.disabled.border')};
|
||||
cursor: not-allowed;
|
||||
|
||||
svg * {
|
||||
fill: ${prop('Button.disabled.foreground')};
|
||||
}
|
||||
}
|
||||
|
||||
> * + * {
|
||||
margin-left: ${remSize(8)};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledInlineButton = styled.button`
|
||||
&&& {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
text-decoration: none;
|
||||
|
||||
color: ${prop('primaryTextColor')};
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
line-height: 1;
|
||||
|
||||
svg * {
|
||||
fill: ${prop('primaryTextColor')};
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
> * + * {
|
||||
margin-left: ${remSize(8)};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledIconButton = styled.button`
|
||||
&&& {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
width: ${remSize(32)}px;
|
||||
height: ${remSize(32)}px;
|
||||
text-decoration: none;
|
||||
|
||||
color: ${prop('Button.default.foreground')};
|
||||
background-color: ${prop('Button.hover.background')};
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 50%;
|
||||
padding: ${remSize(8)} ${remSize(25)};
|
||||
line-height: 1;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
color: ${prop('Button.hover.foreground')};
|
||||
background-color: ${prop('Button.hover.background')};
|
||||
|
||||
svg * {
|
||||
fill: ${prop('Button.hover.foreground')};
|
||||
}
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
color: ${prop('Button.active.foreground')};
|
||||
background-color: ${prop('Button.active.background')};
|
||||
|
||||
svg * {
|
||||
fill: ${prop('Button.active.foreground')};
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: ${prop('Button.disabled.foreground')};
|
||||
background-color: ${prop('Button.disabled.background')};
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
> * + * {
|
||||
margin-left: ${remSize(8)};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* A Button performs an primary action
|
||||
*/
|
||||
const Button = ({
|
||||
children, href, kind, iconBefore, iconAfter, 'aria-label': ariaLabel, to, type, ...props
|
||||
}) => {
|
||||
const hasChildren = React.Children.count(children) > 0;
|
||||
const content = <>{iconBefore}{hasChildren && <span>{children}</span>}{iconAfter}</>;
|
||||
let StyledComponent = StyledButton;
|
||||
|
||||
if (kind === kinds.inline) {
|
||||
StyledComponent = StyledInlineButton;
|
||||
} else if (kind === kinds.icon) {
|
||||
StyledComponent = StyledIconButton;
|
||||
}
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
<StyledComponent
|
||||
kind={kind}
|
||||
as="a"
|
||||
aria-label={ariaLabel}
|
||||
href={href}
|
||||
{...props}
|
||||
>
|
||||
{content}
|
||||
</StyledComponent>
|
||||
);
|
||||
}
|
||||
|
||||
if (to) {
|
||||
return <StyledComponent kind={kind} as={Link} aria-label={ariaLabel} to={to} {...props}>{content}</StyledComponent>;
|
||||
}
|
||||
|
||||
return <StyledComponent kind={kind} aria-label={ariaLabel} type={type} {...props}>{content}</StyledComponent>;
|
||||
};
|
||||
|
||||
Button.defaultProps = {
|
||||
'children': null,
|
||||
'disabled': false,
|
||||
'iconAfter': null,
|
||||
'iconBefore': null,
|
||||
'kind': kinds.block,
|
||||
'href': null,
|
||||
'aria-label': null,
|
||||
'to': null,
|
||||
'type': 'button',
|
||||
};
|
||||
|
||||
Button.kinds = kinds;
|
||||
|
||||
Button.propTypes = {
|
||||
/**
|
||||
* The visible part of the button, telling the user what
|
||||
* the action is
|
||||
*/
|
||||
'children': PropTypes.element,
|
||||
/**
|
||||
If the button can be activated or not
|
||||
*/
|
||||
'disabled': PropTypes.bool,
|
||||
/**
|
||||
* SVG icon to place after child content
|
||||
*/
|
||||
'iconAfter': PropTypes.element,
|
||||
/**
|
||||
* SVG icon to place before child content
|
||||
*/
|
||||
'iconBefore': PropTypes.element,
|
||||
/**
|
||||
* The kind of button - determines how it appears visually
|
||||
*/
|
||||
'kind': PropTypes.oneOf(Object.values(kinds)),
|
||||
/**
|
||||
* Specifying an href will use an <a> to link to the URL
|
||||
*/
|
||||
'href': PropTypes.string,
|
||||
/*
|
||||
* An ARIA Label used for accessibility
|
||||
*/
|
||||
'aria-label': PropTypes.string,
|
||||
/**
|
||||
* Specifying a to URL will use a react-router Link
|
||||
*/
|
||||
'to': PropTypes.string,
|
||||
/**
|
||||
* If using a button, then type is defines the type of button
|
||||
*/
|
||||
'type': PropTypes.oneOf(['button', 'submit']),
|
||||
};
|
||||
|
||||
export default Button;
|
70
client/common/Button.stories.jsx
Normal file
|
@ -0,0 +1,70 @@
|
|||
import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { boolean, text } from '@storybook/addon-knobs';
|
||||
|
||||
import Button from './Button';
|
||||
import { GithubIcon, DropdownArrowIcon, PlusIcon } from './icons';
|
||||
|
||||
export default {
|
||||
title: 'Common/Button',
|
||||
component: Button
|
||||
};
|
||||
|
||||
export const AllFeatures = () => (
|
||||
<Button
|
||||
disabled={boolean('disabled', false)}
|
||||
type="submit"
|
||||
label={text('label', 'submit')}
|
||||
>
|
||||
{text('children', 'this is the button')}
|
||||
</Button>
|
||||
);
|
||||
|
||||
export const SubmitButton = () => (
|
||||
<Button type="submit" label="submit">This is a submit button</Button>
|
||||
);
|
||||
|
||||
export const DefaultTypeButton = () => <Button label="login" onClick={action('onClick')}>Log In</Button>;
|
||||
|
||||
export const DisabledButton = () => <Button disabled label="login" onClick={action('onClick')}>Log In</Button>;
|
||||
|
||||
export const AnchorButton = () => (
|
||||
<Button href="http://p5js.org" label="submit">Actually an anchor</Button>
|
||||
);
|
||||
|
||||
export const ReactRouterLink = () => (
|
||||
<Button to="./somewhere" label="submit">Actually a Link</Button>
|
||||
);
|
||||
|
||||
export const ButtonWithIconBefore = () => (
|
||||
<Button
|
||||
iconBefore={<GithubIcon aria-label="Github logo" />}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
);
|
||||
|
||||
export const ButtonWithIconAfter = () => (
|
||||
<Button
|
||||
iconAfter={<GithubIcon aria-label="Github logo" />}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
);
|
||||
|
||||
export const InlineButtonWithIconAfter = () => (
|
||||
<Button
|
||||
iconAfter={<DropdownArrowIcon />}
|
||||
kind={Button.kinds.inline}
|
||||
>
|
||||
File name
|
||||
</Button>
|
||||
);
|
||||
|
||||
export const InlineIconOnlyButton = () => (
|
||||
<Button
|
||||
aria-label="Add to collection"
|
||||
iconBefore={<PlusIcon />}
|
||||
kind={Button.kinds.inline}
|
||||
/>
|
||||
);
|
80
client/common/icons.jsx
Normal file
|
@ -0,0 +1,80 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from 'styled-components';
|
||||
import { prop } from '../theme';
|
||||
import SortArrowUp from '../images/sort-arrow-up.svg';
|
||||
import SortArrowDown from '../images/sort-arrow-down.svg';
|
||||
import Github from '../images/github.svg';
|
||||
import Google from '../images/google.svg';
|
||||
import Plus from '../images/plus-icon.svg';
|
||||
import Close from '../images/close.svg';
|
||||
import Exit from '../images/exit.svg';
|
||||
import DropdownArrow from '../images/down-filled-triangle.svg';
|
||||
import Preferences from '../images/preferences.svg';
|
||||
import Play from '../images/triangle-arrow-right.svg';
|
||||
import Code from '../images/code.svg';
|
||||
import Terminal from '../images/terminal.svg';
|
||||
|
||||
|
||||
// HOC that adds the right web accessibility props
|
||||
// https://www.scottohara.me/blog/2019/05/22/contextual-images-svgs-and-a11y.html
|
||||
|
||||
// could also give these a default size, color, etc. based on the theme
|
||||
// Need to add size to these - like small icon, medium icon, large icon. etc.
|
||||
function withLabel(SvgComponent) {
|
||||
const Icon = (props) => {
|
||||
const StyledIcon = styled(SvgComponent)`
|
||||
&&& {
|
||||
color: ${prop('Icon.default')};
|
||||
& g, & path, & polygon {
|
||||
opacity: 1;
|
||||
fill: ${prop('Icon.default')};
|
||||
}
|
||||
&:hover {
|
||||
color: ${prop('Icon.hover')};
|
||||
& g, & path, & polygon {
|
||||
opacity: 1;
|
||||
fill: ${prop('Icon.hover')};
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const { 'aria-label': ariaLabel } = props;
|
||||
if (ariaLabel) {
|
||||
return (<StyledIcon
|
||||
{...props}
|
||||
aria-label={ariaLabel}
|
||||
role="img"
|
||||
focusable="false"
|
||||
/>);
|
||||
}
|
||||
return (<StyledIcon
|
||||
{...props}
|
||||
aria-hidden
|
||||
focusable="false"
|
||||
/>);
|
||||
};
|
||||
|
||||
Icon.propTypes = {
|
||||
'aria-label': PropTypes.string
|
||||
};
|
||||
|
||||
Icon.defaultProps = {
|
||||
'aria-label': null
|
||||
};
|
||||
|
||||
return Icon;
|
||||
}
|
||||
|
||||
export const SortArrowUpIcon = withLabel(SortArrowUp);
|
||||
export const SortArrowDownIcon = withLabel(SortArrowDown);
|
||||
export const GithubIcon = withLabel(Github);
|
||||
export const GoogleIcon = withLabel(Google);
|
||||
export const PlusIcon = withLabel(Plus);
|
||||
export const CloseIcon = withLabel(Close);
|
||||
export const ExitIcon = withLabel(Exit);
|
||||
export const DropdownArrowIcon = withLabel(DropdownArrow);
|
||||
export const PreferencesIcon = withLabel(Preferences);
|
||||
export const PlayIcon = withLabel(Play);
|
||||
export const TerminalIcon = withLabel(Terminal);
|
18
client/common/icons.stories.jsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
import React from 'react';
|
||||
import { select } from '@storybook/addon-knobs';
|
||||
|
||||
import * as icons from './icons';
|
||||
|
||||
export default {
|
||||
title: 'Common/Icons',
|
||||
component: icons
|
||||
};
|
||||
|
||||
export const AllIcons = () => {
|
||||
const names = Object.keys(icons);
|
||||
|
||||
const SelectedIcon = icons[select('name', names, names[0])];
|
||||
return (
|
||||
<SelectedIcon />
|
||||
);
|
||||
};
|
|
@ -1,17 +1,20 @@
|
|||
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');
|
||||
import AddIcon from '../images/plus.svg';
|
||||
import RemoveIcon from '../images/minus.svg';
|
||||
|
||||
const AddRemoveButton = ({ type, onClick }) => {
|
||||
const alt = type === 'add' ? 'add to collection' : 'remove from collection';
|
||||
const icon = type === 'add' ? addIcon : removeIcon;
|
||||
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
|
||||
className="overlay__close-button"
|
||||
onClick={onClick}
|
||||
aria-label={alt}
|
||||
>
|
||||
<Icon focusable="false" aria-hidden="true" />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -3,21 +3,21 @@ import React from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import { withRouter } from 'react-router';
|
||||
import { Link } from 'react-router';
|
||||
import InlineSVG from 'react-inlinesvg';
|
||||
import classNames from 'classnames';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import i18next from 'i18next';
|
||||
import * as IDEActions from '../modules/IDE/actions/ide';
|
||||
import * as toastActions from '../modules/IDE/actions/toast';
|
||||
import * as projectActions from '../modules/IDE/actions/project';
|
||||
import { setAllAccessibleOutput } from '../modules/IDE/actions/preferences';
|
||||
import { logoutUser } from '../modules/User/actions';
|
||||
|
||||
import getConfig from '../utils/getConfig';
|
||||
import { metaKeyName, } from '../utils/metaKey';
|
||||
import caretLeft from '../images/left-arrow.svg';
|
||||
|
||||
const triangleUrl = require('../images/down-filled-triangle.svg');
|
||||
const logoUrl = require('../images/p5js-logo-small.svg');
|
||||
|
||||
const __process = (typeof global !== 'undefined' ? global : window).process;
|
||||
import CaretLeftIcon from '../images/left-arrow.svg';
|
||||
import TriangleIcon from '../images/down-filled-triangle.svg';
|
||||
import LogoIcon from '../images/p5js-logo-small.svg';
|
||||
|
||||
class Nav extends React.PureComponent {
|
||||
constructor(props) {
|
||||
|
@ -57,6 +57,10 @@ class Nav extends React.PureComponent {
|
|||
this.handleFocusForHelp = this.handleFocus.bind(this, 'help');
|
||||
this.toggleDropdownForAccount = this.toggleDropdown.bind(this, 'account');
|
||||
this.handleFocusForAccount = this.handleFocus.bind(this, 'account');
|
||||
this.toggleDropdownForLang = this.toggleDropdown.bind(this, 'lang');
|
||||
this.handleFocusForLang = this.handleFocus.bind(this, 'lang');
|
||||
this.handleLangSelection = this.handleLangSelection.bind(this);
|
||||
|
||||
this.closeDropDown = this.closeDropDown.bind(this);
|
||||
}
|
||||
|
||||
|
@ -165,6 +169,13 @@ class Nav extends React.PureComponent {
|
|||
this.setDropdown('none');
|
||||
}
|
||||
|
||||
handleLangSelection(event) {
|
||||
i18next.changeLanguage(event.target.value);
|
||||
this.props.showToast(1500);
|
||||
this.props.setToastText('LangChange');
|
||||
this.setDropdown('none');
|
||||
}
|
||||
|
||||
handleLogout() {
|
||||
this.props.logoutUser();
|
||||
this.setDropdown('none');
|
||||
|
@ -229,13 +240,13 @@ class Nav extends React.PureComponent {
|
|||
return (
|
||||
<ul className="nav__items-left">
|
||||
<li className="nav__item-logo">
|
||||
<InlineSVG src={logoUrl} alt="p5.js logo" className="svg__logo" />
|
||||
<LogoIcon role="img" aria-label="p5.js Logo" focusable="false" 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" />
|
||||
<CaretLeftIcon className="nav__back-icon" focusable="false" aria-hidden="true" />
|
||||
<span className="nav__item-header">
|
||||
Back to Editor
|
||||
{this.props.t('BackEditor')}
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
|
@ -247,7 +258,7 @@ class Nav extends React.PureComponent {
|
|||
return (
|
||||
<ul className="nav__items-left">
|
||||
<li className="nav__item-logo">
|
||||
<InlineSVG src={logoUrl} alt="p5.js logo" className="svg__logo" />
|
||||
<LogoIcon role="img" aria-label="p5.js Logo" focusable="false" className="svg__logo" />
|
||||
</li>
|
||||
<li className={navDropdownState.file}>
|
||||
<button
|
||||
|
@ -260,8 +271,8 @@ class Nav extends React.PureComponent {
|
|||
}
|
||||
}}
|
||||
>
|
||||
<span className="nav__item-header">File</span>
|
||||
<InlineSVG className="nav__item-header-triangle" src={triangleUrl} />
|
||||
<span className="nav__item-header">{this.props.t('File')}</span>
|
||||
<TriangleIcon className="nav__item-header-triangle" focusable="false" aria-hidden="true" />
|
||||
</button>
|
||||
<ul className="nav__dropdown">
|
||||
<li className="nav__dropdown-item">
|
||||
|
@ -270,18 +281,18 @@ class Nav extends React.PureComponent {
|
|||
onFocus={this.handleFocusForFile}
|
||||
onBlur={this.handleBlur}
|
||||
>
|
||||
New
|
||||
{this.props.t('New')}
|
||||
</button>
|
||||
</li>
|
||||
{ __process.env.LOGIN_ENABLED && (!this.props.project.owner || this.isUserOwner()) &&
|
||||
{ getConfig('LOGIN_ENABLED') && (!this.props.project.owner || this.isUserOwner()) &&
|
||||
<li className="nav__dropdown-item">
|
||||
<button
|
||||
onClick={this.handleSave}
|
||||
onFocus={this.handleFocusForFile}
|
||||
onBlur={this.handleBlur}
|
||||
>
|
||||
Save
|
||||
<span className="nav__keyboard-shortcut">{metaKeyName}+s</span>
|
||||
{this.props.t('Save')}
|
||||
<span className="nav__keyboard-shortcut">{metaKeyName}+S</span>
|
||||
</button>
|
||||
</li> }
|
||||
{ this.props.project.id && this.props.user.authenticated &&
|
||||
|
@ -291,7 +302,7 @@ class Nav extends React.PureComponent {
|
|||
onFocus={this.handleFocusForFile}
|
||||
onBlur={this.handleBlur}
|
||||
>
|
||||
Duplicate
|
||||
{this.props.t('Duplicate')}
|
||||
</button>
|
||||
</li> }
|
||||
{ this.props.project.id &&
|
||||
|
@ -301,7 +312,7 @@ class Nav extends React.PureComponent {
|
|||
onFocus={this.handleFocusForFile}
|
||||
onBlur={this.handleBlur}
|
||||
>
|
||||
Share
|
||||
{this.props.t('Share')}
|
||||
</button>
|
||||
</li> }
|
||||
{ this.props.project.id &&
|
||||
|
@ -311,7 +322,7 @@ class Nav extends React.PureComponent {
|
|||
onFocus={this.handleFocusForFile}
|
||||
onBlur={this.handleBlur}
|
||||
>
|
||||
Download
|
||||
{this.props.t('Download')}
|
||||
</button>
|
||||
</li> }
|
||||
{ this.props.user.authenticated &&
|
||||
|
@ -322,10 +333,10 @@ class Nav extends React.PureComponent {
|
|||
onBlur={this.handleBlur}
|
||||
onClick={this.setDropdownForNone}
|
||||
>
|
||||
Open
|
||||
{this.props.t('Open')}
|
||||
</Link>
|
||||
</li> }
|
||||
{__process.env.UI_COLLECTIONS_ENABLED &&
|
||||
{getConfig('UI_COLLECTIONS_ENABLED') &&
|
||||
this.props.user.authenticated &&
|
||||
this.props.project.id &&
|
||||
<li className="nav__dropdown-item">
|
||||
|
@ -335,10 +346,10 @@ class Nav extends React.PureComponent {
|
|||
onBlur={this.handleBlur}
|
||||
onClick={this.setDropdownForNone}
|
||||
>
|
||||
Add to Collection
|
||||
{this.props.t('AddToCollection')}
|
||||
</Link>
|
||||
</li>}
|
||||
{ __process.env.EXAMPLES_ENABLED &&
|
||||
{ getConfig('EXAMPLES_ENABLED') &&
|
||||
<li className="nav__dropdown-item">
|
||||
<Link
|
||||
to="/p5/sketches"
|
||||
|
@ -346,7 +357,7 @@ class Nav extends React.PureComponent {
|
|||
onBlur={this.handleBlur}
|
||||
onClick={this.setDropdownForNone}
|
||||
>
|
||||
Examples
|
||||
{this.props.t('Examples')}
|
||||
</Link>
|
||||
</li> }
|
||||
</ul>
|
||||
|
@ -362,8 +373,8 @@ class Nav extends React.PureComponent {
|
|||
}
|
||||
}}
|
||||
>
|
||||
<span className="nav__item-header">Edit</span>
|
||||
<InlineSVG className="nav__item-header-triangle" src={triangleUrl} />
|
||||
<span className="nav__item-header">{this.props.t('Edit')}</span>
|
||||
<TriangleIcon className="nav__item-header-triangle" focusable="false" aria-hidden="true" />
|
||||
</button>
|
||||
<ul className="nav__dropdown" >
|
||||
<li className="nav__dropdown-item">
|
||||
|
@ -375,7 +386,7 @@ class Nav extends React.PureComponent {
|
|||
onFocus={this.handleFocusForEdit}
|
||||
onBlur={this.handleBlur}
|
||||
>
|
||||
Tidy Code
|
||||
{this.props.t('TidyCode')}
|
||||
<span className="nav__keyboard-shortcut">{'\u21E7'}+Tab</span>
|
||||
</button>
|
||||
</li>
|
||||
|
@ -385,7 +396,7 @@ class Nav extends React.PureComponent {
|
|||
onFocus={this.handleFocusForEdit}
|
||||
onBlur={this.handleBlur}
|
||||
>
|
||||
Find
|
||||
{this.props.t('Find')}
|
||||
<span className="nav__keyboard-shortcut">{metaKeyName}+F</span>
|
||||
</button>
|
||||
</li>
|
||||
|
@ -395,7 +406,7 @@ class Nav extends React.PureComponent {
|
|||
onFocus={this.handleFocusForEdit}
|
||||
onBlur={this.handleBlur}
|
||||
>
|
||||
Find Next
|
||||
{this.props.t('FindNext')}
|
||||
<span className="nav__keyboard-shortcut">{metaKeyName}+G</span>
|
||||
</button>
|
||||
</li>
|
||||
|
@ -405,7 +416,7 @@ class Nav extends React.PureComponent {
|
|||
onFocus={this.handleFocusForEdit}
|
||||
onBlur={this.handleBlur}
|
||||
>
|
||||
Find Previous
|
||||
{this.props.t('FindPrevious')}
|
||||
<span className="nav__keyboard-shortcut">{'\u21E7'}+{metaKeyName}+G</span>
|
||||
</button>
|
||||
</li>
|
||||
|
@ -422,8 +433,8 @@ class Nav extends React.PureComponent {
|
|||
}
|
||||
}}
|
||||
>
|
||||
<span className="nav__item-header">Sketch</span>
|
||||
<InlineSVG className="nav__item-header-triangle" src={triangleUrl} />
|
||||
<span className="nav__item-header">{this.props.t('Sketch')}</span>
|
||||
<TriangleIcon className="nav__item-header-triangle" focusable="false" aria-hidden="true" />
|
||||
</button>
|
||||
<ul className="nav__dropdown">
|
||||
<li className="nav__dropdown-item">
|
||||
|
@ -432,7 +443,7 @@ class Nav extends React.PureComponent {
|
|||
onFocus={this.handleFocusForSketch}
|
||||
onBlur={this.handleBlur}
|
||||
>
|
||||
Add File
|
||||
{this.props.t('AddFile')}
|
||||
</button>
|
||||
</li>
|
||||
<li className="nav__dropdown-item">
|
||||
|
@ -441,7 +452,7 @@ class Nav extends React.PureComponent {
|
|||
onFocus={this.handleFocusForSketch}
|
||||
onBlur={this.handleBlur}
|
||||
>
|
||||
Add Folder
|
||||
{this.props.t('AddFolder')}
|
||||
</button>
|
||||
</li>
|
||||
<li className="nav__dropdown-item">
|
||||
|
@ -450,7 +461,7 @@ class Nav extends React.PureComponent {
|
|||
onFocus={this.handleFocusForSketch}
|
||||
onBlur={this.handleBlur}
|
||||
>
|
||||
Run
|
||||
{this.props.t('Run')}
|
||||
<span className="nav__keyboard-shortcut">{metaKeyName}+Enter</span>
|
||||
</button>
|
||||
</li>
|
||||
|
@ -460,7 +471,7 @@ class Nav extends React.PureComponent {
|
|||
onFocus={this.handleFocusForSketch}
|
||||
onBlur={this.handleBlur}
|
||||
>
|
||||
Stop
|
||||
{this.props.t('Stop')}
|
||||
<span className="nav__keyboard-shortcut">{'\u21E7'}+{metaKeyName}+Enter</span>
|
||||
</button>
|
||||
</li>
|
||||
|
@ -497,8 +508,8 @@ class Nav extends React.PureComponent {
|
|||
}
|
||||
}}
|
||||
>
|
||||
<span className="nav__item-header">Help</span>
|
||||
<InlineSVG className="nav__item-header-triangle" src={triangleUrl} />
|
||||
<span className="nav__item-header">{this.props.t('Help')}</span>
|
||||
<TriangleIcon className="nav__item-header-triangle" focusable="false" aria-hidden="true" />
|
||||
</button>
|
||||
<ul className="nav__dropdown">
|
||||
<li className="nav__dropdown-item">
|
||||
|
@ -507,7 +518,7 @@ class Nav extends React.PureComponent {
|
|||
onBlur={this.handleBlur}
|
||||
onClick={this.handleKeyboardShortcuts}
|
||||
>
|
||||
Keyboard Shortcuts
|
||||
{this.props.t('KeyboardShortcuts')}
|
||||
</button>
|
||||
</li>
|
||||
<li className="nav__dropdown-item">
|
||||
|
@ -518,7 +529,7 @@ class Nav extends React.PureComponent {
|
|||
onFocus={this.handleFocusForHelp}
|
||||
onBlur={this.handleBlur}
|
||||
onClick={this.setDropdownForNone}
|
||||
>Reference
|
||||
>{this.props.t('Reference')}
|
||||
</a>
|
||||
</li>
|
||||
<li className="nav__dropdown-item">
|
||||
|
@ -528,7 +539,7 @@ class Nav extends React.PureComponent {
|
|||
onBlur={this.handleBlur}
|
||||
onClick={this.setDropdownForNone}
|
||||
>
|
||||
About
|
||||
{this.props.t('About')}
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -537,18 +548,73 @@ class Nav extends React.PureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
renderLanguageMenu(navDropdownState) {
|
||||
return (
|
||||
<ul className="nav__items-right" title="user-menu">
|
||||
<li className={navDropdownState.lang}>
|
||||
<button
|
||||
onClick={this.toggleDropdownForLang}
|
||||
onBlur={this.handleBlur}
|
||||
onFocus={this.clearHideTimeout}
|
||||
onMouseOver={() => {
|
||||
if (this.state.dropdownOpen !== 'none') {
|
||||
this.setDropdown('lang');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="nav__item-header"> {this.props.t('Lang')}</span>
|
||||
<TriangleIcon className="nav__item-header-triangle" focusable="false" aria-hidden="true" />
|
||||
</button>
|
||||
<ul className="nav__dropdown">
|
||||
|
||||
<li className="nav__dropdown-item">
|
||||
<button
|
||||
onFocus={this.handleFocusForLang}
|
||||
onBlur={this.handleBlur}
|
||||
value="it"
|
||||
onClick={e => this.handleLangSelection(e)}
|
||||
>
|
||||
Italian (Test Fallback)
|
||||
</button>
|
||||
</li>
|
||||
<li className="nav__dropdown-item">
|
||||
<button
|
||||
onFocus={this.handleFocusForLang}
|
||||
onBlur={this.handleBlur}
|
||||
value="en-US"
|
||||
onClick={e => this.handleLangSelection(e)}
|
||||
>English
|
||||
</button>
|
||||
</li>
|
||||
<li className="nav__dropdown-item">
|
||||
<button
|
||||
onFocus={this.handleFocusForLang}
|
||||
onBlur={this.handleBlur}
|
||||
value="es-419"
|
||||
onClick={e => this.handleLangSelection(e)}
|
||||
>
|
||||
Español
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
renderUnauthenticatedUserMenu(navDropdownState) {
|
||||
return (
|
||||
<ul className="nav__items-right" title="user-menu">
|
||||
<li className="nav__item">
|
||||
<Link to="/login">
|
||||
<span className="nav__item-header">Log in</span>
|
||||
<Link to="/login" className="nav__auth-button">
|
||||
<span className="nav__item-header">{this.props.t('Login')}</span>
|
||||
</Link>
|
||||
</li>
|
||||
<span className="nav__item-spacer">or</span>
|
||||
<span className="nav__item-or">{this.props.t('LoginOr')}</span>
|
||||
<li className="nav__item">
|
||||
<Link to="/signup">
|
||||
<span className="nav__item-header">Sign up</span>
|
||||
<Link to="/signup" className="nav__auth-button">
|
||||
<span className="nav__item-header">{this.props.t('SignUp')}</span>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -559,7 +625,7 @@ class Nav extends React.PureComponent {
|
|||
return (
|
||||
<ul className="nav__items-right" title="user-menu">
|
||||
<li className="nav__item">
|
||||
<span>Hello, {this.props.user.username}!</span>
|
||||
<span>{this.props.t('Hello')}, {this.props.user.username}!</span>
|
||||
</li>
|
||||
<span className="nav__item-spacer">|</span>
|
||||
<li className={navDropdownState.account}>
|
||||
|
@ -574,8 +640,8 @@ class Nav extends React.PureComponent {
|
|||
}
|
||||
}}
|
||||
>
|
||||
My Account
|
||||
<InlineSVG className="nav__item-header-triangle" src={triangleUrl} />
|
||||
{this.props.t('MyAccount')}
|
||||
<TriangleIcon className="nav__item-header-triangle" focusable="false" aria-hidden="true" />
|
||||
</button>
|
||||
<ul className="nav__dropdown">
|
||||
<li className="nav__dropdown-item">
|
||||
|
@ -585,10 +651,10 @@ class Nav extends React.PureComponent {
|
|||
onBlur={this.handleBlur}
|
||||
onClick={this.setDropdownForNone}
|
||||
>
|
||||
My sketches
|
||||
{this.props.t('MySketches')}
|
||||
</Link>
|
||||
</li>
|
||||
{__process.env.UI_COLLECTIONS_ENABLED &&
|
||||
{getConfig('UI_COLLECTIONS_ENABLED') &&
|
||||
<li className="nav__dropdown-item">
|
||||
<Link
|
||||
to={`/${this.props.user.username}/collections`}
|
||||
|
@ -596,7 +662,7 @@ class Nav extends React.PureComponent {
|
|||
onBlur={this.handleBlur}
|
||||
onClick={this.setDropdownForNone}
|
||||
>
|
||||
My collections
|
||||
{this.props.t('MyCollections')}
|
||||
</Link>
|
||||
</li>
|
||||
}
|
||||
|
@ -607,7 +673,7 @@ class Nav extends React.PureComponent {
|
|||
onBlur={this.handleBlur}
|
||||
onClick={this.setDropdownForNone}
|
||||
>
|
||||
My assets
|
||||
{this.props.t('MyAssets')}
|
||||
</Link>
|
||||
</li>
|
||||
<li className="nav__dropdown-item">
|
||||
|
@ -617,7 +683,7 @@ class Nav extends React.PureComponent {
|
|||
onBlur={this.handleBlur}
|
||||
onClick={this.setDropdownForNone}
|
||||
>
|
||||
Settings
|
||||
{this.props.t('Settings')}
|
||||
</Link>
|
||||
</li>
|
||||
<li className="nav__dropdown-item">
|
||||
|
@ -626,7 +692,7 @@ class Nav extends React.PureComponent {
|
|||
onFocus={this.handleFocusForAccount}
|
||||
onBlur={this.handleBlur}
|
||||
>
|
||||
Log out
|
||||
{this.props.t('LogOut')}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -636,7 +702,7 @@ class Nav extends React.PureComponent {
|
|||
}
|
||||
|
||||
renderUserMenu(navDropdownState) {
|
||||
const isLoginEnabled = __process.env.LOGIN_ENABLED;
|
||||
const isLoginEnabled = getConfig('LOGIN_ENABLED');
|
||||
const isAuthenticated = this.props.user.authenticated;
|
||||
|
||||
if (isLoginEnabled && isAuthenticated) {
|
||||
|
@ -679,25 +745,21 @@ class Nav extends React.PureComponent {
|
|||
account: classNames({
|
||||
'nav__item': true,
|
||||
'nav__item--open': this.state.dropdownOpen === 'account'
|
||||
}),
|
||||
lang: classNames({
|
||||
'nav__item': true,
|
||||
'nav__item--open': this.state.dropdownOpen === 'lang'
|
||||
})
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="nav" title="main-navigation" ref={(node) => { this.node = node; }}>
|
||||
{this.renderLeftLayout(navDropdownState)}
|
||||
{this.renderUserMenu(navDropdownState)}
|
||||
{/*
|
||||
<div className="nav__announce">
|
||||
This is a preview version of the editor, that has not yet been officially released.
|
||||
It is in development, you can report bugs <a
|
||||
href="https://github.com/processing/p5.js-web-editor/issues"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>here</a>.
|
||||
Please use with caution.
|
||||
</div>
|
||||
*/}
|
||||
</nav>
|
||||
<header>
|
||||
<nav className="nav" title="main-navigation" ref={(node) => { this.node = node; }}>
|
||||
{this.renderLeftLayout(navDropdownState)}
|
||||
{getConfig('TRANSLATIONS_ENABLED') && this.renderLanguageMenu(navDropdownState)}
|
||||
{this.renderUserMenu(navDropdownState)}
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -745,7 +807,9 @@ Nav.propTypes = {
|
|||
}).isRequired,
|
||||
params: PropTypes.shape({
|
||||
username: PropTypes.string
|
||||
})
|
||||
}),
|
||||
t: PropTypes.func.isRequired
|
||||
|
||||
};
|
||||
|
||||
Nav.defaultProps = {
|
||||
|
@ -778,5 +842,5 @@ const mapDispatchToProps = {
|
|||
setAllAccessibleOutput
|
||||
};
|
||||
|
||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Nav));
|
||||
export default withTranslation('WebEditor')(withRouter(connect(mapStateToProps, mapDispatchToProps)(Nav)));
|
||||
export { Nav as NavComponent };
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
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');
|
||||
import LogoIcon from '../images/p5js-logo-small.svg';
|
||||
import ArrowIcon from '../images/triangle-arrow-left.svg';
|
||||
|
||||
class NavBasic extends React.PureComponent {
|
||||
static defaultProps = {
|
||||
|
@ -15,13 +14,13 @@ class NavBasic extends React.PureComponent {
|
|||
<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" />
|
||||
<LogoIcon role="img" aria-label="p5.js Logo" focusable="false" 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" />
|
||||
<ArrowIcon focusable="false" aria-hidden="true" />
|
||||
</span>
|
||||
Back to the editor
|
||||
</button>
|
||||
|
|
|
@ -1,24 +1,23 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router';
|
||||
import InlineSVG from 'react-inlinesvg';
|
||||
|
||||
const logoUrl = require('../images/p5js-logo-small.svg');
|
||||
const editorUrl = require('../images/code.svg');
|
||||
import LogoIcon from '../images/p5js-logo-small.svg';
|
||||
import CodeIcon from '../images/code.svg';
|
||||
|
||||
const PreviewNav = ({ owner, project }) => (
|
||||
<nav className="nav preview-nav">
|
||||
<div className="nav__items-left">
|
||||
<div className="nav__item-logo">
|
||||
<InlineSVG src={logoUrl} alt="p5.js logo" />
|
||||
<LogoIcon role="img" aria-label="p5.js Logo" focusable="false" className="svg__logo" />
|
||||
</div>
|
||||
<Link className="nav__item" to={`/${owner.username}/sketches/${project.id}`}>{project.name}</Link>
|
||||
<p className="toolbar__project-owner">by</p>
|
||||
<Link className="nav__item" to={`/${owner.username}/sketches/`}>{owner.username}</Link>
|
||||
</div>
|
||||
<div className="nav__items-right">
|
||||
<Link to={`/${owner.username}/sketches/${project.id}`}>
|
||||
<InlineSVG className="preview-nav__editor-svg" src={editorUrl} />
|
||||
<Link to={`/${owner.username}/sketches/${project.id}`} aria-label="Edit Sketch" >
|
||||
<CodeIcon className="preview-nav__editor-svg" focusable="false" aria-hidden="true" />
|
||||
</Link>
|
||||
</div>
|
||||
</nav>
|
||||
|
|
|
@ -1,71 +0,0 @@
|
|||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,9 +1,9 @@
|
|||
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { NavComponent } from './../Nav';
|
||||
|
||||
import { NavComponent } from '../Nav';
|
||||
|
||||
describe('Nav', () => {
|
||||
const props = {
|
||||
|
@ -44,19 +44,12 @@ describe('Nav', () => {
|
|||
setToastText: jest.fn(),
|
||||
rootFile: {
|
||||
id: 'root-file'
|
||||
}
|
||||
},
|
||||
t: jest.fn()
|
||||
};
|
||||
const getWrapper = () => shallow(<NavComponent {...props} />);
|
||||
|
||||
test('it renders main navigation', () => {
|
||||
const nav = getWrapper();
|
||||
expect(nav.exists('.nav')).toEqual(true);
|
||||
});
|
||||
|
||||
it('renders correctly', () => {
|
||||
const tree = renderer
|
||||
.create(<NavComponent {...props} />)
|
||||
.toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
const { asFragment } = render(<NavComponent {...props} />);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,333 +1,219 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Nav renders correctly 1`] = `
|
||||
<nav
|
||||
className="nav"
|
||||
title="main-navigation"
|
||||
>
|
||||
<ul
|
||||
className="nav__items-left"
|
||||
>
|
||||
<li
|
||||
className="nav__item-logo"
|
||||
<DocumentFragment>
|
||||
<header>
|
||||
<nav
|
||||
class="nav"
|
||||
title="main-navigation"
|
||||
>
|
||||
<span
|
||||
className="isvg loading svg__logo"
|
||||
/>
|
||||
</li>
|
||||
<li
|
||||
className="nav__item"
|
||||
>
|
||||
<button
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
>
|
||||
<span
|
||||
className="nav__item-header"
|
||||
>
|
||||
File
|
||||
</span>
|
||||
<span
|
||||
className="isvg loading nav__item-header-triangle"
|
||||
/>
|
||||
</button>
|
||||
<ul
|
||||
className="nav__dropdown"
|
||||
class="nav__items-left"
|
||||
>
|
||||
<li
|
||||
className="nav__dropdown-item"
|
||||
class="nav__item-logo"
|
||||
>
|
||||
<button
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
>
|
||||
New
|
||||
</button>
|
||||
<test-file-stub
|
||||
aria-label="p5.js Logo"
|
||||
classname="svg__logo"
|
||||
focusable="false"
|
||||
role="img"
|
||||
/>
|
||||
</li>
|
||||
<li
|
||||
className="nav__dropdown-item"
|
||||
class="nav__item"
|
||||
>
|
||||
<button
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
>
|
||||
Duplicate
|
||||
<button>
|
||||
<span
|
||||
class="nav__item-header"
|
||||
/>
|
||||
<test-file-stub
|
||||
aria-hidden="true"
|
||||
classname="nav__item-header-triangle"
|
||||
focusable="false"
|
||||
/>
|
||||
</button>
|
||||
<ul
|
||||
class="nav__dropdown"
|
||||
>
|
||||
<li
|
||||
class="nav__dropdown-item"
|
||||
>
|
||||
<button />
|
||||
</li>
|
||||
<li
|
||||
class="nav__dropdown-item"
|
||||
>
|
||||
<button />
|
||||
</li>
|
||||
<li
|
||||
class="nav__dropdown-item"
|
||||
>
|
||||
<button />
|
||||
</li>
|
||||
<li
|
||||
class="nav__dropdown-item"
|
||||
>
|
||||
<button />
|
||||
</li>
|
||||
<li
|
||||
class="nav__dropdown-item"
|
||||
>
|
||||
<a />
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li
|
||||
className="nav__dropdown-item"
|
||||
class="nav__item"
|
||||
>
|
||||
<button
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
>
|
||||
Share
|
||||
<button>
|
||||
<span
|
||||
class="nav__item-header"
|
||||
/>
|
||||
<test-file-stub
|
||||
aria-hidden="true"
|
||||
classname="nav__item-header-triangle"
|
||||
focusable="false"
|
||||
/>
|
||||
</button>
|
||||
<ul
|
||||
class="nav__dropdown"
|
||||
>
|
||||
<li
|
||||
class="nav__dropdown-item"
|
||||
>
|
||||
<button>
|
||||
<span
|
||||
class="nav__keyboard-shortcut"
|
||||
>
|
||||
⇧+Tab
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
class="nav__dropdown-item"
|
||||
>
|
||||
<button>
|
||||
<span
|
||||
class="nav__keyboard-shortcut"
|
||||
>
|
||||
⌃+F
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
class="nav__dropdown-item"
|
||||
>
|
||||
<button>
|
||||
<span
|
||||
class="nav__keyboard-shortcut"
|
||||
>
|
||||
⌃+G
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
class="nav__dropdown-item"
|
||||
>
|
||||
<button>
|
||||
<span
|
||||
class="nav__keyboard-shortcut"
|
||||
>
|
||||
⇧+⌃+G
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li
|
||||
className="nav__dropdown-item"
|
||||
class="nav__item"
|
||||
>
|
||||
<button
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
>
|
||||
Download
|
||||
<button>
|
||||
<span
|
||||
class="nav__item-header"
|
||||
/>
|
||||
<test-file-stub
|
||||
aria-hidden="true"
|
||||
classname="nav__item-header-triangle"
|
||||
focusable="false"
|
||||
/>
|
||||
</button>
|
||||
<ul
|
||||
class="nav__dropdown"
|
||||
>
|
||||
<li
|
||||
class="nav__dropdown-item"
|
||||
>
|
||||
<button />
|
||||
</li>
|
||||
<li
|
||||
class="nav__dropdown-item"
|
||||
>
|
||||
<button />
|
||||
</li>
|
||||
<li
|
||||
class="nav__dropdown-item"
|
||||
>
|
||||
<button>
|
||||
<span
|
||||
class="nav__keyboard-shortcut"
|
||||
>
|
||||
⌃+Enter
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
class="nav__dropdown-item"
|
||||
>
|
||||
<button>
|
||||
<span
|
||||
class="nav__keyboard-shortcut"
|
||||
>
|
||||
⇧+⌃+Enter
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li
|
||||
className="nav__dropdown-item"
|
||||
class="nav__item"
|
||||
>
|
||||
<a
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
style={Object {}}
|
||||
<button>
|
||||
<span
|
||||
class="nav__item-header"
|
||||
/>
|
||||
<test-file-stub
|
||||
aria-hidden="true"
|
||||
classname="nav__item-header-triangle"
|
||||
focusable="false"
|
||||
/>
|
||||
</button>
|
||||
<ul
|
||||
class="nav__dropdown"
|
||||
>
|
||||
Open
|
||||
</a>
|
||||
<li
|
||||
class="nav__dropdown-item"
|
||||
>
|
||||
<button />
|
||||
</li>
|
||||
<li
|
||||
class="nav__dropdown-item"
|
||||
>
|
||||
<a
|
||||
href="https://p5js.org/reference/"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
/>
|
||||
</li>
|
||||
<li
|
||||
class="nav__dropdown-item"
|
||||
>
|
||||
<a />
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li
|
||||
className="nav__item"
|
||||
>
|
||||
<button
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
>
|
||||
<span
|
||||
className="nav__item-header"
|
||||
>
|
||||
Edit
|
||||
</span>
|
||||
<span
|
||||
className="isvg loading nav__item-header-triangle"
|
||||
/>
|
||||
</button>
|
||||
<ul
|
||||
className="nav__dropdown"
|
||||
>
|
||||
<li
|
||||
className="nav__dropdown-item"
|
||||
>
|
||||
<button
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
>
|
||||
Tidy Code
|
||||
<span
|
||||
className="nav__keyboard-shortcut"
|
||||
>
|
||||
⇧
|
||||
+Tab
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
className="nav__dropdown-item"
|
||||
>
|
||||
<button
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
>
|
||||
Find
|
||||
<span
|
||||
className="nav__keyboard-shortcut"
|
||||
>
|
||||
Ctrl
|
||||
+F
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
className="nav__dropdown-item"
|
||||
>
|
||||
<button
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
>
|
||||
Find Next
|
||||
<span
|
||||
className="nav__keyboard-shortcut"
|
||||
>
|
||||
Ctrl
|
||||
+G
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
className="nav__dropdown-item"
|
||||
>
|
||||
<button
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
>
|
||||
Find Previous
|
||||
<span
|
||||
className="nav__keyboard-shortcut"
|
||||
>
|
||||
⇧
|
||||
+
|
||||
Ctrl
|
||||
+G
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li
|
||||
className="nav__item"
|
||||
>
|
||||
<button
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
>
|
||||
<span
|
||||
className="nav__item-header"
|
||||
>
|
||||
Sketch
|
||||
</span>
|
||||
<span
|
||||
className="isvg loading nav__item-header-triangle"
|
||||
/>
|
||||
</button>
|
||||
<ul
|
||||
className="nav__dropdown"
|
||||
>
|
||||
<li
|
||||
className="nav__dropdown-item"
|
||||
>
|
||||
<button
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
>
|
||||
Add File
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
className="nav__dropdown-item"
|
||||
>
|
||||
<button
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
>
|
||||
Add Folder
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
className="nav__dropdown-item"
|
||||
>
|
||||
<button
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
>
|
||||
Run
|
||||
<span
|
||||
className="nav__keyboard-shortcut"
|
||||
>
|
||||
Ctrl
|
||||
+Enter
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
className="nav__dropdown-item"
|
||||
>
|
||||
<button
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
>
|
||||
Stop
|
||||
<span
|
||||
className="nav__keyboard-shortcut"
|
||||
>
|
||||
⇧
|
||||
+
|
||||
Ctrl
|
||||
+Enter
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li
|
||||
className="nav__item"
|
||||
>
|
||||
<button
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
>
|
||||
<span
|
||||
className="nav__item-header"
|
||||
>
|
||||
Help
|
||||
</span>
|
||||
<span
|
||||
className="isvg loading nav__item-header-triangle"
|
||||
/>
|
||||
</button>
|
||||
<ul
|
||||
className="nav__dropdown"
|
||||
>
|
||||
<li
|
||||
className="nav__dropdown-item"
|
||||
>
|
||||
<button
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
>
|
||||
Keyboard Shortcuts
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
className="nav__dropdown-item"
|
||||
>
|
||||
<a
|
||||
href="https://p5js.org/reference/"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Reference
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className="nav__dropdown-item"
|
||||
>
|
||||
<a
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
style={Object {}}
|
||||
>
|
||||
About
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</nav>
|
||||
</header>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
|
35
client/components/mobile/ActionStrip.jsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { remSize } from '../../theme';
|
||||
import IconButton from './IconButton';
|
||||
import { TerminalIcon } from '../../common/icons';
|
||||
import * as IDEActions from '../../modules/IDE/actions/ide';
|
||||
|
||||
const BottomBarContent = styled.h2`
|
||||
padding: ${remSize(8)};
|
||||
|
||||
svg {
|
||||
max-height: ${remSize(32)};
|
||||
}
|
||||
`;
|
||||
|
||||
export default () => {
|
||||
const { expandConsole, collapseConsole } = bindActionCreators(IDEActions, useDispatch());
|
||||
const { consoleIsExpanded } = useSelector(state => state.ide);
|
||||
|
||||
const actions = [{ icon: TerminalIcon, aria: 'Say Something', action: consoleIsExpanded ? collapseConsole : expandConsole }];
|
||||
|
||||
return (
|
||||
<BottomBarContent>
|
||||
{actions.map(({ icon, aria, action }) =>
|
||||
(<IconButton
|
||||
icon={icon}
|
||||
aria-label={aria}
|
||||
key={`bottom-bar-${aria}`}
|
||||
onClick={() => action()}
|
||||
/>))}
|
||||
</BottomBarContent>
|
||||
);
|
||||
};
|
17
client/components/mobile/Footer.jsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { prop, grays } from '../../theme';
|
||||
|
||||
|
||||
const background = prop('MobilePanel.default.background');
|
||||
const textColor = prop('primaryTextColor');
|
||||
|
||||
export default styled.div`
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
background: ${background};
|
||||
color: ${textColor};
|
||||
|
||||
& > * + * { border-top: dashed 1px ${prop('Separator')} }
|
||||
`;
|
79
client/components/mobile/Header.jsx
Normal file
|
@ -0,0 +1,79 @@
|
|||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import PropTypes from 'prop-types';
|
||||
import { prop, remSize } from '../../theme';
|
||||
|
||||
const background = transparent => prop(transparent ? 'backgroundColor' : 'MobilePanel.default.background');
|
||||
const textColor = prop('primaryTextColor');
|
||||
|
||||
|
||||
const HeaderDiv = styled.div`
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
background: ${props => background(props.transparent === true)};
|
||||
color: ${textColor};
|
||||
padding: ${remSize(12)};
|
||||
padding-left: ${remSize(16)};
|
||||
padding-right: ${remSize(16)};
|
||||
z-index: 1;
|
||||
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
||||
svg {
|
||||
max-height: ${remSize(32)};
|
||||
padding: ${remSize(4)}
|
||||
}
|
||||
`;
|
||||
|
||||
const IconContainer = styled.div`
|
||||
margin-left: ${props => (props.leftButton ? remSize(32) : remSize(4))};
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
|
||||
const TitleContainer = styled.div`
|
||||
margin-left: ${remSize(4)};
|
||||
margin-right: auto;
|
||||
|
||||
${props => props.padded && `h2{
|
||||
padding-top: ${remSize(10)};
|
||||
padding-bottom: ${remSize(10)};
|
||||
}`}
|
||||
`;
|
||||
|
||||
const Header = ({
|
||||
title, subtitle, leftButton, children, transparent
|
||||
}) => (
|
||||
<HeaderDiv transparent={transparent}>
|
||||
{leftButton}
|
||||
<TitleContainer padded={subtitle === null}>
|
||||
{title && <h2>{title}</h2>}
|
||||
{subtitle && <h3>{subtitle}</h3>}
|
||||
</TitleContainer>
|
||||
<IconContainer>
|
||||
{children}
|
||||
</IconContainer>
|
||||
</HeaderDiv>
|
||||
);
|
||||
|
||||
Header.propTypes = {
|
||||
title: PropTypes.string,
|
||||
subtitle: PropTypes.string,
|
||||
leftButton: PropTypes.element,
|
||||
children: PropTypes.oneOfType([PropTypes.element, PropTypes.arrayOf(PropTypes.element)]),
|
||||
transparent: PropTypes.bool
|
||||
};
|
||||
|
||||
Header.defaultProps = {
|
||||
title: null,
|
||||
subtitle: null,
|
||||
leftButton: null,
|
||||
children: [],
|
||||
transparent: false
|
||||
};
|
||||
|
||||
export default Header;
|
8
client/components/mobile/IDEWrapper.jsx
Normal file
|
@ -0,0 +1,8 @@
|
|||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { remSize } from '../../theme';
|
||||
|
||||
export default styled.div`
|
||||
z-index: 0;
|
||||
margin-top: ${remSize(16)};
|
||||
`;
|
31
client/components/mobile/IconButton.jsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from 'styled-components';
|
||||
import Button from '../../common/Button';
|
||||
import { remSize } from '../../theme';
|
||||
|
||||
const ButtonWrapper = styled(Button)`
|
||||
width: ${remSize(48)};
|
||||
> svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
const IconButton = (props) => {
|
||||
const { icon, ...otherProps } = props;
|
||||
const Icon = icon;
|
||||
|
||||
return (<ButtonWrapper
|
||||
iconBefore={<Icon />}
|
||||
kind={Button.kinds.inline}
|
||||
focusable="false"
|
||||
{...otherProps}
|
||||
/>);
|
||||
};
|
||||
|
||||
IconButton.propTypes = {
|
||||
icon: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default IconButton;
|
19
client/components/mobile/MobileScreen.jsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const Screen = ({ children, fullscreen }) => (
|
||||
<div className={fullscreen && 'fullscreen-preview'}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
Screen.defaultProps = {
|
||||
fullscreen: false
|
||||
};
|
||||
|
||||
Screen.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
fullscreen: PropTypes.bool
|
||||
};
|
||||
|
||||
export default Screen;
|
67
client/components/mobile/PreferencePicker.jsx
Normal file
|
@ -0,0 +1,67 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from 'styled-components';
|
||||
import { prop, remSize } from '../../theme';
|
||||
|
||||
|
||||
const PreferenceTitle = styled.h4.attrs(props => ({ ...props, className: 'preference__title' }))`
|
||||
color: ${prop('primaryTextColor')};
|
||||
`;
|
||||
|
||||
const Preference = styled.div.attrs(props => ({ ...props, className: 'preference' }))`
|
||||
flex-wrap: nowrap !important;
|
||||
align-items: baseline !important;
|
||||
justify-items: space-between;
|
||||
`;
|
||||
|
||||
const OptionLabel = styled.label.attrs({ className: 'preference__option' })`
|
||||
font-size: ${remSize(14)} !important;
|
||||
`;
|
||||
|
||||
const PreferencePicker = ({
|
||||
title, value, onSelect, options,
|
||||
}) => (
|
||||
<Preference>
|
||||
<PreferenceTitle>{title}</PreferenceTitle>
|
||||
<div className="preference__options">
|
||||
{options.map(option => (
|
||||
<React.Fragment key={`${option.name}-${option.id}`} >
|
||||
<input
|
||||
type="radio"
|
||||
onChange={() => onSelect(option.value)}
|
||||
aria-label={option.ariaLabel}
|
||||
name={option.name}
|
||||
key={`${option.name}-${option.id}-input`}
|
||||
id={option.id}
|
||||
className="preference__radio-button"
|
||||
value={option.value}
|
||||
checked={value === option.value}
|
||||
/>
|
||||
<OptionLabel
|
||||
key={`${option.name}-${option.id}-label`}
|
||||
htmlFor={option.id}
|
||||
>
|
||||
{option.label}
|
||||
</OptionLabel>
|
||||
</React.Fragment>))}
|
||||
</div>
|
||||
</Preference>
|
||||
);
|
||||
|
||||
PreferencePicker.defaultProps = {
|
||||
options: []
|
||||
};
|
||||
|
||||
PreferencePicker.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]).isRequired,
|
||||
options: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
ariaLabel: PropTypes.string,
|
||||
})),
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default PreferencePicker;
|
38
client/i18n.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
import Backend from 'i18next-http-backend';
|
||||
|
||||
const fallbackLng = ['en-US'];
|
||||
const availableLanguages = ['en-US', 'es-419'];
|
||||
|
||||
const options = {
|
||||
loadPath: '/locales/{{lng}}/translations.json',
|
||||
requestOptions: { // used for fetch, can also be a function (payload) => ({ method: 'GET' })
|
||||
mode: 'no-cors'
|
||||
},
|
||||
allowMultiLoading: false, // set loadPath: '/locales/resources.json?lng={{lng}}&ns={{ns}}' to adapt to multiLoading
|
||||
};
|
||||
|
||||
i18n
|
||||
.use(initReactI18next) // pass the i18n instance to react-i18next.
|
||||
.use(LanguageDetector)// to detect the language from currentBrowser
|
||||
.use(Backend) // to fetch the data from server
|
||||
.init({
|
||||
lng: 'en-US',
|
||||
defaultNS: 'WebEditor',
|
||||
fallbackLng, // if user computer language is not on the list of available languages, than we will be using the fallback language specified earlier
|
||||
debug: false,
|
||||
backend: options,
|
||||
getAsync: false,
|
||||
initImmediate: false,
|
||||
useSuspense: true,
|
||||
whitelist: availableLanguages,
|
||||
interpolation: {
|
||||
escapeValue: false, // react already safes from xss
|
||||
},
|
||||
saveMissing: false, // if a key is not found AND this flag is set to true, i18next will call the handler missingKeyHandler
|
||||
missingKeyHandler: false // function(lng, ns, key, fallbackValue) { } custom logic about how to handle the missing keys
|
||||
});
|
||||
|
||||
export default i18n;
|
54
client/images/console-debug-contrast.svg
Normal file
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg4"
|
||||
sodipodi:docname="console-debug-contrast.svg"
|
||||
inkscape:version="0.92.2 2405546, 2018-03-11">
|
||||
<metadata
|
||||
id="metadata10">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs8" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="640"
|
||||
inkscape:window-height="480"
|
||||
id="namedview6"
|
||||
showgrid="false"
|
||||
inkscape:zoom="23.6"
|
||||
inkscape:cx="5"
|
||||
inkscape:cy="5"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg4" />
|
||||
<path
|
||||
d="M14,12H10V10H14M14,16H10V14H14M20,8H17.19C16.74,7.22 16.12,6.55 15.37,6.04L17,4.41L15.59,3L13.42,5.17C12.96,5.06 12.5,5 12,5C11.5,5 11.04,5.06 10.59,5.17L8.41,3L7,4.41L8.62,6.04C7.88,6.55 7.26,7.22 6.81,8H4V10H6.09C6.04,10.33 6,10.66 6,11V12H4V14H6V15C6,15.34 6.04,15.67 6.09,16H4V18H6.81C7.85,19.79 9.78,21 12,21C14.22,21 16.15,19.79 17.19,18H20V16H17.91C17.96,15.67 18,15.34 18,15V14H20V12H18V11C18,10.66 17.96,10.33 17.91,10H20V8Z"
|
||||
id="path2"
|
||||
fill="#38B6F5" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
|
@ -50,5 +50,5 @@
|
|||
<path
|
||||
d="M14,12H10V10H14M14,16H10V14H14M20,8H17.19C16.74,7.22 16.12,6.55 15.37,6.04L17,4.41L15.59,3L13.42,5.17C12.96,5.06 12.5,5 12,5C11.5,5 11.04,5.06 10.59,5.17L8.41,3L7,4.41L8.62,6.04C7.88,6.55 7.26,7.22 6.81,8H4V10H6.09C6.04,10.33 6,10.66 6,11V12H4V14H6V15C6,15.34 6.04,15.67 6.09,16H4V18H6.81C7.85,19.79 9.78,21 12,21C14.22,21 16.15,19.79 17.19,18H20V16H17.91C17.96,15.67 18,15.34 18,15V14H20V12H18V11C18,10.66 17.96,10.33 17.91,10H20V8Z"
|
||||
id="path2"
|
||||
fill="#007BBB" />
|
||||
fill="#0071AD" />
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
54
client/images/console-error-contrast.svg
Normal file
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg4"
|
||||
sodipodi:docname="console-error-contrast.svg"
|
||||
inkscape:version="0.92.2 2405546, 2018-03-11">
|
||||
<metadata
|
||||
id="metadata10">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs8" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="640"
|
||||
inkscape:window-height="480"
|
||||
id="namedview6"
|
||||
showgrid="false"
|
||||
inkscape:zoom="23.6"
|
||||
inkscape:cx="5"
|
||||
inkscape:cy="5"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg4" />
|
||||
<path
|
||||
d="M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z"
|
||||
id="path2"
|
||||
fill="#EA7B7D" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
54
client/images/console-info-contrast.svg
Normal file
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg4"
|
||||
sodipodi:docname="console-info-contrast.svg"
|
||||
inkscape:version="0.92.2 2405546, 2018-03-11">
|
||||
<metadata
|
||||
id="metadata10">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs8" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="640"
|
||||
inkscape:window-height="480"
|
||||
id="namedview6"
|
||||
showgrid="false"
|
||||
inkscape:zoom="23.6"
|
||||
inkscape:cx="5"
|
||||
inkscape:cy="5"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg4" />
|
||||
<path
|
||||
d="M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z"
|
||||
id="path2"
|
||||
fill="#D9D9D9" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
|
@ -50,5 +50,5 @@
|
|||
<path
|
||||
d="M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z"
|
||||
id="path2"
|
||||
fill="#a3a3a3" />
|
||||
fill="#D9D9D9" />
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
@ -50,5 +50,5 @@
|
|||
<path
|
||||
d="M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z"
|
||||
id="path2"
|
||||
fill="#7D7D7D" />
|
||||
fill="#4D4D4D" />
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
54
client/images/console-warn-contrast.svg
Normal file
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
version="1.1"
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg4"
|
||||
sodipodi:docname="console-warn-contrast.svg"
|
||||
inkscape:version="0.92.2 2405546, 2018-03-11">
|
||||
<metadata
|
||||
id="metadata10">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs8" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="640"
|
||||
inkscape:window-height="480"
|
||||
id="namedview6"
|
||||
showgrid="false"
|
||||
inkscape:zoom="23.6"
|
||||
inkscape:cx="5"
|
||||
inkscape:cy="5"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg4" />
|
||||
<path
|
||||
d="M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z"
|
||||
id="path2"
|
||||
fill="#f5bc38" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -50,5 +50,5 @@
|
|||
<path
|
||||
d="M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z"
|
||||
id="path2"
|
||||
fill="#FAAF00" />
|
||||
fill="#996B00" />
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
@ -3,8 +3,8 @@
|
|||
<!-- Generator: Sketch 39 (31667) - http://www.bohemiancoding.com/sketch -->
|
||||
<!-- <desc>Created with Sketch.</desc> -->
|
||||
<defs></defs>
|
||||
<g id="IDEs" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="0.539999962">
|
||||
<g id="p5js-IDE-styles-foundation-pt-2" transform="translate(-425.000000, -1168.000000)" fill="#333333">
|
||||
<g id="IDEs" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="1.0">
|
||||
<g id="p5js-IDE-styles-foundation-pt-2" transform="translate(-425.000000, -1168.000000)" fill="#AAA">
|
||||
<g id="Icons" transform="translate(16.000000, 1063.000000)">
|
||||
<polygon id="arrow-shape-copy-2" transform="translate(416.000000, 109.198314) rotate(-180.000000) translate(-416.000000, -109.198314) " points="417.4 106.396628 423 111.996628 421.6 113.396628 416 107.796628 410.4 113.396628 409 111.996628 414.6 106.396628 415.996628 105"></polygon>
|
||||
</g>
|
||||
|
|
Before Width: | Height: | Size: 979 B After Width: | Height: | Size: 968 B |
|
@ -5,7 +5,7 @@
|
|||
<!-- <desc>Created with Sketch.</desc> -->
|
||||
<defs></defs>
|
||||
<g id="exit" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Artboard-1" fill="#D8D8D8">
|
||||
<g id="Artboard-1" fill="#AAA">
|
||||
<path d="M8,5.87867966 L2.69669914,0.575378798 L0.575378798,2.69669914 L5.87867966,8 L0.575378798,13.3033009 L2.69669914,15.4246212 L8,10.1213203 L13.3033009,15.4246212 L15.4246212,13.3033009 L10.1213203,8 L15.4246212,2.69669914 L13.3033009,0.575378798 L8,5.87867966 Z" id="exit"></path>
|
||||
</g>
|
||||
</g>
|
||||
|
|
Before Width: | Height: | Size: 826 B After Width: | Height: | Size: 823 B |
|
@ -1,47 +1,9 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||
<path style="fill:#FBBB00;" d="M113.47,309.408L95.648,375.94l-65.139,1.378C11.042,341.211,0,299.9,0,256
|
||||
c0-42.451,10.324-82.483,28.624-117.732h0.014l57.992,10.632l25.404,57.644c-5.317,15.501-8.215,32.141-8.215,49.456
|
||||
C103.821,274.792,107.225,292.797,113.47,309.408z"/>
|
||||
<path style="fill:#518EF8;" d="M507.527,208.176C510.467,223.662,512,239.655,512,256c0,18.328-1.927,36.206-5.598,53.451
|
||||
c-12.462,58.683-45.025,109.925-90.134,146.187l-0.014-0.014l-73.044-3.727l-10.338-64.535
|
||||
c29.932-17.554,53.324-45.025,65.646-77.911h-136.89V208.176h138.887L507.527,208.176L507.527,208.176z"/>
|
||||
<path style="fill:#28B446;" d="M416.253,455.624l0.014,0.014C372.396,490.901,316.666,512,256,512
|
||||
c-97.491,0-182.252-54.491-225.491-134.681l82.961-67.91c21.619,57.698,77.278,98.771,142.53,98.771
|
||||
c28.047,0,54.323-7.582,76.87-20.818L416.253,455.624z"/>
|
||||
<path style="fill:#F14336;" d="M419.404,58.936l-82.933,67.896c-23.335-14.586-50.919-23.012-80.471-23.012
|
||||
c-66.729,0-123.429,42.957-143.965,102.724l-83.397-68.276h-0.014C71.23,56.123,157.06,0,256,0
|
||||
C318.115,0,375.068,22.126,419.404,58.936z"/>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
||||
<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g>
|
||||
<path d="M7.091875,19.338 L5.978,23.49625 L1.9068125,23.582375 C0.690125,21.3256875 0,18.74375 0,16 C0,13.3468125 0.64525,10.8448125 1.789,8.64175 L1.789875,8.64175 L5.414375,9.30625 L7.002125,12.909 C6.6698125,13.8778125 6.4886875,14.9178125 6.4886875,16 C6.4888125,17.1745 6.7015625,18.2998125 7.091875,19.338 Z" fill="#FBBB00"></path>
|
||||
<path d="M31.7204375,13.011 C31.9041875,13.978875 32,14.9784375 32,16 C32,17.1455 31.8795625,18.262875 31.650125,19.3406875 C30.87125,23.008375 28.8360625,26.211 26.01675,28.477375 L26.015875,28.4765 L21.450625,28.2435625 L20.8045,24.210125 C22.67525,23.113 24.13725,21.3960625 24.907375,19.3406875 L16.35175,19.3406875 L16.35175,13.011 L25.0321875,13.011 L31.7204375,13.011 Z" fill="#518EF8"></path>
|
||||
<path d="M26.0158125,28.4765 L26.0166875,28.477375 C23.27475,30.6813125 19.791625,32 16,32 C9.9068125,32 4.60925,28.5943125 1.9068125,23.5824375 L7.091875,19.3380625 C8.4430625,22.9441875 11.92175,25.51125 16,25.51125 C17.7529375,25.51125 19.3951875,25.037375 20.804375,24.210125 L26.0158125,28.4765 Z" fill="#28B446"></path>
|
||||
<path d="M26.21275,3.6835 L21.0294375,7.927 C19.571,7.015375 17.847,6.48875 16,6.48875 C11.8294375,6.48875 8.2856875,9.1735625 7.0021875,12.909 L1.789875,8.64175 L1.789,8.64175 C4.451875,3.5076875 9.81625,0 16,0 C19.8821875,0 23.44175,1.382875 26.21275,3.6835 Z" fill="#F14336"></path>
|
||||
</g>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
@ -1,9 +1,8 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="40px" height="30px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
|
||||
<g>
|
||||
<path d="M49.761,67.969l-17.36-30.241h34.561L49.761,67.969z"/>
|
||||
</g>
|
||||
<svg width="10px" height="9px" viewBox="0 0 10 9" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g transform="translate(0.666667, 0.333333)">
|
||||
<polygon points="4.56711429 7.96345714 0.103114286 0.1872 8.99022857 0.1872"></polygon>
|
||||
</g>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 568 B After Width: | Height: | Size: 539 B |
|
@ -1,9 +1,8 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 15.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="40px" height="30px" viewBox="0 0 100 100" style="enable-background:new 0 0 100 100;" xml:space="preserve">
|
||||
<g>
|
||||
<path d="M49.761,37.728l17.36,30.241H32.561L49.761,37.728z"/>
|
||||
</g>
|
||||
<svg width="10px" height="9px" viewBox="0 0 10 9" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g transform="translate(-0.200000, -0.200000)" >
|
||||
<polygon points="4.93361111 0.202222222 9.75583333 8.6025 0.155833333 8.6025"></polygon>
|
||||
</g>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 567 B After Width: | Height: | Size: 543 B |
5
client/images/terminal.svg
Normal file
|
@ -0,0 +1,5 @@
|
|||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="5" y="8" width="22" height="16" rx="2" fill="#333333"/>
|
||||
<path d="M24 21H14V20H24V21Z" fill="#F0F0F0"/>
|
||||
<path d="M10.4081 16.0231L8.3676 18.0637C8.27757 18.1537 8.15754 18.1537 8.06752 18.0637C7.97749 17.9736 7.97749 17.8536 8.06752 17.7636L9.95802 15.8731L8.06752 13.9826C7.97749 13.8926 7.97749 13.7725 8.06752 13.6675C8.15754 13.5775 8.27757 13.5775 8.3676 13.6675L10.4081 15.723C10.4532 15.753 10.4832 15.8131 10.4832 15.8731C10.4832 15.9181 10.4532 15.9781 10.4081 16.0231Z" fill="#F0F0F0"/>
|
||||
</svg>
|
After Width: | Height: | Size: 608 B |
|
@ -3,7 +3,7 @@
|
|||
<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="environment" transform="translate(-26.000000, -42.000000)" fill="#FFF">
|
||||
<g id="libraries" transform="translate(21.000000, 32.000000)">
|
||||
<polygon id="Triangle-3" transform="translate(7.500000, 12.500000) rotate(180.000000) translate(-7.500000, -12.500000) " points="7.5 10 10 15 5 15"></polygon>
|
||||
</g>
|
||||
|
|
Before Width: | Height: | Size: 698 B After Width: | Height: | Size: 695 B |
|
@ -3,7 +3,7 @@
|
|||
<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="environment" transform="translate(-26.000000, -42.000000)" fill="#000000">
|
||||
<g id="libraries" transform="translate(21.000000, 32.000000)">
|
||||
<polygon id="Triangle-3" transform="translate(7.500000, 12.500000) rotate(180.000000) translate(-7.500000, -12.500000) " points="7.5 10 10 15 5 15"></polygon>
|
||||
</g>
|
||||
|
|
Before Width: | Height: | Size: 698 B After Width: | Height: | Size: 698 B |
|
@ -3,7 +3,7 @@
|
|||
<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="environment" transform="translate(-26.000000, -42.000000)" fill="#000000">
|
||||
<g id="libraries" transform="translate(21.000000, 32.000000)">
|
||||
<polygon id="Triangle-3" transform="translate(7.500000, 12.500000) rotate(90.000000) translate(-7.500000, -12.500000) " points="7.5 10 10 15 5 15"></polygon>
|
||||
</g>
|
||||
|
|
Before Width: | Height: | Size: 698 B After Width: | Height: | Size: 698 B |
|
@ -3,8 +3,8 @@
|
|||
<!-- Generator: Sketch 39 (31667) - http://www.bohemiancoding.com/sketch -->
|
||||
<!-- <desc>Created with Sketch.</desc> -->
|
||||
<defs></defs>
|
||||
<g id="IDEs" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="0.539999962">
|
||||
<g id="p5js-IDE-styles-foundation-pt-2" transform="translate(-394.000000, -1168.000000)" fill="#333333">
|
||||
<g id="IDEs" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" opacity="1">
|
||||
<g id="p5js-IDE-styles-foundation-pt-2" transform="translate(-394.000000, -1168.000000)" fill="#AAA">
|
||||
<g id="Icons" transform="translate(16.000000, 1063.000000)">
|
||||
<polygon id="arrow-shape-copy" points="386.4 106.396628 392 111.996628 390.6 113.396628 385 107.796628 379.4 113.396628 378 111.996628 383.6 106.396628 384.996628 105"></polygon>
|
||||
</g>
|
||||
|
|
Before Width: | Height: | Size: 873 B After Width: | Height: | Size: 860 B |
|
@ -1,10 +1,14 @@
|
|||
import React from 'react';
|
||||
import React, { Suspense } from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { hot } from 'react-hot-loader/root';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Router, browserHistory } from 'react-router';
|
||||
|
||||
import configureStore from './store';
|
||||
import routes from './routes';
|
||||
import ThemeProvider from './modules/App/components/ThemeProvider';
|
||||
import Loader from './modules/App/components/loader';
|
||||
import i18n from './i18n';
|
||||
|
||||
require('./styles/main.scss');
|
||||
|
||||
|
@ -18,13 +22,17 @@ const store = configureStore(initialState);
|
|||
|
||||
const App = () => (
|
||||
<Provider store={store}>
|
||||
<Router history={history} routes={routes(store)} />
|
||||
<ThemeProvider>
|
||||
<Router history={history} routes={routes(store)} />
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
);
|
||||
|
||||
const HotApp = hot(App);
|
||||
|
||||
render(
|
||||
<HotApp />,
|
||||
<Suspense fallback={(<Loader />)}>
|
||||
<HotApp />
|
||||
</Suspense>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
|
7
client/index.stories.mdx
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { Meta } from '@storybook/addon-docs/blocks';
|
||||
|
||||
<Meta title=" |Intro" />
|
||||
|
||||
# Welcome to the P5.js Web Editor Style Guide
|
||||
|
||||
This guide will contain all the components in the project, with examples of how they can be reused.
|
5
client/jest.setup.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
import '@babel/polyfill';
|
||||
|
||||
// See: https://github.com/testing-library/jest-dom
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import '@testing-library/jest-dom';
|
|
@ -1,11 +1,10 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import getConfig from '../../utils/getConfig';
|
||||
import DevTools from './components/DevTools';
|
||||
import { setPreviousPath } from '../IDE/actions/ide';
|
||||
|
||||
const __process = (typeof global !== 'undefined' ? global : window).process;
|
||||
|
||||
class App extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
@ -35,7 +34,7 @@ class App extends React.Component {
|
|||
render() {
|
||||
return (
|
||||
<div className="app">
|
||||
{this.state.isMounted && !window.devToolsExtension && __process.env.NODE_ENV === 'development' && <DevTools />}
|
||||
{this.state.isMounted && !window.devToolsExtension && getConfig('NODE_ENV') === 'development' && <DevTools />}
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import InlineSVG from 'react-inlinesvg';
|
||||
import { browserHistory } from 'react-router';
|
||||
|
||||
const exitUrl = require('../../../images/exit.svg');
|
||||
import ExitIcon from '../../../images/exit.svg';
|
||||
|
||||
class Overlay extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -81,8 +80,8 @@ class Overlay extends React.Component {
|
|||
<h2 className="overlay__title">{title}</h2>
|
||||
<div className="overlay__actions">
|
||||
{actions}
|
||||
<button className="overlay__close-button" onClick={this.close} >
|
||||
<InlineSVG src={exitUrl} alt="close overlay" />
|
||||
<button className="overlay__close-button" onClick={this.close} aria-label={`Close ${title} overlay`} >
|
||||
<ExitIcon focusable="false" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
|
26
client/modules/App/components/ThemeProvider.jsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
|
||||
import theme, { Theme } from '../../../theme';
|
||||
|
||||
const Provider = ({ children, currentTheme }) => (
|
||||
<ThemeProvider theme={{ ...theme[currentTheme] }}>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
Provider.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
currentTheme: PropTypes.oneOf(Object.keys(Theme)).isRequired,
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
currentTheme: state.preferences.theme,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export default connect(mapStateToProps)(Provider);
|
|
@ -1,10 +1,7 @@
|
|||
import axios from 'axios';
|
||||
import apiClient from '../../../utils/apiClient';
|
||||
import * as ActionTypes from '../../../constants';
|
||||
import { startLoader, stopLoader } from './loader';
|
||||
|
||||
const __process = (typeof global !== 'undefined' ? global : window).process;
|
||||
const ROOT_URL = __process.env.API_URL;
|
||||
|
||||
function setAssets(assets, totalSize) {
|
||||
return {
|
||||
type: ActionTypes.SET_ASSETS,
|
||||
|
@ -16,7 +13,7 @@ function setAssets(assets, totalSize) {
|
|||
export function getAssets() {
|
||||
return (dispatch) => {
|
||||
dispatch(startLoader());
|
||||
axios.get(`${ROOT_URL}/S3/objects`, { withCredentials: true })
|
||||
apiClient.get('/S3/objects')
|
||||
.then((response) => {
|
||||
dispatch(setAssets(response.data.assets, response.data.totalSize));
|
||||
dispatch(stopLoader());
|
||||
|
@ -39,7 +36,7 @@ export function deleteAsset(assetKey) {
|
|||
|
||||
export function deleteAssetRequest(assetKey) {
|
||||
return (dispatch) => {
|
||||
axios.delete(`${ROOT_URL}/S3/${assetKey}`, { withCredentials: true })
|
||||
apiClient.delete(`/S3/${assetKey}`)
|
||||
.then((response) => {
|
||||
dispatch(deleteAsset(assetKey));
|
||||
})
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import axios from 'axios';
|
||||
import { browserHistory } from 'react-router';
|
||||
import apiClient from '../../../utils/apiClient';
|
||||
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;
|
||||
|
||||
|
@ -15,11 +13,11 @@ export function getCollections(username) {
|
|||
dispatch(startLoader());
|
||||
let url;
|
||||
if (username) {
|
||||
url = `${ROOT_URL}/${username}/collections`;
|
||||
url = `/${username}/collections`;
|
||||
} else {
|
||||
url = `${ROOT_URL}/collections`;
|
||||
url = '/collections';
|
||||
}
|
||||
axios.get(url, { withCredentials: true })
|
||||
apiClient.get(url)
|
||||
.then((response) => {
|
||||
dispatch({
|
||||
type: ActionTypes.SET_COLLECTIONS,
|
||||
|
@ -27,7 +25,8 @@ export function getCollections(username) {
|
|||
});
|
||||
dispatch(stopLoader());
|
||||
})
|
||||
.catch((response) => {
|
||||
.catch((error) => {
|
||||
const { response } = error;
|
||||
dispatch({
|
||||
type: ActionTypes.ERROR,
|
||||
error: response.data
|
||||
|
@ -40,8 +39,8 @@ export function getCollections(username) {
|
|||
export function createCollection(collection) {
|
||||
return (dispatch) => {
|
||||
dispatch(startLoader());
|
||||
const url = `${ROOT_URL}/collections`;
|
||||
return axios.post(url, collection, { withCredentials: true })
|
||||
const url = '/collections';
|
||||
return apiClient.post(url, collection)
|
||||
.then((response) => {
|
||||
dispatch({
|
||||
type: ActionTypes.CREATE_COLLECTION
|
||||
|
@ -57,7 +56,8 @@ export function createCollection(collection) {
|
|||
|
||||
browserHistory.push(location);
|
||||
})
|
||||
.catch((response) => {
|
||||
.catch((error) => {
|
||||
const { response } = error;
|
||||
console.error('Error creating collection', response.data);
|
||||
dispatch({
|
||||
type: ActionTypes.ERROR,
|
||||
|
@ -71,8 +71,8 @@ export function createCollection(collection) {
|
|||
export function addToCollection(collectionId, projectId) {
|
||||
return (dispatch) => {
|
||||
dispatch(startLoader());
|
||||
const url = `${ROOT_URL}/collections/${collectionId}/${projectId}`;
|
||||
return axios.post(url, { withCredentials: true })
|
||||
const url = `/collections/${collectionId}/${projectId}`;
|
||||
return apiClient.post(url)
|
||||
.then((response) => {
|
||||
dispatch({
|
||||
type: ActionTypes.ADD_TO_COLLECTION,
|
||||
|
@ -87,7 +87,8 @@ export function addToCollection(collectionId, projectId) {
|
|||
|
||||
return response.data;
|
||||
})
|
||||
.catch((response) => {
|
||||
.catch((error) => {
|
||||
const { response } = error;
|
||||
dispatch({
|
||||
type: ActionTypes.ERROR,
|
||||
error: response.data
|
||||
|
@ -102,8 +103,8 @@ export function addToCollection(collectionId, projectId) {
|
|||
export function removeFromCollection(collectionId, projectId) {
|
||||
return (dispatch) => {
|
||||
dispatch(startLoader());
|
||||
const url = `${ROOT_URL}/collections/${collectionId}/${projectId}`;
|
||||
return axios.delete(url, { withCredentials: true })
|
||||
const url = `/collections/${collectionId}/${projectId}`;
|
||||
return apiClient.delete(url)
|
||||
.then((response) => {
|
||||
dispatch({
|
||||
type: ActionTypes.REMOVE_FROM_COLLECTION,
|
||||
|
@ -118,7 +119,8 @@ export function removeFromCollection(collectionId, projectId) {
|
|||
|
||||
return response.data;
|
||||
})
|
||||
.catch((response) => {
|
||||
.catch((error) => {
|
||||
const { response } = error;
|
||||
dispatch({
|
||||
type: ActionTypes.ERROR,
|
||||
error: response.data
|
||||
|
@ -132,8 +134,8 @@ export function removeFromCollection(collectionId, projectId) {
|
|||
|
||||
export function editCollection(collectionId, { name, description }) {
|
||||
return (dispatch) => {
|
||||
const url = `${ROOT_URL}/collections/${collectionId}`;
|
||||
return axios.patch(url, { name, description }, { withCredentials: true })
|
||||
const url = `/collections/${collectionId}`;
|
||||
return apiClient.patch(url, { name, description })
|
||||
.then((response) => {
|
||||
dispatch({
|
||||
type: ActionTypes.EDIT_COLLECTION,
|
||||
|
@ -141,7 +143,8 @@ export function editCollection(collectionId, { name, description }) {
|
|||
});
|
||||
return response.data;
|
||||
})
|
||||
.catch((response) => {
|
||||
.catch((error) => {
|
||||
const { response } = error;
|
||||
dispatch({
|
||||
type: ActionTypes.ERROR,
|
||||
error: response.data
|
||||
|
@ -154,8 +157,8 @@ export function editCollection(collectionId, { name, description }) {
|
|||
|
||||
export function deleteCollection(collectionId) {
|
||||
return (dispatch) => {
|
||||
const url = `${ROOT_URL}/collections/${collectionId}`;
|
||||
return axios.delete(url, { withCredentials: true })
|
||||
const url = `/collections/${collectionId}`;
|
||||
return apiClient.delete(url)
|
||||
.then((response) => {
|
||||
dispatch({
|
||||
type: ActionTypes.DELETE_COLLECTION,
|
||||
|
@ -164,7 +167,8 @@ export function deleteCollection(collectionId) {
|
|||
});
|
||||
return response.data;
|
||||
})
|
||||
.catch((response) => {
|
||||
.catch((error) => {
|
||||
const { response } = error;
|
||||
dispatch({
|
||||
type: ActionTypes.ERROR,
|
||||
error: response.data
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import axios from 'axios';
|
||||
import objectID from 'bson-objectid';
|
||||
import blobUtil from 'blob-util';
|
||||
import { reset } from 'redux-form';
|
||||
import apiClient from '../../../utils/apiClient';
|
||||
import * as ActionTypes from '../../../constants';
|
||||
import { setUnsavedChanges, closeNewFolderModal, closeNewFileModal } from './ide';
|
||||
import { setProjectSavedTime } from './project';
|
||||
|
||||
const __process = (typeof global !== 'undefined' ? global : window).process;
|
||||
const ROOT_URL = __process.env.API_URL;
|
||||
|
||||
function appendToFilename(filename, string) {
|
||||
const dotIndex = filename.lastIndexOf('.');
|
||||
|
@ -50,7 +48,7 @@ export function createFile(formProps) {
|
|||
parentId,
|
||||
children: []
|
||||
};
|
||||
axios.post(`${ROOT_URL}/projects/${state.project.id}/files`, postParams, { withCredentials: true })
|
||||
apiClient.post(`/projects/${state.project.id}/files`, postParams)
|
||||
.then((response) => {
|
||||
dispatch({
|
||||
type: ActionTypes.CREATE_FILE,
|
||||
|
@ -65,10 +63,13 @@ export function createFile(formProps) {
|
|||
// });
|
||||
dispatch(setUnsavedChanges(true));
|
||||
})
|
||||
.catch(response => dispatch({
|
||||
type: ActionTypes.ERROR,
|
||||
error: response.data
|
||||
}));
|
||||
.catch((error) => {
|
||||
const { response } = error;
|
||||
dispatch({
|
||||
type: ActionTypes.ERROR,
|
||||
error: response.data
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const id = objectID().toHexString();
|
||||
dispatch({
|
||||
|
@ -103,7 +104,7 @@ export function createFolder(formProps) {
|
|||
parentId,
|
||||
fileType: 'folder'
|
||||
};
|
||||
axios.post(`${ROOT_URL}/projects/${state.project.id}/files`, postParams, { withCredentials: true })
|
||||
apiClient.post(`/projects/${state.project.id}/files`, postParams)
|
||||
.then((response) => {
|
||||
dispatch({
|
||||
type: ActionTypes.CREATE_FILE,
|
||||
|
@ -113,10 +114,13 @@ export function createFolder(formProps) {
|
|||
dispatch(setProjectSavedTime(response.data.project.updatedAt));
|
||||
dispatch(closeNewFolderModal());
|
||||
})
|
||||
.catch(response => dispatch({
|
||||
type: ActionTypes.ERROR,
|
||||
error: response.data
|
||||
}));
|
||||
.catch((error) => {
|
||||
const { response } = error;
|
||||
dispatch({
|
||||
type: ActionTypes.ERROR,
|
||||
error: response.data
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const id = objectID().toHexString();
|
||||
dispatch({
|
||||
|
@ -155,7 +159,7 @@ export function deleteFile(id, parentId) {
|
|||
parentId
|
||||
}
|
||||
};
|
||||
axios.delete(`${ROOT_URL}/projects/${state.project.id}/files/${id}`, deleteConfig, { withCredentials: true })
|
||||
apiClient.delete(`/projects/${state.project.id}/files/${id}`, deleteConfig)
|
||||
.then(() => {
|
||||
dispatch({
|
||||
type: ActionTypes.DELETE_FILE,
|
||||
|
@ -163,7 +167,8 @@ export function deleteFile(id, parentId) {
|
|||
parentId
|
||||
});
|
||||
})
|
||||
.catch((response) => {
|
||||
.catch((error) => {
|
||||
const { response } = error;
|
||||
dispatch({
|
||||
type: ActionTypes.ERROR,
|
||||
error: response.data
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import axios from 'axios';
|
||||
import apiClient from '../../../utils/apiClient';
|
||||
import * as ActionTypes from '../../../constants';
|
||||
|
||||
const __process = (typeof global !== 'undefined' ? global : window).process;
|
||||
const ROOT_URL = __process.env.API_URL;
|
||||
|
||||
function updatePreferences(formParams, dispatch) {
|
||||
axios.put(`${ROOT_URL}/preferences`, formParams, { withCredentials: true })
|
||||
apiClient.put('/preferences', formParams)
|
||||
.then(() => {
|
||||
})
|
||||
.catch(response => dispatch({
|
||||
type: ActionTypes.ERROR,
|
||||
error: response.data
|
||||
}));
|
||||
.catch((error) => {
|
||||
const { response } = error;
|
||||
dispatch({
|
||||
type: ActionTypes.ERROR,
|
||||
error: response.data
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function setFontSize(value) {
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { browserHistory } from 'react-router';
|
||||
import axios from 'axios';
|
||||
import objectID from 'bson-objectid';
|
||||
import each from 'async/each';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import apiClient from '../../../utils/apiClient';
|
||||
import getConfig from '../../../utils/getConfig';
|
||||
import * as ActionTypes from '../../../constants';
|
||||
import { showToast, setToastText } from './toast';
|
||||
import {
|
||||
|
@ -14,8 +15,9 @@ import {
|
|||
} from './ide';
|
||||
import { clearState, saveState } from '../../../persistState';
|
||||
|
||||
const __process = (typeof global !== 'undefined' ? global : window).process;
|
||||
const ROOT_URL = __process.env.API_URL;
|
||||
const ROOT_URL = getConfig('API_URL');
|
||||
const S3_BUCKET_URL_BASE = getConfig('S3_BUCKET_URL_BASE');
|
||||
const S3_BUCKET = getConfig('S3_BUCKET');
|
||||
|
||||
export function setProject(project) {
|
||||
return {
|
||||
|
@ -49,18 +51,21 @@ export function setNewProject(project) {
|
|||
};
|
||||
}
|
||||
|
||||
export function getProject(id) {
|
||||
export function getProject(id, username) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(justOpenedProject());
|
||||
axios.get(`${ROOT_URL}/projects/${id}`, { withCredentials: true })
|
||||
apiClient.get(`/${username}/projects/${id}`)
|
||||
.then((response) => {
|
||||
dispatch(setProject(response.data));
|
||||
dispatch(setUnsavedChanges(false));
|
||||
})
|
||||
.catch(response => dispatch({
|
||||
type: ActionTypes.ERROR,
|
||||
error: response.data
|
||||
}));
|
||||
.catch((error) => {
|
||||
const { response } = error;
|
||||
dispatch({
|
||||
type: ActionTypes.ERROR,
|
||||
error: response.data
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -139,7 +144,7 @@ export function saveProject(selectedFile = null, autosave = false) {
|
|||
fileToUpdate.content = selectedFile.content;
|
||||
}
|
||||
if (state.project.id) {
|
||||
return axios.put(`${ROOT_URL}/projects/${state.project.id}`, formParams, { withCredentials: true })
|
||||
return apiClient.put(`/projects/${state.project.id}`, formParams)
|
||||
.then((response) => {
|
||||
dispatch(endSavingProject());
|
||||
dispatch(setUnsavedChanges(false));
|
||||
|
@ -152,17 +157,20 @@ export function saveProject(selectedFile = null, autosave = false) {
|
|||
if (!autosave) {
|
||||
if (state.ide.justOpenedProject && state.preferences.autosave) {
|
||||
dispatch(showToast(5500));
|
||||
dispatch(setToastText('Project saved.'));
|
||||
dispatch(setToastText('Sketch saved.'));
|
||||
setTimeout(() => dispatch(setToastText('Autosave enabled.')), 1500);
|
||||
dispatch(resetJustOpenedProject());
|
||||
} else {
|
||||
dispatch(showToast(1500));
|
||||
dispatch(setToastText('Project saved.'));
|
||||
dispatch(setToastText('Sketch saved.'));
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((response) => {
|
||||
.catch((error) => {
|
||||
const { response } = error;
|
||||
dispatch(endSavingProject());
|
||||
dispatch(setToastText('Failed to save sketch.'));
|
||||
dispatch(showToast(1500));
|
||||
if (response.status === 403) {
|
||||
dispatch(showErrorModal('staleSession'));
|
||||
} else if (response.status === 409) {
|
||||
|
@ -173,7 +181,7 @@ export function saveProject(selectedFile = null, autosave = false) {
|
|||
});
|
||||
}
|
||||
|
||||
return axios.post(`${ROOT_URL}/projects`, formParams, { withCredentials: true })
|
||||
return apiClient.post('/projects', formParams)
|
||||
.then((response) => {
|
||||
dispatch(endSavingProject());
|
||||
const { hasChanges, synchedProject } = getSynchedProject(getState(), response.data);
|
||||
|
@ -191,17 +199,20 @@ export function saveProject(selectedFile = null, autosave = false) {
|
|||
if (!autosave) {
|
||||
if (state.preferences.autosave) {
|
||||
dispatch(showToast(5500));
|
||||
dispatch(setToastText('Project saved.'));
|
||||
dispatch(setToastText('Sketch saved.'));
|
||||
setTimeout(() => dispatch(setToastText('Autosave enabled.')), 1500);
|
||||
dispatch(resetJustOpenedProject());
|
||||
} else {
|
||||
dispatch(showToast(1500));
|
||||
dispatch(setToastText('Project saved.'));
|
||||
dispatch(setToastText('Sketch saved.'));
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((response) => {
|
||||
.catch((error) => {
|
||||
const { response } = error;
|
||||
dispatch(endSavingProject());
|
||||
dispatch(setToastText('Failed to save sketch.'));
|
||||
dispatch(showToast(1500));
|
||||
if (response.status === 403) {
|
||||
dispatch(showErrorModal('staleSession'));
|
||||
} else {
|
||||
|
@ -255,7 +266,7 @@ export function cloneProject(id) {
|
|||
if (!id) {
|
||||
resolve(getState());
|
||||
} else {
|
||||
fetch(`${ROOT_URL}/projects/${id}`)
|
||||
apiClient.get(`/projects/${id}`)
|
||||
.then(res => res.json())
|
||||
.then(data => resolve({
|
||||
files: data.files,
|
||||
|
@ -278,11 +289,11 @@ export function cloneProject(id) {
|
|||
|
||||
// duplicate all files hosted on S3
|
||||
each(newFiles, (file, callback) => {
|
||||
if (file.url && file.url.includes('amazonaws')) {
|
||||
if (file.url && (file.url.includes(S3_BUCKET_URL_BASE) || file.url.includes(S3_BUCKET))) {
|
||||
const formParams = {
|
||||
url: file.url
|
||||
};
|
||||
axios.post(`${ROOT_URL}/S3/copy`, formParams, { withCredentials: true })
|
||||
apiClient.post('/S3/copy', formParams)
|
||||
.then((response) => {
|
||||
file.url = response.data.url;
|
||||
callback(null);
|
||||
|
@ -293,15 +304,18 @@ export function cloneProject(id) {
|
|||
}, (err) => {
|
||||
// if not errors in duplicating the files on S3, then duplicate it
|
||||
const formParams = Object.assign({}, { name: `${state.project.name} copy` }, { files: newFiles });
|
||||
axios.post(`${ROOT_URL}/projects`, formParams, { withCredentials: true })
|
||||
apiClient.post('/projects', formParams)
|
||||
.then((response) => {
|
||||
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
|
||||
dispatch(setNewProject(response.data));
|
||||
})
|
||||
.catch(response => dispatch({
|
||||
type: ActionTypes.PROJECT_SAVE_FAIL,
|
||||
error: response.data
|
||||
}));
|
||||
.catch((error) => {
|
||||
const { response } = error;
|
||||
dispatch({
|
||||
type: ActionTypes.PROJECT_SAVE_FAIL,
|
||||
error: response.data
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
@ -329,7 +343,7 @@ export function setProjectSavedTime(updatedAt) {
|
|||
export function changeProjectName(id, newName) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
axios.put(`${ROOT_URL}/projects/${id}`, { name: newName }, { withCredentials: true })
|
||||
apiClient.put(`/projects/${id}`, { name: newName })
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
dispatch({
|
||||
|
@ -344,8 +358,8 @@ export function changeProjectName(id, newName) {
|
|||
}
|
||||
}
|
||||
})
|
||||
.catch((response) => {
|
||||
console.log(response);
|
||||
.catch((error) => {
|
||||
const { response } = error;
|
||||
dispatch({
|
||||
type: ActionTypes.PROJECT_SAVE_FAIL,
|
||||
error: response.data
|
||||
|
@ -356,7 +370,7 @@ export function changeProjectName(id, newName) {
|
|||
|
||||
export function deleteProject(id) {
|
||||
return (dispatch, getState) => {
|
||||
axios.delete(`${ROOT_URL}/projects/${id}`, { withCredentials: true })
|
||||
apiClient.delete(`/projects/${id}`)
|
||||
.then(() => {
|
||||
const state = getState();
|
||||
if (id === state.project.id) {
|
||||
|
@ -368,7 +382,8 @@ export function deleteProject(id) {
|
|||
id
|
||||
});
|
||||
})
|
||||
.catch((response) => {
|
||||
.catch((error) => {
|
||||
const { response } = error;
|
||||
if (response.status === 403) {
|
||||
dispatch(showErrorModal('staleSession'));
|
||||
} else {
|
||||
|
|
|
@ -1,21 +1,18 @@
|
|||
import axios from 'axios';
|
||||
import apiClient from '../../../utils/apiClient';
|
||||
import * as ActionTypes from '../../../constants';
|
||||
import { startLoader, stopLoader } from './loader';
|
||||
|
||||
const __process = (typeof global !== 'undefined' ? global : window).process;
|
||||
const ROOT_URL = __process.env.API_URL;
|
||||
|
||||
// eslint-disable-next-line
|
||||
export function getProjects(username) {
|
||||
return (dispatch) => {
|
||||
dispatch(startLoader());
|
||||
let url;
|
||||
if (username) {
|
||||
url = `${ROOT_URL}/${username}/projects`;
|
||||
url = `/${username}/projects`;
|
||||
} else {
|
||||
url = `${ROOT_URL}/projects`;
|
||||
url = '/projects';
|
||||
}
|
||||
axios.get(url, { withCredentials: true })
|
||||
apiClient.get(url)
|
||||
.then((response) => {
|
||||
dispatch({
|
||||
type: ActionTypes.SET_PROJECTS,
|
||||
|
@ -23,7 +20,8 @@ export function getProjects(username) {
|
|||
});
|
||||
dispatch(stopLoader());
|
||||
})
|
||||
.catch((response) => {
|
||||
.catch((error) => {
|
||||
const { response } = error;
|
||||
dispatch({
|
||||
type: ActionTypes.ERROR,
|
||||
error: response.data
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import axios from 'axios';
|
||||
import apiClient from '../../../utils/apiClient';
|
||||
import getConfig from '../../../utils/getConfig';
|
||||
import { createFile } from './files';
|
||||
import { TEXT_FILE_REGEX } from '../../../../server/utils/fileUtils';
|
||||
|
||||
const __process = (typeof global !== 'undefined' ? global : window).process;
|
||||
const s3BucketHttps = __process.env.S3_BUCKET_URL_BASE ||
|
||||
`https://s3-${__process.env.AWS_REGION}.amazonaws.com/${__process.env.S3_BUCKET}/`;
|
||||
const ROOT_URL = __process.env.API_URL;
|
||||
const s3BucketHttps = getConfig('S3_BUCKET_URL_BASE') ||
|
||||
`https://s3-${getConfig('AWS_REGION')}.amazonaws.com/${getConfig('S3_BUCKET')}/`;
|
||||
const MAX_LOCAL_FILE_SIZE = 80000; // bytes, aka 80 KB
|
||||
|
||||
function localIntercept(file, options = {}) {
|
||||
|
@ -46,18 +45,13 @@ export function dropzoneAcceptCallback(userId, file, done) {
|
|||
});
|
||||
} else {
|
||||
file.postData = []; // eslint-disable-line
|
||||
axios.post(
|
||||
`${ROOT_URL}/S3/sign`, {
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
size: file.size,
|
||||
userId
|
||||
apiClient.post('/S3/sign', {
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
size: file.size,
|
||||
userId
|
||||
// _csrf: document.getElementById('__createPostToken').value
|
||||
},
|
||||
{
|
||||
withCredentials: true
|
||||
}
|
||||
)
|
||||
})
|
||||
.then((response) => {
|
||||
file.custom_status = 'ready'; // eslint-disable-line
|
||||
file.postData = response.data; // eslint-disable-line
|
||||
|
@ -65,7 +59,8 @@ export function dropzoneAcceptCallback(userId, file, done) {
|
|||
file.previewTemplate.className += ' uploading'; // eslint-disable-line
|
||||
done();
|
||||
})
|
||||
.catch((response) => {
|
||||
.catch((error) => {
|
||||
const { response } = error;
|
||||
file.custom_status = 'rejected'; // eslint-disable-line
|
||||
if (response.data && response.data.responseText && response.data.responseText.message) {
|
||||
done(response.data.responseText.message);
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
import React from 'react';
|
||||
import InlineSVG from 'react-inlinesvg';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
const squareLogoUrl = require('../../../images/p5js-square-logo.svg');
|
||||
// const playUrl = require('../../../images/play.svg');
|
||||
const asteriskUrl = require('../../../images/p5-asterisk.svg');
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import SquareLogoIcon from '../../../images/p5js-square-logo.svg';
|
||||
// import PlayIcon from '../../../images/play.svg';
|
||||
import AsteriskIcon from '../../../images/p5-asterisk.svg';
|
||||
|
||||
function About(props) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="about__content">
|
||||
<Helmet>
|
||||
<title>p5.js Web Editor | About</title>
|
||||
<title>p5.js Web Editor | About </title>
|
||||
</Helmet>
|
||||
<div className="about__content-column">
|
||||
<InlineSVG className="about__logo" src={squareLogoUrl} alt="p5js Square Logo" />
|
||||
<SquareLogoIcon className="about__logo" role="img" aria-label="p5.js Logo" focusable="false" />
|
||||
{/* Video button to hello p5 video page */}
|
||||
{/* <p className="about__play-video">
|
||||
<a
|
||||
|
@ -21,20 +21,20 @@ function About(props) {
|
|||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<InlineSVG className="about__play-video-button" src={playUrl} alt="Play Hello Video" />
|
||||
<PlayIcon className="about__play-video-button" title="Play Hello Video" />
|
||||
Play hello! video</a>
|
||||
</p> */}
|
||||
</div>
|
||||
<div className="about__content-column">
|
||||
<h3 className="about__content-column-title">New to p5.js?</h3>
|
||||
<h3 className="about__content-column-title">{t('NewP5')}</h3>
|
||||
<p className="about__content-column-list">
|
||||
<a
|
||||
href="https://p5js.org/examples/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" />
|
||||
Examples
|
||||
<AsteriskIcon className="about__content-column-asterisk" aria-hidden="true" focusable="false" />
|
||||
{t('Examples')}
|
||||
</a>
|
||||
</p>
|
||||
<p className="about__content-column-list">
|
||||
|
@ -43,21 +43,21 @@ function About(props) {
|
|||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" />
|
||||
Learn
|
||||
<AsteriskIcon className="about__content-column-asterisk" aria-hidden="true" focusable="false" />
|
||||
{t('Learn')}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div className="about__content-column">
|
||||
<h3 className="about__content-column-title">Resources</h3>
|
||||
<h3 className="about__content-column-title">{t('Resources')}</h3>
|
||||
<p className="about__content-column-list">
|
||||
<a
|
||||
href="https://p5js.org/libraries/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" />
|
||||
Libraries
|
||||
<AsteriskIcon className="about__content-column-asterisk" aria-hidden="true" focusable="false" />
|
||||
{t('Libraries')}
|
||||
</a>
|
||||
</p>
|
||||
<p className="about__content-column-list">
|
||||
|
@ -66,8 +66,8 @@ function About(props) {
|
|||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" />
|
||||
Reference
|
||||
<AsteriskIcon className="about__content-column-asterisk" aria-hidden="true" focusable="false" />
|
||||
{t('Reference')}
|
||||
</a>
|
||||
</p>
|
||||
<p className="about__content-column-list">
|
||||
|
@ -76,8 +76,8 @@ function About(props) {
|
|||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<InlineSVG className="about__content-column-asterisk" src={asteriskUrl} alt="p5 asterisk" />
|
||||
Forum
|
||||
<AsteriskIcon className="about__content-column-asterisk" aria-hidden="true" focusable="false" />
|
||||
{t('Forum')}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
@ -87,7 +87,7 @@ function About(props) {
|
|||
href="https://github.com/processing/p5.js-web-editor"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>Contribute
|
||||
>{t('Contribute')}
|
||||
</a>
|
||||
</p>
|
||||
<p className="about__footer-list">
|
||||
|
@ -95,7 +95,7 @@ function About(props) {
|
|||
href="https://github.com/processing/p5.js-web-editor/issues/new"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>Report a bug
|
||||
>{t('Report')}
|
||||
</a>
|
||||
</p>
|
||||
<p className="about__footer-list">
|
||||
|
|
|
@ -5,11 +5,10 @@ import { bindActionCreators } from 'redux';
|
|||
import { Link } from 'react-router';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import prettyBytes from 'pretty-bytes';
|
||||
import InlineSVG from 'react-inlinesvg';
|
||||
|
||||
import Loader from '../../App/components/loader';
|
||||
import * as AssetActions from '../actions/assets';
|
||||
import downFilledTriangle from '../../../images/down-filled-triangle.svg';
|
||||
import DownFilledTriangleIcon from '../../../images/down-filled-triangle.svg';
|
||||
|
||||
class AssetListRowBase extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -86,8 +85,9 @@ class AssetListRowBase extends React.Component {
|
|||
onClick={this.toggleOptions}
|
||||
onBlur={this.onBlurComponent}
|
||||
onFocus={this.onFocusComponent}
|
||||
aria-label="Toggle Open/Close Asset Options"
|
||||
>
|
||||
<InlineSVG src={downFilledTriangle} alt="Menu" />
|
||||
<DownFilledTriangleIcon focusable="false" aria-hidden="true" />
|
||||
</button>
|
||||
{optionsOpen &&
|
||||
<ul
|
||||
|
@ -175,7 +175,7 @@ class AssetList extends React.Component {
|
|||
render() {
|
||||
const { assetList } = this.props;
|
||||
return (
|
||||
<div className="asset-table-container">
|
||||
<article className="asset-table-container">
|
||||
<Helmet>
|
||||
<title>{this.getAssetsTitle()}</title>
|
||||
</Helmet>
|
||||
|
@ -195,7 +195,7 @@ class AssetList extends React.Component {
|
|||
{assetList.map(asset => <AssetListRow asset={asset} key={asset.key} />)}
|
||||
</tbody>
|
||||
</table>}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,8 +3,9 @@ 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;
|
||||
import getConfig from '../../../utils/getConfig';
|
||||
|
||||
const limit = getConfig('UPLOAD_LIMIT') || 250000000;
|
||||
const MAX_SIZE_B = limit;
|
||||
|
||||
const formatPercent = (percent) => {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
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';
|
||||
|
@ -19,8 +18,8 @@ import { SketchSearchbar } from '../Searchbar';
|
|||
|
||||
import CollectionListRow from './CollectionListRow';
|
||||
|
||||
const arrowUp = require('../../../../images/sort-arrow-up.svg');
|
||||
const arrowDown = require('../../../../images/sort-arrow-down.svg');
|
||||
import ArrowUpIcon from '../../../../images/sort-arrow-up.svg';
|
||||
import ArrowDownIcon from '../../../../images/sort-arrow-down.svg';
|
||||
|
||||
class CollectionList extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -83,21 +82,43 @@ class CollectionList extends React.Component {
|
|||
return null;
|
||||
}
|
||||
|
||||
_getButtonLabel = (fieldName, displayName) => {
|
||||
const { field, direction } = this.props.sorting;
|
||||
let buttonLabel;
|
||||
if (field !== fieldName) {
|
||||
if (field === 'name') {
|
||||
buttonLabel = `Sort by ${displayName} ascending.`;
|
||||
} else {
|
||||
buttonLabel = `Sort by ${displayName} descending.`;
|
||||
}
|
||||
} else if (direction === SortingActions.DIRECTION.ASC) {
|
||||
buttonLabel = `Sort by ${displayName} descending.`;
|
||||
} else {
|
||||
buttonLabel = `Sort by ${displayName} ascending.`;
|
||||
}
|
||||
return buttonLabel;
|
||||
}
|
||||
|
||||
_renderFieldHeader = (fieldName, displayName) => {
|
||||
const { field, direction } = this.props.sorting;
|
||||
const headerClass = classNames({
|
||||
'sketches-table__header': true,
|
||||
'sketches-table__header--selected': field === fieldName
|
||||
});
|
||||
const buttonLabel = this._getButtonLabel(fieldName, displayName);
|
||||
return (
|
||||
<th scope="col">
|
||||
<button className="sketch-list__sort-button" onClick={() => this.props.toggleDirectionForField(fieldName)}>
|
||||
<button
|
||||
className="sketch-list__sort-button"
|
||||
onClick={() => this.props.toggleDirectionForField(fieldName)}
|
||||
aria-label={buttonLabel}
|
||||
>
|
||||
<span className={headerClass}>{displayName}</span>
|
||||
{field === fieldName && direction === SortingActions.DIRECTION.ASC &&
|
||||
<InlineSVG src={arrowUp} />
|
||||
<ArrowUpIcon role="img" aria-label="Ascending" focusable="false" />
|
||||
}
|
||||
{field === fieldName && direction === SortingActions.DIRECTION.DESC &&
|
||||
<InlineSVG src={arrowDown} />
|
||||
<ArrowDownIcon role="img" aria-label="Descending" focusable="false" />
|
||||
}
|
||||
</button>
|
||||
</th>
|
||||
|
@ -108,7 +129,7 @@ class CollectionList extends React.Component {
|
|||
const username = this.props.username !== undefined ? this.props.username : this.props.user.username;
|
||||
|
||||
return (
|
||||
<div className="sketches-table-container">
|
||||
<article className="sketches-table-container">
|
||||
<Helmet>
|
||||
<title>{this.getTitle()}</title>
|
||||
</Helmet>
|
||||
|
@ -155,7 +176,7 @@ class CollectionList extends React.Component {
|
|||
</Overlay>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
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';
|
||||
|
@ -10,7 +9,7 @@ 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');
|
||||
import DownFilledTriangleIcon from '../../../../images/down-filled-triangle.svg';
|
||||
|
||||
class CollectionListRowBase extends React.Component {
|
||||
static projectInCollection(project, collection) {
|
||||
|
@ -129,8 +128,9 @@ class CollectionListRowBase extends React.Component {
|
|||
onClick={this.toggleOptions}
|
||||
onBlur={this.onBlurComponent}
|
||||
onFocus={this.onFocusComponent}
|
||||
aria-label="Toggle Open/Close collection options"
|
||||
>
|
||||
<InlineSVG src={downFilledTriangle} alt="Menu" />
|
||||
<DownFilledTriangleIcon title="Menu" />
|
||||
</button>
|
||||
{optionsOpen &&
|
||||
<ul
|
||||
|
|
|
@ -1,144 +1,144 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import InlineSVG from 'react-inlinesvg';
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
import { bindActionCreators } from 'redux';
|
||||
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import { Console as ConsoleFeed } from 'console-feed';
|
||||
import {
|
||||
CONSOLE_FEED_WITHOUT_ICONS, CONSOLE_FEED_LIGHT_STYLES,
|
||||
CONSOLE_FEED_DARK_STYLES, CONSOLE_FEED_CONTRAST_STYLES
|
||||
} from '../../../styles/components/_console-feed.scss';
|
||||
import warnLightUrl from '../../../images/console-warn-light.svg';
|
||||
import warnDarkUrl from '../../../images/console-warn-dark.svg';
|
||||
import errorLightUrl from '../../../images/console-error-light.svg';
|
||||
import errorDarkUrl from '../../../images/console-error-dark.svg';
|
||||
import debugLightUrl from '../../../images/console-debug-light.svg';
|
||||
import debugDarkUrl from '../../../images/console-debug-dark.svg';
|
||||
import infoLightUrl from '../../../images/console-info-light.svg';
|
||||
import infoDarkUrl from '../../../images/console-info-dark.svg';
|
||||
import warnLightUrl from '../../../images/console-warn-light.svg?byUrl';
|
||||
import warnDarkUrl from '../../../images/console-warn-dark.svg?byUrl';
|
||||
import warnContrastUrl from '../../../images/console-warn-contrast.svg?byUrl';
|
||||
import errorLightUrl from '../../../images/console-error-light.svg?byUrl';
|
||||
import errorDarkUrl from '../../../images/console-error-dark.svg?byUrl';
|
||||
import errorContrastUrl from '../../../images/console-error-contrast.svg?byUrl';
|
||||
import debugLightUrl from '../../../images/console-debug-light.svg?byUrl';
|
||||
import debugDarkUrl from '../../../images/console-debug-dark.svg?byUrl';
|
||||
import debugContrastUrl from '../../../images/console-debug-contrast.svg?byUrl';
|
||||
import infoLightUrl from '../../../images/console-info-light.svg?byUrl';
|
||||
import infoDarkUrl from '../../../images/console-info-dark.svg?byUrl';
|
||||
import infoContrastUrl from '../../../images/console-info-contrast.svg?byUrl';
|
||||
|
||||
const upArrowUrl = require('../../../images/up-arrow.svg');
|
||||
const downArrowUrl = require('../../../images/down-arrow.svg');
|
||||
import UpArrowIcon from '../../../images/up-arrow.svg';
|
||||
import DownArrowIcon from '../../../images/down-arrow.svg';
|
||||
|
||||
class Console extends React.Component {
|
||||
componentDidUpdate(prevProps) {
|
||||
this.consoleMessages.scrollTop = this.consoleMessages.scrollHeight;
|
||||
if (this.props.theme !== prevProps.theme) {
|
||||
this.props.clearConsole();
|
||||
this.props.dispatchConsoleEvent(this.props.consoleEvents);
|
||||
}
|
||||
import * as IDEActions from '../../IDE/actions/ide';
|
||||
import * as ConsoleActions from '../../IDE/actions/console';
|
||||
import { useDidUpdate } from '../../../utils/custom-hooks';
|
||||
|
||||
if (this.props.fontSize !== prevProps.fontSize) {
|
||||
this.props.clearConsole();
|
||||
this.props.dispatchConsoleEvent(this.props.consoleEvents);
|
||||
}
|
||||
const getConsoleFeedStyle = (theme, times, fontSize) => {
|
||||
const style = {};
|
||||
const CONSOLE_FEED_LIGHT_ICONS = {
|
||||
LOG_WARN_ICON: `url(${warnLightUrl})`,
|
||||
LOG_ERROR_ICON: `url(${errorLightUrl})`,
|
||||
LOG_DEBUG_ICON: `url(${debugLightUrl})`,
|
||||
LOG_INFO_ICON: `url(${infoLightUrl})`
|
||||
};
|
||||
const CONSOLE_FEED_DARK_ICONS = {
|
||||
LOG_WARN_ICON: `url(${warnDarkUrl})`,
|
||||
LOG_ERROR_ICON: `url(${errorDarkUrl})`,
|
||||
LOG_DEBUG_ICON: `url(${debugDarkUrl})`,
|
||||
LOG_INFO_ICON: `url(${infoDarkUrl})`
|
||||
};
|
||||
const CONSOLE_FEED_CONTRAST_ICONS = {
|
||||
LOG_WARN_ICON: `url(${warnContrastUrl})`,
|
||||
LOG_ERROR_ICON: `url(${errorContrastUrl})`,
|
||||
LOG_DEBUG_ICON: `url(${debugContrastUrl})`,
|
||||
LOG_INFO_ICON: `url(${infoContrastUrl})`
|
||||
};
|
||||
const CONSOLE_FEED_SIZES = {
|
||||
TREENODE_LINE_HEIGHT: 1.2,
|
||||
BASE_FONT_SIZE: fontSize,
|
||||
ARROW_FONT_SIZE: fontSize,
|
||||
LOG_ICON_WIDTH: fontSize,
|
||||
LOG_ICON_HEIGHT: 1.45 * fontSize,
|
||||
};
|
||||
|
||||
if (times > 1) {
|
||||
Object.assign(style, CONSOLE_FEED_WITHOUT_ICONS);
|
||||
}
|
||||
|
||||
getConsoleFeedStyle(theme, times) {
|
||||
const style = {};
|
||||
const CONSOLE_FEED_LIGHT_ICONS = {
|
||||
LOG_WARN_ICON: `url(${warnLightUrl})`,
|
||||
LOG_ERROR_ICON: `url(${errorLightUrl})`,
|
||||
LOG_DEBUG_ICON: `url(${debugLightUrl})`,
|
||||
LOG_INFO_ICON: `url(${infoLightUrl})`
|
||||
};
|
||||
const CONSOLE_FEED_DARK_ICONS = {
|
||||
LOG_WARN_ICON: `url(${warnDarkUrl})`,
|
||||
LOG_ERROR_ICON: `url(${errorDarkUrl})`,
|
||||
LOG_DEBUG_ICON: `url(${debugDarkUrl})`,
|
||||
LOG_INFO_ICON: `url(${infoDarkUrl})`
|
||||
};
|
||||
const CONSOLE_FEED_SIZES = {
|
||||
TREENODE_LINE_HEIGHT: 1.2,
|
||||
BASE_FONT_SIZE: this.props.fontSize,
|
||||
ARROW_FONT_SIZE: this.props.fontSize,
|
||||
LOG_ICON_WIDTH: this.props.fontSize,
|
||||
LOG_ICON_HEIGHT: 1.45 * this.props.fontSize,
|
||||
};
|
||||
|
||||
if (times > 1) {
|
||||
Object.assign(style, CONSOLE_FEED_WITHOUT_ICONS);
|
||||
}
|
||||
switch (theme) {
|
||||
case 'light':
|
||||
return Object.assign(CONSOLE_FEED_LIGHT_STYLES, CONSOLE_FEED_LIGHT_ICONS, CONSOLE_FEED_SIZES, style);
|
||||
case 'dark':
|
||||
return Object.assign(CONSOLE_FEED_DARK_STYLES, CONSOLE_FEED_DARK_ICONS, CONSOLE_FEED_SIZES, style);
|
||||
case 'contrast':
|
||||
return Object.assign(CONSOLE_FEED_CONTRAST_STYLES, CONSOLE_FEED_DARK_ICONS, CONSOLE_FEED_SIZES, style);
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
switch (theme) {
|
||||
case 'light':
|
||||
return Object.assign(CONSOLE_FEED_LIGHT_STYLES, CONSOLE_FEED_LIGHT_ICONS, CONSOLE_FEED_SIZES, style);
|
||||
case 'dark':
|
||||
return Object.assign(CONSOLE_FEED_DARK_STYLES, CONSOLE_FEED_DARK_ICONS, CONSOLE_FEED_SIZES, style);
|
||||
case 'contrast':
|
||||
return Object.assign(CONSOLE_FEED_CONTRAST_STYLES, CONSOLE_FEED_CONTRAST_ICONS, CONSOLE_FEED_SIZES, style);
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const consoleClass = classNames({
|
||||
'preview-console': true,
|
||||
'preview-console--collapsed': !this.props.isExpanded
|
||||
});
|
||||
const Console = () => {
|
||||
const consoleEvents = useSelector(state => state.console);
|
||||
const isExpanded = useSelector(state => state.ide.consoleIsExpanded);
|
||||
const { theme, fontSize } = useSelector(state => state.preferences);
|
||||
|
||||
return (
|
||||
<div className={consoleClass} role="main">
|
||||
<div className="preview-console__header">
|
||||
<h2 className="preview-console__header-title">Console</h2>
|
||||
<div className="preview-console__header-buttons">
|
||||
<button className="preview-console__clear" onClick={this.props.clearConsole} aria-label="clear console">
|
||||
Clear
|
||||
</button>
|
||||
<button
|
||||
className="preview-console__collapse"
|
||||
onClick={this.props.collapseConsole}
|
||||
aria-label="collapse console"
|
||||
>
|
||||
<InlineSVG src={downArrowUrl} />
|
||||
</button>
|
||||
<button className="preview-console__expand" onClick={this.props.expandConsole} aria-label="expand console">
|
||||
<InlineSVG src={upArrowUrl} />
|
||||
</button>
|
||||
</div>
|
||||
const {
|
||||
collapseConsole, expandConsole, clearConsole, dispatchConsoleEvent
|
||||
} = bindActionCreators({ ...IDEActions, ...ConsoleActions }, useDispatch());
|
||||
|
||||
useDidUpdate(() => {
|
||||
clearConsole();
|
||||
dispatchConsoleEvent(consoleEvents);
|
||||
}, [theme, fontSize]);
|
||||
|
||||
const cm = useRef({});
|
||||
|
||||
useDidUpdate(() => { cm.current.scrollTop = cm.current.scrollHeight; });
|
||||
|
||||
const consoleClass = classNames({
|
||||
'preview-console': true,
|
||||
'preview-console--collapsed': !isExpanded
|
||||
});
|
||||
|
||||
return (
|
||||
<section className={consoleClass} >
|
||||
<header className="preview-console__header">
|
||||
<h2 className="preview-console__header-title">Console</h2>
|
||||
<div className="preview-console__header-buttons">
|
||||
<button className="preview-console__clear" onClick={clearConsole} aria-label="Clear console">
|
||||
Clear
|
||||
</button>
|
||||
<button
|
||||
className="preview-console__collapse"
|
||||
onClick={collapseConsole}
|
||||
aria-label="Close console"
|
||||
>
|
||||
<DownArrowIcon focusable="false" aria-hidden="true" />
|
||||
</button>
|
||||
<button className="preview-console__expand" onClick={expandConsole} aria-label="Open console" >
|
||||
<UpArrowIcon focusable="false" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<div ref={(element) => { this.consoleMessages = element; }} className="preview-console__messages">
|
||||
{this.props.consoleEvents.map((consoleEvent) => {
|
||||
const { method, times } = consoleEvent;
|
||||
const { theme } = this.props;
|
||||
return (
|
||||
<div key={consoleEvent.id} className={`preview-console__message preview-console__message--${method}`}>
|
||||
{ times > 1 &&
|
||||
<div
|
||||
className="preview-console__logged-times"
|
||||
style={{ fontSize: this.props.fontSize, borderRadius: this.props.fontSize / 2 }}
|
||||
>
|
||||
{times}
|
||||
</div>
|
||||
}
|
||||
<ConsoleFeed
|
||||
styles={this.getConsoleFeedStyle(theme, times)}
|
||||
logs={[consoleEvent]}
|
||||
/>
|
||||
</header>
|
||||
<div ref={cm} className="preview-console__messages">
|
||||
{consoleEvents.map((consoleEvent) => {
|
||||
const { method, times } = consoleEvent;
|
||||
return (
|
||||
<div key={consoleEvent.id} className={`preview-console__message preview-console__message--${method}`}>
|
||||
{ times > 1 &&
|
||||
<div
|
||||
className="preview-console__logged-times"
|
||||
style={{ fontSize, borderRadius: fontSize / 2 }}
|
||||
>
|
||||
{times}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
<ConsoleFeed
|
||||
styles={getConsoleFeedStyle(theme, times, fontSize)}
|
||||
logs={[consoleEvent]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Console.propTypes = {
|
||||
consoleEvents: PropTypes.arrayOf(PropTypes.shape({
|
||||
method: PropTypes.string.isRequired,
|
||||
args: PropTypes.arrayOf(PropTypes.string)
|
||||
})),
|
||||
isExpanded: PropTypes.bool.isRequired,
|
||||
collapseConsole: PropTypes.func.isRequired,
|
||||
expandConsole: PropTypes.func.isRequired,
|
||||
clearConsole: PropTypes.func.isRequired,
|
||||
dispatchConsoleEvent: PropTypes.func.isRequired,
|
||||
theme: PropTypes.string.isRequired,
|
||||
fontSize: PropTypes.number.isRequired
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
Console.defaultProps = {
|
||||
consoleEvents: []
|
||||
};
|
||||
|
||||
export default Console;
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Clipboard from 'clipboard';
|
||||
import InlineSVG from 'react-inlinesvg';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import shareUrl from '../../../images/share.svg';
|
||||
import ShareIcon from '../../../images/share.svg';
|
||||
|
||||
class CopyableInput extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -70,8 +69,9 @@ class CopyableInput extends React.Component {
|
|||
rel="noopener noreferrer"
|
||||
href={value}
|
||||
className="copyable-input__preview"
|
||||
aria-label={`Open ${label} view in new tab`}
|
||||
>
|
||||
<InlineSVG src={shareUrl} alt={`open ${label} view in new tab`} />
|
||||
<ShareIcon focusable="false" aria-hidden="true" />
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
|
|
|
@ -1,13 +1,9 @@
|
|||
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" />;
|
||||
}
|
||||
import EditIcon from '../../../images/pencil.svg';
|
||||
|
||||
// TODO I think this needs a description prop so that it's accessible
|
||||
function EditableInput({
|
||||
validate, value, emptyPlaceholder, InputComponent, inputProps, onChange
|
||||
}) {
|
||||
|
@ -52,9 +48,13 @@ function EditableInput({
|
|||
|
||||
return (
|
||||
<span className={classes}>
|
||||
<button className="editable-input__label" onClick={beginEditing}>
|
||||
<button
|
||||
className="editable-input__label"
|
||||
onClick={beginEditing}
|
||||
aria-label={`Edit ${displayValue} value`}
|
||||
>
|
||||
<span>{displayValue}</span>
|
||||
<EditIcon />
|
||||
<EditIcon className="editable-input__icon" focusable="false" aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
<InputComponent
|
||||
|
|
|
@ -24,7 +24,6 @@ import 'codemirror/addon/edit/matchbrackets';
|
|||
import { JSHINT } from 'jshint';
|
||||
import { CSSLint } from 'csslint';
|
||||
import { HTMLHint } from 'htmlhint';
|
||||
import InlineSVG from 'react-inlinesvg';
|
||||
import classNames from 'classnames';
|
||||
import { debounce } from 'lodash';
|
||||
import '../../../utils/htmlmixed';
|
||||
|
@ -36,6 +35,11 @@ import { metaKey, } from '../../../utils/metaKey';
|
|||
|
||||
import search from '../../../utils/codemirror-search';
|
||||
|
||||
import beepUrl from '../../../sounds/audioAlert.mp3';
|
||||
import UnsavedChangesDotIcon from '../../../images/unsaved-changes-dot.svg';
|
||||
import RightArrowIcon from '../../../images/right-arrow.svg';
|
||||
import LeftArrowIcon from '../../../images/left-arrow.svg';
|
||||
|
||||
search(CodeMirror);
|
||||
|
||||
const beautifyCSS = beautifyJS.css;
|
||||
|
@ -45,11 +49,6 @@ window.JSHINT = JSHINT;
|
|||
window.CSSLint = CSSLint;
|
||||
window.HTMLHint = HTMLHint;
|
||||
|
||||
const beepUrl = require('../../../sounds/audioAlert.mp3');
|
||||
const unsavedChangesDotUrl = require('../../../images/unsaved-changes-dot.svg');
|
||||
const rightArrowUrl = require('../../../images/right-arrow.svg');
|
||||
const leftArrowUrl = require('../../../images/left-arrow.svg');
|
||||
|
||||
const IS_TAB_INDENT = false;
|
||||
const INDENTATION_AMOUNT = 2;
|
||||
|
||||
|
@ -315,29 +314,30 @@ class Editor extends React.Component {
|
|||
});
|
||||
|
||||
return (
|
||||
<section
|
||||
role="main"
|
||||
className={editorSectionClass}
|
||||
>
|
||||
<section className={editorSectionClass} >
|
||||
<header className="editor__header">
|
||||
<button
|
||||
aria-label="collapse file navigation"
|
||||
aria-label="Open Sketch files navigation"
|
||||
className="sidebar__contract"
|
||||
onClick={this.props.collapseSidebar}
|
||||
>
|
||||
<InlineSVG src={leftArrowUrl} />
|
||||
<LeftArrowIcon focusable="false" aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
aria-label="expand file navigation"
|
||||
aria-label="Close sketch files navigation"
|
||||
className="sidebar__expand"
|
||||
onClick={this.props.expandSidebar}
|
||||
>
|
||||
<InlineSVG src={rightArrowUrl} />
|
||||
<RightArrowIcon focusable="false" aria-hidden="true" />
|
||||
</button>
|
||||
<div className="editor__file-name">
|
||||
<span>
|
||||
{this.props.file.name}
|
||||
{this.props.unsavedChanges ? <InlineSVG src={unsavedChangesDotUrl} /> : null}
|
||||
<span className="editor__unsaved-changes">
|
||||
{this.props.unsavedChanges ?
|
||||
<UnsavedChangesDotIcon role="img" aria-label="Sketch has unsaved changes" focusable="false" /> :
|
||||
null}
|
||||
</span>
|
||||
</span>
|
||||
<Timer
|
||||
projectSavedTime={this.props.projectSavedTime}
|
||||
|
@ -345,8 +345,8 @@ class Editor extends React.Component {
|
|||
/>
|
||||
</div>
|
||||
</header>
|
||||
<div ref={(element) => { this.codemirrorContainer = element; }} className={editorHolderClass} >
|
||||
</div>
|
||||
<article ref={(element) => { this.codemirrorContainer = element; }} className={editorHolderClass} >
|
||||
</article>
|
||||
<EditorAccessibility
|
||||
lintMessages={this.props.lintMessages}
|
||||
/>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react';
|
||||
import InlineSVG from 'react-inlinesvg';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import githubLogoUrl from '../../../images/github.svg';
|
||||
import GitHubLogo from '../../../images/github.svg';
|
||||
|
||||
function Feedback(props) {
|
||||
return (
|
||||
|
@ -24,7 +23,7 @@ function Feedback(props) {
|
|||
className="feedback__github-link"
|
||||
>
|
||||
Go to Github
|
||||
<InlineSVG className="feedback__github-logo" src={githubLogoUrl} />
|
||||
<GitHubLogo className="feedback__github-logo" focusable="false" aria-hidden="true" />
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
@ -2,14 +2,13 @@ import PropTypes from 'prop-types';
|
|||
import React from 'react';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import InlineSVG from 'react-inlinesvg';
|
||||
import classNames from 'classnames';
|
||||
import * as IDEActions from '../actions/ide';
|
||||
import * as FileActions from '../actions/files';
|
||||
import downArrowUrl from '../../../images/down-filled-triangle.svg';
|
||||
import folderRightUrl from '../../../images/triangle-arrow-right.svg';
|
||||
import folderDownUrl from '../../../images/triangle-arrow-down.svg';
|
||||
import fileUrl from '../../../images/file.svg';
|
||||
import DownArrowIcon from '../../../images/down-filled-triangle.svg';
|
||||
import FolderRightIcon from '../../../images/triangle-arrow-right.svg';
|
||||
import FolderDownIcon from '../../../images/triangle-arrow-down.svg';
|
||||
import FileIcon from '../../../images/file.svg';
|
||||
|
||||
export class FileNode extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -185,7 +184,7 @@ export class FileNode extends React.Component {
|
|||
<span className="file-item__spacer"></span>
|
||||
{ isFile &&
|
||||
<span className="sidebar__file-item-icon">
|
||||
<InlineSVG src={fileUrl} />
|
||||
<FileIcon focusable="false" aria-hidden="true" />
|
||||
</span>
|
||||
}
|
||||
{ isFolder &&
|
||||
|
@ -193,24 +192,28 @@ export class FileNode extends React.Component {
|
|||
<button
|
||||
className="sidebar__file-item-closed"
|
||||
onClick={this.showFolderChildren}
|
||||
aria-label="Open folder contents"
|
||||
>
|
||||
<InlineSVG className="folder-right" src={folderRightUrl} />
|
||||
<FolderRightIcon className="folder-right" focusable="false" aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
className="sidebar__file-item-open"
|
||||
onClick={this.hideFolderChildren}
|
||||
aria-label="Close file contents"
|
||||
>
|
||||
<InlineSVG className="folder-down" src={folderDownUrl} />
|
||||
<FolderDownIcon className="folder-down" focusable="false" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
<button
|
||||
aria-label="Name"
|
||||
className="sidebar__file-item-name"
|
||||
onClick={this.handleFileClick}
|
||||
>
|
||||
{this.state.updatedName}
|
||||
</button>
|
||||
<input
|
||||
data-testid="input"
|
||||
type="text"
|
||||
className="sidebar__file-item-input"
|
||||
value={this.state.updatedName}
|
||||
|
@ -222,14 +225,14 @@ export class FileNode extends React.Component {
|
|||
/>
|
||||
<button
|
||||
className="sidebar__file-item-show-options"
|
||||
aria-label="view file options"
|
||||
aria-label="Toggle open/close file options"
|
||||
ref={(element) => { this[`fileOptions-${this.props.id}`] = element; }}
|
||||
tabIndex="0"
|
||||
onClick={this.toggleFileOptions}
|
||||
onBlur={this.onBlurComponent}
|
||||
onFocus={this.onFocusComponent}
|
||||
>
|
||||
<InlineSVG src={downArrowUrl} />
|
||||
<DownArrowIcon focusable="false" aria-hidden="true" />
|
||||
</button>
|
||||
<div className="sidebar__file-item-options">
|
||||
<ul title="file options">
|
||||
|
|
31
client/modules/IDE/components/FileNode.stories.jsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { FileNode } from './FileNode';
|
||||
|
||||
export default {
|
||||
title: 'IDE/FileNode',
|
||||
component: FileNode
|
||||
};
|
||||
|
||||
export const Show = () => (
|
||||
<FileNode
|
||||
id="nodeId"
|
||||
parantId="parentId"
|
||||
name="File name"
|
||||
fileType="jpeg"
|
||||
isSelectedFile
|
||||
isFolderClosed={false}
|
||||
setSelectedFile={action('setSelectedFile')}
|
||||
deleteFile={action('deleteFile')}
|
||||
updateFileName={action('updateFileName')}
|
||||
resetSelectedFile={action('resetSelectedFile')}
|
||||
newFile={action('newFile')}
|
||||
newFolder={action('newFolder')}
|
||||
showFolderChildren={action('showFolderChildren')}
|
||||
hideFolderChildren={action('hideFolderChildren')}
|
||||
openUploadFileModal={action('openUploadFileModal')}
|
||||
canEdit
|
||||
authenticated
|
||||
/>
|
||||
);
|
127
client/modules/IDE/components/FileNode.test.jsx
Normal file
|
@ -0,0 +1,127 @@
|
|||
import React from 'react';
|
||||
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
||||
import { FileNode } from './FileNode';
|
||||
|
||||
describe('<FileNode />', () => {
|
||||
const changeName = (newFileName) => {
|
||||
const renameButton = screen.getByText(/Rename/i);
|
||||
fireEvent.click(renameButton);
|
||||
|
||||
const input = screen.getByTestId('input');
|
||||
fireEvent.change(input, { target: { value: newFileName } });
|
||||
fireEvent.blur(input);
|
||||
};
|
||||
|
||||
const expectFileNameToBe = async (expectedName) => {
|
||||
const name = screen.getByLabelText(/Name/i);
|
||||
await waitFor(() => within(name).queryByText(expectedName));
|
||||
};
|
||||
|
||||
const renderFileNode = (fileType, extraProps = {}) => {
|
||||
const props = {
|
||||
...extraProps,
|
||||
id: '0',
|
||||
name: fileType === 'folder' ? 'afolder' : 'test.jsx',
|
||||
fileType,
|
||||
canEdit: true,
|
||||
children: [],
|
||||
authenticated: false,
|
||||
setSelectedFile: jest.fn(),
|
||||
deleteFile: jest.fn(),
|
||||
updateFileName: jest.fn(),
|
||||
resetSelectedFile: jest.fn(),
|
||||
newFile: jest.fn(),
|
||||
newFolder: jest.fn(),
|
||||
showFolderChildren: jest.fn(),
|
||||
hideFolderChildren: jest.fn(),
|
||||
openUploadFileModal: jest.fn(),
|
||||
setProjectName: jest.fn(),
|
||||
};
|
||||
|
||||
render(<FileNode {...props} />);
|
||||
|
||||
return props;
|
||||
};
|
||||
|
||||
describe('fileType: file', () => {
|
||||
it('cannot change to an empty name', async () => {
|
||||
const props = renderFileNode('file');
|
||||
|
||||
changeName('');
|
||||
|
||||
await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
|
||||
await expectFileNameToBe(props.name);
|
||||
});
|
||||
|
||||
it('can change to a valid filename', async () => {
|
||||
const newName = 'newname.jsx';
|
||||
const props = renderFileNode('file');
|
||||
|
||||
changeName(newName);
|
||||
|
||||
await waitFor(() => expect(props.updateFileName).toHaveBeenCalledWith(props.id, newName));
|
||||
await expectFileNameToBe(newName);
|
||||
});
|
||||
|
||||
it('must have an extension', async () => {
|
||||
const newName = 'newname';
|
||||
const props = renderFileNode('file');
|
||||
|
||||
changeName(newName);
|
||||
|
||||
await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
|
||||
await expectFileNameToBe(props.name);
|
||||
});
|
||||
|
||||
it('can change to a different extension', async () => {
|
||||
const newName = 'newname.gif';
|
||||
const props = renderFileNode('file');
|
||||
|
||||
changeName(newName);
|
||||
|
||||
await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
|
||||
await expectFileNameToBe(props.name);
|
||||
});
|
||||
|
||||
it('cannot be just an extension', async () => {
|
||||
const newName = '.jsx';
|
||||
const props = renderFileNode('file');
|
||||
|
||||
changeName(newName);
|
||||
|
||||
await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
|
||||
await expectFileNameToBe(props.name);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fileType: folder', () => {
|
||||
it('cannot change to an empty name', async () => {
|
||||
const props = renderFileNode('folder');
|
||||
|
||||
changeName('');
|
||||
|
||||
await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
|
||||
await expectFileNameToBe(props.name);
|
||||
});
|
||||
|
||||
it('can change to another name', async () => {
|
||||
const newName = 'foldername';
|
||||
const props = renderFileNode('folder');
|
||||
|
||||
changeName(newName);
|
||||
|
||||
await waitFor(() => expect(props.updateFileName).toHaveBeenCalledWith(props.id, newName));
|
||||
await expectFileNameToBe(newName);
|
||||
});
|
||||
|
||||
it('cannot have a file extension', async () => {
|
||||
const newName = 'foldername.jsx';
|
||||
const props = renderFileNode('folder');
|
||||
|
||||
changeName(newName);
|
||||
|
||||
await waitFor(() => expect(props.updateFileName).not.toHaveBeenCalled());
|
||||
await expectFileNameToBe(props.name);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -4,11 +4,11 @@ import Dropzone from 'dropzone';
|
|||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import * as UploaderActions from '../actions/uploader';
|
||||
import getConfig from '../../../utils/getConfig';
|
||||
import { fileExtensionsAndMimeTypes } from '../../../../server/utils/fileUtils';
|
||||
|
||||
const __process = (typeof global !== 'undefined' ? global : window).process;
|
||||
const s3Bucket = __process.env.S3_BUCKET_URL_BASE ||
|
||||
`https://s3-${__process.env.AWS_REGION}.amazonaws.com/${__process.env.S3_BUCKET}/`;
|
||||
const s3Bucket = getConfig('S3_BUCKET_URL_BASE') ||
|
||||
`https://s3-${getConfig('AWS_REGION')}.amazonaws.com/${getConfig('S3_BUCKET')}/`;
|
||||
|
||||
class FileUploader extends React.Component {
|
||||
componentDidMount() {
|
||||
|
|
|
@ -1,92 +1,91 @@
|
|||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { metaKeyName, } from '../../../utils/metaKey';
|
||||
|
||||
function KeyboardShortcutModal() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<ul className="keyboard-shortcuts" title="keyboard shortcuts">
|
||||
<li className="keyboard-shortcut-item">
|
||||
<span className="keyboard-shortcut__command">{'\u21E7'} + Tab</span>
|
||||
<span>Tidy</span>
|
||||
</li>
|
||||
<li className="keyboard-shortcut-item">
|
||||
<span className="keyboard-shortcut__command">
|
||||
{metaKeyName} + S
|
||||
</span>
|
||||
<span>Save</span>
|
||||
</li>
|
||||
<li className="keyboard-shortcut-item">
|
||||
<span className="keyboard-shortcut__command">
|
||||
{metaKeyName} + F
|
||||
</span>
|
||||
<span>Find Text</span>
|
||||
</li>
|
||||
<li className="keyboard-shortcut-item">
|
||||
<span className="keyboard-shortcut__command">
|
||||
{metaKeyName} + G
|
||||
</span>
|
||||
<span>Find Next Text Match</span>
|
||||
</li>
|
||||
<li className="keyboard-shortcut-item">
|
||||
<span className="keyboard-shortcut__command">
|
||||
{metaKeyName} + {'\u21E7'} + G
|
||||
</span>
|
||||
<span>Find Previous Text Match</span>
|
||||
</li>
|
||||
<li className="keyboard-shortcut-item">
|
||||
<span className="keyboard-shortcut__command">
|
||||
{metaKeyName} + [
|
||||
</span>
|
||||
<span>Indent Code Left</span>
|
||||
</li>
|
||||
<li className="keyboard-shortcut-item">
|
||||
<span className="keyboard-shortcut__command">
|
||||
{metaKeyName} + ]
|
||||
</span>
|
||||
<span>Indent Code Right</span>
|
||||
</li>
|
||||
<li className="keyboard-shortcut-item">
|
||||
<span className="keyboard-shortcut__command">
|
||||
{metaKeyName} + /
|
||||
</span>
|
||||
<span>Comment Line</span>
|
||||
</li>
|
||||
<li className="keyboard-shortcut-item">
|
||||
<span className="keyboard-shortcut__command">
|
||||
{metaKeyName} + Enter
|
||||
</span>
|
||||
<span>Start Sketch</span>
|
||||
</li>
|
||||
<li className="keyboard-shortcut-item">
|
||||
<span className="keyboard-shortcut__command">
|
||||
{metaKeyName} + {'\u21E7'} + Enter
|
||||
</span>
|
||||
<span>Stop Sketch</span>
|
||||
</li>
|
||||
<li className="keyboard-shortcut-item">
|
||||
<span className="keyboard-shortcut__command">
|
||||
{metaKeyName} + {'\u21E7'} + 1
|
||||
</span>
|
||||
<span>Turn on Accessible Output</span>
|
||||
</li>
|
||||
<li className="keyboard-shortcut-item">
|
||||
<span className="keyboard-shortcut__command">
|
||||
{metaKeyName} + {'\u21E7'} + 2
|
||||
</span>
|
||||
<span>Turn off Accessible Output</span>
|
||||
</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>
|
||||
<div className="keyboard-shortcuts">
|
||||
<h3 className="keyboard-shortcuts__title">{t('CodeEditing')}</h3>
|
||||
<p className="keyboard-shortcuts__description">
|
||||
{t('Code editing keyboard shortcuts follow')} <a href="https://shortcuts.design/toolspage-sublimetext.html" target="_blank" rel="noopener noreferrer">{t('Sublime Text shortcuts')}</a>.
|
||||
</p>
|
||||
<ul className="keyboard-shortcuts__list">
|
||||
<li className="keyboard-shortcut-item">
|
||||
<span className="keyboard-shortcut__command">{'\u21E7'} + Tab</span>
|
||||
<span>{t('Tidy')}</span>
|
||||
</li>
|
||||
<li className="keyboard-shortcut-item">
|
||||
<span className="keyboard-shortcut__command">
|
||||
{metaKeyName} + F
|
||||
</span>
|
||||
<span>{t('FindText')}</span>
|
||||
</li>
|
||||
<li className="keyboard-shortcut-item">
|
||||
<span className="keyboard-shortcut__command">
|
||||
{metaKeyName} + G
|
||||
</span>
|
||||
<span>{t('FindNextTextMatch')}</span>
|
||||
</li>
|
||||
<li className="keyboard-shortcut-item">
|
||||
<span className="keyboard-shortcut__command">
|
||||
{metaKeyName} + {'\u21E7'} + G
|
||||
</span>
|
||||
<span>{t('FindPreviousTextMatch')}</span>
|
||||
</li>
|
||||
<li className="keyboard-shortcut-item">
|
||||
<span className="keyboard-shortcut__command">
|
||||
{metaKeyName} + [
|
||||
</span>
|
||||
<span>{t('IndentCodeLeft')}</span>
|
||||
</li>
|
||||
<li className="keyboard-shortcut-item">
|
||||
<span className="keyboard-shortcut__command">
|
||||
{metaKeyName} + ]
|
||||
</span>
|
||||
<span>{t('IndentCodeRight')}</span>
|
||||
</li>
|
||||
<li className="keyboard-shortcut-item">
|
||||
<span className="keyboard-shortcut__command">
|
||||
{metaKeyName} + /
|
||||
</span>
|
||||
<span>{t('CommentLine')}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<h3 className="keyboard-shortcuts__title">General</h3>
|
||||
<ul className="keyboard-shortcuts__list">
|
||||
<li className="keyboard-shortcut-item">
|
||||
<span className="keyboard-shortcut__command">
|
||||
{metaKeyName} + S
|
||||
</span>
|
||||
<span>{t('Save')}</span>
|
||||
</li>
|
||||
<li className="keyboard-shortcut-item">
|
||||
<span className="keyboard-shortcut__command">
|
||||
{metaKeyName} + Enter
|
||||
</span>
|
||||
<span>{t('StartSketch')}</span>
|
||||
</li>
|
||||
<li className="keyboard-shortcut-item">
|
||||
<span className="keyboard-shortcut__command">
|
||||
{metaKeyName} + {'\u21E7'} + Enter
|
||||
</span>
|
||||
<span>{t('StopSketch')}</span>
|
||||
</li>
|
||||
<li className="keyboard-shortcut-item">
|
||||
<span className="keyboard-shortcut__command">
|
||||
{metaKeyName} + {'\u21E7'} + 1
|
||||
</span>
|
||||
<span>{t('TurnOnAccessibleOutput')}</span>
|
||||
</li>
|
||||
<li className="keyboard-shortcut-item">
|
||||
<span className="keyboard-shortcut__command">
|
||||
{metaKeyName} + {'\u21E7'} + 2
|
||||
</span>
|
||||
<span>{t('TurnOffAccessibleOutput')}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@ import PropTypes from 'prop-types';
|
|||
import React from 'react';
|
||||
import { domOnlyProps } from '../../../utils/reduxFormUtils';
|
||||
|
||||
import Button from '../../../common/Button';
|
||||
|
||||
class NewFileForm extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -33,7 +35,10 @@ class NewFileForm extends React.Component {
|
|||
{...domOnlyProps(name)}
|
||||
ref={(element) => { this.fileName = element; }}
|
||||
/>
|
||||
<input type="submit" value="Add File" aria-label="add file" />
|
||||
<Button
|
||||
type="submit"
|
||||
>Add File
|
||||
</Button>
|
||||
</div>
|
||||
{name.touched && name.error && <span className="form-error">{name.error}</span>}
|
||||
</form>
|
||||
|
|
|
@ -3,13 +3,12 @@ import React from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators, compose } from 'redux';
|
||||
import { reduxForm } from 'redux-form';
|
||||
import InlineSVG from 'react-inlinesvg';
|
||||
import NewFileForm from './NewFileForm';
|
||||
import { closeNewFileModal } from '../actions/ide';
|
||||
import { createFile } from '../actions/files';
|
||||
import { CREATE_FILE_REGEX } from '../../../../server/utils/fileUtils';
|
||||
|
||||
const exitUrl = require('../../../images/exit.svg');
|
||||
import ExitIcon from '../../../images/exit.svg';
|
||||
|
||||
|
||||
// At some point this will probably be generalized to a generic modal
|
||||
|
@ -35,8 +34,12 @@ class NewFileModal extends React.Component {
|
|||
<div className="modal-content">
|
||||
<div className="modal__header">
|
||||
<h2 className="modal__title">Create File</h2>
|
||||
<button className="modal__exit-button" onClick={this.props.closeNewFileModal}>
|
||||
<InlineSVG src={exitUrl} alt="Close New File Modal" />
|
||||
<button
|
||||
className="modal__exit-button"
|
||||
onClick={this.props.closeNewFileModal}
|
||||
aria-label="Close New File Modal"
|
||||
>
|
||||
<ExitIcon focusable="false" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<NewFileForm
|
||||
|
|
|
@ -2,6 +2,8 @@ import PropTypes from 'prop-types';
|
|||
import React from 'react';
|
||||
import { domOnlyProps } from '../../../utils/reduxFormUtils';
|
||||
|
||||
import Button from '../../../common/Button';
|
||||
|
||||
class NewFolderForm extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -34,7 +36,10 @@ class NewFolderForm extends React.Component {
|
|||
ref={(element) => { this.fileName = element; }}
|
||||
{...domOnlyProps(name)}
|
||||
/>
|
||||
<input type="submit" value="Add Folder" aria-label="add folder" />
|
||||
<Button
|
||||
type="submit"
|
||||
>Add Folder
|
||||
</Button>
|
||||
</div>
|
||||
{name.touched && name.error && <span className="form-error">{name.error}</span>}
|
||||
</form>
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { reduxForm } from 'redux-form';
|
||||
import InlineSVG from 'react-inlinesvg';
|
||||
import NewFolderForm from './NewFolderForm';
|
||||
|
||||
const exitUrl = require('../../../images/exit.svg');
|
||||
import ExitIcon from '../../../images/exit.svg';
|
||||
|
||||
class NewFolderModal extends React.Component {
|
||||
componentDidMount() {
|
||||
|
@ -17,8 +16,12 @@ class NewFolderModal extends React.Component {
|
|||
<div className="modal-content-folder">
|
||||
<div className="modal__header">
|
||||
<h2 className="modal__title">Create Folder</h2>
|
||||
<button className="modal__exit-button" onClick={this.props.closeModal}>
|
||||
<InlineSVG src={exitUrl} alt="Close New Folder Modal" />
|
||||
<button
|
||||
className="modal__exit-button"
|
||||
onClick={this.props.closeModal}
|
||||
aria-label="Close New Folder Modal"
|
||||
>
|
||||
<ExitIcon focusable="false" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<NewFolderForm {...this.props} />
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
export const optionsOnOff = (name, onLabel = 'On', offLabel = 'Off') => [
|
||||
{
|
||||
value: true, label: onLabel, ariaLabel: `${name} on`, name: `${name}`, id: `${name}-on`.replace(' ', '-')
|
||||
},
|
||||
{
|
||||
value: false, label: offLabel, ariaLabel: `${name} off`, name: `${name}`, id: `${name}-off`.replace(' ', '-')
|
||||
},
|
||||
];
|
||||
|
||||
export const optionsPickOne = (name, ...options) => options.map(option => ({
|
||||
value: option,
|
||||
label: option,
|
||||
ariaLabel: `${option} ${name} on`,
|
||||
name: `${option} ${name}`,
|
||||
id: `${option}-${name}-on`.replace(' ', '-')
|
||||
}));
|
||||
|
||||
const nameToValueName = x => (x && x.toLowerCase().replace(/#|_|-/g, ' '));
|
||||
|
||||
// preferenceOnOff: name, value and onSelect are mandatory. propname is optional
|
||||
export const preferenceOnOff = (name, value, onSelect, propname) => ({
|
||||
title: name,
|
||||
value,
|
||||
options: optionsOnOff(propname || nameToValueName(name)),
|
||||
onSelect
|
||||
});
|
|
@ -1,15 +1,15 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import InlineSVG from 'react-inlinesvg';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
// import { bindActionCreators } from 'redux';
|
||||
// import { connect } from 'react-redux';
|
||||
// import * as PreferencesActions from '../actions/preferences';
|
||||
|
||||
const plusUrl = require('../../../images/plus.svg');
|
||||
const minusUrl = require('../../../images/minus.svg');
|
||||
const beepUrl = require('../../../sounds/audioAlert.mp3');
|
||||
import PlusIcon from '../../../../images/plus.svg';
|
||||
import MinusIcon from '../../../../images/minus.svg';
|
||||
import beepUrl from '../../../../sounds/audioAlert.mp3';
|
||||
|
||||
class Preferences extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -99,13 +99,13 @@ class Preferences extends React.Component {
|
|||
<Tabs>
|
||||
<TabList>
|
||||
<div className="tabs__titles">
|
||||
<Tab><h4 className="tabs__title">General Settings</h4></Tab>
|
||||
<Tab><h4 className="tabs__title">Accessibility</h4></Tab>
|
||||
<Tab><h4 className="tabs__title">{this.props.t('GeneralSettings')}</h4></Tab>
|
||||
<Tab><h4 className="tabs__title">{this.props.t('Accessibility')}</h4></Tab>
|
||||
</div>
|
||||
</TabList>
|
||||
<TabPanel>
|
||||
<div className="preference">
|
||||
<h4 className="preference__title">Theme</h4>
|
||||
<h4 className="preference__title">{this.props.t('Theme')}</h4>
|
||||
<div className="preference__options">
|
||||
<input
|
||||
type="radio"
|
||||
|
@ -117,7 +117,7 @@ class Preferences extends React.Component {
|
|||
value="light"
|
||||
checked={this.props.theme === 'light'}
|
||||
/>
|
||||
<label htmlFor="light-theme-on" className="preference__option">Light</label>
|
||||
<label htmlFor="light-theme-on" className="preference__option">{this.props.t('Light')}</label>
|
||||
<input
|
||||
type="radio"
|
||||
onChange={() => this.props.setTheme('dark')}
|
||||
|
@ -128,7 +128,7 @@ class Preferences extends React.Component {
|
|||
value="dark"
|
||||
checked={this.props.theme === 'dark'}
|
||||
/>
|
||||
<label htmlFor="dark-theme-on" className="preference__option">Dark</label>
|
||||
<label htmlFor="dark-theme-on" className="preference__option">{this.props.t('Dark')}</label>
|
||||
<input
|
||||
type="radio"
|
||||
onChange={() => this.props.setTheme('contrast')}
|
||||
|
@ -139,19 +139,19 @@ class Preferences extends React.Component {
|
|||
value="contrast"
|
||||
checked={this.props.theme === 'contrast'}
|
||||
/>
|
||||
<label htmlFor="high-contrast-theme-on" className="preference__option">High Contrast</label>
|
||||
<label htmlFor="high-contrast-theme-on" className="preference__option">{this.props.t('HighContrast')}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="preference">
|
||||
<h4 className="preference__title">Text size</h4>
|
||||
<h4 className="preference__title">{this.props.t('TextSize')}</h4>
|
||||
<button
|
||||
className="preference__minus-button"
|
||||
onClick={this.decreaseFontSize}
|
||||
aria-label="decrease font size"
|
||||
disabled={this.state.fontSize <= 8}
|
||||
>
|
||||
<InlineSVG src={minusUrl} alt="Decrease Font Size" />
|
||||
<h6 className="preference__label">Decrease</h6>
|
||||
<MinusIcon focusable="false" aria-hidden="true" />
|
||||
<h6 className="preference__label">{this.props.t('Decrease')}</h6>
|
||||
</button>
|
||||
<form onSubmit={this.onFontInputSubmit}>
|
||||
<input
|
||||
|
@ -171,12 +171,12 @@ class Preferences extends React.Component {
|
|||
aria-label="increase font size"
|
||||
disabled={this.state.fontSize >= 36}
|
||||
>
|
||||
<InlineSVG src={plusUrl} alt="Increase Font Size" />
|
||||
<h6 className="preference__label">Increase</h6>
|
||||
<PlusIcon focusable="false" aria-hidden="true" />
|
||||
<h6 className="preference__label">{this.props.t('Increase')}</h6>
|
||||
</button>
|
||||
</div>
|
||||
<div className="preference">
|
||||
<h4 className="preference__title">Autosave</h4>
|
||||
<h4 className="preference__title">{this.props.t('Autosave')}</h4>
|
||||
<div className="preference__options">
|
||||
<input
|
||||
type="radio"
|
||||
|
@ -188,7 +188,7 @@ class Preferences extends React.Component {
|
|||
value="On"
|
||||
checked={this.props.autosave}
|
||||
/>
|
||||
<label htmlFor="autosave-on" className="preference__option">On</label>
|
||||
<label htmlFor="autosave-on" className="preference__option">{this.props.t('On')}</label>
|
||||
<input
|
||||
type="radio"
|
||||
onChange={() => this.props.setAutosave(false)}
|
||||
|
@ -199,11 +199,11 @@ class Preferences extends React.Component {
|
|||
value="Off"
|
||||
checked={!this.props.autosave}
|
||||
/>
|
||||
<label htmlFor="autosave-off" className="preference__option">Off</label>
|
||||
<label htmlFor="autosave-off" className="preference__option">{this.props.t('Off')}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="preference">
|
||||
<h4 className="preference__title">Word Wrap</h4>
|
||||
<h4 className="preference__title">{this.props.t('WordWrap')}</h4>
|
||||
<div className="preference__options">
|
||||
<input
|
||||
type="radio"
|
||||
|
@ -215,7 +215,7 @@ class Preferences extends React.Component {
|
|||
value="On"
|
||||
checked={this.props.linewrap}
|
||||
/>
|
||||
<label htmlFor="linewrap-on" className="preference__option">On</label>
|
||||
<label htmlFor="linewrap-on" className="preference__option">{this.props.t('On')}</label>
|
||||
<input
|
||||
type="radio"
|
||||
onChange={() => this.props.setLinewrap(false)}
|
||||
|
@ -226,13 +226,13 @@ class Preferences extends React.Component {
|
|||
value="Off"
|
||||
checked={!this.props.linewrap}
|
||||
/>
|
||||
<label htmlFor="linewrap-off" className="preference__option">Off</label>
|
||||
<label htmlFor="linewrap-off" className="preference__option">{this.props.t('Off')}</label>
|
||||
</div>
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<div className="preference">
|
||||
<h4 className="preference__title">Line numbers</h4>
|
||||
<h4 className="preference__title">{this.props.t('LineNumbers')}</h4>
|
||||
<div className="preference__options">
|
||||
<input
|
||||
type="radio"
|
||||
|
@ -244,7 +244,7 @@ class Preferences extends React.Component {
|
|||
value="On"
|
||||
checked={this.props.lineNumbers}
|
||||
/>
|
||||
<label htmlFor="line-numbers-on" className="preference__option">On</label>
|
||||
<label htmlFor="line-numbers-on" className="preference__option">{this.props.t('On')}</label>
|
||||
<input
|
||||
type="radio"
|
||||
onChange={() => this.props.setLineNumbers(false)}
|
||||
|
@ -255,11 +255,11 @@ class Preferences extends React.Component {
|
|||
value="Off"
|
||||
checked={!this.props.lineNumbers}
|
||||
/>
|
||||
<label htmlFor="line-numbers-off" className="preference__option">Off</label>
|
||||
<label htmlFor="line-numbers-off" className="preference__option">{this.props.t('Off')}</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="preference">
|
||||
<h4 className="preference__title">Lint warning sound</h4>
|
||||
<h4 className="preference__title">{this.props.t('LintWarningSound')}</h4>
|
||||
<div className="preference__options">
|
||||
<input
|
||||
type="radio"
|
||||
|
@ -271,7 +271,7 @@ class Preferences extends React.Component {
|
|||
value="On"
|
||||
checked={this.props.lintWarning}
|
||||
/>
|
||||
<label htmlFor="lint-warning-on" className="preference__option">On</label>
|
||||
<label htmlFor="lint-warning-on" className="preference__option">{this.props.t('On')}</label>
|
||||
<input
|
||||
type="radio"
|
||||
onChange={() => this.props.setLintWarning(false)}
|
||||
|
@ -282,19 +282,19 @@ class Preferences extends React.Component {
|
|||
value="Off"
|
||||
checked={!this.props.lintWarning}
|
||||
/>
|
||||
<label htmlFor="lint-warning-off" className="preference__option">Off</label>
|
||||
<label htmlFor="lint-warning-off" className="preference__option">{this.props.t('Off')}</label>
|
||||
<button
|
||||
className="preference__preview-button"
|
||||
onClick={() => beep.play()}
|
||||
aria-label="preview sound"
|
||||
>
|
||||
Preview sound
|
||||
{this.props.t('PreviewSound')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="preference">
|
||||
<h4 className="preference__title">Accessible text-based canvas</h4>
|
||||
<h6 className="preference__subtitle">Used with screen reader</h6>
|
||||
<h4 className="preference__title">{this.props.t('AccessibleTextBasedCanvas')}</h4>
|
||||
<h6 className="preference__subtitle">{this.props.t('UsedScreenReader')}</h6>
|
||||
|
||||
<div className="preference__options">
|
||||
<input
|
||||
|
@ -308,7 +308,7 @@ class Preferences extends React.Component {
|
|||
value="On"
|
||||
checked={(this.props.textOutput)}
|
||||
/>
|
||||
<label htmlFor="text-output-on" className="preference__option preference__canvas">Plain-text</label>
|
||||
<label htmlFor="text-output-on" className="preference__option preference__canvas">{this.props.t('PlainText')}</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
onChange={(event) => {
|
||||
|
@ -320,7 +320,7 @@ class Preferences extends React.Component {
|
|||
value="On"
|
||||
checked={(this.props.gridOutput)}
|
||||
/>
|
||||
<label htmlFor="table-output-on" className="preference__option preference__canvas">Table-text</label>
|
||||
<label htmlFor="table-output-on" className="preference__option preference__canvas">{this.props.t('TableText')}</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
onChange={(event) => {
|
||||
|
@ -332,7 +332,7 @@ class Preferences extends React.Component {
|
|||
value="On"
|
||||
checked={(this.props.soundOutput)}
|
||||
/>
|
||||
<label htmlFor="sound-output-on" className="preference__option preference__canvas">Sound</label>
|
||||
<label htmlFor="sound-output-on" className="preference__option preference__canvas">{this.props.t('Sound')}</label>
|
||||
</div>
|
||||
</div>
|
||||
</TabPanel>
|
||||
|
@ -361,6 +361,7 @@ Preferences.propTypes = {
|
|||
setLintWarning: PropTypes.func.isRequired,
|
||||
theme: PropTypes.string.isRequired,
|
||||
setTheme: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default Preferences;
|
||||
export default withTranslation('WebEditor')(Preferences);
|
|
@ -22,6 +22,23 @@ import {
|
|||
import { hijackConsoleErrorsScript, startTag, getAllScriptOffsets }
|
||||
from '../../../utils/consoleUtils';
|
||||
|
||||
|
||||
const shouldRenderSketch = (props, prevProps = undefined) => {
|
||||
const { isPlaying, previewIsRefreshing, fullView } = props;
|
||||
|
||||
// if the user explicitly clicks on the play button
|
||||
if (isPlaying && previewIsRefreshing) return true;
|
||||
|
||||
if (!prevProps) return false;
|
||||
|
||||
return (props.isPlaying !== prevProps.isPlaying // if sketch starts or stops playing, want to rerender
|
||||
|| props.isAccessibleOutputPlaying !== prevProps.isAccessibleOutputPlaying // if user switches textoutput preferences
|
||||
|| props.textOutput !== prevProps.textOutput
|
||||
|| props.gridOutput !== prevProps.gridOutput
|
||||
|| props.soundOutput !== prevProps.soundOutput
|
||||
|| (fullView && props.files[0].id !== prevProps.files[0].id));
|
||||
};
|
||||
|
||||
class PreviewFrame extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -30,53 +47,25 @@ class PreviewFrame extends React.Component {
|
|||
|
||||
componentDidMount() {
|
||||
window.addEventListener('message', this.handleConsoleEvent);
|
||||
|
||||
const props = {
|
||||
...this.props,
|
||||
previewIsRefreshing: this.props.previewIsRefreshing,
|
||||
isAccessibleOutputPlaying: this.props.isAccessibleOutputPlaying
|
||||
};
|
||||
if (shouldRenderSketch(props)) this.renderSketch();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
// if sketch starts or stops playing, want to rerender
|
||||
if (this.props.isPlaying !== prevProps.isPlaying) {
|
||||
this.renderSketch();
|
||||
return;
|
||||
}
|
||||
|
||||
// if the user explicitly clicks on the play button
|
||||
if (this.props.isPlaying && this.props.previewIsRefreshing) {
|
||||
this.renderSketch();
|
||||
return;
|
||||
}
|
||||
|
||||
// if user switches textoutput preferences
|
||||
if (this.props.isAccessibleOutputPlaying !== prevProps.isAccessibleOutputPlaying) {
|
||||
this.renderSketch();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.props.textOutput !== prevProps.textOutput) {
|
||||
this.renderSketch();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.props.gridOutput !== prevProps.gridOutput) {
|
||||
this.renderSketch();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.props.soundOutput !== prevProps.soundOutput) {
|
||||
this.renderSketch();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.props.fullView && this.props.files[0].id !== prevProps.files[0].id) {
|
||||
this.renderSketch();
|
||||
}
|
||||
|
||||
if (shouldRenderSketch(this.props, prevProps)) this.renderSketch();
|
||||
// small bug - if autorefresh is on, and the usr changes files
|
||||
// in the sketch, preview will reload
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('message', this.handleConsoleEvent);
|
||||
ReactDOM.unmountComponentAtNode(this.iframeElement.contentDocument.body);
|
||||
const iframeBody = this.iframeElement.contentDocument.body;
|
||||
if (iframeBody) { ReactDOM.unmountComponentAtNode(iframeBody); }
|
||||
}
|
||||
|
||||
handleConsoleEvent(messageEvent) {
|
||||
|
@ -249,16 +238,18 @@ class PreviewFrame extends React.Component {
|
|||
jsFileStrings.forEach((jsFileString) => {
|
||||
if (jsFileString.match(MEDIA_FILE_QUOTED_REGEX)) {
|
||||
const filePath = jsFileString.substr(1, jsFileString.length - 2);
|
||||
const quoteCharacter = jsFileString.substr(0, 1);
|
||||
const resolvedFile = resolvePathToFile(filePath, files);
|
||||
|
||||
if (resolvedFile) {
|
||||
if (resolvedFile.url) {
|
||||
newContent = newContent.replace(filePath, resolvedFile.url);
|
||||
newContent = newContent.replace(jsFileString, quoteCharacter + resolvedFile.url + quoteCharacter);
|
||||
} else if (resolvedFile.name.match(PLAINTEXT_FILE_REGEX)) {
|
||||
// could also pull file from API instead of using bloburl
|
||||
const blobURL = getBlobUrl(resolvedFile);
|
||||
this.props.setBlobUrl(resolvedFile, blobURL);
|
||||
const filePathRegex = new RegExp(filePath, 'gi');
|
||||
newContent = newContent.replace(filePathRegex, blobURL);
|
||||
|
||||
newContent = newContent.replace(jsFileString, quoteCharacter + blobURL + quoteCharacter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -274,10 +265,11 @@ class PreviewFrame extends React.Component {
|
|||
cssFileStrings.forEach((cssFileString) => {
|
||||
if (cssFileString.match(MEDIA_FILE_QUOTED_REGEX)) {
|
||||
const filePath = cssFileString.substr(1, cssFileString.length - 2);
|
||||
const quoteCharacter = cssFileString.substr(0, 1);
|
||||
const resolvedFile = resolvePathToFile(filePath, files);
|
||||
if (resolvedFile) {
|
||||
if (resolvedFile.url) {
|
||||
newContent = newContent.replace(filePath, resolvedFile.url);
|
||||
newContent = newContent.replace(cssFileString, quoteCharacter + resolvedFile.url + quoteCharacter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -353,6 +345,8 @@ class PreviewFrame extends React.Component {
|
|||
'preview-frame': true,
|
||||
'preview-frame--full-view': this.props.fullView
|
||||
});
|
||||
const sandboxAttributes =
|
||||
'allow-scripts allow-pointer-lock allow-same-origin allow-popups allow-forms allow-modals allow-downloads';
|
||||
return (
|
||||
<iframe
|
||||
id="canvas_frame"
|
||||
|
@ -362,7 +356,7 @@ class PreviewFrame extends React.Component {
|
|||
frameBorder="0"
|
||||
title="sketch preview"
|
||||
ref={(element) => { this.iframeElement = element; }}
|
||||
sandbox="allow-scripts allow-pointer-lock allow-same-origin allow-popups allow-forms allow-modals"
|
||||
sandbox={sandboxAttributes}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -393,7 +387,7 @@ PreviewFrame.propTypes = {
|
|||
clearConsole: PropTypes.func.isRequired,
|
||||
cmController: PropTypes.shape({
|
||||
getContent: PropTypes.func
|
||||
})
|
||||
}),
|
||||
};
|
||||
|
||||
PreviewFrame.defaultProps = {
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
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');
|
||||
import CheckIcon from '../../../../images/check_encircled.svg';
|
||||
import CloseIcon from '../../../../images/close.svg';
|
||||
|
||||
const Icons = ({ isAdded }) => {
|
||||
const classes = [
|
||||
|
@ -13,9 +12,9 @@ const Icons = ({ isAdded }) => {
|
|||
|
||||
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" />
|
||||
<CloseIcon className="quick-add__remove-icon" role="img" aria-label="Descending" focusable="false" />
|
||||
<CheckIcon className="quick-add__in-icon" role="img" aria-label="Descending" focusable="false" />
|
||||
<CloseIcon className="quick-add__add-icon" role="img" aria-label="Descending" focusable="false" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,22 +6,25 @@ 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 buttonLabel = isAdded ? 'Remove from collection' : 'Add to collection';
|
||||
return (
|
||||
<li className="quick-add__item" onClick={onSelect}> { /* eslint-disable-line */ }
|
||||
<button className="quick-add__item-toggle" onClick={onSelect} aria-label={buttonLabel}>
|
||||
<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,
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import InlineSVG from 'react-inlinesvg';
|
||||
import { throttle } from 'lodash';
|
||||
|
||||
const searchIcon = require('../../../../images/magnifyingglass.svg');
|
||||
import SearchIcon from '../../../../images/magnifyingglass.svg';
|
||||
|
||||
class Searchbar extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -24,20 +23,13 @@ class Searchbar extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
handleSearchEnter = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
this.searchChange();
|
||||
}
|
||||
}
|
||||
|
||||
searchChange = () => {
|
||||
if (this.state.searchValue.trim().length === 0) return;
|
||||
this.props.setSearchTerm(this.state.searchValue.trim());
|
||||
};
|
||||
|
||||
handleSearchChange = (e) => {
|
||||
this.setState({ searchValue: e.target.value }, () => {
|
||||
this.throttledSearchChange(this.state.searchValue);
|
||||
this.throttledSearchChange(this.state.searchValue.trim());
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -46,7 +38,7 @@ class Searchbar extends React.Component {
|
|||
return (
|
||||
<div className={`searchbar ${searchValue === '' ? 'searchbar--is-empty' : ''}`}>
|
||||
<div className="searchbar__button">
|
||||
<InlineSVG className="searchbar__icon" src={searchIcon} />
|
||||
<SearchIcon className="searchbar__icon" focusable="false" aria-hidden="true" />
|
||||
</div>
|
||||
<input
|
||||
className="searchbar__input"
|
||||
|
@ -54,7 +46,6 @@ class Searchbar extends React.Component {
|
|||
value={searchValue}
|
||||
placeholder={this.props.searchLabel}
|
||||
onChange={this.handleSearchChange}
|
||||
onKeyUp={this.handleSearchEnter}
|
||||
/>
|
||||
<button
|
||||
className="searchbar__clear-button"
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import InlineSVG from 'react-inlinesvg';
|
||||
import ConnectedFileNode from './FileNode';
|
||||
|
||||
const downArrowUrl = require('../../../images/down-filled-triangle.svg');
|
||||
import DownArrowIcon from '../../../images/down-filled-triangle.svg';
|
||||
|
||||
class Sidebar extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -69,14 +68,14 @@ class Sidebar extends React.Component {
|
|||
const rootFile = this.props.files.filter(file => file.name === 'root')[0];
|
||||
|
||||
return (
|
||||
<nav className={sidebarClass} title="file-navigation" >
|
||||
<div className="sidebar__header" onContextMenu={this.toggleProjectOptions}>
|
||||
<section className={sidebarClass}>
|
||||
<header className="sidebar__header" onContextMenu={this.toggleProjectOptions}>
|
||||
<h3 className="sidebar__title">
|
||||
<span>Sketch Files</span>
|
||||
</h3>
|
||||
<div className="sidebar__icons">
|
||||
<button
|
||||
aria-label="add file or folder"
|
||||
aria-label="Toggle open/close sketch file options"
|
||||
className="sidebar__add"
|
||||
tabIndex="0"
|
||||
ref={(element) => { this.sidebarOptions = element; }}
|
||||
|
@ -84,7 +83,7 @@ class Sidebar extends React.Component {
|
|||
onBlur={this.onBlurComponent}
|
||||
onFocus={this.onFocusComponent}
|
||||
>
|
||||
<InlineSVG src={downArrowUrl} />
|
||||
<DownArrowIcon focusable="false" aria-hidden="true" />
|
||||
</button>
|
||||
<ul className="sidebar__project-options">
|
||||
<li>
|
||||
|
@ -131,12 +130,12 @@ class Sidebar extends React.Component {
|
|||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<ConnectedFileNode
|
||||
id={rootFile.id}
|
||||
canEdit={canEditProject}
|
||||
/>
|
||||
</nav>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ import format from 'date-fns/format';
|
|||
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 { Link } from 'react-router';
|
||||
import { bindActionCreators } from 'redux';
|
||||
|
@ -19,9 +18,9 @@ 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 arrowDown = require('../../../images/sort-arrow-down.svg');
|
||||
const downFilledTriangle = require('../../../images/down-filled-triangle.svg');
|
||||
import ArrowUpIcon from '../../../images/sort-arrow-up.svg';
|
||||
import ArrowDownIcon from '../../../images/sort-arrow-down.svg';
|
||||
import DownFilledTriangleIcon from '../../../images/down-filled-triangle.svg';
|
||||
|
||||
class SketchListRowBase extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -168,8 +167,9 @@ class SketchListRowBase extends React.Component {
|
|||
onClick={this.toggleOptions}
|
||||
onBlur={this.onBlurComponent}
|
||||
onFocus={this.onFocusComponent}
|
||||
aria-label="Toggle Open/Close Sketch Options"
|
||||
>
|
||||
<InlineSVG src={downFilledTriangle} alt="Menu" />
|
||||
<DownFilledTriangleIcon focusable="false" aria-hidden="true" />
|
||||
</button>
|
||||
{optionsOpen &&
|
||||
<ul
|
||||
|
@ -326,15 +326,15 @@ class SketchList extends React.Component {
|
|||
super(props);
|
||||
this.props.getProjects(this.props.username);
|
||||
this.props.resetSorting();
|
||||
this._renderFieldHeader = this._renderFieldHeader.bind(this);
|
||||
|
||||
this.state = {
|
||||
isInitialDataLoad: true,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.sketches !== nextProps.sketches && Array.isArray(nextProps.sketches)) {
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.sketches !== prevProps.sketches && Array.isArray(this.props.sketches)) {
|
||||
// eslint-disable-next-line react/no-did-update-set-state
|
||||
this.setState({
|
||||
isInitialDataLoad: false,
|
||||
});
|
||||
|
@ -368,21 +368,43 @@ class SketchList extends React.Component {
|
|||
return null;
|
||||
}
|
||||
|
||||
_renderFieldHeader(fieldName, displayName) {
|
||||
_getButtonLabel = (fieldName, displayName) => {
|
||||
const { field, direction } = this.props.sorting;
|
||||
let buttonLabel;
|
||||
if (field !== fieldName) {
|
||||
if (field === 'name') {
|
||||
buttonLabel = `Sort by ${displayName} ascending.`;
|
||||
} else {
|
||||
buttonLabel = `Sort by ${displayName} descending.`;
|
||||
}
|
||||
} else if (direction === SortingActions.DIRECTION.ASC) {
|
||||
buttonLabel = `Sort by ${displayName} descending.`;
|
||||
} else {
|
||||
buttonLabel = `Sort by ${displayName} ascending.`;
|
||||
}
|
||||
return buttonLabel;
|
||||
}
|
||||
|
||||
_renderFieldHeader = (fieldName, displayName) => {
|
||||
const { field, direction } = this.props.sorting;
|
||||
const headerClass = classNames({
|
||||
'sketches-table__header': true,
|
||||
'sketches-table__header--selected': field === fieldName
|
||||
});
|
||||
const buttonLabel = this._getButtonLabel(fieldName, displayName);
|
||||
return (
|
||||
<th scope="col">
|
||||
<button className="sketch-list__sort-button" onClick={() => this.props.toggleDirectionForField(fieldName)}>
|
||||
<button
|
||||
className="sketch-list__sort-button"
|
||||
onClick={() => this.props.toggleDirectionForField(fieldName)}
|
||||
aria-label={buttonLabel}
|
||||
>
|
||||
<span className={headerClass}>{displayName}</span>
|
||||
{field === fieldName && direction === SortingActions.DIRECTION.ASC &&
|
||||
<InlineSVG src={arrowUp} />
|
||||
<ArrowUpIcon role="img" aria-label="Ascending" focusable="false" />
|
||||
}
|
||||
{field === fieldName && direction === SortingActions.DIRECTION.DESC &&
|
||||
<InlineSVG src={arrowDown} />
|
||||
<ArrowDownIcon role="img" aria-label="Descending" focusable="false" />
|
||||
}
|
||||
</button>
|
||||
</th>
|
||||
|
@ -392,7 +414,7 @@ class SketchList extends React.Component {
|
|||
render() {
|
||||
const username = this.props.username !== undefined ? this.props.username : this.props.user.username;
|
||||
return (
|
||||
<div className="sketches-table-container">
|
||||
<article className="sketches-table-container">
|
||||
<Helmet>
|
||||
<title>{this.getSketchesTitle()}</title>
|
||||
</Helmet>
|
||||
|
@ -435,7 +457,7 @@ class SketchList extends React.Component {
|
|||
/>
|
||||
</Overlay>
|
||||
}
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,19 +2,20 @@ import PropTypes from 'prop-types';
|
|||
import React from 'react';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import InlineSVG from 'react-inlinesvg';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import * as ToastActions from '../actions/toast';
|
||||
|
||||
const exitUrl = require('../../../images/exit.svg');
|
||||
import ExitIcon from '../../../images/exit.svg';
|
||||
|
||||
function Toast(props) {
|
||||
const { t } = useTranslation('WebEditor');
|
||||
return (
|
||||
<section className="toast">
|
||||
<p>
|
||||
{props.text}
|
||||
{t(props.text)}
|
||||
</p>
|
||||
<button className="toast__close" onClick={props.hideToast}>
|
||||
<InlineSVG src={exitUrl} alt="Close Keyboard Shortcuts Overlay" />
|
||||
<button className="toast__close" onClick={props.hideToast} aria-label="Close Alert" >
|
||||
<ExitIcon focusable="false" aria-hidden="true" />
|
||||
</button>
|
||||
</section>
|
||||
);
|
||||
|
|
|
@ -3,37 +3,50 @@ import React from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import { Link } from 'react-router';
|
||||
import classNames from 'classnames';
|
||||
import InlineSVG from 'react-inlinesvg';
|
||||
|
||||
import * as IDEActions from '../actions/ide';
|
||||
import * as preferenceActions from '../actions/preferences';
|
||||
import * as projectActions from '../actions/project';
|
||||
|
||||
const playUrl = require('../../../images/play.svg');
|
||||
const stopUrl = require('../../../images/stop.svg');
|
||||
const preferencesUrl = require('../../../images/preferences.svg');
|
||||
const editProjectNameUrl = require('../../../images/pencil.svg');
|
||||
import PlayIcon from '../../../images/play.svg';
|
||||
import StopIcon from '../../../images/stop.svg';
|
||||
import PreferencesIcon from '../../../images/preferences.svg';
|
||||
import EditProjectNameIcon from '../../../images/pencil.svg';
|
||||
|
||||
class Toolbar extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleKeyPress = this.handleKeyPress.bind(this);
|
||||
this.handleProjectNameChange = this.handleProjectNameChange.bind(this);
|
||||
this.handleProjectNameSave = this.handleProjectNameSave.bind(this);
|
||||
|
||||
this.state = {
|
||||
projectNameInputValue: props.project.name,
|
||||
};
|
||||
}
|
||||
|
||||
handleKeyPress(event) {
|
||||
if (event.key === 'Enter') {
|
||||
this.props.hideEditProjectName();
|
||||
this.projectNameInput.blur();
|
||||
}
|
||||
}
|
||||
|
||||
handleProjectNameChange(event) {
|
||||
this.props.setProjectName(event.target.value);
|
||||
this.setState({ projectNameInputValue: event.target.value });
|
||||
}
|
||||
|
||||
validateProjectName() {
|
||||
if ((this.props.project.name.trim()).length === 0) {
|
||||
this.props.setProjectName(this.originalProjectName);
|
||||
handleProjectNameSave() {
|
||||
const newProjectName = this.state.projectNameInputValue.trim();
|
||||
if (newProjectName.length === 0) {
|
||||
this.setState({
|
||||
projectNameInputValue: this.props.project.name,
|
||||
});
|
||||
} else {
|
||||
this.props.setProjectName(newProjectName);
|
||||
this.props.hideEditProjectName();
|
||||
if (this.props.project.id) {
|
||||
this.props.saveProject();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -61,6 +74,8 @@ class Toolbar extends React.Component {
|
|||
'toolbar__project-name-container--editing': this.props.project.isEditingName
|
||||
});
|
||||
|
||||
const canEditProjectName = this.canEditProjectName();
|
||||
|
||||
return (
|
||||
<div className="toolbar">
|
||||
<button
|
||||
|
@ -70,25 +85,25 @@ class Toolbar extends React.Component {
|
|||
this.props.setTextOutput(true);
|
||||
this.props.setGridOutput(true);
|
||||
}}
|
||||
aria-label="play sketch"
|
||||
aria-label="Play sketch"
|
||||
disabled={this.props.infiniteLoop}
|
||||
>
|
||||
<InlineSVG src={playUrl} alt="Play Sketch" />
|
||||
<PlayIcon focusable="false" aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
className={playButtonClass}
|
||||
onClick={this.props.startSketch}
|
||||
aria-label="play only visual sketch"
|
||||
aria-label="Play only visual sketch"
|
||||
disabled={this.props.infiniteLoop}
|
||||
>
|
||||
<InlineSVG src={playUrl} alt="Play only visual Sketch" />
|
||||
<PlayIcon focusable="false" aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
className={stopButtonClass}
|
||||
onClick={this.props.stopSketch}
|
||||
aria-label="stop sketch"
|
||||
aria-label="Stop sketch"
|
||||
>
|
||||
<InlineSVG src={stopUrl} alt="Stop Sketch" />
|
||||
<StopIcon focusable="false" aria-hidden="true" />
|
||||
</button>
|
||||
<div className="toolbar__autorefresh">
|
||||
<input
|
||||
|
@ -104,38 +119,36 @@ class Toolbar extends React.Component {
|
|||
</label>
|
||||
</div>
|
||||
<div className={nameContainerClass}>
|
||||
<a
|
||||
<button
|
||||
className="toolbar__project-name"
|
||||
href={this.props.owner ? `/${this.props.owner.username}/sketches/${this.props.project.id}` : ''}
|
||||
onClick={(e) => {
|
||||
if (this.canEditProjectName()) {
|
||||
e.preventDefault();
|
||||
this.originalProjectName = this.props.project.name;
|
||||
onClick={() => {
|
||||
if (canEditProjectName) {
|
||||
this.props.showEditProjectName();
|
||||
setTimeout(() => this.projectNameInput.focus(), 0);
|
||||
}
|
||||
}}
|
||||
disabled={!canEditProjectName}
|
||||
aria-label="Edit sketch name"
|
||||
>
|
||||
<span>{this.props.project.name}</span>
|
||||
{
|
||||
this.canEditProjectName() &&
|
||||
<InlineSVG className="toolbar__edit-name-button" src={editProjectNameUrl} alt="Edit Project Name" />
|
||||
canEditProjectName &&
|
||||
<EditProjectNameIcon
|
||||
className="toolbar__edit-name-button"
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
}
|
||||
</a>
|
||||
</button>
|
||||
<input
|
||||
type="text"
|
||||
maxLength="128"
|
||||
className="toolbar__project-name-input"
|
||||
value={this.props.project.name}
|
||||
aria-label="New sketch name"
|
||||
value={this.state.projectNameInputValue}
|
||||
onChange={this.handleProjectNameChange}
|
||||
ref={(element) => { this.projectNameInput = element; }}
|
||||
onBlur={() => {
|
||||
this.validateProjectName();
|
||||
this.props.hideEditProjectName();
|
||||
if (this.props.project.id) {
|
||||
this.props.saveProject();
|
||||
}
|
||||
}}
|
||||
onBlur={this.handleProjectNameSave}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
/>
|
||||
{(() => { // eslint-disable-line
|
||||
|
@ -151,9 +164,9 @@ class Toolbar extends React.Component {
|
|||
<button
|
||||
className={preferencesButtonClass}
|
||||
onClick={this.props.openPreferences}
|
||||
aria-label="preferences"
|
||||
aria-label="Open Preferences"
|
||||
>
|
||||
<InlineSVG src={preferencesUrl} alt="Preferences" />
|
||||
<PreferencesIcon focusable="false" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
@ -210,4 +223,5 @@ const mapDispatchToProps = {
|
|||
...projectActions,
|
||||
};
|
||||
|
||||
export const ToolbarComponent = Toolbar;
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Toolbar);
|
||||
|
|
84
client/modules/IDE/components/Toolbar.test.jsx
Normal file
|
@ -0,0 +1,84 @@
|
|||
import React from 'react';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import lodash from 'lodash';
|
||||
|
||||
|
||||
import { ToolbarComponent } from './Toolbar';
|
||||
|
||||
const renderComponent = (extraProps = {}) => {
|
||||
const props = lodash.merge({
|
||||
isPlaying: false,
|
||||
preferencesIsVisible: false,
|
||||
stopSketch: jest.fn(),
|
||||
setProjectName: jest.fn(),
|
||||
openPreferences: jest.fn(),
|
||||
showEditProjectName: jest.fn(),
|
||||
hideEditProjectName: jest.fn(),
|
||||
infiniteLoop: false,
|
||||
autorefresh: false,
|
||||
setAutorefresh: jest.fn(),
|
||||
setTextOutput: jest.fn(),
|
||||
setGridOutput: jest.fn(),
|
||||
startSketch: jest.fn(),
|
||||
startAccessibleSketch: jest.fn(),
|
||||
saveProject: jest.fn(),
|
||||
currentUser: 'me',
|
||||
originalProjectName: 'testname',
|
||||
|
||||
owner: {
|
||||
username: 'me'
|
||||
},
|
||||
project: {
|
||||
name: 'testname',
|
||||
isEditingName: false,
|
||||
id: 'id',
|
||||
},
|
||||
}, extraProps);
|
||||
|
||||
render(<ToolbarComponent {...props} />);
|
||||
|
||||
return props;
|
||||
};
|
||||
|
||||
describe('<ToolbarComponent />', () => {
|
||||
it('sketch owner can switch to sketch name editing mode', async () => {
|
||||
const props = renderComponent();
|
||||
const sketchName = screen.getByLabelText('Edit sketch name');
|
||||
|
||||
fireEvent.click(sketchName);
|
||||
|
||||
await waitFor(() => expect(props.showEditProjectName).toHaveBeenCalled());
|
||||
});
|
||||
|
||||
it('non-owner can\t switch to sketch editing mode', async () => {
|
||||
const props = renderComponent({ currentUser: 'not-me' });
|
||||
const sketchName = screen.getByLabelText('Edit sketch name');
|
||||
|
||||
fireEvent.click(sketchName);
|
||||
|
||||
expect(sketchName).toBeDisabled();
|
||||
await waitFor(() => expect(props.showEditProjectName).not.toHaveBeenCalled());
|
||||
});
|
||||
|
||||
it('sketch owner can change name', async () => {
|
||||
const props = renderComponent({ project: { isEditingName: true } });
|
||||
|
||||
const sketchNameInput = screen.getByLabelText('New sketch name');
|
||||
fireEvent.change(sketchNameInput, { target: { value: 'my new sketch name' } });
|
||||
fireEvent.blur(sketchNameInput);
|
||||
|
||||
await waitFor(() => expect(props.setProjectName).toHaveBeenCalledWith('my new sketch name'));
|
||||
await waitFor(() => expect(props.saveProject).toHaveBeenCalled());
|
||||
});
|
||||
|
||||
it('sketch owner can\'t change to empty name', async () => {
|
||||
const props = renderComponent({ project: { isEditingName: true } });
|
||||
|
||||
const sketchNameInput = screen.getByLabelText('New sketch name');
|
||||
fireEvent.change(sketchNameInput, { target: { value: '' } });
|
||||
fireEvent.blur(sketchNameInput);
|
||||
|
||||
await waitFor(() => expect(props.setProjectName).not.toHaveBeenCalled());
|
||||
await waitFor(() => expect(props.saveProject).not.toHaveBeenCalled());
|
||||
});
|
||||
});
|
|
@ -2,14 +2,13 @@ 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 getConfig from '../../../utils/getConfig';
|
||||
import FileUploader from './FileUploader';
|
||||
import { getreachedTotalSizeLimit } from '../selectors/users';
|
||||
import exitUrl from '../../../images/exit.svg';
|
||||
import ExitIcon from '../../../images/exit.svg';
|
||||
|
||||
const __process = (typeof global !== 'undefined' ? global : window).process;
|
||||
const limit = __process.env.UPLOAD_LIMIT || 250000000;
|
||||
const limit = getConfig('UPLOAD_LIMIT') || 250000000;
|
||||
const limitText = prettyBytes(limit);
|
||||
|
||||
class UploadFileModal extends React.Component {
|
||||
|
@ -33,8 +32,12 @@ class UploadFileModal extends React.Component {
|
|||
<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
|
||||
className="modal__exit-button"
|
||||
onClick={this.props.closeModal}
|
||||
aria-label="Close upload file modal"
|
||||
>
|
||||
<ExitIcon focusable="false" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
{ this.props.reachedTotalSizeLimit &&
|
||||
|
|
|
@ -10,7 +10,7 @@ import * as ProjectActions from '../actions/project';
|
|||
|
||||
class FullView extends React.Component {
|
||||
componentDidMount() {
|
||||
this.props.getProject(this.props.params.project_id);
|
||||
this.props.getProject(this.props.params.project_id, this.props.params.username);
|
||||
}
|
||||
|
||||
ident = () => {}
|
||||
|
@ -25,7 +25,7 @@ class FullView extends React.Component {
|
|||
owner={{ username: this.props.project.owner ? `${this.props.project.owner.username}` : '' }}
|
||||
project={{ name: this.props.project.name, id: this.props.params.project_id }}
|
||||
/>
|
||||
<div className="preview-frame-holder">
|
||||
<main className="preview-frame-holder">
|
||||
<PreviewFrame
|
||||
htmlFile={this.props.htmlFile}
|
||||
jsFiles={this.props.jsFiles}
|
||||
|
@ -48,7 +48,7 @@ class FullView extends React.Component {
|
|||
expandConsole={this.ident}
|
||||
clearConsole={this.ident}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -56,7 +56,8 @@ class FullView extends React.Component {
|
|||
|
||||
FullView.propTypes = {
|
||||
params: PropTypes.shape({
|
||||
project_id: PropTypes.string
|
||||
project_id: PropTypes.string,
|
||||
username: PropTypes.string
|
||||
}).isRequired,
|
||||
project: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
|
|
|
@ -3,13 +3,14 @@ import React from 'react';
|
|||
import { bindActionCreators } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRouter } from 'react-router';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import SplitPane from 'react-split-pane';
|
||||
import Editor from '../components/Editor';
|
||||
import Sidebar from '../components/Sidebar';
|
||||
import PreviewFrame from '../components/PreviewFrame';
|
||||
import Toolbar from '../components/Toolbar';
|
||||
import Preferences from '../components/Preferences';
|
||||
import Preferences from '../components/Preferences/index';
|
||||
import NewFileModal from '../components/NewFileModal';
|
||||
import NewFolderModal from '../components/NewFolderModal';
|
||||
import UploadFileModal from '../components/UploadFileModal';
|
||||
|
@ -34,11 +35,38 @@ import AddToCollectionList from '../components/AddToCollectionList';
|
|||
import Feedback from '../components/Feedback';
|
||||
import { CollectionSearchbar } from '../components/Searchbar';
|
||||
|
||||
function getTitle(props) {
|
||||
const { id } = props.project;
|
||||
return id ? `p5.js Web Editor | ${props.project.name}` : 'p5.js Web Editor';
|
||||
}
|
||||
|
||||
function isUserOwner(props) {
|
||||
return props.project.owner && props.project.owner.id === props.user.id;
|
||||
}
|
||||
|
||||
function warnIfUnsavedChanges(props) { // eslint-disable-line
|
||||
const { route } = props.route;
|
||||
if (route && (route.action === 'PUSH' && (route.pathname === '/login' || route.pathname === '/signup'))) {
|
||||
// don't warn
|
||||
props.persistState();
|
||||
window.onbeforeunload = null;
|
||||
} else if (route && (props.location.pathname === '/login' || props.location.pathname === '/signup')) {
|
||||
// don't warn
|
||||
props.persistState();
|
||||
window.onbeforeunload = null;
|
||||
} else if (props.ide.unsavedChanges) {
|
||||
if (!window.confirm(props.t('WarningUnsavedChanges'))) {
|
||||
return false;
|
||||
}
|
||||
props.setUnsavedChanges(false);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class IDEView extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleGlobalKeydown = this.handleGlobalKeydown.bind(this);
|
||||
this.warnIfUnsavedChanges = this.warnIfUnsavedChanges.bind(this);
|
||||
|
||||
this.state = {
|
||||
consoleSize: props.ide.consoleIsExpanded ? 150 : 29,
|
||||
|
@ -53,18 +81,18 @@ class IDEView extends React.Component {
|
|||
|
||||
this.props.stopSketch();
|
||||
if (this.props.params.project_id) {
|
||||
const id = this.props.params.project_id;
|
||||
const { project_id: id, username } = this.props.params;
|
||||
if (id !== this.props.project.id) {
|
||||
this.props.getProject(id);
|
||||
this.props.getProject(id, username);
|
||||
}
|
||||
}
|
||||
|
||||
this.isMac = navigator.userAgent.toLowerCase().indexOf('mac') !== -1;
|
||||
document.addEventListener('keydown', this.handleGlobalKeydown, false);
|
||||
|
||||
this.props.router.setRouteLeaveHook(this.props.route, route => this.warnIfUnsavedChanges(route));
|
||||
this.props.router.setRouteLeaveHook(this.props.route, this.handleUnsavedChanges);
|
||||
|
||||
window.onbeforeunload = () => this.warnIfUnsavedChanges();
|
||||
window.onbeforeunload = this.handleUnsavedChanges;
|
||||
|
||||
this.autosaveInterval = null;
|
||||
}
|
||||
|
@ -92,7 +120,7 @@ class IDEView extends React.Component {
|
|||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.isUserOwner() && this.props.project.id) {
|
||||
if (isUserOwner(this.props) && this.props.project.id) {
|
||||
if (this.props.preferences.autosave && this.props.ide.unsavedChanges && !this.props.ide.justOpenedProject) {
|
||||
if (
|
||||
this.props.selectedFile.name === prevProps.selectedFile.name &&
|
||||
|
@ -113,7 +141,7 @@ class IDEView extends React.Component {
|
|||
}
|
||||
|
||||
if (this.props.route.path !== prevProps.route.path) {
|
||||
this.props.router.setRouteLeaveHook(this.props.route, route => this.warnIfUnsavedChanges(route));
|
||||
this.props.router.setRouteLeaveHook(this.props.route, () => warnIfUnsavedChanges(this.props));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -123,16 +151,12 @@ class IDEView extends React.Component {
|
|||
this.autosaveInterval = null;
|
||||
}
|
||||
|
||||
isUserOwner() {
|
||||
return this.props.project.owner && this.props.project.owner.id === this.props.user.id;
|
||||
}
|
||||
|
||||
handleGlobalKeydown(e) {
|
||||
// 83 === s
|
||||
if (e.keyCode === 83 && ((e.metaKey && this.isMac) || (e.ctrlKey && !this.isMac))) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (this.isUserOwner() || (this.props.user.authenticated && !this.props.project.owner)) {
|
||||
if (isUserOwner(this.props) || (this.props.user.authenticated && !this.props.project.owner)) {
|
||||
this.props.saveProject(this.cmController.getContent());
|
||||
} else if (this.props.user.authenticated) {
|
||||
this.props.cloneProject();
|
||||
|
@ -170,42 +194,34 @@ class IDEView extends React.Component {
|
|||
} else {
|
||||
this.props.expandConsole();
|
||||
}
|
||||
} else if (e.keyCode === 27) {
|
||||
if (this.props.ide.newFolderModalVisible) {
|
||||
this.props.closeNewFolderModal();
|
||||
} else if (this.props.ide.uploadFileModalVisible) {
|
||||
this.props.closeUploadFileModal();
|
||||
} else if (this.props.ide.modalIsVisible) {
|
||||
this.props.closeNewFileModal();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
warnIfUnsavedChanges(route) { // eslint-disable-line
|
||||
if (route && (route.action === 'PUSH' && (route.pathname === '/login' || route.pathname === '/signup'))) {
|
||||
// don't warn
|
||||
this.props.persistState();
|
||||
window.onbeforeunload = null;
|
||||
} else if (route && (this.props.location.pathname === '/login' || this.props.location.pathname === '/signup')) {
|
||||
// don't warn
|
||||
this.props.persistState();
|
||||
window.onbeforeunload = null;
|
||||
} else if (this.props.ide.unsavedChanges) {
|
||||
if (!window.confirm('Are you sure you want to leave this page? You have unsaved changes.')) {
|
||||
return false;
|
||||
}
|
||||
this.props.setUnsavedChanges(false);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
handleUnsavedChanges = () => warnIfUnsavedChanges(this.props);
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="ide">
|
||||
<Helmet>
|
||||
<title>p5.js Web Editor | {this.props.project.name}</title>
|
||||
<title>{getTitle(this.props)}</title>
|
||||
</Helmet>
|
||||
{this.props.toast.isVisible && <Toast />}
|
||||
<Nav
|
||||
warnIfUnsavedChanges={this.warnIfUnsavedChanges}
|
||||
warnIfUnsavedChanges={this.handleUnsavedChanges}
|
||||
cmController={this.cmController}
|
||||
/>
|
||||
<Toolbar />
|
||||
<Toolbar key={this.props.project.id} />
|
||||
{this.props.ide.preferencesIsVisible &&
|
||||
<Overlay
|
||||
title="Settings"
|
||||
title={this.props.t('Settings')}
|
||||
ariaLabel="settings"
|
||||
closeOverlay={this.props.closePreferences}
|
||||
>
|
||||
|
@ -231,7 +247,7 @@ class IDEView extends React.Component {
|
|||
/>
|
||||
</Overlay>
|
||||
}
|
||||
<div className="editor-preview-container">
|
||||
<main className="editor-preview-container">
|
||||
<SplitPane
|
||||
split="vertical"
|
||||
size={this.state.sidebarSize}
|
||||
|
@ -300,7 +316,7 @@ class IDEView extends React.Component {
|
|||
isExpanded={this.props.ide.sidebarIsExpanded}
|
||||
expandSidebar={this.props.expandSidebar}
|
||||
collapseSidebar={this.props.collapseSidebar}
|
||||
isUserOwner={this.isUserOwner()}
|
||||
isUserOwner={isUserOwner(this.props)}
|
||||
clearConsole={this.props.clearConsole}
|
||||
consoleEvents={this.props.console}
|
||||
showRuntimeErrorWarning={this.props.showRuntimeErrorWarning}
|
||||
|
@ -308,20 +324,11 @@ class IDEView extends React.Component {
|
|||
runtimeErrorWarningVisible={this.props.ide.runtimeErrorWarningVisible}
|
||||
provideController={(ctl) => { this.cmController = ctl; }}
|
||||
/>
|
||||
<Console
|
||||
fontSize={this.props.preferences.fontSize}
|
||||
consoleEvents={this.props.console}
|
||||
isExpanded={this.props.ide.consoleIsExpanded}
|
||||
expandConsole={this.props.expandConsole}
|
||||
collapseConsole={this.props.collapseConsole}
|
||||
clearConsole={this.props.clearConsole}
|
||||
dispatchConsoleEvent={this.props.dispatchConsoleEvent}
|
||||
theme={this.props.preferences.theme}
|
||||
/>
|
||||
<Console />
|
||||
</SplitPane>
|
||||
<div className="preview-frame-holder">
|
||||
<section className="preview-frame-holder">
|
||||
<header className="preview-frame__header">
|
||||
<h2 className="preview-frame__title">Preview</h2>
|
||||
<h2 className="preview-frame__title">{this.props.t('Preview')}</h2>
|
||||
</header>
|
||||
<div className="preview-frame__content">
|
||||
<div className="preview-frame-overlay" ref={(element) => { this.overlay = element; }}>
|
||||
|
@ -362,10 +369,10 @@ class IDEView extends React.Component {
|
|||
cmController={this.cmController}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</SplitPane>
|
||||
</SplitPane>
|
||||
</div>
|
||||
</main>
|
||||
{ this.props.ide.modalIsVisible &&
|
||||
<NewFileModal />
|
||||
}
|
||||
|
@ -382,7 +389,7 @@ class IDEView extends React.Component {
|
|||
}
|
||||
{ this.props.location.pathname === '/about' &&
|
||||
<Overlay
|
||||
title="About"
|
||||
title={this.props.t('About')}
|
||||
previousPath={this.props.ide.previousPath}
|
||||
ariaLabel="about"
|
||||
>
|
||||
|
@ -428,7 +435,7 @@ class IDEView extends React.Component {
|
|||
}
|
||||
{this.props.ide.keyboardShortcutVisible &&
|
||||
<Overlay
|
||||
title="Keyboard Shortcuts"
|
||||
title={this.props.t('KeyboardShortcuts')}
|
||||
ariaLabel="keyboard shortcuts"
|
||||
closeOverlay={this.props.closeKeyboardShortcutModal}
|
||||
>
|
||||
|
@ -562,6 +569,7 @@ IDEView.propTypes = {
|
|||
closeProjectOptions: PropTypes.func.isRequired,
|
||||
newFolder: PropTypes.func.isRequired,
|
||||
closeNewFolderModal: PropTypes.func.isRequired,
|
||||
closeNewFileModal: PropTypes.func.isRequired,
|
||||
createFolder: PropTypes.func.isRequired,
|
||||
closeShareModal: PropTypes.func.isRequired,
|
||||
showEditorOptions: PropTypes.func.isRequired,
|
||||
|
@ -590,12 +598,12 @@ IDEView.propTypes = {
|
|||
showErrorModal: PropTypes.func.isRequired,
|
||||
hideErrorModal: PropTypes.func.isRequired,
|
||||
clearPersistedState: PropTypes.func.isRequired,
|
||||
persistState: PropTypes.func.isRequired,
|
||||
showRuntimeErrorWarning: PropTypes.func.isRequired,
|
||||
hideRuntimeErrorWarning: PropTypes.func.isRequired,
|
||||
startSketch: PropTypes.func.isRequired,
|
||||
openUploadFileModal: PropTypes.func.isRequired,
|
||||
closeUploadFileModal: PropTypes.func.isRequired
|
||||
closeUploadFileModal: PropTypes.func.isRequired,
|
||||
t: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
function mapStateToProps(state) {
|
||||
|
@ -632,4 +640,4 @@ function mapDispatchToProps(dispatch) {
|
|||
);
|
||||
}
|
||||
|
||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(IDEView));
|
||||
export default withTranslation('WebEditor')(withRouter(connect(mapStateToProps, mapDispatchToProps)(IDEView)));
|
||||
|
|
262
client/modules/IDE/pages/MobileIDEView.jsx
Normal file
|
@ -0,0 +1,262 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRouter } from 'react-router';
|
||||
import { useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
// Imports to be Refactored
|
||||
import { bindActionCreators } from 'redux';
|
||||
import * as FileActions from '../actions/files';
|
||||
import * as IDEActions from '../actions/ide';
|
||||
import * as ProjectActions from '../actions/project';
|
||||
import * as EditorAccessibilityActions from '../actions/editorAccessibility';
|
||||
import * as PreferencesActions from '../actions/preferences';
|
||||
import * as UserActions from '../../User/actions';
|
||||
import * as ToastActions from '../actions/toast';
|
||||
import * as ConsoleActions from '../actions/console';
|
||||
import { getHTMLFile } from '../reducers/files';
|
||||
|
||||
// Local Imports
|
||||
import Editor from '../components/Editor';
|
||||
import { PreferencesIcon, PlayIcon, ExitIcon } from '../../../common/icons';
|
||||
|
||||
import IconButton from '../../../components/mobile/IconButton';
|
||||
import Header from '../../../components/mobile/Header';
|
||||
import Screen from '../../../components/mobile/MobileScreen';
|
||||
import Footer from '../../../components/mobile/Footer';
|
||||
import IDEWrapper from '../../../components/mobile/IDEWrapper';
|
||||
import Console from '../components/Console';
|
||||
import { remSize } from '../../../theme';
|
||||
import ActionStrip from '../../../components/mobile/ActionStrip';
|
||||
|
||||
const isUserOwner = ({ project, user }) => (project.owner && project.owner.id === user.id);
|
||||
|
||||
|
||||
const Expander = styled.div`
|
||||
height: ${props => (props.expanded ? remSize(160) : remSize(27))};
|
||||
`;
|
||||
|
||||
const MobileIDEView = (props) => {
|
||||
const {
|
||||
preferences, ide, editorAccessibility, project, updateLintMessage, clearLintMessage,
|
||||
selectedFile, updateFileContent, files,
|
||||
closeEditorOptions, showEditorOptions, showKeyboardShortcutModal, setUnsavedChanges,
|
||||
startRefreshSketch, stopSketch, expandSidebar, collapseSidebar, clearConsole, console,
|
||||
showRuntimeErrorWarning, hideRuntimeErrorWarning, startSketch
|
||||
} = props;
|
||||
|
||||
const [tmController, setTmController] = useState(null); // eslint-disable-line
|
||||
const [overlay, setOverlay] = useState(null); // eslint-disable-line
|
||||
|
||||
return (
|
||||
<Screen fullscreen>
|
||||
<Header
|
||||
title={project.name}
|
||||
subtitle={selectedFile.name}
|
||||
leftButton={
|
||||
<IconButton to="/mobile" icon={ExitIcon} aria-label="Return to original editor" />
|
||||
}
|
||||
>
|
||||
<IconButton
|
||||
to="/mobile/preferences"
|
||||
onClick={() => setOverlay('preferences')}
|
||||
icon={PreferencesIcon}
|
||||
aria-label="Open preferences menu"
|
||||
/>
|
||||
<IconButton to="/mobile/preview" onClick={() => { startSketch(); }} icon={PlayIcon} aria-label="Run sketch" />
|
||||
</Header>
|
||||
|
||||
<IDEWrapper>
|
||||
<Editor
|
||||
lintWarning={preferences.lintWarning}
|
||||
linewrap={preferences.linewrap}
|
||||
lintMessages={editorAccessibility.lintMessages}
|
||||
updateLintMessage={updateLintMessage}
|
||||
clearLintMessage={clearLintMessage}
|
||||
file={selectedFile}
|
||||
updateFileContent={updateFileContent}
|
||||
fontSize={preferences.fontSize}
|
||||
lineNumbers={preferences.lineNumbers}
|
||||
files={files}
|
||||
editorOptionsVisible={ide.editorOptionsVisible}
|
||||
showEditorOptions={showEditorOptions}
|
||||
closeEditorOptions={closeEditorOptions}
|
||||
showKeyboardShortcutModal={showKeyboardShortcutModal}
|
||||
setUnsavedChanges={setUnsavedChanges}
|
||||
isPlaying={ide.isPlaying}
|
||||
theme={preferences.theme}
|
||||
startRefreshSketch={startRefreshSketch}
|
||||
stopSketch={stopSketch}
|
||||
autorefresh={preferences.autorefresh}
|
||||
unsavedChanges={ide.unsavedChanges}
|
||||
projectSavedTime={project.updatedAt}
|
||||
isExpanded={ide.sidebarIsExpanded}
|
||||
expandSidebar={expandSidebar}
|
||||
collapseSidebar={collapseSidebar}
|
||||
isUserOwner={isUserOwner(props)}
|
||||
clearConsole={clearConsole}
|
||||
consoleEvents={console}
|
||||
showRuntimeErrorWarning={showRuntimeErrorWarning}
|
||||
hideRuntimeErrorWarning={hideRuntimeErrorWarning}
|
||||
runtimeErrorWarningVisible={ide.runtimeErrorWarningVisible}
|
||||
provideController={setTmController}
|
||||
/>
|
||||
</IDEWrapper>
|
||||
<Footer>
|
||||
{ide.consoleIsExpanded && <Expander expanded><Console /></Expander>}
|
||||
<ActionStrip />
|
||||
</Footer>
|
||||
</Screen>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
MobileIDEView.propTypes = {
|
||||
|
||||
preferences: PropTypes.shape({
|
||||
fontSize: PropTypes.number.isRequired,
|
||||
autosave: PropTypes.bool.isRequired,
|
||||
linewrap: PropTypes.bool.isRequired,
|
||||
lineNumbers: PropTypes.bool.isRequired,
|
||||
lintWarning: PropTypes.bool.isRequired,
|
||||
textOutput: PropTypes.bool.isRequired,
|
||||
gridOutput: PropTypes.bool.isRequired,
|
||||
soundOutput: PropTypes.bool.isRequired,
|
||||
theme: PropTypes.string.isRequired,
|
||||
autorefresh: PropTypes.bool.isRequired
|
||||
}).isRequired,
|
||||
|
||||
ide: PropTypes.shape({
|
||||
isPlaying: PropTypes.bool.isRequired,
|
||||
isAccessibleOutputPlaying: PropTypes.bool.isRequired,
|
||||
consoleEvent: PropTypes.array,
|
||||
modalIsVisible: PropTypes.bool.isRequired,
|
||||
sidebarIsExpanded: PropTypes.bool.isRequired,
|
||||
consoleIsExpanded: PropTypes.bool.isRequired,
|
||||
preferencesIsVisible: PropTypes.bool.isRequired,
|
||||
projectOptionsVisible: PropTypes.bool.isRequired,
|
||||
newFolderModalVisible: PropTypes.bool.isRequired,
|
||||
shareModalVisible: PropTypes.bool.isRequired,
|
||||
shareModalProjectId: PropTypes.string.isRequired,
|
||||
shareModalProjectName: PropTypes.string.isRequired,
|
||||
shareModalProjectUsername: PropTypes.string.isRequired,
|
||||
editorOptionsVisible: PropTypes.bool.isRequired,
|
||||
keyboardShortcutVisible: PropTypes.bool.isRequired,
|
||||
unsavedChanges: PropTypes.bool.isRequired,
|
||||
infiniteLoop: PropTypes.bool.isRequired,
|
||||
previewIsRefreshing: PropTypes.bool.isRequired,
|
||||
infiniteLoopMessage: PropTypes.string.isRequired,
|
||||
projectSavedTime: PropTypes.string,
|
||||
previousPath: PropTypes.string.isRequired,
|
||||
justOpenedProject: PropTypes.bool.isRequired,
|
||||
errorType: PropTypes.string,
|
||||
runtimeErrorWarningVisible: PropTypes.bool.isRequired,
|
||||
uploadFileModalVisible: PropTypes.bool.isRequired
|
||||
}).isRequired,
|
||||
|
||||
editorAccessibility: PropTypes.shape({
|
||||
lintMessages: PropTypes.array.isRequired,
|
||||
}).isRequired,
|
||||
|
||||
project: PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
owner: PropTypes.shape({
|
||||
username: PropTypes.string,
|
||||
id: PropTypes.string
|
||||
}),
|
||||
updatedAt: PropTypes.string
|
||||
}).isRequired,
|
||||
|
||||
startSketch: PropTypes.func.isRequired,
|
||||
|
||||
updateLintMessage: PropTypes.func.isRequired,
|
||||
|
||||
clearLintMessage: PropTypes.func.isRequired,
|
||||
|
||||
selectedFile: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
content: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired
|
||||
}).isRequired,
|
||||
|
||||
updateFileContent: PropTypes.func.isRequired,
|
||||
|
||||
files: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
content: PropTypes.string.isRequired
|
||||
})).isRequired,
|
||||
|
||||
closeEditorOptions: PropTypes.func.isRequired,
|
||||
|
||||
showEditorOptions: PropTypes.func.isRequired,
|
||||
|
||||
showKeyboardShortcutModal: PropTypes.func.isRequired,
|
||||
|
||||
setUnsavedChanges: PropTypes.func.isRequired,
|
||||
|
||||
startRefreshSketch: PropTypes.func.isRequired,
|
||||
|
||||
stopSketch: PropTypes.func.isRequired,
|
||||
|
||||
expandSidebar: PropTypes.func.isRequired,
|
||||
|
||||
collapseSidebar: PropTypes.func.isRequired,
|
||||
|
||||
clearConsole: PropTypes.func.isRequired,
|
||||
|
||||
console: PropTypes.arrayOf(PropTypes.shape({
|
||||
method: PropTypes.string.isRequired,
|
||||
args: PropTypes.arrayOf(PropTypes.string)
|
||||
})).isRequired,
|
||||
|
||||
showRuntimeErrorWarning: PropTypes.func.isRequired,
|
||||
|
||||
hideRuntimeErrorWarning: PropTypes.func.isRequired,
|
||||
|
||||
user: PropTypes.shape({
|
||||
authenticated: PropTypes.bool.isRequired,
|
||||
id: PropTypes.string,
|
||||
username: PropTypes.string
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
files: state.files,
|
||||
selectedFile: state.files.find(file => file.isSelectedFile) ||
|
||||
state.files.find(file => file.name === 'sketch.js') ||
|
||||
state.files.find(file => file.name !== 'root'),
|
||||
htmlFile: getHTMLFile(state.files),
|
||||
ide: state.ide,
|
||||
preferences: state.preferences,
|
||||
editorAccessibility: state.editorAccessibility,
|
||||
user: state.user,
|
||||
project: state.project,
|
||||
toast: state.toast,
|
||||
console: state.console
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return bindActionCreators(
|
||||
Object.assign(
|
||||
{},
|
||||
EditorAccessibilityActions,
|
||||
FileActions,
|
||||
ProjectActions,
|
||||
IDEActions,
|
||||
PreferencesActions,
|
||||
UserActions,
|
||||
ToastActions,
|
||||
ConsoleActions
|
||||
),
|
||||
dispatch
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(MobileIDEView));
|
|
@ -37,6 +37,11 @@ const getSortedCollections = createSelector(
|
|||
return orderBy(collections, 'name', 'desc');
|
||||
}
|
||||
return orderBy(collections, 'name', 'asc');
|
||||
} else if (field === 'numItems') {
|
||||
if (direction === DIRECTION.DESC) {
|
||||
return orderBy(collections, 'items.length', 'desc');
|
||||
}
|
||||
return orderBy(collections, 'items.length', 'asc');
|
||||
}
|
||||
const sortedCollections = [...collections].sort((a, b) => {
|
||||
const result =
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import getConfig from '../../../utils/getConfig';
|
||||
|
||||
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;
|
||||
const limit = getConfig('UPLOAD_LIMIT') || 250000000;
|
||||
|
||||
export const getCanUploadMedia = createSelector(
|
||||
getAuthenticated,
|
||||
|
|
92
client/modules/Mobile/MobilePreferences.jsx
Normal file
|
@ -0,0 +1,92 @@
|
|||
import React from 'react';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect, useSelector, useDispatch } from 'react-redux';
|
||||
import { withRouter } from 'react-router';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import * as PreferencesActions from '../IDE/actions/preferences';
|
||||
import * as IdeActions from '../IDE/actions/ide';
|
||||
|
||||
import IconButton from '../../components/mobile/IconButton';
|
||||
import Screen from '../../components/mobile/MobileScreen';
|
||||
import Header from '../../components/mobile/Header';
|
||||
import PreferencePicker from '../../components/mobile/PreferencePicker';
|
||||
import { ExitIcon } from '../../common/icons';
|
||||
import { remSize, prop } from '../../theme';
|
||||
import { optionsOnOff, optionsPickOne, preferenceOnOff } from '../IDE/components/Preferences/PreferenceCreators';
|
||||
|
||||
const Content = styled.div`
|
||||
z-index: 0;
|
||||
margin-top: ${remSize(68)};
|
||||
`;
|
||||
|
||||
const SectionHeader = styled.h2`
|
||||
color: ${prop('primaryTextColor')};
|
||||
padding-top: ${remSize(32)};
|
||||
`;
|
||||
|
||||
const SectionSubeader = styled.h3`
|
||||
color: ${prop('primaryTextColor')};
|
||||
`;
|
||||
|
||||
|
||||
const MobilePreferences = () => {
|
||||
// Props
|
||||
const {
|
||||
theme, autosave, linewrap, textOutput, gridOutput, soundOutput, lineNumbers, lintWarning
|
||||
} = useSelector(state => state.preferences);
|
||||
|
||||
// Actions
|
||||
const {
|
||||
setTheme, setAutosave, setLinewrap, setTextOutput, setGridOutput, setSoundOutput, setLineNumbers, setLintWarning,
|
||||
} = bindActionCreators({ ...PreferencesActions, ...IdeActions }, useDispatch());
|
||||
|
||||
|
||||
const generalSettings = [
|
||||
{
|
||||
title: 'Theme',
|
||||
value: theme,
|
||||
options: optionsPickOne('theme', 'light', 'dark', 'contrast'),
|
||||
onSelect: x => setTheme(x) // setTheme
|
||||
},
|
||||
preferenceOnOff('Autosave', autosave, setAutosave, 'autosave'),
|
||||
preferenceOnOff('Word Wrap', linewrap, setLinewrap, 'linewrap')
|
||||
];
|
||||
|
||||
const outputSettings = [
|
||||
preferenceOnOff('Plain-text', textOutput, setTextOutput, 'text output'),
|
||||
preferenceOnOff('Table-text', gridOutput, setGridOutput, 'table output'),
|
||||
preferenceOnOff('Lint Warning Sound', soundOutput, setSoundOutput, 'sound output')
|
||||
];
|
||||
|
||||
const accessibilitySettings = [
|
||||
preferenceOnOff('Line Numbers', lineNumbers, setLineNumbers),
|
||||
preferenceOnOff('Lint Warning Sound', lintWarning, setLintWarning)
|
||||
];
|
||||
|
||||
return (
|
||||
<Screen fullscreen>
|
||||
<section>
|
||||
<Header transparent title="Preferences">
|
||||
<IconButton to="/mobile" icon={ExitIcon} aria-label="Return to ide view" />
|
||||
</Header>
|
||||
<section className="preferences">
|
||||
<Content>
|
||||
<SectionHeader>General Settings</SectionHeader>
|
||||
{ generalSettings.map(option => <PreferencePicker key={`${option.title}wrapper`} {...option} />) }
|
||||
|
||||
<SectionHeader>Accessibility</SectionHeader>
|
||||
{ accessibilitySettings.map(option => <PreferencePicker key={`${option.title}wrapper`} {...option} />) }
|
||||
|
||||
<SectionHeader>Accessible Output</SectionHeader>
|
||||
<SectionSubeader>Used with screen reader</SectionSubeader>
|
||||
{ outputSettings.map(option => <PreferencePicker key={`${option.title}wrapper`} {...option} />) }
|
||||
|
||||
</Content>
|
||||
</section>
|
||||
</section>
|
||||
</Screen>);
|
||||
};
|
||||
|
||||
export default withRouter(MobilePreferences);
|
84
client/modules/Mobile/MobileSketchView.jsx
Normal file
|
@ -0,0 +1,84 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect, useSelector, useDispatch } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
import Header from '../../components/mobile/Header';
|
||||
import IconButton from '../../components/mobile/IconButton';
|
||||
import PreviewFrame from '../IDE/components/PreviewFrame';
|
||||
import Screen from '../../components/mobile/MobileScreen';
|
||||
import Console from '../IDE/components/Console';
|
||||
import * as ProjectActions from '../IDE/actions/project';
|
||||
import * as IDEActions from '../IDE/actions/ide';
|
||||
import * as PreferencesActions from '../IDE/actions/preferences';
|
||||
import * as ConsoleActions from '../IDE/actions/console';
|
||||
import * as FilesActions from '../IDE/actions/files';
|
||||
|
||||
import { getHTMLFile } from '../IDE/reducers/files';
|
||||
|
||||
import { ExitIcon } from '../../common/icons';
|
||||
import { remSize } from '../../theme';
|
||||
import Footer from '../../components/mobile/Footer';
|
||||
|
||||
const Content = styled.div`
|
||||
z-index: 0;
|
||||
margin-top: ${remSize(68)};
|
||||
`;
|
||||
|
||||
const MobileSketchView = (props) => {
|
||||
const { files, ide, preferences } = useSelector(state => state);
|
||||
|
||||
const htmlFile = useSelector(state => getHTMLFile(state.files));
|
||||
const projectName = useSelector(state => state.project.name);
|
||||
const selectedFile = useSelector(state => state.files.find(file => file.isSelectedFile) ||
|
||||
state.files.find(file => file.name === 'sketch.js') ||
|
||||
state.files.find(file => file.name !== 'root'));
|
||||
|
||||
const {
|
||||
setTextOutput, setGridOutput, setSoundOutput, dispatchConsoleEvent,
|
||||
endSketchRefresh, stopSketch, setBlobUrl, expandConsole, clearConsole
|
||||
} = bindActionCreators({
|
||||
...ProjectActions, ...IDEActions, ...PreferencesActions, ...ConsoleActions, ...FilesActions
|
||||
}, useDispatch());
|
||||
|
||||
return (
|
||||
<Screen fullscreen>
|
||||
<Header
|
||||
leftButton={<IconButton to="/mobile" icon={ExitIcon} aria-label="Return to original editor" />}
|
||||
title={projectName}
|
||||
/>
|
||||
<Content>
|
||||
<PreviewFrame
|
||||
htmlFile={htmlFile}
|
||||
files={files}
|
||||
head={<link type="text/css" rel="stylesheet" href="/preview-styles.css" />}
|
||||
|
||||
content={selectedFile.content}
|
||||
|
||||
isPlaying
|
||||
isAccessibleOutputPlaying={ide.isAccessibleOutputPlaying}
|
||||
previewIsRefreshing={ide.previewIsRefreshing}
|
||||
|
||||
textOutput={preferences.textOutput}
|
||||
gridOutput={preferences.gridOutput}
|
||||
soundOutput={preferences.soundOutput}
|
||||
autorefresh={preferences.autorefresh}
|
||||
|
||||
setTextOutput={setTextOutput}
|
||||
setGridOutput={setGridOutput}
|
||||
setSoundOutput={setSoundOutput}
|
||||
dispatchConsoleEvent={dispatchConsoleEvent}
|
||||
endSketchRefresh={endSketchRefresh}
|
||||
stopSketch={stopSketch}
|
||||
setBlobUrl={setBlobUrl}
|
||||
expandConsole={expandConsole}
|
||||
clearConsole={clearConsole}
|
||||
/>
|
||||
</Content>
|
||||
<Footer>
|
||||
<Console />
|
||||
</Footer>
|
||||
</Screen>);
|
||||
};
|
||||
|
||||
export default MobileSketchView;
|
|
@ -1,13 +1,9 @@
|
|||
import { browserHistory } from 'react-router';
|
||||
import axios from 'axios';
|
||||
import * as ActionTypes from '../../constants';
|
||||
import apiClient from '../../utils/apiClient';
|
||||
import { showErrorModal, justOpenedProject } from '../IDE/actions/ide';
|
||||
import { showToast, setToastText } from '../IDE/actions/toast';
|
||||
|
||||
|
||||
const __process = (typeof global !== 'undefined' ? global : window).process;
|
||||
const ROOT_URL = __process.env.API_URL;
|
||||
|
||||
export function authError(error) {
|
||||
return {
|
||||
type: ActionTypes.AUTH_ERROR,
|
||||
|
@ -17,7 +13,7 @@ export function authError(error) {
|
|||
|
||||
export function signUpUser(previousPath, formValues) {
|
||||
return (dispatch) => {
|
||||
axios.post(`${ROOT_URL}/signup`, formValues, { withCredentials: true })
|
||||
apiClient.post('/signup', formValues)
|
||||
.then((response) => {
|
||||
dispatch({
|
||||
type: ActionTypes.AUTH_USER,
|
||||
|
@ -26,12 +22,15 @@ export function signUpUser(previousPath, formValues) {
|
|||
dispatch(justOpenedProject());
|
||||
browserHistory.push(previousPath);
|
||||
})
|
||||
.catch(response => dispatch(authError(response.data.error)));
|
||||
.catch((error) => {
|
||||
const { response } = error;
|
||||
dispatch(authError(response.data.error));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function loginUser(formValues) {
|
||||
return axios.post(`${ROOT_URL}/login`, formValues, { withCredentials: true });
|
||||
return apiClient.post('/login', formValues);
|
||||
}
|
||||
|
||||
export function loginUserSuccess(user) {
|
||||
|
@ -71,7 +70,7 @@ export function validateAndLoginUser(previousPath, formProps, dispatch) {
|
|||
|
||||
export function getUser() {
|
||||
return (dispatch) => {
|
||||
axios.get(`${ROOT_URL}/session`, { withCredentials: true })
|
||||
apiClient.get('/session')
|
||||
.then((response) => {
|
||||
dispatch({
|
||||
type: ActionTypes.AUTH_USER,
|
||||
|
@ -82,7 +81,8 @@ export function getUser() {
|
|||
preferences: response.data.preferences
|
||||
});
|
||||
})
|
||||
.catch((response) => {
|
||||
.catch((error) => {
|
||||
const { response } = error;
|
||||
const message = response.message || response.data.error;
|
||||
dispatch(authError(message));
|
||||
});
|
||||
|
@ -91,14 +91,15 @@ export function getUser() {
|
|||
|
||||
export function validateSession() {
|
||||
return (dispatch, getState) => {
|
||||
axios.get(`${ROOT_URL}/session`, { withCredentials: true })
|
||||
apiClient.get('/session')
|
||||
.then((response) => {
|
||||
const state = getState();
|
||||
if (state.user.username !== response.data.username) {
|
||||
dispatch(showErrorModal('staleSession'));
|
||||
}
|
||||
})
|
||||
.catch((response) => {
|
||||
.catch((error) => {
|
||||
const { response } = error;
|
||||
if (response.status === 404) {
|
||||
dispatch(showErrorModal('staleSession'));
|
||||
}
|
||||
|
@ -108,13 +109,16 @@ export function validateSession() {
|
|||
|
||||
export function logoutUser() {
|
||||
return (dispatch) => {
|
||||
axios.get(`${ROOT_URL}/logout`, { withCredentials: true })
|
||||
apiClient.get('/logout')
|
||||
.then(() => {
|
||||
dispatch({
|
||||
type: ActionTypes.UNAUTH_USER
|
||||
});
|
||||
})
|
||||
.catch(response => dispatch(authError(response.data.error)));
|
||||
.catch((error) => {
|
||||
const { response } = error;
|
||||
dispatch(authError(response.data.error));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -123,14 +127,17 @@ export function initiateResetPassword(formValues) {
|
|||
dispatch({
|
||||
type: ActionTypes.RESET_PASSWORD_INITIATE
|
||||
});
|
||||
axios.post(`${ROOT_URL}/reset-password`, formValues, { withCredentials: true })
|
||||
apiClient.post('/reset-password', formValues)
|
||||
.then(() => {
|
||||
// do nothing
|
||||
})
|
||||
.catch(response => dispatch({
|
||||
type: ActionTypes.ERROR,
|
||||
message: response.data
|
||||
}));
|
||||
.catch((error) => {
|
||||
const { response } = error;
|
||||
dispatch({
|
||||
type: ActionTypes.ERROR,
|
||||
message: response.data
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -139,14 +146,17 @@ export function initiateVerification() {
|
|||
dispatch({
|
||||
type: ActionTypes.EMAIL_VERIFICATION_INITIATE
|
||||
});
|
||||
axios.post(`${ROOT_URL}/verify/send`, {}, { withCredentials: true })
|
||||
apiClient.post('/verify/send', {})
|
||||
.then(() => {
|
||||
// do nothing
|
||||
})
|
||||
.catch(response => dispatch({
|
||||
type: ActionTypes.ERROR,
|
||||
message: response.data
|
||||
}));
|
||||
.catch((error) => {
|
||||
const { response } = error;
|
||||
dispatch({
|
||||
type: ActionTypes.ERROR,
|
||||
message: response.data
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -156,15 +166,18 @@ export function verifyEmailConfirmation(token) {
|
|||
type: ActionTypes.EMAIL_VERIFICATION_VERIFY,
|
||||
state: 'checking',
|
||||
});
|
||||
return axios.get(`${ROOT_URL}/verify?t=${token}`, {}, { withCredentials: true })
|
||||
return apiClient.get(`/verify?t=${token}`, {})
|
||||
.then(response => dispatch({
|
||||
type: ActionTypes.EMAIL_VERIFICATION_VERIFIED,
|
||||
message: response.data,
|
||||
}))
|
||||
.catch(response => dispatch({
|
||||
type: ActionTypes.EMAIL_VERIFICATION_INVALID,
|
||||
message: response.data
|
||||
}));
|
||||
.catch((error) => {
|
||||
const { response } = error;
|
||||
dispatch({
|
||||
type: ActionTypes.EMAIL_VERIFICATION_INVALID,
|
||||
message: response.data
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -177,7 +190,7 @@ export function resetPasswordReset() {
|
|||
|
||||
export function validateResetPasswordToken(token) {
|
||||
return (dispatch) => {
|
||||
axios.get(`${ROOT_URL}/reset-password/${token}`)
|
||||
apiClient.get(`/reset-password/${token}`)
|
||||
.then(() => {
|
||||
// do nothing if the token is valid
|
||||
})
|
||||
|
@ -189,7 +202,7 @@ export function validateResetPasswordToken(token) {
|
|||
|
||||
export function updatePassword(token, formValues) {
|
||||
return (dispatch) => {
|
||||
axios.post(`${ROOT_URL}/reset-password/${token}`, formValues)
|
||||
apiClient.post(`/reset-password/${token}`, formValues)
|
||||
.then((response) => {
|
||||
dispatch(loginUserSuccess(response.data));
|
||||
browserHistory.push('/');
|
||||
|
@ -209,14 +222,17 @@ export function updateSettingsSuccess(user) {
|
|||
|
||||
export function updateSettings(formValues) {
|
||||
return dispatch =>
|
||||
axios.put(`${ROOT_URL}/account`, formValues, { withCredentials: true })
|
||||
apiClient.put('/account', formValues)
|
||||
.then((response) => {
|
||||
dispatch(updateSettingsSuccess(response.data));
|
||||
browserHistory.push('/');
|
||||
dispatch(showToast(5500));
|
||||
dispatch(setToastText('Settings saved.'));
|
||||
})
|
||||
.catch(response => Promise.reject(new Error(response.data.error)));
|
||||
.catch((error) => {
|
||||
const { response } = error;
|
||||
Promise.reject(new Error(response.data.error));
|
||||
});
|
||||
}
|
||||
|
||||
export function createApiKeySuccess(user) {
|
||||
|
@ -228,21 +244,27 @@ export function createApiKeySuccess(user) {
|
|||
|
||||
export function createApiKey(label) {
|
||||
return dispatch =>
|
||||
axios.post(`${ROOT_URL}/account/api-keys`, { label }, { withCredentials: true })
|
||||
apiClient.post('/account/api-keys', { label })
|
||||
.then((response) => {
|
||||
dispatch(createApiKeySuccess(response.data));
|
||||
})
|
||||
.catch(response => Promise.reject(new Error(response.data.error)));
|
||||
.catch((error) => {
|
||||
const { response } = error;
|
||||
Promise.reject(new Error(response.data.error));
|
||||
});
|
||||
}
|
||||
|
||||
export function removeApiKey(keyId) {
|
||||
return dispatch =>
|
||||
axios.delete(`${ROOT_URL}/account/api-keys/${keyId}`, { withCredentials: true })
|
||||
apiClient.delete(`/account/api-keys/${keyId}`)
|
||||
.then((response) => {
|
||||
dispatch({
|
||||
type: ActionTypes.API_KEY_REMOVED,
|
||||
user: response.data
|
||||
});
|
||||
})
|
||||
.catch(response => Promise.reject(new Error(response.data.error)));
|
||||
.catch((error) => {
|
||||
const { response } = error;
|
||||
Promise.reject(new Error(response.data.error));
|
||||
});
|
||||
}
|
||||
|
|