Merge branch 'feature/mobile-examples' of https://github.com/ghalestrilo/p5.js-web-editor into feature/mobile-files-tab

This commit is contained in:
ghalestrilo 2020-08-11 17:04:45 -03:00
commit b88a40327e
18 changed files with 228 additions and 78 deletions

View file

@ -0,0 +1,18 @@
import React from 'react';
import styled from 'styled-components';
import { Link } from 'react-router';
import { prop, remSize } from '../../theme';
export default styled(Link)`
box-sizing: border-box;
background: transparent;
/* border-top: ${remSize(4)} solid ${props => prop(props.selected ? 'colors.p5jsPink' : 'MobilePanel.default.background')}; */
border-top: ${remSize(4)} solid ${props => (props.selected ? prop('TabHighlight') : 'transparent')};
color: ${prop('primaryTextColor')};
padding: ${remSize(8)} ${remSize(16)};
width: 30%;
`;

View file

@ -0,0 +1,15 @@
import React from 'react';
import styled from 'styled-components';
import { prop, remSize } from '../../theme';
export default styled.div`
display: flex;
justify-content: space-between;
h3 { text-align: center; width: 100%; }
border-top: 1px solid ${prop('Separator')};
background: ${props => prop('backgroundColor')};
`;

View file

@ -214,8 +214,8 @@ class CollectionListRowBase extends React.Component {
</span> </span>
</th> </th>
{(!mobile) && <td>{format(new Date(collection.createdAt), 'MMM D, YYYY')}</td>} {(!mobile) && <td>{format(new Date(collection.createdAt), 'MMM D, YYYY')}</td>}
<td>{formatDateCell(collection.updatedAt)}</td> <td>{mobile && 'Updated: '}{formatDateCell(collection.updatedAt)}</td>
<td>{(collection.items || []).length}</td> <td>{mobile && '# sketches: '}{(collection.items || []).length}</td>
<td className="sketch-list__dropdown-column"> <td className="sketch-list__dropdown-column">
{this.renderActions()} {this.renderActions()}
</td> </td>

View file

@ -1,5 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { withTranslation } from 'react-i18next';
import { domOnlyProps } from '../../../utils/reduxFormUtils'; import { domOnlyProps } from '../../../utils/reduxFormUtils';
import Button from '../../../common/Button'; import Button from '../../../common/Button';
@ -35,14 +36,17 @@ class NewFileForm extends React.Component {
className="new-file-form__name-input" className="new-file-form__name-input"
id="name" id="name"
type="text" type="text"
placeholder="Name" placeholder={this.props.t('NewFileForm.Placeholder')}
maxLength="128" maxLength="128"
{...domOnlyProps(name)} {...domOnlyProps(name)}
ref={(element) => { ref={(element) => {
this.fileName = element; this.fileName = element;
}} }}
/> />
<Button type="submit">Add File</Button> <Button
type="submit"
>{this.props.t('NewFileForm.AddFileSubmit')}
</Button>
</div> </div>
{name.touched && name.error && ( {name.touched && name.error && (
<span className="form-error">{name.error}</span> <span className="form-error">{name.error}</span>
@ -59,6 +63,7 @@ NewFileForm.propTypes = {
handleSubmit: PropTypes.func.isRequired, handleSubmit: PropTypes.func.isRequired,
createFile: PropTypes.func.isRequired, createFile: PropTypes.func.isRequired,
focusOnModal: PropTypes.func.isRequired, focusOnModal: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
}; };
export default NewFileForm; export default withTranslation()(NewFileForm);

View file

@ -3,6 +3,8 @@ import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators, compose } from 'redux'; import { bindActionCreators, compose } from 'redux';
import { reduxForm } from 'redux-form'; import { reduxForm } from 'redux-form';
import { withTranslation } from 'react-i18next';
import i18n from 'i18next';
import NewFileForm from './NewFileForm'; import NewFileForm from './NewFileForm';
import { closeNewFileModal } from '../actions/ide'; import { closeNewFileModal } from '../actions/ide';
import { createFile } from '../actions/files'; import { createFile } from '../actions/files';
@ -33,11 +35,11 @@ class NewFileModal extends React.Component {
<section className="modal" ref={(element) => { this.modal = element; }}> <section className="modal" ref={(element) => { this.modal = element; }}>
<div className="modal-content"> <div className="modal-content">
<div className="modal__header"> <div className="modal__header">
<h2 className="modal__title">Create File</h2> <h2 className="modal__title">{this.props.t('NewFileModal.Title')}</h2>
<button <button
className="modal__exit-button" className="modal__exit-button"
onClick={this.props.closeNewFileModal} onClick={this.props.closeNewFileModal}
aria-label="Close New File Modal" aria-label={this.props.t('NewFileModal.CloseButtonARIA')}
> >
<ExitIcon focusable="false" aria-hidden="true" /> <ExitIcon focusable="false" aria-hidden="true" />
</button> </button>
@ -54,16 +56,17 @@ class NewFileModal extends React.Component {
NewFileModal.propTypes = { NewFileModal.propTypes = {
createFile: PropTypes.func.isRequired, createFile: PropTypes.func.isRequired,
closeNewFileModal: PropTypes.func.isRequired closeNewFileModal: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
}; };
function validate(formProps) { function validate(formProps) {
const errors = {}; const errors = {};
if (!formProps.name) { if (!formProps.name) {
errors.name = 'Please enter a name'; errors.name = i18n.t('NewFileModal.EnterName');
} else if (!formProps.name.match(CREATE_FILE_REGEX)) { } else if (!formProps.name.match(CREATE_FILE_REGEX)) {
errors.name = 'Invalid file type. Valid extensions are .js, .css, .json, .txt, .csv, .tsv, .frag, and .vert.'; errors.name = i18n.t('NewFileModal.InvalidType');
} }
return errors; return errors;
@ -77,11 +80,11 @@ function mapDispatchToProps(dispatch) {
return bindActionCreators({ createFile, closeNewFileModal }, dispatch); return bindActionCreators({ createFile, closeNewFileModal }, dispatch);
} }
export default compose( export default withTranslation()(compose(
connect(mapStateToProps, mapDispatchToProps), connect(mapStateToProps, mapDispatchToProps),
reduxForm({ reduxForm({
form: 'new-file', form: 'new-file',
fields: ['name'], fields: ['name'],
validate validate
}) })
)(NewFileModal); )(NewFileModal));

View file

@ -1,9 +1,11 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { withTranslation } from 'react-i18next';
import { domOnlyProps } from '../../../utils/reduxFormUtils'; import { domOnlyProps } from '../../../utils/reduxFormUtils';
import Button from '../../../common/Button'; import Button from '../../../common/Button';
class NewFolderForm extends React.Component { class NewFolderForm extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -35,13 +37,14 @@ class NewFolderForm extends React.Component {
id="name" id="name"
type="text" type="text"
maxLength="128" maxLength="128"
placeholder="Name" placeholder={this.props.t('NewFolderForm.Placeholder')}
ref={(element) => { ref={(element) => { this.fileName = element; }}
this.fileName = element;
}}
{...domOnlyProps(name)} {...domOnlyProps(name)}
/> />
<Button type="submit">Add Folder</Button> <Button
type="submit"
>{this.props.t('NewFolderForm.AddFolderSubmit')}
</Button>
</div> </div>
{name.touched && name.error && ( {name.touched && name.error && (
<span className="form-error">{name.error}</span> <span className="form-error">{name.error}</span>
@ -60,9 +63,10 @@ NewFolderForm.propTypes = {
closeModal: PropTypes.func.isRequired, closeModal: PropTypes.func.isRequired,
submitting: PropTypes.bool, submitting: PropTypes.bool,
pristine: PropTypes.bool, pristine: PropTypes.bool,
t: PropTypes.func.isRequired
}; };
NewFolderForm.defaultProps = { NewFolderForm.defaultProps = {
submitting: false, submitting: false,
pristine: true, pristine: true,
}; };
export default NewFolderForm; export default withTranslation()(NewFolderForm);

View file

@ -1,6 +1,8 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { reduxForm } from 'redux-form'; import { reduxForm } from 'redux-form';
import { withTranslation } from 'react-i18next';
import i18n from 'i18next';
import NewFolderForm from './NewFolderForm'; import NewFolderForm from './NewFolderForm';
import ExitIcon from '../../../images/exit.svg'; import ExitIcon from '../../../images/exit.svg';
@ -15,11 +17,11 @@ class NewFolderModal extends React.Component {
<section className="modal" ref={(element) => { this.newFolderModal = element; }} > <section className="modal" ref={(element) => { this.newFolderModal = element; }} >
<div className="modal-content-folder"> <div className="modal-content-folder">
<div className="modal__header"> <div className="modal__header">
<h2 className="modal__title">Create Folder</h2> <h2 className="modal__title">{this.props.t('NewFolderModal.Title')}</h2>
<button <button
className="modal__exit-button" className="modal__exit-button"
onClick={this.props.closeModal} onClick={this.props.closeModal}
aria-label="Close New Folder Modal" aria-label={this.props.t('NewFolderModal.CloseButtonARIA')}
> >
<ExitIcon focusable="false" aria-hidden="true" /> <ExitIcon focusable="false" aria-hidden="true" />
</button> </button>
@ -32,23 +34,24 @@ class NewFolderModal extends React.Component {
} }
NewFolderModal.propTypes = { NewFolderModal.propTypes = {
closeModal: PropTypes.func.isRequired closeModal: PropTypes.func.isRequired,
t: PropTypes.func.isRequired
}; };
function validate(formProps) { function validate(formProps) {
const errors = {}; const errors = {};
if (!formProps.name) { if (!formProps.name) {
errors.name = 'Please enter a name'; errors.name = i18n.t('NewFolderModal.EnterName');
} else if (formProps.name.trim().length === 0) { } else if (formProps.name.trim().length === 0) {
errors.name = 'Folder name cannot contain only spaces'; errors.name = i18n.t('NewFolderModal.EmptyName');
} else if (formProps.name.match(/\.+/i)) { } else if (formProps.name.match(/\.+/i)) {
errors.name = 'Folder name cannot contain an extension'; errors.name = i18n.t('NewFolderModal.InvalidExtension');
} }
return errors; return errors;
} }
export default reduxForm({ export default withTranslation()(reduxForm({
form: 'new-folder', form: 'new-folder',
fields: ['name'], fields: ['name'],
validate validate
})(NewFolderModal); })(NewFolderModal));

View file

@ -293,8 +293,8 @@ class SketchListRowBase extends React.Component {
<th scope="row"> <th scope="row">
{name} {name}
</th> </th>
<td>{formatDateCell(sketch.createdAt, mobile)}</td> <td>{mobile && 'Created: '}{formatDateCell(sketch.createdAt, mobile)}</td>
<td>{formatDateCell(sketch.updatedAt, mobile)}</td> <td>{mobile && 'Updated: '}{formatDateCell(sketch.updatedAt, mobile)}</td>
{this.renderDropdown()} {this.renderDropdown()}
</tr> </tr>
</React.Fragment>); </React.Fragment>);

View file

@ -2,27 +2,31 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import styled from 'styled-components'; import styled from 'styled-components';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { withRouter, Link } from 'react-router'; import { withRouter } from 'react-router';
import Screen from '../../components/mobile/MobileScreen'; import Screen from '../../components/mobile/MobileScreen';
import Header from '../../components/mobile/Header'; import Header from '../../components/mobile/Header';
import IconButton from '../../components/mobile/IconButton'; import IconButton from '../../components/mobile/IconButton';
import { ExitIcon } from '../../common/icons'; import { ExitIcon, MoreIcon } from '../../common/icons';
import Footer from '../../components/mobile/Footer'; import Footer from '../../components/mobile/Footer';
import { prop, remSize } from '../../theme'; import { remSize, prop } from '../../theme';
import SketchList from '../IDE/components/SketchList'; import SketchList from '../IDE/components/SketchList';
import CollectionList from '../IDE/components/CollectionList'; import CollectionList from '../IDE/components/CollectionList';
import AssetList from '../IDE/components/AssetList'; import AssetList from '../IDE/components/AssetList';
import Content from './MobileViewContent'; import Content from './MobileViewContent';
import { SketchSearchbar, CollectionSearchbar } from '../IDE/components/Searchbar'; import { SketchSearchbar, CollectionSearchbar } from '../IDE/components/Searchbar';
import Button from '../../common/Button'; import Button from '../../common/Button';
import useAsModal from '../../components/useAsModal';
import Dropdown from '../../components/Dropdown';
import FooterTabSwitcher from '../../components/mobile/TabSwitcher';
import FooterTab from '../../components/mobile/Tab';
import Loader from '../App/components/loader';
const EXAMPLE_USERNAME = 'p5'; const EXAMPLE_USERNAME = 'p5';
const ContentWrapper = styled(Content)` const ContentWrapper = styled(Content)`
table { table {
table-layout: fixed; table-layout: fixed;
/* white-space: nowrap; */
} }
td ,thead button { td ,thead button {
@ -31,38 +35,57 @@ const ContentWrapper = styled(Content)`
text-align: left; text-align: left;
}; };
thead th { padding-left: 0; }
thead th:not(:first-child), tbody td {
width: ${remSize(54)};
}
tbody th { font-weight: bold; };
tbody th { tbody th {
font-size: ${remSize(12)}; font-size: ${remSize(16)};
width: 100%; width: 100%;
padding-right: ${remSize(12)} padding-right: ${remSize(12)};
font-weight: bold;
display: flex;
grid-area: name;
}; };
tbody td { tbody td, thead th {
text-align: center; justify-self: stretch;
align-self: flex-end;
color: ${prop('primaryTextColor')}
} }
.sketch-list__sort-button { padding: 0 } tbody td { justify-self: center; padding-left: ${remSize(12)}}
tbody {
height: ${remSize(48)}; thead th svg { margin-left: ${remSize(8)} }
tbody td:last-child { justify-self: end; text-align: end; };
.sketches-table .sketch-list__dropdown-column { min-width: unset };
tbody { height: ${remSize(48)}; }
.sketches-table-container {
padding-bottom: ${remSize(160)};
background: ${prop('SketchList.background')};
}
.sketches-table__row {
background: ${prop('SketchList.card.background')} !important; height: auto
} }
.sketches-table-container { padding-bottom: ${remSize(160)} } tr {
`; align-self: start;
display: grid;
box-shadow: 0 0 18px 0 ${prop('shadowColor')};
};
const FooterTab = styled(Link)` thead tr {
background: ${props => prop(props.selected ? 'backgroundColor' : 'MobilePanel.default.foreground')}; grid-template-columns: auto ${remSize(100)} ${remSize(100)} 0fr;
color: ${props => prop(`MobilePanel.default.${props.selected ? 'foreground' : 'background'}`)}; }
padding: ${remSize(8)} ${remSize(16)};
width: 100%; tbody tr {
display: flex; padding: ${remSize(8)};
border-radius: ${remSize(4)};
grid-template-columns: 5fr 5fr 2fr;
grid-template-areas: "name name name" "content content content";
}
.loader-container { position: fixed ; padding-bottom: 32% }
`; `;
const Subheader = styled.div` const Subheader = styled.div`
@ -81,23 +104,17 @@ const SubheaderButton = styled(Button)`
border-radius: 0px !important; border-radius: 0px !important;
`; `;
const FooterTabSwitcher = styled.div`
display: flex;
h3 { text-align: center; width: 100%; }
`;
const Panels = { const Panels = {
sketches: SketchList, sketches: SketchList,
collections: CollectionList, collections: CollectionList,
assets: AssetList assets: AssetList
}; };
const CreatePathname = {
sketches: '/mobile', const navOptions = username => [
collections: '/mobile/:username/collections/create', { title: 'Create Sketch', href: '/mobile' },
}; { title: 'Create Collection', href: `/mobile/${username}/collections/create` }
];
const getPanel = (pathname) => { const getPanel = (pathname) => {
@ -106,7 +123,10 @@ const getPanel = (pathname) => {
return matches && matches.length > 0 && matches[0]; return matches && matches.length > 0 && matches[0];
}; };
const getCreatePathname = (panel, username) => (CreatePathname[panel] || '#').replace(':username', username); const NavItem = styled.li`
position: relative;
`;
const isOwner = (user, params) => user && params && user.username === params.username; const isOwner = (user, params) => user && params && user.username === params.username;
@ -114,27 +134,41 @@ const renderPanel = (name, props) => (Component => (Component && <Component {...
const MobileDashboard = ({ params, location }) => { const MobileDashboard = ({ params, location }) => {
const user = useSelector(state => state.user); const user = useSelector(state => state.user);
const { username } = params; const { username: paramsUsername } = params;
const { pathname } = location; const { pathname } = location;
const Tabs = Object.keys(Panels); const Tabs = Object.keys(Panels);
const isExamples = username === EXAMPLE_USERNAME; const isExamples = paramsUsername === EXAMPLE_USERNAME;
const panel = getPanel(pathname); const panel = getPanel(pathname);
const [toggleNavDropdown, NavDropdown] = useAsModal(<Dropdown
items={navOptions(user.username)}
align="right"
/>);
return ( return (
<Screen fullscreen key={pathname}> <Screen fullscreen key={pathname}>
<Header slim inverted title={isExamples ? 'Examples' : 'My Stuff'}> <Header slim inverted title={isExamples ? 'Examples' : 'My Stuff'}>
<NavItem>
<IconButton
onClick={toggleNavDropdown}
icon={MoreIcon}
aria-label="Options"
/>
<NavDropdown />
</NavItem>
<IconButton to="/mobile" icon={ExitIcon} aria-label="Return to ide view" /> <IconButton to="/mobile" icon={ExitIcon} aria-label="Return to ide view" />
</Header> </Header>
<ContentWrapper slimheader> <ContentWrapper slimheader>
<Subheader> <Subheader>
{isOwner(user, params) && (panel !== Tabs[2]) && <SubheaderButton to={getCreatePathname(panel, username)}>new</SubheaderButton>}
{panel === Tabs[0] && <SketchSearchbar />} {panel === Tabs[0] && <SketchSearchbar />}
{panel === Tabs[1] && <CollectionSearchbar />} {panel === Tabs[1] && <CollectionSearchbar />}
</Subheader> </Subheader>
{renderPanel(panel, { username, key: pathname })} {renderPanel(panel, { username: paramsUsername, key: pathname })}
</ContentWrapper> </ContentWrapper>
<Footer> <Footer>
{!isExamples && {!isExamples &&

View file

@ -6,5 +6,5 @@ import { remSize } from '../../theme';
export default styled.div` export default styled.div`
/* Dashboard Styles */ /* Dashboard Styles */
z-index: 0; z-index: 0;
margin-top: ${props => remSize(props.slimheader ? 50 : 68)}; margin-top: ${props => remSize(props.slimheader ? 49 : 68)};
`; `;

View file

@ -58,6 +58,7 @@ export const prop = key => (props) => {
return value; return value;
}; };
export default { export default {
[Theme.light]: { [Theme.light]: {
colors, colors,
@ -103,6 +104,14 @@ export default {
border: grays.middleLight border: grays.middleLight
}, },
Separator: grays.middleLight, Separator: grays.middleLight,
TabHighlight: colors.p5jsPink,
SketchList: {
background: grays.lighter,
card: {
background: grays.lighter
}
}
}, },
[Theme.dark]: { [Theme.dark]: {
colors, colors,
@ -148,6 +157,14 @@ export default {
border: grays.middleDark border: grays.middleDark
}, },
Separator: grays.middleDark, Separator: grays.middleDark,
TabHighlight: colors.p5jsPink,
SketchList: {
background: grays.darker,
card: {
background: grays.dark
}
}
}, },
[Theme.contrast]: { [Theme.contrast]: {
colors, colors,
@ -193,5 +210,13 @@ export default {
border: grays.middleDark border: grays.middleDark
}, },
Separator: grays.middleDark, Separator: grays.middleDark,
TabHighlight: grays.darker,
SketchList: {
background: colors.yellow,
card: {
background: grays.dark
}
}
}, },
}; };

2
package-lock.json generated
View file

@ -1,6 +1,6 @@
{ {
"name": "p5.js-web-editor", "name": "p5.js-web-editor",
"version": "1.0.6", "version": "1.0.7",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View file

@ -1,6 +1,6 @@
{ {
"name": "p5.js-web-editor", "name": "p5.js-web-editor",
"version": "1.0.6", "version": "1.0.7",
"description": "The web editor for p5.js.", "description": "The web editor for p5.js.",
"scripts": { "scripts": {
"clean": "rimraf dist", "clean": "rimraf dist",

View file

@ -30,14 +30,15 @@ export function updateProject(req, res) {
$set: req.body $set: req.body
}, },
{ {
new: true new: true,
runValidators: true
} }
) )
.populate('user', 'username') .populate('user', 'username')
.exec((updateProjectErr, updatedProject) => { .exec((updateProjectErr, updatedProject) => {
if (updateProjectErr) { if (updateProjectErr) {
console.log(updateProjectErr); console.log(updateProjectErr);
res.json({ success: false }); res.status(400).json({ success: false });
return; return;
} }
if (req.body.files && updatedProject.files.length !== req.body.files.length) { if (req.body.files && updatedProject.files.length !== req.body.files.length) {
@ -50,7 +51,7 @@ export function updateProject(req, res) {
updatedProject.save((innerErr, savedProject) => { updatedProject.save((innerErr, savedProject) => {
if (innerErr) { if (innerErr) {
console.log(innerErr); console.log(innerErr);
res.json({ success: false }); res.status(400).json({ success: false });
return; return;
} }
res.json(savedProject); res.json(savedProject);

View file

@ -9,7 +9,7 @@ export default function createProject(req, res) {
projectValues = Object.assign(projectValues, req.body); projectValues = Object.assign(projectValues, req.body);
function sendFailure() { function sendFailure() {
res.json({ success: false }); res.status(400).json({ success: false });
} }
function populateUserData(newProject) { function populateUserData(newProject) {

View file

@ -29,7 +29,7 @@ fileSchema.set('toJSON', {
const projectSchema = new Schema( const projectSchema = new Schema(
{ {
name: { type: String, default: "Hello p5.js, it's the server" }, name: { type: String, default: "Hello p5.js, it's the server", maxlength: 128 },
user: { type: Schema.Types.ObjectId, ref: 'User' }, user: { type: Schema.Types.ObjectId, ref: 'User' },
serveSecure: { type: Boolean, default: false }, serveSecure: { type: Boolean, default: false },
files: { type: [fileSchema] }, files: { type: [fileSchema] },

View file

@ -173,5 +173,26 @@
}, },
"IDEView": { "IDEView": {
"SubmitFeedback": "Submit Feedback" "SubmitFeedback": "Submit Feedback"
},
"NewFileModal": {
"Title": "Create File",
"CloseButtonARIA": "Close New File Modal",
"EnterName": "Please enter a name",
"InvalidType": "Invalid file type. Valid extensions are .js, .css, .json, .txt, .csv, .tsv, .frag, and .vert."
},
"NewFileForm": {
"AddFileSubmit": "Add File",
"Placeholder": "Name"
},
"NewFolderModal": {
"Title": "Create Folder",
"CloseButtonARIA": "Close New Folder Modal",
"EnterName": "Please enter a name",
"EmptyName": "Folder name cannot contain only spaces",
"InvalidExtension": "Folder name cannot contain an extension"
},
"NewFolderForm": {
"AddFolderSubmit": "Add Folder",
"Placeholder": "Name"
} }
} }

View file

@ -172,6 +172,27 @@
}, },
"IDEView": { "IDEView": {
"SubmitFeedback": "Enviar retroalimentación" "SubmitFeedback": "Enviar retroalimentación"
},
"NewFileModal": {
"Title": "Crear Archivo",
"CloseButtonARIA": "Cerrar diálogo de crear archivo",
"EnterName": "Por favor introduce un nombre",
"InvalidType": "Tipo de archivo inválido. Las extensiones válidas son .js, .css, .json, .txt, .csv, .tsv, .frag y .vert."
},
"NewFileForm": {
"AddFileSubmit": "Agregar Archivo",
"Placeholder": "Nombre"
},
"NewFolderModal": {
"Title": "Crear Directorio",
"CloseButtonARIA": "Cerrar Diálogo de Nuevo Directorio",
"EnterName": "Por favor introduce un nombre",
"EmptyName": " El nombre del directorio no debe contener solo espacios vacíos",
"InvalidExtension": "El nombre del directorio no debe contener una extensión"
},
"NewFolderForm": {
"AddFolderSubmit": "Agregar Directorio",
"Placeholder": "Nombre"
} }
} }