Merge pull request #3215 from kristijanribaric/feature/workspace-specific-bookmarks

Feature:  Workspace-specific bookmarks
This commit is contained in:
mr. m
2024-11-28 18:55:51 +01:00
committed by GitHub
9 changed files with 761 additions and 20 deletions

View File

@@ -13,6 +13,13 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
};
_hoveringSidebar = false;
_lastScrollTime = 0;
bookmarkMenus = [
"PlacesToolbar",
"bookmarks-menu-button",
"BMB_bookmarksToolbar",
"BMB_unsortedBookmarks",
"BMB_mobileBookmarks"
];
async init() {
if (!this.shouldHaveWorkspaces) {
@@ -61,6 +68,11 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
}
Services.obs.addObserver(this, 'weave:engine:sync:finish');
Services.obs.addObserver(async function observe(subject) {
this._workspaceBookmarksCache = null;
await this.workspaceBookmarks();
this._invalidateBookmarkContainers();
}.bind(this), "workspace-bookmarks-updated");
}
initializeWorkspaceNavigation() {
@@ -128,7 +140,7 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
}
// Change workspace based on scroll direction
const direction = event.deltaX > 0 ? -1 : 1;
const direction = event.deltaX > 0 ? 1 : -1;
await this.changeWorkspaceShortcut(direction);
this._lastScrollTime = currentTime;
}, { passive: true });
@@ -320,6 +332,21 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
return this._workspaceCache;
}
async workspaceBookmarks() {
if (this._workspaceBookmarksCache) {
return this._workspaceBookmarksCache;
}
const [bookmarks, lastChangeTimestamp] = await Promise.all([
ZenWorkspaceBookmarksStorage.getBookmarkGuidsByWorkspace(),
ZenWorkspaceBookmarksStorage.getLastChangeTimestamp(),
]);
this._workspaceBookmarksCache = { bookmarks, lastChangeTimestamp };
return this._workspaceCache;
}
async onWorkspacesEnabledChanged() {
if (this.workspaceEnabled) {
throw Error("Shoud've had reloaded the window");
@@ -339,6 +366,7 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
if (this.workspaceEnabled) {
this._initializeWorkspaceCreationIcons();
this._initializeWorkspaceTabContextMenus();
await this.workspaceBookmarks();
window.addEventListener('TabBrowserInserted', this.onTabBrowserInserted.bind(this));
await SessionStore.promiseInitialized;
let workspaces = await this._workspaces();
@@ -754,8 +782,10 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
if(clearCache) {
browser.ZenWorkspaces._workspaceCache = null;
browser.ZenWorkspaces._workspaceBookmarksCache = null;
}
let workspaces = await browser.ZenWorkspaces._workspaces();
await browser.ZenWorkspaces.workspaceBookmarks();
workspaceList.innerHTML = '';
workspaceList.parentNode.style.display = 'flex';
if (workspaces.workspaces.length <= 0) {
@@ -1268,16 +1298,23 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
}
}
// Reset bookmarks toolbar
const placesToolbar = document.getElementById("PlacesToolbar");
if (placesToolbar?._placesView) {
placesToolbar._placesView.invalidateContainer(placesToolbar._placesView._resultNode);
}
// Reset bookmarks
this._invalidateBookmarkContainers();
// Update workspace indicator
await this.updateWorkspaceIndicator();
}
_invalidateBookmarkContainers() {
for (let i = 0, len = this.bookmarkMenus.length; i < len; i++) {
const element = document.getElementById(this.bookmarkMenus[i]);
if (element && element._placesView) {
const placesView = element._placesView;
placesView.invalidateContainer(placesView._resultNode);
}
}
}
async updateWorkspaceIndicator() {
// Update current workspace indicator
const currentWorkspace = await this.getActiveWorkspace();
@@ -1550,12 +1587,24 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature {
}
isBookmarkInAnotherWorkspace(bookmark) {
let tags = bookmark.tags;
// if any tag starts with "_workspace_id" and the workspace id doesnt match the active workspace id, return null
if (tags) {
for (let tag of tags.split(",")) {
return !!(tag.startsWith("zen_workspace_") && this.getActiveWorkspaceFromCache()?.uuid !== tag.split("_")[2]);
if (!this._workspaceBookmarksCache?.bookmarks) return false;
const bookmarkGuid = bookmark.bookmarkGuid;
const activeWorkspaceUuid = this.activeWorkspace;
let isInActiveWorkspace = false;
let isInOtherWorkspace = false;
for (const [workspaceUuid, bookmarkGuids] of Object.entries(this._workspaceBookmarksCache.bookmarks)) {
if (bookmarkGuids.includes(bookmarkGuid)) {
if (workspaceUuid === activeWorkspaceUuid) {
isInActiveWorkspace = true;
} else {
isInOtherWorkspace = true;
}
}
}
// Return true only if the bookmark is in another workspace and not in the active one
return isInOtherWorkspace && !isInActiveWorkspace;
}
})();

View File

@@ -2,6 +2,8 @@ var ZenWorkspacesStorage = {
async init() {
console.log('ZenWorkspacesStorage: Initializing...');
await this._ensureTable();
await ZenWorkspaceBookmarksStorage.init();
ZenWorkspaces._delayedStartup();
},
async _ensureTable() {
@@ -64,7 +66,6 @@ var ZenWorkspacesStorage = {
await ZenWorkspacesStorage.migrateWorkspacesFromJSON();
}
ZenWorkspaces._delayedStartup();
});
},
@@ -405,3 +406,152 @@ var ZenWorkspacesStorage = {
this._notifyWorkspacesChanged("zen-workspace-updated", Array.from(changedUUIDs));
},
};
// Integration of workspace-specific bookmarks into Places
var ZenWorkspaceBookmarksStorage = {
async init() {
await this._ensureTable();
},
async _ensureTable() {
await PlacesUtils.withConnectionWrapper('ZenWorkspaceBookmarksStorage.init', async (db) => {
// Create table using GUIDs instead of IDs
await db.execute(`
CREATE TABLE IF NOT EXISTS zen_bookmarks_workspaces (
id INTEGER PRIMARY KEY,
bookmark_guid TEXT NOT NULL,
workspace_uuid TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
UNIQUE(bookmark_guid, workspace_uuid),
FOREIGN KEY(workspace_uuid) REFERENCES zen_workspaces(uuid) ON DELETE CASCADE,
FOREIGN KEY(bookmark_guid) REFERENCES moz_bookmarks(guid) ON DELETE CASCADE
)
`);
// Create index for fast lookups
await db.execute(`
CREATE INDEX IF NOT EXISTS idx_bookmarks_workspaces_lookup
ON zen_bookmarks_workspaces(workspace_uuid, bookmark_guid)
`);
// Add changes tracking table
await db.execute(`
CREATE TABLE IF NOT EXISTS zen_bookmarks_workspaces_changes (
id INTEGER PRIMARY KEY,
bookmark_guid TEXT NOT NULL,
workspace_uuid TEXT NOT NULL,
change_type TEXT NOT NULL,
timestamp INTEGER NOT NULL,
UNIQUE(bookmark_guid, workspace_uuid),
FOREIGN KEY(workspace_uuid) REFERENCES zen_workspaces(uuid) ON DELETE CASCADE,
FOREIGN KEY(bookmark_guid) REFERENCES moz_bookmarks(guid) ON DELETE CASCADE
)
`);
// Create index for changes tracking
await db.execute(`
CREATE INDEX IF NOT EXISTS idx_bookmarks_workspaces_changes
ON zen_bookmarks_workspaces_changes(bookmark_guid, workspace_uuid)
`);
});
},
/**
* Updates the last change timestamp in the metadata table.
* @param {Object} db - The database connection.
*/
async updateLastChangeTimestamp(db) {
const now = Date.now();
await db.execute(`
INSERT OR REPLACE INTO moz_meta (key, value)
VALUES ('zen_bookmarks_workspaces_last_change', :now)
`, { now });
},
/**
* Gets the timestamp of the last change.
* @returns {Promise<number>} The timestamp of the last change.
*/
async getLastChangeTimestamp() {
const db = await PlacesUtils.promiseDBConnection();
const result = await db.executeCached(`
SELECT value FROM moz_meta WHERE key = 'zen_bookmarks_workspaces_last_change'
`);
return result.length ? parseInt(result[0].getResultByName('value'), 10) : 0;
},
async getBookmarkWorkspaces(bookmarkGuid) {
const db = await PlacesUtils.promiseDBConnection();
const rows = await db.execute(`
SELECT workspace_uuid
FROM zen_bookmarks_workspaces
WHERE bookmark_guid = :bookmark_guid
`, { bookmark_guid: bookmarkGuid });
return rows.map(row => row.getResultByName("workspace_uuid"));
},
/**
* Get all bookmark GUIDs organized by workspace UUID.
* @returns {Promise<Object>} A dictionary with workspace UUIDs as keys and arrays of bookmark GUIDs as values.
* @example
* // Returns:
* {
* "workspace-uuid-1": ["bookmark-guid-1", "bookmark-guid-2"],
* "workspace-uuid-2": ["bookmark-guid-3"]
* }
*/
async getBookmarkGuidsByWorkspace() {
const db = await PlacesUtils.promiseDBConnection();
const rows = await db.execute(`
SELECT workspace_uuid, GROUP_CONCAT(bookmark_guid) as bookmark_guids
FROM zen_bookmarks_workspaces
GROUP BY workspace_uuid
`);
const result = {};
for (const row of rows) {
const workspaceUuid = row.getResultByName("workspace_uuid");
const bookmarkGuids = row.getResultByName("bookmark_guids");
result[workspaceUuid] = bookmarkGuids ? bookmarkGuids.split(',') : [];
}
return result;
},
/**
* Get all changed bookmarks with their change types.
* @returns {Promise<Object>} An object mapping bookmark+workspace pairs to their change data.
*/
async getChangedIDs() {
const db = await PlacesUtils.promiseDBConnection();
const rows = await db.execute(`
SELECT bookmark_guid, workspace_uuid, change_type, timestamp
FROM zen_bookmarks_workspaces_changes
`);
const changes = {};
for (const row of rows) {
const key = `${row.getResultByName('bookmark_guid')}:${row.getResultByName('workspace_uuid')}`;
changes[key] = {
type: row.getResultByName('change_type'),
timestamp: row.getResultByName('timestamp')
};
}
return changes;
},
/**
* Clear all recorded changes.
*/
async clearChangedIDs() {
await PlacesUtils.withConnectionWrapper('ZenWorkspaceBookmarksStorage.clearChangedIDs', async (db) => {
await db.execute(`DELETE FROM zen_bookmarks_workspaces_changes`);
});
},
};

