Initial commit
This commit is contained in:
24
Fusion Backend Theme/fusion_backend_theme/__init__.py
Normal file
24
Fusion Backend Theme/fusion_backend_theme/__init__.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from . import controllers
|
||||
from . import models
|
||||
|
||||
import base64
|
||||
|
||||
from odoo.tools import file_open
|
||||
|
||||
|
||||
def _setup_module(env):
|
||||
if env.ref('base.main_company', False):
|
||||
with file_open('web/static/img/favicon.ico', 'rb') as file:
|
||||
env.ref('base.main_company').write({
|
||||
'favicon': base64.b64encode(file.read())
|
||||
})
|
||||
with file_open(
|
||||
'fusion_backend_theme/static/img/background.png', 'rb'
|
||||
) as file:
|
||||
env.ref('base.main_company').write({
|
||||
'background_image': base64.b64encode(file.read())
|
||||
})
|
||||
|
||||
|
||||
def _uninstall_cleanup(env):
|
||||
env['res.config.settings']._reset_theme_color_assets()
|
||||
167
Fusion Backend Theme/fusion_backend_theme/__manifest__.py
Normal file
167
Fusion Backend Theme/fusion_backend_theme/__manifest__.py
Normal file
@@ -0,0 +1,167 @@
|
||||
{
|
||||
'name': 'Fusion Backend Theme',
|
||||
'summary': 'Enterprise-grade backend theme for Odoo Community with dark/light mode',
|
||||
'description': '''
|
||||
Fusion Backend Theme provides a polished backend experience for
|
||||
Odoo Community Edition featuring dark/light mode switching, a
|
||||
customizable sidebar, an enhanced app switcher with drag-and-drop
|
||||
reordering, resizable chatter, fullscreen dialogs, auto-refresh
|
||||
views, and comprehensive color customization.
|
||||
''',
|
||||
'version': '19.0.1.0.0',
|
||||
'category': 'Themes/Backend',
|
||||
'license': 'LGPL-3',
|
||||
'author': 'Nexa Systems Inc',
|
||||
'website': 'https://nexasystems.ca',
|
||||
'contributors': [
|
||||
'Nexa Systems Inc <help@nexasystems.ca>',
|
||||
],
|
||||
'depends': [
|
||||
'web',
|
||||
'base_setup',
|
||||
'mail',
|
||||
],
|
||||
'excludes': [
|
||||
'web_enterprise',
|
||||
],
|
||||
'data': [
|
||||
'templates/webclient.xml',
|
||||
'templates/web_layout.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/res_users_views.xml',
|
||||
],
|
||||
'assets': {
|
||||
# Primary SCSS variables
|
||||
'web._assets_primary_variables': [
|
||||
('prepend', 'fusion_backend_theme/static/src/scss/colors.scss'),
|
||||
(
|
||||
'before',
|
||||
'fusion_backend_theme/static/src/scss/colors.scss',
|
||||
'fusion_backend_theme/static/src/scss/colors_light.scss',
|
||||
),
|
||||
(
|
||||
'after',
|
||||
'fusion_backend_theme/static/src/scss/colors_light.scss',
|
||||
'fusion_backend_theme/static/src/scss/primary_variables.scss',
|
||||
),
|
||||
],
|
||||
# Backend helper mixins
|
||||
'web._assets_backend_helpers': [
|
||||
'fusion_backend_theme/static/src/scss/helpers.scss',
|
||||
],
|
||||
# Main backend assets
|
||||
'web.assets_backend': [
|
||||
# Webclient structure
|
||||
(
|
||||
'after',
|
||||
'web/static/src/webclient/webclient.js',
|
||||
'fusion_backend_theme/static/src/webclient/webclient.js',
|
||||
),
|
||||
(
|
||||
'after',
|
||||
'web/static/src/webclient/webclient.xml',
|
||||
'fusion_backend_theme/static/src/webclient/webclient.xml',
|
||||
),
|
||||
'fusion_backend_theme/static/src/webclient/webclient.scss',
|
||||
# Sidebar
|
||||
(
|
||||
'after',
|
||||
'web/static/src/webclient/webclient.js',
|
||||
'fusion_backend_theme/static/src/webclient/sidebar/menu_service.js',
|
||||
),
|
||||
(
|
||||
'after',
|
||||
'web/static/src/webclient/webclient.js',
|
||||
'fusion_backend_theme/static/src/webclient/sidebar/sidebar.js',
|
||||
),
|
||||
'fusion_backend_theme/static/src/webclient/sidebar/sidebar.xml',
|
||||
'fusion_backend_theme/static/src/webclient/sidebar/sidebar.scss',
|
||||
# Navbar
|
||||
'fusion_backend_theme/static/src/webclient/navbar/navbar.js',
|
||||
'fusion_backend_theme/static/src/webclient/navbar/navbar.xml',
|
||||
'fusion_backend_theme/static/src/webclient/navbar/navbar.scss',
|
||||
# App Switcher
|
||||
'fusion_backend_theme/static/src/webclient/app_switcher/app_switcher.js',
|
||||
'fusion_backend_theme/static/src/webclient/app_switcher/app_switcher.xml',
|
||||
'fusion_backend_theme/static/src/webclient/app_switcher/app_switcher.scss',
|
||||
# Core services
|
||||
'fusion_backend_theme/static/src/core/color_scheme/color_scheme_service.js',
|
||||
# Core dialog
|
||||
(
|
||||
'after',
|
||||
'web/static/src/core/dialog/dialog.js',
|
||||
'fusion_backend_theme/static/src/core/dialog/dialog.js',
|
||||
),
|
||||
(
|
||||
'after',
|
||||
'web/static/src/core/dialog/dialog.scss',
|
||||
'fusion_backend_theme/static/src/core/dialog/dialog.scss',
|
||||
),
|
||||
(
|
||||
'after',
|
||||
'web/static/src/core/dialog/dialog.xml',
|
||||
'fusion_backend_theme/static/src/core/dialog/dialog.xml',
|
||||
),
|
||||
(
|
||||
'after',
|
||||
'web/static/src/views/view_dialogs/select_create_dialog.js',
|
||||
'fusion_backend_theme/static/src/core/dialog/select_create_dialog.js',
|
||||
),
|
||||
# Core thread
|
||||
'fusion_backend_theme/static/src/core/thread/thread.js',
|
||||
'fusion_backend_theme/static/src/core/thread/thread.xml',
|
||||
# Chatter
|
||||
'fusion_backend_theme/static/src/chatter/chatter.scss',
|
||||
'fusion_backend_theme/static/src/chatter/chatter.xml',
|
||||
(
|
||||
'after',
|
||||
'mail/static/src/chatter/web_portal/chatter.js',
|
||||
'fusion_backend_theme/static/src/chatter/chatter.js',
|
||||
),
|
||||
# Form views
|
||||
'fusion_backend_theme/static/src/views/form/form.scss',
|
||||
(
|
||||
'after',
|
||||
'mail/static/src/chatter/web/form_compiler.js',
|
||||
'fusion_backend_theme/static/src/views/form/form_compiler.js',
|
||||
),
|
||||
'fusion_backend_theme/static/src/views/form/form_renderer.js',
|
||||
# Search views
|
||||
(
|
||||
'after',
|
||||
'/web/static/src/search/control_panel/control_panel.js',
|
||||
'fusion_backend_theme/static/src/views/search/auto_refresh.js',
|
||||
),
|
||||
(
|
||||
'after',
|
||||
'/web/static/src/search/control_panel/control_panel.xml',
|
||||
'fusion_backend_theme/static/src/views/search/auto_refresh.xml',
|
||||
),
|
||||
'fusion_backend_theme/static/src/views/search/expand_groups.js',
|
||||
'fusion_backend_theme/static/src/views/search/expand_groups.xml',
|
||||
'fusion_backend_theme/static/src/views/search/collapse_groups.js',
|
||||
'fusion_backend_theme/static/src/views/search/collapse_groups.xml',
|
||||
],
|
||||
# Dark mode
|
||||
'web.assets_web_dark': [
|
||||
(
|
||||
'after',
|
||||
'fusion_backend_theme/static/src/scss/colors.scss',
|
||||
'fusion_backend_theme/static/src/scss/colors_dark.scss',
|
||||
),
|
||||
'fusion_backend_theme/static/src/**/*.dark.scss',
|
||||
],
|
||||
# Tests
|
||||
'web.assets_unit_tests': [
|
||||
'fusion_backend_theme/static/tests/**/*',
|
||||
],
|
||||
},
|
||||
'images': [
|
||||
'static/description/banner.png',
|
||||
],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'auto_install': False,
|
||||
'post_init_hook': '_setup_module',
|
||||
'uninstall_hook': '_uninstall_cleanup',
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
from . import main
|
||||
@@ -0,0 +1,15 @@
|
||||
from odoo.addons.web.controllers import home as web_home
|
||||
from odoo.http import request, route
|
||||
|
||||
|
||||
class WebClient(web_home.Home):
|
||||
|
||||
@route()
|
||||
def web_client(self, s_action=None, **kw):
|
||||
response = super().web_client(s_action, **kw)
|
||||
if response.status_code == 200:
|
||||
response.set_cookie(
|
||||
'color_scheme',
|
||||
request.env['ir.http'].color_scheme(),
|
||||
)
|
||||
return response
|
||||
@@ -0,0 +1,6 @@
|
||||
from . import color_editor
|
||||
from . import ir_http
|
||||
from . import res_company
|
||||
from . import res_config_settings
|
||||
from . import res_users
|
||||
from . import res_users_settings
|
||||
133
Fusion Backend Theme/fusion_backend_theme/models/color_editor.py
Normal file
133
Fusion Backend Theme/fusion_backend_theme/models/color_editor.py
Normal file
@@ -0,0 +1,133 @@
|
||||
import re
|
||||
import base64
|
||||
|
||||
from odoo import models, fields, api
|
||||
from odoo.tools import misc
|
||||
|
||||
from odoo.addons.base.models.assetsbundle import EXTENSIONS
|
||||
|
||||
|
||||
class ColorEditor(models.AbstractModel):
|
||||
|
||||
_name = 'fusion_backend_theme.color_editor'
|
||||
_description = 'Theme Color Editor'
|
||||
|
||||
@api.model
|
||||
def _get_custom_url(self, url, bundle):
|
||||
return f'/_custom/{bundle}{url}'
|
||||
|
||||
@api.model
|
||||
def _get_url_info(self, url):
|
||||
regex = re.compile(
|
||||
r'^(/_custom/([^/]+))?/(\w+)/([/\w.]+\.\w+)$'
|
||||
)
|
||||
match = regex.match(url)
|
||||
if not match:
|
||||
return False
|
||||
return {
|
||||
'module': match.group(3),
|
||||
'resource_path': match.group(4),
|
||||
'customized': bool(match.group(1)),
|
||||
'bundle': match.group(2) or False
|
||||
}
|
||||
|
||||
@api.model
|
||||
def _find_attachment(self, custom_url):
|
||||
return self.env['ir.attachment'].search([
|
||||
('url', '=', custom_url)
|
||||
])
|
||||
|
||||
@api.model
|
||||
def _find_asset(self, custom_url):
|
||||
return self.env['ir.asset'].search([
|
||||
('path', 'like', custom_url)
|
||||
])
|
||||
|
||||
@api.model
|
||||
def _read_scss(self, url, bundle):
|
||||
custom_url = self._get_custom_url(url, bundle)
|
||||
url_info = self._get_url_info(custom_url)
|
||||
if url_info and url_info['customized']:
|
||||
attachment = self._find_attachment(custom_url)
|
||||
if attachment:
|
||||
return base64.b64decode(attachment.datas)
|
||||
with misc.file_open(
|
||||
url.strip('/'), 'rb', filter_ext=EXTENSIONS
|
||||
) as f:
|
||||
return f.read()
|
||||
|
||||
def _extract_variable(self, content, variable):
|
||||
value = re.search(
|
||||
fr'\$o_fusion_{variable}\:?\s(.*?);', content
|
||||
)
|
||||
return value and value.group(1)
|
||||
|
||||
def _extract_variables(self, content, variables):
|
||||
return {
|
||||
var: self._extract_variable(content, var)
|
||||
for var in variables
|
||||
}
|
||||
|
||||
def _update_variables(self, content, variables):
|
||||
for variable in variables:
|
||||
if not variable.get("value"):
|
||||
continue
|
||||
content = re.sub(
|
||||
fr'{variable["name"]}\:?\s(.*?);',
|
||||
f'{variable["name"]}: {variable["value"]};',
|
||||
content
|
||||
)
|
||||
return content
|
||||
|
||||
@api.model
|
||||
def _write_scss(self, url, bundle, content):
|
||||
custom_url = self._get_custom_url(url, bundle)
|
||||
asset_url = url[1:] if url.startswith(('/', '\\')) else url
|
||||
datas = base64.b64encode((content or '\n').encode('utf-8'))
|
||||
custom_attachment = self._find_attachment(custom_url)
|
||||
if custom_attachment:
|
||||
custom_attachment.write({'datas': datas})
|
||||
self.env.registry.clear_cache('assets')
|
||||
else:
|
||||
attachment_values = {
|
||||
'name': url.split('/')[-1],
|
||||
'type': 'binary',
|
||||
'mimetype': 'text/scss',
|
||||
'datas': datas,
|
||||
'url': custom_url,
|
||||
}
|
||||
asset_values = {
|
||||
'path': custom_url,
|
||||
'target': url,
|
||||
'directive': 'replace',
|
||||
}
|
||||
target_asset = self._find_asset(asset_url)
|
||||
if target_asset:
|
||||
asset_values['name'] = '%s override' % target_asset.name
|
||||
asset_values['bundle'] = target_asset.bundle
|
||||
asset_values['sequence'] = target_asset.sequence
|
||||
else:
|
||||
asset_values['name'] = '%s: replace %s' % (
|
||||
bundle, custom_url.split('/')[-1]
|
||||
)
|
||||
asset_values['bundle'] = (
|
||||
self.env['ir.asset']._get_related_bundle(url, bundle)
|
||||
)
|
||||
self.env['ir.attachment'].create(attachment_values)
|
||||
self.env['ir.asset'].create(asset_values)
|
||||
|
||||
def get_variable_values(self, url, bundle, variables):
|
||||
content = self._read_scss(url, bundle)
|
||||
return self._extract_variables(
|
||||
content.decode('utf-8'), variables
|
||||
)
|
||||
|
||||
def set_variable_values(self, url, bundle, variables):
|
||||
original = self._read_scss(url, bundle).decode('utf-8')
|
||||
content = self._update_variables(original, variables)
|
||||
self._write_scss(url, bundle, content)
|
||||
|
||||
def reset_asset(self, url, bundle):
|
||||
custom_url = self._get_custom_url(url, bundle)
|
||||
self._find_attachment(custom_url).unlink()
|
||||
self._find_asset(custom_url).unlink()
|
||||
42
Fusion Backend Theme/fusion_backend_theme/models/ir_http.py
Normal file
42
Fusion Backend Theme/fusion_backend_theme/models/ir_http.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from odoo import models
|
||||
from odoo.http import request
|
||||
|
||||
|
||||
class IrHttp(models.AbstractModel):
|
||||
|
||||
_inherit = "ir.http"
|
||||
|
||||
@classmethod
|
||||
def _post_logout(cls):
|
||||
super()._post_logout()
|
||||
request.future_response.set_cookie('color_scheme', max_age=0)
|
||||
|
||||
def color_scheme(self):
|
||||
cookie_scheme = request.httprequest.cookies.get('color_scheme')
|
||||
scheme = cookie_scheme if cookie_scheme else super().color_scheme()
|
||||
if user := request.env.user:
|
||||
if user._is_public():
|
||||
return super().color_scheme()
|
||||
if user_scheme := user.res_users_settings_id.color_scheme:
|
||||
if user_scheme in ('light', 'dark'):
|
||||
return user_scheme
|
||||
return scheme
|
||||
|
||||
def session_info(self):
|
||||
result = super().session_info()
|
||||
user = self.env.user
|
||||
if user._is_internal():
|
||||
for company in user.company_ids.with_context(bin_size=True):
|
||||
result['user_companies']['allowed_companies'][company.id].update({
|
||||
'has_background_image': bool(company.background_image),
|
||||
'has_appsbar_image': bool(company.appbar_image),
|
||||
})
|
||||
result['chatter_position'] = user.chatter_position
|
||||
result['dialog_size'] = user.dialog_size
|
||||
result['pager_autoload_interval'] = int(
|
||||
self.env['ir.config_parameter'].sudo().get_param(
|
||||
'fusion_backend_theme.pager_autoload_interval',
|
||||
default=30000
|
||||
)
|
||||
)
|
||||
return result
|
||||
@@ -0,0 +1,19 @@
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class ResCompany(models.Model):
|
||||
|
||||
_inherit = 'res.company'
|
||||
|
||||
favicon = fields.Binary(
|
||||
string="Company Favicon",
|
||||
attachment=True,
|
||||
)
|
||||
background_image = fields.Binary(
|
||||
string='Apps Menu Background Image',
|
||||
attachment=True,
|
||||
)
|
||||
appbar_image = fields.Binary(
|
||||
string='Sidebar Logo Image',
|
||||
attachment=True,
|
||||
)
|
||||
@@ -0,0 +1,264 @@
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Properties
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
@property
|
||||
def COLOR_FIELDS(self):
|
||||
return [
|
||||
'color_brand',
|
||||
'color_primary',
|
||||
'color_success',
|
||||
'color_info',
|
||||
'color_warning',
|
||||
'color_danger',
|
||||
]
|
||||
|
||||
@property
|
||||
def THEME_COLOR_FIELDS(self):
|
||||
return [
|
||||
'color_appsmenu_text',
|
||||
'color_sidebar_text',
|
||||
'color_sidebar_active',
|
||||
'color_sidebar_background',
|
||||
]
|
||||
|
||||
@property
|
||||
def COLOR_ASSET_LIGHT_URL(self):
|
||||
return '/fusion_backend_theme/static/src/scss/colors_light.scss'
|
||||
|
||||
@property
|
||||
def COLOR_BUNDLE_LIGHT_NAME(self):
|
||||
return 'web._assets_primary_variables'
|
||||
|
||||
@property
|
||||
def COLOR_ASSET_DARK_URL(self):
|
||||
return '/fusion_backend_theme/static/src/scss/colors_dark.scss'
|
||||
|
||||
@property
|
||||
def COLOR_BUNDLE_DARK_NAME(self):
|
||||
return 'web.assets_web_dark'
|
||||
|
||||
@property
|
||||
def COLOR_ASSET_THEME_URL(self):
|
||||
return '/fusion_backend_theme/static/src/scss/colors_light.scss'
|
||||
|
||||
@property
|
||||
def COLOR_BUNDLE_THEME_NAME(self):
|
||||
return 'web._assets_primary_variables'
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Fields - Company Assets
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
theme_favicon = fields.Binary(
|
||||
related='company_id.favicon',
|
||||
readonly=False,
|
||||
)
|
||||
theme_background_image = fields.Binary(
|
||||
related='company_id.background_image',
|
||||
readonly=False,
|
||||
)
|
||||
appbar_image = fields.Binary(
|
||||
related='company_id.appbar_image',
|
||||
readonly=False,
|
||||
)
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Fields - Light Mode Colors
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
color_brand_light = fields.Char(string='Brand Light Color')
|
||||
color_primary_light = fields.Char(string='Primary Light Color')
|
||||
color_success_light = fields.Char(string='Success Light Color')
|
||||
color_info_light = fields.Char(string='Info Light Color')
|
||||
color_warning_light = fields.Char(string='Warning Light Color')
|
||||
color_danger_light = fields.Char(string='Danger Light Color')
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Fields - Dark Mode Colors
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
color_brand_dark = fields.Char(string='Brand Dark Color')
|
||||
color_primary_dark = fields.Char(string='Primary Dark Color')
|
||||
color_success_dark = fields.Char(string='Success Dark Color')
|
||||
color_info_dark = fields.Char(string='Info Dark Color')
|
||||
color_warning_dark = fields.Char(string='Warning Dark Color')
|
||||
color_danger_dark = fields.Char(string='Danger Dark Color')
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Fields - Theme Colors
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
theme_color_appsmenu_text = fields.Char(string='App Switcher Text Color')
|
||||
theme_color_sidebar_text = fields.Char(string='Sidebar Text Color')
|
||||
theme_color_sidebar_active = fields.Char(string='Sidebar Active Color')
|
||||
theme_color_sidebar_background = fields.Char(string='Sidebar Background Color')
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Light Color Helpers
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
def _get_light_color_values(self):
|
||||
return self.env['fusion_backend_theme.color_editor'].get_variable_values(
|
||||
self.COLOR_ASSET_LIGHT_URL,
|
||||
self.COLOR_BUNDLE_LIGHT_NAME,
|
||||
self.COLOR_FIELDS,
|
||||
)
|
||||
|
||||
def _set_light_color_values(self, values):
|
||||
colors = self._get_light_color_values()
|
||||
for var, value in colors.items():
|
||||
values[f'{var}_light'] = value
|
||||
return values
|
||||
|
||||
def _detect_light_color_change(self):
|
||||
colors = self._get_light_color_values()
|
||||
return any(
|
||||
val is not None and self[f'{var}_light'] != val
|
||||
for var, val in colors.items()
|
||||
)
|
||||
|
||||
def _replace_light_color_values(self):
|
||||
variables = [
|
||||
{'name': field, 'value': self[f'{field}_light']}
|
||||
for field in self.COLOR_FIELDS
|
||||
]
|
||||
return self.env['fusion_backend_theme.color_editor'].set_variable_values(
|
||||
self.COLOR_ASSET_LIGHT_URL,
|
||||
self.COLOR_BUNDLE_LIGHT_NAME,
|
||||
variables,
|
||||
)
|
||||
|
||||
def _reset_light_color_assets(self):
|
||||
self.env['fusion_backend_theme.color_editor'].reset_asset(
|
||||
self.COLOR_ASSET_LIGHT_URL,
|
||||
self.COLOR_BUNDLE_LIGHT_NAME,
|
||||
)
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Dark Color Helpers
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
def _get_dark_color_values(self):
|
||||
return self.env['fusion_backend_theme.color_editor'].get_variable_values(
|
||||
self.COLOR_ASSET_DARK_URL,
|
||||
self.COLOR_BUNDLE_DARK_NAME,
|
||||
self.COLOR_FIELDS,
|
||||
)
|
||||
|
||||
def _set_dark_color_values(self, values):
|
||||
colors = self._get_dark_color_values()
|
||||
for var, value in colors.items():
|
||||
values[f'{var}_dark'] = value
|
||||
return values
|
||||
|
||||
def _detect_dark_color_change(self):
|
||||
colors = self._get_dark_color_values()
|
||||
return any(
|
||||
val is not None and self[f'{var}_dark'] != val
|
||||
for var, val in colors.items()
|
||||
)
|
||||
|
||||
def _replace_dark_color_values(self):
|
||||
variables = [
|
||||
{'name': field, 'value': self[f'{field}_dark']}
|
||||
for field in self.COLOR_FIELDS
|
||||
]
|
||||
return self.env['fusion_backend_theme.color_editor'].set_variable_values(
|
||||
self.COLOR_ASSET_DARK_URL,
|
||||
self.COLOR_BUNDLE_DARK_NAME,
|
||||
variables,
|
||||
)
|
||||
|
||||
def _reset_dark_color_assets(self):
|
||||
self.env['fusion_backend_theme.color_editor'].reset_asset(
|
||||
self.COLOR_ASSET_DARK_URL,
|
||||
self.COLOR_BUNDLE_DARK_NAME,
|
||||
)
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Theme Color Helpers
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
def _get_theme_color_values(self):
|
||||
return self.env['fusion_backend_theme.color_editor'].get_variable_values(
|
||||
self.COLOR_ASSET_THEME_URL,
|
||||
self.COLOR_BUNDLE_THEME_NAME,
|
||||
self.THEME_COLOR_FIELDS,
|
||||
)
|
||||
|
||||
def _set_theme_color_values(self, values):
|
||||
colors = self._get_theme_color_values()
|
||||
for var, value in colors.items():
|
||||
values[f'theme_{var}'] = value
|
||||
return values
|
||||
|
||||
def _detect_theme_color_change(self):
|
||||
colors = self._get_theme_color_values()
|
||||
return any(
|
||||
val is not None and self[f'theme_{var}'] != val
|
||||
for var, val in colors.items()
|
||||
)
|
||||
|
||||
def _replace_theme_color_values(self):
|
||||
variables = [
|
||||
{'name': field, 'value': self[f'theme_{field}']}
|
||||
for field in self.THEME_COLOR_FIELDS
|
||||
]
|
||||
return self.env['fusion_backend_theme.color_editor'].set_variable_values(
|
||||
self.COLOR_ASSET_THEME_URL,
|
||||
self.COLOR_BUNDLE_THEME_NAME,
|
||||
variables,
|
||||
)
|
||||
|
||||
def _reset_theme_color_assets(self):
|
||||
self.env['fusion_backend_theme.color_editor'].reset_asset(
|
||||
self.COLOR_ASSET_THEME_URL,
|
||||
self.COLOR_BUNDLE_THEME_NAME,
|
||||
)
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# Actions
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
def action_reset_light_color_assets(self):
|
||||
self._reset_light_color_assets()
|
||||
return {'type': 'ir.actions.client', 'tag': 'reload'}
|
||||
|
||||
def action_reset_dark_color_assets(self):
|
||||
self._reset_dark_color_assets()
|
||||
return {'type': 'ir.actions.client', 'tag': 'reload'}
|
||||
|
||||
def action_reset_theme_color_assets(self):
|
||||
self._reset_light_color_assets()
|
||||
self._reset_dark_color_assets()
|
||||
self._reset_theme_color_assets()
|
||||
return {'type': 'ir.actions.client', 'tag': 'reload'}
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# CRUD
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
def get_values(self):
|
||||
res = super().get_values()
|
||||
res = self._set_light_color_values(res)
|
||||
res = self._set_dark_color_values(res)
|
||||
res = self._set_theme_color_values(res)
|
||||
return res
|
||||
|
||||
def set_values(self):
|
||||
res = super().set_values()
|
||||
if self._detect_light_color_change():
|
||||
self._replace_light_color_values()
|
||||
if self._detect_dark_color_change():
|
||||
self._replace_dark_color_values()
|
||||
if self._detect_theme_color_change():
|
||||
self._replace_theme_color_values()
|
||||
return res
|
||||
@@ -0,0 +1,57 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResUsers(models.Model):
|
||||
|
||||
_inherit = "res.users"
|
||||
|
||||
color_scheme = fields.Selection(
|
||||
related="res_users_settings_id.color_scheme",
|
||||
readonly=False,
|
||||
)
|
||||
sidebar_type = fields.Selection(
|
||||
selection=[
|
||||
('invisible', 'Invisible'),
|
||||
('small', 'Small'),
|
||||
('large', 'Large'),
|
||||
],
|
||||
string="Sidebar Type",
|
||||
default='large',
|
||||
required=True,
|
||||
)
|
||||
chatter_position = fields.Selection(
|
||||
selection=[
|
||||
('side', 'Side'),
|
||||
('bottom', 'Bottom'),
|
||||
],
|
||||
string="Chatter Position",
|
||||
default='side',
|
||||
required=True,
|
||||
)
|
||||
dialog_size = fields.Selection(
|
||||
selection=[
|
||||
('minimize', 'Minimize'),
|
||||
('maximize', 'Maximize'),
|
||||
],
|
||||
string="Dialog Size",
|
||||
default='minimize',
|
||||
required=True,
|
||||
)
|
||||
|
||||
@property
|
||||
def SELF_READABLE_FIELDS(self):
|
||||
return super().SELF_READABLE_FIELDS + [
|
||||
'color_scheme',
|
||||
'sidebar_type',
|
||||
'chatter_position',
|
||||
'dialog_size',
|
||||
]
|
||||
|
||||
@property
|
||||
def SELF_WRITEABLE_FIELDS(self):
|
||||
return super().SELF_WRITEABLE_FIELDS + [
|
||||
'color_scheme',
|
||||
'sidebar_type',
|
||||
'chatter_position',
|
||||
'dialog_size',
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResUsersSettings(models.Model):
|
||||
|
||||
_inherit = 'res.users.settings'
|
||||
|
||||
homemenu_config = fields.Json(
|
||||
string="Home Menu Configuration",
|
||||
readonly=True,
|
||||
)
|
||||
color_scheme = fields.Selection(
|
||||
[("system", "System"), ("light", "Light"), ("dark", "Dark")],
|
||||
default="system",
|
||||
required=True,
|
||||
string="Color Scheme",
|
||||
)
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 100 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 100 100" width="100pt" height="100pt"><defs><clipPath id="_clipPath_JQKfeprzGCqweYHysNwFC93s33TTLYMa"><rect width="100" height="100"/></clipPath></defs><g clip-path="url(#_clipPath_JQKfeprzGCqweYHysNwFC93s33TTLYMa)"><rect width="100" height="100" style="fill:rgb(0,0,0)" fill-opacity="0"/><g><path d=" M 73.153 24.364 C 74.115 25.121 75.195 25.725 76.364 26.15 C 77.481 26.557 78.643 26.788 79.811 26.841 C 79.345 31.478 78.276 37.25 76.809 43.024 L 63.516 43.024 C 67.067 34.977 70.347 28.628 73.153 24.364 Z " fill="rgb(247,134,19)"/><g><rect x="44.544" y="-0.228" width="13.952" height="43.252" transform="matrix(1,0,0,1,0,0)" fill="rgb(0,160,121)"/><path d=" M 44.544 -0.228 L 58.496 -0.228 L 58.496 5.121 L 53.846 5.121 C 52.562 5.121 51.52 6.162 51.52 7.446 C 51.52 8.73 52.562 9.771 53.846 9.771 L 58.496 9.771 L 58.496 14.422 L 53.846 14.422 C 52.562 14.422 51.52 15.463 51.52 16.747 C 51.52 18.032 52.562 19.073 53.846 19.073 L 58.496 19.073 L 58.496 23.723 L 53.846 23.723 C 52.562 23.723 51.52 24.764 51.52 26.049 C 51.52 27.333 52.562 28.374 53.846 28.374 L 58.496 28.374 L 58.496 33.025 L 53.846 33.025 C 52.562 33.025 51.52 34.066 51.52 35.35 C 51.52 36.635 52.562 37.676 53.846 37.676 L 58.496 37.676 L 58.496 43.024 L 44.544 43.024 L 44.544 -0.228 Z " fill="rgb(0,206,179)"/></g><path d=" M 17.747 7.168 L 28.646 19.92 C 27.183 22.113 25.114 23.728 22.681 24.547 C 20.258 25.364 17.71 25.31 15.348 24.419 L 17.747 7.168 Z " fill="rgb(46,188,250)"/><path d=" M 15.288 23.257 C 16.318 23.473 17.367 23.583 18.422 23.583 C 19.766 23.583 21.12 23.406 22.453 23.049 C 24.834 22.412 26.979 21.241 28.763 19.643 L 31.201 28.742 L 34.872 43.024 L 20.423 43.024 L 17.737 32.397 C 17.734 32.386 17.731 32.375 17.728 32.365 L 15.288 23.257 Z " fill="rgb(8,139,245)"/><g><path d=" M 79.61 93.252 C 79.61 97.098 76.481 100.228 72.634 100.228 L 26.127 100.228 C 22.28 100.228 19.151 97.098 19.151 93.252 L 19.151 47.675 L 79.61 47.675 L 79.61 93.252 Z " fill="rgb(152,81,132)"/><path d=" M 84.261 45.349 C 84.261 45.97 84.019 46.554 83.579 46.993 C 83.14 47.433 82.556 47.675 81.935 47.675 L 16.825 47.675 C 15.543 47.675 14.5 46.631 14.5 45.349 C 14.5 44.729 14.742 44.145 15.182 43.706 C 15.621 43.266 16.205 43.024 16.825 43.024 L 37.843 43.024 C 37.848 43.024 37.852 43.024 37.857 43.024 C 37.86 43.024 37.863 43.024 37.866 43.024 L 61.636 43.024 C 61.64 43.024 61.644 43.024 61.648 43.024 C 61.65 43.024 61.653 43.024 61.656 43.024 L 80.275 43.024 C 80.278 43.024 80.281 43.024 80.284 43.024 C 80.288 43.024 80.293 43.024 80.297 43.024 L 81.936 43.024 C 83.217 43.024 84.261 44.067 84.261 45.349 Z " fill="rgb(113,34,88)"/><g><path d=" M 26.127 61.627 C 24.843 61.627 23.801 62.668 23.801 63.952 L 23.801 93.252 C 23.801 94.536 24.843 95.577 26.127 95.577 C 27.411 95.577 28.452 94.536 28.452 93.252 L 28.452 63.952 C 28.452 62.668 27.411 61.627 26.127 61.627 Z " fill="rgb(113,34,88)"/><path d=" M 26.127 52.325 C 24.843 52.325 23.801 53.367 23.801 54.651 C 23.801 55.934 24.843 56.976 26.127 56.976 C 27.41 56.976 28.452 55.934 28.452 54.651 C 28.452 53.367 27.41 52.325 26.127 52.325 Z " fill="rgb(113,34,88)"/></g></g><path d=" M 85.197 22.249 C 84.376 24.502 82.486 26.157 80.134 26.676 C 78.848 26.963 77.506 26.874 76.255 26.419 C 74.959 25.947 73.843 25.112 73.028 24.006 C 73.028 24.005 73.028 24.005 73.027 24.004 C 71.637 22.118 71.285 19.678 72.084 17.478 C 72.956 15.085 78.628 9.982 83.565 6.1 C 84.923 12.271 86.067 19.86 85.197 22.249 Z " fill="rgb(251,185,69)"/></g></g></svg>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 494 KiB |
@@ -0,0 +1,24 @@
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
|
||||
import { Chatter } from "@mail/chatter/web_portal/chatter";
|
||||
|
||||
patch(Chatter.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
const showNotificationMessages = browser.localStorage.getItem(
|
||||
'fusion_backend_theme.notifications'
|
||||
);
|
||||
this.state.showNotificationMessages = (
|
||||
showNotificationMessages != null ?
|
||||
JSON.parse(showNotificationMessages) : true
|
||||
);
|
||||
},
|
||||
onClickNotificationsToggle() {
|
||||
const showNotificationMessages = !this.state.showNotificationMessages;
|
||||
browser.localStorage.setItem(
|
||||
'fusion_backend_theme.notifications', showNotificationMessages
|
||||
);
|
||||
this.state.showNotificationMessages = showNotificationMessages;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
// = Chatter Variables
|
||||
// ============================================================================
|
||||
|
||||
$o-form-renderer-max-width: 3840px;
|
||||
$o-form-view-sheet-max-width: 2560px;
|
||||
|
||||
// = Chatter
|
||||
// ============================================================================
|
||||
|
||||
.o-mail-Chatter-top:has(.o-mail-Chatter-sendMessage.active) .o-mail-Composer-send {
|
||||
@extend .btn-danger
|
||||
}
|
||||
|
||||
.o-mail-Form-chatter.o-aside {
|
||||
.o_fusion_chatter_handle {
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 5px;
|
||||
height: 100%;
|
||||
cursor: col-resize;
|
||||
position: absolute;
|
||||
z-index: $o-mail-NavigableList-zIndex + 1;
|
||||
border-left: var(--ControlPanel-border-bottom, 1px solid $o-gray-300);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
|
||||
<templates xml:space="preserve">
|
||||
<t
|
||||
t-name="fusion_backend_theme.Chatter"
|
||||
t-inherit="mail.Chatter"
|
||||
t-inherit-mode="extension"
|
||||
>
|
||||
<xpath expr="//button[hasclass('o-mail-Chatter-sendMessage')]" position="replace">
|
||||
<button
|
||||
class="o-mail-Chatter-sendMessage btn text-nowrap me-1"
|
||||
t-att-class="{
|
||||
'btn-danger': !state.composerType,
|
||||
'btn-primary': state.composerType === 'message',
|
||||
'btn-secondary': state.composerType !== 'message',
|
||||
'active': state.composerType === 'message',
|
||||
'my-2': !props.compactHeight
|
||||
}"
|
||||
t-att-disabled="!state.thread.hasWriteAccess and !(state.thread.hasReadAccess and state.thread.canPostOnReadonly) and props.threadId"
|
||||
data-hotkey="m"
|
||||
t-on-click="() => this.toggleComposer('message')"
|
||||
>
|
||||
<i t-if="env.isSmall" class="fa fa-envelope me-sm-1" />
|
||||
<span class="d-none d-sm-inline">Message</span>
|
||||
</button>
|
||||
</xpath>
|
||||
<xpath expr="//button[hasclass('o-mail-Chatter-logNote')]" position="replace">
|
||||
<button
|
||||
class="o-mail-Chatter-logNote btn text-nowrap me-1"
|
||||
t-att-class="{
|
||||
'btn-primary': state.composerType === 'note',
|
||||
'btn-secondary': state.composerType !== 'note',
|
||||
'active': state.composerType === 'note',
|
||||
'my-2': !props.compactHeight
|
||||
}"
|
||||
t-att-disabled="!state.thread.hasWriteAccess and !(state.thread.hasReadAccess and state.thread.canPostOnReadonly) and props.threadId"
|
||||
data-hotkey="shift+m"
|
||||
t-on-click="() => this.toggleComposer('note')"
|
||||
>
|
||||
<i t-if="env.isSmall" class="fa fa-sticky-note me-sm-1" />
|
||||
<span class="d-none d-sm-inline">Note</span>
|
||||
</button>
|
||||
</xpath>
|
||||
<xpath expr="//button[hasclass('o-mail-Chatter-activity')]/span" position="before">
|
||||
<i t-if="env.isSmall" class="fa fa-clock-o me-sm-1"/>
|
||||
</xpath>
|
||||
<xpath expr="//button[hasclass('o-mail-Chatter-activity')]/span" position="attributes">
|
||||
<attribute name="class" add="d-none d-sm-inline" separator=" " />
|
||||
</xpath>
|
||||
<xpath expr="//button[@t-if='props.hasAttachmentPreview and state.thread.attachmentsInWebClientView.length']" position="attributes">
|
||||
<attribute name="t-if">props.isChatterAside and props.hasAttachmentPreview and state.thread.attachmentsInWebClientView.length</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//button[@t-on-click='onClickSearch']" position="after">
|
||||
<button
|
||||
class="btn btn-link text-action px-1"
|
||||
aria-label="Show/Hide Notifications"
|
||||
title="Show/Hide Notifications"
|
||||
t-on-click="onClickNotificationsToggle"
|
||||
t-att-disabled="state.isSearchOpen"
|
||||
>
|
||||
<i
|
||||
class="fa fa-lg"
|
||||
t-att-class="{
|
||||
'fa-eye': state.showNotificationMessages,
|
||||
'fa-eye-slash': !state.showNotificationMessages,
|
||||
}"
|
||||
/>
|
||||
</button>
|
||||
</xpath>
|
||||
<xpath expr="//Thread" position="attributes">
|
||||
<attribute name="showNotificationMessages">state.showNotificationMessages</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1,52 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { cookie } from "@web/core/browser/cookie";
|
||||
import { user } from "@web/core/user";
|
||||
|
||||
const serviceRegistry = registry.category("services");
|
||||
|
||||
export function systemColorScheme() {
|
||||
return browser.matchMedia("(prefers-color-scheme:dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
}
|
||||
|
||||
export function currentColorScheme() {
|
||||
return cookie.get("color_scheme");
|
||||
}
|
||||
|
||||
export const colorSchemeService = {
|
||||
async start() {
|
||||
let newColorScheme = systemColorScheme();
|
||||
if (["light", "dark"].includes(user.settings.color_scheme)) {
|
||||
newColorScheme = user.settings.color_scheme;
|
||||
}
|
||||
const current = currentColorScheme();
|
||||
if (newColorScheme !== current) {
|
||||
cookie.set("color_scheme", newColorScheme);
|
||||
if (current || (!current && newColorScheme === "dark")) {
|
||||
this.reload();
|
||||
// Block rendering to prevent flash of wrong theme
|
||||
await new Promise(() => {});
|
||||
}
|
||||
}
|
||||
return {
|
||||
get systemColorScheme() {
|
||||
return systemColorScheme();
|
||||
},
|
||||
get currentColorScheme() {
|
||||
return currentColorScheme();
|
||||
},
|
||||
get userColorScheme() {
|
||||
return user.settings.color_scheme;
|
||||
},
|
||||
};
|
||||
},
|
||||
reload() {
|
||||
browser.location.reload();
|
||||
},
|
||||
};
|
||||
|
||||
serviceRegistry.add("color_scheme", colorSchemeService);
|
||||
@@ -0,0 +1,17 @@
|
||||
import { session } from '@web/session';
|
||||
import { patch } from '@web/core/utils/patch';
|
||||
|
||||
import { Dialog } from '@web/core/dialog/dialog';
|
||||
|
||||
patch(Dialog.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.data.size = (
|
||||
session.dialog_size !== 'maximize' ? this.props.size : 'fs'
|
||||
);
|
||||
this.data.initalSize = this.props?.size || 'lg';
|
||||
},
|
||||
onClickDialogSizeToggle() {
|
||||
this.data.size = this.data.size === 'fs' ? this.data.initalSize : 'fs';
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
// = Dialog Variables
|
||||
// ============================================================================
|
||||
|
||||
$o-form-renderer-max-width: 3840px;
|
||||
$o-form-view-sheet-max-width: 2560px;
|
||||
|
||||
// = Dialog Toggle
|
||||
// ============================================================================
|
||||
|
||||
.o_fusion_dialog_toggle {
|
||||
color: $btn-close-color;
|
||||
opacity: $btn-close-opacity;
|
||||
&:hover {
|
||||
opacity: $btn-close-hover-opacity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t
|
||||
t-name="fusion_backend_theme.Dialog"
|
||||
t-inherit="web.Dialog"
|
||||
t-inherit-mode="extension"
|
||||
>
|
||||
<xpath expr="//div[@t-attf-class='modal-{{props.size}}']" position="attributes">
|
||||
<attribute name="t-attf-class">modal-{{ this.data?.size || 'lg' }}</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t
|
||||
t-name="fusion_backend_theme.Dialog.header"
|
||||
t-inherit="web.Dialog.header"
|
||||
t-inherit-mode="extension"
|
||||
>
|
||||
<xpath expr="//h4[hasclass('modal-title')]" position="attributes">
|
||||
<attribute name="class" add="flex-shrink-0 w-75" separator=" " />
|
||||
</xpath>
|
||||
<xpath expr="//button[@t-on-click='onExpand']" position="attributes">
|
||||
<attribute name="class">fa fa-lg fa-external-link btn o_expand_button</attribute>
|
||||
<attribute name="style">margin-top: 2px !important;</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//t[@t-if='!fullscreen']" position="before">
|
||||
<button
|
||||
t-if="!fullscreen"
|
||||
type="button"
|
||||
class="btn float-end me-1 o_fusion_dialog_toggle"
|
||||
t-on-click="() => this.onClickDialogSizeToggle()"
|
||||
>
|
||||
<i t-attf-class="fa fa-lg {{ this.data?.size === 'fs' ? 'fa-compress' : 'fa-expand' }}" />
|
||||
</button>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,11 @@
|
||||
import { patch } from '@web/core/utils/patch';
|
||||
|
||||
import { SelectCreateDialog } from '@web/views/view_dialogs/select_create_dialog';
|
||||
|
||||
patch(SelectCreateDialog.prototype, {
|
||||
onClickDialogSizeToggle() {
|
||||
this.env.dialogData.size = (
|
||||
this.env.dialogData.size === 'fs' ? this.env.dialogData.initalSize : 'fs'
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
import { Thread } from '@mail/core/common/thread';
|
||||
|
||||
patch(Thread.prototype, {
|
||||
get displayMessages() {
|
||||
let messages = (
|
||||
this.props.order === 'asc' ?
|
||||
this.props.thread.nonEmptyMessages :
|
||||
[...this.props.thread.nonEmptyMessages].reverse()
|
||||
);
|
||||
if (!this.props.showNotificationMessages) {
|
||||
messages = messages.filter(
|
||||
(msg) => !['user_notification', 'notification'].includes(
|
||||
msg.message_type
|
||||
)
|
||||
);
|
||||
}
|
||||
return messages;
|
||||
},
|
||||
});
|
||||
|
||||
Thread.props = [
|
||||
...Thread.props,
|
||||
'showNotificationMessages?',
|
||||
];
|
||||
Thread.defaultProps = {
|
||||
...Thread.defaultProps,
|
||||
showNotificationMessages: true,
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
|
||||
<templates xml:space="preserve">
|
||||
<t
|
||||
t-name="fusion_backend_theme.Thread"
|
||||
t-inherit="mail.Thread"
|
||||
t-inherit-mode="extension"
|
||||
>
|
||||
<xpath expr="//t[@t-key='msg.id']" position="attributes">
|
||||
<attribute name="t-foreach">displayMessages</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -0,0 +1 @@
|
||||
// Color Assets (placeholder for custom overrides)
|
||||
@@ -0,0 +1,43 @@
|
||||
// Customizable Colors (Dark Mode)
|
||||
|
||||
$o_fusion_color_brand: #1e3040;
|
||||
$o_fusion_color_primary: #5D8DA8;
|
||||
|
||||
$o_fusion_color_success: #1DC959;
|
||||
$o_fusion_color_info: #6AB5FB;
|
||||
$o_fusion_color_warning: #FBB56A;
|
||||
$o_fusion_color_danger: #b83232;
|
||||
|
||||
$o_fusion_color_appsmenu_text: #E4E4E4;
|
||||
$o_fusion_color_sidebar_text: #E4E4E4;
|
||||
$o_fusion_color_sidebar_active: #5D8DA8;
|
||||
$o_fusion_color_sidebar_background: #3C3E4B;
|
||||
|
||||
// Map to Odoo variables
|
||||
|
||||
$o-community-color: $o_fusion_color_brand;
|
||||
$o-enterprise-color: $o_fusion_color_brand;
|
||||
$o-enterprise-action-color: $o_fusion_color_primary;
|
||||
|
||||
$o-brand-odoo: $o_fusion_color_brand;
|
||||
$o-brand-primary: $o_fusion_color_primary;
|
||||
|
||||
$o-success: $o_fusion_color_success;
|
||||
$o-info: $o_fusion_color_info;
|
||||
$o-warning: $o_fusion_color_warning;
|
||||
$o-danger: $o_fusion_color_danger;
|
||||
$o-action: $o_fusion_color_primary !default;
|
||||
$light: $o-gray-300 !default;
|
||||
$dark: $o-gray-700 !default;
|
||||
|
||||
$o-main-link-color: $o-action !default;
|
||||
$o-main-favorite-color: #ffd532 !default;
|
||||
$o-main-code-color: #c58bc8 !default;
|
||||
|
||||
$o-theme-text-colors: (
|
||||
"primary": lighten($o_fusion_color_primary, 15%),
|
||||
"success": $o_fusion_color_success,
|
||||
"info": $o_fusion_color_info,
|
||||
"warning": $o_fusion_color_warning,
|
||||
"danger": #ff5757,
|
||||
) !default;
|
||||
@@ -0,0 +1,36 @@
|
||||
// Customizable Colors (Light Mode)
|
||||
|
||||
$o_fusion_color_brand: #243742;
|
||||
$o_fusion_color_primary: #5D8DA8;
|
||||
|
||||
$o_fusion_color_success: #28A745;
|
||||
$o_fusion_color_info: #17A2B8;
|
||||
$o_fusion_color_warning: #FFAC00;
|
||||
$o_fusion_color_danger: #DC3545;
|
||||
|
||||
$o_fusion_color_appsmenu_text: #F8F9FA;
|
||||
$o_fusion_color_sidebar_text: #DEE2E6;
|
||||
$o_fusion_color_sidebar_active: #5D8DA8;
|
||||
$o_fusion_color_sidebar_background: #111827;
|
||||
|
||||
// Map to Odoo variables
|
||||
|
||||
$o-community-color: $o_fusion_color_brand;
|
||||
$o-enterprise-color: $o_fusion_color_brand;
|
||||
$o-enterprise-action-color: $o_fusion_color_primary;
|
||||
|
||||
$o-brand-odoo: $o_fusion_color_brand;
|
||||
$o-brand-primary: $o_fusion_color_primary;
|
||||
|
||||
$o-success: $o_fusion_color_success;
|
||||
$o-info: $o_fusion_color_info;
|
||||
$o-warning: $o_fusion_color_warning;
|
||||
$o-danger: $o_fusion_color_danger;
|
||||
|
||||
$o-theme-text-colors: (
|
||||
"primary": $o_fusion_color_brand,
|
||||
"success": $o-success,
|
||||
"info": $o-info,
|
||||
"warning": $o-warning,
|
||||
"danger": $o-danger,
|
||||
) !default;
|
||||
@@ -0,0 +1,7 @@
|
||||
@mixin o-fusion-disable-scrollbar {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
///
|
||||
/// Fusion Backend Theme - Dark Mode Overrides
|
||||
/// Color variables come from colors_dark.scss
|
||||
///
|
||||
|
||||
// = Gray Scale (inverted for dark mode)
|
||||
// ============================================================================
|
||||
|
||||
$o-white: #000 !default;
|
||||
$o-black: #FFF !default;
|
||||
|
||||
$o-gray-100: #1B1D26 !default;
|
||||
$o-gray-200: #262A36 !default;
|
||||
$o-gray-300: #3C3E4B !default;
|
||||
$o-gray-400: #5A5E6B !default;
|
||||
$o-gray-500: #6B707F !default;
|
||||
$o-gray-600: #7E8392 !default;
|
||||
$o-gray-700: #B1B3BC !default;
|
||||
$o-gray-800: #D1D1D1 !default;
|
||||
$o-gray-900: #E4E4E4 !default;
|
||||
|
||||
// = Webclient
|
||||
// ============================================================================
|
||||
|
||||
$o-webclient-color-scheme: dark !default;
|
||||
$o-webclient-background-color: $o-gray-100 !default;
|
||||
$o-view-background-color: $o-gray-200 !default;
|
||||
|
||||
// = Inputs
|
||||
// ============================================================================
|
||||
|
||||
$o-input-border-required: $o-black !default;
|
||||
|
||||
// = Components
|
||||
// ============================================================================
|
||||
|
||||
$o-component-active-bg: mix($o-action, $o-gray-300, 10%) !default;
|
||||
$o-form-lightsecondary: $o-gray-300 !default;
|
||||
|
||||
// = List-group
|
||||
// ============================================================================
|
||||
|
||||
$o-list-group-active-bg: rgba(saturate(adjust-hue($o-info, 15), 1.8), .5) !default;
|
||||
|
||||
// = Modal
|
||||
// ============================================================================
|
||||
|
||||
$modal-backdrop-bg: $o-white !default;
|
||||
|
||||
// = Buttons
|
||||
// ============================================================================
|
||||
|
||||
$o-btns-bs-override: () !default;
|
||||
$o-btns-bs-override: map-merge((
|
||||
"primary": (
|
||||
background: $o-fusion-color-primary,
|
||||
border: $o-fusion-color-primary,
|
||||
color: $o-black,
|
||||
|
||||
hover-background: lighten($o-fusion-color-primary, 5%),
|
||||
hover-border: lighten($o-fusion-color-primary, 5%),
|
||||
hover-color: $o-black,
|
||||
|
||||
active-background: lighten($o-fusion-color-primary, 10%),
|
||||
active-border: lighten($o-fusion-color-primary, 10%),
|
||||
active-color: $o-black,
|
||||
),
|
||||
|
||||
"secondary": (
|
||||
background: $o-gray-300,
|
||||
border: $o-gray-300,
|
||||
color: $o-gray-900,
|
||||
|
||||
hover-background: $o-gray-400,
|
||||
hover-border: $o-gray-400,
|
||||
hover-color: $o-gray-900,
|
||||
|
||||
active-background: mix($o-action, $o-gray-100, 15%),
|
||||
active-border: lighten($o-action, 10%),
|
||||
active-color: $o-black,
|
||||
),
|
||||
|
||||
"light": (
|
||||
background: $o-gray-200,
|
||||
border: $o-gray-200,
|
||||
color: $o-gray-900,
|
||||
|
||||
hover-background: $o-gray-300,
|
||||
hover-border: $o-gray-300,
|
||||
hover-color: $o-black,
|
||||
|
||||
active-background: mix($o-action, $o-gray-100, 15%),
|
||||
active-border: darken($o-action, 10%),
|
||||
active-color: $o-black,
|
||||
),
|
||||
|
||||
"danger": (
|
||||
background: $o-fusion-color-danger,
|
||||
border: $o-fusion-color-danger,
|
||||
color: $o-black,
|
||||
|
||||
hover-background: lighten($o-fusion-color-danger, 5%),
|
||||
hover-border: lighten($o-fusion-color-danger, 5%),
|
||||
hover-color: $o-black,
|
||||
|
||||
active-background: lighten($o-fusion-color-danger, 10%),
|
||||
active-border: lighten($o-fusion-color-danger, 10%),
|
||||
active-color: $o-black,
|
||||
),
|
||||
), $o-btns-bs-override);
|
||||
|
||||
$o-btns-bs-outline-override: () !default;
|
||||
$o-btns-bs-outline-override: map-merge((
|
||||
"primary": (
|
||||
background: transparent,
|
||||
border: map-get($o-theme-text-colors, 'primary'),
|
||||
color: map-get($o-theme-text-colors, 'primary'),
|
||||
|
||||
hover-background: lighten($o-fusion-color-primary, 5%),
|
||||
hover-border: lighten($o-fusion-color-primary, 5%),
|
||||
hover-color: $o-black,
|
||||
|
||||
active-background: lighten($o-fusion-color-primary, 10%),
|
||||
active-border: lighten($o-fusion-color-primary, 10%),
|
||||
active-color: $o-black,
|
||||
),
|
||||
"secondary": (
|
||||
background: transparent,
|
||||
border: $o-gray-300,
|
||||
color: $o-gray-900,
|
||||
|
||||
hover-background: $o-gray-300,
|
||||
hover-border: $o-gray-300,
|
||||
hover-color: $o-black,
|
||||
|
||||
active-background: mix($o-action, $o-gray-100, 15%),
|
||||
active-border: lighten($o-action, 10%),
|
||||
active-color: $o-black,
|
||||
),
|
||||
|
||||
"light": (
|
||||
background: transparent,
|
||||
border: $o-gray-300,
|
||||
color: $o-black,
|
||||
|
||||
hover-background: $o-gray-300,
|
||||
hover-border: $o-gray-300,
|
||||
hover-color: $o-black,
|
||||
|
||||
active-background: mix($o-action, $o-gray-100, 15%),
|
||||
active-border: lighten($o-action, 10%),
|
||||
active-color: $o-black,
|
||||
),
|
||||
|
||||
"danger": (
|
||||
background: transparent,
|
||||
border: $o-fusion-color-danger,
|
||||
color: $o-fusion-color-danger,
|
||||
|
||||
hover-background: lighten($o-fusion-color-danger, 5%),
|
||||
hover-border: lighten($o-fusion-color-danger, 5%),
|
||||
hover-color: $o-black,
|
||||
|
||||
active-background: lighten($o-fusion-color-danger, 10%),
|
||||
active-border: lighten($o-fusion-color-danger, 10%),
|
||||
active-color: $o-black,
|
||||
),
|
||||
), $o-btns-bs-outline-override);
|
||||
@@ -0,0 +1,101 @@
|
||||
///
|
||||
/// Fusion Backend Theme - Primary Variables
|
||||
/// Colors are defined in colors_light.scss / colors_dark.scss
|
||||
///
|
||||
|
||||
// = Gray Scale
|
||||
// ============================================================================
|
||||
|
||||
$o-white: #FFF !default;
|
||||
$o-black: #000 !default;
|
||||
|
||||
$o-gray-100: #F9FAFB !default;
|
||||
$o-gray-200: #e7e9ed !default;
|
||||
$o-gray-300: #d8dadd !default;
|
||||
$o-gray-400: #9a9ca5 !default;
|
||||
$o-gray-500: #7c7f89 !default;
|
||||
$o-gray-600: #5f636f !default;
|
||||
$o-gray-700: #374151 !default;
|
||||
$o-gray-800: #1F2937 !default;
|
||||
$o-gray-900: #111827 !default;
|
||||
|
||||
// = Derived from color variables
|
||||
// ============================================================================
|
||||
|
||||
$o-action: $o-fusion-color-primary !default;
|
||||
$o-main-link-color: $o-fusion-color-primary !default;
|
||||
|
||||
// = Sidebar Dimensions
|
||||
// ============================================================================
|
||||
|
||||
$o-fusion-sidebar-large-width: 146px !default;
|
||||
$o-fusion-sidebar-small-width: 46px !default;
|
||||
|
||||
// = Navbar
|
||||
// ============================================================================
|
||||
|
||||
$o-navbar-badge-bg: $o-brand-primary;
|
||||
|
||||
// = Components
|
||||
// ============================================================================
|
||||
|
||||
$o-component-active-color: $o-black !default;
|
||||
$o-component-active-bg: mix($o-action, $o-white, 10%) !default;
|
||||
$o-component-active-border: $o-action !default;
|
||||
|
||||
$o-form-lightsecondary: $o-gray-200 !default;
|
||||
|
||||
// = Inputs
|
||||
// ============================================================================
|
||||
|
||||
$o-input-border-required: $o-black !default;
|
||||
|
||||
// = Buttons
|
||||
// ============================================================================
|
||||
|
||||
$o-btns-bs-override: () !default;
|
||||
$o-btns-bs-override: map-merge((
|
||||
"primary": (
|
||||
background: $o-fusion-color-primary,
|
||||
border: $o-fusion-color-primary,
|
||||
color: $o-white,
|
||||
|
||||
hover-background: darken($o-fusion-color-primary, 10%),
|
||||
hover-border: darken($o-fusion-color-primary, 10%),
|
||||
hover-color: $o-white,
|
||||
|
||||
active-background: mix($o-fusion-color-primary, $o-white, 10%),
|
||||
active-border: $o-fusion-color-primary,
|
||||
active-color: $o-fusion-color-primary,
|
||||
),
|
||||
"secondary": (
|
||||
background: $o-gray-200,
|
||||
border: $o-gray-200,
|
||||
color: $o-gray-900,
|
||||
|
||||
hover-background: $o-gray-300,
|
||||
hover-border: $o-gray-300,
|
||||
hover-color: $o-black,
|
||||
|
||||
active-background: $o-component-active-bg,
|
||||
active-border: $o-component-active-border,
|
||||
active-color: $o-component-active-color,
|
||||
),
|
||||
), $o-btns-bs-override);
|
||||
|
||||
$o-btns-bs-outline-override: () !default;
|
||||
$o-btns-bs-outline-override: map-merge((
|
||||
"secondary": (
|
||||
background: transparent,
|
||||
border: $o-gray-300,
|
||||
color: $o-gray-900,
|
||||
|
||||
hover-background: $o-gray-200,
|
||||
hover-border: $o-gray-300,
|
||||
hover-color: $o-black,
|
||||
|
||||
active-background: mix($o-fusion-color-primary, $o-white, 10%),
|
||||
active-border: $o-fusion-color-primary,
|
||||
active-color: $o-component-active-color,
|
||||
),
|
||||
), $o-btns-bs-outline-override);
|
||||
@@ -0,0 +1,11 @@
|
||||
// = Form View Dark Mode
|
||||
// ============================================================================
|
||||
|
||||
.o_form_view {
|
||||
&:not(.o_field_highlight) .o_field_widget:not(.o_field_invalid):not(.o_field_highlight) .o_input:not(:hover):not(:focus) {
|
||||
--o-input-border-color: #{$o-gray-300};
|
||||
}
|
||||
&:not(.o_field_highlight) .o_required_modifier.o_field_widget:not(.o_field_invalid):not(.o_field_highlight) .o_input:not(:hover):not(:focus) {
|
||||
--o-input-border-color: #{$o-gray-400};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
.o_form_view {
|
||||
&:not(.o_field_highlight) .o_field_widget:not(.o_field_invalid):not(.o_field_highlight) .o_input:not(:hover):not(:focus) {
|
||||
--o-input-border-color: #{$gray-200};
|
||||
}
|
||||
&:not(.o_field_highlight) .o_required_modifier.o_field_widget:not(.o_field_invalid):not(.o_field_highlight) .o_input:not(:hover):not(:focus) {
|
||||
--o-input-border-color: #{$gray-400};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { session } from '@web/session';
|
||||
import { patch } from '@web/core/utils/patch';
|
||||
import { append, createElement, setAttributes } from '@web/core/utils/xml';
|
||||
|
||||
import {FormCompiler} from '@web/views/form/form_compiler';
|
||||
|
||||
patch(FormCompiler.prototype, {
|
||||
compile(node, params) {
|
||||
const res = super.compile(node, params);
|
||||
const chatterContainerHookXml = res.querySelector(
|
||||
'.o_form_renderer > .o-mail-Form-chatter'
|
||||
);
|
||||
if (!chatterContainerHookXml) {
|
||||
return res;
|
||||
}
|
||||
setAttributes(chatterContainerHookXml, {
|
||||
't-ref': 'chatterContainer',
|
||||
});
|
||||
if (session.chatter_position === 'bottom') {
|
||||
const formSheetBgXml = res.querySelector('.o_form_sheet_bg');
|
||||
if (!chatterContainerHookXml || !formSheetBgXml?.parentNode) {
|
||||
return res;
|
||||
}
|
||||
const webClientViewAttachmentViewHookXml = res.querySelector(
|
||||
'.o_attachment_preview'
|
||||
);
|
||||
const chatterContainerXml = chatterContainerHookXml.querySelector(
|
||||
"t[t-component='__comp__.mailComponents.Chatter']"
|
||||
);
|
||||
const sheetBgChatterContainerHookXml = chatterContainerHookXml.cloneNode(true);
|
||||
const sheetBgChatterContainerXml = sheetBgChatterContainerHookXml.querySelector(
|
||||
"t[t-component='__comp__.mailComponents.Chatter']"
|
||||
);
|
||||
sheetBgChatterContainerHookXml.classList.add('o-isInFormSheetBg', 'w-auto');
|
||||
append(formSheetBgXml, sheetBgChatterContainerHookXml);
|
||||
setAttributes(sheetBgChatterContainerXml, {
|
||||
isInFormSheetBg: 'true',
|
||||
isChatterAside: 'false',
|
||||
});
|
||||
setAttributes(chatterContainerXml, {
|
||||
isInFormSheetBg: 'true',
|
||||
isChatterAside: 'false',
|
||||
});
|
||||
setAttributes(chatterContainerHookXml, {
|
||||
't-if': 'false',
|
||||
});
|
||||
if (webClientViewAttachmentViewHookXml) {
|
||||
setAttributes(webClientViewAttachmentViewHookXml, {
|
||||
't-if': 'false',
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setAttributes(chatterContainerHookXml, {
|
||||
't-att-style': '__comp__.chatterState.width ? `width: ${__comp__.chatterState.width}px; max-width: ${__comp__.chatterState.width}px;` : ""',
|
||||
});
|
||||
const chatterContainerResizeHookXml = createElement('span');
|
||||
chatterContainerResizeHookXml.classList.add('o_fusion_chatter_handle');
|
||||
setAttributes(chatterContainerResizeHookXml, {
|
||||
't-on-mousedown.stop.prevent': '__comp__.onStartChatterResize.bind(__comp__)',
|
||||
't-on-dblclick.stop.prevent': '__comp__.onDoubleClickChatterResize.bind(__comp__)',
|
||||
});
|
||||
append(chatterContainerHookXml, chatterContainerResizeHookXml);
|
||||
}
|
||||
return res;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
import { useState, useRef } from '@odoo/owl';
|
||||
import { patch } from '@web/core/utils/patch';
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
|
||||
import { FormRenderer } from '@web/views/form/form_renderer';
|
||||
|
||||
patch(FormRenderer.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.chatterState = useState({
|
||||
width: browser.localStorage.getItem('fusion_backend_theme.width'),
|
||||
});
|
||||
this.chatterContainer = useRef('chatterContainer');
|
||||
},
|
||||
onStartChatterResize(ev) {
|
||||
if (ev.button !== 0) {
|
||||
return;
|
||||
}
|
||||
const initialX = ev.pageX;
|
||||
const chatterElement = this.chatterContainer.el;
|
||||
const initialWidth = chatterElement.offsetWidth;
|
||||
console.log("hi", ev, initialX, initialWidth)
|
||||
const resizeStoppingEvents = [
|
||||
'keydown', 'mousedown', 'mouseup'
|
||||
];
|
||||
const resizePanel = (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const newWidth = Math.min(
|
||||
Math.max(50, initialWidth - (ev.pageX - initialX)),
|
||||
Math.max(chatterElement.parentElement.offsetWidth - 250, 250)
|
||||
);
|
||||
browser.localStorage.setItem('fusion_backend_theme.width', newWidth);
|
||||
this.chatterState.width = newWidth;
|
||||
};
|
||||
const stopResize = (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
if (ev.type === 'mousedown' && ev.button === 0) {
|
||||
return;
|
||||
}
|
||||
document.removeEventListener('mousemove', resizePanel, true);
|
||||
resizeStoppingEvents.forEach((stoppingEvent) => {
|
||||
document.removeEventListener(stoppingEvent, stopResize, true);
|
||||
});
|
||||
document.activeElement.blur();
|
||||
};
|
||||
document.addEventListener('mousemove', resizePanel, true);
|
||||
resizeStoppingEvents.forEach((stoppingEvent) => {
|
||||
document.addEventListener(stoppingEvent, stopResize, true);
|
||||
});
|
||||
},
|
||||
onDoubleClickChatterResize(ev) {
|
||||
browser.localStorage.removeItem('fusion_backend_theme.width');
|
||||
this.chatterState.width = false;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
import { useState, onWillStart, useEffect } from '@odoo/owl';
|
||||
|
||||
import { browser } from '@web/core/browser/browser';
|
||||
import { patch } from '@web/core/utils/patch';
|
||||
import { session } from '@web/session';
|
||||
|
||||
import {ControlPanel} from '@web/search/control_panel/control_panel';
|
||||
|
||||
patch(ControlPanel.prototype, {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
this.autoLoadState = useState({
|
||||
active: false,
|
||||
counter: 0,
|
||||
});
|
||||
onWillStart(() => {
|
||||
if (
|
||||
this.checkAutoLoadAvailability() &&
|
||||
this.getAutoLoadStorageValue()
|
||||
) {
|
||||
this.autoLoadState.active = true;
|
||||
}
|
||||
});
|
||||
useEffect(
|
||||
() => {
|
||||
if (!this.autoLoadState.active) {
|
||||
return;
|
||||
}
|
||||
this.autoLoadState.counter = (
|
||||
this.getAutoLoadRefreshInterval()
|
||||
);
|
||||
const interval = browser.setInterval(
|
||||
() => {
|
||||
this.autoLoadState.counter = (
|
||||
this.autoLoadState.counter ?
|
||||
this.autoLoadState.counter - 1 :
|
||||
this.getAutoLoadRefreshInterval()
|
||||
);
|
||||
if (this.autoLoadState.counter <= 0) {
|
||||
this.autoLoadState.counter = (
|
||||
this.getAutoLoadRefreshInterval()
|
||||
);
|
||||
if (this.pagerProps?.onUpdate) {
|
||||
this.pagerProps.onUpdate({
|
||||
offset: this.pagerProps.offset,
|
||||
limit: this.pagerProps.limit
|
||||
});
|
||||
} else if (typeof this.env.searchModel?.search) {
|
||||
this.env.searchModel.search();
|
||||
}
|
||||
}
|
||||
},
|
||||
1000
|
||||
);
|
||||
return () => browser.clearInterval(interval);
|
||||
},
|
||||
() => [this.autoLoadState.active]
|
||||
);
|
||||
},
|
||||
checkAutoLoadAvailability() {
|
||||
return ['kanban', 'list'].includes(this.env.config.viewType);
|
||||
},
|
||||
getAutoLoadRefreshInterval() {
|
||||
return (session.pager_autoload_interval ?? 30000) / 1000;
|
||||
},
|
||||
getAutoLoadStorageKey() {
|
||||
const keys = [
|
||||
this.env?.config?.actionId ?? '',
|
||||
this.env?.config?.viewType ?? '',
|
||||
this.env?.config?.viewId ?? '',
|
||||
];
|
||||
return `pager_autoload:${keys.join(',')}`;
|
||||
},
|
||||
getAutoLoadStorageValue() {
|
||||
return browser.localStorage.getItem(
|
||||
this.getAutoLoadStorageKey()
|
||||
);
|
||||
},
|
||||
setAutoLoadStorageValue() {
|
||||
browser.localStorage.setItem(
|
||||
this.getAutoLoadStorageKey(), true
|
||||
);
|
||||
},
|
||||
removeAutoLoadStorageValue() {
|
||||
browser.localStorage.removeItem(
|
||||
this.getAutoLoadStorageKey()
|
||||
);
|
||||
},
|
||||
toggleAutoLoad() {
|
||||
this.autoLoadState.active = (
|
||||
!this.autoLoadState.active
|
||||
);
|
||||
if (this.autoLoadState.active) {
|
||||
this.setAutoLoadStorageValue();
|
||||
} else {
|
||||
this.removeAutoLoadStorageValue();
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-inherit="web.ControlPanel" t-inherit-mode="extension">
|
||||
<xpath expr="//Pager/.." position="before">
|
||||
<div
|
||||
t-if="this.checkAutoLoadAvailability()"
|
||||
class="d-inline-flex align-items-center gap-1 align-self-center"
|
||||
>
|
||||
<span
|
||||
t-if="this.autoLoadState.active and this.autoLoadState.counter > 0"
|
||||
class="small text-muted"
|
||||
>
|
||||
<t t-out="this.autoLoadState.counter"/>s
|
||||
</span>
|
||||
<button
|
||||
t-if="!env.isSmall"
|
||||
class="btn btn-link p-0 d-inline-flex align-items-center justify-content-center"
|
||||
type="button"
|
||||
t-on-click.stop="this.toggleAutoLoad"
|
||||
>
|
||||
<i
|
||||
class="fa fa-refresh fa-fw"
|
||||
t-att-class="this.autoLoadState.active ? 'text-info fa-spin' : 'text-muted'"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Component } from '@odoo/owl';
|
||||
import { registry } from '@web/core/registry';
|
||||
import { DropdownItem } from '@web/core/dropdown/dropdown_item';
|
||||
|
||||
const cogMenuRegistry = registry.category('cogMenu');
|
||||
|
||||
export class CollapseAll extends Component {
|
||||
|
||||
static template = 'fusion_backend_theme.CollapseAll';
|
||||
static components = { DropdownItem };
|
||||
static props = {};
|
||||
|
||||
async onCollapseButtonClicked() {
|
||||
let groups = this.env.model.root.groups;
|
||||
while (groups.length) {
|
||||
const unfoldedGroups = groups.filter(
|
||||
(group) => !group._config.isFolded
|
||||
);
|
||||
if (unfoldedGroups.length) {
|
||||
for (const group of unfoldedGroups) {
|
||||
await group.toggle();
|
||||
}
|
||||
}
|
||||
const subGroups = unfoldedGroups.map(
|
||||
(group) => group.list.groups || []
|
||||
);
|
||||
groups = subGroups.reduce(
|
||||
(a, b) => a.concat(b), []
|
||||
);
|
||||
}
|
||||
await this.env.model.root.load();
|
||||
this.env.model.notify();
|
||||
}
|
||||
}
|
||||
|
||||
export const collapseAllItem = {
|
||||
Component: CollapseAll,
|
||||
groupNumber: 15,
|
||||
isDisplayed: async (env) => (
|
||||
['kanban', 'list'].includes(env.config.viewType) &&
|
||||
env.model.root.isGrouped
|
||||
)
|
||||
};
|
||||
|
||||
cogMenuRegistry.add('collapse-all-menu', collapseAllItem, { sequence: 2 });
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="fusion_backend_theme.CollapseGroups">
|
||||
<DropdownItem
|
||||
class="'o_fusion_collapse_groups'"
|
||||
onSelected.bind="onCollapseButtonClicked"
|
||||
>
|
||||
<i class="fa fa-fw fa-compress me-1"/>Collapse All
|
||||
</DropdownItem>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Component } from '@odoo/owl';
|
||||
import { registry } from '@web/core/registry';
|
||||
import { DropdownItem } from '@web/core/dropdown/dropdown_item';
|
||||
|
||||
const cogMenuRegistry = registry.category('cogMenu');
|
||||
|
||||
export class ExpandAll extends Component {
|
||||
|
||||
static template = 'fusion_backend_theme.ExpandAll';
|
||||
static components = { DropdownItem };
|
||||
static props = {};
|
||||
|
||||
async onExpandButtonClicked() {
|
||||
let groups = this.env.model.root.groups;
|
||||
while (groups.length) {
|
||||
const foldedGroups = groups.filter(
|
||||
(group) => group._config.isFolded
|
||||
);
|
||||
if (foldedGroups.length) {
|
||||
for (const group of foldedGroups) {
|
||||
await group.toggle();
|
||||
}
|
||||
}
|
||||
const subGroups = foldedGroups.map(
|
||||
(group) => group.list.groups || []
|
||||
);
|
||||
groups = subGroups.reduce(
|
||||
(a, b) => a.concat(b), []
|
||||
);
|
||||
}
|
||||
await this.env.model.root.load();
|
||||
this.env.model.notify();
|
||||
}
|
||||
}
|
||||
|
||||
export const expandAllItem = {
|
||||
Component: ExpandAll,
|
||||
groupNumber: 15,
|
||||
isDisplayed: async (env) => (
|
||||
['kanban', 'list'].includes(env.config.viewType) &&
|
||||
env.model.root.isGrouped
|
||||
)
|
||||
};
|
||||
|
||||
cogMenuRegistry.add('expand-all-menu', expandAllItem, { sequence: 1 });
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="fusion_backend_theme.ExpandGroups">
|
||||
<DropdownItem
|
||||
class="'o_fusion_expand_groups'"
|
||||
onSelected.bind="onExpandButtonClicked"
|
||||
>
|
||||
<i class="fa fa-fw fa-expand me-1"/>Expand All
|
||||
</DropdownItem>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,26 @@
|
||||
// = App Switcher Dark Mode
|
||||
// ============================================================================
|
||||
|
||||
.o_fusion_app_switcher.dropdown-menu {
|
||||
--o-fusion-appsmenu-bg-color: #000511;
|
||||
--o-fusion-app-icon-bg: rgba(255, 255, 255, 0.95);
|
||||
--o-fusion-app-icon-inset-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.1);
|
||||
--o-fusion-appsmenu-caption-color: #E4E4E4;
|
||||
|
||||
scrollbar-color: rgba(255, 255, 255, .3) transparent;
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, .3);
|
||||
}
|
||||
|
||||
.o_app > a .o_fusion_app_name {
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, .75),
|
||||
0 2px 5px rgba(0, 0, 0, .05),
|
||||
0 0 5px rgba(0, 0, 0, .05);
|
||||
}
|
||||
|
||||
.o_app > a.o_fusion_app_focused {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
outline: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { useEffect, useRef } from "@odoo/owl";
|
||||
import { user } from "@web/core/user";
|
||||
import { url } from "@web/core/utils/urls";
|
||||
import { useBus, useService } from "@web/core/utils/hooks";
|
||||
import { useSortable } from "@web/core/utils/sortable_owl";
|
||||
import { useHotkey } from "@web/core/hotkeys/hotkey_hook";
|
||||
|
||||
import { Dropdown } from "@web/core/dropdown/dropdown";
|
||||
|
||||
export class AppSwitcher extends Dropdown {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.commandPaletteOpen = false;
|
||||
this.commandService = useService("command");
|
||||
this.ui = useService("ui");
|
||||
this.sortableRef = useRef("sortableArea");
|
||||
this.state.focusedIndex = null;
|
||||
if (user.activeCompany.has_background_image) {
|
||||
this.imageUrl = url('/web/image', {
|
||||
model: 'res.company',
|
||||
field: 'background_image',
|
||||
id: user.activeCompany.id,
|
||||
});
|
||||
} else {
|
||||
this.imageUrl = '/fusion_backend_theme/static/src/img/background.png';
|
||||
}
|
||||
|
||||
// Drag-and-drop reordering
|
||||
useSortable({
|
||||
enable: () => this.state.isOpen,
|
||||
ref: this.sortableRef,
|
||||
elements: ".o_app",
|
||||
cursor: "move",
|
||||
delay: 500,
|
||||
tolerance: 10,
|
||||
onDrop: (params) => this._onSortDrop(params),
|
||||
});
|
||||
|
||||
useEffect(
|
||||
(isOpen) => {
|
||||
if (isOpen) {
|
||||
this.state.focusedIndex = null;
|
||||
const openMainPalette = (ev) => {
|
||||
if (
|
||||
!this.commandPaletteOpen &&
|
||||
ev.key.length === 1 &&
|
||||
!ev.ctrlKey &&
|
||||
!ev.altKey
|
||||
) {
|
||||
this.commandService.openMainPalette(
|
||||
{ searchValue: `/${ev.key}` },
|
||||
() => { this.commandPaletteOpen = false; }
|
||||
);
|
||||
this.commandPaletteOpen = true;
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", openMainPalette);
|
||||
return () => {
|
||||
window.removeEventListener("keydown", openMainPalette);
|
||||
this.commandPaletteOpen = false;
|
||||
};
|
||||
}
|
||||
},
|
||||
() => [this.state.isOpen]
|
||||
);
|
||||
useBus(this.env.bus, "ACTION_MANAGER:UI-UPDATED", () => {
|
||||
if (this.state.close) {
|
||||
this.state.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Keyboard navigation
|
||||
this._registerHotkeys();
|
||||
}
|
||||
|
||||
get maxIconNumber() {
|
||||
const w = window.innerWidth;
|
||||
if (w < 576) {
|
||||
return 3;
|
||||
} else if (w < 768) {
|
||||
return 4;
|
||||
} else {
|
||||
return 6;
|
||||
}
|
||||
}
|
||||
|
||||
_registerHotkeys() {
|
||||
const hotkeys = [
|
||||
["ArrowDown", () => this._updateFocusedIndex("nextLine")],
|
||||
["ArrowRight", () => this._updateFocusedIndex("nextColumn")],
|
||||
["ArrowUp", () => this._updateFocusedIndex("previousLine")],
|
||||
["ArrowLeft", () => this._updateFocusedIndex("previousColumn")],
|
||||
["Tab", () => this._updateFocusedIndex("nextElem")],
|
||||
["shift+Tab", () => this._updateFocusedIndex("previousElem")],
|
||||
["Escape", () => {
|
||||
if (this.state.close) {
|
||||
this.state.close();
|
||||
}
|
||||
}],
|
||||
];
|
||||
for (const [hotkey, callback] of hotkeys) {
|
||||
useHotkey(hotkey, callback, { allowRepeat: true });
|
||||
}
|
||||
}
|
||||
|
||||
_updateFocusedIndex(cmd) {
|
||||
if (!this.state.isOpen) {
|
||||
return;
|
||||
}
|
||||
const apps = this._getAppElements();
|
||||
const nbrApps = apps.length;
|
||||
const lastIndex = nbrApps - 1;
|
||||
const focusedIndex = this.state.focusedIndex;
|
||||
if (lastIndex < 0) {
|
||||
return;
|
||||
}
|
||||
if (focusedIndex === null) {
|
||||
this.state.focusedIndex = 0;
|
||||
return;
|
||||
}
|
||||
const lineNumber = Math.ceil(nbrApps / this.maxIconNumber);
|
||||
const currentLine = Math.ceil((focusedIndex + 1) / this.maxIconNumber);
|
||||
let newIndex;
|
||||
switch (cmd) {
|
||||
case "previousElem":
|
||||
newIndex = focusedIndex - 1;
|
||||
break;
|
||||
case "nextElem":
|
||||
newIndex = focusedIndex + 1;
|
||||
break;
|
||||
case "previousColumn":
|
||||
if (focusedIndex % this.maxIconNumber) {
|
||||
newIndex = focusedIndex - 1;
|
||||
} else {
|
||||
newIndex = focusedIndex + Math.min(lastIndex - focusedIndex, this.maxIconNumber - 1);
|
||||
}
|
||||
break;
|
||||
case "nextColumn":
|
||||
if (focusedIndex === lastIndex || (focusedIndex + 1) % this.maxIconNumber === 0) {
|
||||
newIndex = (currentLine - 1) * this.maxIconNumber;
|
||||
} else {
|
||||
newIndex = focusedIndex + 1;
|
||||
}
|
||||
break;
|
||||
case "previousLine":
|
||||
if (currentLine === 1) {
|
||||
newIndex = focusedIndex + (lineNumber - 1) * this.maxIconNumber;
|
||||
if (newIndex > lastIndex) {
|
||||
newIndex = lastIndex;
|
||||
}
|
||||
} else {
|
||||
newIndex = focusedIndex - this.maxIconNumber;
|
||||
}
|
||||
break;
|
||||
case "nextLine":
|
||||
if (currentLine === lineNumber) {
|
||||
newIndex = focusedIndex % this.maxIconNumber;
|
||||
} else {
|
||||
newIndex = focusedIndex + Math.min(this.maxIconNumber, lastIndex - focusedIndex);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (newIndex < 0) {
|
||||
newIndex = lastIndex;
|
||||
} else if (newIndex > lastIndex) {
|
||||
newIndex = 0;
|
||||
}
|
||||
this.state.focusedIndex = newIndex;
|
||||
}
|
||||
|
||||
_getAppElements() {
|
||||
if (this.menuRef && this.menuRef.el) {
|
||||
return [...this.menuRef.el.querySelectorAll('.o_app')];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
_onSortDrop({ element, previous }) {
|
||||
const apps = this._getAppElements();
|
||||
const order = apps.map((el) => {
|
||||
const link = el.querySelector('a[data-menu-xmlid]') || el;
|
||||
return link.dataset.menuXmlid;
|
||||
}).filter(Boolean);
|
||||
|
||||
const elementLink = element.querySelector('a[data-menu-xmlid]') || element;
|
||||
const elementId = elementLink.dataset.menuXmlid;
|
||||
const elementIndex = order.indexOf(elementId);
|
||||
if (elementIndex === -1) {
|
||||
return;
|
||||
}
|
||||
order.splice(elementIndex, 1);
|
||||
if (previous) {
|
||||
const prevLink = previous.querySelector('a[data-menu-xmlid]') || previous;
|
||||
const prevIndex = order.indexOf(prevLink.dataset.menuXmlid);
|
||||
order.splice(prevIndex + 1, 0, elementId);
|
||||
} else {
|
||||
order.splice(0, 0, elementId);
|
||||
}
|
||||
user.setUserSettings("homemenu_config", JSON.stringify(order));
|
||||
}
|
||||
|
||||
onOpened() {
|
||||
super.onOpened();
|
||||
if (this.menuRef && this.menuRef.el) {
|
||||
this.menuRef.el.style.backgroundImage = `url('${this.imageUrl}')`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
.o_navbar_apps_menu .dropdown-toggle {
|
||||
padding: 0px 14px !important;
|
||||
}
|
||||
|
||||
.o_fusion_app_switcher.dropdown-menu {
|
||||
display: flex !important;
|
||||
flex-direction: row !important;
|
||||
flex-wrap: wrap !important;
|
||||
align-content: flex-start;
|
||||
right: 0 !important;
|
||||
left: 0 !important;
|
||||
bottom: 0 !important;
|
||||
max-height: 100vh;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
user-select: none;
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
background: {
|
||||
size: cover;
|
||||
repeat: no-repeat;
|
||||
position: center;
|
||||
color: var(--o-fusion-appsmenu-bg-color, #{$o-gray-200});
|
||||
}
|
||||
scrollbar-color: rgba(255, 255, 255, .4) transparent;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, .4);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
padding: {
|
||||
left: 20vw;
|
||||
right: 20vw;
|
||||
}
|
||||
}
|
||||
|
||||
.o_fusion_apps_grid {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.o_app {
|
||||
margin-top: 20px;
|
||||
width: percentage(1/3);
|
||||
background: none !important;
|
||||
@include media-breakpoint-up(sm) {
|
||||
width: percentage(1/4);
|
||||
}
|
||||
@include media-breakpoint-up(md) {
|
||||
width: percentage(1/6);
|
||||
}
|
||||
> a {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
border-radius: 0.375rem;
|
||||
padding: 8px;
|
||||
transition: background-color 0.15s ease;
|
||||
|
||||
.o_fusion_app_icon {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
max-width: 70px;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 0.375rem;
|
||||
background-color: var(--o-fusion-app-icon-bg, rgba(255, 255, 255, 1));
|
||||
object-fit: cover;
|
||||
transform-origin: center bottom;
|
||||
transition: box-shadow ease-in 0.1s, transform ease-in 0.1s;
|
||||
box-shadow:
|
||||
var(--o-fusion-app-icon-inset-shadow, inset 0 0 0 1px rgba(0, 0, 0, 0.2)),
|
||||
0 1px 1px rgba(0, 0, 0, 0.02),
|
||||
0 2px 2px rgba(0, 0, 0, 0.02),
|
||||
0 4px 4px rgba(0, 0, 0, 0.02),
|
||||
0 8px 8px rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
.o_fusion_app_name {
|
||||
color: var(--o-fusion-appsmenu-caption-color, #{$o-fusion-color-appsmenu-text});
|
||||
text-align: center;
|
||||
margin-top: 8px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&.o_fusion_app_focused {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
outline: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
> a .o_fusion_app_icon {
|
||||
box-shadow:
|
||||
var(--o-fusion-app-icon-inset-shadow, inset 0 0 0 1px rgba(0, 0, 0, 0.2)),
|
||||
0 2px 2px rgba(0, 0, 0, 0.03),
|
||||
0 4px 4px rgba(0, 0, 0, 0.03),
|
||||
0 8px 8px rgba(0, 0, 0, 0.03),
|
||||
0 12px 12px rgba(0, 0, 0, 0.03),
|
||||
0 24px 24px rgba(0, 0, 0, 0.03);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
&:active {
|
||||
> a .o_fusion_app_icon {
|
||||
transform: translateY(-2px) scale(.98);
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Drag-and-drop visual feedback
|
||||
.o_dragged_app,
|
||||
.o_app.o_dragging {
|
||||
transition: transform 0.5s;
|
||||
transform: rotate(6deg);
|
||||
> a .o_fusion_app_icon {
|
||||
box-shadow: 0 8px 15px -10px black;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<!-- Replace the apps menu dropdown with app switcher -->
|
||||
<t
|
||||
t-name="fusion_backend_theme.NavBar.AppSwitcher"
|
||||
t-inherit="web.NavBar.AppsMenu"
|
||||
t-inherit-mode="extension"
|
||||
>
|
||||
<xpath expr="//Dropdown" position="replace">
|
||||
<AppSwitcher menuClass="'o_fusion_app_switcher'">
|
||||
<button data-hotkey="h" title="Home Menu">
|
||||
<i class="oi oi-apps" />
|
||||
</button>
|
||||
<t t-set-slot="content">
|
||||
<div t-ref="sortableArea" class="o_fusion_apps_grid d-flex flex-wrap align-content-start w-100">
|
||||
<DropdownItem
|
||||
t-foreach="this.appMenuService.getAppsMenuItems()"
|
||||
t-as="app"
|
||||
t-key="app.id"
|
||||
class="'o_app o_draggable'"
|
||||
attrs="{ href: app.href, 'data-menu-xmlid': app.xmlid, 'data-section': app.id }"
|
||||
onSelected="() => this.onNavBarDropdownItemSelection(app)"
|
||||
closingMode="'none'"
|
||||
>
|
||||
<a
|
||||
t-att-href="app.href"
|
||||
t-on-click.prevent=""
|
||||
t-att-class="{ 'o_fusion_app_focused': state.focusedIndex === app_index }"
|
||||
>
|
||||
<img
|
||||
t-if="app.webIconData"
|
||||
class="o_fusion_app_icon"
|
||||
t-att-src="app.webIconData"
|
||||
/>
|
||||
<img
|
||||
t-else=""
|
||||
class="o_fusion_app_icon"
|
||||
src="/base/static/description/icon.png"
|
||||
/>
|
||||
<span class="o_fusion_app_name">
|
||||
<t t-out="app.label"/>
|
||||
</span>
|
||||
</a>
|
||||
</DropdownItem>
|
||||
</div>
|
||||
</t>
|
||||
</AppSwitcher>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,11 @@
|
||||
// = Navbar Dark Mode
|
||||
// ============================================================================
|
||||
|
||||
.o_main_navbar {
|
||||
--o-fusion-navbar-active-color: #E4E4E4;
|
||||
--o-fusion-navbar-active-border: #5D8DA8;
|
||||
--o-fusion-navbar-active-bg: rgba(93, 141, 168, 0.15);
|
||||
--o-fusion-navbar-hover-bg: #3C3E4B;
|
||||
--o-fusion-navbar-focus-bg: #3C3E4B;
|
||||
--o-fusion-navbar-toggle-color: #5D8DA8;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { patch } from '@web/core/utils/patch';
|
||||
import { useService } from '@web/core/utils/hooks';
|
||||
import { useRef } from '@odoo/owl';
|
||||
|
||||
import { NavBar } from '@web/webclient/navbar/navbar';
|
||||
import { AppSwitcher } from "@fusion_backend_theme/webclient/app_switcher/app_switcher";
|
||||
|
||||
patch(NavBar.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.appMenuService = useService('app_menu');
|
||||
this.navRef = useRef("nav");
|
||||
},
|
||||
});
|
||||
|
||||
patch(NavBar, {
|
||||
components: {
|
||||
...NavBar.components,
|
||||
AppSwitcher,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
// = Main Navbar
|
||||
// ============================================================================
|
||||
|
||||
.o_main_navbar {
|
||||
border-bottom: none !important;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
// Ensure consistent navbar entry colors for dark brand background
|
||||
--NavBar-entry-color: rgba(255, 255, 255, 0.9);
|
||||
--NavBar-entry-color--active: #{$o-white};
|
||||
--NavBar-entry-borderColor-active: #{$o-fusion-color-primary};
|
||||
--NavBar-entry-backgroundColor: #{$o-fusion-color-brand};
|
||||
--NavBar-entry-backgroundColor--active: #{darken($o-fusion-color-brand, 5%)};
|
||||
--NavBar-entry-backgroundColor--hover: #{lighten($o-fusion-color-brand, 8%)};
|
||||
--NavBar-entry-backgroundColor--focus: #{lighten($o-fusion-color-brand, 8%)};
|
||||
|
||||
.o_menu_toggle {
|
||||
color: var(--NavBar-entry-color, rgba(255, 255, 255, 0.9));
|
||||
}
|
||||
|
||||
.o_menu_brand {
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.o_menu_systray .badge {
|
||||
font-size: 0.65em;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure Discuss keeps the dark navbar consistent with all other apps
|
||||
.o_web_client:has(.o-mail-Discuss) {
|
||||
.o_main_navbar {
|
||||
background: $o-fusion-color-brand !important;
|
||||
--NavBar-entry-backgroundColor: #{$o-fusion-color-brand};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<!-- Extend the NavBar to add the nav ref -->
|
||||
<t
|
||||
t-name="fusion_backend_theme.NavBar"
|
||||
t-inherit="web.NavBar"
|
||||
t-inherit-mode="extension"
|
||||
>
|
||||
<xpath expr="//nav" position="attributes">
|
||||
<attribute name="t-ref">nav</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,33 @@
|
||||
import { registry } from "@web/core/registry";
|
||||
import { user } from "@web/core/user";
|
||||
|
||||
import { computeAppsAndMenuItems, reorderApps } from "@web/webclient/menus/menu_helpers";
|
||||
|
||||
export const appMenuService = {
|
||||
dependencies: ["menu"],
|
||||
async start(env, { menu }) {
|
||||
return {
|
||||
getCurrentApp () {
|
||||
return menu.getCurrentApp();
|
||||
},
|
||||
getAppsMenuItems() {
|
||||
const menuItems = computeAppsAndMenuItems(
|
||||
menu.getMenuAsTree('root')
|
||||
)
|
||||
const apps = menuItems.apps;
|
||||
const menuConfig = JSON.parse(
|
||||
user.settings?.homemenu_config || 'null'
|
||||
);
|
||||
if (menuConfig) {
|
||||
reorderApps(apps, menuConfig);
|
||||
}
|
||||
return apps;
|
||||
},
|
||||
selectApp(app) {
|
||||
menu.selectMenu(app);
|
||||
}
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
registry.category("services").add("app_menu", appMenuService);
|
||||
@@ -0,0 +1,6 @@
|
||||
// = Sidebar Dark Mode
|
||||
// ============================================================================
|
||||
|
||||
// Dark mode sidebar colors ($o-fusion-color-sidebar-text, $o-fusion-color-sidebar-background)
|
||||
// are defined in primary_variables.dark.scss and automatically applied
|
||||
// to the sidebar component styles.
|
||||
@@ -0,0 +1,34 @@
|
||||
import { url } from '@web/core/utils/urls';
|
||||
import { useService } from '@web/core/utils/hooks';
|
||||
import { user } from "@web/core/user";
|
||||
|
||||
import { Component, onWillUnmount } from '@odoo/owl';
|
||||
|
||||
export class Sidebar extends Component {
|
||||
static template = 'fusion_backend_theme.Sidebar';
|
||||
static props = {};
|
||||
setup() {
|
||||
this.appMenuService = useService('app_menu');
|
||||
if (user.activeCompany.has_appsbar_image) {
|
||||
this.sidebarImageUrl = url('/web/image', {
|
||||
model: 'res.company',
|
||||
field: 'appbar_image',
|
||||
id: user.activeCompany.id,
|
||||
});
|
||||
}
|
||||
const renderAfterMenuChange = () => {
|
||||
this.render();
|
||||
};
|
||||
this.env.bus.addEventListener(
|
||||
'MENUS:APP-CHANGED', renderAfterMenuChange
|
||||
);
|
||||
onWillUnmount(() => {
|
||||
this.env.bus.removeEventListener(
|
||||
'MENUS:APP-CHANGED', renderAfterMenuChange
|
||||
);
|
||||
});
|
||||
}
|
||||
_onAppClick(app) {
|
||||
return this.appMenuService.selectApp(app);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
.o_fusion_sidebar_panel {
|
||||
@include o-fusion-disable-scrollbar();
|
||||
background-color: $o-fusion-color-sidebar-background;
|
||||
width: var(--o-fusion-sidebar-width, 0);
|
||||
overflow-y: auto;
|
||||
.o_fusion_sidebar {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
white-space: nowrap;
|
||||
.o_fusion_sidebar_menu {
|
||||
padding: 0;
|
||||
> li > a {
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 300;
|
||||
overflow: hidden;
|
||||
padding: 8px 11px;
|
||||
text-decoration: none;
|
||||
color: $o-fusion-color-sidebar-text;
|
||||
text-overflow: ellipsis;
|
||||
.o_fusion_sidebar_icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
> li.active > a {
|
||||
background: $o-fusion-color-sidebar-active;
|
||||
}
|
||||
> li:hover > a {
|
||||
background: $o-fusion-color-sidebar-active;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_fusion_sidebar_type_large {
|
||||
--o-fusion-sidebar-width: #{$o-fusion-sidebar-large-width};
|
||||
}
|
||||
|
||||
.o_fusion_sidebar_type_small {
|
||||
--o-fusion-sidebar-width: #{$o-fusion-sidebar-small-width};
|
||||
.o_fusion_sidebar_name {
|
||||
display: none;
|
||||
}
|
||||
.o_fusion_sidebar_icon {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
.o_fusion_sidebar_logo {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.o_fusion_sidebar_type_invisible {
|
||||
--o-fusion-sidebar-width: 0;
|
||||
}
|
||||
|
||||
.editor_has_snippets_hide_backend_navbar,
|
||||
.o_home_menu_background,
|
||||
.o_fullscreen {
|
||||
--o-fusion-sidebar-width: 0;
|
||||
}
|
||||
|
||||
.editor_has_snippets_hide_backend_navbar .o_fusion_sidebar_panel {
|
||||
transition: width 300ms;
|
||||
}
|
||||
|
||||
@include media-breakpoint-only(md) {
|
||||
.o_fusion_sidebar_type_large {
|
||||
--o-fusion-sidebar-width: #{$o-fusion-sidebar-small-width};
|
||||
.o_fusion_sidebar_name {
|
||||
display: none;
|
||||
}
|
||||
.o_fusion_sidebar_icon {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
.o_fusion_sidebar_logo {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
.o_fusion_sidebar_type_large, .o_fusion_sidebar_type_small {
|
||||
--o-fusion-sidebar-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
.o_fusion_sidebar_type_large, .o_fusion_sidebar_type_small {
|
||||
--o-fusion-sidebar-width: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="fusion_backend_theme.Sidebar">
|
||||
<div class="o_fusion_sidebar_panel">
|
||||
<div class="o_fusion_sidebar">
|
||||
<ul class="o_fusion_sidebar_menu">
|
||||
<t t-foreach="this.appMenuService.getAppsMenuItems()" t-as="app" t-key="app.id">
|
||||
<li t-attf-class="nav-item {{ app.id === this.appMenuService.getCurrentApp()?.id ? 'active' : '' }}">
|
||||
<a
|
||||
t-att-href="app.href"
|
||||
t-att-data-menu-id="app.id"
|
||||
t-att-data-menu-xmlid="app.xmlid"
|
||||
t-att-data-action-id="app.actionID"
|
||||
t-on-click.prevent="() => this._onAppClick(app)"
|
||||
class="nav-link"
|
||||
role="menuitem"
|
||||
>
|
||||
<img
|
||||
t-if="app.webIconData"
|
||||
class="o_fusion_sidebar_icon"
|
||||
t-att-src="app.webIconData"
|
||||
/>
|
||||
<img
|
||||
t-else=""
|
||||
class="o_fusion_sidebar_icon"
|
||||
src="/base/static/description/icon.png"
|
||||
/>
|
||||
<span class="o_fusion_sidebar_name">
|
||||
<t t-out="app.label"/>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</t>
|
||||
</ul>
|
||||
<div t-if="sidebarImageUrl" class="o_fusion_sidebar_logo p-2">
|
||||
<img class="img-fluid mx-auto" t-att-src="sidebarImageUrl" alt="Logo"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,11 @@
|
||||
import { patch } from '@web/core/utils/patch';
|
||||
|
||||
import { WebClient } from '@web/webclient/webclient';
|
||||
import { Sidebar } from '@fusion_backend_theme/webclient/sidebar/sidebar';
|
||||
|
||||
patch(WebClient, {
|
||||
components: {
|
||||
...WebClient.components,
|
||||
Sidebar,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
.o_web_client {
|
||||
display: grid !important;
|
||||
grid-template-areas:
|
||||
"banner banner"
|
||||
"navbar navbar"
|
||||
"sidebar content"
|
||||
"components components";
|
||||
grid-template-rows: auto auto 1fr auto;
|
||||
grid-template-columns: auto 1fr;
|
||||
> div:has(#oe_neutralize_banner) {
|
||||
grid-area: banner;
|
||||
}
|
||||
> .o_navbar {
|
||||
grid-area: navbar;
|
||||
}
|
||||
> .o_fusion_sidebar_panel {
|
||||
grid-area: sidebar;
|
||||
}
|
||||
> .o_action_manager {
|
||||
grid-area: content;
|
||||
}
|
||||
> .o-main-components-container {
|
||||
grid-area: components;
|
||||
}
|
||||
> iframe {
|
||||
grid-column: 1 / -1;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t
|
||||
t-name="fusion_backend_theme.WebClient"
|
||||
t-inherit="web.WebClient"
|
||||
t-inherit-mode="extension"
|
||||
>
|
||||
<xpath expr="//NavBar" position="after">
|
||||
<Sidebar/>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,52 @@
|
||||
import { expect, test } from '@odoo/hoot';
|
||||
import {
|
||||
models,
|
||||
fields,
|
||||
defineModels,
|
||||
mountView,
|
||||
contains,
|
||||
onRpc,
|
||||
} from '@web/../tests/web_test_helpers';
|
||||
|
||||
class Category extends models.Model {
|
||||
name = fields.Char();
|
||||
_records = [
|
||||
{ id: 1, name: 'Cat A' },
|
||||
{ id: 2, name: 'Cat B' },
|
||||
];
|
||||
}
|
||||
|
||||
class Product extends models.Model {
|
||||
name = fields.Char();
|
||||
category_id = fields.Many2one({
|
||||
relation: 'category',
|
||||
});
|
||||
_records = [
|
||||
{ id: 1, name: 'A-1', category_id: 1 },
|
||||
{ id: 2, name: 'A-2', category_id: 1 },
|
||||
{ id: 3, name: 'B-1', category_id: 2 },
|
||||
];
|
||||
}
|
||||
|
||||
defineModels({ Category, Product });
|
||||
|
||||
onRpc('has_group', () => true);
|
||||
|
||||
test('expand/collapse all groups from cog menu in grouped list', async () => {
|
||||
await mountView({
|
||||
type: 'list',
|
||||
resModel: 'product',
|
||||
groupBy: ['category_id'],
|
||||
arch: `<list string='Products'><field name='name'/><field name='category_id'/></list>`,
|
||||
});
|
||||
expect('.o_group_header').toHaveCount(2);
|
||||
await contains('.o_cp_action_menus .dropdown-toggle').click();
|
||||
expect('.o_fusion_expand_groups').toHaveCount(1);
|
||||
expect('.o_fusion_collapse_groups').toHaveCount(1);
|
||||
await contains('.o_fusion_expand_groups').click();
|
||||
expect('tbody tr.o_data_row').toHaveCount(3);
|
||||
await contains('.o_cp_action_menus .dropdown-toggle').click();
|
||||
await contains('.o_fusion_collapse_groups').click();
|
||||
expect('tbody tr.o_data_row').toHaveCount(0);
|
||||
expect('.o_group_header').toHaveCount(2);
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { expect, test } from '@odoo/hoot';
|
||||
import {
|
||||
models,
|
||||
fields,
|
||||
defineModels,
|
||||
mountView,
|
||||
onRpc,
|
||||
contains,
|
||||
} from '@web/../tests/web_test_helpers';
|
||||
|
||||
class Product extends models.Model {
|
||||
name = fields.Char();
|
||||
_records = [
|
||||
{ id: 1, name: 'Test 1' },
|
||||
{ id: 2, name: 'Test 2' },
|
||||
];
|
||||
}
|
||||
defineModels({ Product });
|
||||
|
||||
onRpc('has_group', () => true);
|
||||
|
||||
test(
|
||||
'refresh toggle switches active state',
|
||||
async () => {
|
||||
await mountView({
|
||||
type: 'list',
|
||||
resModel: 'product',
|
||||
arch: `<list><field name='name'/></list>`,
|
||||
});
|
||||
expect('.o_control_panel i.fa-refresh').toHaveClass('text-muted');
|
||||
expect('.o_control_panel i.fa-refresh').not.toHaveClass('fa-spin');
|
||||
await contains('.o_control_panel i.fa-refresh').click();
|
||||
expect('.o_control_panel i.fa-refresh').toHaveClass('fa-spin');
|
||||
expect('.o_control_panel i.fa-refresh').toHaveClass('text-info');
|
||||
expect('.o_control_panel i.fa-refresh').not.toHaveClass('text-muted');
|
||||
await contains('.o_control_panel i.fa-refresh').click();
|
||||
expect('.o_control_panel i.fa-refresh').not.toHaveClass('fa-spin');
|
||||
expect('.o_control_panel i.fa-refresh').toHaveClass('text-muted');
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
|
||||
<template id="layout" inherit_id="web.layout">
|
||||
<xpath expr="//link[@rel='shortcut icon']" position="before">
|
||||
<t
|
||||
t-set="x_icon"
|
||||
t-value="x_icon or '/web/image/res.company/%s/favicon' % request.env.company.id"
|
||||
/>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<odoo>
|
||||
|
||||
<template id="webclient_bootstrap" name="Web Client" inherit_id="web.webclient_bootstrap">
|
||||
<xpath expr="//meta[@name='theme-color']" position="replace">
|
||||
<meta name="theme-color" t-att-content="'#1B1D26' if color_scheme == 'dark' else '#243742'"/>
|
||||
</xpath>
|
||||
<xpath expr="//t[@t-set='body_classname']" position="after">
|
||||
<t t-set="body_sidebar_classname" t-value="'o_fusion_sidebar_type_' + request.env.user.sidebar_type or 'large'"/>
|
||||
<t t-set="body_classname" t-value="'%s %s' % (body_classname, body_sidebar_classname)"/>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,154 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<odoo>
|
||||
|
||||
<record id="view_res_config_settings_form" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.form</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="base_setup.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//block[@id='user_default_rights']" position="before">
|
||||
<block title="Backend Theme" id="theme_settings">
|
||||
<setting string="Theme Colors" help="Customize the look and feel of the theme">
|
||||
<div class="w-50 row">
|
||||
<label for="color_brand_light" string="Brand" class="d-block w-75 py-2"/>
|
||||
<field name="color_brand_light" class="d-block w-25 p-0 m-0" widget="color"/>
|
||||
</div>
|
||||
<div class="w-50 row">
|
||||
<label for="color_primary_light" string="Primary" class="d-block w-75 py-2"/>
|
||||
<field name="color_primary_light" class="d-block w-25 p-0 m-0" widget="color"/>
|
||||
</div>
|
||||
<div class="w-50 row">
|
||||
<label for="theme_color_sidebar_text" string="Sidebar Text" class="d-block w-75 py-2"/>
|
||||
<field name="theme_color_sidebar_text" class="d-block w-25 p-0 m-0" widget="color"/>
|
||||
</div>
|
||||
<div class="w-50 row">
|
||||
<label for="theme_color_sidebar_active" string="Sidebar Active" class="d-block w-75 py-2"/>
|
||||
<field name="theme_color_sidebar_active" class="d-block w-25 p-0 m-0" widget="color"/>
|
||||
</div>
|
||||
<div class="w-50 row">
|
||||
<label for="theme_color_sidebar_background" string="Sidebar Background" class="d-block w-75 py-2"/>
|
||||
<field name="theme_color_sidebar_background" class="d-block w-25 p-0 m-0" widget="color"/>
|
||||
</div>
|
||||
<div class="w-50 row">
|
||||
<label for="theme_color_appsmenu_text" string="Apps Menu Text" class="d-block w-75 py-2"/>
|
||||
<field name="theme_color_appsmenu_text" class="d-block w-25 p-0 m-0" widget="color"/>
|
||||
</div>
|
||||
</setting>
|
||||
<setting string="Context Colors" help="Customize context colors of the system">
|
||||
<div class="w-50 row">
|
||||
<label for="color_info_light" string="Info" class="d-block w-75 py-2"/>
|
||||
<field name="color_info_light" class="d-block w-25 p-0 m-0" widget="color"/>
|
||||
</div>
|
||||
<div class="w-50 row">
|
||||
<label for="color_success_light" string="Success" class="d-block w-75 py-2"/>
|
||||
<field name="color_success_light" class="d-block w-25 p-0 m-0" widget="color"/>
|
||||
</div>
|
||||
<div class="w-50 row">
|
||||
<label for="color_warning_light" string="Warning" class="d-block w-75 py-2"/>
|
||||
<field name="color_warning_light" class="d-block w-25 p-0 m-0" widget="color"/>
|
||||
</div>
|
||||
<div class="w-50 row">
|
||||
<label for="color_danger_light" string="Danger" class="d-block w-75 py-2"/>
|
||||
<field name="color_danger_light" class="d-block w-25 p-0 m-0" widget="color"/>
|
||||
</div>
|
||||
<button
|
||||
name="action_reset_theme_color_assets"
|
||||
icon="fa-refresh"
|
||||
type="object"
|
||||
string="Reset Theme Colors"
|
||||
class="btn-link"
|
||||
/>
|
||||
</setting>
|
||||
<setting
|
||||
string="Background Image"
|
||||
company_dependent="1"
|
||||
help="Set the background image for the apps menu"
|
||||
>
|
||||
<field name="theme_background_image" widget="image" class="ml-4 w-75"/>
|
||||
</setting>
|
||||
<setting
|
||||
string="Favicon & Logo"
|
||||
company_dependent="1"
|
||||
help="Set your own favicon and logo for the sidebar"
|
||||
>
|
||||
<div class="w-50 row">
|
||||
<label for="appbar_image" string="Sidebar Logo" class="o_light_label mb-1"/>
|
||||
<field name="appbar_image" widget="image" class="ml-4 oe_avatar"/>
|
||||
<div class="w-100 mt-1"/>
|
||||
<label for="theme_favicon" string="Favicon" class="o_light_label mb-1"/>
|
||||
<field name="theme_favicon" widget="image" class="ml-4 oe_avatar"/>
|
||||
</div>
|
||||
</setting>
|
||||
<setting string="Light Mode Colors" help="Customize the look and feel of the light mode">
|
||||
<div class="w-50 row">
|
||||
<label for="color_brand_light" string="Brand" class="d-block w-75 py-2"/>
|
||||
<field name="color_brand_light" class="d-block w-25 p-0 m-0" widget="color"/>
|
||||
</div>
|
||||
<div class="w-50 row">
|
||||
<label for="color_primary_light" string="Primary" class="d-block w-75 py-2"/>
|
||||
<field name="color_primary_light" class="d-block w-25 p-0 m-0" widget="color"/>
|
||||
</div>
|
||||
<div class="w-50 row">
|
||||
<label for="color_info_light" string="Info" class="d-block w-75 py-2"/>
|
||||
<field name="color_info_light" class="d-block w-25 p-0 m-0" widget="color"/>
|
||||
</div>
|
||||
<div class="w-50 row">
|
||||
<label for="color_success_light" string="Success" class="d-block w-75 py-2"/>
|
||||
<field name="color_success_light" class="d-block w-25 p-0 m-0" widget="color"/>
|
||||
</div>
|
||||
<div class="w-50 row">
|
||||
<label for="color_warning_light" string="Warning" class="d-block w-75 py-2"/>
|
||||
<field name="color_warning_light" class="d-block w-25 p-0 m-0" widget="color"/>
|
||||
</div>
|
||||
<div class="w-50 row">
|
||||
<label for="color_danger_light" string="Danger" class="d-block w-75 py-2"/>
|
||||
<field name="color_danger_light" class="d-block w-25 p-0 m-0" widget="color"/>
|
||||
</div>
|
||||
<button
|
||||
name="action_reset_light_color_assets"
|
||||
icon="fa-refresh"
|
||||
type="object"
|
||||
string="Reset Light Colors"
|
||||
class="btn-link"
|
||||
/>
|
||||
</setting>
|
||||
<setting string="Dark Mode Colors" help="Customize the look and feel of the dark mode">
|
||||
<div class="w-50 row">
|
||||
<label for="color_brand_dark" string="Brand" class="d-block w-75 py-2"/>
|
||||
<field name="color_brand_dark" class="d-block w-25 p-0 m-0" widget="color"/>
|
||||
</div>
|
||||
<div class="w-50 row">
|
||||
<label for="color_primary_dark" string="Primary" class="d-block w-75 py-2"/>
|
||||
<field name="color_primary_dark" class="d-block w-25 p-0 m-0" widget="color"/>
|
||||
</div>
|
||||
<div class="w-50 row">
|
||||
<label for="color_info_dark" string="Info" class="d-block w-75 py-2"/>
|
||||
<field name="color_info_dark" class="d-block w-25 p-0 m-0" widget="color"/>
|
||||
</div>
|
||||
<div class="w-50 row">
|
||||
<label for="color_success_dark" string="Success" class="d-block w-75 py-2"/>
|
||||
<field name="color_success_dark" class="d-block w-25 p-0 m-0" widget="color"/>
|
||||
</div>
|
||||
<div class="w-50 row">
|
||||
<label for="color_warning_dark" string="Warning" class="d-block w-75 py-2"/>
|
||||
<field name="color_warning_dark" class="d-block w-25 p-0 m-0" widget="color"/>
|
||||
</div>
|
||||
<div class="w-50 row">
|
||||
<label for="color_danger_dark" string="Danger" class="d-block w-75 py-2"/>
|
||||
<field name="color_danger_dark" class="d-block w-25 p-0 m-0" widget="color"/>
|
||||
</div>
|
||||
<button
|
||||
name="action_reset_dark_color_assets"
|
||||
icon="fa-refresh"
|
||||
type="object"
|
||||
string="Reset Dark Colors"
|
||||
class="btn-link"
|
||||
/>
|
||||
</setting>
|
||||
</block>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<odoo>
|
||||
|
||||
<!-- Update Preferences form -->
|
||||
<record id="view_users_form_simple_modif" model="ir.ui.view">
|
||||
<field name="name">res.users.preferences.form.fusion</field>
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="base.view_users_form_simple_modif"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@name='other_preferences']" position="inside">
|
||||
<field
|
||||
name="color_scheme"
|
||||
widget="radio"
|
||||
string="Theme"
|
||||
options="{'horizontal': true}"
|
||||
/>
|
||||
<field name="sidebar_type" readonly="0"/>
|
||||
<field name="chatter_position" readonly="0"/>
|
||||
<field name="dialog_size" readonly="0"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Update User form -->
|
||||
<record id="view_users_form" model="ir.ui.view">
|
||||
<field name="name">res.users.form.fusion</field>
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="base.view_users_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@name='other_preferences']" position="inside">
|
||||
<field
|
||||
name="color_scheme"
|
||||
widget="radio"
|
||||
string="Theme"
|
||||
options="{'horizontal': true}"
|
||||
/>
|
||||
<field name="sidebar_type" readonly="0"/>
|
||||
<field name="chatter_position" readonly="0"/>
|
||||
<field name="dialog_size" readonly="0"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user