initial commit
This commit is contained in:
7
odoo/addons/project_html_rtl/__init__.py
Normal file
7
odoo/addons/project_html_rtl/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from . import hooks
|
||||
from . import models
|
||||
|
||||
|
||||
def post_init(cr, registry):
|
||||
"""Post install hook proxy for older Odoo hooks invocation style."""
|
||||
hooks.post_init(cr, registry)
|
||||
18
odoo/addons/project_html_rtl/__manifest__.py
Normal file
18
odoo/addons/project_html_rtl/__manifest__.py
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "Project HTML editor + RTL",
|
||||
"version": "1.0.0",
|
||||
"summary": "Enable HTML editor for project task descriptions and notes, and force RTL when needed",
|
||||
"description": "This small addon configures project task description fields to use the HTML editor and injects a small JS to enable RTL rendering in the editor on project task forms.",
|
||||
"category": "Project",
|
||||
"author": "Automated",
|
||||
"depends": ["project", "html_editor"],
|
||||
"license": "LGPL-3",
|
||||
"data": [
|
||||
"views/project_task_view.xml",
|
||||
],
|
||||
"installable": True,
|
||||
"application": False,
|
||||
"assets": {
|
||||
"web.assets_backend": ["project_html_rtl/static/src/js/html_field_patch.js"],
|
||||
},
|
||||
}
|
||||
Binary file not shown.
BIN
odoo/addons/project_html_rtl/__pycache__/hooks.cpython-312.pyc
Normal file
BIN
odoo/addons/project_html_rtl/__pycache__/hooks.cpython-312.pyc
Normal file
Binary file not shown.
5
odoo/addons/project_html_rtl/hooks.py
Normal file
5
odoo/addons/project_html_rtl/hooks.py
Normal file
@@ -0,0 +1,5 @@
|
||||
def post_init(cr, registry):
|
||||
# Lightweight post-init: nothing heavy here. This placeholder can be
|
||||
# extended later if database changes are desired at install time.
|
||||
# Keep this simple to avoid unexpected side-effects.
|
||||
print("project_html_rtl: post_init hook called")
|
||||
1
odoo/addons/project_html_rtl/models/__init__.py
Normal file
1
odoo/addons/project_html_rtl/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import project_task
|
||||
Binary file not shown.
Binary file not shown.
11
odoo/addons/project_html_rtl/models/project_task.py
Normal file
11
odoo/addons/project_html_rtl/models/project_task.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class ProjectTask(models.Model):
|
||||
_inherit = "project.task"
|
||||
|
||||
rtl_enable = fields.Boolean(
|
||||
string="Force RTL in HTML editor",
|
||||
help="When set, the HTML editor on this task will render in right-to-left direction",
|
||||
default=False,
|
||||
)
|
||||
178
odoo/addons/project_html_rtl/static/src/js/editor_rtl.js.bak
Normal file
178
odoo/addons/project_html_rtl/static/src/js/editor_rtl.js.bak
Normal file
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* Small script to force RTL direction on HTML editors inside project task forms.
|
||||
* It finds HTML editor fields added by our view extension and sets dir="rtl" on
|
||||
* the editable area so the editor displays in right-to-left mode.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
function setEditorRTL() {
|
||||
// Target editable areas that we marked via the view (data-project-html="1")
|
||||
var elems = document.querySelectorAll('[data-project-html="1"]');
|
||||
elems.forEach(function (el) {
|
||||
var editable = el.querySelector('[contenteditable]');
|
||||
var shouldRTL = false;
|
||||
// Try to detect a nearby boolean field named rtl_enable in the same form
|
||||
var form = el.closest('form');
|
||||
if (form) {
|
||||
// Look for a checkbox input (classic boolean) by name
|
||||
var cb = form.querySelector('input[name="rtl_enable"]');
|
||||
if (cb) {
|
||||
shouldRTL = cb.checked;
|
||||
} else {
|
||||
// Fallback: look for any element with data-field="rtl_enable"
|
||||
var df = form.querySelector('[data-field="rtl_enable"]');
|
||||
if (df) {
|
||||
// Try to find checkbox inside
|
||||
var inner = df.querySelector('input[type="checkbox"]');
|
||||
if (inner) shouldRTL = inner.checked;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (editable) {
|
||||
if (shouldRTL) {
|
||||
editable.setAttribute('dir', 'rtl');
|
||||
editable.style.direction = 'rtl';
|
||||
} else {
|
||||
editable.removeAttribute('dir');
|
||||
editable.style.direction = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Run after DOM ready and also on Odoo bus events when views are re-rendered.
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
setEditorRTL();
|
||||
// start observer after body exists
|
||||
startObserver();
|
||||
});
|
||||
} else {
|
||||
setEditorRTL();
|
||||
startObserver();
|
||||
}
|
||||
|
||||
function startObserver() {
|
||||
// Odoo may dynamically rerender; observe mutations under the document body
|
||||
// and re-apply the RTL attribute when new nodes appear.
|
||||
try {
|
||||
if (!document.body) return;
|
||||
var observer = new MutationObserver(function () {
|
||||
setEditorRTL();
|
||||
});
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
} catch (e) {
|
||||
// Fail silently — we don't want to break the client if MutationObserver isn't available
|
||||
console.warn('project_html_rtl: could not start MutationObserver', e && e.message);
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
||||
/**
|
||||
* Small script to force RTL direction on HTML editors inside project task forms.
|
||||
* It finds HTML editor fields added by our view extension and sets dir="rtl" on
|
||||
* the editable area so the editor displays in right-to-left mode.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
function setEditorRTL() {
|
||||
// Target editable areas that we marked via the view (data-project-html="1")
|
||||
var elems = document.querySelectorAll('[data-project-html="1"]');
|
||||
elems.forEach(function (el) {
|
||||
var editable = el.querySelector('[contenteditable]');
|
||||
var shouldRTL = false;
|
||||
// Try to detect a nearby boolean field named rtl_enable in the same form
|
||||
var form = el.closest('form');
|
||||
if (form) {
|
||||
// Look for a checkbox input (classic boolean) by name
|
||||
var cb = form.querySelector('input[name="rtl_enable"]');
|
||||
if (cb) {
|
||||
shouldRTL = cb.checked;
|
||||
} else {
|
||||
// Fallback: look for any element with data-field="rtl_enable"
|
||||
var df = form.querySelector('[data-field="rtl_enable"]');
|
||||
if (df) {
|
||||
// Try to find checkbox inside
|
||||
var inner = df.querySelector('input[type="checkbox"]');
|
||||
if (inner) shouldRTL = inner.checked;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (editable) {
|
||||
if (shouldRTL) {
|
||||
editable.setAttribute('dir', 'rtl');
|
||||
editable.style.direction = 'rtl';
|
||||
/**
|
||||
* Small script to force RTL direction on HTML editors inside project task forms.
|
||||
* It finds HTML editor fields added by our view extension and sets dir="rtl" on
|
||||
* the editable area so the editor displays in right-to-left mode.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
function setEditorRTL() {
|
||||
// Target editable areas that we marked via the view (data-project-html="1")
|
||||
var elems = document.querySelectorAll('[data-project-html="1"]');
|
||||
elems.forEach(function (el) {
|
||||
var editable = el.querySelector('[contenteditable]');
|
||||
var shouldRTL = false;
|
||||
// Try to detect a nearby boolean field named rtl_enable in the same form
|
||||
var form = el.closest('form');
|
||||
if (form) {
|
||||
// Look for a checkbox input (classic boolean) by name
|
||||
var cb = form.querySelector('input[name="rtl_enable"]');
|
||||
if (cb) {
|
||||
shouldRTL = cb.checked;
|
||||
} else {
|
||||
// Fallback: look for any element with data-field="rtl_enable"
|
||||
var df = form.querySelector('[data-field="rtl_enable"]');
|
||||
if (df) {
|
||||
// Try to find checkbox inside
|
||||
var inner = df.querySelector('input[type="checkbox"]');
|
||||
if (inner) shouldRTL = inner.checked;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (editable) {
|
||||
if (shouldRTL) {
|
||||
editable.setAttribute('dir', 'rtl');
|
||||
editable.style.direction = 'rtl';
|
||||
} else {
|
||||
editable.removeAttribute('dir');
|
||||
editable.style.direction = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Run after DOM ready and also on Odoo bus events when views are re-rendered.
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
setEditorRTL();
|
||||
// start observer after body exists
|
||||
startObserver();
|
||||
});
|
||||
} else {
|
||||
setEditorRTL();
|
||||
startObserver();
|
||||
}
|
||||
|
||||
function startObserver() {
|
||||
// Odoo may dynamically rerender; observe mutations under the document body
|
||||
// and re-apply the RTL attribute when new nodes appear.
|
||||
try {
|
||||
if (!document.body) return;
|
||||
var observer = new MutationObserver(function () {
|
||||
setEditorRTL();
|
||||
});
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
} catch (e) {
|
||||
// Fail silently — we don't want to break the client if MutationObserver isn't available
|
||||
console.warn('project_html_rtl: could not start MutationObserver', e && e.message);
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
||||
} else {
|
||||
132
odoo/addons/project_html_rtl/static/src/js/editor_rtl_v2.js
Normal file
132
odoo/addons/project_html_rtl/static/src/js/editor_rtl_v2.js
Normal file
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* editor_rtl_v2.js — safer version of the RTL injector for the HTML editor.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var MANAGED_FLAG = 'data-project-html-rtl-managed';
|
||||
var IFRAME_LISTENER_FLAG = 'data-project-html-rtl-listener';
|
||||
var TARGET_FIELDS = ['description', 'notes'];
|
||||
|
||||
function findRtlCheckbox(form) {
|
||||
if (!form) {
|
||||
return null;
|
||||
}
|
||||
var direct = form.querySelector('input[name="rtl_enable"]');
|
||||
if (direct) {
|
||||
return direct;
|
||||
}
|
||||
var wrapper = form.querySelector('[data-field="rtl_enable"]');
|
||||
if (!wrapper) {
|
||||
return null;
|
||||
}
|
||||
return wrapper.querySelector('input[type="checkbox"]');
|
||||
}
|
||||
|
||||
function eachTargetField(callback) {
|
||||
TARGET_FIELDS.forEach(function (fieldName) {
|
||||
var containers = document.querySelectorAll('[data-field="' + fieldName + '"]');
|
||||
containers.forEach(function (container) {
|
||||
callback(container, fieldName);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getEditableFromContainer(container) {
|
||||
if (!container) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Try the standard inline editable div first
|
||||
var editable = container.querySelector('.odoo-editor-editable');
|
||||
if (editable) {
|
||||
return editable;
|
||||
}
|
||||
|
||||
// Older/editor fallback: any contenteditable descendant
|
||||
editable = container.querySelector('[contenteditable="true"]');
|
||||
if (editable) {
|
||||
return editable;
|
||||
}
|
||||
|
||||
// Some variants render inside an iframe
|
||||
var iframe = container.querySelector('iframe');
|
||||
if (iframe) {
|
||||
if (!iframe.hasAttribute(IFRAME_LISTENER_FLAG)) {
|
||||
iframe.setAttribute(IFRAME_LISTENER_FLAG, '1');
|
||||
iframe.addEventListener('load', function () {
|
||||
setEditorRTL();
|
||||
});
|
||||
}
|
||||
try {
|
||||
if (iframe.contentDocument && iframe.contentDocument.body) {
|
||||
return iframe.contentDocument.body;
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('project_html_rtl v2: unable to access iframe body', err && err.message);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function setEditorRTL() {
|
||||
eachTargetField(function (container) {
|
||||
try {
|
||||
var editable = getEditableFromContainer(container);
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
var form = container.closest('form');
|
||||
var cb = findRtlCheckbox(form);
|
||||
var shouldRTL = cb ? cb.checked : false;
|
||||
var isManaged = editable.hasAttribute(MANAGED_FLAG);
|
||||
|
||||
if (shouldRTL) {
|
||||
editable.setAttribute('dir', 'rtl');
|
||||
editable.style.direction = 'rtl';
|
||||
if (!isManaged) {
|
||||
editable.setAttribute(MANAGED_FLAG, '1');
|
||||
}
|
||||
} else if (isManaged) {
|
||||
editable.removeAttribute('dir');
|
||||
editable.style.direction = '';
|
||||
editable.removeAttribute(MANAGED_FLAG);
|
||||
}
|
||||
} catch (e) {
|
||||
// Don't break the app if something unexpected happens
|
||||
console.warn('project_html_rtl v2: error applying RTL', e && e.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function startObserver() {
|
||||
try {
|
||||
if (!document.body) return;
|
||||
var observer = new MutationObserver(function () {
|
||||
setEditorRTL();
|
||||
});
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
} catch (e) {
|
||||
console.warn('project_html_rtl v2: could not start MutationObserver', e && e.message);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('change', function (ev) {
|
||||
var target = ev.target;
|
||||
if (target && target.name === 'rtl_enable') {
|
||||
setEditorRTL();
|
||||
}
|
||||
});
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
setEditorRTL();
|
||||
startObserver();
|
||||
});
|
||||
} else {
|
||||
setEditorRTL();
|
||||
startObserver();
|
||||
}
|
||||
|
||||
})();
|
||||
@@ -0,0 +1,87 @@
|
||||
odoo.define('project_html_rtl.html_field_patch', [], function (require) {
|
||||
'use strict';
|
||||
|
||||
const { patch } = require('@web/core/utils/patch');
|
||||
const { useEffect } = require('@odoo/owl');
|
||||
const { HtmlField } = require('@html_editor/fields/html_field');
|
||||
|
||||
// Capture original methods so we can call them from our patch.
|
||||
const origSetup = HtmlField.prototype.setup;
|
||||
const origOnEditorLoad = HtmlField.prototype.onEditorLoad;
|
||||
const origOnBlur = HtmlField.prototype.onBlur;
|
||||
|
||||
const TARGET_MODEL = 'project.task';
|
||||
const TARGET_FIELDS = new Set(['description', 'notes']);
|
||||
const MANAGED_FLAG = 'data-project-html-rtl-managed';
|
||||
|
||||
function isTarget(component) {
|
||||
return (
|
||||
component.props?.record?.resModel === TARGET_MODEL &&
|
||||
TARGET_FIELDS.has(component.props?.name)
|
||||
);
|
||||
}
|
||||
|
||||
function applyDirection(component) {
|
||||
if (!isTarget(component) || component.state?.showCodeView) {
|
||||
return;
|
||||
}
|
||||
const editor = component.editor;
|
||||
if (!editor || !editor.editable) {
|
||||
return;
|
||||
}
|
||||
const shouldRtl = !!component.props.record?.data?.rtl_enable;
|
||||
const editable = editor.editable;
|
||||
const isManaged = editable.hasAttribute(MANAGED_FLAG);
|
||||
|
||||
if (shouldRtl) {
|
||||
editable.setAttribute('dir', 'rtl');
|
||||
editable.style.direction = 'rtl';
|
||||
if (!isManaged) {
|
||||
editable.setAttribute(MANAGED_FLAG, '1');
|
||||
}
|
||||
} else if (isManaged) {
|
||||
editable.removeAttribute('dir');
|
||||
editable.style.direction = '';
|
||||
editable.removeAttribute(MANAGED_FLAG);
|
||||
}
|
||||
}
|
||||
|
||||
patch(
|
||||
HtmlField.prototype,
|
||||
{
|
||||
setup() {
|
||||
// Call the original setup implementation if present
|
||||
if (typeof origSetup === 'function') {
|
||||
origSetup.apply(this, arguments);
|
||||
}
|
||||
if (!isTarget(this)) {
|
||||
return;
|
||||
}
|
||||
useEffect(
|
||||
() => {
|
||||
applyDirection(this);
|
||||
},
|
||||
() => [this.props.record?.data?.rtl_enable, this.state?.showCodeView]
|
||||
);
|
||||
},
|
||||
|
||||
onEditorLoad() {
|
||||
if (typeof origOnEditorLoad === 'function') {
|
||||
origOnEditorLoad.apply(this, arguments);
|
||||
}
|
||||
if (isTarget(this)) {
|
||||
applyDirection(this);
|
||||
}
|
||||
},
|
||||
|
||||
onBlur() {
|
||||
const result = typeof origOnBlur === 'function' ? origOnBlur.apply(this, arguments) : undefined;
|
||||
if (isTarget(this)) {
|
||||
applyDirection(this);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
},
|
||||
'project_html_rtl'
|
||||
);
|
||||
});
|
||||
21
odoo/addons/project_html_rtl/views/project_task_view.xml
Normal file
21
odoo/addons/project_html_rtl/views/project_task_view.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="project_task_form_inherit_html" model="ir.ui.view">
|
||||
<field name="name">project.task.form.inherit.html</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="inherit_id" ref="project.view_task_form2"/>
|
||||
<field name="arch" type="xml">
|
||||
<!-- Enable HTML widget on description and notes when present; add a marker attribute so JS can target them. -->
|
||||
<xpath expr="//field[@name='description']|//field[@name='notes']" position="attributes">
|
||||
<attribute name="widget">html</attribute>
|
||||
<attribute name="data-project-html">1</attribute>
|
||||
</xpath>
|
||||
<!-- Add per-task RTL toggle to the form (inside the first sheet) -->
|
||||
<xpath expr="//form//sheet" position="inside">
|
||||
<group>
|
||||
<field name="rtl_enable"/>
|
||||
</group>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
33
odoo/odoo-init.yml
Normal file
33
odoo/odoo-init.yml
Normal file
@@ -0,0 +1,33 @@
|
||||
---
|
||||
# One-shot init service: creates and initializes the Odoo database and installs
|
||||
# the `base` module on first start. It waits for Postgres and will exit after
|
||||
# successful initialization.
|
||||
services:
|
||||
odoo:
|
||||
depends_on:
|
||||
odoo-init:
|
||||
condition: service_completed_successfully
|
||||
odoo-init:
|
||||
image: odoo:19
|
||||
container_name: odoo-init
|
||||
restart: "no"
|
||||
# Run the wrapper as root so it can chown volumes. The wrapper will drop to
|
||||
# the 'odoo' user before executing the init script.
|
||||
user: "0:0"
|
||||
volumes:
|
||||
- odoo-db-data:/var/lib/odoo
|
||||
- odoo-config:/etc/odoo
|
||||
- ./scripts/odoo/init-odoo.sh:/init-odoo.sh
|
||||
- ./scripts/odoo/init-wrapper.sh:/init-wrapper.sh:ro
|
||||
# Use a small wrapper script (mounted from host) to fix permissions, then run init
|
||||
entrypoint: ["/bin/sh","/init-wrapper.sh"]
|
||||
command: []
|
||||
environment:
|
||||
- HOST=${ODOO_DB_HOST}
|
||||
- USER=${ODOO_DB_USER}
|
||||
- PASSWORD=${ODOO_DB_PASSWORD}
|
||||
- DB_NAME=${ODOO_DB}
|
||||
|
||||
volumes:
|
||||
odoo-db-data:
|
||||
odoo-config:
|
||||
27
odoo/odoo.yml
Normal file
27
odoo/odoo.yml
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
services:
|
||||
odoo:
|
||||
image: odoo:19
|
||||
container_name: odoo
|
||||
restart: always
|
||||
environment:
|
||||
- HOST=${ODOO_DB_HOST}
|
||||
- USER=${ODOO_DB_USER}
|
||||
- PASSWORD=${ODOO_DB_PASSWORD}
|
||||
- DB_NAME=${ODOO_DB}
|
||||
volumes:
|
||||
# Persistent Odoo data (filestore, sessions, attachments)
|
||||
- odoo-db-data:/var/lib/odoo
|
||||
# Keep local ./addons for development; you can remove this to use the named volume instead
|
||||
- ./odoo/addons:/mnt/extra-addons
|
||||
# Odoo configuration and custom config files
|
||||
- odoo-config:/etc/odoo
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.odoo.entrypoints=https"
|
||||
- "traefik.http.routers.odoo.rule=Host(`${ODOO_HOST:-odoo.opencloud.test}`)"
|
||||
- "traefik.http.services.odoo.loadbalancer.server.port=8069"
|
||||
- "traefik.http.routers.odoo.service=odoo"
|
||||
- "traefik.http.routers.odoo.${TRAEFIK_SERVICES_TLS_CONFIG}"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user