Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Private Sketch feat. #3034

Open
wants to merge 11 commits into
base: develop
Choose a base branch
from
1 change: 1 addition & 0 deletions client/constants.js
Original file line number Diff line number Diff line change
@@ -41,6 +41,7 @@ export const DELETE_COLLECTION = 'DELETE_COLLECTION';
export const ADD_TO_COLLECTION = 'ADD_TO_COLLECTION';
export const REMOVE_FROM_COLLECTION = 'REMOVE_FROM_COLLECTION';
export const EDIT_COLLECTION = 'EDIT_COLLECTION';
export const CHANGE_VISIBILITY = 'CHANGE_VISIBILITY';

export const DELETE_PROJECT = 'DELETE_PROJECT';

39 changes: 39 additions & 0 deletions client/modules/IDE/actions/project.js
Original file line number Diff line number Diff line change
@@ -410,3 +410,42 @@ export function deleteProject(id) {
});
};
}
export function changeVisibility(projectId, projectName, visibility) {
return (dispatch, getState) => {
const state = getState();

apiClient
.patch('/project/visibility', { projectId, visibility })
.then((response) => {
if (response.status === 200) {
const { visibility: newVisibility } = response.data;

dispatch({
type: ActionTypes.CHANGE_VISIBILITY,
payload: {
id: response.data.id,
visibility: newVisibility
}
});

if (state.project.id === response.data.id) {
dispatch({
type: ActionTypes.SET_PROJECT_NAME,
name: response.data.name
});
}

dispatch(
setToastText(`${projectName} is now ${newVisibility.toLowerCase()}`)
);
dispatch(showToast(2000));
}
})
.catch((error) => {
dispatch({
type: ActionTypes.ERROR,
error: error?.response?.data
});
});
};
}
51 changes: 47 additions & 4 deletions client/modules/IDE/components/Header/MobileNav.jsx
Original file line number Diff line number Diff line change
@@ -35,6 +35,7 @@ import { setLanguage } from '../../actions/preferences';
import Overlay from '../../../App/components/Overlay';
import ProjectName from './ProjectName';
import CollectionCreate from '../../../User/components/CollectionCreate';
import { changeVisibility } from '../../actions/project';

const Nav = styled(NavBar)`
background: ${prop('MobilePanel.default.background')};
@@ -75,6 +76,13 @@ const Title = styled.div`
margin: 0;
}

> section {
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
}

> h5 {
font-size: ${remSize(13)};
font-weight: normal;
@@ -203,6 +211,7 @@ const LanguageSelect = styled.div`
const MobileNav = () => {
const project = useSelector((state) => state.project);
const user = useSelector((state) => state.user);
const dispatch = useDispatch();

const { t } = useTranslation();

@@ -228,19 +237,53 @@ const MobileNav = () => {
}

const title = useMemo(resolveTitle, [pageName, project.name]);
const userIsOwner = user?.username === project.owner?.username;

const Logo = AsteriskIcon;

const toggleVisibility = (e) => {
try {
const isChecked = e.target.checked;

dispatch(
changeVisibility(
project.id,
project.name,
isChecked ? 'Private' : 'Public'
)
);
} catch (error) {
console.log(error);
}
};
return (
<Nav>
<LogoContainer>
<Logo />
</LogoContainer>
<Title>
<h1>{title === project.name ? <ProjectName /> : title}</h1>
{project?.owner && title === project.name && (
<h5>by {project?.owner?.username}</h5>
)}
<h1>{title === project?.name ? <ProjectName /> : title}</h1>
{(() => {
if (project?.owner && title === project.name && userIsOwner) {
return (
<main className="toolbar__makeprivate">
<p>Private</p>
<input
className="toolbar__togglevisibility"
type="checkbox"
onChange={toggleVisibility}
defaultChecked={project.visibility === 'Private'}
/>
</main>
);
}
if (project?.owner && title === project.name) {
return <h5>by {project?.owner?.username}</h5>;
}
return null;
})()}
</Title>

{/* check if the user is in login page */}
{pageName === 'login' || pageName === 'signup' ? (
// showing the CrossIcon
36 changes: 33 additions & 3 deletions client/modules/IDE/components/Header/Toolbar.jsx
Original file line number Diff line number Diff line change
@@ -15,22 +15,39 @@ import {
setGridOutput,
setTextOutput
} from '../../actions/preferences';

import PlayIcon from '../../../../images/play.svg';
import StopIcon from '../../../../images/stop.svg';
import PreferencesIcon from '../../../../images/preferences.svg';
import ProjectName from './ProjectName';
import { changeVisibility } from '../../actions/project';

const Toolbar = (props) => {
const { isPlaying, infiniteLoop, preferencesIsVisible } = useSelector(
(state) => state.ide
);
const project = useSelector((state) => state.project);
const user = useSelector((state) => state.user);
const autorefresh = useSelector((state) => state.preferences.autorefresh);
const dispatch = useDispatch();

const { t } = useTranslation();

console.log(project.visibility);
const userIsOwner = user?.username === project.owner?.username;
const toggleVisibility = (e) => {
try {
const isChecked = e.target.checked;
dispatch(
changeVisibility(
project.id,
project.name,
isChecked ? 'Private' : 'Public'
)
);
} catch (error) {
console.log(error);
}
};

const playButtonClass = classNames({
'toolbar__play-button': true,
'toolbar__play-button--selected': isPlaying
@@ -99,7 +116,20 @@ const Toolbar = (props) => {
<div className="toolbar__project-name-container">
<ProjectName />
{(() => {
if (project.owner) {
if (project?.owner && userIsOwner) {
return (
<main className="toolbar__makeprivate">
<p>Private</p>
<input
type="checkbox"
className="toolbar__togglevisibility"
defaultChecked={project.visibility === 'Private'}
onChange={toggleVisibility}
/>
</main>
);
}
if (project?.owner && !userIsOwner) {
return (
<p className="toolbar__project-project.owner">
{t('Toolbar.By')}{' '}
Original file line number Diff line number Diff line change
@@ -150,6 +150,22 @@ exports[`Nav renders dashboard version for mobile 1`] = `
margin: 0;
}

