Update sketch list styling (#819)
* parentb3c3efcec9
author Laksh Singla <lakshsingla@gmail.com> 1549106083 +0530 committer Cassie Tarakajian <ctarakajian@gmail.com> 1560540243 -0400 parentb3c3efcec9
author Laksh Singla <lakshsingla@gmail.com> 1549106083 +0530 committer Cassie Tarakajian <ctarakajian@gmail.com> 1560540198 -0400 parentb3c3efcec9
author Laksh Singla <lakshsingla@gmail.com> 1549106083 +0530 committer Cassie Tarakajian <ctarakajian@gmail.com> 1560539667 -0400 Created initial html structure and styling for new SketchList design Final styling of ActionDialogueBox commplete Dropdown menu disappearing while clicking anywhere on the table Fixed linting issues and renamed variables Minor tweaks in the SketchList dropdown dialogue UI Themifyed the dropdown Made changes in the dropdown: Arrow positioned slightly updwards, Removed blank space and added box-shadow in dropdown, themifyed dropdowns dashed border color Added Delete and Share functionality to Dialog box Added Duplicate functionality to Dialog box Added download functionality to Dialog box SketchList does not open a sketch if dialogue box is opened SketchList Rename initial UI completed Enter key handled for rename project option [WIP] Updating rename functionality Download option now working for all the sketches Duplicate functionality extended for non opened sketches too Modified overlay behaviour to close only the last overlay Share modal can now display different projects Dropdown closes when Share and Delete are closing for a more natural UX fix broken files from rebasing Created initial html structure and styling for new SketchList design Final styling of ActionDialogueBox commplete Added Delete and Share functionality to Dialog box Added Duplicate functionality to Dialog box [WIP] Updating rename functionality Duplicate functionality extended for non opened sketches too Modified overlay behaviour to close only the last overlay Share modal can now display different projects Final styling of ActionDialogueBox commplete Fixed linting issues and renamed variables Minor tweaks in the SketchList dropdown dialogue UI Themifyed the dropdown Added Delete and Share functionality to Dialog box [WIP] Updating rename functionality Modified overlay behaviour to close only the last overlay Share modal can now display different projects Dropdown closes when Share and Delete are closing for a more natural UX fix broken files from rebasing Final styling of ActionDialogueBox commplete Minor tweaks in the SketchList dropdown dialogue UI Themifyed the dropdown [WIP] Updating rename functionality Duplicate functionality extended for non opened sketches too Modified overlay behaviour to close only the last overlay Share modal can now display different projects Dropdown closes when Share and Delete are closing for a more natural UX * fix bugs in merge commit * move sketch list dialogue to ul/li * update sketch option dropdown to use dropdown placeholder, remove unused css * major refactor of sketchlist component, fix showShareModal action, minor updates ot icon sizing * fix broken links on asset list * remove unused image, fix options for different users in sketch list
This commit is contained in:
parent
4bd081b307
commit
735adcfa05
14 changed files with 448 additions and 145 deletions
|
@ -20,6 +20,7 @@ export const AUTH_ERROR = 'AUTH_ERROR';
|
||||||
export const SETTINGS_UPDATED = 'SETTINGS_UPDATED';
|
export const SETTINGS_UPDATED = 'SETTINGS_UPDATED';
|
||||||
|
|
||||||
export const SET_PROJECT_NAME = 'SET_PROJECT_NAME';
|
export const SET_PROJECT_NAME = 'SET_PROJECT_NAME';
|
||||||
|
export const RENAME_PROJECT = 'RENAME_PROJECT';
|
||||||
|
|
||||||
export const PROJECT_SAVE_SUCCESS = 'PROJECT_SAVE_SUCCESS';
|
export const PROJECT_SAVE_SUCCESS = 'PROJECT_SAVE_SUCCESS';
|
||||||
export const PROJECT_SAVE_FAIL = 'PROJECT_SAVE_FAIL';
|
export const PROJECT_SAVE_FAIL = 'PROJECT_SAVE_FAIL';
|
||||||
|
|
|
@ -33,7 +33,7 @@ class Overlay extends React.Component {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.handleClickOutside();
|
this.handleClickOutside(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClickOutside() {
|
handleClickOutside() {
|
||||||
|
@ -49,6 +49,10 @@ class Overlay extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
|
// Only close if it is the last (and therefore the topmost overlay)
|
||||||
|
const overlays = document.getElementsByClassName('overlay');
|
||||||
|
if (this.node.parentElement.parentElement !== overlays[overlays.length - 1]) return;
|
||||||
|
|
||||||
if (!this.props.closeOverlay) {
|
if (!this.props.closeOverlay) {
|
||||||
browserHistory.push(this.props.previousPath);
|
browserHistory.push(this.props.previousPath);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -134,9 +134,17 @@ export function closeNewFolderModal() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showShareModal() {
|
export function showShareModal(projectId, projectName, ownerUsername) {
|
||||||
return {
|
return (dispatch, getState) => {
|
||||||
type: ActionTypes.SHOW_SHARE_MODAL
|
const { project, user } = getState();
|
||||||
|
dispatch({
|
||||||
|
type: ActionTypes.SHOW_SHARE_MODAL,
|
||||||
|
payload: {
|
||||||
|
shareModalProjectId: projectId || project.id,
|
||||||
|
shareModalProjectName: projectName || project.name,
|
||||||
|
shareModalProjectUsername: ownerUsername || user.username
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,8 @@ import {
|
||||||
setUnsavedChanges,
|
setUnsavedChanges,
|
||||||
justOpenedProject,
|
justOpenedProject,
|
||||||
resetJustOpenedProject,
|
resetJustOpenedProject,
|
||||||
showErrorModal
|
showErrorModal,
|
||||||
|
setPreviousPath
|
||||||
} from './ide';
|
} from './ide';
|
||||||
import { clearState, saveState } from '../../../persistState';
|
import { clearState, saveState } from '../../../persistState';
|
||||||
|
|
||||||
|
@ -246,47 +247,61 @@ function generateNewIdsForChildren(file, files) {
|
||||||
file.children = newChildren; // eslint-disable-line
|
file.children = newChildren; // eslint-disable-line
|
||||||
}
|
}
|
||||||
|
|
||||||
export function cloneProject() {
|
export function cloneProject(id) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch(setUnsavedChanges(false));
|
dispatch(setUnsavedChanges(false));
|
||||||
const state = getState();
|
new Promise((resolve, reject) => {
|
||||||
const newFiles = state.files.map((file) => { // eslint-disable-line
|
if (!id) {
|
||||||
return { ...file };
|
resolve(getState());
|
||||||
});
|
|
||||||
|
|
||||||
// generate new IDS for all files
|
|
||||||
const rootFile = newFiles.find(file => file.name === 'root');
|
|
||||||
const newRootFileId = objectID().toHexString();
|
|
||||||
rootFile.id = newRootFileId;
|
|
||||||
rootFile._id = newRootFileId;
|
|
||||||
generateNewIdsForChildren(rootFile, newFiles);
|
|
||||||
|
|
||||||
// duplicate all files hosted on S3
|
|
||||||
each(newFiles, (file, callback) => {
|
|
||||||
if (file.url && file.url.includes('amazonaws')) {
|
|
||||||
const formParams = {
|
|
||||||
url: file.url
|
|
||||||
};
|
|
||||||
axios.post(`${ROOT_URL}/S3/copy`, formParams, { withCredentials: true })
|
|
||||||
.then((response) => {
|
|
||||||
file.url = response.data.url;
|
|
||||||
callback(null);
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
callback(null);
|
fetch(`${ROOT_URL}/projects/${id}`)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => resolve({
|
||||||
|
files: data.files,
|
||||||
|
project: {
|
||||||
|
name: data.name
|
||||||
|
}
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}, (err) => {
|
}).then((state) => {
|
||||||
// if not errors in duplicating the files on S3, then duplicate it
|
const newFiles = state.files.map((file) => { // eslint-disable-line
|
||||||
const formParams = Object.assign({}, { name: `${state.project.name} copy` }, { files: newFiles });
|
return { ...file };
|
||||||
axios.post(`${ROOT_URL}/projects`, formParams, { withCredentials: true })
|
});
|
||||||
.then((response) => {
|
|
||||||
browserHistory.push(`/${response.data.user.username}/sketches/${response.data.id}`);
|
// generate new IDS for all files
|
||||||
dispatch(setNewProject(response.data));
|
const rootFile = newFiles.find(file => file.name === 'root');
|
||||||
})
|
const newRootFileId = objectID().toHexString();
|
||||||
.catch(response => dispatch({
|
rootFile.id = newRootFileId;
|
||||||
type: ActionTypes.PROJECT_SAVE_FAIL,
|
rootFile._id = newRootFileId;
|
||||||
error: response.data
|
generateNewIdsForChildren(rootFile, newFiles);
|
||||||
}));
|
|
||||||
|
// duplicate all files hosted on S3
|
||||||
|
each(newFiles, (file, callback) => {
|
||||||
|
if (file.url && file.url.includes('amazonaws')) {
|
||||||
|
const formParams = {
|
||||||
|
url: file.url
|
||||||
|
};
|
||||||
|
axios.post(`${ROOT_URL}/S3/copy`, formParams, { withCredentials: true })
|
||||||
|
.then((response) => {
|
||||||
|
file.url = response.data.url;
|
||||||
|
callback(null);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
callback(null);
|
||||||
|
}
|
||||||
|
}, (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 })
|
||||||
|
.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
|
||||||
|
}));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -309,3 +324,58 @@ export function setProjectSavedTime(updatedAt) {
|
||||||
value: updatedAt
|
value: updatedAt
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function changeProjectName(id, newName) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
const state = getState();
|
||||||
|
axios.put(`${ROOT_URL}/projects/${id}`, { name: newName }, { withCredentials: true })
|
||||||
|
.then((response) => {
|
||||||
|
if (response.status === 200) {
|
||||||
|
dispatch({
|
||||||
|
type: ActionTypes.RENAME_PROJECT,
|
||||||
|
payload: { id: response.data.id, name: response.data.name }
|
||||||
|
});
|
||||||
|
if (state.project.id === response.data.id) {
|
||||||
|
dispatch({
|
||||||
|
type: ActionTypes.SET_PROJECT_NAME,
|
||||||
|
name: response.data.name
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((response) => {
|
||||||
|
console.log(response);
|
||||||
|
dispatch({
|
||||||
|
type: ActionTypes.PROJECT_SAVE_FAIL,
|
||||||
|
error: response.data
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteProject(id) {
|
||||||
|
return (dispatch, getState) => {
|
||||||
|
axios.delete(`${ROOT_URL}/projects/${id}`, { withCredentials: true })
|
||||||
|
.then(() => {
|
||||||
|
const state = getState();
|
||||||
|
if (id === state.project.id) {
|
||||||
|
dispatch(resetProject());
|
||||||
|
dispatch(setPreviousPath('/'));
|
||||||
|
}
|
||||||
|
dispatch({
|
||||||
|
type: ActionTypes.DELETE_PROJECT,
|
||||||
|
id
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((response) => {
|
||||||
|
if (response.status === 403) {
|
||||||
|
dispatch(showErrorModal('staleSession'));
|
||||||
|
} else {
|
||||||
|
dispatch({
|
||||||
|
type: ActionTypes.ERROR,
|
||||||
|
error: response.data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import * as ActionTypes from '../../../constants';
|
import * as ActionTypes from '../../../constants';
|
||||||
import { showErrorModal, setPreviousPath } from './ide';
|
|
||||||
import { resetProject } from './project';
|
|
||||||
import { startLoader, stopLoader } from './loader';
|
import { startLoader, stopLoader } from './loader';
|
||||||
|
|
||||||
const __process = (typeof global !== 'undefined' ? global : window).process;
|
const __process = (typeof global !== 'undefined' ? global : window).process;
|
||||||
const ROOT_URL = __process.env.API_URL;
|
const ROOT_URL = __process.env.API_URL;
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
export function getProjects(username) {
|
export function getProjects(username) {
|
||||||
return (dispatch) => {
|
return (dispatch) => {
|
||||||
dispatch(startLoader());
|
dispatch(startLoader());
|
||||||
|
@ -33,30 +32,3 @@ export function getProjects(username) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteProject(id) {
|
|
||||||
return (dispatch, getState) => {
|
|
||||||
axios.delete(`${ROOT_URL}/projects/${id}`, { withCredentials: true })
|
|
||||||
.then(() => {
|
|
||||||
const state = getState();
|
|
||||||
if (id === state.project.id) {
|
|
||||||
dispatch(resetProject());
|
|
||||||
dispatch(setPreviousPath('/'));
|
|
||||||
}
|
|
||||||
dispatch({
|
|
||||||
type: ActionTypes.DELETE_PROJECT,
|
|
||||||
id
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((response) => {
|
|
||||||
if (response.status === 403) {
|
|
||||||
dispatch(showErrorModal('staleSession'));
|
|
||||||
} else {
|
|
||||||
dispatch({
|
|
||||||
type: ActionTypes.ERROR,
|
|
||||||
error: response.data
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
@ -17,13 +17,14 @@ class AssetList extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
getAssetsTitle() {
|
getAssetsTitle() {
|
||||||
if (this.props.username === this.props.user.username) {
|
if (!this.props.username || this.props.username === this.props.user.username) {
|
||||||
return 'p5.js Web Editor | My assets';
|
return 'p5.js Web Editor | My assets';
|
||||||
}
|
}
|
||||||
return `p5.js Web Editor | ${this.props.username}'s assets`;
|
return `p5.js Web Editor | ${this.props.username}'s assets`;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const username = this.props.username !== undefined ? this.props.username : this.props.user.username;
|
||||||
return (
|
return (
|
||||||
<div className="asset-table-container">
|
<div className="asset-table-container">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
|
@ -49,7 +50,7 @@ class AssetList extends React.Component {
|
||||||
<td>{asset.name}</td>
|
<td>{asset.name}</td>
|
||||||
<td>{prettyBytes(asset.size)}</td>
|
<td>{prettyBytes(asset.size)}</td>
|
||||||
<td><Link to={asset.url} target="_blank">View</Link></td>
|
<td><Link to={asset.url} target="_blank">View</Link></td>
|
||||||
<td><Link to={`/${this.props.username}/sketches/${asset.sketchId}`}>{asset.sketchName}</Link></td>
|
<td><Link to={`/${username}/sketches/${asset.sketchId}`}>{asset.sketchName}</Link></td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
@ -4,19 +4,255 @@ import React from 'react';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
import InlineSVG from 'react-inlinesvg';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { browserHistory, Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import * as ProjectActions from '../actions/project';
|
import * as ProjectActions from '../actions/project';
|
||||||
import * as SketchActions from '../actions/projects';
|
import * as ProjectsActions from '../actions/projects';
|
||||||
import * as ToastActions from '../actions/toast';
|
import * as ToastActions from '../actions/toast';
|
||||||
import * as SortingActions from '../actions/sorting';
|
import * as SortingActions from '../actions/sorting';
|
||||||
|
import * as IdeActions from '../actions/ide';
|
||||||
import getSortedSketches from '../selectors/projects';
|
import getSortedSketches from '../selectors/projects';
|
||||||
import Loader from '../../App/components/loader';
|
import Loader from '../../App/components/loader';
|
||||||
|
|
||||||
const trashCan = require('../../../images/trash-can.svg');
|
|
||||||
const arrowUp = require('../../../images/sort-arrow-up.svg');
|
const arrowUp = require('../../../images/sort-arrow-up.svg');
|
||||||
const arrowDown = require('../../../images/sort-arrow-down.svg');
|
const arrowDown = require('../../../images/sort-arrow-down.svg');
|
||||||
|
const downFilledTriangle = require('../../../images/down-filled-triangle.svg');
|
||||||
|
|
||||||
|
class SketchListRowBase extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
optionsOpen: false,
|
||||||
|
renameOpen: false,
|
||||||
|
renameValue: props.sketch.name,
|
||||||
|
isFocused: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onFocusComponent = () => {
|
||||||
|
this.setState({ isFocused: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onBlurComponent = () => {
|
||||||
|
this.setState({ isFocused: false });
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!this.state.isFocused) {
|
||||||
|
this.closeAll();
|
||||||
|
}
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
openOptions = () => {
|
||||||
|
this.setState({
|
||||||
|
optionsOpen: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
closeOptions = () => {
|
||||||
|
this.setState({
|
||||||
|
optionsOpen: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleOptions = () => {
|
||||||
|
if (this.state.optionsOpen) {
|
||||||
|
this.closeOptions();
|
||||||
|
} else {
|
||||||
|
this.openOptions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openRename = () => {
|
||||||
|
this.setState({
|
||||||
|
renameOpen: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
closeRename = () => {
|
||||||
|
this.setState({
|
||||||
|
renameOpen: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
closeAll = () => {
|
||||||
|
this.setState({
|
||||||
|
renameOpen: false,
|
||||||
|
optionsOpen: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRenameChange = (e) => {
|
||||||
|
this.setState({
|
||||||
|
renameValue: e.target.value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRenameEnter = (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
// TODO pass this func
|
||||||
|
this.props.changeProjectName(this.props.sketch.id, this.state.renameValue);
|
||||||
|
this.closeAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resetSketchName = () => {
|
||||||
|
this.setState({
|
||||||
|
renameValue: this.props.sketch.name
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleDropdownOpen = () => {
|
||||||
|
this.closeAll();
|
||||||
|
this.openOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRenameOpen = () => {
|
||||||
|
this.closeAll();
|
||||||
|
this.openRename();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSketchDownload = () => {
|
||||||
|
this.props.exportProjectAsZip(this.props.sketch.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSketchDuplicate = () => {
|
||||||
|
this.closeAll();
|
||||||
|
this.props.cloneProject(this.props.sketch.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSketchShare = () => {
|
||||||
|
this.closeAll();
|
||||||
|
this.props.showShareModal(this.props.sketch.id, this.props.sketch.name, this.props.username);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSketchDelete = () => {
|
||||||
|
this.closeAll();
|
||||||
|
if (window.confirm(`Are you sure you want to delete "${this.props.sketch.name}"?`)) {
|
||||||
|
this.props.deleteProject(this.props.sketch.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { sketch, username } = this.props;
|
||||||
|
const { renameOpen, optionsOpen, renameValue } = this.state;
|
||||||
|
const userIsOwner = this.props.user.username === this.props.username;
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
className="sketches-table__row"
|
||||||
|
key={sketch.id}
|
||||||
|
>
|
||||||
|
<th scope="row">
|
||||||
|
<Link to={`/${username}/sketches/${sketch.id}`}>
|
||||||
|
{renameOpen ? '' : sketch.name}
|
||||||
|
</Link>
|
||||||
|
{renameOpen
|
||||||
|
&&
|
||||||
|
<input
|
||||||
|
value={renameValue}
|
||||||
|
onChange={this.handleRenameChange}
|
||||||
|
onKeyUp={this.handleRenameEnter}
|
||||||
|
onBlur={this.resetSketchName}
|
||||||
|
onClick={e => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</th>
|
||||||
|
<td>{format(new Date(sketch.createdAt), 'MMM D, YYYY h:mm A')}</td>
|
||||||
|
<td>{format(new Date(sketch.updatedAt), 'MMM D, YYYY h:mm A')}</td>
|
||||||
|
<td className="sketch-list__dropdown-column">
|
||||||
|
<button
|
||||||
|
className="sketch-list__dropdown-button"
|
||||||
|
onClick={this.toggleOptions}
|
||||||
|
onBlur={this.onBlurComponent}
|
||||||
|
onFocus={this.onFocusComponent}
|
||||||
|
>
|
||||||
|
<InlineSVG src={downFilledTriangle} alt="Menu" />
|
||||||
|
</button>
|
||||||
|
{optionsOpen &&
|
||||||
|
<ul
|
||||||
|
className="sketch-list__action-dialogue"
|
||||||
|
>
|
||||||
|
{userIsOwner &&
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
className="sketch-list__action-option"
|
||||||
|
onClick={this.handleRenameOpen}
|
||||||
|
onBlur={this.onBlurComponent}
|
||||||
|
onFocus={this.onFocusComponent}
|
||||||
|
>
|
||||||
|
Rename
|
||||||
|
</button>
|
||||||
|
</li>}
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
className="sketch-list__action-option"
|
||||||
|
onClick={this.handleSketchDownload}
|
||||||
|
onBlur={this.onBlurComponent}
|
||||||
|
onFocus={this.onFocusComponent}
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
{this.props.user.authenticated &&
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
className="sketch-list__action-option"
|
||||||
|
onClick={this.handleSketchDuplicate}
|
||||||
|
onBlur={this.onBlurComponent}
|
||||||
|
onFocus={this.onFocusComponent}
|
||||||
|
>
|
||||||
|
Duplicate
|
||||||
|
</button>
|
||||||
|
</li>}
|
||||||
|
{ /* <li>
|
||||||
|
<button
|
||||||
|
className="sketch-list__action-option"
|
||||||
|
onClick={this.handleSketchShare}
|
||||||
|
onBlur={this.onBlurComponent}
|
||||||
|
onFocus={this.onFocusComponent}
|
||||||
|
>
|
||||||
|
Share
|
||||||
|
</button>
|
||||||
|
</li> */ }
|
||||||
|
{userIsOwner &&
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
className="sketch-list__action-option"
|
||||||
|
onClick={this.handleSketchDelete}
|
||||||
|
onBlur={this.onBlurComponent}
|
||||||
|
onFocus={this.onFocusComponent}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</li>}
|
||||||
|
</ul>}
|
||||||
|
</td>
|
||||||
|
</tr>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SketchListRowBase.propTypes = {
|
||||||
|
sketch: PropTypes.shape({
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
username: PropTypes.string.isRequired,
|
||||||
|
user: PropTypes.shape({
|
||||||
|
username: PropTypes.string,
|
||||||
|
authenticated: PropTypes.bool.isRequired
|
||||||
|
}).isRequired,
|
||||||
|
deleteProject: PropTypes.func.isRequired,
|
||||||
|
showShareModal: PropTypes.func.isRequired,
|
||||||
|
cloneProject: PropTypes.func.isRequired,
|
||||||
|
exportProjectAsZip: PropTypes.func.isRequired,
|
||||||
|
changeProjectName: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
function mapDispatchToPropsSketchListRow(dispatch) {
|
||||||
|
return bindActionCreators(Object.assign({}, ProjectActions, IdeActions), dispatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
const SketchListRow = connect(null, mapDispatchToPropsSketchListRow)(SketchListRowBase);
|
||||||
|
|
||||||
class SketchList extends React.Component {
|
class SketchList extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -83,43 +319,20 @@ class SketchList extends React.Component {
|
||||||
<table className="sketches-table" summary="table containing all saved projects">
|
<table className="sketches-table" summary="table containing all saved projects">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th className="sketch-list__trash-column" scope="col"></th>
|
|
||||||
{this._renderFieldHeader('name', 'Sketch')}
|
{this._renderFieldHeader('name', 'Sketch')}
|
||||||
{this._renderFieldHeader('createdAt', 'Date Created')}
|
{this._renderFieldHeader('createdAt', 'Date Created')}
|
||||||
{this._renderFieldHeader('updatedAt', 'Date Updated')}
|
{this._renderFieldHeader('updatedAt', 'Date Updated')}
|
||||||
|
<th scope="col"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{this.props.sketches.map(sketch =>
|
{this.props.sketches.map(sketch =>
|
||||||
// eslint-disable-next-line
|
(<SketchListRow
|
||||||
<tr
|
|
||||||
className="sketches-table__row visibility-toggle"
|
|
||||||
key={sketch.id}
|
key={sketch.id}
|
||||||
onClick={() => browserHistory.push(`/${username}/sketches/${sketch.id}`)}
|
sketch={sketch}
|
||||||
>
|
user={this.props.user}
|
||||||
<td className="sketch-list__trash-column">
|
username={username}
|
||||||
{(() => { // eslint-disable-line
|
/>))}
|
||||||
if (this.props.username === this.props.user.username || this.props.username === undefined) {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className="sketch-list__trash-button"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (window.confirm(`Are you sure you want to delete "${sketch.name}"?`)) {
|
|
||||||
this.props.deleteProject(sketch.id);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<InlineSVG src={trashCan} alt="Delete Project" />
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})()}
|
|
||||||
</td>
|
|
||||||
<th scope="row"><Link to={`/${username}/sketches/${sketch.id}`}>{sketch.name}</Link></th>
|
|
||||||
<td>{format(new Date(sketch.createdAt), 'MMM D, YYYY h:mm A')}</td>
|
|
||||||
<td>{format(new Date(sketch.updatedAt), 'MMM D, YYYY h:mm A')}</td>
|
|
||||||
</tr>)}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>}
|
</table>}
|
||||||
</div>
|
</div>
|
||||||
|
@ -129,7 +342,8 @@ class SketchList extends React.Component {
|
||||||
|
|
||||||
SketchList.propTypes = {
|
SketchList.propTypes = {
|
||||||
user: PropTypes.shape({
|
user: PropTypes.shape({
|
||||||
username: PropTypes.string
|
username: PropTypes.string,
|
||||||
|
authenticated: PropTypes.bool.isRequired
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
getProjects: PropTypes.func.isRequired,
|
getProjects: PropTypes.func.isRequired,
|
||||||
sketches: PropTypes.arrayOf(PropTypes.shape({
|
sketches: PropTypes.arrayOf(PropTypes.shape({
|
||||||
|
@ -140,16 +354,25 @@ SketchList.propTypes = {
|
||||||
})).isRequired,
|
})).isRequired,
|
||||||
username: PropTypes.string,
|
username: PropTypes.string,
|
||||||
loading: PropTypes.bool.isRequired,
|
loading: PropTypes.bool.isRequired,
|
||||||
deleteProject: PropTypes.func.isRequired,
|
|
||||||
toggleDirectionForField: PropTypes.func.isRequired,
|
toggleDirectionForField: PropTypes.func.isRequired,
|
||||||
resetSorting: PropTypes.func.isRequired,
|
resetSorting: PropTypes.func.isRequired,
|
||||||
sorting: PropTypes.shape({
|
sorting: PropTypes.shape({
|
||||||
field: PropTypes.string.isRequired,
|
field: PropTypes.string.isRequired,
|
||||||
direction: PropTypes.string.isRequired
|
direction: PropTypes.string.isRequired
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
|
project: PropTypes.shape({
|
||||||
|
id: PropTypes.string,
|
||||||
|
owner: PropTypes.shape({
|
||||||
|
id: PropTypes.string
|
||||||
|
})
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
SketchList.defaultProps = {
|
SketchList.defaultProps = {
|
||||||
|
project: {
|
||||||
|
id: undefined,
|
||||||
|
owner: undefined
|
||||||
|
},
|
||||||
username: undefined
|
username: undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -158,12 +381,13 @@ function mapStateToProps(state) {
|
||||||
user: state.user,
|
user: state.user,
|
||||||
sketches: getSortedSketches(state),
|
sketches: getSortedSketches(state),
|
||||||
sorting: state.sorting,
|
sorting: state.sorting,
|
||||||
loading: state.loading
|
loading: state.loading,
|
||||||
|
project: state.project
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch) {
|
function mapDispatchToProps(dispatch) {
|
||||||
return bindActionCreators(Object.assign({}, SketchActions, ProjectActions, ToastActions, SortingActions), dispatch);
|
return bindActionCreators(Object.assign({}, ProjectsActions, ToastActions, SortingActions), dispatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(SketchList);
|
export default connect(mapStateToProps, mapDispatchToProps)(SketchList);
|
||||||
|
|
|
@ -414,9 +414,9 @@ class IDEView extends React.Component {
|
||||||
closeOverlay={this.props.closeShareModal}
|
closeOverlay={this.props.closeShareModal}
|
||||||
>
|
>
|
||||||
<ShareModal
|
<ShareModal
|
||||||
projectId={this.props.project.id}
|
projectId={this.props.ide.shareModalProjectId}
|
||||||
projectName={this.props.project.name}
|
projectName={this.props.ide.shareModalProjectName}
|
||||||
ownerUsername={this.props.project.owner.username}
|
ownerUsername={this.props.ide.shareModalProjectUsername}
|
||||||
/>
|
/>
|
||||||
</Overlay>
|
</Overlay>
|
||||||
}
|
}
|
||||||
|
@ -481,6 +481,9 @@ IDEView.propTypes = {
|
||||||
projectOptionsVisible: PropTypes.bool.isRequired,
|
projectOptionsVisible: PropTypes.bool.isRequired,
|
||||||
newFolderModalVisible: PropTypes.bool.isRequired,
|
newFolderModalVisible: PropTypes.bool.isRequired,
|
||||||
shareModalVisible: PropTypes.bool.isRequired,
|
shareModalVisible: PropTypes.bool.isRequired,
|
||||||
|
shareModalProjectId: PropTypes.string.isRequired,
|
||||||
|
shareModalProjectName: PropTypes.string.isRequired,
|
||||||
|
shareModalProjectUsername: PropTypes.string.isRequired,
|
||||||
editorOptionsVisible: PropTypes.bool.isRequired,
|
editorOptionsVisible: PropTypes.bool.isRequired,
|
||||||
keyboardShortcutVisible: PropTypes.bool.isRequired,
|
keyboardShortcutVisible: PropTypes.bool.isRequired,
|
||||||
unsavedChanges: PropTypes.bool.isRequired,
|
unsavedChanges: PropTypes.bool.isRequired,
|
||||||
|
|
|
@ -10,6 +10,9 @@ const initialState = {
|
||||||
projectOptionsVisible: false,
|
projectOptionsVisible: false,
|
||||||
newFolderModalVisible: false,
|
newFolderModalVisible: false,
|
||||||
shareModalVisible: false,
|
shareModalVisible: false,
|
||||||
|
shareModalProjectId: null,
|
||||||
|
shareModalProjectName: null,
|
||||||
|
shareModalProjectUsername: null,
|
||||||
editorOptionsVisible: false,
|
editorOptionsVisible: false,
|
||||||
keyboardShortcutVisible: false,
|
keyboardShortcutVisible: false,
|
||||||
unsavedChanges: false,
|
unsavedChanges: false,
|
||||||
|
@ -61,7 +64,12 @@ const ide = (state = initialState, action) => {
|
||||||
case ActionTypes.CLOSE_NEW_FOLDER_MODAL:
|
case ActionTypes.CLOSE_NEW_FOLDER_MODAL:
|
||||||
return Object.assign({}, state, { newFolderModalVisible: false });
|
return Object.assign({}, state, { newFolderModalVisible: false });
|
||||||
case ActionTypes.SHOW_SHARE_MODAL:
|
case ActionTypes.SHOW_SHARE_MODAL:
|
||||||
return Object.assign({}, state, { shareModalVisible: true });
|
return Object.assign({}, state, {
|
||||||
|
shareModalVisible: true,
|
||||||
|
shareModalProjectId: action.payload.shareModalProjectId,
|
||||||
|
shareModalProjectName: action.payload.shareModalProjectName,
|
||||||
|
shareModalProjectUsername: action.payload.shareModalProjectUsername,
|
||||||
|
});
|
||||||
case ActionTypes.CLOSE_SHARE_MODAL:
|
case ActionTypes.CLOSE_SHARE_MODAL:
|
||||||
return Object.assign({}, state, { shareModalVisible: false });
|
return Object.assign({}, state, { shareModalVisible: false });
|
||||||
case ActionTypes.SHOW_EDITOR_OPTIONS:
|
case ActionTypes.SHOW_EDITOR_OPTIONS:
|
||||||
|
|
|
@ -7,6 +7,14 @@ const sketches = (state = [], action) => {
|
||||||
case ActionTypes.DELETE_PROJECT:
|
case ActionTypes.DELETE_PROJECT:
|
||||||
return state.filter(sketch =>
|
return state.filter(sketch =>
|
||||||
sketch.id !== action.id);
|
sketch.id !== action.id);
|
||||||
|
case ActionTypes.RENAME_PROJECT: {
|
||||||
|
return state.map((sketch) => {
|
||||||
|
if (sketch.id === action.payload.id) {
|
||||||
|
return { ...sketch, name: action.payload.name };
|
||||||
|
}
|
||||||
|
return { ...sketch };
|
||||||
|
});
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -214,6 +214,9 @@
|
||||||
height: auto;
|
height: auto;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
border-radius: #{6 / $base-font-size}rem;
|
border-radius: #{6 / $base-font-size}rem;
|
||||||
|
& li:first-child {
|
||||||
|
border-radius: #{5 / $base-font-size}rem #{5 / $base-font-size}rem 0 0;
|
||||||
|
}
|
||||||
& li:last-child {
|
& li:last-child {
|
||||||
border-radius: 0 0 #{5 / $base-font-size}rem #{5 / $base-font-size}rem;
|
border-radius: 0 0 #{5 / $base-font-size}rem #{5 / $base-font-size}rem;
|
||||||
}
|
}
|
||||||
|
@ -248,17 +251,9 @@
|
||||||
%dropdown-open-left {
|
%dropdown-open-left {
|
||||||
@extend %dropdown-open;
|
@extend %dropdown-open;
|
||||||
left: 0;
|
left: 0;
|
||||||
border-top-left-radius: 0px;
|
|
||||||
& li:first-child {
|
|
||||||
border-radius: 0 #{5 / $base-font-size}rem 0 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
%dropdown-open-right {
|
%dropdown-open-right {
|
||||||
@extend %dropdown-open;
|
@extend %dropdown-open;
|
||||||
right: 0;
|
right: 0;
|
||||||
border-top-right-radius: 0px;
|
|
||||||
& li:first-child {
|
|
||||||
border-radius: #{5 / $base-font-size}rem 0 0 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar__add {
|
.sidebar__add {
|
||||||
|
width: #{20 / $base-font-size}rem;
|
||||||
|
height: #{20 / $base-font-size}rem;
|
||||||
@include icon();
|
@include icon();
|
||||||
.sidebar--contracted & {
|
.sidebar--contracted & {
|
||||||
display: none;
|
display: none;
|
||||||
|
@ -121,10 +123,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar__file-item-show-options {
|
.sidebar__file-item-show-options {
|
||||||
|
width: #{20 / $base-font-size}rem;
|
||||||
|
height: #{20 / $base-font-size}rem;
|
||||||
@include icon();
|
@include icon();
|
||||||
@include themify() {
|
@include themify() {
|
||||||
padding: #{4 / $base-font-size}rem 0;
|
margin-right: #{5 / $base-font-size}rem;
|
||||||
padding-right: #{6 / $base-font-size}rem;
|
|
||||||
}
|
}
|
||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
@ -10,8 +10,9 @@
|
||||||
padding: #{10 / $base-font-size}rem #{20 / $base-font-size}rem;
|
padding: #{10 / $base-font-size}rem #{20 / $base-font-size}rem;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
& .sketch-list__trash-column {
|
& .sketch-list__dropdown-column {
|
||||||
width: #{23 / $base-font-size}rem;
|
width: #{60 / $base-font-size}rem;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,11 +45,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sketches-table thead th:nth-child(1){
|
||||||
|
padding-left: #{12 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
||||||
.sketches-table__row {
|
.sketches-table__row {
|
||||||
margin: #{10 / $base-font-size}rem;
|
margin: #{10 / $base-font-size}rem;
|
||||||
height: #{72 / $base-font-size}rem;
|
height: #{72 / $base-font-size}rem;
|
||||||
font-size: #{16 / $base-font-size}rem;
|
font-size: #{16 / $base-font-size}rem;
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sketches-table__row:nth-child(odd) {
|
.sketches-table__row:nth-child(odd) {
|
||||||
|
@ -57,6 +61,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sketches-table__row > th:nth-child(1) {
|
||||||
|
padding-left: #{12 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
||||||
.sketches-table__row a {
|
.sketches-table__row a {
|
||||||
@include themify() {
|
@include themify() {
|
||||||
color: getThemifyVariable('primary-text-color');
|
color: getThemifyVariable('primary-text-color');
|
||||||
|
@ -75,28 +83,26 @@
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.visibility-toggle .sketch-list__trash-button {
|
.sketch-list__dropdown-button {
|
||||||
@extend %hidden-element;
|
width:#{25 / $base-font-size}rem;
|
||||||
width:#{20 / $base-font-size}rem;
|
height:#{25 / $base-font-size}rem;
|
||||||
height:#{20 / $base-font-size}rem;
|
@include themify() {
|
||||||
}
|
& polygon {
|
||||||
|
fill: getThemifyVariable('dropdown-color');
|
||||||
.visibility-toggle:hover .sketch-list__trash-button {
|
|
||||||
@include themify() {
|
|
||||||
background-color: transparent;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0;
|
|
||||||
position: initial;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
& g {
|
|
||||||
opacity: 1;
|
|
||||||
fill: getThemifyVariable('icon-hover-color');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sketch-list__action-dialogue {
|
||||||
|
@extend %dropdown-open-right;
|
||||||
|
top: 63%;
|
||||||
|
right: calc(100% - 26px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sketch-list__action-option {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
.sketches-table__empty {
|
.sketches-table__empty {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: #{16 / $base-font-size}rem;
|
font-size: #{16 / $base-font-size}rem;
|
||||||
|
|
|
@ -40,7 +40,7 @@ export function updateProject(req, res) {
|
||||||
res.json({ success: false });
|
res.json({ success: false });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (updatedProject.files.length !== req.body.files.length) {
|
if (req.body.files && updatedProject.files.length !== req.body.files.length) {
|
||||||
const oldFileIds = updatedProject.files.map(file => file.id);
|
const oldFileIds = updatedProject.files.map(file => file.id);
|
||||||
const newFileIds = req.body.files.map(file => file.id);
|
const newFileIds = req.body.files.map(file => file.id);
|
||||||
const staleIds = oldFileIds.filter(id => newFileIds.indexOf(id) === -1);
|
const staleIds = oldFileIds.filter(id => newFileIds.indexOf(id) === -1);
|
||||||
|
|
Loading…
Reference in a new issue