Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
14 changes: 14 additions & 0 deletions frappe_restic/desktop_icon/restic_backup.json
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions frappe_restic/patches.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
[pre_model_sync]

[post_model_sync]
frappe_restic.patches.name_desk_entries_after_module
Empty file.
15 changes: 15 additions & 0 deletions frappe_restic/patches/name_desk_entries_after_module.py
Original file line number Diff line number Diff line change
@@ -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)
10 changes: 10 additions & 0 deletions frappe_restic/public/icons/desktop_icons/solid/restic_backup.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions frappe_restic/public/icons/desktop_icons/subtle/restic_backup.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 9 additions & 9 deletions frappe_restic/public/scss/restic_backups.bundle.scss
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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");
});
Expand Down
Original file line number Diff line number Diff line change
@@ -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"),
Expand All @@ -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;
Expand Down Expand Up @@ -234,7 +239,7 @@ class ResticBackupControl {
.map((day) => {
const selected = entry.days.includes(day);
return `<button type="button"
class="btn btn-sm restic-backup-day${selected ? " is-selected" : ""}"
class="btn btn-sm ${selected ? "btn-primary" : "btn-default"} restic-backup-day"
data-index="${index}"
data-day="${day}"
aria-pressed="${selected}">${__(day.slice(0, 3))}</button>`;
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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"
}
64 changes: 64 additions & 0 deletions frappe_restic/test_desk.py
Original file line number Diff line number Diff line change
@@ -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))
33 changes: 33 additions & 0 deletions frappe_restic/workspace_sidebar/restic_backup.json
Original file line number Diff line number Diff line change
@@ -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"
}
Loading