🔀 pull from feature/mobile-files-tab

This commit is contained in:
ghalestrilo 2020-08-17 15:45:55 -03:00
commit 54ae17f29d
17 changed files with 238 additions and 34 deletions

View File

@ -17,6 +17,12 @@ import Code from '../images/code.svg';
import Save from '../images/save.svg';
import Terminal from '../images/terminal.svg';
import Folder from '../images/folder-padded.svg';
import CircleTerminal from '../images/circle-terminal.svg';
import CircleFolder from '../images/circle-folder.svg';
import CircleInfo from '../images/circle-info.svg';
// HOC that adds the right web accessibility props
// https://www.scottohara.me/blog/2019/05/22/contextual-images-svgs-and-a11y.html
@ -83,3 +89,9 @@ export const MoreIcon = withLabel(More);
export const TerminalIcon = withLabel(Terminal);
export const CodeIcon = withLabel(Code);
export const SaveIcon = withLabel(Save);
export const FolderIcon = withLabel(Folder);
export const CircleTerminalIcon = withLabel(CircleTerminal);
export const CircleFolderIcon = withLabel(CircleFolder);
export const CircleInfoIcon = withLabel(CircleInfo);

View File

@ -25,7 +25,7 @@ const DropdownWrapper = styled.ul`
display: flex;
flex-direction: column;
height: auto;
z-index: 9999;
z-index: 2;
border-radius: ${remSize(6)};
& li:first-child { border-radius: ${remSize(5)} ${remSize(5)} 0 0; }

View File

@ -1,23 +1,34 @@
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import { remSize } from '../../theme';
import { remSize, prop } from '../../theme';
import IconButton from './IconButton';
const BottomBarContent = styled.div`
padding: ${remSize(8)};
display: grid;
grid-template-columns: repeat(8,1fr);
padding: ${remSize(8)};
svg {
max-height: ${remSize(32)};
}
path { fill: ${prop('primaryTextColor')} !important }
.inverted {
path { fill: ${prop('backgroundColor')} !important }
rect { fill: ${prop('primaryTextColor')} !important }
}
`;
const ActionStrip = ({ actions }) => (
<BottomBarContent>
{actions.map(({ icon, aria, action }) =>
{actions.map(({
icon, aria, action, inverted
}) =>
(<IconButton
inverted={inverted}
className={inverted && 'inverted'}
icon={icon}
aria-label={aria}
key={`bottom-bar-${aria}`}
@ -29,7 +40,8 @@ ActionStrip.propTypes = {
actions: PropTypes.arrayOf(PropTypes.shape({
icon: PropTypes.element.isRequired,
aria: PropTypes.string.isRequired,
action: PropTypes.func.isRequired
action: PropTypes.func.isRequired,
inverted: PropTypes.bool
})).isRequired
};

View File

@ -0,0 +1,24 @@
import React from 'react';
import styled from 'styled-components';
import PropTypes from 'prop-types';
import Sidebar from './Sidebar';
import ConnectedFileNode from '../../modules/IDE/components/FileNode';
const Explorer = ({ id, canEdit, onPressClose }) => (
<Sidebar title="Files" onPressClose={onPressClose}>
<ConnectedFileNode id={id} canEdit={canEdit} onClickFile={() => onPressClose()} />
</Sidebar>
);
Explorer.propTypes = {
id: PropTypes.number.isRequired,
onPressClose: PropTypes.func,
canEdit: PropTypes.bool
};
Explorer.defaultProps = {
canEdit: false,
onPressClose: () => {}
};
export default Explorer;

View File

@ -0,0 +1,43 @@
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'styled-components';
import { remSize, prop } from '../../theme';
import Button from '../../common/Button';
import IconButton from './IconButton';
const FloatingContainer = styled.div`
position: fixed;
right: ${remSize(16)};
top: ${remSize(80)};
text-align: right;
z-index: 3;
svg { width: ${remSize(32)}; };
svg > path { fill: ${prop('Button.default.background')} !important };
`;
const FloatingNav = ({ items }) => (
<FloatingContainer>
{ items.map(({ icon, onPress }) =>
(
<IconButton
onClick={onPress}
icon={icon}
/>
))}
</FloatingContainer>
);
FloatingNav.propTypes = {
items: PropTypes.arrayOf(PropTypes.shape({
icon: PropTypes.element,
onPress: PropTypes.func
}))
};
FloatingNav.defaultProps = {
items: []
};
export default FloatingNav;

View File

@ -14,7 +14,7 @@ const textColor = ({ transparent, inverted }) => prop((transparent === false &&
const HeaderDiv = styled.div`
position: fixed;
${props => props.fixed && 'position: fixed;'}
width: 100%;
background: ${props => background(props)};
color: ${textColor};
@ -63,9 +63,9 @@ const TitleContainer = styled.div`
const Header = ({
title, subtitle, leftButton, children,
transparent, inverted, slim
transparent, inverted, slim, fixed
}) => (
<HeaderDiv transparent={transparent} slim={slim} inverted={inverted}>
<HeaderDiv transparent={transparent} slim={slim} inverted={inverted} fixed={fixed}>
{leftButton}
<TitleContainer padded={subtitle === null}>
{title && <h2>{title}</h2>}
@ -86,6 +86,7 @@ Header.propTypes = {
transparent: PropTypes.bool,
inverted: PropTypes.bool,
slim: PropTypes.bool,
fixed: PropTypes.bool,
};
Header.defaultProps = {
@ -95,7 +96,8 @@ Header.defaultProps = {
children: [],
transparent: false,
inverted: false,
slim: false
slim: false,
fixed: true
};
export default Header;

View File

@ -2,7 +2,10 @@ import React from 'react';
import styled from 'styled-components';
import { remSize } from '../../theme';
// Applies padding to top and bottom so editor content is always visible
export default styled.div`
z-index: 0;
margin-top: ${remSize(16)};
.CodeMirror-sizer > * { padding-bottom: ${remSize(320)}; };
`;

View File

@ -0,0 +1,46 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router';
import styled from 'styled-components';
import { remSize, prop, common } from '../../theme';
import Header from './Header';
import IconButton from './IconButton';
import { ExitIcon } from '../../common/icons';
const SidebarWrapper = styled.div`
height: 100%;
width: ${remSize(180)};
position: fixed;
z-index: 2;
left: 0;
background: white;
box-shadow: 0 6px 6px 0 rgba(0,0,0,0.10);
`;
const Sidebar = ({ title, onPressClose, children }) => (
<SidebarWrapper>
{title &&
<Header slim title={title} fixed={false}>
<IconButton onClick={onPressClose} icon={ExitIcon} aria-label="Return to ide view" />
</Header>}
{children}
</SidebarWrapper>
);
Sidebar.propTypes = {
title: PropTypes.string,
onPressClose: PropTypes.func,
children: PropTypes.oneOfType([PropTypes.element, PropTypes.arrayOf(PropTypes.element)]),
};
Sidebar.defaultProps = {
title: null,
children: [],
onPressClose: () => {}
};
export default Sidebar;

View File

@ -1,10 +1,29 @@
import React from 'react';
import styled from 'styled-components';
import { useModalBehavior } from '../utils/custom-hooks';
export default (component) => {
const [visible, trigger, setRef] = useModalBehavior();
const BackgroundOverlay = styled.div`
position: fixed;
z-index: 2;
width: 100% !important;
height: 100% !important;
background: black;
opacity: 0.3;
`;
const wrapper = () => <div ref={setRef}> {visible && component} </div>; // eslint-disable-line
export default (Element, hasOverlay = false) => {
const [visible, toggle, setRef] = useModalBehavior();
return [trigger, wrapper];
const wrapper = () => (visible &&
<div>
{hasOverlay && <BackgroundOverlay />}
<div ref={setRef}>
{ (typeof (Element) === 'function')
? Element(toggle)
: Element}
</div>
</div>);
return [toggle, wrapper];
};

View File

@ -0,0 +1,5 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="16" cy="16" r="16" fill="#333333"/>
<path d="M25.144 12.0961V22.4321C25.144 22.8801 24.792 23.2321 24.344 23.2321H7.768C7.32 23.2321 7 22.8801 7 22.4321V12.0961C7 11.2321 7.704 10.5281 8.568 10.5281H23.576C24.44 10.5281 25.144 11.2321 25.144 12.0961Z" fill="#F0F0F0"/>
<path d="M9.24023 9.6C9.24023 9.6 9.24023 8 10.5842 8H15.1282C16.4402 8 16.4402 9.6 16.4402 9.6H9.24023Z" fill="#F0F0F0"/>
</svg>

After

Width:  |  Height:  |  Size: 507 B

View File

@ -0,0 +1,4 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="16" cy="16" r="16" fill="#333333"/>
<path d="M16 7C11.0154 7 7 11.0154 7 16C7 20.95 11.0154 25 16 25C20.95 25 25 20.95 25 16C25 11.0154 20.95 7 16 7ZM17.3846 21.5038H14.6846V13.7154H17.3846V21.5038ZM16 12.9538C15.1 12.9538 14.4077 12.2615 14.4077 11.3615C14.4077 10.4962 15.1 9.80385 16 9.80385C16.9 9.80385 17.5923 10.4962 17.5923 11.3615C17.5923 12.2615 16.9 12.9538 16 12.9538Z" fill="#F0F0F0"/>
</svg>

After

Width:  |  Height:  |  Size: 514 B

View File

@ -0,0 +1,6 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="16" cy="16" r="16" fill="#333333"/>
<rect x="5" y="8" width="22" height="16" rx="2" fill="#F0F0F0"/>
<path d="M24 21H14V20H24V21Z" fill="#333333"/>
<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="#333333"/>
</svg>

After

Width:  |  Height:  |  Size: 656 B

View File

@ -0,0 +1,4 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M25.144 12.0961V22.4321C25.144 22.8801 24.792 23.2321 24.344 23.2321H7.768C7.32 23.2321 7 22.8801 7 22.4321V12.0961C7 11.2321 7.704 10.5281 8.568 10.5281H23.576C24.44 10.5281 25.144 11.2321 25.144 12.0961Z" fill="#F0F0F0"/>
<path d="M9.24023 9.6C9.24023 9.6 9.24023 8 10.5842 8H15.1282C16.4402 8 16.4402 9.6 16.4402 9.6H9.24023Z" fill="#F0F0F0"/>
</svg>

After

Width:  |  Height:  |  Size: 459 B

View File

@ -19,7 +19,9 @@ class App extends React.Component {
componentWillReceiveProps(nextProps) {
const locationWillChange = nextProps.location !== this.props.location;
const shouldSkipRemembering = nextProps.location.state && nextProps.location.state.skipSavingPath === true;
const shouldSkipRemembering =
nextProps.location.state &&
nextProps.location.state.skipSavingPath === true;
if (locationWillChange && !shouldSkipRemembering) {
this.props.setPreviousPath(this.props.location.pathname);

View File

@ -108,10 +108,15 @@ export class FileNode extends React.Component {
handleFileClick = (event) => {
event.stopPropagation();
const { isDeleting } = this.state;
const { id, setSelectedFile, name } = this.props;
const {
id, setSelectedFile, name, onClickFile
} = this.props;
if (name !== 'root' && !isDeleting) {
setSelectedFile(id);
}
// debugger; // eslint-disable-line
if (onClickFile) { onClickFile(); }
}
handleFileNameChange = (event) => {
@ -214,7 +219,7 @@ export class FileNode extends React.Component {
renderChild = childId => (
<li key={childId}>
<ConnectedFileNode id={childId} parentId={this.props.id} canEdit={this.props.canEdit} />
<ConnectedFileNode id={childId} parentId={this.props.id} canEdit={this.props.canEdit} onClickFile={this.props.onClickFile} />
</li>
)
@ -233,7 +238,7 @@ export class FileNode extends React.Component {
const isRoot = this.props.name === 'root';
return (
<div className={itemClass}>
<div className={itemClass} >
{ !isRoot &&
<div className="file-item__content" onContextMenu={this.toggleFileOptions}>
<span className="file-item__spacer"></span>
@ -382,10 +387,12 @@ FileNode.propTypes = {
hideFolderChildren: PropTypes.func.isRequired,
canEdit: PropTypes.bool.isRequired,
openUploadFileModal: PropTypes.func.isRequired,
authenticated: PropTypes.bool.isRequired
authenticated: PropTypes.bool.isRequired,
onClickFile: PropTypes.func
};
FileNode.defaultProps = {
onClickFile: null,
parentId: '0',
isSelectedFile: false,
isFolderClosed: false,

View File

@ -16,7 +16,8 @@ import * as PreferencesActions from '../actions/preferences';
// Local Imports
import Editor from '../components/Editor';
import { PlayIcon, MoreIcon } from '../../../common/icons';
import { PlayIcon, MoreIcon, FolderIcon, PreferencesIcon, TerminalIcon, SaveIcon } from '../../../common/icons';
import UnsavedChangesDotIcon from '../../../images/unsaved-changes-dot.svg';
import IconButton from '../../../components/mobile/IconButton';
@ -24,12 +25,12 @@ 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 MobileExplorer from '../../../components/mobile/Explorer';
import Console from '../components/Console';
import { remSize } from '../../../theme';
// import OverlayManager from '../../../components/OverlayManager';
import ActionStrip from '../../../components/mobile/ActionStrip';
import useAsModal from '../../../components/useAsModal';
import { PreferencesIcon, TerminalIcon, SaveIcon } from '../../../common/icons';
import Dropdown from '../../../components/Dropdown';
@ -46,6 +47,8 @@ const withChangeDot = (title, unsavedChanges = false) => (
</span>
</span>
);
const getRootFile = files => files && files.filter(file => file.name === 'root')[0];
const getRootFileID = files => (root => root && root.id)(getRootFile(files));
const Expander = styled.div`
height: ${props => (props.expanded ? remSize(160) : remSize(27))};
@ -55,7 +58,7 @@ const NavItem = styled.li`
position: relative;
`;
const getNatOptions = (username = undefined) =>
const getNavOptions = (username = undefined) =>
(username
? [
{ icon: PreferencesIcon, title: 'Preferences', href: '/mobile/preferences', },
@ -170,7 +173,7 @@ const autosave = (autosaveInterval, setAutosaveInterval) => (props, prevProps) =
const MobileIDEView = (props) => {
const {
ide, preferences, project, selectedFile, user, params, unsavedChanges, expandConsole, collapseConsole,
stopSketch, startSketch, getProject, clearPersistedState, autosaveProject, saveProject
stopSketch, startSketch, getProject, clearPersistedState, autosaveProject, saveProject, files
} = props;
@ -180,10 +183,6 @@ const MobileIDEView = (props) => {
const { consoleIsExpanded } = ide;
const { name: filename } = selectedFile;
const [triggerNavDropdown, NavDropDown] = useAsModal(<Dropdown
items={getNatOptions(username)}
align="right"
/>);
// Force state reset
useEffect(clearPersistedState, []);
@ -204,6 +203,18 @@ const MobileIDEView = (props) => {
setCurrentProjectID(params.project_id);
}, [params, project, username]);
// Screen Modals
const [toggleNavDropdown, NavDropDown] = useAsModal(<Dropdown
items={getNavOptions(username)}
align="right"
/>);
const [toggleExplorer, Explorer] = useAsModal(toggle =>
(<MobileExplorer
id={getRootFileID(files)}
canEdit={false}
onPressClose={toggle}
/>), true);
// TODO: This behavior could move to <Editor />
const [autosaveInterval, setAutosaveInterval] = useState(null);
@ -214,19 +225,23 @@ const MobileIDEView = (props) => {
useEventListener('keydown', handleGlobalKeydown(props, cmController), false, [props]);
const projectActions =
[{ icon: TerminalIcon, aria: 'Toggle console open/closed', action: consoleIsExpanded ? collapseConsole : expandConsole },
{ icon: SaveIcon, aria: 'Save project', action: () => saveProject(cmController.getContent(), false, true) }
[{
icon: TerminalIcon, aria: 'Toggle console open/closed', action: consoleIsExpanded ? collapseConsole : expandConsole, inverted: true
},
{ icon: SaveIcon, aria: 'Save project', action: () => saveProject(cmController.getContent(), false, true) },
{ icon: FolderIcon, aria: 'Open files explorer', action: toggleExplorer }
];
return (
<Screen fullscreen>
<Explorer />
<Header
title={withChangeDot(project.name, unsavedChanges)}
subtitle={filename}
>
<NavItem>
<IconButton
onClick={triggerNavDropdown}
onClick={toggleNavDropdown}
icon={MoreIcon}
aria-label="Options"
/>
@ -299,6 +314,8 @@ MobileIDEView.propTypes = {
username: PropTypes.string,
}).isRequired,
getProject: PropTypes.func.isRequired,
clearPersistedState: PropTypes.func.isRequired,
params: PropTypes.shape({
project_id: PropTypes.string,
username: PropTypes.string
@ -308,8 +325,6 @@ MobileIDEView.propTypes = {
startSketch: PropTypes.func.isRequired,
stopSketch: PropTypes.func.isRequired,
getProject: PropTypes.func.isRequired,
clearPersistedState: PropTypes.func.isRequired,
autosaveProject: PropTypes.func.isRequired,

View File

@ -28,7 +28,7 @@ export const useModalBehavior = (hideOverlay) => {
const handleClickOutside = ({ target }) => {
if (ref && ref.current && !ref.current.contains(target)) {
if (ref && ref.current && !(ref.current.contains && ref.current.contains(target))) {
hide();
}
};