mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-20 12:12:10 +02:00
19.0 vanilla
This commit is contained in:
parent
d1963a3c3a
commit
2d3ee4855a
7430 changed files with 2687981 additions and 2965473 deletions
Binary file not shown.
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 1.8 KiB |
|
|
@ -1 +1 @@
|
|||
<svg width="70" height="70" viewBox="0 0 70 70" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>mail/static/description/icon</title><defs><path d="M4 0h61c4 0 5 1 5 5v60c0 4-1 5-5 5H4c-3 0-4-1-4-5V5c0-4 1-5 4-5z" id="a"/><linearGradient x1="100%" y1="0%" x2="0%" y2="100%" id="c"><stop stop-color="#CD7690" offset="0%"/><stop stop-color="#CA5377" offset="100%"/></linearGradient><path d="M57.706 35.71c0 9.276-10.012 16.777-22.351 16.777-3.749 0-7.287-.694-10.392-1.92-3.127 2.517-6.969 4.057-11.051 4.493a.835.835 0 0 1-.893-.621c-.1-.404.21-.654.513-.952 1.497-1.484 3.313-2.646 4.027-7.63-2.855-2.815-4.555-6.331-4.555-10.146 0-9.267 10.011-16.776 22.35-16.776 12.34 0 22.352 7.509 22.352 16.776zm-33.647-6.534a1 1 0 0 0-1 1v1.353a1 1 0 0 0 1 1h22.588a1 1 0 0 0 1-1v-1.353a1 1 0 0 0-1-1H24.06zm0 8.056a1 1 0 0 0-1 1v1.353a1 1 0 0 0 1 1h22.588a1 1 0 0 0 1-1v-1.353a1 1 0 0 0-1-1H24.06z" id="d"/><path d="M57.706 33.71c0 9.276-10.012 16.777-22.351 16.777-3.749 0-7.287-.694-10.392-1.92-3.127 2.517-6.969 4.057-11.051 4.493a.835.835 0 0 1-.893-.621c-.1-.404.21-.654.513-.952 1.497-1.484 3.313-2.646 4.027-7.63-2.855-2.815-4.555-6.331-4.555-10.146 0-9.267 10.011-16.776 22.35-16.776 12.34 0 22.352 7.509 22.352 16.776zm-33.647-6.534a1 1 0 0 0-1 1v1.353a1 1 0 0 0 1 1h22.588a1 1 0 0 0 1-1v-1.353a1 1 0 0 0-1-1H24.06zm0 8.056a1 1 0 0 0-1 1v1.353a1 1 0 0 0 1 1h22.588a1 1 0 0 0 1-1v-1.353a1 1 0 0 0-1-1H24.06z" id="e"/></defs><g fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><g mask="url(#b)"><path fill="url(#c)" d="M0 0H70V70H0z"/><path d="M4 1h61c2.667 0 4.333.667 5 2V0H0v3c.667-1.333 2-2 4-2z" fill="#FFF" fill-opacity=".383"/><path d="M36.847 68.65L4 69c-2 0-4-.146-4-4.098V46.5L17.288 24 54 24.95v17.92L36.847 68.65z" fill="#393939" opacity=".324"/><path d="M4 69h61c2.667 0 4.333-1 5-3v4H0v-4c.667 2 2 3 4 3z" fill="#000" fill-opacity=".383"/><use fill="#000" fill-rule="nonzero" opacity=".3" xlink:href="#d"/><use fill="#FFF" fill-rule="nonzero" xlink:href="#e"/></g></g></svg>
|
||||
<svg width="50" height="50" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg"><path d="M4 25C4 13.402 13.402 4 25 4s21 9.402 21 21-9.402 21-21 21H7.111A3.111 3.111 0 0 1 4 42.889V25Z" fill="#F86126"/><path d="M5.767 45.696C23.95 43.356 38 27.819 38 9c0-.166-.001-.331-.003-.496A20.91 20.91 0 0 0 25 4C13.402 4 4 13.402 4 25v17.889c0 1.237.722 2.305 1.767 2.807Z" fill="#F78613"/></svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 2 KiB After Width: | Height: | Size: 391 B |
BIN
odoo-bringout-oca-ocb-mail/mail/static/description/icon_hi.png
Normal file
BIN
odoo-bringout-oca-ocb-mail/mail/static/description/icon_hi.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.1 KiB |
|
|
@ -0,0 +1,81 @@
|
|||
// idb-keyval.js 3.2.0
|
||||
// https://github.com/jakearchibald/idb-keyval
|
||||
// Copyright 2016, Jake Archibald
|
||||
// Licensed under the Apache License, Version 2.0
|
||||
|
||||
var idbKeyval = (function (exports) {
|
||||
'use strict';
|
||||
|
||||
class Store {
|
||||
constructor(dbName = 'keyval-store', storeName = 'keyval') {
|
||||
this.storeName = storeName;
|
||||
this._dbp = new Promise((resolve, reject) => {
|
||||
const openreq = indexedDB.open(dbName, 1);
|
||||
openreq.onerror = () => reject(openreq.error);
|
||||
openreq.onsuccess = () => resolve(openreq.result);
|
||||
// First time setup: create an empty object store
|
||||
openreq.onupgradeneeded = () => {
|
||||
openreq.result.createObjectStore(storeName);
|
||||
};
|
||||
});
|
||||
}
|
||||
_withIDBStore(type, callback) {
|
||||
return this._dbp.then(db => new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction(this.storeName, type);
|
||||
transaction.oncomplete = () => resolve();
|
||||
transaction.onabort = transaction.onerror = () => reject(transaction.error);
|
||||
callback(transaction.objectStore(this.storeName));
|
||||
}));
|
||||
}
|
||||
}
|
||||
let store;
|
||||
function getDefaultStore() {
|
||||
if (!store)
|
||||
store = new Store();
|
||||
return store;
|
||||
}
|
||||
function get(key, store = getDefaultStore()) {
|
||||
let req;
|
||||
return store._withIDBStore('readonly', store => {
|
||||
req = store.get(key);
|
||||
}).then(() => req.result);
|
||||
}
|
||||
function set(key, value, store = getDefaultStore()) {
|
||||
return store._withIDBStore('readwrite', store => {
|
||||
store.put(value, key);
|
||||
});
|
||||
}
|
||||
function del(key, store = getDefaultStore()) {
|
||||
return store._withIDBStore('readwrite', store => {
|
||||
store.delete(key);
|
||||
});
|
||||
}
|
||||
function clear(store = getDefaultStore()) {
|
||||
return store._withIDBStore('readwrite', store => {
|
||||
store.clear();
|
||||
});
|
||||
}
|
||||
function keys(store = getDefaultStore()) {
|
||||
const keys = [];
|
||||
return store._withIDBStore('readonly', store => {
|
||||
// This would be store.getAllKeys(), but it isn't supported by Edge or Safari.
|
||||
// And openKeyCursor isn't supported by Safari.
|
||||
(store.openKeyCursor || store.openCursor).call(store).onsuccess = function () {
|
||||
if (!this.result)
|
||||
return;
|
||||
keys.push(this.result.key);
|
||||
this.result.continue();
|
||||
};
|
||||
}).then(() => keys);
|
||||
}
|
||||
|
||||
exports.Store = Store;
|
||||
exports.get = get;
|
||||
exports.set = set;
|
||||
exports.del = del;
|
||||
exports.clear = clear;
|
||||
exports.keys = keys;
|
||||
|
||||
return exports;
|
||||
|
||||
}({}));
|
||||
15524
odoo-bringout-oca-ocb-mail/mail/static/lib/lame/lame.js
Normal file
15524
odoo-bringout-oca-ocb-mail/mail/static/lib/lame/lame.js
Normal file
File diff suppressed because it is too large
Load diff
13832
odoo-bringout-oca-ocb-mail/mail/static/lib/odoo_sfu/odoo_sfu.js
Normal file
13832
odoo-bringout-oca-ocb-mail/mail/static/lib/odoo_sfu/odoo_sfu.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,282 @@
|
|||
Name: mediasoup-client
|
||||
Version: 3.18.3
|
||||
License: ISC
|
||||
Private: false
|
||||
Description: mediasoup client side TypeScript library
|
||||
Repository: git+https://github.com/versatica/mediasoup-client.git
|
||||
Homepage: https://mediasoup.org
|
||||
Contributors:
|
||||
Iñaki Baz Castillo <ibc@aliax.net> (https://inakibaz.me)
|
||||
José Luis Millán <jmillan@aliax.net> (https://github.com/jmillan)
|
||||
License Copyright:
|
||||
===
|
||||
|
||||
ISC License
|
||||
|
||||
Copyright © 2015, Iñaki Baz Castillo <ibc@aliax.net>
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Name: debug
|
||||
Version: 4.4.3
|
||||
License: MIT
|
||||
Private: false
|
||||
Description: Lightweight debugging utility for Node.js and the browser
|
||||
Repository: git://github.com/debug-js/debug.git
|
||||
Author: Josh Junon (https://github.com/qix-)
|
||||
Contributors:
|
||||
TJ Holowaychuk <tj@vision-media.ca>
|
||||
Nathan Rajlich <nathan@tootallnate.net> (http://n8.io)
|
||||
Andrew Rhyne <rhyneandrew@gmail.com>
|
||||
License Copyright:
|
||||
===
|
||||
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2014-2017 TJ Holowaychuk <tj@vision-media.ca>
|
||||
Copyright (c) 2018-2021 Josh Junon
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
and associated documentation files (the 'Software'), to deal in the Software without restriction,
|
||||
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
Name: ms
|
||||
Version: 2.1.3
|
||||
License: MIT
|
||||
Private: false
|
||||
Description: Tiny millisecond conversion utility
|
||||
Repository: undefined
|
||||
License Copyright:
|
||||
===
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2020 Vercel, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Name: events
|
||||
Version: 3.3.0
|
||||
License: MIT
|
||||
Private: false
|
||||
Description: Node's event emitter for all engines.
|
||||
Repository: git://github.com/Gozala/events.git
|
||||
Author: Irakli Gozalishvili <rfobic@gmail.com> (http://jeditoolkit.com)
|
||||
License Copyright:
|
||||
===
|
||||
|
||||
MIT
|
||||
|
||||
Copyright Joyent, Inc. and other Node contributors.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to permit
|
||||
persons to whom the Software is furnished to do so, subject to the
|
||||
following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Name: h264-profile-level-id
|
||||
Version: 2.3.2
|
||||
License: ISC
|
||||
Private: false
|
||||
Description: TypeScript utility to process H264 profile-level-id values
|
||||
Repository: https://github.com/versatica/h264-profile-level-id.git
|
||||
Author: Iñaki Baz Castillo <ibc@aliax.net> (https://inakibaz.me)
|
||||
License Copyright:
|
||||
===
|
||||
|
||||
ISC License
|
||||
|
||||
Copyright © 2019, Iñaki Baz Castillo <ibc@aliax.net>
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Name: awaitqueue
|
||||
Version: 3.3.0
|
||||
License: ISC
|
||||
Private: false
|
||||
Description: TypeScript utility to enqueue asynchronous tasks and run them sequentially one after another
|
||||
Repository: https://github.com/versatica/awaitqueue.git
|
||||
Author: Iñaki Baz Castillo <ibc@aliax.net> (https://inakibaz.me)
|
||||
License Copyright:
|
||||
===
|
||||
|
||||
ISC License
|
||||
|
||||
Copyright © 2019, Iñaki Baz Castillo <ibc@aliax.net>
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Name: sdp-transform
|
||||
Version: 3.0.0
|
||||
License: MIT
|
||||
Private: false
|
||||
Description: A simple parser/writer for the Session Description Protocol
|
||||
Repository: clux/sdp-transform
|
||||
Author: Eirik Albrigtsen <sszynrae@gmail.com>
|
||||
License Copyright:
|
||||
===
|
||||
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2013 Eirik Albrigtsen
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
'Software'), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Name: fake-mediastreamtrack
|
||||
Version: 2.2.1
|
||||
License: ISC
|
||||
Private: false
|
||||
Description: Fake W3C MediaStreamTrack implementation
|
||||
Repository: git+https://github.com/ibc/fake-mediastreamtrack.git
|
||||
Author: Iñaki Baz Castillo <ibc@aliax.net> (https://inakibaz.me)
|
||||
License Copyright:
|
||||
===
|
||||
|
||||
ISC License
|
||||
|
||||
Copyright © 2020, Iñaki Baz Castillo <ibc@aliax.net>
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
|
||||
---
|
||||
|
||||
Name: @lukeed/uuid
|
||||
Version: 2.0.1
|
||||
License: MIT
|
||||
Private: false
|
||||
Description: A tiny (230B) and fast UUID (v4) generator for Node and the browser
|
||||
Repository: undefined
|
||||
Author: Luke Edwards <luke.edwards05@gmail.com> (https://lukeed.com)
|
||||
License Copyright:
|
||||
===
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) Luke Edwards <luke.edwards05@gmail.com> (lukeed.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
|
@ -12,35 +12,102 @@
|
|||
# while exim uses a syntax that looks like:
|
||||
#
|
||||
# *: |/home/odoo/src/odoo-mail.py
|
||||
#
|
||||
# Note python2 was chosen on purpose for backward compatibility with old mail
|
||||
# servers.
|
||||
#
|
||||
import optparse
|
||||
|
||||
# Dev Note exit codes should comply with https://www.unix.com/man-page/freebsd/3/sysexits/
|
||||
# see http://www.postfix.org/aliases.5.html, output may end up in bounce mails
|
||||
EX_USAGE = 64
|
||||
EX_NOUSER = 67
|
||||
EX_NOHOST = 68
|
||||
EX_UNAVAILABLE = 69
|
||||
EX_SOFTWARE = 70
|
||||
EX_TEMPFAIL = 75
|
||||
EX_NOPERM = 77
|
||||
EX_CONFIG = 78
|
||||
|
||||
|
||||
import sys
|
||||
import traceback
|
||||
import xmlrpclib
|
||||
try:
|
||||
import traceback
|
||||
try:
|
||||
import xmlrpclib
|
||||
except ImportError:
|
||||
import xmlrpc.client as xmlrpclib
|
||||
import socket
|
||||
from optparse import OptionParser as _OptionParser
|
||||
except ImportError as e:
|
||||
sys.stderr.write('%s\n' % e)
|
||||
sys.exit(EX_SOFTWARE)
|
||||
|
||||
|
||||
class OptionParser(_OptionParser):
|
||||
def exit(self, status=0, msg=None):
|
||||
if msg:
|
||||
sys.stderr.write(msg)
|
||||
sys.stderr.write(" optparse status: %s\n" % status)
|
||||
sys.exit(EX_USAGE)
|
||||
|
||||
|
||||
def postfix_exit(exit_code=EX_SOFTWARE, message=None, debug=False):
|
||||
try:
|
||||
if debug:
|
||||
traceback.print_exc(None, sys.stderr)
|
||||
if message:
|
||||
sys.stderr.write(message)
|
||||
except Exception:
|
||||
pass # error handling failed, exit
|
||||
finally:
|
||||
sys.exit(exit_code)
|
||||
|
||||
|
||||
def main():
|
||||
op = optparse.OptionParser(usage='usage: %prog [options]', version='%prog v1.2')
|
||||
op = OptionParser(usage='usage: %prog [options]', version='%prog v1.3')
|
||||
op.add_option("-d", "--database", dest="database", help="Odoo database name (default: %default)", default='odoo')
|
||||
op.add_option("-u", "--userid", dest="userid", help="Odoo user id to connect with (default: %default)", default=1, type=int)
|
||||
op.add_option("-p", "--password", dest="password", help="Odoo user password (default: %default)", default='admin')
|
||||
op.add_option("--host", dest="host", help="Odoo host (default: %default)", default='localhost')
|
||||
op.add_option("--port", dest="port", help="Odoo port (default: %default)", default=8069, type=int)
|
||||
op.add_option("--proto", dest="protocol", help="Protocol to use (default: %default), http or https", default='http')
|
||||
op.add_option("--debug", dest="debug", action="store_true", help="Enable debug (may lead to stack traces in bounce mails)", default=False)
|
||||
op.add_option("--retry-status", dest="retry", action="store_true", help="Send temporary failure status code on connection errors.", default=False)
|
||||
(o, args) = op.parse_args()
|
||||
if args:
|
||||
op.print_help()
|
||||
sys.stderr.write("unknown arguments: %s\n" % args)
|
||||
sys.exit(EX_USAGE)
|
||||
if o.protocol not in ['http', 'https']:
|
||||
op.print_help()
|
||||
sys.stderr.write("unknown protocol: %s\n" % o.protocol)
|
||||
sys.exit(EX_USAGE)
|
||||
|
||||
try:
|
||||
msg = sys.stdin.read()
|
||||
models = xmlrpclib.ServerProxy('http://%s:%s/xmlrpc/2/object' % (o.host, o.port), allow_none=True)
|
||||
if sys.version_info > (3,):
|
||||
msg = msg.encode()
|
||||
models = xmlrpclib.ServerProxy('%s://%s:%s/xmlrpc/2/object' % (o.protocol, o.host, o.port), allow_none=True)
|
||||
models.execute_kw(o.database, o.userid, o.password, 'mail.thread', 'message_process', [False, xmlrpclib.Binary(msg)], {})
|
||||
except xmlrpclib.Fault as e:
|
||||
# reformat xmlrpc faults to print a readable traceback
|
||||
err = "xmlrpclib.Fault: %s\n%s" % (e.faultCode, e.faultString)
|
||||
sys.exit(err)
|
||||
except Exception as e:
|
||||
traceback.print_exc(None, sys.stderr)
|
||||
sys.exit(2)
|
||||
if e.faultString == 'Access denied' and e.faultCode == 3:
|
||||
postfix_exit(EX_NOPERM, debug=False)
|
||||
elif 'database' in e.faultString and 'does not exist' in e.faultString and e.faultCode == 1:
|
||||
postfix_exit(EX_CONFIG, "database does not exist: %s\n" % o.database, o.debug)
|
||||
elif 'No possible route' in e.faultString and e.faultCode == 1:
|
||||
postfix_exit(EX_NOUSER, "alias does not exist in odoo\n", o.debug)
|
||||
else:
|
||||
postfix_exit(EX_SOFTWARE, "xmlrpclib.Fault\n", o.debug)
|
||||
except (socket.error, socket.gaierror) as e:
|
||||
postfix_exit(
|
||||
exit_code=EX_TEMPFAIL if o.retry else EX_NOHOST,
|
||||
message="connection error: %s: %s (%s)\n" % (e.__class__.__name__, e, o.host),
|
||||
debug=o.debug,
|
||||
)
|
||||
except Exception:
|
||||
postfix_exit(EX_SOFTWARE, "", o.debug)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
try:
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
except Exception:
|
||||
# Handle all unhandled exceptions to prevent postfix from sending
|
||||
# a bounce mail that includes the invoked command with args which
|
||||
# may include the password for the odoo user.
|
||||
postfix_exit(EX_SOFTWARE, "", True)
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/call-join.mp3
Normal file
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/call-join.mp3
Normal file
Binary file not shown.
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/call-join.ogg
Normal file
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/call-join.ogg
Normal file
Binary file not shown.
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/call-leave.mp3
Normal file
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/call-leave.mp3
Normal file
Binary file not shown.
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/call-leave.ogg
Normal file
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/call-leave.ogg
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/earphone-on.mp3
Normal file
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/earphone-on.mp3
Normal file
Binary file not shown.
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/earphone-on.ogg
Normal file
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/earphone-on.ogg
Normal file
Binary file not shown.
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/mic-off.mp3
Normal file
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/mic-off.mp3
Normal file
Binary file not shown.
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/mic-off.ogg
Normal file
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/mic-off.ogg
Normal file
Binary file not shown.
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/mic-on.mp3
Normal file
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/mic-on.mp3
Normal file
Binary file not shown.
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/mic-on.ogg
Normal file
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/mic-on.ogg
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/new-message.mp3
Normal file
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/new-message.mp3
Normal file
Binary file not shown.
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/new-message.ogg
Normal file
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/new-message.ogg
Normal file
Binary file not shown.
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/ptt-press.mp3
Normal file
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/ptt-press.mp3
Normal file
Binary file not shown.
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/ptt-press.ogg
Normal file
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/ptt-press.ogg
Normal file
Binary file not shown.
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/ptt-release.mp3
Normal file
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/ptt-release.mp3
Normal file
Binary file not shown.
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/ptt-release.ogg
Normal file
BIN
odoo-bringout-oca-ocb-mail/mail/static/src/audio/ptt-release.ogg
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,26 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { useRefToModel } from '@mail/component_hooks/use_ref_to_model';
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class ActivityButtonView extends Component {
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
useRefToModel({ fieldName: 'buttonRef', refName: 'button' });
|
||||
}
|
||||
|
||||
get activityButtonView() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(ActivityButtonView, {
|
||||
props: { record: Object },
|
||||
template: 'mail.ActivityButtonView',
|
||||
});
|
||||
|
||||
registerMessagingComponent(ActivityButtonView);
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.ActivityButtonView" owl="1">
|
||||
<a class="o_ActivityButtonView" role="button" t-on-click.prevent="activityButtonView.onClick" t-ref="button">
|
||||
<i class="o_ActivityButtonView_icon fa fa-fw fa-lg" t-att-class="activityButtonView.buttonClass" role="img"/>
|
||||
</a>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { standardFieldProps } from "@web/views/fields/standard_field_props";
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
class ActivityException extends Component {
|
||||
|
||||
get textClass() {
|
||||
if (this.props.value) {
|
||||
return 'text-' + this.props.value + ' fa ' + this.props.record.data.activity_exception_icon;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(ActivityException, {
|
||||
props: standardFieldProps,
|
||||
template: 'mail.ActivityException',
|
||||
fieldDependencies: {
|
||||
activity_exception_icon: { type: 'char' },
|
||||
},
|
||||
noLabel: true,
|
||||
});
|
||||
|
||||
registry.category('fields').add('activity_exception', ActivityException);
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class ActivityListView extends Component {
|
||||
|
||||
get activityListView() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(ActivityListView, {
|
||||
props: { record: Object },
|
||||
template: 'mail.ActivityListView',
|
||||
});
|
||||
|
||||
registerMessagingComponent(ActivityListView);
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
.o_ActivityListView {
|
||||
width: #{"min(95vw, 300px)"};
|
||||
max-height: #{"min(95vh, 350px)"};
|
||||
}
|
||||
|
||||
.o_ActivityListView_activityList {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="mail.ActivityListView" owl="1">
|
||||
<div class="o_ActivityListView d-flex flex-column" t-ref="root">
|
||||
<div class="o_ActivityListView_activityList d-flex flex-column flex-grow-1">
|
||||
<t t-if="activityListView.activityListViewItems.length === 0">
|
||||
<span class="p-3 text-center fst-italic text-500 border-bottom">Schedule activities to help you get things done.</span>
|
||||
</t>
|
||||
<t t-if="activityListView.overdueActivityListViewItems.length > 0">
|
||||
<div class="d-flex bg-100 py-2 border-bottom">
|
||||
<span class="text-danger fw-bold mx-3">Overdue</span>
|
||||
<span class="flex-grow-1"/>
|
||||
<span class="badge rounded-pill text-bg-danger mx-3 align-self-center" t-esc="activityListView.overdueActivityListViewItems.length"/>
|
||||
</div>
|
||||
<t t-foreach="activityListView.overdueActivityListViewItems" t-as="activityListViewItem" t-key="activityListViewItem">
|
||||
<ActivityListViewItem record="activityListViewItem"/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="activityListView.todayActivityListViewItems.length > 0">
|
||||
<div class="d-flex bg-100 py-2 border-bottom">
|
||||
<span class="text-warning fw-bold mx-3">Today</span>
|
||||
<span class="flex-grow-1"/>
|
||||
<span class="badge rounded-pill text-bg-warning mx-3 align-self-center" t-esc="activityListView.todayActivityListViewItems.length"/>
|
||||
</div>
|
||||
<t t-foreach="activityListView.todayActivityListViewItems" t-as="activityListViewItem" t-key="activityListViewItem">
|
||||
<ActivityListViewItem record="activityListViewItem"/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="activityListView.plannedActivityListViewItems.length > 0">
|
||||
<div class="d-flex bg-100 py-2 border-bottom">
|
||||
<span class="text-success fw-bold mx-3">Planned</span>
|
||||
<span class="flex-grow-1"/>
|
||||
<span class="badge rounded-pill text-bg-success mx-3 align-self-center" t-esc="activityListView.plannedActivityListViewItems.length"/>
|
||||
</div>
|
||||
<t t-foreach="activityListView.plannedActivityListViewItems" t-as="activityListViewItem" t-key="activityListViewItem">
|
||||
<ActivityListViewItem record="activityListViewItem"/>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
<button class="o_ActivityListView_addActivityButton btn btn-secondary p-3 text-center" t-on-click="activityListView.onClickAddActivityButton">
|
||||
<i class="fa fa-plus fa-fw"></i><strong>Schedule an activity</strong>
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class ActivityListViewItem extends Component {
|
||||
|
||||
get activityListViewItem() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(ActivityListViewItem, {
|
||||
props: { record: Object },
|
||||
template: 'mail.ActivityListViewItem',
|
||||
});
|
||||
|
||||
registerMessagingComponent(ActivityListViewItem);
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
.o_ActivityListViewItem_actionLink {
|
||||
@include o-hover-text-color($text-muted, map-get($theme-colors, 'success'));
|
||||
@include o-hover-opacity(0.5, 1);
|
||||
}
|
||||
|
||||
.o_ActivityListViewItem_editButton {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.o_ActivityListViewItem:hover .o_ActivityListViewItem_editButton {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.o_ActivityListViewItem_container {
|
||||
min-width: 0;
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="mail.ActivityListViewItem" owl="1">
|
||||
<div class="o_ActivityListViewItem d-flex flex-column border-bottom py-2">
|
||||
<div class="o_ActivityListViewItem_container d-flex align-items-baseline ms-3 me-1">
|
||||
<i t-if="activityListViewItem.activity.icon" class="fa small me-2" t-attf-class="{{ activityListViewItem.activity.icon }}" role="img"/>
|
||||
<t t-if="activityListViewItem.activity.summary">
|
||||
<b class="text-900 me-2 text-truncate flex-grow-1 flex-basis-0" t-esc="activityListViewItem.activity.summary"/>
|
||||
</t>
|
||||
<t t-if="!activityListViewItem.activity.summary and activityListViewItem.activity.type">
|
||||
<b class="text-900 me-2 text-truncate flex-grow-1" t-esc="activityListViewItem.activity.type.displayName"/>
|
||||
</t>
|
||||
<button t-if="activityListViewItem.hasEditButton" class="o_ActivityListViewItem_editButton btn btn-sm btn-link" t-on-click="activityListViewItem.onClickEditActivityButton">
|
||||
<i class="fa fa-pencil"/>
|
||||
</button>
|
||||
<t t-if="activityListViewItem.activity.canWrite">
|
||||
<button t-if="activityListViewItem.fileUploader" class="o_ActivityListViewItem_actionLink btn btn-link shadow-none fs-4 fa fa-upload" title="Upload file" aria-label="Upload File" t-on-click="activityListViewItem.onClickUploadDocument"/>
|
||||
<button t-if="activityListViewItem.hasMarkDoneButton" class="o_ActivityListViewItem_actionLink o_ActivityListViewItem_markAsDone btn btn-link shadow-none fs-4 fa fa-check-circle" title="Mark as done" aria-label="Mark as done" t-on-click="activityListViewItem.onClickMarkAsDone" t-ref="markDoneButton"/>
|
||||
</t>
|
||||
</div>
|
||||
<div t-if="activityListViewItem.activity.state !== 'today'" class="d-flex align-items-baseline flex-wrap mx-3">
|
||||
<i class="fa fa-clock-o me-2 text-muted" role="img" aria-label="Deadline" title="Deadline"/>
|
||||
<t t-if="!activityListViewItem.activity.isCurrentPartnerAssignee and activityListViewItem.activity.assignee">
|
||||
<small class="text-truncate" t-esc="activityListViewItem.activity.assignee.displayName"/>
|
||||
<small class="mx-1">-</small>
|
||||
</t>
|
||||
<small t-att-title="activityListViewItem.activity.dateDeadline" t-esc="activityListViewItem.delayLabel"/>
|
||||
</div>
|
||||
<ActivityMarkDonePopoverContent t-if="activityListViewItem.markDoneView" record="activityListViewItem.markDoneView"/>
|
||||
<div t-if="activityListViewItem.mailTemplateViews.length > 0" class="mx-3 mt-2">
|
||||
<MailTemplate
|
||||
t-foreach="activityListViewItem.mailTemplateViews" t-as="mailTemplateView" t-key="mailTemplateView"
|
||||
record="mailTemplateView"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class KanbanFieldActivityView extends Component {
|
||||
|
||||
get kanbanFieldActivityView() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(KanbanFieldActivityView, {
|
||||
props: { record: Object },
|
||||
template: 'mail.KanbanFieldActivityView',
|
||||
});
|
||||
|
||||
registerMessagingComponent(KanbanFieldActivityView);
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.KanbanFieldActivityView" owl="1">
|
||||
<ActivityButtonView record="kanbanFieldActivityView.activityButtonView"/>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
// ensure components are registered beforehand.
|
||||
import '@mail/backend_components/kanban_field_activity_view/kanban_field_activity_view';
|
||||
import { getMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { standardFieldProps } from "@web/views/fields/standard_field_props";
|
||||
|
||||
const { Component, onWillDestroy, onWillUpdateProps } = owl;
|
||||
|
||||
const getNextId = (function () {
|
||||
let tmpId = 0;
|
||||
return () => {
|
||||
tmpId += 1;
|
||||
return tmpId;
|
||||
};
|
||||
})();
|
||||
|
||||
/**
|
||||
* Container for messaging component KanbanFieldActivityView ensuring messaging
|
||||
* records are ready before rendering KanbanFieldActivityView component.
|
||||
*/
|
||||
export class KanbanFieldActivityViewContainer extends Component {
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
setup() {
|
||||
super.setup();
|
||||
this.kanbanFieldActivityView = undefined;
|
||||
this.kanbanFieldActivityViewId = getNextId();
|
||||
this._insertFromProps(this.props);
|
||||
onWillUpdateProps(nextProps => this._insertFromProps(nextProps));
|
||||
onWillDestroy(() => this._deleteRecord());
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_deleteRecord() {
|
||||
if (this.kanbanFieldActivityView) {
|
||||
if (this.kanbanFieldActivityView.exists()) {
|
||||
this.kanbanFieldActivityView.delete();
|
||||
}
|
||||
this.kanbanFieldActivityView = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
async _insertFromProps(props) {
|
||||
const messaging = await this.env.services.messaging.get();
|
||||
if (owl.status(this) === "destroyed") {
|
||||
this._deleteRecord();
|
||||
return;
|
||||
}
|
||||
const kanbanFieldActivityView = messaging.models['KanbanFieldActivityView'].insert({
|
||||
id: this.kanbanFieldActivityViewId,
|
||||
thread: {
|
||||
activities: props.value.records.map(activityData => {
|
||||
return {
|
||||
id: activityData.resId,
|
||||
};
|
||||
}),
|
||||
hasActivities: true,
|
||||
id: props.record.resId,
|
||||
model: props.record.resModel,
|
||||
},
|
||||
webRecord: props.record,
|
||||
});
|
||||
if (kanbanFieldActivityView !== this.kanbanFieldActivityView) {
|
||||
this._deleteRecord();
|
||||
this.kanbanFieldActivityView = kanbanFieldActivityView;
|
||||
}
|
||||
this.render();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(KanbanFieldActivityViewContainer, {
|
||||
components: { KanbanFieldActivityView: getMessagingComponent('KanbanFieldActivityView') },
|
||||
fieldDependencies: {
|
||||
activity_exception_decoration: { type: 'selection' },
|
||||
activity_exception_icon: { type: 'char' },
|
||||
activity_state: { type: 'selection' },
|
||||
activity_summary: { type: 'char' },
|
||||
activity_type_icon: { type: 'char' },
|
||||
activity_type_id: { type: 'many2one', relation: 'mail.activity.type' },
|
||||
},
|
||||
props: {
|
||||
...standardFieldProps,
|
||||
},
|
||||
template: 'mail.KanbanFieldActivityViewContainer',
|
||||
});
|
||||
|
||||
registry.category('fields').add('kanban_activity', KanbanFieldActivityViewContainer);
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.KanbanFieldActivityViewContainer" owl="1">
|
||||
<KanbanFieldActivityView t-if="kanbanFieldActivityView" record="kanbanFieldActivityView"/>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class ListFieldActivityView extends Component {
|
||||
|
||||
get listFieldActivityView() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(ListFieldActivityView, {
|
||||
props: { record: Object },
|
||||
template: 'mail.ListFieldActivityView',
|
||||
});
|
||||
|
||||
registerMessagingComponent(ListFieldActivityView);
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.ListFieldActivityView" owl="1">
|
||||
<ActivityButtonView record="listFieldActivityView.activityButtonView"/>
|
||||
<span class="o_ListFieldActivityView_summary" t-out="listFieldActivityView.summaryText"/>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
// ensure components are registered beforehand.
|
||||
import '@mail/backend_components/list_field_activity_view/list_field_activity_view';
|
||||
import { getMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { standardFieldProps } from "@web/views/fields/standard_field_props";
|
||||
|
||||
const { Component, onWillDestroy, onWillUpdateProps } = owl;
|
||||
|
||||
const getNextId = (function () {
|
||||
let tmpId = 0;
|
||||
return () => {
|
||||
tmpId += 1;
|
||||
return tmpId;
|
||||
};
|
||||
})();
|
||||
|
||||
/**
|
||||
* Container for messaging component ListFieldActivityView ensuring messaging
|
||||
* records are ready before rendering ListFieldActivityView component.
|
||||
*/
|
||||
export class ListFieldActivityViewContainer extends Component {
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
setup() {
|
||||
super.setup();
|
||||
this.listFieldActivityView = undefined;
|
||||
this.listFieldActivityViewId = getNextId();
|
||||
this._insertFromProps(this.props);
|
||||
onWillUpdateProps(nextProps => this._insertFromProps(nextProps));
|
||||
onWillDestroy(() => this._deleteRecord());
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_deleteRecord() {
|
||||
if (this.listFieldActivityView) {
|
||||
if (this.listFieldActivityView.exists()) {
|
||||
this.listFieldActivityView.delete();
|
||||
}
|
||||
this.listFieldActivityView = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
async _insertFromProps(props) {
|
||||
const messaging = await this.env.services.messaging.get();
|
||||
if (owl.status(this) === "destroyed") {
|
||||
this._deleteRecord();
|
||||
return;
|
||||
}
|
||||
const listFieldActivityView = messaging.models['ListFieldActivityView'].insert({
|
||||
id: this.listFieldActivityViewId,
|
||||
thread: {
|
||||
activities: props.value.records.map(activityData => {
|
||||
return {
|
||||
id: activityData.resId,
|
||||
};
|
||||
}),
|
||||
hasActivities: true,
|
||||
id: props.record.resId,
|
||||
model: props.record.resModel,
|
||||
},
|
||||
webRecord: props.record,
|
||||
});
|
||||
if (listFieldActivityView !== this.listFieldActivityView) {
|
||||
this._deleteRecord();
|
||||
this.listFieldActivityView = listFieldActivityView;
|
||||
}
|
||||
this.render();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(ListFieldActivityViewContainer, {
|
||||
components: { ListFieldActivityView: getMessagingComponent('ListFieldActivityView') },
|
||||
fieldDependencies: {
|
||||
activity_exception_decoration: { type: 'selection' },
|
||||
activity_exception_icon: { type: 'char' },
|
||||
activity_state: { type: 'selection' },
|
||||
activity_summary: { type: 'char' },
|
||||
activity_type_icon: { type: 'char' },
|
||||
activity_type_id: { type: 'many2one', relation: 'mail.activity.type' },
|
||||
},
|
||||
props: {
|
||||
...standardFieldProps,
|
||||
},
|
||||
template: 'mail.ListFieldActivityViewContainer',
|
||||
});
|
||||
|
||||
registry.category('fields').add('list_activity', ListFieldActivityViewContainer);
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.ListFieldActivityViewContainer" owl="1">
|
||||
<ListFieldActivityView t-if="listFieldActivityView" record="listFieldActivityView"/>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
16
odoo-bringout-oca-ocb-mail/mail/static/src/chatter/web/@types/models.d.ts
vendored
Normal file
16
odoo-bringout-oca-ocb-mail/mail/static/src/chatter/web/@types/models.d.ts
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
declare module "models" {
|
||||
import { ScheduledMessage as ScheduledMessageClass } from "@mail/chatter/web/scheduled_message_model";
|
||||
|
||||
export interface ScheduledMessage extends ScheduledMessageClass {}
|
||||
|
||||
export interface Store {
|
||||
"mail.scheduled.message": StaticMailRecord<ScheduledMessage, typeof ScheduledMessageClass>;
|
||||
}
|
||||
export interface Thread {
|
||||
scheduledMessages: ScheduledMessage[];
|
||||
}
|
||||
|
||||
export interface Models {
|
||||
"mail.scheduled.message": ScheduledMessage;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.Chatter">
|
||||
<div t-if="state.thread" class="o-mail-Chatter w-100 h-100 flex-grow-1 d-flex flex-column bg-inherit" t-att-class="{ 'overflow-auto o-scrollbar-thin': props.isChatterAside, 'o-chatter-disabled': props.threadId === false }" t-on-scroll="onScrollDebounced" t-ref="root">
|
||||
<div class="o-mail-Chatter-top d-print-none position-sticky top-0" t-att-class="{ 'shadow-sm': state.isTopStickyPinned }" t-ref="top">
|
||||
<div class="o-mail-Chatter-topbar d-flex flex-shrink-0 flex-grow-0 overflow-x-auto">
|
||||
<div t-if="state.isSearchOpen" class="flex-grow-1">
|
||||
<SearchMessageInput closeSearch.bind="closeSearch" messageSearch="messageSearch" thread="state.thread"/>
|
||||
</div>
|
||||
<t t-else="" name="chatter-topbar-left-buttons">
|
||||
<button class="o-mail-Chatter-sendMessage btn text-nowrap me-1" t-att-class="{
|
||||
'btn-primary': state.composerType !== 'note',
|
||||
'btn-secondary': state.composerType === 'note',
|
||||
'active': state.composerType === 'message',
|
||||
'my-2': !props.compactHeight
|
||||
}" t-att-disabled="!state.thread.canPostMessage and props.threadId" data-hotkey="m" t-on-click="() => this.toggleComposer('message')">
|
||||
Send message
|
||||
</button>
|
||||
<button class="o-mail-Chatter-logNote btn text-nowrap me-1" t-att-class="{
|
||||
'btn-primary active': state.composerType === 'note',
|
||||
'btn-secondary': state.composerType !== 'note',
|
||||
'my-2': !props.compactHeight
|
||||
}" t-att-disabled="!state.thread.canPostMessage and props.threadId" data-hotkey="shift+m" t-on-click="() => this.toggleComposer('note')">
|
||||
Log note
|
||||
</button>
|
||||
<button t-if="props.has_activities" class="o-mail-Chatter-activity btn btn-secondary text-nowrap" t-att-class="{ 'my-2': !props.compactHeight }"
|
||||
t-att-disabled="!state.thread.canPostMessage and props.threadId"
|
||||
data-hotkey="shift+a" t-on-click="scheduleActivity">
|
||||
<span>Activity</span>
|
||||
</button>
|
||||
<span class="o-mail-Chatter-topbarGrow flex-grow-1 pe-2"/>
|
||||
<button class="btn btn-link text-action" aria-label="Search Messages" title="Search Messages" t-on-click="onClickSearch">
|
||||
<i class="oi oi-search" role="img"/>
|
||||
</button>
|
||||
</t>
|
||||
<button t-if="props.hasAttachmentPreview and state.thread.attachmentsInWebClientView.length" class="btn btn-link text-action" t-on-click="popoutAttachment">
|
||||
<i class="fa fa-window-restore" aria-hidden="Pop out Attachments" title="Pop out Attachments"/>
|
||||
</button>
|
||||
<FileUploader t-if="attachments.length === 0" fileUploadClass="'o-mail-Chatter-fileUploader'" multiUpload="true" onUploaded.bind="onUploaded" onClick="(ev) => this.onClickAttachFile(ev)">
|
||||
<t t-set-slot="toggler">
|
||||
<t t-call="mail.Chatter.attachFiles"/>
|
||||
</t>
|
||||
</FileUploader>
|
||||
<t t-else="" t-call="mail.Chatter.attachFiles"/>
|
||||
<div class="o-mail-Followers d-flex me-1">
|
||||
<Dropdown position="'bottom-end'" menuClass="'o-mail-Followers-dropdown ' + store.discussDropdownMenuClass(this)" state="followerListDropdown">
|
||||
<button t-att-class="'o-mail-Followers-button btn btn-link d-flex align-items-center text-action px-1 ' + (props.compactHeight ? '' : 'my-2') + ' ' + (state.thread.selfFollower ? 'active' : '')" t-att-disabled="isDisabled" t-att-title="followerButtonLabel">
|
||||
<i class="fa fa-fw" role="img" t-attf-class="{{ state.thread.selfFollower ? 'fa-user' : 'fa-user-o' }}"/>
|
||||
<i t-if="state.thread.id and state.thread.followersCount === undefined" class="fa fa-circle-o-notch fa-spin"/>
|
||||
<sup t-else="" class="o-mail-Followers-counter" t-esc="state.thread.followersCount ?? 0"/>
|
||||
</button>
|
||||
<t t-set-slot="content">
|
||||
<FollowerList onAddFollowers.bind="onAddFollowers" onFollowerChanged.bind="onFollowerChanged" thread="state.thread" dropdown="followerListDropdown"/>
|
||||
</t>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<t t-if="state.composerType">
|
||||
<t t-if="state.composerType === 'message'">
|
||||
<div class="d-flex py-1 ps-3">
|
||||
<span class="flex-shrink-0 text-end fw-bold px-2" style="width:46px">To: </span>
|
||||
<RecipientsInput thread="state.thread"/>
|
||||
</div>
|
||||
</t>
|
||||
<Composer composer="state.thread.composer" autofocus="true" className="state.composerType === 'message' ? '' : 'pt-4'" mode="'extended'" onPostCallback.bind="onPostCallback" onCloseFullComposerCallback.bind="onCloseFullComposerCallback" dropzoneRef="rootRef" type="state.composerType" t-key="props.threadId"/>
|
||||
</t>
|
||||
</div>
|
||||
<div class="o-mail-Chatter-content d-flex flex-column flex-grow-1 bg-inherit">
|
||||
<t>
|
||||
<t t-if="props.has_activities and activities.length and !messageSearch.searching and !messageSearch.searched">
|
||||
<t t-call="mail.ActivityList"/>
|
||||
</t>
|
||||
<t t-if="scheduledMessages.length">
|
||||
<t t-call="mail.ScheduledMessagesList"/>
|
||||
</t>
|
||||
<div t-if="state.isAttachmentBoxOpened" class="o-mail-AttachmentBox position-relative">
|
||||
<div class="d-flex align-items-center">
|
||||
<hr class="flex-grow-1"/>
|
||||
<span class="p-3 fw-bold">
|
||||
Files
|
||||
</span>
|
||||
<hr class="flex-grow-1"/>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<AttachmentList
|
||||
attachments="attachments"
|
||||
unlinkAttachment.bind="unlinkAttachment"
|
||||
/>
|
||||
<FileUploader multiUpload="true" fileUploadClass="'o-mail-Chatter-fileUploader'" onUploaded.bind="onUploaded" onClick="(ev) => this.onClickAttachFile(ev)">
|
||||
<t t-set-slot="toggler">
|
||||
<button class="btn btn-link" type="button" t-att-disabled="!state.thread.canPostMessage">
|
||||
<i class="fa fa-plus-square"/>
|
||||
Attach files
|
||||
</button>
|
||||
</t>
|
||||
</FileUploader>
|
||||
</div>
|
||||
</div>
|
||||
<SearchMessageResult t-if="messageSearch.searching or messageSearch.searched" thread="state.thread" messageSearch="messageSearch" onClickJump.bind="closeSearch"/>
|
||||
<Thread t-else="" thread="state.thread" t-key="state.thread.localId" order="'desc'" scrollRef="rootRef" jumpPresent="state.jumpThreadPresent"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="mail.ActivityList">
|
||||
<div class="o-mail-ActivityList">
|
||||
<div class="d-flex pt-2 px-2 cursor-pointer fw-bolder" t-on-click="toggleActivities">
|
||||
<hr class="flex-grow-1 fs-3"/>
|
||||
<div class="d-flex align-items-center px-3">
|
||||
<i class="fa fa-fw" t-att-class="state.showActivities ? 'fa-caret-down' : 'fa-caret-right'"/>
|
||||
Planned Activities
|
||||
<span t-if="!state.showActivities" class="badge rounded-pill ms-2 text-bg-success"><t t-esc="activities.length"/></span>
|
||||
</div>
|
||||
<hr class="flex-grow-1 fe-3"/>
|
||||
</div>
|
||||
<t t-if="state.showActivities">
|
||||
<t t-foreach="activities" t-as="activity" t-key="activity.id">
|
||||
<Activity activity="activity" onActivityChanged.bind="onActivityChanged" reloadParentView.bind="reloadParentView"/>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="mail.ScheduledMessagesList">
|
||||
<div class="o-mail-ScheduledMessagesList">
|
||||
<div class="d-flex pt-2 cursor-pointer fw-bolder" t-on-click="toggleScheduledMessages">
|
||||
<hr class="flex-grow-1 fs-3"/>
|
||||
<div class="d-flex align-items-center px-3">
|
||||
<i class="fa fa-fw" t-att-class="state.showScheduledMessages ? 'fa-caret-down' : 'fa-caret-right'"/>
|
||||
Scheduled Messages
|
||||
<span t-if="!state.showScheduledMessages" class="badge rounded-pill ms-2 text-bg-success" t-out="scheduledMessages.length"/>
|
||||
</div>
|
||||
<hr class="flex-grow-1 fe-3"/>
|
||||
</div>
|
||||
<div t-if="state.showScheduledMessages">
|
||||
<t t-foreach="scheduledMessages" t-as="scheduledMessage" t-key="scheduledMessage.id">
|
||||
<ScheduledMessage scheduledMessage="scheduledMessage" onScheduledMessageChanged.bind="onScheduledMessageChanged"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="mail.Chatter.attachFiles">
|
||||
<button class="o-mail-Chatter-attachFiles btn btn-link text-action px-1 d-flex align-items-center" aria-label="Attach files" t-att-class="{ 'my-2': !props.compactHeight }" t-on-click="onClickAddAttachments"
|
||||
t-att-disabled="state.isSearchOpen || (!state.thread.canPostMessage and attachments.length === 0)">
|
||||
<i class="fa fa-paperclip fa-lg me-1"/>
|
||||
<sup t-if="attachments.length > 0" t-esc="attachments.length"/>
|
||||
<i t-if="!state.thread.areAttachmentsLoaded and state.thread.isLoadingAttachments and state.showAttachmentLoading" class="fa fa-circle-o-notch fa-spin" aria-label="Attachment counter loading..."/>
|
||||
</button>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,478 @@
|
|||
import { ScheduledMessage } from "@mail/chatter/web/scheduled_message";
|
||||
import { Activity } from "@mail/core/web/activity";
|
||||
import { AttachmentList } from "@mail/core/common/attachment_list";
|
||||
import { Chatter } from "@mail/chatter/web_portal/chatter";
|
||||
import { FollowerList } from "@mail/core/web/follower_list";
|
||||
import { assignGetter, isDragSourceExternalFile } from "@mail/utils/common/misc";
|
||||
import { useAttachmentUploader } from "@mail/core/common/attachment_uploader_hook";
|
||||
import { useCustomDropzone } from "@web/core/dropzone/dropzone_hook";
|
||||
import { useHover, useMessageScrolling } from "@mail/utils/common/hooks";
|
||||
import { MailAttachmentDropzone } from "@mail/core/common/mail_attachment_dropzone";
|
||||
import { RecipientsInput } from "@mail/core/web/recipients_input";
|
||||
import { SearchMessageInput } from "@mail/core/common/search_message_input";
|
||||
import { SearchMessageResult } from "@mail/core/common/search_message_result";
|
||||
import { KeepLast } from "@web/core/utils/concurrency";
|
||||
import { status, useEffect } from "@odoo/owl";
|
||||
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { Dropdown } from "@web/core/dropdown/dropdown";
|
||||
import { FileUploader } from "@web/views/fields/file_handler";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { useDropdownState } from "@web/core/dropdown/dropdown_hooks";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { useMessageSearch } from "@mail/core/common/message_search_hook";
|
||||
import { usePopoutAttachment } from "@mail/core/common/attachment_view";
|
||||
import { rpc } from "@web/core/network/rpc";
|
||||
import { useRecordObserver } from "@web/model/relational_model/utils";
|
||||
|
||||
export const DELAY_FOR_SPINNER = 1000;
|
||||
|
||||
Object.assign(Chatter.components, {
|
||||
Activity,
|
||||
AttachmentList,
|
||||
Dropdown,
|
||||
FileUploader,
|
||||
FollowerList,
|
||||
RecipientsInput,
|
||||
ScheduledMessage,
|
||||
SearchMessageInput,
|
||||
SearchMessageResult,
|
||||
});
|
||||
|
||||
Chatter.props.push(
|
||||
"close?",
|
||||
"compactHeight?",
|
||||
"has_activities?",
|
||||
"hasAttachmentPreview?",
|
||||
"hasParentReloadOnActivityChanged?",
|
||||
"hasParentReloadOnAttachmentsChanged?",
|
||||
"hasParentReloadOnFollowersUpdate?",
|
||||
"hasParentReloadOnMessagePosted?",
|
||||
"highlightMessageId?",
|
||||
"isAttachmentBoxVisibleInitially?",
|
||||
"isChatterAside?",
|
||||
"isInFormSheetBg?",
|
||||
"saveRecord?",
|
||||
"record?"
|
||||
);
|
||||
|
||||
Object.assign(Chatter.defaultProps, {
|
||||
compactHeight: false,
|
||||
has_activities: true,
|
||||
hasAttachmentPreview: false,
|
||||
hasParentReloadOnActivityChanged: false,
|
||||
hasParentReloadOnAttachmentsChanged: false,
|
||||
hasParentReloadOnFollowersUpdate: false,
|
||||
hasParentReloadOnMessagePosted: false,
|
||||
isAttachmentBoxVisibleInitially: false,
|
||||
isChatterAside: false,
|
||||
isInFormSheetBg: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* @type {import("@mail/chatter/web_portal/chatter").Chatter }
|
||||
* @typedef {Object} Props
|
||||
* @property {function} [close]
|
||||
*/
|
||||
patch(Chatter.prototype, {
|
||||
setup() {
|
||||
this.messageHighlight = useMessageScrolling();
|
||||
super.setup(...arguments);
|
||||
this.orm = useService("orm");
|
||||
this.keepLastSuggestedRecipientsUpdate = new KeepLast();
|
||||
/** @deprecated equivalent to partner_fields and primary_email_field on thread */
|
||||
this.mailImpactingFields = { recordFields: [], emailFields: [] };
|
||||
useRecordObserver((record) => this.updateRecipients(record));
|
||||
this.attachmentPopout = usePopoutAttachment();
|
||||
Object.assign(this.state, {
|
||||
composerType: false,
|
||||
isAttachmentBoxOpened: this.props.isAttachmentBoxVisibleInitially,
|
||||
isSearchOpen: false,
|
||||
showActivities: true,
|
||||
showAttachmentLoading: false,
|
||||
showScheduledMessages: true,
|
||||
});
|
||||
this.messageSearch = useMessageSearch();
|
||||
this.attachmentUploader = useAttachmentUploader(
|
||||
this.store.Thread.insert({ model: this.props.threadModel, id: this.props.threadId })
|
||||
);
|
||||
this.unfollowHover = useHover("unfollow");
|
||||
this.followerListDropdown = useDropdownState();
|
||||
/** @type {number|null} */
|
||||
this.loadingAttachmentTimeout = null;
|
||||
useCustomDropzone(
|
||||
this.rootRef,
|
||||
MailAttachmentDropzone,
|
||||
{
|
||||
extraClass: "o-mail-Chatter-dropzone",
|
||||
/** @param {Event} ev */
|
||||
onDrop: async (ev) => {
|
||||
if (this.state.composerType) {
|
||||
return;
|
||||
}
|
||||
if (isDragSourceExternalFile(ev.dataTransfer)) {
|
||||
const files = [...ev.dataTransfer.files];
|
||||
if (!this.state.thread.id) {
|
||||
const saved = await this.props.saveRecord?.();
|
||||
if (!saved) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
Promise.all(
|
||||
files.map((file) => this.attachmentUploader.uploadFile(file))
|
||||
).then(() => {
|
||||
if (this.props.hasParentReloadOnAttachmentsChanged) {
|
||||
this.reloadParentView();
|
||||
}
|
||||
});
|
||||
this.state.isAttachmentBoxOpened = true;
|
||||
}
|
||||
},
|
||||
},
|
||||
() => !this.store.meetingViewOpened || this.env.inMeetingView
|
||||
);
|
||||
useEffect(
|
||||
() => {
|
||||
if (!this.state.thread) {
|
||||
return;
|
||||
}
|
||||
browser.clearTimeout(this.loadingAttachmentTimeout);
|
||||
if (this.state.thread?.isLoadingAttachments) {
|
||||
this.loadingAttachmentTimeout = browser.setTimeout(
|
||||
() => (this.state.showAttachmentLoading = true),
|
||||
DELAY_FOR_SPINNER
|
||||
);
|
||||
} else {
|
||||
this.state.showAttachmentLoading = false;
|
||||
this.state.isAttachmentBoxOpened =
|
||||
this.state.isAttachmentBoxOpened ||
|
||||
(this.props.isAttachmentBoxVisibleInitially && this.attachments.length > 0);
|
||||
}
|
||||
return () => browser.clearTimeout(this.loadingAttachmentTimeout);
|
||||
},
|
||||
() => [this.state.thread, this.state.thread?.isLoadingAttachments]
|
||||
);
|
||||
useEffect(
|
||||
() => {
|
||||
if (
|
||||
this.state.thread &&
|
||||
!["new", "loading"].includes(this.state.thread.status) &&
|
||||
this.attachments.length === 0
|
||||
) {
|
||||
this.state.isAttachmentBoxOpened = false;
|
||||
}
|
||||
},
|
||||
() => [this.state.thread?.status, this.attachments]
|
||||
);
|
||||
useEffect(
|
||||
() => {
|
||||
this.state.aside = this.props.isChatterAside;
|
||||
},
|
||||
() => [this.props.isChatterAside]
|
||||
);
|
||||
},
|
||||
|
||||
async updateRecipients(record, mode = this.state.composerType) {
|
||||
if (!record) {
|
||||
return;
|
||||
}
|
||||
// Hack: Make the useRecordObserver subscribe to the record changes
|
||||
Object.keys(record.data).forEach((field) => record.data[field]);
|
||||
const partnerIds = []; // Ensure that we don't have duplicates
|
||||
let email;
|
||||
this.mailImpactingFields.recordFields.forEach((field) => {
|
||||
const value = record._changes[field];
|
||||
if (record.data[field] !== undefined && value) {
|
||||
partnerIds.push(value.id);
|
||||
}
|
||||
});
|
||||
this.mailImpactingFields.emailFields.forEach((field) => {
|
||||
const value = record._changes[field];
|
||||
if (record.data[field] !== undefined && value) {
|
||||
email = value;
|
||||
return;
|
||||
}
|
||||
});
|
||||
if ((!partnerIds.length && !email) || mode !== "message" || status(this) === "destroyed") {
|
||||
return;
|
||||
}
|
||||
const recipients = await this.keepLastSuggestedRecipientsUpdate.add(
|
||||
rpc("/mail/thread/recipients/get_suggested_recipients", {
|
||||
thread_model: this.props.threadModel,
|
||||
thread_id: this.props.threadId,
|
||||
partner_ids: partnerIds,
|
||||
main_email: email,
|
||||
})
|
||||
);
|
||||
if (status(this) === "destroyed" && !this.state.thread) {
|
||||
return;
|
||||
}
|
||||
this.state.thread.suggestedRecipients = recipients.map((result) => ({
|
||||
display_name: result.display_name,
|
||||
email: result.email,
|
||||
partner_id: result.partner_id,
|
||||
name: result.name || result.email,
|
||||
}));
|
||||
this.state.thread.additionalRecipients = this.state.thread.additionalRecipients.filter(
|
||||
(additionalRecipient) =>
|
||||
this.state.thread.suggestedRecipients.every(
|
||||
(suggestedRecipient) =>
|
||||
suggestedRecipient.partner_id !== additionalRecipient.partner_id
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* @returns {import("models").Activity[]}
|
||||
*/
|
||||
get activities() {
|
||||
return this.state.thread?.activities ?? [];
|
||||
},
|
||||
|
||||
get afterPostRequestList() {
|
||||
return [
|
||||
...super.afterPostRequestList,
|
||||
"followers",
|
||||
"scheduledMessages",
|
||||
"suggestedRecipients",
|
||||
];
|
||||
},
|
||||
|
||||
get attachments() {
|
||||
return this.state.thread?.attachments ?? [];
|
||||
},
|
||||
|
||||
get childSubEnv() {
|
||||
const res = Object.assign(super.childSubEnv, { messageHighlight: this.messageHighlight });
|
||||
assignGetter(res.inChatter, { aside: () => this.props.isChatterAside });
|
||||
Object.assign(res.inChatter, { toggleComposer: this.toggleComposer.bind(this) });
|
||||
return res;
|
||||
},
|
||||
|
||||
get followerButtonLabel() {
|
||||
return _t("Show Followers");
|
||||
},
|
||||
|
||||
get followingText() {
|
||||
return _t("Following");
|
||||
},
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get isDisabled() {
|
||||
return !this.state.thread.id || !this.state.thread?.hasReadAccess;
|
||||
},
|
||||
|
||||
get onCloseFullComposerRequestList() {
|
||||
return [...super.onCloseFullComposerRequestList, "scheduledMessages"];
|
||||
},
|
||||
|
||||
get requestList() {
|
||||
return [
|
||||
...super.requestList,
|
||||
"activities",
|
||||
"attachments",
|
||||
"contact_fields",
|
||||
"followers",
|
||||
"scheduledMessages",
|
||||
"suggestedRecipients",
|
||||
];
|
||||
},
|
||||
|
||||
get scheduledMessages() {
|
||||
return this.state.thread?.scheduledMessages ?? [];
|
||||
},
|
||||
|
||||
get unfollowText() {
|
||||
return _t("Unfollow");
|
||||
},
|
||||
|
||||
changeThread(threadModel, threadId) {
|
||||
super.changeThread(...arguments);
|
||||
this.attachmentUploader.thread = this.state.thread;
|
||||
if (threadId === false) {
|
||||
this.state.composerType = false;
|
||||
} else {
|
||||
this.onThreadCreated?.(this.state.thread);
|
||||
this.onThreadCreated = null;
|
||||
this.messageSearch.thread = this.state.thread;
|
||||
this.closeSearch();
|
||||
}
|
||||
},
|
||||
|
||||
closeSearch() {
|
||||
this.messageSearch.clear();
|
||||
this.state.isSearchOpen = false;
|
||||
},
|
||||
|
||||
/** @override */
|
||||
async load(thread, requestList) {
|
||||
await super.load(...arguments);
|
||||
if (!thread.id || !this.state.thread?.eq(thread)) {
|
||||
return;
|
||||
}
|
||||
this.mailImpactingFields = {
|
||||
emailFields: this.state.thread.primary_email_field
|
||||
? [this.state.thread.primary_email_field]
|
||||
: [],
|
||||
recordFields: this.state.thread.partner_fields || [],
|
||||
};
|
||||
this.updateRecipients(this.props.record);
|
||||
},
|
||||
|
||||
onActivityChanged(thread) {
|
||||
this.load(thread, [...this.requestList, "messages"]);
|
||||
if (this.props.hasParentReloadOnActivityChanged) {
|
||||
this.reloadParentView();
|
||||
}
|
||||
},
|
||||
|
||||
onAddFollowers() {
|
||||
this.load(this.state.thread, ["followers", "suggestedRecipients"]);
|
||||
if (this.props.hasParentReloadOnFollowersUpdate) {
|
||||
this.reloadParentView();
|
||||
}
|
||||
},
|
||||
|
||||
onClickAddAttachments() {
|
||||
if (this.attachments.length === 0) {
|
||||
return;
|
||||
}
|
||||
this.state.isAttachmentBoxOpened = !this.state.isAttachmentBoxOpened;
|
||||
if (this.state.isAttachmentBoxOpened) {
|
||||
this.rootRef.el.scrollTop = 0;
|
||||
this.state.thread.scrollTop = "bottom";
|
||||
}
|
||||
},
|
||||
|
||||
async onClickAttachFile(ev) {
|
||||
if (this.state.thread.id) {
|
||||
return;
|
||||
}
|
||||
const saved = await this.props.saveRecord?.();
|
||||
if (!saved) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
onClickSearch() {
|
||||
this.state.composerType = false;
|
||||
this.state.isSearchOpen = !this.state.isSearchOpen;
|
||||
},
|
||||
|
||||
onCloseFullComposerCallback(isDiscard) {
|
||||
this.toggleComposer();
|
||||
super.onCloseFullComposerCallback();
|
||||
if (!isDiscard) {
|
||||
this.reloadParentView();
|
||||
}
|
||||
},
|
||||
|
||||
onFollowerChanged() {
|
||||
document.body.click(); // hack to close dropdown
|
||||
this.reloadParentView();
|
||||
},
|
||||
|
||||
_onMounted() {
|
||||
super._onMounted();
|
||||
if (this.state.thread && this.props.highlightMessageId) {
|
||||
this.state.thread.highlightMessage = this.props.highlightMessageId;
|
||||
}
|
||||
},
|
||||
|
||||
onPostCallback() {
|
||||
if (this.props.hasParentReloadOnMessagePosted) {
|
||||
this.reloadParentView();
|
||||
}
|
||||
this.toggleComposer();
|
||||
super.onPostCallback();
|
||||
},
|
||||
|
||||
onScheduledMessageChanged(thread) {
|
||||
// reload messages as well as a scheduled message could have been sent
|
||||
this.load(thread, ["scheduledMessages", "messages"]);
|
||||
// sending a message could trigger another action (eg. move so to quotation sent)
|
||||
this.reloadParentView();
|
||||
},
|
||||
|
||||
onSuggestedRecipientAdded(thread) {
|
||||
this.load(thread, ["suggestedRecipients"]);
|
||||
},
|
||||
|
||||
async onUploaded(data) {
|
||||
await this.attachmentUploader.uploadData(data);
|
||||
if (this.props.hasParentReloadOnAttachmentsChanged) {
|
||||
this.reloadParentView();
|
||||
}
|
||||
this.state.isAttachmentBoxOpened = true;
|
||||
if (this.rootRef.el) {
|
||||
this.rootRef.el.scrollTop = 0;
|
||||
}
|
||||
this.state.thread.scrollTop = "bottom";
|
||||
},
|
||||
|
||||
async reloadParentView() {
|
||||
await this.props.saveRecord?.();
|
||||
if (this.props.record) {
|
||||
await this.props.record.load();
|
||||
}
|
||||
},
|
||||
|
||||
async scheduleActivity() {
|
||||
this.closeSearch();
|
||||
const schedule = async (thread) => {
|
||||
await this.store.scheduleActivity(thread.model, [thread.id]);
|
||||
this.load(thread, ["activities", "messages"]);
|
||||
if (this.props.hasParentReloadOnActivityChanged) {
|
||||
await this.reloadParentView();
|
||||
}
|
||||
};
|
||||
if (this.state.thread.id) {
|
||||
schedule(this.state.thread);
|
||||
} else {
|
||||
this.onThreadCreated = schedule;
|
||||
this.props.saveRecord?.();
|
||||
}
|
||||
},
|
||||
|
||||
toggleActivities() {
|
||||
this.state.showActivities = !this.state.showActivities;
|
||||
},
|
||||
|
||||
toggleComposer(mode = false, { force = false } = {}) {
|
||||
this.closeSearch();
|
||||
const toggle = async () => {
|
||||
if (!force && this.state.composerType === mode) {
|
||||
this.state.composerType = false;
|
||||
} else {
|
||||
if (mode === "message") {
|
||||
await this.updateRecipients(this.props.record, mode);
|
||||
}
|
||||
this.state.composerType = mode;
|
||||
}
|
||||
};
|
||||
if (this.state.thread.id) {
|
||||
toggle();
|
||||
} else {
|
||||
this.onThreadCreated = toggle;
|
||||
this.props.saveRecord?.();
|
||||
}
|
||||
},
|
||||
|
||||
toggleScheduledMessages() {
|
||||
this.state.showScheduledMessages = !this.state.showScheduledMessages;
|
||||
},
|
||||
|
||||
async unlinkAttachment(attachment) {
|
||||
await this.attachmentUploader.unlink(attachment);
|
||||
if (this.props.hasParentReloadOnAttachmentsChanged) {
|
||||
this.reloadParentView();
|
||||
}
|
||||
},
|
||||
|
||||
popoutAttachment() {
|
||||
this.attachmentPopout.popout();
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { patch } from "@web/core/utils/patch";
|
||||
import { FormArchParser } from "@web/views/form/form_arch_parser";
|
||||
|
||||
patch(FormArchParser.prototype, {
|
||||
parse(xmlDoc, models, modelName) {
|
||||
const result = super.parse(...arguments);
|
||||
result.has_activities = Boolean(models[modelName].has_activities);
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { append, createElement, extractAttributes, setAttributes } from "@web/core/utils/xml";
|
||||
import { FormCompiler } from "@web/views/form/form_compiler";
|
||||
|
||||
/** @this {FormCompiler} */
|
||||
function compileChatter(node, params) {
|
||||
const chatterContainerXml = createElement("t");
|
||||
setAttributes(chatterContainerXml, {
|
||||
"t-component": "__comp__.mailComponents.Chatter",
|
||||
has_activities: "__comp__.props.archInfo.has_activities",
|
||||
hasAttachmentPreview: Boolean(
|
||||
this.templates.FormRenderer.querySelector(".o_attachment_preview")
|
||||
),
|
||||
hasParentReloadOnActivityChanged: Boolean(node.getAttribute("reload_on_activity")),
|
||||
hasParentReloadOnAttachmentsChanged: Boolean(node.getAttribute("reload_on_attachment")),
|
||||
hasParentReloadOnFollowersUpdate: Boolean(node.getAttribute("reload_on_follower")),
|
||||
hasParentReloadOnMessagePosted: Boolean(node.getAttribute("reload_on_post")),
|
||||
isAttachmentBoxVisibleInitially: Boolean(node.getAttribute("open_attachments")),
|
||||
threadId: "__comp__.props.record.resId or undefined",
|
||||
threadModel: "__comp__.props.record.resModel",
|
||||
record: "__comp__.props.record",
|
||||
saveRecord: "() => __comp__.save and __comp__.save()",
|
||||
highlightMessageId: "__comp__.highlightMessageId",
|
||||
});
|
||||
const chatterContainerHookXml = createElement("div");
|
||||
chatterContainerHookXml.classList.add("o-mail-ChatterContainer", "o-mail-Form-chatter");
|
||||
setAttributes(chatterContainerHookXml, { "t-if": "!__comp__.env.inDialog" });
|
||||
append(chatterContainerHookXml, chatterContainerXml);
|
||||
return chatterContainerHookXml;
|
||||
}
|
||||
|
||||
function compileAttachmentPreview(node, params) {
|
||||
const webClientViewAttachmentViewContainerHookXml = createElement("div");
|
||||
webClientViewAttachmentViewContainerHookXml.classList.add("o_attachment_preview");
|
||||
const webClientViewAttachmentViewContainerXml = createElement("t");
|
||||
setAttributes(webClientViewAttachmentViewContainerXml, {
|
||||
"t-component": "__comp__.mailComponents.AttachmentView",
|
||||
threadId: "__comp__.props.record.resId or undefined",
|
||||
threadModel: "__comp__.props.record.resModel",
|
||||
});
|
||||
append(webClientViewAttachmentViewContainerHookXml, webClientViewAttachmentViewContainerXml);
|
||||
return webClientViewAttachmentViewContainerHookXml;
|
||||
}
|
||||
|
||||
registry.category("form_compilers").add("chatter_compiler", {
|
||||
selector: "chatter",
|
||||
fn: compileChatter,
|
||||
});
|
||||
|
||||
registry.category("form_compilers").add("attachment_preview_compiler", {
|
||||
selector: "div.o_attachment_preview",
|
||||
fn: compileAttachmentPreview,
|
||||
});
|
||||
|
||||
patch(FormCompiler.prototype, {
|
||||
compile(node, params) {
|
||||
const res = super.compile(node, params);
|
||||
const chatterContainerHookXml = res.querySelector(".o-mail-Form-chatter");
|
||||
if (!chatterContainerHookXml) {
|
||||
return res; // no chatter, keep the result as it is
|
||||
}
|
||||
const chatterContainerXml = chatterContainerHookXml.querySelector(
|
||||
"t[t-component='__comp__.mailComponents.Chatter']"
|
||||
);
|
||||
setAttributes(chatterContainerXml, {
|
||||
isChatterAside: "false",
|
||||
isInFormSheetBg: "false",
|
||||
saveRecord: "__comp__.props.saveRecord",
|
||||
});
|
||||
if (chatterContainerHookXml.parentNode.classList.contains("o_form_sheet")) {
|
||||
return res; // if chatter is inside sheet, keep it there
|
||||
}
|
||||
const formSheetBgXml = res.querySelector(".o_form_sheet_bg");
|
||||
const parentXml = formSheetBgXml && formSheetBgXml.parentNode;
|
||||
if (!parentXml) {
|
||||
return res; // miss-config: a sheet-bg is required for the rest
|
||||
}
|
||||
|
||||
const webClientViewAttachmentViewHookXml = res.querySelector(".o_attachment_preview");
|
||||
const hasPreview = !!webClientViewAttachmentViewHookXml;
|
||||
if (webClientViewAttachmentViewHookXml) {
|
||||
// in sheet bg (attachment viewer present)
|
||||
setAttributes(webClientViewAttachmentViewHookXml, {
|
||||
"t-if": `__comp__.mailLayout(${hasPreview}).includes("COMBO")`,
|
||||
});
|
||||
const sheetBgChatterContainerHookXml = chatterContainerHookXml.cloneNode(true);
|
||||
sheetBgChatterContainerHookXml.classList.add("o-isInFormSheetBg", "w-auto");
|
||||
setAttributes(sheetBgChatterContainerHookXml, {
|
||||
"t-if": `__comp__.mailLayout(${hasPreview}) == "COMBO"`,
|
||||
});
|
||||
append(formSheetBgXml, sheetBgChatterContainerHookXml);
|
||||
const sheetBgChatterContainerXml = sheetBgChatterContainerHookXml.querySelector(
|
||||
"t[t-component='__comp__.mailComponents.Chatter']"
|
||||
);
|
||||
setAttributes(sheetBgChatterContainerXml, {
|
||||
isInFormSheetBg: "true",
|
||||
isChatterAside: "false",
|
||||
});
|
||||
}
|
||||
// after sheet bg (standard position, either aside or below)
|
||||
setAttributes(chatterContainerXml, {
|
||||
isInFormSheetBg: `["COMBO", "BOTTOM_CHATTER"].includes(__comp__.mailLayout(${hasPreview}))`,
|
||||
isChatterAside: `["SIDE_CHATTER", "EXTERNAL_COMBO_XXL", "EXTERNAL_COMBO"].includes(__comp__.mailLayout(${hasPreview}))`,
|
||||
});
|
||||
const { ["t-if"]: tIf } = extractAttributes(chatterContainerHookXml, ["t-if"]);
|
||||
setAttributes(chatterContainerHookXml, {
|
||||
"t-if": `${
|
||||
tIf ? tIf : "true"
|
||||
} and (!["COMBO", "NONE"].includes(__comp__.mailLayout(${hasPreview})))`, // opposite of sheetBgChatterContainerHookXml
|
||||
"t-attf-class": `{{ ["SIDE_CHATTER", "EXTERNAL_COMBO_XXL"].includes(__comp__.mailLayout(${hasPreview})) ? "o-aside w-print-100" : "mt-4 mt-md-0" }}`,
|
||||
});
|
||||
append(parentXml, chatterContainerHookXml);
|
||||
return res;
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import { EventBus, useSubEnv } from "@odoo/owl";
|
||||
|
||||
import { x2ManyCommands } from "@web/core/orm_service";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { createDocumentFragmentFromContent } from "@web/core/utils/html";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { FormController } from "@web/views/form/form_controller";
|
||||
|
||||
FormController.props = {
|
||||
...FormController.props,
|
||||
fullComposerBus: { type: EventBus, optional: true },
|
||||
};
|
||||
|
||||
patch(FormController.prototype, {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
if (this.env.services["mail.store"]) {
|
||||
this.mailStore = useService("mail.store");
|
||||
}
|
||||
useSubEnv({
|
||||
chatter: {
|
||||
fetchThreadData: true,
|
||||
fetchMessages: true,
|
||||
},
|
||||
});
|
||||
},
|
||||
onWillLoadRoot(nextConfiguration) {
|
||||
super.onWillLoadRoot(...arguments);
|
||||
this.env.chatter.fetchThreadData = true;
|
||||
this.env.chatter.fetchMessages = true;
|
||||
const isSameThread =
|
||||
this.model.root?.resId === nextConfiguration.resId &&
|
||||
this.model.root?.resModel === nextConfiguration.resModel;
|
||||
if (isSameThread) {
|
||||
// not first load
|
||||
const { resModel, resId } = this.model.root;
|
||||
this.env.bus.trigger("MAIL:RELOAD-THREAD", { model: resModel, id: resId });
|
||||
}
|
||||
},
|
||||
|
||||
async onWillSaveRecord(record, changes) {
|
||||
if (record.resModel === "mail.compose.message") {
|
||||
const doc = createDocumentFragmentFromContent(changes.body);
|
||||
const partnerElements = doc.querySelectorAll('[data-oe-model="res.partner"]');
|
||||
const partnerIds = Array.from(partnerElements).map((element) =>
|
||||
parseInt(element.dataset.oeId)
|
||||
);
|
||||
if (partnerIds.length) {
|
||||
if (changes.partner_ids[0] && changes.partner_ids[0][0] === x2ManyCommands.SET) {
|
||||
partnerIds.push(...changes.partner_ids[0][2]);
|
||||
}
|
||||
changes.partner_ids.push(...partnerIds.map((pid) => x2ManyCommands.link(pid)));
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-inherit="web.FormView" t-inherit-mode="extension">
|
||||
<xpath expr="//Layout/t[@t-component='props.Renderer']" position="attributes">
|
||||
<attribute name="saveRecord">() => this.save()</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import { AttachmentView } from "@mail/core/common/attachment_view";
|
||||
import { Chatter } from "@mail/chatter/web_portal/chatter";
|
||||
|
||||
import { onMounted, onWillUnmount, useState } from "@odoo/owl";
|
||||
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { router } from "@web/core/browser/router";
|
||||
import { SIZES } from "@web/core/ui/ui_service";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { useDebounced } from "@web/core/utils/timing";
|
||||
import { FormRenderer } from "@web/views/form/form_renderer";
|
||||
|
||||
patch(FormRenderer.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.mailComponents = {
|
||||
AttachmentView,
|
||||
Chatter,
|
||||
};
|
||||
this.highlightMessageId = router.current.highlight_message_id;
|
||||
this.messagingState = useState({
|
||||
/** @type {import("models").Thread} */
|
||||
thread: undefined,
|
||||
});
|
||||
if (this.env.services["mail.store"]) {
|
||||
this.mailStore = useService("mail.store");
|
||||
}
|
||||
this.uiService = useService("ui");
|
||||
this.mailPopoutService = useService("mail.popout");
|
||||
|
||||
this.onResize = useDebounced(this.render, 200);
|
||||
onMounted(() => browser.addEventListener("resize", this.onResize));
|
||||
onWillUnmount(() => browser.removeEventListener("resize", this.onResize));
|
||||
},
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasFile() {
|
||||
if (!this.mailStore || !this.props.record.resId) {
|
||||
return false;
|
||||
}
|
||||
this.messagingState.thread = this.mailStore.Thread.insert({
|
||||
id: this.props.record.resId,
|
||||
model: this.props.record.resModel,
|
||||
});
|
||||
return this.messagingState.thread.attachmentsInWebClientView.length > 0;
|
||||
},
|
||||
mailLayout(hasAttachmentContainer) {
|
||||
const xxl = this.uiService.size >= SIZES.XXL;
|
||||
const hasFile = this.hasFile();
|
||||
const hasChatter = !!this.mailStore;
|
||||
const hasExternalWindow = !!this.mailPopoutService.externalWindow;
|
||||
if (hasExternalWindow && hasFile && hasAttachmentContainer) {
|
||||
if (xxl) {
|
||||
return "EXTERNAL_COMBO_XXL"; // chatter on the side, attachment in separate tab
|
||||
}
|
||||
return "EXTERNAL_COMBO"; // chatter on the bottom, attachment in separate tab
|
||||
}
|
||||
if (hasChatter) {
|
||||
if (xxl) {
|
||||
if (hasAttachmentContainer && hasFile) {
|
||||
return "COMBO"; // chatter on the bottom, attachment on the side
|
||||
}
|
||||
return "SIDE_CHATTER"; // chatter on the side, no attachment
|
||||
}
|
||||
return "BOTTOM_CHATTER"; // chatter on the bottom, no attachment
|
||||
}
|
||||
return "NONE";
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
// ------------------------------------------------------------------
|
||||
// Layout
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.o-mail-ChatterContainer, .o-mail-Form-chatter {
|
||||
background-color: $o-webclient-background-color;
|
||||
--Chatter-asideExtraWidth: 0px; // to take into account more items, e.g. "close" chatter feature
|
||||
&.o-aside {
|
||||
@media (max-width: 1920px) {
|
||||
--ChatterAsideForm-padding-left: #{map-get($spacers, 2)};
|
||||
}
|
||||
|
||||
flex-shrink: 0;
|
||||
flex-grow: 1;
|
||||
padding: map-get($spacers, 0);
|
||||
width: calc(#{$o-mail-Chatter-minWidth} + var(--Chatter-asideExtraWidth));
|
||||
}
|
||||
|
||||
&.o-isInFormSheetBg {
|
||||
margin-left: calc(var(--formView-sheetBg-padding-x) * -1);
|
||||
margin-right: calc(var(--formView-sheetBg-padding-x) * -1);
|
||||
}
|
||||
|
||||
&:not(.o-aside):not(.o-full-width) .o-mail-Chatter {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// Reduce horizontal spacing if the next sibling is an aside chatter
|
||||
.o_form_sheet_bg:has(+ .o-mail-Form-chatter.o-aside) {
|
||||
@media screen and (max-width: 1920px) {
|
||||
--formView-sheetBg-padding-right: #{map-get($spacers, 2)};
|
||||
}
|
||||
}
|
||||
|
||||
// Disable scroll anchoring when chatter is not in aside mode to prevent unwanted layout shifts
|
||||
.o_content:has(.o-mail-Form-chatter:not(.o-aside)) {
|
||||
overflow-anchor: none;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Style
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.o-mail-ChatterContainer, .o-mail-Form-chatter {
|
||||
display: flex;
|
||||
margin: 0 auto;
|
||||
padding-bottom: map-get($spacers, 5);
|
||||
width: 100%;
|
||||
}
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
import { formView } from "@web/views/form/form_view";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { EventBus, toRaw, useEffect, useRef, useSubEnv } from "@odoo/owl";
|
||||
import { useCustomDropzone } from "@web/core/dropzone/dropzone_hook";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { useX2ManyCrud } from "@web/views/fields/relational_utils";
|
||||
import { MailAttachmentDropzone } from "@mail/core/common/mail_attachment_dropzone";
|
||||
|
||||
export class MailComposerFormController extends formView.Controller {
|
||||
static props = {
|
||||
...formView.Controller.props,
|
||||
fullComposerBus: { type: EventBus, optional: true },
|
||||
};
|
||||
static defaultProps = { fullComposerBus: new EventBus() };
|
||||
setup() {
|
||||
super.setup();
|
||||
toRaw(this.env.dialogData).model = "mail.compose.message";
|
||||
useSubEnv({
|
||||
fullComposerBus: this.props.fullComposerBus,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class MailComposerFormRenderer extends formView.Renderer {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.orm = useService("orm");
|
||||
// Autofocus the visible editor in edition mode.
|
||||
this.root = useRef("compiled_view_root");
|
||||
useEffect(
|
||||
(isInEdition, el) => {
|
||||
if (
|
||||
el &&
|
||||
isInEdition &&
|
||||
this.props.record.data.composition_comment_option === "reply_all"
|
||||
) {
|
||||
const element = el.querySelector(".note-editable[contenteditable]");
|
||||
if (element) {
|
||||
element.focus();
|
||||
document.dispatchEvent(new Event("selectionchange", {}));
|
||||
}
|
||||
}
|
||||
},
|
||||
() => [this.props.record.isInEdition, this.root.el, this.props.record.resId]
|
||||
);
|
||||
|
||||
const getActiveMailThreads = () =>
|
||||
JSON.parse(this.props.record.data.res_ids).map((resId) => {
|
||||
const thread = this.mailStore.Thread.insert({
|
||||
model: this.props.record.data.model,
|
||||
id: resId,
|
||||
});
|
||||
return thread;
|
||||
});
|
||||
|
||||
// Add file dropzone on full mail composer:
|
||||
this.attachmentUploadService = useService("mail.attachment_upload");
|
||||
this.operations = useX2ManyCrud(() => this.props.record.data["attachment_ids"], true);
|
||||
|
||||
useCustomDropzone(this.root, MailAttachmentDropzone, {
|
||||
/** @param {Event} event */
|
||||
onDrop: async (event) => {
|
||||
for (const thread of getActiveMailThreads()) {
|
||||
for (const file of event.dataTransfer.files) {
|
||||
const attachment = await this.attachmentUploadService.upload(
|
||||
thread,
|
||||
thread.composer,
|
||||
file
|
||||
);
|
||||
await this.operations.saveRecord([attachment.id]);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/** @param {function} */
|
||||
const onCloseWizardModal = (callback) => {
|
||||
this.env.dialogData.dismiss = callback;
|
||||
};
|
||||
|
||||
onCloseWizardModal(async () => {
|
||||
const selectedPartnerIds = this.props.record.data.partner_ids.currentIds;
|
||||
const selectedPartners = await this.orm.searchRead(
|
||||
"res.partner",
|
||||
[["id", "in", selectedPartnerIds]],
|
||||
["email", "id", "lang", "name"]
|
||||
);
|
||||
|
||||
/**
|
||||
* @param {SuggestedRecipient} recipient
|
||||
* @returns {SuggestedRecipient}
|
||||
*/
|
||||
const updateRecipientWithCorrespondingPartner = (recipient) => {
|
||||
const partner = selectedPartners.find(
|
||||
(partner) => partner.id === recipient.id || partner.email === recipient.email
|
||||
);
|
||||
if (partner) {
|
||||
return {
|
||||
...recipient,
|
||||
email: partner.email,
|
||||
lang: partner.lang,
|
||||
name: partner.name,
|
||||
partner_id: partner.id,
|
||||
};
|
||||
}
|
||||
return recipient;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {SuggestedRecipient} recipient
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const isRecipientSelectedFromFullMailComposer = (recipient) =>
|
||||
selectedPartnerIds.includes(recipient.partner_id);
|
||||
|
||||
for (const thread of getActiveMailThreads()) {
|
||||
// Update the recipient lists:
|
||||
thread.suggestedRecipients = thread.suggestedRecipients.map(
|
||||
updateRecipientWithCorrespondingPartner
|
||||
);
|
||||
thread.additionalRecipients = thread.additionalRecipients.map(
|
||||
updateRecipientWithCorrespondingPartner
|
||||
);
|
||||
|
||||
// Remove the recipients that got removed from the composer:
|
||||
thread.suggestedRecipients = thread.suggestedRecipients.filter(
|
||||
isRecipientSelectedFromFullMailComposer
|
||||
);
|
||||
thread.additionalRecipients = thread.additionalRecipients.filter(
|
||||
isRecipientSelectedFromFullMailComposer
|
||||
);
|
||||
|
||||
// Add the recipients that got added to the composer:
|
||||
for (const partner of selectedPartners) {
|
||||
const allRecipients = [
|
||||
...thread.suggestedRecipients,
|
||||
...thread.additionalRecipients,
|
||||
];
|
||||
if (!allRecipients.some((recipient) => recipient.partner_id === partner.id)) {
|
||||
thread.additionalRecipients.push({
|
||||
display_name: partner.display_name,
|
||||
email: partner.email,
|
||||
lang: partner.lang,
|
||||
name: partner.name,
|
||||
partner_id: partner.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("views").add("mail_composer_form", {
|
||||
...formView,
|
||||
Controller: MailComposerFormController,
|
||||
Renderer: MailComposerFormRenderer,
|
||||
});
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { formView } from "@web/views/form/form_view";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
|
||||
export class MailComposerSaveTemplateFormController extends formView.Controller {
|
||||
/** @override */
|
||||
setup() {
|
||||
super.setup();
|
||||
this.actionService = useService("action");
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async afterExecuteActionButton(clickParams) {
|
||||
if (clickParams.special !== "cancel") {
|
||||
return await super.afterExecuteActionButton(...arguments);
|
||||
}
|
||||
await this.actionService.doActionButton({
|
||||
type: "object",
|
||||
name: "cancel_save_template",
|
||||
resId: this.model.root.resId,
|
||||
resModel: this.model.root.resModel,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("views").add("mail_composer_save_template_form", {
|
||||
...formView,
|
||||
Controller: MailComposerSaveTemplateFormController,
|
||||
});
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
import { AttachmentList } from "@mail/core/common/attachment_list";
|
||||
import { RelativeTime } from "@mail/core/common/relative_time";
|
||||
import { AvatarCardPopover } from "@mail/discuss/web/avatar_card/avatar_card_popover";
|
||||
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { usePopover } from "@web/core/popover/popover_hook";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
import { Component, useState } from "@odoo/owl";
|
||||
|
||||
export const SCHEDULED_MESSAGE_TRUNCATE_THRESHOLD = 50; // arbitrary, ~ 1 line on large screen
|
||||
|
||||
export class ScheduledMessage extends Component {
|
||||
static props = {
|
||||
onScheduledMessageChanged: Function,
|
||||
scheduledMessage: Object,
|
||||
};
|
||||
static template = "mail.ScheduledMessage";
|
||||
static components = {
|
||||
AttachmentList,
|
||||
RelativeTime,
|
||||
};
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
this.state = useState({
|
||||
readMore: false,
|
||||
});
|
||||
this.avatarCard = usePopover(AvatarCardPopover);
|
||||
this.dialogService = useService("dialog");
|
||||
}
|
||||
|
||||
get isShort() {
|
||||
return (
|
||||
this.props.scheduledMessage.textContent.length < SCHEDULED_MESSAGE_TRUNCATE_THRESHOLD
|
||||
);
|
||||
}
|
||||
|
||||
get scheduledDate() {
|
||||
return this.props.scheduledMessage.scheduled_date.toLocaleString(
|
||||
luxon.DateTime.DATETIME_SHORT
|
||||
);
|
||||
}
|
||||
|
||||
get truncatedMessage() {
|
||||
return (
|
||||
this.props.scheduledMessage.textContent.substring(
|
||||
0,
|
||||
SCHEDULED_MESSAGE_TRUNCATE_THRESHOLD
|
||||
) + "..."
|
||||
);
|
||||
}
|
||||
|
||||
async cancel() {
|
||||
const thread = this.props.scheduledMessage.thread;
|
||||
await this.props.scheduledMessage.cancel();
|
||||
this.props.onScheduledMessageChanged(thread);
|
||||
}
|
||||
|
||||
onClick(ev) {
|
||||
this.props.scheduledMessage.store.handleClickOnLink(ev, this.props.scheduledMessage.thread);
|
||||
}
|
||||
|
||||
async onClickAttachmentUnlink(attachment) {
|
||||
attachment.remove();
|
||||
}
|
||||
|
||||
onClickAuthor(ev) {
|
||||
if (!this.avatarCard.isOpen) {
|
||||
this.avatarCard.open(ev.currentTarget, {
|
||||
id: this.props.scheduledMessage.author_id.main_user_id?.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onClickCancel() {
|
||||
this.dialogService.add(ConfirmationDialog, {
|
||||
body: _t("Are you sure you want to cancel the scheduled message?"),
|
||||
cancel: () => {},
|
||||
cancelLabel: _t("Close"),
|
||||
confirm: this.cancel.bind(this),
|
||||
confirmLabel: _t("Cancel Message"),
|
||||
});
|
||||
}
|
||||
|
||||
async onClickEdit() {
|
||||
await this.props.scheduledMessage.edit();
|
||||
this.props.onScheduledMessageChanged(this.props.scheduledMessage.thread);
|
||||
}
|
||||
|
||||
async onClickSendNow() {
|
||||
await this.props.scheduledMessage.send();
|
||||
this.props.onScheduledMessageChanged(this.props.scheduledMessage.thread);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.ScheduledMessage">
|
||||
<div class="o-mail-Scheduled-Message py-1 mb-2">
|
||||
<div class="o-mail-Message-core position-relative d-flex flex-shrink-0">
|
||||
<div class="o-mail-Message-sidebar d-flex flex-shrink-0">
|
||||
<div class="o-mail-Message-avatarContainer position-relative bg-view" t-on-click="ev => this.onClickAuthor(ev)">
|
||||
<img class="o-mail-Message-avatar w-100 h-100 rounded o_redirect cursor-pointer object-fit-cover" t-att-src="props.scheduledMessage.author_id.avatarUrl"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-100 o-min-width-0">
|
||||
<div class="o-mail-Message-header d-flex flex-wrap align-items-baseline mb-1 lh-1">
|
||||
<span class="o-mail-Message-author cursor-pointer o-hover-text-underline" t-on-click="ev => this.onClickAuthor(ev)">
|
||||
<strong class="me-1 text-truncate o_redirect cursor-pointer" t-out="props.scheduledMessage.author_id.name"/>
|
||||
</span>
|
||||
<small class="o-mail-Message-date text-muted opacity-75" t-att-title="scheduledDate">
|
||||
<i class="fa fa-paper-plane-o mx-1"/>
|
||||
<RelativeTime datetime="props.scheduledMessage.scheduled_date"/>
|
||||
</small>
|
||||
</div>
|
||||
<div class="position-relative d-flex justify-content-end">
|
||||
<div class="o-mail-Message-content o-min-width-0 w-100">
|
||||
<div class="o-mail-Message-textContent position-relative d-flex">
|
||||
<div class="position-relative overflow-x-auto d-inline-block">
|
||||
<div class="o-mail-Message-bubble rounded-end-3 rounded-bottom-3 position-absolute top-0 start-0 w-100 h-100"
|
||||
t-att-class="{
|
||||
'bg-success-light opacity-25': !props.scheduledMessage.is_note and props.scheduledMessage.isSelfAuthored,
|
||||
'bg-info-light opacity-25': !props.scheduledMessage.is_note and !props.scheduledMessage.isSelfAuthored
|
||||
}"/>
|
||||
<div t-on-click="onClick"
|
||||
t-attf-class="position-relative text-break o-mail-Message-body #{props.scheduledMessage.is_note ? 'p-1' : 'mb-0 py-2 px-3 align-self-start'}">
|
||||
<t t-if="props.scheduledMessage.subject and !props.scheduledMessage.isSubjectThreadName">
|
||||
<em class="mb-1 me-2">Subject: <t t-esc="props.scheduledMessage.subject"/></em>
|
||||
</t>
|
||||
<t t-if="isShort or state.readMore" t-out="props.scheduledMessage.body || ''"/>
|
||||
<p t-else="" t-out="truncatedMessage"/>
|
||||
<button t-if="!isShort" t-attf-class="btn btn-link #{state.readMore ? '' : 'ps-0'}" t-on-click="() => this.state.readMore = !this.state.readMore">
|
||||
<t t-if="state.readMore">
|
||||
Read Less
|
||||
</t>
|
||||
<t t-else="">
|
||||
Read More
|
||||
</t>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AttachmentList
|
||||
t-if="props.scheduledMessage.attachment_ids.length > 0"
|
||||
attachments="props.scheduledMessage.attachment_ids.map((a) => a)"
|
||||
unlinkAttachment.bind="onClickAttachmentUnlink"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o-mail-Scheduled-Message-buttons ps-1 d-flex">
|
||||
<span class="btn btn-link btn-success p-0 me-3" t-if="props.scheduledMessage.editable" t-on-click="onClickSendNow">
|
||||
<i class="fa fa-send"/> Send Now
|
||||
</span>
|
||||
<span class="btn btn-link text-action p-0 me-3" t-if="props.scheduledMessage.editable" t-on-click="onClickEdit">
|
||||
<i class="fa fa-pencil"/> Edit
|
||||
</span>
|
||||
<span class="btn btn-link btn-danger p-0" t-if="props.scheduledMessage.deletable" t-on-click="onClickCancel">
|
||||
<i class="fa fa-times"/> Cancel
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
import { fields, Record } from "@mail/core/common/record";
|
||||
import { htmlToTextContentInline } from "@mail/utils/common/format";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
|
||||
export class ScheduledMessage extends Record {
|
||||
static _name = "mail.scheduled.message";
|
||||
static id = "id";
|
||||
/** @type {Object.<number, import("models").ScheduledMessage>} */
|
||||
static records = {};
|
||||
/** @returns {import("models").ScheduledMessage} */
|
||||
static get(data) {
|
||||
return super.get(data);
|
||||
}
|
||||
/** @type {number} */
|
||||
id;
|
||||
attachment_ids = fields.Many("ir.attachment");
|
||||
author_id = fields.One("res.partner");
|
||||
body = fields.Html("");
|
||||
/** @type {boolean} */
|
||||
composition_batch;
|
||||
scheduled_date = fields.Datetime();
|
||||
/** @type {boolean} */
|
||||
is_note;
|
||||
textContent = fields.Attr(false, {
|
||||
compute() {
|
||||
if (!this.body) {
|
||||
return "";
|
||||
}
|
||||
return htmlToTextContentInline(this.body);
|
||||
},
|
||||
});
|
||||
thread = fields.One("Thread");
|
||||
// Editors of the records can delete scheduled messages
|
||||
get deletable() {
|
||||
return this.store.self.main_user_id?.is_admin || this.thread.hasWriteAccess;
|
||||
}
|
||||
|
||||
get editable() {
|
||||
return this.store.self.main_user_id?.is_admin || this.isSelfAuthored;
|
||||
}
|
||||
|
||||
get isSelfAuthored() {
|
||||
return this.author_id.eq(this.store.self);
|
||||
}
|
||||
|
||||
get isSubjectThreadName() {
|
||||
return (
|
||||
this.thread.display_name?.trim().toLowerCase() === this.subject?.trim().toLowerCase()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the scheduled message.
|
||||
*/
|
||||
async cancel() {
|
||||
await this.store.env.services.orm.unlink("mail.scheduled.message", [this.id]);
|
||||
this.delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the mail_compose_mesage form view to allow edition of the scheduled message.
|
||||
* If the message has already been sent, displays a notification instead.
|
||||
*/
|
||||
async edit() {
|
||||
let action;
|
||||
try {
|
||||
action = await this.store.env.services.orm.call(
|
||||
"mail.scheduled.message",
|
||||
"open_edit_form",
|
||||
[this.id]
|
||||
);
|
||||
} catch {
|
||||
this.notifyAlreadySent();
|
||||
return;
|
||||
}
|
||||
return new Promise((resolve) =>
|
||||
this.store.env.services.action.doAction(action, { onClose: resolve })
|
||||
);
|
||||
}
|
||||
|
||||
notifyAlreadySent() {
|
||||
this.store.env.services.notification.add(_t("This message has already been sent."), {
|
||||
type: "warning",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the scheduled message directly
|
||||
*/
|
||||
async send() {
|
||||
try {
|
||||
await this.store.env.services.orm.call("mail.scheduled.message", "post_message", [
|
||||
this.id,
|
||||
]);
|
||||
} catch {
|
||||
// already sent (by someone else or by cron)
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScheduledMessage.register();
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import { fields } from "@mail/core/common/record";
|
||||
import { Thread } from "@mail/core/common/thread_model";
|
||||
import { compareDatetime } from "@mail/utils/common/misc";
|
||||
import "@mail/chatter/web_portal/thread_model_patch";
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
/** @type {import("models").Thread} */
|
||||
const threadPatch = {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.scheduledMessages = fields.Many("mail.scheduled.message", {
|
||||
sort: (a, b) => compareDatetime(a.scheduled_date, b.scheduled_date) || a.id - b.id,
|
||||
inverse: "thread",
|
||||
});
|
||||
},
|
||||
|
||||
/** @param {string[]} requestList */
|
||||
async fetchThreadData(requestList) {
|
||||
this.isLoadingAttachments =
|
||||
this.isLoadingAttachments || requestList.includes("attachments");
|
||||
await super.fetchThreadData(requestList);
|
||||
if (!this.message_main_attachment_id && this.attachmentsInWebClientView.length > 0) {
|
||||
this.setMainAttachmentFromIndex(0);
|
||||
}
|
||||
},
|
||||
};
|
||||
patch(Thread.prototype, threadPatch);
|
||||
5
odoo-bringout-oca-ocb-mail/mail/static/src/chatter/web_portal/@types/models.d.ts
vendored
Normal file
5
odoo-bringout-oca-ocb-mail/mail/static/src/chatter/web_portal/@types/models.d.ts
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
declare module "models" {
|
||||
export interface Thread {
|
||||
fetchThreadData: (requestList: string[]) => Promise<void>;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
import { Composer } from "@mail/core/common/composer";
|
||||
import { Thread } from "@mail/core/common/thread";
|
||||
|
||||
import {
|
||||
Component,
|
||||
onMounted,
|
||||
onWillUpdateProps,
|
||||
useChildSubEnv,
|
||||
useRef,
|
||||
useState,
|
||||
} from "@odoo/owl";
|
||||
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { useThrottleForAnimation } from "@web/core/utils/timing";
|
||||
|
||||
/**
|
||||
* @typedef {Object} Props
|
||||
* @extends {Component<Props, Env>}
|
||||
*/
|
||||
export class Chatter extends Component {
|
||||
static template = "mail.Chatter";
|
||||
static components = { Thread, Composer };
|
||||
static props = ["composer?", "threadId?", "threadModel", "twoColumns?"];
|
||||
static defaultProps = { composer: true, threadId: false, twoColumns: false };
|
||||
|
||||
setup() {
|
||||
this.store = useService("mail.store");
|
||||
this.state = useState({
|
||||
jumpThreadPresent: 0,
|
||||
/** @type {import("models").Thread} */
|
||||
thread: undefined,
|
||||
aside: false,
|
||||
disabled: !this.props.threadId,
|
||||
});
|
||||
this.rootRef = useRef("root");
|
||||
this.onScrollDebounced = useThrottleForAnimation(this.onScroll);
|
||||
useChildSubEnv(this.childSubEnv);
|
||||
|
||||
onMounted(this._onMounted);
|
||||
onWillUpdateProps((nextProps) => {
|
||||
this.state.disabled = !nextProps.threadId;
|
||||
if (
|
||||
this.props.threadId !== nextProps.threadId ||
|
||||
this.props.threadModel !== nextProps.threadModel
|
||||
) {
|
||||
this.changeThread(nextProps.threadModel, nextProps.threadId);
|
||||
}
|
||||
if (!this.env.chatter || this.env.chatter?.fetchThreadData) {
|
||||
if (this.env.chatter) {
|
||||
this.env.chatter.fetchThreadData = false;
|
||||
}
|
||||
this.load(this.state.thread, this.requestList);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get afterPostRequestList() {
|
||||
return ["messages"];
|
||||
}
|
||||
|
||||
get childSubEnv() {
|
||||
return { inChatter: this.state };
|
||||
}
|
||||
|
||||
get onCloseFullComposerRequestList() {
|
||||
return ["messages"];
|
||||
}
|
||||
|
||||
get requestList() {
|
||||
return [];
|
||||
}
|
||||
|
||||
changeThread(threadModel, threadId) {
|
||||
this.state.thread = this.store.Thread.insert({ model: threadModel, id: threadId });
|
||||
if (threadId === false) {
|
||||
if (this.state.thread.messages.length === 0) {
|
||||
this.state.thread.messages.push({
|
||||
id: this.store.getNextTemporaryId(),
|
||||
author_id: this.state.thread.effectiveSelf,
|
||||
body: _t("Creating a new record..."),
|
||||
message_type: "notification",
|
||||
thread: this.state.thread,
|
||||
trackingValues: [],
|
||||
res_id: threadId,
|
||||
model: threadModel,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch data for the thread according to the request list.
|
||||
* @param {import("models").Thread} thread
|
||||
* @param {string[]} requestList
|
||||
*/
|
||||
async load(thread, requestList) {
|
||||
if (!thread.id || !this.state.thread?.eq(thread)) {
|
||||
return;
|
||||
}
|
||||
await thread.fetchThreadData(requestList);
|
||||
}
|
||||
|
||||
onCloseFullComposerCallback() {
|
||||
this.load(this.state.thread, this.onCloseFullComposerRequestList);
|
||||
}
|
||||
|
||||
_onMounted() {
|
||||
this.changeThread(this.props.threadModel, this.props.threadId);
|
||||
if (!this.env.chatter || this.env.chatter?.fetchThreadData) {
|
||||
if (this.env.chatter) {
|
||||
this.env.chatter.fetchThreadData = false;
|
||||
}
|
||||
this.load(this.state.thread, this.requestList);
|
||||
}
|
||||
}
|
||||
|
||||
onPostCallback() {
|
||||
this.state.jumpThreadPresent++;
|
||||
// Load new messages to fetch potential new messages from other users (useful due to lack of auto-sync in chatter).
|
||||
this.load(this.state.thread, this.afterPostRequestList);
|
||||
}
|
||||
|
||||
onScroll() {
|
||||
this.state.isTopStickyPinned = this.rootRef.el.scrollTop !== 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
.o-mail-Chatter .o-mail-Message-body [summary~="o_mail_notification"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.o-mail-Chatter-top {
|
||||
z-index: $o-mail-NavigableList-zIndex - 2;
|
||||
background-color: $o-webclient-background-color;
|
||||
|
||||
&.shadow-sm {
|
||||
background-image: linear-gradient(90deg, transparent, $o-view-background-color, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.o-mail-Chatter-top, .o-mail-Chatter-content {
|
||||
--Chatter-default-padding-x: #{$o-spacer};
|
||||
|
||||
padding-right: var(--Chatter-default-padding-x);
|
||||
}
|
||||
|
||||
.o-mail-Chatter-top .o-mail-Composer {
|
||||
--Chatter-default-padding-x: #{$o-spacer};
|
||||
--mail-Chatter-contentPaddingLeft: var(--ChatterAsideForm-padding-left, calc(var(--Chatter-default-padding-x) / 2));
|
||||
margin-left: calc((var(--mail-Chatter-contentPaddingLeft)) * -1);
|
||||
}
|
||||
|
||||
.o-mail-Chatter-top {
|
||||
padding-left: var(--ChatterAsideForm-padding-left, var(--Chatter-default-padding-x));
|
||||
}
|
||||
|
||||
.o-mail-Chatter-content {
|
||||
--mail-Chatter-contentPaddingLeft: var(--ChatterAsideForm-padding-left, var(--Chatter-default-padding-x));
|
||||
padding-left: calc(var(--mail-Chatter-contentPaddingLeft) - #{($o-mail-Message-sidebarWidth - $o-mail-Avatar-size) / 2});
|
||||
}
|
||||
|
||||
.o-mail-Followers-button:focus {
|
||||
background-color: $gray-200;
|
||||
}
|
||||
|
||||
.o-mail-Followers-dropdown {
|
||||
/**
|
||||
* Note: Min() refers to CSS min() and not SCSS min().
|
||||
*
|
||||
* To by-pass SCSS min() shadowing CSS min(), we rely on SCSS being case-sensitive while CSS isn't.
|
||||
*/
|
||||
max-width: Min(400px, 95vw);
|
||||
max-height: Min(500px, 50vh);
|
||||
}
|
||||
|
||||
.o-mail-Follower-avatar {
|
||||
--Avatar-size: #{$o-mail-Avatar-sizeSmall};
|
||||
}
|
||||
|
||||
.o-mail-Follower:has(.o-mail-Follower-action:hover), .o-mail-FollowerList-unfollow:has(.o-mail-Follower-action:hover) {
|
||||
background-color: inherit !important;
|
||||
}
|
||||
|
||||
.btn.o-mail-Chatter-follow:hover {
|
||||
color: $black !important;
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { Composer } from "@mail/core/common/composer";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(Composer.prototype, {
|
||||
get placeholder() {
|
||||
if (this.thread && this.thread.model !== "discuss.channel" && !this.props.placeholder) {
|
||||
if (this.props.type === "message") {
|
||||
return _t("Send a message to followers…");
|
||||
} else {
|
||||
return _t("Log an internal note…");
|
||||
}
|
||||
}
|
||||
return super.placeholder;
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import { Thread } from "@mail/core/common/thread_model";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(Thread.prototype, {
|
||||
/** @param {string[]} requestList */
|
||||
async fetchThreadData(requestList) {
|
||||
if (requestList.includes("messages")) {
|
||||
this.fetchNewMessages();
|
||||
}
|
||||
await this.store.fetchStoreData("mail.thread", {
|
||||
access_params: this.rpcParams,
|
||||
request_list: requestList,
|
||||
thread_id: this.id,
|
||||
thread_model: this.model,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { clear } from '@mail/model/model_field_command';
|
||||
|
||||
const { onWillUpdateProps, useComponent } = owl;
|
||||
|
||||
/**
|
||||
* This hook provides support for saving the reference of the component directly
|
||||
* into the field of a record, and appropriately updates it when necessary
|
||||
* (props change or destroy).
|
||||
*
|
||||
* @param {Object} param0
|
||||
* @param {string} param0.fieldName Name of the field on the target record.
|
||||
*/
|
||||
export function useComponentToModel({ fieldName }) {
|
||||
const component = useComponent();
|
||||
component.props.record.update({ [fieldName]: component });
|
||||
onWillUpdateProps(nextProps => {
|
||||
const currentRecord = component.props.record;
|
||||
const nextRecord = nextProps.record;
|
||||
if (currentRecord.exists() && currentRecord !== nextRecord) {
|
||||
currentRecord.update({ [fieldName]: clear() });
|
||||
}
|
||||
nextRecord.update({ [fieldName]: component });
|
||||
});
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { Listener } from '@mail/model/model_listener';
|
||||
|
||||
const { onRendered, onWillDestroy, onWillRender, useComponent } = owl;
|
||||
|
||||
/**
|
||||
* This hook provides support for automatically re-rendering when used records
|
||||
* or fields changed.
|
||||
*
|
||||
* Components that use this hook must be instantiated after messaging service is
|
||||
* started. However there is no restriction on the messaging record (coming from
|
||||
* the modelManager of the messaging service) being already initialized or even
|
||||
* created.
|
||||
*/
|
||||
export function useModels() {
|
||||
const component = useComponent();
|
||||
const listener = new Listener({
|
||||
isLocking: false, // unfortunately __render has side effects such as children components updating their reference to their corresponding model
|
||||
name: `useModels() of ${component}`,
|
||||
onChange: () => component.render(),
|
||||
});
|
||||
onWillRender(() => {
|
||||
component.env.services.messaging.modelManager.startListening(listener);
|
||||
});
|
||||
onRendered(() => {
|
||||
component.env.services.messaging.modelManager.stopListening(listener);
|
||||
});
|
||||
onWillDestroy(() => {
|
||||
component.env.services.messaging.modelManager.removeListener(listener);
|
||||
});
|
||||
component.env.services.messaging.modelManager.messagingCreatedPromise.then(() => {
|
||||
component.render();
|
||||
});
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { clear } from '@mail/model/model_field_command';
|
||||
|
||||
const { onWillUpdateProps, useComponent, useRef } = owl;
|
||||
|
||||
/**
|
||||
* This hook provides support for saving the result of useRef directly into the
|
||||
* field of a record, and appropriately updates it when necessary (props change
|
||||
* or destroy).
|
||||
*
|
||||
* @param {Object} param0
|
||||
* @param {string} param0.fieldName Name of the field on the target record.
|
||||
* @param {string} param0.refName Name of the t-ref on this component.
|
||||
*/
|
||||
export function useRefToModel({ fieldName, refName }) {
|
||||
const component = useComponent();
|
||||
const ref = useRef(refName);
|
||||
component.props.record.update({ [fieldName]: ref });
|
||||
onWillUpdateProps(nextProps => {
|
||||
const currentRecord = component.props.record;
|
||||
const nextRecord = nextProps.record;
|
||||
if (currentRecord.exists() && currentRecord !== nextRecord) {
|
||||
currentRecord.update({ [fieldName]: clear() });
|
||||
}
|
||||
nextRecord.update({ [fieldName]: ref });
|
||||
});
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
const { useComponent } = owl;
|
||||
|
||||
/**
|
||||
* This hook provides support for dynamic-refs.
|
||||
*
|
||||
* @returns {function} returns object whose keys are t-ref values of active refs.
|
||||
* and values are refs.
|
||||
*/
|
||||
export function useRefs() {
|
||||
const component = useComponent();
|
||||
return function () {
|
||||
return component.__owl__.refs || {};
|
||||
};
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { Listener } from '@mail/model/model_listener';
|
||||
|
||||
const { onMounted, onPatched, onWillDestroy, onWillRender, useComponent } = owl;
|
||||
|
||||
/**
|
||||
* This hooks provides support for accessing the values returned by the given
|
||||
* selector at the time of the last render. The values will be updated after
|
||||
* every mount/patch.
|
||||
*
|
||||
* @param {function} selector function that will be executed at the time of the
|
||||
* render and of which the result will be stored for future reference.
|
||||
* @returns {function} function to call to retrieve the last rendered values.
|
||||
*/
|
||||
export function useRenderedValues(selector) {
|
||||
const component = useComponent();
|
||||
let renderedValues;
|
||||
let patchedValues;
|
||||
const listener = new Listener({
|
||||
name: `useRenderedValues() of ${component}`,
|
||||
onChange: () => component.render(),
|
||||
});
|
||||
onWillRender(() => {
|
||||
component.env.services.messaging.modelManager.startListening(listener);
|
||||
renderedValues = selector();
|
||||
component.env.services.messaging.modelManager.stopListening(listener);
|
||||
});
|
||||
onMounted(onUpdate);
|
||||
onPatched(onUpdate);
|
||||
function onUpdate() {
|
||||
patchedValues = renderedValues;
|
||||
}
|
||||
onWillDestroy(() => component.env.services.messaging.modelManager.removeListener(listener));
|
||||
return () => patchedValues;
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { Listener } from '@mail/model/model_listener';
|
||||
|
||||
const { onMounted, onPatched, onWillDestroy, useComponent } = owl;
|
||||
|
||||
/**
|
||||
* This hook provides support for executing code after update (render or patch).
|
||||
*
|
||||
* @param {Object} param0
|
||||
* @param {function} param0.func the function to execute after the update.
|
||||
*/
|
||||
export function useUpdate({ func }) {
|
||||
const component = useComponent();
|
||||
const listener = new Listener({
|
||||
isLocking: false, // unfortunately onUpdate methods often have side effect
|
||||
name: `useUpdate() of ${component}`,
|
||||
onChange: () => component.render(),
|
||||
});
|
||||
function onUpdate() {
|
||||
component.env.services.messaging.modelManager.startListening(listener);
|
||||
func();
|
||||
component.env.services.messaging.modelManager.stopListening(listener);
|
||||
}
|
||||
onMounted(onUpdate);
|
||||
onPatched(onUpdate);
|
||||
onWillDestroy(() => {
|
||||
component.env.services.messaging.modelManager.removeListener(listener);
|
||||
});
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { useUpdate } from '@mail/component_hooks/use_update';
|
||||
|
||||
const { useComponent } = owl;
|
||||
|
||||
/**
|
||||
* This hook provides support for binding the onMounted/onPatched hooks to the
|
||||
* method of a target record.
|
||||
*
|
||||
* @param {Object} param0
|
||||
* @param {string} param0.methodName Name of the method on the target record.
|
||||
*/
|
||||
export function useUpdateToModel({ methodName }) {
|
||||
const component = useComponent();
|
||||
useUpdate({ func: () => {
|
||||
component.props.record[methodName]();
|
||||
} });
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { useComponentToModel } from '@mail/component_hooks/use_component_to_model';
|
||||
import { useRefToModel } from '@mail/component_hooks/use_ref_to_model';
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
import Popover from "web.Popover";
|
||||
import { LegacyComponent } from "@web/legacy/legacy_component";
|
||||
|
||||
export class Activity extends LegacyComponent {
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
setup() {
|
||||
super.setup();
|
||||
useComponentToModel({ fieldName: 'component' });
|
||||
useRefToModel({ fieldName: 'markDoneButtonRef', refName: 'markDoneButton', });
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {ActivityView}
|
||||
*/
|
||||
get activityView() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(Activity, {
|
||||
props: { record: Object },
|
||||
template: 'mail.Activity',
|
||||
components: { Popover },
|
||||
});
|
||||
|
||||
registerMessagingComponent(Activity);
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
// ------------------------------------------------------------------
|
||||
// Layout
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.o_Activity_core {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.o_Activity_detailsUserAvatar {
|
||||
object-fit: cover;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
.o_Activity_iconContainer {
|
||||
@include o-position-absolute($top: auto, $left: auto, $bottom: -16%, $right: -5%);
|
||||
}
|
||||
|
||||
.o_Activity_note p {
|
||||
margin-bottom: map-get($spacers, 0);
|
||||
}
|
||||
|
||||
.o_Activity_sidebar {
|
||||
width: $o-mail-thread-avatar-size;
|
||||
min-width: $o-mail-thread-avatar-size;
|
||||
height: $o-mail-thread-avatar-size;
|
||||
}
|
||||
|
||||
// From python template
|
||||
.o_mail_note_title {
|
||||
margin-top: map-get($spacers, 2);
|
||||
}
|
||||
|
||||
.o_mail_note_title + div p {
|
||||
margin-bottom: map-get($spacers, 0);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Style
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
.o_Activity_detailsButton {
|
||||
@include o-hover-text-color($default-color: $o-main-color-muted);
|
||||
}
|
||||
|
||||
.o_Activity_iconContainer {
|
||||
box-shadow: 0 0 0 2px $o-view-background-color;
|
||||
}
|
||||
|
|
@ -1,145 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="mail.Activity" owl="1">
|
||||
<t t-if="activityView">
|
||||
<div class="o_Activity d-flex py-2 px-3" t-attf-class="{{ className }}" t-on-click="activityView.onClickActivity" t-ref="root">
|
||||
<div class="o_Activity_sidebar me-3">
|
||||
<div class="o_Activity_user position-relative h-100 w-100">
|
||||
<t t-if="activityView.activity.assignee">
|
||||
<img class="o_Activity_userAvatar rounded-circle h-100 w-100 o_object_fit_cover" t-attf-src="/web/image/res.users/{{ activityView.activity.assignee.id }}/avatar_128" t-att-alt="activityView.activity.assignee.nameOrDisplayName"/>
|
||||
</t>
|
||||
<div class="o_Activity_iconContainer d-flex align-items-center justify-content-center rounded-circle w-50 h-50"
|
||||
t-att-class="{
|
||||
'text-bg-success': activityView.activity.state === 'planned',
|
||||
'text-bg-warning': activityView.activity.state === 'today',
|
||||
'text-bg-danger': activityView.activity.state === 'overdue',
|
||||
}"
|
||||
>
|
||||
<i class="o_Activity_icon fa small" t-attf-class="{{ activityView.activity.icon }}"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_Activity_core">
|
||||
<div class="o_Activity_info d-flex align-items-baseline">
|
||||
<div class="o_Activity_dueDateText me-2"
|
||||
t-att-class="{
|
||||
'text-danger': activityView.activity.state === 'overdue',
|
||||
'text-success': activityView.activity.state === 'planned',
|
||||
'text-warning': activityView.activity.state === 'today',
|
||||
}"
|
||||
>
|
||||
<b t-esc="activityView.delayLabel"/>
|
||||
</div>
|
||||
<t t-if="activityView.activity.summary">
|
||||
<b class="o_Activity_summary text-900 me-2">
|
||||
<t t-esc="activityView.summary"/>
|
||||
</b>
|
||||
</t>
|
||||
<t t-elif="activityView.activity.type">
|
||||
<b class="o_Activity_summary o_Activity_type text-900 me-2">
|
||||
<t t-esc="activityView.activity.type.displayName"/>
|
||||
</b>
|
||||
</t>
|
||||
<t t-if="activityView.activity.assignee">
|
||||
<div class="o_Activity_userName">
|
||||
<t t-esc="activityView.assignedUserText"/>
|
||||
</div>
|
||||
</t>
|
||||
<a
|
||||
href="#"
|
||||
class="o_Activity_detailsButton btn py-0"
|
||||
t-att-class="activityView.areDetailsVisible ? 'text-primary' : 'btn-link btn-primary'"
|
||||
t-att-aria-expanded="activityView.areDetailsVisible ? 'true' : 'false'"
|
||||
t-on-click="activityView.onClickDetailsButton"
|
||||
role="button"
|
||||
>
|
||||
<i class="fa fa-info-circle" role="img" title="Info" aria-label="Info"/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<t t-if="activityView.areDetailsVisible">
|
||||
<div class="o_Activity_details">
|
||||
<div class="d-md-table table table-sm mt-2 mb-3">
|
||||
<div t-if="activityView.activity.type" class="d-md-table-row mb-3">
|
||||
<div class="d-md-table-cell fw-bold text-md-end m-0 py-md-1 px-md-4">Activity type</div>
|
||||
<div class="o_Activity_type d-md-table-cell py-md-1 pe-4">
|
||||
<t t-esc="activityView.activity.type.displayName"/>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="activityView.activity.creator" class="d-md-table-row mb-3">
|
||||
<div class="d-md-table-cell fw-bold text-md-end m-0 py-md-1 px-md-4">Created</div>
|
||||
<div class="o_Activity_detailsCreation d-md-table-cell py-md-1 pe-4">
|
||||
<t t-esc="activityView.formattedCreateDatetime"/>, <br t-if="messaging.device.isSmall"/>by
|
||||
<img class="o_Activity_detailsUserAvatar o_Activity_detailsCreatorAvatar ms-1 me-1 rounded-circle align-text-bottom p-0" t-attf-src="/web/image/res.users/{{ activityView.activity.creator.id }}/avatar_128" t-att-title="activityView.activity.creator.nameOrDisplayName" t-att-alt="activityView.activity.creator.nameOrDisplayName"/>
|
||||
<b class="o_Activity_detailsCreator">
|
||||
<t t-esc="activityView.activity.creator.nameOrDisplayName"/>
|
||||
</b>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="activityView.activity.assignee" class="d-md-table-row mb-3">
|
||||
<div class="d-md-table-cell fw-bold text-md-end m-0 py-md-1 px-md-4">Assigned to</div>
|
||||
<div class="o_Activity_detailsAssignation d-md-table-cell py-md-1 pe-4">
|
||||
<img class="o_Activity_detailsUserAvatar o_Activity_detailsAssignationUserAvatar me-1 rounded-circle align-text-bottom p-0" t-attf-src="/web/image/res.users/{{ activityView.activity.assignee.id }}/avatar_128" t-att-title="activityView.activity.assignee.nameOrDisplayName" t-att-alt="activityView.activity.assignee.nameOrDisplayName"/>
|
||||
<b t-esc="activityView.activity.assignee.nameOrDisplayName"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-md-table-row">
|
||||
<div class="d-md-table-cell fw-bold text-md-end m-0 py-md-1 px-md-4">Due on</div>
|
||||
<div class="o_Activity_detailsDueDate d-md-table-cell py-md-1 pe-4">
|
||||
<span class="o_Activity_deadlineDateText"
|
||||
t-att-class="{
|
||||
'text-danger': activityView.activity.state === 'overdue',
|
||||
'text-success': activityView.activity.state === 'planned',
|
||||
'text-warning': activityView.activity.state === 'today',
|
||||
}"
|
||||
>
|
||||
<t t-esc="activityView.formattedDeadlineDate"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-if="activityView.activity.note">
|
||||
<div class="o_Activity_note">
|
||||
<t t-out="activityView.activity.noteAsMarkup"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-if="activityView.mailTemplateViews.length > 0">
|
||||
<div class="o_Activity_mailTemplates">
|
||||
<t t-foreach="activityView.mailTemplateViews" t-as="mailTemplateView" t-key="mailTemplateView.localId">
|
||||
<MailTemplate
|
||||
className="'o_Activity_mailTemplate'"
|
||||
record="mailTemplateView"
|
||||
/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-if="activityView.activity.canWrite">
|
||||
<div name="tools" class="o_Activity_tools d-flex">
|
||||
<button class="o_Activity_toolButton o_Activity_markDoneButton btn btn-link btn-primary pt-0 ps-0" t-att-title="activityView.markDoneText" t-ref="markDoneButton" t-on-click="activityView.onClickMarkDoneButton">
|
||||
<i class="fa fa-check"/> Mark Done
|
||||
</button>
|
||||
<t t-if="activityView.fileUploader">
|
||||
<button class="o_Activity_toolButton o_Activity_uploadButton btn btn-link btn-primary pt-0 ps-0" t-on-click="activityView.onClickUploadDocument">
|
||||
<i class="fa fa-upload"/> Upload Document
|
||||
</button>
|
||||
</t>
|
||||
<button class="o_Activity_toolButton o_Activity_editButton btn btn-link btn-primary pt-0" t-on-click="activityView.onClickEdit">
|
||||
<i class="fa fa-pencil"/> Edit
|
||||
</button>
|
||||
<button class="o_Activity_toolButton o_Activity_cancelButton btn btn-link btn-primary pt-0" t-on-click="activityView.onClickCancel" >
|
||||
<i class="fa fa-times"/> Cancel
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registerMessagingComponent } from '@mail/utils/messaging_component';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class ActivityBox extends Component {
|
||||
|
||||
/**
|
||||
* @returns {ActivityBoxView}
|
||||
*/
|
||||
get activityBoxView() {
|
||||
return this.props.record;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Object.assign(ActivityBox, {
|
||||
props: { record: Object },
|
||||
template: 'mail.ActivityBox',
|
||||
});
|
||||
|
||||
registerMessagingComponent(ActivityBox);
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue