Update sketch list styling (#819)

* parent b3c3efcec9
author Laksh Singla <lakshsingla@gmail.com> 1549106083 +0530
committer Cassie Tarakajian <ctarakajian@gmail.com> 1560540243 -0400

parent b3c3efcec9
author Laksh Singla <lakshsingla@gmail.com> 1549106083 +0530
committer Cassie Tarakajian <ctarakajian@gmail.com> 1560540198 -0400

parent b3c3efcec9
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:
Laksh Singla 2019-06-20 01:51:25 +05:30 committed by Cassie Tarakajian
parent 4bd081b307
commit 735adcfa05
14 changed files with 448 additions and 145 deletions

View file

@ -20,6 +20,7 @@ export const AUTH_ERROR = 'AUTH_ERROR';
export const SETTINGS_UPDATED = 'SETTINGS_UPDATED';
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_FAIL = 'PROJECT_SAVE_FAIL';

View file

@ -33,7 +33,7 @@ class Overlay extends React.Component {
return;
}
this.handleClickOutside();
this.handleClickOutside(e);
}
handleClickOutside() {
@ -49,6 +49,10 @@ class Overlay extends React.Component {
}
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) {
browserHistory.push(this.props.previousPath);
} else {

View file

@ -134,9 +134,17 @@ export function closeNewFolderModal() {
};
}
export function showShareModal() {
return {
type: ActionTypes.SHOW_SHARE_MODAL
export function showShareModal(projectId, projectName, ownerUsername) {
return (dispatch, getState) => {
const { project, user } = getState();
dispatch({
type: ActionTypes.SHOW_SHARE_MODAL,
payload: {
shareModalProjectId: projectId || project.id,
shareModalProjectName: projectName || project.name,
shareModalProjectUsername: ownerUsername || user.username
}
});
};
}

View file

@ -9,7 +9,8 @@ import {
setUnsavedChanges,
justOpenedProject,
resetJustOpenedProject,
showErrorModal
showErrorModal,
setPreviousPath
} from './ide';
import { clearState, saveState } from '../../../persistState';
@ -246,10 +247,23 @@ function generateNewIdsForChildren(file, files) {
file.children = newChildren; // eslint-disable-line
}
export function cloneProject() {
export function cloneProject(id) {
return (dispatch, getState) => {
dispatch(setUnsavedChanges(false));
const state = getState();
new Promise((resolve, reject) => {
if (!id) {
resolve(getState());
} else {
fetch(`${ROOT_URL}/projects/${id}`)
.then(res => res.json())
.then(data => resolve({
files: data.files,
project: {
name: data.name
}
}));
}
}).then((state) => {
const newFiles = state.files.map((file) => { // eslint-disable-line
return { ...file };
});
@ -288,6 +302,7 @@ export function cloneProject() {
error: response.data
}));
});
});
};
}
@ -309,3 +324,58 @@ export function setProjectSavedTime(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
});
}
});
};
}

View file

@ -1,12 +1,11 @@
import axios from 'axios';
import * as ActionTypes from '../../../constants';
import { showErrorModal, setPreviousPath } from './ide';
import { resetProject } from './project';
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());
@ -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
});
}
});
};
}

View file

@ -17,13 +17,14 @@ class AssetList extends React.Component {
}
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 | ${this.props.username}'s assets`;
}
render() {
const username = this.props.username !== undefined ? this.props.username : this.props.user.username;
return (
<div className="asset-table-container">
<Helmet>
@ -49,7 +50,7 @@ class AssetList extends React.Component {
<td>{asset.name}</td>
<td>{prettyBytes(asset.size)}</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>
))}
</tbody>

View file