View File

@@ -0,0 +1,159 @@
diff --git a/browser/components/places/PlacesUIUtils.sys.mjs b/browser/components/places/PlacesUIUtils.sys.mjs
index 0f79ba5dd42116d626445b86f6b24731d2fa8aad..76d692db1731e84b28d9035b03e34c176c12bd23 100644
--- a/browser/components/places/PlacesUIUtils.sys.mjs
+++ b/browser/components/places/PlacesUIUtils.sys.mjs
@@ -58,6 +58,7 @@ class BookmarkState {
info,
tags = "",
keyword = "",
+ workspaces = [],
isFolder = false,
children = [],
autosave = false,
@@ -82,12 +83,18 @@ class BookmarkState {
keyword,
parentGuid: info.parentGuid,
index,
+ workspaces,
};
// Edited bookmark
this._newState = {};
}
+ async _workspacesChanged(workspaces) {
+ this._newState.workspaces = workspaces;
+ await this._maybeSave();
+ }
+
/**
* Save edited title for the bookmark
*
@@ -181,6 +188,14 @@ class BookmarkState {
"BookmarkState::createBookmark"
);
this._guid = results?.[0];
+
+ if ('workspaces' in this._newState) {
+ try {
+ await this.updateBookmarkWorkspaces(this._guid, this._newState.workspaces);
+ } catch (ex) {
+ console.error("Failed to update workspace assignments:", ex);
+ }
+ }
return this._guid;
}
@@ -214,6 +229,14 @@ class BookmarkState {
"BookmarkState::save::createFolder"
);
this._guid = results[0];
+
+ if ('workspaces' in this._newState) {
+ try {
+ await this.updateBookmarkWorkspaces(this._guid, this._newState.workspaces);
+ } catch (ex) {
+ console.error("Failed to update workspace assignments:", ex);
+ }
+ }
return this._guid;
}
@@ -300,11 +323,97 @@ class BookmarkState {
await lazy.PlacesTransactions.batch(transactions, "BookmarkState::save");
}
+ if ('workspaces' in this._newState) {
+ try {
+ await this.updateBookmarkWorkspaces(this._guid, this._newState.workspaces);
+ } catch (ex) {
+ console.error("Failed to update workspace assignments:", ex);
+ }
+ }
this._originalState = { ...this._originalState, ...this._newState };
this._newState = {};
return this._guid;
}
+ async updateBookmarkWorkspaces(bookmarkGuid, workspaces) {
+ await lazy.PlacesUtils.withConnectionWrapper('ZenWorkspaceBookmarksStorage.updateBookmarkWorkspaces', async (db) => {
+ const now = Date.now();
+
+ await db.executeTransaction(async () => {
+ const rows = await db.execute(`
+ SELECT workspace_uuid
+ FROM zen_bookmarks_workspaces
+ WHERE bookmark_guid = :bookmark_guid
+ `, { bookmark_guid: bookmarkGuid });
+
+ const currentWorkspaces = rows.map(row => row.getResultByName("workspace_uuid"));
+ const workspacesToRemove = currentWorkspaces.filter(w => !workspaces.includes(w));
+ const workspacesToAdd = workspaces.filter(w => !currentWorkspaces.includes(w));
+
+ // If there are workspaces to remove, delete only those specific associations
+ if (workspacesToRemove.length > 0) {
+ const placeholders = workspacesToRemove.map(() => '?').join(',');
+ await db.execute(`
+ DELETE FROM zen_bookmarks_workspaces
+ WHERE bookmark_guid = :bookmark_guid
+ AND workspace_uuid IN (${placeholders})
+ `, [bookmarkGuid, ...workspacesToRemove]);
+
+ // Record removals
+ for (const workspace of workspacesToRemove) {
+ await this._recordChange(db, bookmarkGuid, workspace, 'removed');
+ }
+ }
+
+ // Add only new associations
+ for (const workspaceUuid of workspacesToAdd) {
+ await db.execute(`
+ INSERT INTO zen_bookmarks_workspaces (
+ bookmark_guid, workspace_uuid, created_at, updated_at
+ ) VALUES (
+ :bookmark_guid, :workspace_uuid, :now, :now
+ )
+ `, {
+ bookmark_guid: bookmarkGuid,
+ workspace_uuid: workspaceUuid,
+ now
+ });
+
+ await this._recordChange(db, bookmarkGuid, workspaceUuid, 'added');
+ }
+ });
+ });
+
+ const changes = { bookmarkGuid, workspaces };
+ Services.obs.notifyObservers(null, "workspace-bookmarks-updated", JSON.stringify(changes));
+ }
+
+ async _recordChange(db, bookmarkGuid, workspaceUuid, changeType) {
+ const now = Date.now();
+ await db.execute(`
+ INSERT OR REPLACE INTO zen_bookmarks_workspaces_changes (
+ bookmark_guid, workspace_uuid, change_type, timestamp
+ ) VALUES (
+ :bookmark_guid, :workspace_uuid, :change_type, :timestamp
+ )
+ `, {
+ bookmark_guid: bookmarkGuid,
+ workspace_uuid: workspaceUuid,
+ change_type: changeType,
+ timestamp: Math.floor(now / 1000)
+ });
+
+ await this._updateLastChangeTimestamp(db);
+ }
+
+ async _updateLastChangeTimestamp(db) {
+ const now = Date.now();
+ await db.execute(`
+ INSERT OR REPLACE INTO moz_meta (key, value)
+ VALUES ('zen_bookmarks_workspaces_last_change', :now)
+ `, { now });
+ }
+
/**
* Append transactions to update tags by given information.
*

View File

@@ -1,5 +1,5 @@
diff --git a/browser/components/places/content/bookmarkProperties.xhtml b/browser/components/places/content/bookmarkProperties.xhtml
index 047652a52e705d49f870399992873fce536c07b9..2932eb94e8c16eb05f172322a6ce3ea201ecd0b1 100644
index 047652a52e705d49f870399992873fce536c07b9..8bc7d1c5e44c33d90f82fdc6f66d9e2e80c60bae 100644
--- a/browser/components/places/content/bookmarkProperties.xhtml
+++ b/browser/components/places/content/bookmarkProperties.xhtml
@@ -37,6 +37,7 @@
@@ -10,3 +10,11 @@ index 047652a52e705d49f870399992873fce536c07b9..2932eb94e8c16eb05f172322a6ce3ea2
</linkset>
<stringbundleset id="stringbundleset">
@@ -44,6 +45,7 @@
src="chrome://browser/locale/places/bookmarkProperties.properties"/>
</stringbundleset>
+ <script src="chrome://browser/content/zen-components/ZenWorkspacesStorage.mjs" />
<script src="chrome://browser/content/places/editBookmark.js"/>
<script src="chrome://browser/content/places/bookmarkProperties.js"/>
<script src="chrome://global/content/globalOverlay.js"/>

View File

@@ -1,8 +1,35 @@
diff --git a/browser/components/places/content/browserPlacesViews.js b/browser/components/places/content/browserPlacesViews.js
index 1bfa0af16178c9b42172bc1b1e0249d28ff8e9e6..e7e76a6d548b32887c1d39053e42c5e3dafbb839 100644
index 1bfa0af16178c9b42172bc1b1e0249d28ff8e9e6..417a9dc4e55208bdc9c1422a3bae14361a4964c5 100644
--- a/browser/components/places/content/browserPlacesViews.js
+++ b/browser/components/places/content/browserPlacesViews.js
@@ -393,6 +393,7 @@ class PlacesViewBase {
@@ -330,12 +330,23 @@ class PlacesViewBase {
this._cleanPopup(aPopup);
+ let children = [];
let cc = resultNode.childCount;
- if (cc > 0) {
+ for (let i = 0; i < cc; ++i) {
+ let child = resultNode.getChild(i);
+ // Skip nodes that don't belong in current workspace
+ if (PlacesUtils.nodeIsURI(child) || PlacesUtils.containerTypes.includes(child.type)) {
+ if (ZenWorkspaces.isBookmarkInAnotherWorkspace(child)) {
+ continue;
+ }
+ }
+ children.push(child);
+ }
+
+ if (children.length > 0) {
this._setEmptyPopupStatus(aPopup, false);
let fragment = document.createDocumentFragment();
- for (let i = 0; i < cc; ++i) {
- let child = resultNode.getChild(i);
+ for (let child of children) {
this._insertNewItemToPopup(child, fragment);
}
aPopup.insertBefore(fragment, aPopup._endMarker);
@@ -393,6 +404,7 @@ class PlacesViewBase {
"scheme",
PlacesUIUtils.guessUrlSchemeForUI(aPlacesNode.uri)
);
@@ -10,16 +37,89 @@ index 1bfa0af16178c9b42172bc1b1e0249d28ff8e9e6..e7e76a6d548b32887c1d39053e42c5e3
} else if (PlacesUtils.containerTypes.includes(type)) {
element = document.createXULElement("menu");
element.setAttribute("container", "true");
@@ -1087,6 +1088,8 @@ class PlacesToolbar extends PlacesViewBase {
@@ -981,25 +993,33 @@ class PlacesToolbar extends PlacesViewBase {
this._rootElt.firstChild.remove();
}
+ let visibleNodes = [];
let cc = this._resultNode.childCount;
- if (cc > 0) {
- // There could be a lot of nodes, but we only want to build the ones that
- // are more likely to be shown, not all of them.
- // We also don't want to wait for reflows at every node insertion, to
- // calculate a precise number of visible items, thus we guess a size from
- // the first non-separator node (because separators have flexible size).
+ for (let i = 0; i < cc; i++) {
+ let child = this._resultNode.getChild(i);
+ if (PlacesUtils.nodeIsURI(child) || PlacesUtils.containerTypes.includes(child.type)) {
+ if (!ZenWorkspaces.isBookmarkInAnotherWorkspace(child)) {
+ visibleNodes.push(child);
+ }
+ } else {
+ // Always include separators
+ visibleNodes.push(child);
+ }
+ }
+
+ if (visibleNodes.length > 0) {
+ // Look for the first non-separator node.
let startIndex = 0;
let limit = await this._runBeforeFrameRender(() => {
if (!this._isAlive) {
- return cc;
+ return visibleNodes.length;
}
- // Look for the first non-separator node.
let elt;
- while (startIndex < cc) {
+ while (startIndex < visibleNodes.length) {
elt = this._insertNewItem(
- this._resultNode.getChild(startIndex),
- this._rootElt
+ visibleNodes[startIndex],
+ this._rootElt
);
++startIndex;
if (elt.localName != "toolbarseparator") {
@@ -1007,15 +1027,12 @@ class PlacesToolbar extends PlacesViewBase {
}
}
if (!elt) {
- return cc;
+ return visibleNodes.length;
}
return window.promiseDocumentFlushed(() => {
- // We assume a button with just the icon will be more or less a square,
- // then compensate the measurement error by considering a larger screen
- // width. Moreover the window could be bigger than the screen.
- let size = elt.clientHeight || 1; // Sanity fallback.
- return Math.min(cc, parseInt((window.screen.width * 1.5) / size));
+ let size = elt.clientHeight || 1;
+ return Math.min(visibleNodes.length, parseInt((window.screen.width * 1.5) / size));
});
});
@@ -1025,7 +1042,7 @@ class PlacesToolbar extends PlacesViewBase {
let fragment = document.createDocumentFragment();
for (let i = startIndex; i < limit; ++i) {
- this._insertNewItem(this._resultNode.getChild(i), fragment);
+ this._insertNewItem(visibleNodes[i], fragment);
}
await new Promise(resolve => window.requestAnimationFrame(resolve));
if (!this._isAlive) {
@@ -1087,6 +1104,8 @@ class PlacesToolbar extends PlacesViewBase {
"scheme",
PlacesUIUtils.guessUrlSchemeForUI(aChild.uri)
);
+ button.hidden = ZenWorkspaces.isBookmarkInAnotherWorkspace(aChild);
+
+ button.addEventListener("command", gZenGlanceManager.openGlanceForBookmark.bind(gZenGlanceManager));
}
}
@@ -2235,7 +2238,7 @@ this.PlacesPanelview = class PlacesPanelview extends PlacesViewBase {
@@ -2235,7 +2254,7 @@ this.PlacesPanelview = class PlacesPanelview extends PlacesViewBase {
PlacesUIUtils.guessUrlSchemeForUI(placesNode.uri)
);
element.setAttribute("label", PlacesUIUtils.getBestTitle(placesNode));

View File

@@ -0,0 +1,182 @@
diff --git a/browser/components/places/content/editBookmark.js b/browser/components/places/content/editBookmark.js
index 9f17174fdd9cc1eaefb4330da1e10f40eeda2f31..e2c38872ee3a5c45d2e288e67d33f9ce24cab2b9 100644
--- a/browser/components/places/content/editBookmark.js
+++ b/browser/components/places/content/editBookmark.js
@@ -370,6 +370,10 @@ var gEditItemOverlay = {
this._keywordField.readOnly = this.readOnly;
}
+ if (showOrCollapse("workspaceRow", true, "workspace")) {
+ await this._initWorkspaceDropdown(aInfo);
+ }
+
// Collapse the tag selector if the item does not accept tags.
if (showOrCollapse("tagsRow", isBookmark || bulkTagging, "tags")) {
this._initTagsField();
@@ -682,6 +686,7 @@ var gEditItemOverlay = {
if (this._paneInfo.isBookmark) {
options.tags = this._element("tagsField").value;
options.keyword = this._keyword;
+ options.workspaces = this._selectedWorkspaces;
}
if (this._paneInfo.bulkTagging) {
@@ -1232,6 +1237,148 @@ var gEditItemOverlay = {
get bookmarkState() {
return this._bookmarkState;
},
+
+ async _initWorkspaceSelector() {
+ if(document.documentElement.getAttribute("windowtype") === "Places:Organizer") {
+ return;
+ }
+ this._workspaces = await ZenWorkspacesStorage.getWorkspaces();
+
+ const selectElement = this._workspaceSelect;
+
+ // Clear any existing options
+ while (selectElement.firstChild) {
+ selectElement.removeChild(selectElement.firstChild);
+ }
+
+ // For each workspace, create an option element
+ for (let workspace of this._workspaces) {
+ const option = document.createElementNS("http://www.w3.org/1999/xhtml", "option");
+ option.textContent = workspace.name;
+ option.value = workspace.uuid;
+ selectElement.appendChild(option);
+ }
+
+ selectElement.disabled = this.readOnly;
+ },
+ async onWorkspaceSelectionChange(event) {
+ if(document.documentElement.getAttribute("windowtype") === "Places:Organizer") {
+ return;
+ }
+ event.stopPropagation();
+
+ // Add new workspaces uuids
+ const checkboxes = this._workspaceList.querySelectorAll("input[type='checkbox']");
+ const newWorkspaces = [];
+ const selectedNames = [];
+
+ checkboxes.forEach(checkbox => {
+ if (checkbox.checked) {
+ newWorkspaces.push(checkbox.value);
+
+ const label = checkbox.parentNode.textContent.trim();
+ selectedNames.push(label);
+ }
+ });
+
+ this._selectedWorkspaces = [ ...newWorkspaces];
+
+ // Update the bookmark state
+ if (this._bookmarkState) {
+ await this._bookmarkState._workspacesChanged(this._selectedWorkspaces);
+ }
+
+ // Update summary text
+ this._workspaceSummary.textContent = selectedNames.length
+ ? selectedNames.join(", ")
+ : "-";
+ },
+
+ onWorkspaceDropdownToggle() {
+ if(document.documentElement.getAttribute("windowtype") === "Places:Organizer") {
+ return;
+ }
+
+ // Toggle active class on the container
+ const dropdown = this._workspaceList;
+ const button = this._workspaceSummary;
+
+ dropdown.hidden = !dropdown.hidden;
+
+ var expander = this._element("workspacesSelectorExpander");
+ expander.classList.toggle("expander-up", !dropdown.hidden);
+ expander.classList.toggle("expander-down", dropdown.hidden);
+
+ // Only update summary text when closing the dropdown
+ if (dropdown.hidden) {
+ const checkboxes = this._workspaceList.querySelectorAll("input[type='checkbox']");
+ const selectedLabels = [];
+
+ checkboxes.forEach(checkbox => {
+ if (checkbox.checked) {
+ const label = checkbox.parentNode.textContent.trim();
+ selectedLabels.push(label);
+ }
+ });
+
+ button.textContent = selectedLabels.length
+ ? selectedLabels.join(", ")
+ : "-";
+ }
+ },
+
+ async _initWorkspaceDropdown(aInfo) {
+ if(document.documentElement.getAttribute("windowtype") === "Places:Organizer") {
+ return;
+ }
+ this._workspaces = await ZenWorkspacesStorage.getWorkspaces();
+ const workspaceList = this._workspaceList;
+ if(aInfo.node?.bookmarkGuid) {
+ this._selectedWorkspaces = await ZenWorkspaceBookmarksStorage.getBookmarkWorkspaces(aInfo.node.bookmarkGuid);
+ }
+
+ // Clear existing items
+ workspaceList.innerHTML = "";
+
+ // Create checkbox items for each workspace
+ for (let workspace of this._workspaces) {
+ const li = document.createElementNS("http://www.w3.org/1999/xhtml", "li");
+ const label = document.createElementNS("http://www.w3.org/1999/xhtml", "label");
+ const input = document.createElementNS("http://www.w3.org/1999/xhtml", "input");
+
+ input.setAttribute("type", "checkbox");
+ input.setAttribute("name", "workspace");
+ input.setAttribute("value", workspace.uuid);
+
+ // Check if this workspace is selected
+ input.checked = this._selectedWorkspaces?.includes(workspace.uuid) ?? false;
+
+ input.addEventListener("click", this.onWorkspaceSelectionChange.bind(this));
+
+ label.appendChild(input);
+ label.appendChild(document.createTextNode(workspace.name));
+ li.appendChild(label);
+ workspaceList.appendChild(li);
+ }
+
+ // Get the names of selected workspaces for initial summary
+ const selectedNames = this._workspaces
+ .filter(ws => this._selectedWorkspaces?.includes(ws.uuid))
+ .map(ws => ws.name);
+
+ // Update summary text with comma-separated list
+ this._workspaceSummary.textContent = selectedNames.length
+ ? selectedNames.join(", ")
+ : "-";
+
+ // Handle read-only state
+ if (this.readOnly) {
+ this._workspaceDropdown.setAttribute("disabled", "true");
+ } else {
+ this._workspaceDropdown.removeAttribute("disabled");
+ }
+ },
+ _selectedWorkspaces : [],
};
ChromeUtils.defineLazyGetter(gEditItemOverlay, "_folderTree", () => {
@@ -1267,6 +1414,9 @@ for (let elt of [
"locationField",
"keywordField",
"tagsField",
+ "workspaceDropdown",
+ "workspaceSummary",
+ "workspaceList",
]) {
let eltScoped = elt;
ChromeUtils.defineLazyGetter(gEditItemOverlay, `_${eltScoped}`, () =>

View File

@@ -1,5 +1,5 @@
diff --git a/browser/components/places/content/editBookmarkPanel.inc.xhtml b/browser/components/places/content/editBookmarkPanel.inc.xhtml
index 3ec3f094831c2143a818b43d1761a571f0ffa63d..309dfa8ed628f4cc124fe16d20b7411065c09f23 100644
index 3ec3f094831c2143a818b43d1761a571f0ffa63d..c4dd904604ee10a909bbcc7c03dd0dd3536020b1 100644
--- a/browser/components/places/content/editBookmarkPanel.inc.xhtml
+++ b/browser/components/places/content/editBookmarkPanel.inc.xhtml
@@ -5,7 +5,7 @@
@@ -29,11 +29,30 @@ index 3ec3f094831c2143a818b43d1761a571f0ffa63d..309dfa8ed628f4cc124fe16d20b74110
<label data-l10n-id="bookmark-overlay-location-2"
class="editBMPanel_folderRow hideable"
control="editBMPanel_folderMenuList"/>
@@ -51,6 +51,7 @@
@@ -51,6 +51,26 @@
data-l10n-id="bookmark-overlay-folders-expander2"
oncommand="gEditItemOverlay.toggleFolderTreeVisibility();"/>
</hbox>
+</hbox>
+ <vbox>
+ <label data-l10n-id="zen-bookmark-edit-panel-workspace-selector"
+ class="hideable"
+ control="editBMPanel_workspacesSelectorExpander"/>
+ <div id="editBMPanel_workspaceDropdown"
+ class="editBMPanel_workspaceRow hideable workspace-dropdown">
+ <div
+ id="editBMPanel_workspaceSummary"
+ class="workspace-trigger">-</div>
+ <button id="editBMPanel_workspacesSelectorExpander"
+ class="expander-down panel-button"
+ data-l10n-id="bookmark-overlay-tags-expander2"
+ oncommand="gEditItemOverlay.onWorkspaceDropdownToggle();"/>
+
+ </div>
+ </vbox>
+
+ <ul id="editBMPanel_workspaceList" class="workspace-list hideable" hidden="true">
+ </ul>
<vbox id="editBMPanel_folderTreeRow"
class="hideable"

View File

@@ -0,0 +1,15 @@
diff --git a/browser/locales/en-US/browser/editBookmarkOverlay.ftl b/browser/locales/en-US/browser/editBookmarkOverlay.ftl
index da74660e48620fe9097d05a51ba4be34f21246e6..d70b98c71ca3a70732c633d939079f9fb589726f 100644
--- a/browser/locales/en-US/browser/editBookmarkOverlay.ftl
+++ b/browser/locales/en-US/browser/editBookmarkOverlay.ftl
@@ -37,6 +37,10 @@ bookmark-overlay-tags-2 =
.value = Tags
.accesskey = T
+zen-bookmark-edit-panel-workspace-selector =
+ .value = Workspaces
+ .accesskey = W
+
bookmark-overlay-tags-empty-description =
.placeholder = Separate tags with commas

View File

@@ -0,0 +1,59 @@
diff --git a/browser/themes/shared/places/editBookmark.css b/browser/themes/shared/places/editBookmark.css
index 4c00982e620f4cfd5aa1d97d45a276f5d41d0d74..58018015d6046895c996f808785ab7282e5fed81 100644
--- a/browser/themes/shared/places/editBookmark.css
+++ b/browser/themes/shared/places/editBookmark.css
@@ -158,3 +158,53 @@
font-size: 0.9em;
margin: 2px 4px;
}
+
+/*Bookmark workspace selector styles*/
+.workspace-dropdown {
+ position: relative;
+ width: 100%;
+ display: flex;
+ gap: 16px;
+}
+
+.workspace-trigger {
+ width: 100%;
+ text-align: left;
+ padding: 8px 12px;
+ border: 1px solid var(--card-outline-color);
+ border-radius: 4px;
+ background-color: var(--zen-colors-tertiary);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.workspace-list {
+ flex-direction: column;
+ width: 100%;
+ max-height: 200px;
+ overflow-y: auto;
+ margin-top: 4px;
+ border: 1px solid var(--card-outline-color);
+ border-radius: 4px;
+ background-color: var(--zen-colors-tertiary);
+ box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
+ padding: 4px 0;
+}
+
+.workspace-list li {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.workspace-list li > label {
+ display: flex;
+ align-items: center;
+ padding: 4px 12px;
+ cursor: pointer;
+}
+
+.workspace-list input[type="checkbox"] {
+ margin-right: 8px;
+}
\ No newline at end of file