diff --git a/README.md b/README.md index 175c414..a21f450 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ encrypted Frappe archives cannot be validated before upload. ## Usage -Open **Restic Backups → Backup Control** (`/app/restic-backup-control` on v15, +Open **Restic Backup → Backup Control** (`/app/restic-backup-control` on v15, `/desk/restic-backup-control` on v16). From there you can run a backup, check off-site snapshots and change settings. Only System Managers can access the page, the settings, the history and the backup APIs; there is no restore endpoint on the web. diff --git a/frappe_restic/desktop_icon/restic_backup.json b/frappe_restic/desktop_icon/restic_backup.json new file mode 100644 index 0000000..df19e53 --- /dev/null +++ b/frappe_restic/desktop_icon/restic_backup.json @@ -0,0 +1,14 @@ +{ + "app": "frappe_restic", + "doctype": "Desktop Icon", + "hidden": 0, + "icon": "database", + "icon_type": "Link", + "label": "Restic Backup", + "link_to": "Restic Backup", + "link_type": "Workspace Sidebar", + "modified": "2026-09-30 12:00:00.000000", + "name": "Restic Backup", + "restrict_removal": 0, + "standard": 1 +} diff --git a/frappe_restic/patches.txt b/frappe_restic/patches.txt index 8a2400b..229c7e0 100644 --- a/frappe_restic/patches.txt +++ b/frappe_restic/patches.txt @@ -1,3 +1,4 @@ [pre_model_sync] [post_model_sync] +frappe_restic.patches.name_desk_entries_after_module diff --git a/frappe_restic/patches/__init__.py b/frappe_restic/patches/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/frappe_restic/patches/name_desk_entries_after_module.py b/frappe_restic/patches/name_desk_entries_after_module.py new file mode 100644 index 0000000..f5e38f7 --- /dev/null +++ b/frappe_restic/patches/name_desk_entries_after_module.py @@ -0,0 +1,15 @@ +import frappe + +LEGACY = "Restic Backups" + + +def execute(): + """Remove the desk entries named after the old "Restic Backups" workspace. + + The workspace, sidebar and desktop icon are now named after the module, so Frappe v16 finds + one sidebar for every backup screen instead of generating a second one for the module. + """ + # Workspace Sidebar and Desktop Icon exist from Frappe v16; v15 only has the workspace. + for doctype in ("Desktop Icon", "Workspace Sidebar", "Workspace"): + if frappe.db.table_exists(doctype) and frappe.db.exists(doctype, LEGACY): + frappe.delete_doc(doctype, LEGACY, force=True, ignore_permissions=True) diff --git a/frappe_restic/public/icons/desktop_icons/solid/restic_backup.svg b/frappe_restic/public/icons/desktop_icons/solid/restic_backup.svg new file mode 100644 index 0000000..5d7325d --- /dev/null +++ b/frappe_restic/public/icons/desktop_icons/solid/restic_backup.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frappe_restic/public/icons/desktop_icons/subtle/restic_backup.svg b/frappe_restic/public/icons/desktop_icons/subtle/restic_backup.svg new file mode 100644 index 0000000..64fbad9 --- /dev/null +++ b/frappe_restic/public/icons/desktop_icons/subtle/restic_backup.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frappe_restic/public/scss/restic_backups.bundle.scss b/frappe_restic/public/scss/restic_backups.bundle.scss index 904dabc..b75c24f 100644 --- a/frappe_restic/public/scss/restic_backups.bundle.scss +++ b/frappe_restic/public/scss/restic_backups.bundle.scss @@ -1,3 +1,12 @@ +// Frappe themes its own .frappe-card, but Bootstrap's .card stays white in dark mode. +.restic-backup-control, +.restic-backup-settings-dialog { + .card { + border-color: var(--border-color); + background-color: var(--card-bg); + } +} + .restic-backup-settings-dialog { .modal-dialog { max-width: 880px; @@ -35,15 +44,6 @@ .restic-backup-day { min-width: 2.9rem; - border-color: var(--gray-300); - background: var(--control-bg); - color: var(--text-color); - - &.is-selected { - border-color: var(--primary-color); - background: var(--primary-color); - color: var(--fg-color); - } } .restic-backup-schedule-time { diff --git a/frappe_restic/restic_backup/doctype/restic_backup_run/restic_backup_run_list.js b/frappe_restic/restic_backup/doctype/restic_backup_run/restic_backup_run_list.js index b2fee8e..e56bc76 100644 --- a/frappe_restic/restic_backup/doctype/restic_backup_run/restic_backup_run_list.js +++ b/frappe_restic/restic_backup/doctype/restic_backup_run/restic_backup_run_list.js @@ -65,11 +65,6 @@ listview.page.add_inner_button(__("Backup Control"), () => frappe.set_route("restic-backup-control") ); - frappe.breadcrumbs.add({ - type: "Custom", - route: frappe.router.make_url(["restic-backup-control"]), - label: __("Backup Control"), - }); }, formatters: { // Frappe inserts subject text with textContent, not HTML. diff --git a/frappe_restic/restic_backup/doctype/restic_backup_settings/restic_backup_settings.js b/frappe_restic/restic_backup/doctype/restic_backup_settings/restic_backup_settings.js index 0250f80..c40f3ea 100644 --- a/frappe_restic/restic_backup/doctype/restic_backup_settings/restic_backup_settings.js +++ b/frappe_restic/restic_backup/doctype/restic_backup_settings/restic_backup_settings.js @@ -1,11 +1,6 @@ frappe.ui.form.on("Restic Backup Settings", { refresh(frm) { frm.page.set_title(__("Backup Settings")); - frappe.breadcrumbs.add({ - type: "Custom", - route: frappe.router.make_url(["restic-backup-control"]), - label: __("Backup Control"), - }); frm.add_custom_button(__("Back to Backup Control"), () => { frappe.set_route("restic-backup-control"); }); diff --git a/frappe_restic/restic_backup/page/restic_backup_control/restic_backup_control.js b/frappe_restic/restic_backup/page/restic_backup_control/restic_backup_control.js index bf9d7f1..b94035e 100644 --- a/frappe_restic/restic_backup/page/restic_backup_control/restic_backup_control.js +++ b/frappe_restic/restic_backup/page/restic_backup_control/restic_backup_control.js @@ -1,5 +1,4 @@ frappe.pages["restic-backup-control"].on_page_load = function (wrapper) { - frappe.breadcrumbs.add("Setup"); const page = frappe.ui.make_app_page({ parent: wrapper, title: __("Backup Control"), @@ -11,6 +10,12 @@ frappe.pages["restic-backup-control"].on_page_load = function (wrapper) { page.add_inner_button(__("Settings"), () => backupControl.openSettings()); }; +// Frappe v16 picks the sidebar for a page from the module of the last doctype opened, so opening +// this page from another app's screen keeps that app's sidebar. Pick again from this page's route. +frappe.pages["restic-backup-control"].on_page_show = function () { + frappe.app.sidebar?.set_workspace_sidebar?.(); +}; + class ResticBackupControl { constructor(page, wrapper) { this.page = page; @@ -234,7 +239,7 @@ class ResticBackupControl { .map((day) => { const selected = entry.days.includes(day); return ``; diff --git a/frappe_restic/restic_backup/workspace/restic_backups/restic_backups.json b/frappe_restic/restic_backup/workspace/restic_backup/restic_backup.json similarity index 75% rename from frappe_restic/restic_backup/workspace/restic_backups/restic_backups.json rename to frappe_restic/restic_backup/workspace/restic_backup/restic_backup.json index d0417db..9a9461c 100644 --- a/frappe_restic/restic_backup/workspace/restic_backups/restic_backups.json +++ b/frappe_restic/restic_backup/workspace/restic_backup/restic_backup.json @@ -1,8 +1,9 @@ { "doctype": "Workspace", - "name": "Restic Backups", - "label": "Restic Backups", - "title": "Restic Backups", + "name": "Restic Backup", + "label": "Restic Backup", + "title": "Restic Backup", + "app": "frappe_restic", "module": "Restic Backup", "public": 1, "is_hidden": 0, @@ -22,5 +23,5 @@ } ], "content": "[{\"id\": \"backup-shortcut\", \"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Backup Control\", \"col\": 3}}]", - "modified": "2026-09-23 13:48:25.000000" + "modified": "2026-09-30 12:00:00.000000" } diff --git a/frappe_restic/test_desk.py b/frappe_restic/test_desk.py new file mode 100644 index 0000000..6dcc1de --- /dev/null +++ b/frappe_restic/test_desk.py @@ -0,0 +1,64 @@ +import frappe +from frappe.tests.utils import FrappeTestCase + +from frappe_restic.patches import name_desk_entries_after_module + +MODULE = "Restic Backup" +LEGACY = "Restic Backups" + + +def has_sidebars(): + # Workspace Sidebar and Desktop Icon exist from Frappe v16. + return frappe.db.table_exists("Workspace Sidebar") + + +class TestDeskEntries(FrappeTestCase): + def test_workspace_is_named_after_the_module(self): + self.assertEqual(frappe.get_all("Workspace", filters={"module": MODULE}, pluck="name"), [MODULE]) + + def test_one_sidebar_and_icon_for_the_module(self): + if not has_sidebars(): + self.skipTest("Frappe v15 has no sidebars") + from frappe.desk.doctype.workspace_sidebar.workspace_sidebar import ( + auto_generate_sidebar_from_module, + ) + + sidebars = frappe.get_all( + "Workspace Sidebar", filters={"module": MODULE, "for_user": ["is", "not set"]}, pluck="name" + ) + self.assertEqual(sidebars, [MODULE]) + # Frappe generates a second sidebar for a module that has none named after it. + self.assertNotIn(MODULE, [sidebar.title for sidebar in auto_generate_sidebar_from_module()]) + icons = frappe.get_all("Desktop Icon", filters={"link_to": ["in", [MODULE, LEGACY]]}, pluck="name") + self.assertEqual(icons, [MODULE]) + + def test_patch_removes_the_entries_named_after_the_old_workspace(self): + frappe.get_doc( + { + "doctype": "Workspace", + "label": LEGACY, + "title": LEGACY, + "module": MODULE, + "public": 1, + "content": "[]", + } + ).insert() + doctypes = ["Workspace"] + if has_sidebars(): + frappe.get_doc({"doctype": "Workspace Sidebar", "title": LEGACY, "module": MODULE}).insert() + frappe.get_doc( + { + "doctype": "Desktop Icon", + "label": LEGACY, + "icon_type": "Link", + "link_type": "Workspace Sidebar", + "link_to": LEGACY, + } + ).insert() + doctypes += ["Workspace Sidebar", "Desktop Icon"] + + name_desk_entries_after_module.execute() + + for doctype in doctypes: + self.assertFalse(frappe.db.exists(doctype, LEGACY), doctype) + self.assertTrue(frappe.db.exists("Workspace", MODULE)) diff --git a/frappe_restic/workspace_sidebar/restic_backup.json b/frappe_restic/workspace_sidebar/restic_backup.json new file mode 100644 index 0000000..843c2df --- /dev/null +++ b/frappe_restic/workspace_sidebar/restic_backup.json @@ -0,0 +1,33 @@ +{ + "app": "frappe_restic", + "doctype": "Workspace Sidebar", + "header_icon": "database", + "items": [ + { + "icon": "database-backup", + "label": "Backup Control", + "link_to": "restic-backup-control", + "link_type": "Page", + "type": "Link" + }, + { + "icon": "history", + "label": "Backup Runs", + "link_to": "Restic Backup Run", + "link_type": "DocType", + "type": "Link" + }, + { + "icon": "settings", + "label": "Backup Settings", + "link_to": "Restic Backup Settings", + "link_type": "DocType", + "type": "Link" + } + ], + "modified": "2026-09-30 12:00:00.000000", + "module": "Restic Backup", + "name": "Restic Backup", + "standard": 1, + "title": "Restic Backup" +}