@ -4,19 +4,255 @@ import React from 'react';
import { Helmet } from 'react-helmet';
import InlineSVG from 'react-inlinesvg';
import { connect } from 'react-redux';
import { browserHistory, Link } from 'react-router';
import { Link } from 'react-router';
import { bindActionCreators } from 'redux';
import classNames from 'classnames';
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 SortingActions from '../actions/sorting';
import * as IdeActions from '../actions/ide';
import getSortedSketches from '../selectors/projects';
import Loader from '../../App/components/loader';
const trashCan = require('../../../images/trash-can.svg');
const arrowUp = require('../../../images/sort-arrow-up.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 {
constructor(props) {
@ -83,43 +319,20 @@ class SketchList extends React.Component {
<table className="sketches-table" summary="table containing all saved projects">
<thead>
<tr>
<th className="sketch-list__trash-column" scope="col"></th>
{this._renderFieldHeader('name', 'Sketch')}
{this._renderFieldHeader('createdAt', 'Date Created')}
{this._renderFieldHeader('updatedAt', 'Date Updated')}
<th scope="col"></th>
</tr>
</thead>
<tbody>
{this.props.sketches.map(sketch =>
// eslint-disable-next-line
<tr
className="sketches-table__row visibility-toggle"
(<SketchListRow
key={sketch.id}
onClick={() => browserHistory.push(`/${username}/sketches/${sketch.id}`)}
>
<td className="sketch-list__trash-column">
{(() => { // 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>)}
sketch={sketch}
user={this.props.user}
username={username}
/>))}
</tbody>
</table>}
</div>
@ -129,7 +342,8 @@ class SketchList extends React.Component {
SketchList.propTypes = {
user: PropTypes.shape({
username: PropTypes.string
username: PropTypes.string,
authenticated: PropTypes.bool.isRequired
}).isRequired,
getProjects: PropTypes.func.isRequired,
sketches: PropTypes.arrayOf(PropTypes.shape({
@ -140,16 +354,25 @@ SketchList.propTypes = {
})).isRequired,
username: PropTypes.string,
loading: PropTypes.bool.isRequired,
deleteProject: PropTypes.func.isRequired,
toggleDirectionForField: PropTypes.func.isRequired,
resetSorting: PropTypes.func.isRequired,
sorting: PropTypes.shape({
field: PropTypes.string.isRequired,
direction: PropTypes.string.isRequired
}).isRequired,
project: PropTypes.shape({
id: PropTypes.string,
owner: PropTypes.shape({
id: PropTypes.string
})
})
};
SketchList.defaultProps = {
project: {
id: undefined,
owner: undefined
},
username: undefined
};
@ -158,12 +381,13 @@ function mapStateToProps(state) {
user: state.user,
sketches: getSortedSketches(state),
sorting: state.sorting,
loading: state.loading
loading: state.loading,
project: state.project
};
}
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);

View file

@ -414,9 +414,9 @@ class IDEView extends React.Component {
closeOverlay={this.props.closeShareModal}
>
<ShareModal
projectId={this.props.project.id}
projectName={this.props.project.name}
ownerUsername={this.props.project.owner.username}
projectId={this.props.ide.shareModalProjectId}
projectName={this.props.ide.shareModalProjectName}
ownerUsername={this.props.ide.shareModalProjectUsername}
/>
</Overlay>
}
@ -481,6 +481,9 @@ IDEView.propTypes = {
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,

View file

@ -10,6 +10,9 @@ const initialState = {
projectOptionsVisible: false,
newFolderModalVisible: false,
shareModalVisible: false,
shareModalProjectId: null,
shareModalProjectName: null,
shareModalProjectUsername: null,
editorOptionsVisible: false,
keyboardShortcutVisible: false,
unsavedChanges: false,
@ -61,7 +64,12 @@ const ide = (state = initialState, action) => {
case ActionTypes.CLOSE_NEW_FOLDER_MODAL:
return Object.assign({}, state, { newFolderModalVisible: false });
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:
return Object.assign({}, state, { shareModalVisible: false });
case ActionTypes.SHOW_EDITOR_OPTIONS:

View file

@ -7,6 +7,14 @@ const sketches = (state = [], action) => {
case ActionTypes.DELETE_PROJECT:
return state.filter(sketch =>
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:
return state;
}

View file

@ -214,6 +214,9 @@
height: auto;
z-index: 9999;
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 {
border-radius: 0 0 #{5 / $base-font-size}rem #{5 / $base-font-size}rem;
}
@ -248,17 +251,9 @@
%dropdown-open-left {
@extend %dropdown-open;
left: 0;
border-top-left-radius: 0px;
& li:first-child {
border-radius: 0 #{5 / $base-font-size}rem 0 0;
}
}
%dropdown-open-right {
@extend %dropdown-open;
right: 0;
border-top-right-radius: 0px;
& li:first-child {
border-radius: #{5 / $base-font-size}rem 0 0 0;
}
}

View file

@ -25,6 +25,8 @@
}
.sidebar__add {
width: #{20 / $base-font-size}rem;
height: #{20 / $base-font-size}rem;
@include icon();
.sidebar--contracted & {
display: none;
@ -121,10 +123,11 @@
}
.sidebar__file-item-show-options {
width: #{20 / $base-font-size}rem;
height: #{20 / $base-font-size}rem;
@include icon();
@include themify() {
padding: #{4 / $base-font-size}rem 0;
padding-right: #{6 / $base-font-size}rem;
margin-right: #{5 / $base-font-size}rem;
}
display: none;
position: absolute;

View file

@ -10,8 +10,9 @@
padding: #{10 / $base-font-size}rem #{20 / $base-font-size}rem;
max-height: 100%;
border-spacing: 0;
& .sketch-list__trash-column {
width: #{23 / $base-font-size}rem;
& .sketch-list__dropdown-column {
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 {
margin: #{10 / $base-font-size}rem;
height: #{72 / $base-font-size}rem;
font-size: #{16 / $base-font-size}rem;
cursor: pointer;
}
.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 {
@include themify() {
color: getThemifyVariable('primary-text-color');
@ -75,26 +83,24 @@
font-weight: normal;
}
.visibility-toggle .sketch-list__trash-button {
@extend %hidden-element;
width:#{20 / $base-font-size}rem;
height:#{20 / $base-font-size}rem;
.sketch-list__dropdown-button {
width:#{25 / $base-font-size}rem;
height:#{25 / $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 {

View file

@ -40,7 +40,7 @@ export function updateProject(req, res) {
res.json({ success: false });
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 newFileIds = req.body.files.map(file => file.id);
const staleIds = oldFileIds.filter(id => newFileIds.indexOf(id) === -1);