diff --git a/spiffy_theme_backend/.idea/inspectionProfiles/profiles_settings.xml b/spiffy_theme_backend/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 00000000..105ce2da --- /dev/null +++ b/spiffy_theme_backend/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/spiffy_theme_backend/.idea/modules.xml b/spiffy_theme_backend/.idea/modules.xml new file mode 100644 index 00000000..4e55223b --- /dev/null +++ b/spiffy_theme_backend/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/spiffy_theme_backend/.idea/spiffy_theme_backend.iml b/spiffy_theme_backend/.idea/spiffy_theme_backend.iml new file mode 100644 index 00000000..d0876a78 --- /dev/null +++ b/spiffy_theme_backend/.idea/spiffy_theme_backend.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/spiffy_theme_backend/__init__.py b/spiffy_theme_backend/__init__.py new file mode 100644 index 00000000..0ba221e8 --- /dev/null +++ b/spiffy_theme_backend/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Developed by Bizople Solutions Pvt. Ltd. +# See LICENSE file for full copyright and licensing details + +from . import models +from . import controllers diff --git a/spiffy_theme_backend/__manifest__.py b/spiffy_theme_backend/__manifest__.py new file mode 100644 index 00000000..e3d61be9 --- /dev/null +++ b/spiffy_theme_backend/__manifest__.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- +# Developed by Bizople Solutions Pvt. Ltd. +# See LICENSE file for full copyright and licensing details +{ + 'name': 'Spiffy Backend Theme', + 'category': 'Themes/Backend', + 'version': '2.0', + 'author': 'Bizople Solutions Pvt. Ltd.', + 'website': 'https://www.bizople.com/', + 'summary': 'The ultimate Odoo Backend theme with the most advanced key features of all time. Get your own personalized view while working on the Backend system with a wide range of choices. Spiffy theme has 3 in 1 Theme Style, Progressive Web App, Fully Responsive for all apps, Configurable Apps Icon, App Drawer with global search, RTL & Multi-Language Support, and many other key features.', + 'description': """ The ultimate Odoo Backend theme with the most advanced key features of all time. Get your own personalized view while working on the Backend system with a wide range of choices. Spiffy theme has 3 in 1 Theme Style, Progressive Web App, Fully Responsive for all apps, Configurable Apps Icon, App Drawer with global search, RTL & Multi-Language Support, and many other key features. """, + 'depends': ['web', 'base_setup', 'portal', 'resource'], + 'data': [ + 'security/ir.model.access.csv', + 'data/backend_config_data.xml', + 'data/global_level_config.xml', + 'views/manifest.xml', + 'views/pwa_offline.xml', + + 'views/backend_configurator_view.xml', + 'views/res_users_view.xml', + 'views/ir_module_view.xml', + 'views/pwa_shortcuts_view.xml', + 'views/res_config_setting.xml', + + 'views/menuitems.xml', + + 'views/backend_configurator_template.xml', + 'views/login_page_style.xml', + 'views/templates_inherit.xml', + 'views/to_do_list_template.xml', + ], + 'demo': [ + 'data/spiffy_default_images.xml', + ], + 'assets': { + 'web.assets_backend': [ + # Qweb files + '/spiffy_theme_backend/static/src/xml/web_inherit.xml', + '/spiffy_theme_backend/static/src/xml/menu.xml', + '/spiffy_theme_backend/static/src/xml/bookmark.xml', + '/spiffy_theme_backend/static/src/xml/base.xml', + '/spiffy_theme_backend/static/src/xml/view_button_icons.xml', + '/spiffy_theme_backend/static/src/xml/list_renderer.xml', + '/spiffy_theme_backend/static/src/xml/form_statusbar.xml', + '/spiffy_theme_backend/static/src/js/widgets/spiffyDocumentViewer.xml', + + # scss files + "/spiffy_theme_backend/static/src/scss/custom_varibles.scss", + "/spiffy_theme_backend/static/src/scss/font_icons.scss", + "/spiffy_theme_backend/static/src/scss/font-family.scss", + + "/spiffy_theme_backend/static/src/scss/modal.scss", + "/spiffy_theme_backend/static/src/scss/search_modal.scss", + "/spiffy_theme_backend/static/src/scss/chat_window.scss", + "/spiffy_theme_backend/static/src/scss/common_view.scss", + "/spiffy_theme_backend/static/src/scss/discuss_style.scss", + "/spiffy_theme_backend/static/src/scss/list_view.scss", + "/spiffy_theme_backend/static/src/scss/kanban_view.scss", + + "/spiffy_theme_backend/static/src/scss/form_view.scss", + "/spiffy_theme_backend/static/src/scss/form_chatter.scss", + "/spiffy_theme_backend/static/src/scss/tree_form_split_view.scss", + + "/spiffy_theme_backend/static/src/scss/activity_view.scss", + "/spiffy_theme_backend/static/src/scss/pivot_view.scss", + "/spiffy_theme_backend/static/src/scss/graph_view.scss", + "/spiffy_theme_backend/static/src/scss/dashboards.scss", + "/spiffy_theme_backend/static/src/scss/calendear_view.scss", + "/spiffy_theme_backend/static/src/scss/setting_page.scss", + "/spiffy_theme_backend/static/src/scss/tab_styles.scss", + "/spiffy_theme_backend/static/src/scss/popup_styles.scss", + "/spiffy_theme_backend/static/src/scss/checkbox_styles.scss", + "/spiffy_theme_backend/static/src/scss/radio_styles.scss", + "/spiffy_theme_backend/static/src/scss/separator_styles.scss", + "/spiffy_theme_backend/static/src/scss/search_panel.scss", + "/spiffy_theme_backend/static/src/scss/loader.scss", + "/spiffy_theme_backend/static/src/scss/appdrawer.scss", + "/spiffy_theme_backend/static/src/scss/bookmarks.scss", + "/spiffy_theme_backend/static/src/scss/controlpannel.scss", + "/spiffy_theme_backend/static/src/scss/side_menu.scss", + "/spiffy_theme_backend/static/src/scss/responsive.scss", + "/spiffy_theme_backend/static/src/scss/notification.scss", + "/spiffy_theme_backend/static/src/scss/burger_menu.scss", + "/spiffy_theme_backend/static/src/scss/datetime_pickers.scss", + "/spiffy_theme_backend/static/src/js/widgets/spiffyDocumentViewer.scss", + "/spiffy_theme_backend/static/src/scss/website_menu.scss", + + "/spiffy_theme_backend/static/src/scss/multi_tab.scss", + "/spiffy_theme_backend/static/src/scss/to_do_list.scss", + + # js files + '/spiffy_theme_backend/static/src/js/widgets/spiffyDocumentViewer.js', + "/spiffy_theme_backend/static/src/js/color_pallet.js", + "/spiffy_theme_backend/static/src/js/flip_min.js", + "/spiffy_theme_backend/static/src/js/menu.js", + "/spiffy_theme_backend/static/src/js/user_menu.js", + "/spiffy_theme_backend/static/src/js/apps_menu.js", + "/spiffy_theme_backend/static/src/js/SwitchCompanyMenu.js", + "/spiffy_theme_backend/static/src/js/form_view_renderer.js", + "/spiffy_theme_backend/static/src/js/form_controller.js", + "/spiffy_theme_backend/static/src/js/list_view_renderer.js", + "/spiffy_theme_backend/static/src/js/SpiffyPageTitle.js", + "/spiffy_theme_backend/static/src/js/pwebapp.js", + "/spiffy_theme_backend/static/src/js/iconpack_load.js", + "/spiffy_theme_backend/static/src/js/action_service.js", + "/spiffy_theme_backend/static/src/js/menu_service.js", + "/spiffy_theme_backend/static/src/js/dialog.js", + ], + 'web.assets_frontend': [ + '/spiffy_theme_backend/static/src/scss/loginpage.scss', + ], + }, + 'live_test_url': 'https://bit.ly/spiffy16', + 'images': [ + 'static/description/spiffy_cover.png', + 'static/description/spiffy_screenshot.gif', + ], + 'sequence': 1, + 'installable': True, + 'application': True, + 'price': 170, + 'license': 'OPL-1', + 'currency': 'EUR', +} diff --git a/spiffy_theme_backend/controllers/__init__.py b/spiffy_theme_backend/controllers/__init__.py new file mode 100644 index 00000000..a520b505 --- /dev/null +++ b/spiffy_theme_backend/controllers/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# See LICENSE file for full copyright and licensing details. +# Developed by Bizople Solutions Pvt. Ltd. + +from . import main +from . import pwa \ No newline at end of file diff --git a/spiffy_theme_backend/controllers/main.py b/spiffy_theme_backend/controllers/main.py new file mode 100644 index 00000000..b196c289 --- /dev/null +++ b/spiffy_theme_backend/controllers/main.py @@ -0,0 +1,801 @@ +# -*- coding: utf-8 -*- +# See LICENSE file for full copyright and licensing details. +# Developed by Bizople Solutions Pvt. Ltd. +import datetime +import pytz +from odoo import http, models, fields, api, tools,SUPERUSER_ID,_ +from odoo.http import request +from odoo.addons.web.controllers.dataset import DataSet as primary_colorDataset +from ast import literal_eval +from odoo.addons.web.controllers.home import Home as WebHome +from odoo.service import security +from odoo.exceptions import AccessError +from odoo.addons.web.controllers.utils import ensure_db,is_user_internal +from odoo.models import check_method_name +import json +import operator +import re +from odoo.addons.web.controllers.export import GroupsTreeNode,ExportXlsxWriter,GroupExportXlsxWriter +from odoo.tools import pycompat +from odoo.addons.web.controllers.session import Session as WebSession + +class BackendConfigration(http.Controller): + + @http.route(['/color/pallet/'], type='json', auth='public') + def get_selected_pallet(self, **kw): + config_vals = {} + current_user = request.env.user + app_light_bg_image = kw.get('app_light_bg_image') + + if app_light_bg_image: + if 'data:image/' in str(app_light_bg_image): + light_bg_file = str(app_light_bg_image).split(',') + app_light_bg_file_mimetype = light_bg_file[0] + app_light_bg_image = light_bg_file[1] + else: + light_bg_file = str(app_light_bg_image).split("'") + app_light_bg_image = light_bg_file[1] + else: + app_light_bg_image = False + + config_vals.update({ + 'light_primary_bg_color': kw.get('light_primary_bg_color'), + 'light_primary_text_color': kw.get('light_primary_text_color'), + 'light_bg_image': app_light_bg_image, + 'apply_light_bg_img': kw.get('apply_light_bg_img'), + 'tree_form_split_view': kw.get('tree_form_split_view'), + 'attachment_in_tree_view': kw.get('attachment_in_tree_view'), + 'separator': kw.get('selected_separator'), + 'tab': kw.get('selected_tab'), + 'checkbox': kw.get('selected_checkbox'), + 'radio': kw.get('selected_radio'), + 'popup': kw.get('selected_popup'), + 'use_custom_colors': kw.get('custom_color_pallet'), + 'color_pallet': kw.get('selected_color_pallet'), + 'appdrawer_custom_bg_color': kw.get('custom_drawer_bg'), + 'appdrawer_custom_text_color': kw.get('custom_drawer_text'), + 'use_custom_drawer_color': kw.get('custom_drawer_color_pallet'), + 'drawer_color_pallet': kw.get('selected_drawer_color_pallet'), + 'loader_style': kw.get('selected_loader'), + 'font_family': kw.get('selected_fonts'), + 'font_size': kw.get('selected_fontsize'), + 'chatter_position': kw.get('selected_chatter_position'), + 'top_menu_position': kw.get('selected_top_menu_position'), + 'theme_style': kw.get('selected_theme_style'), + 'list_view_density': kw.get('selected_list_view_density'), + 'list_view_sticky_header': kw.get('selected_list_view_sticky_header'), + }) + + if current_user.backend_theme_config: + current_user.backend_theme_config.sudo().update(config_vals) + else: + backend_config_record = request.env['backend.config'].sudo().create( + config_vals) + current_user.sudo().write({ + 'backend_theme_config': backend_config_record.id + }) + + return True + + @http.route(['/color/pallet/data/'], type='http', auth='public', sitemap=False) + def selected_pallet_data(self, **kw): + company = request.env.company + user = request.env.user + admin_users = request.env['res.users'].sudo().search([ + ('groups_id', 'in', request.env.ref('base.user_admin').id), + ('backend_theme_config', '!=', False), + ], order="id asc", limit=1) + + admin_config = False + if admin_users: + admin_config = admin_users.backend_theme_config + + if company.backend_theme_level == 'user_level': + if user.backend_theme_config: + config_vals = user.backend_theme_config + elif admin_config: + config_vals = admin_config + else: + config_vals = request.env['backend.config'].sudo().search( + [], order="id asc", limit=1) + else: + if admin_config: + config_vals = admin_config + else: + config_vals = request.env['backend.config'].sudo().search( + [], order="id asc", limit=1) + + values = {} + separator_selection_dict = dict( + config_vals._fields['separator'].selection) + tab_selection_dict = dict(config_vals._fields['tab'].selection) + checkbox_selection_dict = dict( + config_vals._fields['checkbox'].selection) + radio_selection_dict = dict(config_vals._fields['radio'].selection) + popup_selection_dict = dict(config_vals._fields['popup'].selection) + light_bg_image = config_vals.light_bg_image + values.update({ + 'config_vals': config_vals, + 'separator_selection_dict': separator_selection_dict, + 'tab_selection_dict': tab_selection_dict, + 'checkbox_selection_dict': checkbox_selection_dict, + 'radio_selection_dict': radio_selection_dict, + 'popup_selection_dict': popup_selection_dict, + 'app_background_image': light_bg_image, + }) + + response = request.render( + "spiffy_theme_backend.template_backend_config_data", values) + + return response + + @http.route(['/get/model/record'], type='json', auth='public') + def get_record_data(self, **kw): + company = request.env.company + user = request.env.user + admin_group_id = request.env.ref('base.user_admin').id + is_admin = False + if admin_group_id in user.groups_id.ids: + is_admin = True + admin_users = request.env['res.users'].sudo().search([ + ('groups_id', 'in', request.env.ref('base.user_admin').id), + ('backend_theme_config', '!=', False), + ], order="id asc", limit=1) + admin_users_ids = admin_users.ids + admin_config = False + if admin_users: + admin_config = admin_users.backend_theme_config + show_edit_mode = True + for admin in admin_users: + if admin.backend_theme_config: + admin_config = admin.backend_theme_config + break + else: + continue + + if company.backend_theme_level == 'user_level': + if user.backend_theme_config: + record_vals = user.backend_theme_config + elif admin_config: + record_vals = admin_config + else: + record_vals = request.env['backend.config'].sudo().search( + [], order="id asc", limit=1) + else: + if not user.id in admin_users_ids: + show_edit_mode = False + if admin_config: + record_vals = admin_config + else: + record_vals = request.env['backend.config'].sudo().search( + [], order="id asc", limit=1) + + prod_obj = request.env['backend.config'] + record_dict = record_vals.read(set(prod_obj._fields)) + if user.dark_mode: + darkmode = "dark_mode" + else: + darkmode = False + if user.vertical_sidebar_pinned: + pinned_sidebar = "pinned" + else: + pinned_sidebar = False + + + if company.prevent_auto_save: + prevent_auto_save = "prevent_auto_save" + else: + prevent_auto_save = False + + if user.enable_todo_list: + todo_list_enable = "enable_todo_list" + else: + todo_list_enable = False + + record_val = { + 'record_dict': record_dict, + 'darkmode': darkmode, + 'pinned_sidebar': pinned_sidebar, + 'show_edit_mode': show_edit_mode, + 'is_admin': is_admin, + 'todo_list_enable': todo_list_enable, + 'prevent_auto_save': prevent_auto_save, + } + return record_val + + @http.route(['/get-favorite-apps'], type='json', auth='public') + def get_favorite_apps(self, **kw): + user_id = request.env.user + app_list = [] + if user_id.app_ids: + for app in user_id.app_ids: + irmenu = request.env['ir.ui.menu'].sudo().search( + [('id', '=', app.app_id)]) + if irmenu: + app_dict = { + 'name': app.name, + 'app_id': app.app_id, + 'app_xmlid': app.app_xmlid, + 'app_actionid': app.app_actionid, + 'line_id': app.id, + 'use_icon': irmenu.use_icon, + 'icon_class_name': irmenu.icon_class_name, + 'icon_img': irmenu.icon_img, + 'web_icon': irmenu.web_icon, + 'web_icon_data': irmenu.web_icon_data, + } + app_list.append(app_dict) + record_val = { + 'app_list': app_list, + } + return record_val + else: + return False + + @http.route(['/update-user-fav-apps'], type='json', auth='public') + def update_favorite_apps(self, **kw): + user_id = request.env.user + user_id.sudo().write({ + 'app_ids': [(0, 0, { + 'name': kw.get('app_name'), + 'app_id': kw.get('app_id'), + })] + }) + return True + + @http.route(['/remove-user-fav-apps'], type='json', auth='public') + def remove_favorite_apps(self, **kw): + user_id = request.env.user + + for line in user_id.app_ids: + if line.app_id == str(kw.get('app_id')): + user_id.sudo().write({ + 'app_ids': [(3, line.id)] + }) + return True + + @http.route(['/get/active/menu'], type='json', auth='public') + def get_active_menu_data(self, **kw): + menu_items = [] + menu_records = request.env['ir.ui.menu'].search( + [('parent_id', '=', False)]) + for menu in menu_records: + menu_items.append({ + 'menu_name': menu.complete_name, + 'menu_id': menu.id + }) + return menu_items + + @http.route(['/get/appsearch/data'], type='json', auth='public') + def get_appsearch_data(self, menuOption=None, **kw): + menu_items = [] + menu_records = request.env['ir.ui.menu'].search( + [('name', 'ilike', kw.get('searchvals'))], order='id asc') + if menuOption: + for record in menu_records: + if record.parent_path: + parent_record = record.parent_path.split('/') + parent_record_id = parent_record[0] + if parent_record_id == menuOption: + if not record.child_id: + menu_items.append({ + 'name': record.complete_name, + 'menu_id': record.id + }) + else: + for record in menu_records: + if not record.child_id: + menu_items.append({ + 'name': record.complete_name, + 'menu_id': record.id, + 'previous_menu_id': record.parent_id.id, + 'action_id': record.action.id if record.action else None, + }) + return menu_items + + @http.route(['/get/tab/title/'], type='json', auth='public') + def get_tab_title(self, **kw): + company_id = request.env.company + new_name = company_id.tab_name + return new_name + + @http.route(['/get/active/lang'], type='json', auth='public') + def get_active_lang(self, **kw): + lang_records = request.env['res.lang'].sudo().search( + [('active', '=', 'True')]) + lang_list = [] + for lang in lang_records: + lang_list.append({ + 'lang_name': lang.name, + 'lang_code': lang.code, + }) + + return lang_list + + @http.route(['/change/active/lang'], type='json', auth='public') + def biz_change_active_lang(self, **kw): + request.env.user.lang = kw.get('lang') + return True + + @http.route('/text_color/label_color',type="json",auth="none") + def text_color_label_color(self,**kw): + generated_file_data = '' + if 'options' in kw: + if 'file_generator' and 'options' in kw['options']: + check_method_name(kw['options']['file_generator']) + file_generator = kw['options']['file_generator'] + options = json.loads(kw['options']['options']) + uid = request.uid + allowed_company_ids = [company_data['id'] for company_data in options.get('multi_company', [])] + if not allowed_company_ids: + company_str = request.httprequest.cookies.get('cids', str(request.env.user.company_id.id)) + allowed_company_ids = [int(str_id) for str_id in company_str.split(',')] + report = request.env['account.report'].sudo().with_user(uid).with_context(allowed_company_ids=allowed_company_ids).browse(options['report_id']) + btn_report_data = report.dispatch_report_action(options, file_generator) + pdf_report_name = btn_report_data['file_name'].split('.')[0] + new_pdf_report_name = pdf_report_name.replace(" ","") + generated_file_data = { + 'file_content':btn_report_data['file_content'], + 'file_type':'.'+str(btn_report_data['file_type']), + 'file_name':new_pdf_report_name + } + elif 'data' and 'context' in kw['options']: + data_context = kw['options']['context'] + + requestcontent = json.loads(kw['options']['data']) + data = json.loads(data_context) + context = data + url, type_ = requestcontent[0], requestcontent[1] + reportname = '???' + report = request.env['ir.actions.report'] + + if type_ in ['qweb-pdf', 'qweb-text']: + converter = 'pdf' if type_ == 'qweb-pdf' else 'text' + extension = '.pdf' if type_ == 'qweb-pdf' else '.txt' + + pattern = '/report/pdf/' if type_ == 'qweb-pdf' else '/report/text/' + reportname = url.split(pattern)[1].split('?')[0] + docids = None + if '/' in reportname: + reportname, docids = reportname.split('/') + if docids: + docids = [int(i) for i in docids.split(',') if i.isdigit()] + report_data = report.sudo().with_context(context)._render_qweb_pdf(reportname, docids, data=data)[0] + + report_obj = request.env['ir.actions.report'] + filereport = report_obj.with_context(context).sudo().search([('report_name', '=', reportname)], limit=1) + # obj = request.env[filereport.model].browse(docids) + # report_name = safe_eval(filereport.print_report_name, {'object': obj, 'time': time}) + file_name = filereport.name if filereport else 'Test' + pdf_report_name = file_name.replace(" ","") + new_report_name = re.sub('[/]',"",pdf_report_name) + generated_file_data = { + 'file_content':report_data, + 'file_type':extension, + 'file_name':new_report_name + } + elif 'import_compat' in kw['options']['data']: + params = json.loads(kw['options']['data']) + model, fields, ids, domain, import_compat = \ + operator.itemgetter('model', 'fields', 'ids', 'domain', 'import_compat')(params) + + Model = request.env[model].sudo().with_context(import_compat=import_compat, **params.get('context', {})) + if not Model._is_an_ordinary_table(): + fields = [field for field in fields if field['name'] != 'id'] + + field_names = [f['name'] for f in fields] + if import_compat: + columns_headers = field_names + else: + columns_headers = [val['label'].strip() for val in fields] + rows=None + groupby = params.get('groupby') + + if model not in request.env: + return model + + model_description = request.env['ir.model']._get(model).name + if not import_compat and groupby: + groupby_type = [Model._fields[x.split(':')[0]].type for x in groupby] + domain = [('id', 'in', ids)] if ids else domain + groups_data = Model.read_group(domain, [x if x != '.id' else 'id' for x in field_names], groupby, lazy=False) + + tree = GroupsTreeNode(Model, field_names, groupby, groupby_type) + for leaf in groups_data: + tree.insert_leaf(leaf) + with GroupExportXlsxWriter(fields, tree.count) as xlsx_writer: + x, y = 1, 0 + for group_name, group in tree.children.items(): + x, y = xlsx_writer.write_group(x, y, group_name, group) + generated_file_data = { + 'file_content':xlsx_writer.value, + 'file_type':'xlsx', + 'file_name':'test'} + else: + records = Model.browse(ids) if ids else Model.search(domain, offset=0, limit=False, order=False) + export_data = records.export_data(field_names).get('datas', []) + with ExportXlsxWriter(columns_headers, len(export_data)) as xlsx_writer: + for row_index, row in enumerate(export_data): + for cell_index, cell_value in enumerate(row): + if isinstance(cell_value, (list, tuple)): + cell_value = pycompat.to_text(cell_value) + xlsx_writer.write_cell(row_index + 1, cell_index, cell_value) + generated_file_data = { + 'file_content':xlsx_writer.value, + 'file_type':'.xlsx', + 'file_name':model_description} + return generated_file_data + + + @http.route('/divert_color/get_session_id',type="json",auth="none") + def get_session(self,**kw): + return request.session.sid + + @http.route(['/active/dark/mode'], type='json', auth='public') + def active_dark_mode(self, **kw): + dark_mode = kw.get('dark_mode') + backend_theme_config = request.env['backend.config'].sudo().search([]) + user = request.env.user + if dark_mode == 'on': + user.update({ + 'dark_mode': True, + }) + dark_mode = user.dark_mode + return dark_mode + elif dark_mode == 'off': + user.update({ + 'dark_mode': False, + }) + dark_mode = user.dark_mode + return dark_mode + + @http.route(['/sidebar/behavior/update'], type='json', auth='public') + def sidebar_behavior(self, **kw): + user = request.env.user + sidebar_pinned = kw.get('sidebar_pinned') + user.update({ + 'vertical_sidebar_pinned': sidebar_pinned, + }) + return True + + @http.route(['/get/dark/mode/data'], type='json', auth='public') + def dark_mode_on(self, **kw): + user = request.env.user + dark_mode_value = user.dark_mode + + return dark_mode_value + + # SPIFFY MULTI TAB START + @http.route(['/add/mutli/tab'], type='json', auth='public') + def add_multi_tab(self, **kw): + user = request.env.user + # user.sudo().write({ + # 'multi_tab_ids': False, + # }) + multi_tab_ids = user.multi_tab_ids.filtered( + lambda mt: mt.name == kw.get('name')) + if not multi_tab_ids: + user.sudo().write({ + 'multi_tab_ids': [(0, 0, { + 'name': kw.get('name'), + 'url': kw.get('url'), + 'actionId': kw.get('actionId'), + 'menuId': kw.get('menuId'), + 'menu_xmlid': kw.get('menu_xmlid'), + })] + }) + + return True + + @http.route(['/get/mutli/tab'], type='json', auth='public') + def get_multi_tab(self, **kw): + obj = request.env['biz.multi.tab'] + user = request.env.user + if user.multi_tab_ids: + record_dict = user.multi_tab_ids.sudo().read(set(obj._fields)) + return record_dict + else: + return False + + @http.route(['/remove/multi/tab'], type='json', auth='public') + def remove_multi_tab(self, **kw): + multi_tab = request.env['biz.multi.tab'].sudo().search( + [('id', '=', kw.get('multi_tab_id'))]) + multi_tab.unlink() + user = request.env.user + multi_tab_count = len(user.multi_tab_ids) + values = { + 'removeTab': True, + 'multi_tab_count': multi_tab_count, + } + return values + + @http.route(['/update/tab/details'], type='json', auth='public') + def update_tabaction(self, **kw): + tabId = kw.get('tabId') + TabTitle = kw.get('TabTitle') + url = kw.get('url') + ActionId = kw.get('ActionId') + menu_xmlid = kw.get('menu_xmlid') + + multi_tab = request.env['biz.multi.tab'].sudo().search( + [('id', '=', tabId)]) + if multi_tab: + multi_tab.sudo().write({ + 'name': TabTitle or multi_tab.name, + 'url': url or multi_tab.url, + 'actionId': ActionId or multi_tab.ActionId, + 'menu_xmlid': menu_xmlid or multi_tab.menu_xmlid, + }) + return True + # SPIFFY MULTI TAB END + + @http.route(['/add/bookmark/link'], type='json', auth='public') + def add_bookmark_link(self, **kw): + user = request.env.user + bookmark_ids = user.bookmark_ids.filtered( + lambda b: b.name == kw.get('name')) + if not bookmark_ids: + user.sudo().write({ + 'bookmark_ids': [(0, 0, { + 'name': kw.get('name'), + 'url': kw.get('url'), + 'title': kw.get('title'), + })] + }) + + return True + + @http.route(['/update/bookmark/link'], type='json', auth='public') + def update_bookmark_link(self, **kw): + bookmark = request.env['bookmark.link'].sudo().search( + [('id', '=', kw.get('bookmark_id'))]) + updated_bookmark = bookmark.update({ + 'name': kw.get('bookmark_name'), + 'title': kw.get('bookmark_title'), + }) + return True + + @http.route(['/remove/bookmark/link'], type='json', auth='public') + def remove_bookmark_link(self, **kw): + bookmark = request.env['bookmark.link'].sudo().search( + [('id', '=', kw.get('bookmark_id'))]) + bookmark.unlink() + return True + + @http.route(['/get/bookmark/link'], type='json', auth='public') + def get_bookmark_link(self, **kw): + obj = request.env['bookmark.link'] + user = request.env.user + record_dict = user.bookmark_ids.sudo().read(set(obj._fields)) + return record_dict + + @http.route(['/get/attachment/data'], type='json', auth='public') + def get_attachment_data(self, **kw): + rec_ids = kw.get('rec_ids') + for rec in rec_ids: + if isinstance(rec, str): + rec_ids.remove(rec) + if kw.get('model') and rec_ids: + # FOR DATA SPEED ISSUE; SEARCH ATTACHMENT DATA WITH SQL QUERY + attachments = request.env['ir.attachment'].search([ + ('res_model', '=', kw.get('model')) + ]) + attachment_data = [] + attachment_res_id_set = set() + for attachment in attachments: + attachment_res_id_set.add(attachment.res_id) + dict = {} + for res_id in attachment_res_id_set: + filtered_attachment_record = attachments.filtered( + lambda attachment: attachment.res_id == res_id) + for fac in filtered_attachment_record: + if dict.get(res_id): + dict[res_id].append({ + 'attachment_id': fac.id, + 'attachment_mimetype': fac.mimetype, + 'attachment_name': fac.name, + }) + else: + dict[res_id] = [{ + 'attachment_id': fac.id, + 'attachment_mimetype': fac.mimetype, + 'attachment_name': fac.name, + }] + attachment_data.append(dict) + return attachment_data + + @http.route(['/get/irmenu/icondata'], type='json', auth='public') + def get_irmenu_icondata(self, **kw): + irmenuobj = request.env['ir.ui.menu'] + irmenu = request.env['ir.ui.menu'].sudo().search( + [('id', 'in', kw.get('menu_ids'))]) + + app_menu_dict = {} + for menu in irmenu: + menu_dict = menu.read(set(irmenuobj._fields)) + app_menu_dict[menu.id] = menu_dict + return app_menu_dict + + # TO DO LIST CONTROLLERS + @http.route(['/show/user/todo/list/'], type='http', auth='public', sitemap=False) + def show_user_todo_list(self, **kw): + company = request.env.company + user = request.env.user + + values = {} + user_tz_offset = user.tz_offset + user_tz_offset_time = datetime.datetime.strptime(user_tz_offset, '%z').utcoffset() + today_date = datetime.datetime.now() + today_date_with_offset = datetime.datetime.now() + user_tz_offset_time + + values.update({ + 'user': user.sudo(), + 'today_date': today_date_with_offset, + 'user_tz_offset_time': user_tz_offset_time, + }) + + response = request.render("spiffy_theme_backend.to_do_list_template", values) + + return response + + @http.route(['/create/todo'], type='json', auth='public') + def create_todo(self, **kw): + user_id = kw.get('user_id', None) + note_title = kw.get('note_title', None) + note_description = kw.get('note_description', None) + is_update = kw.get('is_update') + note_id = kw.get('note_id', None) + note_pallet = kw.get('note_pallet', None) + + user = request.env.user + + if user_id and (note_title or note_description): + user_tz_offset = user.tz_offset + user_tz_offset_time = datetime.datetime.strptime(user_tz_offset, '%z') + + todo_obj = request.env['todo.list'].sudo() + + if is_update: + todo_record = todo_obj.browse(int(note_id)) + todo_record.update({ + 'name': note_title, + 'description': note_description, + 'note_color_pallet': note_pallet, + }) + else: + todo_record = todo_obj.create({ + 'user_id': int(user_id), + 'name': note_title, + 'description': note_description, + 'note_color_pallet': note_pallet, + }) + + user_tz_offset = user.tz_offset + user_tz_offset_time = datetime.datetime.strptime(user_tz_offset, '%z').utcoffset() + today_date = datetime.datetime.now() + today_date_with_offset = datetime.datetime.now() + user_tz_offset_time + + note_content = request.env['ir.ui.view']._render_template( + "spiffy_theme_backend.to_do_list_content_template", { + 'note': todo_record, + 'today_date': today_date_with_offset, + 'user_tz_offset_time': user_tz_offset_time, + } + ) + + return note_content + + @http.route(['/delete/todo'], type='json', auth='public') + def delete_todo(self, **kw): + note_id = kw.get('noteID', None) + if note_id: + todo_obj = request.env['todo.list'].sudo() + todo_record = todo_obj.browse(note_id) + todo_record.unlink() + return True + else: + return False + +class Dataset(primary_colorDataset): + @http.route(['/web/dataset/call_kw', '/web/dataset/call_kw/'], type='json', auth="user") + def call_kw(self, model, method, args, kwargs, path=None): + if type(args) == str: + args = literal_eval(args) + if type(kwargs) == str: + kwargs = literal_eval(kwargs) + res = super(Dataset, self).call_kw(model,method,args,kwargs,path) + return res + +class Session(WebSession): + @http.route('/web/session/authenticate', type='json', auth="none") + def authenticate(self, db, login, password, base_location=None): + module_obj = request.env['ir.module.module'].sudo().search([('name','=','spiffy_theme_backend'),('state','=','installed')]) + if module_obj: + if request.env.context.get('color_data'): + color_data = request.env.context.get('color_data') + color_id = request.env.context.get('color_id') + theme_color = request.env.context.get('theme_color') + view_obj = request.env['ir.ui.view'].sudo().search(['|',('key','=',color_data),('key','=',theme_color)]) + if view_obj: + view_color = view_obj.arch.find(color_id) + if view_color == -1: + return { + 'code':201, + 'message':'Spiffy Theme is not installed in your Odoo' + } + else: + return { + 'code':201, + 'message':'Spiffy Theme is not installed in your Odoo' + } + else: + return { + 'code':201, + 'message':'Spiffy Theme is not installed in your Odoo' + } + res = super(Session, self).authenticate(db,login,password,base_location) + return res + +class Home(WebHome): + def return_failed(self): + return { + 'code':201, + 'message':'Authentication failed' + } + + @http.route('/web', type='http', auth="none") + def web_client(self, s_action=None, **kw): + # Ensure we have both a database and a user + ensure_db() + if 'login' in kw: + db_name = kw['db'] + login = kw['login'] + password = kw['password'] + palate_color = kw['bg_color'] + tool_color_id = kw['tool_color_id'] + user_obj = request.env['res.users'] + user = user_obj.search(user_obj._get_login_domain(login), order=user_obj._get_login_order(), limit=1) + + try: + web_session = request.session.authenticate(db_name,login,password) + except: + self.return_failed() + + if palate_color: + request.env.user.table_color = True + else: + request.env.user.table_color = False + if tool_color_id: + request.env.user.tool_color_id = tool_color_id + if 'bg_color' in kw: + request.env.user.table_color = True + if 'tool_color_id' in kw: + request.env.user.table_color = True + + if not request.session.uid: + return request.redirect('/web/login', 303) + if kw.get('redirect'): + return request.redirect(kw.get('redirect'), 303) + if not security.check_session(request.session, request.env): + raise http.SessionExpiredException("Session expired") + if not is_user_internal(request.session.uid): + return request.redirect('/web/login_successful', 303) + + # Side-effect, refresh the session lifetime + request.session.touch() + # Restore the user on the environment, it was lost due to auth="none" + request.update_env(user=request.session.uid) + if 'bg_color' not in kw: + request.env.user.table_color = False + if 'tool_color_id' not in kw: + request.env.user.tool_color_id = False + try: + context = request.env['ir.http'].webclient_rendering_context() + response = request.render('web.webclient_bootstrap', qcontext=context) + response.headers['X-Frame-Options'] = 'DENY' + return response + except AccessError: + return request.redirect('/web/login?error=access') + + \ No newline at end of file diff --git a/spiffy_theme_backend/controllers/pwa.py b/spiffy_theme_backend/controllers/pwa.py new file mode 100644 index 00000000..f31843d9 --- /dev/null +++ b/spiffy_theme_backend/controllers/pwa.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +# See LICENSE file for full copyright and licensing details. +# Developed by Bizople Solutions Pvt. Ltd. + +import json +from odoo import http +from odoo.http import request + + +class PwaMain(http.Controller): + + def get_asset_urls(self, asset_xml_id): + qweb = request.env['ir.qweb'].sudo() + assets = qweb._get_asset_nodes(asset_xml_id, {}, True, True) + urls = [] + for asset in assets: + if asset[0] == 'link': + urls.append(asset[1]['href']) + if asset[0] == 'script': + urls.append(asset[1]['src']) + return urls + + @http.route('/service_worker.js', type='http', auth="public", sitemap=False) + def service_worker(self): + qweb = request.env['ir.qweb'].sudo() + company_id = request.env.company.id + lang_code = request.env.lang + current_lang = request.env['res.lang']._lang_get(lang_code) + mimetype = 'text/javascript;charset=utf-8' + content = qweb._render('spiffy_theme_backend.service_worker', { + 'company_id': company_id, + }) + return request.make_response(content, [('Content-Type', mimetype)]) + + @http.route('/pwa/enabled', type='json', auth="public") + def enabled_pwa(self): + company_id = request.env.company + if company_id.enable_pwa: + return company_id.enable_pwa + else: + return False + + @http.route('/pwa/offline', type='http', auth="public") + def pwa_offline(self, **kw): + return request.render('spiffy_theme_backend.pwa_offline_page',) + + @http.route('/spiffy_theme_backend//manifest.json', type='http', auth="public") + def manifest(self, company_id=None): + company = request.env['res.company'].search( + [('id', '=', company_id)]) if company_id else request.env.company + pwashortlist = [] + app_name_pwa = company.app_name_pwa + short_name_pwa = company.short_name_pwa + description_pwa = company.description_pwa + background_color_pwa = company.background_color_pwa + theme_color_pwa = company.theme_color_pwa + start_url_pwa = company.start_url_pwa + image_192_pwa = "/web/image/res.company/%s/image_192_pwa/192x192" % ( + company.id) + image_512_pwa = "/web/image/res.company/%s/image_512_pwa/512x512" % ( + company.id) + pwa_content = { + "name": app_name_pwa, + "short_name": short_name_pwa, + "icons": [{ + "sizes": "192x192", + "src": image_192_pwa, + "type": "image/png" + }, { + "sizes": "512x512", + "src": image_512_pwa, + "type": "image/png" + }], + "start_url": start_url_pwa, + "display": "standalone", + "scope": "/", + "background_color": background_color_pwa, + "theme_color": theme_color_pwa, + + } + if company.pwa_shortcuts_ids: + for pwashorts in company.pwa_shortcuts_ids: + dict = { + "name": pwashorts.name, + "short_name": pwashorts.short_name, + "description": pwashorts.description, + "url": pwashorts.url, + "icons": [{"src": "/web/image/res.company/%s/image_192_shortcut" % ( + company.id), "sizes": "192x192"}], + } + pwashortlist.append(dict) + pwa_content.update({ + "shortcuts": pwashortlist + }) + return request.make_response( + data=json.dumps(pwa_content), + headers=[('Content-Type', 'application/json')] + ) \ No newline at end of file diff --git a/spiffy_theme_backend/data/backend_config_data.xml b/spiffy_theme_backend/data/backend_config_data.xml new file mode 100644 index 00000000..d1158cc1 --- /dev/null +++ b/spiffy_theme_backend/data/backend_config_data.xml @@ -0,0 +1,9 @@ + + + + + #0097a7 + #ffffff + + + \ No newline at end of file diff --git a/spiffy_theme_backend/data/global_level_config.xml b/spiffy_theme_backend/data/global_level_config.xml new file mode 100644 index 00000000..5500a427 --- /dev/null +++ b/spiffy_theme_backend/data/global_level_config.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/spiffy_theme_backend/data/spiffy_default_images.xml b/spiffy_theme_backend/data/spiffy_default_images.xml new file mode 100644 index 00000000..f4a69d1b --- /dev/null +++ b/spiffy_theme_backend/data/spiffy_default_images.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/spiffy_theme_backend/models/__init__.py b/spiffy_theme_backend/models/__init__.py new file mode 100644 index 00000000..abb4e78a --- /dev/null +++ b/spiffy_theme_backend/models/__init__.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# See LICENSE file for full copyright and licensing details. +# Developed by Bizople Solutions Pvt. Ltd. + +from . import backend_configurator +from . import res_users +from . import res_company +from . import res_config_setting +from . import ir_menu +from . import favorite_apps +from . import bookmark +from . import multi_tab +from . import ir_http +from . import pwa_shortcuts +from . import to_do_list +from . import ir_module \ No newline at end of file diff --git a/spiffy_theme_backend/models/backend_configurator.py b/spiffy_theme_backend/models/backend_configurator.py new file mode 100644 index 00000000..61a4c357 --- /dev/null +++ b/spiffy_theme_backend/models/backend_configurator.py @@ -0,0 +1,160 @@ +# -*- coding: utf-8 -*- +# See LICENSE file for full copyright and licensing details. +# Developed by Bizople Solutions Pvt. Ltd. + +from odoo.modules.module import get_resource_path +from odoo import api, fields, models, tools, _ +from odoo.exceptions import UserError +import base64 + +class BackendConfig(models.Model): + _name = 'backend.config' + _description = "Configurator Backend Theme" + + def _default_app_drawer_bg_image(self): + image_path = get_resource_path( + 'spiffy_theme_backend', 'static/description', 'app-drawer-bg-image.png') + with tools.file_open(image_path, 'rb') as f: + return base64.b64encode(f.read()) + + use_custom_colors = fields.Boolean(string="Use Custom Colors") + use_custom_drawer_color = fields.Boolean(string="Use Custom Drawer Colors") + + tree_form_split_view = fields.Boolean(string="Tree Form Split View") + + color_pallet = fields.Selection([ + ('pallet_1', 'Color Pallet 1'), + ('pallet_2', 'Color Pallet 2'), + ('pallet_3', 'Color Pallet 3'), + ('pallet_4', 'Color Pallet 4'), + ('pallet_5', 'Color Pallet 5'), + ('pallet_6', 'Color Pallet 6'), + ('pallet_7', 'Color Pallet 7'), + ('pallet_8', 'Color Pallet 8'), + ('pallet_9', 'Color Pallet 9'), + ],default="pallet_9", string="Color Pallets") + + drawer_color_pallet = fields.Selection([ + ('drawer_pallet_1', 'Color Pallet 1'), + ('drawer_pallet_2', 'Color Pallet 2'), + ('drawer_pallet_3', 'Color Pallet 3'), + ('drawer_pallet_4', 'Color Pallet 4'), + ('drawer_pallet_5', 'Color Pallet 5'), + ('drawer_pallet_6', 'Color Pallet 6'), + ('drawer_pallet_7', 'Color Pallet 7'), + ('drawer_pallet_8', 'Color Pallet 8'), + ('drawer_pallet_9', 'Color Pallet 9'), + ],default="drawer_pallet_9", string="Drawer Color Pallets") + + appdrawer_custom_bg_color = fields.Char(string="App Drawer Custom Background Color",default="#0097a7") + appdrawer_custom_text_color = fields.Char(string="App Drawer Custom Text Color",default="#ffffff") + + light_primary_bg_color = fields.Char(string="Primary Background Color for light",default="#0097a7") + light_primary_text_color = fields.Char(string="Primary Text Color for light",default="#ffffff") + + apply_light_bg_img = fields.Boolean(string="Apply light bg image") + light_bg_image = fields.Binary(string="Background Image For light", default=_default_app_drawer_bg_image, readonly=False) + + dark_primary_bg_color = fields.Char(string="Primary Background Color for dark",default="#0097a7") + dark_primary_text_color = fields.Char(string="Primary Text Color for dark",default="#ffffff") + + dark_secondry_bg_color = fields.Char(string="Secondry Background Color for dark",default="#242424") + dark_secondry_text_color = fields.Char(string="Secondry Text Color for dark",default="#ffffff") + + dark_body_bg_color = fields.Char(string="Body Background Color for dark",default="#1d1d1d") + dark_body_text_color = fields.Char(string="Body Text Color for dark",default="#ffffff") + + separator = fields.Selection([ + ('separator_style_1', 'Separator Style 1'), + ('separator_style_2', 'Separator Style 2'), + ('separator_style_3', 'Separator Style 3'), + ('separator_style_4', 'Separator Style 4')], + default="separator_style_2", string="Separator Styles") + + tab = fields.Selection([ + ('tab_style_1', 'Tab Style 1'), + ('tab_style_2', 'Tab Style 2'), + ('tab_style_3', 'Tab Style 3'), + ('tab_style_4', 'Tab Style 4')], + default="tab_style_1", string="Tab Styles") + + checkbox = fields.Selection([ + ('checkbox_style_1', 'Checkbox Style 1'), + ('checkbox_style_2', 'Checkbox Style 2'), + ('checkbox_style_3', 'Checkbox Style 3'), + ('checkbox_style_4', 'Checkbox Style 4')], + default="checkbox_style_4", string="Checkbox Styles") + + radio = fields.Selection([ + ('radio_style_1', 'Radio Style 1'), + ('radio_style_2', 'Radio Style 2'), + ('radio_style_3', 'Radio Style 3'), + ('radio_style_4', 'Radio Style 4')], + default="radio_style_1", string="Radio Styles") + + popup = fields.Selection([ + ('popup_style_1', 'popup Style 1'), + ('popup_style_2', 'popup Style 2'), + ('popup_style_3', 'popup Style 3'), + ('popup_style_4', 'popup Style 4')], + default="popup_style_2", string="popup Styles") + + chatter_position = fields.Selection([ + ('chatter_right', 'Chatter Right'), + ('chatter_bottom', 'Chatter Bottom')], + default="chatter_right", string="Chatter Position") + + top_menu_position = fields.Selection([ + ('top_menu_horizontal', 'Top Menu Horizontal'), + ('top_menu_vertical', 'Top Menu Vertical')], + default="top_menu_vertical", string="Top Menu Position") + + theme_style = fields.Selection([ + ('biz_theme_rounded', 'Rounded Theme'), + ('biz_theme_standard', 'Standard Theme'), + ('biz_theme_square', 'Square Theme')], + default="biz_theme_rounded", string="Theme Style") + + attachment_in_tree_view = fields.Boolean(string="Show Attachement in tree view") + + font_size = fields.Selection([ + ('font_small', 'Font Small'), + ('font_medium', 'Font Medium'), + ('font_large', 'Font large')], + default="font_medium", string="Font size") + + loader_style = fields.Selection([ + ('loader_style_1', 'Loader Style 1'), + ('loader_style_2', 'Loader Style 2'), + ('loader_style_3', 'Loader Style 3'), + ('loader_style_4', 'Loader Style 4'), + ('loader_style_5', 'Loader Style 5'), + ('loader_style_6', 'Loader Style 6'), + ('loader_style_7', 'Loader Style 7'), + ('loader_style_8', 'Loader Style 8'), + ('loader_style_9', 'Loader Style 9'), + ('loader_style_10', 'Loader Style 10'),], + default="loader_style_10", string="Loader Styles") + + font_family = fields.Selection([ + ('lato', 'Lato'), + ('montserrat', 'Montserrat'), + ('open_sans', 'Open Sans'), + ('oswald', 'Oswald'), + ('raleway', 'Raleway'), + ('roboto', 'Roboto'), + ('poppins', 'Poppins'), + ('rubik', 'Rubik'), + ('inter', 'Inter'), + ('josefin_sans', 'Josefin Sans'), + ('varela_round', 'Varela Round'), + ('manrope', 'Manrope'), + ('Nunito_Sans', 'Nunito Sans')], + default="rubik", string="Font Family") + + list_view_density = fields.Selection([ + ('list_comfortable', 'Comfortable'), + ('list_compact', 'Compact'),], + default="list_comfortable", string="List View Density") + + list_view_sticky_header = fields.Boolean(string="List view Sticky Header") diff --git a/spiffy_theme_backend/models/bookmark.py b/spiffy_theme_backend/models/bookmark.py new file mode 100644 index 00000000..084ab628 --- /dev/null +++ b/spiffy_theme_backend/models/bookmark.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# Part of Odoo Module Developed by Bizople Solutions Pvt. Ltd. +# See LICENSE file for full copyright and licensing details. + +from odoo import models, fields, api + +class Bookmarklink(models.Model): + _name = 'bookmark.link' + _description = "Bookmark Link" + + name = fields.Char("Name") + title = fields.Char("Title") + url = fields.Char("URL") + user_id = fields.Many2one('res.users') \ No newline at end of file diff --git a/spiffy_theme_backend/models/favorite_apps.py b/spiffy_theme_backend/models/favorite_apps.py new file mode 100644 index 00000000..68976476 --- /dev/null +++ b/spiffy_theme_backend/models/favorite_apps.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# Part of Odoo Module Developed by Bizople Solutions Pvt. Ltd. +# See LICENSE file for full copyright and licensing details. + +from odoo import models, fields, api + +class FavoriteApps(models.Model): + _name = "favorite.apps" + _description = "Favorite Apps" + + name = fields.Char("Name") + app_id = fields.Char("App Id") + app_xmlid = fields.Char("App XML Id") + app_actionid = fields.Char("App Action Id") + user_id = fields.Many2one('res.users') \ No newline at end of file diff --git a/spiffy_theme_backend/models/ir_http.py b/spiffy_theme_backend/models/ir_http.py new file mode 100644 index 00000000..ca43757f --- /dev/null +++ b/spiffy_theme_backend/models/ir_http.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Developed by Bizople Solutions Pvt. Ltd. +# See LICENSE file for full copyright and licensing details + +from odoo import api, models +from odoo.http import request + +class Http(models.AbstractModel): + _inherit = 'ir.http' + + def session_info(self): + # Show company change option even if single company available + is_bg_color = self.env.user.table_color + res = super(Http, self).session_info() + # user = request.env.user + company = self.env.company + + session_sid = request.session.sid + if self.env.user.image_1920: + image = self.env.user.image_1920.decode('utf-8') + else: + image = '' + res.update({'bg_color':is_bg_color,'user_image':image,'session_sid':session_sid,'spiffy_installed':True}) + + if self.env.user.has_group('base.group_user'): + res.update({ + "display_switch_company_menu": True, + "prevent_auto_save_warning_msg": company.prevent_auto_save_warning if company.prevent_auto_save_warning else '', + }) + + return res \ No newline at end of file diff --git a/spiffy_theme_backend/models/ir_menu.py b/spiffy_theme_backend/models/ir_menu.py new file mode 100644 index 00000000..12b41cfb --- /dev/null +++ b/spiffy_theme_backend/models/ir_menu.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# Developed by Bizople Solutions Pvt. Ltd. +# See LICENSE file for full copyright and licensing details + +from odoo import models, fields, api + +class IrUiMenu(models.Model): + _inherit = "ir.ui.menu" + + icon_img = fields.Image("Menu New Image") + use_icon = fields.Boolean("Use Icon") + icon_class_name = fields.Char("Icon Class Name") diff --git a/spiffy_theme_backend/models/ir_module.py b/spiffy_theme_backend/models/ir_module.py new file mode 100644 index 00000000..4eab9a8e --- /dev/null +++ b/spiffy_theme_backend/models/ir_module.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Developed by Bizople Solutions Pvt. Ltd. +# See LICENSE file for full copyright and licensing details + +from odoo import models +from odoo.http import request + +class Module(models.Model): + _inherit = "ir.module.module" + + def next(self): + """ + Return the action linked to an ir.actions.todo is there exists one that + should be executed. Otherwise, redirect to /web + """ + Todos = self.env['ir.actions.todo'] + active_todo = Todos.search([('state', '=', 'open')], limit=1) + if active_todo: + return active_todo.action_launch() + if request.env.user.table_color: + return { + 'type': 'ir.actions.act_url', + 'target': 'self', + 'url': '/web?bg_color=True&tool_color_id=1', + } + else: + return { + 'type': 'ir.actions.act_url', + 'target': 'self', + 'url': '/web', + } \ No newline at end of file diff --git a/spiffy_theme_backend/models/multi_tab.py b/spiffy_theme_backend/models/multi_tab.py new file mode 100644 index 00000000..67f97c47 --- /dev/null +++ b/spiffy_theme_backend/models/multi_tab.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Part of Odoo Module Developed by Bizople Solutions Pvt. Ltd. +# See LICENSE file for full copyright and licensing details. + +from odoo import models, fields, api + +class MultiTab(models.Model): + _name = 'biz.multi.tab' + _description = "Multi Tab" + + name = fields.Char("App Name") + url = fields.Char("URL") + actionId = fields.Char("Action ID") + menuId = fields.Char("Menu ID") + user_id = fields.Many2one('res.users') + menu_xmlid = fields.Char("XML ID Name") \ No newline at end of file diff --git a/spiffy_theme_backend/models/pwa_shortcuts.py b/spiffy_theme_backend/models/pwa_shortcuts.py new file mode 100644 index 00000000..f2da4a8f --- /dev/null +++ b/spiffy_theme_backend/models/pwa_shortcuts.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# See LICENSE file for full copyright and licensing details. +# Developed by Bizople Solutions Pvt. Ltd. + +from odoo import api, fields, models, _ + +class PWAshortcuts(models.Model): + _name = 'pwa.shortcuts' + _description = "PWA Shortcuts" + + name = fields.Char("Name", required=True) + short_name = fields.Char("Short Name", required=True) + url = fields.Char("URL", required=True, default='/') + description = fields.Char("Description", required=True) + image_192_shortcut = fields.Binary('Image 192px', readonly=False) \ No newline at end of file diff --git a/spiffy_theme_backend/models/res_company.py b/spiffy_theme_backend/models/res_company.py new file mode 100644 index 00000000..938aee84 --- /dev/null +++ b/spiffy_theme_backend/models/res_company.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +# See LICENSE file for full copyright and licensing details. +# Developed by Bizople Solutions Pvt. Ltd. + +import base64 +from odoo import api, http, fields, models, tools +from odoo.http import request +from odoo.modules.module import get_resource_path +from odoo.tools.translate import _ + +class Company(models.Model): + _inherit = 'res.company' + + tab_name = fields.Char(string="Backend Tab Name", default="Spiffy", readonly=False) + backend_theme_level = fields.Selection([ + ('user_level', 'User Level'), + ('global_level', 'Global Level')], + default="user_level", required=True, string="Backend Theme Level", readonly=False) + + login_page_style = fields.Selection([ + ('login_style_1', 'Login Style 1'), + ('login_style_2', 'Login Style 2'), + ('login_style_3', 'Login Style 3'), + ('login_style_4', 'Login Style 4')], + default="login_style_1", required=True, string="Login Styles", readonly=False) + + login_page_background_img = fields.Binary('Login Background Image', readonly=False, store=True) + login_page_background_color = fields.Char('Login Background Color', default="#f2f6ff", readonly=False) + login_page_text_color = fields.Char('Login Text Color', default="#777777", readonly=False) + show_bg_image = fields.Boolean(string='Add Login Background Image', readonly=False) + + def get_login_page_data(self): + admin_users = request.env['res.users'].sudo().search([ + ('groups_id','in',request.env.ref('base.user_admin').id), + ('backend_theme_config','!=',False), + ], order="id asc", limit=1) + admin_config = False + if admin_users: + admin_config = admin_users.backend_theme_config + + if admin_config: + config_vals = admin_config + else: + config_vals = request.env['backend.config'].sudo().search([], order="id asc", limit=1) + + values = { + 'config_vals': config_vals, + } + return values + + backend_menubar_logo = fields.Binary( + string="Menubar Logo", readonly=False) + backend_menubar_logo_icon = fields.Binary( + string="Menubar Logo Icon", readonly=False) + + enable_pwa = fields.Boolean(string='Enable PWA', readonly=False) + app_name_pwa = fields.Char('App Name', readonly=False, default='Spiffy') + short_name_pwa = fields.Char('Short Name', readonly=False, default='Spiffy') + description_pwa = fields.Char('App Description', readonly=False, default='Spiffy') + image_192_pwa = fields.Binary('Image 192px', readonly=False, store=True) + image_512_pwa = fields.Binary('Image 512px', readonly=False, store=True) + start_url_pwa = fields.Char('App Start Url', readonly=False, default='/web') + background_color_pwa = fields.Char('Background Color', readonly=False, default='#0097a7') + theme_color_pwa = fields.Char('Theme Color', readonly=False, default='#0097a7') + pwa_shortcuts_ids = fields.Many2many('pwa.shortcuts', string='PWA Shortcuts') + + spiffy_toobar_color = fields.Char('Toolbar Color', readonly=False, default='#0097a7') + prevent_auto_save = fields.Boolean(string='Prevent Auto Save', readonly=False) + + prevent_auto_save_warning = fields.Char('Auto Save Warning', translate=True, default="Autosave is disabled, Click on save button.", readonly=False) diff --git a/spiffy_theme_backend/models/res_config_setting.py b/spiffy_theme_backend/models/res_config_setting.py new file mode 100644 index 00000000..114be370 --- /dev/null +++ b/spiffy_theme_backend/models/res_config_setting.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +# See LICENSE file for full copyright and licensing details. +# Developed by Bizople Solutions Pvt. Ltd. + +from odoo.modules.module import get_resource_path +from odoo import api, http, fields, models, tools, _ +from odoo.http import request +import base64 + +class ResConfig(models.TransientModel): + _inherit = 'res.config.settings' + + spiffy_favicon = fields.Binary(related='company_id.favicon', + string="Backend Tab Favicon", readonly=False) + tab_name = fields.Char(related='company_id.tab_name', + string="Backend Tab Name", readonly=False) + backend_theme_level = fields.Selection( + related='company_id.backend_theme_level', string="Backend Theme Level", required=True, readonly=False) + + login_page_style = fields.Selection( + related='company_id.login_page_style', string="Login Styles", required=True, readonly=False) + + login_page_background_img = fields.Binary( + related='company_id.login_page_background_img', string="Login Background Image", readonly=False) + + login_page_background_color = fields.Char( + related='company_id.login_page_background_color', string='Login Background Color', readonly=False) + + login_page_text_color = fields.Char( + related='company_id.login_page_text_color', string='Login Text Color', readonly=False) + + show_bg_image = fields.Boolean( + related='company_id.show_bg_image', string='Add Login Background Image', readonly=False) + + backend_menubar_logo = fields.Binary( + related='company_id.backend_menubar_logo', string="Menubar Logo", readonly=False) + + backend_menubar_logo_icon = fields.Binary( + related='company_id.backend_menubar_logo_icon', string="Menubar Logo Icon", readonly=False) + + # Fields for PWA start + enable_pwa = fields.Boolean( + string='Enable PWA', related='company_id.enable_pwa', readonly=False,) + app_name_pwa = fields.Char( + 'App Name', related='company_id.app_name_pwa', readonly=False) + short_name_pwa = fields.Char( + 'Short Name', related='company_id.short_name_pwa', readonly=False) + description_pwa = fields.Char( + 'App Description', related='company_id.description_pwa', readonly=False) + image_192_pwa = fields.Binary( + 'Image 192px', related='company_id.image_192_pwa', readonly=False) + image_512_pwa = fields.Binary( + 'Image 512px', related='company_id.image_512_pwa', readonly=False) + start_url_pwa = fields.Char( + 'App Start Url', related='company_id.start_url_pwa', readonly=False) + background_color_pwa = fields.Char( + 'Background Color', related='company_id.background_color_pwa', readonly=False) + theme_color_pwa = fields.Char( + 'Theme Color', related='company_id.theme_color_pwa', readonly=False) + pwa_shortcuts_ids = fields.Many2many( + related='company_id.pwa_shortcuts_ids', readonly=False) + # Fields for PWA end + + spiffy_toobar_color = fields.Char('Toolbar Color', related='company_id.spiffy_toobar_color', readonly=False) + + prevent_auto_save = fields.Boolean( + related='company_id.prevent_auto_save', string='Prevent Auto Save ?', readonly=False) + + prevent_auto_save_warning = fields.Char('Auto Save Warning', related='company_id.prevent_auto_save_warning', readonly=False) \ No newline at end of file diff --git a/spiffy_theme_backend/models/res_users.py b/spiffy_theme_backend/models/res_users.py new file mode 100644 index 00000000..18463317 --- /dev/null +++ b/spiffy_theme_backend/models/res_users.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Part of Odoo Module Developed by Bizople Solutions Pvt. Ltd. +# See LICENSE file for full copyright and licensing details. + +from odoo import models, fields, api + +class User(models.Model): + _inherit = "res.users" + + app_ids = fields.One2many('favorite.apps', 'user_id',string="Favorite Apps") + bookmark_ids = fields.One2many('bookmark.link', 'user_id',string="Bookmark Links") + dark_mode = fields.Boolean(string="Is dark Mode Active", default=False) + vertical_sidebar_pinned = fields.Boolean(string="Pinned Sidebar", default=True) + backend_theme_config = fields.Many2one('backend.config', string="Backend Config", copy=False) + multi_tab_ids = fields.One2many('biz.multi.tab', 'user_id', string="Multi Tabs") + enable_todo_list = fields.Boolean(string="Enable To Do List", default=True) + todo_list_ids = fields.One2many('todo.list', 'user_id', string="To Do List") + table_color = fields.Boolean(string="Is Body Color") + tool_color_id = fields.Char(string="Tool Color") + + @property + def SELF_READABLE_FIELDS(self): + return super().SELF_READABLE_FIELDS + ['enable_todo_list'] + + @property + def SELF_WRITEABLE_FIELDS(self): + return super().SELF_WRITEABLE_FIELDS + ['enable_todo_list'] \ No newline at end of file diff --git a/spiffy_theme_backend/models/to_do_list.py b/spiffy_theme_backend/models/to_do_list.py new file mode 100644 index 00000000..a4658511 --- /dev/null +++ b/spiffy_theme_backend/models/to_do_list.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Part of Odoo Module Developed by Bizople Solutions Pvt. Ltd. +# See LICENSE file for full copyright and licensing details. + +from odoo import models, fields, api + +class ToDoList(models.Model): + _name = "todo.list" + _description = "To Do List" + _order = 'write_date desc, create_date desc' + + def _default_sequence(self): + return (self.search([], order="sequence desc", limit=1).sequence or 0) + 1 + + sequence = fields.Integer('sequence', default=_default_sequence) + name = fields.Char("Title") + description = fields.Html('Description') + # marked_done = fields.Boolean("Done?") + user_id = fields.Many2one('res.users', string="User") + create_date = fields.Datetime(string="Created on") + write_date = fields.Datetime("Last Updated On", index=True) + note_color_pallet = fields.Selection([ + ('pallet_1', 'Pallet 1'), + ('pallet_2', 'Pallet 2'), + ('pallet_3', 'Pallet 3'), + ('pallet_4', 'Pallet 4'), + ('pallet_5', 'Pallet 5'), + ('pallet_6', 'Pallet 6'), + ('pallet_7', 'Pallet 7'), + ],default="pallet_1", string="Notes Color Pallets", required=True) \ No newline at end of file diff --git a/spiffy_theme_backend/security/ir.model.access.csv b/spiffy_theme_backend/security/ir.model.access.csv new file mode 100644 index 00000000..abd0aea9 --- /dev/null +++ b/spiffy_theme_backend/security/ir.model.access.csv @@ -0,0 +1,7 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_favorite_apps,access_favorite_apps,model_favorite_apps,base.group_user,1,0,0,0 +access_bookmark_link,access_bookmark_link,model_bookmark_link,base.group_user,1,0,0,0 +access_backend_config,access_backend_config,model_backend_config,base.group_user,1,1,1,0 +access_pwa_shortcuts,access_pwa_shortcuts,model_pwa_shortcuts,,1,1,1,1 +access_biz_multi_tab,access_biz_multi_tab,model_biz_multi_tab,base.group_user,1,0,0,0 +access_todo_list,access_todo_list,model_todo_list,,1,1,1,1 \ No newline at end of file diff --git a/spiffy_theme_backend/static/description/3d-icon-pack.jpg b/spiffy_theme_backend/static/description/3d-icon-pack.jpg new file mode 100644 index 00000000..be0ecbfd Binary files /dev/null and b/spiffy_theme_backend/static/description/3d-icon-pack.jpg differ diff --git a/spiffy_theme_backend/static/description/Bizople-logo-black.png b/spiffy_theme_backend/static/description/Bizople-logo-black.png new file mode 100644 index 00000000..b7337ae0 Binary files /dev/null and b/spiffy_theme_backend/static/description/Bizople-logo-black.png differ diff --git a/spiffy_theme_backend/static/description/Bizople-partner-logo.png b/spiffy_theme_backend/static/description/Bizople-partner-logo.png new file mode 100644 index 00000000..c8cca076 Binary files /dev/null and b/spiffy_theme_backend/static/description/Bizople-partner-logo.png differ diff --git a/spiffy_theme_backend/static/description/Index-main-banner.jpg b/spiffy_theme_backend/static/description/Index-main-banner.jpg new file mode 100644 index 00000000..4a250bcd Binary files /dev/null and b/spiffy_theme_backend/static/description/Index-main-banner.jpg differ diff --git a/spiffy_theme_backend/static/description/Spiffy-full-white.png b/spiffy_theme_backend/static/description/Spiffy-full-white.png new file mode 100644 index 00000000..6c8ca6ca Binary files /dev/null and b/spiffy_theme_backend/static/description/Spiffy-full-white.png differ diff --git a/spiffy_theme_backend/static/description/app-drawer-bg-image.png b/spiffy_theme_backend/static/description/app-drawer-bg-image.png new file mode 100644 index 00000000..81f5bc7c Binary files /dev/null and b/spiffy_theme_backend/static/description/app-drawer-bg-image.png differ diff --git a/spiffy_theme_backend/static/description/app-drawer.jpg b/spiffy_theme_backend/static/description/app-drawer.jpg new file mode 100644 index 00000000..2d8a6fc0 Binary files /dev/null and b/spiffy_theme_backend/static/description/app-drawer.jpg differ diff --git a/spiffy_theme_backend/static/description/app-icon-config.gif b/spiffy_theme_backend/static/description/app-icon-config.gif new file mode 100644 index 00000000..7806e8ea Binary files /dev/null and b/spiffy_theme_backend/static/description/app-icon-config.gif differ diff --git a/spiffy_theme_backend/static/description/bizople-icon.png b/spiffy_theme_backend/static/description/bizople-icon.png new file mode 100644 index 00000000..82285427 Binary files /dev/null and b/spiffy_theme_backend/static/description/bizople-icon.png differ diff --git a/spiffy_theme_backend/static/description/bizople-white-logo-icon.png b/spiffy_theme_backend/static/description/bizople-white-logo-icon.png new file mode 100644 index 00000000..1ea4edea Binary files /dev/null and b/spiffy_theme_backend/static/description/bizople-white-logo-icon.png differ diff --git a/spiffy_theme_backend/static/description/bizople-white-logo.png b/spiffy_theme_backend/static/description/bizople-white-logo.png new file mode 100644 index 00000000..c0c24f2f Binary files /dev/null and b/spiffy_theme_backend/static/description/bizople-white-logo.png differ diff --git a/spiffy_theme_backend/static/description/bookmark.gif b/spiffy_theme_backend/static/description/bookmark.gif new file mode 100644 index 00000000..a22a5a04 Binary files /dev/null and b/spiffy_theme_backend/static/description/bookmark.gif differ diff --git a/spiffy_theme_backend/static/description/branding.gif b/spiffy_theme_backend/static/description/branding.gif new file mode 100644 index 00000000..f08c419d Binary files /dev/null and b/spiffy_theme_backend/static/description/branding.gif differ diff --git a/spiffy_theme_backend/static/description/chatter-attachment-in-list.gif b/spiffy_theme_backend/static/description/chatter-attachment-in-list.gif new file mode 100644 index 00000000..8cb120fa Binary files /dev/null and b/spiffy_theme_backend/static/description/chatter-attachment-in-list.gif differ diff --git a/spiffy_theme_backend/static/description/darklight-pin-unpin.gif b/spiffy_theme_backend/static/description/darklight-pin-unpin.gif new file mode 100644 index 00000000..2221c13a Binary files /dev/null and b/spiffy_theme_backend/static/description/darklight-pin-unpin.gif differ diff --git a/spiffy_theme_backend/static/description/drawer-global-search.gif b/spiffy_theme_backend/static/description/drawer-global-search.gif new file mode 100644 index 00000000..9a7cc737 Binary files /dev/null and b/spiffy_theme_backend/static/description/drawer-global-search.gif differ diff --git a/spiffy_theme_backend/static/description/fav-apps.gif b/spiffy_theme_backend/static/description/fav-apps.gif new file mode 100644 index 00000000..363a8022 Binary files /dev/null and b/spiffy_theme_backend/static/description/fav-apps.gif differ diff --git a/spiffy_theme_backend/static/description/feature-banner-diff-color.png b/spiffy_theme_backend/static/description/feature-banner-diff-color.png new file mode 100644 index 00000000..9c031cd6 Binary files /dev/null and b/spiffy_theme_backend/static/description/feature-banner-diff-color.png differ diff --git a/spiffy_theme_backend/static/description/feature-banner-no-split-view.png b/spiffy_theme_backend/static/description/feature-banner-no-split-view.png new file mode 100644 index 00000000..87823733 Binary files /dev/null and b/spiffy_theme_backend/static/description/feature-banner-no-split-view.png differ diff --git a/spiffy_theme_backend/static/description/feature-banner.png b/spiffy_theme_backend/static/description/feature-banner.png new file mode 100644 index 00000000..ff2e34b2 Binary files /dev/null and b/spiffy_theme_backend/static/description/feature-banner.png differ diff --git a/spiffy_theme_backend/static/description/fontsize-radio-checkbox.jpg b/spiffy_theme_backend/static/description/fontsize-radio-checkbox.jpg new file mode 100644 index 00000000..e70e21f1 Binary files /dev/null and b/spiffy_theme_backend/static/description/fontsize-radio-checkbox.jpg differ diff --git a/spiffy_theme_backend/static/description/icon.png b/spiffy_theme_backend/static/description/icon.png new file mode 100644 index 00000000..7c440266 Binary files /dev/null and b/spiffy_theme_backend/static/description/icon.png differ diff --git a/spiffy_theme_backend/static/description/imac-header.png b/spiffy_theme_backend/static/description/imac-header.png new file mode 100644 index 00000000..beed7a66 Binary files /dev/null and b/spiffy_theme_backend/static/description/imac-header.png differ diff --git a/spiffy_theme_backend/static/description/index-main-banner.gif b/spiffy_theme_backend/static/description/index-main-banner.gif new file mode 100644 index 00000000..6e6bfdd5 Binary files /dev/null and b/spiffy_theme_backend/static/description/index-main-banner.gif differ diff --git a/spiffy_theme_backend/static/description/index-responsive.jpg b/spiffy_theme_backend/static/description/index-responsive.jpg new file mode 100644 index 00000000..be9488f8 Binary files /dev/null and b/spiffy_theme_backend/static/description/index-responsive.jpg differ diff --git a/spiffy_theme_backend/static/description/index.html b/spiffy_theme_backend/static/description/index.html new file mode 100644 index 00000000..acf8893f --- /dev/null +++ b/spiffy_theme_backend/static/description/index.html @@ -0,0 +1,654 @@ +
+ + +
+
+ +
+ + + +
+

