From a82e2c2765ce2ef901ffba2015f5675e7917865f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristijan=20Ribari=C4=87?= Date: Sun, 3 Nov 2024 20:50:59 +0100 Subject: [PATCH] feat: Add group support for pinned tabs This commit introduces the ability to create groups of pinned tabs, allowing for more organized and hierarchical management of pinned tabs. Key changes: - Added `parentUuid` field to `zen_pins` table to represent parent group relationships. - Implemented `getGroupChildren` function to fetch children of a given group. - Updated `removePin` function to also remove all children of a group when deleting. - Modified `getPins` function to sort pins by parent group and then by position. - Added `isGroup` and `isEssential` fields to `zen_pins` table to represent the type of pinned item. - Improved database schema with additional indices for performance optimization. - Removed unused `recordChange` function. --- src/ZenPinnedTabsStorage.mjs | 136 +++++++++++++++++++++++++---------- 1 file changed, 97 insertions(+), 39 deletions(-) diff --git a/src/ZenPinnedTabsStorage.mjs b/src/ZenPinnedTabsStorage.mjs index 8c84312..d529ea8 100644 --- a/src/ZenPinnedTabsStorage.mjs +++ b/src/ZenPinnedTabsStorage.mjs @@ -1,11 +1,7 @@ var ZenPinnedTabsStorage = { async init() { console.log('ZenPinnedTabsStorage: Initializing...'); - try { - await this._ensureTable(); - } catch (e) { - console.warn('ZenPinnedTabsStorage: Failed to initialize', e); - } + await this._ensureTable(); }, async _ensureTable() { @@ -13,23 +9,32 @@ var ZenPinnedTabsStorage = { // Create the pins table if it doesn't exist await db.execute(` CREATE TABLE IF NOT EXISTS zen_pins ( - id INTEGER PRIMARY KEY, - uuid TEXT UNIQUE NOT NULL, - title TEXT NOT NULL, - url TEXT NOT NULL, - container_id INTEGER, - workspace_uuid TEXT, - position INTEGER NOT NULL DEFAULT 0, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL - ) + id INTEGER PRIMARY KEY, + uuid TEXT UNIQUE NOT NULL, + title TEXT NOT NULL, + url TEXT, + container_id INTEGER, + workspace_uuid TEXT, + position INTEGER NOT NULL DEFAULT 0, + is_essential BOOLEAN NOT NULL DEFAULT 0, + is_group BOOLEAN NOT NULL DEFAULT 0, + parent_uuid TEXT, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + FOREIGN KEY (parent_uuid) REFERENCES zen_pins(uuid) ON DELETE SET NULL + ) `); - // Create an index on the uuid column + + // Create indices await db.execute(` CREATE INDEX IF NOT EXISTS idx_zen_pins_uuid ON zen_pins(uuid) `); + await db.execute(` + CREATE INDEX IF NOT EXISTS idx_zen_pins_parent_uuid ON zen_pins(parent_uuid) + `); + // Create the changes tracking table if it doesn't exist await db.execute(` CREATE TABLE IF NOT EXISTS zen_pins_changes ( @@ -70,33 +75,40 @@ var ZenPinnedTabsStorage = { if ('position' in pin && Number.isFinite(pin.position)) { newPosition = pin.position; } else { - // Get the maximum position - const maxPositionResult = await db.execute(`SELECT MAX("position") as max_position FROM zen_pins`); + // Get the maximum position within the same parent group (or null for root level) + const maxPositionResult = await db.execute(` + SELECT MAX("position") as max_position + FROM zen_pins + WHERE COALESCE(parent_uuid, '') = COALESCE(:parent_uuid, '') + `, { parent_uuid: pin.parentUuid || null }); const maxPosition = maxPositionResult[0].getResultByName('max_position') || 0; - newPosition = maxPosition + 1000; // Add a large increment to avoid frequent reordering + newPosition = maxPosition + 1000; } // Insert or replace the pin await db.executeCached(` INSERT OR REPLACE INTO zen_pins ( uuid, title, url, container_id, workspace_uuid, position, - created_at, updated_at + is_essential, is_group, parent_uuid, created_at, updated_at ) VALUES ( :uuid, :title, :url, :container_id, :workspace_uuid, :position, + :is_essential, :is_group, :parent_uuid, COALESCE((SELECT created_at FROM zen_pins WHERE uuid = :uuid), :now), :now ) `, { uuid: pin.uuid, title: pin.title, - url: pin.url, + url: pin.isGroup ? null : pin.url, container_id: pin.containerTabId || null, workspace_uuid: pin.workspaceUuid || null, position: newPosition, + is_essential: pin.isEssential || false, + is_group: pin.isGroup || false, + parent_uuid: pin.parentUuid || null, now }); - // Record the change await db.execute(` INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) VALUES (:uuid, :timestamp) @@ -106,7 +118,6 @@ var ZenPinnedTabsStorage = { }); changedUUIDs.add(pin.uuid); - await this.updateLastChangeTimestamp(db); }); }); @@ -119,7 +130,8 @@ var ZenPinnedTabsStorage = { async getPins() { const db = await PlacesUtils.promiseDBConnection(); const rows = await db.executeCached(` - SELECT * FROM zen_pins ORDER BY position ASC + SELECT * FROM zen_pins + ORDER BY parent_uuid NULLS FIRST, position ASC `); return rows.map((row) => ({ uuid: row.getResultByName('uuid'), @@ -127,7 +139,31 @@ var ZenPinnedTabsStorage = { url: row.getResultByName('url'), containerTabId: row.getResultByName('container_id'), workspaceUuid: row.getResultByName('workspace_uuid'), - position: row.getResultByName('position') + position: row.getResultByName('position'), + isEssential: Boolean(row.getResultByName('is_essential')), + isGroup: Boolean(row.getResultByName('is_group')), + parentUuid: row.getResultByName('parent_uuid') + })); + }, + + async getGroupChildren(groupUuid) { + const db = await PlacesUtils.promiseDBConnection(); + const rows = await db.executeCached(` + SELECT * FROM zen_pins + WHERE parent_uuid = :groupUuid + ORDER BY position ASC + `, { groupUuid }); + + return rows.map((row) => ({ + uuid: row.getResultByName('uuid'), + title: row.getResultByName('title'), + url: row.getResultByName('url'), + containerTabId: row.getResultByName('container_id'), + workspaceUuid: row.getResultByName('workspace_uuid'), + position: row.getResultByName('position'), + isEssential: Boolean(row.getResultByName('is_essential')), + isGroup: Boolean(row.getResultByName('is_group')), + parentUuid: row.getResultByName('parent_uuid') })); }, @@ -135,22 +171,44 @@ var ZenPinnedTabsStorage = { const changedUUIDs = [uuid]; await PlacesUtils.withConnectionWrapper('ZenPinnedTabsStorage.removePin', async (db) => { - await db.execute( - `DELETE FROM zen_pins WHERE uuid = :uuid`, - { uuid } - ); + await db.executeTransaction(async () => { + // Get all child UUIDs first for change tracking + const children = await db.execute( + `SELECT uuid FROM zen_pins WHERE parent_uuid = :uuid`, + { uuid } + ); - // Record the removal as a change - const now = Date.now(); - await db.execute(` - INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) - VALUES (:uuid, :timestamp) - `, { - uuid, - timestamp: Math.floor(now / 1000) + // Add child UUIDs to changedUUIDs array + for (const child of children) { + changedUUIDs.push(child.getResultByName('uuid')); + } + + // Delete all children in a single statement + await db.execute( + `DELETE FROM zen_pins WHERE parent_uuid = :uuid`, + { uuid } + ); + + // Delete the pin/group itself + await db.execute( + `DELETE FROM zen_pins WHERE uuid = :uuid`, + { uuid } + ); + + // Record the changes + const now = Math.floor(Date.now() / 1000); + for (const changedUuid of changedUUIDs) { + await db.execute(` + INSERT OR REPLACE INTO zen_pins_changes (uuid, timestamp) + VALUES (:uuid, :timestamp) + `, { + uuid: changedUuid, + timestamp: now + }); + } + + await this.updateLastChangeTimestamp(db); }); - - await this.updateLastChangeTimestamp(db); }); if (notifyObservers) {