implement project manager and bind with actions and menus

This commit is contained in:
Val Erastov 2019-02-24 20:22:17 -08:00
parent f43ff8debf
commit f1ee1ba20a
8 changed files with 136 additions and 60 deletions

View file

@ -61,7 +61,7 @@ export default [
id: 'NativeFormatExport',
appearance: {
cssIcons: ['book'],
label: 'Download Bundle',
label: 'Download Project',
info: 'export model and its sketches as a json bundle',
},
invoke: (context) => context.services.export.nativeFormat()
@ -70,13 +70,43 @@ export default [
{
id: 'NativeFormatImport',
appearance: {
cssIcons: ['book'],
label: 'Import Bundle',
info: 'import native format json(model and its sketches)',
cssIcons: ['download', 'flip-vertical'],
label: 'Import Project',
info: 'empty current project and import replacing with native format json(model and its sketches)',
},
invoke: (context) => context.services.projectManager.importProject()
},
{
id: 'NativeFormatImportAs',
appearance: {
cssIcons: ['download', 'flip-vertical'],
label: 'Import Project as...',
info: 'import native format json(model and its sketches) as a new project',
},
invoke: (context) => context.services.projectManager.importProjectAs()
},
{
id: 'NewProject',
appearance: {
cssIcons: ['file-o'],
label: 'New Project...',
info: 'create new project and open in a new tab',
},
invoke: (context) => context.services.projectManager.newProject()
},
{
id: 'CloneCurrentProject',
appearance: {
cssIcons: ['copy'],
label: 'Clone Project...',
info: 'clone current project and open in a new tab',
},
invoke: (context) => context.services.projectManager.cloneProject(context.services.project.id)
},
{
id: 'RefreshSketches',
appearance: {

View file

@ -2,7 +2,8 @@ export default [
{
id: 'file',
cssIcons: ['file'],
actions: ['Save', 'StlExport', 'ImagePngExport', 'NativeFormatExport', '-', 'NativeFormatImport', '-', 'ReassignSketch']
actions: ['NewProject', '-', 'Save', 'StlExport', 'ImagePngExport', 'NativeFormatExport', '-', 'NativeFormatImport',
'NativeFormatImportAs', '-', 'CloneCurrentProject', '-', 'ReassignSketch']
},
{
id: 'craft',
@ -13,7 +14,7 @@ export default [
{
id: 'primitives',
label: 'add',
cssIcons: ['cube', 'plus'],
cssIcons: ['cube'],
info: 'set of available solid creation operations',
actions: ['PLANE', 'BOX', 'SPHERE', 'CONE', 'CYLINDER', 'TORUS']
},

View file

@ -5,28 +5,36 @@ import Fa from 'ui/components/Fa';
import ls from './ProjectManager.less';
import {ContextMenu, ContextMenuItem} from 'ui/components/Menu';
import cmn from 'ui/styles/common.less';
import Button from '../../../../modules/ui/components/controls/Button';
import Folder from '../../../../modules/ui/components/Folder';
import Folder from 'ui/components/Folder';
import connect from 'ui/connect';
@mapContext(ctx => ({
projectManager: ctx.services.projectManager
projectManager: ctx.services.projectManager,
download: projectId => ctx.services.projectManager.exportProject(projectId),
clone: projectId => ctx.services.projectManager.cloneProject(projectId, true),
rename: projectId => ctx.services.projectManager.renameProject(projectId, true),
remove: projectId => ctx.services.projectManager.deleteProject(projectId),
}))
@connect(streams => streams.storage.update)
export class ProjectManager extends React.Component {
render() {
let {projectManager} = this.props;
let projects = projectManager.listProjects();
return <div className={ls.root}>
<Button className={ls.btn}><Fa fw icon='download' /> Download Project</Button>
<Button className={ls.btn}><Fa fw icon='upload' /> Import Project</Button>
<Folder title='Project List'>
<div className={cmn.scrollable}>
{projects.map(p => <Section key={p.id}
label={<ContextMenu items={
<React.Fragment>
<ContextMenuItem label='Download' icon={<Fa fw icon='download' />} onClick={() => alert(1)}/>
<ContextMenuItem label='Clone' icon={<Fa fw icon='copy' />} onClick={() => alert(1)}/>
<ContextMenuItem label='Delete' icon={<Fa className={cmn.dangerColor} fw icon='remove' />} onClick={() => alert(1)}/>
<ContextMenuItem label='Download' icon={<Fa fw icon='download' />}
onClick={() => this.props.download(p.id)}/>
<ContextMenuItem label='Clone' icon={<Fa fw icon='copy' />}
onClick={() => this.props.clone(p.id)}/>
<ContextMenuItem label='Rename' icon={<Fa fw icon='pencil' />}
onClick={() => this.props.rename(p.id)}/>
<ContextMenuItem label='Delete' icon={<Fa className={cmn.dangerColor} fw icon='remove' />}
onClick={() => this.props.remove(p.id)}/>
</React.Fragment>
}>
<a href={'?' + p.id} target="_blank"><Fa icon='file'/> {p.id}</a>

View file

@ -1,9 +1,3 @@
.root a {
text-decoration: none;
}
.btn {
width: 90%;
margin: 2px;
display: block;
}

View file

@ -3,9 +3,8 @@ import {ProjectManager} from './ProjectManager';
import exportTextData from '../../../../modules/gems/exportTextData';
export function activate(ctx) {
function importProject() {
// let uploader = document.getElementById("uploader");
function importProjectImpl(getId, onDone) {
let uploader = document.createElement('input');
uploader.setAttribute('type', 'file');
uploader.style.display = 'none';
@ -17,21 +16,14 @@ export function activate(ctx) {
reader.onload = () => {
try {
let bundle = JSON.parse(reader.result);
let promptedId = uploader.value;
let iof = promptedId.search(/([^/\\]+)$/);
if (iof !== -1) {
promptedId = promptedId.substring(iof).replace(/\.json$/, '');
}
let projectId = prompt("New Project Name", promptedId);
if (projectId && !checkExistence(projectId)) {
let projectId = getId(uploader.value, bundle);
if (projectId) {
let sketchesNamespace = PROJECTS_PREFIX + projectId + SKETCH_SUFFIX;
ctx.services.storage.set(PROJECTS_PREFIX + projectId, JSON.stringify(bundle.model));
bundle.sketches.forEach(s => ctx.services.storage.set(sketchesNamespace + s.id, JSON.stringify(s.data)));
openProject(projectId);
onDone(projectId);
}
} finally {
document.body.removeChild(uploader);
@ -42,6 +34,30 @@ export function activate(ctx) {
uploader.addEventListener('change', read, false);
}
function importProjectAs() {
function promptId(fileName, bundle) {
let promptedId = fileName;
let iof = promptedId.search(/([^/\\]+)$/);
if (iof !== -1) {
promptedId = promptedId.substring(iof).replace(/\.json$/, '');
}
let projectId = prompt("New Project Name", promptedId);
if (!projectId && !checkExistence(projectId)) {
return null
}
return projectId;
}
importProjectImpl(promptId, openProject);
}
function importProject() {
if (confirm('Current project will be wiped off and replaced with the being imported one. Continue?')) {
ctx.services.project.empty();
importProjectImpl(() => ctx.services.project.id, ctx.services.project.load);
}
}
function exportProject(id) {
let modelData = ctx.services.storage.get(PROJECTS_PREFIX + id);
if (modelData) {
@ -63,13 +79,13 @@ export function activate(ctx) {
return false;
}
function cloneProject(oldId, newId) {
function cloneProjectImpl(oldId, newId) {
if (checkExistence(newId)) {
return
}
let data = ctx.services.storage.get(PROJECTS_PREFIX + oldId);
if (data) {
ctx.services.storage.set(PROJECTS_PREFIX + newId);
ctx.services.storage.set(PROJECTS_PREFIX + newId, data);
let sketchesNamespace = PROJECTS_PREFIX + oldId + SKETCH_SUFFIX;
let sketchKeys = ctx.services.storage.getAllKeysFromNamespace(sketchesNamespace);
sketchKeys.forEach(key => {
@ -78,6 +94,18 @@ export function activate(ctx) {
}
}
function cloneProject(oldProjectId, silent) {
let newProjectId = prompt("New Project Name", oldProjectId);
if (newProjectId) {
cloneProjectImpl(oldProjectId, newProjectId);
if (!silent) {
openProject(newProjectId);
}
return true;
}
return false;
}
function exists(projectId) {
return ctx.services.storage.exists(PROJECTS_PREFIX + projectId);
}
@ -114,35 +142,42 @@ export function activate(ctx) {
return Object.keys(projects).sort().map(key => projects[key]);
}
function deleteProjectImpl(projectId) {
ctx.services.storage.remove(PROJECTS_PREFIX + projectId);
let sketchesNamespace = PROJECTS_PREFIX + projectId + SKETCH_SUFFIX;
ctx.services.storage.getAllKeysFromNamespace(sketchesNamespace).forEach(key => ctx.services.storage.remove(key));
}
function deleteProject(projectId) {
if (confirm(`Project ${projectId} will be deleted. Continue?`)) {
ctx.services.storage.remove(PROJECTS_PREFIX + projectId);
deleteProjectImpl(projectId)
}
}
function renameProject(oldProjectId, newProjectId) {
if (checkExistence(newProjectId)) {
return
function renameProject(oldProjectId, silent) {
if (cloneProject(oldProjectId, silent)) {
deleteProjectImpl(oldProjectId);
}
cloneProject(oldProjectId, newProjectId);
deleteProject(oldProjectId);
}
function newProject(newProjectId) {
if (checkExistence(newProjectId)) {
return
function newProject() {
let newProjectId = prompt("New Project Name");
if (newProjectId) {
if (checkExistence(newProjectId)) {
return
}
openProject(newProjectId);
}
openProject(newProjectId);
}
function openProject(projectId) {
window.open('?' + projectId);
}
ctx.services.ui.registerFloatView('ProjectManager', ProjectManager, 'Project Manager', 'book');
ctx.services.ui.registerFloatView('ProjectManager', ProjectManager, 'Project Manager', 'database');
ctx.services.projectManager = {
listProjects, openProject, newProject, renameProject, deleteProject,
exists, cloneProject, exportProject, importProject
exists, cloneProject, exportProject, importProjectAs, importProject
}
}

View file

@ -8,10 +8,9 @@ import DPR from 'dpr';
export class InPlaceSketcher {
constructor(ctx, onSketchUpdate) {
constructor(ctx) {
this.face = null; // should be only one in the state
this.ctx = ctx;
this.onSketchUpdate = onSketchUpdate;
}
get inEditMode() {
@ -37,7 +36,7 @@ export class InPlaceSketcher {
this.viewer.toolManager.setDefaultTool(new DelegatingPanTool(this.viewer, viewer3d.sceneSetup.renderer.domElement));
viewer3d.sceneSetup.trackballControls.addEventListener( 'change', this.onCameraChange);
let sketchData = localStorage.getItem(this.sketchStorageKey);
let sketchData = this.ctx.services.storage.get(this.sketchStorageKey);
this.viewer.historyManager.init(sketchData);
this.viewer.io.loadSketch(sketchData);
this.ctx.streams.sketcher.sketchingFace.value = face;
@ -101,8 +100,7 @@ export class InPlaceSketcher {
};
save() {
localStorage.setItem(this.sketchStorageKey, this.viewer.io.serializeSketch());
this.onSketchUpdate({key: this.sketchStorageKey});
this.ctx.services.storage.set(this.sketchStorageKey, this.viewer.io.serializeSketch());
}
}

View file

@ -95,7 +95,7 @@ export function activate(ctx) {
services.storage.set(sketchStorageKey, JSON.stringify(data));
}
let inPlaceEditor = new InPlaceSketcher(ctx, onSketchUpdate);
let inPlaceEditor = new InPlaceSketcher(ctx);
function sketchFace(face) {
updateSketchBoundaries(face);
if (inPlaceEditor.inEditMode) {

View file

@ -10,6 +10,7 @@ export function activate({services, streams}) {
function set(key, value) {
localStorage.setItem(key, value);
notify(key);
}
function get(key) {
@ -17,12 +18,23 @@ export function activate({services, streams}) {
}
function remove(key) {
return localStorage.removeItem(key);
try {
return localStorage.removeItem(key);
} finally {
notify(key);
}
}
function exists(key) {
return localStorage.hasOwnProperty(key);
}
function notify(key) {
streams.storage.update.next({
key,
timestamp: Date.now
});
}
function getAllKeysFromNamespace(namespace) {
let keys = [];
@ -35,11 +47,9 @@ export function activate({services, streams}) {
return keys;
}
function addListener(handler) {
window.addEventListener('storage', handler, false);
}
window.addEventListener('storage', evt => notify(evt.key), false);
addListener(() => streams.storage.update.next(Date.now));
const addListener = listener => streams.storage.update.attach(listener);
services.storage = {
set, get, remove, addListener, getAllKeysFromNamespace, exists