.c2 > section {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
gap: 5px;
}

.c2 > h5 {
font-size: 1.0833333333333333rem;
font-weight: normal;
@@ -780,6 +796,22 @@ exports[`Nav renders editor version for mobile 1`] = `
margin: 0;
}

.c2 > section {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
gap: 5px;
}

.c2 > h5 {
font-size: 1.0833333333333333rem;
font-weight: normal;
83 changes: 75 additions & 8 deletions client/modules/IDE/components/SketchList.jsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable max-len */
import PropTypes from 'prop-types';
import React from 'react';
import { Helmet } from 'react-helmet';
@@ -21,25 +22,23 @@ import Loader from '../../App/components/loader';
import Overlay from '../../App/components/Overlay';
import AddToCollectionList from './AddToCollectionList';
import getConfig from '../../../utils/getConfig';

import ArrowUpIcon from '../../../images/sort-arrow-up.svg';
import ArrowDownIcon from '../../../images/sort-arrow-down.svg';

const ROOT_URL = getConfig('API_URL');

const formatDateCell = (date, mobile = false) =>
dates.format(date, { showTime: !mobile });

class SketchListRowBase extends React.Component {
constructor(props) {
super(props);
this.state = {
renameOpen: false,
renameValue: props.sketch.name
renameValue: props.sketch.name,
isPrivate: props.sketch.visibility === 'Private'
};
this.renameInput = React.createRef();
}

openRename = () => {
this.setState(
{
@@ -118,6 +117,61 @@ class SketchListRowBase extends React.Component {
}
};

handleToggleVisibilityChange = (e, index, name) => {
const isChecked = e.target.checked;
this.setState(
{
isPrivate: isChecked
},
() => {
console.log(this.state.isPrivate);
const newVisibility = isChecked ? 'Private' : 'Public';
this.props.changeVisibility(index, name, newVisibility);
}
);
};
renderToggleVisibility = (index, name) => (
<div key={index}>
<input
defaultChecked={this.props.sketch.visibility === 'Private'}
type="checkbox"
className="visibility__toggle-checkbox"
id={`toggle-${index}`}
onChange={(e) => this.handleToggleVisibilityChange(e, index, name)}
/>

<label htmlFor={`toggle-${index}`} className="visibility__toggle-label">
<svg
width="8"
height="11"
viewBox="0 0 8 11"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="lock"
>
<path
d="M8 5.68627V10.0784C8 10.5882 7.54067 11 7.00478 11H0.995215C0.440191 11 0 10.5686 0 10.0784V5.68627C0 5.17647 0.440191 4.7451 0.995215 4.7451C1.09035 4.7451 1.16746 4.66798 1.16746 4.57285V2.90196C1.16746 1.29412 2.43062 0 3.98086 0C5.55024 0 6.8134 1.29412 6.8134 2.90196V4.55371C6.8134 4.65941 6.89908 4.7451 7.00478 4.7451C7.54067 4.7451 8 5.15686 8 5.68627ZM2.33716 3.11732C2.34653 4.01904 3.08017 4.7451 3.98194 4.7451C4.89037 4.7451 5.62679 4.00867 5.62679 3.10024V2.90196C5.62679 1.96078 4.89952 1.21569 3.98086 1.21569C3.10048 1.21569 2.33493 1.96078 2.33493 2.90196L2.33716 3.11732Z"
fill="white"
fillOpacity="0.4"
/>
</svg>

<svg
width="10"
height="10"
viewBox="0 0 10 10"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="earth"
>
<path
d="M10 5C10 5.42308 9.96154 5.80769 9.86539 6.15385C9.32692 8.34615 7.34615 10 5 10C2.57692 10 0.538462 8.25 0.0961538 5.96154C0.0384615 5.65385 0 5.34615 0 5V4.98077C0 4.84615 7.26432e-08 4.73077 0.0192308 4.63461C0.125111 3.25818 0.781552 2.01128 1.78053 1.1614C1.91355 1.04823 2.15362 1.13705 2.32692 1.11538C2.67308 1.03846 2.90385 0.788462 3.07692 0.846154C3.26923 0.903846 3.42308 1.11538 3.19231 1.26923C2.94231 1.40385 2.88462 1.63462 3.01923 1.75C3.15385 1.86538 3.34615 1.63462 3.61538 1.63462C3.88462 1.63462 4.21154 1.96154 4.19231 2.21154C4.15385 2.55769 4.15385 3 4.30769 3.28846C4.46154 3.57692 4.80769 4.01923 5.23077 4.13462C5.61539 4.23077 6.26923 4.32692 6.34615 4.34615C6.63419 4.45588 6.57131 4.6983 6.33349 4.89437C6.21892 4.98883 6.11852 5.09107 6.09615 5.17308C5.86539 5.88462 6.84615 6.11538 6.67308 6.59615C6.55769 6.94231 6.17308 7.28846 6.03846 7.63462C5.95671 7.84484 5.9246 8.14727 5.98523 8.37391C6.02693 8.52981 6.28488 8.43597 6.40385 8.32692C6.63462 8.13462 7.11539 7.44231 7.44231 7.21154C7.78846 6.98077 8.57692 6.78846 8.82692 6.01923C8.96154 5.59615 8.94231 5.21154 8.36539 4.88462C7.78846 4.55769 8.17308 4.15385 7.67308 4.15385C7.17308 4.15385 7.15385 4.34615 6.78846 4.21154C5.53846 3.71154 5.90385 3.23077 6.21154 3.21154C6.34615 3.19231 6.48077 3.25 6.65385 3.32692C6.82692 3.42308 6.88462 3.32692 6.84615 3.05769C6.80769 2.78846 6.86539 2.42308 7 1.98077C7.21154 1.26923 6.80769 0.634615 6.19231 0.557692C5.61539 0.442308 5.57692 0.653846 5.19231 1.07692C4.90385 1.40385 4.34615 1.13462 3.88462 0.807692C3.61454 0.611276 3.90971 0.105968 4.2399 0.0560849C4.48198 0.019514 4.72894 0 4.98077 0C7.69649 0 9.91771 2.18723 9.97993 4.91613C9.9806 4.94511 10 4.97101 10 5Z"
fill="#929292"
/>
</svg>
</label>
</div>
);
renderDropdown = () => {
const userIsOwner = this.props.user.username === this.props.username;

@@ -136,6 +190,7 @@ class SketchListRowBase extends React.Component {
>
{this.props.t('SketchList.DropdownDuplicate')}
</MenuItem>

<MenuItem
hideIf={!this.props.user.authenticated}
onClick={() => {
@@ -159,7 +214,8 @@ class SketchListRowBase extends React.Component {
};

render() {
const { sketch, username, mobile } = this.props;
const { sketch, username, mobile, user } = this.props;
const userIsOwner = user.username === username;
const { renameOpen, renameValue } = this.state;
let url = `/${username}/sketches/${sketch.id}`;
if (username === 'p5') {
@@ -192,6 +248,9 @@ class SketchListRowBase extends React.Component {
<th scope="row">{name}</th>
<td>{formatDateCell(sketch.createdAt, mobile)}</td>
<td>{formatDateCell(sketch.updatedAt, mobile)}</td>
<td hidden={!userIsOwner}>
{this.renderToggleVisibility(sketch.id, sketch.name)}
</td>
{this.renderDropdown()}
</tr>
</React.Fragment>
@@ -204,7 +263,8 @@ SketchListRowBase.propTypes = {
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
createdAt: PropTypes.string.isRequired,
updatedAt: PropTypes.string.isRequired
updatedAt: PropTypes.string.isRequired,
visibility: PropTypes.string
}).isRequired,
username: PropTypes.string.isRequired,
user: PropTypes.shape({
@@ -216,6 +276,7 @@ SketchListRowBase.propTypes = {
cloneProject: PropTypes.func.isRequired,
changeProjectName: PropTypes.func.isRequired,
onAddToCollection: PropTypes.func.isRequired,
changeVisibility: PropTypes.func.isRequired,
mobile: PropTypes.bool,
t: PropTypes.func.isRequired
};
@@ -241,7 +302,6 @@ class SketchList extends React.Component {
super(props);
this.props.getProjects(this.props.username);
this.props.resetSorting();

this.state = {
isInitialDataLoad: true
};
@@ -354,6 +414,7 @@ class SketchList extends React.Component {
};

render() {
const userIsOwner = this.props.user.username === this.props.username;
const username =
this.props.username !== undefined
? this.props.username
@@ -389,6 +450,11 @@ class SketchList extends React.Component {
context: mobile ? 'mobile' : ''
})
)}

{userIsOwner &&
this._renderFieldHeader('makePrivate', 'Make Private', {
context: mobile ? 'mobile' : ''
})}
<th scope="col"></th>
</tr>
</thead>
@@ -438,7 +504,8 @@ SketchList.propTypes = {
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
createdAt: PropTypes.string.isRequired,
updatedAt: PropTypes.string.isRequired
updatedAt: PropTypes.string.isRequired,
visibility: PropTypes.string
})
).isRequired,
username: PropTypes.string,
Original file line number Diff line number Diff line change
@@ -79,6 +79,20 @@ exports[`<Sketchlist /> snapshot testing 1`] = `
</span>
</button>
</th>
<th
scope="col"
>
<button
aria-label="Sort by Make Private descending."
class="sketch-list__sort-button"
>
<span
class="sketches-table__header"
>
Make Private
</span>
</button>
</th>
<th
scope="col"
/>
@@ -103,6 +117,47 @@ exports[`<Sketchlist /> snapshot testing 1`] = `
<td>
Feb 26, 2021, 4:58:29 AM
</td>
<td>
<div>
<input
class="visibility__toggle-checkbox"
id="toggle-testid1"
type="checkbox"
/>
<label
class="visibility__toggle-label"
for="toggle-testid1"
>
<svg
class="lock"
fill="none"
height="11"
viewBox="0 0 8 11"
width="8"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8 5.68627V10.0784C8 10.5882 7.54067 11 7.00478 11H0.995215C0.440191 11 0 10.5686 0 10.0784V5.68627C0 5.17647 0.440191 4.7451 0.995215 4.7451C1.09035 4.7451 1.16746 4.66798 1.16746 4.57285V2.90196C1.16746 1.29412 2.43062 0 3.98086 0C5.55024 0 6.8134 1.29412 6.8134 2.90196V4.55371C6.8134 4.65941 6.89908 4.7451 7.00478 4.7451C7.54067 4.7451 8 5.15686 8 5.68627ZM2.33716 3.11732C2.34653 4.01904 3.08017 4.7451 3.98194 4.7451C4.89037 4.7451 5.62679 4.00867 5.62679 3.10024V2.90196C5.62679 1.96078 4.89952 1.21569 3.98086 1.21569C3.10048 1.21569 2.33493 1.96078 2.33493 2.90196L2.33716 3.11732Z"
fill="white"
fill-opacity="0.4"
/>
</svg>
<svg
class="earth"
fill="none"
height="10"
viewBox="0 0 10 10"
width="10"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10 5C10 5.42308 9.96154 5.80769 9.86539 6.15385C9.32692 8.34615 7.34615 10 5 10C2.57692 10 0.538462 8.25 0.0961538 5.96154C0.0384615 5.65385 0 5.34615 0 5V4.98077C0 4.84615 7.26432e-08 4.73077 0.0192308 4.63461C0.125111 3.25818 0.781552 2.01128 1.78053 1.1614C1.91355 1.04823 2.15362 1.13705 2.32692 1.11538C2.67308 1.03846 2.90385 0.788462 3.07692 0.846154C3.26923 0.903846 3.42308 1.11538 3.19231 1.26923C2.94231 1.40385 2.88462 1.63462 3.01923 1.75C3.15385 1.86538 3.34615 1.63462 3.61538 1.63462C3.88462 1.63462 4.21154 1.96154 4.19231 2.21154C4.15385 2.55769 4.15385 3 4.30769 3.28846C4.46154 3.57692 4.80769 4.01923 5.23077 4.13462C5.61539 4.23077 6.26923 4.32692 6.34615 4.34615C6.63419 4.45588 6.57131 4.6983 6.33349 4.89437C6.21892 4.98883 6.11852 5.09107 6.09615 5.17308C5.86539 5.88462 6.84615 6.11538 6.67308 6.59615C6.55769 6.94231 6.17308 7.28846 6.03846 7.63462C5.95671 7.84484 5.9246 8.14727 5.98523 8.37391C6.02693 8.52981 6.28488 8.43597 6.40385 8.32692C6.63462 8.13462 7.11539 7.44231 7.44231 7.21154C7.78846 6.98077 8.57692 6.78846 8.82692 6.01923C8.96154 5.59615 8.94231 5.21154 8.36539 4.88462C7.78846 4.55769 8.17308 4.15385 7.67308 4.15385C7.17308 4.15385 7.15385 4.34615 6.78846 4.21154C5.53846 3.71154 5.90385 3.23077 6.21154 3.21154C6.34615 3.19231 6.48077 3.25 6.65385 3.32692C6.82692 3.42308 6.88462 3.32692 6.84615 3.05769C6.80769 2.78846 6.86539 2.42308 7 1.98077C7.21154 1.26923 6.80769 0.634615 6.19231 0.557692C5.61539 0.442308 5.57692 0.653846 5.19231 1.07692C4.90385 1.40385 4.34615 1.13462 3.88462 0.807692C3.61454 0.611276 3.90971 0.105968 4.2399 0.0560849C4.48198 0.019514 4.72894 0 4.98077 0C7.69649 0 9.91771 2.18723 9.97993 4.91613C9.9806 4.94511 10 4.97101 10 5Z"
fill="#929292"
/>
</svg>
</label>
</div>
</td>
<td
class="sketch-list__dropdown-column"
>
@@ -139,6 +194,47 @@ exports[`<Sketchlist /> snapshot testing 1`] = `
<td>
Feb 23, 2021, 5:40:43 PM
</td>
<td>
<div>
<input
class="visibility__toggle-checkbox"
id="toggle-testid2"
type="checkbox"
/>
<label
class="visibility__toggle-label"
for="toggle-testid2"
>
<svg
class="lock"
fill="none"
height="11"
viewBox="0 0 8 11"
width="8"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8 5.68627V10.0784C8 10.5882 7.54067 11 7.00478 11H0.995215C0.440191 11 0 10.5686 0 10.0784V5.68627C0 5.17647 0.440191 4.7451 0.995215 4.7451C1.09035 4.7451 1.16746 4.66798 1.16746 4.57285V2.90196C1.16746 1.29412 2.43062 0 3.98086 0C5.55024 0 6.8134 1.29412 6.8134 2.90196V4.55371C6.8134 4.65941 6.89908 4.7451 7.00478 4.7451C7.54067 4.7451 8 5.15686 8 5.68627ZM2.33716 3.11732C2.34653 4.01904 3.08017 4.7451 3.98194 4.7451C4.89037 4.7451 5.62679 4.00867 5.62679 3.10024V2.90196C5.62679 1.96078 4.89952 1.21569 3.98086 1.21569C3.10048 1.21569 2.33493 1.96078 2.33493 2.90196L2.33716 3.11732Z"
fill="white"
fill-opacity="0.4"
/>
</svg>
<svg
class="earth"
fill="none"
height="10"
viewBox="0 0 10 10"
width="10"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10 5C10 5.42308 9.96154 5.80769 9.86539 6.15385C9.32692 8.34615 7.34615 10 5 10C2.57692 10 0.538462 8.25 0.0961538 5.96154C0.0384615 5.65385 0 5.34615 0 5V4.98077C0 4.84615 7.26432e-08 4.73077 0.0192308 4.63461C0.125111 3.25818 0.781552 2.01128 1.78053 1.1614C1.91355 1.04823 2.15362 1.13705 2.32692 1.11538C2.67308 1.03846 2.90385 0.788462 3.07692 0.846154C3.26923 0.903846 3.42308 1.11538 3.19231 1.26923C2.94231 1.40385 2.88462 1.63462 3.01923 1.75C3.15385 1.86538 3.34615 1.63462 3.61538 1.63462C3.88462 1.63462 4.21154 1.96154 4.19231 2.21154C4.15385 2.55769 4.15385 3 4.30769 3.28846C4.46154 3.57692 4.80769 4.01923 5.23077 4.13462C5.61539 4.23077 6.26923 4.32692 6.34615 4.34615C6.63419 4.45588 6.57131 4.6983 6.33349 4.89437C6.21892 4.98883 6.11852 5.09107 6.09615 5.17308C5.86539 5.88462 6.84615 6.11538 6.67308 6.59615C6.55769 6.94231 6.17308 7.28846 6.03846 7.63462C5.95671 7.84484 5.9246 8.14727 5.98523 8.37391C6.02693 8.52981 6.28488 8.43597 6.40385 8.32692C6.63462 8.13462 7.11539 7.44231 7.44231 7.21154C7.78846 6.98077 8.57692 6.78846 8.82692 6.01923C8.96154 5.59615 8.94231 5.21154 8.36539 4.88462C7.78846 4.55769 8.17308 4.15385 7.67308 4.15385C7.17308 4.15385 7.15385 4.34615 6.78846 4.21154C5.53846 3.71154 5.90385 3.23077 6.21154 3.21154C6.34615 3.19231 6.48077 3.25 6.65385 3.32692C6.82692 3.42308 6.88462 3.32692 6.84615 3.05769C6.80769 2.78846 6.86539 2.42308 7 1.98077C7.21154 1.26923 6.80769 0.634615 6.19231 0.557692C5.61539 0.442308 5.57692 0.653846 5.19231 1.07692C4.90385 1.40385 4.34615 1.13462 3.88462 0.807692C3.61454 0.611276 3.90971 0.105968 4.2399 0.0560849C4.48198 0.019514 4.72894 0 4.98077 0C7.69649 0 9.91771 2.18723 9.97993 4.91613C9.9806 4.94511 10 4.97101 10 5Z"
fill="#929292"
/>
</svg>
</label>
</div>
</td>
<td
class="sketch-list__dropdown-column"
>
12 changes: 9 additions & 3 deletions client/modules/IDE/pages/IDEView.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useEffect, useRef, useState } from 'react';
import { useLocation, Prompt, useParams } from 'react-router-dom';
import { useLocation, Prompt, useParams, useHistory } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { Helmet } from 'react-helmet';
@@ -10,7 +10,6 @@ import PreviewFrame from '../components/PreviewFrame';
import Console from '../components/Console';
import Toast from '../components/Toast';
import { updateFileContent } from '../actions/files';

