From 835c5fdd66323ae6e4709298f647335735c55e90 Mon Sep 17 00:00:00 2001 From: "HRG @ SCExC" Date: Tue, 4 Jun 2024 03:01:19 -0400 Subject: [PATCH] add cloning logic --- index.js | 213 ++++++++++++++++++++++- migrations/000_bootstrap_db.sql | 3 + public/js/bliss_inspector.js | 130 ++++++++++---- views/workshop/clone_structure_modal.eta | 24 +++ views/workshop/new_route_modal.eta | 3 +- views/workshop/new_template_modal.eta | 3 +- views/workshop/settings.eta | 34 ++++ views/workshop/sidebar.eta | 6 +- 8 files changed, 367 insertions(+), 49 deletions(-) create mode 100644 views/workshop/clone_structure_modal.eta create mode 100644 views/workshop/settings.eta diff --git a/index.js b/index.js index f2b2e4a..9cfdf40 100644 --- a/index.js +++ b/index.js @@ -28,7 +28,16 @@ const db = betterSqlite3("./dbs/0.sqlite"); db.pragma("journal_mode = WAL"); function getAllRoutes(db) { - return db.prepare("SELECT * from routes ORDER BY id ASC;").all(); + return db + .prepare( + ` + SELECT routes.*, structures.route_prefix + FROM routes + JOIN structures ON routes.structure_id = structures.id + ORDER BY routes.id ASC; + ` + ) + .all(); } app.use( @@ -71,8 +80,11 @@ function applyMigrations() { function buildRoutes() { for (let route of getAllRoutes(db)) { + const prefix = route.route_prefix; + console.log(route); + const p = prefix ? path.join(route.route_prefix, route.path) : route.path; routes[route.verb].push({ - matcher: match(route.path, { decode: decodeURIComponent }), + matcher: match(p, { decode: decodeURIComponent }), id: route.id, path: route.path, }); @@ -211,6 +223,17 @@ function updateDb(db, appDb) { db.prepare(sql).run(...values); } +function updateStruct(db, struct) { + const fields = ["name", "route_prefix"]; + console.log(struct, fields); + const values = fields.map((field) => struct[field]); + const placeholders = fields.map((field) => `${field} = ?`).join(", "); + + const sql = `UPDATE structures SET ${placeholders} WHERE id = ?`; + values.push(struct.id); // Add routeId to the end for the WHERE clause + db.prepare(sql).run(...values); +} + function updateTemplate(db, template) { const fields = ["content", "name", "test_object"]; const values = fields.map((field) => template[field]); @@ -371,7 +394,92 @@ function getDbInstance(dbId) { return dbInstance; } -function bootstrapTemplateWithHTMXetc(htmlString, blissRoute, wrapHTML) { +function cloneStructure( + structId, + newStructureName, + userId, + routePrefix = "", + cloneDb = false +) { + const transaction = db.transaction(() => { + const cloneStructure = db.prepare(` + INSERT INTO structures (name, user_id, route_prefix, cloned_from) + VALUES (?, ?, ?, ?); + `); + + const newStructId = cloneStructure.run( + newStructureName, + userId, + routePrefix, + structId + ).lastInsertRowid; + + if (cloneDb) { + const dbIds = db + .prepare(`select db_id from structure_dbs where structure_id = ?;`) + .all(structId); + + for (let { db_id } of dbIds) { + const newDb = db + .prepare( + ` + INSERT INTO dbs (name, structure_id, library) + SELECT name, ?, library FROM dbs WHERE id = ?; + ` + ) + .run(newStructId, db_id).lastInsertRowid; + + db.prepare( + ` + INSERT INTO structure_dbs (db_id, structure_id, alias) + SELECT ?, ?, alias FROM structure_dbs WHERE db_id = ? AND structure_id = ?; + ` + ).run(newDb, newStructId, db_id, structId); + + db.prepare(`select id from dbs where structure_id = ?`) + .all(newStructId) + .map((new_db) => { + console.log(db_id, new_db.id); + const srcPath = path.join(__dirname, "dbs", `${db_id}.sqlite`); + const destPath = path.join(__dirname, "dbs", `${new_db.id}.sqlite`); + fs.copyFileSync(srcPath, destPath); + fs.copyFileSync(srcPath + "-shm", destPath + "-shm"); + fs.copyFileSync(srcPath + "-wal", destPath + "-wal"); + }); + } + } else { + db.prepare( + ` + INSERT INTO structure_dbs (db_id, structure_id, alias) + SELECT db_id, ?, alias FROM structure_dbs WHERE structure_id = ?; + ` + ).run(newStructId, structId); + } + const cloneTemplates = db.prepare(` + INSERT INTO templates (name, content, structure_id, test_object, engine) + SELECT name, content, ?, test_object, engine FROM templates WHERE structure_id = ?; + `); + cloneTemplates.run(newStructId, structId); + + const cloneRoutes = db.prepare(` + INSERT INTO routes (verb, path, structure_id, handler) + SELECT verb, path, ?, handler FROM routes WHERE structure_id = ?; + `); + cloneRoutes.run(newStructId, structId); + + return newStructId; + }); + + return transaction(); +} + +function bootstrapTemplateWithHTMXetc( + htmlString, + blissRoute, + blissClone, + blissCopy, + wrapHTML +) { if (htmlString.startsWith("") || wrapHTML) { const $ = cheerio.load(htmlString); let head = $("head"); @@ -390,13 +498,18 @@ function bootstrapTemplateWithHTMXetc(htmlString, blissRoute, wrapHTML) { `); if (blissRoute) { - $.root().children().attr("data-bliss-route", blissRoute); + $("body").attr("data-bliss-route", blissRoute); + $("body").attr("data-bliss-clone", blissClone); + if (blissCopy) $("body").attr("data-bliss-copy", blissCopy); } htmlString = $.html(); } else if (blissRoute) { const $ = cheerio.load(htmlString, null, false); $.root().children().attr("data-bliss-route", blissRoute); + $.root().children().attr("data-bliss-clone", blissClone); + if (blissCopy) $.root().children().attr("data-bliss-copy", blissCopy); + console.log(blissCopy, "fleem") htmlString = $.html(); } @@ -444,6 +557,27 @@ app.get("/workshop/:structure_id", (req, res) => { ); }); +app.post("/workshop/:structure_id/clone", (req, res) => { + let routePrefix = req.body.route_prefix; + routePrefix = routePrefix[0] == "/" ? routePrefix : "/" + routePrefix; + const cloneDb = req.body.clone_db == "true"; + let newStructureId; + try { + newStructureId = cloneStructure( + req.params.structure_id, + req.body.name, + 1, + // req.session.userId, + routePrefix, + cloneDb + ); + buildRoutes(); + } catch (e) { + return res.send(e.stack); + } + return smartRedirect(req, res, `/workshop/${newStructureId}`); +}); + app.post("/workshop/:structure_id/db", (req, res) => { const dbId = createDb(db, req.params.structure_id, req.body.name); const redirectUrl = `/workshop/${req.params.structure_id}/db/${dbId}`; @@ -485,6 +619,7 @@ app.post("/workshop/:structure_id/route", (req, res) => { req.params.structure_id, "// put your handler code here\n\nfunction handler(req, res) {\n res.send('henlo world') \n}" ); + // todo optimize by only adding new, don't just rebuild all buildRoutes(); return smartRedirect( req, @@ -591,15 +726,22 @@ app.get("/workshop/:structure_id/template/:template_id", (req, res) => { app.get("/workshop/:structure_id/template/:template_id/preview", (req, res) => { const template = getTemplate(db, req.params.template_id); + const struct = getStructure(db, req.params.structure_id); const eta = getTemplater(req.params.structure_id); const context = vm.createContext({ it: null }); vm.runInContext(template.test_object, context); + context.it.route = function (url) { + return struct.route_prefix ? path.join(struct.route_prefix, url) : url; + }; + return res.send( bootstrapTemplateWithHTMXetc( eta.render(template.name, context.it), null, + null, + null, true ) ); @@ -607,11 +749,15 @@ app.get("/workshop/:structure_id/template/:template_id/preview", (req, res) => { app.get("/workshop/:structure_id/route/:id", (req, res) => { const route = getRoute(db, req.params.id); + const routePrefix = getStructure(db, req.params.structure_id).route_prefix; return res.send( bootstrapTemplateWithHTMXetc( eta.render("workshop/route", { route: route, - previewUrl: route["verb"] == "GET" ? route.path : "/workshop/tip", + previewUrl: + route["verb"] == "GET" + ? path.join(routePrefix, route.path) + : "/workshop/tip", ...sidebarStuff(db, req.params.structure_id), }) ) @@ -682,6 +828,25 @@ app.get("/workshop/:structure_id/files", (req, res) => { ); }); +app.get("/workshop/:structure_id/settings", (req, res) => { + return res.send( + bootstrapTemplateWithHTMXetc( + eta.render("workshop/settings", { + ...sidebarStuff(db, req.params.structure_id), + }) + ) + ); +}); + +app.put("/workshop/:structure_id/settings", (req, res) => { + let structId = req.params.structure_id; + let struct = getStructure(db, structId); + updateStruct(db, { ...struct, route_prefix: req.body.route_prefix }); + struct = getStructure(db, structId); + + res.send("success!"); +}); + app.get("/workshop/:structure_id/new_template_modal", (req, res) => { return res.send( bootstrapTemplateWithHTMXetc( @@ -712,15 +877,31 @@ app.get("/workshop/:structure_id/new_db_modal", (req, res) => { ); }); +app.get("/workshop/:structure_id/clone_modal", (req, res) => { + const structure = getStructure(db, req.params.structure_id); + return res.send( + bootstrapTemplateWithHTMXetc( + eta.render("workshop/clone_structure_modal", { + structure, + defaultRoutePrefix: path.join(structure.route_prefix || "", "clone"), + }) + ) + ); +}); + +function embedHTML(url) { + return `
`; +} + app.all("*", (req, res) => { - const path = req.path; + const req_path = req.path; const verb = req.method; try { let routeMatch = null; for (let { matcher, id } of routes[verb]) { - let matchFromRoute = matcher(path); + let matchFromRoute = matcher(req_path); if (matchFromRoute) { routeMatch = { params: matchFromRoute.params, id: id }; } @@ -733,6 +914,7 @@ app.all("*", (req, res) => { .get(routeMatch.id); let dbs = getDbsForStructure(db, route.structure_id); + let __urlPrefix = getStructure(db, route.structure_id).route_prefix; const allDbInstances = {}; @@ -762,13 +944,26 @@ app.all("*", (req, res) => { res.rawSend( bootstrapTemplateWithHTMXetc( args[0], - `/workshop/${route.structure_id}/route/${route.id}` + `/workshop/${route.structure_id}/route/${route.id}`, + `/workshop/${route.structure_id}/clone/`, + verb == "GET" ? embedHTML(req.originalUrl) : null ) ); }; res.render = (template, context) => { + context.route = function (url) { + return __urlPrefix ? path.join(__urlPrefix, url) : url; + }; const eta = getTemplater(route.structure_id); - res.send(bootstrapTemplateWithHTMXetc(eta.render(template, context))); + console.log(route.method, "\n\n\n"); + res.rawSend( + bootstrapTemplateWithHTMXetc( + eta.render(template, context), + `/workshop/${route.structure_id}/route/${route.id}`, + `/workshop/${route.structure_id}/clone_modal/`, + verb == "GET" ? embedHTML(req.originalUrl) : null + ) + ); }; return vm.runInContext( (route.handler += `\n\nhandler(req, res)`), diff --git a/migrations/000_bootstrap_db.sql b/migrations/000_bootstrap_db.sql index 692241e..be72e56 100644 --- a/migrations/000_bootstrap_db.sql +++ b/migrations/000_bootstrap_db.sql @@ -11,9 +11,12 @@ CREATE TABLE structures ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, user_id INTEGER, + route_prefix TEXT, + cloned_from INTEGER, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(user_id) REFERENCES users(id), + FOREIGN KEY(cloned_from) REFERENCES structures(id), UNIQUE(name, user_id) ); diff --git a/public/js/bliss_inspector.js b/public/js/bliss_inspector.js index 72d57e8..5e7a513 100644 --- a/public/js/bliss_inspector.js +++ b/public/js/bliss_inspector.js @@ -1,65 +1,121 @@ -document.addEventListener("DOMContentLoaded", function() { - console.log("supp", document.location) +document.addEventListener("DOMContentLoaded", function () { // Create the magnifying glass emoji button - const magnifyingGlass = document.createElement('div'); - magnifyingGlass.innerHTML = '🔍'; - magnifyingGlass.className = 'fixed top-2 right-2 cursor-pointer text-2xl z-50'; + const magnifyingGlass = document.createElement("div"); + magnifyingGlass.innerHTML = "🔍"; + magnifyingGlass.className = + "fixed bottom-2 right-2 cursor-pointer text-2xl z-50"; document.body.appendChild(magnifyingGlass); let highlighted = false; // Function to highlight elements function highlightElements() { - const elements = document.querySelectorAll('[data-bliss-route]'); - elements.forEach(el => { - if (!el.classList.contains('highlighted')) { - el.classList.add('border-2', 'border-yellow-500', 'relative', 'highlighted'); + const modal = document.createElement("div"); + modal.id = "inspector-modal"; + document.body.appendChild(modal); + + const elements = document.querySelectorAll("[data-bliss-route]"); + elements.forEach((el) => { + if (!el.classList.contains("highlighted")) { + el.classList.add( + "border-2", + "border-yellow-500", + "relative", + "highlighted", + "p-2" + ); + let controls = document.createElement("div"); + controls.className = + "flex absolute gap-1 -bottom-3 right-0 inspector-controls"; // Create the edit icon - const editIcon = document.createElement('a'); - editIcon.href = el.getAttribute('data-bliss-route'); - editIcon.innerHTML = '✏️'; - editIcon.className = 'absolute text-lg bg-white rounded-full p-1 shadow -bottom-3 -right-3'; - el.appendChild(editIcon); + const editIcon = document.createElement("a"); + editIcon.href = el.getAttribute("data-bliss-route"); + editIcon.innerHTML = "✏️"; + editIcon.className = + "text-lg bg-white rounded-full p-1 shadow edit-icon"; + controls.appendChild(editIcon); + + // Create the copy icon + if (el.getAttribute("data-bliss-copy")) { + const copyIcon = document.createElement("button"); + copyIcon.innerHTML = "📋"; + copyIcon.className = + "text-lg bg-white rounded-full p-1 shadow copy-icon"; + controls.appendChild(copyIcon); + copyIcon.addEventListener("click", () => { + navigator.clipboard + .writeText(el.getAttribute("data-bliss-copy")) + .then(() => { + copyIcon.innerHTML = "✅"; + }) + .catch((err) => { + copyIcon.innerHTML = "❌"; + }); + }); + } + + // Create the clone icon + const cloneIcon = document.createElement("div"); + cloneIcon.innerHTML = "👯‍♀️"; + cloneIcon.className = + "text-lg bg-white rounded-full cursor-pointer p-1 shadow clone-icon"; + cloneIcon.setAttribute("hx-get", el.getAttribute("data-bliss-clone")); + cloneIcon.setAttribute("hx-target", "#inspector-modal"); + + controls.setAttribute( + "_", + `on mouseover set my.style.zIndex to 10000 + on mouseout set my.style.zIndex to "initial" + ` + ); + controls.appendChild(cloneIcon); + + el.appendChild(controls); + htmx.process(controls); + _hyperscript.processNode(controls); } }); } // Function to remove highlights function removeHighlights() { - const elements = document.querySelectorAll('[data-bliss-route].highlighted'); - elements.forEach(el => { - el.classList.remove('border-2', 'border-yellow-500', 'relative', 'highlighted'); - const editIcon = el.querySelector('a'); - if (editIcon) { - el.removeChild(editIcon); - } + const elements = document.querySelectorAll( + "[data-bliss-route].highlighted" + ); + elements.forEach((el) => { + el.classList.remove( + "border-2", + "border-yellow-500", + "relative", + "highlighted", + "p-2" + ); + el.querySelectorAll(".inspector-controls").forEach((el) => el.remove()); }); } - magnifyingGlass.addEventListener('click', function() { + magnifyingGlass.addEventListener("click", function () { if (!highlighted) { highlightElements(); - magnifyingGlass.innerHTML = '❌'; + magnifyingGlass.innerHTML = "❌"; } else { removeHighlights(); - magnifyingGlass.innerHTML = '🔍'; + magnifyingGlass.innerHTML = "🔍"; } highlighted = !highlighted; }); // Observe the document for changes - const observer = new MutationObserver(function(mutations) { - mutations.forEach(function(mutation) { - if (highlighted) { - highlightElements(); - } - }); - }); + // const observer = new MutationObserver(function (mutations) { + // if (highlighted) { + // highlightElements(); + // } + // }); - // Configure the observer - observer.observe(document.body, { - childList: true, - subtree: true - }); -}); \ No newline at end of file + // // Configure the observer + // observer.observe(document.body, { + // childList: true, + // subtree: true, + // }); +}); diff --git a/views/workshop/clone_structure_modal.eta b/views/workshop/clone_structure_modal.eta new file mode 100644 index 0000000..fa12058 --- /dev/null +++ b/views/workshop/clone_structure_modal.eta @@ -0,0 +1,24 @@ +
+
+
Clone Structure
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
\ No newline at end of file diff --git a/views/workshop/new_route_modal.eta b/views/workshop/new_route_modal.eta index 19d4e37..bca4175 100644 --- a/views/workshop/new_route_modal.eta +++ b/views/workshop/new_route_modal.eta @@ -38,4 +38,5 @@
- \ No newline at end of file + + \ No newline at end of file diff --git a/views/workshop/new_template_modal.eta b/views/workshop/new_template_modal.eta index 7f6cdf7..d4fe971 100644 --- a/views/workshop/new_template_modal.eta +++ b/views/workshop/new_template_modal.eta @@ -19,4 +19,5 @@
- \ No newline at end of file + + \ No newline at end of file diff --git a/views/workshop/settings.eta b/views/workshop/settings.eta new file mode 100644 index 0000000..d2bfcab --- /dev/null +++ b/views/workshop/settings.eta @@ -0,0 +1,34 @@ + + + + + + + + +
+
+
Workshop - <%~ include('/workshop/structure_name', {structure: it.structure}) %> +
+
+
+
+ <%~ include('/workshop/sidebar', it) %> +
+
+
+
+
+ + +
+ +
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/views/workshop/sidebar.eta b/views/workshop/sidebar.eta index bb97825..2c10444 100644 --- a/views/workshop/sidebar.eta +++ b/views/workshop/sidebar.eta @@ -6,7 +6,8 @@ <% it.routes.map(route => { %>
  • - <%= route.verb %> - <%= route.path %> + <%= route.verb %> - + <%= it.structure.route_prefix %><%= route.path %>
  • <% }) %> @@ -59,5 +60,8 @@
  • Files
  • +
  • + Settings +
  • \ No newline at end of file