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

@@ -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"