import {
autosaveProject,
clearPersistedState,
@@ -104,7 +103,7 @@ const IDEView = () => {
const [sidebarSize, setSidebarSize] = useState(160);
const [isOverlayVisible, setIsOverlayVisible] = useState(false);
const [MaxSize, setMaxSize] = useState(window.innerWidth);

const history = useHistory();
const cmRef = useRef({});

const autosaveIntervalRef = useRef(null);
@@ -125,6 +124,13 @@ const IDEView = () => {
}
}, [dispatch, params, project.id]);

useEffect(() => {
if (!isUserOwner && project.visibility === 'Private') {
// TODO: we might want to have a 'Sorry, this sketch is private' page for this
history.push('/');
}
}, [isUserOwner, project.visibility, history]);

const autosaveAllowed = isUserOwner && project.id && preferences.autosave;
const shouldAutosave = autosaveAllowed && ide.unsavedChanges;

9 changes: 6 additions & 3 deletions client/modules/IDE/reducers/project.js
Original file line number Diff line number Diff line change
@@ -8,7 +8,8 @@ const initialState = () => {
return {
name: generatedName,
updatedAt: '',
isSaving: false
isSaving: false,
visibility: 'Public'
};
};

@@ -25,15 +26,17 @@ const project = (state, action) => {
name: action.project.name,
updatedAt: action.project.updatedAt,
owner: action.owner,
isSaving: false
isSaving: false,
visibility: action.project.visibility
};
case ActionTypes.SET_PROJECT:
return {
id: action.project.id,
name: action.project.name,
updatedAt: action.project.updatedAt,
owner: action.owner,
isSaving: false
isSaving: false,
visibility: action.project.visibility
};
case ActionTypes.RESET_PROJECT:
return initialState();
8 changes: 8 additions & 0 deletions client/modules/IDE/reducers/projects.js
Original file line number Diff line number Diff line change
@@ -6,6 +6,14 @@ const sketches = (state = [], action) => {
return action.projects;
case ActionTypes.DELETE_PROJECT:
return state.filter((sketch) => sketch.id !== action.id);
case ActionTypes.CHANGE_VISIBILITY: {
return state.map((sketch) => {
if (sketch.id === action.payload.id) {
return { ...sketch, visibility: action.payload.visibility };
}
return { ...sketch };
});
}
case ActionTypes.RENAME_PROJECT: {
return state.map((sketch) => {
if (sketch.id === action.payload.id) {
26 changes: 18 additions & 8 deletions client/modules/User/components/Collection.jsx
Original file line number Diff line number Diff line change
@@ -32,6 +32,8 @@ const CollectionItemRowBase = ({

const projectIsDeleted = item.isDeleted;

const projectIsPrivate = !isOwner && item.project.visibility === 'Private';

const handleSketchRemove = () => {
const name = projectIsDeleted ? 'deleted sketch' : item.project.name;

@@ -44,21 +46,28 @@ const CollectionItemRowBase = ({
}
};

const name = projectIsDeleted ? (
<span>{t('Collection.SketchDeleted')}</span>
) : (
<Link to={`/${item.project.user.username}/sketches/${item.projectId}`}>
{item.project.name}
</Link>
);
let name;
if (projectIsDeleted) {
name = <span>{t('Collection.SketchDeleted')}</span>;
} else if (projectIsPrivate) {
name = <span>Sketch is Private</span>;
} else {
name = (
<Link to={`/${item.project.user.username}/sketches/${item.projectId}`}>
{item.project.name}
</Link>
);
}

const sketchOwnerUsername = projectIsDeleted
? null
: item.project.user.username;

return (
<tr
className={`sketches-table__row ${projectIsDeleted ? 'is-deleted' : ''}`}
className={`sketches-table__row ${
projectIsDeleted || projectIsPrivate ? 'is-deleted-or-private' : ''
}`}
>
<th scope="row">{name}</th>
<td>{dates.format(item.createdAt)}</td>
@@ -90,6 +99,7 @@ CollectionItemRowBase.propTypes = {
project: PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
visibility: PropTypes.string,
user: PropTypes.shape({
username: PropTypes.string.isRequired
})
85 changes: 76 additions & 9 deletions client/styles/components/_sketch-list.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,58 @@
@use "sass:math";

.sketch-visibility__title {
@include themify() {
color: getThemifyVariable('hint-arrow-background-color');
}
}

.sketch-visibility__icons {
height: 30px;
width: 30px;
display: flex;
gap: 5px;
}

.sketch-visibility {
padding: 30px;
display: flex;
flex-direction: column;
align-items: center;
}

.sketch-visibility hr {
border: none;
height: 1px;
background: linear-gradient(to right, transparent, white, transparent);
}

.sketch-visibility_ul {
list-style-type: none;
padding: 0;
margin: 0;
margin-bottom: 7px;
}

.sketch-visibility_ul li {
margin-bottom: 7px;
display: flex;
align-items: center;
}

.sketch-visibility_ul li::before {
content: "\2022";

@include themify() {
color: getThemifyVariable('hint-arrow-background-color');
}

font-size: 34px;
margin-right: 10px;
}




.sketches-table-container {
overflow-y: auto;
max-width: 100%;
@@ -26,6 +79,8 @@
flex-direction: column;
gap: #{math.div(12, $base-font-size)}rem;



.sketches-table__row {
margin: 0;
position: relative;
@@ -40,14 +95,15 @@
background-color: getThemifyVariable("search-background-color") !important;
}

> th {
padding-left: 0;
width: 100%;
.sketches-table_name {
display: flex;
gap: 5px;
font-weight: bold;
margin-bottom: #{math.div(6, $base-font-size)}rem;
align-items: center;
}

> td {

>td {
padding-left: 0;
width: 30%;
font-size: #{math.div(14, $base-font-size)}rem;
@@ -57,6 +113,13 @@
}
}

.sketches-table__rowname {
display: flex;
gap: 5px;
justify-content: center;
align-items: center;
}

.sketch-list__dropdown-column {
position: absolute;
top: 0;
@@ -75,6 +138,7 @@

max-height: 100%;
border-spacing: 0;

& .sketch-list__dropdown-column {
width: #{math.div(60, $base-font-size)}rem;
position: relative;
@@ -86,6 +150,7 @@
position: sticky;
top: 0;
z-index: 1;

@include themify() {
background-color: getThemifyVariable("background-color");
}
@@ -110,6 +175,7 @@
.sketches-table__header {
border-bottom: 2px dashed transparent;
padding: #{math.div(3, $base-font-size)}rem 0;

@include themify() {
color: getThemifyVariable("inactive-text-color");
}
@@ -137,11 +203,11 @@
}
}

.sketches-table__row > th:nth-child(1) {
.sketches-table__row>th:nth-child(1) {
padding-left: #{math.div(12, $base-font-size)}rem;
}

.sketches-table__row > td {
.sketches-table__row>td {
padding-left: #{math.div(8, $base-font-size)}rem;
}

@@ -151,12 +217,13 @@
}
}

.sketches-table__row.is-deleted > * {
.sketches-table__row.is-deleted-or-private>* {
font-style: italic;
}

.sketches-table thead {
font-size: #{math.div(12, $base-font-size)}rem;

@include themify() {
color: getThemifyVariable("inactive-text-color");
}
@@ -179,4 +246,4 @@
text-align: center;
font-size: #{math.div(16, $base-font-size)}rem;
padding: #{math.div(42, $base-font-size)}rem 0;
}
}
63 changes: 63 additions & 0 deletions client/styles/components/_toggle.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
.visibility__toggle-checkbox {
display: none;
}

.visibility__toggle-label {
position: relative;
cursor: pointer;
width: 50px;
height: 20px;
background: grey;
border-radius: 10px;
transition: background-color 0.3s;
display: flex;
justify-content: center;
align-items: center;
}

.lock,
.earth {
position: absolute;
height: 12px;
width: 12px;
}

.lock {
left: 4px;
}

.earth {
right: 4px;
}

.visibility__toggle-label::after {
content: '';
position: absolute;
top: 1px;
left: 1px;
width: 18px;
height: 18px;
background: #fff;
border-radius: 50%;
transition: all 0.3s;
display: flex;
justify-content: center;
align-items: center;
}

.visibility__toggle-checkbox:checked+.visibility__toggle-label {
background: #ED225D;
}

.visibility__toggle-checkbox:checked+.visibility__toggle-label::after {
left: calc(100% - 1px);
transform: translateX(-100%);
}

.visibility__toggle-label:active:after {
width: 30px;
}

.visibility__toggle-checkbox:checked+.visibility__toggle-label::after {
animation: slideText 0.3s ease-in-out forwards;
}
77 changes: 68 additions & 9 deletions client/styles/components/_toolbar.scss
Original file line number Diff line number Diff line change
@@ -7,23 +7,32 @@
justify-content: center;
align-items: center;
padding: 0 0 0 #{math.div(3, $base-font-size)}rem;

&--selected {
@extend %toolbar-button--selected;
}

&:disabled {
cursor: auto;
& g, & path {

& g,
& path {
fill: getThemifyVariable('button-border-color');
}

&:hover {
background-color: getThemifyVariable('toolbar-button-background-color');
& g, & path {

& g,
& path {
fill: getThemifyVariable('button-border-color');
}
}
}
}
}

margin-right: #{math.div(15, $base-font-size)}rem;

span {
padding-left: #{math.div(4, $base-font-size)}rem;
display: flex;
@@ -46,10 +55,12 @@
align-items: center;
margin-right: #{math.div(15, $base-font-size)}rem;
padding: 0;

&--selected {
@extend %toolbar-button--selected;
}
}

span {
display: flex;
align-items: center;
@@ -66,11 +77,14 @@
justify-content: center;
align-items: center;
padding: 0;

&--selected {
@extend %toolbar-button--selected;
}
}

margin-left: auto;

& span {
padding-left: #{math.div(1, $base-font-size)}rem;
display: flex;
@@ -83,8 +97,11 @@

.toolbar__logo {
margin-right: #{math.div(30, $base-font-size)}rem;

@include themify() {
& g, & path {

& g,
& path {
fill: getThemifyVariable('logo-color');
}
}
@@ -94,21 +111,43 @@
padding: #{math.div(10, $base-font-size)}rem #{math.div(20, $base-font-size)}rem;
display: flex;
align-items: center;

@include themify() {
border-bottom: 1px dashed map-get($theme-map, 'nav-border-color');
}
}

.lock-icon {
height: 60%;
width: 60%;
}

.unlock-icon {
height: 60%;
width: 60%;
}


.toolbar__project-name-container {
margin-left: #{math.div(10, $base-font-size)}rem;
padding-left: #{math.div(10, $base-font-size)}rem;
display: flex;
align-items: center;
display: flex;
align-items: center;

>section {
display: flex;
align-items: center;
justify-content: center;
height: 30px;
width: 30px;
}

}

.toolbar .editable-input__label {
@include themify() {
color: getThemifyVariable('secondary-text-color');

& path {
fill: getThemifyVariable('secondary-text-color');
}
@@ -121,19 +160,23 @@

.toolbar__project-owner {
margin-left: #{math.div(5, $base-font-size)}rem;

@include themify() {
color: getThemifyVariable('secondary-text-color');
}
}

.toolbar__autorefresh-label {
cursor: pointer;

@include themify() {
color: getThemifyVariable('secondary-text-color');

&:hover {
color: getThemifyVariable('logo-color');
}
}

margin-left: #{math.div(5, $base-font-size)}rem;
font-size: #{math.div(12, $base-font-size)}rem;
}
@@ -143,9 +186,25 @@
align-items: center;
}

.checkbox__autorefresh{
.checkbox__autorefresh {
cursor: pointer;
@include themify(){
accent-color:getThemifyVariable('logo-color');

@include themify() {
accent-color: getThemifyVariable('logo-color');
}
}


.toolbar__makeprivate {
display: flex;
gap: 2px;
align-items: center;
}

.toolbar__togglevisibility {
cursor: pointer;

@include themify() {
accent-color: getThemifyVariable('logo-color');
}
}
4 changes: 2 additions & 2 deletions client/styles/main.scss
Original file line number Diff line number Diff line change
@@ -54,6 +54,6 @@
@import 'components/collection';
@import 'components/collection-create';
@import 'components/quick-add';

@import 'components/toggle';
@import 'layout/dashboard';
@import 'layout/ide';
@import 'layout/ide';
Original file line number Diff line number Diff line change
@@ -60,7 +60,7 @@ export default function addProjectToCollection(req, res) {
{ path: 'owner', select: ['id', 'username'] },
{
path: 'items.project',
select: ['id', 'name', 'slug'],
select: ['id', 'name', 'slug', 'visibility'],
populate: {
path: 'user',
select: ['username']
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@ export default function createCollection(req, res) {
{ path: 'owner', select: ['id', 'username'] },
{
path: 'items.project',
select: ['id', 'name', 'slug'],
select: ['id', 'name', 'slug', 'visibility'],
populate: {
path: 'user',
select: ['username']
Original file line number Diff line number Diff line change
@@ -32,7 +32,7 @@ export default function listCollections(req, res) {
{ path: 'owner', select: ['id', 'username'] },
{
path: 'items.project',
select: ['id', 'name', 'slug'],
select: ['id', 'name', 'slug', 'visibility'],
populate: {
path: 'user',
select: ['username']
Original file line number Diff line number Diff line change
@@ -41,7 +41,7 @@ export default function addProjectToCollection(req, res) {
{ path: 'owner', select: ['id', 'username'] },
{
path: 'items.project',
select: ['id', 'name', 'slug'],
select: ['id', 'name', 'slug', 'visibility'],
populate: {
path: 'user',
select: ['username']
Original file line number Diff line number Diff line change
@@ -43,7 +43,7 @@ export default function createCollection(req, res) {
{ path: 'owner', select: ['id', 'username'] },
{
path: 'items.project',
select: ['id', 'name', 'slug'],
select: ['id', 'name', 'slug', 'visibility'],
populate: {
path: 'user',
select: ['username']
37 changes: 37 additions & 0 deletions server/controllers/project.controller.js
Original file line number Diff line number Diff line change
@@ -266,3 +266,40 @@ export async function downloadProjectAsZip(req, res) {
// save project to some path
buildZip(project, req, res);
}

export async function changeProjectVisibility(req, res) {
try {
const { projectId, visibility: newVisibility } = req.body;

const project = await Project.findOne({
$or: [{ _id: projectId }, { slug: projectId }]
});

if (!project) {
return res
.status(404)
.json({ success: false, message: 'No project found.' });
}

if (newVisibility !== 'Private' && newVisibility !== 'Public') {
return res.status(400).json({ success: false, message: 'Invalid data.' });
}

const updatedProject = await Project.findByIdAndUpdate(
projectId,
{
visibility: newVisibility
},
{
new: true,
runValidators: true
}
)
.populate('user', 'username')
.exec();

return res.status(200).json(updatedProject);
} catch (error) {
return res.status(500).json(error);
}
}
17 changes: 14 additions & 3 deletions server/controllers/project.controller/getProjectsForUser.js
Original file line number Diff line number Diff line change
@@ -10,24 +10,35 @@ import { toApi as toApiProjectObject } from '../../domain-objects/Project';
const createCoreHandler = (mapProjectsToResponse) => async (req, res) => {
try {
const { username } = req.params;

if (!username) {
res.status(422).json({ message: 'Username not provided' });
return;
}

const user = await User.findByUsername(username);
if (!user) {
res
.status(404)
.json({ message: 'User with that username does not exist.' });
return;
}
const projects = await Project.find({ user: user._id })

const canViewPrivate = req.user && req.user._id.equals(user._id);

const filter = { user: user._id };
if (!canViewPrivate) {
filter.visibility = { $ne: 'Private' };
}

const projects = await Project.find(filter)
.sort('-createdAt')
.select('name files id createdAt updatedAt')
.select('name files id createdAt updatedAt visibility')
.exec();

const response = mapProjectsToResponse(projects);
res.json(response);
} catch (e) {
} catch (error) {
res.status(500).json({ message: 'Error fetching projects' });
}
};
7 changes: 6 additions & 1 deletion server/models/project.js
Original file line number Diff line number Diff line change
@@ -38,6 +38,11 @@ const projectSchema = new Schema(
serveSecure: { type: Boolean, default: false },
files: { type: [fileSchema] },
_id: { type: String, default: shortid.generate },
visibility: {
type: String,
enum: ['Private', 'Public'],
default: 'Public'
},
slug: { type: String }
},
{ timestamps: true, usePushEach: true }
@@ -57,7 +62,7 @@ projectSchema.pre('save', function generateSlug(next) {
if (!project.slug) {
project.slug = slugify(project.name, '_');
}

project.visibility = 'Public';
return next();
});

2 changes: 2 additions & 0 deletions server/routes/project.routes.js
Original file line number Diff line number Diff line change
@@ -26,4 +26,6 @@ router.get('/:username/projects', ProjectController.getProjectsForUser);

router.get('/projects/:project_id/zip', ProjectController.downloadProjectAsZip);

router.patch('/project/visibility', ProjectController.changeProjectVisibility);

export default router;