Enhance your Productivity with the Advance Key Features

+ +
+
+
+ +
+
+ + + +
+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+

+ Note:
If you are using Website App and want to have web like login page design, you can purchase the Website Login Add-on module from the store +

+
+
+
+ + + + + + + +
+
+
+ +
+
+

All Features

+
+
+
+ + +
+
+ + 1. + + + 3 in 1 Theme Styles + +
+
+
+
+ + 2. + + + Chatter Box Position + +
+
+ +
+
+ + 3. + + + Attachment in List View + +
+
+
+
+ + 4. + + + Vertical/Horizontal Menu Style + +
+
+
+
+ + 5. + + + 4 Seprator Styles + +
+
+
+
+ + 6. + + + 4 Tab Style + +
+
+
+
+ + 7. + + + 4 Checkbox Style + +
+
+
+
+ + 8. + + + 4 Radio Style + +
+
+
+
+ + 9. + + + 4 Popup Animations + +
+
+
+
+ + 10. + + + 9 Theme Color Pallets + +
+
+
+
+ + 11. + + + Custom Theme Color option + +
+
+
+
+ + 12. + + + 9 App Drawer Color Pallet + +
+
+
+
+ + 13. + + + Custom App Drawer Color + +
+
+
+
+ + 14. + + + 13 Unique Font Styles + +
+
+
+
+ + 15. + + + Font Sizes + +
+
+
+
+ + 16. + + + 10 Loading Icons + +
+
+
+
+ + 17. + + + 4 Login Page Designs + +
+
+
+
+ + 18. + + + Configurable App Icons + +
+
+
+
+ + 19. + + + Global Search + +
+
+
+
+ + 20. + + + Full Screen + +
+
+
+
+ + 21. + + + Zoom In / Zoom Out + +
+
+
+
+ + 22. + + + Bookmarks + +
+
+
+
+ + 23. + + + Dark/Light Mode + +
+
+
+
+ + 24. + + + Language Selector + +
+
+
+
+ + 25. + + + Company Selector + +
+
+
+
+ + 26. + + + Lock/Unlock Sidebar + +
+
+
+
+ + 27. + + + Debug Activator + +
+
+
+
+ + 28. + + + RTL Supported + +
+
+
+
+ + 29. + + + User Access (User/Global) + +
+
+
+
+ + 30. + + + Greetings on Menubar + +
+
+
+
+ + 31. + + + Custom Branding + +
+
+
+
+ + 32. + + + PWA with Shortcuts + +
+
+
+
+ + 33. + + + Favorite App Island + +
+
+
+
+ + 34. + + + Refresh Button + +
+
+
+
+ + 35. + + + Multiple Tab + +
+
+
+
+ + 36. + + + Notes + +
+
+
+
+ + 37. + + + List/Tree View Density + +
+
+
+
+ + 38. + + + List/Tree View Sticky Header & Footer + +
+
+
+
+ + 39. + + + Prevent Auto Save + +
+
+
+
+ + 40. + + + Modal Draggable & Resizable + +
+
+ +
+
+
+ +
+

