From 0431c653fd6aeb2a62debb3e7a3cbaf42f11648c Mon Sep 17 00:00:00 2001 From: Bhushan Barbuddhe Date: Fri, 27 Jun 2025 18:32:08 +0530 Subject: [PATCH 001/274] fix: CSS issue on forgot password --- .../public/css/frappe_desk_theme.css | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/frappe_desk_theme/public/css/frappe_desk_theme.css b/frappe_desk_theme/public/css/frappe_desk_theme.css index 23c1973..ed28e9b 100644 --- a/frappe_desk_theme/public/css/frappe_desk_theme.css +++ b/frappe_desk_theme/public/css/frappe_desk_theme.css @@ -58,13 +58,17 @@ label.control-label { } /* Login form container - supports absolute positioning (Left/Right/Center) */ -.for-login { +.for-login,.for-signup,.for-forgot, .for-login-with-email-link { position: var(--login-box-position, static); right: var(--login-box-right, auto); left: var(--login-box-left, auto); top: var(--login-box-top, 18%); background-color: var(--login-box-bg-override, transparent) !important; border-radius: var(--login-box-border-radius, 0) !important; +} + +/* Login animation - only for login forms */ +.for-login { opacity: 0; transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out; transform: translateY(20px); @@ -88,7 +92,7 @@ label.control-label { } /* Login form card - inner container with padding and width controls */ -.login-content.page-card { +.login-content.page-card, .signup-content.page-card { background-color: var(--login-box-bg,#fff) !important; border: 2px solid var(--login-box-bg,#fff) !important; width: var(--login-box-width,400px); @@ -96,8 +100,19 @@ label.control-label { } /* Login content border - customizable border for form container */ -.login-content { +/* .login-content, .forgot-content, .signup-content { border: var(--login-content-border,none); + box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1); +} */ + +/* Signup message - when app details are inside the box, make it part of the container */ +.sign-up-message { + background: transparent !important; + border: none !important; + box-shadow: none !important; + margin-top: 20px; + padding: 0 !important; + color: inherit; } /* Default login page title - can be hidden when custom title is used */ @@ -326,9 +341,11 @@ div.level-right { ======================================== */ /* Number widgets - dashboard widgets and statistical cards */ -.widget.number-widget-box { +.widget.number-widget-box, +.widget.dashboard-widget-box { background-color: var(--widget-bg); border: 2px solid var(--widget-border); + box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1); } /* Widget content - all text elements within widgets */ From eab1434fbc91f427be0db203778cb88e525c1667 Mon Sep 17 00:00:00 2001 From: Bhushan Barbuddhe Date: Fri, 27 Jun 2025 20:08:55 +0530 Subject: [PATCH 002/274] Feat: Desk Footer Funtionlity Added --- frappe_desk_theme/api.py | 22 ++- .../doctype/desk_theme/desk_theme.json | 45 ++++- .../doctype/desk_theme/desk_theme.py | 22 +++ .../public/css/frappe_desk_theme.css | 145 ++++++++++++++ .../public/js/frappe_desk_theme.js | 183 +++++++++++++++++- .../templates/includes/desk_footer.html | 22 +++ 6 files changed, 434 insertions(+), 5 deletions(-) create mode 100644 frappe_desk_theme/templates/includes/desk_footer.html diff --git a/frappe_desk_theme/api.py b/frappe_desk_theme/api.py index 85bf55c..6e64de7 100644 --- a/frappe_desk_theme/api.py +++ b/frappe_desk_theme/api.py @@ -1,5 +1,25 @@ import frappe +from frappe import _ @frappe.whitelist(allow_guest=True) def get_custom_theme(): - return frappe.get_doc("Desk Theme") \ No newline at end of file + return frappe.get_doc("Desk Theme") + +@frappe.whitelist(allow_guest=True) +def get_footer_html(): + """Get rendered footer HTML template with theme data""" + try: + theme = frappe.get_doc("Desk Theme") + + # Prepare context for template + context = { + 'copyright_text': theme.copyright_text, + 'footer_powered_by': theme.footer_powered_by, + 'sticky_footer': theme.sticky_footer + } + + # Render the template + return frappe.render_template("frappe_desk_theme/templates/includes/desk_footer.html", context) + except Exception as e: + frappe.log_error(f"Error rendering footer template: {str(e)}") + return "" \ No newline at end of file diff --git a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json index 22e5d08..d188dad 100644 --- a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json +++ b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json @@ -74,7 +74,13 @@ "input_border_color", "column_break_pdaj", "input_text_color", - "input_label_color" + "input_label_color", + "footer_tab", + "footer_section", + "copyright_text", + "sticky_footer", + "column_break_footer", + "footer_powered_by" ], "fields": [ { @@ -430,11 +436,44 @@ }, { "depends_on": "eval:doc.hide_app_switcher == 1", - "description": "Select default app when app switcher is hidden. Required when app switcher is hidden.", + "description": "Select default app when app switcher is hidden.", "fieldname": "default_app", "fieldtype": "Select", "label": "Default App", "mandatory_depends_on": "eval:doc.hide_app_switcher == 1" + }, + { + "fieldname": "footer_tab", + "fieldtype": "Tab Break", + "label": "Footer" + }, + { + "fieldname": "footer_section", + "fieldtype": "Section Break", + "label": "Footer Settings" + }, + { + "description": "Copyright text to display in footer", + "fieldname": "copyright_text", + "fieldtype": "Data", + "label": "Copyright Text" + }, + { + "default": "0", + "description": "Makes footer stick to bottom of screen", + "fieldname": "sticky_footer", + "fieldtype": "Check", + "label": "Sticky Footer" + }, + { + "fieldname": "column_break_footer", + "fieldtype": "Column Break" + }, + { + "description": "Custom powered by text for footer", + "fieldname": "footer_powered_by", + "fieldtype": "Small Text", + "label": "Footer \"Powered By\"" } ], "grid_page_length": 50, @@ -442,7 +481,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-06-26 12:58:37.343023", + "modified": "2025-06-27 19:36:22.479301", "modified_by": "Administrator", "module": "Frappe Desk Theme", "name": "Desk Theme", diff --git a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.py b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.py index 6b3b258..8440eca 100644 --- a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.py +++ b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.py @@ -15,6 +15,28 @@ def on_update(self): # Update system settings with the selected default app if self.hide_app_switcher and self.default_app: update_system_default_app(self.default_app) + + # Update website settings with footer information + self.update_website_settings() + + def update_website_settings(self): + """Update Website Settings with copyright and powered by text from Desk Theme""" + try: + website_settings = frappe.get_single("Website Settings") + + # Update copyright text if provided + if self.copyright_text: + website_settings.copyright = self.copyright_text + + # Update footer powered by text if provided + if self.footer_powered_by: + website_settings.footer_powered = self.footer_powered_by + + # Save without triggering permissions check + website_settings.save(ignore_permissions=True) + + except Exception as e: + frappe.log_error(f"Error updating website settings: {str(e)}") @frappe.whitelist() diff --git a/frappe_desk_theme/public/css/frappe_desk_theme.css b/frappe_desk_theme/public/css/frappe_desk_theme.css index ed28e9b..070ff15 100644 --- a/frappe_desk_theme/public/css/frappe_desk_theme.css +++ b/frappe_desk_theme/public/css/frappe_desk_theme.css @@ -374,3 +374,148 @@ div.level-right { } } +/* ======================================== + FOOTER STYLING + ======================================== */ + +/* Ensure main-section takes full height for proper footer positioning */ +.main-section { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +/* Main content area should flex to push footer down */ +.main-section > .container, +.main-section > .page-container { + flex: 1; +} + +/* Main footer container - positioned to align with main content area */ +.desk-footer { + background-color: var(--footer-bg, #f8f9fa); + color: var(--footer-color, #495057); + padding: 15px 20px; + border-top: 1px solid var(--footer-border, #dee2e6); + font-size: 14px; + display: var(--footer-display, flex); + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 10px; + margin-top: auto; + /* Position footer to align with main content area, not spanning full width */ + margin-left: 50px; /* Default collapsed sidebar width */ + transition: margin-left 200ms ease; + /* Ensure footer has consistent height */ + min-height: 50px; + flex-shrink: 0; /* Prevent footer from shrinking */ +} + +/* Adjust footer margin when sidebar is expanded */ +.body-sidebar-container.expanded ~ * .desk-footer, +.body-sidebar-container.expanded + .main-section .desk-footer { + margin-left: var(--left-sidebar-width, 220px); +} + +/* Alternative approach: Position footer within main-section if it exists */ +.main-section .desk-footer { + margin-left: 0 !important; +} + +/* Sticky footer positioning - aligns with content area */ +.desk-footer.sticky { + position: fixed; + bottom: 0; + left: 50px; /* Default collapsed sidebar width */ + right: 0; + z-index: 1000; + box-shadow: 0 -2px 4px rgba(0,0,0,0.1); + margin-left: 0; + transition: left 200ms ease-in-out; + /* Ensure consistent height for sticky footer */ + height: 50px; + box-sizing: border-box; +} + +/* JavaScript will handle dynamic positioning, but keep CSS fallback */ +.body-sidebar-container.expanded ~ * .desk-footer.sticky, +body[data-sidebar="1"] .desk-footer.sticky { + left: 220px; /* Use fixed value instead of CSS variable for better performance */ +} + +/* When footer is sticky, add padding to main section to prevent content overlap */ +.main-section.has-sticky-footer, +body.has-sticky-footer .main-section { + padding-bottom: 60px; /* 50px footer height + 10px buffer */ +} + +/* Fallback: If main-section doesn't exist, add padding to body */ +body.has-sticky-footer:not(:has(.main-section)) { + padding-bottom: 60px; +} + +/* Ensure content doesn't overlap with sticky footer */ +.main-section.has-sticky-footer { + min-height: calc(100vh - 60px); /* Full height minus footer and buffer */ +} + +/* Footer left section - copyright text */ +.desk-footer-left { + display: flex; + align-items: center; + gap: 5px; +} + +/* Footer right section - powered by text */ +.desk-footer-right { + display: flex; + align-items: center; + gap: 5px; + color: var(--footer-powered-color, #6c757d); + font-size: 13px; +} + +/* Footer links styling */ +.desk-footer a { + color: var(--footer-link-color, #007bff); + text-decoration: none; +} + +.desk-footer a:hover { + color: var(--footer-link-hover-color, #0056b3); + text-decoration: underline; +} + +/* Responsive footer for mobile and tablets */ +@media (max-width: 992px) { + /* On mobile/tablet, footer should span full width as sidebar becomes overlay */ + .desk-footer { + margin-left: 0 !important; + } + + .desk-footer.sticky { + left: 0 !important; + } +} + +@media (max-width: 768px) { + .desk-footer { + flex-direction: column; + text-align: center; + padding: 12px 15px; + gap: 8px; + } + + .desk-footer-left, + .desk-footer-right { + justify-content: center; + } + + .main-section.has-sticky-footer, + body.has-sticky-footer .main-section, + body.has-sticky-footer:not(:has(.main-section)) { + padding-bottom: 70px; /* Reduced padding for mobile */ + } +} + diff --git a/frappe_desk_theme/public/js/frappe_desk_theme.js b/frappe_desk_theme/public/js/frappe_desk_theme.js index cfde41c..973512f 100644 --- a/frappe_desk_theme/public/js/frappe_desk_theme.js +++ b/frappe_desk_theme/public/js/frappe_desk_theme.js @@ -10,6 +10,10 @@ class FrappeDeskTheme { // Cache configuration this.cacheKey = 'frappe_desk_theme_cache'; this.cacheTimeout = 24 * 60 * 60 * 1000; // 24 hours in milliseconds + // Footer creation throttling and caching + this.footerCreating = false; + this.footerHtmlCache = null; + this.stickyFooterListenerSetup = false; this.init(); } @@ -168,6 +172,9 @@ class FrappeDeskTheme { */ async refreshTheme() { try { + // Clear footer cache to ensure fresh data + this.footerHtmlCache = null; + await this.loadTheme(); this.applyTheme(); @@ -235,7 +242,8 @@ class FrappeDeskTheme { '--login-title-after-justify', '--login-title-after-margin', '--login-title-after-content', '--login-title-after-color', '--login-box-top', '--login-box-bg-override', '--login-box-border-radius', '--search-bar-display', '--navbar-toggler-border', '--breadcrumb-disabled-color', '--help-nav-link-color', '--help-nav-link-stroke', - '--hide-app-switcher', '--app-switcher-pointer-events' + '--hide-app-switcher', '--app-switcher-pointer-events', '--footer-bg', '--footer-color', '--footer-border', + '--footer-display', '--footer-powered-color', '--footer-link-color', '--footer-link-hover-color' ]; // Remove each CSS variable from document root @@ -276,6 +284,15 @@ class FrappeDeskTheme { root.style.setProperty('--breadcrumb-disabled-color', '#6c757d'); root.style.setProperty('--help-nav-link-color', 'inherit'); root.style.setProperty('--help-nav-link-stroke', 'currentColor'); + + // Footer defaults + root.style.setProperty('--footer-display', 'flex'); + root.style.setProperty('--footer-bg', '#f8f9fa'); + root.style.setProperty('--footer-color', '#495057'); + root.style.setProperty('--footer-border', '#dee2e6'); + root.style.setProperty('--footer-powered-color', '#6c757d'); + root.style.setProperty('--footer-link-color', '#007bff'); + root.style.setProperty('--footer-link-hover-color', '#0056b3'); } /** @@ -477,6 +494,7 @@ class FrappeDeskTheme { this.toggleSearchBar(); this.setDefaultApp(); this.showLoginBox(); + this.createFooter(); } /** @@ -547,6 +565,159 @@ class FrappeDeskTheme { } } + /** + * Create and display footer in desk view using HTML template + * Much more efficient than creating DOM elements dynamically + */ + async createFooter() { + // Don't create footer on login page + if (document.body.classList.contains('login-page') || document.querySelector('#page-login')) { + return; + } + + // Remove existing footer if any + const existingFooter = document.querySelector('#desk-footer'); + if (existingFooter) { + existingFooter.remove(); + // Clean up sticky footer classes + document.body.classList.remove('has-sticky-footer'); + const mainSection = document.querySelector('.main-section'); + if (mainSection) { + mainSection.classList.remove('has-sticky-footer'); + } + } + + // Check if footer should be displayed (basic check to avoid unnecessary API calls) + if (!this.themeData.copyright_text && !this.themeData.footer_powered_by) { + return; + } + + // Throttle footer creation to prevent multiple simultaneous calls + if (this.footerCreating) { + return; + } + this.footerCreating = true; + + try { + let footerHtml = this.footerHtmlCache; + + // Only fetch from server if not cached + if (!footerHtml) { + // Get rendered footer HTML from server + const response = await fetch('/api/method/frappe_desk_theme.api.get_footer_html', { + method: 'GET', + headers: { + 'Accept': 'application/json', + } + }); + + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + + const data = await response.json(); + footerHtml = data?.message || ''; + + // Cache the HTML for subsequent calls + this.footerHtmlCache = footerHtml; + } + + if (footerHtml.trim()) { + // Create a temporary container to hold the HTML + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = footerHtml; + + // Get the footer element from the template + const footerElement = tempDiv.querySelector('#desk-footer'); + if (footerElement) { + // Try to append to main-section first, then fall back to body + const mainSection = document.querySelector('.main-section'); + if (mainSection) { + mainSection.appendChild(footerElement); + if (this.themeData.sticky_footer) { + mainSection.classList.add('has-sticky-footer'); + // Set up sticky footer sidebar toggle listener + this.setupStickyFooterToggle(); + } + } else { + // Fallback to body if main-section doesn't exist + document.body.appendChild(footerElement); + if (this.themeData.sticky_footer) { + document.body.classList.add('has-sticky-footer'); + // Set up sticky footer sidebar toggle listener + this.setupStickyFooterToggle(); + } + } + } + } + } catch (error) { + console.warn('Failed to load footer template:', error); + } finally { + this.footerCreating = false; + } + } + + /** + * Set up dynamic positioning for sticky footer when sidebar toggles + * Ensures footer position updates in real-time with sidebar state + */ + setupStickyFooterToggle() { + // Avoid setting up multiple listeners + if (this.stickyFooterListenerSetup) { + return; + } + this.stickyFooterListenerSetup = true; + + // Function to update sticky footer position + const updateStickyFooterPosition = () => { + const footer = document.querySelector('#desk-footer.sticky'); + if (!footer) return; + + const sidebarContainer = document.querySelector('.body-sidebar-container'); + const isExpanded = sidebarContainer && sidebarContainer.classList.contains('expanded'); + + // Update footer position based on sidebar state + if (isExpanded) { + footer.style.left = '220px'; + } else { + footer.style.left = '50px'; + } + }; + + // Listen for sidebar toggle events + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.type === 'attributes' && + mutation.attributeName === 'class' && + mutation.target.classList.contains('body-sidebar-container')) { + // Delay to ensure CSS transitions complete + setTimeout(updateStickyFooterPosition, 50); + } + }); + }); + + // Observe sidebar container for class changes + const sidebarContainer = document.querySelector('.body-sidebar-container'); + if (sidebarContainer) { + observer.observe(sidebarContainer, { + attributes: true, + attributeFilter: ['class'] + }); + } + + // Also listen for sidebar toggle via click events + document.addEventListener('click', (event) => { + // Check if clicked element or its parent is a sidebar toggle + const isToggle = event.target.closest('.collapse-sidebar-link, .sidebar-toggle, [data-toggle="sidebar"]'); + if (isToggle) { + setTimeout(updateStickyFooterPosition, 200); // Allow time for animation + } + }); + + // Initial position update + updateStickyFooterPosition(); + } + /** * Set up event listeners for dynamic theme updates and DOM changes * Handles real-time theme changes and new element detection @@ -559,8 +730,18 @@ class FrappeDeskTheme { // Listen for DOM changes to apply theme to dynamically added elements // Frappe uses dynamic content loading, so we need to monitor for new elements + let footerTimeout; const observer = new MutationObserver(() => { this.toggleSearchBar(); + + // Debounce footer creation to avoid performance issues + clearTimeout(footerTimeout); + footerTimeout = setTimeout(() => { + // Only create footer if it doesn't exist + if (!document.querySelector('#desk-footer')) { + this.createFooter(); + } + }, 500); // 500ms delay to avoid constant recreation }); // Observe all changes in document body and its children diff --git a/frappe_desk_theme/templates/includes/desk_footer.html b/frappe_desk_theme/templates/includes/desk_footer.html new file mode 100644 index 0000000..d69a978 --- /dev/null +++ b/frappe_desk_theme/templates/includes/desk_footer.html @@ -0,0 +1,22 @@ +{% if copyright_text or footer_powered_by %} + + +{% if sticky_footer %} + +{% endif %} +{% endif %} \ No newline at end of file From e7db6f7e5825bfae939753e6be5004cf02779498 Mon Sep 17 00:00:00 2001 From: Bhushan Barbuddhe Date: Fri, 27 Jun 2025 20:22:02 +0530 Subject: [PATCH 003/274] Fix:Footer Loading Improvement --- .../public/js/frappe_desk_theme.js | 115 ++++++++++++++---- 1 file changed, 90 insertions(+), 25 deletions(-) diff --git a/frappe_desk_theme/public/js/frappe_desk_theme.js b/frappe_desk_theme/public/js/frappe_desk_theme.js index 973512f..bdf8c63 100644 --- a/frappe_desk_theme/public/js/frappe_desk_theme.js +++ b/frappe_desk_theme/public/js/frappe_desk_theme.js @@ -9,10 +9,12 @@ class FrappeDeskTheme { this.themeData = null; // Cache configuration this.cacheKey = 'frappe_desk_theme_cache'; - this.cacheTimeout = 24 * 60 * 60 * 1000; // 24 hours in milliseconds + this.footerCacheStorageKey = 'frappe_desk_theme_footer_cache'; + this.cacheTimeout = 30 * 24 * 60 * 60 * 1000; // 30 days (1 month) in milliseconds // Footer creation throttling and caching this.footerCreating = false; this.footerHtmlCache = null; + this.footerCacheKey = null; // Track what theme data the footer was cached for this.stickyFooterListenerSetup = false; this.init(); } @@ -37,7 +39,7 @@ class FrappeDeskTheme { this.setupEventListeners(); } catch (error) { - // Silent fail in production - apply default theme and show login box + // Production-ready silent fail - apply default theme and show login box this.applyTheme(); this.showLoginBoxFallback(); } @@ -111,7 +113,7 @@ class FrappeDeskTheme { const now = Date.now(); const cacheAge = now - cachedData.timestamp; - return cacheAge < this.cacheTimeout; + return cacheAge < this.cacheTimeout; // 30 days } /** @@ -174,6 +176,7 @@ class FrappeDeskTheme { try { // Clear footer cache to ensure fresh data this.footerHtmlCache = null; + this.footerCacheKey = null; await this.loadTheme(); this.applyTheme(); @@ -183,7 +186,48 @@ class FrappeDeskTheme { detail: { themeData: this.themeData } })); } catch (error) { - console.error('Failed to refresh theme:', error); + // Silent fail - theme refresh errors should not interrupt user experience + } + } + + /** + * Save footer cache to localStorage + */ + saveFooterCache(footerHtml, cacheKey) { + try { + const cacheData = { + html: footerHtml, + key: cacheKey, + timestamp: Date.now() + }; + localStorage.setItem(this.footerCacheStorageKey, JSON.stringify(cacheData)); + } catch (error) { + // localStorage might be full or disabled + } + } + + /** + * Load footer cache from localStorage + */ + loadFooterCache() { + try { + const cached = localStorage.getItem(this.footerCacheStorageKey); + if (!cached) return null; + + const cacheData = JSON.parse(cached); + const now = Date.now(); + const cacheAge = now - cacheData.timestamp; + + // Return cached data if it's still valid (within 30-day timeout) + if (cacheAge < this.cacheTimeout) { + return cacheData; + } else { + // Remove expired cache + localStorage.removeItem(this.footerCacheStorageKey); + return null; + } + } catch (error) { + return null; } } @@ -193,6 +237,10 @@ class FrappeDeskTheme { clearCache() { try { localStorage.removeItem(this.cacheKey); + localStorage.removeItem(this.footerCacheStorageKey); + // Also clear footer cache + this.footerHtmlCache = null; + this.footerCacheKey = null; } catch (error) { // Ignore localStorage errors } @@ -558,10 +606,9 @@ class FrappeDeskTheme { try { // Set the current app to the default app (same as breadcrumbs.js line 83) frappe.app.sidebar.apps_switcher.set_current_app(this.themeData.default_app); - } catch (error) { - // Silent fail if app switcher is not available or app doesn't exist - console.warn('Failed to set default app:', error); - } + } catch (error) { + // Silent fail if app switcher is not available or app doesn't exist + } } } @@ -599,27 +646,45 @@ class FrappeDeskTheme { this.footerCreating = true; try { + // Create a cache key from footer-related theme data + const currentFooterKey = JSON.stringify({ + copyright_text: this.themeData.copyright_text, + footer_powered_by: this.themeData.footer_powered_by, + sticky_footer: this.themeData.sticky_footer + }); + let footerHtml = this.footerHtmlCache; - // Only fetch from server if not cached - if (!footerHtml) { - // Get rendered footer HTML from server - const response = await fetch('/api/method/frappe_desk_theme.api.get_footer_html', { - method: 'GET', - headers: { - 'Accept': 'application/json', + // Check in-memory cache first, then localStorage, then API + if (!footerHtml || this.footerCacheKey !== currentFooterKey) { + // Try to load from localStorage + const cachedFooter = this.loadFooterCache(); + if (cachedFooter && cachedFooter.key === currentFooterKey) { + footerHtml = cachedFooter.html; + this.footerHtmlCache = footerHtml; + this.footerCacheKey = currentFooterKey; + } else { + + // Get rendered footer HTML from server + const response = await fetch('/api/method/frappe_desk_theme.api.get_footer_html', { + method: 'GET', + headers: { + 'Accept': 'application/json', + } + }); + + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); } - }); - if (!response.ok) { - throw new Error(`HTTP error! Status: ${response.status}`); + const data = await response.json(); + footerHtml = data?.message || ''; + + // Cache the HTML and key for subsequent calls (both memory and localStorage) + this.footerHtmlCache = footerHtml; + this.footerCacheKey = currentFooterKey; + this.saveFooterCache(footerHtml, currentFooterKey); } - - const data = await response.json(); - footerHtml = data?.message || ''; - - // Cache the HTML for subsequent calls - this.footerHtmlCache = footerHtml; } if (footerHtml.trim()) { @@ -651,7 +716,7 @@ class FrappeDeskTheme { } } } catch (error) { - console.warn('Failed to load footer template:', error); + // Silent fail - footer is optional, don't show errors to user } finally { this.footerCreating = false; } From 83a1f896f687c4aeeab68dcd44e4aa7c610f267e Mon Sep 17 00:00:00 2001 From: Bhushan Barbuddhe Date: Sat, 12 Jul 2025 11:22:53 +0530 Subject: [PATCH 004/274] fix: Login page image css --- frappe_desk_theme/public/css/frappe_desk_theme.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frappe_desk_theme/public/css/frappe_desk_theme.css b/frappe_desk_theme/public/css/frappe_desk_theme.css index 070ff15..f8db0eb 100644 --- a/frappe_desk_theme/public/css/frappe_desk_theme.css +++ b/frappe_desk_theme/public/css/frappe_desk_theme.css @@ -29,6 +29,10 @@ background-image: var(--login-bg-image, none) !important; background-size: cover; height: 100vh; + background-repeat: no-repeat; + background-position: center center; + height: 100vh; + margin: 0; } /* Login form buttons - primary action buttons with hover states */ From 3dae0bd7c0a86724a2e51ff5d18320aff9f4c62d Mon Sep 17 00:00:00 2001 From: Yash bhargava Date: Mon, 21 Jul 2025 19:31:21 +0530 Subject: [PATCH 005/274] add: carousel support --- frappe_desk_theme/api.py | 8 +- .../doctype/desk_theme/desk_theme.json | 21 ++- .../doctype/desk_theme/desk_theme.py | 18 +++ .../desk_theme_carousel_images/__init__.py | 0 .../desk_theme_carousel_images.json | 33 ++++ .../desk_theme_carousel_images.py | 9 ++ .../public/js/frappe_desk_theme.js | 141 +++++++++++++++++- 7 files changed, 222 insertions(+), 8 deletions(-) create mode 100644 frappe_desk_theme/frappe_desk_theme/doctype/desk_theme_carousel_images/__init__.py create mode 100644 frappe_desk_theme/frappe_desk_theme/doctype/desk_theme_carousel_images/desk_theme_carousel_images.json create mode 100644 frappe_desk_theme/frappe_desk_theme/doctype/desk_theme_carousel_images/desk_theme_carousel_images.py diff --git a/frappe_desk_theme/api.py b/frappe_desk_theme/api.py index 6e64de7..c23cfec 100644 --- a/frappe_desk_theme/api.py +++ b/frappe_desk_theme/api.py @@ -3,7 +3,13 @@ @frappe.whitelist(allow_guest=True) def get_custom_theme(): - return frappe.get_doc("Desk Theme") + theme = frappe.get_doc("Desk Theme") + data = theme.as_dict() + # Add carousel data if present + carousel_data = theme.get_carousel_data() if hasattr(theme, 'get_carousel_data') else None + if carousel_data: + data["carousel"] = carousel_data + return data @frappe.whitelist(allow_guest=True) def get_footer_html(): diff --git a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json index d188dad..971d071 100644 --- a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json +++ b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json @@ -10,6 +10,8 @@ "login_button_background_color", "login_page_button_hover_background_color", "page_background_type", + "allow_manual_navigation", + "carousel_images", "login_page_background_color", "login_page_background_image", "is_app_details_inside_the_box", @@ -152,7 +154,7 @@ "fieldname": "page_background_type", "fieldtype": "Select", "label": "Page Background Type", - "options": "\nColor\nImage" + "options": "\nColor\nImage\nCarousel" }, { "depends_on": "eval:doc.page_background_type == \"Color\"", @@ -474,6 +476,21 @@ "fieldname": "footer_powered_by", "fieldtype": "Small Text", "label": "Footer \"Powered By\"" + }, + { + "depends_on": "eval:doc.page_background_type==\"Carousel\"", + "fieldname": "carousel_images", + "fieldtype": "Table", + "label": "Carousel Images", + "mandatory_depends_on": "eval:doc.page_background_type==\"Carousel\"", + "options": "Desk Theme Carousel Images" + }, + { + "default": "0", + "depends_on": "eval:doc.page_background_type==\"Carousel\"", + "fieldname": "allow_manual_navigation", + "fieldtype": "Check", + "label": "Allow Manual Navigation" } ], "grid_page_length": 50, @@ -481,7 +498,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-06-27 19:36:22.479301", + "modified": "2025-07-21 18:13:14.615451", "modified_by": "Administrator", "module": "Frappe Desk Theme", "name": "Desk Theme", diff --git a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.py b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.py index 8440eca..19296d1 100644 --- a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.py +++ b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.py @@ -11,6 +11,13 @@ def validate(self): if self.hide_app_switcher and not self.default_app: frappe.throw("Default App is required when App Switcher is hidden") + # Carousel validation: if carousel selected, must have at least one image + if self.page_background_type == "Carousel": + if not self.carousel_images or not any(img.image for img in self.carousel_images): + # Fallback: clear page_background_type + self.page_background_type = "" + frappe.msgprint("No carousel images found. Falling back to default background.") + def on_update(self): # Update system settings with the selected default app if self.hide_app_switcher and self.default_app: @@ -38,6 +45,17 @@ def update_website_settings(self): except Exception as e: frappe.log_error(f"Error updating website settings: {str(e)}") + def get_carousel_data(self): + """Return carousel images and config for API""" + if self.page_background_type != "Carousel": + return None + images = [img.image for img in self.carousel_images if img.image] + return { + "images": images, + "manual_navigation": getattr(self, "carousel_manual_navigation", True), + "auto_advance": getattr(self, "carousel_auto_advance", True), + } + @frappe.whitelist() def update_system_default_app(default_app): diff --git a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme_carousel_images/__init__.py b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme_carousel_images/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme_carousel_images/desk_theme_carousel_images.json b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme_carousel_images/desk_theme_carousel_images.json new file mode 100644 index 0000000..408f6da --- /dev/null +++ b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme_carousel_images/desk_theme_carousel_images.json @@ -0,0 +1,33 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2025-07-21 18:09:28.145951", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "image" + ], + "fields": [ + { + "fieldname": "image", + "fieldtype": "Attach Image", + "label": "Image", + "reqd": 1 + } + ], + "grid_page_length": 50, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2025-07-21 18:10:28.202655", + "modified_by": "Administrator", + "module": "Frappe Desk Theme", + "name": "Desk Theme Carousel Images", + "owner": "Administrator", + "permissions": [], + "row_format": "Dynamic", + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} diff --git a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme_carousel_images/desk_theme_carousel_images.py b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme_carousel_images/desk_theme_carousel_images.py new file mode 100644 index 0000000..e41c8c7 --- /dev/null +++ b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme_carousel_images/desk_theme_carousel_images.py @@ -0,0 +1,9 @@ +# Copyright (c) 2025, Dhwani RIS and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class DeskThemeCarouselImages(Document): + pass diff --git a/frappe_desk_theme/public/js/frappe_desk_theme.js b/frappe_desk_theme/public/js/frappe_desk_theme.js index bdf8c63..cc2fc92 100644 --- a/frappe_desk_theme/public/js/frappe_desk_theme.js +++ b/frappe_desk_theme/public/js/frappe_desk_theme.js @@ -359,11 +359,15 @@ class FrappeDeskTheme { this.setDefaultCSSVariables(); // Login page background customization - if (theme.login_page_background_color) { - root.style.setProperty('--login-bg-color', theme.login_page_background_color); - } - if (theme.login_page_background_image) { - root.style.setProperty('--login-bg-image', `url("${theme.login_page_background_image}")`); + if (theme.carousel && theme.carousel.images && theme.carousel.images.length > 0) { + // Skip static background image/color for carousel mode + } else { + if (theme.login_page_background_color) { + root.style.setProperty('--login-bg-color', theme.login_page_background_color); + } + if (theme.login_page_background_image) { + root.style.setProperty('--login-bg-image', `url("${theme.login_page_background_image}")`); + } } // Login box positioning - supports Left, Right, or Default positioning @@ -541,6 +545,11 @@ class FrappeDeskTheme { this.toggleSidebar(); this.toggleSearchBar(); this.setDefaultApp(); + if (this.themeData.carousel && this.themeData.carousel.images && this.themeData.carousel.images.length > 0) { + this.renderLoginCarousel(); + } else { + this.removeLoginCarousel(); + } this.showLoginBox(); this.createFooter(); } @@ -817,6 +826,128 @@ class FrappeDeskTheme { } + renderLoginCarousel() { + // Only on login page + const loginPage = document.querySelector('#page-login'); + if (!loginPage) return; + // Remove any existing carousel + this.removeLoginCarousel(); + const images = this.themeData.carousel.images; + if (!images || images.length === 0) return; + // Create carousel container + const carousel = document.createElement('div'); + carousel.id = 'login-bg-carousel'; + carousel.style.position = 'absolute'; + carousel.style.top = '0'; + carousel.style.left = '0'; + carousel.style.width = '100%'; + carousel.style.height = '100%'; + carousel.style.zIndex = '0'; + carousel.style.overflow = 'hidden'; + carousel.style.pointerEvents = 'none'; + // Add images + images.forEach((src, idx) => { + const img = document.createElement('img'); + img.src = src; + img.className = 'carousel-bg-img'; + img.style.position = 'absolute'; + img.style.top = '0'; + img.style.left = '0'; + img.style.width = '100%'; + img.style.height = '100%'; + img.style.objectFit = 'cover'; + img.style.opacity = idx === 0 ? '1' : '0'; + img.style.transition = 'opacity 0.7s'; + img.style.pointerEvents = 'none'; + carousel.appendChild(img); + }); + // Add navigation if enabled + const manual = this.themeData.carousel.manual_navigation !== false; + if (manual && images.length > 1) { + const left = document.createElement('button'); + left.innerHTML = '←'; + left.className = 'carousel-nav carousel-nav-left'; + left.style.position = 'absolute'; + left.style.top = '50%'; + left.style.left = '20px'; + left.style.transform = 'translateY(-50%)'; + left.style.zIndex = '2'; + left.style.pointerEvents = 'auto'; + left.style.background = 'rgba(0,0,0,0.3)'; + left.style.color = '#fff'; + left.style.border = 'none'; + left.style.fontSize = '2rem'; + left.style.cursor = 'pointer'; + left.style.borderRadius = '50%'; + left.style.width = '40px'; + left.style.height = '40px'; + left.style.display = 'flex'; + left.style.alignItems = 'center'; + left.style.justifyContent = 'center'; + left.addEventListener('click', (e) => { + e.stopPropagation(); + e.preventDefault(); + this.carouselShowImage(this._carouselIndex - 1); + }); + const right = document.createElement('button'); + right.innerHTML = '→'; + right.className = 'carousel-nav carousel-nav-right'; + right.style.position = 'absolute'; + right.style.top = '50%'; + right.style.right = '20px'; + right.style.transform = 'translateY(-50%)'; + right.style.zIndex = '2'; + right.style.pointerEvents = 'auto'; + right.style.background = 'rgba(0,0,0,0.3)'; + right.style.color = '#fff'; + right.style.border = 'none'; + right.style.fontSize = '2rem'; + right.style.cursor = 'pointer'; + right.style.borderRadius = '50%'; + right.style.width = '40px'; + right.style.height = '40px'; + right.style.display = 'flex'; + right.style.alignItems = 'center'; + right.style.justifyContent = 'center'; + right.addEventListener('click', (e) => { + e.stopPropagation(); + e.preventDefault(); + this.carouselShowImage(this._carouselIndex + 1); + }); + carousel.appendChild(left); + carousel.appendChild(right); + } + loginPage.insertBefore(carousel, loginPage.firstChild); + // Carousel state + this._carouselIndex = 0; + this._carouselImages = carousel.querySelectorAll('.carousel-bg-img'); + // Auto-advance + if (this._carouselTimer) clearInterval(this._carouselTimer); + const auto = this.themeData.carousel.auto_advance !== false; + if (auto && images.length > 1) { + this._carouselTimer = setInterval(() => { + this.carouselShowImage(this._carouselIndex + 1); + }, 5000); + } + } + + removeLoginCarousel() { + const existing = document.getElementById('login-bg-carousel'); + if (existing) existing.remove(); + if (this._carouselTimer) clearInterval(this._carouselTimer); + this._carouselImages = null; + this._carouselIndex = 0; + } + + carouselShowImage(idx) { + if (!this._carouselImages) return; + const total = this._carouselImages.length; + idx = (idx + total) % total; + this._carouselImages.forEach((img, i) => { + img.style.opacity = i === idx ? '1' : '0'; + }); + this._carouselIndex = idx; + } } // Initialize theme system when DOM is ready From 1eb51561a031844bc110e90a025dc7c68857cb60 Mon Sep 17 00:00:00 2001 From: Yash bhargava Date: Mon, 21 Jul 2025 19:33:00 +0530 Subject: [PATCH 006/274] fix: make carousel images public by default --- .../desk_theme_carousel_images/desk_theme_carousel_images.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme_carousel_images/desk_theme_carousel_images.json b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme_carousel_images/desk_theme_carousel_images.json index 408f6da..d9081de 100644 --- a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme_carousel_images/desk_theme_carousel_images.json +++ b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme_carousel_images/desk_theme_carousel_images.json @@ -13,6 +13,7 @@ "fieldname": "image", "fieldtype": "Attach Image", "label": "Image", + "make_attachment_public": 1, "reqd": 1 } ], @@ -20,7 +21,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-07-21 18:10:28.202655", + "modified": "2025-07-21 19:32:26.277666", "modified_by": "Administrator", "module": "Frappe Desk Theme", "name": "Desk Theme Carousel Images", From 249485435bd9385d5b5d075ec872ec20845d6857 Mon Sep 17 00:00:00 2001 From: Yash bhargava Date: Mon, 21 Jul 2025 19:43:43 +0530 Subject: [PATCH 007/274] fix: manual navigation --- .../frappe_desk_theme/doctype/desk_theme/desk_theme.py | 2 +- frappe_desk_theme/public/js/frappe_desk_theme.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.py b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.py index 19296d1..c614776 100644 --- a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.py +++ b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.py @@ -52,7 +52,7 @@ def get_carousel_data(self): images = [img.image for img in self.carousel_images if img.image] return { "images": images, - "manual_navigation": getattr(self, "carousel_manual_navigation", True), + "manual_navigation": getattr(self, "allow_manual_navigation", True), "auto_advance": getattr(self, "carousel_auto_advance", True), } diff --git a/frappe_desk_theme/public/js/frappe_desk_theme.js b/frappe_desk_theme/public/js/frappe_desk_theme.js index cc2fc92..a74d0b2 100644 --- a/frappe_desk_theme/public/js/frappe_desk_theme.js +++ b/frappe_desk_theme/public/js/frappe_desk_theme.js @@ -862,7 +862,7 @@ class FrappeDeskTheme { carousel.appendChild(img); }); // Add navigation if enabled - const manual = this.themeData.carousel.manual_navigation !== false; + const manual = !!this.themeData.carousel.manual_navigation; if (manual && images.length > 1) { const left = document.createElement('button'); left.innerHTML = '←'; From 8e8c1f8d6b82598a0f1fa68708905e26db4be8f0 Mon Sep 17 00:00:00 2001 From: Yash bhargava Date: Mon, 21 Jul 2025 20:25:06 +0530 Subject: [PATCH 008/274] add: 5mb file limit --- .../desk_theme_carousel_images.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme_carousel_images/desk_theme_carousel_images.py b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme_carousel_images/desk_theme_carousel_images.py index e41c8c7..d40eb82 100644 --- a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme_carousel_images/desk_theme_carousel_images.py +++ b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme_carousel_images/desk_theme_carousel_images.py @@ -6,4 +6,9 @@ class DeskThemeCarouselImages(Document): - pass + def validate(self): + # Validate image size (max 5 MB) + if self.image: + file_doc = frappe.get_doc('File', {'file_url': self.image}) + if file_doc.file_size > 5 * 1024 * 1024: + frappe.throw('Carousel image size must be 5 MB or less.') From 6408c4965e7cce08e8ad7ab155c20f9e31d97fc9 Mon Sep 17 00:00:00 2001 From: Yash bhargava Date: Tue, 22 Jul 2025 16:56:39 +0530 Subject: [PATCH 009/274] add: css variables --- .../public/css/frappe_desk_theme.css | 42 ++++ .../public/js/frappe_desk_theme.js | 234 ++++++++++-------- 2 files changed, 166 insertions(+), 110 deletions(-) diff --git a/frappe_desk_theme/public/css/frappe_desk_theme.css b/frappe_desk_theme/public/css/frappe_desk_theme.css index f8db0eb..6d1f099 100644 --- a/frappe_desk_theme/public/css/frappe_desk_theme.css +++ b/frappe_desk_theme/public/css/frappe_desk_theme.css @@ -35,6 +35,21 @@ margin: 0; } +#page-login::before { + content: ''; + position: absolute; + inset: 0; + z-index: 0; + pointer-events: none; + background-image: var(--login-bg-carousel-image, var(--login-bg-image, none)); + background-size: cover; + background-repeat: no-repeat; + background-position: center center; + opacity: var(--carousel-fade-opacity, 1); + transition: opacity 0.7s; +} + + /* Login form buttons - primary action buttons with hover states */ .btn-primary.btn-login, .btn-primary.btn-forgot { @@ -523,3 +538,30 @@ body.has-sticky-footer:not(:has(.main-section)) { } } +.carousel-nav { + background: var(--carousel-nav-bg, rgba(0,0,0,0.3)); + color: var(--carousel-nav-color, #fff); + border: none; + font-size: var(--carousel-nav-size, 2rem); + cursor: pointer; + border-radius: var(--carousel-nav-radius, 50%); + width: var(--carousel-nav-size, 2rem); + height: var(--carousel-nav-size, 2rem); + display: flex; + align-items: center; + justify-content: center; + position: absolute; + top: 50%; + transform: translateY(-50%); + z-index: var(--carousel-nav-z, 2); + pointer-events: auto; +} + +.carousel-nav-left { + left: 20px; +} + +.carousel-nav-right { + right: 20px; +} + diff --git a/frappe_desk_theme/public/js/frappe_desk_theme.js b/frappe_desk_theme/public/js/frappe_desk_theme.js index a74d0b2..a373ccc 100644 --- a/frappe_desk_theme/public/js/frappe_desk_theme.js +++ b/frappe_desk_theme/public/js/frappe_desk_theme.js @@ -291,7 +291,9 @@ class FrappeDeskTheme { '--login-box-top', '--login-box-bg-override', '--login-box-border-radius', '--search-bar-display', '--navbar-toggler-border', '--breadcrumb-disabled-color', '--help-nav-link-color', '--help-nav-link-stroke', '--hide-app-switcher', '--app-switcher-pointer-events', '--footer-bg', '--footer-color', '--footer-border', - '--footer-display', '--footer-powered-color', '--footer-link-color', '--footer-link-hover-color' + '--footer-display', '--footer-powered-color', '--footer-link-color', '--footer-link-hover-color', + '--carousel-nav-bg', '--carousel-nav-color', '--carousel-nav-size', '--carousel-nav-radius', '--carousel-nav-z', + '--carousel-fade-opacity' ]; // Remove each CSS variable from document root @@ -341,6 +343,15 @@ class FrappeDeskTheme { root.style.setProperty('--footer-powered-color', '#6c757d'); root.style.setProperty('--footer-link-color', '#007bff'); root.style.setProperty('--footer-link-hover-color', '#0056b3'); + + // Carousel nav button defaults + root.style.setProperty('--carousel-nav-bg', 'rgba(0,0,0,0.3)'); + root.style.setProperty('--carousel-nav-color', '#fff'); + root.style.setProperty('--carousel-nav-size', '2rem'); + root.style.setProperty('--carousel-nav-radius', '50%'); + root.style.setProperty('--carousel-nav-z', '2'); + // Carousel fade default + root.style.setProperty('--carousel-fade-opacity', '1'); } /** @@ -534,6 +545,23 @@ class FrappeDeskTheme { if (theme.hide_side_bar !== undefined) { root.style.setProperty('--sidebar-expanded', theme.hide_side_bar === 0 ? 'expanded' : ''); } + + // Carousel nav button theming + if (theme.carousel_nav_bg) { + root.style.setProperty('--carousel-nav-bg', theme.carousel_nav_bg); + } + if (theme.carousel_nav_color) { + root.style.setProperty('--carousel-nav-color', theme.carousel_nav_color); + } + if (theme.carousel_nav_size) { + root.style.setProperty('--carousel-nav-size', theme.carousel_nav_size); + } + if (theme.carousel_nav_radius) { + root.style.setProperty('--carousel-nav-radius', theme.carousel_nav_radius); + } + if (theme.carousel_nav_z) { + root.style.setProperty('--carousel-nav-z', theme.carousel_nav_z); + } } /** @@ -826,127 +854,113 @@ class FrappeDeskTheme { } + // Navigation buttons + + ensureButton(loginPage, images, id, html, onClick) { + const manual = !!this.themeData.carousel.manual_navigation; + let btn = document.getElementById(id); + if (!manual || images.length <= 1) { + if (btn) btn.remove(); + return null; + } + if (!btn) { + btn = document.createElement('button'); + btn.id = id; + btn.className = `carousel-nav ${id === 'carousel-nav-left' ? 'carousel-nav-left' : 'carousel-nav-right'}`; + btn.innerHTML = html; + btn.addEventListener('click', onClick); + loginPage.appendChild(btn); + } + return btn; + }; + renderLoginCarousel() { - // Only on login page const loginPage = document.querySelector('#page-login'); if (!loginPage) return; - // Remove any existing carousel - this.removeLoginCarousel(); + const root = document.documentElement; const images = this.themeData.carousel.images; if (!images || images.length === 0) return; - // Create carousel container - const carousel = document.createElement('div'); - carousel.id = 'login-bg-carousel'; - carousel.style.position = 'absolute'; - carousel.style.top = '0'; - carousel.style.left = '0'; - carousel.style.width = '100%'; - carousel.style.height = '100%'; - carousel.style.zIndex = '0'; - carousel.style.overflow = 'hidden'; - carousel.style.pointerEvents = 'none'; - // Add images - images.forEach((src, idx) => { - const img = document.createElement('img'); - img.src = src; - img.className = 'carousel-bg-img'; - img.style.position = 'absolute'; - img.style.top = '0'; - img.style.left = '0'; - img.style.width = '100%'; - img.style.height = '100%'; - img.style.objectFit = 'cover'; - img.style.opacity = idx === 0 ? '1' : '0'; - img.style.transition = 'opacity 0.7s'; - img.style.pointerEvents = 'none'; - carousel.appendChild(img); - }); - // Add navigation if enabled - const manual = !!this.themeData.carousel.manual_navigation; - if (manual && images.length > 1) { - const left = document.createElement('button'); - left.innerHTML = '←'; - left.className = 'carousel-nav carousel-nav-left'; - left.style.position = 'absolute'; - left.style.top = '50%'; - left.style.left = '20px'; - left.style.transform = 'translateY(-50%)'; - left.style.zIndex = '2'; - left.style.pointerEvents = 'auto'; - left.style.background = 'rgba(0,0,0,0.3)'; - left.style.color = '#fff'; - left.style.border = 'none'; - left.style.fontSize = '2rem'; - left.style.cursor = 'pointer'; - left.style.borderRadius = '50%'; - left.style.width = '40px'; - left.style.height = '40px'; - left.style.display = 'flex'; - left.style.alignItems = 'center'; - left.style.justifyContent = 'center'; - left.addEventListener('click', (e) => { - e.stopPropagation(); - e.preventDefault(); - this.carouselShowImage(this._carouselIndex - 1); - }); - const right = document.createElement('button'); - right.innerHTML = '→'; - right.className = 'carousel-nav carousel-nav-right'; - right.style.position = 'absolute'; - right.style.top = '50%'; - right.style.right = '20px'; - right.style.transform = 'translateY(-50%)'; - right.style.zIndex = '2'; - right.style.pointerEvents = 'auto'; - right.style.background = 'rgba(0,0,0,0.3)'; - right.style.color = '#fff'; - right.style.border = 'none'; - right.style.fontSize = '2rem'; - right.style.cursor = 'pointer'; - right.style.borderRadius = '50%'; - right.style.width = '40px'; - right.style.height = '40px'; - right.style.display = 'flex'; - right.style.alignItems = 'center'; - right.style.justifyContent = 'center'; - right.addEventListener('click', (e) => { - e.stopPropagation(); - e.preventDefault(); - this.carouselShowImage(this._carouselIndex + 1); - }); - carousel.appendChild(left); - carousel.appendChild(right); + + // Set initial state and background + if (typeof this._carouselIndex !== 'number' || this._carouselIndex >= images.length) { + this._carouselIndex = 0; } - loginPage.insertBefore(carousel, loginPage.firstChild); - // Carousel state - this._carouselIndex = 0; - this._carouselImages = carousel.querySelectorAll('.carousel-bg-img'); - // Auto-advance - if (this._carouselTimer) clearInterval(this._carouselTimer); - const auto = this.themeData.carousel.auto_advance !== false; - if (auto && images.length > 1) { - this._carouselTimer = setInterval(() => { - this.carouselShowImage(this._carouselIndex + 1); + root.style.setProperty('--login-bg-carousel-image', `url("${images[this._carouselIndex]}")`); + + // Remove any previous timer + if (this._carouselTimer) { + clearTimeout(this._carouselTimer); + this._carouselTimer = null; + } + + + this.ensureButton(loginPage, images,'carousel-nav-left', '←', (e) => { + e.stopPropagation(); e.preventDefault(); + if (this._carouselTimer) { + clearTimeout(this._carouselTimer); + this._carouselTimer = null; + } + this.carouselShowImage(this._carouselIndex - 1, images, root, -1); + }); + this.ensureButton(loginPage, images,'carousel-nav-right', '→', (e) => { + e.stopPropagation(); e.preventDefault(); + if (this._carouselTimer) { + clearTimeout(this._carouselTimer); + this._carouselTimer = null; + } + this.carouselShowImage(this._carouselIndex + 1, images, root, 1); + }); + + // Auto-advance: handled in carouselShowImage after animation + if (this.themeData.carousel.auto_advance !== false && images.length > 1 && !this._carouselTimer) { + this._carouselTimer = setTimeout(() => { + this._carouselTimer = null; + this.carouselShowImage(this._carouselIndex + 1, images, root, 1); }, 5000); } } + - removeLoginCarousel() { - const existing = document.getElementById('login-bg-carousel'); - if (existing) existing.remove(); - if (this._carouselTimer) clearInterval(this._carouselTimer); - this._carouselImages = null; - this._carouselIndex = 0; + carouselShowImage(idx, images, root, direction = 1) { + const total = images.length; + idx = (idx + total) % total; + if (idx === this._carouselIndex || this._carouselSliding) return; + + this._carouselSliding = true; + // Fade out + root.style.setProperty('--carousel-fade-opacity', '0'); + setTimeout(() => { + root.style.setProperty('--login-bg-carousel-image', `url("${images[idx]}")`); + root.style.setProperty('--carousel-fade-opacity', '1'); + this._carouselIndex = idx; + this._carouselSliding = false; + // Auto-advance + const auto = this.themeData.carousel.auto_advance !== false; + if (auto && images.length > 1 && !this._carouselTimer) { + this._carouselTimer = setTimeout(() => { + this._carouselTimer = null; + this.carouselShowImage(this._carouselIndex + 1, images, root, 1); + }, 5000); + } + }, 400); } + + + - carouselShowImage(idx) { - if (!this._carouselImages) return; - const total = this._carouselImages.length; - idx = (idx + total) % total; - this._carouselImages.forEach((img, i) => { - img.style.opacity = i === idx ? '1' : '0'; - }); - this._carouselIndex = idx; + removeLoginCarousel() { + // Remove navigation buttons if present + const left = document.getElementById('carousel-nav-left'); + const right = document.getElementById('carousel-nav-right'); + if (left) left.remove(); + if (right) right.remove(); + if (this._carouselTimer) { + clearTimeout(this._carouselTimer); + this._carouselTimer = null; + } + // Remove the CSS variable + document.documentElement.style.removeProperty('--login-bg-carousel-image'); + this._carouselIndex = 0; } } From 506cdfbca7db596b2bd6de55feabeb961f961061 Mon Sep 17 00:00:00 2001 From: Yash bhargava Date: Tue, 22 Jul 2025 17:10:26 +0530 Subject: [PATCH 010/274] add: 1 mb file size limit --- .../desk_theme_carousel_images/desk_theme_carousel_images.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme_carousel_images/desk_theme_carousel_images.py b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme_carousel_images/desk_theme_carousel_images.py index d40eb82..3db7109 100644 --- a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme_carousel_images/desk_theme_carousel_images.py +++ b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme_carousel_images/desk_theme_carousel_images.py @@ -10,5 +10,5 @@ def validate(self): # Validate image size (max 5 MB) if self.image: file_doc = frappe.get_doc('File', {'file_url': self.image}) - if file_doc.file_size > 5 * 1024 * 1024: - frappe.throw('Carousel image size must be 5 MB or less.') + if file_doc.file_size > 1 * 1024 * 1024: + frappe.throw('Carousel image size must be 1 MB or less.') From 05592f1316af516968f490e7f381710852f1be54 Mon Sep 17 00:00:00 2001 From: Yash bhargava Date: Tue, 22 Jul 2025 19:28:49 +0530 Subject: [PATCH 011/274] fix: remove redundant theme properties --- .../public/js/frappe_desk_theme.js | 26 +------------------ 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/frappe_desk_theme/public/js/frappe_desk_theme.js b/frappe_desk_theme/public/js/frappe_desk_theme.js index a373ccc..76b4e13 100644 --- a/frappe_desk_theme/public/js/frappe_desk_theme.js +++ b/frappe_desk_theme/public/js/frappe_desk_theme.js @@ -292,8 +292,7 @@ class FrappeDeskTheme { '--navbar-toggler-border', '--breadcrumb-disabled-color', '--help-nav-link-color', '--help-nav-link-stroke', '--hide-app-switcher', '--app-switcher-pointer-events', '--footer-bg', '--footer-color', '--footer-border', '--footer-display', '--footer-powered-color', '--footer-link-color', '--footer-link-hover-color', - '--carousel-nav-bg', '--carousel-nav-color', '--carousel-nav-size', '--carousel-nav-radius', '--carousel-nav-z', - '--carousel-fade-opacity' + '--carousel-fade-opacity', '--login-bg-carousel-image' ]; // Remove each CSS variable from document root @@ -344,12 +343,6 @@ class FrappeDeskTheme { root.style.setProperty('--footer-link-color', '#007bff'); root.style.setProperty('--footer-link-hover-color', '#0056b3'); - // Carousel nav button defaults - root.style.setProperty('--carousel-nav-bg', 'rgba(0,0,0,0.3)'); - root.style.setProperty('--carousel-nav-color', '#fff'); - root.style.setProperty('--carousel-nav-size', '2rem'); - root.style.setProperty('--carousel-nav-radius', '50%'); - root.style.setProperty('--carousel-nav-z', '2'); // Carousel fade default root.style.setProperty('--carousel-fade-opacity', '1'); } @@ -545,23 +538,6 @@ class FrappeDeskTheme { if (theme.hide_side_bar !== undefined) { root.style.setProperty('--sidebar-expanded', theme.hide_side_bar === 0 ? 'expanded' : ''); } - - // Carousel nav button theming - if (theme.carousel_nav_bg) { - root.style.setProperty('--carousel-nav-bg', theme.carousel_nav_bg); - } - if (theme.carousel_nav_color) { - root.style.setProperty('--carousel-nav-color', theme.carousel_nav_color); - } - if (theme.carousel_nav_size) { - root.style.setProperty('--carousel-nav-size', theme.carousel_nav_size); - } - if (theme.carousel_nav_radius) { - root.style.setProperty('--carousel-nav-radius', theme.carousel_nav_radius); - } - if (theme.carousel_nav_z) { - root.style.setProperty('--carousel-nav-z', theme.carousel_nav_z); - } } /** From 743d74b7cd62fb8b08fc20ebd41793bed538c9b0 Mon Sep 17 00:00:00 2001 From: Bhushan Barbuddhe Date: Sun, 3 Aug 2025 13:44:14 +0530 Subject: [PATCH 012/274] add: footer background and text color customization options --- .../doctype/desk_theme/desk_theme.json | 18 +++++++++++++++--- .../desk_theme_carousel_images.json | 2 +- .../public/js/frappe_desk_theme.js | 9 +++++++++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json index 971d071..73fc34e 100644 --- a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json +++ b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json @@ -80,9 +80,11 @@ "footer_tab", "footer_section", "copyright_text", + "footer_background_color", "sticky_footer", "column_break_footer", - "footer_powered_by" + "footer_powered_by", + "footer_text_color" ], "fields": [ { @@ -474,7 +476,7 @@ { "description": "Custom powered by text for footer", "fieldname": "footer_powered_by", - "fieldtype": "Small Text", + "fieldtype": "Data", "label": "Footer \"Powered By\"" }, { @@ -491,6 +493,16 @@ "fieldname": "allow_manual_navigation", "fieldtype": "Check", "label": "Allow Manual Navigation" + }, + { + "fieldname": "footer_background_color", + "fieldtype": "Color", + "label": "Background Color" + }, + { + "fieldname": "footer_text_color", + "fieldtype": "Color", + "label": "Text Color" } ], "grid_page_length": 50, @@ -498,7 +510,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-07-21 18:13:14.615451", + "modified": "2025-08-03 13:37:21.116029", "modified_by": "Administrator", "module": "Frappe Desk Theme", "name": "Desk Theme", diff --git a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme_carousel_images/desk_theme_carousel_images.json b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme_carousel_images/desk_theme_carousel_images.json index d9081de..8a3051e 100644 --- a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme_carousel_images/desk_theme_carousel_images.json +++ b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme_carousel_images/desk_theme_carousel_images.json @@ -21,7 +21,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-07-21 19:32:26.277666", + "modified": "2025-08-03 13:15:22.432727", "modified_by": "Administrator", "module": "Frappe Desk Theme", "name": "Desk Theme Carousel Images", diff --git a/frappe_desk_theme/public/js/frappe_desk_theme.js b/frappe_desk_theme/public/js/frappe_desk_theme.js index 76b4e13..bb885ba 100644 --- a/frappe_desk_theme/public/js/frappe_desk_theme.js +++ b/frappe_desk_theme/public/js/frappe_desk_theme.js @@ -534,6 +534,15 @@ class FrappeDeskTheme { root.style.setProperty('--widget-color', theme.number_card_text_color); } + // Footer styling + if (theme.footer_background_color) { + root.style.setProperty('--footer-bg', theme.footer_background_color); + } + if (theme.footer_text_color) { + root.style.setProperty('--footer-color', theme.footer_text_color); + root.style.setProperty('--footer-powered-color', theme.footer_text_color); + } + // Sidebar visibility control if (theme.hide_side_bar !== undefined) { root.style.setProperty('--sidebar-expanded', theme.hide_side_bar === 0 ? 'expanded' : ''); From e719bb272cb33eff0652e042b0ad3b3fe8ba5fff Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 6 Aug 2025 12:55:35 +0530 Subject: [PATCH 013/274] Update .pre-commit-config.yaml From df5bcfdf357dd7995558a009e1e8a68e9b6b6e40 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 6 Aug 2025 12:55:36 +0530 Subject: [PATCH 014/274] Create .github/workflows/ci.yml --- .github/workflows/ci.yml | 329 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 329 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..65adb57 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,329 @@ +name: CI + +on: + push: + branches: [ main, develop, master ] + pull_request: + branches: [ main, develop, master ] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.event_name }}-${{ github.event.number || github.sha }} + cancel-in-progress: true + +jobs: + commit-lint: + name: 'Semantic Commits' + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 200 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + check-latest: true + + - name: Check commit titles + run: | + npm install @commitlint/cli @commitlint/config-conventional + npx commitlint --verbose --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} + + semgrep: + name: 'Semgrep Security Rules' + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + cache: pip + + - name: Download Semgrep rules + run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules + + - name: Run Semgrep rules + run: | + pip install semgrep + semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness + + dependency-vulnerability: + name: 'Vulnerable Dependency Check' + runs-on: ubuntu-latest + + steps: + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Cache pip + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + + - name: Install and run pip-audit + run: | + pip install pip-audit + cd ${GITHUB_WORKSPACE} + pip-audit --desc on --ignore-vuln PYSEC-2023-312 . + + pre-commit-checks: + name: 'Pre-Commit Hooks' + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + cache: pip + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: 'npm' + + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip + pip install pre-commit ruff black + + - name: Install Node.js dependencies + run: | + npm ci + npm install -g prettier eslint + + - name: Check for pre-commit config + run: | + if [ ! -f .pre-commit-config.yaml ]; then + echo "Pre-commit config not found. Downloading..." + curl -o .pre-commit-config.yaml https://raw.githubusercontent.com/dhwani-ris/frappe-pre-commit/main/examples/.pre-commit-config.yaml + else + echo "Pre-commit config already exists." + fi + + - name: Install pre-commit hooks + run: pre-commit install + + - name: Run pre-commit hooks + run: pre-commit run --all-files + + lint-and-format: + name: 'Lint and Format Check' + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: 'npm' + + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip + pip install ruff black + + - name: Install Node.js dependencies + run: npm ci + + - name: Run Python linting (Ruff) + run: | + ruff check . + ruff format --check . + + - name: Run JavaScript linting (ESLint) + run: | + npm run lint || echo "No lint script found, skipping ESLint" + + - name: Run JavaScript formatting (Prettier) + run: | + npm run format:check || echo "No format:check script found, skipping Prettier" + + test: + name: 'Test' + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8", "3.11", "3.13"] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e ".[dev]" || pip install -r requirements.txt || echo "No requirements found" + + - name: Run tests + run: | + pytest --cov=. --cov-report=xml || echo "No tests found or pytest not configured" + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + file: ./coverage.xml + fail_ci_if_error: false + if: always() + + security: + name: 'Security Check' + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install security tools + run: | + python -m pip install --upgrade pip + pip install bandit safety + + - name: Run security checks + run: | + bandit -r . -f json -o bandit-report.json || true + safety check --json --output safety-report.json || true + + - name: Upload security reports + uses: actions/upload-artifact@v4 + with: + name: security-reports + path: | + bandit-report.json + safety-report.json + + dependency-update: + name: 'Dependency Update Check' + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: 'npm' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pip-audit + npm ci + + - name: Check for outdated Python dependencies + run: | + pip list --outdated --format=freeze | grep -v '^\-e' | cut -d = -f 1 | xargs -n1 pip show || echo "No outdated Python packages found" + + - name: Check for outdated Node.js dependencies + run: npm outdated || echo "No outdated Node.js packages found" + + - name: Run pip-audit + run: | + pip-audit --format json --output pip-audit-report.json || true + + - name: Upload audit reports + uses: actions/upload-artifact@v4 + with: + name: audit-reports + path: pip-audit-report.json + + build: + name: 'Build Check' + runs-on: ubuntu-latest + needs: [lint-and-format, test] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: 'npm' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + npm ci + + - name: Build Python package + run: | + python -m build || echo "No Python package to build" + + - name: Build frontend assets + run: | + npm run build || echo "No build script found" + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: build-artifacts + path: | + dist/ + build/ + if-no-files-found: ignore From e0b4bbbc5c287d9b6f3f3313a60a5b2a6bb91de3 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 6 Aug 2025 12:55:37 +0530 Subject: [PATCH 015/274] Create .github/workflows/linters.yml --- .github/workflows/linters.yml | 106 ++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 .github/workflows/linters.yml diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml new file mode 100644 index 0000000..f32d952 --- /dev/null +++ b/.github/workflows/linters.yml @@ -0,0 +1,106 @@ +# When updating this file, please also update the linter_workflow_template in frappe/utils/boilerplate.py +name: Linters + +on: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: commitcheck-frappe-${{ github.event_name }}-${{ github.event.number }} + cancel-in-progress: true + +jobs: + commit-lint: + name: 'Semantic Commits' + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 200 + - uses: actions/setup-node@v4 + with: + node-version: 22 + check-latest: true + + - name: Check commit titles + run: | + npm install @commitlint/cli @commitlint/config-conventional + npx commitlint --verbose --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} + + linter: + name: 'Semgrep Rules' + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.13' + cache: pip + + - name: Download Semgrep rules + run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules + + - name: Run Semgrep rules + run: | + pip install semgrep + semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness + + deps-vulnerable-check: + name: 'Vulnerable Dependency Check' + runs-on: ubuntu-latest + + steps: + - uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - uses: actions/checkout@v4 + + - name: Cache pip + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + + - name: Install and run pip-audit + run: | + pip install pip-audit + cd ${GITHUB_WORKSPACE} + pip-audit --desc on --ignore-vuln PYSEC-2023-312 . + + precommit: + name: 'Pre-Commit' + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.13' + cache: pip + - uses: pre-commit/action@v3.0.1 + - name: Check for pre-commit config + run: | + if [ ! -f .pre-commit-config.yaml ]; then + echo "Pre-commit config not found. Downloading..." + curl -o .pre-commit-config.yaml https://raw.githubusercontent.com/dhwani-ris/frappe-pre-commit/main/examples/.pre-commit-config.yaml + else + echo "Pre-commit config already exists." + fi + + - name: Install pre-commit + run: pip install pre-commit + + - name: Run pre-commit + run: pre-commit run --all-files From 473d14cc007fa1c457bffc7442c38979259a6aa9 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 7 Aug 2025 09:44:47 +0530 Subject: [PATCH 016/274] Update .pre-commit-config.yaml From ad8e6965aecc63ade71cc6e03c8afdba9b7146fe Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 7 Aug 2025 09:44:48 +0530 Subject: [PATCH 017/274] Update .github/workflows/ci.yml From 6b6ab01e5ac9ddee92ca7bc33f249681a1a99f6d Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 7 Aug 2025 09:44:50 +0530 Subject: [PATCH 018/274] Update .github/workflows/linters.yml From 8d2b307ea5d69a6888ff766b319869b93044f94e Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 7 Aug 2025 09:44:51 +0530 Subject: [PATCH 019/274] Create .github/workflows/label-base-on-title.yml --- .github/workflows/label-base-on-title.yml | 30 +++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/label-base-on-title.yml diff --git a/.github/workflows/label-base-on-title.yml b/.github/workflows/label-base-on-title.yml new file mode 100644 index 0000000..4e811ed --- /dev/null +++ b/.github/workflows/label-base-on-title.yml @@ -0,0 +1,30 @@ +name: "Auto-label PRs based on title" + +on: + pull_request_target: + types: [opened, reopened] + +jobs: + add-label-if-prefix-matches: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - name: Check PR title and add label if it matches prefixes + uses: actions/github-script@v7 + continue-on-error: true + with: + script: | + const title = context.payload.pull_request.title.toLowerCase(); + const prefixes = ['chore', 'ci', 'style', 'test', 'refactor']; + + // Check if the PR title starts with any of the prefixes + if (prefixes.some(prefix => title.startsWith(prefix))) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + labels: ['skip-release-notes'] + }); + } From f4c94b8fdd0778e7c2ddeacfedd320c94e2b1733 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 7 Aug 2025 09:44:53 +0530 Subject: [PATCH 020/274] Create .github/workflows/labeller.yml --- .github/workflows/labeller.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/workflows/labeller.yml diff --git a/.github/workflows/labeller.yml b/.github/workflows/labeller.yml new file mode 100644 index 0000000..39672c3 --- /dev/null +++ b/.github/workflows/labeller.yml @@ -0,0 +1,18 @@ +name: "Pull Request Labeler" +on: + pull_request_target: + types: [opened, reopened] + +jobs: + triage: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - name: Clone + uses: actions/checkout@v4 + + - uses: actions/labeler@v5 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" From 2f88d3533f4669d02826031676fa64daae13f919 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 7 Aug 2025 10:05:49 +0530 Subject: [PATCH 021/274] Update .pre-commit-config.yaml From 564f0138dba1ac76ffe3a15c61f77ed9441ce638 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 7 Aug 2025 10:05:51 +0530 Subject: [PATCH 022/274] Update .github/workflows/ci.yml From fae130e83d20369f5153a018f687e5151f43bad5 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 7 Aug 2025 10:05:53 +0530 Subject: [PATCH 023/274] Update .github/workflows/linters.yml From b5b038c77bb983253b6fdcae959603731f423f51 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 7 Aug 2025 10:05:54 +0530 Subject: [PATCH 024/274] Update .github/workflows/label-base-on-title.yml From ba33998449906ec7e5a777cd82887e19bbdd9577 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 7 Aug 2025 10:05:56 +0530 Subject: [PATCH 025/274] Update .github/workflows/labeller.yml --- .github/workflows/labeller.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/labeller.yml b/.github/workflows/labeller.yml index 39672c3..37e8ede 100644 --- a/.github/workflows/labeller.yml +++ b/.github/workflows/labeller.yml @@ -15,4 +15,4 @@ jobs: - uses: actions/labeler@v5 with: - repo-token: "${{ secrets.GITHUB_TOKEN }}" + repo-token: "${{ secrets.DHWANI_FRAPPE_TOKEN }}" From e517069321fb1df2aadb859464321d8490ad227a Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 7 Aug 2025 14:21:29 +0530 Subject: [PATCH 026/274] Update .pre-commit-config.yaml --- .pre-commit-config.yaml | 44 +++++++++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 18828e4..23dd014 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,3 +1,6 @@ +# Copy this to your project root as .pre-commit-config.yaml +# IMPORTANT: First install the package: pip install frappe-pre-commit + exclude: 'node_modules|.git' default_stages: [pre-commit] fail_fast: false @@ -8,14 +11,17 @@ repos: rev: v5.0.0 hooks: - id: trailing-whitespace - files: "frappe_desk_theme.*" + files: "frappe.*" exclude: ".*json$|.*txt$|.*csv|.*md|.*svg" + - id: no-commit-to-branch + args: ['--branch', 'develop'] - id: check-merge-conflict - id: check-ast - id: check-json - id: check-toml - id: check-yaml - id: debug-statements + exclude: ^frappe/tests/classes/context_managers\.py$ - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.8.1 @@ -38,11 +44,13 @@ repos: # Ignore any files that might contain jinja / bundles exclude: | (?x)^( - frappe_desk_theme/public/dist/.*| + frappe/public/dist/.*| .*node_modules.*| .*boilerplate.*| - frappe_desk_theme/templates/includes/.*| - frappe_desk_theme/public/js/lib/.* + frappe/www/website_script.js| + frappe/templates/includes/.*| + frappe/public/js/lib/.*| + frappe/website/doctype/website_theme/website_theme_template.scss )$ @@ -55,15 +63,35 @@ repos: # Ignore any files that might contain jinja / bundles exclude: | (?x)^( - frappe_desk_theme/public/dist/.*| + frappe/public/dist/.*| cypress/.*| .*node_modules.*| .*boilerplate.*| - frappe_desk_theme/templates/includes/.*| - frappe_desk_theme/public/js/lib/.* + frappe/www/website_script.js| + frappe/templates/includes/.*| + frappe/public/js/lib/.* )$ + # Frappe-specific coding standards + # The frappe-pre-commit package will be installed automatically + - repo: https://github.com/dhwani-ris/frappe-pre-commit + rev: v1.0.2 + hooks: + - id: frappe-sql-security + - id: frappe-doctype-naming + - id: frappe-coding-standards + +# Alternative: Use all checks in one hook +# - repo: https://github.com/dhwani-ris/frappe-pre-commit +# rev: v1.0.0 +# hooks: +# - id: frappe-all-checks + +# Configuration for specific tools +default_language_version: + python: python3 + ci: autoupdate_schedule: weekly skip: [] - submodules: false + submodules: false \ No newline at end of file From 554efd9817bf88248538c9fd2b823c9947f10c7c Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 7 Aug 2025 14:21:31 +0530 Subject: [PATCH 027/274] Update .github/workflows/ci.yml From 5934988eb39a5baa3ec2088b6b45b92aeee547ee Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 7 Aug 2025 14:21:33 +0530 Subject: [PATCH 028/274] Update .github/workflows/linters.yml From b7cffce90ff27e92b0ba596a7e84e7be2ba424de Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 7 Aug 2025 14:21:35 +0530 Subject: [PATCH 029/274] Update .github/workflows/label-base-on-title.yml From 51c6bb642497bb23ab6b9615a47f2af86a53cd80 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 7 Aug 2025 14:21:37 +0530 Subject: [PATCH 030/274] Update .github/workflows/labeller.yml From 771c02b406d7c6eb49e05c23dfdd21ac982672ba Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 7 Aug 2025 14:21:40 +0530 Subject: [PATCH 031/274] Create commitlint.config.js --- commitlint.config.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 commitlint.config.js diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 0000000..0c582f5 --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1,26 @@ +module.exports = { + parserPreset: "conventional-changelog-conventionalcommits", + rules: { + "subject-empty": [2, "never"], + "type-case": [2, "always", "lower-case"], + "type-empty": [2, "never"], + "type-enum": [ + 2, + "always", + [ + "build", + "chore", + "ci", + "docs", + "feat", + "fix", + "perf", + "refactor", + "revert", + "style", + "test", + "deprecate", // deprecation decision + ], + ], + }, +}; From 3ead848fa51c1beb0de1b104fa5e60089e3e255b Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Tue, 12 Aug 2025 11:25:30 +0530 Subject: [PATCH 032/274] Update .pre-commit-config.yaml From ddc34330e113acb740b873a2ca456e06a22fd01f Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Tue, 12 Aug 2025 11:25:31 +0530 Subject: [PATCH 033/274] Update .github/workflows/ci.yml From 82fc8fb87594a274f0fd2ff70b4bca2657468da9 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Tue, 12 Aug 2025 11:25:33 +0530 Subject: [PATCH 034/274] Update .github/workflows/linters.yml From 26a8a7eeaf582f8101e7ae501a804335115a1fd7 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Tue, 12 Aug 2025 11:25:34 +0530 Subject: [PATCH 035/274] Update .github/workflows/label-base-on-title.yml From 0f7aaf7a5271f411ec1bed3fef708549feaa44c6 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Tue, 12 Aug 2025 11:25:36 +0530 Subject: [PATCH 036/274] Update .github/workflows/labeller.yml From 4de3fc0ef0701d0f81d0d416e37386a195153b6f Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Tue, 12 Aug 2025 11:25:37 +0530 Subject: [PATCH 037/274] Update commitlint.config.js From 7fdf25f63ec938bc574f8e3354341e1f7d40c082 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Tue, 12 Aug 2025 11:32:24 +0530 Subject: [PATCH 038/274] Update .pre-commit-config.yaml From adf359d906bb94e60af8be49cfad269017db6c2d Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Tue, 12 Aug 2025 11:32:25 +0530 Subject: [PATCH 039/274] Update .github/workflows/ci.yml From 84c922d68f8e7ee9cbee8794ccb6102b37354d72 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Tue, 12 Aug 2025 11:32:27 +0530 Subject: [PATCH 040/274] Update .github/workflows/linters.yml From d6f1bd2816232277cc6ab361cf6ec0e9c61e77e0 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Tue, 12 Aug 2025 11:32:28 +0530 Subject: [PATCH 041/274] Update .github/workflows/label-base-on-title.yml From 1c003f3b738580c0b9d0b3b0438ab88471bc6925 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Tue, 12 Aug 2025 11:32:30 +0530 Subject: [PATCH 042/274] Create .github/labeller.yml --- .github/labeller.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/labeller.yml diff --git a/.github/labeller.yml b/.github/labeller.yml new file mode 100644 index 0000000..37e8ede --- /dev/null +++ b/.github/labeller.yml @@ -0,0 +1,18 @@ +name: "Pull Request Labeler" +on: + pull_request_target: + types: [opened, reopened] + +jobs: + triage: + permissions: + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - name: Clone + uses: actions/checkout@v4 + + - uses: actions/labeler@v5 + with: + repo-token: "${{ secrets.DHWANI_FRAPPE_TOKEN }}" From 60b653462a85470356f1c83548c540ef2ba99bcf Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Tue, 12 Aug 2025 11:32:31 +0530 Subject: [PATCH 043/274] Update commitlint.config.js From 082b1f70c234651015322b05554c85d842a1a71d Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Mon, 18 Aug 2025 17:21:08 +0530 Subject: [PATCH 044/274] Create .github/helper/documentation.py --- .github/helper/documentation.py | 259 ++++++++++++++++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 .github/helper/documentation.py diff --git a/.github/helper/documentation.py b/.github/helper/documentation.py new file mode 100644 index 0000000..9f787f3 --- /dev/null +++ b/.github/helper/documentation.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python3 +""" +Generic Documentation Link Checker for GitHub Pull Requests + +This script checks if PRs that add new features include proper documentation links. +Works with any repository by using environment variables for configuration. + +Usage: + python documentation.py [github_token] + +Environment Variables: + GITHUB_REPOSITORY: owner/repo format (e.g., "myorg/myproject") + GITHUB_TOKEN: GitHub API token (optional, increases rate limits) + DOCUMENTATION_DOMAINS: Comma-separated list of docs domains + DOCUMENTATION_KEYWORDS: Comma-separated keywords that indicate docs + SKIP_KEYWORDS: Comma-separated keywords that skip docs requirement +""" + +import os +import sys +from urllib.parse import urlparse + +import requests + + +def get_documentation_domains(): + """Get documentation domains from environment or use defaults.""" + env_domains = os.getenv("DOCUMENTATION_DOMAINS", "") + if env_domains: + return [domain.strip() for domain in env_domains.split(",")] + + # Default domains for common documentation sites + return [ + "docs.github.io", + "readthedocs.io", + "gitbook.io", + "notion.site", + "confluence.com", + "docs.google.com", + "frappeframework.com", + "docs.erpnext.com", + "docs.frappe.io", + ] + + +def get_documentation_keywords(): + """Get keywords that indicate documentation from environment.""" + env_keywords = os.getenv("DOCUMENTATION_KEYWORDS", "") + if env_keywords: + return [keyword.strip().lower() for keyword in env_keywords.split(",")] + + # Default keywords that indicate documentation + return [ + "docs", + "documentation", + "readme", + "guide", + "tutorial", + "wiki", + "manual", + "reference", + "api docs", + "user guide", + "developer guide", + ] + + +def get_skip_keywords(): + """Get keywords that skip documentation requirement.""" + env_keywords = os.getenv("SKIP_KEYWORDS", "") + if env_keywords: + return [keyword.strip().lower() for keyword in env_keywords.split(",")] + + # Default keywords that skip documentation requirement + return [ + "no-docs", + "skip-docs", + "no docs", + "skip docs", + "backport", + "revert", + "hotfix", + "emergency", + "internal", + "wip", + "work in progress", + ] + + +def is_valid_url(url: str) -> bool: + """Check if URL has valid structure.""" + try: + parts = urlparse(url) + return all((parts.scheme, parts.netloc, parts.path)) + except Exception: + return False + + +def is_documentation_link(word: str) -> bool: + """Check if a word/URL points to documentation.""" + if not word.startswith("http") or not is_valid_url(word): + return False + + parsed_url = urlparse(word) + documentation_domains = get_documentation_domains() + + # Check if domain is in documentation domains + for domain in documentation_domains: + if domain in parsed_url.netloc: + return True + + # Check for GitHub links to docs + if parsed_url.netloc == "github.com": + path_parts = parsed_url.path.split("/") + # Check for /owner/repo/wiki, /owner/repo/blob/main/docs, etc. + if len(path_parts) >= 4: + if "wiki" in path_parts or "docs" in parsed_url.path.lower(): + return True + + return False + + +def contains_documentation_keywords(text: str) -> bool: + """Check if text contains documentation-related keywords.""" + text_lower = text.lower() + documentation_keywords = get_documentation_keywords() + + return any(keyword in text_lower for keyword in documentation_keywords) + + +def contains_documentation_link(body: str) -> bool: + """Check if PR body contains documentation links.""" + words = [word for line in body.splitlines() for word in line.split()] + return any(is_documentation_link(word) for word in words) + + +def should_skip_documentation_check(title: str, body: str) -> bool: + """Check if documentation requirement should be skipped.""" + skip_keywords = get_skip_keywords() + combined_text = f"{title} {body}".lower() + + return any(keyword in combined_text for keyword in skip_keywords) + + +def get_github_repository(): + """Get GitHub repository from environment.""" + repo = os.getenv("GITHUB_REPOSITORY") + if not repo: + raise ValueError( + "GITHUB_REPOSITORY environment variable not set. " + "Should be in format 'owner/repo'" + ) + return repo + + +def get_github_headers(): + """Get GitHub API headers with optional authentication.""" + headers = { + "Accept": "application/vnd.github.v3+json", + "User-Agent": "Documentation-Checker/1.0" + } + + token = os.getenv("DHWANI_FRAPPE_TOKEN") + if token: + headers["Authorization"] = f"token {token}" + + return headers + + +def check_pull_request(pr_number: str) -> "tuple[int, str]": + """ + Check if a pull request includes proper documentation. + + Returns: + tuple[int, str]: (exit_code, message) + exit_code: 0 for success, 1 for failure + message: Human-readable status message + """ + try: + repository = get_github_repository() + headers = get_github_headers() + + # Fetch PR details + url = f"https://api.github.com/repos/{repository}/pulls/{pr_number}" + response = requests.get(url, headers=headers, timeout=30) + + if not response.ok: + if response.status_code == 404: + return 0, f"Pull Request #{pr_number} not found - may have been deleted or merged already. Skipping documentation check. ✅" + elif response.status_code == 403: + return 0, f"GitHub API rate limit or permissions issue. Skipping documentation check. ⚠️" + else: + return 0, f"GitHub API error: {response.status_code}. Skipping documentation check. ⚠️" + + payload = response.json() + title = (payload.get("title") or "").strip() + body = (payload.get("body") or "").strip() + head_sha = (payload.get("head") or {}).get("sha") + + # Basic validation + if not head_sha: + return 1, "Invalid pull request data! ⚠️" + + # Check if this is a feature that needs documentation + title_lower = title.lower() + if not (title_lower.startswith("feat") or "feature" in title_lower): + return 0, "Not a feature PR - skipping documentation check 🏃" + + # Check if documentation should be skipped + if should_skip_documentation_check(title, body): + return 0, "Documentation check skipped (found skip keyword) 🏃" + + # Check for documentation links + if contains_documentation_link(body): + return 0, "Documentation link found! You're awesome! 🎉" + + # Check for documentation keywords (less strict) + if contains_documentation_keywords(body): + return 0, "Documentation keywords found in PR description 📚" + + # No documentation found + return 1, ( + "Documentation not found! ⚠️\n" + "Feature PRs should include:\n" + "• Link to documentation\n" + "• Documentation keywords in description\n" + "• Or add 'no-docs' if no documentation needed" + ) + + except requests.RequestException as e: + return 1, f"Network error checking documentation: {e} ⚠️" + except Exception as e: + return 1, f"Error checking documentation: {e} ⚠️" + + +def main(): + """Main entry point.""" + if len(sys.argv) < 2: + print("Usage: python documentation.py ") + print("Environment variables:") + print(" GITHUB_REPOSITORY (required): owner/repo") + print(" GITHUB_TOKEN (optional): GitHub API token") + print(" DOCUMENTATION_DOMAINS (optional): comma-separated domains") + print(" SKIP_KEYWORDS (optional): comma-separated skip keywords") + sys.exit(1) + + pr_number = sys.argv[1] + + try: + exit_code, message = check_pull_request(pr_number) + print(message) + sys.exit(exit_code) + except ValueError as e: + print(f"Configuration error: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main() From f10627f53b2bc10c9562e9219e7acf5f99fc2405 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Mon, 18 Aug 2025 17:21:09 +0530 Subject: [PATCH 045/274] Create .github/helper/update-version.py --- .github/helper/update-version.py | 54 ++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .github/helper/update-version.py diff --git a/.github/helper/update-version.py b/.github/helper/update-version.py new file mode 100644 index 0000000..af9a905 --- /dev/null +++ b/.github/helper/update-version.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +""" +Helper script to update version in Frappe app __init__.py files +Usage: python .github/helper/update-version.py +""" +import os +import re +import sys + +def update_version_in_init_files(new_version): + """Update version in all app __init__.py files""" + updated_files = [] + + # Look for directories that contain __init__.py (potential Frappe apps) + for item in os.listdir('.'): + if os.path.isdir(item) and item not in ['node_modules', '.git', '__pycache__', '.github']: + init_file = os.path.join(item, '__init__.py') + if os.path.exists(init_file): + try: + with open(init_file, 'r') as f: + content = f.read() + + # Update version using regex + pattern = r'__version__\s*=\s*["\'][0-9]+\.[0-9]+\.[0-9]+["\']' + replacement = f'__version__ = "{new_version}"' + + if re.search(pattern, content): + updated_content = re.sub(pattern, replacement, content) + + with open(init_file, 'w') as f: + f.write(updated_content) + + updated_files.append(init_file) + print(f"Updated version in {init_file} to {new_version}") + else: + print(f"No version found in {init_file}") + + except Exception as e: + print(f"Error updating {init_file}: {e}") + + return updated_files + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: python scripts/update-version.py ") + sys.exit(1) + + new_version = sys.argv[1] + updated_files = update_version_in_init_files(new_version) + + if updated_files: + print(f"Successfully updated version to {new_version} in {len(updated_files)} files") + else: + print("No files were updated") From e84b146057f4dfbbda70d25f307e602c7c4aec0e Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Mon, 18 Aug 2025 17:21:11 +0530 Subject: [PATCH 046/274] Create .github/release.yml --- .github/release.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .github/release.yml diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..e4ebba6 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,4 @@ +changelog: + exclude: + labels: + - skip-release-notes From 4d9256e25e717c0bef28bbe64f280e119c81a9fc Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Mon, 18 Aug 2025 17:21:12 +0530 Subject: [PATCH 047/274] Update .github/workflows/ci.yml --- .github/workflows/ci.yml | 524 +++++++++++++++------------------------ 1 file changed, 195 insertions(+), 329 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 65adb57..7d7b260 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,329 +1,195 @@ -name: CI - -on: - push: - branches: [ main, develop, master ] - pull_request: - branches: [ main, develop, master ] - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: ci-${{ github.event_name }}-${{ github.event.number || github.sha }} - cancel-in-progress: true - -jobs: - commit-lint: - name: 'Semantic Commits' - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 200 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - check-latest: true - - - name: Check commit titles - run: | - npm install @commitlint/cli @commitlint/config-conventional - npx commitlint --verbose --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} - - semgrep: - name: 'Semgrep Security Rules' - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: '3.13' - cache: pip - - - name: Download Semgrep rules - run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules - - - name: Run Semgrep rules - run: | - pip install semgrep - semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness - - dependency-vulnerability: - name: 'Vulnerable Dependency Check' - runs-on: ubuntu-latest - - steps: - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: '3.13' - - - name: Checkout code - uses: actions/checkout@v4 - - - name: Cache pip - uses: actions/cache@v4 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }} - restore-keys: | - ${{ runner.os }}-pip- - ${{ runner.os }}- - - - name: Install and run pip-audit - run: | - pip install pip-audit - cd ${GITHUB_WORKSPACE} - pip-audit --desc on --ignore-vuln PYSEC-2023-312 . - - pre-commit-checks: - name: 'Pre-Commit Hooks' - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: '3.13' - cache: pip - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: 'npm' - - - name: Install Python dependencies - run: | - python -m pip install --upgrade pip - pip install pre-commit ruff black - - - name: Install Node.js dependencies - run: | - npm ci - npm install -g prettier eslint - - - name: Check for pre-commit config - run: | - if [ ! -f .pre-commit-config.yaml ]; then - echo "Pre-commit config not found. Downloading..." - curl -o .pre-commit-config.yaml https://raw.githubusercontent.com/dhwani-ris/frappe-pre-commit/main/examples/.pre-commit-config.yaml - else - echo "Pre-commit config already exists." - fi - - - name: Install pre-commit hooks - run: pre-commit install - - - name: Run pre-commit hooks - run: pre-commit run --all-files - - lint-and-format: - name: 'Lint and Format Check' - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - cache: pip - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: 'npm' - - - name: Install Python dependencies - run: | - python -m pip install --upgrade pip - pip install ruff black - - - name: Install Node.js dependencies - run: npm ci - - - name: Run Python linting (Ruff) - run: | - ruff check . - ruff format --check . - - - name: Run JavaScript linting (ESLint) - run: | - npm run lint || echo "No lint script found, skipping ESLint" - - - name: Run JavaScript formatting (Prettier) - run: | - npm run format:check || echo "No format:check script found, skipping Prettier" - - test: - name: 'Test' - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.8", "3.11", "3.13"] - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - cache: pip - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -e ".[dev]" || pip install -r requirements.txt || echo "No requirements found" - - - name: Run tests - run: | - pytest --cov=. --cov-report=xml || echo "No tests found or pytest not configured" - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 - with: - file: ./coverage.xml - fail_ci_if_error: false - if: always() - - security: - name: 'Security Check' - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: "3.13" - - - name: Install security tools - run: | - python -m pip install --upgrade pip - pip install bandit safety - - - name: Run security checks - run: | - bandit -r . -f json -o bandit-report.json || true - safety check --json --output safety-report.json || true - - - name: Upload security reports - uses: actions/upload-artifact@v4 - with: - name: security-reports - path: | - bandit-report.json - safety-report.json - - dependency-update: - name: 'Dependency Update Check' - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: "3.13" - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: 'npm' - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install pip-audit - npm ci - - - name: Check for outdated Python dependencies - run: | - pip list --outdated --format=freeze | grep -v '^\-e' | cut -d = -f 1 | xargs -n1 pip show || echo "No outdated Python packages found" - - - name: Check for outdated Node.js dependencies - run: npm outdated || echo "No outdated Node.js packages found" - - - name: Run pip-audit - run: | - pip-audit --format json --output pip-audit-report.json || true - - - name: Upload audit reports - uses: actions/upload-artifact@v4 - with: - name: audit-reports - path: pip-audit-report.json - - build: - name: 'Build Check' - runs-on: ubuntu-latest - needs: [lint-and-format, test] - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: "3.13" - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: 'npm' - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install build - npm ci - - - name: Build Python package - run: | - python -m build || echo "No Python package to build" - - - name: Build frontend assets - run: | - npm run build || echo "No build script found" - - - name: Upload build artifacts - uses: actions/upload-artifact@v4 - with: - name: build-artifacts - path: | - dist/ - build/ - if-no-files-found: ignore +name: CI + +on: + push: + branches: [ main, develop, development, master ] + pull_request: + branches: [ main, develop, development, master ] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.event_name }}-${{ github.event.number || github.sha }} + cancel-in-progress: true + +jobs: + dependency-vulnerability: + name: 'Vulnerable Dependency Check' + runs-on: ubuntu-latest + + steps: + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Cache pip + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + + - name: Install and run pip-audit + run: | + pip install pip-audit + cd ${GITHUB_WORKSPACE} + pip-audit --desc on --ignore-vuln PYSEC-2023-312 . + + test: + name: 'Test' + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8", "3.11", "3.13"] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e ".[dev]" || pip install -r requirements.txt || echo "No requirements found" + + - name: Run tests + run: | + pytest --cov=. --cov-report=xml || echo "No tests found or pytest not configured" + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + file: ./coverage.xml + fail_ci_if_error: false + if: always() + + security: + name: 'Security Check' + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install security tools + run: | + python -m pip install --upgrade pip + pip install bandit safety + + - name: Run security checks + run: | + bandit -r . -f json -o bandit-report.json || true + safety check --json --output safety-report.json || true + + - name: Upload security reports + uses: actions/upload-artifact@v4 + with: + name: security-reports + path: | + bandit-report.json + safety-report.json + + dependency-update: + name: 'Dependency Update Check' + runs-on: ubuntu-latest + needs: dependency-vulnerability + if: github.event_name == 'pull_request' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: 'npm' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pip-audit + npm ci + + - name: Check for outdated Python dependencies + run: | + pip list --outdated --format=freeze | grep -v '^\-e' | cut -d = -f 1 | xargs -n1 pip show || echo "No outdated Python packages found" + + - name: Check for outdated Node.js dependencies + run: npm outdated || echo "No outdated Node.js packages found" + + - name: Run pip-audit + run: | + pip-audit --format json --output pip-audit-report.json || true + + - name: Upload audit reports + uses: actions/upload-artifact@v4 + with: + name: audit-reports + path: pip-audit-report.json + + build: + name: 'Build Check' + runs-on: ubuntu-latest + needs: [lint-and-format, test] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: 'npm' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + npm ci + + - name: Build Python package + run: | + python -m build || echo "No Python package to build" + + - name: Build frontend assets + run: | + npm run build || echo "No build script found" + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: build-artifacts + path: | + dist/ + build/ + if-no-files-found: ignore From ecf5ffd8f7cd50a356cc39f946218f9eb5c2d4e8 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Mon, 18 Aug 2025 17:21:14 +0530 Subject: [PATCH 048/274] Create .github/workflows/code-quality.yml --- .github/workflows/code-quality.yml | 111 +++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 .github/workflows/code-quality.yml diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml new file mode 100644 index 0000000..622be62 --- /dev/null +++ b/.github/workflows/code-quality.yml @@ -0,0 +1,111 @@ +name: Quality Checks + +on: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: commitcheck-frappe-${{ github.event_name }}-${{ github.event.number }} + cancel-in-progress: true + +jobs: + commit-lint: + name: 'Semantic Commits' + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 200 + - uses: actions/setup-node@v4 + with: + node-version: 22 + check-latest: true + + - name: Check commit titles + run: | + npm install @commitlint/cli @commitlint/config-conventional + npx commitlint --verbose --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} + + docs-required: + name: 'Documentation Required' + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - name: 'Setup Environment' + uses: actions/setup-python@v5 + with: + python-version: '3.13' + - uses: actions/checkout@v4 + + - name: Validate Docs + env: + PR_NUMBER: ${{ github.event.number }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + run: | + pip install requests --quiet + + # Check if PR number is valid + if [ -z "$PR_NUMBER" ] || [ "$PR_NUMBER" = "null" ]; then + echo "No valid PR number found. Skipping documentation check. ✅" + exit 0 + fi + + # Run documentation checker with error handling + python $GITHUB_WORKSPACE/.github/helper/documentation.py $PR_NUMBER || { + echo "Documentation checker failed, but continuing workflow. ⚠️" + exit 0 + } + + linter: + name: 'Semgrep Rules' + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.13' + cache: pip + + - name: Download Semgrep rules + run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules + + - name: Run Semgrep rules + run: | + pip install semgrep + semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness + + precommit: + name: 'Pre-Commit' + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.13' + cache: pip + - uses: pre-commit/action@v3.0.1 + - name: Check for pre-commit config + run: | + if [ ! -f .pre-commit-config.yaml ]; then + echo "Pre-commit config not found. Downloading..." + curl -o .pre-commit-config.yaml https://raw.githubusercontent.com/dhwani-ris/frappe-pre-commit/main/examples/.pre-commit-config.yaml + else + echo "Pre-commit config already exists." + fi + + - name: Install pre-commit + run: pip install pre-commit + + - name: Run pre-commit + run: pre-commit run --all-files \ No newline at end of file From a73d36fc039df9b0a62cce00d28dc650eaa6b543 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Mon, 18 Aug 2025 17:21:16 +0530 Subject: [PATCH 049/274] Create .github/workflows/pr-labeler.yml --- .github/workflows/pr-labeler.yml | 130 +++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 .github/workflows/pr-labeler.yml diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml new file mode 100644 index 0000000..106cea1 --- /dev/null +++ b/.github/workflows/pr-labeler.yml @@ -0,0 +1,130 @@ +# .github/workflows/pr-labeler.yml +name: "PR Labeler" + +on: + pull_request: + types: [opened, reopened] + branches: [main, master, develop, development, uat] # Label PRs to these branches + +permissions: + contents: read + pull-requests: write + +jobs: + label: + runs-on: ubuntu-latest + # Only run for PRs targeting main, master, develop, development, or uat branches + if: contains(fromJson('["main", "master", "develop", "development", "uat"]'), github.event.pull_request.base.ref) + steps: + - name: Auto Label PR + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.pull_request.number, + }); + + const title = pr.title.toLowerCase(); + const targetBranch = pr.base.ref; + const labels = []; + + console.log(`Labeling PR #${pr.number} targeting ${targetBranch} branch`); + + // Skip release note for maintenance tasks + const skipPrefixes = ['chore', 'ci', 'style', 'test', 'refactor']; + const hasSkipPrefix = skipPrefixes.some(prefix => + title.startsWith(prefix + ':') || title.startsWith(prefix + '(') + ); + + if (hasSkipPrefix) { + labels.push('skip-release-notes'); + } + + // Type detection from title + if (title.includes('feat') || title.includes('feature')) { + labels.push('feature'); + } else if (title.includes('fix') || title.includes('bug')) { + labels.push('bug'); + } else if (title.includes('docs') || title.includes('doc')) { + labels.push('docs'); + } else if (title.includes('refactor')) { + labels.push('refactor'); + } + + // Add branch-specific labels + if (targetBranch === 'main' || targetBranch === 'master') { + labels.push('release'); + console.log('🚀 Production release PR'); + } else if (targetBranch === 'uat') { + labels.push('uat-release'); + console.log('🧪 UAT release PR'); + } else if (targetBranch === 'develop' || targetBranch === 'development') { + labels.push('development'); + console.log('🛠️ Development PR'); + } + + // Add labels with error handling + if (labels.length > 0) { + try { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + labels: labels + }); + + console.log(`✅ Added labels: ${labels.join(', ')}`); + } catch (error) { + if (error.status === 422) { + // Handle missing labels by creating them first + console.log('Some labels don\'t exist, creating them...'); + + const labelConfigs = { + 'skip-release-notes': { color: '6c757d', description: 'Skip in release notes' }, + 'uat-release': { color: 'fd7e14', description: 'UAT release PR' }, + 'released': { color: 'e83e8c', description: 'Production release PR' }, + 'development': { color: '17a2b8', description: 'Development branch PR' }, + 'feature': { color: '0e8a16', description: 'New feature' }, + 'bug': { color: 'd73a4a', description: 'Bug fix' }, + 'docs': { color: '0052cc', description: 'Documentation' }, + 'refactor': { color: 'fbca04', description: 'Code refactoring' } + }; + + // Create missing labels + for (const label of labels) { + if (labelConfigs[label]) { + try { + await github.rest.issues.createLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: label, + color: labelConfigs[label].color, + description: labelConfigs[label].description + }); + console.log(`🆕 Created label: ${label}`); + } catch (createError) { + console.log(`⚠️ Could not create label ${label}: ${createError.message}`); + } + } + } + + // Try adding labels again + try { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + labels: labels + }); + console.log(`✅ Added labels after creation: ${labels.join(', ')}`); + } catch (retryError) { + console.log(`⚠️ Still couldn't add labels: ${retryError.message}`); + } + } else { + console.log(`⚠️ Error adding labels: ${error.message}`); + } + } + } From 81dc31b7d68141ccdb09c4ceaf4d67be0cbabfbf Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Mon, 18 Aug 2025 17:21:17 +0530 Subject: [PATCH 050/274] Create .github/workflows/release.yml --- .github/workflows/release.yml | 41 +++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..0d52218 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,41 @@ +name: Generate Semantic Release +on: + push: + branches: [ main, master ] + +permissions: + contents: write + issues: write + pull-requests: write + id-token: write + actions: read + +jobs: + release: + name: Release + runs-on: ubuntu-latest + steps: + - name: Checkout Entire Repository + uses: actions/checkout@v5 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + persist-credentials: true + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Setup dependencies + run: | + npm install @semantic-release/git @semantic-release/exec @semantic-release/github @semantic-release/changelog --no-save + + - name: Create Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GIT_AUTHOR_NAME: "github-actions[bot]" + GIT_AUTHOR_EMAIL: "github-actions[bot]@users.noreply.github.com" + GIT_COMMITTER_NAME: "github-actions[bot]" + GIT_COMMITTER_EMAIL: "github-actions[bot]@users.noreply.github.com" + run: npx semantic-release From f2b1006b75350e15e8024a503a862b9cc0428a71 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Mon, 18 Aug 2025 17:21:19 +0530 Subject: [PATCH 051/274] Update .gitignore --- .gitignore | 110 ++++++++++++++++++++++++++--------------------------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/.gitignore b/.gitignore index b50d332..f7f0335 100644 --- a/.gitignore +++ b/.gitignore @@ -1,55 +1,55 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class -*.pyc -*.py~ - -# Distribution / packaging -.Python -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -*.egg-info/ -.installed.cfg -*.egg -tags -MANIFEST - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Dependency directories -node_modules/ -jspm_packages/ - -# IDEs and editors -.vscode/ -.vs/ -.idea/ -.kdev4/ -*.kdev4 -*.DS_Store -*.swp -*.comp.js -.wnf-lang-status -*debug.log - -# Helix Editor -.helix/ - -# Aider AI Chat -.aider* +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class +*.pyc +*.py~ + +# Distribution / packaging +.Python +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +tags +MANIFEST + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Dependency directories +node_modules/ +jspm_packages/ + +# IDEs and editors +.vscode/ +.vs/ +.idea/ +.kdev4/ +*.kdev4 +*.DS_Store +*.swp +*.comp.js +.wnf-lang-status +*debug.log + +# Helix Editor +.helix/ + +# Aider AI Chat +.aider* From 32910a320a8b14e3d52bcefb09b6445e7a49e032 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Mon, 18 Aug 2025 17:21:20 +0530 Subject: [PATCH 052/274] Update .pre-commit-config.yaml --- .pre-commit-config.yaml | 192 ++++++++++++++++++++-------------------- 1 file changed, 96 insertions(+), 96 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 23dd014..4784f5b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,97 +1,97 @@ -# Copy this to your project root as .pre-commit-config.yaml -# IMPORTANT: First install the package: pip install frappe-pre-commit - -exclude: 'node_modules|.git' -default_stages: [pre-commit] -fail_fast: false - - -repos: - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 - hooks: - - id: trailing-whitespace - files: "frappe.*" - exclude: ".*json$|.*txt$|.*csv|.*md|.*svg" - - id: no-commit-to-branch - args: ['--branch', 'develop'] - - id: check-merge-conflict - - id: check-ast - - id: check-json - - id: check-toml - - id: check-yaml - - id: debug-statements - exclude: ^frappe/tests/classes/context_managers\.py$ - - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.1 - hooks: - - id: ruff - name: "Run ruff import sorter" - args: ["--select=I", "--fix"] - - - id: ruff - name: "Run ruff linter" - - - id: ruff-format - name: "Run ruff formatter" - - - repo: https://github.com/pre-commit/mirrors-prettier - rev: v2.7.1 - hooks: - - id: prettier - types_or: [javascript, vue, scss] - # Ignore any files that might contain jinja / bundles - exclude: | - (?x)^( - frappe/public/dist/.*| - .*node_modules.*| - .*boilerplate.*| - frappe/www/website_script.js| - frappe/templates/includes/.*| - frappe/public/js/lib/.*| - frappe/website/doctype/website_theme/website_theme_template.scss - )$ - - - - repo: https://github.com/pre-commit/mirrors-eslint - rev: v8.44.0 - hooks: - - id: eslint - types_or: [javascript] - args: ['--quiet'] - # Ignore any files that might contain jinja / bundles - exclude: | - (?x)^( - frappe/public/dist/.*| - cypress/.*| - .*node_modules.*| - .*boilerplate.*| - frappe/www/website_script.js| - frappe/templates/includes/.*| - frappe/public/js/lib/.* - )$ - - # Frappe-specific coding standards - # The frappe-pre-commit package will be installed automatically - - repo: https://github.com/dhwani-ris/frappe-pre-commit - rev: v1.0.2 - hooks: - - id: frappe-sql-security - - id: frappe-doctype-naming - - id: frappe-coding-standards - -# Alternative: Use all checks in one hook -# - repo: https://github.com/dhwani-ris/frappe-pre-commit -# rev: v1.0.0 -# hooks: -# - id: frappe-all-checks - -# Configuration for specific tools -default_language_version: - python: python3 - -ci: - autoupdate_schedule: weekly - skip: [] +# Copy this to your project root as .pre-commit-config.yaml +# IMPORTANT: First install the package: pip install frappe-pre-commit + +exclude: 'node_modules|.git' +default_stages: [pre-commit] +fail_fast: false + + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + files: "frappe.*" + exclude: ".*json$|.*txt$|.*csv|.*md|.*svg" + - id: no-commit-to-branch + args: ['--branch', 'develop'] + - id: check-merge-conflict + - id: check-ast + - id: check-json + - id: check-toml + - id: check-yaml + - id: debug-statements + exclude: ^frappe/tests/classes/context_managers\.py$ + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.8.1 + hooks: + - id: ruff + name: "Run ruff import sorter" + args: ["--select=I", "--fix"] + + - id: ruff + name: "Run ruff linter" + + - id: ruff-format + name: "Run ruff formatter" + + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v2.7.1 + hooks: + - id: prettier + types_or: [javascript, vue, scss] + # Ignore any files that might contain jinja / bundles + exclude: | + (?x)^( + frappe/public/dist/.*| + .*node_modules.*| + .*boilerplate.*| + frappe/www/website_script.js| + frappe/templates/includes/.*| + frappe/public/js/lib/.*| + frappe/website/doctype/website_theme/website_theme_template.scss + )$ + + + - repo: https://github.com/pre-commit/mirrors-eslint + rev: v8.44.0 + hooks: + - id: eslint + types_or: [javascript] + args: ['--quiet'] + # Ignore any files that might contain jinja / bundles + exclude: | + (?x)^( + frappe/public/dist/.*| + cypress/.*| + .*node_modules.*| + .*boilerplate.*| + frappe/www/website_script.js| + frappe/templates/includes/.*| + frappe/public/js/lib/.* + )$ + + # Frappe-specific coding standards + # The frappe-pre-commit package will be installed automatically + - repo: https://github.com/dhwani-ris/frappe-pre-commit + rev: v1.0.2 + hooks: + - id: frappe-sql-security + - id: frappe-doctype-naming + - id: frappe-coding-standards + +# Alternative: Use all checks in one hook +# - repo: https://github.com/dhwani-ris/frappe-pre-commit +# rev: v1.0.0 +# hooks: +# - id: frappe-all-checks + +# Configuration for specific tools +default_language_version: + python: python3 + +ci: + autoupdate_schedule: weekly + skip: [] submodules: false \ No newline at end of file From 27a57ba1c96d651888dfdcee0b06198a8d620687 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Mon, 18 Aug 2025 17:21:22 +0530 Subject: [PATCH 053/274] Create .releaserc --- .releaserc | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .releaserc diff --git a/.releaserc b/.releaserc new file mode 100644 index 0000000..8b457ba --- /dev/null +++ b/.releaserc @@ -0,0 +1,21 @@ +{ + "branches": ["main", "master"], + "plugins": [ + ["@semantic-release/commit-analyzer", { + "preset": "angular" + }], + "@semantic-release/release-notes-generator", + [ + "@semantic-release/exec", { + "prepareCmd": "python $GITHUB_WORKSPACE/.github/helper/update-version.py ${nextRelease.version}" + } + ], + [ + "@semantic-release/git", { + "assets": ["*/__init__.py"], + "message": "chore(release): Bumped to Version ${nextRelease.version}" + } + ], + "@semantic-release/github" + ] +} From 8a94ddf3bcc1d3151b25f6747b43befb7e4ba94e Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Mon, 18 Aug 2025 17:21:23 +0530 Subject: [PATCH 054/274] Update commitlint.config.js --- commitlint.config.js | 52 ++++++++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/commitlint.config.js b/commitlint.config.js index 0c582f5..b97aaf0 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -1,26 +1,26 @@ -module.exports = { - parserPreset: "conventional-changelog-conventionalcommits", - rules: { - "subject-empty": [2, "never"], - "type-case": [2, "always", "lower-case"], - "type-empty": [2, "never"], - "type-enum": [ - 2, - "always", - [ - "build", - "chore", - "ci", - "docs", - "feat", - "fix", - "perf", - "refactor", - "revert", - "style", - "test", - "deprecate", // deprecation decision - ], - ], - }, -}; +module.exports = { + parserPreset: "conventional-changelog-conventionalcommits", + rules: { + "subject-empty": [2, "never"], + "type-case": [2, "always", "lower-case"], + "type-empty": [2, "never"], + "type-enum": [ + 2, + "always", + [ + "build", + "chore", + "ci", + "docs", + "feat", + "fix", + "perf", + "refactor", + "revert", + "style", + "test", + "deprecate", // deprecation decision + ], + ], + }, +}; From 2f41d18ec3b66808293227d509b29fc4972bc8e7 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Mon, 18 Aug 2025 17:29:02 +0530 Subject: [PATCH 055/274] ci: update GitHub workflows From a7188468843e602efcee196b855b48255decece0 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Mon, 18 Aug 2025 17:29:04 +0530 Subject: [PATCH 056/274] ci: update GitHub workflows From b1749cc7fce3f1e183431f57b86b00ecf19d45a7 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Mon, 18 Aug 2025 17:29:05 +0530 Subject: [PATCH 057/274] ci: update GitHub workflows From 8f50c4114363890b9e766d164b1844b56c8a6fa9 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Mon, 18 Aug 2025 17:29:07 +0530 Subject: [PATCH 058/274] ci: update GitHub workflows From 6ec09e961d1acc9a2d42e777405060bfd8b88564 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Mon, 18 Aug 2025 17:29:08 +0530 Subject: [PATCH 059/274] ci: update GitHub workflows From 9fce4d982a50cb417e97215fd3e207fe31099e3d Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Mon, 18 Aug 2025 17:29:10 +0530 Subject: [PATCH 060/274] ci: update GitHub workflows From 1ca9621d5b8b3603e74e784081c77b31964250ea Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Mon, 18 Aug 2025 17:29:11 +0530 Subject: [PATCH 061/274] ci: update GitHub workflows From 0758a386b911171aef245d75c12f482114e03df1 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Mon, 18 Aug 2025 17:29:13 +0530 Subject: [PATCH 062/274] ci: update GitHub workflows From 715c8dddeeb3bcbe23237c2f094144420dbe21dc Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Mon, 18 Aug 2025 17:29:14 +0530 Subject: [PATCH 063/274] ci: update GitHub workflows From d6530eebecab5f0404b5a253e726c9f6af958cc2 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Mon, 18 Aug 2025 17:29:16 +0530 Subject: [PATCH 064/274] ci: update GitHub workflows From 48262b32a429114b67ed2a755d07a571a2e53a51 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Mon, 18 Aug 2025 17:29:17 +0530 Subject: [PATCH 065/274] ci: update GitHub workflows From 87057a0daee0136783c45d314cea02ac1042ca83 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Fri, 22 Aug 2025 11:14:57 +0530 Subject: [PATCH 066/274] chore: remove .github/helper/documentation.py --- .github/helper/documentation.py | 259 -------------------------------- 1 file changed, 259 deletions(-) delete mode 100644 .github/helper/documentation.py diff --git a/.github/helper/documentation.py b/.github/helper/documentation.py deleted file mode 100644 index 9f787f3..0000000 --- a/.github/helper/documentation.py +++ /dev/null @@ -1,259 +0,0 @@ -#!/usr/bin/env python3 -""" -Generic Documentation Link Checker for GitHub Pull Requests - -This script checks if PRs that add new features include proper documentation links. -Works with any repository by using environment variables for configuration. - -Usage: - python documentation.py [github_token] - -Environment Variables: - GITHUB_REPOSITORY: owner/repo format (e.g., "myorg/myproject") - GITHUB_TOKEN: GitHub API token (optional, increases rate limits) - DOCUMENTATION_DOMAINS: Comma-separated list of docs domains - DOCUMENTATION_KEYWORDS: Comma-separated keywords that indicate docs - SKIP_KEYWORDS: Comma-separated keywords that skip docs requirement -""" - -import os -import sys -from urllib.parse import urlparse - -import requests - - -def get_documentation_domains(): - """Get documentation domains from environment or use defaults.""" - env_domains = os.getenv("DOCUMENTATION_DOMAINS", "") - if env_domains: - return [domain.strip() for domain in env_domains.split(",")] - - # Default domains for common documentation sites - return [ - "docs.github.io", - "readthedocs.io", - "gitbook.io", - "notion.site", - "confluence.com", - "docs.google.com", - "frappeframework.com", - "docs.erpnext.com", - "docs.frappe.io", - ] - - -def get_documentation_keywords(): - """Get keywords that indicate documentation from environment.""" - env_keywords = os.getenv("DOCUMENTATION_KEYWORDS", "") - if env_keywords: - return [keyword.strip().lower() for keyword in env_keywords.split(",")] - - # Default keywords that indicate documentation - return [ - "docs", - "documentation", - "readme", - "guide", - "tutorial", - "wiki", - "manual", - "reference", - "api docs", - "user guide", - "developer guide", - ] - - -def get_skip_keywords(): - """Get keywords that skip documentation requirement.""" - env_keywords = os.getenv("SKIP_KEYWORDS", "") - if env_keywords: - return [keyword.strip().lower() for keyword in env_keywords.split(",")] - - # Default keywords that skip documentation requirement - return [ - "no-docs", - "skip-docs", - "no docs", - "skip docs", - "backport", - "revert", - "hotfix", - "emergency", - "internal", - "wip", - "work in progress", - ] - - -def is_valid_url(url: str) -> bool: - """Check if URL has valid structure.""" - try: - parts = urlparse(url) - return all((parts.scheme, parts.netloc, parts.path)) - except Exception: - return False - - -def is_documentation_link(word: str) -> bool: - """Check if a word/URL points to documentation.""" - if not word.startswith("http") or not is_valid_url(word): - return False - - parsed_url = urlparse(word) - documentation_domains = get_documentation_domains() - - # Check if domain is in documentation domains - for domain in documentation_domains: - if domain in parsed_url.netloc: - return True - - # Check for GitHub links to docs - if parsed_url.netloc == "github.com": - path_parts = parsed_url.path.split("/") - # Check for /owner/repo/wiki, /owner/repo/blob/main/docs, etc. - if len(path_parts) >= 4: - if "wiki" in path_parts or "docs" in parsed_url.path.lower(): - return True - - return False - - -def contains_documentation_keywords(text: str) -> bool: - """Check if text contains documentation-related keywords.""" - text_lower = text.lower() - documentation_keywords = get_documentation_keywords() - - return any(keyword in text_lower for keyword in documentation_keywords) - - -def contains_documentation_link(body: str) -> bool: - """Check if PR body contains documentation links.""" - words = [word for line in body.splitlines() for word in line.split()] - return any(is_documentation_link(word) for word in words) - - -def should_skip_documentation_check(title: str, body: str) -> bool: - """Check if documentation requirement should be skipped.""" - skip_keywords = get_skip_keywords() - combined_text = f"{title} {body}".lower() - - return any(keyword in combined_text for keyword in skip_keywords) - - -def get_github_repository(): - """Get GitHub repository from environment.""" - repo = os.getenv("GITHUB_REPOSITORY") - if not repo: - raise ValueError( - "GITHUB_REPOSITORY environment variable not set. " - "Should be in format 'owner/repo'" - ) - return repo - - -def get_github_headers(): - """Get GitHub API headers with optional authentication.""" - headers = { - "Accept": "application/vnd.github.v3+json", - "User-Agent": "Documentation-Checker/1.0" - } - - token = os.getenv("DHWANI_FRAPPE_TOKEN") - if token: - headers["Authorization"] = f"token {token}" - - return headers - - -def check_pull_request(pr_number: str) -> "tuple[int, str]": - """ - Check if a pull request includes proper documentation. - - Returns: - tuple[int, str]: (exit_code, message) - exit_code: 0 for success, 1 for failure - message: Human-readable status message - """ - try: - repository = get_github_repository() - headers = get_github_headers() - - # Fetch PR details - url = f"https://api.github.com/repos/{repository}/pulls/{pr_number}" - response = requests.get(url, headers=headers, timeout=30) - - if not response.ok: - if response.status_code == 404: - return 0, f"Pull Request #{pr_number} not found - may have been deleted or merged already. Skipping documentation check. ✅" - elif response.status_code == 403: - return 0, f"GitHub API rate limit or permissions issue. Skipping documentation check. ⚠️" - else: - return 0, f"GitHub API error: {response.status_code}. Skipping documentation check. ⚠️" - - payload = response.json() - title = (payload.get("title") or "").strip() - body = (payload.get("body") or "").strip() - head_sha = (payload.get("head") or {}).get("sha") - - # Basic validation - if not head_sha: - return 1, "Invalid pull request data! ⚠️" - - # Check if this is a feature that needs documentation - title_lower = title.lower() - if not (title_lower.startswith("feat") or "feature" in title_lower): - return 0, "Not a feature PR - skipping documentation check 🏃" - - # Check if documentation should be skipped - if should_skip_documentation_check(title, body): - return 0, "Documentation check skipped (found skip keyword) 🏃" - - # Check for documentation links - if contains_documentation_link(body): - return 0, "Documentation link found! You're awesome! 🎉" - - # Check for documentation keywords (less strict) - if contains_documentation_keywords(body): - return 0, "Documentation keywords found in PR description 📚" - - # No documentation found - return 1, ( - "Documentation not found! ⚠️\n" - "Feature PRs should include:\n" - "• Link to documentation\n" - "• Documentation keywords in description\n" - "• Or add 'no-docs' if no documentation needed" - ) - - except requests.RequestException as e: - return 1, f"Network error checking documentation: {e} ⚠️" - except Exception as e: - return 1, f"Error checking documentation: {e} ⚠️" - - -def main(): - """Main entry point.""" - if len(sys.argv) < 2: - print("Usage: python documentation.py ") - print("Environment variables:") - print(" GITHUB_REPOSITORY (required): owner/repo") - print(" GITHUB_TOKEN (optional): GitHub API token") - print(" DOCUMENTATION_DOMAINS (optional): comma-separated domains") - print(" SKIP_KEYWORDS (optional): comma-separated skip keywords") - sys.exit(1) - - pr_number = sys.argv[1] - - try: - exit_code, message = check_pull_request(pr_number) - print(message) - sys.exit(exit_code) - except ValueError as e: - print(f"Configuration error: {e}") - sys.exit(1) - - -if __name__ == "__main__": - main() From e482a1b56898deca1d628df129ac5019fe9f0456 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Fri, 22 Aug 2025 11:14:58 +0530 Subject: [PATCH 067/274] chore: remove .github/helper/update-version.py --- .github/helper/update-version.py | 54 -------------------------------- 1 file changed, 54 deletions(-) delete mode 100644 .github/helper/update-version.py diff --git a/.github/helper/update-version.py b/.github/helper/update-version.py deleted file mode 100644 index af9a905..0000000 --- a/.github/helper/update-version.py +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env python3 -""" -Helper script to update version in Frappe app __init__.py files -Usage: python .github/helper/update-version.py -""" -import os -import re -import sys - -def update_version_in_init_files(new_version): - """Update version in all app __init__.py files""" - updated_files = [] - - # Look for directories that contain __init__.py (potential Frappe apps) - for item in os.listdir('.'): - if os.path.isdir(item) and item not in ['node_modules', '.git', '__pycache__', '.github']: - init_file = os.path.join(item, '__init__.py') - if os.path.exists(init_file): - try: - with open(init_file, 'r') as f: - content = f.read() - - # Update version using regex - pattern = r'__version__\s*=\s*["\'][0-9]+\.[0-9]+\.[0-9]+["\']' - replacement = f'__version__ = "{new_version}"' - - if re.search(pattern, content): - updated_content = re.sub(pattern, replacement, content) - - with open(init_file, 'w') as f: - f.write(updated_content) - - updated_files.append(init_file) - print(f"Updated version in {init_file} to {new_version}") - else: - print(f"No version found in {init_file}") - - except Exception as e: - print(f"Error updating {init_file}: {e}") - - return updated_files - -if __name__ == "__main__": - if len(sys.argv) != 2: - print("Usage: python scripts/update-version.py ") - sys.exit(1) - - new_version = sys.argv[1] - updated_files = update_version_in_init_files(new_version) - - if updated_files: - print(f"Successfully updated version to {new_version} in {len(updated_files)} files") - else: - print("No files were updated") From 7089a47628968cd29869157708f5be4561900d25 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Fri, 22 Aug 2025 11:14:59 +0530 Subject: [PATCH 068/274] chore: remove .github/labeller.yml --- .github/labeller.yml | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 .github/labeller.yml diff --git a/.github/labeller.yml b/.github/labeller.yml deleted file mode 100644 index 37e8ede..0000000 --- a/.github/labeller.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: "Pull Request Labeler" -on: - pull_request_target: - types: [opened, reopened] - -jobs: - triage: - permissions: - contents: read - pull-requests: write - runs-on: ubuntu-latest - steps: - - name: Clone - uses: actions/checkout@v4 - - - uses: actions/labeler@v5 - with: - repo-token: "${{ secrets.DHWANI_FRAPPE_TOKEN }}" From 7a80fcc5e08914f5a5c5a4521555244e7fdf69f3 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Fri, 22 Aug 2025 11:14:59 +0530 Subject: [PATCH 069/274] chore: remove .github/release.yml --- .github/release.yml | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 .github/release.yml diff --git a/.github/release.yml b/.github/release.yml deleted file mode 100644 index e4ebba6..0000000 --- a/.github/release.yml +++ /dev/null @@ -1,4 +0,0 @@ -changelog: - exclude: - labels: - - skip-release-notes From 86aa75ed579de74a1a73c4c098b3a4120f7b490c Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Fri, 22 Aug 2025 11:15:01 +0530 Subject: [PATCH 070/274] chore: remove .github/workflows/ci.yml --- .github/workflows/ci.yml | 195 --------------------------------------- 1 file changed, 195 deletions(-) delete mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 7d7b260..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,195 +0,0 @@ -name: CI - -on: - push: - branches: [ main, develop, development, master ] - pull_request: - branches: [ main, develop, development, master ] - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: ci-${{ github.event_name }}-${{ github.event.number || github.sha }} - cancel-in-progress: true - -jobs: - dependency-vulnerability: - name: 'Vulnerable Dependency Check' - runs-on: ubuntu-latest - - steps: - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: '3.13' - - - name: Checkout code - uses: actions/checkout@v4 - - - name: Cache pip - uses: actions/cache@v4 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }} - restore-keys: | - ${{ runner.os }}-pip- - ${{ runner.os }}- - - - name: Install and run pip-audit - run: | - pip install pip-audit - cd ${GITHUB_WORKSPACE} - pip-audit --desc on --ignore-vuln PYSEC-2023-312 . - - test: - name: 'Test' - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.8", "3.11", "3.13"] - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - cache: pip - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -e ".[dev]" || pip install -r requirements.txt || echo "No requirements found" - - - name: Run tests - run: | - pytest --cov=. --cov-report=xml || echo "No tests found or pytest not configured" - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 - with: - file: ./coverage.xml - fail_ci_if_error: false - if: always() - - security: - name: 'Security Check' - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: "3.13" - - - name: Install security tools - run: | - python -m pip install --upgrade pip - pip install bandit safety - - - name: Run security checks - run: | - bandit -r . -f json -o bandit-report.json || true - safety check --json --output safety-report.json || true - - - name: Upload security reports - uses: actions/upload-artifact@v4 - with: - name: security-reports - path: | - bandit-report.json - safety-report.json - - dependency-update: - name: 'Dependency Update Check' - runs-on: ubuntu-latest - needs: dependency-vulnerability - if: github.event_name == 'pull_request' - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: "3.13" - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: 'npm' - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install pip-audit - npm ci - - - name: Check for outdated Python dependencies - run: | - pip list --outdated --format=freeze | grep -v '^\-e' | cut -d = -f 1 | xargs -n1 pip show || echo "No outdated Python packages found" - - - name: Check for outdated Node.js dependencies - run: npm outdated || echo "No outdated Node.js packages found" - - - name: Run pip-audit - run: | - pip-audit --format json --output pip-audit-report.json || true - - - name: Upload audit reports - uses: actions/upload-artifact@v4 - with: - name: audit-reports - path: pip-audit-report.json - - build: - name: 'Build Check' - runs-on: ubuntu-latest - needs: [lint-and-format, test] - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: "3.13" - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: 'npm' - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install build - npm ci - - - name: Build Python package - run: | - python -m build || echo "No Python package to build" - - - name: Build frontend assets - run: | - npm run build || echo "No build script found" - - - name: Upload build artifacts - uses: actions/upload-artifact@v4 - with: - name: build-artifacts - path: | - dist/ - build/ - if-no-files-found: ignore From c2fb039b5af2c860dc63bad8d11959aa78ebcfe8 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Fri, 22 Aug 2025 11:15:02 +0530 Subject: [PATCH 071/274] chore: remove .github/workflows/code-quality.yml --- .github/workflows/code-quality.yml | 111 ----------------------------- 1 file changed, 111 deletions(-) delete mode 100644 .github/workflows/code-quality.yml diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml deleted file mode 100644 index 622be62..0000000 --- a/.github/workflows/code-quality.yml +++ /dev/null @@ -1,111 +0,0 @@ -name: Quality Checks - -on: - pull_request: - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: commitcheck-frappe-${{ github.event_name }}-${{ github.event.number }} - cancel-in-progress: true - -jobs: - commit-lint: - name: 'Semantic Commits' - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 200 - - uses: actions/setup-node@v4 - with: - node-version: 22 - check-latest: true - - - name: Check commit titles - run: | - npm install @commitlint/cli @commitlint/config-conventional - npx commitlint --verbose --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} - - docs-required: - name: 'Documentation Required' - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' - - steps: - - name: 'Setup Environment' - uses: actions/setup-python@v5 - with: - python-version: '3.13' - - uses: actions/checkout@v4 - - - name: Validate Docs - env: - PR_NUMBER: ${{ github.event.number }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_REPOSITORY: ${{ github.repository }} - run: | - pip install requests --quiet - - # Check if PR number is valid - if [ -z "$PR_NUMBER" ] || [ "$PR_NUMBER" = "null" ]; then - echo "No valid PR number found. Skipping documentation check. ✅" - exit 0 - fi - - # Run documentation checker with error handling - python $GITHUB_WORKSPACE/.github/helper/documentation.py $PR_NUMBER || { - echo "Documentation checker failed, but continuing workflow. ⚠️" - exit 0 - } - - linter: - name: 'Semgrep Rules' - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' - - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: '3.13' - cache: pip - - - name: Download Semgrep rules - run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules - - - name: Run Semgrep rules - run: | - pip install semgrep - semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness - - precommit: - name: 'Pre-Commit' - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' - - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: '3.13' - cache: pip - - uses: pre-commit/action@v3.0.1 - - name: Check for pre-commit config - run: | - if [ ! -f .pre-commit-config.yaml ]; then - echo "Pre-commit config not found. Downloading..." - curl -o .pre-commit-config.yaml https://raw.githubusercontent.com/dhwani-ris/frappe-pre-commit/main/examples/.pre-commit-config.yaml - else - echo "Pre-commit config already exists." - fi - - - name: Install pre-commit - run: pip install pre-commit - - - name: Run pre-commit - run: pre-commit run --all-files \ No newline at end of file From 6e9ca16c4ebbaf8f62644f01508eba994858a95c Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Fri, 22 Aug 2025 11:15:03 +0530 Subject: [PATCH 072/274] chore: remove .github/workflows/label-base-on-title.yml --- .github/workflows/label-base-on-title.yml | 30 ----------------------- 1 file changed, 30 deletions(-) delete mode 100644 .github/workflows/label-base-on-title.yml diff --git a/.github/workflows/label-base-on-title.yml b/.github/workflows/label-base-on-title.yml deleted file mode 100644 index 4e811ed..0000000 --- a/.github/workflows/label-base-on-title.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: "Auto-label PRs based on title" - -on: - pull_request_target: - types: [opened, reopened] - -jobs: - add-label-if-prefix-matches: - permissions: - contents: read - pull-requests: write - runs-on: ubuntu-latest - steps: - - name: Check PR title and add label if it matches prefixes - uses: actions/github-script@v7 - continue-on-error: true - with: - script: | - const title = context.payload.pull_request.title.toLowerCase(); - const prefixes = ['chore', 'ci', 'style', 'test', 'refactor']; - - // Check if the PR title starts with any of the prefixes - if (prefixes.some(prefix => title.startsWith(prefix))) { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.payload.pull_request.number, - labels: ['skip-release-notes'] - }); - } From 600ba8264a6caa794eed8f443f0a83a110cbd54f Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Fri, 22 Aug 2025 11:15:03 +0530 Subject: [PATCH 073/274] chore: remove .github/workflows/labeller.yml --- .github/workflows/labeller.yml | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 .github/workflows/labeller.yml diff --git a/.github/workflows/labeller.yml b/.github/workflows/labeller.yml deleted file mode 100644 index 37e8ede..0000000 --- a/.github/workflows/labeller.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: "Pull Request Labeler" -on: - pull_request_target: - types: [opened, reopened] - -jobs: - triage: - permissions: - contents: read - pull-requests: write - runs-on: ubuntu-latest - steps: - - name: Clone - uses: actions/checkout@v4 - - - uses: actions/labeler@v5 - with: - repo-token: "${{ secrets.DHWANI_FRAPPE_TOKEN }}" From e4ae9f2711376de9405c1f3d367205e1091af485 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Fri, 22 Aug 2025 11:15:04 +0530 Subject: [PATCH 074/274] chore: remove .github/workflows/linters.yml --- .github/workflows/linters.yml | 106 ---------------------------------- 1 file changed, 106 deletions(-) delete mode 100644 .github/workflows/linters.yml diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml deleted file mode 100644 index f32d952..0000000 --- a/.github/workflows/linters.yml +++ /dev/null @@ -1,106 +0,0 @@ -# When updating this file, please also update the linter_workflow_template in frappe/utils/boilerplate.py -name: Linters - -on: - pull_request: - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: commitcheck-frappe-${{ github.event_name }}-${{ github.event.number }} - cancel-in-progress: true - -jobs: - commit-lint: - name: 'Semantic Commits' - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 200 - - uses: actions/setup-node@v4 - with: - node-version: 22 - check-latest: true - - - name: Check commit titles - run: | - npm install @commitlint/cli @commitlint/config-conventional - npx commitlint --verbose --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} - - linter: - name: 'Semgrep Rules' - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' - - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: '3.13' - cache: pip - - - name: Download Semgrep rules - run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules - - - name: Run Semgrep rules - run: | - pip install semgrep - semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness - - deps-vulnerable-check: - name: 'Vulnerable Dependency Check' - runs-on: ubuntu-latest - - steps: - - uses: actions/setup-python@v5 - with: - python-version: '3.13' - - - uses: actions/checkout@v4 - - - name: Cache pip - uses: actions/cache@v4 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }} - restore-keys: | - ${{ runner.os }}-pip- - ${{ runner.os }}- - - - name: Install and run pip-audit - run: | - pip install pip-audit - cd ${GITHUB_WORKSPACE} - pip-audit --desc on --ignore-vuln PYSEC-2023-312 . - - precommit: - name: 'Pre-Commit' - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' - - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: '3.13' - cache: pip - - uses: pre-commit/action@v3.0.1 - - name: Check for pre-commit config - run: | - if [ ! -f .pre-commit-config.yaml ]; then - echo "Pre-commit config not found. Downloading..." - curl -o .pre-commit-config.yaml https://raw.githubusercontent.com/dhwani-ris/frappe-pre-commit/main/examples/.pre-commit-config.yaml - else - echo "Pre-commit config already exists." - fi - - - name: Install pre-commit - run: pip install pre-commit - - - name: Run pre-commit - run: pre-commit run --all-files From 3f37b54eb090b3c79e67aaa5d27feb270b9465f7 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Fri, 22 Aug 2025 11:15:05 +0530 Subject: [PATCH 075/274] chore: remove .github/workflows/pr-labeler.yml --- .github/workflows/pr-labeler.yml | 130 ------------------------------- 1 file changed, 130 deletions(-) delete mode 100644 .github/workflows/pr-labeler.yml diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml deleted file mode 100644 index 106cea1..0000000 --- a/.github/workflows/pr-labeler.yml +++ /dev/null @@ -1,130 +0,0 @@ -# .github/workflows/pr-labeler.yml -name: "PR Labeler" - -on: - pull_request: - types: [opened, reopened] - branches: [main, master, develop, development, uat] # Label PRs to these branches - -permissions: - contents: read - pull-requests: write - -jobs: - label: - runs-on: ubuntu-latest - # Only run for PRs targeting main, master, develop, development, or uat branches - if: contains(fromJson('["main", "master", "develop", "development", "uat"]'), github.event.pull_request.base.ref) - steps: - - name: Auto Label PR - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { data: pr } = await github.rest.pulls.get({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: context.payload.pull_request.number, - }); - - const title = pr.title.toLowerCase(); - const targetBranch = pr.base.ref; - const labels = []; - - console.log(`Labeling PR #${pr.number} targeting ${targetBranch} branch`); - - // Skip release note for maintenance tasks - const skipPrefixes = ['chore', 'ci', 'style', 'test', 'refactor']; - const hasSkipPrefix = skipPrefixes.some(prefix => - title.startsWith(prefix + ':') || title.startsWith(prefix + '(') - ); - - if (hasSkipPrefix) { - labels.push('skip-release-notes'); - } - - // Type detection from title - if (title.includes('feat') || title.includes('feature')) { - labels.push('feature'); - } else if (title.includes('fix') || title.includes('bug')) { - labels.push('bug'); - } else if (title.includes('docs') || title.includes('doc')) { - labels.push('docs'); - } else if (title.includes('refactor')) { - labels.push('refactor'); - } - - // Add branch-specific labels - if (targetBranch === 'main' || targetBranch === 'master') { - labels.push('release'); - console.log('🚀 Production release PR'); - } else if (targetBranch === 'uat') { - labels.push('uat-release'); - console.log('🧪 UAT release PR'); - } else if (targetBranch === 'develop' || targetBranch === 'development') { - labels.push('development'); - console.log('🛠️ Development PR'); - } - - // Add labels with error handling - if (labels.length > 0) { - try { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.payload.pull_request.number, - labels: labels - }); - - console.log(`✅ Added labels: ${labels.join(', ')}`); - } catch (error) { - if (error.status === 422) { - // Handle missing labels by creating them first - console.log('Some labels don\'t exist, creating them...'); - - const labelConfigs = { - 'skip-release-notes': { color: '6c757d', description: 'Skip in release notes' }, - 'uat-release': { color: 'fd7e14', description: 'UAT release PR' }, - 'released': { color: 'e83e8c', description: 'Production release PR' }, - 'development': { color: '17a2b8', description: 'Development branch PR' }, - 'feature': { color: '0e8a16', description: 'New feature' }, - 'bug': { color: 'd73a4a', description: 'Bug fix' }, - 'docs': { color: '0052cc', description: 'Documentation' }, - 'refactor': { color: 'fbca04', description: 'Code refactoring' } - }; - - // Create missing labels - for (const label of labels) { - if (labelConfigs[label]) { - try { - await github.rest.issues.createLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: label, - color: labelConfigs[label].color, - description: labelConfigs[label].description - }); - console.log(`🆕 Created label: ${label}`); - } catch (createError) { - console.log(`⚠️ Could not create label ${label}: ${createError.message}`); - } - } - } - - // Try adding labels again - try { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.payload.pull_request.number, - labels: labels - }); - console.log(`✅ Added labels after creation: ${labels.join(', ')}`); - } catch (retryError) { - console.log(`⚠️ Still couldn't add labels: ${retryError.message}`); - } - } else { - console.log(`⚠️ Error adding labels: ${error.message}`); - } - } - } From ee7ef21bb619b551821d3a9fb89ad5bc07520484 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Fri, 22 Aug 2025 11:15:06 +0530 Subject: [PATCH 076/274] chore: remove .github/workflows/release.yml --- .github/workflows/release.yml | 41 ----------------------------------- 1 file changed, 41 deletions(-) delete mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 0d52218..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Generate Semantic Release -on: - push: - branches: [ main, master ] - -permissions: - contents: write - issues: write - pull-requests: write - id-token: write - actions: read - -jobs: - release: - name: Release - runs-on: ubuntu-latest - steps: - - name: Checkout Entire Repository - uses: actions/checkout@v5 - with: - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - persist-credentials: true - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - - - name: Setup dependencies - run: | - npm install @semantic-release/git @semantic-release/exec @semantic-release/github @semantic-release/changelog --no-save - - - name: Create Release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GIT_AUTHOR_NAME: "github-actions[bot]" - GIT_AUTHOR_EMAIL: "github-actions[bot]@users.noreply.github.com" - GIT_COMMITTER_NAME: "github-actions[bot]" - GIT_COMMITTER_EMAIL: "github-actions[bot]@users.noreply.github.com" - run: npx semantic-release From e4fa5c339ba0ac27806e8c93c77499756b4c0004 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 3 Sep 2025 18:30:53 +0530 Subject: [PATCH 077/274] ci: update GitHub workflows --- .github/helper/documentation.py | 259 ++++++++++++++++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 .github/helper/documentation.py diff --git a/.github/helper/documentation.py b/.github/helper/documentation.py new file mode 100644 index 0000000..9f787f3 --- /dev/null +++ b/.github/helper/documentation.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python3 +""" +Generic Documentation Link Checker for GitHub Pull Requests + +This script checks if PRs that add new features include proper documentation links. +Works with any repository by using environment variables for configuration. + +Usage: + python documentation.py [github_token] + +Environment Variables: + GITHUB_REPOSITORY: owner/repo format (e.g., "myorg/myproject") + GITHUB_TOKEN: GitHub API token (optional, increases rate limits) + DOCUMENTATION_DOMAINS: Comma-separated list of docs domains + DOCUMENTATION_KEYWORDS: Comma-separated keywords that indicate docs + SKIP_KEYWORDS: Comma-separated keywords that skip docs requirement +""" + +import os +import sys +from urllib.parse import urlparse + +import requests + + +def get_documentation_domains(): + """Get documentation domains from environment or use defaults.""" + env_domains = os.getenv("DOCUMENTATION_DOMAINS", "") + if env_domains: + return [domain.strip() for domain in env_domains.split(",")] + + # Default domains for common documentation sites + return [ + "docs.github.io", + "readthedocs.io", + "gitbook.io", + "notion.site", + "confluence.com", + "docs.google.com", + "frappeframework.com", + "docs.erpnext.com", + "docs.frappe.io", + ] + + +def get_documentation_keywords(): + """Get keywords that indicate documentation from environment.""" + env_keywords = os.getenv("DOCUMENTATION_KEYWORDS", "") + if env_keywords: + return [keyword.strip().lower() for keyword in env_keywords.split(",")] + + # Default keywords that indicate documentation + return [ + "docs", + "documentation", + "readme", + "guide", + "tutorial", + "wiki", + "manual", + "reference", + "api docs", + "user guide", + "developer guide", + ] + + +def get_skip_keywords(): + """Get keywords that skip documentation requirement.""" + env_keywords = os.getenv("SKIP_KEYWORDS", "") + if env_keywords: + return [keyword.strip().lower() for keyword in env_keywords.split(",")] + + # Default keywords that skip documentation requirement + return [ + "no-docs", + "skip-docs", + "no docs", + "skip docs", + "backport", + "revert", + "hotfix", + "emergency", + "internal", + "wip", + "work in progress", + ] + + +def is_valid_url(url: str) -> bool: + """Check if URL has valid structure.""" + try: + parts = urlparse(url) + return all((parts.scheme, parts.netloc, parts.path)) + except Exception: + return False + + +def is_documentation_link(word: str) -> bool: + """Check if a word/URL points to documentation.""" + if not word.startswith("http") or not is_valid_url(word): + return False + + parsed_url = urlparse(word) + documentation_domains = get_documentation_domains() + + # Check if domain is in documentation domains + for domain in documentation_domains: + if domain in parsed_url.netloc: + return True + + # Check for GitHub links to docs + if parsed_url.netloc == "github.com": + path_parts = parsed_url.path.split("/") + # Check for /owner/repo/wiki, /owner/repo/blob/main/docs, etc. + if len(path_parts) >= 4: + if "wiki" in path_parts or "docs" in parsed_url.path.lower(): + return True + + return False + + +def contains_documentation_keywords(text: str) -> bool: + """Check if text contains documentation-related keywords.""" + text_lower = text.lower() + documentation_keywords = get_documentation_keywords() + + return any(keyword in text_lower for keyword in documentation_keywords) + + +def contains_documentation_link(body: str) -> bool: + """Check if PR body contains documentation links.""" + words = [word for line in body.splitlines() for word in line.split()] + return any(is_documentation_link(word) for word in words) + + +def should_skip_documentation_check(title: str, body: str) -> bool: + """Check if documentation requirement should be skipped.""" + skip_keywords = get_skip_keywords() + combined_text = f"{title} {body}".lower() + + return any(keyword in combined_text for keyword in skip_keywords) + + +def get_github_repository(): + """Get GitHub repository from environment.""" + repo = os.getenv("GITHUB_REPOSITORY") + if not repo: + raise ValueError( + "GITHUB_REPOSITORY environment variable not set. " + "Should be in format 'owner/repo'" + ) + return repo + + +def get_github_headers(): + """Get GitHub API headers with optional authentication.""" + headers = { + "Accept": "application/vnd.github.v3+json", + "User-Agent": "Documentation-Checker/1.0" + } + + token = os.getenv("DHWANI_FRAPPE_TOKEN") + if token: + headers["Authorization"] = f"token {token}" + + return headers + + +def check_pull_request(pr_number: str) -> "tuple[int, str]": + """ + Check if a pull request includes proper documentation. + + Returns: + tuple[int, str]: (exit_code, message) + exit_code: 0 for success, 1 for failure + message: Human-readable status message + """ + try: + repository = get_github_repository() + headers = get_github_headers() + + # Fetch PR details + url = f"https://api.github.com/repos/{repository}/pulls/{pr_number}" + response = requests.get(url, headers=headers, timeout=30) + + if not response.ok: + if response.status_code == 404: + return 0, f"Pull Request #{pr_number} not found - may have been deleted or merged already. Skipping documentation check. ✅" + elif response.status_code == 403: + return 0, f"GitHub API rate limit or permissions issue. Skipping documentation check. ⚠️" + else: + return 0, f"GitHub API error: {response.status_code}. Skipping documentation check. ⚠️" + + payload = response.json() + title = (payload.get("title") or "").strip() + body = (payload.get("body") or "").strip() + head_sha = (payload.get("head") or {}).get("sha") + + # Basic validation + if not head_sha: + return 1, "Invalid pull request data! ⚠️" + + # Check if this is a feature that needs documentation + title_lower = title.lower() + if not (title_lower.startswith("feat") or "feature" in title_lower): + return 0, "Not a feature PR - skipping documentation check 🏃" + + # Check if documentation should be skipped + if should_skip_documentation_check(title, body): + return 0, "Documentation check skipped (found skip keyword) 🏃" + + # Check for documentation links + if contains_documentation_link(body): + return 0, "Documentation link found! You're awesome! 🎉" + + # Check for documentation keywords (less strict) + if contains_documentation_keywords(body): + return 0, "Documentation keywords found in PR description 📚" + + # No documentation found + return 1, ( + "Documentation not found! ⚠️\n" + "Feature PRs should include:\n" + "• Link to documentation\n" + "• Documentation keywords in description\n" + "• Or add 'no-docs' if no documentation needed" + ) + + except requests.RequestException as e: + return 1, f"Network error checking documentation: {e} ⚠️" + except Exception as e: + return 1, f"Error checking documentation: {e} ⚠️" + + +def main(): + """Main entry point.""" + if len(sys.argv) < 2: + print("Usage: python documentation.py ") + print("Environment variables:") + print(" GITHUB_REPOSITORY (required): owner/repo") + print(" GITHUB_TOKEN (optional): GitHub API token") + print(" DOCUMENTATION_DOMAINS (optional): comma-separated domains") + print(" SKIP_KEYWORDS (optional): comma-separated skip keywords") + sys.exit(1) + + pr_number = sys.argv[1] + + try: + exit_code, message = check_pull_request(pr_number) + print(message) + sys.exit(exit_code) + except ValueError as e: + print(f"Configuration error: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main() From b6c67c5a49357cf858292fc587f03d5045de2cf9 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 3 Sep 2025 18:30:55 +0530 Subject: [PATCH 078/274] ci: update GitHub workflows --- .github/helper/update-version.py | 54 ++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .github/helper/update-version.py diff --git a/.github/helper/update-version.py b/.github/helper/update-version.py new file mode 100644 index 0000000..af9a905 --- /dev/null +++ b/.github/helper/update-version.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +""" +Helper script to update version in Frappe app __init__.py files +Usage: python .github/helper/update-version.py +""" +import os +import re +import sys + +def update_version_in_init_files(new_version): + """Update version in all app __init__.py files""" + updated_files = [] + + # Look for directories that contain __init__.py (potential Frappe apps) + for item in os.listdir('.'): + if os.path.isdir(item) and item not in ['node_modules', '.git', '__pycache__', '.github']: + init_file = os.path.join(item, '__init__.py') + if os.path.exists(init_file): + try: + with open(init_file, 'r') as f: + content = f.read() + + # Update version using regex + pattern = r'__version__\s*=\s*["\'][0-9]+\.[0-9]+\.[0-9]+["\']' + replacement = f'__version__ = "{new_version}"' + + if re.search(pattern, content): + updated_content = re.sub(pattern, replacement, content) + + with open(init_file, 'w') as f: + f.write(updated_content) + + updated_files.append(init_file) + print(f"Updated version in {init_file} to {new_version}") + else: + print(f"No version found in {init_file}") + + except Exception as e: + print(f"Error updating {init_file}: {e}") + + return updated_files + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: python scripts/update-version.py ") + sys.exit(1) + + new_version = sys.argv[1] + updated_files = update_version_in_init_files(new_version) + + if updated_files: + print(f"Successfully updated version to {new_version} in {len(updated_files)} files") + else: + print("No files were updated") From bd21102802877e146f9cde0527be612e7a476712 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 3 Sep 2025 18:30:56 +0530 Subject: [PATCH 079/274] ci: update GitHub workflows --- .github/release.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .github/release.yml diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..e4ebba6 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,4 @@ +changelog: + exclude: + labels: + - skip-release-notes From 67766b0f66b496ce7f4d72bdb8ad7763d3ecefd9 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 3 Sep 2025 18:30:58 +0530 Subject: [PATCH 080/274] ci: update GitHub workflows --- .github/workflows/ci.yml | 195 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7d7b260 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,195 @@ +name: CI + +on: + push: + branches: [ main, develop, development, master ] + pull_request: + branches: [ main, develop, development, master ] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.event_name }}-${{ github.event.number || github.sha }} + cancel-in-progress: true + +jobs: + dependency-vulnerability: + name: 'Vulnerable Dependency Check' + runs-on: ubuntu-latest + + steps: + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Cache pip + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + + - name: Install and run pip-audit + run: | + pip install pip-audit + cd ${GITHUB_WORKSPACE} + pip-audit --desc on --ignore-vuln PYSEC-2023-312 . + + test: + name: 'Test' + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8", "3.11", "3.13"] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e ".[dev]" || pip install -r requirements.txt || echo "No requirements found" + + - name: Run tests + run: | + pytest --cov=. --cov-report=xml || echo "No tests found or pytest not configured" + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + file: ./coverage.xml + fail_ci_if_error: false + if: always() + + security: + name: 'Security Check' + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install security tools + run: | + python -m pip install --upgrade pip + pip install bandit safety + + - name: Run security checks + run: | + bandit -r . -f json -o bandit-report.json || true + safety check --json --output safety-report.json || true + + - name: Upload security reports + uses: actions/upload-artifact@v4 + with: + name: security-reports + path: | + bandit-report.json + safety-report.json + + dependency-update: + name: 'Dependency Update Check' + runs-on: ubuntu-latest + needs: dependency-vulnerability + if: github.event_name == 'pull_request' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: 'npm' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pip-audit + npm ci + + - name: Check for outdated Python dependencies + run: | + pip list --outdated --format=freeze | grep -v '^\-e' | cut -d = -f 1 | xargs -n1 pip show || echo "No outdated Python packages found" + + - name: Check for outdated Node.js dependencies + run: npm outdated || echo "No outdated Node.js packages found" + + - name: Run pip-audit + run: | + pip-audit --format json --output pip-audit-report.json || true + + - name: Upload audit reports + uses: actions/upload-artifact@v4 + with: + name: audit-reports + path: pip-audit-report.json + + build: + name: 'Build Check' + runs-on: ubuntu-latest + needs: [lint-and-format, test] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: 'npm' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + npm ci + + - name: Build Python package + run: | + python -m build || echo "No Python package to build" + + - name: Build frontend assets + run: | + npm run build || echo "No build script found" + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: build-artifacts + path: | + dist/ + build/ + if-no-files-found: ignore From 924dd87e8a92d4a88f828cdd2e623d6d60912568 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 3 Sep 2025 18:30:59 +0530 Subject: [PATCH 081/274] ci: update GitHub workflows --- .github/workflows/code-quality.yml | 111 +++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 .github/workflows/code-quality.yml diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml new file mode 100644 index 0000000..622be62 --- /dev/null +++ b/.github/workflows/code-quality.yml @@ -0,0 +1,111 @@ +name: Quality Checks + +on: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: commitcheck-frappe-${{ github.event_name }}-${{ github.event.number }} + cancel-in-progress: true + +jobs: + commit-lint: + name: 'Semantic Commits' + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 200 + - uses: actions/setup-node@v4 + with: + node-version: 22 + check-latest: true + + - name: Check commit titles + run: | + npm install @commitlint/cli @commitlint/config-conventional + npx commitlint --verbose --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} + + docs-required: + name: 'Documentation Required' + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - name: 'Setup Environment' + uses: actions/setup-python@v5 + with: + python-version: '3.13' + - uses: actions/checkout@v4 + + - name: Validate Docs + env: + PR_NUMBER: ${{ github.event.number }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + run: | + pip install requests --quiet + + # Check if PR number is valid + if [ -z "$PR_NUMBER" ] || [ "$PR_NUMBER" = "null" ]; then + echo "No valid PR number found. Skipping documentation check. ✅" + exit 0 + fi + + # Run documentation checker with error handling + python $GITHUB_WORKSPACE/.github/helper/documentation.py $PR_NUMBER || { + echo "Documentation checker failed, but continuing workflow. ⚠️" + exit 0 + } + + linter: + name: 'Semgrep Rules' + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.13' + cache: pip + + - name: Download Semgrep rules + run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules + + - name: Run Semgrep rules + run: | + pip install semgrep + semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness + + precommit: + name: 'Pre-Commit' + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.13' + cache: pip + - uses: pre-commit/action@v3.0.1 + - name: Check for pre-commit config + run: | + if [ ! -f .pre-commit-config.yaml ]; then + echo "Pre-commit config not found. Downloading..." + curl -o .pre-commit-config.yaml https://raw.githubusercontent.com/dhwani-ris/frappe-pre-commit/main/examples/.pre-commit-config.yaml + else + echo "Pre-commit config already exists." + fi + + - name: Install pre-commit + run: pip install pre-commit + + - name: Run pre-commit + run: pre-commit run --all-files \ No newline at end of file From 247bef3957641da39c60e91137509f143b67bcb8 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 3 Sep 2025 18:31:01 +0530 Subject: [PATCH 082/274] ci: update GitHub workflows --- .github/workflows/pr-labeler.yml | 130 +++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 .github/workflows/pr-labeler.yml diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml new file mode 100644 index 0000000..106cea1 --- /dev/null +++ b/.github/workflows/pr-labeler.yml @@ -0,0 +1,130 @@ +# .github/workflows/pr-labeler.yml +name: "PR Labeler" + +on: + pull_request: + types: [opened, reopened] + branches: [main, master, develop, development, uat] # Label PRs to these branches + +permissions: + contents: read + pull-requests: write + +jobs: + label: + runs-on: ubuntu-latest + # Only run for PRs targeting main, master, develop, development, or uat branches + if: contains(fromJson('["main", "master", "develop", "development", "uat"]'), github.event.pull_request.base.ref) + steps: + - name: Auto Label PR + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.pull_request.number, + }); + + const title = pr.title.toLowerCase(); + const targetBranch = pr.base.ref; + const labels = []; + + console.log(`Labeling PR #${pr.number} targeting ${targetBranch} branch`); + + // Skip release note for maintenance tasks + const skipPrefixes = ['chore', 'ci', 'style', 'test', 'refactor']; + const hasSkipPrefix = skipPrefixes.some(prefix => + title.startsWith(prefix + ':') || title.startsWith(prefix + '(') + ); + + if (hasSkipPrefix) { + labels.push('skip-release-notes'); + } + + // Type detection from title + if (title.includes('feat') || title.includes('feature')) { + labels.push('feature'); + } else if (title.includes('fix') || title.includes('bug')) { + labels.push('bug'); + } else if (title.includes('docs') || title.includes('doc')) { + labels.push('docs'); + } else if (title.includes('refactor')) { + labels.push('refactor'); + } + + // Add branch-specific labels + if (targetBranch === 'main' || targetBranch === 'master') { + labels.push('release'); + console.log('🚀 Production release PR'); + } else if (targetBranch === 'uat') { + labels.push('uat-release'); + console.log('🧪 UAT release PR'); + } else if (targetBranch === 'develop' || targetBranch === 'development') { + labels.push('development'); + console.log('🛠️ Development PR'); + } + + // Add labels with error handling + if (labels.length > 0) { + try { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + labels: labels + }); + + console.log(`✅ Added labels: ${labels.join(', ')}`); + } catch (error) { + if (error.status === 422) { + // Handle missing labels by creating them first + console.log('Some labels don\'t exist, creating them...'); + + const labelConfigs = { + 'skip-release-notes': { color: '6c757d', description: 'Skip in release notes' }, + 'uat-release': { color: 'fd7e14', description: 'UAT release PR' }, + 'released': { color: 'e83e8c', description: 'Production release PR' }, + 'development': { color: '17a2b8', description: 'Development branch PR' }, + 'feature': { color: '0e8a16', description: 'New feature' }, + 'bug': { color: 'd73a4a', description: 'Bug fix' }, + 'docs': { color: '0052cc', description: 'Documentation' }, + 'refactor': { color: 'fbca04', description: 'Code refactoring' } + }; + + // Create missing labels + for (const label of labels) { + if (labelConfigs[label]) { + try { + await github.rest.issues.createLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: label, + color: labelConfigs[label].color, + description: labelConfigs[label].description + }); + console.log(`🆕 Created label: ${label}`); + } catch (createError) { + console.log(`⚠️ Could not create label ${label}: ${createError.message}`); + } + } + } + + // Try adding labels again + try { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + labels: labels + }); + console.log(`✅ Added labels after creation: ${labels.join(', ')}`); + } catch (retryError) { + console.log(`⚠️ Still couldn't add labels: ${retryError.message}`); + } + } else { + console.log(`⚠️ Error adding labels: ${error.message}`); + } + } + } From 2a92e8d4a86e9fa5e557952fa7c7a2c4ca64be3a Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 3 Sep 2025 18:31:02 +0530 Subject: [PATCH 083/274] ci: update GitHub workflows --- .github/workflows/release.yml | 41 +++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..0d52218 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,41 @@ +name: Generate Semantic Release +on: + push: + branches: [ main, master ] + +permissions: + contents: write + issues: write + pull-requests: write + id-token: write + actions: read + +jobs: + release: + name: Release + runs-on: ubuntu-latest + steps: + - name: Checkout Entire Repository + uses: actions/checkout@v5 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + persist-credentials: true + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Setup dependencies + run: | + npm install @semantic-release/git @semantic-release/exec @semantic-release/github @semantic-release/changelog --no-save + + - name: Create Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GIT_AUTHOR_NAME: "github-actions[bot]" + GIT_AUTHOR_EMAIL: "github-actions[bot]@users.noreply.github.com" + GIT_COMMITTER_NAME: "github-actions[bot]" + GIT_COMMITTER_EMAIL: "github-actions[bot]@users.noreply.github.com" + run: npx semantic-release From 361df54b41f9063f9f50ad6610dea99d8e280df3 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 3 Sep 2025 18:31:04 +0530 Subject: [PATCH 084/274] ci: update GitHub workflows From e63b5de7e5e8d7cc0a3c4fe879f50d5243f611bb Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 3 Sep 2025 18:31:05 +0530 Subject: [PATCH 085/274] ci: update GitHub workflows --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4784f5b..a2d0e5e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -75,7 +75,7 @@ repos: # Frappe-specific coding standards # The frappe-pre-commit package will be installed automatically - repo: https://github.com/dhwani-ris/frappe-pre-commit - rev: v1.0.2 + rev: v1.0.3 hooks: - id: frappe-sql-security - id: frappe-doctype-naming @@ -94,4 +94,4 @@ default_language_version: ci: autoupdate_schedule: weekly skip: [] - submodules: false \ No newline at end of file + submodules: false From 3f2f0384da4d4bed144a653177323a41fe1dee78 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 3 Sep 2025 18:31:07 +0530 Subject: [PATCH 086/274] ci: update GitHub workflows From e5f92ea45cb901c976d54c239a8740d30216e7dd Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 3 Sep 2025 18:31:09 +0530 Subject: [PATCH 087/274] ci: update GitHub workflows From cd90a3b46e932f06889524f352a4dafd68771e92 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 4 Sep 2025 21:56:47 +0530 Subject: [PATCH 088/274] ci: update GitHub workflows From 2a1a9b542e9cf2d3e7b1f8f4eabeca2985a92328 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 4 Sep 2025 21:56:49 +0530 Subject: [PATCH 089/274] ci: update GitHub workflows From 5e4484b03f78e24bb8d58c1aa6943d3ec20d3518 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 4 Sep 2025 21:56:51 +0530 Subject: [PATCH 090/274] ci: update GitHub workflows From 37648d4eaa02917da14cac49ee22e1922fb0085c Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 4 Sep 2025 21:56:54 +0530 Subject: [PATCH 091/274] ci: update GitHub workflows From 81581604fdd41b7eaf3c0c6b8695368e4a590027 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 4 Sep 2025 21:56:56 +0530 Subject: [PATCH 092/274] ci: update GitHub workflows From 6d5c2073384f8a5916edf5b4f6553ae804dd5b2d Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 4 Sep 2025 21:56:57 +0530 Subject: [PATCH 093/274] ci: update GitHub workflows From bdf625a67a0a8104d32852bd825e2611a7ff674f Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 4 Sep 2025 21:56:59 +0530 Subject: [PATCH 094/274] ci: update GitHub workflows From 14ea6329e3a66fb0eb032ec5a62f9af2a59a4ba9 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 4 Sep 2025 21:57:01 +0530 Subject: [PATCH 095/274] ci: update GitHub workflows From 81f02936d10e96325cda9f132af425513041ee9a Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 4 Sep 2025 21:57:03 +0530 Subject: [PATCH 096/274] ci: update GitHub workflows From 1448eaafe697972f93e23462fc15b185536e9383 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 4 Sep 2025 21:57:05 +0530 Subject: [PATCH 097/274] ci: update GitHub workflows From d840221afa374d5390e7446e0f37cc4489c2f434 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 4 Sep 2025 21:57:08 +0530 Subject: [PATCH 098/274] ci: update GitHub workflows --- commitlint.config.js | 48 ++++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/commitlint.config.js b/commitlint.config.js index b97aaf0..9739731 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -1,26 +1,26 @@ module.exports = { - parserPreset: "conventional-changelog-conventionalcommits", - rules: { - "subject-empty": [2, "never"], - "type-case": [2, "always", "lower-case"], - "type-empty": [2, "never"], - "type-enum": [ - 2, - "always", - [ - "build", - "chore", - "ci", - "docs", - "feat", - "fix", - "perf", - "refactor", - "revert", - "style", - "test", - "deprecate", // deprecation decision - ], - ], - }, + parserPreset: "conventional-changelog-conventionalcommits", + rules: { + "subject-empty": [2, "never"], + "type-case": [2, "always", "lower-case"], + "type-empty": [2, "never"], + "type-enum": [ + 2, + "always", + [ + "build", + "chore", + "ci", + "docs", + "feat", + "fix", + "perf", + "refactor", + "revert", + "style", + "test", + "deprecate", // deprecation decision + ], + ], + }, }; From f771dd06aa53b251c6390db639737b78bf058024 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 4 Sep 2025 21:57:10 +0530 Subject: [PATCH 099/274] ci: update GitHub workflows --- .eslintrc | 248 +++++++++++++++++++++++++++--------------------------- 1 file changed, 124 insertions(+), 124 deletions(-) diff --git a/.eslintrc b/.eslintrc index c5e7d68..12d3eee 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,124 +1,124 @@ -{ - "env": { - "browser": true, - "node": true, - "es2022": true - }, - "parserOptions": { - "sourceType": "module" - }, - "extends": "eslint:recommended", - "rules": { - "indent": "off", - "brace-style": "off", - "no-mixed-spaces-and-tabs": "off", - "no-useless-escape": "off", - "space-unary-ops": ["error", { "words": true }], - "linebreak-style": "off", - "quotes": ["off"], - "semi": "off", - "camelcase": "off", - "no-unused-vars": "off", - "no-console": ["warn"], - "no-extra-boolean-cast": ["off"], - "no-control-regex": ["off"], - }, - "root": true, - "globals": { - "frappe": true, - "Vue": true, - "SetVueGlobals": true, - "__": true, - "repl": true, - "Class": true, - "locals": true, - "cint": true, - "cstr": true, - "cur_frm": true, - "cur_dialog": true, - "cur_page": true, - "cur_list": true, - "cur_tree": true, - "msg_dialog": true, - "is_null": true, - "in_list": true, - "has_common": true, - "posthog": true, - "has_words": true, - "validate_email": true, - "open_web_template_values_editor": true, - "validate_name": true, - "validate_phone": true, - "validate_url": true, - "get_number_format": true, - "format_number": true, - "format_currency": true, - "comment_when": true, - "open_url_post": true, - "toTitle": true, - "lstrip": true, - "rstrip": true, - "strip": true, - "strip_html": true, - "replace_all": true, - "flt": true, - "precision": true, - "CREATE": true, - "AMEND": true, - "CANCEL": true, - "copy_dict": true, - "get_number_format_info": true, - "strip_number_groups": true, - "print_table": true, - "Layout": true, - "web_form_settings": true, - "$c": true, - "$a": true, - "$i": true, - "$bg": true, - "$y": true, - "$c_obj": true, - "refresh_many": true, - "refresh_field": true, - "toggle_field": true, - "get_field_obj": true, - "get_query_params": true, - "unhide_field": true, - "hide_field": true, - "set_field_options": true, - "getCookie": true, - "getCookies": true, - "get_url_arg": true, - "md5": true, - "$": true, - "jQuery": true, - "moment": true, - "hljs": true, - "Awesomplete": true, - "Sortable": true, - "Showdown": true, - "Taggle": true, - "Gantt": true, - "Slick": true, - "Webcam": true, - "PhotoSwipe": true, - "PhotoSwipeUI_Default": true, - "io": true, - "JsBarcode": true, - "L": true, - "Chart": true, - "DataTable": true, - "Cypress": true, - "cy": true, - "it": true, - "describe": true, - "expect": true, - "context": true, - "before": true, - "beforeEach": true, - "after": true, - "qz": true, - "localforage": true, - "extend_cscript": true - } -} +{ + "env": { + "browser": true, + "node": true, + "es2022": true + }, + "parserOptions": { + "sourceType": "module" + }, + "extends": "eslint:recommended", + "rules": { + "indent": "off", + "brace-style": "off", + "no-mixed-spaces-and-tabs": "off", + "no-useless-escape": "off", + "space-unary-ops": ["error", { "words": true }], + "linebreak-style": "off", + "quotes": ["off"], + "semi": "off", + "camelcase": "off", + "no-unused-vars": "off", + "no-console": ["warn"], + "no-extra-boolean-cast": ["off"], + "no-control-regex": ["off"], + }, + "root": true, + "globals": { + "frappe": true, + "Vue": true, + "SetVueGlobals": true, + "__": true, + "repl": true, + "Class": true, + "locals": true, + "cint": true, + "cstr": true, + "cur_frm": true, + "cur_dialog": true, + "cur_page": true, + "cur_list": true, + "cur_tree": true, + "msg_dialog": true, + "is_null": true, + "in_list": true, + "has_common": true, + "posthog": true, + "has_words": true, + "validate_email": true, + "open_web_template_values_editor": true, + "validate_name": true, + "validate_phone": true, + "validate_url": true, + "get_number_format": true, + "format_number": true, + "format_currency": true, + "comment_when": true, + "open_url_post": true, + "toTitle": true, + "lstrip": true, + "rstrip": true, + "strip": true, + "strip_html": true, + "replace_all": true, + "flt": true, + "precision": true, + "CREATE": true, + "AMEND": true, + "CANCEL": true, + "copy_dict": true, + "get_number_format_info": true, + "strip_number_groups": true, + "print_table": true, + "Layout": true, + "web_form_settings": true, + "$c": true, + "$a": true, + "$i": true, + "$bg": true, + "$y": true, + "$c_obj": true, + "refresh_many": true, + "refresh_field": true, + "toggle_field": true, + "get_field_obj": true, + "get_query_params": true, + "unhide_field": true, + "hide_field": true, + "set_field_options": true, + "getCookie": true, + "getCookies": true, + "get_url_arg": true, + "md5": true, + "$": true, + "jQuery": true, + "moment": true, + "hljs": true, + "Awesomplete": true, + "Sortable": true, + "Showdown": true, + "Taggle": true, + "Gantt": true, + "Slick": true, + "Webcam": true, + "PhotoSwipe": true, + "PhotoSwipeUI_Default": true, + "io": true, + "JsBarcode": true, + "L": true, + "Chart": true, + "DataTable": true, + "Cypress": true, + "cy": true, + "it": true, + "describe": true, + "expect": true, + "context": true, + "before": true, + "beforeEach": true, + "after": true, + "qz": true, + "localforage": true, + "extend_cscript": true + } +} From 98ca5a597e0c8917ca9646fa2457c04b8c437b3d Mon Sep 17 00:00:00 2001 From: Bhushan Barbuddhe <141770295+bhushan-barbuddhe@users.noreply.github.com> Date: Sun, 21 Sep 2025 07:51:48 +0530 Subject: [PATCH 100/274] feat: add sidebar hover colors for app switcher and collapse link elements --- .../doctype/desk_theme/desk_theme.json | 14 ++- frappe_desk_theme/hooks.py | 9 +- ...theme.css => frappe_desk_theme.bundle.css} | 98 +++++++++++++++++++ ...k_theme.js => frappe_desk_theme.bundle.js} | 9 ++ 4 files changed, 124 insertions(+), 6 deletions(-) rename frappe_desk_theme/public/css/{frappe_desk_theme.css => frappe_desk_theme.bundle.css} (82%) rename frappe_desk_theme/public/js/{frappe_desk_theme.js => frappe_desk_theme.bundle.js} (98%) diff --git a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json index 73fc34e..7873cb8 100644 --- a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json +++ b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json @@ -53,8 +53,10 @@ "main_body_content_box_text_color", "sidebar_section", "sidebar_background_color", + "sidebar_hover_background_color", "column_break_wdml", "sidebar_text_color", + "sidebar_hover_text_color", "table_tab", "list_table_section", "table_head_background_color", @@ -503,6 +505,16 @@ "fieldname": "footer_text_color", "fieldtype": "Color", "label": "Text Color" + }, + { + "fieldname": "sidebar_hover_background_color", + "fieldtype": "Color", + "label": "Hover Background Color" + }, + { + "fieldname": "sidebar_hover_text_color", + "fieldtype": "Color", + "label": "Hover Text Color" } ], "grid_page_length": 50, @@ -510,7 +522,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-08-03 13:37:21.116029", + "modified": "2025-09-20 18:42:32.444450", "modified_by": "Administrator", "module": "Frappe Desk Theme", "name": "Desk Theme", diff --git a/frappe_desk_theme/hooks.py b/frappe_desk_theme/hooks.py index 762c6ab..0067af0 100644 --- a/frappe_desk_theme/hooks.py +++ b/frappe_desk_theme/hooks.py @@ -23,14 +23,13 @@ # Includes in # ------------------ -import time # include js, css files in header of desk.html -app_include_css = "/assets/frappe_desk_theme/css/frappe_desk_theme.css?v={}".format(time.time()) -app_include_js = "/assets/frappe_desk_theme/js/frappe_desk_theme.js?v={}".format(time.time()) +app_include_css = "/assets/frappe_desk_theme/css/frappe_desk_theme.bundle.css" +app_include_js = "/assets/frappe_desk_theme/js/frappe_desk_theme.bundle.js" # include js, css files in header of web template -web_include_css = "/assets/frappe_desk_theme/css/frappe_desk_theme.css?v={}".format(time.time()) -web_include_js = "/assets/frappe_desk_theme/js/frappe_desk_theme.js?v={}".format(time.time()) +web_include_css = "/assets/frappe_desk_theme/css/frappe_desk_theme.bundle.css" +web_include_js = "/assets/frappe_desk_theme/js/frappe_desk_theme.bundle.js" # include custom scss in every website theme (without file extension ".scss") # website_theme_scss = "frappe_desk_theme/public/scss/website" diff --git a/frappe_desk_theme/public/css/frappe_desk_theme.css b/frappe_desk_theme/public/css/frappe_desk_theme.bundle.css similarity index 82% rename from frappe_desk_theme/public/css/frappe_desk_theme.css rename to frappe_desk_theme/public/css/frappe_desk_theme.bundle.css index 6d1f099..c5476cb 100644 --- a/frappe_desk_theme/public/css/frappe_desk_theme.css +++ b/frappe_desk_theme/public/css/frappe_desk_theme.bundle.css @@ -297,12 +297,110 @@ body { color: var(--sidebar-text-color,#525252) !important; } +/* Sidebar item hover state - background and text color on hover */ +.standard-sidebar-item:hover { + background-color: var(--sidebar-hover-bg, #e9ecef) !important; + color: var(--sidebar-hover-text-color, #212529) !important; +} + +/* Sidebar item hover state - child elements inherit hover colors */ +.standard-sidebar-item:hover .item-anchor, +.standard-sidebar-item:hover .sidebar-item-label, +.standard-sidebar-item:hover .sidebar-item-icon, +.standard-sidebar-item:hover .sidebar-item-icon svg { + color: var(--sidebar-hover-text-color, #212529) !important; +} + +/* Sidebar item active state - same as hover for active items */ +.standard-sidebar-item.active-sidebar, +.standard-sidebar-item.active-sidebar:hover { + background-color: var(--sidebar-hover-bg, #e9ecef) !important; + color: var(--sidebar-hover-text-color, #212529) !important; +} + +/* Sidebar item active state - child elements inherit active colors */ +.standard-sidebar-item.active-sidebar .item-anchor, +.standard-sidebar-item.active-sidebar .sidebar-item-label, +.standard-sidebar-item.active-sidebar .sidebar-item-icon, +.standard-sidebar-item.active-sidebar .sidebar-item-icon svg, +.standard-sidebar-item.active-sidebar:hover .item-anchor, +.standard-sidebar-item.active-sidebar:hover .sidebar-item-label, +.standard-sidebar-item.active-sidebar:hover .sidebar-item-icon, +.standard-sidebar-item.active-sidebar:hover .sidebar-item-icon svg { + color: var(--sidebar-hover-text-color, #212529) !important; +} + /* Sidebar icons - SVG elements with proper fill and stroke colors */ .sidebar-item-icon svg { fill: var(--sidebar-bg,#f8f8f8) !important; stroke: var(--sidebar-text-color,#525252) !important; } +/* Sidebar icons hover state - SVG elements with hover colors */ +.standard-sidebar-item:hover .sidebar-item-icon svg, +.standard-sidebar-item.active-sidebar .sidebar-item-icon svg { + fill: var(--sidebar-hover-bg, #e9ecef) !important; + stroke: var(--sidebar-hover-text-color, #212529) !important; +} + +/* Collapse sidebar link */ +.collapse-sidebar-link { + color: var(--sidebar-text-color, #525252) !important; +} + +/* Collapse sidebar link SVG icons */ +.collapse-sidebar-link svg { + fill: var(--sidebar-text-color, #525252) !important; + stroke: var(--sidebar-text-color, #525252) !important; +} + +/* App switcher dropdown - main container styling */ +.app-switcher-dropdown { + background-color: var(--sidebar-bg, #f8f8f8) !important; + color: var(--sidebar-text-color, #525252) !important; +} + +.app-switcher-dropdown:hover { + background-color: var(--sidebar-hover-bg, #e9ecef) !important; + color: var(--sidebar-hover-text-color, #212529) !important; +} + +/* App switcher dropdown - child elements inherit colors */ +.app-switcher-dropdown .app-title, +.app-switcher-dropdown .sidebar-item-label { + color: var(--sidebar-text-color, #525252) !important; +} + +.app-switcher-dropdown:hover .app-title, +.app-switcher-dropdown:hover .sidebar-item-label { + color: var(--sidebar-hover-text-color, #212529) !important; +} + +/* App switcher menu container */ +.app-switcher-menu { + background-color: var(--sidebar-bg, #f8f8f8) !important; +} + +/* App switcher menu items */ +.app-item { + background-color: var(--sidebar-bg, #f8f8f8) !important; + color: var(--sidebar-text-color, #525252) !important; +} + +.app-item:hover { + background-color: var(--sidebar-hover-bg, #e9ecef) !important; + color: var(--sidebar-hover-text-color, #212529) !important; +} + +/* App item titles */ +.app-item-title { + color: var(--sidebar-text-color, #525252) !important; +} + +.app-item:hover .app-item-title { + color: var(--sidebar-hover-text-color, #212529) !important; +} + /* ======================================== TABLE STYLING ======================================== */ diff --git a/frappe_desk_theme/public/js/frappe_desk_theme.js b/frappe_desk_theme/public/js/frappe_desk_theme.bundle.js similarity index 98% rename from frappe_desk_theme/public/js/frappe_desk_theme.js rename to frappe_desk_theme/public/js/frappe_desk_theme.bundle.js index bb885ba..e77d5f4 100644 --- a/frappe_desk_theme/public/js/frappe_desk_theme.js +++ b/frappe_desk_theme/public/js/frappe_desk_theme.bundle.js @@ -286,6 +286,7 @@ class FrappeDeskTheme { '--btn-secondary-color', '--btn-secondary-hover-bg', '--btn-secondary-hover-color', '--body-bg', '--content-bg', '--table-head-bg', '--table-head-color', '--table-body-bg', '--table-body-color', '--hide-like-comment', '--widget-bg', '--widget-border', '--widget-color', '--sidebar-expanded', + '--sidebar-bg', '--sidebar-text-color', '--sidebar-hover-bg', '--sidebar-hover-text-color', '--login-content-border', '--login-title-display', '--login-title-after-display', '--login-title-after-justify', '--login-title-after-margin', '--login-title-after-content', '--login-title-after-color', '--login-box-top', '--login-box-bg-override', '--login-box-border-radius', '--search-bar-display', @@ -325,6 +326,8 @@ class FrappeDeskTheme { root.style.setProperty('--hide-app-switcher', 'block'); root.style.setProperty('--app-switcher-pointer-events', 'auto'); root.style.setProperty('--sidebar-expanded', ''); + root.style.setProperty('--sidebar-hover-bg', '#e9ecef'); + root.style.setProperty('--sidebar-hover-text-color', '#212529'); root.style.setProperty('--login-box-width', '400px'); root.style.setProperty('--search-bar-display', 'block'); @@ -505,6 +508,12 @@ class FrappeDeskTheme { if (theme.sidebar_text_color) { root.style.setProperty('--sidebar-text-color', theme.sidebar_text_color); } + if (theme.sidebar_hover_background_color) { + root.style.setProperty('--sidebar-hover-bg', theme.sidebar_hover_background_color); + } + if (theme.sidebar_hover_text_color) { + root.style.setProperty('--sidebar-hover-text-color', theme.sidebar_hover_text_color); + } // Data table styling if (theme.table_head_background_color) { From afb05dce419015f929bc0c54b7551a7c8bba4bae Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 5 Nov 2025 17:06:04 +0530 Subject: [PATCH 101/274] ci: update GitHub workflows From 3c84dd873a2492c4e056c630533f6ed29e0d7e73 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 5 Nov 2025 17:06:06 +0530 Subject: [PATCH 102/274] ci: update GitHub workflows From f7ff83df13cdefcf09562adca2fafb7c50a108a4 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 5 Nov 2025 17:06:07 +0530 Subject: [PATCH 103/274] ci: update GitHub workflows From fde2d1c5b60ccce98eed47a293a177a93c902bff Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 5 Nov 2025 17:06:09 +0530 Subject: [PATCH 104/274] ci: update GitHub workflows --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d7b260..3c93593 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -154,7 +154,7 @@ jobs: build: name: 'Build Check' runs-on: ubuntu-latest - needs: [lint-and-format, test] + needs: [test] steps: - name: Checkout code From 0f451b86da448596ddd64b0dd8a913d0ac68f420 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 5 Nov 2025 17:06:10 +0530 Subject: [PATCH 105/274] ci: update GitHub workflows From 0fa7039529358766b9d3c98f64dadfabb770f1df Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 5 Nov 2025 17:06:12 +0530 Subject: [PATCH 106/274] ci: update GitHub workflows From aeda696b666a370feb5c886957a48edba8b22e10 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 5 Nov 2025 17:06:13 +0530 Subject: [PATCH 107/274] ci: update GitHub workflows From 5ef0bf35592669e7604692b527e50b6604f03598 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 5 Nov 2025 17:06:15 +0530 Subject: [PATCH 108/274] ci: update GitHub workflows From 6cefc0dd259c69a8656be1b274a9afd242eb3b1a Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 5 Nov 2025 17:06:16 +0530 Subject: [PATCH 109/274] ci: update GitHub workflows --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a2d0e5e..460801c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -75,7 +75,7 @@ repos: # Frappe-specific coding standards # The frappe-pre-commit package will be installed automatically - repo: https://github.com/dhwani-ris/frappe-pre-commit - rev: v1.0.3 + rev: v1.0.4 hooks: - id: frappe-sql-security - id: frappe-doctype-naming From 706c6402dc81a5d02718eceb9d934bb7f857e12b Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 5 Nov 2025 17:06:18 +0530 Subject: [PATCH 110/274] ci: update GitHub workflows From 346ba355dd5a76f3459c5eec15b9178795b4acad Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 5 Nov 2025 17:06:19 +0530 Subject: [PATCH 111/274] ci: update GitHub workflows From 2cbcdcceca44c53eca4cb9c54f718152f1e52b43 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 5 Nov 2025 17:06:21 +0530 Subject: [PATCH 112/274] ci: update GitHub workflows From 3ccf690ab82217428816a12b6faa3854c6eeca90 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 6 Nov 2025 17:47:51 +0530 Subject: [PATCH 113/274] ci: update GitHub workflows From d73fb29526eb2f09d299356706ba573265dcc35e Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 6 Nov 2025 17:47:53 +0530 Subject: [PATCH 114/274] ci: update GitHub workflows From 32a660c931bd157d29a8bd16fc9f71f39b3f516e Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 6 Nov 2025 17:47:55 +0530 Subject: [PATCH 115/274] ci: update GitHub workflows From 01c68b25eac8d19e253bf4feac6778d7134edfd0 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 6 Nov 2025 17:47:56 +0530 Subject: [PATCH 116/274] ci: update GitHub workflows --- .github/workflows/ci.yml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c93593..212da06 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -126,20 +126,20 @@ jobs: uses: actions/setup-node@v4 with: node-version: 22 - cache: 'npm' - name: Install dependencies run: | python -m pip install --upgrade pip pip install pip-audit - npm ci + if [ -f package-lock.json ] || [ -f npm-shrinkwrap.json ]; then npm ci; else echo "No npm lockfile, skipping npm ci"; fi - name: Check for outdated Python dependencies run: | pip list --outdated --format=freeze | grep -v '^\-e' | cut -d = -f 1 | xargs -n1 pip show || echo "No outdated Python packages found" - name: Check for outdated Node.js dependencies - run: npm outdated || echo "No outdated Node.js packages found" + run: | + if [ -f package.json ]; then npm outdated || echo "No outdated Node.js packages found"; else echo "No package.json, skipping npm outdated"; fi - name: Run pip-audit run: | @@ -169,13 +169,12 @@ jobs: uses: actions/setup-node@v4 with: node-version: 22 - cache: 'npm' - name: Install dependencies run: | python -m pip install --upgrade pip pip install build - npm ci + if [ -f package-lock.json ] || [ -f npm-shrinkwrap.json ]; then npm ci; else echo "No npm lockfile, skipping npm ci"; fi - name: Build Python package run: | @@ -183,7 +182,7 @@ jobs: - name: Build frontend assets run: | - npm run build || echo "No build script found" + if [ -f package.json ]; then npm run build || echo "No build script found"; else echo "No package.json, skipping frontend build"; fi - name: Upload build artifacts uses: actions/upload-artifact@v4 From ad532aee1acb35a38a3b1b683fa0359ac44bc832 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 6 Nov 2025 17:47:58 +0530 Subject: [PATCH 117/274] ci: update GitHub workflows From c24ce8c8d3b16710cfe6c88d99990893ecaef3ee Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 6 Nov 2025 17:48:00 +0530 Subject: [PATCH 118/274] ci: update GitHub workflows From f27e443a69d603837dc5705bd9c00b86c86a4397 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 6 Nov 2025 17:48:01 +0530 Subject: [PATCH 119/274] ci: update GitHub workflows From 4f075131ed8a2fe5dab7df7225e1ad682b578810 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 6 Nov 2025 17:48:03 +0530 Subject: [PATCH 120/274] ci: update GitHub workflows From 1ab675d6c4bef790a98dfd624bcf416b6b059345 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 6 Nov 2025 17:48:05 +0530 Subject: [PATCH 121/274] ci: update GitHub workflows From f42b3046ee382e661798315910ffda5659b9c7e3 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 6 Nov 2025 17:48:06 +0530 Subject: [PATCH 122/274] ci: update GitHub workflows From 9a97884caef4c8258d4536112bee03341ee558ac Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 6 Nov 2025 17:48:08 +0530 Subject: [PATCH 123/274] ci: update GitHub workflows From 05ddd572482906fb6210761e5b3b7a36afc1dc31 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 6 Nov 2025 17:48:09 +0530 Subject: [PATCH 124/274] ci: update GitHub workflows From 8cce206f63789a800df73e76806ed3a79891230d Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 6 Nov 2025 19:13:03 +0530 Subject: [PATCH 125/274] ci: update GitHub workflows From 2e0905c288a7a46c5978aabb6d9eb7e9ac7d35f4 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 6 Nov 2025 19:13:05 +0530 Subject: [PATCH 126/274] ci: update GitHub workflows From 9c0c21c4183108a579d246e19eb9897007bc216d Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 6 Nov 2025 19:13:06 +0530 Subject: [PATCH 127/274] ci: update GitHub workflows From 05d97e48bccd9f5224ab74de175d30f9b58bd7bf Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 6 Nov 2025 19:13:08 +0530 Subject: [PATCH 128/274] ci: update GitHub workflows From 963c4da908e0189efc9277df69964926053522d2 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 6 Nov 2025 19:13:10 +0530 Subject: [PATCH 129/274] ci: update GitHub workflows From 9131d170dae81e1c4f1b7be9b669190630e8954c Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 6 Nov 2025 19:13:11 +0530 Subject: [PATCH 130/274] ci: update GitHub workflows From 4f022d1376402affbbb480a26a78425d30d95456 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 6 Nov 2025 19:13:13 +0530 Subject: [PATCH 131/274] ci: update GitHub workflows From 056d2076ec62be0628130d69a605d4ced5372359 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 6 Nov 2025 19:13:15 +0530 Subject: [PATCH 132/274] ci: update GitHub workflows From 5b126789088c2923b7532386c1acaee14d64b3c1 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 6 Nov 2025 19:13:16 +0530 Subject: [PATCH 133/274] ci: update GitHub workflows From dfb9161429b2cf7b03e5f087a26b684645e8f5e1 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 6 Nov 2025 19:13:18 +0530 Subject: [PATCH 134/274] ci: update GitHub workflows From 8df08cc8ce3c4bc2cd1f359cfbc8acf79689eb29 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 6 Nov 2025 19:13:19 +0530 Subject: [PATCH 135/274] ci: update GitHub workflows From 91d737f9614e903b4a6b3209b444851ab8c9507b Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 6 Nov 2025 19:13:21 +0530 Subject: [PATCH 136/274] ci: update GitHub workflows From 7b61b01cfdfd375b9417855ac63374921a77b053 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 6 Nov 2025 19:14:35 +0530 Subject: [PATCH 137/274] ci: update GitHub workflows From 8dadad4c137d7b89ce056cbdfb81d1cccc5e254f Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 6 Nov 2025 19:14:36 +0530 Subject: [PATCH 138/274] ci: update GitHub workflows From 049c24be0eeffd3ac027b8585183c5974c146541 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 6 Nov 2025 19:14:38 +0530 Subject: [PATCH 139/274] ci: update GitHub workflows From f5b2f4d9a9041475e8ad7ff088ac21a3647d8732 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 6 Nov 2025 19:14:39 +0530 Subject: [PATCH 140/274] ci: update GitHub workflows --- .github/workflows/ci.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 212da06..3d62c85 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,11 +1,13 @@ name: CI on: + # Workflow disabled - uncomment below to re-enable + # push: + # branches: [ main, develop, development, master ] + # workflow_dispatch: push: - branches: [ main, develop, development, master ] - pull_request: - branches: [ main, develop, development, master ] - workflow_dispatch: + branches: + - 'never-run-this-workflow-disabled-branch-12345' permissions: contents: read From 9a240825d72b3e234166b3549874f1e99b0bdb76 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 6 Nov 2025 19:14:41 +0530 Subject: [PATCH 141/274] ci: update GitHub workflows From 873690b772aac7a3ccb144782c057900505f860f Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 6 Nov 2025 19:14:43 +0530 Subject: [PATCH 142/274] ci: update GitHub workflows From ec67b6e35228df381f69002824b100fb562034d5 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 6 Nov 2025 19:14:44 +0530 Subject: [PATCH 143/274] ci: update GitHub workflows From 027f9c8a0842cb8c5aaa3e6ee116dc0277a1a6a6 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 6 Nov 2025 19:14:46 +0530 Subject: [PATCH 144/274] ci: update GitHub workflows From 4d7127d32b09521a67e94ca0e38429368f91e2fd Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 6 Nov 2025 19:14:47 +0530 Subject: [PATCH 145/274] ci: update GitHub workflows From 827b2c3c06e5741de2d1e3a7689975d1484376f9 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 6 Nov 2025 19:14:49 +0530 Subject: [PATCH 146/274] ci: update GitHub workflows From 11b5380857832486dcb536c62cb48566bbffecee Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 6 Nov 2025 19:14:50 +0530 Subject: [PATCH 147/274] ci: update GitHub workflows From 017128c140806d44bdf54c8323c7868364fcd727 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 6 Nov 2025 19:14:52 +0530 Subject: [PATCH 148/274] ci: update GitHub workflows From 9ffab38eed25977875bbd6bf7542b1d330f8dd77 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 6 Nov 2025 19:17:00 +0530 Subject: [PATCH 149/274] ci: update GitHub workflows From e099e9f4da3248fabbc42c6b0e96a87b63ce8c58 Mon Sep 17 00:00:00 2001 From: sureshdhwaniris123 Date: Fri, 14 Nov 2025 16:51:05 +0530 Subject: [PATCH 150/274] fix: Frappe sidebase issue resolved (#6) * fix: Frappe sidebase issue resolved * feat: implement custom sidebar behavior and breadcrumb updates * refactor: pre-commit issue resolved * fix: improve error message for carousel image size validation --------- Co-authored-by: Bhushan Barbuddhe --- commitlint.config.js | 52 +- frappe_desk_theme/api.py | 48 +- .../doctype/desk_theme/desk_theme.js | 34 +- .../doctype/desk_theme/desk_theme.json | 6 +- .../doctype/desk_theme/desk_theme.py | 20 +- .../doctype/desk_theme/test_desk_theme.py | 2 - .../desk_theme_carousel_images.py | 15 +- frappe_desk_theme/hooks.py | 8 +- .../public/css/frappe_desk_theme.bundle.css | 18 +- .../public/js/frappe_desk_theme.bundle.js | 2027 +++++++++-------- .../js/sidebar/breadcrumb_override.bundle.js | 70 + .../js/sidebar/sidebar_override.bundle.js | 79 + .../templates/includes/desk_footer.html | 2 +- 13 files changed, 1321 insertions(+), 1060 deletions(-) create mode 100644 frappe_desk_theme/public/js/sidebar/breadcrumb_override.bundle.js create mode 100644 frappe_desk_theme/public/js/sidebar/sidebar_override.bundle.js diff --git a/commitlint.config.js b/commitlint.config.js index 9739731..0c582f5 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -1,26 +1,26 @@ -module.exports = { - parserPreset: "conventional-changelog-conventionalcommits", - rules: { - "subject-empty": [2, "never"], - "type-case": [2, "always", "lower-case"], - "type-empty": [2, "never"], - "type-enum": [ - 2, - "always", - [ - "build", - "chore", - "ci", - "docs", - "feat", - "fix", - "perf", - "refactor", - "revert", - "style", - "test", - "deprecate", // deprecation decision - ], - ], - }, -}; +module.exports = { + parserPreset: "conventional-changelog-conventionalcommits", + rules: { + "subject-empty": [2, "never"], + "type-case": [2, "always", "lower-case"], + "type-empty": [2, "never"], + "type-enum": [ + 2, + "always", + [ + "build", + "chore", + "ci", + "docs", + "feat", + "fix", + "perf", + "refactor", + "revert", + "style", + "test", + "deprecate", // deprecation decision + ], + ], + }, +}; diff --git a/frappe_desk_theme/api.py b/frappe_desk_theme/api.py index c23cfec..6c3dbe7 100644 --- a/frappe_desk_theme/api.py +++ b/frappe_desk_theme/api.py @@ -1,31 +1,33 @@ import frappe from frappe import _ + @frappe.whitelist(allow_guest=True) def get_custom_theme(): - theme = frappe.get_doc("Desk Theme") - data = theme.as_dict() - # Add carousel data if present - carousel_data = theme.get_carousel_data() if hasattr(theme, 'get_carousel_data') else None - if carousel_data: - data["carousel"] = carousel_data - return data + theme = frappe.get_doc("Desk Theme") + data = theme.as_dict() + # Add carousel data if present + carousel_data = theme.get_carousel_data() if hasattr(theme, "get_carousel_data") else None + if carousel_data: + data["carousel"] = carousel_data + return data + @frappe.whitelist(allow_guest=True) def get_footer_html(): - """Get rendered footer HTML template with theme data""" - try: - theme = frappe.get_doc("Desk Theme") - - # Prepare context for template - context = { - 'copyright_text': theme.copyright_text, - 'footer_powered_by': theme.footer_powered_by, - 'sticky_footer': theme.sticky_footer - } - - # Render the template - return frappe.render_template("frappe_desk_theme/templates/includes/desk_footer.html", context) - except Exception as e: - frappe.log_error(f"Error rendering footer template: {str(e)}") - return "" \ No newline at end of file + """Get rendered footer HTML template with theme data""" + try: + theme = frappe.get_doc("Desk Theme") + + # Prepare context for template + context = { + "copyright_text": theme.copyright_text, + "footer_powered_by": theme.footer_powered_by, + "sticky_footer": theme.sticky_footer, + } + + # Render the template + return frappe.render_template("frappe_desk_theme/templates/includes/desk_footer.html", context) + except Exception as e: + frappe.log_error(f"Error rendering footer template: {e!s}") + return "" diff --git a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.js b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.js index 751c19e..0066aa5 100644 --- a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.js +++ b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.js @@ -21,22 +21,22 @@ frappe.ui.form.on("Desk Theme", { method: "frappe.client.get_value", args: { doctype: "System Settings", - fieldname: "default_app" + fieldname: "default_app", }, - callback: function(r) { + callback: function (r) { if (r.message && r.message.default_app) { frm.set_value("default_app", r.message.default_app); } - } + }, }); } - // Add refresh theme button - frm.add_custom_button(__('Refresh Theme'), function() { - window.frappeDeskTheme?.clearCache(); - window.frappeDeskTheme?.refreshTheme(); - frappe.show_alert({message: __('Theme refreshed'), indicator: 'green'}); - }); + // Add refresh theme button + frm.add_custom_button(__("Refresh Theme"), function () { + window.frappeDeskTheme?.clearCache(); + window.frappeDeskTheme?.refreshTheme(); + frappe.show_alert({ message: __("Theme refreshed"), indicator: "green" }); + }); }, hide_app_switcher(frm) { @@ -46,13 +46,13 @@ frappe.ui.form.on("Desk Theme", { method: "frappe.client.get_value", args: { doctype: "System Settings", - fieldname: "default_app" + fieldname: "default_app", }, - callback: function(r) { + callback: function (r) { if (r.message && r.message.default_app) { frm.set_value("default_app", r.message.default_app); } - } + }, }); } else { // Clear default_app when hide_app_switcher is unchecked @@ -73,17 +73,17 @@ frappe.ui.form.on("Desk Theme", { frappe.call({ method: "frappe_desk_theme.frappe_desk_theme.doctype.desk_theme.desk_theme.update_system_default_app", args: { - default_app: frm.doc.default_app + default_app: frm.doc.default_app, }, - callback: function(r) { + callback: function (r) { if (r.message && r.message.success) { frappe.show_alert({ message: __("System default app updated successfully"), - indicator: "green" + indicator: "green", }); } - } + }, }); } - } + }, }); diff --git a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json index 7873cb8..8e2069c 100644 --- a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json +++ b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json @@ -346,7 +346,7 @@ "default": "0", "fieldname": "disable_card_view_on_mobile_view", "fieldtype": "Check", - "label": "Disable card view on mobile view" + "label": "Disable Card View On Mobile View" }, { "fieldname": "input_tab", @@ -408,7 +408,7 @@ "depends_on": "eval:doc.disable_card_view_on_mobile_view == 0", "fieldname": "disable_flex_card_content_on_mobile_view", "fieldtype": "Check", - "label": "Disable flex card content on mobile view" + "label": "Disable Flex Card Content On Mobile View" }, { "fieldname": "main_body_content_box_text_color", @@ -479,7 +479,7 @@ "description": "Custom powered by text for footer", "fieldname": "footer_powered_by", "fieldtype": "Data", - "label": "Footer \"Powered By\"" + "label": "Footer Powered By" }, { "depends_on": "eval:doc.page_background_type==\"Carousel\"", diff --git a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.py b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.py index c614776..8c80b69 100644 --- a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.py +++ b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.py @@ -22,7 +22,7 @@ def on_update(self): # Update system settings with the selected default app if self.hide_app_switcher and self.default_app: update_system_default_app(self.default_app) - + # Update website settings with footer information self.update_website_settings() @@ -30,20 +30,20 @@ def update_website_settings(self): """Update Website Settings with copyright and powered by text from Desk Theme""" try: website_settings = frappe.get_single("Website Settings") - + # Update copyright text if provided if self.copyright_text: website_settings.copyright = self.copyright_text - + # Update footer powered by text if provided if self.footer_powered_by: website_settings.footer_powered = self.footer_powered_by - + # Save without triggering permissions check website_settings.save(ignore_permissions=True) - + except Exception as e: - frappe.log_error(f"Error updating website settings: {str(e)}") + frappe.log_error(f"Error updating website settings: {e!s}") def get_carousel_data(self): """Return carousel images and config for API""" @@ -65,13 +65,13 @@ def update_system_default_app(default_app): installed_apps = frappe.get_installed_apps() if default_app not in installed_apps: frappe.throw(f"App '{default_app}' is not installed") - + # Update system settings system_settings = frappe.get_single("System Settings") system_settings.default_app = default_app system_settings.save(ignore_permissions=True) - + return {"success": True} except Exception as e: - frappe.log_error(f"Error updating system default app: {str(e)}") - frappe.throw(f"Failed to update system default app: {str(e)}") + frappe.log_error(f"Error updating system default app: {e!s}") + frappe.throw(f"Failed to update system default app: {e!s}") diff --git a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/test_desk_theme.py b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/test_desk_theme.py index 3b58e1d..4153c63 100644 --- a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/test_desk_theme.py +++ b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/test_desk_theme.py @@ -4,7 +4,6 @@ # import frappe from frappe.tests import IntegrationTestCase - # On IntegrationTestCase, the doctype test records and all # link-field test record dependencies are recursively loaded # Use these module variables to add/remove to/from that list @@ -12,7 +11,6 @@ IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"] - class IntegrationTestDeskTheme(IntegrationTestCase): """ Integration tests for DeskTheme. diff --git a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme_carousel_images/desk_theme_carousel_images.py b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme_carousel_images/desk_theme_carousel_images.py index 3db7109..0a68fd7 100644 --- a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme_carousel_images/desk_theme_carousel_images.py +++ b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme_carousel_images/desk_theme_carousel_images.py @@ -1,14 +1,15 @@ # Copyright (c) 2025, Dhwani RIS and contributors # For license information, please see license.txt -# import frappe +import frappe +from frappe import _ from frappe.model.document import Document class DeskThemeCarouselImages(Document): - def validate(self): - # Validate image size (max 5 MB) - if self.image: - file_doc = frappe.get_doc('File', {'file_url': self.image}) - if file_doc.file_size > 1 * 1024 * 1024: - frappe.throw('Carousel image size must be 1 MB or less.') + def validate(self): + # Validate image size (max 5 MB) + if self.image: + file_doc = frappe.get_doc("File", {"file_url": self.image}) + if file_doc.file_size > 1 * 1024 * 1024: + frappe.throw(_("Carousel image size must be 1 MB or less.")) diff --git a/frappe_desk_theme/hooks.py b/frappe_desk_theme/hooks.py index 0067af0..8319d2f 100644 --- a/frappe_desk_theme/hooks.py +++ b/frappe_desk_theme/hooks.py @@ -25,8 +25,13 @@ # ------------------ # include js, css files in header of desk.html app_include_css = "/assets/frappe_desk_theme/css/frappe_desk_theme.bundle.css" -app_include_js = "/assets/frappe_desk_theme/js/frappe_desk_theme.bundle.js" +# app_include_js = "/assets/frappe_desk_theme/js/frappe_desk_theme.bundle.js" +app_include_js = [ + "/assets/frappe_desk_theme/js/frappe_desk_theme.bundle.js", + "/assets/frappe_desk_theme/js/sidebar/sidebar_override.bundle.js", + "/assets/frappe_desk_theme/js/sidebar/breadcrumb_override.bundle.js", +] # include js, css files in header of web template web_include_css = "/assets/frappe_desk_theme/css/frappe_desk_theme.bundle.css" web_include_js = "/assets/frappe_desk_theme/js/frappe_desk_theme.bundle.js" @@ -235,4 +240,3 @@ # default_log_clearing_doctypes = { # "Logging DocType Name": 30 # days to retain logs # } - diff --git a/frappe_desk_theme/public/css/frappe_desk_theme.bundle.css b/frappe_desk_theme/public/css/frappe_desk_theme.bundle.css index c5476cb..020ba77 100644 --- a/frappe_desk_theme/public/css/frappe_desk_theme.bundle.css +++ b/frappe_desk_theme/public/css/frappe_desk_theme.bundle.css @@ -1,9 +1,9 @@ /** * Frappe Desk Theme - Custom CSS Styling - * + * * This stylesheet provides comprehensive theming support for Frappe Desk through CSS custom properties. * It covers login page customization, navigation styling, form elements, tables, widgets, and responsive design. - * + * * The theme system works by: * 1. JavaScript sets CSS custom properties (--variable-name) based on theme configuration * 2. CSS rules use these variables with var() function for dynamic styling @@ -121,7 +121,7 @@ label.control-label { /* Login content border - customizable border for form container */ /* .login-content, .forgot-content, .signup-content { border: var(--login-content-border,none); - box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1); + box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1); } */ /* Signup message - when app details are inside the box, make it part of the container */ @@ -151,7 +151,7 @@ label.control-label { /* Page headings - consistent styling for all page titles */ .page-card-head h4 { color: var(--page-heading-color) !important; -} +} /* ======================================== NAVIGATION BAR STYLING @@ -462,7 +462,7 @@ div.level-right { .widget.dashboard-widget-box { background-color: var(--widget-bg); border: 2px solid var(--widget-border); - box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1); + box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1); } /* Widget content - all text elements within widgets */ @@ -484,7 +484,7 @@ div.level-right { .for-login { position: static; } - + /* Login form width - allows full width on mobile devices */ .login-content.page-card { width: auto; @@ -610,7 +610,7 @@ body.has-sticky-footer:not(:has(.main-section)) { .desk-footer { margin-left: 0 !important; } - + .desk-footer.sticky { left: 0 !important; } @@ -623,12 +623,12 @@ body.has-sticky-footer:not(:has(.main-section)) { padding: 12px 15px; gap: 8px; } - + .desk-footer-left, .desk-footer-right { justify-content: center; } - + .main-section.has-sticky-footer, body.has-sticky-footer .main-section, body.has-sticky-footer:not(:has(.main-section)) { diff --git a/frappe_desk_theme/public/js/frappe_desk_theme.bundle.js b/frappe_desk_theme/public/js/frappe_desk_theme.bundle.js index e77d5f4..3672609 100644 --- a/frappe_desk_theme/public/js/frappe_desk_theme.bundle.js +++ b/frappe_desk_theme/public/js/frappe_desk_theme.bundle.js @@ -4,968 +4,1075 @@ * Supports dynamic theme changes, user role-based hiding, and real-time DOM updates */ class FrappeDeskTheme { - constructor() { - // Store theme configuration data from server - this.themeData = null; - // Cache configuration - this.cacheKey = 'frappe_desk_theme_cache'; - this.footerCacheStorageKey = 'frappe_desk_theme_footer_cache'; - this.cacheTimeout = 30 * 24 * 60 * 60 * 1000; // 30 days (1 month) in milliseconds - // Footer creation throttling and caching - this.footerCreating = false; - this.footerHtmlCache = null; - this.footerCacheKey = null; // Track what theme data the footer was cached for - this.stickyFooterListenerSetup = false; - this.init(); - } - - /** - * Initialize the theme system - * First applies cached theme immediately, then loads fresh data if needed - * Uses async/await pattern with graceful error handling - */ - async init() { - try { - // Apply cached theme immediately to prevent flickering - this.applyCachedTheme(); - - // Load fresh theme data if needed (async) - await this.loadThemeIfNeeded(); - - // Apply fresh theme if we got new data - if (this.themeData) { - this.applyTheme(); - } - - this.setupEventListeners(); - } catch (error) { - // Production-ready silent fail - apply default theme and show login box - this.applyTheme(); - this.showLoginBoxFallback(); - } - } - - /** - * Fallback method to show login box if theme loading fails - * Ensures login form is always visible even if theme fails to load - */ - showLoginBoxFallback() { - const loginBox = document.querySelector('.for-login'); - if (loginBox && !loginBox.classList.contains('theme-ready')) { - setTimeout(() => { - loginBox.classList.add('theme-ready'); - }, 100); - } - } - - /** - * Apply cached theme immediately to prevent UI flickering - */ - applyCachedTheme() { - const cachedData = this.getCachedTheme(); - if (cachedData && cachedData.data) { - this.themeData = cachedData.data; - this.applyTheme(); - } else { - // No cached theme, but still show login box to prevent indefinite hiding - this.showLoginBoxFallback(); - } - } - - /** - * Get cached theme data from localStorage - * @returns {Object|null} Cached theme data with timestamp - */ - getCachedTheme() { - try { - const cached = localStorage.getItem(this.cacheKey); - return cached ? JSON.parse(cached) : null; - } catch (error) { - return null; - } - } - - /** - * Save theme data to localStorage with timestamp - * @param {Object} themeData Theme configuration data - */ - setCachedTheme(themeData) { - try { - const cacheData = { - data: themeData, - timestamp: Date.now(), - version: 1 // Increment this when theme structure changes - }; - localStorage.setItem(this.cacheKey, JSON.stringify(cacheData)); - } catch (error) { - // localStorage might be full or disabled - } - } - - /** - * Check if cached theme is still valid - * @returns {boolean} True if cache is valid and not expired - */ - isCacheValid() { - const cachedData = this.getCachedTheme(); - if (!cachedData) return false; - - const now = Date.now(); - const cacheAge = now - cachedData.timestamp; - - return cacheAge < this.cacheTimeout; // 30 days - } - - /** - * Load theme only if cache is invalid or doesn't exist - */ - async loadThemeIfNeeded() { - // Skip API call if cache is still valid - if (this.isCacheValid()) { - return; - } - - await this.loadTheme(); - } - - /** - * Load theme configuration from server API - * Fetches custom theme data via REST API endpoint - * Handles response parsing and error states - */ - async loadTheme() { - try { - const response = await fetch('/api/method/frappe_desk_theme.api.get_custom_theme', { - method: 'GET', - headers: { - 'Accept': 'application/json', - } - }); - - if (!response.ok) { - throw new Error(`HTTP error! Status: ${response.status}`); - } - - const data = await response.json(); - // Handle different response formats - some APIs wrap data in 'message' property - this.themeData = data?.message || data; - - if (!this.themeData) { - throw new Error('No theme data received'); - } - - // Cache the new theme data - this.setCachedTheme(this.themeData); - - } catch (error) { - // If API fails, try to use cached data as fallback - const cachedData = this.getCachedTheme(); - if (cachedData && cachedData.data) { - this.themeData = cachedData.data; - } else { - throw error; - } - } - } - - /** - * Force refresh theme from server (ignores cache) - * Useful for manual theme updates or admin changes - */ - async refreshTheme() { - try { - // Clear footer cache to ensure fresh data - this.footerHtmlCache = null; - this.footerCacheKey = null; - - await this.loadTheme(); - this.applyTheme(); - - // Dispatch event for other components - document.dispatchEvent(new CustomEvent('themeRefreshed', { - detail: { themeData: this.themeData } - })); - } catch (error) { - // Silent fail - theme refresh errors should not interrupt user experience - } - } - - /** - * Save footer cache to localStorage - */ - saveFooterCache(footerHtml, cacheKey) { - try { - const cacheData = { - html: footerHtml, - key: cacheKey, - timestamp: Date.now() - }; - localStorage.setItem(this.footerCacheStorageKey, JSON.stringify(cacheData)); - } catch (error) { - // localStorage might be full or disabled - } - } - - /** - * Load footer cache from localStorage - */ - loadFooterCache() { - try { - const cached = localStorage.getItem(this.footerCacheStorageKey); - if (!cached) return null; - - const cacheData = JSON.parse(cached); - const now = Date.now(); - const cacheAge = now - cacheData.timestamp; - - // Return cached data if it's still valid (within 30-day timeout) - if (cacheAge < this.cacheTimeout) { - return cacheData; - } else { - // Remove expired cache - localStorage.removeItem(this.footerCacheStorageKey); - return null; - } - } catch (error) { - return null; - } - } - - /** - * Clear theme cache (useful for debugging or forced refresh) - */ - clearCache() { - try { - localStorage.removeItem(this.cacheKey); - localStorage.removeItem(this.footerCacheStorageKey); - // Also clear footer cache - this.footerHtmlCache = null; - this.footerCacheKey = null; - } catch (error) { - // Ignore localStorage errors - } - } - - /** - * Check if current user's roles match hide_search configuration - * Used to conditionally hide search bar based on user permissions - * @returns {boolean} True if search should be hidden for current user - */ - getUserRoles() { - const currentUser = frappe?.boot?.user?.roles; - // Exit early if no user roles or no hide_search config - if (!currentUser || !this.themeData?.hide_search) { - return false; - } - - // Special handling for Administrator role - if (currentUser.includes('Administrator')) { - return this.themeData.hide_search.some(u => u.role === 'Administrator'); - } - - // Check if any user role matches hide_search configuration - return currentUser.some(role => - this.themeData.hide_search.some(u => u.role === role) - ); - } - - /** - * Clear all theme-related CSS custom properties from document root - * Used to reset theme state before applying new theme values - * Ensures clean slate for theme updates - */ - clearCSSVariables() { - const root = document.documentElement; - // Comprehensive list of all theme CSS variables - const cssVariables = [ - '--login-bg-color', '--login-bg-image', '--login-box-position', '--login-box-right', '--login-box-left', - '--login-btn-bg', '--login-btn-color', '--login-btn-hover-bg', '--login-btn-hover-color', - '--login-box-bg', '--page-heading-color', '--input-bg', '--input-color', '--input-border', - '--input-label-color', '--navbar-bg', '--navbar-color', '--hide-help', '--btn-primary-bg', - '--btn-primary-color', '--btn-primary-hover-bg', '--btn-primary-hover-color', '--btn-secondary-bg', - '--btn-secondary-color', '--btn-secondary-hover-bg', '--btn-secondary-hover-color', '--body-bg', - '--content-bg', '--table-head-bg', '--table-head-color', '--table-body-bg', '--table-body-color', - '--hide-like-comment', '--widget-bg', '--widget-border', '--widget-color', '--sidebar-expanded', - '--sidebar-bg', '--sidebar-text-color', '--sidebar-hover-bg', '--sidebar-hover-text-color', - '--login-content-border', '--login-title-display', '--login-title-after-display', - '--login-title-after-justify', '--login-title-after-margin', '--login-title-after-content', '--login-title-after-color', - '--login-box-top', '--login-box-bg-override', '--login-box-border-radius', '--search-bar-display', - '--navbar-toggler-border', '--breadcrumb-disabled-color', '--help-nav-link-color', '--help-nav-link-stroke', - '--hide-app-switcher', '--app-switcher-pointer-events', '--footer-bg', '--footer-color', '--footer-border', - '--footer-display', '--footer-powered-color', '--footer-link-color', '--footer-link-hover-color', - '--carousel-fade-opacity', '--login-bg-carousel-image' - ]; - - // Remove each CSS variable from document root - cssVariables.forEach(variable => { - root.style.removeProperty(variable); - }); - } - - /** - * Set default CSS variable values - * Provides fallback values when theme configuration is missing or incomplete - * Ensures UI remains functional even without complete theme data - */ - setDefaultCSSVariables() { - const root = document.documentElement; - - // Login page defaults - ensures login form remains usable - root.style.setProperty('--login-box-position', 'static'); - root.style.setProperty('--login-box-right', 'auto'); - root.style.setProperty('--login-box-left', 'auto'); - root.style.setProperty('--login-box-top', '18%'); - root.style.setProperty('--login-box-bg', '#fff'); - root.style.setProperty('--login-content-border', '2px solid #d1d8dd'); - root.style.setProperty('--login-title-display', 'block'); - root.style.setProperty('--login-title-after-display', 'none'); - - // UI element visibility defaults - root.style.setProperty('--hide-help', 'block'); - root.style.setProperty('--hide-like-comment', 'block'); - root.style.setProperty('--hide-app-switcher', 'block'); - root.style.setProperty('--app-switcher-pointer-events', 'auto'); - root.style.setProperty('--sidebar-expanded', ''); - root.style.setProperty('--sidebar-hover-bg', '#e9ecef'); - root.style.setProperty('--sidebar-hover-text-color', '#212529'); - root.style.setProperty('--login-box-width', '400px'); - root.style.setProperty('--search-bar-display', 'block'); - - // Navigation and UI component defaults - root.style.setProperty('--navbar-toggler-border', '#dee2e6'); - root.style.setProperty('--breadcrumb-disabled-color', '#6c757d'); - root.style.setProperty('--help-nav-link-color', 'inherit'); - root.style.setProperty('--help-nav-link-stroke', 'currentColor'); - - // Footer defaults - root.style.setProperty('--footer-display', 'flex'); - root.style.setProperty('--footer-bg', '#f8f9fa'); - root.style.setProperty('--footer-color', '#495057'); - root.style.setProperty('--footer-border', '#dee2e6'); - root.style.setProperty('--footer-powered-color', '#6c757d'); - root.style.setProperty('--footer-link-color', '#007bff'); - root.style.setProperty('--footer-link-hover-color', '#0056b3'); - - // Carousel fade default - root.style.setProperty('--carousel-fade-opacity', '1'); - } - - /** - * Apply theme configuration to CSS custom properties - * Maps theme data fields to corresponding CSS variables - * Only sets variables when theme values are provided (conditional application) - */ - setCSSVariables() { - const root = document.documentElement; - const theme = this.themeData; - - // Reset all variables to clean state - this.clearCSSVariables(); - - // Establish default values first - this.setDefaultCSSVariables(); - - // Login page background customization - if (theme.carousel && theme.carousel.images && theme.carousel.images.length > 0) { - // Skip static background image/color for carousel mode - } else { - if (theme.login_page_background_color) { - root.style.setProperty('--login-bg-color', theme.login_page_background_color); - } - if (theme.login_page_background_image) { - root.style.setProperty('--login-bg-image', `url("${theme.login_page_background_image}")`); - } - } - - // Login box positioning - supports Left, Right, or Default positioning - if (theme.login_box_position && theme.login_box_position !== 'Default') { - root.style.setProperty('--login-box-position', 'absolute'); - root.style.setProperty('--login-box-right', theme.login_box_position === 'Right' ? '10%' : 'auto'); - root.style.setProperty('--login-box-left', theme.login_box_position === 'Left' ? '10%' : 'auto'); - root.style.setProperty('--login-box-padding', theme.is_app_details_inside_the_box === 1 ? '18px 40px 40px 40px' : '40px'); - } - - // Login box vertical positioning and app details integration - if (theme.is_app_details_inside_the_box !== undefined) { - root.style.setProperty('--login-box-top', theme.is_app_details_inside_the_box === 1 ? '26%' : '18%'); - } - - // Special styling when app details are inside the login box - if (theme.is_app_details_inside_the_box === 1) { - root.style.setProperty('--login-box-bg-override', theme.login_box_background_color); - root.style.setProperty('--login-box-border-radius', '10px'); - } - - // Login button styling - if (theme.login_button_background_color) { - root.style.setProperty('--login-btn-bg', theme.login_button_background_color); - } - if (theme.login_button_text_color) { - root.style.setProperty('--login-btn-color', theme.login_button_text_color); - } - if (theme.login_page_button_hover_background_color) { - root.style.setProperty('--login-btn-hover-bg', theme.login_page_button_hover_background_color); - } - if (theme.login_page_button_hover_text_color) { - root.style.setProperty('--login-btn-hover-color', theme.login_page_button_hover_text_color); - } - if (theme.login_box_background_color) { - root.style.setProperty('--login-box-bg', theme.login_box_background_color); - } - if (theme.page_heading_text_color) { - root.style.setProperty('--page-heading-color', theme.page_heading_text_color); - } - - // Login content border - removed when app details are inside box - if (theme.is_app_details_inside_the_box === 1) { - root.style.setProperty('--login-content-border', 'none'); - } - - // Custom login page title - replaces default Frappe title - if (theme.login_page_title) { - root.style.setProperty('--login-title-display', 'none'); - root.style.setProperty('--login-title-after-display', 'flex'); - root.style.setProperty('--login-title-after-justify', 'center'); - root.style.setProperty('--login-title-after-margin', '10px'); - root.style.setProperty('--login-title-after-content', `'${theme.login_page_title}'`); - if (theme.page_heading_text_color) { - root.style.setProperty('--login-title-after-color', theme.page_heading_text_color); - } - } - - // Form input field customization - if (theme.input_background_color) { - root.style.setProperty('--input-bg', theme.input_background_color); - } - if (theme.input_text_color) { - root.style.setProperty('--input-color', theme.input_text_color); - } - if (theme.input_border_color) { - root.style.setProperty('--input-border', theme.input_border_color); - } - if (theme.input_label_color) { - root.style.setProperty('--input-label-color', theme.input_label_color); - } - - // Navigation bar customization - if (theme.navbar_color) { - root.style.setProperty('--navbar-bg', theme.navbar_color); - } - if (theme.navbar_text_color) { - root.style.setProperty('--navbar-color', theme.navbar_text_color); - } - if (theme.hide_help_button !== undefined) { - root.style.setProperty('--hide-help', theme.hide_help_button ? 'none' : 'block'); - } - if (theme.hide_app_switcher !== undefined) { - root.style.setProperty('--hide-app-switcher', theme.hide_app_switcher ? 'none' : 'block'); - root.style.setProperty('--app-switcher-pointer-events', theme.hide_app_switcher ? 'none' : 'auto'); - } - - // Primary button styling - if (theme.button_background_color) { - root.style.setProperty('--btn-primary-bg', theme.button_background_color); - } - if (theme.button_text_color) { - root.style.setProperty('--btn-primary-color', theme.button_text_color); - } - if (theme.button_hover_background_color) { - root.style.setProperty('--btn-primary-hover-bg', theme.button_hover_background_color); - } - if (theme.button_hover_text_color) { - root.style.setProperty('--btn-primary-hover-color', theme.button_hover_text_color); - } - - // Secondary button styling - if (theme.secondary_button_background_color) { - root.style.setProperty('--btn-secondary-bg', theme.secondary_button_background_color); - } - if (theme.secondary_button_text_color) { - root.style.setProperty('--btn-secondary-color', theme.secondary_button_text_color); - } - if (theme.secondary_button_hover_background_color) { - root.style.setProperty('--btn-secondary-hover-bg', theme.secondary_button_hover_background_color); - } - if (theme.secondary_button_hover_text_color) { - root.style.setProperty('--btn-secondary-hover-color', theme.secondary_button_hover_text_color); - } - - // Main body and content area styling - if (theme.body_background_color) { - root.style.setProperty('--body-bg', theme.body_background_color); - } - if (theme.main_body_content_box_background_color) { - root.style.setProperty('--content-bg', theme.main_body_content_box_background_color); - } - if (theme.main_body_content_box_text_color) { - root.style.setProperty('--content-text-color', theme.main_body_content_box_text_color); - } - - // Sidebar customization - if (theme.sidebar_background_color) { - root.style.setProperty('--sidebar-bg', theme.sidebar_background_color); - } - if (theme.sidebar_text_color) { - root.style.setProperty('--sidebar-text-color', theme.sidebar_text_color); - } - if (theme.sidebar_hover_background_color) { - root.style.setProperty('--sidebar-hover-bg', theme.sidebar_hover_background_color); - } - if (theme.sidebar_hover_text_color) { - root.style.setProperty('--sidebar-hover-text-color', theme.sidebar_hover_text_color); - } - - // Data table styling - if (theme.table_head_background_color) { - root.style.setProperty('--table-head-bg', theme.table_head_background_color); - } - if (theme.table_head_text_color) { - root.style.setProperty('--table-head-color', theme.table_head_text_color); - } - if (theme.table_body_background_color) { - root.style.setProperty('--table-body-bg', theme.table_body_background_color); - } - if (theme.table_body_text_color) { - root.style.setProperty('--table-body-color', theme.table_body_text_color); - } - if (theme.table_hide_like_comment_section !== undefined) { - root.style.setProperty('--hide-like-comment', theme.table_hide_like_comment_section ? 'none' : 'block'); - } - - // Widget/card styling (number cards, dashboard widgets) - if (theme.number_card_background_color) { - root.style.setProperty('--widget-bg', theme.number_card_background_color); - } - if (theme.number_card_border_color) { - root.style.setProperty('--widget-border', theme.number_card_border_color); - } - if (theme.number_card_text_color) { - root.style.setProperty('--widget-color', theme.number_card_text_color); - } - - // Footer styling - if (theme.footer_background_color) { - root.style.setProperty('--footer-bg', theme.footer_background_color); - } - if (theme.footer_text_color) { - root.style.setProperty('--footer-color', theme.footer_text_color); - root.style.setProperty('--footer-powered-color', theme.footer_text_color); - } - - // Sidebar visibility control - if (theme.hide_side_bar !== undefined) { - root.style.setProperty('--sidebar-expanded', theme.hide_side_bar === 0 ? 'expanded' : ''); - } - } - - /** - * Apply all theme configurations to the current page - * Orchestrates the application of CSS variables and UI element toggles - */ - applyTheme() { - this.setCSSVariables(); - this.toggleSidebar(); - this.toggleSearchBar(); - this.setDefaultApp(); - if (this.themeData.carousel && this.themeData.carousel.images && this.themeData.carousel.images.length > 0) { - this.renderLoginCarousel(); - } else { - this.removeLoginCarousel(); - } - this.showLoginBox(); - this.createFooter(); - } - - /** - * Show login box with smooth transition after theme is applied - * Prevents flickering by revealing the login form only after positioning is set - */ - showLoginBox() { - const loginBox = document.querySelector('.for-login'); - if (loginBox) { - // Small delay to ensure CSS variables are applied - setTimeout(() => { - loginBox.classList.add('theme-ready'); - }, 50); - } - } - - /** - * Toggle sidebar visibility based on theme configuration - * Adds/removes 'expanded' class to control sidebar state - */ - toggleSidebar() { - const sidebarContainer = document.querySelector('.body-sidebar-container'); - if (!sidebarContainer) { - return; - } - - if (this.themeData.hide_side_bar === 0) { - sidebarContainer.classList.add('expanded'); - } else { - sidebarContainer.classList.remove('expanded'); - } - } - - /** - * Toggle search bar visibility based on user roles - * Hides search bar if current user's role matches hide_search configuration - */ - toggleSearchBar() { - const searchBar = document.querySelector('.input-group.search-bar.text-muted'); - if (!searchBar) { - return; - } - - if (this.getUserRoles()) { - searchBar.style.display = 'none'; - } - } - - /** - * Set current app to default app when app switcher is hidden - * Similar to breadcrumbs.js line 83 functionality - */ - setDefaultApp() { - // Only proceed if hide_app_switcher is enabled and default_app is set - if (!this.themeData.hide_app_switcher || !this.themeData.default_app) { - return; - } - - // Check if frappe.app.sidebar.apps_switcher exists (similar to breadcrumbs.js) - if (frappe?.app?.sidebar?.apps_switcher?.set_current_app) { - try { - // Set the current app to the default app (same as breadcrumbs.js line 83) - frappe.app.sidebar.apps_switcher.set_current_app(this.themeData.default_app); - } catch (error) { - // Silent fail if app switcher is not available or app doesn't exist - } - } - } - - /** - * Create and display footer in desk view using HTML template - * Much more efficient than creating DOM elements dynamically - */ - async createFooter() { - // Don't create footer on login page - if (document.body.classList.contains('login-page') || document.querySelector('#page-login')) { - return; - } - - // Remove existing footer if any - const existingFooter = document.querySelector('#desk-footer'); - if (existingFooter) { - existingFooter.remove(); - // Clean up sticky footer classes - document.body.classList.remove('has-sticky-footer'); - const mainSection = document.querySelector('.main-section'); - if (mainSection) { - mainSection.classList.remove('has-sticky-footer'); - } - } - - // Check if footer should be displayed (basic check to avoid unnecessary API calls) - if (!this.themeData.copyright_text && !this.themeData.footer_powered_by) { - return; - } - - // Throttle footer creation to prevent multiple simultaneous calls - if (this.footerCreating) { - return; - } - this.footerCreating = true; - - try { - // Create a cache key from footer-related theme data - const currentFooterKey = JSON.stringify({ - copyright_text: this.themeData.copyright_text, - footer_powered_by: this.themeData.footer_powered_by, - sticky_footer: this.themeData.sticky_footer - }); - - let footerHtml = this.footerHtmlCache; - - // Check in-memory cache first, then localStorage, then API - if (!footerHtml || this.footerCacheKey !== currentFooterKey) { - // Try to load from localStorage - const cachedFooter = this.loadFooterCache(); - if (cachedFooter && cachedFooter.key === currentFooterKey) { - footerHtml = cachedFooter.html; - this.footerHtmlCache = footerHtml; - this.footerCacheKey = currentFooterKey; - } else { - - // Get rendered footer HTML from server - const response = await fetch('/api/method/frappe_desk_theme.api.get_footer_html', { - method: 'GET', - headers: { - 'Accept': 'application/json', - } - }); - - if (!response.ok) { - throw new Error(`HTTP error! Status: ${response.status}`); - } - - const data = await response.json(); - footerHtml = data?.message || ''; - - // Cache the HTML and key for subsequent calls (both memory and localStorage) - this.footerHtmlCache = footerHtml; - this.footerCacheKey = currentFooterKey; - this.saveFooterCache(footerHtml, currentFooterKey); - } - } - - if (footerHtml.trim()) { - // Create a temporary container to hold the HTML - const tempDiv = document.createElement('div'); - tempDiv.innerHTML = footerHtml; - - // Get the footer element from the template - const footerElement = tempDiv.querySelector('#desk-footer'); - if (footerElement) { - // Try to append to main-section first, then fall back to body - const mainSection = document.querySelector('.main-section'); - if (mainSection) { - mainSection.appendChild(footerElement); - if (this.themeData.sticky_footer) { - mainSection.classList.add('has-sticky-footer'); - // Set up sticky footer sidebar toggle listener - this.setupStickyFooterToggle(); - } - } else { - // Fallback to body if main-section doesn't exist - document.body.appendChild(footerElement); - if (this.themeData.sticky_footer) { - document.body.classList.add('has-sticky-footer'); - // Set up sticky footer sidebar toggle listener - this.setupStickyFooterToggle(); - } - } - } - } - } catch (error) { - // Silent fail - footer is optional, don't show errors to user - } finally { - this.footerCreating = false; - } - } - - /** - * Set up dynamic positioning for sticky footer when sidebar toggles - * Ensures footer position updates in real-time with sidebar state - */ - setupStickyFooterToggle() { - // Avoid setting up multiple listeners - if (this.stickyFooterListenerSetup) { - return; - } - this.stickyFooterListenerSetup = true; - - // Function to update sticky footer position - const updateStickyFooterPosition = () => { - const footer = document.querySelector('#desk-footer.sticky'); - if (!footer) return; - - const sidebarContainer = document.querySelector('.body-sidebar-container'); - const isExpanded = sidebarContainer && sidebarContainer.classList.contains('expanded'); - - // Update footer position based on sidebar state - if (isExpanded) { - footer.style.left = '220px'; - } else { - footer.style.left = '50px'; - } - }; - - // Listen for sidebar toggle events - const observer = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - if (mutation.type === 'attributes' && - mutation.attributeName === 'class' && - mutation.target.classList.contains('body-sidebar-container')) { - // Delay to ensure CSS transitions complete - setTimeout(updateStickyFooterPosition, 50); - } - }); - }); - - // Observe sidebar container for class changes - const sidebarContainer = document.querySelector('.body-sidebar-container'); - if (sidebarContainer) { - observer.observe(sidebarContainer, { - attributes: true, - attributeFilter: ['class'] - }); - } - - // Also listen for sidebar toggle via click events - document.addEventListener('click', (event) => { - // Check if clicked element or its parent is a sidebar toggle - const isToggle = event.target.closest('.collapse-sidebar-link, .sidebar-toggle, [data-toggle="sidebar"]'); - if (isToggle) { - setTimeout(updateStickyFooterPosition, 200); // Allow time for animation - } - }); - - // Initial position update - updateStickyFooterPosition(); - } - - /** - * Set up event listeners for dynamic theme updates and DOM changes - * Handles real-time theme changes and new element detection - */ - setupEventListeners() { - // Listen for theme changes - allows for runtime theme updates - document.addEventListener('themeChanged', () => { - this.loadTheme().then(() => this.applyTheme()); - }); - - // Listen for DOM changes to apply theme to dynamically added elements - // Frappe uses dynamic content loading, so we need to monitor for new elements - let footerTimeout; - const observer = new MutationObserver(() => { - this.toggleSearchBar(); - - // Debounce footer creation to avoid performance issues - clearTimeout(footerTimeout); - footerTimeout = setTimeout(() => { - // Only create footer if it doesn't exist - if (!document.querySelector('#desk-footer')) { - this.createFooter(); - } - }, 500); // 500ms delay to avoid constant recreation - }); - - // Observe all changes in document body and its children - observer.observe(document.body, { - childList: true, // Watch for element additions/removals - subtree: true // Watch all descendant nodes - }); - - } - - // Navigation buttons - - ensureButton(loginPage, images, id, html, onClick) { - const manual = !!this.themeData.carousel.manual_navigation; - let btn = document.getElementById(id); - if (!manual || images.length <= 1) { - if (btn) btn.remove(); - return null; - } - if (!btn) { - btn = document.createElement('button'); - btn.id = id; - btn.className = `carousel-nav ${id === 'carousel-nav-left' ? 'carousel-nav-left' : 'carousel-nav-right'}`; - btn.innerHTML = html; - btn.addEventListener('click', onClick); - loginPage.appendChild(btn); - } - return btn; - }; - - renderLoginCarousel() { - const loginPage = document.querySelector('#page-login'); - if (!loginPage) return; - const root = document.documentElement; - const images = this.themeData.carousel.images; - if (!images || images.length === 0) return; - - // Set initial state and background - if (typeof this._carouselIndex !== 'number' || this._carouselIndex >= images.length) { - this._carouselIndex = 0; - } - root.style.setProperty('--login-bg-carousel-image', `url("${images[this._carouselIndex]}")`); - - // Remove any previous timer - if (this._carouselTimer) { - clearTimeout(this._carouselTimer); - this._carouselTimer = null; - } - - - this.ensureButton(loginPage, images,'carousel-nav-left', '←', (e) => { - e.stopPropagation(); e.preventDefault(); - if (this._carouselTimer) { - clearTimeout(this._carouselTimer); - this._carouselTimer = null; - } - this.carouselShowImage(this._carouselIndex - 1, images, root, -1); - }); - this.ensureButton(loginPage, images,'carousel-nav-right', '→', (e) => { - e.stopPropagation(); e.preventDefault(); - if (this._carouselTimer) { - clearTimeout(this._carouselTimer); - this._carouselTimer = null; - } - this.carouselShowImage(this._carouselIndex + 1, images, root, 1); - }); - - // Auto-advance: handled in carouselShowImage after animation - if (this.themeData.carousel.auto_advance !== false && images.length > 1 && !this._carouselTimer) { - this._carouselTimer = setTimeout(() => { - this._carouselTimer = null; - this.carouselShowImage(this._carouselIndex + 1, images, root, 1); - }, 5000); - } - } - - - carouselShowImage(idx, images, root, direction = 1) { - const total = images.length; - idx = (idx + total) % total; - if (idx === this._carouselIndex || this._carouselSliding) return; - - this._carouselSliding = true; - // Fade out - root.style.setProperty('--carousel-fade-opacity', '0'); - setTimeout(() => { - root.style.setProperty('--login-bg-carousel-image', `url("${images[idx]}")`); - root.style.setProperty('--carousel-fade-opacity', '1'); - this._carouselIndex = idx; - this._carouselSliding = false; - // Auto-advance - const auto = this.themeData.carousel.auto_advance !== false; - if (auto && images.length > 1 && !this._carouselTimer) { - this._carouselTimer = setTimeout(() => { - this._carouselTimer = null; - this.carouselShowImage(this._carouselIndex + 1, images, root, 1); - }, 5000); - } - }, 400); - } - - - - - removeLoginCarousel() { - // Remove navigation buttons if present - const left = document.getElementById('carousel-nav-left'); - const right = document.getElementById('carousel-nav-right'); - if (left) left.remove(); - if (right) right.remove(); - if (this._carouselTimer) { - clearTimeout(this._carouselTimer); - this._carouselTimer = null; - } - // Remove the CSS variable - document.documentElement.style.removeProperty('--login-bg-carousel-image'); - this._carouselIndex = 0; - } + constructor() { + // Store theme configuration data from server + this.themeData = null; + // Cache configuration + this.cacheKey = "frappe_desk_theme_cache"; + this.footerCacheStorageKey = "frappe_desk_theme_footer_cache"; + this.cacheTimeout = 30 * 24 * 60 * 60 * 1000; // 30 days (1 month) in milliseconds + // Footer creation throttling and caching + this.footerCreating = false; + this.footerHtmlCache = null; + this.footerCacheKey = null; // Track what theme data the footer was cached for + this.stickyFooterListenerSetup = false; + this.init(); + } + + /** + * Initialize the theme system + * First applies cached theme immediately, then loads fresh data if needed + * Uses async/await pattern with graceful error handling + */ + async init() { + try { + // Apply cached theme immediately to prevent flickering + this.applyCachedTheme(); + + // Load fresh theme data if needed (async) + await this.loadThemeIfNeeded(); + + // Apply fresh theme if we got new data + if (this.themeData) { + this.applyTheme(); + } + + this.setupEventListeners(); + } catch (error) { + // Production-ready silent fail - apply default theme and show login box + this.applyTheme(); + this.showLoginBoxFallback(); + } + } + + /** + * Fallback method to show login box if theme loading fails + * Ensures login form is always visible even if theme fails to load + */ + showLoginBoxFallback() { + const loginBox = document.querySelector(".for-login"); + if (loginBox && !loginBox.classList.contains("theme-ready")) { + setTimeout(() => { + loginBox.classList.add("theme-ready"); + }, 100); + } + } + + /** + * Apply cached theme immediately to prevent UI flickering + */ + applyCachedTheme() { + const cachedData = this.getCachedTheme(); + if (cachedData && cachedData.data) { + this.themeData = cachedData.data; + this.applyTheme(); + } else { + // No cached theme, but still show login box to prevent indefinite hiding + this.showLoginBoxFallback(); + } + } + + /** + * Get cached theme data from localStorage + * @returns {Object|null} Cached theme data with timestamp + */ + getCachedTheme() { + try { + const cached = localStorage.getItem(this.cacheKey); + return cached ? JSON.parse(cached) : null; + } catch (error) { + return null; + } + } + + /** + * Save theme data to localStorage with timestamp + * @param {Object} themeData Theme configuration data + */ + setCachedTheme(themeData) { + try { + const cacheData = { + data: themeData, + timestamp: Date.now(), + version: 1, // Increment this when theme structure changes + }; + localStorage.setItem(this.cacheKey, JSON.stringify(cacheData)); + } catch (error) { + // localStorage might be full or disabled + } + } + + /** + * Check if cached theme is still valid + * @returns {boolean} True if cache is valid and not expired + */ + isCacheValid() { + const cachedData = this.getCachedTheme(); + if (!cachedData) return false; + + const now = Date.now(); + const cacheAge = now - cachedData.timestamp; + + return cacheAge < this.cacheTimeout; // 30 days + } + + /** + * Load theme only if cache is invalid or doesn't exist + */ + async loadThemeIfNeeded() { + // Skip API call if cache is still valid + if (this.isCacheValid()) { + return; + } + + await this.loadTheme(); + } + + /** + * Load theme configuration from server API + * Fetches custom theme data via REST API endpoint + * Handles response parsing and error states + */ + async loadTheme() { + try { + const response = await fetch("/api/method/frappe_desk_theme.api.get_custom_theme", { + method: "GET", + headers: { + Accept: "application/json", + }, + }); + + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + + const data = await response.json(); + // Handle different response formats - some APIs wrap data in 'message' property + this.themeData = data?.message || data; + + if (!this.themeData) { + throw new Error("No theme data received"); + } + + // Cache the new theme data + this.setCachedTheme(this.themeData); + } catch (error) { + // If API fails, try to use cached data as fallback + const cachedData = this.getCachedTheme(); + if (cachedData && cachedData.data) { + this.themeData = cachedData.data; + } else { + throw error; + } + } + } + + /** + * Force refresh theme from server (ignores cache) + * Useful for manual theme updates or admin changes + */ + async refreshTheme() { + try { + // Clear footer cache to ensure fresh data + this.footerHtmlCache = null; + this.footerCacheKey = null; + + await this.loadTheme(); + this.applyTheme(); + + // Dispatch event for other components + document.dispatchEvent( + new CustomEvent("themeRefreshed", { + detail: { themeData: this.themeData }, + }) + ); + } catch (error) { + // Silent fail - theme refresh errors should not interrupt user experience + } + } + + /** + * Save footer cache to localStorage + */ + saveFooterCache(footerHtml, cacheKey) { + try { + const cacheData = { + html: footerHtml, + key: cacheKey, + timestamp: Date.now(), + }; + localStorage.setItem(this.footerCacheStorageKey, JSON.stringify(cacheData)); + } catch (error) { + // localStorage might be full or disabled + } + } + + /** + * Load footer cache from localStorage + */ + loadFooterCache() { + try { + const cached = localStorage.getItem(this.footerCacheStorageKey); + if (!cached) return null; + + const cacheData = JSON.parse(cached); + const now = Date.now(); + const cacheAge = now - cacheData.timestamp; + + // Return cached data if it's still valid (within 30-day timeout) + if (cacheAge < this.cacheTimeout) { + return cacheData; + } else { + // Remove expired cache + localStorage.removeItem(this.footerCacheStorageKey); + return null; + } + } catch (error) { + return null; + } + } + + /** + * Clear theme cache (useful for debugging or forced refresh) + */ + clearCache() { + try { + localStorage.removeItem(this.cacheKey); + localStorage.removeItem(this.footerCacheStorageKey); + // Also clear footer cache + this.footerHtmlCache = null; + this.footerCacheKey = null; + } catch (error) { + // Ignore localStorage errors + } + } + + /** + * Check if current user's roles match hide_search configuration + * Used to conditionally hide search bar based on user permissions + * @returns {boolean} True if search should be hidden for current user + */ + getUserRoles() { + const currentUser = frappe?.boot?.user?.roles; + // Exit early if no user roles or no hide_search config + if (!currentUser || !this.themeData?.hide_search) { + return false; + } + + // Special handling for Administrator role + if (currentUser.includes("Administrator")) { + return this.themeData.hide_search.some((u) => u.role === "Administrator"); + } + + // Check if any user role matches hide_search configuration + return currentUser.some((role) => this.themeData.hide_search.some((u) => u.role === role)); + } + + /** + * Clear all theme-related CSS custom properties from document root + * Used to reset theme state before applying new theme values + * Ensures clean slate for theme updates + */ + clearCSSVariables() { + const root = document.documentElement; + // Comprehensive list of all theme CSS variables + const cssVariables = [ + "--login-bg-color", + "--login-bg-image", + "--login-box-position", + "--login-box-right", + "--login-box-left", + "--login-btn-bg", + "--login-btn-color", + "--login-btn-hover-bg", + "--login-btn-hover-color", + "--login-box-bg", + "--page-heading-color", + "--input-bg", + "--input-color", + "--input-border", + "--input-label-color", + "--navbar-bg", + "--navbar-color", + "--hide-help", + "--btn-primary-bg", + "--btn-primary-color", + "--btn-primary-hover-bg", + "--btn-primary-hover-color", + "--btn-secondary-bg", + "--btn-secondary-color", + "--btn-secondary-hover-bg", + "--btn-secondary-hover-color", + "--body-bg", + "--content-bg", + "--table-head-bg", + "--table-head-color", + "--table-body-bg", + "--table-body-color", + "--hide-like-comment", + "--widget-bg", + "--widget-border", + "--widget-color", + "--sidebar-expanded", + "--sidebar-bg", + "--sidebar-text-color", + "--sidebar-hover-bg", + "--sidebar-hover-text-color", + "--login-content-border", + "--login-title-display", + "--login-title-after-display", + "--login-title-after-justify", + "--login-title-after-margin", + "--login-title-after-content", + "--login-title-after-color", + "--login-box-top", + "--login-box-bg-override", + "--login-box-border-radius", + "--search-bar-display", + "--navbar-toggler-border", + "--breadcrumb-disabled-color", + "--help-nav-link-color", + "--help-nav-link-stroke", + "--hide-app-switcher", + "--app-switcher-pointer-events", + "--footer-bg", + "--footer-color", + "--footer-border", + "--footer-display", + "--footer-powered-color", + "--footer-link-color", + "--footer-link-hover-color", + "--carousel-fade-opacity", + "--login-bg-carousel-image", + ]; + + // Remove each CSS variable from document root + cssVariables.forEach((variable) => { + root.style.removeProperty(variable); + }); + } + + /** + * Set default CSS variable values + * Provides fallback values when theme configuration is missing or incomplete + * Ensures UI remains functional even without complete theme data + */ + setDefaultCSSVariables() { + const root = document.documentElement; + + // Login page defaults - ensures login form remains usable + root.style.setProperty("--login-box-position", "static"); + root.style.setProperty("--login-box-right", "auto"); + root.style.setProperty("--login-box-left", "auto"); + root.style.setProperty("--login-box-top", "18%"); + root.style.setProperty("--login-box-bg", "#fff"); + root.style.setProperty("--login-content-border", "2px solid #d1d8dd"); + root.style.setProperty("--login-title-display", "block"); + root.style.setProperty("--login-title-after-display", "none"); + + // UI element visibility defaults + root.style.setProperty("--hide-help", "block"); + root.style.setProperty("--hide-like-comment", "block"); + root.style.setProperty("--hide-app-switcher", "block"); + root.style.setProperty("--app-switcher-pointer-events", "auto"); + root.style.setProperty("--sidebar-expanded", ""); + root.style.setProperty("--sidebar-hover-bg", "#e9ecef"); + root.style.setProperty("--sidebar-hover-text-color", "#212529"); + root.style.setProperty("--login-box-width", "400px"); + root.style.setProperty("--search-bar-display", "block"); + + // Navigation and UI component defaults + root.style.setProperty("--navbar-toggler-border", "#dee2e6"); + root.style.setProperty("--breadcrumb-disabled-color", "#6c757d"); + root.style.setProperty("--help-nav-link-color", "inherit"); + root.style.setProperty("--help-nav-link-stroke", "currentColor"); + + // Footer defaults + root.style.setProperty("--footer-display", "flex"); + root.style.setProperty("--footer-bg", "#f8f9fa"); + root.style.setProperty("--footer-color", "#495057"); + root.style.setProperty("--footer-border", "#dee2e6"); + root.style.setProperty("--footer-powered-color", "#6c757d"); + root.style.setProperty("--footer-link-color", "#007bff"); + root.style.setProperty("--footer-link-hover-color", "#0056b3"); + + // Carousel fade default + root.style.setProperty("--carousel-fade-opacity", "1"); + } + + /** + * Apply theme configuration to CSS custom properties + * Maps theme data fields to corresponding CSS variables + * Only sets variables when theme values are provided (conditional application) + */ + setCSSVariables() { + const root = document.documentElement; + const theme = this.themeData; + + // Reset all variables to clean state + this.clearCSSVariables(); + + // Establish default values first + this.setDefaultCSSVariables(); + + // Login page background customization + if (theme.carousel && theme.carousel.images && theme.carousel.images.length > 0) { + // Skip static background image/color for carousel mode + } else { + if (theme.login_page_background_color) { + root.style.setProperty("--login-bg-color", theme.login_page_background_color); + } + if (theme.login_page_background_image) { + root.style.setProperty( + "--login-bg-image", + `url("${theme.login_page_background_image}")` + ); + } + } + + // Login box positioning - supports Left, Right, or Default positioning + if (theme.login_box_position && theme.login_box_position !== "Default") { + root.style.setProperty("--login-box-position", "absolute"); + root.style.setProperty( + "--login-box-right", + theme.login_box_position === "Right" ? "10%" : "auto" + ); + root.style.setProperty( + "--login-box-left", + theme.login_box_position === "Left" ? "10%" : "auto" + ); + root.style.setProperty( + "--login-box-padding", + theme.is_app_details_inside_the_box === 1 ? "18px 40px 40px 40px" : "40px" + ); + } + + // Login box vertical positioning and app details integration + if (theme.is_app_details_inside_the_box !== undefined) { + root.style.setProperty( + "--login-box-top", + theme.is_app_details_inside_the_box === 1 ? "26%" : "18%" + ); + } + + // Special styling when app details are inside the login box + if (theme.is_app_details_inside_the_box === 1) { + root.style.setProperty("--login-box-bg-override", theme.login_box_background_color); + root.style.setProperty("--login-box-border-radius", "10px"); + } + + // Login button styling + if (theme.login_button_background_color) { + root.style.setProperty("--login-btn-bg", theme.login_button_background_color); + } + if (theme.login_button_text_color) { + root.style.setProperty("--login-btn-color", theme.login_button_text_color); + } + if (theme.login_page_button_hover_background_color) { + root.style.setProperty( + "--login-btn-hover-bg", + theme.login_page_button_hover_background_color + ); + } + if (theme.login_page_button_hover_text_color) { + root.style.setProperty( + "--login-btn-hover-color", + theme.login_page_button_hover_text_color + ); + } + if (theme.login_box_background_color) { + root.style.setProperty("--login-box-bg", theme.login_box_background_color); + } + if (theme.page_heading_text_color) { + root.style.setProperty("--page-heading-color", theme.page_heading_text_color); + } + + // Login content border - removed when app details are inside box + if (theme.is_app_details_inside_the_box === 1) { + root.style.setProperty("--login-content-border", "none"); + } + + // Custom login page title - replaces default Frappe title + if (theme.login_page_title) { + root.style.setProperty("--login-title-display", "none"); + root.style.setProperty("--login-title-after-display", "flex"); + root.style.setProperty("--login-title-after-justify", "center"); + root.style.setProperty("--login-title-after-margin", "10px"); + root.style.setProperty("--login-title-after-content", `'${theme.login_page_title}'`); + if (theme.page_heading_text_color) { + root.style.setProperty("--login-title-after-color", theme.page_heading_text_color); + } + } + + // Form input field customization + if (theme.input_background_color) { + root.style.setProperty("--input-bg", theme.input_background_color); + } + if (theme.input_text_color) { + root.style.setProperty("--input-color", theme.input_text_color); + } + if (theme.input_border_color) { + root.style.setProperty("--input-border", theme.input_border_color); + } + if (theme.input_label_color) { + root.style.setProperty("--input-label-color", theme.input_label_color); + } + + // Navigation bar customization + if (theme.navbar_color) { + root.style.setProperty("--navbar-bg", theme.navbar_color); + } + if (theme.navbar_text_color) { + root.style.setProperty("--navbar-color", theme.navbar_text_color); + } + if (theme.hide_help_button !== undefined) { + root.style.setProperty("--hide-help", theme.hide_help_button ? "none" : "block"); + } + if (theme.hide_app_switcher !== undefined) { + root.style.setProperty( + "--hide-app-switcher", + theme.hide_app_switcher ? "none" : "block" + ); + root.style.setProperty( + "--app-switcher-pointer-events", + theme.hide_app_switcher ? "none" : "auto" + ); + } + + // Primary button styling + if (theme.button_background_color) { + root.style.setProperty("--btn-primary-bg", theme.button_background_color); + } + if (theme.button_text_color) { + root.style.setProperty("--btn-primary-color", theme.button_text_color); + } + if (theme.button_hover_background_color) { + root.style.setProperty("--btn-primary-hover-bg", theme.button_hover_background_color); + } + if (theme.button_hover_text_color) { + root.style.setProperty("--btn-primary-hover-color", theme.button_hover_text_color); + } + + // Secondary button styling + if (theme.secondary_button_background_color) { + root.style.setProperty("--btn-secondary-bg", theme.secondary_button_background_color); + } + if (theme.secondary_button_text_color) { + root.style.setProperty("--btn-secondary-color", theme.secondary_button_text_color); + } + if (theme.secondary_button_hover_background_color) { + root.style.setProperty( + "--btn-secondary-hover-bg", + theme.secondary_button_hover_background_color + ); + } + if (theme.secondary_button_hover_text_color) { + root.style.setProperty( + "--btn-secondary-hover-color", + theme.secondary_button_hover_text_color + ); + } + + // Main body and content area styling + if (theme.body_background_color) { + root.style.setProperty("--body-bg", theme.body_background_color); + } + if (theme.main_body_content_box_background_color) { + root.style.setProperty("--content-bg", theme.main_body_content_box_background_color); + } + if (theme.main_body_content_box_text_color) { + root.style.setProperty("--content-text-color", theme.main_body_content_box_text_color); + } + + // Sidebar customization + if (theme.sidebar_background_color) { + root.style.setProperty("--sidebar-bg", theme.sidebar_background_color); + } + if (theme.sidebar_text_color) { + root.style.setProperty("--sidebar-text-color", theme.sidebar_text_color); + } + if (theme.sidebar_hover_background_color) { + root.style.setProperty("--sidebar-hover-bg", theme.sidebar_hover_background_color); + } + if (theme.sidebar_hover_text_color) { + root.style.setProperty("--sidebar-hover-text-color", theme.sidebar_hover_text_color); + } + + // Data table styling + if (theme.table_head_background_color) { + root.style.setProperty("--table-head-bg", theme.table_head_background_color); + } + if (theme.table_head_text_color) { + root.style.setProperty("--table-head-color", theme.table_head_text_color); + } + if (theme.table_body_background_color) { + root.style.setProperty("--table-body-bg", theme.table_body_background_color); + } + if (theme.table_body_text_color) { + root.style.setProperty("--table-body-color", theme.table_body_text_color); + } + if (theme.table_hide_like_comment_section !== undefined) { + root.style.setProperty( + "--hide-like-comment", + theme.table_hide_like_comment_section ? "none" : "block" + ); + } + + // Widget/card styling (number cards, dashboard widgets) + if (theme.number_card_background_color) { + root.style.setProperty("--widget-bg", theme.number_card_background_color); + } + if (theme.number_card_border_color) { + root.style.setProperty("--widget-border", theme.number_card_border_color); + } + if (theme.number_card_text_color) { + root.style.setProperty("--widget-color", theme.number_card_text_color); + } + + // Footer styling + if (theme.footer_background_color) { + root.style.setProperty("--footer-bg", theme.footer_background_color); + } + if (theme.footer_text_color) { + root.style.setProperty("--footer-color", theme.footer_text_color); + root.style.setProperty("--footer-powered-color", theme.footer_text_color); + } + + // Sidebar visibility control + if (theme.hide_side_bar !== undefined) { + root.style.setProperty( + "--sidebar-expanded", + theme.hide_side_bar === 0 ? "expanded" : "" + ); + } + } + + /** + * Apply all theme configurations to the current page + * Orchestrates the application of CSS variables and UI element toggles + */ + applyTheme() { + this.setCSSVariables(); + this.toggleSidebar(); + this.toggleSearchBar(); + this.setDefaultApp(); + if ( + this.themeData.carousel && + this.themeData.carousel.images && + this.themeData.carousel.images.length > 0 + ) { + this.renderLoginCarousel(); + } else { + this.removeLoginCarousel(); + } + this.showLoginBox(); + this.createFooter(); + } + + /** + * Show login box with smooth transition after theme is applied + * Prevents flickering by revealing the login form only after positioning is set + */ + showLoginBox() { + const loginBox = document.querySelector(".for-login"); + if (loginBox) { + // Small delay to ensure CSS variables are applied + setTimeout(() => { + loginBox.classList.add("theme-ready"); + }, 50); + } + } + + /** + * Toggle sidebar visibility based on theme configuration + * Adds/removes 'expanded' class to control sidebar state + */ + toggleSidebar() { + const sidebarContainer = document.querySelector(".body-sidebar-container"); + if (!sidebarContainer) { + return; + } + + if (this.themeData.hide_side_bar === 0) { + sidebarContainer.classList.add("expanded"); + } else { + sidebarContainer.classList.remove("expanded"); + } + } + + /** + * Toggle search bar visibility based on user roles + * Hides search bar if current user's role matches hide_search configuration + */ + toggleSearchBar() { + const searchBar = document.querySelector(".input-group.search-bar.text-muted"); + if (!searchBar) { + return; + } + + if (this.getUserRoles()) { + searchBar.style.display = "none"; + } + } + + /** + * Set current app to default app when app switcher is hidden + * Similar to breadcrumbs.js line 83 functionality + */ + setDefaultApp() { + // Only proceed if hide_app_switcher is enabled and default_app is set + if (!this.themeData.hide_app_switcher || !this.themeData.default_app) { + return; + } + + // Check if frappe.app.sidebar.apps_switcher exists (similar to breadcrumbs.js) + if (frappe?.app?.sidebar?.apps_switcher?.set_current_app) { + try { + // Set the current app to the default app (same as breadcrumbs.js line 83) + frappe.app.sidebar.apps_switcher.set_current_app(this.themeData.default_app); + } catch (error) { + // Silent fail if app switcher is not available or app doesn't exist + } + } + } + + /** + * Create and display footer in desk view using HTML template + * Much more efficient than creating DOM elements dynamically + */ + async createFooter() { + // Don't create footer on login page + if ( + document.body.classList.contains("login-page") || + document.querySelector("#page-login") + ) { + return; + } + + // Remove existing footer if any + const existingFooter = document.querySelector("#desk-footer"); + if (existingFooter) { + existingFooter.remove(); + // Clean up sticky footer classes + document.body.classList.remove("has-sticky-footer"); + const mainSection = document.querySelector(".main-section"); + if (mainSection) { + mainSection.classList.remove("has-sticky-footer"); + } + } + + // Check if footer should be displayed (basic check to avoid unnecessary API calls) + if (!this.themeData.copyright_text && !this.themeData.footer_powered_by) { + return; + } + + // Throttle footer creation to prevent multiple simultaneous calls + if (this.footerCreating) { + return; + } + this.footerCreating = true; + + try { + // Create a cache key from footer-related theme data + const currentFooterKey = JSON.stringify({ + copyright_text: this.themeData.copyright_text, + footer_powered_by: this.themeData.footer_powered_by, + sticky_footer: this.themeData.sticky_footer, + }); + + let footerHtml = this.footerHtmlCache; + + // Check in-memory cache first, then localStorage, then API + if (!footerHtml || this.footerCacheKey !== currentFooterKey) { + // Try to load from localStorage + const cachedFooter = this.loadFooterCache(); + if (cachedFooter && cachedFooter.key === currentFooterKey) { + footerHtml = cachedFooter.html; + this.footerHtmlCache = footerHtml; + this.footerCacheKey = currentFooterKey; + } else { + // Get rendered footer HTML from server + const response = await fetch( + "/api/method/frappe_desk_theme.api.get_footer_html", + { + method: "GET", + headers: { + Accept: "application/json", + }, + } + ); + + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + + const data = await response.json(); + footerHtml = data?.message || ""; + + // Cache the HTML and key for subsequent calls (both memory and localStorage) + this.footerHtmlCache = footerHtml; + this.footerCacheKey = currentFooterKey; + this.saveFooterCache(footerHtml, currentFooterKey); + } + } + + if (footerHtml.trim()) { + // Create a temporary container to hold the HTML + const tempDiv = document.createElement("div"); + tempDiv.innerHTML = footerHtml; + + // Get the footer element from the template + const footerElement = tempDiv.querySelector("#desk-footer"); + if (footerElement) { + // Try to append to main-section first, then fall back to body + const mainSection = document.querySelector(".main-section"); + if (mainSection) { + mainSection.appendChild(footerElement); + if (this.themeData.sticky_footer) { + mainSection.classList.add("has-sticky-footer"); + // Set up sticky footer sidebar toggle listener + this.setupStickyFooterToggle(); + } + } else { + // Fallback to body if main-section doesn't exist + document.body.appendChild(footerElement); + if (this.themeData.sticky_footer) { + document.body.classList.add("has-sticky-footer"); + // Set up sticky footer sidebar toggle listener + this.setupStickyFooterToggle(); + } + } + } + } + } catch (error) { + // Silent fail - footer is optional, don't show errors to user + } finally { + this.footerCreating = false; + } + } + + /** + * Set up dynamic positioning for sticky footer when sidebar toggles + * Ensures footer position updates in real-time with sidebar state + */ + setupStickyFooterToggle() { + // Avoid setting up multiple listeners + if (this.stickyFooterListenerSetup) { + return; + } + this.stickyFooterListenerSetup = true; + + // Function to update sticky footer position + const updateStickyFooterPosition = () => { + const footer = document.querySelector("#desk-footer.sticky"); + if (!footer) return; + + const sidebarContainer = document.querySelector(".body-sidebar-container"); + const isExpanded = sidebarContainer && sidebarContainer.classList.contains("expanded"); + + // Update footer position based on sidebar state + if (isExpanded) { + footer.style.left = "220px"; + } else { + footer.style.left = "50px"; + } + }; + + // Listen for sidebar toggle events + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if ( + mutation.type === "attributes" && + mutation.attributeName === "class" && + mutation.target.classList.contains("body-sidebar-container") + ) { + // Delay to ensure CSS transitions complete + setTimeout(updateStickyFooterPosition, 50); + } + }); + }); + + // Observe sidebar container for class changes + const sidebarContainer = document.querySelector(".body-sidebar-container"); + if (sidebarContainer) { + observer.observe(sidebarContainer, { + attributes: true, + attributeFilter: ["class"], + }); + } + + // Also listen for sidebar toggle via click events + document.addEventListener("click", (event) => { + // Check if clicked element or its parent is a sidebar toggle + const isToggle = event.target.closest( + '.collapse-sidebar-link, .sidebar-toggle, [data-toggle="sidebar"]' + ); + if (isToggle) { + setTimeout(updateStickyFooterPosition, 200); // Allow time for animation + } + }); + + // Initial position update + updateStickyFooterPosition(); + } + + /** + * Set up event listeners for dynamic theme updates and DOM changes + * Handles real-time theme changes and new element detection + */ + setupEventListeners() { + // Listen for theme changes - allows for runtime theme updates + document.addEventListener("themeChanged", () => { + this.loadTheme().then(() => this.applyTheme()); + }); + + // Listen for DOM changes to apply theme to dynamically added elements + // Frappe uses dynamic content loading, so we need to monitor for new elements + let footerTimeout; + const observer = new MutationObserver(() => { + this.toggleSearchBar(); + + // Debounce footer creation to avoid performance issues + clearTimeout(footerTimeout); + footerTimeout = setTimeout(() => { + // Only create footer if it doesn't exist + if (!document.querySelector("#desk-footer")) { + this.createFooter(); + } + }, 500); // 500ms delay to avoid constant recreation + }); + + // Observe all changes in document body and its children + observer.observe(document.body, { + childList: true, // Watch for element additions/removals + subtree: true, // Watch all descendant nodes + }); + } + + // Navigation buttons + + ensureButton(loginPage, images, id, html, onClick) { + const manual = !!this.themeData.carousel.manual_navigation; + let btn = document.getElementById(id); + if (!manual || images.length <= 1) { + if (btn) btn.remove(); + return null; + } + if (!btn) { + btn = document.createElement("button"); + btn.id = id; + btn.className = `carousel-nav ${ + id === "carousel-nav-left" ? "carousel-nav-left" : "carousel-nav-right" + }`; + btn.innerHTML = html; + btn.addEventListener("click", onClick); + loginPage.appendChild(btn); + } + return btn; + } + + renderLoginCarousel() { + const loginPage = document.querySelector("#page-login"); + if (!loginPage) return; + const root = document.documentElement; + const images = this.themeData.carousel.images; + if (!images || images.length === 0) return; + + // Set initial state and background + if (typeof this._carouselIndex !== "number" || this._carouselIndex >= images.length) { + this._carouselIndex = 0; + } + root.style.setProperty( + "--login-bg-carousel-image", + `url("${images[this._carouselIndex]}")` + ); + + // Remove any previous timer + if (this._carouselTimer) { + clearTimeout(this._carouselTimer); + this._carouselTimer = null; + } + + this.ensureButton(loginPage, images, "carousel-nav-left", "←", (e) => { + e.stopPropagation(); + e.preventDefault(); + if (this._carouselTimer) { + clearTimeout(this._carouselTimer); + this._carouselTimer = null; + } + this.carouselShowImage(this._carouselIndex - 1, images, root, -1); + }); + this.ensureButton(loginPage, images, "carousel-nav-right", "→", (e) => { + e.stopPropagation(); + e.preventDefault(); + if (this._carouselTimer) { + clearTimeout(this._carouselTimer); + this._carouselTimer = null; + } + this.carouselShowImage(this._carouselIndex + 1, images, root, 1); + }); + + // Auto-advance: handled in carouselShowImage after animation + if ( + this.themeData.carousel.auto_advance !== false && + images.length > 1 && + !this._carouselTimer + ) { + this._carouselTimer = setTimeout(() => { + this._carouselTimer = null; + this.carouselShowImage(this._carouselIndex + 1, images, root, 1); + }, 5000); + } + } + + carouselShowImage(idx, images, root, direction = 1) { + const total = images.length; + idx = (idx + total) % total; + if (idx === this._carouselIndex || this._carouselSliding) return; + + this._carouselSliding = true; + // Fade out + root.style.setProperty("--carousel-fade-opacity", "0"); + setTimeout(() => { + root.style.setProperty("--login-bg-carousel-image", `url("${images[idx]}")`); + root.style.setProperty("--carousel-fade-opacity", "1"); + this._carouselIndex = idx; + this._carouselSliding = false; + // Auto-advance + const auto = this.themeData.carousel.auto_advance !== false; + if (auto && images.length > 1 && !this._carouselTimer) { + this._carouselTimer = setTimeout(() => { + this._carouselTimer = null; + this.carouselShowImage(this._carouselIndex + 1, images, root, 1); + }, 5000); + } + }, 400); + } + + removeLoginCarousel() { + // Remove navigation buttons if present + const left = document.getElementById("carousel-nav-left"); + const right = document.getElementById("carousel-nav-right"); + if (left) left.remove(); + if (right) right.remove(); + if (this._carouselTimer) { + clearTimeout(this._carouselTimer); + this._carouselTimer = null; + } + // Remove the CSS variable + document.documentElement.style.removeProperty("--login-bg-carousel-image"); + this._carouselIndex = 0; + } } // Initialize theme system when DOM is ready // Handles both immediate initialization and delayed initialization for slow-loading pages -if (document.readyState === 'loading') { - // DOM is still loading, wait for DOMContentLoaded event - document.addEventListener('DOMContentLoaded', () => { - window.frappeDeskTheme = new FrappeDeskTheme(); - }); +if (document.readyState === "loading") { + // DOM is still loading, wait for DOMContentLoaded event + document.addEventListener("DOMContentLoaded", () => { + window.frappeDeskTheme = new FrappeDeskTheme(); + }); } else { - // DOM is already loaded, initialize immediately - window.frappeDeskTheme = new FrappeDeskTheme(); -} \ No newline at end of file + // DOM is already loaded, initialize immediately + window.frappeDeskTheme = new FrappeDeskTheme(); +} diff --git a/frappe_desk_theme/public/js/sidebar/breadcrumb_override.bundle.js b/frappe_desk_theme/public/js/sidebar/breadcrumb_override.bundle.js new file mode 100644 index 0000000..d59ce86 --- /dev/null +++ b/frappe_desk_theme/public/js/sidebar/breadcrumb_override.bundle.js @@ -0,0 +1,70 @@ +frappe.breadcrumbs.update = function () { + var breadcrumbs = this.all[frappe.breadcrumbs.current_page()]; + this.clear(); + if (!breadcrumbs) return this.toggle(false); + + if (breadcrumbs.type === "Custom") { + this.set_custom_breadcrumbs(breadcrumbs); + } else { + let view = frappe.get_route()[0]; + view = view ? view.toLowerCase() : null; + + if (breadcrumbs.doctype || view === "list") { + const route = frappe.get_route(); + const last = frappe.route_history.slice(-2)[0]; + const from_workspace = last && last[0] === "Workspaces"; + + this.clear(); + + if (route[0] === "Form") { + if (from_workspace) { + this.append_breadcrumb_element( + `/app/${frappe.router.slug(last[1])}`, + __(last[1]) + ); + } + this.append_breadcrumb_element( + `/app/${frappe.router.slug(breadcrumbs.doctype)}`, + __(breadcrumbs.doctype) + ); + const name = route[2]; + this.append_breadcrumb_element( + `/app/${frappe.router.slug(breadcrumbs.doctype)}/${encodeURIComponent(name)}`, + __(name) + ); + return this.toggle(true); + } else if (route[0] === "List") { + if (from_workspace) { + this.append_breadcrumb_element( + `/app/${frappe.router.slug(last[1])}`, + __(last[1]) + ); + } + this.append_breadcrumb_element( + `/app/${frappe.router.slug(breadcrumbs.doctype)}`, + __(breadcrumbs.doctype) + ); + return this.toggle(true); + } + } + + this.set_workspace_breadcrumb(breadcrumbs); + + if (breadcrumbs.doctype && ["print", "form"].includes(view)) { + this.set_list_breadcrumb(breadcrumbs); + this.set_form_breadcrumb(breadcrumbs, view); + } else if (breadcrumbs.doctype && view == "dashboard-view") { + this.set_list_breadcrumb(breadcrumbs); + } + } + + if ( + breadcrumbs.workspace && + frappe.workspace_map[breadcrumbs.workspace]?.app && + frappe.workspace_map[breadcrumbs.workspace]?.app != frappe.current_app + ) { + let app = frappe.workspace_map[breadcrumbs.workspace].app; + frappe.app.sidebar.apps_switcher.set_current_app(app); + } + this.toggle(true); +}; diff --git a/frappe_desk_theme/public/js/sidebar/sidebar_override.bundle.js b/frappe_desk_theme/public/js/sidebar/sidebar_override.bundle.js new file mode 100644 index 0000000..8546ca2 --- /dev/null +++ b/frappe_desk_theme/public/js/sidebar/sidebar_override.bundle.js @@ -0,0 +1,79 @@ +frappe.ui.Sidebar = class CustomSidebar extends frappe.ui.Sidebar { + // Improved method to handle sidebar item clicks + handle_sidebar_click(item_element, item_name, item_title) { + $(".standard-sidebar-item").removeClass("active-sidebar"); + $(item_element).closest(".standard-sidebar-item").addClass("active-sidebar"); + this.active_item = $(item_element).closest(".standard-sidebar-item"); + localStorage.setItem("sidebar-active-item", item_name || item_title); + } + set_active_workspace_item() { + const current_route = frappe.get_route(); + if (!current_route || !current_route.length) return; + + const current_item = current_route[1]; + if (!current_item) return; + + const $match = this.$sidebar.find(`.sidebar-item-container[item-name="${current_item}"]`); + if ($match.length) { + this.$sidebar.find(".standard-sidebar-item").removeClass("active-sidebar"); + $match.find(".standard-sidebar-item").addClass("active-sidebar"); + this.active_item = $match; + + // If nested, expand parent + const $parent_container = $match.closest(".sidebar-child-item"); + if ($parent_container.length) { + $parent_container.removeClass("hidden"); + const $toggle_btn = $parent_container + .siblings(".sidebar-item-control") + .find(".drop-icon"); + $toggle_btn.find("use").attr("href", "#icon-chevron-up"); + } + } + } + build_sidebar_section(title, root_pages) { + let sidebar_section = $( + `
` + ); + + this.prepare_sidebar(root_pages, sidebar_section, this.wrapper.find(".sidebar-items")); + + if (Object.keys(root_pages).length === 0) { + sidebar_section.addClass("hidden"); + } + + // Fixed single-click active + breadcrumb update + $(".item-anchor") + .off("click") + .on("click", (e) => { + const $target = $(e.currentTarget); + const item_name = $target.closest(".sidebar-item-container").attr("item-name"); + const item_title = $target.attr("title"); + + // Delay to let route update + setTimeout(() => { + this.set_active_workspace_item(); + this.handle_sidebar_click(e.currentTarget, item_name, item_title); + frappe.breadcrumbs.update(); + + // Scroll to item if needed + if (!frappe.dom.is_element_in_viewport($target)) { + $target[0].scrollIntoView({ behavior: "smooth", block: "center" }); + } + }, 50); + + $(".list-sidebar.hidden-xs.hidden-sm").removeClass("opened"); + $("body").css("overflow", "auto"); + + if (frappe.is_mobile()) { + this.close_sidebar(); + } + }); + + if ( + sidebar_section.find(".sidebar-item-container").length && + sidebar_section.find("> [item-is-hidden='0']").length == 0 + ) { + sidebar_section.addClass("hidden show-in-edit-mode"); + } + } +}; diff --git a/frappe_desk_theme/templates/includes/desk_footer.html b/frappe_desk_theme/templates/includes/desk_footer.html index d69a978..989f72e 100644 --- a/frappe_desk_theme/templates/includes/desk_footer.html +++ b/frappe_desk_theme/templates/includes/desk_footer.html @@ -19,4 +19,4 @@ document.body.classList.add('has-sticky-footer'); {% endif %} -{% endif %} \ No newline at end of file +{% endif %} \ No newline at end of file From 199af3ac2c9228ba5e84e59aa848a15b59ab8522 Mon Sep 17 00:00:00 2001 From: Bhushan Barbuddhe <141770295+bhushan-barbuddhe@users.noreply.github.com> Date: Fri, 14 Nov 2025 17:07:12 +0530 Subject: [PATCH 151/274] feat: add new SVG icons (#7) --- frappe_desk_theme/hooks.py | 2 +- frappe_desk_theme/public/icons.svg | 1275 ++++++++++++++++++++++++++++ 2 files changed, 1276 insertions(+), 1 deletion(-) create mode 100644 frappe_desk_theme/public/icons.svg diff --git a/frappe_desk_theme/hooks.py b/frappe_desk_theme/hooks.py index 8319d2f..21eb600 100644 --- a/frappe_desk_theme/hooks.py +++ b/frappe_desk_theme/hooks.py @@ -55,7 +55,7 @@ # Svg Icons # ------------------ # include app icons in desk -# app_include_icons = "frappe_desk_theme/public/icons.svg" +app_include_icons = "/assets/frappe_desk_theme/icons.svg" # Home Pages # ---------- diff --git a/frappe_desk_theme/public/icons.svg b/frappe_desk_theme/public/icons.svg new file mode 100644 index 0000000..ce98548 --- /dev/null +++ b/frappe_desk_theme/public/icons.svg @@ -0,0 +1,1275 @@ + + \ No newline at end of file From 202bbd0a7e7c89eb029b53d5d843c003f39c2c4b Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Sun, 4 Jan 2026 15:27:10 +0530 Subject: [PATCH 152/274] ci: update GitHub workflows From 932ee4f0e8db34d8a1672c392e865f3609ed1cb7 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Sun, 4 Jan 2026 15:27:12 +0530 Subject: [PATCH 153/274] ci: update GitHub workflows From 1cf068d218bc862dd0e917ab0a965f33d5f15958 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Sun, 4 Jan 2026 15:27:13 +0530 Subject: [PATCH 154/274] ci: update GitHub workflows --- .github/workflows/auto-reviewer.yml | 90 +++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 .github/workflows/auto-reviewer.yml diff --git a/.github/workflows/auto-reviewer.yml b/.github/workflows/auto-reviewer.yml new file mode 100644 index 0000000..940ddde --- /dev/null +++ b/.github/workflows/auto-reviewer.yml @@ -0,0 +1,90 @@ +name: Auto Request Review + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + branches: [ main, master ] + +permissions: + pull-requests: write + contents: read + +jobs: + request-review: + name: Request Review from Default Reviewer + runs-on: ubuntu-latest + if: | + github.event.pull_request.base.ref == 'main' || + github.event.pull_request.base.ref == 'master' + + steps: + - name: Request review from default reviewer + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const reviewer = 'dhwani-ankit'; + const pr = context.payload.pull_request; + + console.log(`Processing PR #${pr.number} targeting ${pr.base.ref}`); + + // Check if PR is not in draft state + if (pr.draft) { + console.log('PR is in draft state, skipping review request'); + return; + } + + // Wait a bit to ensure PR is fully ready + await new Promise(resolve => setTimeout(resolve, 2000)); + + try { + // Get current reviewers + const { data: currentReviews } = await github.rest.pulls.listRequestedReviewers({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr.number, + }); + + console.log('Current reviewers:', JSON.stringify(currentReviews, null, 2)); + + // Check if reviewer is already requested + const isAlreadyRequested = currentReviews.users?.some( + user => user.login.toLowerCase() === reviewer.toLowerCase() + ) || currentReviews.teams?.some( + team => team.slug.toLowerCase() === reviewer.toLowerCase() + ); + + if (isAlreadyRequested) { + console.log(`✅ Review already requested from ${reviewer}`); + return; + } + + // Request review from default reviewer + await github.rest.pulls.requestReviewers({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr.number, + reviewers: [reviewer], + }); + + console.log(`✅ Successfully requested review from ${reviewer}`); + } catch (error) { + console.error(`❌ Error requesting review from ${reviewer}:`, error); + console.error('Error details:', JSON.stringify(error, null, 2)); + + // If user not found, try to add as assignee instead + if (error.status === 422 || error.message.includes('not found')) { + try { + await github.rest.issues.addAssignees({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + assignees: [reviewer], + }); + console.log(`✅ Added ${reviewer} as assignee instead`); + } catch (assignError) { + console.error(`⚠️ Could not add ${reviewer} as assignee:`, assignError.message); + } + } + } + From 5c703c78cf53af23ac1443a0aad76959087c7960 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Sun, 4 Jan 2026 15:27:15 +0530 Subject: [PATCH 155/274] ci: update GitHub workflows --- .github/workflows/bot-handler.yml | 162 ++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 .github/workflows/bot-handler.yml diff --git a/.github/workflows/bot-handler.yml b/.github/workflows/bot-handler.yml new file mode 100644 index 0000000..e91c578 --- /dev/null +++ b/.github/workflows/bot-handler.yml @@ -0,0 +1,162 @@ +name: Dhwani Release Bot Handler + +on: + issue_comment: + types: [created, edited] + pull_request: + types: [opened, synchronize, reopened] + branches: [ main, master ] + +permissions: + contents: write + issues: write + pull-requests: write + id-token: write + actions: write + +jobs: + handle-bot-mention: + name: Handle Bot Commands + runs-on: ubuntu-latest + if: | + (github.event.issue_comment.body != null && contains(github.event.issue_comment.body, '@dhwani-release-bot')) || + (github.event.pull_request.body != null && contains(github.event.pull_request.body, '@dhwani-release-bot')) + + steps: + - name: Generate GitHub App Token + id: generate-token + uses: tibdex/github-app-token@v1 + with: + app_id: ${{ secrets.DHWANI_RELEASE_BOT_APP_ID }} + private_key: ${{ secrets.DHWANI_RELEASE_BOT_PRIVATE_KEY }} + + - name: Checkout Repository + uses: actions/checkout@v5 + with: + token: ${{ steps.generate-token.outputs.token }} + fetch-depth: 0 + + - name: Parse Bot Command + id: parse-command + run: | + if [ "${{ github.event_name }}" == "issue_comment" ]; then + COMMENT_BODY="${{ github.event.issue_comment.body }}" + else + COMMENT_BODY="${{ github.event.pull_request.body }}" + fi + + echo "comment_body<> $GITHUB_OUTPUT + echo "$COMMENT_BODY" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + # Extract command after @dhwani-release-bot + COMMAND=$(echo "$COMMENT_BODY" | grep -oP '@dhwani-release-bot\s+\K\S+' | head -1 || echo "") + + if [ -z "$COMMAND" ]; then + COMMAND="help" + fi + + echo "command=$COMMAND" >> $GITHUB_OUTPUT + echo "Command detected: $COMMAND" + + - name: Handle Release Command + if: steps.parse-command.outputs.command == 'release' || steps.parse-command.outputs.command == 'trigger-release' + uses: actions/github-script@v7 + with: + github-token: ${{ steps.generate-token.outputs.token }} + script: | + const context = github.context; + const command = '${{ steps.parse-command.outputs.command }}'; + + // Trigger the release workflow + await github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'release.yml', + ref: context.ref || 'main' + }); + + // Add a comment + const comment = `🚀 Release workflow triggered by @${context.actor}!\n\nI've started the semantic release process. Check the [workflow run](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions) for progress.`; + + if (context.eventName === 'issue_comment') { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: comment + }); + } else if (context.eventName === 'pull_request') { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + body: comment + }); + } + + - name: Handle Help Command + if: steps.parse-command.outputs.command == 'help' || steps.parse-command.outputs.command == '' + uses: actions/github-script@v7 + with: + github-token: ${{ steps.generate-token.outputs.token }} + script: | + const context = github.context; + const helpText = `## 🤖 Dhwani Release Bot Commands + + Available commands: + + - \`@dhwani-release-bot release\` or \`@dhwani-release-bot trigger-release\` - Trigger a semantic release + - \`@dhwani-release-bot help\` - Show this help message + + ### How it works: + - The bot automatically creates releases when code is pushed to \`main\` or \`master\` branches + - You can also manually trigger releases using the commands above + - Developers can commit normally - the bot only handles releases + + Need help? Check the [workflow documentation](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/workflows/release.yml).`; + + if (context.eventName === 'issue_comment') { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: helpText + }); + } else if (context.eventName === 'pull_request') { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + body: helpText + }); + } + + - name: Handle Unknown Command + if: steps.parse-command.outputs.command != 'release' && steps.parse-command.outputs.command != 'trigger-release' && steps.parse-command.outputs.command != 'help' && steps.parse-command.outputs.command != '' + uses: actions/github-script@v7 + with: + github-token: ${{ steps.generate-token.outputs.token }} + script: | + const context = github.context; + const command = '${{ steps.parse-command.outputs.command }}'; + const unknownCommandText = `❓ Unknown command: \`${command}\` + + Type \`@dhwani-release-bot help\` to see available commands.`; + + if (context.eventName === 'issue_comment') { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: unknownCommandText + }); + } else if (context.eventName === 'pull_request') { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + body: unknownCommandText + }); + } + From 6db5ffb6fbc55658cf0d5cc2172e2d7cbfb6f3c8 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Sun, 4 Jan 2026 15:27:17 +0530 Subject: [PATCH 156/274] ci: update GitHub workflows --- .github/workflows/ci.yml | 280 +++++++++++++++++++++++++++++++++------ 1 file changed, 240 insertions(+), 40 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3d62c85..21fd72f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,25 +1,27 @@ name: CI on: - # Workflow disabled - uncomment below to re-enable - # push: - # branches: [ main, develop, development, master ] - # workflow_dispatch: push: - branches: - - 'never-run-this-workflow-disabled-branch-12345' + branches: [ main, master ] + pull_request: + branches: [ main, master ] + workflow_dispatch: permissions: contents: read +# Wait for init workflow to complete first (for push to main/master) +# Use same concurrency group as init workflow to ensure init runs first concurrency: - group: ci-${{ github.event_name }}-${{ github.event.number || github.sha }} - cancel-in-progress: true + group: ${{ (github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master')) && format('repo-init-{0}-{1}', github.repository, github.ref) || format('ci-{0}-{1}-{2}', github.event_name, github.ref, github.event.number || github.sha) }} + cancel-in-progress: false jobs: dependency-vulnerability: name: 'Vulnerable Dependency Check' runs-on: ubuntu-latest + # Run on merge to main/master (push event) + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') steps: - name: Setup Python @@ -48,6 +50,8 @@ jobs: test: name: 'Test' runs-on: ubuntu-latest + # Run on both PR and merge + if: github.event_name == 'pull_request' || (github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master')) strategy: matrix: python-version: ["3.8", "3.11", "3.13"] @@ -65,55 +69,260 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip + pip install pytest pytest-cov pytest-xdist pip install -e ".[dev]" || pip install -r requirements.txt || echo "No requirements found" - - name: Run tests + - name: Run tests with coverage run: | - pytest --cov=. --cov-report=xml || echo "No tests found or pytest not configured" + pytest --cov=. --cov-report=xml --cov-report=term --cov-report=html || echo "No tests found or pytest not configured" + continue-on-error: true + + - name: Check if coverage file exists + id: coverage-check + run: | + if [ -f coverage.xml ]; then + echo "exists=true" >> $GITHUB_OUTPUT + echo "Coverage file found" + else + echo "exists=false" >> $GITHUB_OUTPUT + echo "Coverage file not found - tests may not have run or pytest-cov not installed" + fi - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: - file: ./coverage.xml + files: ./coverage.xml fail_ci_if_error: false - if: always() + token: ${{ secrets.CODECOV_TOKEN }} + slug: ${{ github.repository }} + flags: unittests + name: codecov-umbrella + verbose: true + override_commit: ${{ github.event.pull_request.head.sha || github.sha }} + override_branch: ${{ github.event.pull_request.head.ref || github.ref_name }} + if: steps.coverage-check.outputs.exists == 'true' + + - name: Codecov Upload Status + if: always() && steps.coverage-check.outputs.exists == 'true' + run: | + echo "Codecov upload completed. Check https://codecov.io/gh/${{ github.repository }} for coverage reports." - security: - name: 'Security Check' + frappe-bench-test: + name: 'Frappe Bench App Tests' runs-on: ubuntu-latest + # Run on merge to main/master (push event) + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') + + services: + mariadb: + image: mariadb:10.11 + env: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: test_frappe + ports: + - 3306:3306 + options: >- + --health-cmd="mysqladmin ping -h localhost" + --health-interval=10s + --health-timeout=5s + --health-retries=5 + redis: + image: redis:7-alpine + ports: + - 6379:6379 + options: >- + --health-cmd="redis-cli ping" + --health-interval=10s + --health-timeout=5s + --health-retries=5 steps: - name: Checkout code uses: actions/checkout@v4 + - name: Check if Frappe app exists + id: check-app + run: | + if [ -f "__init__.py" ] || [ -f "hooks.py" ]; then + echo "exists=true" >> $GITHUB_OUTPUT + echo "Frappe app files found, proceeding with tests" + else + echo "exists=false" >> $GITHUB_OUTPUT + echo "No Frappe app files found (__init__.py or hooks.py), skipping Frappe bench tests" + fi + - name: Setup Python + if: steps.check-app.outputs.exists == 'true' uses: actions/setup-python@v5 with: - python-version: "3.13" + python-version: '3.12' + cache: pip - - name: Install security tools + - name: Setup Node.js + if: steps.check-app.outputs.exists == 'true' + uses: actions/setup-node@v4 + with: + node-version: 18 + + - name: Install system dependencies + if: steps.check-app.outputs.exists == 'true' run: | - python -m pip install --upgrade pip - pip install bandit safety + sudo apt-get update + sudo apt-get install -y \ + mariadb-client \ + redis-tools \ + curl \ + git \ + wget \ + xvfb \ + libfontconfig1 \ + libfreetype6 \ + libxrender1 \ + libjpeg-turbo8 \ + xfonts-75dpi \ + xfonts-base + + - name: Install Frappe Bench CLI + if: steps.check-app.outputs.exists == 'true' + run: | + pip install frappe-bench - - name: Run security checks + - name: Wait for MariaDB to be ready + if: steps.check-app.outputs.exists == 'true' + run: | + for i in {1..30}; do + if mysqladmin ping -h 127.0.0.1 -P 3306 -u root -proot --silent; then + echo "MariaDB is ready" + break + fi + echo "Waiting for MariaDB... ($i/30)" + sleep 2 + done + + - name: Wait for Redis to be ready + if: steps.check-app.outputs.exists == 'true' run: | - bandit -r . -f json -o bandit-report.json || true - safety check --json --output safety-report.json || true + for i in {1..30}; do + if redis-cli -h 127.0.0.1 -p 6379 ping | grep -q PONG; then + echo "Redis is ready" + break + fi + echo "Waiting for Redis... ($i/30)" + sleep 2 + done + + - name: Detect app name + if: steps.check-app.outputs.exists == 'true' + id: app-name + run: | + # Derive app name from repo name, removing frappe_ prefix if present + REPO_NAME=$(echo "${{ github.repository }}" | cut -d'/' -f2) + APP_NAME=$(echo "$REPO_NAME" | tr '-' '_' | tr '[:upper:]' '[:lower:]' | sed 's/^frappe_//') + + # If repo root has __init__.py or hooks.py, it IS the app + if [ -f "__init__.py" ] || [ -f "hooks.py" ]; then + echo "Repo root is the app: $APP_NAME" + else + # Try to find app directory + FOUND_APP=$(find . -maxdepth 2 -name "__init__.py" -type f | head -1 | xargs dirname | xargs basename) + if [ -n "$FOUND_APP" ] && [ "$FOUND_APP" != "." ]; then + APP_NAME="$FOUND_APP" + echo "Found app directory: $APP_NAME" + fi + fi + + if [ -z "$APP_NAME" ] || [ "$APP_NAME" = "." ]; then + echo "Error: Could not detect app name" + exit 1 + fi + + echo "name=$APP_NAME" >> $GITHUB_OUTPUT + echo "APP_PATH=$(pwd)" >> $GITHUB_OUTPUT + echo "Detected app name: $APP_NAME" + + - name: Initialize Frappe Bench + if: steps.check-app.outputs.exists == 'true' + run: | + bench init --skip-redis-config-generation --skip-assets --frappe-branch version-15 frappe-bench + cd frappe-bench - - name: Upload security reports + - name: Get app into bench + if: steps.check-app.outputs.exists == 'true' + working-directory: frappe-bench + run: | + APP_NAME="${{ steps.app-name.outputs.name }}" + APP_PATH="${{ steps.app-name.outputs.APP_PATH }}" + + # Check if app is in repo root (most common case) + if [ -f "$APP_PATH/__init__.py" ] || [ -f "$APP_PATH/hooks.py" ]; then + echo "App is in repo root, using local path" + bench get-app $APP_NAME $APP_PATH + elif [ -d "$APP_PATH/$APP_NAME" ] && [ -f "$APP_PATH/$APP_NAME/__init__.py" ]; then + echo "App is in subdirectory" + bench get-app $APP_NAME $APP_PATH/$APP_NAME + else + echo "Error: Could not find app at expected location" + exit 1 + fi + + - name: Create test site + if: steps.check-app.outputs.exists == 'true' + working-directory: frappe-bench + env: + DB_HOST: 127.0.0.1 + DB_PORT: 3306 + DB_ROOT_USER: root + DB_ROOT_PASSWORD: root + REDIS_CACHE: redis://127.0.0.1:6379 + REDIS_QUEUE: redis://127.0.0.1:6379 + run: | + APP_NAME="${{ steps.app-name.outputs.name }}" + + # Create site with proper database connection + bench new-site test_site \ + --db-type mariadb \ + --admin-password admin \ + --no-mariadb-socket \ + --mariadb-host 127.0.0.1 \ + --mariadb-port 3306 \ + --mariadb-root-password root \ + --install-app $APP_NAME || { + echo "Site creation or app installation failed" + exit 1 + } + + - name: Run app-specific tests + if: steps.check-app.outputs.exists == 'true' + working-directory: frappe-bench + env: + DB_HOST: 127.0.0.1 + DB_PORT: 3306 + REDIS_CACHE: redis://127.0.0.1:6379 + REDIS_QUEUE: redis://127.0.0.1:6379 + run: | + APP_NAME="${{ steps.app-name.outputs.name }}" + echo "Running tests for app: $APP_NAME" + + # Run tests and fail if they fail + bench --site test_site run-tests --app $APP_NAME || { + echo "Tests failed for app $APP_NAME" + exit 1 + } + + - name: Upload test results + if: always() && steps.check-app.outputs.exists == 'true' uses: actions/upload-artifact@v4 with: - name: security-reports + name: frappe-test-results path: | - bandit-report.json - safety-report.json + frappe-bench/sites/test_site/logs/*.log + if-no-files-found: ignore dependency-update: name: 'Dependency Update Check' runs-on: ubuntu-latest - needs: dependency-vulnerability - if: github.event_name == 'pull_request' + # Run only on PR (to check if dependencies need updating before merge) + if: github.event_name == 'pull_request' && (github.base_ref == 'main' || github.base_ref == 'master') steps: - name: Checkout code @@ -132,7 +341,6 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pip-audit if [ -f package-lock.json ] || [ -f npm-shrinkwrap.json ]; then npm ci; else echo "No npm lockfile, skipping npm ci"; fi - name: Check for outdated Python dependencies @@ -143,20 +351,12 @@ jobs: run: | if [ -f package.json ]; then npm outdated || echo "No outdated Node.js packages found"; else echo "No package.json, skipping npm outdated"; fi - - name: Run pip-audit - run: | - pip-audit --format json --output pip-audit-report.json || true - - - name: Upload audit reports - uses: actions/upload-artifact@v4 - with: - name: audit-reports - path: pip-audit-report.json - build: name: 'Build Check' runs-on: ubuntu-latest needs: [test] + # Run on merge to main/master (push event) + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') steps: - name: Checkout code @@ -193,4 +393,4 @@ jobs: path: | dist/ build/ - if-no-files-found: ignore + if-no-files-found: ignore \ No newline at end of file From c4951ee48aef7b5184ec24e69c5fae6c62d90289 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Sun, 4 Jan 2026 15:27:18 +0530 Subject: [PATCH 157/274] ci: update GitHub workflows --- .github/workflows/code-quality.yml | 49 +++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 622be62..6df133a 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -2,6 +2,8 @@ name: Quality Checks on: pull_request: + branches: [ main, master ] + types: [opened, synchronize, reopened, ready_for_review] workflow_dispatch: permissions: @@ -29,6 +31,34 @@ jobs: - name: Check commit titles run: | npm install @commitlint/cli @commitlint/config-conventional + + # Verify config file exists + if [ ! -f commitlint.config.js ]; then + echo "Error: commitlint.config.js not found" + exit 1 + fi + + echo "=== Commitlint config file ===" + cat commitlint.config.js + echo "" + + # Check if packages are installed + echo "=== Installed packages ===" + npm list @commitlint/cli @commitlint/config-conventional || true + echo "" + + # Test commitlint config loading + echo "=== Testing commitlint config ===" + npx commitlint --print-config || echo "Config test failed" + echo "" + + # Show commits to be checked + echo "=== Commits to check ===" + git log --oneline ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }} || echo "No commits found" + echo "" + + # Run commitlint (it will auto-detect commitlint.config.js in root) + echo "=== Running commitlint ===" npx commitlint --verbose --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} docs-required: @@ -94,7 +124,7 @@ jobs: with: python-version: '3.13' cache: pip - - uses: pre-commit/action@v3.0.1 + - name: Check for pre-commit config run: | if [ ! -f .pre-commit-config.yaml ]; then @@ -104,8 +134,17 @@ jobs: echo "Pre-commit config already exists." fi - - name: Install pre-commit - run: pip install pre-commit + - name: Install pre-commit and dependencies + run: | + pip install pre-commit + pre-commit install --install-hooks - - name: Run pre-commit - run: pre-commit run --all-files \ No newline at end of file + - name: Run pre-commit (check only, no auto-fix) + run: | + # Run pre-commit in CI mode - it will check but not modify files + # This will fail if files need formatting, which is what we want + pre-commit run --all-files --show-diff-on-failure || { + echo "❌ Pre-commit checks failed. Some files need formatting or have issues." + echo "Please run 'pre-commit run --all-files' locally to fix the issues." + exit 1 + } \ No newline at end of file From 0a395a5be5cf704e4cbbcfed76c1c64c86a4edc9 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Sun, 4 Jan 2026 15:27:20 +0530 Subject: [PATCH 158/274] ci: update GitHub workflows --- .github/workflows/pr-labeler.yml | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml index 106cea1..82e654b 100644 --- a/.github/workflows/pr-labeler.yml +++ b/.github/workflows/pr-labeler.yml @@ -4,7 +4,7 @@ name: "PR Labeler" on: pull_request: types: [opened, reopened] - branches: [main, master, develop, development, uat] # Label PRs to these branches + branches: [main, master] # Label PRs to these branches permissions: contents: read @@ -13,8 +13,8 @@ permissions: jobs: label: runs-on: ubuntu-latest - # Only run for PRs targeting main, master, develop, development, or uat branches - if: contains(fromJson('["main", "master", "develop", "development", "uat"]'), github.event.pull_request.base.ref) + # Only run for PRs targeting main or master branches + if: contains(fromJson('["main", "master"]'), github.event.pull_request.base.ref) steps: - name: Auto Label PR uses: actions/github-script@v7 @@ -58,12 +58,6 @@ jobs: if (targetBranch === 'main' || targetBranch === 'master') { labels.push('release'); console.log('🚀 Production release PR'); - } else if (targetBranch === 'uat') { - labels.push('uat-release'); - console.log('🧪 UAT release PR'); - } else if (targetBranch === 'develop' || targetBranch === 'development') { - labels.push('development'); - console.log('🛠️ Development PR'); } // Add labels with error handling @@ -127,4 +121,4 @@ jobs: console.log(`⚠️ Error adding labels: ${error.message}`); } } - } + } \ No newline at end of file From 3ee16aa7d8e6a30688298c870dbc1eb965197e01 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Sun, 4 Jan 2026 15:27:21 +0530 Subject: [PATCH 159/274] ci: update GitHub workflows --- .github/workflows/release.yml | 102 +++++++++++++++++++++++++++++++--- 1 file changed, 94 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0d52218..afee259 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,6 +2,13 @@ name: Generate Semantic Release on: push: branches: [ main, master ] + workflow_dispatch: + inputs: + force_release: + description: 'Force release even if no changes detected' + required: false + default: 'false' + type: boolean permissions: contents: write @@ -9,33 +16,112 @@ permissions: pull-requests: write id-token: write actions: read + +# Wait for init workflow to complete first +# Use same concurrency group as init workflow to ensure init runs first +concurrency: + group: repo-init-${{ github.repository }}-${{ github.ref }} + cancel-in-progress: false jobs: release: name: Release runs-on: ubuntu-latest steps: + - name: Generate GitHub App Token + id: generate-token + uses: tibdex/github-app-token@v1 + with: + app_id: ${{ secrets.DHWANI_RELEASE_BOT_APP_ID }} + private_key: ${{ secrets.DHWANI_RELEASE_BOT_PRIVATE_KEY }} + - name: Checkout Entire Repository uses: actions/checkout@v5 with: fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ steps.generate-token.outputs.token }} persist-credentials: true - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 22 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' - name: Setup dependencies run: | - npm install @semantic-release/git @semantic-release/exec @semantic-release/github @semantic-release/changelog --no-save + npm install semantic-release @semantic-release/git @semantic-release/exec @semantic-release/github @semantic-release/changelog @semantic-release/commit-analyzer @semantic-release/release-notes-generator --no-save + + - name: Configure Git + run: | + git config --global user.name "dhwani-release-bot" + git config --global user.email "dhwani-release-bot[bot]@users.noreply.github.com" + + - name: Debug - Check commits + run: | + echo "=== Recent commits ===" + git log --oneline -10 + echo "" + echo "=== Commits since last tag ===" + LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + if [ -z "$LAST_TAG" ]; then + echo "No previous tag, showing all commits" + git log --oneline --no-merges + else + echo "Commits since $LAST_TAG:" + git log --oneline --no-merges ${LAST_TAG}..HEAD + fi + echo "" + echo "=== Last tag ===" + git describe --tags --abbrev=0 2>/dev/null || echo "No tags found" + echo "" + echo "=== Semantic commits only ===" + git log --oneline --no-merges --grep="^feat\|^fix\|^perf\|^revert" -10 || echo "No semantic commits found" + + - name: Filter merge commits + run: | + echo "=== Filtering merge commits ===" + LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + if [ -z "$LAST_TAG" ]; then + echo "No previous tag, checking all commits" + git log --oneline --no-merges --format="%H %s" > /tmp/filtered_commits.txt + else + echo "Checking commits since $LAST_TAG" + git log --oneline --no-merges ${LAST_TAG}..HEAD --format="%H %s" > /tmp/filtered_commits.txt + fi + echo "Filtered commits (excluding merge commits):" + cat /tmp/filtered_commits.txt || echo "No commits found" - name: Create Release env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GIT_AUTHOR_NAME: "github-actions[bot]" - GIT_AUTHOR_EMAIL: "github-actions[bot]@users.noreply.github.com" - GIT_COMMITTER_NAME: "github-actions[bot]" - GIT_COMMITTER_EMAIL: "github-actions[bot]@users.noreply.github.com" - run: npx semantic-release + GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }} + GIT_AUTHOR_NAME: "dhwani-release-bot" + GIT_AUTHOR_EMAIL: "dhwani-release-bot[bot]@users.noreply.github.com" + GIT_COMMITTER_NAME: "dhwani-release-bot" + GIT_COMMITTER_EMAIL: "dhwani-release-bot[bot]@users.noreply.github.com" + NODE_ENV: production + run: | + echo "Running semantic-release..." + npx semantic-release --debug || { + echo "Semantic-release exited with code $?" + echo "This might be normal if no release is needed" + exit 0 + } + + - name: Verify Release Created + if: always() + run: | + echo "=== Checking for new releases ===" + LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "none") + echo "Latest tag: $LATEST_TAG" + + if [ "$LATEST_TAG" != "none" ]; then + echo "✅ Release tag found: $LATEST_TAG" + git show-ref --tags | tail -5 + else + echo "ℹ️ No new release tag created (this is normal if no semantic commits found)" + fi \ No newline at end of file From 79e826a0576d725173baf9aa9c4017366bfcf94a Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Sun, 4 Jan 2026 15:27:23 +0530 Subject: [PATCH 160/274] ci: update GitHub workflows --- .github/workflows/security-scan.yml | 234 ++++++++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 .github/workflows/security-scan.yml diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml new file mode 100644 index 0000000..3093b64 --- /dev/null +++ b/.github/workflows/security-scan.yml @@ -0,0 +1,234 @@ +name: Enhanced Security Scan + +on: + pull_request: + branches: [ main, master ] + types: [opened, synchronize, reopened, ready_for_review] + push: + branches: [ main, master ] + schedule: + # Run daily security scans on main/master + - cron: '0 2 * * *' + workflow_dispatch: + +permissions: + contents: read + security-events: write + actions: read + +jobs: + codeql-analysis: + name: CodeQL Security Analysis + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: ['python', 'javascript'] + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + queries: security-extended,security-and-quality + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{ matrix.language }}" + + secret-scanning: + name: Secret Scanning + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Run Gitleaks + uses: gitleaks/gitleaks-action@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Run TruffleHog + uses: trufflesecurity/trufflehog@main + with: + path: ./ + base: ${{ github.event.repository.default_branch }} + head: HEAD + extra_args: --only-verified + + dependency-scanning: + name: Dependency Vulnerability Scan + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Run pip-audit + run: | + pip install pip-audit + pip-audit --desc --format json --output pip-audit-report.json || true + + - name: Run Safety Check + run: | + pip install safety + safety check --json --output safety-report.json || true + + - name: Run Bandit Security Scan + run: | + pip install bandit[toml] + bandit -r . -f json -o bandit-report.json || true + + - name: Upload security reports + uses: actions/upload-artifact@v4 + with: + name: security-reports-${{ github.run_id }} + path: | + pip-audit-report.json + safety-report.json + bandit-report.json + retention-days: 30 + + - name: Comment PR with findings + if: github.event_name == 'pull_request' + uses: actions/github-script@v8 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs'); + const path = require('path'); + + let comment = '## 🔒 Security Scan Results\n\n'; + let hasIssues = false; + + // Check pip-audit + try { + if (fs.existsSync('pip-audit-report.json')) { + const report = JSON.parse(fs.readFileSync('pip-audit-report.json', 'utf8')); + if (report.vulnerabilities && report.vulnerabilities.length > 0) { + hasIssues = true; + comment += `### ⚠️ Dependency Vulnerabilities Found\n\n`; + comment += `Found ${report.vulnerabilities.length} vulnerable dependency(ies).\n\n`; + report.vulnerabilities.slice(0, 10).forEach(vuln => { + comment += `- **${vuln.name}**: ${vuln.id} - ${vuln.fix_versions ? `Fix: ${vuln.fix_versions.join(', ')}` : 'No fix available'}\n`; + }); + comment += `\n`; + } + } + } catch (e) { + console.log('Error reading pip-audit report:', e); + } + + // Check safety + try { + if (fs.existsSync('safety-report.json')) { + const report = JSON.parse(fs.readFileSync('safety-report.json', 'utf8')); + if (report.vulnerabilities && report.vulnerabilities.length > 0) { + hasIssues = true; + comment += `### ⚠️ Safety Check Findings\n\n`; + comment += `Found ${report.vulnerabilities.length} issue(s).\n\n`; + } + } + } catch (e) { + console.log('Error reading safety report:', e); + } + + // Check bandit + try { + if (fs.existsSync('bandit-report.json')) { + const report = JSON.parse(fs.readFileSync('bandit-report.json', 'utf8')); + if (report.metrics && report.metrics._totals) { + const totals = report.metrics._totals; + if (totals.HIGH > 0 || totals.MEDIUM > 0) { + hasIssues = true; + comment += `### ⚠️ Code Security Issues (Bandit)\n\n`; + comment += `- High: ${totals.HIGH}\n`; + comment += `- Medium: ${totals.MEDIUM}\n`; + comment += `- Low: ${totals.LOW}\n\n`; + } + } + } + } catch (e) { + console.log('Error reading bandit report:', e); + } + + if (!hasIssues) { + comment += '✅ No security issues detected in this scan.\n'; + } else { + comment += '\n📋 Full reports are available in the workflow artifacts.\n'; + } + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: comment + }); + + container-scanning: + name: Container Security Scan + runs-on: ubuntu-latest + if: hashFiles('**/Dockerfile') != '' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + scan-type: 'fs' + scan-ref: '.' + format: 'sarif' + output: 'trivy-results.sarif' + + - name: Upload Trivy results to GitHub Security + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: 'trivy-results.sarif' + + security-summary: + name: Security Summary + runs-on: ubuntu-latest + needs: [codeql-analysis, secret-scanning, dependency-scanning] + if: always() + + steps: + - name: Generate Security Summary + uses: actions/github-script@v8 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const needs = ${{ toJSON(needs) }}; + const summary = `## 🔒 Security Scan Summary + + | Scan Type | Status | + |-----------|--------| + | CodeQL Analysis | ${needs.codeql-analysis.result === 'success' ? '✅ Passed' : needs.codeql-analysis.result === 'failure' ? '❌ Failed' : '⚠️ Skipped'} | + | Secret Scanning | ${needs.secret-scanning.result === 'success' ? '✅ Passed' : needs.secret-scanning.result === 'failure' ? '❌ Failed' : '⚠️ Skipped'} | + | Dependency Scan | ${needs.dependency-scanning.result === 'success' ? '✅ Passed' : needs.dependency-scanning.result === 'failure' ? '❌ Failed' : '⚠️ Skipped'} | + + **View detailed results in the Security tab or workflow artifacts.**`; + + core.summary.addRaw(summary).write(); + From 86d010763d5037b53e1d7ca43c2474059ffd9fc5 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Sun, 4 Jan 2026 15:27:24 +0530 Subject: [PATCH 161/274] ci: update GitHub workflows From 385d2aa69ff1df87d5db30581965c3ba641259fc Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Sun, 4 Jan 2026 15:27:26 +0530 Subject: [PATCH 162/274] ci: update GitHub workflows --- .pre-commit-config.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 460801c..7d1e0e5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,12 +29,15 @@ repos: - id: ruff name: "Run ruff import sorter" args: ["--select=I", "--fix"] + exclude: ^\.github/helper/ - id: ruff name: "Run ruff linter" + exclude: ^\.github/helper/ - id: ruff-format name: "Run ruff formatter" + exclude: ^\.github/helper/ - repo: https://github.com/pre-commit/mirrors-prettier rev: v2.7.1 From 99fbc9d1a5aad192169d1bbf71022e3d3a8702d2 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Sun, 4 Jan 2026 15:27:27 +0530 Subject: [PATCH 163/274] ci: update GitHub workflows --- .releaserc | 78 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 68 insertions(+), 10 deletions(-) diff --git a/.releaserc b/.releaserc index 8b457ba..f12ae44 100644 --- a/.releaserc +++ b/.releaserc @@ -1,21 +1,79 @@ { "branches": ["main", "master"], "plugins": [ - ["@semantic-release/commit-analyzer", { - "preset": "angular" - }], - "@semantic-release/release-notes-generator", [ - "@semantic-release/exec", { + "@semantic-release/commit-analyzer", + { + "preset": "angular", + "releaseRules": [ + { "type": "feat", "release": "minor" }, + { "type": "fix", "release": "patch" }, + { "type": "perf", "release": "patch" }, + { "type": "revert", "release": "patch" }, + { "type": "docs", "release": false }, + { "type": "style", "release": false }, + { "type": "chore", "release": false }, + { "type": "refactor", "release": false }, + { "type": "test", "release": false }, + { "type": "build", "release": false }, + { "type": "ci", "release": false }, + { "breaking": true, "release": "major" } + ], + "parserOpts": { + "noteKeywords": ["BREAKING CHANGE", "BREAKING CHANGES"] + } + } + ], + [ + "@semantic-release/release-notes-generator", + { + "preset": "angular", + "presetConfig": { + "types": [ + { "type": "feat", "section": "✨ Features" }, + { "type": "fix", "section": "🐛 Bug Fixes" }, + { "type": "perf", "section": "⚡ Performance Improvements" }, + { "type": "revert", "section": "⏪ Reverts" }, + { "type": "docs", "section": "📝 Documentation", "hidden": false }, + { "type": "style", "section": "💎 Styles", "hidden": false }, + { "type": "refactor", "section": "♻️ Code Refactoring", "hidden": false }, + { "type": "test", "section": "✅ Tests", "hidden": false }, + { "type": "build", "section": "🏗️ Build System", "hidden": false }, + { "type": "ci", "section": "👷 Continuous Integration", "hidden": false }, + { "type": "chore", "section": "🔧 Miscellaneous Chores", "hidden": false } + ] + }, + "writerOpts": { + "commitsSort": ["scope", "subject"] + } + } + ], + [ + "@semantic-release/changelog", + { + "changelogFile": "CHANGELOG.md" + } + ], + [ + "@semantic-release/exec", + { "prepareCmd": "python $GITHUB_WORKSPACE/.github/helper/update-version.py ${nextRelease.version}" } ], [ - "@semantic-release/git", { - "assets": ["*/__init__.py"], - "message": "chore(release): Bumped to Version ${nextRelease.version}" + "@semantic-release/git", + { + "assets": ["CHANGELOG.md", "*/__init__.py"], + "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" } ], - "@semantic-release/github" + [ + "@semantic-release/github", + { + "successComment": false, + "releasedLabels": false, + "assets": [] + } + ] ] -} +} \ No newline at end of file From 161758b8bb4fb52ae6e5f483e52f2682067c9ffc Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Sun, 4 Jan 2026 15:27:29 +0530 Subject: [PATCH 164/274] ci: update GitHub workflows --- commitlint.config.js | 29 +++-------------------------- 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/commitlint.config.js b/commitlint.config.js index 0c582f5..1953490 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -1,26 +1,3 @@ -module.exports = { - parserPreset: "conventional-changelog-conventionalcommits", - rules: { - "subject-empty": [2, "never"], - "type-case": [2, "always", "lower-case"], - "type-empty": [2, "never"], - "type-enum": [ - 2, - "always", - [ - "build", - "chore", - "ci", - "docs", - "feat", - "fix", - "perf", - "refactor", - "revert", - "style", - "test", - "deprecate", // deprecation decision - ], - ], - }, -}; +module.exports = { + extends: ['@commitlint/config-conventional'], +}; From 90b6f7efcaca13636ba448ca3368b2eca298cf5e Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Sun, 4 Jan 2026 15:27:31 +0530 Subject: [PATCH 165/274] ci: update GitHub workflows From d42b4c55c327fa72c831e7ac452b173f8d9e299d Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Mon, 5 Jan 2026 18:06:00 +0530 Subject: [PATCH 166/274] ci: update GitHub workflows From 5b66affb5e85c6334a99f16304876f2dea48db57 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Mon, 5 Jan 2026 18:06:01 +0530 Subject: [PATCH 167/274] ci: update GitHub workflows From 92c02f52f96f629934841295d5bbdaeadae25ab5 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Mon, 5 Jan 2026 18:06:03 +0530 Subject: [PATCH 168/274] ci: update GitHub workflows From cf8a5c4fa2ec02821343e4cda5dd798c3b07b854 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Mon, 5 Jan 2026 18:06:04 +0530 Subject: [PATCH 169/274] ci: update GitHub workflows From 75c77428ed2bcc247cd4d41d4771f6c4ff2ea64b Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Mon, 5 Jan 2026 18:06:06 +0530 Subject: [PATCH 170/274] ci: update GitHub workflows --- .github/workflows/ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 21fd72f..68a7eb9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: [ main, master ] + branches: [ main, master, develop, development ] pull_request: - branches: [ main, master ] + branches: [ main, master, develop, development ] workflow_dispatch: permissions: @@ -21,7 +21,7 @@ jobs: name: 'Vulnerable Dependency Check' runs-on: ubuntu-latest # Run on merge to main/master (push event) - if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/development') steps: - name: Setup Python @@ -51,7 +51,7 @@ jobs: name: 'Test' runs-on: ubuntu-latest # Run on both PR and merge - if: github.event_name == 'pull_request' || (github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master')) + if: github.event_name == 'pull_request' || (github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/development')) strategy: matrix: python-version: ["3.8", "3.11", "3.13"] @@ -111,7 +111,7 @@ jobs: name: 'Frappe Bench App Tests' runs-on: ubuntu-latest # Run on merge to main/master (push event) - if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/development') services: mariadb: @@ -356,7 +356,7 @@ jobs: runs-on: ubuntu-latest needs: [test] # Run on merge to main/master (push event) - if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/development') steps: - name: Checkout code From 2fca21f4c6a9d8fedb213d922810434df41f5fc5 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Mon, 5 Jan 2026 18:06:07 +0530 Subject: [PATCH 171/274] ci: update GitHub workflows --- .github/workflows/code-quality.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 6df133a..5218307 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -2,8 +2,10 @@ name: Quality Checks on: pull_request: - branches: [ main, master ] + branches: [ main, master, develop, development ] types: [opened, synchronize, reopened, ready_for_review] + push: + branches: [ develop, development ] workflow_dispatch: permissions: @@ -64,7 +66,7 @@ jobs: docs-required: name: 'Documentation Required' runs-on: ubuntu-latest - if: github.event_name == 'pull_request' + if: github.event_name == 'pull_request' || (github.event_name == 'push' && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/development')) steps: - name: 'Setup Environment' @@ -96,7 +98,7 @@ jobs: linter: name: 'Semgrep Rules' runs-on: ubuntu-latest - if: github.event_name == 'pull_request' + if: github.event_name == 'pull_request' || (github.event_name == 'push' && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/development')) steps: - uses: actions/checkout@v4 @@ -116,7 +118,7 @@ jobs: precommit: name: 'Pre-Commit' runs-on: ubuntu-latest - if: github.event_name == 'pull_request' + if: github.event_name == 'pull_request' || (github.event_name == 'push' && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/development')) steps: - uses: actions/checkout@v4 From 18c66e18b2edd25cc8ce07e46ecef41bdfe5c825 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Mon, 5 Jan 2026 18:06:09 +0530 Subject: [PATCH 172/274] ci: update GitHub workflows From 022b720457b1e513b4528bca18603ec6f2246a76 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Mon, 5 Jan 2026 18:06:10 +0530 Subject: [PATCH 173/274] ci: update GitHub workflows From 6e9f8ba2ddce770f5bcda75f079bea850bae7803 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Mon, 5 Jan 2026 18:06:12 +0530 Subject: [PATCH 174/274] ci: update GitHub workflows From 1ae62b19ca90660290de0ec59d304cdad1051765 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Mon, 5 Jan 2026 18:06:14 +0530 Subject: [PATCH 175/274] ci: update GitHub workflows From 8a3c2480b22dff42406dde276fa85cdeb6f034df Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Mon, 5 Jan 2026 18:06:15 +0530 Subject: [PATCH 176/274] ci: update GitHub workflows From 66de08d69fb173ad63179e223c686867a1fdefb5 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Mon, 5 Jan 2026 18:06:17 +0530 Subject: [PATCH 177/274] ci: update GitHub workflows From 85ca8b571a2be1623e2e493870f96345812339e4 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Mon, 5 Jan 2026 18:06:18 +0530 Subject: [PATCH 178/274] ci: update GitHub workflows From 9ca09841b19672dcf7f8ac52261cbedd7aa67da3 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Mon, 5 Jan 2026 18:06:20 +0530 Subject: [PATCH 179/274] ci: update GitHub workflows From 17e17c9e49f88accb2ba88767288afbd49b36d6a Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Tue, 6 Jan 2026 18:43:55 +0530 Subject: [PATCH 180/274] ci: update GitHub workflows From 975eb4ac04d41789aef410906c145a18de634ba6 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Tue, 6 Jan 2026 18:43:56 +0530 Subject: [PATCH 181/274] ci: update GitHub workflows From 940f6c6912372b7abaa21e6d971c2ca39f512a10 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Tue, 6 Jan 2026 18:43:58 +0530 Subject: [PATCH 182/274] ci: update GitHub workflows From d15d124f037a8912a76bd7d5a1c6712fdd6a99ed Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Tue, 6 Jan 2026 18:43:59 +0530 Subject: [PATCH 183/274] ci: update GitHub workflows From d596fb4d7868c3560d7fd4022f1dd474bdaf7458 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Tue, 6 Jan 2026 18:44:01 +0530 Subject: [PATCH 184/274] ci: update GitHub workflows From 31c05947fc3bf2c3f3c6efec173b91402000f543 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Tue, 6 Jan 2026 18:44:02 +0530 Subject: [PATCH 185/274] ci: update GitHub workflows From 9551300d05a8422e0560ff91082ad797d9ac05d8 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Tue, 6 Jan 2026 18:44:04 +0530 Subject: [PATCH 186/274] ci: update GitHub workflows From 15321d03c8f1e275a11486fdc6b52f74708e5ec0 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Tue, 6 Jan 2026 18:44:06 +0530 Subject: [PATCH 187/274] ci: update GitHub workflows From dc93e2abcb3e2284f67c883b622bd58f48cae08e Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Tue, 6 Jan 2026 18:44:07 +0530 Subject: [PATCH 188/274] ci: update GitHub workflows From 9ecdf6207f7fd576d27f6b0142db23ac42ae2aa2 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Tue, 6 Jan 2026 18:44:09 +0530 Subject: [PATCH 189/274] ci: update GitHub workflows From 43d217ee620ade98155279d4cb359d767c44fdba Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Tue, 6 Jan 2026 18:44:10 +0530 Subject: [PATCH 190/274] ci: update GitHub workflows From 8342767b3c61e8aab76ca71690156b19e6aebf0f Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Tue, 6 Jan 2026 18:44:12 +0530 Subject: [PATCH 191/274] ci: update GitHub workflows From 3100e3ee0f957712a42a967b1fd69acd7881e4a9 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Tue, 6 Jan 2026 18:44:13 +0530 Subject: [PATCH 192/274] ci: update GitHub workflows --- commitlint.config.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/commitlint.config.js b/commitlint.config.js index 1953490..66dc0af 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -1,3 +1,7 @@ module.exports = { extends: ['@commitlint/config-conventional'], + // Disable default 100-char (or 72-char) header length limit for commit messages + rules: { + 'header-max-length': [0, 'always', 100], + }, }; From e7f95244c6134f363deb0ae7c4cf1d7fbfab525f Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Tue, 6 Jan 2026 18:44:15 +0530 Subject: [PATCH 193/274] ci: update GitHub workflows From de40ad223b6916ae3ddd46f2e5ec71002fb741f5 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:01:40 +0530 Subject: [PATCH 194/274] ci: update GitHub workflows From be6f68582eca57c7aa8f29a921093a4d233d7ba0 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:01:42 +0530 Subject: [PATCH 195/274] ci: update GitHub workflows From f64306042b6f3ef7daeba5c72579e9a4141a0d4c Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:01:43 +0530 Subject: [PATCH 196/274] ci: update GitHub workflows From acea8b6033761146484cadf673c960a426d13a72 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:01:45 +0530 Subject: [PATCH 197/274] ci: update GitHub workflows From 41855de0725f67f562add6a8e7d45ffe16c8b767 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:01:47 +0530 Subject: [PATCH 198/274] ci: update GitHub workflows From 05743003421a1a9308504ded459899420b518ec2 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:01:48 +0530 Subject: [PATCH 199/274] ci: update GitHub workflows From d2bacade42d3c5de58c7c1aa4edc5194ababb95c Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:01:50 +0530 Subject: [PATCH 200/274] ci: update GitHub workflows From 51401cc0b46434c7baeba74a3c8cc172a0e2f4b3 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:01:52 +0530 Subject: [PATCH 201/274] ci: update GitHub workflows From 017592335fd4e4d8c79ec80b3d814361fdcc3ffd Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:01:54 +0530 Subject: [PATCH 202/274] ci: update GitHub workflows From c144a12b7b51c0d9b3683872e32d767a5d72f150 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:01:55 +0530 Subject: [PATCH 203/274] ci: update GitHub workflows From bf08d0e0808780f1a2feccddc48e98f71758f497 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:01:58 +0530 Subject: [PATCH 204/274] ci: update GitHub workflows From 8179ba68f429827491c0dc09735ed6e81dcd43f3 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:01:59 +0530 Subject: [PATCH 205/274] ci: update GitHub workflows From 507a81d61b44817ffc90f4bd5b52a2f7ab7d9595 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:02:01 +0530 Subject: [PATCH 206/274] ci: update GitHub workflows --- commitlint.config.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/commitlint.config.js b/commitlint.config.js index 66dc0af..55f1dff 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -1,7 +1,7 @@ -module.exports = { - extends: ['@commitlint/config-conventional'], - // Disable default 100-char (or 72-char) header length limit for commit messages - rules: { - 'header-max-length': [0, 'always', 100], - }, -}; +module.exports = { + extends: ["@commitlint/config-conventional"], + // Disable default 100-char (or 72-char) header length limit for commit messages + rules: { + "header-max-length": [0, "always", 100], + }, +}; From 6c9a3c7250c739f30c6aebc601dc82c6b4e7f163 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:02:03 +0530 Subject: [PATCH 207/274] ci: update GitHub workflows From b1abe7aaf685189488b78d1500867ac2fc9c31d5 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 7 Jan 2026 17:23:34 +0530 Subject: [PATCH 208/274] ci: update GitHub workflows From 78e453d18ad1b6ecb4fc2223540d52cde5e75e8f Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 7 Jan 2026 17:23:36 +0530 Subject: [PATCH 209/274] ci: update GitHub workflows From 461c1c4dc41375f148db0ee0670807d9cb71d9af Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 7 Jan 2026 17:23:37 +0530 Subject: [PATCH 210/274] ci: update GitHub workflows From c9daf3907948ad2e90e1244c625b761b243a48ee Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 7 Jan 2026 17:23:39 +0530 Subject: [PATCH 211/274] ci: update GitHub workflows From ef3e439eab6efbed1d57926bc378bc3d8e6c45e7 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 7 Jan 2026 17:23:41 +0530 Subject: [PATCH 212/274] ci: update GitHub workflows From 2c0641fe4129a5411ba74398c7efb07eddfc96ac Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 7 Jan 2026 17:23:42 +0530 Subject: [PATCH 213/274] ci: update GitHub workflows From 827a82870372ae69d5598caef0667ee51b045f08 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 7 Jan 2026 17:23:43 +0530 Subject: [PATCH 214/274] ci: update GitHub workflows From 95f6f4ef7ece04fb349dd2ffc41d6c063fa735bc Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 7 Jan 2026 17:23:45 +0530 Subject: [PATCH 215/274] ci: update GitHub workflows From 8ddcb2a989595ce593360f6dc6766d84e45945a2 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 7 Jan 2026 17:23:47 +0530 Subject: [PATCH 216/274] ci: update GitHub workflows --- .github/workflows/security-scan.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index 3093b64..f064aa6 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -188,13 +188,22 @@ jobs: container-scanning: name: Container Security Scan runs-on: ubuntu-latest - if: hashFiles('**/Dockerfile') != '' steps: - name: Checkout repository uses: actions/checkout@v4 + - name: Check for Dockerfile + id: check-dockerfile + run: | + if find . -name "Dockerfile" -type f | grep -q .; then + echo "exists=true" >> $GITHUB_OUTPUT + else + echo "exists=false" >> $GITHUB_OUTPUT + fi + - name: Run Trivy vulnerability scanner + if: steps.check-dockerfile.outputs.exists == 'true' uses: aquasecurity/trivy-action@master with: scan-type: 'fs' @@ -203,6 +212,7 @@ jobs: output: 'trivy-results.sarif' - name: Upload Trivy results to GitHub Security + if: steps.check-dockerfile.outputs.exists == 'true' uses: github/codeql-action/upload-sarif@v3 with: sarif_file: 'trivy-results.sarif' From 2b09aeae19543105e2072bbedc7fb794e17a48a2 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 7 Jan 2026 17:23:48 +0530 Subject: [PATCH 217/274] ci: update GitHub workflows From cf3fe960e2655f71c6c65dd3c2b1516ebbdf68c6 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 7 Jan 2026 17:23:50 +0530 Subject: [PATCH 218/274] ci: update GitHub workflows From 86401761fdb5d173b0854174e514c30d2cca97c4 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 7 Jan 2026 17:23:51 +0530 Subject: [PATCH 219/274] ci: update GitHub workflows From a1164eae61fe5645b0a51997df26f92a48dca603 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 7 Jan 2026 17:23:53 +0530 Subject: [PATCH 220/274] ci: update GitHub workflows From 75e2fd1dfe5de6d3bf402c063bb5b666c948e418 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Wed, 7 Jan 2026 17:23:54 +0530 Subject: [PATCH 221/274] ci: update GitHub workflows From 77bcd1bb4e94e50571035a8a798517587fc26d77 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 11:58:49 +0530 Subject: [PATCH 222/274] ci: update GitHub workflows From 6a44a07011b91315f0778ce72faf32db35c47d36 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 11:58:50 +0530 Subject: [PATCH 223/274] ci: update GitHub workflows From 144442085716f2f19cf30ddd79fc2d8e63ac6b24 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 11:58:52 +0530 Subject: [PATCH 224/274] ci: update GitHub workflows From 4516c4a0ca956a5b03cb066850698f080590a086 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 11:58:53 +0530 Subject: [PATCH 225/274] ci: update GitHub workflows From b19d1ec658ade6869f350f887c60c16dcf59732a Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 11:58:56 +0530 Subject: [PATCH 226/274] ci: update GitHub workflows From 814840c9b1494c21b0f90ee6ae4039223c9cd07f Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 11:58:57 +0530 Subject: [PATCH 227/274] ci: update GitHub workflows From 4a2bf8f61dfff0de554ac54e8256d5f9f73ad487 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 11:58:58 +0530 Subject: [PATCH 228/274] ci: update GitHub workflows From 71708d0a763717f4782bdebba7367d5a4833d91e Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 11:59:00 +0530 Subject: [PATCH 229/274] ci: update GitHub workflows From 770f2e69f9042b95718be78bbbc4f7721cab74b9 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 11:59:01 +0530 Subject: [PATCH 230/274] ci: update GitHub workflows --- .github/workflows/security-scan.yml | 50 +++++++++++++---------------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index f064aa6..86e024d 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -44,33 +44,20 @@ jobs: uses: github/codeql-action/autobuild@v3 - name: Perform CodeQL Analysis + id: codeql-analysis uses: github/codeql-action/analyze@v3 + continue-on-error: true with: category: "/language:${{ matrix.language }}" - - secret-scanning: - name: Secret Scanning - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Run Gitleaks - uses: gitleaks/gitleaks-action@v2 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Run TruffleHog - uses: trufflesecurity/trufflehog@main - with: - path: ./ - base: ${{ github.event.repository.default_branch }} - head: HEAD - extra_args: --only-verified - + - name: Check CodeQL upload status + if: always() && steps.codeql-analysis.outcome == 'failure' + run: | + echo "⚠️ CodeQL analysis completed but upload failed." + echo "This is expected if Code Security is not enabled for this repository." + echo "To enable Code Security, go to: Settings > Code security and analysis > Code scanning" + echo "The analysis results are still available in the workflow artifacts." + dependency-scanning: name: Dependency Vulnerability Scan runs-on: ubuntu-latest @@ -214,13 +201,14 @@ jobs: - name: Upload Trivy results to GitHub Security if: steps.check-dockerfile.outputs.exists == 'true' uses: github/codeql-action/upload-sarif@v3 + continue-on-error: true with: sarif_file: 'trivy-results.sarif' security-summary: name: Security Summary runs-on: ubuntu-latest - needs: [codeql-analysis, secret-scanning, dependency-scanning] + needs: [codeql-analysis, dependency-scanning] if: always() steps: @@ -230,13 +218,21 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const needs = ${{ toJSON(needs) }}; + const codeqlResult = needs['codeql-analysis']?.result || 'unknown'; + const dependencyResult = needs['dependency-scanning']?.result || 'unknown'; + + const getStatus = (result) => { + if (result === 'success') return '✅ Passed'; + if (result === 'failure') return '❌ Failed'; + return '⚠️ Skipped'; + }; + const summary = `## 🔒 Security Scan Summary | Scan Type | Status | |-----------|--------| - | CodeQL Analysis | ${needs.codeql-analysis.result === 'success' ? '✅ Passed' : needs.codeql-analysis.result === 'failure' ? '❌ Failed' : '⚠️ Skipped'} | - | Secret Scanning | ${needs.secret-scanning.result === 'success' ? '✅ Passed' : needs.secret-scanning.result === 'failure' ? '❌ Failed' : '⚠️ Skipped'} | - | Dependency Scan | ${needs.dependency-scanning.result === 'success' ? '✅ Passed' : needs.dependency-scanning.result === 'failure' ? '❌ Failed' : '⚠️ Skipped'} | + | CodeQL Analysis | ${getStatus(codeqlResult)} | + | Dependency Scan | ${getStatus(dependencyResult)} | **View detailed results in the Security tab or workflow artifacts.**`; From b4e560a14c90685a442241d65b12576857eaf25e Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 11:59:03 +0530 Subject: [PATCH 231/274] ci: update GitHub workflows From 9a2859bd4565a037cbaf237636e3aa43746222af Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 11:59:04 +0530 Subject: [PATCH 232/274] ci: update GitHub workflows From 1b7e30200c65a847c11378808c21f2226bdb9bc7 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 11:59:05 +0530 Subject: [PATCH 233/274] ci: update GitHub workflows From be85482e01da9571fb0db7a7a5d4e2ae6244bbe5 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 11:59:08 +0530 Subject: [PATCH 234/274] ci: update GitHub workflows From a1fae78ba3d19fa4eedde4da1ad11b34d7453ca3 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 11:59:09 +0530 Subject: [PATCH 235/274] ci: update GitHub workflows From 0b14c1174a5decc4f6b7f34c315d18f309184864 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 12:23:59 +0530 Subject: [PATCH 236/274] ci: update GitHub workflows From 11b483ec45333140bb258a486c2196ff5aa25407 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 12:24:01 +0530 Subject: [PATCH 237/274] ci: update GitHub workflows From 05090ceb2192d1272a27c268a7d49cdab56687b3 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 12:24:02 +0530 Subject: [PATCH 238/274] ci: update GitHub workflows From 4a34799b8c6b038ee7eed508ca3eddcbff1ce59c Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 12:24:04 +0530 Subject: [PATCH 239/274] ci: update GitHub workflows From 0405e340a319ce65a988d30b8db9682f854f5483 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 12:24:05 +0530 Subject: [PATCH 240/274] ci: update GitHub workflows From 604254dfb975470f9eb408739a3d72e649d471b3 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 12:24:06 +0530 Subject: [PATCH 241/274] ci: update GitHub workflows From 48aa5c30ab0d9fad9a963fa137fae9dcd13f6ed6 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 12:24:08 +0530 Subject: [PATCH 242/274] ci: update GitHub workflows From 2c5d0066829c8619c006d630888bf6bede5d3fcf Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 12:24:09 +0530 Subject: [PATCH 243/274] ci: update GitHub workflows From dda769ba640a71cb03731b4cb608c8a341067242 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 12:24:11 +0530 Subject: [PATCH 244/274] ci: update GitHub workflows --- .github/workflows/security-scan.yml | 33 ----------------------------- 1 file changed, 33 deletions(-) diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index 86e024d..9fbe092 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -172,39 +172,6 @@ jobs: body: comment }); - container-scanning: - name: Container Security Scan - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Check for Dockerfile - id: check-dockerfile - run: | - if find . -name "Dockerfile" -type f | grep -q .; then - echo "exists=true" >> $GITHUB_OUTPUT - else - echo "exists=false" >> $GITHUB_OUTPUT - fi - - - name: Run Trivy vulnerability scanner - if: steps.check-dockerfile.outputs.exists == 'true' - uses: aquasecurity/trivy-action@master - with: - scan-type: 'fs' - scan-ref: '.' - format: 'sarif' - output: 'trivy-results.sarif' - - - name: Upload Trivy results to GitHub Security - if: steps.check-dockerfile.outputs.exists == 'true' - uses: github/codeql-action/upload-sarif@v3 - continue-on-error: true - with: - sarif_file: 'trivy-results.sarif' - security-summary: name: Security Summary runs-on: ubuntu-latest From 2cdf8d163b2768f2b954947955a12101a2c020e8 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 12:24:13 +0530 Subject: [PATCH 245/274] ci: update GitHub workflows From 6c80b3d1b5b7b2c85f78354386ad6070ed3a4b88 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 12:24:14 +0530 Subject: [PATCH 246/274] ci: update GitHub workflows From 1353187fa7f6da185619c16ae13f7b8855731e4f Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 12:24:15 +0530 Subject: [PATCH 247/274] ci: update GitHub workflows From f304f9def440dc0c1669893a8ec1e425769631af Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 12:24:17 +0530 Subject: [PATCH 248/274] ci: update GitHub workflows From 7834a63cf7d42aac5f1119fce026edbea5be6022 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 12:24:18 +0530 Subject: [PATCH 249/274] ci: update GitHub workflows From bff20f3f18e91749cdd7fa5df933950c1342d62b Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 22:03:06 +0530 Subject: [PATCH 250/274] ci: update GitHub workflows From 6fb5dc087a86aab9b162c601ae82ae059cd54767 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 22:03:09 +0530 Subject: [PATCH 251/274] ci: update GitHub workflows From a063cb3df7161833dfa889548e9f42c3c1b4f181 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 22:03:10 +0530 Subject: [PATCH 252/274] ci: update GitHub workflows From 8fb2b64c0c994a70e32a750ff6f914d98519877d Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 22:03:14 +0530 Subject: [PATCH 253/274] ci: update GitHub workflows From a6ff28eaf11ff8dea2f0aec4a60f20b8450d68f2 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 22:03:16 +0530 Subject: [PATCH 254/274] ci: update GitHub workflows --- .github/workflows/ci.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 68a7eb9..01dae23 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,8 +1,10 @@ name: CI on: + # Run on push only for main/master (merged code validation) push: - branches: [ main, master, develop, development ] + branches: [ main, master ] + # Run on PRs for all branches (pre-merge validation) pull_request: branches: [ main, master, develop, development ] workflow_dispatch: @@ -11,9 +13,8 @@ permissions: contents: read # Wait for init workflow to complete first (for push to main/master) -# Use same concurrency group as init workflow to ensure init runs first concurrency: - group: ${{ (github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master')) && format('repo-init-{0}-{1}', github.repository, github.ref) || format('ci-{0}-{1}-{2}', github.event_name, github.ref, github.event.number || github.sha) }} + group: ${{ (github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master')) && format('repo-init-{0}-{1}', github.repository, github.ref) || format('ci-{0}-{1}', github.repository, github.ref) }} cancel-in-progress: false jobs: From b640c99d42189aab9cff996c06d3049c74e9261a Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 22:03:17 +0530 Subject: [PATCH 255/274] ci: update GitHub workflows --- .github/workflows/code-quality.yml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 5218307..2967390 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -1,20 +1,15 @@ name: Quality Checks on: + # Only run on PRs - no push trigger to avoid duplicates pull_request: branches: [ main, master, develop, development ] types: [opened, synchronize, reopened, ready_for_review] - push: - branches: [ develop, development ] workflow_dispatch: permissions: contents: read -concurrency: - group: commitcheck-frappe-${{ github.event_name }}-${{ github.event.number }} - cancel-in-progress: true - jobs: commit-lint: name: 'Semantic Commits' @@ -144,8 +139,9 @@ jobs: - name: Run pre-commit (check only, no auto-fix) run: | # Run pre-commit in CI mode - it will check but not modify files + # Skip no-commit-to-branch hook in CI (it's meant for local development) # This will fail if files need formatting, which is what we want - pre-commit run --all-files --show-diff-on-failure || { + SKIP=no-commit-to-branch pre-commit run --all-files --show-diff-on-failure || { echo "❌ Pre-commit checks failed. Some files need formatting or have issues." echo "Please run 'pre-commit run --all-files' locally to fix the issues." exit 1 From 44cdf91b6d38c525ba9c29750fc98bdc3a7a387a Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 22:03:19 +0530 Subject: [PATCH 256/274] ci: update GitHub workflows From b9cf19be5ee57582d8b65dad560c44da52504149 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 22:03:20 +0530 Subject: [PATCH 257/274] ci: update GitHub workflows From 5d11c6797ca9c58216061577595a6111c98e5d44 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 22:03:22 +0530 Subject: [PATCH 258/274] ci: update GitHub workflows --- .github/workflows/security-scan.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index 9fbe092..c666cf2 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -1,9 +1,7 @@ name: Enhanced Security Scan on: - pull_request: - branches: [ main, master ] - types: [opened, synchronize, reopened, ready_for_review] + # Only run on push to main/master (after merge) - not on PRs to avoid duplicates push: branches: [ main, master ] schedule: From 08816e05934e6ca2f44d1e31412fee0621a0818c Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 22:03:24 +0530 Subject: [PATCH 259/274] ci: update GitHub workflows From a38f19f80ac0a8ae642539329e6b087e07453360 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 22:03:26 +0530 Subject: [PATCH 260/274] ci: update GitHub workflows --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7d1e0e5..0f1acaf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: files: "frappe.*" exclude: ".*json$|.*txt$|.*csv|.*md|.*svg" - id: no-commit-to-branch - args: ['--branch', 'develop'] + args: ['--branch', 'main', '--branch', 'master'] - id: check-merge-conflict - id: check-ast - id: check-json From e803da015da341adb3231adf3e8a0fe35ad4f101 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 22:03:28 +0530 Subject: [PATCH 261/274] ci: update GitHub workflows From 541232a93b42e48da5fbe5a35bf32bb0dfb47ce6 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 22:03:30 +0530 Subject: [PATCH 262/274] ci: update GitHub workflows From e93ed4e55fb6e5b392cf9b6ac0c566b7c023f1c6 Mon Sep 17 00:00:00 2001 From: dhwani-ankit <124347605+dhwani-ankit@users.noreply.github.com> Date: Thu, 8 Jan 2026 22:03:32 +0530 Subject: [PATCH 263/274] ci: update GitHub workflows From e35319b342e41fd6b311969ca1d64e8cb1659ca2 Mon Sep 17 00:00:00 2001 From: Vaishali Sahni <118843977+vaishalisahni@users.noreply.github.com> Date: Thu, 15 Jan 2026 08:49:43 +0530 Subject: [PATCH 264/274] fix(breadcrumbs): update patch for Frappe v15 (#10) --- commitlint.config.js | 10 +-- frappe_desk_theme/hooks.py | 2 +- .../js/sidebar/breadcrumb_override_patch.js | 88 +++++++++++++++++++ 3 files changed, 94 insertions(+), 6 deletions(-) create mode 100644 frappe_desk_theme/public/js/sidebar/breadcrumb_override_patch.js diff --git a/commitlint.config.js b/commitlint.config.js index 55f1dff..ce91a16 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -1,7 +1,7 @@ module.exports = { - extends: ["@commitlint/config-conventional"], - // Disable default 100-char (or 72-char) header length limit for commit messages - rules: { - "header-max-length": [0, "always", 100], - }, + extends: ["@commitlint/config-conventional"], + // Disable default 100-char (or 72-char) header length limit for commit messages + rules: { + "header-max-length": [0, "always", 100], + }, }; diff --git a/frappe_desk_theme/hooks.py b/frappe_desk_theme/hooks.py index 21eb600..021a770 100644 --- a/frappe_desk_theme/hooks.py +++ b/frappe_desk_theme/hooks.py @@ -30,7 +30,7 @@ app_include_js = [ "/assets/frappe_desk_theme/js/frappe_desk_theme.bundle.js", "/assets/frappe_desk_theme/js/sidebar/sidebar_override.bundle.js", - "/assets/frappe_desk_theme/js/sidebar/breadcrumb_override.bundle.js", + "/assets/frappe_desk_theme/js/sidebar/breadcrumb_override_patch.js", ] # include js, css files in header of web template web_include_css = "/assets/frappe_desk_theme/css/frappe_desk_theme.bundle.css" diff --git a/frappe_desk_theme/public/js/sidebar/breadcrumb_override_patch.js b/frappe_desk_theme/public/js/sidebar/breadcrumb_override_patch.js new file mode 100644 index 0000000..2a17a5a --- /dev/null +++ b/frappe_desk_theme/public/js/sidebar/breadcrumb_override_patch.js @@ -0,0 +1,88 @@ +function patch_breadcrumbs_update() { + if (!frappe.breadcrumbs || !frappe.breadcrumbs.update) { + setTimeout(patch_breadcrumbs_update, 100); + return; + } + + const original_update = frappe.breadcrumbs.update; + + frappe.breadcrumbs.update = function () { + const page = frappe.breadcrumbs.current_page(); + const breadcrumbs = this.all[page]; + + if (!breadcrumbs) { + return original_update.call(this); + } + + this.clear(); + + if (breadcrumbs.type === "Custom") { + this.set_custom_breadcrumbs(breadcrumbs); + } else { + let view = frappe.get_route()[0]; + view = view ? view.toLowerCase() : null; + + if (breadcrumbs.doctype || view === "list") { + const route = frappe.get_route(); + const last = frappe.route_history.slice(-2)[0]; + const from_workspace = last && last[0] === "Workspaces"; + + this.clear(); + + if (route[0] === "Form") { + if (from_workspace) { + this.append_breadcrumb_element( + `/app/${frappe.router.slug(last[1])}`, + __(last[1]) + ); + } + this.append_breadcrumb_element( + `/app/${frappe.router.slug(breadcrumbs.doctype)}`, + __(breadcrumbs.doctype) + ); + const name = route[2]; + this.append_breadcrumb_element( + `/app/${frappe.router.slug(breadcrumbs.doctype)}/${encodeURIComponent( + name + )}`, + __(name) + ); + return this.toggle(true); + } else if (route[0] === "List") { + if (from_workspace) { + this.append_breadcrumb_element( + `/app/${frappe.router.slug(last[1])}`, + __(last[1]) + ); + } + this.append_breadcrumb_element( + `/app/${frappe.router.slug(breadcrumbs.doctype)}`, + __(breadcrumbs.doctype) + ); + return this.toggle(true); + } + } + + this.set_workspace_breadcrumb(breadcrumbs); + + if (breadcrumbs.doctype && ["print", "form"].includes(view)) { + this.set_list_breadcrumb(breadcrumbs); + this.set_form_breadcrumb(breadcrumbs, view); + } else if (breadcrumbs.doctype && view == "dashboard-view") { + this.set_list_breadcrumb(breadcrumbs); + } + } + + if ( + breadcrumbs.workspace && + frappe.workspace_map[breadcrumbs.workspace]?.app && + frappe.workspace_map[breadcrumbs.workspace]?.app != frappe.current_app + ) { + let app = frappe.workspace_map[breadcrumbs.workspace].app; + frappe.app.sidebar.apps_switcher.set_current_app(app); + } + this.toggle(true); + }; +} + +patch_breadcrumbs_update(); From d30a52cac8e098ab91416da984397e632e46d066 Mon Sep 17 00:00:00 2001 From: devang-dhwaniris Date: Thu, 26 Feb 2026 20:00:49 +0530 Subject: [PATCH 265/274] refactor: remove outdated breadcrumb override scripts (#13) * refactor: remove outdated breadcrumb override scripts * refactor: remove breadcrumb override script from hooks --- frappe_desk_theme/hooks.py | 1 - .../js/sidebar/breadcrumb_override.bundle.js | 70 --------------- .../js/sidebar/breadcrumb_override_patch.js | 88 ------------------- 3 files changed, 159 deletions(-) delete mode 100644 frappe_desk_theme/public/js/sidebar/breadcrumb_override.bundle.js delete mode 100644 frappe_desk_theme/public/js/sidebar/breadcrumb_override_patch.js diff --git a/frappe_desk_theme/hooks.py b/frappe_desk_theme/hooks.py index 021a770..ccdec54 100644 --- a/frappe_desk_theme/hooks.py +++ b/frappe_desk_theme/hooks.py @@ -30,7 +30,6 @@ app_include_js = [ "/assets/frappe_desk_theme/js/frappe_desk_theme.bundle.js", "/assets/frappe_desk_theme/js/sidebar/sidebar_override.bundle.js", - "/assets/frappe_desk_theme/js/sidebar/breadcrumb_override_patch.js", ] # include js, css files in header of web template web_include_css = "/assets/frappe_desk_theme/css/frappe_desk_theme.bundle.css" diff --git a/frappe_desk_theme/public/js/sidebar/breadcrumb_override.bundle.js b/frappe_desk_theme/public/js/sidebar/breadcrumb_override.bundle.js deleted file mode 100644 index d59ce86..0000000 --- a/frappe_desk_theme/public/js/sidebar/breadcrumb_override.bundle.js +++ /dev/null @@ -1,70 +0,0 @@ -frappe.breadcrumbs.update = function () { - var breadcrumbs = this.all[frappe.breadcrumbs.current_page()]; - this.clear(); - if (!breadcrumbs) return this.toggle(false); - - if (breadcrumbs.type === "Custom") { - this.set_custom_breadcrumbs(breadcrumbs); - } else { - let view = frappe.get_route()[0]; - view = view ? view.toLowerCase() : null; - - if (breadcrumbs.doctype || view === "list") { - const route = frappe.get_route(); - const last = frappe.route_history.slice(-2)[0]; - const from_workspace = last && last[0] === "Workspaces"; - - this.clear(); - - if (route[0] === "Form") { - if (from_workspace) { - this.append_breadcrumb_element( - `/app/${frappe.router.slug(last[1])}`, - __(last[1]) - ); - } - this.append_breadcrumb_element( - `/app/${frappe.router.slug(breadcrumbs.doctype)}`, - __(breadcrumbs.doctype) - ); - const name = route[2]; - this.append_breadcrumb_element( - `/app/${frappe.router.slug(breadcrumbs.doctype)}/${encodeURIComponent(name)}`, - __(name) - ); - return this.toggle(true); - } else if (route[0] === "List") { - if (from_workspace) { - this.append_breadcrumb_element( - `/app/${frappe.router.slug(last[1])}`, - __(last[1]) - ); - } - this.append_breadcrumb_element( - `/app/${frappe.router.slug(breadcrumbs.doctype)}`, - __(breadcrumbs.doctype) - ); - return this.toggle(true); - } - } - - this.set_workspace_breadcrumb(breadcrumbs); - - if (breadcrumbs.doctype && ["print", "form"].includes(view)) { - this.set_list_breadcrumb(breadcrumbs); - this.set_form_breadcrumb(breadcrumbs, view); - } else if (breadcrumbs.doctype && view == "dashboard-view") { - this.set_list_breadcrumb(breadcrumbs); - } - } - - if ( - breadcrumbs.workspace && - frappe.workspace_map[breadcrumbs.workspace]?.app && - frappe.workspace_map[breadcrumbs.workspace]?.app != frappe.current_app - ) { - let app = frappe.workspace_map[breadcrumbs.workspace].app; - frappe.app.sidebar.apps_switcher.set_current_app(app); - } - this.toggle(true); -}; diff --git a/frappe_desk_theme/public/js/sidebar/breadcrumb_override_patch.js b/frappe_desk_theme/public/js/sidebar/breadcrumb_override_patch.js deleted file mode 100644 index 2a17a5a..0000000 --- a/frappe_desk_theme/public/js/sidebar/breadcrumb_override_patch.js +++ /dev/null @@ -1,88 +0,0 @@ -function patch_breadcrumbs_update() { - if (!frappe.breadcrumbs || !frappe.breadcrumbs.update) { - setTimeout(patch_breadcrumbs_update, 100); - return; - } - - const original_update = frappe.breadcrumbs.update; - - frappe.breadcrumbs.update = function () { - const page = frappe.breadcrumbs.current_page(); - const breadcrumbs = this.all[page]; - - if (!breadcrumbs) { - return original_update.call(this); - } - - this.clear(); - - if (breadcrumbs.type === "Custom") { - this.set_custom_breadcrumbs(breadcrumbs); - } else { - let view = frappe.get_route()[0]; - view = view ? view.toLowerCase() : null; - - if (breadcrumbs.doctype || view === "list") { - const route = frappe.get_route(); - const last = frappe.route_history.slice(-2)[0]; - const from_workspace = last && last[0] === "Workspaces"; - - this.clear(); - - if (route[0] === "Form") { - if (from_workspace) { - this.append_breadcrumb_element( - `/app/${frappe.router.slug(last[1])}`, - __(last[1]) - ); - } - this.append_breadcrumb_element( - `/app/${frappe.router.slug(breadcrumbs.doctype)}`, - __(breadcrumbs.doctype) - ); - const name = route[2]; - this.append_breadcrumb_element( - `/app/${frappe.router.slug(breadcrumbs.doctype)}/${encodeURIComponent( - name - )}`, - __(name) - ); - return this.toggle(true); - } else if (route[0] === "List") { - if (from_workspace) { - this.append_breadcrumb_element( - `/app/${frappe.router.slug(last[1])}`, - __(last[1]) - ); - } - this.append_breadcrumb_element( - `/app/${frappe.router.slug(breadcrumbs.doctype)}`, - __(breadcrumbs.doctype) - ); - return this.toggle(true); - } - } - - this.set_workspace_breadcrumb(breadcrumbs); - - if (breadcrumbs.doctype && ["print", "form"].includes(view)) { - this.set_list_breadcrumb(breadcrumbs); - this.set_form_breadcrumb(breadcrumbs, view); - } else if (breadcrumbs.doctype && view == "dashboard-view") { - this.set_list_breadcrumb(breadcrumbs); - } - } - - if ( - breadcrumbs.workspace && - frappe.workspace_map[breadcrumbs.workspace]?.app && - frappe.workspace_map[breadcrumbs.workspace]?.app != frappe.current_app - ) { - let app = frappe.workspace_map[breadcrumbs.workspace].app; - frappe.app.sidebar.apps_switcher.set_current_app(app); - } - this.toggle(true); - }; -} - -patch_breadcrumbs_update(); From 66944464cf9a5799b22757ec27dc2fb89db6b9a2 Mon Sep 17 00:00:00 2001 From: bhushan-barbuddhe Date: Fri, 27 Feb 2026 21:49:36 +0530 Subject: [PATCH 266/274] feat: add fixed sidebar and login redirect options to desk theme --- .../doctype/desk_theme/desk_theme.json | 24 ++- .../public/js/frappe_desk_theme.bundle.js | 179 +++++++++++++++++- 2 files changed, 201 insertions(+), 2 deletions(-) diff --git a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json index 8e2069c..34b098e 100644 --- a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json +++ b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json @@ -86,7 +86,10 @@ "sticky_footer", "column_break_footer", "footer_powered_by", - "footer_text_color" + "footer_text_color", + "desk_behavior_tab", + "fixed_sidebar", + "redirect_to_sidebar_on_login" ], "fields": [ { @@ -515,6 +518,25 @@ "fieldname": "sidebar_hover_text_color", "fieldtype": "Color", "label": "Hover Text Color" + }, + { + "fieldname": "desk_behavior_tab", + "fieldtype": "Tab Break", + "label": "Desk Behavior" + }, + { + "description": "Fixed workspace sidebar to show for all modules", + "fieldname": "fixed_sidebar", + "fieldtype": "Link", + "label": "Fixed Workspace Sidebar", + "options": "Workspace Sidebar" + }, + { + "default": "0", + "description": "If enabled, after login redirect to the first item from the fixed sidebar instead of the default desktop screen", + "fieldname": "redirect_to_sidebar_on_login", + "fieldtype": "Check", + "label": "Redirect to Sidebar After Login" } ], "grid_page_length": 50, diff --git a/frappe_desk_theme/public/js/frappe_desk_theme.bundle.js b/frappe_desk_theme/public/js/frappe_desk_theme.bundle.js index 3672609..cec14d0 100644 --- a/frappe_desk_theme/public/js/frappe_desk_theme.bundle.js +++ b/frappe_desk_theme/public/js/frappe_desk_theme.bundle.js @@ -3,7 +3,7 @@ * Handles loading, applying, and managing custom theme configurations for Frappe Desk * Supports dynamic theme changes, user role-based hiding, and real-time DOM updates */ -class FrappeDeskTheme { + class FrappeDeskTheme { constructor() { // Store theme configuration data from server this.themeData = null; @@ -16,6 +16,8 @@ class FrappeDeskTheme { this.footerHtmlCache = null; this.footerCacheKey = null; // Track what theme data the footer was cached for this.stickyFooterListenerSetup = false; + // Internal flag to ensure we only perform one redirect from sidebar after login + this.didInitialSidebarLoginRedirect = false; this.init(); } @@ -656,6 +658,8 @@ class FrappeDeskTheme { this.toggleSidebar(); this.toggleSearchBar(); this.setDefaultApp(); + this.applyFixedSidebarBehavior(); + this.performInitialSidebarLoginRedirect(); if ( this.themeData.carousel && this.themeData.carousel.images && @@ -736,6 +740,142 @@ class FrappeDeskTheme { } } + /** + * Force Desk to always use a single, fixed workspace sidebar + * Uses the 'fixed_sidebar' link field from the Desk Theme doctype + */ + applyFixedSidebarBehavior() { + // Only applicable inside Desk (not on login page) + if (document.body.classList.contains("login-page") || document.querySelector("#page-login")) { + return; + } + + if (!this.themeData || !this.themeData.fixed_sidebar) { + return; + } + + if (typeof frappe === "undefined" || !frappe.app || !frappe.app.sidebar) { + return; + } + + const sidebar = frappe.app.sidebar; + + // Only patch once + if (sidebar.__fixed_sidebar_patched) { + return; + } + sidebar.__fixed_sidebar_patched = true; + + const fixedSidebarLabel = this.themeData.fixed_sidebar; + + // Override sidebar switching methods to always use the configured sidebar + sidebar.set_workspace_sidebar = function () { + this.setup(fixedSidebarLabel); + this.set_active_workspace_item(); + }; + + sidebar.show_sidebar_for_module = function () { + // No-op: keep using the fixed sidebar + return; + }; + + sidebar.set_sidebar_for_page = function () { + this.setup(fixedSidebarLabel); + }; + + // Apply immediately for current page if possible + try { + sidebar.setup(fixedSidebarLabel); + sidebar.set_active_workspace_item(); + } catch (e) { + // Silent fail – sidebar might not be fully initialised yet + } + } + + /** + * On first Desk load after login, redirect user directly to a page + * derived from the fixed sidebar (first link item), instead of the + * default desktop / workspace landing. + */ + performInitialSidebarLoginRedirect() { + // Only run once per page load + if (this.didInitialSidebarLoginRedirect) { + return; + } + + // Must be enabled in theme and have a fixed sidebar configured + if ( + !this.themeData || + !this.themeData.redirect_to_sidebar_on_login || + !this.themeData.fixed_sidebar + ) { + return; + } + + // Not applicable on the login page + if (document.body.classList.contains("login-page") || document.querySelector("#page-login")) { + return; + } + + // Only act inside Desk + if (!window.location.pathname.startsWith("/desk")) { + return; + } + + if (typeof frappe === "undefined" || !frappe.get_route || !frappe.boot) { + return; + } + + const route = frappe.get_route() || []; + + // Heuristic: only redirect from generic initial desk routes + const isInitialDeskRoute = + route.length === 0 || + route[0] === "desktop" || + (route[0] === "Workspaces" && route.length <= 2); + + if (!isInitialDeskRoute) { + return; + } + + const fixedSidebarLabel = this.themeData.fixed_sidebar; + const sidebarBoot = frappe.boot.workspace_sidebar_item || {}; + const sidebarKey = (fixedSidebarLabel || "").toLowerCase(); + const sidebarData = sidebarBoot[sidebarKey]; + + if (!sidebarData || !Array.isArray(sidebarData.items) || !sidebarData.items.length) { + return; + } + + // Choose first link-type item from the configured sidebar + const firstLink = sidebarData.items.find((item) => item.type === "Link" && item.link_to); + if (!firstLink) { + return; + } + + this.didInitialSidebarLoginRedirect = true; + + try { + const linkType = (firstLink.link_type || "").toLowerCase(); + + if (linkType === "workspace") { + // Open workspace from sidebar link + frappe.set_route("Workspaces", firstLink.link_to); + } else if (linkType === "doctype") { + // Go to list view for the DocType + frappe.set_route("List", firstLink.link_to); + } else if (linkType === "page") { + frappe.set_route("Page", firstLink.link_to); + } else if (linkType === "report") { + frappe.set_route("query-report", firstLink.link_to); + } else if (linkType === "url") { + window.location.href = firstLink.link_to; + } + } catch (e) { + // Silent fail – don't break desk if redirect fails + } + } + /** * Create and display footer in desk view using HTML template * Much more efficient than creating DOM elements dynamically @@ -931,6 +1071,8 @@ class FrappeDeskTheme { let footerTimeout; const observer = new MutationObserver(() => { this.toggleSearchBar(); + this.applyFixedSidebarBehavior(); + this.performInitialSidebarLoginRedirect(); // Debounce footer creation to avoid performance issues clearTimeout(footerTimeout); @@ -1071,8 +1213,43 @@ if (document.readyState === "loading") { // DOM is still loading, wait for DOMContentLoaded event document.addEventListener("DOMContentLoaded", () => { window.frappeDeskTheme = new FrappeDeskTheme(); + // Hook into Frappe's reload / clear cache button to also refresh theme + attachFrappeReloadThemeHook(); }); } else { // DOM is already loaded, initialize immediately window.frappeDeskTheme = new FrappeDeskTheme(); + attachFrappeReloadThemeHook(); +} + +/** + * Attach a hook so that when the user clicks Frappe's + * "Reload / Clear Cache" button, the desk theme cache + * is cleared and refreshed as well. + */ +function attachFrappeReloadThemeHook() { + if (typeof frappe === "undefined") return; + if (!frappe.ui || !frappe.ui.toolbar || !frappe.ui.toolbar.clear_cache) return; + + // Avoid double-wrapping + if (frappe.ui.toolbar.__theme_reload_hooked) { + return; + } + frappe.ui.toolbar.__theme_reload_hooked = true; + + const originalClearCache = frappe.ui.toolbar.clear_cache; + + frappe.ui.toolbar.clear_cache = function () { + try { + if (window.frappeDeskTheme) { + // Clear local theme cache and force a fresh fetch + window.frappeDeskTheme.clearCache(); + window.frappeDeskTheme.refreshTheme(); + } + } catch (e) { + // Ignore theme errors; still allow core clear_cache to run + } + + return originalClearCache.apply(this, arguments); + }; } From a1d5a06e1ce0780b14721ed05168eccb4d6f22b2 Mon Sep 17 00:00:00 2001 From: bhushan-barbuddhe Date: Sat, 28 Feb 2026 20:08:51 +0530 Subject: [PATCH 267/274] feat: enhance desk theme with new navbar and button styles, and add dependencies for Frappe --- .../doctype/desk_theme/desk_theme.json | 12 ++++ .../public/css/frappe_desk_theme.bundle.css | 69 ++++++++++++------- .../public/js/frappe_desk_theme.bundle.js | 51 +++++++++++--- pyproject.toml | 6 +- 4 files changed, 103 insertions(+), 35 deletions(-) diff --git a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json index 34b098e..240a8d7 100644 --- a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json +++ b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json @@ -30,6 +30,8 @@ "default_app", "column_break_jevx", "navbar_text_color", + "navbar_toggler_border_color", + "navbar_breadcrumb_disabled_color", "hide_search", "buttons_tab", "primary_button_section", @@ -106,6 +108,16 @@ "fieldtype": "Color", "label": "Text Color" }, + { + "fieldname": "navbar_toggler_border_color", + "fieldtype": "Color", + "label": "Toggler Border Color" + }, + { + "fieldname": "navbar_breadcrumb_disabled_color", + "fieldtype": "Color", + "label": "Breadcrumb Disabled Color" + }, { "fieldname": "column_break_jevx", "fieldtype": "Column Break" diff --git a/frappe_desk_theme/public/css/frappe_desk_theme.bundle.css b/frappe_desk_theme/public/css/frappe_desk_theme.bundle.css index 020ba77..869e32a 100644 --- a/frappe_desk_theme/public/css/frappe_desk_theme.bundle.css +++ b/frappe_desk_theme/public/css/frappe_desk_theme.bundle.css @@ -154,10 +154,10 @@ label.control-label { } /* ======================================== - NAVIGATION BAR STYLING + NAVIGATION BAR STYLING (Desk Theme > Navbar) ======================================== */ -/* Main navigation bar - top-level container with custom background */ +/* Main navigation bar - Background Color, Text Color, Toggler Border, Breadcrumb Disabled */ .navbar { background-color: var(--navbar-bg, #fff); } @@ -177,24 +177,46 @@ label.control-label { stroke-width: 0; } -/* Mobile menu toggle button - border color for hamburger menu */ +/* Mobile menu toggle button - border color for hamburger menu (theme: Navbar > Toggler Border Color) */ button.navbar-toggler { - border-color: var(--navbar-color) !important; + border-color: var(--navbar-toggler-border, var(--navbar-color, #dee2e6)) !important; } -/* Breadcrumb navigation links - maintains consistent color scheme */ -#navbar-breadcrumbs li a { - color: var(--navbar-color); +/* Page head bar - navbar background only (override any body-bg inheritance or other rules) */ +.page-head, +.page-head.flex { + background-color: var(--navbar-bg, #fff) !important; } -/* Breadcrumb separators - adds arrow separators between breadcrumb items */ -#navbar-breadcrumbs li a::before { - content: '›'; +/* Page head breadcrumbs - override Frappe's ink-gray vars; use Desk Theme Navbar colors */ +.page-head .navbar-breadcrumbs li a { + color: var(--navbar-color, #555) !important; } -/* Disabled breadcrumb items - styling for non-clickable breadcrumb elements */ -#navbar-breadcrumbs li.disabled a { - color: var(--navbar-color) !important; +.page-head .navbar-breadcrumbs li a svg, +.page-head .navbar-breadcrumbs li a svg use { + fill: var(--navbar-color, #555) !important; + stroke: var(--navbar-color, #555) !important; +} + +/* Breadcrumb separators - match navbar color (Frappe uses "/", we keep theme color) */ +.page-head .navbar-breadcrumbs li a::before { + color: var(--navbar-color, #555) !important; +} + +/* Last/current breadcrumb item - Desk Theme Breadcrumb Disabled Color (overrides Frappe's li:last-child) */ +.page-head .navbar-breadcrumbs li.disabled a, +.page-head .navbar-breadcrumbs li:last-child a { + color: var(--breadcrumb-disabled-color, var(--navbar-color, #6c757d)) !important; +} + +/* Sidebar toggle in page head - match navbar color */ +.page-head .sidebar-toggle-btn.navbar-brand, +.page-head .sidebar-toggle-btn.navbar-brand svg, +.page-head .sidebar-toggle-btn.navbar-brand svg use { + color: var(--navbar-color, #555) !important; + fill: var(--navbar-color, #555) !important; + stroke: var(--navbar-color, #555) !important; } /* Navigation link buttons - text and icon color consistency */ @@ -210,10 +232,10 @@ button.navbar-toggler { } /* ======================================== - BUTTON STYLING + BUTTON STYLING (Desk Theme > Buttons) ======================================== */ -/* Primary buttons - main action buttons throughout the application */ +/* Primary buttons - Background/Text/Hover from theme */ .btn-primary, .btn-primary:active { background-color: var(--btn-primary-bg,#171717) !important; @@ -225,14 +247,14 @@ button.navbar-toggler { color: var(--btn-primary-color) !important; } -/* Primary button hover state - provides visual feedback on mouse over */ +/* Primary button hover state - fallback to primary when not set in theme */ .btn-primary:hover { - background-color: var(--btn-primary-hover-bg); + background-color: var(--btn-primary-hover-bg, var(--btn-primary-bg, #171717)); } -/* Primary button hover text - maintains readability during hover state */ +/* Primary button hover text - fallback to primary color when not set in theme */ .btn-primary:hover span { - color: var(--btn-primary-hover-color); + color: var(--btn-primary-hover-color, var(--btn-primary-color, #fff)); } /* Secondary/default buttons - alternative action buttons with distinct styling */ @@ -243,11 +265,11 @@ button.navbar-toggler { color: var(--btn-secondary-color); } -/* Secondary button hover effects - consistent interaction feedback */ +/* Secondary button hover effects - fallback to secondary when not set in theme */ .btn.btn-default.ellipsis:hover, .btn-default:hover { - background-color: var(--btn-secondary-hover-bg,#f3f3f3); - color: var(--btn-secondary-hover-color); + background-color: var(--btn-secondary-hover-bg, var(--btn-secondary-bg, #f3f3f3)); + color: var(--btn-secondary-hover-color, var(--btn-secondary-color, inherit)); } /* ======================================== @@ -260,8 +282,7 @@ body { } /* Page containers - main content wrapper background */ -.content.page-container, -.page-head { +.content.page-container { background-color: var(--body-bg,#fff); } diff --git a/frappe_desk_theme/public/js/frappe_desk_theme.bundle.js b/frappe_desk_theme/public/js/frappe_desk_theme.bundle.js index cec14d0..bde6beb 100644 --- a/frappe_desk_theme/public/js/frappe_desk_theme.bundle.js +++ b/frappe_desk_theme/public/js/frappe_desk_theme.bundle.js @@ -527,6 +527,15 @@ if (theme.navbar_text_color) { root.style.setProperty("--navbar-color", theme.navbar_text_color); } + if (theme.navbar_toggler_border_color) { + root.style.setProperty("--navbar-toggler-border", theme.navbar_toggler_border_color); + } + if (theme.navbar_breadcrumb_disabled_color) { + root.style.setProperty( + "--breadcrumb-disabled-color", + theme.navbar_breadcrumb_disabled_color + ); + } if (theme.hide_help_button !== undefined) { root.style.setProperty("--hide-help", theme.hide_help_button ? "none" : "block"); } @@ -541,7 +550,7 @@ ); } - // Primary button styling + // Primary button styling (hover fallbacks to normal when not set) if (theme.button_background_color) { root.style.setProperty("--btn-primary-bg", theme.button_background_color); } @@ -550,9 +559,13 @@ } if (theme.button_hover_background_color) { root.style.setProperty("--btn-primary-hover-bg", theme.button_hover_background_color); + } else if (theme.button_background_color) { + root.style.setProperty("--btn-primary-hover-bg", theme.button_background_color); } if (theme.button_hover_text_color) { root.style.setProperty("--btn-primary-hover-color", theme.button_hover_text_color); + } else if (theme.button_text_color) { + root.style.setProperty("--btn-primary-hover-color", theme.button_text_color); } // Secondary button styling @@ -567,12 +580,22 @@ "--btn-secondary-hover-bg", theme.secondary_button_hover_background_color ); + } else if (theme.secondary_button_background_color) { + root.style.setProperty( + "--btn-secondary-hover-bg", + theme.secondary_button_background_color + ); } if (theme.secondary_button_hover_text_color) { root.style.setProperty( "--btn-secondary-hover-color", theme.secondary_button_hover_text_color ); + } else if (theme.secondary_button_text_color) { + root.style.setProperty( + "--btn-secondary-hover-color", + theme.secondary_button_text_color + ); } // Main body and content area styling @@ -798,9 +821,17 @@ * default desktop / workspace landing. */ performInitialSidebarLoginRedirect() { - // Only run once per page load - if (this.didInitialSidebarLoginRedirect) { - return; + // Only run once per browser tab (use sessionStorage so it persists across reloads) + const redirectFlagKey = "frappe_desk_theme_sidebar_redirect_done"; + try { + if (sessionStorage.getItem(redirectFlagKey) === "1") { + return; + } + } catch (e) { + // If sessionStorage is unavailable, fall back to in-memory flag + if (this.didInitialSidebarLoginRedirect) { + return; + } } // Must be enabled in theme and have a fixed sidebar configured @@ -829,10 +860,9 @@ const route = frappe.get_route() || []; // Heuristic: only redirect from generic initial desk routes - const isInitialDeskRoute = - route.length === 0 || - route[0] === "desktop" || - (route[0] === "Workspaces" && route.length <= 2); + // We *don't* treat specific workspace routes as initial, so reloads + // on "Workspaces / " won't be redirected. + const isInitialDeskRoute = route.length === 0 || route[0] === "desktop"; if (!isInitialDeskRoute) { return; @@ -854,6 +884,11 @@ } this.didInitialSidebarLoginRedirect = true; + try { + sessionStorage.setItem(redirectFlagKey, "1"); + } catch (e) { + // Ignore storage errors + } try { const linkType = (firstLink.link_type || "").toLowerCase(); diff --git a/pyproject.toml b/pyproject.toml index 79f8dc5..c60b79f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,9 +15,9 @@ dependencies = [ requires = ["flit_core >=3.4,<4"] build-backend = "flit_core.buildapi" -# These dependencies are only installed when developer mode is enabled -[tool.bench.dev-dependencies] -# package_name = "~=1.1.0" +[tool.bench.frappe-dependencies] +frappe = ">=16.0.0,<17.0.0" + [tool.ruff] line-length = 110 From c6b1853f82759cc5ca40d56c74d12e854db8e636 Mon Sep 17 00:00:00 2001 From: bhushan-barbuddhe Date: Sun, 1 Mar 2026 13:19:37 +0530 Subject: [PATCH 268/274] feat: streamline page routing in sidebar and main script for improved navigation --- .../public/js/frappe_desk_theme.bundle.js | 3 ++- .../public/js/sidebar/sidebar_override.bundle.js | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/frappe_desk_theme/public/js/frappe_desk_theme.bundle.js b/frappe_desk_theme/public/js/frappe_desk_theme.bundle.js index bde6beb..536ea8c 100644 --- a/frappe_desk_theme/public/js/frappe_desk_theme.bundle.js +++ b/frappe_desk_theme/public/js/frappe_desk_theme.bundle.js @@ -900,7 +900,8 @@ // Go to list view for the DocType frappe.set_route("List", firstLink.link_to); } else if (linkType === "page") { - frappe.set_route("Page", firstLink.link_to); + // Use desk/page-name instead of desk/Page/page-name + frappe.set_route(firstLink.link_to); } else if (linkType === "report") { frappe.set_route("query-report", firstLink.link_to); } else if (linkType === "url") { diff --git a/frappe_desk_theme/public/js/sidebar/sidebar_override.bundle.js b/frappe_desk_theme/public/js/sidebar/sidebar_override.bundle.js index 8546ca2..ddd99a4 100644 --- a/frappe_desk_theme/public/js/sidebar/sidebar_override.bundle.js +++ b/frappe_desk_theme/public/js/sidebar/sidebar_override.bundle.js @@ -10,8 +10,9 @@ frappe.ui.Sidebar = class CustomSidebar extends frappe.ui.Sidebar { const current_route = frappe.get_route(); if (!current_route || !current_route.length) return; - const current_item = current_route[1]; - if (!current_item) return; + // For workspaces: route is ["Workspaces", workspace_name]. For doctype list: ["List", "DocType", ...]. + // For Page doctype: route is ["page-name"] (desk/page-name) or ["Page", "page-name"] (desk/Page/page-name). + const current_item = current_route[1] || current_route[0]; const $match = this.$sidebar.find(`.sidebar-item-container[item-name="${current_item}"]`); if ($match.length) { @@ -37,6 +38,15 @@ frappe.ui.Sidebar = class CustomSidebar extends frappe.ui.Sidebar { this.prepare_sidebar(root_pages, sidebar_section, this.wrapper.find(".sidebar-items")); + // Rewrite Page links from desk/Page/page-name or desk/page/page-name to desk/page-name + sidebar_section.find(".item-anchor[href]").each(function () { + const href = $(this).attr("href") || ""; + const match = href.match(/^\/desk\/(?:Page|page)\/([^/?#]+)/); + if (match) { + $(this).attr("href", "/desk/" + match[1]); + } + }); + if (Object.keys(root_pages).length === 0) { sidebar_section.addClass("hidden"); } From 4641b24b2d80b990e115e1c0859f7bfa008817eb Mon Sep 17 00:00:00 2001 From: bhushan-barbuddhe Date: Mon, 2 Mar 2026 10:24:21 +0530 Subject: [PATCH 269/274] refactor: improve readability of conditional checks in sidebar behavior methods --- .../public/js/frappe_desk_theme.bundle.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/frappe_desk_theme/public/js/frappe_desk_theme.bundle.js b/frappe_desk_theme/public/js/frappe_desk_theme.bundle.js index 536ea8c..192aa95 100644 --- a/frappe_desk_theme/public/js/frappe_desk_theme.bundle.js +++ b/frappe_desk_theme/public/js/frappe_desk_theme.bundle.js @@ -3,7 +3,7 @@ * Handles loading, applying, and managing custom theme configurations for Frappe Desk * Supports dynamic theme changes, user role-based hiding, and real-time DOM updates */ - class FrappeDeskTheme { +class FrappeDeskTheme { constructor() { // Store theme configuration data from server this.themeData = null; @@ -769,7 +769,10 @@ */ applyFixedSidebarBehavior() { // Only applicable inside Desk (not on login page) - if (document.body.classList.contains("login-page") || document.querySelector("#page-login")) { + if ( + document.body.classList.contains("login-page") || + document.querySelector("#page-login") + ) { return; } @@ -844,7 +847,10 @@ } // Not applicable on the login page - if (document.body.classList.contains("login-page") || document.querySelector("#page-login")) { + if ( + document.body.classList.contains("login-page") || + document.querySelector("#page-login") + ) { return; } From 62b1b3dfb26653ee940abd943cdef4dc468a40fe Mon Sep 17 00:00:00 2001 From: bhushan-barbuddhe Date: Mon, 2 Mar 2026 13:24:32 +0530 Subject: [PATCH 270/274] refactor: remove app switcher functionality and streamline desk theme settings --- .../doctype/desk_theme/desk_theme.js | 70 ---------- .../doctype/desk_theme/desk_theme.json | 113 +++++++++------- .../doctype/desk_theme/desk_theme.py | 28 ---- .../public/css/frappe_desk_theme.bundle.css | 128 ++++++++++++++++-- .../public/js/frappe_desk_theme.bundle.js | 41 ++---- 5 files changed, 193 insertions(+), 187 deletions(-) diff --git a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.js b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.js index 0066aa5..d42b3a7 100644 --- a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.js +++ b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.js @@ -9,28 +9,6 @@ frappe.ui.form.on("Desk Theme", { refresh(frm) { - // Load app options for default_app field - frappe.xcall("frappe.apps.get_apps").then((r) => { - let apps = r?.map((r) => r.name) || []; - frm.set_df_property("default_app", "options", ["", ...apps]); - }); - - // Load current system default app if hide_app_switcher is enabled - if (frm.doc.hide_app_switcher) { - frappe.call({ - method: "frappe.client.get_value", - args: { - doctype: "System Settings", - fieldname: "default_app", - }, - callback: function (r) { - if (r.message && r.message.default_app) { - frm.set_value("default_app", r.message.default_app); - } - }, - }); - } - // Add refresh theme button frm.add_custom_button(__("Refresh Theme"), function () { window.frappeDeskTheme?.clearCache(); @@ -38,52 +16,4 @@ frappe.ui.form.on("Desk Theme", { frappe.show_alert({ message: __("Theme refreshed"), indicator: "green" }); }); }, - - hide_app_switcher(frm) { - if (frm.doc.hide_app_switcher) { - // Load current system default app when hide_app_switcher is checked - frappe.call({ - method: "frappe.client.get_value", - args: { - doctype: "System Settings", - fieldname: "default_app", - }, - callback: function (r) { - if (r.message && r.message.default_app) { - frm.set_value("default_app", r.message.default_app); - } - }, - }); - } else { - // Clear default_app when hide_app_switcher is unchecked - frm.set_value("default_app", ""); - } - }, - - validate(frm) { - // Validate that default_app is set when hide_app_switcher is checked - if (frm.doc.hide_app_switcher && !frm.doc.default_app) { - frappe.throw(__("Default App is required when App Switcher is hidden")); - } - }, - - after_save(frm) { - // Update system settings with the selected default app - if (frm.doc.hide_app_switcher && frm.doc.default_app) { - frappe.call({ - method: "frappe_desk_theme.frappe_desk_theme.doctype.desk_theme.desk_theme.update_system_default_app", - args: { - default_app: frm.doc.default_app, - }, - callback: function (r) { - if (r.message && r.message.success) { - frappe.show_alert({ - message: __("System default app updated successfully"), - indicator: "green", - }); - } - }, - }); - } - }, }); diff --git a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json index 240a8d7..81ebc7b 100644 --- a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json +++ b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json @@ -8,31 +8,29 @@ "login_page_tab", "login_page_section", "login_button_background_color", + "login_button_text_color", + "column_break_umnl", "login_page_button_hover_background_color", - "page_background_type", + "login_page_button_hover_text_color", + "column_break_ntnk", + "login_box_background_color", + "page_heading_text_color", + "page_setting_section", + "login_template", + "login_page_title", + "is_app_details_inside_the_box", "allow_manual_navigation", - "carousel_images", "login_page_background_color", + "carousel_images", "login_page_background_image", - "is_app_details_inside_the_box", - "login_page_title", - "column_break_umnl", - "login_button_text_color", - "login_page_button_hover_text_color", + "column_break_xyqa", "login_box_position", - "page_heading_text_color", - "login_box_background_color", + "page_background_type", "navbar_tab", "navbar_section", "navbar_color", - "hide_help_button", - "hide_app_switcher", - "default_app", "column_break_jevx", "navbar_text_color", - "navbar_toggler_border_color", - "navbar_breadcrumb_disabled_color", - "hide_search", "buttons_tab", "primary_button_section", "button_background_color", @@ -53,9 +51,13 @@ "column_break_ojdd", "main_body_content_box_background_color", "main_body_content_box_text_color", + "sidebar_tab", "sidebar_section", "sidebar_background_color", "sidebar_hover_background_color", + "hide_standard_menu", + "hide_notification", + "hide_search", "column_break_wdml", "sidebar_text_color", "sidebar_hover_text_color", @@ -91,6 +93,7 @@ "footer_text_color", "desk_behavior_tab", "fixed_sidebar", + "column_break_cgnw", "redirect_to_sidebar_on_login" ], "fields": [ @@ -108,16 +111,6 @@ "fieldtype": "Color", "label": "Text Color" }, - { - "fieldname": "navbar_toggler_border_color", - "fieldtype": "Color", - "label": "Toggler Border Color" - }, - { - "fieldname": "navbar_breadcrumb_disabled_color", - "fieldtype": "Color", - "label": "Breadcrumb Disabled Color" - }, { "fieldname": "column_break_jevx", "fieldtype": "Column Break" @@ -148,7 +141,8 @@ }, { "fieldname": "login_page_section", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Color Management" }, { "fieldname": "login_button_text_color", @@ -215,6 +209,14 @@ "fieldtype": "Color", "label": "Box Background Color" }, + { + "default": "default", + "description": "Choose which login page layout to show. All templates use the same Desk Theme colors and options above.", + "fieldname": "login_template", + "fieldtype": "Select", + "label": "Login Template", + "options": "default\nmodern\nminimal\nsplit\ncentered" + }, { "fieldname": "main_body_section", "fieldtype": "Section Break", @@ -345,12 +347,6 @@ "fieldtype": "Tab Break", "label": "Table" }, - { - "default": "0", - "fieldname": "hide_help_button", - "fieldtype": "Check", - "label": "Hide Help Button" - }, { "default": "0", "fieldname": "table_hide_like_comment_section", @@ -432,8 +428,7 @@ }, { "fieldname": "sidebar_section", - "fieldtype": "Section Break", - "label": "Sidebar" + "fieldtype": "Section Break" }, { "fieldname": "sidebar_background_color", @@ -449,20 +444,6 @@ "fieldtype": "Color", "label": "Sidebar Text Color" }, - { - "default": "0", - "fieldname": "hide_app_switcher", - "fieldtype": "Check", - "label": "Hide App Switcher" - }, - { - "depends_on": "eval:doc.hide_app_switcher == 1", - "description": "Select default app when app switcher is hidden.", - "fieldname": "default_app", - "fieldtype": "Select", - "label": "Default App", - "mandatory_depends_on": "eval:doc.hide_app_switcher == 1" - }, { "fieldname": "footer_tab", "fieldtype": "Tab Break", @@ -549,6 +530,40 @@ "fieldname": "redirect_to_sidebar_on_login", "fieldtype": "Check", "label": "Redirect to Sidebar After Login" + }, + { + "fieldname": "column_break_ntnk", + "fieldtype": "Column Break" + }, + { + "fieldname": "sidebar_tab", + "fieldtype": "Tab Break", + "label": "Sidebar" + }, + { + "fieldname": "column_break_cgnw", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "hide_standard_menu", + "fieldtype": "Check", + "label": "Hide Standard Menu" + }, + { + "default": "0", + "fieldname": "hide_notification", + "fieldtype": "Check", + "label": "Hide Notification" + }, + { + "fieldname": "page_setting_section", + "fieldtype": "Section Break", + "label": "Page Setting" + }, + { + "fieldname": "column_break_xyqa", + "fieldtype": "Column Break" } ], "grid_page_length": 50, @@ -556,7 +571,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2025-09-20 18:42:32.444450", + "modified": "2026-03-02 13:13:51.800906", "modified_by": "Administrator", "module": "Frappe Desk Theme", "name": "Desk Theme", diff --git a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.py b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.py index 8c80b69..6fb68f4 100644 --- a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.py +++ b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.py @@ -7,10 +7,6 @@ class DeskTheme(Document): def validate(self): - # Validate that default_app is set when hide_app_switcher is checked - if self.hide_app_switcher and not self.default_app: - frappe.throw("Default App is required when App Switcher is hidden") - # Carousel validation: if carousel selected, must have at least one image if self.page_background_type == "Carousel": if not self.carousel_images or not any(img.image for img in self.carousel_images): @@ -19,10 +15,6 @@ def validate(self): frappe.msgprint("No carousel images found. Falling back to default background.") def on_update(self): - # Update system settings with the selected default app - if self.hide_app_switcher and self.default_app: - update_system_default_app(self.default_app) - # Update website settings with footer information self.update_website_settings() @@ -55,23 +47,3 @@ def get_carousel_data(self): "manual_navigation": getattr(self, "allow_manual_navigation", True), "auto_advance": getattr(self, "carousel_auto_advance", True), } - - -@frappe.whitelist() -def update_system_default_app(default_app): - """Update the system default app setting""" - try: - # Check if the app exists in installed apps - installed_apps = frappe.get_installed_apps() - if default_app not in installed_apps: - frappe.throw(f"App '{default_app}' is not installed") - - # Update system settings - system_settings = frappe.get_single("System Settings") - system_settings.default_app = default_app - system_settings.save(ignore_permissions=True) - - return {"success": True} - except Exception as e: - frappe.log_error(f"Error updating system default app: {e!s}") - frappe.throw(f"Failed to update system default app: {e!s}") diff --git a/frappe_desk_theme/public/css/frappe_desk_theme.bundle.css b/frappe_desk_theme/public/css/frappe_desk_theme.bundle.css index 869e32a..7e57849 100644 --- a/frappe_desk_theme/public/css/frappe_desk_theme.bundle.css +++ b/frappe_desk_theme/public/css/frappe_desk_theme.bundle.css @@ -258,6 +258,13 @@ button.navbar-toggler { } /* Secondary/default buttons - alternative action buttons with distinct styling */ +.btn.btn-default.ellipsis svg, +.btn-default svg, +.btn-default svg use { + stroke: var(--btn-secondary-color); + fill: var(--btn-secondary-color); +} +.btn.btn-default.icon-btn, .btn.btn-default.ellipsis, .btn-default, .btn-default:active { @@ -266,6 +273,7 @@ button.navbar-toggler { } /* Secondary button hover effects - fallback to secondary when not set in theme */ +.btn.btn-default.icon-btn:hover, .btn.btn-default.ellipsis:hover, .btn-default:hover { background-color: var(--btn-secondary-hover-bg, var(--btn-secondary-bg, #f3f3f3)); @@ -308,6 +316,14 @@ body { color: var(--sidebar-text-color,#525252) !important; } +/* Sidebar structural sections - ensure all sidebar areas use theme colors */ +.body-sidebar .standard-items-sections, +.body-sidebar .body-sidebar-cards, +.body-sidebar .body-sidebar-bottom { + background-color: var(--sidebar-bg,#f8f8f8) !important; + color: var(--sidebar-text-color,#525252) !important; +} + /* Sidebar items - all text elements in sidebar navigation */ .standard-sidebar-item, .item-anchor, @@ -318,12 +334,41 @@ body { color: var(--sidebar-text-color,#525252) !important; } +/* Sidebar header - use sidebar background and text colors */ +.sidebar-header { + background-color: var(--sidebar-bg,#f8f8f8) !important; + color: var(--sidebar-text-color,#525252) !important; +} + +.sidebar-header .sidebar-item-label, +.sidebar-header .header-title, +.sidebar-header .header-subtitle { + color: var(--sidebar-text-color,#525252) !important; +} + /* Sidebar item hover state - background and text color on hover */ .standard-sidebar-item:hover { background-color: var(--sidebar-hover-bg, #e9ecef) !important; color: var(--sidebar-hover-text-color, #212529) !important; } +/* Sidebar header hover + active state - match sidebar items */ +.sidebar-header:hover, +.sidebar-header.active-sidebar, +.sidebar-header.active-sidebar:hover { + background-color: var(--sidebar-hover-bg, #e9ecef) !important; + color: var(--sidebar-hover-text-color, #212529) !important; +} + +.sidebar-header:hover .sidebar-item-label, +.sidebar-header:hover .header-title, +.sidebar-header:hover .header-subtitle, +.sidebar-header.active-sidebar .sidebar-item-label, +.sidebar-header.active-sidebar .header-title, +.sidebar-header.active-sidebar .header-subtitle { + color: var(--sidebar-hover-text-color, #212529) !important; +} + /* Sidebar item hover state - child elements inherit hover colors */ .standard-sidebar-item:hover .item-anchor, .standard-sidebar-item:hover .sidebar-item-label, @@ -369,6 +414,78 @@ body { color: var(--sidebar-text-color, #525252) !important; } +/* Collapse sidebar and bottom actions hover - use sidebar hover colors */ +.body-sidebar .collapse-sidebar-link:hover, +.body-sidebar .onboarding-sidebar:hover, +.body-sidebar .promotional-banner:hover, +.body-sidebar .dropdown-navbar-user:hover, +.body-sidebar .dropdown-navbar-user .nav-link:hover, +.body-sidebar .dropdown-navbar-user:hover .nav-link { + background-color: var(--sidebar-hover-bg, #e9ecef) !important; + color: var(--sidebar-hover-text-color, #212529) !important; +} + +/* User block (avatar + name) in sidebar - use sidebar text color */ +.body-sidebar .dropdown-navbar-user, +.body-sidebar .dropdown-navbar-user .nav-link, +.body-sidebar .dropdown-navbar-user span { + color: var(--sidebar-text-color, #525252) !important; +} + +.body-sidebar .dropdown-navbar-user svg, +.body-sidebar .dropdown-navbar-user svg use { + fill: var(--sidebar-text-color, #525252) !important; + stroke: var(--sidebar-text-color, #525252) !important; +} + +/* User menu dropdown (inside sidebar) - background and hover colors */ +.body-sidebar .dropdown-menu { + background-color: var(--sidebar-bg, #f8f8f8) !important; + color: var(--sidebar-text-color, #525252) !important; +} + +.body-sidebar .dropdown-menu .dropdown-item { + background-color: transparent !important; + color: var(--sidebar-text-color, #525252) !important; +} + +.body-sidebar .dropdown-menu .dropdown-item:hover, +.body-sidebar .dropdown-menu .dropdown-item:focus { + background-color: var(--sidebar-hover-bg, #e9ecef) !important; + color: var(--sidebar-hover-text-color, #212529) !important; +} + +/* App/toolbar context menu (frappe-menu) - match sidebar theme */ +.frappe-menu.context-menu { + background-color: var(--sidebar-bg, #f8f8f8) !important; + color: var(--sidebar-text-color, #525252) !important; +} + +.frappe-menu.context-menu .dropdown-menu-item a, +.frappe-menu.context-menu .menu-item-title { + color: var(--sidebar-text-color, #525252) !important; +} + +.frappe-menu.context-menu .menu-item-icon svg, +.frappe-menu.context-menu .menu-item-icon svg use { + fill: var(--sidebar-text-color, #525252) !important; + stroke: var(--sidebar-text-color, #525252) !important; +} + +.frappe-menu.context-menu .dropdown-menu-item:hover, +.frappe-menu.context-menu .dropdown-menu-item:focus { + background-color: var(--sidebar-hover-bg, #e9ecef) !important; + color: var(--sidebar-hover-text-color, #212529) !important; +} + +.frappe-menu.context-menu .dropdown-menu-item:hover .menu-item-title, +.frappe-menu.context-menu .dropdown-menu-item:hover .menu-item-icon svg, +.frappe-menu.context-menu .dropdown-menu-item:hover .menu-item-icon svg use { + color: var(--sidebar-hover-text-color, #212529) !important; + fill: var(--sidebar-hover-text-color, #212529) !important; + stroke: var(--sidebar-hover-text-color, #212529) !important; +} + /* Collapse sidebar link SVG icons */ .collapse-sidebar-link svg { fill: var(--sidebar-text-color, #525252) !important; @@ -463,17 +580,6 @@ div.level-right { display: var(--hide-help, block) !important; } -/* App switcher - navigation dropdown that can be hidden based on theme settings */ -.sidebar-item-control{ - display: var(--hide-app-switcher, block) !important; -} - -/* App switcher anchor - disable clicking when app switcher is hidden */ -.app-switcher-dropdown { - pointer-events: var(--app-switcher-pointer-events, auto) !important; -} - - /* ======================================== WIDGET/CARD STYLING ======================================== */ diff --git a/frappe_desk_theme/public/js/frappe_desk_theme.bundle.js b/frappe_desk_theme/public/js/frappe_desk_theme.bundle.js index 192aa95..5134ce1 100644 --- a/frappe_desk_theme/public/js/frappe_desk_theme.bundle.js +++ b/frappe_desk_theme/public/js/frappe_desk_theme.bundle.js @@ -335,8 +335,6 @@ class FrappeDeskTheme { "--breadcrumb-disabled-color", "--help-nav-link-color", "--help-nav-link-stroke", - "--hide-app-switcher", - "--app-switcher-pointer-events", "--footer-bg", "--footer-color", "--footer-border", @@ -375,8 +373,6 @@ class FrappeDeskTheme { // UI element visibility defaults root.style.setProperty("--hide-help", "block"); root.style.setProperty("--hide-like-comment", "block"); - root.style.setProperty("--hide-app-switcher", "block"); - root.style.setProperty("--app-switcher-pointer-events", "auto"); root.style.setProperty("--sidebar-expanded", ""); root.style.setProperty("--sidebar-hover-bg", "#e9ecef"); root.style.setProperty("--sidebar-hover-text-color", "#212529"); @@ -539,16 +535,6 @@ class FrappeDeskTheme { if (theme.hide_help_button !== undefined) { root.style.setProperty("--hide-help", theme.hide_help_button ? "none" : "block"); } - if (theme.hide_app_switcher !== undefined) { - root.style.setProperty( - "--hide-app-switcher", - theme.hide_app_switcher ? "none" : "block" - ); - root.style.setProperty( - "--app-switcher-pointer-events", - theme.hide_app_switcher ? "none" : "auto" - ); - } // Primary button styling (hover fallbacks to normal when not set) if (theme.button_background_color) { @@ -680,7 +666,7 @@ class FrappeDeskTheme { this.setCSSVariables(); this.toggleSidebar(); this.toggleSearchBar(); - this.setDefaultApp(); + this.hideStandardMenu(); this.applyFixedSidebarBehavior(); this.performInitialSidebarLoginRedirect(); if ( @@ -743,24 +729,20 @@ class FrappeDeskTheme { } /** - * Set current app to default app when app switcher is hidden - * Similar to breadcrumbs.js line 83 functionality + * Hide the standard toolbar/context menu when configured + * Uses the 'hide_standard_menu' flag from Desk Theme doctype */ - setDefaultApp() { - // Only proceed if hide_app_switcher is enabled and default_app is set - if (!this.themeData.hide_app_switcher || !this.themeData.default_app) { + hideStandardMenu() { + // Only act when explicitly enabled in theme configuration + if (!this.themeData || !this.themeData.hide_standard_menu) { return; } - // Check if frappe.app.sidebar.apps_switcher exists (similar to breadcrumbs.js) - if (frappe?.app?.sidebar?.apps_switcher?.set_current_app) { - try { - // Set the current app to the default app (same as breadcrumbs.js line 83) - frappe.app.sidebar.apps_switcher.set_current_app(this.themeData.default_app); - } catch (error) { - // Silent fail if app switcher is not available or app doesn't exist - } - } + // Hide all Frappe context menus that use the standard menu class + const menus = document.querySelectorAll(".frappe-menu.context-menu"); + menus.forEach((menu) => { + menu.style.display = "none"; + }); } /** @@ -1113,6 +1095,7 @@ class FrappeDeskTheme { let footerTimeout; const observer = new MutationObserver(() => { this.toggleSearchBar(); + this.hideStandardMenu(); this.applyFixedSidebarBehavior(); this.performInitialSidebarLoginRedirect(); From 32698b5832c501c4d30745853701713054d36354 Mon Sep 17 00:00:00 2001 From: bhushan-barbuddhe Date: Mon, 2 Mar 2026 17:02:17 +0530 Subject: [PATCH 271/274] refactor: reorganize desk theme settings and remove unused login template field --- .../doctype/desk_theme/desk_theme.json | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json index 81ebc7b..7446010 100644 --- a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json +++ b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json @@ -16,16 +16,15 @@ "login_box_background_color", "page_heading_text_color", "page_setting_section", - "login_template", "login_page_title", - "is_app_details_inside_the_box", - "allow_manual_navigation", + "page_background_type", "login_page_background_color", - "carousel_images", "login_page_background_image", "column_break_xyqa", "login_box_position", - "page_background_type", + "is_app_details_inside_the_box", + "carousel_images", + "allow_manual_navigation", "navbar_tab", "navbar_section", "navbar_color", @@ -209,14 +208,6 @@ "fieldtype": "Color", "label": "Box Background Color" }, - { - "default": "default", - "description": "Choose which login page layout to show. All templates use the same Desk Theme colors and options above.", - "fieldname": "login_template", - "fieldtype": "Select", - "label": "Login Template", - "options": "default\nmodern\nminimal\nsplit\ncentered" - }, { "fieldname": "main_body_section", "fieldtype": "Section Break", @@ -571,7 +562,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2026-03-02 13:13:51.800906", + "modified": "2026-03-02 17:01:54.837120", "modified_by": "Administrator", "module": "Frappe Desk Theme", "name": "Desk Theme", From da0c8142675546ad3f83a4da4564b6226fe00ff8 Mon Sep 17 00:00:00 2001 From: devang-dhwaniris Date: Mon, 9 Mar 2026 12:13:33 +0530 Subject: [PATCH 272/274] feat: add sidebar section break customization options for text color and boldness --- .../doctype/desk_theme/desk_theme.json | 15 +++++++++++++++ .../public/css/frappe_desk_theme.bundle.css | 6 ++++++ .../public/js/frappe_desk_theme.bundle.js | 11 +++++++++++ 3 files changed, 32 insertions(+) diff --git a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json index 7446010..9078a54 100644 --- a/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json +++ b/frappe_desk_theme/frappe_desk_theme/doctype/desk_theme/desk_theme.json @@ -60,6 +60,8 @@ "column_break_wdml", "sidebar_text_color", "sidebar_hover_text_color", + "sidebar_section_break_text_color", + "sidebar_section_break_bold", "table_tab", "list_table_section", "table_head_background_color", @@ -503,6 +505,19 @@ "fieldtype": "Color", "label": "Hover Text Color" }, + { + "description": "Color for sidebar section headers (e.g. Modules, Tools). Leave blank to use default sidebar text color.", + "fieldname": "sidebar_section_break_text_color", + "fieldtype": "Color", + "label": "Section Break Text Color" + }, + { + "default": "0", + "description": "Make section break labels bold.", + "fieldname": "sidebar_section_break_bold", + "fieldtype": "Check", + "label": "Section Break Bold" + }, { "fieldname": "desk_behavior_tab", "fieldtype": "Tab Break", diff --git a/frappe_desk_theme/public/css/frappe_desk_theme.bundle.css b/frappe_desk_theme/public/css/frappe_desk_theme.bundle.css index 7e57849..36c151f 100644 --- a/frappe_desk_theme/public/css/frappe_desk_theme.bundle.css +++ b/frappe_desk_theme/public/css/frappe_desk_theme.bundle.css @@ -409,6 +409,12 @@ body { stroke: var(--sidebar-hover-text-color, #212529) !important; } +/* Section break labels (sidebar group headers) - custom color and bold */ +.body-sidebar .section-break .sidebar-item-label { + color: var(--sidebar-section-break-color, var(--sidebar-text-color, #525252)) !important; + font-weight: var(--sidebar-section-break-font-weight, normal) !important; +} + /* Collapse sidebar link */ .collapse-sidebar-link { color: var(--sidebar-text-color, #525252) !important; diff --git a/frappe_desk_theme/public/js/frappe_desk_theme.bundle.js b/frappe_desk_theme/public/js/frappe_desk_theme.bundle.js index 5134ce1..2436adb 100644 --- a/frappe_desk_theme/public/js/frappe_desk_theme.bundle.js +++ b/frappe_desk_theme/public/js/frappe_desk_theme.bundle.js @@ -608,6 +608,17 @@ class FrappeDeskTheme { if (theme.sidebar_hover_text_color) { root.style.setProperty("--sidebar-hover-text-color", theme.sidebar_hover_text_color); } + if (theme.sidebar_section_break_text_color) { + root.style.setProperty( + "--sidebar-section-break-color", + theme.sidebar_section_break_text_color + ); + } + if (theme.sidebar_section_break_bold) { + root.style.setProperty("--sidebar-section-break-font-weight", "700"); + } else { + root.style.setProperty("--sidebar-section-break-font-weight", "normal"); + } // Data table styling if (theme.table_head_background_color) { From 7ff429272729b15af2ccc86cadcd9c830ddbe321 Mon Sep 17 00:00:00 2001 From: Navneet Patteri Date: Mon, 16 Mar 2026 12:13:17 +0530 Subject: [PATCH 273/274] fix: remove scrollbar in sidebar menu --- frappe_desk_theme/public/css/frappe_desk_theme.bundle.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frappe_desk_theme/public/css/frappe_desk_theme.bundle.css b/frappe_desk_theme/public/css/frappe_desk_theme.bundle.css index 36c151f..160d580 100644 --- a/frappe_desk_theme/public/css/frappe_desk_theme.bundle.css +++ b/frappe_desk_theme/public/css/frappe_desk_theme.bundle.css @@ -316,12 +316,18 @@ body { color: var(--sidebar-text-color,#525252) !important; } +.body-sidebar .body-sidebar-top{ + scrollbar-width: none; +} + /* Sidebar structural sections - ensure all sidebar areas use theme colors */ .body-sidebar .standard-items-sections, .body-sidebar .body-sidebar-cards, .body-sidebar .body-sidebar-bottom { background-color: var(--sidebar-bg,#f8f8f8) !important; color: var(--sidebar-text-color,#525252) !important; + scrollbar-width: none; + } /* Sidebar items - all text elements in sidebar navigation */ From b95ab37793780e0d49429e1ba6037fac249e0003 Mon Sep 17 00:00:00 2001 From: Ankit Jangir Date: Thu, 16 Apr 2026 10:08:01 +0530 Subject: [PATCH 274/274] chore: sync optimized workflows from frappe-repo-starter Synced from dhwani-ris/frappe-repo-starter to reduce GitHub Actions cost. Changes include path filters, concurrency cancellation, reduced matrix builds, weekly security scans, and merged PR workflows. --- .github/workflows/auto-reviewer.yml | 90 ---- .github/workflows/bot-handler.yml | 4 +- .github/workflows/ci.yml | 667 ++++++++++--------------- .github/workflows/code-quality.yml | 256 ++++------ .github/workflows/devops-checklist.yml | 386 ++++++++++++++ .github/workflows/notifications.yml | 127 +++++ .github/workflows/pr-labeler.yml | 251 +++++----- .github/workflows/release.yml | 208 +++----- .github/workflows/security-scan.yml | 342 +++++-------- .pre-commit-config.yaml | 2 +- commitlint.config.js | 10 +- 11 files changed, 1244 insertions(+), 1099 deletions(-) delete mode 100644 .github/workflows/auto-reviewer.yml create mode 100644 .github/workflows/devops-checklist.yml create mode 100644 .github/workflows/notifications.yml diff --git a/.github/workflows/auto-reviewer.yml b/.github/workflows/auto-reviewer.yml deleted file mode 100644 index 940ddde..0000000 --- a/.github/workflows/auto-reviewer.yml +++ /dev/null @@ -1,90 +0,0 @@ -name: Auto Request Review - -on: - pull_request: - types: [opened, synchronize, reopened, ready_for_review] - branches: [ main, master ] - -permissions: - pull-requests: write - contents: read - -jobs: - request-review: - name: Request Review from Default Reviewer - runs-on: ubuntu-latest - if: | - github.event.pull_request.base.ref == 'main' || - github.event.pull_request.base.ref == 'master' - - steps: - - name: Request review from default reviewer - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const reviewer = 'dhwani-ankit'; - const pr = context.payload.pull_request; - - console.log(`Processing PR #${pr.number} targeting ${pr.base.ref}`); - - // Check if PR is not in draft state - if (pr.draft) { - console.log('PR is in draft state, skipping review request'); - return; - } - - // Wait a bit to ensure PR is fully ready - await new Promise(resolve => setTimeout(resolve, 2000)); - - try { - // Get current reviewers - const { data: currentReviews } = await github.rest.pulls.listRequestedReviewers({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: pr.number, - }); - - console.log('Current reviewers:', JSON.stringify(currentReviews, null, 2)); - - // Check if reviewer is already requested - const isAlreadyRequested = currentReviews.users?.some( - user => user.login.toLowerCase() === reviewer.toLowerCase() - ) || currentReviews.teams?.some( - team => team.slug.toLowerCase() === reviewer.toLowerCase() - ); - - if (isAlreadyRequested) { - console.log(`✅ Review already requested from ${reviewer}`); - return; - } - - // Request review from default reviewer - await github.rest.pulls.requestReviewers({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: pr.number, - reviewers: [reviewer], - }); - - console.log(`✅ Successfully requested review from ${reviewer}`); - } catch (error) { - console.error(`❌ Error requesting review from ${reviewer}:`, error); - console.error('Error details:', JSON.stringify(error, null, 2)); - - // If user not found, try to add as assignee instead - if (error.status === 422 || error.message.includes('not found')) { - try { - await github.rest.issues.addAssignees({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number, - assignees: [reviewer], - }); - console.log(`✅ Added ${reviewer} as assignee instead`); - } catch (assignError) { - console.error(`⚠️ Could not add ${reviewer} as assignee:`, assignError.message); - } - } - } - diff --git a/.github/workflows/bot-handler.yml b/.github/workflows/bot-handler.yml index e91c578..7775760 100644 --- a/.github/workflows/bot-handler.yml +++ b/.github/workflows/bot-handler.yml @@ -2,9 +2,9 @@ name: Dhwani Release Bot Handler on: issue_comment: - types: [created, edited] + types: [created] pull_request: - types: [opened, synchronize, reopened] + types: [opened] branches: [ main, master ] permissions: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 01dae23..1add21e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,397 +1,270 @@ -name: CI - -on: - # Run on push only for main/master (merged code validation) - push: - branches: [ main, master ] - # Run on PRs for all branches (pre-merge validation) - pull_request: - branches: [ main, master, develop, development ] - workflow_dispatch: - -permissions: - contents: read - -# Wait for init workflow to complete first (for push to main/master) -concurrency: - group: ${{ (github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master')) && format('repo-init-{0}-{1}', github.repository, github.ref) || format('ci-{0}-{1}', github.repository, github.ref) }} - cancel-in-progress: false - -jobs: - dependency-vulnerability: - name: 'Vulnerable Dependency Check' - runs-on: ubuntu-latest - # Run on merge to main/master (push event) - if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/development') - - steps: - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: '3.13' - - - name: Checkout code - uses: actions/checkout@v4 - - - name: Cache pip - uses: actions/cache@v4 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }} - restore-keys: | - ${{ runner.os }}-pip- - ${{ runner.os }}- - - - name: Install and run pip-audit - run: | - pip install pip-audit - cd ${GITHUB_WORKSPACE} - pip-audit --desc on --ignore-vuln PYSEC-2023-312 . - - test: - name: 'Test' - runs-on: ubuntu-latest - # Run on both PR and merge - if: github.event_name == 'pull_request' || (github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/development')) - strategy: - matrix: - python-version: ["3.8", "3.11", "3.13"] - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - cache: pip - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install pytest pytest-cov pytest-xdist - pip install -e ".[dev]" || pip install -r requirements.txt || echo "No requirements found" - - - name: Run tests with coverage - run: | - pytest --cov=. --cov-report=xml --cov-report=term --cov-report=html || echo "No tests found or pytest not configured" - continue-on-error: true - - - name: Check if coverage file exists - id: coverage-check - run: | - if [ -f coverage.xml ]; then - echo "exists=true" >> $GITHUB_OUTPUT - echo "Coverage file found" - else - echo "exists=false" >> $GITHUB_OUTPUT - echo "Coverage file not found - tests may not have run or pytest-cov not installed" - fi - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 - with: - files: ./coverage.xml - fail_ci_if_error: false - token: ${{ secrets.CODECOV_TOKEN }} - slug: ${{ github.repository }} - flags: unittests - name: codecov-umbrella - verbose: true - override_commit: ${{ github.event.pull_request.head.sha || github.sha }} - override_branch: ${{ github.event.pull_request.head.ref || github.ref_name }} - if: steps.coverage-check.outputs.exists == 'true' - - - name: Codecov Upload Status - if: always() && steps.coverage-check.outputs.exists == 'true' - run: | - echo "Codecov upload completed. Check https://codecov.io/gh/${{ github.repository }} for coverage reports." - - frappe-bench-test: - name: 'Frappe Bench App Tests' - runs-on: ubuntu-latest - # Run on merge to main/master (push event) - if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/development') - - services: - mariadb: - image: mariadb:10.11 - env: - MYSQL_ROOT_PASSWORD: root - MYSQL_DATABASE: test_frappe - ports: - - 3306:3306 - options: >- - --health-cmd="mysqladmin ping -h localhost" - --health-interval=10s - --health-timeout=5s - --health-retries=5 - redis: - image: redis:7-alpine - ports: - - 6379:6379 - options: >- - --health-cmd="redis-cli ping" - --health-interval=10s - --health-timeout=5s - --health-retries=5 - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Check if Frappe app exists - id: check-app - run: | - if [ -f "__init__.py" ] || [ -f "hooks.py" ]; then - echo "exists=true" >> $GITHUB_OUTPUT - echo "Frappe app files found, proceeding with tests" - else - echo "exists=false" >> $GITHUB_OUTPUT - echo "No Frappe app files found (__init__.py or hooks.py), skipping Frappe bench tests" - fi - - - name: Setup Python - if: steps.check-app.outputs.exists == 'true' - uses: actions/setup-python@v5 - with: - python-version: '3.12' - cache: pip - - - name: Setup Node.js - if: steps.check-app.outputs.exists == 'true' - uses: actions/setup-node@v4 - with: - node-version: 18 - - - name: Install system dependencies - if: steps.check-app.outputs.exists == 'true' - run: | - sudo apt-get update - sudo apt-get install -y \ - mariadb-client \ - redis-tools \ - curl \ - git \ - wget \ - xvfb \ - libfontconfig1 \ - libfreetype6 \ - libxrender1 \ - libjpeg-turbo8 \ - xfonts-75dpi \ - xfonts-base - - - name: Install Frappe Bench CLI - if: steps.check-app.outputs.exists == 'true' - run: | - pip install frappe-bench - - - name: Wait for MariaDB to be ready - if: steps.check-app.outputs.exists == 'true' - run: | - for i in {1..30}; do - if mysqladmin ping -h 127.0.0.1 -P 3306 -u root -proot --silent; then - echo "MariaDB is ready" - break - fi - echo "Waiting for MariaDB... ($i/30)" - sleep 2 - done - - - name: Wait for Redis to be ready - if: steps.check-app.outputs.exists == 'true' - run: | - for i in {1..30}; do - if redis-cli -h 127.0.0.1 -p 6379 ping | grep -q PONG; then - echo "Redis is ready" - break - fi - echo "Waiting for Redis... ($i/30)" - sleep 2 - done - - - name: Detect app name - if: steps.check-app.outputs.exists == 'true' - id: app-name - run: | - # Derive app name from repo name, removing frappe_ prefix if present - REPO_NAME=$(echo "${{ github.repository }}" | cut -d'/' -f2) - APP_NAME=$(echo "$REPO_NAME" | tr '-' '_' | tr '[:upper:]' '[:lower:]' | sed 's/^frappe_//') - - # If repo root has __init__.py or hooks.py, it IS the app - if [ -f "__init__.py" ] || [ -f "hooks.py" ]; then - echo "Repo root is the app: $APP_NAME" - else - # Try to find app directory - FOUND_APP=$(find . -maxdepth 2 -name "__init__.py" -type f | head -1 | xargs dirname | xargs basename) - if [ -n "$FOUND_APP" ] && [ "$FOUND_APP" != "." ]; then - APP_NAME="$FOUND_APP" - echo "Found app directory: $APP_NAME" - fi - fi - - if [ -z "$APP_NAME" ] || [ "$APP_NAME" = "." ]; then - echo "Error: Could not detect app name" - exit 1 - fi - - echo "name=$APP_NAME" >> $GITHUB_OUTPUT - echo "APP_PATH=$(pwd)" >> $GITHUB_OUTPUT - echo "Detected app name: $APP_NAME" - - - name: Initialize Frappe Bench - if: steps.check-app.outputs.exists == 'true' - run: | - bench init --skip-redis-config-generation --skip-assets --frappe-branch version-15 frappe-bench - cd frappe-bench - - - name: Get app into bench - if: steps.check-app.outputs.exists == 'true' - working-directory: frappe-bench - run: | - APP_NAME="${{ steps.app-name.outputs.name }}" - APP_PATH="${{ steps.app-name.outputs.APP_PATH }}" - - # Check if app is in repo root (most common case) - if [ -f "$APP_PATH/__init__.py" ] || [ -f "$APP_PATH/hooks.py" ]; then - echo "App is in repo root, using local path" - bench get-app $APP_NAME $APP_PATH - elif [ -d "$APP_PATH/$APP_NAME" ] && [ -f "$APP_PATH/$APP_NAME/__init__.py" ]; then - echo "App is in subdirectory" - bench get-app $APP_NAME $APP_PATH/$APP_NAME - else - echo "Error: Could not find app at expected location" - exit 1 - fi - - - name: Create test site - if: steps.check-app.outputs.exists == 'true' - working-directory: frappe-bench - env: - DB_HOST: 127.0.0.1 - DB_PORT: 3306 - DB_ROOT_USER: root - DB_ROOT_PASSWORD: root - REDIS_CACHE: redis://127.0.0.1:6379 - REDIS_QUEUE: redis://127.0.0.1:6379 - run: | - APP_NAME="${{ steps.app-name.outputs.name }}" - - # Create site with proper database connection - bench new-site test_site \ - --db-type mariadb \ - --admin-password admin \ - --no-mariadb-socket \ - --mariadb-host 127.0.0.1 \ - --mariadb-port 3306 \ - --mariadb-root-password root \ - --install-app $APP_NAME || { - echo "Site creation or app installation failed" - exit 1 - } - - - name: Run app-specific tests - if: steps.check-app.outputs.exists == 'true' - working-directory: frappe-bench - env: - DB_HOST: 127.0.0.1 - DB_PORT: 3306 - REDIS_CACHE: redis://127.0.0.1:6379 - REDIS_QUEUE: redis://127.0.0.1:6379 - run: | - APP_NAME="${{ steps.app-name.outputs.name }}" - echo "Running tests for app: $APP_NAME" - - # Run tests and fail if they fail - bench --site test_site run-tests --app $APP_NAME || { - echo "Tests failed for app $APP_NAME" - exit 1 - } - - - name: Upload test results - if: always() && steps.check-app.outputs.exists == 'true' - uses: actions/upload-artifact@v4 - with: - name: frappe-test-results - path: | - frappe-bench/sites/test_site/logs/*.log - if-no-files-found: ignore - - dependency-update: - name: 'Dependency Update Check' - runs-on: ubuntu-latest - # Run only on PR (to check if dependencies need updating before merge) - if: github.event_name == 'pull_request' && (github.base_ref == 'main' || github.base_ref == 'master') - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: "3.13" - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - if [ -f package-lock.json ] || [ -f npm-shrinkwrap.json ]; then npm ci; else echo "No npm lockfile, skipping npm ci"; fi - - - name: Check for outdated Python dependencies - run: | - pip list --outdated --format=freeze | grep -v '^\-e' | cut -d = -f 1 | xargs -n1 pip show || echo "No outdated Python packages found" - - - name: Check for outdated Node.js dependencies - run: | - if [ -f package.json ]; then npm outdated || echo "No outdated Node.js packages found"; else echo "No package.json, skipping npm outdated"; fi - - build: - name: 'Build Check' - runs-on: ubuntu-latest - needs: [test] - # Run on merge to main/master (push event) - if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/development') - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: "3.13" - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install build - if [ -f package-lock.json ] || [ -f npm-shrinkwrap.json ]; then npm ci; else echo "No npm lockfile, skipping npm ci"; fi - - - name: Build Python package - run: | - python -m build || echo "No Python package to build" - - - name: Build frontend assets - run: | - if [ -f package.json ]; then npm run build || echo "No build script found"; else echo "No package.json, skipping frontend build"; fi - - - name: Upload build artifacts - uses: actions/upload-artifact@v4 - with: - name: build-artifacts - path: | - dist/ - build/ - if-no-files-found: ignore \ No newline at end of file +name: CI + +on: + push: + branches: [ main, master ] + paths: + - '**.py' + - '**.js' + - '**.json' + - '**.cfg' + - '**.toml' + - '**.txt' + - '**.yml' + - '**.yaml' + pull_request: + branches: [ main, master, develop, development ] + paths: + - '**.py' + - '**.js' + - '**.json' + - '**.cfg' + - '**.toml' + - '**.txt' + - '**.yml' + - '**.yaml' + workflow_dispatch: + +permissions: + contents: read + +# Cancel previous runs on the same PR/branch (saves minutes on rapid pushes) +concurrency: + group: ci-${{ github.repository }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + frappe-bench-test: + name: 'Frappe Bench App Tests' + runs-on: ubuntu-latest + # Only on merge to main/master — not on PRs (too expensive) + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') + + services: + mariadb: + image: mariadb:10.11 + env: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: test_frappe + ports: + - 3306:3306 + options: >- + --health-cmd="mysqladmin ping -h localhost" + --health-interval=10s + --health-timeout=5s + --health-retries=5 + redis: + image: redis:7-alpine + ports: + - 6379:6379 + options: >- + --health-cmd="redis-cli ping" + --health-interval=10s + --health-timeout=5s + --health-retries=5 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Check if Frappe app exists + id: check-app + run: | + if [ -f "__init__.py" ] || [ -f "hooks.py" ]; then + echo "exists=true" >> $GITHUB_OUTPUT + else + echo "exists=false" >> $GITHUB_OUTPUT + echo "No Frappe app files found, skipping bench tests" + fi + + - name: Setup Python + if: steps.check-app.outputs.exists == 'true' + uses: actions/setup-python@v5 + with: + python-version: '3.12' + cache: pip + + - name: Setup Node.js + if: steps.check-app.outputs.exists == 'true' + uses: actions/setup-node@v4 + with: + node-version: 18 + + - name: Install system dependencies + if: steps.check-app.outputs.exists == 'true' + run: | + sudo apt-get update + sudo apt-get install -y \ + mariadb-client \ + redis-tools \ + curl \ + git \ + wget \ + xvfb \ + libfontconfig1 \ + libfreetype6 \ + libxrender1 \ + libjpeg-turbo8 \ + xfonts-75dpi \ + xfonts-base + + - name: Install Frappe Bench CLI + if: steps.check-app.outputs.exists == 'true' + run: pip install frappe-bench + + - name: Wait for MariaDB + if: steps.check-app.outputs.exists == 'true' + run: | + for i in {1..30}; do + if mysqladmin ping -h 127.0.0.1 -P 3306 -u root -proot --silent; then + echo "MariaDB is ready" + break + fi + echo "Waiting for MariaDB... ($i/30)" + sleep 2 + done + + - name: Wait for Redis + if: steps.check-app.outputs.exists == 'true' + run: | + for i in {1..30}; do + if redis-cli -h 127.0.0.1 -p 6379 ping | grep -q PONG; then + echo "Redis is ready" + break + fi + echo "Waiting for Redis... ($i/30)" + sleep 2 + done + + - name: Detect app name + if: steps.check-app.outputs.exists == 'true' + id: app-name + run: | + REPO_NAME=$(echo "${{ github.repository }}" | cut -d'/' -f2) + APP_NAME=$(echo "$REPO_NAME" | tr '-' '_' | tr '[:upper:]' '[:lower:]' | sed 's/^frappe_//') + + if [ -f "__init__.py" ] || [ -f "hooks.py" ]; then + echo "Repo root is the app: $APP_NAME" + else + FOUND_APP=$(find . -maxdepth 2 -name "__init__.py" -type f | head -1 | xargs dirname | xargs basename) + if [ -n "$FOUND_APP" ] && [ "$FOUND_APP" != "." ]; then + APP_NAME="$FOUND_APP" + fi + fi + + if [ -z "$APP_NAME" ] || [ "$APP_NAME" = "." ]; then + echo "Error: Could not detect app name" + exit 1 + fi + + echo "name=$APP_NAME" >> $GITHUB_OUTPUT + echo "APP_PATH=$(pwd)" >> $GITHUB_OUTPUT + echo "Detected app name: $APP_NAME" + + - name: Initialize Frappe Bench + if: steps.check-app.outputs.exists == 'true' + run: bench init --skip-redis-config-generation --skip-assets --frappe-branch version-15 frappe-bench + + - name: Get app into bench + if: steps.check-app.outputs.exists == 'true' + working-directory: frappe-bench + run: | + APP_NAME="${{ steps.app-name.outputs.name }}" + APP_PATH="${{ steps.app-name.outputs.APP_PATH }}" + + if [ -f "$APP_PATH/__init__.py" ] || [ -f "$APP_PATH/hooks.py" ]; then + bench get-app $APP_NAME $APP_PATH + elif [ -d "$APP_PATH/$APP_NAME" ] && [ -f "$APP_PATH/$APP_NAME/__init__.py" ]; then + bench get-app $APP_NAME $APP_PATH/$APP_NAME + else + echo "Error: Could not find app at expected location" + exit 1 + fi + + - name: Create test site + if: steps.check-app.outputs.exists == 'true' + working-directory: frappe-bench + env: + DB_HOST: 127.0.0.1 + DB_PORT: 3306 + DB_ROOT_USER: root + DB_ROOT_PASSWORD: root + REDIS_CACHE: redis://127.0.0.1:6379 + REDIS_QUEUE: redis://127.0.0.1:6379 + run: | + APP_NAME="${{ steps.app-name.outputs.name }}" + bench new-site test_site \ + --db-type mariadb \ + --admin-password admin \ + --no-mariadb-socket \ + --mariadb-host 127.0.0.1 \ + --mariadb-port 3306 \ + --mariadb-root-password root \ + --install-app $APP_NAME || { + echo "Site creation or app installation failed" + exit 1 + } + + - name: Run app-specific tests + if: steps.check-app.outputs.exists == 'true' + working-directory: frappe-bench + env: + DB_HOST: 127.0.0.1 + DB_PORT: 3306 + REDIS_CACHE: redis://127.0.0.1:6379 + REDIS_QUEUE: redis://127.0.0.1:6379 + run: | + APP_NAME="${{ steps.app-name.outputs.name }}" + bench --site test_site run-tests --app $APP_NAME || { + echo "Tests failed for app $APP_NAME" + exit 1 + } + + - name: Upload test results + if: always() && steps.check-app.outputs.exists == 'true' + uses: actions/upload-artifact@v4 + with: + name: frappe-test-results + path: frappe-bench/sites/test_site/logs/*.log + if-no-files-found: ignore + + build: + name: 'Build Check' + runs-on: ubuntu-latest + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install build + if [ -f package-lock.json ] || [ -f npm-shrinkwrap.json ]; then npm ci; else echo "No npm lockfile, skipping npm ci"; fi + + - name: Build Python package + run: python -m build || echo "No Python package to build" + + - name: Build frontend assets + run: | + if [ -f package.json ]; then npm run build || echo "No build script found"; else echo "No package.json, skipping frontend build"; fi + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: build-artifacts + path: | + dist/ + build/ + if-no-files-found: ignore diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 2967390..0f2dac9 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -1,148 +1,108 @@ -name: Quality Checks - -on: - # Only run on PRs - no push trigger to avoid duplicates - pull_request: - branches: [ main, master, develop, development ] - types: [opened, synchronize, reopened, ready_for_review] - workflow_dispatch: - -permissions: - contents: read - -jobs: - commit-lint: - name: 'Semantic Commits' - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 200 - - uses: actions/setup-node@v4 - with: - node-version: 22 - check-latest: true - - - name: Check commit titles - run: | - npm install @commitlint/cli @commitlint/config-conventional - - # Verify config file exists - if [ ! -f commitlint.config.js ]; then - echo "Error: commitlint.config.js not found" - exit 1 - fi - - echo "=== Commitlint config file ===" - cat commitlint.config.js - echo "" - - # Check if packages are installed - echo "=== Installed packages ===" - npm list @commitlint/cli @commitlint/config-conventional || true - echo "" - - # Test commitlint config loading - echo "=== Testing commitlint config ===" - npx commitlint --print-config || echo "Config test failed" - echo "" - - # Show commits to be checked - echo "=== Commits to check ===" - git log --oneline ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }} || echo "No commits found" - echo "" - - # Run commitlint (it will auto-detect commitlint.config.js in root) - echo "=== Running commitlint ===" - npx commitlint --verbose --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} - - docs-required: - name: 'Documentation Required' - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' || (github.event_name == 'push' && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/development')) - - steps: - - name: 'Setup Environment' - uses: actions/setup-python@v5 - with: - python-version: '3.13' - - uses: actions/checkout@v4 - - - name: Validate Docs - env: - PR_NUMBER: ${{ github.event.number }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_REPOSITORY: ${{ github.repository }} - run: | - pip install requests --quiet - - # Check if PR number is valid - if [ -z "$PR_NUMBER" ] || [ "$PR_NUMBER" = "null" ]; then - echo "No valid PR number found. Skipping documentation check. ✅" - exit 0 - fi - - # Run documentation checker with error handling - python $GITHUB_WORKSPACE/.github/helper/documentation.py $PR_NUMBER || { - echo "Documentation checker failed, but continuing workflow. ⚠️" - exit 0 - } - - linter: - name: 'Semgrep Rules' - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' || (github.event_name == 'push' && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/development')) - - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: '3.13' - cache: pip - - - name: Download Semgrep rules - run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules - - - name: Run Semgrep rules - run: | - pip install semgrep - semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness - - precommit: - name: 'Pre-Commit' - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' || (github.event_name == 'push' && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/development')) - - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: '3.13' - cache: pip - - - name: Check for pre-commit config - run: | - if [ ! -f .pre-commit-config.yaml ]; then - echo "Pre-commit config not found. Downloading..." - curl -o .pre-commit-config.yaml https://raw.githubusercontent.com/dhwani-ris/frappe-pre-commit/main/examples/.pre-commit-config.yaml - else - echo "Pre-commit config already exists." - fi - - - name: Install pre-commit and dependencies - run: | - pip install pre-commit - pre-commit install --install-hooks - - - name: Run pre-commit (check only, no auto-fix) - run: | - # Run pre-commit in CI mode - it will check but not modify files - # Skip no-commit-to-branch hook in CI (it's meant for local development) - # This will fail if files need formatting, which is what we want - SKIP=no-commit-to-branch pre-commit run --all-files --show-diff-on-failure || { - echo "❌ Pre-commit checks failed. Some files need formatting or have issues." - echo "Please run 'pre-commit run --all-files' locally to fix the issues." - exit 1 - } \ No newline at end of file +name: Quality Checks + +on: + pull_request: + branches: [ main, master, develop, development ] + types: [opened, synchronize, reopened, ready_for_review] + paths: + - '**.py' + - '**.js' + - '**.css' + - '**.html' + - '**.json' + - '.pre-commit-config.yaml' + - 'commitlint.config.js' + workflow_dispatch: + +permissions: + contents: read + +# Cancel previous quality check runs on the same PR +concurrency: + group: quality-${{ github.repository }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + commit-lint: + name: 'Semantic Commits' + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 200 + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Check commit titles + run: | + npm install @commitlint/cli @commitlint/config-conventional + npx commitlint --verbose --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} + + linter: + name: 'Semgrep Rules' + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.13' + cache: pip + + - name: Cache Semgrep rules + uses: actions/cache@v4 + with: + path: frappe-semgrep-rules + key: semgrep-rules-${{ github.run_id }} + restore-keys: semgrep-rules- + + - name: Download Semgrep rules + run: | + if [ -d "frappe-semgrep-rules" ]; then + cd frappe-semgrep-rules && git pull --ff-only || true + else + git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules + fi + + - name: Run Semgrep rules + run: | + pip install semgrep + semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness + + precommit: + name: 'Pre-Commit' + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.13' + cache: pip + + - name: Check for pre-commit config + run: | + if [ ! -f .pre-commit-config.yaml ]; then + curl -s -o .pre-commit-config.yaml https://raw.githubusercontent.com/dhwani-ris/frappe-pre-commit/main/examples/.pre-commit-config.yaml + fi + + - name: Cache pre-commit hooks + uses: actions/cache@v4 + with: + path: ~/.cache/pre-commit + key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + restore-keys: pre-commit- + + - name: Install and run pre-commit + run: | + pip install pre-commit + pre-commit install --install-hooks + SKIP=no-commit-to-branch pre-commit run --all-files --show-diff-on-failure || { + echo "❌ Pre-commit checks failed. Run 'pre-commit run --all-files' locally." + exit 1 + } diff --git a/.github/workflows/devops-checklist.yml b/.github/workflows/devops-checklist.yml new file mode 100644 index 0000000..383e529 --- /dev/null +++ b/.github/workflows/devops-checklist.yml @@ -0,0 +1,386 @@ +name: DevOps Checklist Reminder + +on: + pull_request: + # Only on open/reopen — not on synchronize (saves a full run per push to PR) + types: [opened, reopened] + branches: [ main, master ] + +permissions: + pull-requests: write + contents: read + +jobs: + checklist-reminder: + name: Add DevOps Checklist Reminder + runs-on: ubuntu-latest + if: | + github.event.pull_request.base.ref == 'main' || + github.event.pull_request.base.ref == 'master' + + steps: + - name: Check if deployment notes comment exists + id: check-deployment-notes + uses: actions/github-script@v8 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const pr = context.payload.pull_request; + const comments = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + }); + + const botComment = comments.data.find( + comment => comment.user.type === 'Bot' && + comment.body.includes('Production Deployment Release Document') + ); + + return { exists: !!botComment, commentId: botComment?.id }; + + - name: Generate Deployment Notes automatically (Comment 1) + if: steps.check-deployment-notes.outputs.exists != 'true' || github.event.action == 'opened' + uses: actions/github-script@v8 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const pr = context.payload.pull_request; + + // Get all commits in the PR using GitHub API + let commits = []; + try { + const { data: prCommits } = await github.rest.pulls.listCommits({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr.number + }); + + commits = prCommits.map(commit => ({ + hash: commit.sha.substring(0, 7), + message: commit.commit.message.split('\n')[0], + author: commit.commit.author.name, + date: commit.commit.author.date.split('T')[0] + })); + } catch (e) { + console.log('Error getting commits:', e.message); + } + + // Determine release type from commits + let releaseType = 'Patch'; + const hasBreaking = commits.some(c => + c.message.includes('BREAKING') || + c.message.includes('BREAKING CHANGE') || + c.message.startsWith('feat!') || + c.message.match(/^feat\(.+\)!:/) + ); + const hasFeature = commits.some(c => + c.message.startsWith('feat') && !c.message.startsWith('feat!') + ); + const hasFix = commits.some(c => c.message.startsWith('fix')); + + if (hasBreaking) { + releaseType = 'Major'; + } else if (hasFeature) { + releaseType = 'Minor'; + } else if (hasFix) { + releaseType = 'Patch'; + } + + // Get version from latest release or estimate from release type + let version = 'TBA'; + try { + const { data: releases } = await github.rest.repos.listReleases({ + owner: context.repo.owner, + repo: context.repo.repo, + per_page: 1 + }); + + if (releases.length > 0) { + const latestRelease = releases[0]; + const versionMatch = latestRelease.tag_name.match(/v?(\d+\.\d+\.\d+)/); + if (versionMatch) { + const [major, minor, patch] = versionMatch[1].split('.').map(Number); + if (releaseType === 'Major') { + version = `${major + 1}.0.0`; + } else if (releaseType === 'Minor') { + version = `${major}.${minor + 1}.0`; + } else { + version = `${major}.${minor}.${patch + 1}`; + } + } + } else { + // No releases yet, start with 1.0.0 + version = releaseType === 'Major' ? '1.0.0' : releaseType === 'Minor' ? '0.1.0' : '0.0.1'; + } + } catch (e) { + console.log('Could not determine version:', e.message); + } + + // Get reviewers + let reviewers = []; + try { + const { data: reviews } = await github.rest.pulls.listReviews({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr.number + }); + const approvedReviewers = reviews + .filter(r => r.state === 'APPROVED') + .map(r => r.user.login) + .filter((v, i, a) => a.indexOf(v) === i); // unique + reviewers = approvedReviewers; + } catch (e) { + console.log('Could not get reviewers:', e.message); + } + + // Group commits by type for feature details + const features = commits.filter(c => c.message.startsWith('feat')).map(c => c.message.replace(/^feat(\(.+?\))?:\s*/i, '')); + const fixes = commits.filter(c => c.message.startsWith('fix')).map(c => c.message.replace(/^fix(\(.+?\))?:\s*/i, '')); + const other = commits.filter(c => !c.message.startsWith('feat') && !c.message.startsWith('fix') && !c.message.startsWith('chore') && !c.message.startsWith('ci')); + + // Build feature details + let featureDetails = []; + if (features.length > 0) { + featureDetails.push(...features.map(f => `1) ${f}`)); + } + if (fixes.length > 0) { + featureDetails.push(...fixes.map(f => `2) ${f}`)); + } + if (other.length > 0) { + featureDetails.push(...other.slice(0, 5).map((o, i) => `${i + 3}) ${o.message}`)); + } + + const today = new Date().toISOString().split('T')[0]; + const releaseDate = today.split('-').reverse().join('-'); // Format: DD-MM-YYYY + + // Format feature details better + const formatFeatureDetails = (details) => { + if (details.length === 0) return 'See commits above'; + return details.map((f, i) => `${i + 1}) ${f}`).join('
'); + }; + + const deploymentNotes = `## 📝 Production Deployment Release Document + + **Release Information:** + - **Version:** \`${version}\` + - **Release Date:** \`${releaseDate}\` + - **Prepared by:** \`${pr.user.login}\` + - **Approved by:** \`${reviewers.length > 0 ? reviewers.join(', ') : 'Pending'}\` + - **Release Type:** \`${releaseType}\` + + **Deployment Branches:** + \`\`\` + ${pr.base.ref} + \`\`\` + + **Overview:** + - Here are the key feature details for this release: + + **Repository Details:** + + | S.No. | Repository Name | Release Number | Feature Details | + |-------|-----------------|----------------|-----------------| + | 1. | \`${context.repo.repo}\` | \`${pr.base.ref}-release-${version}\` | ${formatFeatureDetails(featureDetails)} | + + **Dependencies:** + - Dependencies updated: \`TBD\` *(Please review and update)* + \`\`\` + + \`\`\` + + **Database Changes (Queries to run):** + - Database changes required: \`TBD\` *(Please review and update)* + \`\`\` + + \`\`\` + + **Testing:** + - [ ] Unit tests passed + - [ ] Integration tests passed + - [ ] E2E tests passed + - [ ] Manual testing completed + \`\`\` + + \`\`\` + + **Known Issues:** + - Known issues: \`TBD\` *(Please review and update)* + \`\`\` + + \`\`\` + + **Contact Information:** + - Support Team Email: \`\`\`\`\`\` + - Support Team Phone: \`\`\`\`\`\` + + **Attachments:** + - Deployment files attached/committed: \`TBD\` *(Please review and update)* + \`\`\` + + \`\`\` + + --- + + ### For DevOps Team Use Only + *(To be filled by the DevOps team after deploying the release)* + + **Deployment Details:** + - Date and time of deployment: \`\`\`\`\`\` + - Deployed by: \`\`\`\`\`\` + - Deployment Status: \`\`\`\`\`\` + + **Deployment Instructions:** + - [ ] Pre-deployment tasks completed (backups, etc.) + - [ ] Production environment accessed securely + - [ ] Latest release pulled from version control + - [ ] Dependencies installed/updated + - [ ] Database migrations run (if applicable) + - [ ] Application services restarted + - [ ] Deployment monitored and verified + + **Rollback Plan:** + - [ ] Rollback procedure documented + - [ ] Previous version tag identified: \`\`\`\`\`\` + - [ ] Database rollback scripts prepared (if applicable) + - [ ] Rollback tested in staging environment + + **Post-Deployment Checklist:** + - [ ] Service availability and response times verified + - [ ] System resources monitored + - [ ] Critical user scenarios tested + - [ ] Data integrity confirmed + - [ ] Error logs reviewed + - [ ] Security scans completed + - [ ] Server and infrastructure health checked + - [ ] Backup and disaster recovery procedures validated + + **Notes:** + \`\`\` + + \`\`\` + + **Acknowledgment:** + - [ ] Deployment acknowledged and system ready for production use + + --- + **Note:** This deployment document was **automatically generated** from PR commits and information. Please review and update the TBD sections before merging.`; + + // Check if comment already exists + const comments = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + }); + + const existingComment = comments.data.find( + comment => comment.user.type === 'Bot' && + comment.body.includes('Production Deployment Release Document') + ); + + if (existingComment) { + // Update existing comment + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existingComment.id, + body: deploymentNotes + }); + } else { + // Create new comment + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + body: deploymentNotes + }); + } + + - name: Check if checklist comment exists + id: check-comment + uses: actions/github-script@v8 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const pr = context.payload.pull_request; + const comments = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + }); + + const botComment = comments.data.find( + comment => comment.user.type === 'Bot' && + comment.body.includes('DevOps Checklist - Workflow Review') + ); + + return { exists: !!botComment, commentId: botComment?.id }; + + - name: Add or update DevOps checklist reminder (Comment 2) + uses: actions/github-script@v8 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const pr = context.payload.pull_request; + const checklist = `## 🔧 DevOps Checklist - Workflow Review + + **Please review all workflows and checks before merging:** + + ### Workflow Status Review + - [ ] All CI/CD workflows are passing + - [ ] Quality Checks workflow passed + - [ ] Security Scan workflow passed + - [ ] Code quality checks passed + - [ ] Test coverage meets requirements + + ### Review Status + - [ ] All required reviewers have approved + - [ ] Code review completed + - [ ] Security review completed (if applicable) + + ### Pre-Merge Verification + - [ ] Deployment Notes document reviewed (see Deployment Notes comment above) + - [ ] All commits reviewed + - [ ] Breaking changes identified (if any) + - [ ] Version number verified (if applicable) + + ### Final Checks + - [ ] No blocking issues or errors + - [ ] Ready for production deployment + - [ ] Rollback plan understood (if high-risk) + + --- + **Note:** This checklist is for DevOps team to verify all workflows and checks before merging.`; + + // Check if comment already exists + const comments = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + }); + + const existingComment = comments.data.find( + comment => comment.user.type === 'Bot' && + comment.body.includes('DevOps Checklist - Workflow Review') + ); + + if (existingComment) { + // Update existing comment + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existingComment.id, + body: checklist + }); + console.log('Updated existing DevOps Checklist comment'); + } else { + // Create new comment + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + body: checklist + }); + console.log('Created new DevOps Checklist comment'); + } + diff --git a/.github/workflows/notifications.yml b/.github/workflows/notifications.yml new file mode 100644 index 0000000..bcfce93 --- /dev/null +++ b/.github/workflows/notifications.yml @@ -0,0 +1,127 @@ +name: Workflow Notifications + +on: + workflow_run: + workflows: + - "CI" + - "Generate Semantic Release" + types: [completed] + workflow_dispatch: + +permissions: + contents: read + issues: write + pull-requests: write + +jobs: + notify-on-failure: + name: Notify on Critical Failures + runs-on: ubuntu-latest + if: | + (github.event.workflow_run.head_branch == 'main' || github.event.workflow_run.head_branch == 'master') && + github.event.workflow_run.conclusion == 'failure' + + steps: + - name: Get workflow run details and notify + uses: actions/github-script@v8 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const run = context.payload.workflow_run; + const workflowName = run.name; + const workflowUrl = run.html_url; + const commitSha = run.head_sha.substring(0, 7); + const branch = run.head_branch; + const actor = run.actor.login; + + // Get commit message + let commitMessage = 'N/A'; + try { + const { data: commit } = await github.rest.repos.getCommit({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: run.head_sha + }); + commitMessage = commit.commit.message.split('\n')[0]; + } catch (e) { + console.log('Could not fetch commit details:', e.message); + } + + const title = `🚨 Workflow Failure: ${workflowName}`; + const body = `## Workflow Failure Alert + + **Workflow:** ${workflowName} + **Branch:** \`${branch}\` + **Commit:** ${commitSha} — ${commitMessage} + **Triggered by:** @${actor} + + [View failed workflow run](${workflowUrl}) + + --- + *Auto-generated by workflow notification system.*`; + + // Check for existing open issue + const { data: issues } = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + labels: 'workflow-failure' + }); + + const existing = issues.find(i => + i.title.includes(workflowName) && i.body.includes(commitSha) + ); + + if (!existing) { + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: title, + body: body, + labels: ['workflow-failure', 'bug'] + }); + } + + - name: Send Slack notification (optional) + if: env.SLACK_WEBHOOK_URL != '' + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + run: | + curl -s -X POST -H 'Content-type: application/json' \ + --data "{\"text\":\"🚨 ${GITHUB_REPOSITORY}: ${{ github.event.workflow_run.name }} failed on ${{ github.event.workflow_run.head_branch }}. ${{ github.event.workflow_run.html_url }}\"}" \ + $SLACK_WEBHOOK_URL || true + + notify-on-success: + name: Notify on Release Success + runs-on: ubuntu-latest + if: | + (github.event.workflow_run.head_branch == 'main' || github.event.workflow_run.head_branch == 'master') && + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.name == 'Generate Semantic Release' + + steps: + - name: Check for new release + uses: actions/github-script@v8 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { data: releases } = await github.rest.repos.listReleases({ + owner: context.repo.owner, + repo: context.repo.repo, + per_page: 1 + }); + + if (releases.length === 0) { + console.log('No releases found'); + return; + } + + const latest = releases[0]; + const diffMinutes = (new Date() - new Date(latest.created_at)) / (1000 * 60); + + if (diffMinutes > 10) { + console.log('Release older than 10 minutes, skipping'); + return; + } + + console.log(`New release: ${latest.tag_name}`); diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml index 82e654b..61e3b00 100644 --- a/.github/workflows/pr-labeler.yml +++ b/.github/workflows/pr-labeler.yml @@ -1,124 +1,127 @@ -# .github/workflows/pr-labeler.yml -name: "PR Labeler" - -on: - pull_request: - types: [opened, reopened] - branches: [main, master] # Label PRs to these branches - -permissions: - contents: read - pull-requests: write - -jobs: - label: - runs-on: ubuntu-latest - # Only run for PRs targeting main or master branches - if: contains(fromJson('["main", "master"]'), github.event.pull_request.base.ref) - steps: - - name: Auto Label PR - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { data: pr } = await github.rest.pulls.get({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: context.payload.pull_request.number, - }); - - const title = pr.title.toLowerCase(); - const targetBranch = pr.base.ref; - const labels = []; - - console.log(`Labeling PR #${pr.number} targeting ${targetBranch} branch`); - - // Skip release note for maintenance tasks - const skipPrefixes = ['chore', 'ci', 'style', 'test', 'refactor']; - const hasSkipPrefix = skipPrefixes.some(prefix => - title.startsWith(prefix + ':') || title.startsWith(prefix + '(') - ); - - if (hasSkipPrefix) { - labels.push('skip-release-notes'); - } - - // Type detection from title - if (title.includes('feat') || title.includes('feature')) { - labels.push('feature'); - } else if (title.includes('fix') || title.includes('bug')) { - labels.push('bug'); - } else if (title.includes('docs') || title.includes('doc')) { - labels.push('docs'); - } else if (title.includes('refactor')) { - labels.push('refactor'); - } - - // Add branch-specific labels - if (targetBranch === 'main' || targetBranch === 'master') { - labels.push('release'); - console.log('🚀 Production release PR'); - } - - // Add labels with error handling - if (labels.length > 0) { - try { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.payload.pull_request.number, - labels: labels - }); - - console.log(`✅ Added labels: ${labels.join(', ')}`); - } catch (error) { - if (error.status === 422) { - // Handle missing labels by creating them first - console.log('Some labels don\'t exist, creating them...'); - - const labelConfigs = { - 'skip-release-notes': { color: '6c757d', description: 'Skip in release notes' }, - 'uat-release': { color: 'fd7e14', description: 'UAT release PR' }, - 'released': { color: 'e83e8c', description: 'Production release PR' }, - 'development': { color: '17a2b8', description: 'Development branch PR' }, - 'feature': { color: '0e8a16', description: 'New feature' }, - 'bug': { color: 'd73a4a', description: 'Bug fix' }, - 'docs': { color: '0052cc', description: 'Documentation' }, - 'refactor': { color: 'fbca04', description: 'Code refactoring' } - }; - - // Create missing labels - for (const label of labels) { - if (labelConfigs[label]) { - try { - await github.rest.issues.createLabel({ - owner: context.repo.owner, - repo: context.repo.repo, - name: label, - color: labelConfigs[label].color, - description: labelConfigs[label].description - }); - console.log(`🆕 Created label: ${label}`); - } catch (createError) { - console.log(`⚠️ Could not create label ${label}: ${createError.message}`); - } - } - } - - // Try adding labels again - try { - await github.rest.issues.addLabels({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.payload.pull_request.number, - labels: labels - }); - console.log(`✅ Added labels after creation: ${labels.join(', ')}`); - } catch (retryError) { - console.log(`⚠️ Still couldn't add labels: ${retryError.message}`); - } - } else { - console.log(`⚠️ Error adding labels: ${error.message}`); - } - } - } \ No newline at end of file +name: "PR Setup" + +on: + pull_request: + types: [opened, reopened, ready_for_review] + branches: [main, master] + +permissions: + contents: read + pull-requests: write + +jobs: + pr-setup: + name: Label & Assign Reviewer + runs-on: ubuntu-latest + if: contains(fromJson('["main", "master"]'), github.event.pull_request.base.ref) + steps: + - name: Auto Label and Request Review + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const pr = context.payload.pull_request; + const title = pr.title.toLowerCase(); + const labels = []; + + // --- Label based on title --- + const skipPrefixes = ['chore', 'ci', 'style', 'test', 'refactor']; + if (skipPrefixes.some(p => title.startsWith(p + ':') || title.startsWith(p + '('))) { + labels.push('skip-release-notes'); + } + + if (title.includes('feat') || title.includes('feature')) { + labels.push('feature'); + } else if (title.includes('fix') || title.includes('bug')) { + labels.push('bug'); + } else if (title.includes('docs') || title.includes('doc')) { + labels.push('docs'); + } else if (title.includes('refactor')) { + labels.push('refactor'); + } + + labels.push('release'); + + // Apply labels + if (labels.length > 0) { + try { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + labels: labels + }); + console.log(`Added labels: ${labels.join(', ')}`); + } catch (error) { + if (error.status === 422) { + const labelConfigs = { + 'skip-release-notes': { color: '6c757d', description: 'Skip in release notes' }, + 'feature': { color: '0e8a16', description: 'New feature' }, + 'bug': { color: 'd73a4a', description: 'Bug fix' }, + 'docs': { color: '0052cc', description: 'Documentation' }, + 'refactor': { color: 'fbca04', description: 'Code refactoring' }, + 'release': { color: 'e83e8c', description: 'Production release PR' } + }; + for (const label of labels) { + if (labelConfigs[label]) { + try { + await github.rest.issues.createLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: label, + color: labelConfigs[label].color, + description: labelConfigs[label].description + }); + } catch (e) { /* label may already exist */ } + } + } + // Retry + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + labels: labels + }).catch(e => console.log(`Label retry failed: ${e.message}`)); + } + } + } + + // --- Request review --- + if (pr.draft) { + console.log('PR is draft, skipping review request'); + return; + } + + const reviewer = 'dhwani-ankit'; + try { + const { data: currentReviews } = await github.rest.pulls.listRequestedReviewers({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr.number, + }); + + const alreadyRequested = currentReviews.users?.some( + u => u.login.toLowerCase() === reviewer.toLowerCase() + ); + + if (!alreadyRequested) { + await github.rest.pulls.requestReviewers({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr.number, + reviewers: [reviewer], + }); + console.log(`Requested review from ${reviewer}`); + } + } catch (error) { + console.log(`Could not request review: ${error.message}`); + // Fallback: add as assignee + try { + await github.rest.issues.addAssignees({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + assignees: [reviewer], + }); + } catch (e) { /* ignore */ } + } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index afee259..c61b8e1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,127 +1,81 @@ -name: Generate Semantic Release -on: - push: - branches: [ main, master ] - workflow_dispatch: - inputs: - force_release: - description: 'Force release even if no changes detected' - required: false - default: 'false' - type: boolean - -permissions: - contents: write - issues: write - pull-requests: write - id-token: write - actions: read - -# Wait for init workflow to complete first -# Use same concurrency group as init workflow to ensure init runs first -concurrency: - group: repo-init-${{ github.repository }}-${{ github.ref }} - cancel-in-progress: false - -jobs: - release: - name: Release - runs-on: ubuntu-latest - steps: - - name: Generate GitHub App Token - id: generate-token - uses: tibdex/github-app-token@v1 - with: - app_id: ${{ secrets.DHWANI_RELEASE_BOT_APP_ID }} - private_key: ${{ secrets.DHWANI_RELEASE_BOT_PRIVATE_KEY }} - - - name: Checkout Entire Repository - uses: actions/checkout@v5 - with: - fetch-depth: 0 - token: ${{ steps.generate-token.outputs.token }} - persist-credentials: true - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - - name: Setup dependencies - run: | - npm install semantic-release @semantic-release/git @semantic-release/exec @semantic-release/github @semantic-release/changelog @semantic-release/commit-analyzer @semantic-release/release-notes-generator --no-save - - - name: Configure Git - run: | - git config --global user.name "dhwani-release-bot" - git config --global user.email "dhwani-release-bot[bot]@users.noreply.github.com" - - - name: Debug - Check commits - run: | - echo "=== Recent commits ===" - git log --oneline -10 - echo "" - echo "=== Commits since last tag ===" - LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") - if [ -z "$LAST_TAG" ]; then - echo "No previous tag, showing all commits" - git log --oneline --no-merges - else - echo "Commits since $LAST_TAG:" - git log --oneline --no-merges ${LAST_TAG}..HEAD - fi - echo "" - echo "=== Last tag ===" - git describe --tags --abbrev=0 2>/dev/null || echo "No tags found" - echo "" - echo "=== Semantic commits only ===" - git log --oneline --no-merges --grep="^feat\|^fix\|^perf\|^revert" -10 || echo "No semantic commits found" - - - name: Filter merge commits - run: | - echo "=== Filtering merge commits ===" - LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") - if [ -z "$LAST_TAG" ]; then - echo "No previous tag, checking all commits" - git log --oneline --no-merges --format="%H %s" > /tmp/filtered_commits.txt - else - echo "Checking commits since $LAST_TAG" - git log --oneline --no-merges ${LAST_TAG}..HEAD --format="%H %s" > /tmp/filtered_commits.txt - fi - echo "Filtered commits (excluding merge commits):" - cat /tmp/filtered_commits.txt || echo "No commits found" - - - name: Create Release - env: - GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }} - GIT_AUTHOR_NAME: "dhwani-release-bot" - GIT_AUTHOR_EMAIL: "dhwani-release-bot[bot]@users.noreply.github.com" - GIT_COMMITTER_NAME: "dhwani-release-bot" - GIT_COMMITTER_EMAIL: "dhwani-release-bot[bot]@users.noreply.github.com" - NODE_ENV: production - run: | - echo "Running semantic-release..." - npx semantic-release --debug || { - echo "Semantic-release exited with code $?" - echo "This might be normal if no release is needed" - exit 0 - } - - - name: Verify Release Created - if: always() - run: | - echo "=== Checking for new releases ===" - LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "none") - echo "Latest tag: $LATEST_TAG" - - if [ "$LATEST_TAG" != "none" ]; then - echo "✅ Release tag found: $LATEST_TAG" - git show-ref --tags | tail -5 - else - echo "ℹ️ No new release tag created (this is normal if no semantic commits found)" - fi \ No newline at end of file +name: Generate Semantic Release +on: + push: + branches: [ main, master ] + paths: + - '**.py' + - '**.js' + - '**.json' + - '**.toml' + - '**.cfg' + workflow_dispatch: + inputs: + force_release: + description: 'Force release even if no changes detected' + required: false + default: 'false' + type: boolean + +permissions: + contents: write + issues: write + pull-requests: write + id-token: write + actions: read + +# Wait for init workflow to complete first +concurrency: + group: repo-init-${{ github.repository }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + release: + name: Release + runs-on: ubuntu-latest + steps: + - name: Generate GitHub App Token + id: generate-token + uses: tibdex/github-app-token@v1 + with: + app_id: ${{ secrets.DHWANI_RELEASE_BOT_APP_ID }} + private_key: ${{ secrets.DHWANI_RELEASE_BOT_PRIVATE_KEY }} + + - name: Checkout Entire Repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ steps.generate-token.outputs.token }} + persist-credentials: true + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Setup dependencies + run: | + npm install semantic-release @semantic-release/git @semantic-release/exec @semantic-release/github @semantic-release/changelog @semantic-release/commit-analyzer @semantic-release/release-notes-generator --no-save + + - name: Configure Git + run: | + git config --global user.name "dhwani-release-bot" + git config --global user.email "dhwani-release-bot[bot]@users.noreply.github.com" + + - name: Create Release + env: + GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }} + GIT_AUTHOR_NAME: "dhwani-release-bot" + GIT_AUTHOR_EMAIL: "dhwani-release-bot[bot]@users.noreply.github.com" + GIT_COMMITTER_NAME: "dhwani-release-bot" + GIT_COMMITTER_EMAIL: "dhwani-release-bot[bot]@users.noreply.github.com" + NODE_ENV: production + run: | + npx semantic-release || { + echo "No release needed or semantic-release exited with code $?" + exit 0 + } diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index c666cf2..d87ad19 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -1,205 +1,137 @@ -name: Enhanced Security Scan - -on: - # Only run on push to main/master (after merge) - not on PRs to avoid duplicates - push: - branches: [ main, master ] - schedule: - # Run daily security scans on main/master - - cron: '0 2 * * *' - workflow_dispatch: - -permissions: - contents: read - security-events: write - actions: read - -jobs: - codeql-analysis: - name: CodeQL Security Analysis - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: ['python', 'javascript'] - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - with: - languages: ${{ matrix.language }} - queries: security-extended,security-and-quality - - - name: Autobuild - uses: github/codeql-action/autobuild@v3 - - - name: Perform CodeQL Analysis - id: codeql-analysis - uses: github/codeql-action/analyze@v3 - continue-on-error: true - with: - category: "/language:${{ matrix.language }}" - - - name: Check CodeQL upload status - if: always() && steps.codeql-analysis.outcome == 'failure' - run: | - echo "⚠️ CodeQL analysis completed but upload failed." - echo "This is expected if Code Security is not enabled for this repository." - echo "To enable Code Security, go to: Settings > Code security and analysis > Code scanning" - echo "The analysis results are still available in the workflow artifacts." - - dependency-scanning: - name: Dependency Vulnerability Scan - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: '3.13' - - - name: Run pip-audit - run: | - pip install pip-audit - pip-audit --desc --format json --output pip-audit-report.json || true - - - name: Run Safety Check - run: | - pip install safety - safety check --json --output safety-report.json || true - - - name: Run Bandit Security Scan - run: | - pip install bandit[toml] - bandit -r . -f json -o bandit-report.json || true - - - name: Upload security reports - uses: actions/upload-artifact@v4 - with: - name: security-reports-${{ github.run_id }} - path: | - pip-audit-report.json - safety-report.json - bandit-report.json - retention-days: 30 - - - name: Comment PR with findings - if: github.event_name == 'pull_request' - uses: actions/github-script@v8 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const fs = require('fs'); - const path = require('path'); - - let comment = '## 🔒 Security Scan Results\n\n'; - let hasIssues = false; - - // Check pip-audit - try { - if (fs.existsSync('pip-audit-report.json')) { - const report = JSON.parse(fs.readFileSync('pip-audit-report.json', 'utf8')); - if (report.vulnerabilities && report.vulnerabilities.length > 0) { - hasIssues = true; - comment += `### ⚠️ Dependency Vulnerabilities Found\n\n`; - comment += `Found ${report.vulnerabilities.length} vulnerable dependency(ies).\n\n`; - report.vulnerabilities.slice(0, 10).forEach(vuln => { - comment += `- **${vuln.name}**: ${vuln.id} - ${vuln.fix_versions ? `Fix: ${vuln.fix_versions.join(', ')}` : 'No fix available'}\n`; - }); - comment += `\n`; - } - } - } catch (e) { - console.log('Error reading pip-audit report:', e); - } - - // Check safety - try { - if (fs.existsSync('safety-report.json')) { - const report = JSON.parse(fs.readFileSync('safety-report.json', 'utf8')); - if (report.vulnerabilities && report.vulnerabilities.length > 0) { - hasIssues = true; - comment += `### ⚠️ Safety Check Findings\n\n`; - comment += `Found ${report.vulnerabilities.length} issue(s).\n\n`; - } - } - } catch (e) { - console.log('Error reading safety report:', e); - } - - // Check bandit - try { - if (fs.existsSync('bandit-report.json')) { - const report = JSON.parse(fs.readFileSync('bandit-report.json', 'utf8')); - if (report.metrics && report.metrics._totals) { - const totals = report.metrics._totals; - if (totals.HIGH > 0 || totals.MEDIUM > 0) { - hasIssues = true; - comment += `### ⚠️ Code Security Issues (Bandit)\n\n`; - comment += `- High: ${totals.HIGH}\n`; - comment += `- Medium: ${totals.MEDIUM}\n`; - comment += `- Low: ${totals.LOW}\n\n`; - } - } - } - } catch (e) { - console.log('Error reading bandit report:', e); - } - - if (!hasIssues) { - comment += '✅ No security issues detected in this scan.\n'; - } else { - comment += '\n📋 Full reports are available in the workflow artifacts.\n'; - } - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: comment - }); - - security-summary: - name: Security Summary - runs-on: ubuntu-latest - needs: [codeql-analysis, dependency-scanning] - if: always() - - steps: - - name: Generate Security Summary - uses: actions/github-script@v8 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const needs = ${{ toJSON(needs) }}; - const codeqlResult = needs['codeql-analysis']?.result || 'unknown'; - const dependencyResult = needs['dependency-scanning']?.result || 'unknown'; - - const getStatus = (result) => { - if (result === 'success') return '✅ Passed'; - if (result === 'failure') return '❌ Failed'; - return '⚠️ Skipped'; - }; - - const summary = `## 🔒 Security Scan Summary - - | Scan Type | Status | - |-----------|--------| - | CodeQL Analysis | ${getStatus(codeqlResult)} | - | Dependency Scan | ${getStatus(dependencyResult)} | - - **View detailed results in the Security tab or workflow artifacts.**`; - - core.summary.addRaw(summary).write(); - +name: Enhanced Security Scan + +on: + push: + branches: [ main, master ] + paths: + - '**.py' + - '**.js' + - '**/requirements*.txt' + - '**/pyproject.toml' + - '**/setup.py' + - '**/setup.cfg' + - '**/package.json' + - '**/package-lock.json' + schedule: + # Weekly on Monday at 2 AM UTC (was daily — saves ~85% scheduled minutes) + - cron: '0 2 * * 1' + workflow_dispatch: + +permissions: + contents: read + security-events: write + actions: read + +# Cancel in-progress runs for the same branch +concurrency: + group: security-${{ github.repository }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + codeql-analysis: + name: CodeQL Security Analysis + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + # Single language — Python is the primary language in Frappe apps. + # JS/TS CodeQL rarely finds issues beyond what Semgrep catches in code-quality. + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: python + queries: security-extended + + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + - name: Perform CodeQL Analysis + id: codeql-analysis + uses: github/codeql-action/analyze@v3 + continue-on-error: true + with: + category: "/language:python" + + - name: Check CodeQL upload status + if: always() && steps.codeql-analysis.outcome == 'failure' + run: | + echo "⚠️ CodeQL analysis completed but upload failed." + echo "This is expected if Code Security is not enabled for this repository." + echo "To enable: Settings > Code security and analysis > Code scanning" + + dependency-scanning: + name: Dependency Vulnerability Scan + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Cache pip + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-security-pip-${{ hashFiles('**/requirements*.txt', '**/pyproject.toml') }} + restore-keys: ${{ runner.os }}-security-pip- + + - name: Run pip-audit + run: | + pip install pip-audit + pip-audit --desc --format json --output pip-audit-report.json || true + + - name: Run Bandit Security Scan + run: | + pip install bandit[toml] + bandit -r . -f json -o bandit-report.json --exclude ./.git,./node_modules,./frappe-bench || true + + - name: Upload security reports + uses: actions/upload-artifact@v4 + with: + name: security-reports-${{ github.run_id }} + path: | + pip-audit-report.json + bandit-report.json + retention-days: 14 + + security-summary: + name: Security Summary + runs-on: ubuntu-latest + needs: [codeql-analysis, dependency-scanning] + if: always() + + steps: + - name: Generate Security Summary + uses: actions/github-script@v8 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const needs = ${{ toJSON(needs) }}; + const codeqlResult = needs['codeql-analysis']?.result || 'unknown'; + const dependencyResult = needs['dependency-scanning']?.result || 'unknown'; + + const getStatus = (result) => { + if (result === 'success') return '✅ Passed'; + if (result === 'failure') return '❌ Failed'; + return '⚠️ Skipped'; + }; + + const summary = `## 🔒 Security Scan Summary + + | Scan Type | Status | + |-----------|--------| + | CodeQL Analysis (Python) | ${getStatus(codeqlResult)} | + | Dependency Scan | ${getStatus(dependencyResult)} | + + **View detailed results in the Security tab or workflow artifacts.**`; + + core.summary.addRaw(summary).write(); diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0f1acaf..7d1e0e5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ repos: files: "frappe.*" exclude: ".*json$|.*txt$|.*csv|.*md|.*svg" - id: no-commit-to-branch - args: ['--branch', 'main', '--branch', 'master'] + args: ['--branch', 'develop'] - id: check-merge-conflict - id: check-ast - id: check-json diff --git a/commitlint.config.js b/commitlint.config.js index ce91a16..55f1dff 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -1,7 +1,7 @@ module.exports = { - extends: ["@commitlint/config-conventional"], - // Disable default 100-char (or 72-char) header length limit for commit messages - rules: { - "header-max-length": [0, "always", 100], - }, + extends: ["@commitlint/config-conventional"], + // Disable default 100-char (or 72-char) header length limit for commit messages + rules: { + "header-max-length": [0, "always", 100], + }, };