Skip to content

Commit

Permalink
Rename scenes fix #37 (#41)
Browse files Browse the repository at this point in the history
* add some comments and remove a useless log

* add PATCH route on scenes

* fix custom events that were not cancellable

* add UI to rename scenes
  • Loading branch information
sdumetz authored Sep 25, 2023
1 parent 3ed10f6 commit 3bc43a1
Show file tree
Hide file tree
Showing 9 changed files with 167 additions and 6 deletions.
8 changes: 8 additions & 0 deletions source/server/auth/UserManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,14 @@ export default class UserManager {
if(!r || !r.changes) throw new NotFoundError(`No user to delete with uid ${uid}`);
}

/**
* patches permissions on a scene for a given user.
* Special cases for "any" and "default" users.
* Usernames are converted to IDs before being used.
* @param scene
* @param username
* @param role
*/
async grant(scene :string, username :string, role :AccessType){
if(!isAccessType(role)) throw new BadRequestError(`Bad access type requested : ${role}`);
let r = await this.db.run(`
Expand Down
3 changes: 3 additions & 0 deletions source/server/routes/api/v1/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { handlePatchUser } from "./users/uid/patch";
import { postSceneHistory } from "./scenes/scene/history/post";
import handleGetStats from "./stats";
import postScenes from "./scenes/post";
import patchScene from "./scenes/scene/patch";



Expand Down Expand Up @@ -64,7 +65,9 @@ router.patch("/users/:uid", bodyParser.json(), wrap(handlePatchUser));

router.get("/scenes", wrap(getScenes));
router.post("/scenes", isAdministrator, wrap(postScenes));

router.post("/scenes/:scene", isUser, wrap(postScene));
router.patch("/scenes/:scene", canAdmin, bodyParser.json(), wrap(patchScene));

router.use("/scenes/:scene", canRead);
router.get("/scenes/:scene/history", wrap(getSceneHistory));
Expand Down
1 change: 0 additions & 1 deletion source/server/routes/api/v1/scenes/scene/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ export default async function getScene(req :Request, res :Response){
await wrapFormat(res, {
"application/json": async ()=>{
let requester = getUserId(req);
console.log("Get scene : ", scene, requester);
let data = await vfs.getScene(scene, requester);
res.status(200).send(data);
},
Expand Down
53 changes: 53 additions & 0 deletions source/server/routes/api/v1/scenes/scene/patch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@


import request from "supertest";

import Vfs from "../../../../../vfs";
import UserManager from "../../../../../auth/UserManager";

describe("PATCH /api/v1/scenes/:scene", function(){
let vfs:Vfs, userManager:UserManager, ids :number[];
this.beforeEach(async function(){
let locals = await createIntegrationContext(this);
vfs = locals.vfs;
userManager = locals.userManager;
ids = await Promise.all([
vfs.createScene("foo", {0:"admin"}),
vfs.createScene("bar", {0:"admin"}),
]);
});

this.afterEach(async function(){
await cleanIntegrationContext(this);
});

describe("rename a scene", function(){
it("get scene info", async function(){
await request(this.server).patch("/api/v1/scenes/foo")
.send({name: "foofoo"})
.expect(200);
});

it("forces unique names", async function(){
await request(this.server).patch("/api/v1/scenes/foo")
.send({name: "bar"})
.expect(409);
})

it("is admin-protected", async function(){
await userManager.grant("foo", "any", "write");
await userManager.grant("foo", "default", "write");
await request(this.server).patch("/api/v1/scenes/foo")
.send({name: "foofoo"})
.expect(401);
});

it("is access-protected (obfuscated as 404)", async function(){
await userManager.grant("foo", "default", "none");
await request(this.server).patch("/api/v1/scenes/foo")
.send({name: "foofoo"})
.expect(404);
});
});

});
37 changes: 37 additions & 0 deletions source/server/routes/api/v1/scenes/scene/patch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@

import { ConflictError } from "../../../../../utils/errors";
import { getUserId, getVfs } from "../../../../../utils/locals";
import { Request, Response } from "express";



/**
* Patches a scene's properties
* @param req
* @param res
* @returns {Promise<void>}
*/
export default async function patchScene(req :Request, res :Response){
let vfs = getVfs(req);
let user_id = getUserId(req);
let {scene} = req.params;
let {name} = req.body;
//Ensure all or none of the changes are comitted
let result = await getVfs(req).isolate(async (vfs)=>{
let {id} = await vfs.getScene(scene, user_id);
if(name && name !== scene){
try{
await vfs.renameScene(id, name);
}catch(e:any){
if(e.code == "SQLITE_CONSTRAINT" && /UNIQUE constraint failed: scenes.scene_name/.test(e.message)){
throw new ConflictError(`A scene named ${name} already exists`);
}else{
throw e;
}
}
}

return await vfs.getScene(id, user_id);
});
res.status(200).send(result);
};
8 changes: 7 additions & 1 deletion source/server/routes/api/v1/scenes/scene/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ import { Request, Response } from "express";
import { parse_glb } from "../../../../../utils/glTF";



/**
* Creates a new default document for a scene
* uses data embedded in the glb to fill the document where possible
* @param scene
* @param filepath
* @returns
*/
async function getDocument(scene:string, filepath:string){
let {default:orig} = await import("../../../../../utils/documents/default.svx.json");
//dumb inefficient Deep copy because we want to mutate the doc in-place
Expand Down
50 changes: 47 additions & 3 deletions source/ui/screens/SceneHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { nothing } from "lit-html";
import i18n from "../state/translate";
import {getLogin} from "../state/auth";
import { navigate } from "../state/router";
import Modal from "../composants/Modal";


const AccessTypes = [
Expand Down Expand Up @@ -163,7 +164,7 @@ class SceneVersion{
<h3>${articles.size} article${(1 < articles.size?"s":"")}</h3>
<div style="max-width: 300px">
<a class="ff-button ff-control btn-primary" href=${`/ui/scenes/${scene}/edit?lang=${this.language.toUpperCase()}`}><ff-icon name="edit"></ff-icon> ${this.t("ui.editScene")}</a>
<a class="ff-button ff-control btn-primary" style="margin-top:10px" href=${`/ui/scenes/${scene}/view?lang=${this.language.toUpperCase()}`}><ff-icon name="eye"></ff-icon> ${this.t("ui.viewScene")}</a>
<a class="ff-button ff-control btn-primary" style="margin-top:10px" href=${`/ui/scenes/${scene}/view?lang=${this.language.toUpperCase()}`}><ff-icon name="eye"></ff-icon> ${this.t("ui.viewScene")}</a>
<a class="ff-button ff-control btn-primary" style="margin-top:10px" download href="/api/v1/scenes/${scene}?format=zip"><ff-icon name="save"></ff-icon> ${this.t("ui.downloadScene")}</a>
</div>
</div>
Expand All @@ -175,8 +176,9 @@ class SceneVersion{
<div class="section">
${this.renderHistory()}
</div>
${getLogin()?.isAdministrator? html`<div style="padding: 10px 0;display:flex;color:red;justify-content:end;">
<div><ff-button class="btn-danger" icon="trash" text="Delete" @click=${this.onDelete}></ff-button></div>
${getLogin()?.isAdministrator? html`<div style="padding: 10px 0;display:flex;color:red;justify-content:end;gap:10px">
<div><ff-button class="btn-primary" icon="edit" text=${this.t("ui.rename")} @click=${this.onRename}></ff-button></div>
<div><ff-button class="btn-danger" icon="trash" text=${this.t("ui.delete")} @click=${this.onDelete}></ff-button></div>
</div>`:null}
</div>`;
}
Expand Down Expand Up @@ -322,4 +324,46 @@ class SceneVersion{
Notification.show(`Failed to remove ${this.scene} : ${e.message}`);
});
}

onRename = ()=>{
const onRenameSubmit = (ev)=>{
ev.preventDefault();
let name = (Modal.Instance.shadowRoot.getElementById("sceneRenameInput") as HTMLInputElement).value;
Modal.close();
Modal.show({
header: this.t("ui.renameScene"),
body: html`<div style="display:block;position:relative;padding-top:110px"><sv-spinner visible/></div>`,
});
fetch(`/api/v1/scenes/${encodeURIComponent(this.scene)}`, {
method:"PATCH",
headers:{"Content-Type":"application/json"},
body: JSON.stringify({name})
}).then((r)=>{
if(r.ok){
Notification.show("Renamed "+this.scene+" to "+name, "info", 1600);
navigate(this, `/ui/scenes/${encodeURIComponent(name)}/`);
}else{
throw new Error(`[${r.status}] ${r.statusText}`);
}
}).catch((e)=>{
console.error(e);
Notification.show(`Failed to rename ${this.scene} : ${e.message}`);
}).finally(()=>{
setTimeout(()=>{
Modal.close();
}, 500);
});
}

Modal.show({
header: this.t("ui.renameScene"),
body: html`<form class="form-group" @submit=${onRenameSubmit}>
<div class="form-item">
<input type="text" required minlength=3 autocomplete="off" style="padding:.25rem;margin-bottom:.75rem;width:100%;" class="form-control" id="sceneRenameInput" placeholder="${this.scene}">
</div>
</form>`,
buttons: html`<ff-button class="btn-primary" @click=${onRenameSubmit} text=${this.t("ui.rename")}></ff-button>`,
});

}
}
5 changes: 4 additions & 1 deletion source/ui/state/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export function router<T extends Constructor<LitElement>>(baseClass:T) : T & Con
if(url.hostname != window.location.hostname) return window.location.href = url.toString();

if(!this.inPath(url.pathname) && this.path != "/") return; //Return to bubble up the stack
console.debug("Handle navigation event", url.toString());
ev.stopPropagation(); //Handle the route change
ev.preventDefault();

Expand Down Expand Up @@ -133,10 +134,12 @@ export function navigate(that :HTMLElement,href ?:string|URL, queries?:Record<st
const unhandled = (that ?? this).dispatchEvent(new CustomEvent("navigate", {
detail: {href: url},
bubbles: true,
composed: true
composed: true,
cancelable: true,
}));

if(unhandled){
console.log("Unhandled navigation event, redirecting to ", url.toString());
window.location.href = url.toString();
}else{
console.debug("Navigate to :", url.toString(), "with queries : ", queries);
Expand Down
8 changes: 8 additions & 0 deletions source/ui/state/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,10 @@ export default {
fr: "supprimer",
en: "delete"
},
rename:{
fr: "renommer",
en: "rename"
},
alphabet:{
fr: "ordre alphabethique",
en: "alphabetical"
Expand All @@ -202,6 +206,10 @@ export default {
downloadScene:{
fr:"Télécharger la scène",
en:"Download this scene"
},
renameScene:{
fr: "Renommer la scène",
en: "Rename scene"
}
},
info:{
Expand Down

0 comments on commit 3bc43a1

Please sign in to comment.