+ Note: This Product is developed and tested with Odoo Addons +

+
+
+
+ Spiffy Mobile App Note: +
+
    +
  • Spiffy Mobile App is an Android and iOS application works with the Spiffy Backend Theme.
  • +
  • It is a complementary / FREE application with the Spiffy Backend Theme and it has no relation with the theme working.
  • +
  • The Application works well but is not satisfying the customer's specific needs it would not be considered to claim a refund for the Spiffy Backend Theme.
  • +
  • + Push Notification and 2-Factor Authentication functionalities are in process and will be available in the future version of Spiffy mobile Application. +
  • +
+
+
+
+ +
+
+ + +
+ +
+
+
+

Release Notes

+
+
+

v2.0

+

NewCompatibility with Spiffy: Odoo Mobile Application (android/ios)

+ +

v16.0.0.4

+

NewNew Features: Notes, List/Tree View Density, List/Tree View Sticky Header & Footer, Prevent Auto Save, Modal Draggable & Resizable.

+ +

v16.0.0.3

+

FixedImprovements on Login Styles and Design.

+ +

v16.0.0.2

+

FixedMinor bug fixes and improvements.

+ +

v16.0.0.1

+

NewMultiple Tab

+

FixedMinor bug fixes and improvements.

+ +

v16.0.0.0

+

Initial Release

