mirror of
https://github.com/bringout/oca-ocb-project.git
synced 2026-04-20 04:02:00 +02:00
Initial commit: Project packages
This commit is contained in:
commit
89613c97b0
753 changed files with 496325 additions and 0 deletions
|
|
@ -0,0 +1,69 @@
|
|||
.o_FormRenderer_chatterContainer {
|
||||
display: flex;
|
||||
background-color: $white;
|
||||
border-color: $border-color;
|
||||
|
||||
.o_portal_chatter {
|
||||
width: 100%;
|
||||
|
||||
.o_portal_chatter_header {
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
div.o_portal_chatter_composer,
|
||||
div.o_portal_chatter_messages {
|
||||
div.d-flex {
|
||||
gap: 10px;
|
||||
|
||||
.o_portal_chatter_attachments {
|
||||
margin-bottom: 1rem;
|
||||
.o_portal_chatter_attachment {
|
||||
> button {
|
||||
@include o-position-absolute($top: 0, $right: 0);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover > button {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_portal_chatter_avatar {
|
||||
width: 45px;
|
||||
height: 45px;
|
||||
}
|
||||
|
||||
.o_portal_message_internal_off {
|
||||
.btn-danger {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.o_portal_message_internal_on {
|
||||
.btn-success {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.o-aside {
|
||||
flex-direction: column;
|
||||
|
||||
.o_portal_chatter {
|
||||
.o_portal_chatter_header {
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
|
||||
.o_portal_chatter_composer, .o_portal_chatter_messages {
|
||||
margin-left: 1rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
/** @odoo-module */
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class ChatterAttachmentsViewer extends Component {}
|
||||
|
||||
ChatterAttachmentsViewer.template = 'project.ChatterAttachmentsViewer';
|
||||
ChatterAttachmentsViewer.props = {
|
||||
attachments: Array,
|
||||
canDelete: { type: Boolean, optional: true },
|
||||
delete: { type: Function, optional: true },
|
||||
};
|
||||
ChatterAttachmentsViewer.defaultProps = {
|
||||
delete: async () => {},
|
||||
};
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="project.ChatterAttachmentsViewer" owl="1">
|
||||
<div class="o_portal_chatter_attachments mt-3">
|
||||
<div t-if="props.attachments.length" class="row">
|
||||
<div t-foreach="props.attachments" t-as="attachment" t-key="attachment.id" class="col-lg-3 col-md-4 col-sm-6">
|
||||
<div class="o_portal_chatter_attachment mb-2 position-relative text-center">
|
||||
<button
|
||||
t-if="props.canDelete and attachment.state == 'pending'"
|
||||
class="btn btn-sm btn-outline-danger"
|
||||
title="Delete"
|
||||
t-on-click="() => props.delete(attachment)"
|
||||
>
|
||||
<i class="fa fa-times"/>
|
||||
</button>
|
||||
<a t-attf-href="/web/content/#{attachment.id}?download=true&access_token=#{attachment.access_token}" target="_blank">
|
||||
<div class='oe_attachment_embedded o_image' t-att-title="attachment.name" t-att-data-mimetype="attachment.mimetype"/>
|
||||
<div class='o_portal_chatter_attachment_name'>
|
||||
<t t-out='attachment.filename'/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { useService } from '@web/core/utils/hooks';
|
||||
import { TextField } from '@web/views/fields/text/text_field';
|
||||
import { PortalAttachDocument } from '../portal_attach_document/portal_attach_document';
|
||||
import { ChatterAttachmentsViewer } from './chatter_attachments_viewer';
|
||||
|
||||
const { Component, useState, onWillUpdateProps } = owl;
|
||||
|
||||
export class ChatterComposer extends Component {
|
||||
setup() {
|
||||
this.rpc = useService('rpc');
|
||||
this.state = useState({
|
||||
displayError: false,
|
||||
attachments: this.props.attachments.map(file => file.state === 'done'),
|
||||
message: '',
|
||||
loading: false,
|
||||
});
|
||||
|
||||
onWillUpdateProps(this.onWillUpdateProps);
|
||||
}
|
||||
|
||||
onWillUpdateProps(nextProps) {
|
||||
this.clearErrors();
|
||||
this.state.message = '';
|
||||
this.state.attachments = nextProps.attachments.map(file => file.state === 'done');
|
||||
}
|
||||
|
||||
get discussionUrl() {
|
||||
return `${window.location.href.split('#')[0]}#discussion`;
|
||||
}
|
||||
|
||||
update(change) {
|
||||
this.clearErrors();
|
||||
this.state.message = change;
|
||||
}
|
||||
|
||||
prepareMessageData() {
|
||||
const attachment_ids = [];
|
||||
const attachment_tokens = [];
|
||||
for (const attachment of this.state.attachments) {
|
||||
attachment_ids.push(attachment.id);
|
||||
attachment_tokens.push(attachment.access_token);
|
||||
}
|
||||
return {
|
||||
message: this.state.message,
|
||||
attachment_ids,
|
||||
attachment_tokens,
|
||||
res_model: this.props.resModel,
|
||||
res_id: this.props.resId,
|
||||
project_sharing_id: this.props.projectSharingId,
|
||||
};
|
||||
}
|
||||
|
||||
async sendMessage() {
|
||||
this.clearErrors();
|
||||
if (!this.state.message && !this.state.attachments.length) {
|
||||
this.state.displayError = true;
|
||||
return;
|
||||
}
|
||||
|
||||
await this.rpc(
|
||||
"/mail/chatter_post",
|
||||
this.prepareMessageData(),
|
||||
);
|
||||
this.props.postProcessMessageSent();
|
||||
this.state.message = "";
|
||||
this.state.attachments = [];
|
||||
}
|
||||
|
||||
clearErrors() {
|
||||
this.state.displayError = false;
|
||||
}
|
||||
|
||||
async beforeUploadFile() {
|
||||
this.state.loading = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
onFileUpload(files) {
|
||||
this.state.loading = false;
|
||||
this.clearErrors();
|
||||
for (const file of files) {
|
||||
file.state = 'pending';
|
||||
this.state.attachments.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteAttachment(attachment) {
|
||||
this.clearErrors();
|
||||
try {
|
||||
await this.rpc(
|
||||
'/portal/attachment/remove',
|
||||
{
|
||||
attachment_id: attachment.id,
|
||||
access_token: attachment.access_token,
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.state.displayError = true;
|
||||
}
|
||||
this.state.attachments = this.state.attachments.filter(a => a.id !== attachment.id);
|
||||
}
|
||||
}
|
||||
|
||||
ChatterComposer.components = {
|
||||
ChatterAttachmentsViewer,
|
||||
PortalAttachDocument,
|
||||
TextField,
|
||||
};
|
||||
|
||||
ChatterComposer.props = {
|
||||
resModel: String,
|
||||
projectSharingId: Number,
|
||||
resId: { type: Number, optional: true },
|
||||
allowComposer: { type: Boolean, optional: true },
|
||||
displayComposer: { type: Boolean, optional: true },
|
||||
token: { type: String, optional: true },
|
||||
messageCount: { type: Number, optional: true },
|
||||
isUserPublic: { type: Boolean, optional: true },
|
||||
partnerId: { type: Number, optional: true },
|
||||
postProcessMessageSent: { type: Function, optional: true },
|
||||
attachments: { type: Array, optional: true },
|
||||
};
|
||||
ChatterComposer.defaultProps = {
|
||||
allowComposer: true,
|
||||
displayComposer: false,
|
||||
isUserPublic: true,
|
||||
token: '',
|
||||
attachments: [],
|
||||
};
|
||||
|
||||
ChatterComposer.template = 'project.ChatterComposer';
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<!-- Widget PortalComposer (standalone)
|
||||
|
||||
required many options: token, res_model, res_id, ...
|
||||
-->
|
||||
<t t-name="project.ChatterComposer" owl="1">
|
||||
<div t-if="props.allowComposer" class="o_portal_chatter_composer">
|
||||
<t t-if="props.displayComposer">
|
||||
<div t-if="state.displayError" class="alert alert-danger mb8 o_portal_chatter_composer_error" role="alert">
|
||||
Oops! Something went wrong. Try to reload the page and log in.
|
||||
</div>
|
||||
|
||||
<div class="d-flex">
|
||||
<img t-if="!props.isUserPublic or props.token"
|
||||
alt="Avatar"
|
||||
class="o_portal_chatter_avatar o_object_fit_cover align-self-start"
|
||||
t-attf-src="/web/image/res.partner/{{ props.partnerId }}/avatar_128"
|
||||
/>
|
||||
<div class="flex-grow-1">
|
||||
<div class="o_portal_chatter_composer_input">
|
||||
<div class="o_portal_chatter_composer_body mb32">
|
||||
<TextField
|
||||
rowCount="4"
|
||||
placeholder="'Write a message...'"
|
||||
value="state.message"
|
||||
update.bind="update"
|
||||
/>
|
||||
<ChatterAttachmentsViewer
|
||||
attachments="state.attachments"
|
||||
canDelete="true"
|
||||
delete.bind="deleteAttachment"
|
||||
/>
|
||||
<div class="mt8">
|
||||
<button name="send_message" t-on-click="sendMessage" class="btn btn-primary me-1" type="submit" t-att-disabled="state.loading">
|
||||
Send
|
||||
</button>
|
||||
<PortalAttachDocument
|
||||
resModel="props.resModel"
|
||||
resId="props.resId"
|
||||
token="props.token"
|
||||
multiUpload="true"
|
||||
onUpload.bind="onFileUpload"
|
||||
beforeOpen.bind="beforeUploadFile"
|
||||
>
|
||||
<i class="fa fa-paperclip"/>
|
||||
</PortalAttachDocument>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<h4>Leave a comment</h4>
|
||||
<p>You must be <a t-attf-href="/web/login?redirect={{ discussionUrl }}">logged in</a> to post a comment.</p>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { formatDateTime, parseDateTime } from "@web/core/l10n/dates";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { sprintf } from '@web/core/utils/strings';
|
||||
import { ChatterComposer } from "./chatter_composer";
|
||||
import { ChatterMessageCounter } from "./chatter_message_counter";
|
||||
import { ChatterMessages } from "./chatter_messages";
|
||||
import { ChatterPager } from "./chatter_pager";
|
||||
|
||||
const { Component, markup, onWillStart, useState, onWillUpdateProps } = owl;
|
||||
|
||||
export class ChatterContainer extends Component {
|
||||
setup() {
|
||||
this.rpc = useService('rpc');
|
||||
this.state = useState({
|
||||
currentPage: this.props.pagerStart,
|
||||
messages: [],
|
||||
options: this.defaultOptions,
|
||||
});
|
||||
|
||||
onWillStart(this.onWillStart);
|
||||
onWillUpdateProps(this.onWillUpdateProps);
|
||||
}
|
||||
|
||||
get defaultOptions() {
|
||||
return {
|
||||
message_count: 0,
|
||||
is_user_public: true,
|
||||
is_user_employee: false,
|
||||
is_user_published: false,
|
||||
display_composer: Boolean(this.props.resId),
|
||||
partner_id: null,
|
||||
pager_scope: 4,
|
||||
pager_step: 10,
|
||||
};
|
||||
}
|
||||
|
||||
get options() {
|
||||
return this.state.options;
|
||||
}
|
||||
|
||||
set options(options) {
|
||||
this.state.options = {
|
||||
...this.defaultOptions,
|
||||
...options,
|
||||
display_composer: !!options.display_composer,
|
||||
access_token: typeof options.display_composer === 'string' ? options.display_composer : '',
|
||||
};
|
||||
}
|
||||
|
||||
get composerProps() {
|
||||
return {
|
||||
allowComposer: Boolean(this.props.resId),
|
||||
displayComposer: this.state.options.display_composer,
|
||||
partnerId: this.state.options.partner_id || undefined,
|
||||
token: this.state.options.access_token,
|
||||
resModel: this.props.resModel,
|
||||
resId: this.props.resId,
|
||||
projectSharingId: this.props.projectSharingId,
|
||||
postProcessMessageSent: async () => {
|
||||
this.state.currentPage = 1;
|
||||
await this.fetchMessages();
|
||||
},
|
||||
attachments: this.state.options.default_attachment_ids,
|
||||
};
|
||||
}
|
||||
|
||||
onWillStart() {
|
||||
this.initChatter(this.messagesParams(this.props));
|
||||
}
|
||||
|
||||
onWillUpdateProps(nextProps) {
|
||||
this.initChatter(this.messagesParams(nextProps));
|
||||
}
|
||||
|
||||
async onChangePage(page) {
|
||||
this.state.currentPage = page;
|
||||
await this.fetchMessages();
|
||||
}
|
||||
|
||||
async initChatter(params) {
|
||||
if (params.res_id && params.res_model) {
|
||||
const chatterData = await this.rpc(
|
||||
'/mail/chatter_init',
|
||||
params,
|
||||
);
|
||||
this.state.messages = this.preprocessMessages(chatterData.messages);
|
||||
this.options = chatterData.options;
|
||||
} else {
|
||||
this.state.messages = [];
|
||||
this.options = {};
|
||||
}
|
||||
}
|
||||
|
||||
async fetchMessages() {
|
||||
const result = await this.rpc(
|
||||
'/mail/chatter_fetch',
|
||||
this.messagesParams(this.props),
|
||||
);
|
||||
this.state.messages = this.preprocessMessages(result.messages);
|
||||
this.state.options.message_count = result.message_count;
|
||||
return result;
|
||||
}
|
||||
|
||||
messagesParams(props) {
|
||||
const params = {
|
||||
res_model: props.resModel,
|
||||
res_id: props.resId,
|
||||
limit: this.state.options.pager_step,
|
||||
offset: (this.state.currentPage - 1) * this.state.options.pager_step,
|
||||
allow_composer: Boolean(props.resId),
|
||||
project_sharing_id: props.projectSharingId,
|
||||
};
|
||||
if (props.token) {
|
||||
params.token = props.token;
|
||||
}
|
||||
if (props.domain) {
|
||||
params.domain = props.domain;
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
preprocessMessages(messages) {
|
||||
return messages.map(m => ({
|
||||
...m,
|
||||
author_avatar_url: sprintf('/web/image/mail.message/%s/author_avatar/50x50', m.id),
|
||||
published_date_str: sprintf(
|
||||
this.env._t('Published on %s'),
|
||||
formatDateTime(
|
||||
parseDateTime(
|
||||
m.date,
|
||||
{ format: 'MM-dd-yyy HH:mm:ss' },
|
||||
),
|
||||
)
|
||||
),
|
||||
body: markup(m.body),
|
||||
}));
|
||||
}
|
||||
|
||||
updateMessage(message_id, changes) {
|
||||
Object.assign(
|
||||
this.state.messages.find(m => m.id === message_id),
|
||||
changes,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ChatterContainer.components = {
|
||||
ChatterComposer,
|
||||
ChatterMessageCounter,
|
||||
ChatterMessages,
|
||||
ChatterPager,
|
||||
};
|
||||
|
||||
ChatterContainer.props = {
|
||||
token: { type: String, optional: true },
|
||||
resModel: String,
|
||||
resId: { type: Number, optional: true },
|
||||
pid: { type: String, optional: true },
|
||||
hash: { type: String, optional: true },
|
||||
pagerStart: { type: Number, optional: true },
|
||||
twoColumns: { type: Boolean, optional: true },
|
||||
projectSharingId: Number,
|
||||
};
|
||||
ChatterContainer.defaultProps = {
|
||||
token: '',
|
||||
pid: '',
|
||||
hash: '',
|
||||
pagerStart: 1,
|
||||
};
|
||||
ChatterContainer.template = 'project.ChatterContainer';
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="project.ChatterContainer" owl="1">
|
||||
<div t-attf-class="o_portal_chatter p-0 container {{props.twoColumns ? 'row' : ''}}">
|
||||
<div t-attf-class="{{props.twoColumns ? 'col-lg-5' : props.resId ? 'border-bottom' : ''}}">
|
||||
<div class="o_portal_chatter_header">
|
||||
<ChatterMessageCounter count="state.options.message_count"/>
|
||||
</div>
|
||||
<hr/>
|
||||
<ChatterComposer t-props="composerProps"/>
|
||||
</div>
|
||||
<div t-attf-class="{{props.twoColumns ? 'offset-lg-1 col-lg-6' : 'pt-4'}}">
|
||||
<ChatterMessages messages="props.resId ? state.messages : []" isUserEmployee="state.options.is_user_employee" update.bind="updateMessage" />
|
||||
<div class="o_portal_chatter_footer">
|
||||
<ChatterPager
|
||||
page="this.state.currentPage || 1"
|
||||
messageCount="this.state.options.message_count"
|
||||
pagerScope="this.state.options.pager_scope"
|
||||
pagerStep="this.state.options.pager_step"
|
||||
changePage.bind="onChangePage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
/** @odoo-module */
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class ChatterMessageCounter extends Component { }
|
||||
|
||||
ChatterMessageCounter.props = {
|
||||
count: Number,
|
||||
};
|
||||
ChatterMessageCounter.template = 'project.ChatterMessageCounter';
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="project.ChatterMessageCounter" owl="1">
|
||||
<div class="o_message_counter">
|
||||
<t t-if="props.count">
|
||||
<span class="fa fa-comments" />
|
||||
<span class="o_message_count"> <t t-esc="props.count"/> </span>
|
||||
comments
|
||||
</t>
|
||||
<t t-else="">
|
||||
There are no comments for now.
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
import { ChatterAttachmentsViewer } from "./chatter_attachments_viewer";
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class ChatterMessages extends Component {
|
||||
setup() {
|
||||
this.rpc = useService('rpc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the visibility of the message.
|
||||
*
|
||||
* @param {Object} message message to change the visibility
|
||||
*/
|
||||
async toggleMessageVisibility(message) {
|
||||
const result = await this.rpc(
|
||||
'/mail/update_is_internal',
|
||||
{ message_id: message.id, is_internal: !message.is_internal },
|
||||
);
|
||||
this.props.update(message.id, { is_internal: result });
|
||||
}
|
||||
}
|
||||
|
||||
ChatterMessages.template = 'project.ChatterMessages';
|
||||
ChatterMessages.props = {
|
||||
messages: Array,
|
||||
isUserEmployee: { type: Boolean, optional: true },
|
||||
update: { type: Function, optional: true },
|
||||
};
|
||||
ChatterMessages.defaultProps = {
|
||||
update: (message_id, changes) => {},
|
||||
};
|
||||
ChatterMessages.components = { ChatterAttachmentsViewer };
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="project.ChatterMessages" owl="1">
|
||||
<div class="o_portal_chatter_messages">
|
||||
<t t-foreach="props.messages" t-as="message" t-key="message.id">
|
||||
<div class="d-flex o_portal_chatter_message">
|
||||
<img class="o_portal_chatter_avatar" t-att-src="message.author_avatar_url" alt="avatar"/>
|
||||
<div class="flex-grow-1">
|
||||
<t t-if="props.isUserEmployee">
|
||||
<div t-if="message.is_message_subtype_note" class="float-end">
|
||||
<button class="btn btn-secondary" title="Internal notes are only displayed to internal users." disabled="true">Internal Note</button>
|
||||
</div>
|
||||
<div t-else=""
|
||||
t-attf-class="float-end {{message.is_internal ? 'o_portal_message_internal_on' : 'o_portal_message_internal_off'}}"
|
||||
t-on-click="() => this.toggleMessageVisibility(message)"
|
||||
>
|
||||
<button class="btn btn-danger"
|
||||
title="Currently restricted to internal employees, click to make it available to everyone viewing this document."
|
||||
>
|
||||
Employees Only
|
||||
</button>
|
||||
<button class="btn btn-success"
|
||||
title="Currently available to everyone viewing this document, click to restrict to internal employees."
|
||||
>
|
||||
Visible
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
<div class="o_portal_chatter_message_title">
|
||||
<h5 class='mb-1'><t t-out="message.author_id[1]"/></h5>
|
||||
<p class="o_portal_chatter_puslished_date"><t t-out="message.published_date_str"/></p>
|
||||
</div>
|
||||
<t t-out="message.body"/>
|
||||
<div class="o_portal_chatter_attachments">
|
||||
<ChatterAttachmentsViewer attachments="message.attachment_ids"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
/** @odoo-module */
|
||||
|
||||
const { Component, useState, onWillUpdateProps } = owl;
|
||||
|
||||
export class ChatterPager extends Component {
|
||||
setup() {
|
||||
this.state = useState({
|
||||
disabledButtons: false,
|
||||
pageCount: 1,
|
||||
pageStart: 1,
|
||||
pageEnd: 1,
|
||||
pagePrevious: 1,
|
||||
pageNext: 1,
|
||||
pages: [1],
|
||||
offset: 0,
|
||||
});
|
||||
this.computePagerState(this.props);
|
||||
|
||||
onWillUpdateProps(this.onWillUpdateProps);
|
||||
}
|
||||
|
||||
computePagerState(props) {
|
||||
let page = props.page || 1;
|
||||
let scope = props.pagerScope;
|
||||
|
||||
const step = props.pagerStep;
|
||||
|
||||
// Compute Pager
|
||||
this.state.messageCount = Math.ceil(parseFloat(props.messageCount) / step);
|
||||
|
||||
page = Math.max(1, Math.min(page, this.state.messageCount));
|
||||
|
||||
const pageStart = Math.max(page - parseInt(Math.floor(scope / 2)), 1);
|
||||
this.state.pageEnd = Math.min(pageStart + scope, this.state.messageCount);
|
||||
this.state.pageStart = Math.max(this.state.pageEnd - scope, 1);
|
||||
|
||||
this.state.pages = Array.from(
|
||||
{length: this.state.pageEnd - this.state.pageStart + 1},
|
||||
(_, i) => i + this.state.pageStart,
|
||||
);
|
||||
this.state.pagePrevious = Math.max(this.state.pageStart, page - 1);
|
||||
this.state.pageNext = Math.min(this.state.pageEnd, page + 1);
|
||||
}
|
||||
|
||||
onWillUpdateProps(nextProps) {
|
||||
this.computePagerState(nextProps);
|
||||
}
|
||||
|
||||
async onPageChanged(page) {
|
||||
this.state.disabledButtons = true;
|
||||
await this.props.changePage(page);
|
||||
this.state.disabledButtons = false;
|
||||
}
|
||||
}
|
||||
|
||||
ChatterPager.props = {
|
||||
pagerScope: Number,
|
||||
pagerStep: Number,
|
||||
page: Number,
|
||||
messageCount: Number,
|
||||
changePage: Function,
|
||||
};
|
||||
|
||||
ChatterPager.template = 'project.ChatterPager';
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="project.ChatterPager" owl="1">
|
||||
<div class="d-flex justify-content-center">
|
||||
<ul class="pagination mb-0 pb-4" t-if="state.pages.length > 1">
|
||||
<li t-if="props.page != props.page_previous" t-att-data-page="state.pagePrevious" class="page-item o_portal_chatter_pager_btn">
|
||||
<a t-on-click="() => this.onPageChanged(state.pagePrevious)" class="page-link"><i class="fa fa-chevron-left" role="img" aria-label="Previous" title="Previous"/></a>
|
||||
</li>
|
||||
<t t-foreach="state.pages" t-as="page" t-key="page_index">
|
||||
<li t-att-data-page="page" t-attf-class="page-item #{page == props.page ? 'o_portal_chatter_pager_btn active' : 'o_portal_chatter_pager_btn'}">
|
||||
<a t-on-click="() => this.onPageChanged(page)" t-att-disabled="page == props.page" class="page-link"><t t-esc="page"/></a>
|
||||
</li>
|
||||
</t>
|
||||
<li t-if="props.page != state.pageNext" t-att-data-page="state.pageNext" class="page-item o_portal_chatter_pager_btn">
|
||||
<a t-on-click="() => this.onPageChanged(state.pageNext)" class="page-link"><i class="fa fa-chevron-right" role="img" aria-label="Next" title="Next"/></a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import FavoriteMenuLegacy from 'web.FavoriteMenu';
|
||||
import CustomFavoriteItemLegacy from 'web.CustomFavoriteItem';
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
|
||||
/**
|
||||
* Remove all components contained in the favorite menu registry except the CustomFavoriteItem
|
||||
* component for only the project sharing feature.
|
||||
*/
|
||||
export function prepareFavoriteMenuRegister() {
|
||||
let customFavoriteItemKey = 'favorite-generator-menu';
|
||||
const keys = FavoriteMenuLegacy.registry.keys().filter(key => key !== customFavoriteItemKey);
|
||||
FavoriteMenuLegacy.registry = Object.assign(FavoriteMenuLegacy.registry, {
|
||||
map: {},
|
||||
_scoreMapping: {},
|
||||
_sortedKeys: null,
|
||||
});
|
||||
FavoriteMenuLegacy.registry.add(customFavoriteItemKey, CustomFavoriteItemLegacy, 0);
|
||||
// notify the listeners, we keep only one key in this registry.
|
||||
for (const key of keys) {
|
||||
for (const callback of FavoriteMenuLegacy.registry.listeners) {
|
||||
callback(key, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
customFavoriteItemKey = 'custom-favorite-item';
|
||||
const favoriteMenuRegistry = registry.category("favoriteMenu");
|
||||
for (const [key] of favoriteMenuRegistry.getEntries()) {
|
||||
if (key !== customFavoriteItemKey) {
|
||||
favoriteMenuRegistry.remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { PortalFileInput } from '../portal_file_input/portal_file_input';
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class PortalAttachDocument extends Component {}
|
||||
|
||||
PortalAttachDocument.template = 'project.PortalAttachDocument';
|
||||
PortalAttachDocument.components = { PortalFileInput };
|
||||
PortalAttachDocument.props = {
|
||||
highlight: { type: Boolean, optional: true },
|
||||
onUpload: { type: Function, optional: true },
|
||||
beforeOpen: { type: Function, optional: true },
|
||||
slots: {
|
||||
type: Object,
|
||||
shape: {
|
||||
default: Object,
|
||||
},
|
||||
},
|
||||
resId: { type: Number, optional: true },
|
||||
resModel: { type: String, optional: true },
|
||||
multiUpload: { type: Boolean, optional: true },
|
||||
hidden: { type: Boolean, optional: true },
|
||||
acceptedFileExtensions: { type: String, optional: true },
|
||||
token: { type: String, optional: true },
|
||||
};
|
||||
PortalAttachDocument.defaultProps = {
|
||||
acceptedFileExtensions: "*",
|
||||
onUpload: () => {},
|
||||
route: "/portal/attachment/add",
|
||||
beforeOpen: async () => true,
|
||||
};
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="project.PortalAttachDocument" owl="1">
|
||||
<button t-attf-class="btn o_attachment_button #{props.highlight ? 'btn-primary' : 'btn-secondary'}">
|
||||
<PortalFileInput
|
||||
onUpload="props.onUpload"
|
||||
beforeOpen="props.beforeOpen"
|
||||
multiUpload="props.multiUpload"
|
||||
resModel="props.resModel"
|
||||
resId="props.resId"
|
||||
route="props.route"
|
||||
accessToken="props.token"
|
||||
>
|
||||
<t t-set-slot="default">
|
||||
<i class="fa fa-paperclip"/>
|
||||
</t>
|
||||
</PortalFileInput>
|
||||
</button>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { FileInput } from '@web/core/file_input/file_input';
|
||||
|
||||
export class PortalFileInput extends FileInput {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
get httpParams() {
|
||||
const {
|
||||
model: res_model,
|
||||
id: res_id,
|
||||
...otherParams
|
||||
} = super.httpParams;
|
||||
return {
|
||||
res_model,
|
||||
res_id,
|
||||
access_token: this.props.accessToken,
|
||||
...otherParams,
|
||||
}
|
||||
}
|
||||
|
||||
async uploadFiles(params) {
|
||||
const { ufile: files, ...otherParams } = params;
|
||||
const filesData = await Promise.all(
|
||||
files.map(
|
||||
(file) =>
|
||||
super.uploadFiles({
|
||||
file,
|
||||
name: file.name,
|
||||
...otherParams,
|
||||
})
|
||||
)
|
||||
);
|
||||
return filesData;
|
||||
}
|
||||
}
|
||||
|
||||
PortalFileInput.props = {
|
||||
...FileInput.props,
|
||||
accessToken: { type: String, optional: true },
|
||||
};
|
||||
PortalFileInput.defaultProps = {
|
||||
...FileInput.defaultProps,
|
||||
accessToken: '',
|
||||
};
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
/** @odoo-module **/
|
||||
import { startWebClient } from '@web/start';
|
||||
import { ProjectSharingWebClient } from './project_sharing';
|
||||
import { prepareFavoriteMenuRegister } from './components/favorite_menu_registry';
|
||||
|
||||
prepareFavoriteMenuRegister();
|
||||
startWebClient(ProjectSharingWebClient);
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { useBus, useService } from '@web/core/utils/hooks';
|
||||
import { ActionContainer } from '@web/webclient/actions/action_container';
|
||||
import { MainComponentsContainer } from "@web/core/main_components_container";
|
||||
import { useOwnDebugContext } from "@web/core/debug/debug_context";
|
||||
import { session } from '@web/session';
|
||||
|
||||
const { Component, useEffect, useExternalListener, useState } = owl;
|
||||
|
||||
export class ProjectSharingWebClient extends Component {
|
||||
setup() {
|
||||
window.parent.document.body.style.margin = "0"; // remove the margin in the parent body
|
||||
this.actionService = useService('action');
|
||||
this.user = useService("user");
|
||||
useService("legacy_service_provider");
|
||||
useOwnDebugContext({ categories: ["default"] });
|
||||
this.state = useState({
|
||||
fullscreen: false,
|
||||
});
|
||||
useBus(this.env.bus, "ACTION_MANAGER:UI-UPDATED", (mode) => {
|
||||
if (mode !== "new") {
|
||||
this.state.fullscreen = mode === "fullscreen";
|
||||
}
|
||||
});
|
||||
useEffect(
|
||||
() => {
|
||||
this._showView();
|
||||
},
|
||||
() => []
|
||||
);
|
||||
useExternalListener(window, "click", this.onGlobalClick, { capture: true });
|
||||
}
|
||||
|
||||
async _showView() {
|
||||
const { action_name, project_id, open_task_action } = session;
|
||||
await this.actionService.doAction(
|
||||
action_name,
|
||||
{
|
||||
clearBreadcrumbs: true,
|
||||
additionalContext: {
|
||||
active_id: project_id,
|
||||
}
|
||||
}
|
||||
);
|
||||
if (open_task_action) {
|
||||
await this.actionService.doAction(open_task_action);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
onGlobalClick(ev) {
|
||||
// When a ctrl-click occurs inside an <a href/> element
|
||||
// we let the browser do the default behavior and
|
||||
// we do not want any other listener to execute.
|
||||
if (
|
||||
ev.ctrlKey &&
|
||||
((ev.target instanceof HTMLAnchorElement && ev.target.href) ||
|
||||
(ev.target instanceof HTMLElement && ev.target.closest("a[href]:not([href=''])")))
|
||||
) {
|
||||
ev.stopImmediatePropagation();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ProjectSharingWebClient.components = { ActionContainer, MainComponentsContainer };
|
||||
ProjectSharingWebClient.template = 'project.ProjectSharingWebClient';
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="project.ProjectSharingWebClient" owl="1">
|
||||
<ActionContainer />
|
||||
<MainComponentsContainer/>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-inherit="web.CustomFavoriteItem" t-inherit-mode="extension">
|
||||
<xpath expr="//CheckBox[@value='state.isShared']" position="replace"/>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { append, createElement, setAttributes } from "@web/core/utils/xml";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { SIZES } from "@web/core/ui/ui_service";
|
||||
import { getModifier, ViewCompiler } from "@web/views/view_compiler";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { FormCompiler } from "@web/views/form/form_compiler";
|
||||
|
||||
/**
|
||||
* Compiler the portal chatter in project sharing.
|
||||
*
|
||||
* @param {HTMLElement} node
|
||||
* @param {Object} params
|
||||
* @returns
|
||||
*/
|
||||
function compileChatter(node, params) {
|
||||
const chatterContainerXml = createElement('ChatterContainer');
|
||||
const parentURLQuery = new URLSearchParams(window.parent.location.search);
|
||||
setAttributes(chatterContainerXml, {
|
||||
token: `'${parentURLQuery.get('access_token')}'` || '',
|
||||
resModel: params.resModel,
|
||||
resId: params.resId,
|
||||
projectSharingId: params.projectSharingId,
|
||||
});
|
||||
const chatterContainerHookXml = createElement('div');
|
||||
chatterContainerHookXml.classList.add('o_FormRenderer_chatterContainer');
|
||||
append(chatterContainerHookXml, chatterContainerXml);
|
||||
return chatterContainerHookXml;
|
||||
}
|
||||
|
||||
export class ProjectSharingChatterCompiler extends ViewCompiler {
|
||||
setup() {
|
||||
this.compilers.push({ selector: "t", fn: this.compileT });
|
||||
this.compilers.push({ selector: 'div.oe_chatter', fn: this.compileChatter });
|
||||
}
|
||||
|
||||
compile(node, params) {
|
||||
const res = super.compile(node, params).children[0];
|
||||
const chatterContainerHookXml = res.querySelector(".o_FormRenderer_chatterContainer");
|
||||
if (chatterContainerHookXml) {
|
||||
setAttributes(chatterContainerHookXml, {
|
||||
"t-if": `uiService.size >= ${SIZES.XXL}`,
|
||||
});
|
||||
chatterContainerHookXml.classList.add('overflow-x-hidden', 'overflow-y-auto', 'o-aside', 'h-100');
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
compileT(node, params) {
|
||||
const compiledRoot = createElement("t");
|
||||
for (const child of node.childNodes) {
|
||||
const invisible = getModifier(child, "invisible");
|
||||
let compiledChild = this.compileNode(child, params, false);
|
||||
compiledChild = this.applyInvisible(invisible, compiledChild, {
|
||||
...params,
|
||||
recordExpr: "model.root",
|
||||
});
|
||||
append(compiledRoot, compiledChild);
|
||||
}
|
||||
return compiledRoot;
|
||||
}
|
||||
|
||||
compileChatter(node) {
|
||||
return compileChatter(node, {
|
||||
resId: 'model.root.resId or undefined',
|
||||
resModel: 'model.root.resModel',
|
||||
projectSharingId: 'model.root.context.active_id_chatter',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("form_compilers").add("portal_chatter_compiler", {
|
||||
selector: "div.oe_chatter",
|
||||
fn: (node) =>
|
||||
compileChatter(node, {
|
||||
resId: "props.record.resId or undefined",
|
||||
resModel: "props.record.resModel",
|
||||
projectSharingId: "props.record.context.active_id_chatter",
|
||||
}),
|
||||
});
|
||||
|
||||
patch(FormCompiler.prototype, 'project_sharing_chatter', {
|
||||
compile(node, params) {
|
||||
const res = this._super(node, params);
|
||||
const chatterContainerHookXml = res.querySelector('.o_FormRenderer_chatterContainer');
|
||||
if (!chatterContainerHookXml) {
|
||||
return res; // no chatter, keep the result as it is
|
||||
}
|
||||
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
|
||||
}
|
||||
// after sheet bg (standard position, below form)
|
||||
setAttributes(chatterContainerHookXml, {
|
||||
't-if': `uiService.size < ${SIZES.XXL}`,
|
||||
});
|
||||
append(parentXml, chatterContainerHookXml);
|
||||
return res;
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { useService } from '@web/core/utils/hooks';
|
||||
import { createElement } from "@web/core/utils/xml";
|
||||
import { FormController } from '@web/views/form/form_controller';
|
||||
import { useViewCompiler } from '@web/views/view_compiler';
|
||||
import { ProjectSharingChatterCompiler } from './project_sharing_form_compiler';
|
||||
import { ChatterContainer } from '../../components/chatter/chatter_container';
|
||||
|
||||
export class ProjectSharingFormController extends FormController {
|
||||
setup() {
|
||||
super.setup();
|
||||
this.uiService = useService('ui');
|
||||
const { arch, xmlDoc } = this.archInfo;
|
||||
const template = createElement('t');
|
||||
const xmlDocChatter = xmlDoc.querySelector("div.oe_chatter");
|
||||
if (xmlDocChatter && xmlDocChatter.parentNode.nodeName === "form") {
|
||||
template.appendChild(xmlDocChatter.cloneNode(true));
|
||||
}
|
||||
const mailTemplates = useViewCompiler(ProjectSharingChatterCompiler, arch, { Mail: template }, {});
|
||||
this.mailTemplate = mailTemplates.Mail;
|
||||
}
|
||||
|
||||
getActionMenuItems() {
|
||||
return {};
|
||||
}
|
||||
|
||||
get translateAlert() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
ProjectSharingFormController.components = {
|
||||
...FormController.components,
|
||||
ChatterContainer,
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-inherit="web.FormView" t-inherit-mode="extension">
|
||||
<xpath expr="//div[hasclass('o_form_view_container')]" position="after">
|
||||
<t t-if="mailTemplate">
|
||||
<t t-call="{{ mailTemplate }}" />
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { ChatterContainer } from '../../components/chatter/chatter_container';
|
||||
import { FormRenderer } from '@web/views/form/form_renderer';
|
||||
|
||||
export class ProjectSharingFormRenderer extends FormRenderer { }
|
||||
ProjectSharingFormRenderer.components = {
|
||||
...FormRenderer.components,
|
||||
ChatterContainer,
|
||||
};
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { formView } from '@web/views/form/form_view';
|
||||
import { ProjectSharingFormController } from './project_sharing_form_controller';
|
||||
import { ProjectSharingFormRenderer } from './project_sharing_form_renderer';
|
||||
|
||||
formView.Controller = ProjectSharingFormController;
|
||||
formView.Renderer = ProjectSharingFormRenderer;
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { kanbanView } from "@web/views/kanban/kanban_view";
|
||||
import { KanbanDynamicGroupList, KanbanModel } from "@web/views/kanban/kanban_model";
|
||||
|
||||
export class ProjectSharingTaskKanbanDynamicGroupList extends KanbanDynamicGroupList {
|
||||
get context() {
|
||||
return {
|
||||
...super.context,
|
||||
project_kanban: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class ProjectSharingTaskKanbanModel extends KanbanModel {}
|
||||
|
||||
ProjectSharingTaskKanbanModel.DynamicGroupList = ProjectSharingTaskKanbanDynamicGroupList;
|
||||
|
||||
kanbanView.Model = ProjectSharingTaskKanbanModel;
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { ListRenderer } from "@web/views/list/list_renderer";
|
||||
import { evalDomain } from "@web/views/utils";
|
||||
|
||||
const { onWillUpdateProps } = owl;
|
||||
|
||||
export class ProjectSharingListRenderer extends ListRenderer {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
this.setColumns(this.allColumns);
|
||||
onWillUpdateProps((nextProps) => {
|
||||
this.setColumns(nextProps.archInfo.columns);
|
||||
});
|
||||
}
|
||||
|
||||
setColumns(columns) {
|
||||
if (this.props.list.records.length) {
|
||||
const allColumns = [];
|
||||
const firstRecord = this.props.list.records[0];
|
||||
for (const column of columns) {
|
||||
if (
|
||||
column.modifiers.column_invisible &&
|
||||
column.modifiers.column_invisible instanceof Array
|
||||
) {
|
||||
const result = evalDomain(column.modifiers.column_invisible, firstRecord.evalContext);
|
||||
if (result) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
allColumns.push(column);
|
||||
}
|
||||
this.allColumns = allColumns;
|
||||
} else {
|
||||
this.allColumns = columns;
|
||||
}
|
||||
this.state.columns = this.allColumns.filter(
|
||||
(col) => !col.optional || this.optionalActiveFields[col.name]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { listView } from "@web/views/list/list_view";
|
||||
|
||||
import { ProjectSharingListRenderer } from "./list_renderer";
|
||||
|
||||
const props = listView.props;
|
||||
listView.props = function (genericProps, view) {
|
||||
const result = props(genericProps, view);
|
||||
return {
|
||||
...result,
|
||||
allowSelectors: false,
|
||||
};
|
||||
};
|
||||
listView.Renderer = ProjectSharingListRenderer;
|
||||
Loading…
Add table
Add a link
Reference in a new issue