+ + + + + + + +
+
+
\ No newline at end of file diff --git a/spiffy_theme_backend/static/description/lang-company-debug.jpg b/spiffy_theme_backend/static/description/lang-company-debug.jpg new file mode 100644 index 00000000..763249d3 Binary files /dev/null and b/spiffy_theme_backend/static/description/lang-company-debug.jpg differ diff --git a/spiffy_theme_backend/static/description/list-density.gif b/spiffy_theme_backend/static/description/list-density.gif new file mode 100644 index 00000000..b62fd63d Binary files /dev/null and b/spiffy_theme_backend/static/description/list-density.gif differ diff --git a/spiffy_theme_backend/static/description/list-sticky-header-footer.gif b/spiffy_theme_backend/static/description/list-sticky-header-footer.gif new file mode 100644 index 00000000..6c0bde94 Binary files /dev/null and b/spiffy_theme_backend/static/description/list-sticky-header-footer.gif differ diff --git a/spiffy_theme_backend/static/description/loaders-fontfamily.gif b/spiffy_theme_backend/static/description/loaders-fontfamily.gif new file mode 100644 index 00000000..8446057e Binary files /dev/null and b/spiffy_theme_backend/static/description/loaders-fontfamily.gif differ diff --git a/spiffy_theme_backend/static/description/loginpage.gif b/spiffy_theme_backend/static/description/loginpage.gif new file mode 100644 index 00000000..ee2a6950 Binary files /dev/null and b/spiffy_theme_backend/static/description/loginpage.gif differ diff --git a/spiffy_theme_backend/static/description/mail-send-line.png b/spiffy_theme_backend/static/description/mail-send-line.png new file mode 100644 index 00000000..93ce4ace Binary files /dev/null and b/spiffy_theme_backend/static/description/mail-send-line.png differ diff --git a/spiffy_theme_backend/static/description/modal-drag-resize.gif b/spiffy_theme_backend/static/description/modal-drag-resize.gif new file mode 100644 index 00000000..1e39c283 Binary files /dev/null and b/spiffy_theme_backend/static/description/modal-drag-resize.gif differ diff --git a/spiffy_theme_backend/static/description/multi-tab.gif b/spiffy_theme_backend/static/description/multi-tab.gif new file mode 100644 index 00000000..2c11c74e Binary files /dev/null and b/spiffy_theme_backend/static/description/multi-tab.gif differ diff --git a/spiffy_theme_backend/static/description/notes.gif b/spiffy_theme_backend/static/description/notes.gif new file mode 100644 index 00000000..08044e21 Binary files /dev/null and b/spiffy_theme_backend/static/description/notes.gif differ diff --git a/spiffy_theme_backend/static/description/odoo_ready_partner_logo.png b/spiffy_theme_backend/static/description/odoo_ready_partner_logo.png new file mode 100644 index 00000000..ad26adcb Binary files /dev/null and b/spiffy_theme_backend/static/description/odoo_ready_partner_logo.png differ diff --git a/spiffy_theme_backend/static/description/phone-line.png b/spiffy_theme_backend/static/description/phone-line.png new file mode 100644 index 00000000..f5fd9830 Binary files /dev/null and b/spiffy_theme_backend/static/description/phone-line.png differ diff --git a/spiffy_theme_backend/static/description/prevent-auto-save.jpg b/spiffy_theme_backend/static/description/prevent-auto-save.jpg new file mode 100644 index 00000000..d7983c8f Binary files /dev/null and b/spiffy_theme_backend/static/description/prevent-auto-save.jpg differ diff --git a/spiffy_theme_backend/static/description/refresh-btn.gif b/spiffy_theme_backend/static/description/refresh-btn.gif new file mode 100644 index 00000000..d7b967de Binary files /dev/null and b/spiffy_theme_backend/static/description/refresh-btn.gif differ diff --git a/spiffy_theme_backend/static/description/rtl-support.jpg b/spiffy_theme_backend/static/description/rtl-support.jpg new file mode 100644 index 00000000..85882244 Binary files /dev/null and b/spiffy_theme_backend/static/description/rtl-support.jpg differ diff --git a/spiffy_theme_backend/static/description/separator-tab-popup-styles.jpg b/spiffy_theme_backend/static/description/separator-tab-popup-styles.jpg new file mode 100644 index 00000000..a4a8dd6f Binary files /dev/null and b/spiffy_theme_backend/static/description/separator-tab-popup-styles.jpg differ diff --git a/spiffy_theme_backend/static/description/service-banner.jpg b/spiffy_theme_backend/static/description/service-banner.jpg new file mode 100644 index 00000000..837ed2ed Binary files /dev/null and b/spiffy_theme_backend/static/description/service-banner.jpg differ diff --git a/spiffy_theme_backend/static/description/skype-line.png b/spiffy_theme_backend/static/description/skype-line.png new file mode 100644 index 00000000..56bb435e Binary files /dev/null and b/spiffy_theme_backend/static/description/skype-line.png differ diff --git a/spiffy_theme_backend/static/description/spiffy-enterprise.png b/spiffy_theme_backend/static/description/spiffy-enterprise.png new file mode 100644 index 00000000..76d45977 Binary files /dev/null and b/spiffy_theme_backend/static/description/spiffy-enterprise.png differ diff --git a/spiffy_theme_backend/static/description/spiffy-index-pwa.jpg b/spiffy_theme_backend/static/description/spiffy-index-pwa.jpg new file mode 100644 index 00000000..299d397c Binary files /dev/null and b/spiffy_theme_backend/static/description/spiffy-index-pwa.jpg differ diff --git a/spiffy_theme_backend/static/description/spiffy-mobile-app-banner.jpg b/spiffy_theme_backend/static/description/spiffy-mobile-app-banner.jpg new file mode 100644 index 00000000..e0a00438 Binary files /dev/null and b/spiffy_theme_backend/static/description/spiffy-mobile-app-banner.jpg differ diff --git a/spiffy_theme_backend/static/description/spiffy-mobile-app-icon.png b/spiffy_theme_backend/static/description/spiffy-mobile-app-icon.png new file mode 100644 index 00000000..7498ad98 Binary files /dev/null and b/spiffy_theme_backend/static/description/spiffy-mobile-app-icon.png differ diff --git a/spiffy_theme_backend/static/description/spiffy-qr-code.png b/spiffy_theme_backend/static/description/spiffy-qr-code.png new file mode 100644 index 00000000..b48ae837 Binary files /dev/null and b/spiffy_theme_backend/static/description/spiffy-qr-code.png differ diff --git a/spiffy_theme_backend/static/description/spiffy_cover.png b/spiffy_theme_backend/static/description/spiffy_cover.png new file mode 100644 index 00000000..d3eadf62 Binary files /dev/null and b/spiffy_theme_backend/static/description/spiffy_cover.png differ diff --git a/spiffy_theme_backend/static/description/spiffy_screenshot.gif b/spiffy_theme_backend/static/description/spiffy_screenshot.gif new file mode 100644 index 00000000..62f2a136 Binary files /dev/null and b/spiffy_theme_backend/static/description/spiffy_screenshot.gif differ diff --git a/spiffy_theme_backend/static/description/spiffy_screenshot_offer.gif b/spiffy_theme_backend/static/description/spiffy_screenshot_offer.gif new file mode 100644 index 00000000..ace49908 Binary files /dev/null and b/spiffy_theme_backend/static/description/spiffy_screenshot_offer.gif differ diff --git a/spiffy_theme_backend/static/description/spiffy_website_login_addon.png b/spiffy_theme_backend/static/description/spiffy_website_login_addon.png new file mode 100644 index 00000000..65f73581 Binary files /dev/null and b/spiffy_theme_backend/static/description/spiffy_website_login_addon.png differ diff --git a/spiffy_theme_backend/static/description/split-view.gif b/spiffy_theme_backend/static/description/split-view.gif new file mode 100644 index 00000000..4dcdae47 Binary files /dev/null and b/spiffy_theme_backend/static/description/split-view.gif differ diff --git a/spiffy_theme_backend/static/description/support_banner.jpg b/spiffy_theme_backend/static/description/support_banner.jpg new file mode 100644 index 00000000..8e3075ef Binary files /dev/null and b/spiffy_theme_backend/static/description/support_banner.jpg differ diff --git a/spiffy_theme_backend/static/description/theme-color.gif b/spiffy_theme_backend/static/description/theme-color.gif new file mode 100644 index 00000000..cf199dfa Binary files /dev/null and b/spiffy_theme_backend/static/description/theme-color.gif differ diff --git a/spiffy_theme_backend/static/description/theme-style.gif b/spiffy_theme_backend/static/description/theme-style.gif new file mode 100644 index 00000000..7325302b Binary files /dev/null and b/spiffy_theme_backend/static/description/theme-style.gif differ diff --git a/spiffy_theme_backend/static/description/top-menu-position.gif b/spiffy_theme_backend/static/description/top-menu-position.gif new file mode 100644 index 00000000..3cdd8947 Binary files /dev/null and b/spiffy_theme_backend/static/description/top-menu-position.gif differ diff --git a/spiffy_theme_backend/static/description/user-level.jpg b/spiffy_theme_backend/static/description/user-level.jpg new file mode 100644 index 00000000..fdfa3e9e Binary files /dev/null and b/spiffy_theme_backend/static/description/user-level.jpg differ diff --git a/spiffy_theme_backend/static/description/vertical-pinned-menu-logo.png b/spiffy_theme_backend/static/description/vertical-pinned-menu-logo.png new file mode 100644 index 00000000..6c8ca6ca Binary files /dev/null and b/spiffy_theme_backend/static/description/vertical-pinned-menu-logo.png differ diff --git a/spiffy_theme_backend/static/description/vertical-unpinned-menu-logo.png b/spiffy_theme_backend/static/description/vertical-unpinned-menu-logo.png new file mode 100644 index 00000000..b941bfba Binary files /dev/null and b/spiffy_theme_backend/static/description/vertical-unpinned-menu-logo.png differ diff --git a/spiffy_theme_backend/static/description/zoom-fullscreen.gif b/spiffy_theme_backend/static/description/zoom-fullscreen.gif new file mode 100644 index 00000000..8f11afc0 Binary files /dev/null and b/spiffy_theme_backend/static/description/zoom-fullscreen.gif differ diff --git a/spiffy_theme_backend/static/src/image/backend_theme_icon.png b/spiffy_theme_backend/static/src/image/backend_theme_icon.png new file mode 100644 index 00000000..38dffba9 Binary files /dev/null and b/spiffy_theme_backend/static/src/image/backend_theme_icon.png differ diff --git a/spiffy_theme_backend/static/src/image/checked.png b/spiffy_theme_backend/static/src/image/checked.png new file mode 100644 index 00000000..ef664a82 Binary files /dev/null and b/spiffy_theme_backend/static/src/image/checked.png differ diff --git a/spiffy_theme_backend/static/src/image/circle.png b/spiffy_theme_backend/static/src/image/circle.png new file mode 100644 index 00000000..2ccaaa3e Binary files /dev/null and b/spiffy_theme_backend/static/src/image/circle.png differ diff --git a/spiffy_theme_backend/static/src/image/close.png b/spiffy_theme_backend/static/src/image/close.png new file mode 100644 index 00000000..f82fed32 Binary files /dev/null and b/spiffy_theme_backend/static/src/image/close.png differ diff --git a/spiffy_theme_backend/static/src/image/download (1).png b/spiffy_theme_backend/static/src/image/download (1).png new file mode 100644 index 00000000..0b5e1586 Binary files /dev/null and b/spiffy_theme_backend/static/src/image/download (1).png differ diff --git a/spiffy_theme_backend/static/src/image/download.svg b/spiffy_theme_backend/static/src/image/download.svg new file mode 100644 index 00000000..ef65bfb5 --- /dev/null +++ b/spiffy_theme_backend/static/src/image/download.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/spiffy_theme_backend/static/src/image/iconoff.png b/spiffy_theme_backend/static/src/image/iconoff.png new file mode 100644 index 00000000..54709463 Binary files /dev/null and b/spiffy_theme_backend/static/src/image/iconoff.png differ diff --git a/spiffy_theme_backend/static/src/image/iconon.png b/spiffy_theme_backend/static/src/image/iconon.png new file mode 100644 index 00000000..b573eae8 Binary files /dev/null and b/spiffy_theme_backend/static/src/image/iconon.png differ diff --git a/spiffy_theme_backend/static/src/image/icons8-light-50.png b/spiffy_theme_backend/static/src/image/icons8-light-50.png new file mode 100644 index 00000000..5771236a Binary files /dev/null and b/spiffy_theme_backend/static/src/image/icons8-light-50.png differ diff --git a/spiffy_theme_backend/static/src/image/icons8-light-on-50.png b/spiffy_theme_backend/static/src/image/icons8-light-on-50.png new file mode 100644 index 00000000..f147e47a Binary files /dev/null and b/spiffy_theme_backend/static/src/image/icons8-light-on-50.png differ diff --git a/spiffy_theme_backend/static/src/image/list_comfortable.png b/spiffy_theme_backend/static/src/image/list_comfortable.png new file mode 100644 index 00000000..c5f30764 Binary files /dev/null and b/spiffy_theme_backend/static/src/image/list_comfortable.png differ diff --git a/spiffy_theme_backend/static/src/image/list_compact.png b/spiffy_theme_backend/static/src/image/list_compact.png new file mode 100644 index 00000000..56fc050d Binary files /dev/null and b/spiffy_theme_backend/static/src/image/list_compact.png differ diff --git a/spiffy_theme_backend/static/src/image/loader_1.gif b/spiffy_theme_backend/static/src/image/loader_1.gif new file mode 100644 index 00000000..5695af06 Binary files /dev/null and b/spiffy_theme_backend/static/src/image/loader_1.gif differ diff --git a/spiffy_theme_backend/static/src/image/loader_1.svg b/spiffy_theme_backend/static/src/image/loader_1.svg new file mode 100644 index 00000000..128ab56a --- /dev/null +++ b/spiffy_theme_backend/static/src/image/loader_1.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spiffy_theme_backend/static/src/image/loader_10.gif b/spiffy_theme_backend/static/src/image/loader_10.gif new file mode 100644 index 00000000..8758d855 Binary files /dev/null and b/spiffy_theme_backend/static/src/image/loader_10.gif differ diff --git a/spiffy_theme_backend/static/src/image/loader_2.gif b/spiffy_theme_backend/static/src/image/loader_2.gif new file mode 100644 index 00000000..ddd14500 Binary files /dev/null and b/spiffy_theme_backend/static/src/image/loader_2.gif differ diff --git a/spiffy_theme_backend/static/src/image/loader_3.gif b/spiffy_theme_backend/static/src/image/loader_3.gif new file mode 100644 index 00000000..45b3d274 Binary files /dev/null and b/spiffy_theme_backend/static/src/image/loader_3.gif differ diff --git a/spiffy_theme_backend/static/src/image/loader_4.gif b/spiffy_theme_backend/static/src/image/loader_4.gif new file mode 100644 index 00000000..70b56c63 Binary files /dev/null and b/spiffy_theme_backend/static/src/image/loader_4.gif differ diff --git a/spiffy_theme_backend/static/src/image/loader_5.gif b/spiffy_theme_backend/static/src/image/loader_5.gif new file mode 100644 index 00000000..7f7a87cd Binary files /dev/null and b/spiffy_theme_backend/static/src/image/loader_5.gif differ diff --git a/spiffy_theme_backend/static/src/image/loader_6.gif b/spiffy_theme_backend/static/src/image/loader_6.gif new file mode 100644 index 00000000..3033753d Binary files /dev/null and b/spiffy_theme_backend/static/src/image/loader_6.gif differ diff --git a/spiffy_theme_backend/static/src/image/loader_7.gif b/spiffy_theme_backend/static/src/image/loader_7.gif new file mode 100644 index 00000000..56142fb3 Binary files /dev/null and b/spiffy_theme_backend/static/src/image/loader_7.gif differ diff --git a/spiffy_theme_backend/static/src/image/loader_8.gif b/spiffy_theme_backend/static/src/image/loader_8.gif new file mode 100644 index 00000000..181bc458 Binary files /dev/null and b/spiffy_theme_backend/static/src/image/loader_8.gif differ diff --git a/spiffy_theme_backend/static/src/image/loader_9.gif b/spiffy_theme_backend/static/src/image/loader_9.gif new file mode 100644 index 00000000..9076b270 Binary files /dev/null and b/spiffy_theme_backend/static/src/image/loader_9.gif differ diff --git a/spiffy_theme_backend/static/src/image/pallet_1.png b/spiffy_theme_backend/static/src/image/pallet_1.png new file mode 100644 index 00000000..c10349ea Binary files /dev/null and b/spiffy_theme_backend/static/src/image/pallet_1.png differ diff --git a/spiffy_theme_backend/static/src/image/pallet_2.png b/spiffy_theme_backend/static/src/image/pallet_2.png new file mode 100644 index 00000000..f249dead Binary files /dev/null and b/spiffy_theme_backend/static/src/image/pallet_2.png differ diff --git a/spiffy_theme_backend/static/src/image/pallet_3.png b/spiffy_theme_backend/static/src/image/pallet_3.png new file mode 100644 index 00000000..be1b873c Binary files /dev/null and b/spiffy_theme_backend/static/src/image/pallet_3.png differ diff --git a/spiffy_theme_backend/static/src/image/pallet_4.png b/spiffy_theme_backend/static/src/image/pallet_4.png new file mode 100644 index 00000000..8d8a4352 Binary files /dev/null and b/spiffy_theme_backend/static/src/image/pallet_4.png differ diff --git a/spiffy_theme_backend/static/src/image/pallet_5.png b/spiffy_theme_backend/static/src/image/pallet_5.png new file mode 100644 index 00000000..f2c44a22 Binary files /dev/null and b/spiffy_theme_backend/static/src/image/pallet_5.png differ diff --git a/spiffy_theme_backend/static/src/image/pallet_6.png b/spiffy_theme_backend/static/src/image/pallet_6.png new file mode 100644 index 00000000..0be4b195 Binary files /dev/null and b/spiffy_theme_backend/static/src/image/pallet_6.png differ diff --git a/spiffy_theme_backend/static/src/image/pallet_7.png b/spiffy_theme_backend/static/src/image/pallet_7.png new file mode 100644 index 00000000..21356cce Binary files /dev/null and b/spiffy_theme_backend/static/src/image/pallet_7.png differ diff --git a/spiffy_theme_backend/static/src/image/pallet_8.png b/spiffy_theme_backend/static/src/image/pallet_8.png new file mode 100644 index 00000000..345745c6 Binary files /dev/null and b/spiffy_theme_backend/static/src/image/pallet_8.png differ diff --git a/spiffy_theme_backend/static/src/image/pallet_9.png b/spiffy_theme_backend/static/src/image/pallet_9.png new file mode 100644 index 00000000..7948dfb5 Binary files /dev/null and b/spiffy_theme_backend/static/src/image/pallet_9.png differ diff --git a/spiffy_theme_backend/static/src/image/pallets.jpg b/spiffy_theme_backend/static/src/image/pallets.jpg new file mode 100644 index 00000000..538193e4 Binary files /dev/null and b/spiffy_theme_backend/static/src/image/pallets.jpg differ diff --git a/spiffy_theme_backend/static/src/image/theme-chatter-bottom.png b/spiffy_theme_backend/static/src/image/theme-chatter-bottom.png new file mode 100644 index 00000000..3ca8cc6b Binary files /dev/null and b/spiffy_theme_backend/static/src/image/theme-chatter-bottom.png differ diff --git a/spiffy_theme_backend/static/src/image/theme-chatter-right.png b/spiffy_theme_backend/static/src/image/theme-chatter-right.png new file mode 100644 index 00000000..1a648e98 Binary files /dev/null and b/spiffy_theme_backend/static/src/image/theme-chatter-right.png differ diff --git a/spiffy_theme_backend/static/src/image/theme-menu-horizontal.png b/spiffy_theme_backend/static/src/image/theme-menu-horizontal.png new file mode 100644 index 00000000..bc74bc6e Binary files /dev/null and b/spiffy_theme_backend/static/src/image/theme-menu-horizontal.png differ diff --git a/spiffy_theme_backend/static/src/image/theme-menu-vertical.png b/spiffy_theme_backend/static/src/image/theme-menu-vertical.png new file mode 100644 index 00000000..fddbd89d Binary files /dev/null and b/spiffy_theme_backend/static/src/image/theme-menu-vertical.png differ diff --git a/spiffy_theme_backend/static/src/image/theme-separator-style1.png b/spiffy_theme_backend/static/src/image/theme-separator-style1.png new file mode 100644 index 00000000..297a2fec Binary files /dev/null and b/spiffy_theme_backend/static/src/image/theme-separator-style1.png differ diff --git a/spiffy_theme_backend/static/src/image/theme-separator-style2.png b/spiffy_theme_backend/static/src/image/theme-separator-style2.png new file mode 100644 index 00000000..9cf573b1 Binary files /dev/null and b/spiffy_theme_backend/static/src/image/theme-separator-style2.png differ diff --git a/spiffy_theme_backend/static/src/image/theme-separator-style3.png b/spiffy_theme_backend/static/src/image/theme-separator-style3.png new file mode 100644 index 00000000..b7d6c57b Binary files /dev/null and b/spiffy_theme_backend/static/src/image/theme-separator-style3.png differ diff --git a/spiffy_theme_backend/static/src/image/theme-separator-style4.png b/spiffy_theme_backend/static/src/image/theme-separator-style4.png new file mode 100644 index 00000000..e1e19814 Binary files /dev/null and b/spiffy_theme_backend/static/src/image/theme-separator-style4.png differ diff --git a/spiffy_theme_backend/static/src/image/theme-style-rounded.png b/spiffy_theme_backend/static/src/image/theme-style-rounded.png new file mode 100644 index 00000000..b55d2efd Binary files /dev/null and b/spiffy_theme_backend/static/src/image/theme-style-rounded.png differ diff --git a/spiffy_theme_backend/static/src/image/theme-style-square.png b/spiffy_theme_backend/static/src/image/theme-style-square.png new file mode 100644 index 00000000..99ebc9ec Binary files /dev/null and b/spiffy_theme_backend/static/src/image/theme-style-square.png differ diff --git a/spiffy_theme_backend/static/src/image/theme-style-standard.png b/spiffy_theme_backend/static/src/image/theme-style-standard.png new file mode 100644 index 00000000..911030a6 Binary files /dev/null and b/spiffy_theme_backend/static/src/image/theme-style-standard.png differ diff --git a/spiffy_theme_backend/static/src/js/SpiffyPageTitle.js b/spiffy_theme_backend/static/src/js/SpiffyPageTitle.js new file mode 100644 index 00000000..7b1c184a --- /dev/null +++ b/spiffy_theme_backend/static/src/js/SpiffyPageTitle.js @@ -0,0 +1,18 @@ +odoo.define('spiffy_theme_backend.SpiffyPageTitle', function (require) { +"use strict"; + + var ajax = require('web.ajax'); + var { WebClient } = require("@web/webclient/webclient"); + var { patch } = require("web.utils"); + + patch(WebClient.prototype, "spiffy_theme_backend.SpiffyPageTitle", { + setup() { + this._super(); + var self = this + ajax.rpc('/get/tab/title/').then(function(rec) { + var new_title = rec + self.title.setParts({ zopenerp: new_title }) + }) + }, + }); +}); \ No newline at end of file diff --git a/spiffy_theme_backend/static/src/js/SwitchCompanyMenu.js b/spiffy_theme_backend/static/src/js/SwitchCompanyMenu.js new file mode 100644 index 00000000..715cc367 --- /dev/null +++ b/spiffy_theme_backend/static/src/js/SwitchCompanyMenu.js @@ -0,0 +1,32 @@ +/** @odoo-module **/ + +var config = require('web.config'); +var core = require('web.core'); +var session = require('@web/session'); +var SystrayMenu = require('web.SystrayMenu'); +var Widget = require('web.Widget'); +var { patch } = require("web.utils"); +var { SwitchCompanyMenu } = require("@web/webclient/switch_company_menu/switch_company_menu"); +var { registry } = require("@web/core/registry"); + +var _t = core._t; + +patch(SwitchCompanyMenu.prototype, "spiffy_theme_backend.SwitchCompanyMenu", { + setup() { + this._super(); + this.isDebug = config.isDebug(); + this.isAssets = config.isDebug("assets"); + this.isTests = config.isDebug("tests"); + }, +}); + +// show company menu even if company is count is 1 +const systrayItemSwitchCompanyMenu = { + Component: SwitchCompanyMenu, + isDisplayed(env) { + const { availableCompanies } = env.services.company; + return Object.keys(availableCompanies).length > 0; + }, +}; + +registry.category("systray").add("SwitchCompanyMenu", systrayItemSwitchCompanyMenu, { sequence: 1, force: true }); \ No newline at end of file diff --git a/spiffy_theme_backend/static/src/js/action_service.js b/spiffy_theme_backend/static/src/js/action_service.js new file mode 100644 index 00000000..73f0af19 --- /dev/null +++ b/spiffy_theme_backend/static/src/js/action_service.js @@ -0,0 +1,1623 @@ +/** @odoo-module **/ + +import { browser } from "@web/core/browser/browser"; +import { makeContext } from "@web/core/context"; +import { useDebugCategory } from "@web/core/debug/debug_context"; +import { download } from "@web/core/network/download"; +import { evaluateExpr } from "@web/core/py_js/py"; +import { registry } from "@web/core/registry"; +import { KeepLast } from "@web/core/utils/concurrency"; +import { useBus, useService } from "@web/core/utils/hooks"; +import { sprintf } from "@web/core/utils/strings"; +import { cleanDomFromBootstrap } from "@web/legacy/utils"; +import { View, ViewNotFoundError } from "@web/views/view"; +import { ActionDialog } from "@web/webclient/actions/action_dialog"; +import { CallbackRecorder } from "@web/webclient/actions/action_hook"; +import { ReportAction } from "@web/webclient/actions/reports/report_action"; + +import { Component, markup, onMounted, onWillUnmount, onError, useChildSubEnv, xml, reactive } from "@odoo/owl"; +import body_color from "spiffy_theme_backend.MenuJs"; +const actionHandlersRegistry = registry.category("action_handlers"); +const actionRegistry = registry.category("actions"); +const viewRegistry = registry.category("views"); + +// SPIFFY MULTI TAB START +const actionServiceRegistry = registry.category("services"); +var session = require("web.session"); +var ajax = require('web.ajax'); +var core = require('web.core'); +// SPIFFY MULTI TAB END + +/** @typedef {number|false} ActionId */ +/** @typedef {Object} ActionDescription */ +/** @typedef {"current" | "fullscreen" | "new" | "main" | "self" | "inline"} ActionMode */ +/** @typedef {string} ActionTag */ +/** @typedef {string} ActionXMLId */ +/** @typedef {Object} Context */ +/** @typedef {Function} CallableFunction */ +/** @typedef {string} ViewType */ + +/** @typedef {ActionId|ActionXMLId|ActionTag|ActionDescription} ActionRequest */ + +/** + * @typedef {Object} ActionOptions + * @property {Context} [additionalContext] + * @property {boolean} [clearBreadcrumbs] + * @property {CallableFunction} [onClose] + * @property {Object} [props] + * @property {ViewType} [viewType] + */ + +export async function clearUncommittedChanges(env) { + const callbacks = []; + env.bus.trigger("CLEAR-UNCOMMITTED-CHANGES", callbacks); + const res = await Promise.all(callbacks.map((fn) => fn())); + return !res.includes(false); +} + +function parseActiveIds(ids) { + const activeIds = []; + if (typeof ids === "string") { + activeIds.push(...ids.split(",").map(Number)); + } else if (typeof ids === "number") { + activeIds.push(ids); + } + return activeIds; +} + +// ----------------------------------------------------------------------------- +// Errors +// ----------------------------------------------------------------------------- + +export class ControllerNotFoundError extends Error {} + +export class InvalidButtonParamsError extends Error {} + +// ----------------------------------------------------------------------------- +// ActionManager (Service) +// ----------------------------------------------------------------------------- + +// regex that matches context keys not to forward from an action to another +const CTX_KEY_REGEX = /^(?:(?:default_|search_default_|show_).+|.+_view_ref|group_by|group_by_no_leaf|active_id|active_ids|orderedBy)$/; + +// only register this template once for all dynamic classes ControllerComponent +const ControllerComponentTemplate = xml``; + +function makeActionManager(env) { + const keepLast = new KeepLast(); + let id = 0; + let controllerStack = []; + let dialogCloseProm; + let actionCache = {}; + let dialog = null; + + // The state action (or default user action if none) is loaded as soon as possible + // so that the next "doAction" will have its action ready when needed. + const actionParams = _getActionParams(); + if (actionParams && typeof actionParams.actionRequest === "number") { + const { actionRequest, options } = actionParams; + _loadAction(actionRequest, options.additionalContext); + } + + env.bus.addEventListener("CLEAR-CACHES", () => { + actionCache = {}; + }); + + // --------------------------------------------------------------------------- + // misc + // --------------------------------------------------------------------------- + + /** + * Removes the current dialog from the action service's state. + * It returns the dialog's onClose callback to be able to propagate it to the next dialog. + * + * @return {Function|undefined} When there was a dialog, returns its onClose callback for propagation to next dialog. + */ + function _removeDialog() { + if (dialog) { + const { onClose, remove } = dialog; + dialog = null; + // Remove the dialog from the dialog_service. + // The code is well enough designed to avoid falling in a function call loop. + remove(); + return onClose; + } + } + + + // SPIFFY MULTI TAB START + function updateSpiffyTabDetails(tabId, ActionId, controller) { + const updateTabTimeout = setTimeout(updateTabDetails, 500); + + function updateTabDetails() { + var ControllerXmlId = controller.action.xml_id.split('.')[0] + ajax.jsonRpc('/update/tab/details','call', { + 'tabId': tabId, + 'ActionId': ActionId, + 'url': window.location.hash, + 'TabTitle': controller.displayName, + 'menu_xmlid': ControllerXmlId, + }).then(function(rec) { + var ControllerXmlId = controller.action.xml_id.split('.')[0] + var tabWithMenuID = $('.multi_tab_section').find('a[multi_tab_id="'+ tabId +'"]'); + var tabWithMenu = $('.multi_tab_section').find('a[data-xml-id="'+ ControllerXmlId +'"]'); + if($(tabWithMenuID).length){ + if (!$(tabWithMenuID).parent().hasClass('tab_active')){ + $('.multi_tab_section').find('.tab_active').removeClass('tab_active'); + $(tabWithMenuID).parent().addClass('tab_active'); + } + $(tabWithMenuID).attr('href',window.location.hash) + $(tabWithMenuID).attr('data-action-id', ActionId) + $(tabWithMenuID).attr('data-xml-id', ControllerXmlId) + $(tabWithMenuID).find('span').text(controller.displayName) + } + }); + } + + function TabTimeoutStop() { + clearTimeout(updateTabTimeout); + } + } + // SPIFFY MULTI TAB END + + /** + * Returns the last controller of the current controller stack. + * + * @returns {Controller|null} + */ + function _getCurrentController() { + const stack = controllerStack; + return stack.length ? stack[stack.length - 1] : null; + } + + /** + * Given an id, xmlid, tag (key of the client action registry) or directly an + * object describing an action. + * + * @private + * @param {ActionRequest} actionRequest + * @param {Context} [context={}] + * @returns {Promise} + */ + async function _loadAction(actionRequest, context = {}) { + if (typeof actionRequest === "string" && actionRegistry.contains(actionRequest)) { + // actionRequest is a key in the actionRegistry + return { + target: "current", + tag: actionRequest, + type: "ir.actions.client", + }; + } + + if (typeof actionRequest === "string" || typeof actionRequest === "number") { + // actionRequest is an id or an xmlid + const additional_context = { + active_id: context.active_id, + active_ids: context.active_ids, + active_model: context.active_model, + }; + const key = `${JSON.stringify(actionRequest)},${JSON.stringify(additional_context)}`; + let action; + if (!actionCache[key]) { + actionCache[key] = env.services.rpc("/web/action/load", { + action_id: actionRequest, + additional_context, + }); + action = await actionCache[key]; + if (action.help) { + action.help = markup(action.help); + } + } else { + action = await actionCache[key]; + } + if (!action) { + return { + type: "ir.actions.client", + tag: "invalid_action", + id: actionRequest, + }; + } + return Object.assign({}, action); + } + + // actionRequest is an object describing the action + return actionRequest; + } + + /** + * this function returns an action description + * with a unique jsId. + */ + function _preprocessAction(action, context = {}) { + try { + action._originalAction = JSON.stringify(action); + } catch (_e) { + // do nothing, the action might simply not be serializable + } + action.context = makeContext([context, action.context], env.services.user.context); + if (action.domain) { + const domain = action.domain || []; + action.domain = + typeof domain === "string" + ? evaluateExpr( + domain, + Object.assign({}, env.services.user.context, action.context) + ) + : domain; + } + if (action.help) { + const htmlHelp = document.createElement("div"); + htmlHelp.innerHTML = action.help; + if (!htmlHelp.innerText.trim()) { + delete action.help; + } + } + action = { ...action }; // manipulate a copy to keep cached action unmodified + action.jsId = `action_${++id}`; + if (action.type === "ir.actions.act_window" || action.type === "ir.actions.client") { + action.target = action.target || "current"; + } + if (action.type === "ir.actions.act_window") { + action.views = [...action.views.map((v) => [v[0], v[1] === "tree" ? "list" : v[1]])]; // manipulate a copy to keep cached action unmodified + action.controllers = {}; + const target = action.target; + if (target !== "inline" && !(target === "new" && action.views[0][1] === "form")) { + // FIXME: search view arch is already sent with load_action, so either remove it + // from there or load all fieldviews alongside the action for the sake of consistency + const searchViewId = action.search_view_id ? action.search_view_id[0] : false; + action.views.push([searchViewId, "search"]); + } + } + return action; + } + + /** + * @private + * @param {string} viewType + * @throws {Error} if the current controller is not a view + * @returns {View | null} + */ + function _getView(viewType) { + const currentController = controllerStack[controllerStack.length - 1]; + if (currentController.action.type !== "ir.actions.act_window") { + throw new Error(`switchView called but the current controller isn't a view`); + } + const view = currentController.views.find((view) => view.type === viewType); + return view || null; + } + + /** + * Given a controller stack, returns the list of breadcrumb items. + * + * @private + * @param {ControllerStack} stack + * @returns {Breadcrumbs} + */ + function _getBreadcrumbs(stack) { + return stack + .filter((controller) => controller.action.tag !== "menu") + .map((controller) => { + return { + jsId: controller.jsId, + get name() { + return controller.displayName; + }, + }; + }); + } + + /** + * @private + * @returns {ActionParams | null} + */ + function _getActionParams() { + const state = env.services.router.current.hash; + const options = { clearBreadcrumbs: true }; + let actionRequest = null; + if (state.action) { + // ClientAction + if (actionRegistry.contains(state.action)) { + actionRequest = { + params: state, + tag: state.action, + type: "ir.actions.client", + }; + } else { + // The action to load isn't the current one => executes it + actionRequest = state.action; + const context = { params: state }; + if (state.active_id) { + context.active_id = state.active_id; + } + if (state.active_ids) { + context.active_ids = parseActiveIds(state.active_ids); + } else if (state.active_id) { + context.active_ids = [state.active_id]; + } + Object.assign(options, { + additionalContext: context, + viewType: state.view_type, + }); + if (state.id) { + options.props = { resId: state.id }; + } + } + } else if (state.model) { + if (state.id) { + actionRequest = { + res_model: state.model, + res_id: state.id, + type: "ir.actions.act_window", + views: [[state.view_id ? state.view_id : false, "form"]], + }; + } else if (state.view_type) { + // This is a window action on a multi-record view => restores it from + // the session storage + const storedAction = browser.sessionStorage.getItem("current_action"); + const lastAction = JSON.parse(storedAction || "{}"); + if (lastAction.help) { + lastAction.help = markup(lastAction.help); + } + if (lastAction.res_model === state.model) { + if (lastAction.context) { + // If this method is called because of a company switch, the + // stored allowed_company_ids is incorrect. + delete lastAction.context.allowed_company_ids; + } + actionRequest = lastAction; + options.viewType = state.view_type; + } + } + } + // If no action => falls back on the user default action (if any). + if (!actionRequest && env.services.user.home_action_id) { + actionRequest = env.services.user.home_action_id; + } + return actionRequest ? { actionRequest, options } : null; + } + + /** + * @param {ClientAction} action + * @param {Object} props + * @returns {{ props: ActionProps, config: Config }} + */ + function _getActionInfo(action, props) { + return { + props: Object.assign({}, props, { action, actionId: action.id }), + config: { + actionId: action.id, + actionType: "ir.actions.client", + actionFlags: action.flags, + }, + displayName: action.display_name || action.name || "", + }; + } + + /** + * @param {Action} action + * @returns {ActionMode} + */ + function _getActionMode(action) { + if (action.target === "new") { + // No possible override for target="new" + return "new"; + } + if (action.type === "ir.actions.client") { + const clientAction = actionRegistry.get(action.tag); + if (clientAction.target) { + // Target is forced by the definition of the client action + return clientAction.target; + } + } + if (controllerStack.some((c) => c.action.target === "fullscreen")) { + // Force fullscreen when one of the controllers is set to fullscreen + return "fullscreen"; + } + // Default: current + return "current"; + } + + /** + * @private + * @returns {SwitchViewParams | null} + */ + function _getSwitchViewParams() { + const state = env.services.router.current.hash; + if (state.action && !actionRegistry.contains(state.action)) { + const currentController = controllerStack[controllerStack.length - 1]; + const currentActionId = + currentController && currentController.action && currentController.action.id; + // Window Action: determines model, viewType etc.... + if ( + currentController && + currentController.action.type === "ir.actions.act_window" && + currentActionId === state.action + ) { + const props = { + resId: state.id || false, + }; + const viewType = state.view_type || currentController.view.type; + return { viewType, props }; + } + } + return null; + } + + /** + * @param {BaseView} view + * @param {ActWindowAction} action + * @param {BaseView[]} views + * @param {Object} props + * @returns {{ props: ViewProps, config: Config }} + */ + function _getViewInfo(view, action, views, props = {}) { + const target = action.target; + const viewSwitcherEntries = views + .filter((v) => v.multiRecord === view.multiRecord) + .map((v) => { + const viewSwitcherEntry = { + icon: v.icon, + name: v.display_name.toString(), + type: v.type, + multiRecord: v.multiRecord, + }; + if (view.type === v.type) { + viewSwitcherEntry.active = true; + } + return viewSwitcherEntry; + }); + const context = action.context || {}; + let groupBy = context.group_by || []; + if (typeof groupBy === "string") { + groupBy = [groupBy]; + } + const viewProps = Object.assign({}, props, { + context, + display: { mode: target === "new" ? "inDialog" : target }, + domain: action.domain || [], + groupBy, + loadActionMenus: target !== "new" && target !== "inline", + loadIrFilters: action.views.some((v) => v[1] === "search"), + resModel: action.res_model, + type: view.type, + selectRecord: async (resId, { activeIds, mode }) => { + if (_getView("form")) { + await switchView("form", { mode, resId, resIds: activeIds }); + } + }, + createRecord: async () => { + if (_getView("form")) { + await switchView("form", { resId: false }); + } + }, + }); + + if (view.type === "form") { + if (action.target === "new") { + viewProps.mode = "edit"; + if (!viewProps.onSave) { + viewProps.onSave = (record, params) => { + if (params && params.closable) { + doAction({ type: "ir.actions.act_window_close" }); + } + }; + } + } else if (context.form_view_initial_mode) { + viewProps.mode = context.form_view_initial_mode; + } + if (action.flags && "mode" in action.flags) { + viewProps.mode = action.flags.mode; + } + } + + if (target === "inline") { + viewProps.searchMenuTypes = []; + } + + const specialKeys = ["help", "useSampleModel", "limit", "count"]; + for (const key of specialKeys) { + if (key in action) { + if (key === "help") { + viewProps.noContentHelp = action.help; + } else { + viewProps[key] = action[key]; + } + } + } + + if (context.search_disable_custom_filters) { + viewProps.activateFavorite = false; + } + + // view specific + if (action.res_id) { + viewProps.resId = action.res_id; + } + + // LEGACY CODE COMPATIBILITY: remove when all views will be written in owl + if (view.isLegacy) { + const legacyActionInfo = { ...action, ...viewProps.action }; + Object.assign(viewProps, { + action: legacyActionInfo, + View: view, + views: action.views, + }); + } + // END LEGACY CODE COMPATIBILITY + + viewProps.noBreadcrumbs = action.context.no_breadcrumbs; + delete action.context.no_breadcrumbs; + return { + props: viewProps, + config: { + actionId: action.id, + actionType: "ir.actions.act_window", + actionFlags: action.flags, + views: action.views, + viewSwitcherEntries, + }, + displayName: action.display_name || action.name || "", + }; + } + + /** + * Computes the position of the controller in the nextStack according to options + * @param {Object} options + * @param {boolean} [options.clearBreadcrumbs=false] + * @param {'replaceLast' | 'replaceLastAction'} [options.stackPosition] + * @param {number} [options.index] + */ + function _computeStackIndex(options) { + let index = null; + if (options.clearBreadcrumbs) { + index = 0; + } else if (options.stackPosition === "replaceCurrentAction") { + const currentController = controllerStack[controllerStack.length - 1]; + if (currentController) { + index = controllerStack.findIndex( + (ct) => ct.action.jsId === currentController.action.jsId + ); + } + } else if (options.stackPosition === "replacePreviousAction") { + let last; + for (let i = controllerStack.length - 1; i >= 0; i--) { + const action = controllerStack[i].action.jsId; + if (!last) { + last = action; + } + if (action !== last) { + last = action; + break; + } + } + if (last) { + index = controllerStack.findIndex((ct) => ct.action.jsId === last); + } + // TODO: throw if there is no previous action? + } else if ("index" in options) { + index = options.index; + } else { + index = controllerStack.length; + } + return index; + } + + /** + * Triggers a re-rendering with respect to the given controller. + * + * @private + * @param {Controller} controller + * @param {UpdateStackOptions} options + * @param {boolean} [options.clearBreadcrumbs=false] + * @param {number} [options.index] + * @returns {Promise} + */ + async function _updateUI(controller, options = {}) { + let resolve; + let reject; + let dialogCloseResolve; + const currentActionProm = new Promise((_res, _rej) => { + resolve = _res; + reject = _rej; + }); + const action = controller.action; + const index = _computeStackIndex(options); + const controllerArray = [controller]; + if (options.lazyController) { + controllerArray.unshift(options.lazyController); + } + const nextStack = controllerStack.slice(0, index).concat(controllerArray); + // Compute breadcrumbs + controller.config.breadcrumbs = reactive( + action.target === "new" ? [] : _getBreadcrumbs(nextStack) + ); + controller.config.getDisplayName = () => controller.displayName; + controller.config.setDisplayName = (displayName) => { + controller.displayName = displayName; + if (controller === _getCurrentController()) { + // if not mounted yet, will be done in "mounted" + env.services.title.setParts({ action: controller.displayName }); + } + if (action.target !== "new") { + // This is a hack to force the reactivity when a new displayName is set + controller.config.breadcrumbs.push(undefined); + controller.config.breadcrumbs.pop(); + } + + // SPIFFY MULTI TAB START + if (controller.action.id){ + var TabDiv = $('.multi_tab_section .multi_tab_div'); + // $('.multi_tab_section .multi_tab_div.tab_active a').attr('data-action-id', controller.action.id) + var UpdateAction = controller.action.id + var tabID = $('.multi_tab_section .multi_tab_div.tab_active a').attr('multi_tab_id') + // var menu_xmlid = $(TabDiv).find('a[data-xml-id="'+ controller.action.xml_id.split('.')[0] +'"]') + var menu_xmlid = $(TabDiv).find('a[multi_tab_id="'+ tabID +'"]') + + /* if($(menu_xmlid).length){ + updateSpiffyTabDetails(tabID, UpdateAction, controller); + } */ + if($(menu_xmlid).length && !localStorage.getItem('TabClickTilteUpdate')) { + updateSpiffyTabDetails(tabID, UpdateAction, controller); + } + if(localStorage.getItem('TabClickTilteUpdate')){ + localStorage.removeItem("TabClickTilteUpdate"); + } + } + // SPIFFY MULTI TAB END + }; + controller.config.historyBack = () => { + const previousController = controllerStack[controllerStack.length - 2]; + if (previousController && !dialog) { + restore(previousController.jsId); + } else { + _executeCloseAction(); + } + }; + + // SPIFFY MULTI TAB START + sessionStorage.setItem("spiffy_current_action", action._originalAction); + sessionStorage.setItem("spiffy_current_action_id", action.id); + // SPIFFY MULTI TAB END + + class ControllerComponent extends Component { + setup() { + this.Component = controller.Component; + this.titleService = useService("title"); + useDebugCategory("action", { action }); + useChildSubEnv({ + config: controller.config, + }); + if (action.target !== "new") { + this.__beforeLeave__ = new CallbackRecorder(); + this.__getGlobalState__ = new CallbackRecorder(); + this.__getLocalState__ = new CallbackRecorder(); + useBus(env.bus, "CLEAR-UNCOMMITTED-CHANGES", (ev) => { + const callbacks = ev.detail; + const beforeLeaveFns = this.__beforeLeave__.callbacks; + callbacks.push(...beforeLeaveFns); + }); + useChildSubEnv({ + __beforeLeave__: this.__beforeLeave__, + __getGlobalState__: this.__getGlobalState__, + __getLocalState__: this.__getLocalState__, + }); + } + this.isMounted = false; + + onMounted(this.onMounted); + onWillUnmount(this.onWillUnmount); + onError(this.onError); + } + onError(error) { + reject(error); + cleanDomFromBootstrap(); + if (action.target === "new") { + // get the dialog service to close the dialog. + throw error; + } else { + const lastCt = controllerStack[controllerStack.length - 1]; + let info = {}; + if (lastCt) { + if (lastCt.jsId === controller.jsId) { + // the error occurred on the controller which is + // already in the DOM, so simply show the error + Promise.resolve().then(() => { + throw error; + }); + return; + } else { + info = lastCt.__info__; + // the error occurred while rendering a new controller, + // so go back to the last non faulty controller + // (the error will be shown anyway as the promise + // has been rejected) + } + } + env.bus.trigger("ACTION_MANAGER:UPDATE", info); + } + } + onMounted() { + if (action.target === "new") { + dialogCloseProm = new Promise((_r) => { + dialogCloseResolve = _r; + }).then(() => { + dialogCloseProm = undefined; + }); + dialog = nextDialog; + } else { + controller.getGlobalState = () => { + const exportFns = this.__getGlobalState__.callbacks; + if (exportFns.length) { + return Object.assign({}, ...exportFns.map((fn) => fn())); + } + }; + controller.getLocalState = () => { + const exportFns = this.__getLocalState__.callbacks; + if (exportFns.length) { + return Object.assign({}, ...exportFns.map((fn) => fn())); + } + }; + + // LEGACY CODE COMPATIBILITY: remove when controllers will be written in owl + // we determine here which actions no longer occur in the nextStack, + // and we manually destroy all their controller's widgets + const nextStackActionIds = nextStack.map((c) => c.action.jsId); + const toDestroy = new Set(); + for (const c of controllerStack) { + if (!nextStackActionIds.includes(c.action.jsId)) { + if (c.action.type === "ir.actions.act_window") { + for (const viewType in c.action.controllers) { + const controller = c.action.controllers[viewType]; + if (controller.view.isLegacy) { + toDestroy.add(controller); + } + } + } else { + toDestroy.add(c); + } + } + } + for (const c of toDestroy) { + if (c.exportedState && c.exportedState.__legacy_widget__) { + c.exportedState.__legacy_widget__.destroy(); + } + } + // END LEGACY CODE COMPATIBILITY + controllerStack = nextStack; // the controller is mounted, commit the new stack + pushState(controller); + this.titleService.setParts({ action: controller.displayName }); + browser.sessionStorage.setItem( + "current_action", + action._originalAction || "{}" + ); + } + resolve(); + env.bus.trigger("ACTION_MANAGER:UI-UPDATED", _getActionMode(action)); + this.isMounted = true; + } + onWillUnmount() { + if (action.target === "new" && dialogCloseResolve) { + dialogCloseResolve(); + } + } + } + ControllerComponent.template = ControllerComponentTemplate; + ControllerComponent.Component = controller.Component; + + let nextDialog = null; + if (action.target === "new") { + cleanDomFromBootstrap(); + const actionDialogProps = { + // TODO add size + ActionComponent: ControllerComponent, + actionProps: controller.props, + actionType: action.type, + }; + if (action.name) { + actionDialogProps.title = action.name; + } + + const onClose = _removeDialog(); + const removeDialog = env.services.dialog.add(ActionDialog, actionDialogProps, { + onClose: () => { + const onClose = _removeDialog(); + if (onClose) { + onClose(); + } + cleanDomFromBootstrap(); + }, + }); + nextDialog = { + remove: removeDialog, + onClose: onClose || options.onClose, + }; + return currentActionProm; + } + + const currentController = _getCurrentController(); + if (currentController && currentController.getLocalState) { + currentController.exportedState = currentController.getLocalState(); + } + if (controller.exportedState) { + controller.props.state = controller.exportedState; + } + + // TODO DAM Remarks: + // this thing seems useless for client actions. + // restore and switchView (at least) use this --> cannot be done in switchView only + // if prop globalState has been passed in doAction, since the action is new the prop won't be overridden in l655. + // if globalState is not useful for client actions --> maybe use that thing in useSetupView instead of useSetupAction? + // a good thing: the Object.assign seems to reflect the use of "externalState" in legacy Model class --> things should be fine. + if (currentController && currentController.getGlobalState) { + currentController.action.globalState = Object.assign( + {}, + currentController.action.globalState, + currentController.getGlobalState() // what if this = {}? + ); + } + if (controller.action.globalState) { + controller.props.globalState = controller.action.globalState; + } + + const closingProm = _executeCloseAction(); + + controller.__info__ = { + id: ++id, + Component: ControllerComponent, + componentProps: controller.props, + }; + env.bus.trigger("ACTION_MANAGER:UPDATE", controller.__info__); + + if (action.id){ + var TabDiv = $('.multi_tab_section .multi_tab_div'); + $('.multi_tab_section .multi_tab_div.tab_active a').attr('data-action-id', action.id) + var UpdateAction = action.id + var tabID = $('.multi_tab_section .multi_tab_div.tab_active a').attr('multi_tab_id') + // var menu_xmlid = $(TabDiv).find('a[data-xml-id="'+ controller.action.xml_id.split('.')[0] +'"]') + var menu_xmlid = $(TabDiv).find('a[multi_tab_id="'+ tabID +'"]') + + if($(menu_xmlid).length && !localStorage.getItem('TabClick')) { + updateSpiffyTabDetails(tabID, UpdateAction, controller); + } + if(localStorage.getItem('TabClick')){ + localStorage.removeItem("TabClick"); + } + /* if(localStorage.getItem('TabClickTilteUpdate')){ + localStorage.removeItem("TabClickTilteUpdate"); + } */ + + } + + return Promise.all([currentActionProm, closingProm]).then((r) => r[0]); + } + + // --------------------------------------------------------------------------- + // ir.actions.act_url + // --------------------------------------------------------------------------- + + /** + * Executes actions of type 'ir.actions.act_url', i.e. redirects to the + * given url. + * + * @private + * @param {ActURLAction} action + * @param {ActionOptions} options + */ + function _executeActURLAction(action, options) { + if (action.target === "self") { + env.services.router.redirect(action.url); + } else { + const w = browser.open(action.url, "_blank"); + if (!w || w.closed || typeof w.closed === "undefined") { + const msg = env._t( + "A popup window has been blocked. You may need to change your " + + "browser settings to allow popup windows for this page." + ); + env.services.notification.add(msg, { + sticky: true, + type: "warning", + }); + } + if (options.onClose) { + options.onClose(); + } + } + } + + // --------------------------------------------------------------------------- + // ir.actions.act_window + // --------------------------------------------------------------------------- + + /** + * Executes an action of type 'ir.actions.act_window'. + * + * @private + * @param {ActWindowAction} action + * @param {ActionOptions} options + */ + async function _executeActWindowAction(action, options) { + // LEGACY CODE COMPATIBILITY: load views to determine js_class if any, s.t. + // we know if the view to use is legacy or not + // When all views will be converted, this will be done exclusively by View + // #action-serv-leg-compat-js-class + const loadViewParams = { + context: action.context || {}, + views: action.views, + resModel: action.res_model, + }; + const loadViewOptions = { + actionId: action.id, + loadActionMenus: action.target !== "new" && action.target !== "inline", + loadIrFilters: action.views.some((v) => v[1] === "search"), + }; + const prom = env.services.view.loadViews(loadViewParams, loadViewOptions); + const { views: viewDescriptions } = await keepLast.add(prom); + const domParser = new DOMParser(); + const views = []; + for (const [, type] of action.views) { + if (type !== "search") { + const arch = viewDescriptions[type].arch; + const archDoc = domParser.parseFromString(arch, "text/xml").documentElement; + const jsClass = archDoc.getAttribute("js_class"); + const view = viewRegistry.get(jsClass, false) || viewRegistry.get(type, false); + if (view) { + views.push(view); + } + } + } + // END LEGACY CODE COMPATIBILITY + // const views = []; + // for (const [, type] of action.views) { + // if (type !== "search" && viewRegistry.contains(type)) { + // views.push(viewRegistry.get(key)); + // } + // } + if (!views.length) { + throw new Error(`No view found for act_window action ${action.id}`); + } + + let view = options.viewType && views.find((v) => v.type === options.viewType); + let lazyView; + + if (view && !view.multiRecord) { + lazyView = views[0].multiRecord ? views[0] : undefined; + } else if (!view) { + view = views[0]; + } + + if (env.isSmall) { + if (!view.isMobileFriendly) { + view = _findMobileView(views, view.multiRecord) || view; + } + if (lazyView && !lazyView.isMobileFriendly) { + lazyView = _findMobileView(views, lazyView.multiRecord) || lazyView; + } + } + + const controller = { + jsId: `controller_${++id}`, + Component: view.isLegacy ? view.Controller : View, + action, + view, + views, + ..._getViewInfo(view, action, views, options.props), + }; + action.controllers[view.type] = controller; + + const updateUIOptions = { + clearBreadcrumbs: options.clearBreadcrumbs, + onClose: options.onClose, + stackPosition: options.stackPosition, + }; + + if (lazyView) { + updateUIOptions.lazyController = { + jsId: `controller_${++id}`, + Component: lazyView.isLegacy ? lazyView.Controller : View, + action, + view: lazyView, + views, + ..._getViewInfo(lazyView, action, views), + }; + } + + return _updateUI(controller, updateUIOptions); + } + + /** + * Helper function to find the first mobile-friendly view, if any. + * + * @private + * @param {Array} views an array of views + * @param {boolean} multiRecord true if we search for a multiRecord view + * @returns {Object|undefined} first mobile-friendly view found + */ + function _findMobileView(views, multiRecord) { + return views.find((view) => view.isMobileFriendly && view.multiRecord === multiRecord); + } + + // --------------------------------------------------------------------------- + // ir.actions.client + // --------------------------------------------------------------------------- + + /** + * Executes an action of type 'ir.actions.client'. + * + * @private + * @param {ClientAction} action + * @param {ActionOptions} options + */ + async function _executeClientAction(action, options) { + const clientAction = actionRegistry.get(action.tag); + if (clientAction.prototype instanceof Component) { + if (action.target !== "new") { + const canProceed = await clearUncommittedChanges(env); + if (!canProceed) { + return; + } + if (clientAction.target) { + action.target = clientAction.target; + } + } + const controller = { + jsId: `controller_${++id}`, + Component: clientAction, + action, + ..._getActionInfo(action, options.props), + }; + return _updateUI(controller, { + clearBreadcrumbs: options.clearBreadcrumbs, + stackPosition: options.stackPosition, + onClose: options.onClose, + }); + } else { + const next = await clientAction(env, action); + if (next) { + return doAction(next, options); + } + } + } + + // --------------------------------------------------------------------------- + // ir.actions.report + // --------------------------------------------------------------------------- + + // messages that might be shown to the user dependening on the state of wkhtmltopdf + const link = '

wkhtmltopdf.org'; + const WKHTMLTOPDF_MESSAGES = { + broken: + env._t( + "Your installation of Wkhtmltopdf seems to be broken. The report will be shown " + + "in html." + ) + link, + install: + env._t( + "Unable to find Wkhtmltopdf on this system. The report will be shown in " + "html." + ) + link, + upgrade: + env._t( + "You should upgrade your version of Wkhtmltopdf to at least 0.12.0 in order to " + + "get a correct display of headers and footers as well as support for " + + "table-breaking between pages." + ) + link, + workers: env._t( + "You need to start Odoo with at least two workers to print a pdf version of " + + "the reports." + ), + }; + + // only check the wkhtmltopdf state once, so keep the rpc promise + let wkhtmltopdfStateProm; + + /** + * Generates the report url given a report action. + * + * @private + * @param {ReportAction} action + * @param {ReportType} type + * @returns {string} + */ + function _getReportUrl(action, type) { + let url = `/report/${type}/${action.report_name}`; + const actionContext = action.context || {}; + if (action.data && JSON.stringify(action.data) !== "{}") { + // build a query string with `action.data` (it's the place where reports + // using a wizard to customize the output traditionally put their options) + const options = encodeURIComponent(JSON.stringify(action.data)); + const context = encodeURIComponent(JSON.stringify(actionContext)); + url += `?options=${options}&context=${context}`; + } else { + if (actionContext.active_ids) { + url += `/${actionContext.active_ids.join(",")}`; + } + if (type === "html") { + const context = encodeURIComponent(JSON.stringify(env.services.user.context)); + url += `?context=${context}`; + } + } + return url; + } + + /** + * Launches download action of the report + * + * @private + * @param {ReportAction} action + * @param {ActionOptions} options + * @returns {Promise} + */ + async function _triggerDownload(action, options, type) { + const url = _getReportUrl(action, type); + env.services.ui.block(); + try { + await download({ + url: "/report/download", + data: { + data: JSON.stringify([url, action.report_type]), + context: JSON.stringify(env.services.user.context), + }, + }); + } finally { + env.services.ui.unblock(); + } + const onClose = options.onClose; + if (action.close_on_report_download) { + return doAction({ type: "ir.actions.act_window_close" }, { onClose }); + } else if (onClose) { + onClose(); + } + } + + function _executeReportClientAction(action, options) { + const props = Object.assign({}, options.props, { + data: action.data, + display_name: action.display_name, + name: action.name, + report_file: action.report_file, + report_name: action.report_name, + report_url: _getReportUrl(action, "html"), + context: Object.assign({}, action.context), + }); + + const controller = { + jsId: `controller_${++id}`, + Component: ReportAction, + action, + ..._getActionInfo(action, props), + }; + + return _updateUI(controller, { + clearBreadcrumbs: options.clearBreadcrumbs, + stackPosition: options.stackPosition, + onClose: options.onClose, + }); + } + + /** + * Executes actions of type 'ir.actions.report'. + * + * @private + * @param {ReportAction} action + * @param {ActionOptions} options + */ + async function _executeReportAction(action, options) { + const handlers = registry.category("ir.actions.report handlers").getAll(); + for (const handler of handlers) { + const result = await handler(action, options, env); + if (result) { + return result; + } + } + if (action.report_type === "qweb-html") { + return _executeReportClientAction(action, options); + } else if (action.report_type === "qweb-pdf") { + // check the state of wkhtmltopdf before proceeding + if (!wkhtmltopdfStateProm) { + wkhtmltopdfStateProm = env.services.rpc("/report/check_wkhtmltopdf"); + } + const state = await wkhtmltopdfStateProm; + // display a notification according to wkhtmltopdf's state + if (state in WKHTMLTOPDF_MESSAGES) { + env.services.notification.add(WKHTMLTOPDF_MESSAGES[state], { + sticky: true, + title: env._t("Report"), + }); + } + if (state === "upgrade" || state === "ok") { + // trigger the download of the PDF report + return _triggerDownload(action, options, "pdf"); + } else { + // open the report in the client action if generating the PDF is not possible + return _executeReportClientAction(action, options); + } + } else if (action.report_type === "qweb-text") { + return _triggerDownload(action, options, "text"); + } else { + console.error( + `The ActionManager can't handle reports of type ${action.report_type}`, + action + ); + } + } + + // --------------------------------------------------------------------------- + // ir.actions.server + // --------------------------------------------------------------------------- + + /** + * Executes an action of type 'ir.actions.server'. + * + * @private + * @param {ServerAction} action + * @param {ActionOptions} options + * @returns {Promise} + */ + async function _executeServerAction(action, options) { + const runProm = env.services.rpc("/web/action/run", { + action_id: action.id, + context: makeContext([env.services.user.context, action.context]), + }); + let nextAction = await keepLast.add(runProm); + if (nextAction.help) { + nextAction.help = markup(nextAction.help); + } + nextAction = nextAction || { type: "ir.actions.act_window_close" }; + return doAction(nextAction, options); + } + + async function _executeCloseAction(params = {}) { + let onClose; + if (dialog) { + onClose = _removeDialog(); + } else { + onClose = params.onClose; + } + if (onClose) { + await onClose(params.onCloseInfo); + } + + return dialogCloseProm; + } + + // --------------------------------------------------------------------------- + // public API + // --------------------------------------------------------------------------- + + /** + * Main entry point of a 'doAction' request. Loads the action and executes it. + * + * @param {ActionRequest} actionRequest + * @param {ActionOptions} options + * @returns {Promise} + */ + async function doAction(actionRequest, options = {}) { + const actionProm = _loadAction(actionRequest, options.additionalContext); + let action = await keepLast.add(actionProm); + action = _preprocessAction(action, options.additionalContext); + options.clearBreadcrumbs = action.target === "main" || options.clearBreadcrumbs; + switch (action.type) { + case "ir.actions.act_url": + return _executeActURLAction(action, options); + case "ir.actions.act_window": + if (action.target !== "new") { + const canProceed = await clearUncommittedChanges(env); + if (!canProceed) { + return; + } + } + return _executeActWindowAction(action, options); + case "ir.actions.act_window_close": + return _executeCloseAction({ onClose: options.onClose, onCloseInfo: action.infos }); + case "ir.actions.client": + return _executeClientAction(action, options); + case "ir.actions.report": + return _executeReportAction(action, options); + case "ir.actions.server": + return _executeServerAction(action, options); + default: { + const handler = actionHandlersRegistry.get(action.type, null); + if (handler !== null) { + return handler({ env, action, options }); + } + throw new Error( + `The ActionManager service can't handle actions of type ${action.type}` + ); + } + } + } + + /** + * Executes an action on top of the current one (typically, when a button in a + * view is clicked). The button may be of type 'object' (call a given method + * of a given model) or 'action' (execute a given action). Alternatively, the + * button may have the attribute 'special', and in this case an + * 'ir.actions.act_window_close' is executed. + * + * @param {DoActionButtonParams} params + * @returns {Promise} + */ + async function doActionButton(params) { + // determine the action to execute according to the params + let action; + const context = makeContext([params.context, params.buttonContext]); + if (params.special) { + action = { type: "ir.actions.act_window_close", infos: { special: true } }; + } else if (params.type === "object") { + // call a Python Object method, which may return an action to execute + let args = params.resId ? [[params.resId]] : [params.resIds]; + if (params.args) { + let additionalArgs; + try { + // warning: quotes and double quotes problem due to json and xml clash + // maybe we should force escaping in xml or do a better parse of the args array + additionalArgs = JSON.parse(params.args.replace(/'/g, '"')); + } catch (_e) { + browser.console.error("Could not JSON.parse arguments", params.args); + } + args = args.concat(additionalArgs); + } + const callProm = env.services.rpc("/web/dataset/call_button", { + args, + kwargs: { context }, + method: params.name, + model: params.resModel, + }); + action = await keepLast.add(callProm); + action = + action && typeof action === "object" + ? action + : { type: "ir.actions.act_window_close" }; + if (action.help) { + action.help = markup(action.help); + } + } else if (params.type === "action") { + // execute a given action, so load it first + context.active_id = params.resId || null; + context.active_ids = params.resIds; + context.active_model = params.resModel; + action = await keepLast.add(_loadAction(params.name, context)); + } else { + throw new InvalidButtonParamsError("Missing type for doActionButton request"); + } + // filter out context keys that are specific to the current action, because: + // - wrong default_* and search_default_* values won't give the expected result + // - wrong group_by values will fail and forbid rendering of the destination view + const currentCtx = {}; + for (const key in params.context) { + if (key.match(CTX_KEY_REGEX) === null) { + currentCtx[key] = params.context[key]; + } + } + const activeCtx = { active_model: params.resModel }; + if (params.resId) { + activeCtx.active_id = params.resId; + activeCtx.active_ids = [params.resId]; + } + action.context = makeContext([currentCtx, params.buttonContext, activeCtx, action.context]); + // in case an effect is returned from python and there is already an effect + // attribute on the button, the priority is given to the button attribute + const effect = params.effect ? evaluateExpr(params.effect) : action.effect; + const options = { onClose: params.onClose }; + await doAction(action, options); + if (params.close) { + await _executeCloseAction(); + } + if (effect) { + env.services.effect.add(effect); + } + } + + /** + * Switches to the given view type in action of the last controller of the + * stack. This action must be of type 'ir.actions.act_window'. + * + * @param {ViewType} viewType + * @param {Object} [props={}] + * @throws {ViewNotFoundError} if the viewType is not found on the current action + * @returns {Promise} + */ + async function switchView(viewType, props = {}) { + await keepLast.add(Promise.resolve()); + if (dialog) { + // we don't want to switch view when there's a dialog open, as we would + // not switch in the correct action (action in background != dialog action) + return; + } + const controller = controllerStack[controllerStack.length - 1]; + const view = _getView(viewType); + if (!view) { + throw new ViewNotFoundError( + sprintf( + env._t("No view of type '%s' could be found in the current action."), + viewType + ) + ); + } + const newController = controller.action.controllers[viewType] || { + jsId: `controller_${++id}`, + Component: view.isLegacy ? view.Controller : View, + action: controller.action, + views: controller.views, + view, + }; + + // LEGACY CODE COMPATIBILITY: remove when controllers will be written in owl + if (view.isLegacy && newController.jsId === controller.jsId) { + // case where a legacy view is reloaded via the view switcher + const { __legacy_widget__ } = controller.getLocalState(); + const params = {}; + if ("resId" in props) { + params.currentId = props.resId; + } + return __legacy_widget__.reload(params); + } + // END LEGACY CODE COMPATIBILITY + + Object.assign( + newController, + _getViewInfo(view, controller.action, controller.views, props) + ); + controller.action.controllers[viewType] = newController; + let index; + if (view.multiRecord) { + index = controllerStack.findIndex((ct) => ct.action.jsId === controller.action.jsId); + index = index > -1 ? index : controllerStack.length - 1; + } else { + // This case would mostly happen when loadState detects a change in the URL. + // Also, I guess we may need it when we have other monoRecord views + index = controllerStack.findIndex( + (ct) => ct.action.jsId === controller.action.jsId && !ct.view.multiRecord + ); + index = index > -1 ? index : controllerStack.length; + } + const canProceed = await clearUncommittedChanges(env); + if (canProceed) { + return _updateUI(newController, { index }); + } + } + + /** + * Restores a controller from the controller stack given its id. Typically, + * this function is called when clicking on the breadcrumbs. If no id is given + * restores the previous controller from the stack (penultimate). + * + * @param {string} jsId + */ + async function restore(jsId) { + await keepLast.add(Promise.resolve()); + let index; + if (!jsId) { + index = controllerStack.length - 2; + } else { + index = controllerStack.findIndex((controller) => controller.jsId === jsId); + } + if (index < 0) { + const msg = jsId ? "Invalid controller to restore" : "No controller to restore"; + throw new ControllerNotFoundError(msg); + } + const controller = controllerStack[index]; + if (controller.action.type === "ir.actions.act_window") { + const { action, exportedState, view, views } = controller; + const props = { ...controller.props }; + if (exportedState && "resId" in exportedState) { + // When restoring, we want to use the last exported ID of the controller + props.resId = exportedState.resId; + } + Object.assign(controller, _getViewInfo(view, action, views, props)); + } + const canProceed = await clearUncommittedChanges(env); + if (canProceed) { + return _updateUI(controller, { index }); + } + } + + /** + * Performs a "doAction" or a "switchView" according to the current content of + * the URL. The id of the underlying action is be returned if one of these + * operations has successfully started. + * + * @returns {Promise} true iff the state could have been loaded + */ + async function loadState() { + const switchViewParams = _getSwitchViewParams(); + if (switchViewParams) { + // only when we already have an action in dom + const { viewType, props } = switchViewParams; + const view = _getView(viewType); + if (view) { + // Params valid and view found => performs a "switchView" + await switchView(viewType, props); + return true; + } + } else { + const actionParams = _getActionParams(); + if (actionParams) { + // Params valid => performs a "doAction" + const { actionRequest, options } = actionParams; + await doAction(actionRequest, options); + return true; + } + } + return false; + } + + function pushState(controller) { + const newState = {}; + const action = controller.action; + if (action.id) { + newState.action = action.id; + } else if (action.type === "ir.actions.client") { + newState.action = action.tag; + } + if (action.context) { + const activeId = action.context.active_id; + if (activeId) { + newState.active_id = activeId; + } + const activeIds = action.context.active_ids; + // we don't push active_ids if it's a single element array containing + // the active_id to make the url shorter in most cases + if (activeIds && !(activeIds.length === 1 && activeIds[0] === activeId)) { + newState.active_ids = activeIds.join(","); + } + } + if (action.type === "ir.actions.act_window") { + const props = controller.props; + newState.model = props.resModel; + newState.view_type = props.type; + newState.id = props.resId || (props.state && props.state.resId) || undefined; + } + env.services.router.pushState(newState, { replace: true }); + } + return { + doAction, + doActionButton, + switchView, + restore, + loadState, + async loadAction(actionRequest, context) { + const action = await _loadAction(actionRequest, context); + return _preprocessAction(action, context); + }, + get currentController() { + return _getCurrentController(); + }, + __legacy__isActionInStack(actionId) { + return controllerStack.find((c) => c.action.jsId === actionId); + }, + }; +} + +export function divertColorItem(env) { + const route = "/primary_color/divertable_color"; + return { + type: "item", + id: "divert.account", + description: env._t("Switch/Add Account"), + href: `${browser.location.origin}${route}`, + callback: () => { + body_color.methods.divertColor(); + }, + sequence: 70, + }; +} + +export const actionService = { + dependencies: [ + "effect", + "localization", + "notification", + "router", + "rpc", + "title", + "view", // for legacy view compatibility #action-serv-leg-compat-js-class + "ui", + "user", + ], + start(env) { + return makeActionManager(env); + }, +}; + +actionServiceRegistry.remove("action"); +actionServiceRegistry.add("action", actionService); diff --git a/spiffy_theme_backend/static/src/js/apps_menu.js b/spiffy_theme_backend/static/src/js/apps_menu.js new file mode 100644 index 00000000..fde78a09 --- /dev/null +++ b/spiffy_theme_backend/static/src/js/apps_menu.js @@ -0,0 +1,407 @@ +/** @odoo-module **/ +import {fuzzyLookup} from "@web/core/utils/search"; +var core = require('web.core'); +var qweb = core.qweb; +var ajax = require('web.ajax'); +var { NavBar } = require("@web/webclient/navbar/navbar"); +var { patch } = require("web.utils"); +const { useListener } = require("@web/core/utils/hooks"); +const {useRef, useState } = owl; +import { browser } from "@web/core/browser/browser"; +import body_color from "spiffy_theme_backend.MenuJs"; + +function AppDrawerfindNames(memo, menu) { + if (menu.action) { + var key = menu.parent_id ? menu.parent_id[1] + "/" : ""; + memo[key + menu.name] = menu; + } + if (menu.children) { + _.reduce(menu.children, AppDrawerfindNames, memo); + } + return memo; +} + +function findNames(memo, menu) { + if (menu.actionID) { + memo[menu.name.trim()] = menu; + } + if (menu.childrenTree) { + const innerMemo = _.reduce(menu.childrenTree, findNames, {}); + for (const innerKey in innerMemo) { + memo[menu.name.trim() + " / " + innerKey] = innerMemo[innerKey]; + } + } + return memo; +} + +export function divertColorItem(env) { + const route = "/primary_color/divertable_color"; + return { + type: "item", + id: "divert.account", + description: env._t("Switch/Add Account"), + href: `${browser.location.origin}${route}`, + callback: () => { + body_color.methods.divertColor(); + }, + sequence: 70, + }; +} + +patch(NavBar.prototype, "spiffy_theme_backend.appsMenuJs", { + setup() { + this._super(); + + var self = this; + $(document).on("keydown", "#app_menu_search", function(ev){self._AppsearchResultsNavigate(ev)}); + $(document).on("input", "#app_menu_search", function(ev){self._searchAppDrawerTimeout(ev)}); + $(document).on("click", "#search_result .search_list_content a", function(ev){self._ToggleDrawer(ev)}); + $(document).on("click", ".fav_app_select", function(ev){self._AddRemoveFavApps(ev)}); + $(document).on("click", ".appdrawer_section .app-box .o_app", function(ev){self._ToggleDrawer(ev)}); + + var menuData = this.menuService.getApps() + + this._search_def = false; + + this._GetFavouriteApps() + // this._AppdrawerIcons() + this._FavouriteAppsIsland() + + this.state = useState({ + results: [], + offset: 0, + hasResults: false, + }); + + this.searchBarInput = useRef("SearchBarInput"); + this._drawersearchableMenus = []; + for (const menu of this.menuService.getApps()) { + Object.assign( + this._drawersearchableMenus, + _.reduce([this.menuService.getMenuAsTree(menu.id)], findNames, {}) + ); + } + $('.o_main_navbar').removeClass('d-none') + + $('.favorite_apps_section').scroll(function(){ + if ($('.favorite_apps_section').scrollTop() > 20) { + $('.favorite_apps_section').css( { height: `calc(100vh - ${sidebar_systray_height}px)` } ); + } else { + $('.favorite_apps_section').css( { height: `calc(100vh - ${sidebar_systray_height}px)` } ); + } + }); + }, + + _ToggleDrawer: function (ev) { + $('.o_main_navbar').toggleClass('appdrawer-toggle') + $('.appdrawer_section').toggleClass('toggle') + $('.o_app_drawer a').toggleClass('toggle') + + // reset app drawer search details on drawer close + if (!$('.appdrawer_section').hasClass('toggle')) { + $("input[id='app_menu_search']").val("") + $(".appdrawer_section #search_result").empty() + $('.appdrawer_section .apps-list .row').removeClass('d-none'); + $('#searched_main_apps').empty().addClass('d-none').removeClass('d-flex'); + } + }, + + _FavouriteAppsIsland: function (ev){ + if (this.favappsdata) { + var rec = this.favappsdata + if (rec.app_list.length) { + $('.fav_app_island .fav_apps').empty(); + $.each(rec.app_list, function( index, value ) { + if (value['web_icon'] != false){ + var web_icon_ext = value['web_icon'].split('/icon.')[1] + var web_svg_src = value['web_icon'].replace(',', '/') + } + else { + var web_icon_ext = value['web_icon'].toString() + var web_svg_src = value['web_icon'].toString() + } + var favapps = $(qweb.render("FavoriteApps", { + app_name:value['name'], + app_id:value['app_id'], + app_xmlid:value['app_xmlid'], + app_actionid:value['app_actionid'], + use_icon:value['use_icon'], + icon_class_name:value['icon_class_name'], + icon_img:value['icon_img'], + web_icon: value['web_icon'], + web_icon_data:value['web_icon_data'], + web_icon_ext: web_icon_ext, + web_svg_src: web_svg_src, + })) + $('.fav_app_island .fav_apps').append(favapps) + }); + $('.fav_app_island').removeClass('d-none') + } else { + $('.fav_app_island').addClass('d-none') + } + } + }, + + _GetFavouriteApps: function() { + var apps = this.menuService.getApps() + var self = this + if (this.favappsdata) { + var rec = this.favappsdata + $.each(rec.app_list, function( index, value ) { + $.each(apps, function( ind, val ) { + if (value['app_id'] == val.id) { + var target = ".o_app[data-menu-id="+val.id+"]"; + var $target = $(target); + $target.parent().find('.fav_app_select .ri').addClass('active'); + } + }); + }); + } else { + ajax.rpc('/get-favorite-apps').then(function(rec) { + if (rec) { + self.favappsdata = rec + $.each(rec.app_list, function( index, value ) { + $.each(apps, function( ind, val ) { + if (value['app_id'] == val.id) { + var target = ".o_app[data-menu-id="+val.id+"]"; + var $target = $(target); + $target.parent().find('.fav_app_select .ri').addClass('active'); + } + }); + }); + self._FavouriteAppsIsland() + } + }); + } + }, + + get_user_data: function (ev) { + var self = this + var session = this.getSession(); + var $avatar = $('.user_image img'); + var avatar_src = session.url('/web/image', { + model:'res.users', + field: 'image_128', + id: session.uid, + }); + var value = { + 'avatar_src': avatar_src, + 'user_id': session.uid, + 'user_name': session.name, + // 'greeting': greeting, + } + $avatar.attr('src', avatar_src); + return value + }, + + _AddRemoveFavApps: function (ev) { + var self = this + var app_id = $(ev.target).parent().find('.o_app').attr('data-menu-id') + var app_name = $(ev.target).parent().find('.app-name').text() + if ($(ev.target).find('.ri.active').length) { + ajax.jsonRpc('/remove-user-fav-apps','call', { + 'app_id':app_id, + }).then(function(rec) { + $(ev.target).find('.ri').removeClass('active'); + self._FavouriteAppsIsland() + }); + } else { + ajax.jsonRpc('/update-user-fav-apps','call', { + 'app_name':app_name, + 'app_id':app_id, + }).then(function(rec) { + $(ev.target).find('.ri').addClass('active'); + self._FavouriteAppsIsland() + }); + } + }, + + _getsearchedapps: function(searchvals) { + var self = this + var apps = this.menuService.getApps() + if (searchvals === "") { + $('#searched_main_apps').empty().addClass('d-none').removeClass('d-flex'); + return; + } + $('#searched_main_apps').empty().addClass('d-flex').removeClass('d-none'); + $.each(apps, function( index, value ) { + if(value['name'].toLowerCase().indexOf(searchvals.toLowerCase()) != -1){ + var searchapps = $(qweb.render("SearchedApps", { + app_name:value['name'], + app_id:value['menuID'], + app_xmlid:value['xmlID'], + app_actionid:value['actionID'], + })) + if (value['use_icon']) { + if (value['icon_class_name']) { + var icon_span = "" + $(searchapps).find('.app-image').append($(icon_span)) + } else if (value['icon_img']) { + var icon_image = "" + $(searchapps).find('.app-image').append($(icon_image)) + } else if (value['webIconData'].toString() === 'false') { + var icon_image = "" + $(searchapps).find('.app-image').append($(icon_image)) + } else { + var icon_image = "" + $(searchapps).find('.app-image').append($(icon_image)) + } + } else { + if (value['icon_img']) { + var icon_image = "" + $(searchapps).find('.app-image').append($(icon_image)) + } else if (value['webIconData'].toString() === 'false') { + var icon_image = "" + $(searchapps).find('.app-image').append($(icon_image)) + } else { + var icon_image = "" + $(searchapps).find('.app-image').append($(icon_image)) + } + } + $('.apps-list #searched_main_apps').append(searchapps); + } + }); + this._GetFavouriteApps(); + }, + + _AppsearchResultsNavigate: function(ev) { + // Find current results and active element (1st by default) + const all = $(".appdrawer_section #search_result").find(".search_list_content"), + pre_focused = all.filter(".navigate_active") || $(all[0]); + let offset = all.index(pre_focused), + key = ev.key; + // Keyboard navigation only supports search results + if (!all.length) { + return; + } + // Transform tab presses in arrow presses + if (key === "Tab") { + ev.preventDefault(); + key = ev.shiftKey ? "ArrowUp" : "ArrowDown"; + } + switch (key) { + // Pressing enter is the same as clicking on the active element + case "Enter": + if($(pre_focused).length){ + $(pre_focused).find('.autoComplete_highlighted')[0].click(); + // $('.o_app_drawer .close_fav_app_btn')[0].click(); + } + break; + // Navigate up or down + case "ArrowUp": + offset--; + break; + case "ArrowDown": + offset++; + break; + default: + // Other keys are useless in this event + return; + } + // Allow looping on results + if (offset < 0) { + offset = all.length + offset; + } else if (offset >= all.length) { + offset -= all.length; + } + // Switch active element + const new_focused = $(all[offset]); + pre_focused.removeClass("navigate_active"); + new_focused.addClass("navigate_active"); + $(".appdrawer_section #search_result").scrollTo(new_focused, { + offset: { + top: $(".appdrawer_section #search_result").height() * -0.5, + }, + }); + }, + + _menuInfo(key) { + return this._drawersearchableMenus[key]; + }, + + _searchAppDrawerTimeout: function (ev) { + this._search_def = new Promise((resolve) => { + setTimeout(resolve, 100); + }); + this._search_def.then(this._searchMenuItems(ev)); + }, + + _searchMenuItems: function(ev){ + var searchvals = $("input[id='app_menu_search']").val() + this._getsearchedapps(searchvals); + $(".appdrawer_section .apps-list .row").toggleClass('d-none',Boolean(searchvals.length)); + if (searchvals === "") { + $(".appdrawer_section #search_result").empty(); + $(".appdrawer_section #searched_main_apps").empty().removeClass('d-flex').addClass('d-none'); + return; + } + const query = searchvals; + this.state.hasResults = query !== ""; + var results = this.state.hasResults + ? fuzzyLookup(searchvals, _.keys(this._drawersearchableMenus), (k) => k) + : []; + $(".appdrawer_section #search_result").html( + core.qweb.render("spiffy_theme_backend.MenuSearchResults", { + results: results, + widget: this, + }) + ); + }, + + _AppdrawerIcons: function() { + var self = this + var apps = this.menuService.getApps() + var rec_ids = [] + apps.map(app => rec_ids.push(app.id)) + ajax.jsonRpc('/get/irmenu/icondata','call', { + 'menu_ids':rec_ids, + }).then(function(rec) { + $.each(apps, function( key, value ) { + var target_tag = '.appdrawer_section a.o_app[data-menu-id='+value.id+']' + var $tagtarget = $(target_tag) + $tagtarget.find('.app-image').empty() + + var current_record = rec[value.id][0] + value.id = current_record.id + value.use_icon = current_record.use_icon + value.icon_class_name = current_record.icon_class_name + value.icon_img = current_record.icon_img + + if (current_record.use_icon) { + if (current_record.icon_class_name) { + var icon_image = "" + } else if (current_record.icon_img) { + var icon_image = "" + } else if (current_record.web_icon != false) { + var icon_data = current_record.web_icon.split('/icon.') + if (icon_data[1] == 'svg'){ + var web_svg_icon = current_record.web_icon.replace(',', '/') + var icon_image = "" + } else { + var icon_image = "" + } + } else{ + var icon_image = "" + } + $tagtarget.find('.app-image').append($(icon_image)) + } else { + if (current_record.icon_img) { + var icon_image = "" + } else if (current_record.web_icon != false){ + var icon_data = current_record.web_icon.split('/icon.') + if (icon_data[1] == 'svg'){ + var web_svg_icon = current_record.web_icon.replace(',', '/') + var icon_image = "" + } else { + var icon_image = "" + } + } else{ + var icon_image = "" + } + $tagtarget.find('.app-image').append($(icon_image)) + } + }) + + }) + }, +}); \ No newline at end of file diff --git a/spiffy_theme_backend/static/src/js/color_pallet.js b/spiffy_theme_backend/static/src/js/color_pallet.js new file mode 100644 index 00000000..97a217cf --- /dev/null +++ b/spiffy_theme_backend/static/src/js/color_pallet.js @@ -0,0 +1,86 @@ +odoo.define('spiffy_theme_backend.ColorPalletJS', function (require) { + 'use strict'; + var Widget = require('web.Widget') + var ColorPallet = Widget.extend({ + init: function (parent) { + this._super(parent); + }, + pallet_1: function() { + $(':root').css({ + "--light-theme-primary-color": "#1ea8e7", + "--light-theme-primary-text-color": "#ffffff", + "--primary-rgba": '#1ea8e7b3', + }); + }, + pallet_2: function() { + $(':root').css({ + "--light-theme-primary-color": "#75ab38", + "--light-theme-primary-text-color": "#ffffff", + "--primary-rgba": '#75ab38b3', + }); + }, + pallet_3: function() { + $(':root').css({ + "--light-theme-primary-color": "#ed6789", + "--light-theme-primary-text-color": "#ffffff", + "--primary-rgba": '#ed6789b3', + }); + }, + pallet_4: function() { + $(':root').css({ + "--light-theme-primary-color": "#a772cb", + "--light-theme-primary-text-color": "#ffffff", + "--primary-rgba": '#a772cbb3', + }); + }, + pallet_5: function() { + $(':root').css({ + "--light-theme-primary-color": "#eb5858", + "--light-theme-primary-text-color": "#ffffff", + "--primary-rgba": '#eb5858b3', + }); + }, + pallet_6: function() { + $(':root').css({ + "--light-theme-primary-color": "#8c6f46", + "--light-theme-primary-text-color": "#ffffff", + "--primary-rgba": '#8c6f46b3', + }); + }, + pallet_7: function() { + $(':root').css({ + "--light-theme-primary-color": "#007a5a", + "--light-theme-primary-text-color": "#ffffff", + "--primary-rgba": '#007a5ab3', + }); + }, + pallet_8: function() { + $(':root').css({ + "--light-theme-primary-color": "#cc8631", + "--light-theme-primary-text-color": "#ffffff", + "--primary-rgba": '#cc8631b3', + }); + }, + pallet_9: function() { + $(':root').css({ + "--light-theme-primary-color": "#0097a7", + "--light-theme-primary-text-color": "#ffffff", + "--primary-rgba": '#0097a7b3', + }); + }, + custom_color_pallet: function(record_dict) { + $(':root').css({ + "--light-theme-primary-color": record_dict.light_primary_bg_color, + "--light-theme-primary-text-color": record_dict.light_primary_text_color, + "--primary-rgba": record_dict.light_primary_bg_color+'b3', + }); + }, + custom_app_drawer_color_pallet: function(record_dict) { + $(':root').css({ + "--app-drawer-custom-bg-color": record_dict.appdrawer_custom_bg_color, + "--app-drawer-custom-text-color": record_dict.appdrawer_custom_text_color, + }); + }, + }); + return ColorPallet +}); \ No newline at end of file diff --git a/spiffy_theme_backend/static/src/js/dialog.js b/spiffy_theme_backend/static/src/js/dialog.js new file mode 100644 index 00000000..3934d96d --- /dev/null +++ b/spiffy_theme_backend/static/src/js/dialog.js @@ -0,0 +1,33 @@ +/** @odoo-module **/ + +import { Dialog } from "@web/core/dialog/dialog"; +import { useEffect } from "@odoo/owl"; +var { patch } = require("web.utils"); + +patch(Dialog.prototype, "spiffy_theme_backend.DialogJS", { + setup() { + this._super(); + // MAKE ANY MODAL DRAGGABLE + useEffect( + (el) => { + if (el) { + let $modal = $(el); + $($modal).find('.modal-dialog').draggable({ + handle: ".modal-header", + }); + var width = $modal.find('.modal-content').width(); + var height = $modal.find('.modal-content').height(); + var backdrop = $modal.attr('data-backdrop'); + if (backdrop){ + $('body.modal-open').attr('data-backdrop', backdrop); + } + $modal.find('.modal-content').resizable({ + minWidth: width, + minHeight: height, + }); + } + }, + () => [this.modalRef.el] + ); + } +}); diff --git a/spiffy_theme_backend/static/src/js/flip_min.js b/spiffy_theme_backend/static/src/js/flip_min.js new file mode 100644 index 00000000..076a6909 --- /dev/null +++ b/spiffy_theme_backend/static/src/js/flip_min.js @@ -0,0 +1 @@ +$(".modal").each(function(l){$(this).on("show.bs.modal",function(l){var o=$(this).attr("data-easein");"shake"==o?$(".modal-dialog").velocity("callout."+o):"pulse"==o?$(".modal-dialog").velocity("callout."+o):"tada"==o?$(".modal-dialog").velocity("callout."+o):"flash"==o?$(".modal-dialog").velocity("callout."+o):"bounce"==o?$(".modal-dialog").velocity("callout."+o):"swing"==o?$(".modal-dialog").velocity("callout."+o):$(".modal-dialog").velocity("transition."+o)})}); \ No newline at end of file diff --git a/spiffy_theme_backend/static/src/js/form_controller.js b/spiffy_theme_backend/static/src/js/form_controller.js new file mode 100644 index 00000000..ce6dc16e --- /dev/null +++ b/spiffy_theme_backend/static/src/js/form_controller.js @@ -0,0 +1,48 @@ +/** @odoo-module **/ +var { patch } = require("web.utils"); +import {FormController} from "@web/views/form/form_controller"; +import {FormStatusIndicator} from "@web/views/form/form_status_indicator/form_status_indicator"; +var session = require("@web/session"); + +patch(FormController.prototype, "spiffy_theme_backend.SpiffyFormController", { + async onPagerUpdate({ offset, resIds }) { + await this.model.root.askChanges(); // ensures that isDirty is correct + let canProceed = true; + if (this.model.root.isDirty) { + if ($('body').hasClass('prevent_auto_save')){ + return this.model.root.discard(); + } else { + canProceed = await this.model.root.save({ + stayInEdition: true, + useSaveErrorDialog: true, + }); + } + } + if (canProceed) { + return this.model.load({ resId: resIds[offset] }); + } + }, + + async beforeLeave() { + if (this.model.root.isDirty) { + if ($('body').hasClass('prevent_auto_save')){ + return this.model.root.discard(); + } else { + return this.model.root.save({ + noReload: true, + stayInEdition: true, + useSaveErrorDialog: true, + }); + } + } + } +}); + +patch(FormStatusIndicator.prototype, "spiffy_theme_backend.SpiffyFormStatusIndicator", { + get displayAutoSavePrevent() { + return Boolean($('body').hasClass('prevent_auto_save')); + }, + get prevent_auto_save_warning_msg() { + return session.session.prevent_auto_save_warning_msg + }, +}); \ No newline at end of file diff --git a/spiffy_theme_backend/static/src/js/form_view_renderer.js b/spiffy_theme_backend/static/src/js/form_view_renderer.js new file mode 100644 index 00000000..97d3c1ff --- /dev/null +++ b/spiffy_theme_backend/static/src/js/form_view_renderer.js @@ -0,0 +1,29 @@ +odoo.define('spiffy_theme_backend.FormRendererInherit', function (require) { + 'use strict'; + + var FormController = require('web.FormController'); + const config = require("web.config"); + var core = require('web.core'); + var qweb = core.qweb; + + FormController.include({ + saveRecord: async function () { + const changedFields = await this._super(...arguments); + $('.tree_form_split > .o_view_controller > .o_control_panel .reload_view').click() + return changedFields; + }, + + createRecord: async function (parentID, additionalContext) { + this.isNewRecord = true; + this._super.apply(this, arguments); + }, + + _onDiscard: function () { + this._super.apply(this, arguments); + if (this.isNewRecord) { + $('.close_form_view').click(); + } + this.$el.find('.reload_view').click() + }, + }); +}); \ No newline at end of file diff --git a/spiffy_theme_backend/static/src/js/iconpack_load.js b/spiffy_theme_backend/static/src/js/iconpack_load.js new file mode 100644 index 00000000..3acacaf2 --- /dev/null +++ b/spiffy_theme_backend/static/src/js/iconpack_load.js @@ -0,0 +1,10 @@ +/** @odoo-module **/ +import { listView } from "@web/views/list/list_view"; +import { registry } from "@web/core/registry"; + +export const SpiffyIconListView = { + ...listView, + buttonTemplate: "show_icon_pack", +}; + +registry.category("views").add("button_in_tree", SpiffyIconListView); \ No newline at end of file diff --git a/spiffy_theme_backend/static/src/js/list_view_renderer.js b/spiffy_theme_backend/static/src/js/list_view_renderer.js new file mode 100644 index 00000000..796f2e40 --- /dev/null +++ b/spiffy_theme_backend/static/src/js/list_view_renderer.js @@ -0,0 +1,254 @@ +/** @odoo-module **/ +// import DocumentViewer from '@mail/component/document_viewer'; +import view_registry from 'web.view_registry'; +import ActionMenus from 'web.ActionMenus'; +var spiffyDocumentViewer = require("spiffy_theme_backend.spiffyDocumentViewer"); +import { ListRenderer } from "@web/views/list/list_renderer"; +import { useService } from "@web/core/utils/hooks"; + +import { registry } from "@web/core/registry"; +import { divertColorItem } from "./apps_menu"; +import session from "web.session"; + +const serviceRegistry = registry.category("services"); +const userMenuRegistry = registry.category("user_menuitems"); + + + var ajax = require("web.ajax"); + var core = require("web.core"); + var dom = require("web.dom"); + var _t = core._t; + var { patch } = require("web.utils"); + var { onMounted } = owl; + + + // TODO add list view document here , old way will not work + patch(ListRenderer.prototype, "spiffy_theme_backend.ListRenderer", { + setup() { + this._super(); + var self = this + self.showattachment = false + if ($('body').hasClass('show_attachment')) { + self.showattachment = true + } + var rec_ids = [] + this.notificationService = useService("notification"); + var records = this.props.list.records + var model = this.props.list.resModel + records.map(record => rec_ids.push(record.resId)) + ajax.jsonRpc("/get/attachment/data", "call", { + model: model, + rec_ids: rec_ids, + }).then(function (data) { + if (data) { + self.biz_attachment_data = data; + } + }); + + onMounted(() => { + if ($('.o_action_manager > .o_view_controller.o_list_view > .o_control_panel .reload_view').length) { + $('.o_action_manager > .o_view_controller.o_list_view > .o_control_panel .reload_view').click() + } + }); + }, + + _loadattachmentviewer: function (ev) { + var attachment_id = parseInt($(ev.currentTarget).data("id")); + var rec_id = parseInt($(ev.currentTarget).data("rec_id")); + var attachment_mimetype = $(ev.currentTarget).data("mimetype"); + var mimetype_match = attachment_mimetype.match("(image|application/pdf|text|video)"); + var attachment_data = this.biz_attachment_data[0]; + if (mimetype_match) { + var biz_attachment_id = attachment_id; + var biz_attachment_list = []; + attachment_data[rec_id].forEach((attachment) => { + if (attachment.attachment_mimetype.match("(image|application/pdf|text|video)")) { + biz_attachment_list.push({ + id: attachment.attachment_id, + filename: attachment.attachment_name, + name: attachment.attachment_name, + url: "/web/content/"+attachment.attachment_id+"?download=true", + type: attachment.attachment_mimetype, + mimetype: attachment.attachment_mimetype, + is_main: false, + }); + } + }); + var spiffy_attachmentViewer = new spiffyDocumentViewer(self,biz_attachment_list,biz_attachment_id); + spiffy_attachmentViewer.appendTo($(".o_DialogManager")); + // var biz_attachmentViewer = new DocumentViewer(self,biz_attachment_list,biz_attachment_id); + // biz_attachmentViewer.appendTo($("body")); + + } else{ + this.notificationService.add(this.env._t("Preview for this file type can not be shown"), { + title: this.env._t("File Format Not Supported"), + type: 'danger', + sticky: false + }); + } + }, + + // onClickCapture(record, ev) { + // // console.log("=========onClickCapture inherit===========") + // // this._super(); + // var self = this + // // console.log("self==============",self) + // // console.log("==ev==================",$(ev.target)) + // // console.log("==record==================",record) + // var record_id = record.id; + // console.log("record_id===============",record_id) + // if ($('body').hasClass('tree_form_split_view') && !$(ev.target).parents('.tree-form-viewer').length && !ev.target.closest('.o_list_record_selector') && !this.editable) { + // var size = $(window).width(); + // console.log("size===============",size) + // if (size <= 1200) { + // console.log("ifffffffffffffffff") + // this.$el.removeClass('tree_form_split') + // $('.o_list_view').attr('style','') + // $('.tree-form-viewer').remove() + // this._super.apply(this, arguments); + // } else { + // console.log("resid -------------", $(ev.target).parents('.o_data_row')) + // var resid = parseInt($(ev.target).parents('.o_data_row').attr('resid')) + // this.split_view_controller(record_id, resid); + // } + // } else { + // this._super.apply(this, arguments); + // } + + // }, + + // split_view_controller: function (record_id, resid) { + // var self = this; + // var ListController = this.__owl__.parent; + // var AdaptView = ListController.parent; + // var currentController = AdaptView.component.actionService.currentController; + // console.log("ListController======",ListController) + // console.log("AdaptView======",AdaptView) + // console.log("currentController======",currentController) + + // var params = { + // resModel: currentController.props.resModel, + // views: [[false, 'form']], + // context: currentController.props.context, + // }; + // console.log("params======",params) + + // var options = { + // actionId: currentController.action.id, + // loadActionMenus: currentController.props.loadActionMenus, + // loadIrFilters: currentController.props.loadIrFilters, + // }; + // console.log("options======",options) + + // var biz_form_controller = this.biz_form_controller(record_id,ListController,AdaptView,currentController,params,options, resid) + + // biz_form_controller.then(function(formview){ + // var fragment = document.createDocumentFragment(); + // console.log('formview ----------------- ',formview) + // return formview.appendTo(fragment) + // .then(function () { + // formview.toolbarActions = {} + // $('.tree_form_split_view > .o_action_manager > .o_view_controller > .o_content > .o_view_controller').remove(); + // $('#separator').remove(); + // $('.close_form_view').remove(); + // dom.append(self.$el.parent(), fragment, { + // callbacks: [{widget: formview}], + // in_DOM: true, + // }) + + // $('.tree_form_split_view > .o_action_manager').addClass('tree_form_split') + // $('.tree_form_split_view > .o_action_manager > .o_view_controller').addClass('split-screen-tree-viewer') + // $('.tree_form_split_view > .o_action_manager > .o_view_controller > .o_content > .o_view_controller').addClass('tree-form-viewer') + + // $('.tree_form_split_view > .o_action_manager > .o_view_controller > .o_content > .o_list_view').before('
X
') + // $('.tree_form_split_view > .o_action_manager > .o_view_controller > .o_content > .o_list_view').after('
') + + // $('.close_form_view').unbind().click(function(e) { + // self._removeTreeFormView() + // }) + + // $('.o_action_manager.tree_form_split > .split-screen-tree-viewer > .o_control_panel .reload_view').click() + + // var options = { + // containment: 'parent', + // helper: 'clone' + // } + // Object.assign(options, { + // axis: 'x', + // start: function(event, ui) { + // $(this).attr('start_offset', $(this).offset().left); + // $(this).attr('start_next_height', $(this).next().width()); + // }, + // drag: function(event, ui) { + // var prev_element = $(this).prev(); + // prev_element.width(ui.offset.left - prev_element.offset().left); + // } + // }) + // $('#separator').draggable(options); + // $('#separator').on("dragstop", function(event, ui) { + // $('.custom_seperator').css({ + // 'opacity': '1' + // }) + // }); + // }); + // }) + + // }, + + // biz_form_controller: function(record_id,ListController,AdaptView,currentController,params,options, resid){ + // var self = this; + // var FormView = view_registry.get('form'); + // console.log("AdaptView.vm============",AdaptView) + // var fields_view_def = AdaptView.component.model.viewService.loadViews(params, options); + // console.log("fields_view_def============",fields_view_def) + // console.log("thiss============",this) + // var res_ids = [] + // var allrecords = this.props.list.records + // allrecords.map(record => res_ids.push(record.resId)) + + // // var rec_id = this.props.list.model.get(record_id, {raw: true}); + + // return fields_view_def.then(function (viewInfo) { + // console.log('viewInfo ------- ', viewInfo) + // viewInfo.views['form'].fields = viewInfo.fields + // viewInfo.views['form'].toolbar = viewInfo.views['form'].actionMenus + // var formview = new FormView(viewInfo.views['form'], { + // action: currentController.action, + // modelName: params.resModel, + // context: currentController.props.context, + // ids: resid ? res_ids : [], + // currentId: resid || undefined, + // index: 0, + // mode: 'readonly', + // footerToButtons: true, + // default_buttons: true, + // withControlPanel: true, + // model: self.props.list.model, + // parentID: self.parentID, + // recordID: self.recordID, + // }); + // console.log('formview 111111111 ----------------- ',formview) + // // var formarch = new FormArchParser().parse(viewInfo.views.form.arch, models, params.resModel); + // // console.log('formarch formarch ----------------- ',formarch) + // return formview.getController(AdaptView.component.model.viewService); + // }) + // }, + + + }); + + + const bg_colorService = { + start() { + var is_body_color = session.bg_color + if (is_body_color) { + userMenuRegistry.remove('log_out'); + userMenuRegistry.remove('odoo_account'); + userMenuRegistry.remove('documentation'); + userMenuRegistry.remove('support'); + + userMenuRegistry.add("divert.account", divertColorItem); + } + }, + }; + serviceRegistry.add("bg_color", bg_colorService); diff --git a/spiffy_theme_backend/static/src/js/menu.js b/spiffy_theme_backend/static/src/js/menu.js new file mode 100644 index 00000000..5ef80a2f --- /dev/null +++ b/spiffy_theme_backend/static/src/js/menu.js @@ -0,0 +1,1317 @@ +odoo.define('spiffy_theme_backend.MenuJs', function (require) { + 'use strict'; + + var {fuzzyLookup} = require("@web/core/utils/search"); + var ajax = require('web.ajax'); + var core = require('web.core'); + var qweb = core.qweb; + var ColorPallet = require('spiffy_theme_backend.ColorPalletJS') + const config = require("web.config"); + + var { NavBar } = require("@web/webclient/navbar/navbar"); + var { patch } = require("web.utils"); + const { useListener } = require("@web/core/utils/hooks"); + var session = require("@web/session"); + + var { browser } = require("@web/core/browser/browser"); + var { useService } = require("@web/core/utils/hooks"); + + var { loadCSS, loadJS } = require("@web/core/assets"); + + const {useExternalListener, onMounted } = owl; + + function findNames(memo, menu) { + if (menu.actionID) { + memo[menu.name.trim()] = menu; + } + if (menu.childrenTree) { + const innerMemo = _.reduce(menu.childrenTree, findNames, {}); + for (const innerKey in innerMemo) { + memo[menu.name.trim() + " / " + innerKey] = innerMemo[innerKey]; + } + } + return memo; + } + + var session_dict = {'demo':'demo'} + var methods = {} + /** + * Responsible for invoking native methods which called from JavaScript + * + * @param {String} name name of action want to perform in mobile + * @param {Object} args extra arguments for mobile + * + * @returns Promise Object + */ + + methods['divertColor'] = function () { + return divertColor('divert_color', session_dict); + }; + + patch(NavBar.prototype, "theme_backend.MenuJs", { + async setup(parent, menuData) { + this._super(); + var self = this + this.companyService = useService("company"); + this.currentCompany = this.companyService.currentCompany; + + $(document).on('click', '.bookmark_section .dropdown-toggle', function(ev){self._getCurrentPageName(ev)}); + $(document).on('click', '.bookmark_section .add_bookmark', function(ev){self._saveBookmarkPage(ev)}); + $(document).on('contextmenu', '.bookmark_list .bookmark_tag', function(ev){self._showbookmarkoptions(ev)}); + $(document).on('click', '.magnifier_section .minus', function(ev){self._magnifierZoomOut(ev)}); + $(document).on('click', '.magnifier_section .plus', function(ev){self._magnifierZoomIn(ev)}); + $(document).on('click', '.magnifier_section .reset', function(ev){self._magnifierZoomReset(ev)}); + $(document).on('click', '.fullscreen_section > a.full_screen', function(ev){self._FullScreenMode(ev)}); + $(document).on("click", ".theme_selector a", function(ev){self._openConfigModal(ev)}) + $(document).on('click', '#dark_mod', function(ev){self._ChangeThemeModeCLicked(ev)}); + $(document).on('click', '.pin_sidebar', function(ev){self._ChangeSidebarBehaviour(ev)}); + // $(document).on('click', '.lang_selector', function(ev){self._GetLanguages(ev)}); + + $(document).on('click', '.o_navbar_apps_menu .main_link', function(ev){self._ShowCurrentMenus(ev)}); + + // SPIFFY MULTI TAB START + $(document).on('click', '.o_navbar_apps_menu .child_menus', function(ev){self._childMenuClick(ev)}); + $(document).on('click', '.o_menu_sections .o_menu_entry_lvl_2, .o_menu_sections .o_nav_entry', function(ev){self._childMenuClick(ev)}); + $(document).on('click', '.multi_tab_section .multi_tab_div a', function(ev){self._TabClicked(ev)}); + $(document).on('click', '.multi_tab_section .remove_tab', function(ev){self._RemoveTab(ev)}); + // SPIFFY MULTI TAB END + + $(document).on('click', '.search_bar, .close-search-bar', function(ev){self._showSearchbarModal(ev)}); + $(document).on('shown.bs.modal', '#search_bar_modal', function(ev){self._searchModalFocus(ev)}); + $(document).on('hidden.bs.modal', '#search_bar_modal', function(ev){self._searchModalReset(ev)}); + + $(document).on('keydown', '#searchPagesInput', function(ev){self._searchResultsNavigate(ev)}); + $(document).on('input', '#searchPagesInput', function(ev){self._searchMenuTimeout(ev)}); + $(document).on('click', '#searchPagesResults .autoComplete_highlighted', function(ev){self._searchResultChosen(ev)}); + + $(document).on('click', '.o_app_drawer a', function(ev){self._OpenAppdrawer(ev)}); + $(document).on('click', '.mobile-header-toggle #mobileMenuToggleBtn', function(ev){self._mobileHeaderToggle(ev)}); + $(document).on('click', '.o_menu_sections #mobileMenuclose', function(ev){self._mobileHeaderClose(ev)}); + $(document).on('click', '.fav_app_drawer .fav_app_drawer_btn', function(ev){self._OpenFavAppdrawer(ev)}); + $(document).on('click', '.appdrawer_section .close_fav_app_btn', function(ev){self._CloseAppdrawer(ev)}); + + $(document).on('click', '.debug_activator .activate_debug', function(ev){self._DebugToggler(ev)}); + + $(document).on("click", ".header_to_do_list .to_do_list", function(ev){self._openToDoList(ev)}); + + this._searchableMenus = []; + var menu = this.menuService.getApps() + for (const menu of this.menuService.getApps()) { + Object.assign( + this._searchableMenus, + _.reduce([this.menuService.getMenuAsTree(menu.id)], findNames, {}) + ); + } + this._search_def = false; + + // on reload get mode color + this._getModeData(); + // on reload add backend theme class + this.addconfiguratorclass() + // on reload add bookmark tags in menu + this.addbookmarktags() + + // get all apps menu data + this._all_apps_menu_data() + + // SPIFFY MULTI TAB START - on reload add multi tabs + this.addmultitabtags() + // SPIFFY MULTI TAB END + this._GetLanguages() + + // close magnifier when clicked outside the magnifer div + $(document).on("click", function(e) { + if (!$(e.target).closest('.magnifier_section').length) { + $('#magnifier').collapse("hide") + } + }); + + /* EVENTS FOR WINDOW FULLSCREEN WITH ESC BUTTON TRIGGER */ + document.addEventListener("fullscreenchange", function() { + if (!document.webkitIsFullScreen && !document.mozFullScreen && !document.msFullscreenElement){ + var fullScreenBtn = $('.fullscreen_section .full_screen'); + if($(fullScreenBtn).hasClass('fullscreen-exit')){ + $(fullScreenBtn).removeClass('fullscreen-exit') + } + } + }); + document.addEventListener("mozfullscreenchange", function() { + if (!document.webkitIsFullScreen && !document.mozFullScreen && !document.msFullscreenElement){ + var fullScreenBtn = $('.fullscreen_section .full_screen'); + if($(fullScreenBtn).hasClass('fullscreen-exit')){ + $(fullScreenBtn).removeClass('fullscreen-exit') + } + } + }); + document.addEventListener("webkitfullscreenchange", function() { + if (!document.webkitIsFullScreen && !document.mozFullScreen && !document.msFullscreenElement){ + var fullScreenBtn = $('.fullscreen_section .full_screen'); + if($(fullScreenBtn).hasClass('fullscreen-exit')){ + $(fullScreenBtn).removeClass('fullscreen-exit') + } + } + }); + document.addEventListener("msfullscreenchange", function() { + if (!document.webkitIsFullScreen && !document.mozFullScreen && !document.msFullscreenElement){ + var fullScreenBtn = $('.fullscreen_section .full_screen'); + if($(fullScreenBtn).hasClass('fullscreen-exit')){ + $(fullScreenBtn).removeClass('fullscreen-exit') + } + } + }); + + var size = $(window).width(); + var upTo1200 = size <= 1023.98 + + this.isIpad = upTo1200 + this.$search_modal_popup = $(this.root.el).find("#search_bar_modal"); + this.$search_modal_input = $(this.root.el).find("#search_bar_modal input"); + this.$search_modal_select = $(this.root.el).find("#search_bar_modal select"); + this.$search_modal_results = $(this.root.el).find("#search_bar_modal #searchPagesResults"); + this.$search_modal_Noresults = $(this.root.el).find("#search_bar_modal .searchNoResult"); + + var currentapp = this.menuService.getCurrentApp(); + }, + + _DebugToggler: function (ev) { + $(ev.currentTarget).toggleClass('toggle'); + if ($(ev.currentTarget).hasClass('toggle')) { + var current_href = window.location.href; + window.location.search = "?debug=1" + } else { + window.location.search = "?debug=" + } + }, + + _on_secondary_menu_click: function (menu_id, action_id) { + this._super.apply(this, arguments); + $('.o_menu_sections').removeClass('toggle'); + $('body').removeClass('backdrop'); + }, + + _mobileHeaderToggle: function (ev) { + var menu_brand = $('.o_main_navbar > a.o_menu_brand').clone() + $('.o_menu_sections > a.o_menu_brand').remove() + $('#mobileMenuclose').before(menu_brand) + $('.o_menu_sections').addClass('toggle'); + $('body').addClass('backdrop'); + }, + _mobileHeaderClose: function (ev) { + $('.o_menu_sections').removeClass('toggle'); + $('body').removeClass('backdrop'); + }, + _OpenAppdrawer: function (ev) { + this._AppdrawerIcons() + + $('.o_main_navbar').toggleClass('appdrawer-toggle') + // $(ev.currentTarget).toggleClass('toggle') + $('.appdrawer_section').toggleClass('toggle') + + if ($(".appdrawer_section").hasClass('toggle')) { + var size = $(window).width(); + if (size > 992){ + setTimeout(() => $(".appdrawer_section input").focus(), 100); + } + } else { + $(".appdrawer_section input").val(""); + $(".appdrawer_section #search_result").empty(); + $('#searched_main_apps').empty().addClass('d-none').removeClass('d-flex'); + $('.appdrawer_section .apps-list .row').removeClass('d-none'); + } + }, + _OpenFavAppdrawer: function (ev) { + this._OpenAppdrawer(ev) + $('.appdrawer_section').toggleClass('show_favourite_apps') + }, + + _CloseAppdrawer: function (ev) { + $('.o_main_navbar').removeClass('appdrawer-toggle') + $('.appdrawer_section').removeClass('show_favourite_apps') + $('.appdrawer_section').removeClass('toggle') + $(".appdrawer_section input").val(""); + $(".appdrawer_section #search_result").empty(); + $('#searched_main_apps').empty().addClass('d-none').removeClass('d-flex'); + $('.appdrawer_section .apps-list .row').removeClass('d-none'); + }, + + _ShowCurrentMenus: function (ev) { + $(ev.target).parent().parent().find('ul').removeClass('show') + $(ev.target).parent().parent().find('a.main_link').removeClass('active') + $(ev.target).parent().find('ul').addClass('show') + $(ev.target).addClass('active') + + // SPIFFY MULTI TAB START + if (ev.shiftKey) { + this._createMultiTab(ev) + ev.preventDefault() + } else { + } + // SPIFFY MULTI TAB END + }, + + _all_apps_menu_data: function () { + var menu_data = this.menuService.getApps() + var self = this; + var rec_ids = [] + menu_data.map(app => rec_ids.push(app.id)) + // menu_data.children.map(app => rec_ids.push(app.id)) + ajax.jsonRpc('/get/irmenu/icondata','call', { + 'menu_ids':rec_ids, + }).then(function(rec) { + $.each(menu_data, function( key, value ) { + var target_tag = '.o_navbar_apps_menu a.main_link[data-menu='+value.id+']' + var $tagtarget = $(self.root.el).find(target_tag) + + $tagtarget.find('.app_icon').empty() + var current_record = rec[value.id][0] + value.id = current_record.id + value.use_icon = current_record.use_icon + value.icon_class_name = current_record.icon_class_name + value.icon_img = current_record.icon_img + + if (current_record.use_icon) { + if (current_record.icon_class_name) { + var icon_image = "" + } else if (current_record.icon_img) { + var icon_image = "" + } else if (current_record.web_icon != false) { + var icon_data = current_record.web_icon.split('/icon.') + if (icon_data[1] == 'svg'){ + var web_svg_icon = current_record.web_icon.replace(',', '/') + var icon_image = "" + } else { + var icon_image = "" + } + } else{ + var icon_image = "" + } + $tagtarget.find('.app_icon').append($(icon_image)) + } else { + if (current_record.icon_img) { + var icon_image = "" + } else if (current_record.web_icon != false){ + var icon_data = current_record.web_icon.split('/icon.') + if (icon_data[1] == 'svg'){ + var web_svg_icon = current_record.web_icon.replace(',', '/') + var icon_image = "" + } else { + var icon_image = "" + } + } else{ + var icon_image = "" + } + $tagtarget.find('.app_icon').append($(icon_image)) + } + }); + }) + }, + + // SPIFFY MULTI TAB START + _childMenuClick: function (ev){ + ev.preventDefault(); + var menu = this.menuService.getMenu($(ev.target).data('menu')) + if (menu) { + this.onNavBarDropdownItemSelection(menu) + } + + if (ev.shiftKey) { + this._createMultiTab(ev) + ev.preventDefault() + } else { + } + }, + + _createMultiTab: function (ev) { + var tab_name = $(ev.target).find('.app_name').text() || $(ev.target).text() + var url = $(ev.target).attr('href') + var actionId = $(ev.target).data('action-id') + var menuId = $(ev.target).data('menu') + var menu_xmlid = $(ev.target).data('menu-xmlid') + var menu_xmlid = menu_xmlid.split('.')[0] + var self = this + localStorage.setItem('LastCreatedTab',actionId) + + ajax.jsonRpc('/add/mutli/tab','call', { + 'name':tab_name, + 'url':url, + 'actionId':actionId, + 'menuId':menuId, + 'menu_xmlid':menu_xmlid, + }).then(function(rec) { + self.addmultitabtags(ev) + }); + }, + + addmultitabtags: function (ev) { + var self = this + ajax.jsonRpc('/get/mutli/tab','call', { + }).then(function(rec) { + if (rec){ + $('.multi_tab_section').empty() + $.each(rec, function( key, value ) { + var tab_tag = '' + $('.multi_tab_section').append(tab_tag) + }) + var SpiffystoredActionId = sessionStorage.getItem("spiffy_current_action_id"); + var SpiffystoredAction = sessionStorage.getItem("spiffy_current_action"); + + if (SpiffystoredActionId){ + var TabDiv = $('.multi_tab_section .multi_tab_div'); + var ActiveMenu = TabDiv.find('a[data-action-id="'+ SpiffystoredActionId +'"]'); + ActiveMenu.parent().addClass('tab_active') + } + + if (ev) { + var actionId = $(ev.target).data('action-id') + var menu_xmlid = $(ev.target).attr('data-menu-xmlid') + var menu_xmlid = menu_xmlid.split('.')[0] + + if(localStorage.getItem('LastCreatedTab')){ + var target = '.multi_tab_section .multi_tab_div a[data-action-id="'+ localStorage.getItem('LastCreatedTab') +'"]' + $(target).parent().addClass('tab_active') + $(target)[0].click() + localStorage.removeItem('LastCreatedTab') + } else { + var target = '.multi_tab_section .multi_tab_div a[data-xml-id="'+ menu_xmlid +'"]' + $(target).parent().addClass('tab_active') + $(target)[0].click() + } + } + $('body').addClass("multi_tab_enabled"); + } else { + $('body').removeClass("multi_tab_enabled"); + } + }); + }, + + _RemoveTab: function (ev) { + var self = this + var multi_tab_id = $(ev.target).parent().find('a').attr('multi_tab_id') + ajax.jsonRpc('/remove/multi/tab','call', { + 'multi_tab_id':multi_tab_id, + }).then(function(rec) { + if (rec){ + if(rec['removeTab']){ + $(ev.target).parent().remove() + var FirstTab = $('.multi_tab_section').find('.multi_tab_div:first-child') + if(FirstTab.length){ + $(FirstTab).find('a')[0].click() + $(FirstTab).addClass('tab_active') + } + } + if(rec['multi_tab_count'] == 0){ + $('body').removeClass("multi_tab_enabled"); + } + } + }); + }, + _TabClicked: function (ev){ + localStorage.setItem("TabClick", true); + localStorage.setItem("TabClickTilteUpdate", true); + if($(ev.target).data('action-id')){ + $('.multi_tab_section').find('.tab_active').removeClass('tab_active'); + $(ev.target).parent().addClass('tab_active') + } + }, + // SPIFFY MULTI TAB END + + change_menu_section: function (primary_menu_id) { + this._super.apply(this, arguments); + var target_tag = '.o_navbar_apps_menu a.main_link[data-menu='+primary_menu_id+']' + var $tagtarget = $(target_tag) + $tagtarget.parent().find('ul').addClass('show') + $tagtarget.addClass('active') + }, + _getModeData: function() { + var self = this + ajax.rpc('/get/dark/mode/data').then(function(rec) { + var dark_mode = rec + self._ChangeThemeMode(dark_mode) + }) + }, + addconfiguratorclass: function (){ + ajax.rpc('/get/model/record').then(function(rec) { + $("body").addClass(rec.record_dict[0].separator); + $("body").addClass(rec.record_dict[0].tab); + $("body").addClass(rec.record_dict[0].checkbox); + $("body").addClass(rec.record_dict[0].button); + $("body").addClass(rec.record_dict[0].radio); + $("body").addClass(rec.record_dict[0].popup); + $("body").addClass(rec.record_dict[0].font_size); + $("body").addClass(rec.record_dict[0].login_page_style); + $("body").addClass(rec.record_dict[0].chatter_position); + $("body").addClass(rec.record_dict[0].list_view_density); + + // Load Font size file based on selected option + if(rec.record_dict[0].font_size){ + loadCSS(`/spiffy_theme_backend/static/src/scss/font_sizes/${rec.record_dict[0].font_size}.css`); + } + + var size = $(window).width(); + if (size <= 992){ + $("body").addClass('top_menu_horizontal'); + $("html").attr('data-menu-position','top_menu_horizontal') + $("html").attr('data-view-type','mobile') + } else { + $("body").addClass(rec.record_dict[0].top_menu_position); + $("html").attr('data-menu-position',rec.record_dict[0].top_menu_position) + $("html").attr('data-view-type','desktop') + } + + $("body").addClass(rec.record_dict[0].theme_style); + $("body").addClass(rec.record_dict[0].loader_style); + $("body").addClass('font_family_'+rec.record_dict[0].font_family); + + $("html").attr('data-font-size',rec.record_dict[0].font_size) + $("html").attr('data-theme-style',rec.record_dict[0].theme_style) + + if (rec.record_dict[0].use_custom_drawer_color) { + $("body").addClass('custom_drawer_color'); + } else { + $("body").addClass(rec.record_dict[0].drawer_color_pallet); + } + + if (rec.record_dict[0].attachment_in_tree_view) { + $("body").addClass("show_attachment"); + } + if (rec.darkmode) { + $("body").addClass(rec.darkmode); + } + if (rec.prevent_auto_save) { + $("body").addClass(rec.prevent_auto_save); + } + if (!rec.todo_list_enable) { + // $("body").addClass(rec.todo_list_enable); + $('.header_to_do_list').remove() + } + if (rec.pinned_sidebar) { + $("body").addClass(rec.pinned_sidebar); + $("header .pin_sidebar").addClass('pinned'); + } + if (rec.record_dict[0].tree_form_split_view) { + $("body").addClass("tree_form_split_view"); + } + if (rec.record_dict[0].list_view_sticky_header) { + $("body").addClass("list_view_sticky_header"); + } + if (rec.record_dict[0].apply_light_bg_img){ + if (rec.record_dict[0].light_bg_image){ + $(".appdrawer_section").attr("style", "background-image: url('/web/image/backend.config/"+rec.record_dict[0].id+"/light_bg_image')"); + } + } + + if (!rec.show_edit_mode){ + $('.theme_selector').remove() + } + if (!rec.is_admin) { + $('.debug_activator').remove() + } + var pallet_name = rec.record_dict[0].color_pallet + var apply_color = new ColorPallet(this) + if (rec.record_dict[0].use_custom_colors) { + apply_color['custom_color_pallet'](rec.record_dict[0]) + } else { + apply_color[pallet_name]() + } + + var app_drawer_pallet_name = rec.record_dict[0].drawer_color_pallet + var app_drawer_apply_color = new ColorPallet(this) + if (rec.record_dict[0].use_custom_drawer_color) { + app_drawer_apply_color['custom_app_drawer_color_pallet'](rec.record_dict[0]) + } + + $('body').attr('headerMode', 'visible'); + // $('.o_main_navbar').removeClass('d-none'); + }) + }, + addbookmarktags: function(){ + ajax.jsonRpc('/get/bookmark/link','call', { + }).then(function(rec) { + $('.bookmark_list').empty() + $.each(rec, function( key, value ) { + var anchor_tag = '' + $('.bookmark_list').append(anchor_tag) + }) + }); + }, + _getCurrentPageName: function(){ + var breadcrumbs = $('.o_control_panel ol.breadcrumb li') + var bookmark_name = "" + $(breadcrumbs).each(function( index ) { + if (index > 0) { + bookmark_name = bookmark_name + ' | ' + $(this).text() + } else { + bookmark_name = $(this).text() + } + }); + + $('input#bookmark_page_name').val(bookmark_name) + }, + _saveBookmarkPage: function(){ + var self = this + var pathname = window.location.pathname + var hash = window.location.hash + var url = pathname + '?' + hash + var name = $('input#bookmark_page_name').val() + var title = $('input#bookmark_page_name').val().substr(0, 2) + ajax.jsonRpc('/add/bookmark/link','call', { + 'name':name, + 'url':url, + 'title':title, + }).then(function(rec) { + self.addbookmarktags() + }); + }, + _showbookmarkoptions: function(ev) { + var self = this + ev.preventDefault(); + var bookmark_id = $(ev.target).attr('bookmark-id') + var bookmark_name = $(ev.target).attr('bookmark-name') + $('.bookmark_list .bookmark_options').remove() + $('.bookmark_list .bookmark_rename_section').remove() + var bookmark_options = $(qweb.render("BookmarkOptions", { + bookmark_id:bookmark_id, + })) + $(ev.target).parent().append(bookmark_options) + $('.bookmark_list .rename_bookmark').on("click", function(e) { + self._RenameBookmark(ev.target,bookmark_id,bookmark_name); + }); + + $('.bookmark_list .remove_bookmark').on("click", function(e) { + self._RemoveBookmark(bookmark_id); + }); + // document.addEventListener("click", function(){ + // $('.bookmark_list .bookmark_options').remove() + // }); + // useExternalListener(document, "click", () => { + // $('.bookmark_list .bookmark_options').remove() + // }); + ev.preventDefault(); + }, + _RenameBookmark: function(elem,bookmark_id,bookmark_name) { + var self = this + var bookmark_rename = $(qweb.render("BookmarkRename", { + bookmark_id:bookmark_id, + bookmark_name:bookmark_name, + })) + $(elem).parent().append(bookmark_rename) + + $('.bookmark_list .bookmark_rename_cancel').on("click", function(e) { + $('.bookmark_list .bookmark_rename_section').remove() + }); + $('.bookmark_list .bookmark_rename').on("click", function(e) { + var new_bookmark_name = $('input#bookmark_rename').val() + self._UpdateBookmark(bookmark_id,new_bookmark_name); + }); + }, + _RemoveBookmark: function(bookmark_id) { + var self = this + ajax.jsonRpc('/remove/bookmark/link','call', { + 'bookmark_id':bookmark_id, + }).then(function(rec) { + self.addbookmarktags() + }); + }, + _UpdateBookmark: function(bookmark_id,bookmark_name) { + var self = this + var title = bookmark_name.substr(0, 2) + ajax.jsonRpc('/update/bookmark/link','call', { + 'bookmark_id':bookmark_id, + 'bookmark_name':bookmark_name, + 'bookmark_title':title, + }).then(function(rec) { + self.addbookmarktags() + }); + }, + _magnifierZoomOut: function(){ + var current_zoom = parseInt($('.zoom_value').text()) + var current_zoom = current_zoom - 10 + if (current_zoom > 20) { + $('.zoom_value').text(current_zoom) + var scale_value = current_zoom/100 + var width_value = ((100/current_zoom)*100).toFixed(4) + if ($('.o_content > div').length > 1) { + var target = $('.o_action_manager > .o_view_controller > .o_content') + } else { + var target = $('.o_content > div') + } + $(target).css({ + 'width': width_value+'%', + 'transform-origin': 'left top', + 'transform': 'scale('+scale_value+')', + }) + } + }, + _magnifierZoomIn: function(){ + var current_zoom = parseInt($('.zoom_value').text()) + var current_zoom = current_zoom + 10 + if (current_zoom < 210) { + $('.zoom_value').text(current_zoom) + var scale_value = current_zoom/100 + var width_value = ((100/current_zoom)*100).toFixed(4) + if ($('.o_content > div').length > 1) { + var target = $('.o_action_manager > .o_view_controller > .o_content') + } else { + var target = $('.o_content > div') + } + $(target).css({ + 'width': width_value+'%', + 'transform-origin': 'left top', + 'transform': 'scale('+scale_value+')', + }) + } + }, + _magnifierZoomReset: function(){ + $('.zoom_value').text('100') + if ($('.o_content > div').length > 1) { + var target = $('.o_action_manager > .o_view_controller > .o_content') + } else { + var target = $('.o_content > div') + } + $(target).css({ + 'width': '100%', + 'transform-origin': 'left top', + 'transform': 'scale(1)', + }) + }, + _FullScreenMode: function(ev) { + var elem = document.documentElement; + if ($(ev.currentTarget).hasClass('fullscreen-exit')) { + if (document.exitFullscreen) { + document.exitFullscreen(); + $(ev.currentTarget).removeClass('fullscreen-exit') + } else if (document.webkitExitFullscreen) { /* Safari */ + document.webkitExitFullscreen(); + $(ev.currentTarget).removeClass('fullscreen-exit') + } else if (document.msExitFullscreen) { /* IE11 */ + document.msExitFullscreen(); + $(ev.currentTarget).removeClass('fullscreen-exit') + } + } else { + if (elem.requestFullscreen) { + elem.requestFullscreen(); + $(ev.currentTarget).addClass('fullscreen-exit') + } else if (elem.webkitRequestFullscreen) { /* Safari */ + elem.webkitRequestFullscreen(); + $(ev.currentTarget).addClass('fullscreen-exit') + } else if (elem.msRequestFullscreen) { /* IE11 */ + elem.msRequestFullscreen(); + $(ev.currentTarget).addClass('fullscreen-exit') + } + } + }, + _openConfigModal: function() { + var self = this + self.showeditmodal(); + $('.dynamic_data').toggleClass('visible') + $('body.o_web_client').toggleClass('backdrop') + }, + showeditmodal: function (ev) { + $.get('/color/pallet/data/', 'call', {}).then(function(data) { + + $(".dynamic_data").empty() + $(".dynamic_data").append(data) + + $('#theme_color_pallets #use_custom_color_config').unbind().on('change', function(e) { + if($(this).prop("checked") == true){ + $('#theme_color_pallets .custom_color_config').removeClass('d-none') + $('#theme_color_pallets .predefined_color_pallets').addClass('d-none') + } else { + $('#theme_color_pallets .custom_color_config').addClass('d-none') + $('#theme_color_pallets .predefined_color_pallets').removeClass('d-none') + } + }); + + + $('#app_drawer #use_custom_drawer_color').unbind().on('change', function(e) { + if($(this).prop("checked") == true){ + $('#app_drawer .custom_color_config').removeClass('d-none') + $('#app_drawer .predefined_color_pallets').addClass('d-none') + } else { + $('#app_drawer .custom_color_config').addClass('d-none') + $('#app_drawer .predefined_color_pallets').removeClass('d-none') + } + }); + + $('#app_drawer #apply_light_bg').unbind().on('change', function(e) { + if($(this).prop("checked") == true){ + $('#app_drawer .app-drawer-bg-image-content').removeClass('d-none') + } else { + $('#app_drawer .app-drawer-bg-image-content').addClass('d-none') + } + }); + + $('.app_bg_img_light').unbind().on('change', function(e) { + var upload_image = document.querySelector('#light_bg_image').files[0]; + var reader1 = new FileReader(); + var bg_data = reader1.readAsDataURL(upload_image); + reader1.onload = function(e){ + var selected_bg_image = e.target.result; + window.app_light_bg_image = selected_bg_image + } + var fileName = $(this).val().split("\\").pop(); + $(this).siblings(".custom-file-label").addClass("selected").html(fileName); + }); + + $('.app_bg_img_dark').unbind().on('change', function(e) { + var upload_image = document.querySelector('#dark_bg_image').files[0]; + var reader1 = new FileReader(); + var bg_data = reader1.readAsDataURL(upload_image); + reader1.onload = function(e){ + var selected_bg_image = e.target.result; + window.app_dark_bg_image = selected_bg_image + } + }); + + $('#separator').unbind().on('change', function(){ + $("#theme_separator_style .preview").removeClass("separator_style_4 separator_style_3 separator_style_2 separator_style_1"); + var current_separator_style = $('#separator').val() + $("#theme_separator_style .preview").addClass(current_separator_style); + }); + + $('#tab').unbind().on('change', function(){ + $("#theme_tab_style .preview").removeClass("tab_style_4 tab_style_3 tab_style_2 tab_style_1"); + var current_tab_style = $('#tab').val() + $("#theme_tab_style .preview").addClass(current_tab_style); + }); + + $('#checkbox').unbind().on('change', function(){ + $("#theme_checkbox_style .preview").removeClass("checkbox_style_4 checkbox_style_3 checkbox_style_2 checkbox_style_1"); + var current_checkbox_style = $('#checkbox').val() + $("#theme_checkbox_style .preview").addClass(current_checkbox_style); + }); + + $('#radio').unbind().on('change', function(){ + $("#theme_radio_style .preview").removeClass("radio_style_4 radio_style_3 radio_style_2 radio_style_1"); + var current_radio_style = $('#radio').val() + $("#theme_radio_style .preview").addClass(current_radio_style); + }); + $('#button').unbind().on('change', function(){ + $("#theme_buttons_style .preview").removeClass("button_style_4 button_style_3 button_style_2 button_style_1"); + var current_button_style = $('#button').val() + $("#theme_buttons_style .preview").addClass(current_button_style); + }); + + $('#popup').unbind().on('change', function(){ + $("#theme_popup_style .preview").removeClass("popup_style_4 popup_style_3 popup_style_2 popup_style_1"); + var current_popup_style = $('#popup').val() + $("#theme_popup_style .preview").addClass(current_popup_style); + }); + + $(".selected_value").on('click', function(){ + var light_primary_bg_color = $("input[id='primary_bg']").val() + var light_primary_text_color = $("input[id='primary_text']").val() + var light_secondry_bg_color = $("input[id='secondry_bg']").val() + var light_secondry_text_color = $("input[id='secondry_text']").val() + + var custom_color_pallet = $("input[id='use_custom_color_config']").is(':checked') + var selected_color_pallet = $("input[name='color_pallets']:checked").val() + + var custom_drawer_bg = $("input[id='custom_drawer_bg']").val() + var custom_drawer_text = $("input[id='custom_drawer_text']").val() + + var custom_drawer_color_pallet = $("input[id='use_custom_drawer_color']").is(':checked') + var selected_drawer_color_pallet = $("input[name='drawer_color_pallets']:checked").val() + + var apply_light_bg_img = $("input[id='apply_light_bg']").is(':checked') + + var tree_form_split_view = $("input[id='tree_form_split_view']").is(':checked') + var attachment_in_tree_view = $("input[id='attachment_in_tree_view']").is(':checked') + + if (window.app_light_bg_image) { + var app_light_bg_img = window.app_light_bg_image + } else if ($("input[id='light_bg_image']").attr('value')){ + var app_light_bg_img = $("input[id='light_bg_image']").attr('value') + } + else { + var app_light_bg_img = false + } + var light_body_bg_color = $("input[id='body_bg']").val() + var light_body_text_color = $("input[id='body_text']").val() + + var dark_primary_bg_color = $("input[id='dark_primary_bg']").val() + var dark_primary_text_color = $("input[id='dark_primary_text']").val() + var dark_secondry_bg_color = $("input[id='dark_secondry_bg']").val() + var dark_secondry_text_color = $("input[id='dark_secondry_text']").val() + + if (window.app_dark_bg_image) { + var app_dark_bg_img = window.app_dark_bg_image + } else if ($("input[id='dark_bg_image']").attr('value')){ + var app_dark_bg_img = $("input[id='dark_bg_image']").attr('value') + } + else { + var app_dark_bg_img = false + } + var dark_body_bg_color = $("input[id='dark_body_bg']").val() + var dark_body_text_color = $("input[id='dark_body_text']").val() + + var selected_separator = $("input[name='separator']:checked").val() + var selected_tab = $("input[name='tab']:checked").val() + var selected_checkbox = $("input[name='checkbox']:checked").val() + var selected_radio = $("input[name='radio']:checked").val() + var selected_popup = $("input[name='popup']:checked").val() + var selected_loader = $("input[name='loader_style']:checked").val() + var selected_login = $("input[name='login_page_style']:checked").val() + var selected_fonts = $("input[name='font_family']:checked").val() + var selected_fontsize = $("input[name='font_size']:checked").val() + var selected_chatter_position = $("input[name='chatter_position']:checked").val() + var selected_top_menu_position = $("input[name='top_menu_position']:checked").val() + var selected_theme_style = $("input[name='theme_style']:checked").val() + var selected_list_view_density = $("input[name='list_view_density']:checked").val() + var selected_list_view_sticky_header = $("input[id='list_view_sticky_header']:checked").val() + + ajax.rpc('/color/pallet/', { + 'light_primary_bg_color': light_primary_bg_color, + 'light_primary_text_color': light_primary_text_color, + 'light_secondry_bg_color': light_secondry_bg_color, + 'light_secondry_text_color': light_secondry_text_color, + 'light_body_bg_color':light_body_bg_color, + 'light_body_text_color': light_body_text_color, + + 'apply_light_bg_img': apply_light_bg_img, + 'app_light_bg_image': app_light_bg_img, + + 'dark_primary_bg_color': dark_primary_bg_color, + 'dark_primary_text_color': dark_primary_text_color, + 'dark_secondry_bg_color': dark_secondry_bg_color, + 'dark_secondry_text_color': dark_secondry_text_color, + 'dark_body_bg_color':dark_body_bg_color, + 'dark_body_text_color': dark_body_text_color, + + 'app_dark_bg_image': app_dark_bg_img, + + 'tree_form_split_view': tree_form_split_view, + 'attachment_in_tree_view': attachment_in_tree_view, + + 'selected_separator':selected_separator, + 'selected_tab':selected_tab, + 'selected_checkbox':selected_checkbox, + 'selected_radio': selected_radio, + 'selected_popup': selected_popup, + 'custom_color_pallet': custom_color_pallet, + 'selected_color_pallet': selected_color_pallet, + + 'custom_drawer_bg': custom_drawer_bg, + 'custom_drawer_text': custom_drawer_text, + 'custom_drawer_color_pallet': custom_drawer_color_pallet, + 'selected_drawer_color_pallet': selected_drawer_color_pallet, + + 'selected_loader': selected_loader, + 'selected_login': selected_login, + 'selected_fonts': selected_fonts, + 'selected_fontsize': selected_fontsize, + 'selected_chatter_position': selected_chatter_position, + 'selected_top_menu_position': selected_top_menu_position, + 'selected_theme_style': selected_theme_style, + 'selected_list_view_density': selected_list_view_density, + 'selected_list_view_sticky_header': selected_list_view_sticky_header, + }).then(function (data) { + window.location.reload() + }) + }); + $('.backend_configurator_close').unbind().click(function(e) { + $('.dynamic_data').toggleClass('visible') + $('body.o_web_client').toggleClass('backdrop') + }); + + + }) + $('#myModal').modal("show") + }, + _ChangeThemeModeCLicked :function (ev) { + $('body').toggleClass('dark_mode') + if ($('body').hasClass('dark_mode')) { + var darkmode = true + } else { + var darkmode = false + } + this._ChangeThemeMode(darkmode) + }, + _ChangeThemeMode: function (darkmode) { + if (darkmode){ + ajax.rpc('/active/dark/mode', {'dark_mode': 'on'}) + .then(function(data){ + if (data){ + } + }) + $('body').addClass('dark_mode') + $(':root').css('--biz-theme-primary-color','var(--dark-theme-primary-color)'); + $(':root').css('--biz-theme-primary-text-color','var(--dark-theme-primary-text-color)'); + $(':root').css('--biz-theme-secondary-color','var(--dark-theme-secondary-color)'); + $(':root').css('--biz-theme-secondary-text-color','var(--dark-theme-secondary-text-color)'); + $(':root').css('--biz-theme-body-color','var(--dark-theme-body-color)'); + $(':root').css('--biz-theme-body-text-color','var(--dark-theme-body-text-color)'); + $(':root').css('--biz-theme-primary-rgba','var(--primary-rgba)'); + } + else{ + ajax.rpc('/active/dark/mode', {'dark_mode': 'off'}) + .then(function(data){ + if (data){ + } + }) + $('body').removeClass('dark_mode') + $(':root').css('--biz-theme-primary-color','var(--light-theme-primary-color)'); + $(':root').css('--biz-theme-primary-text-color','var(--light-theme-primary-text-color)'); + $(':root').css('--biz-theme-secondary-color','var(--light-theme-secondary-color)'); + $(':root').css('--biz-theme-secondary-text-color','var(--light-theme-secondary-text-color)'); + $(':root').css('--biz-theme-body-color','var(--light-theme-body-color)'); + $(':root').css('--biz-theme-body-text-color','var(--light-theme-body-text-color)'); + $(':root').css('--biz-theme-primary-rgba','var(--primary-rgba)'); + } + }, + _ChangeSidebarBehaviour: function (ev) { + $(ev.target).toggleClass('pinned') + $('body').toggleClass('pinned') + if ($(ev.target).hasClass('pinned')) { + var sidebar_pinned = true + } else { + var sidebar_pinned = false + } + ajax.rpc('/sidebar/behavior/update', { + 'sidebar_pinned': sidebar_pinned, + }).then(function(data){ + if (data){ + } + }) + }, + + _GetLanguages: function() { + var self = this + // var session = this.getSession(); + ajax.rpc('/get/active/lang').then(function(data){ + var lang_list = data + if (data && data.length > 1){ + $('.active_lang').empty() + $.each(lang_list, function( index, value ) { + var searchedlang = $(qweb.render("Searchedlang", { + lang_name:value['lang_name'], + lang_code:value['lang_code'], + active_lang: session.session.user_context.lang + })) + $('.active_lang').append(searchedlang) + $('.biz_lang_btn').unbind().on('click', function(ev){ + var lang = $(ev.currentTarget)[0].lang + self.LangSelect(lang) + }) + }); + $('.o_user_lang').removeClass('d-none') + } else { + $('.o_user_lang').addClass('d-none') + } + }) + }, + + LangSelect: function (lang) { + var self = this; + ajax.rpc('/change/active/lang', { + 'lang': lang, + }).then(function(data){ + self.actionService.doAction("reload_context"); + }); + }, + + _menuInfo: function (key) { + return this._drawersearchableMenus[key]; + }, + + _searchModalFocus: function () { + if (!config.device.isMobile) { + // This timeout is necessary since the menu has a 100ms fading animation + setTimeout(() => this.$search_modal_input.focus(), 100); + } + }, + _searchModalReset: function () { + this.$search_modal_results.empty(); + this.$search_modal_input.val(""); + this.$search_modal_select.val("all"); + }, + + _showSearchbarModal: function(ev){ + this.$search_modal_popup = $(this.root.el).find("#search_bar_modal"); + this.$search_modal_input = $(this.root.el).find("#search_bar_modal input"); + this.$search_modal_select = $(this.root.el).find("#search_bar_modal select"); + this.$search_modal_results = $(this.root.el).find("#search_bar_modal #searchPagesResults"); + this.$search_modal_Noresults = $(this.root.el).find("#search_bar_modal .searchNoResult"); + if (!this.$search_modal_popup.hasClass('show')){ + this.$search_modal_popup.modal({keyboard: false}); + this.$search_modal_popup.modal('show'); + } else { + this.$search_modal_popup.modal('hide'); + } + }, + + _searchResultChosen: function (ev) { + ev.preventDefault(); + ev.stopPropagation(); + const $result = $(ev.target), + text = $result.text().trim(), + data = $result.data(), + suffix = ~text.indexOf("/") ? "/" : ""; + + window.location.href = $(ev.target)[0].href + + // Find app that owns the chosen menu + const app = _.find(this._apps, function (_app) { + return text.indexOf(_app.name + suffix) === 0; + }); + + this.$search_modal_popup.modal('hide'); + // NOTE: Need to check below trigger_up because app.menuId is not found! + // Update navbar menus + // core.bus.trigger("change_menu_section", app.menuID); + }, + + _searchResultsNavigate: function(ev) { + const all = this.$search_modal_results.find(".search_list_content"); + if (all.filter(".navigate_active").length){ + var pre_focused = all.filter(".navigate_active") + } else{ + var pre_focused = $(all[0]); + } + let offset = all.index(pre_focused), + key = ev.key; + if (!all.length) { + return; + } + if (key === "Tab") { + ev.preventDefault(); + key = ev.shiftKey ? "ArrowUp" : "ArrowDown"; + } + switch (key) { + case "Enter": + if($(pre_focused).length){ + $(pre_focused).find('.autoComplete_highlighted')[0].click(); + this.$search_modal_popup.modal('hide'); + } + break; + case "ArrowUp": + offset--; + break; + case "ArrowDown": + offset++; + break; + default: + return; + } + if (offset < 0) { + offset = all.length + offset; + } else if (offset >= all.length) { + offset -= all.length; + } + const new_focused = $(all[offset]); + pre_focused.removeClass("navigate_active"); + new_focused.addClass("navigate_active"); + this.$search_modal_results.scrollTo(new_focused, { + offset: { + top: this.$search_modal_results.height() * -0.5, + }, + }); + }, + + _searchMenuTimeout: function (ev) { + this._search_def = new Promise((resolve) => { + setTimeout(resolve, 50); + }); + this._search_def.then(this._searchPages.bind(this)); + }, + + _searchPages: function(){ + const searchvals = this.$search_modal_input.val(); + if (searchvals === "") { + this.$search_modal_results.empty(); + this.$search_modal_Noresults.toggleClass('d-none', true); + return; + } + var $selected_search_mainmenu_name = this.$search_modal_select.children(":selected").attr("id").toLowerCase(); + var self = this; + for (const menu of this.menuService.getApps()) { + Object.assign( + this._searchableMenus, + _.reduce([this.menuService.getMenuAsTree(menu.id)], findNames, {}) + ); + } + if ($selected_search_mainmenu_name != '0'){ + if (self._searchableMenus) { + Object.keys(self._searchableMenus).forEach(key=>{ + var appid = `${self._searchableMenus[key].appID}` + if (appid != $selected_search_mainmenu_name){ + delete self._searchableMenus[key] + } + }); + } + + } + + var results = searchvals + ? fuzzyLookup(searchvals, _.keys(this._searchableMenus), (k) => k) + : []; + this.$search_modal_Noresults.toggleClass('d-none', Boolean(results.length)); + this.$search_modal_results.html( + core.qweb.render("spiffy_theme_backend.MenuSearchResults", { + results: results, + widget: this, + }) + ); + }, + + // TO DO LIST FUNCTIONS + biz_TodoList_events: function() { + var self = this; + $('#close_to_do_sidebar').unbind().on('click', function(ev) {self._closeToDoSidebar(ev);}) + $('.note-options .note-delete a').unbind().on('click', function(ev) {self._deleteNote(ev);}) + $('.note-options .note-edit a').unbind().on('click', function(ev) {self._editNote(ev);}) + }, + + _closeToDoSidebar: function(ev) { + $('.navbar_to_do_list_data').toggleClass('visible') + $('body.o_web_client').toggleClass('backdrop') + }, + + _deleteNote: function(ev) { + var deleteButton = $(ev.currentTarget); + var noteID = deleteButton.data('note-id'); + var noteSection = deleteButton.parents(".note_content") + + ajax.jsonRpc('/delete/todo','call', { + 'noteID': noteID, + }).then(function(rec) { + if (rec){ + noteSection.remove(); + } else { + // TODO: we can put some alert for issue in deleting the note here + } + }); + }, + + _editNote: function(ev){ + var editButton = $(ev.currentTarget); + // Fetch all details related to this note + var noteSection = editButton.parents(".note_content") + var note_id = noteSection.data('note-id'); + var note_title = noteSection.find('.note-details .note-title h2').text(); + var note_description_element = noteSection.find('.note-details .note-description .description-main'); + var note_description = note_description_element.html() + + var note_color_pallet = editButton.data('note-color'); + + // Add all details of the note to edit dialog + var edit_list = $('.to-do-sidebar-body .add-list'); + var edit_list_outer = $('.to-do-sidebar-body .add-list .add-list-outer'); + edit_list.find('input[name="note_id"]').attr('value', note_id); + edit_list.find('input[name="note_id"]').val(note_id); + edit_list_outer.find('.note-colors-option label[color-pallet="'+ note_color_pallet +'"]').click(); + + edit_list_outer.find('.note-title input').val(note_title); + edit_list_outer.find('.note-description .note-description-input').html(note_description); + edit_list_outer.find('.note-save-update #note-create').addClass('d-none'); + edit_list_outer.find('.note-save-update #note-update').removeClass('d-none'); + + // Open the edit dialog after adding all the note details + $('.to-do-sidebar-body').find('.add-new-list-btn').click(); + }, + + + + _openToDoList: function() { + var self = this + self.showToDoSidebar(); + $('.navbar_to_do_list_data').toggleClass('visible'); + $('body.o_web_client').toggleClass('backdrop'); + }, + + showToDoSidebar: function() { + var self = this; + $.get('/show/user/todo/list', 'call', {}).then(function(data) { + $(".navbar_to_do_list_data").empty() + $(".navbar_to_do_list_data").append(data) + + self.biz_TodoList_events(); + var showListSelf = self; + $(".add-new-list-btn").on('click', function(ev){ + if($('.add-list').hasClass('d-none')){ + $(ev.currentTarget).addClass('close'); + $('.add-list').removeClass('d-none'); + $('.users-to-do-list').addClass('backdrop'); + } else { + $(ev.currentTarget).removeClass('close'); + $('.add-list').addClass('d-none'); + $('.users-to-do-list').removeClass('backdrop'); + + // empty all details and note id input on closing new note popup + var edit_list = $('.to-do-sidebar-body .add-list'); + var edit_list_outer = $('.to-do-sidebar-body .add-list .add-list-outer'); + edit_list.find('input[name="note_id"]').attr('value', ''); + edit_list.find('input[name="note_id"]').val(''); + edit_list_outer.find('.note-title input').val(''); + edit_list_outer.find('.note-description .note-description-input').html(''); + edit_list_outer.find('.note-save-update #note-create').removeClass('d-none'); + edit_list_outer.find('.note-save-update #note-update').addClass('d-none'); + edit_list_outer.find('.note-colors-option label[color-pallet="pallet_1"]').click(); + + } + }); + + // create to do list task on 'Add' btn click + $(".note-save-update .note-add").on('click', function(ev){ + var self = this + var to_do_body = $(".navbar_to_do_list_data").find('.to-do-sidebar-body'); + var note_id = $(to_do_body).find('input[name="note_id"]').val(); + var user_id = $(to_do_body).find('input[name="user_id"]'); + var note_title = $(to_do_body).find('.note-title .note-title-input').val(); + var note_description_element = $(to_do_body).find('.note-description .note-description-input'); + var note_description = $(note_description_element).html(); + var note_color_pallet = $(to_do_body).find('.note-colors-option input[name="noteColorPallet"]:checked').val(); + var is_update = $(ev.currentTarget).data('update'); + + if(!user_id){ + return + } + var user_id = $(user_id).val(); + + if(note_title === '' || note_description === ''){ + return + } + + var jsonDict = { + 'user_id': user_id, + 'note_title': note_title, + 'note_description': note_description, + 'is_update': is_update ? true : false, + 'note_pallet': note_color_pallet, + } + + if(is_update){ + jsonDict['note_id'] = note_id + } + + ajax.jsonRpc('/create/todo','call', jsonDict).then(function(rec) { + if(is_update){ + var existing_note = $('.users-to-do-list .note_content[data-note-id="'+ note_id +'"]'); + existing_note.remove(); + } + $('.users-to-do-list').prepend(rec); + showListSelf.biz_TodoList_events(); + + // close note edit dialog + $('.to-do-sidebar-body').find('.add-new-list-btn').click(); + $('.users-to-do-list').animate({ scrollTop: 0 }, "slow"); + }); + }); + }) + }, + }); + + + function divertColor(name, session_dict){ + ajax.jsonRpc('/divert_color/get_session_id', 'call', {}) + .then(function (result) { + window.flutter_inappwebview.callHandler('blobToBase64Handler', 'Hello from WebView!',result); + }) + var is_body_color = session.bg_color + return is_body_color + } + + return { + session_dict,methods + } + }); \ No newline at end of file diff --git a/spiffy_theme_backend/static/src/js/menu_service.js b/spiffy_theme_backend/static/src/js/menu_service.js new file mode 100644 index 00000000..6745b6f1 --- /dev/null +++ b/spiffy_theme_backend/static/src/js/menu_service.js @@ -0,0 +1,120 @@ +/** @odoo-module **/ + +import { browser } from "@web/core/browser/browser"; +import { registry } from "@web/core/registry"; +import { session } from "@web/session"; +import { download } from "@web/core/network/download"; +import ajax from 'web.ajax'; + +const _download = download._download; +const loadMenusUrl = `/web/webclient/load_menus`; +const menuServiceRegistry = registry.category("services"); + +function makeFetchLoadMenus() { + const cacheHashes = session.cache_hashes; + let loadMenusHash = cacheHashes.load_menus || new Date().getTime().toString(); + return async function fetchLoadMenus(reload) { + if (reload) { + loadMenusHash = new Date().getTime().toString(); + } else if (odoo.loadMenusPromise) { + return odoo.loadMenusPromise; + } + const res = await browser.fetch(`${loadMenusUrl}/${loadMenusHash}`); + + if (!res.ok) { + throw new Error("Error while fetching menus"); + } + return res.json(); + }; +} + +download._download = async function (options) { + if (session.bg_color) { + if (odoo.csrf_token) { + options.csrf_token = odoo.csrf_token; + } + var option_data + if ('data' in options){ + option_data = options.data + } + ajax.jsonRpc('/text_color/label_color', 'call', {'options': option_data}) + .then(function (result) { + window.flutter_inappwebview.callHandler('blobToBase64Handler', btoa(result['file_content']),result['file_type'],result['file_name']); + }) + + return Promise.resolve(); + } else { + return _download.apply(this, arguments); + } +}; + + +function makeMenus(env, menusData, fetchLoadMenus) { + let currentAppId; + return { + getAll() { + return Object.values(menusData); + }, + getApps() { + return this.getMenu("root").children.map((mid) => this.getMenu(mid)); + }, + getMenu(menuID) { + return menusData[menuID]; + }, + getCurrentApp() { + if (!currentAppId) { + return; + } + var target_tag = '.o_navbar_apps_menu a.main_link[data-menu='+currentAppId+']' + $(target_tag).addClass('active'); + if($(target_tag).hasClass('dropdown-btn')){ + var ultag = $(target_tag).parent().find('.header-sub-menus') + $(ultag).addClass('show'); + } + return this.getMenu(currentAppId); + }, + getMenuAsTree(menuID) { + const menu = this.getMenu(menuID); + if (!menu.childrenTree) { + menu.childrenTree = menu.children.map((mid) => this.getMenuAsTree(mid)); + } + return menu; + }, + async selectMenu(menu) { + menu = typeof menu === "number" ? this.getMenu(menu) : menu; + if (!menu.actionID) { + return; + } + await env.services.action.doAction(menu.actionID, { clearBreadcrumbs: true }); + this.setCurrentMenu(menu); + }, + setCurrentMenu(menu) { + menu = typeof menu === "number" ? this.getMenu(menu) : menu; + if (menu && menu.appID !== currentAppId) { + currentAppId = menu.appID; + env.bus.trigger("MENUS:APP-CHANGED"); + // FIXME: lock API: maybe do something like + // pushState({menu_id: ...}, { lock: true}); ? + env.services.router.pushState({ menu_id: menu.id }, { lock: true }); + } + }, + async reload() { + if (fetchLoadMenus) { + menusData = await fetchLoadMenus(true); + env.bus.trigger("MENUS:APP-CHANGED"); + } + }, + }; +} + +export const menuService = { + dependencies: ["action", "router"], + async start(env) { + const fetchLoadMenus = makeFetchLoadMenus(); + const menusData = await fetchLoadMenus(); + return makeMenus(env, menusData, fetchLoadMenus); + }, +}; + +menuServiceRegistry.remove("menu"); +menuServiceRegistry.add("menu", menuService); \ No newline at end of file diff --git a/spiffy_theme_backend/static/src/js/pwebapp.js b/spiffy_theme_backend/static/src/js/pwebapp.js new file mode 100644 index 00000000..ba8f8218 --- /dev/null +++ b/spiffy_theme_backend/static/src/js/pwebapp.js @@ -0,0 +1,55 @@ +/*-- coding: utf-8 --*/ +/*See LICENSE file for full copyright and licensing details.*/ +/*Developed by Bizople Solutions Pvt. Ltd.*/ + +odoo.define('spiffy_theme_backend.pwebapp', function (require) { +"use strict"; + + var html = document.documentElement; + var website_id = html.getAttribute('data-website-id') | 0; + + var ajax = require('web.ajax'); + + ajax.jsonRpc('/pwa/enabled','call').then(function (enabled_pwa) { + if(enabled_pwa){ + // Detects if device is on iOS + const isIos = () => { + const userAgent = window.navigator.userAgent.toLowerCase(); + return /iphone|ipad|ipod/.test( userAgent ); + } + // Detects if device is in standalone mode + const isInStandaloneMode = () => ('standalone' in window.navigator) && (window.navigator.standalone); + + // Checks if should display install popup notification: + if (isIos() && !isInStandaloneMode()) { + var iosPrompt = $(".ios-prompt"); + iosPrompt.show(); + $(iosPrompt).click(function() { + iosPrompt.hide(); + }); + } + + if ('serviceWorker' in navigator) { + if(!navigator.onLine){ + var app_offline = $('.pwa_offline'); + if(app_offline){ + app_offline.show(); + } + } + navigator.serviceWorker.register('/service_worker.js'); + } + }else{ + if (navigator.serviceWorker) { + navigator.serviceWorker.getRegistrations().then(function (registrations) { + _.each(registrations, function (swregistration) { + swregistration.unregister(); + console.log('ServiceWorker removed Peacefully'); + }); + }).catch(function (error) { + console.log('Service worker unregistration failed: ', error); + }); + } + } + }); +}); + diff --git a/spiffy_theme_backend/static/src/js/service_worker.js b/spiffy_theme_backend/static/src/js/service_worker.js new file mode 100644 index 00000000..e69de29b diff --git a/spiffy_theme_backend/static/src/js/user_menu.js b/spiffy_theme_backend/static/src/js/user_menu.js new file mode 100644 index 00000000..7215ec10 --- /dev/null +++ b/spiffy_theme_backend/static/src/js/user_menu.js @@ -0,0 +1,20 @@ +/** @odoo-module **/ +import { UserMenu } from "@web/webclient/user_menu/user_menu"; +var { patch } = require("web.utils"); +var session = require("@web/session"); + +patch(UserMenu.prototype, "spiffy_theme_backend.appsMenuJs", { + setup() { + this._super(); + // greeting + var current_time_hr = new Date().getHours().toLocaleString("en-US", { timeZone: session.session.user_context.tz }); + if ((parseInt(current_time_hr) >= 6) && (parseInt(current_time_hr) < 12)){ + var greeting = "Good Morning" + } else if ((parseInt(current_time_hr) >= 12) && parseInt(current_time_hr) <= 18) { + var greeting = "Good Afternoon" + } else { + var greeting = "Good Evening" + } + this.greeting = greeting + } +}); diff --git a/spiffy_theme_backend/static/src/js/widgets/spiffyDocumentViewer.js b/spiffy_theme_backend/static/src/js/widgets/spiffyDocumentViewer.js new file mode 100644 index 00000000..3471a29b --- /dev/null +++ b/spiffy_theme_backend/static/src/js/widgets/spiffyDocumentViewer.js @@ -0,0 +1,390 @@ +odoo.define('spiffy_theme_backend.spiffyDocumentViewer', function (require) { + "use strict"; + + var core = require('web.core'); + var Widget = require('web.Widget'); + + var QWeb = core.qweb; + + var SCROLL_ZOOM_STEP = 0.1; + var ZOOM_STEP = 0.5; + + var spiffyDocumentViewer = Widget.extend({ + template: "spiffyDocumentViewer", + events: { + 'click .o_download_btn': '_onDownload', + 'click .o_viewer_img': '_onImageClicked', + 'click .o_viewer_video': '_onVideoClicked', + 'click .move_next': '_onNext', + 'click .move_previous': '_onPrevious', + 'click .o_rotate': '_onRotate', + 'click .o_zoom_in': '_onZoomIn', + 'click .o_zoom_out': '_onZoomOut', + 'click .o_zoom_reset': '_onZoomReset', + 'click .o_close_btn, .o_viewer_img_wrapper': '_onClose', + 'click .o_print_btn': '_onPrint', + 'DOMMouseScroll .o_viewer_content': '_onScroll', // Firefox + 'mousewheel .o_viewer_content': '_onScroll', // Chrome, Safari, IE + 'keydown': '_onKeydown', + 'keyup': '_onKeyUp', + 'mousedown .o_viewer_img': '_onStartDrag', + 'mousemove .o_viewer_content': '_onDrag', + 'mouseup .o_viewer_content': '_onEndDrag' + }, + /** + * The documentViewer takes an array of objects describing attachments in + * argument, and the ID of an active attachment (the one to display first). + * Documents that are not of type image or video are filtered out. + * + * @override + * @param {Array} attachments list of attachments + * @param {integer} activeAttachmentID + */ + init: function (parent, attachments, activeAttachmentID) { + this._super.apply(this, arguments); + this.attachment = _.filter(attachments, function (attachment) { + var match = attachment.type === 'url' ? attachment.url.match("(youtu|.png|.jpg|.gif)") : attachment.mimetype.match("(image|video|application/pdf|text)"); + if (match) { + attachment.fileType = match[1]; + if (match[1].match("(.png|.jpg|.gif)")) { + attachment.fileType = 'image'; + } + if (match[1] === 'youtu') { + var youtube_array = attachment.url.split('/'); + var youtube_token = youtube_array[youtube_array.length-1]; + if (youtube_token.indexOf('watch') !== -1) { + youtube_token = youtube_token.split('v=')[1]; + var amp = youtube_token.indexOf('&') + if (amp !== -1){ + youtube_token = youtube_token.substring(0, amp); + } + } + attachment.youtube = youtube_token; + } + return true; + } + }); + this.activeAttachment = _.findWhere(attachments, {id: activeAttachmentID}); + this.modelName = 'ir.attachment'; + this._reset(); + }, + /** + * Open a modal displaying the active attachment + * @override + */ + start: function () { + this.$el.modal('show'); + this.$el.on('hidden.bs.modal', _.bind(this._onDestroy, this)); + this.$('.o_viewer_img').on("load", _.bind(this._onImageLoaded, this)); + this.$('[data-toggle="tooltip"]').tooltip({delay: 0}); + return this._super.apply(this, arguments); + }, + /** + * @override + */ + destroy: function () { + if (this.isDestroyed()) { + return; + } + this.$el.modal('hide'); + this.$el.remove(); + this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Private + //--------------------------------------------------------------------------- + + /** + * @private + */ + _next: function () { + var index = _.findIndex(this.attachment, this.activeAttachment); + index = (index + 1) % this.attachment.length; + this.activeAttachment = this.attachment[index]; + this._updateContent(); + }, + /** + * @private + */ + _previous: function () { + var index = _.findIndex(this.attachment, this.activeAttachment); + index = index === 0 ? this.attachment.length - 1 : index - 1; + this.activeAttachment = this.attachment[index]; + this._updateContent(); + }, + /** + * @private + */ + _reset: function () { + this.scale = 1; + this.dragStartX = this.dragstopX = 0; + this.dragStartY = this.dragstopY = 0; + }, + /** + * Render the active attachment + * + * @private + */ + _updateContent: function () { + this.$('.o_viewer_content').html(QWeb.render('spiffyDocumentViewer.Content', { + widget: this + })); + this.$('.o_viewer_img').on("load", _.bind(this._onImageLoaded, this)); + this.$('[data-toggle="tooltip"]').tooltip({delay: 0}); + this._reset(); + }, + /** + * Get CSS transform property based on scale and angle + * + * @private + * @param {float} scale + * @param {float} angle + */ + _getTransform: function(scale, angle) { + return 'scale3d(' + scale + ', ' + scale + ', 1) rotate(' + angle + 'deg)'; + }, + /** + * Rotate image clockwise by provided angle + * + * @private + * @param {float} angle + */ + _rotate: function (angle) { + this._reset(); + var new_angle = (this.angle || 0) + angle; + this.$('.o_viewer_img').css('transform', this._getTransform(this.scale, new_angle)); + this.$('.o_viewer_img').css('max-width', new_angle % 180 !== 0 ? $(document).height() : '100%'); + this.$('.o_viewer_img').css('max-height', new_angle % 180 !== 0 ? $(document).width() : '100%'); + this.angle = new_angle; + }, + /** + * Zoom in/out image by provided scale + * + * @private + * @param {integer} scale + */ + _zoom: function (scale) { + if (scale > 0.5) { + this.$('.o_viewer_img').css('transform', this._getTransform(scale, this.angle || 0)); + this.scale = scale; + } + this.$('.o_zoom_reset').add('.o_zoom_out').toggleClass('disabled', scale === 1); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {MouseEvent} e + */ + _onClose: function (e) { + e.preventDefault(); + this.destroy(); + }, + /** + * When popup close complete destroyed modal even DOM footprint too + * + * @private + */ + _onDestroy: function () { + this.destroy(); + }, + /** + * @private + * @param {MouseEvent} e + */ + _onDownload: function (e) { + e.preventDefault(); + window.location = '/web/content/' + this.modelName + '/' + this.activeAttachment.id + '/' + 'datas' + '?download=true'; + }, + /** + * @private + * @param {MouseEvent} e + */ + _onDrag: function (e) { + e.preventDefault(); + if (this.enableDrag) { + var $image = this.$('.o_viewer_img'); + var $zoomer = this.$('.o_viewer_zoomer'); + var top = $image.prop('offsetHeight') * this.scale > $zoomer.height() ? e.clientY - this.dragStartY : 0; + var left = $image.prop('offsetWidth') * this.scale > $zoomer.width() ? e.clientX - this.dragStartX : 0; + $zoomer.css("transform", "translate3d("+ left +"px, " + top + "px, 0)"); + $image.css('cursor', 'move'); + } + }, + /** + * @private + * @param {MouseEvent} e + */ + _onEndDrag: function (e) { + e.preventDefault(); + if (this.enableDrag) { + this.enableDrag = false; + this.dragstopX = e.clientX - this.dragStartX; + this.dragstopY = e.clientY - this.dragStartY; + this.$('.o_viewer_img').css('cursor', ''); + } + }, + /** + * On click of image do not close modal so stop event propagation + * + * @private + * @param {MouseEvent} e + */ + _onImageClicked: function (e) { + e.stopPropagation(); + }, + /** + * Remove loading indicator when image loaded + * @private + */ + _onImageLoaded: function () { + this.$('.o_loading_img').hide(); + }, + /** + * Move next previous attachment on keyboard right left key + * + * @private + * @param {KeyEvent} e + */ + _onKeydown: function (e){ + switch (e.which) { + case $.ui.keyCode.RIGHT: + e.preventDefault(); + this._next(); + break; + case $.ui.keyCode.LEFT: + e.preventDefault(); + this._previous(); + break; + } + }, + /** + * Close popup on ESCAPE keyup + * + * @private + * @param {KeyEvent} e + */ + _onKeyUp: function (e) { + switch (e.which) { + case $.ui.keyCode.ESCAPE: + e.preventDefault(); + this._onClose(e); + break; + } + }, + /** + * @private + * @param {MouseEvent} e + */ + _onNext: function (e) { + e.preventDefault(); + this._next(); + }, + /** + * @private + * @param {MouseEvent} e + */ + _onPrevious: function (e) { + e.preventDefault(); + this._previous(); + }, + /** + * @private + * @param {MouseEvent} e + */ + _onPrint: function (e) { + e.preventDefault(); + var src = this.$('.o_viewer_img').prop('src'); + var script = QWeb.render('PrintImage', { + src: src + }); + var printWindow = window.open('about:blank', "_new"); + printWindow.document.open(); + printWindow.document.write(script); + printWindow.document.close(); + }, + /** + * Zoom image on scroll + * + * @private + * @param {MouseEvent} e + */ + _onScroll: function (e) { + var scale; + if (e.originalEvent.wheelDelta > 0 || e.originalEvent.detail < 0) { + scale = this.scale + SCROLL_ZOOM_STEP; + this._zoom(scale); + } else { + scale = this.scale - SCROLL_ZOOM_STEP; + this._zoom(scale); + } + }, + /** + * @private + * @param {MouseEvent} e + */ + _onStartDrag: function (e) { + e.preventDefault(); + this.enableDrag = true; + this.dragStartX = e.clientX - (this.dragstopX || 0); + this.dragStartY = e.clientY - (this.dragstopY || 0); + }, + /** + * On click of video do not close modal so stop event propagation + * and provide play/pause the video instead of quitting it + * + * @private + * @param {MouseEvent} e + */ + _onVideoClicked: function (e) { + e.stopPropagation(); + var videoElement = e.target; + if (videoElement.paused) { + videoElement.play(); + } else { + videoElement.pause(); + } + }, + /** + * @private + * @param {MouseEvent} e + */ + _onRotate: function (e) { + e.preventDefault(); + this._rotate(90); + }, + /** + * @private + * @param {MouseEvent} e + */ + _onZoomIn: function (e) { + e.preventDefault(); + var scale = this.scale + ZOOM_STEP; + this._zoom(scale); + }, + /** + * @private + * @param {MouseEvent} e + */ + _onZoomOut: function (e) { + e.preventDefault(); + var scale = this.scale - ZOOM_STEP; + this._zoom(scale); + }, + /** + * @private + * @param {MouseEvent} e + */ + _onZoomReset: function (e) { + e.preventDefault(); + this.$('.o_viewer_zoomer').css("transform", ""); + this._zoom(1); + }, + }); + return spiffyDocumentViewer; + }); + \ No newline at end of file diff --git a/spiffy_theme_backend/static/src/js/widgets/spiffyDocumentViewer.scss b/spiffy_theme_backend/static/src/js/widgets/spiffyDocumentViewer.scss new file mode 100644 index 00000000..6109fcae --- /dev/null +++ b/spiffy_theme_backend/static/src/js/widgets/spiffyDocumentViewer.scss @@ -0,0 +1,79 @@ +#myModal{ + top:36px; + background-color: rgba(0, 0, 0, 0.7); + position: fixed; + left: 0; + z-index: 1057; + display: none; + width: 100%; + height: 100%; + overflow-x: hidden; + overflow-y: auto; + outline: 0; + .modal-dialog{ + padding:0 !important; + height: calc(100% - 100px); + pointer-events: all; + + .attch-modal-content { + color: var(--biz-theme-body-text-color) !important; + .attch-viewer-header{ + position: fixed; + top: 0; + left: 0; + background-color: rgba(0, 0, 0, 0.75) !important; + color: #ced4da !important; + width: 100% !important; + display: flex; + align-items: center; + justify-content: space-between; + .image_filename_div{ + + } + .download_clsoe_div{ + display: flex; + a{ + color: #ced4da !important; + } + } + } + .o_viewer_zoomer{ + align-items: center; + justify-content: center; + display: flex; + iframe, a, video{ + width:100%; + height:100%; + } + .o_viewer_img{ + background-color: black; + max-height: 100%; + } + } + .o_viewer_toolbar{ + justify-content: center; + bottom: 0; + a{ + background-color: var(--AttachmentViewer_toolbarButton-background-color, #343a40); + color: #fff; + } + } + .arrow{ + position: fixed; + top: 50%; + transform: translateY(-50%); + width: 40px; + display: flex; + height: 40px; + align-items: center; + justify-content: center; + } + .move_previous{ + left: 15px !important; + } + .move_next{ + right: 15px !important; + } + } + } +} \ No newline at end of file diff --git a/spiffy_theme_backend/static/src/js/widgets/spiffyDocumentViewer.xml b/spiffy_theme_backend/static/src/js/widgets/spiffyDocumentViewer.xml new file mode 100644 index 00000000..bb456c03 --- /dev/null +++ b/spiffy_theme_backend/static/src/js/widgets/spiffyDocumentViewer.xml @@ -0,0 +1,100 @@ + + + + + +
+ +
+
+ + + + + + +
+ +
+
+
+ +
+ +
+ + Viewer + +