mirror of
https://github.com/bringout/oca-ocb-project.git
synced 2026-04-20 04:02:00 +02:00
19.0 vanilla
This commit is contained in:
parent
a2f74aefd8
commit
4a4d12c333
844 changed files with 212348 additions and 270090 deletions
|
|
@ -0,0 +1,39 @@
|
|||
import { Chatter } from "@mail/chatter/web_portal/chatter";
|
||||
|
||||
import { useSubEnv } from "@odoo/owl";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
patch(Chatter.prototype, {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
Object.assign(this.state, {
|
||||
isFollower: this.props.isFollower,
|
||||
});
|
||||
this.orm = useService("orm");
|
||||
useSubEnv({
|
||||
// 'inFrontendPortalChatter' is specific to the frontend portal chatters
|
||||
// and should not be set to 'true' in the project sharing chatter environment.
|
||||
projectSharingId: this.props.projectSharingId,
|
||||
});
|
||||
},
|
||||
|
||||
async toggleIsFollower() {
|
||||
this.state.isFollower = await this.orm.call(
|
||||
this.props.threadModel,
|
||||
"project_sharing_toggle_is_follower",
|
||||
[this.props.threadId]
|
||||
);
|
||||
},
|
||||
onPostCallback() {
|
||||
super.onPostCallback();
|
||||
this.state.isFollower = true;
|
||||
},
|
||||
});
|
||||
Chatter.props = [
|
||||
...Chatter.props,
|
||||
"token",
|
||||
"projectSharingId",
|
||||
"isFollower",
|
||||
"displayFollowButton",
|
||||
];
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { ComposerAction } from "@mail/core/common/composer_actions";
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(ComposerAction.prototype, {
|
||||
_condition({ owner }) {
|
||||
if (this.id === "open-full-composer" && owner.env.projectSharingId) {
|
||||
return false;
|
||||
}
|
||||
return super._condition(...arguments);
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
import { Composer } from "@mail/core/common/composer";
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
import { onWillStart } from "@odoo/owl";
|
||||
|
||||
patch(Composer.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
onWillStart(() => {
|
||||
if (!this.thread.id) {
|
||||
this.state.active = false;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
get extraData() {
|
||||
const extraData = super.extraData;
|
||||
if (this.env.projectSharingId) {
|
||||
extraData.project_sharing_id = this.env.projectSharingId;
|
||||
}
|
||||
return extraData;
|
||||
},
|
||||
|
||||
get isSendButtonDisabled() {
|
||||
if (this.thread && !this.thread.id) {
|
||||
return true;
|
||||
}
|
||||
return super.isSendButtonDisabled;
|
||||
},
|
||||
|
||||
get allowUpload() {
|
||||
if (this.thread && !this.thread.id) {
|
||||
return false;
|
||||
}
|
||||
return super.allowUpload;
|
||||
},
|
||||
|
||||
get shouldHideFromMessageListOnDelete() {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { Message } from "@mail/core/common/message";
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(Message.prototype, {
|
||||
get shouldHideFromMessageListOnDelete() {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-inherit="portal.Chatter" t-inherit-mode="extension">
|
||||
<xpath expr="//div[hasclass('o-mail-Chatter-top')]" position="before">
|
||||
<div class="d-flex justify-content-end">
|
||||
<button t-if="props.displayFollowButton and props.threadId" class="btn btn-link w-auto" t-on-click="toggleIsFollower">
|
||||
<t t-if="state.isFollower">
|
||||
Unfollow
|
||||
</t>
|
||||
<t t-else="">
|
||||
Follow
|
||||
</t>
|
||||
</button>
|
||||
</div>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { SuggestionService } from "@mail/core/common/suggestion_service";
|
||||
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(SuggestionService.prototype, {
|
||||
async fetchPartnersRoles(term, thread, { abortSignal } = {}) {
|
||||
if (thread.model === "project.task") {
|
||||
this.store.insert(
|
||||
await this.makeOrmCall(
|
||||
"project.task",
|
||||
"get_mention_suggestions",
|
||||
[thread.id],
|
||||
{ search: term },
|
||||
{ abortSignal }
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
return super.fetchPartnersRoles(...arguments);
|
||||
},
|
||||
});
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
/** @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 () => {},
|
||||
};
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -1,134 +0,0 @@
|
|||
/** @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';
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -1,172 +0,0 @@
|
|||
/** @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';
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
/** @odoo-module */
|
||||
|
||||
const { Component } = owl;
|
||||
|
||||
export class ChatterMessageCounter extends Component { }
|
||||
|
||||
ChatterMessageCounter.props = {
|
||||
count: Number,
|
||||
};
|
||||
ChatterMessageCounter.template = 'project.ChatterMessageCounter';
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
/** @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 };
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
/** @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';
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
<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,10 @@
|
|||
import { ListRenderer } from "@web/views/list/list_renderer";
|
||||
|
||||
export class DependOnIdsListRenderer extends ListRenderer {
|
||||
get nbHiddenRecords() {
|
||||
const { context, records } = this.props.list;
|
||||
return context.depend_on_count - records.length;
|
||||
}
|
||||
}
|
||||
|
||||
DependOnIdsListRenderer.rowsTemplate = "project.DependOnIdsListRowsRenderer";
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
<templates>
|
||||
<t t-name="project.DependOnIdsListRowsRenderer" t-inherit="web.ListRenderer.Rows" t-inherit-mode="primary" owl="1">
|
||||
<xpath expr="//t[@t-foreach='list.records']" position="after">
|
||||
<tr class="o_data_row">
|
||||
<td t-if="nbHiddenRecords" t-att-colspan="nbCols" style="text-align:center;">
|
||||
<i class="text-muted">
|
||||
This task is currently blocked by <t t-out="nbHiddenRecords"/> (other) tasks to which you do not have access.
|
||||
</i>
|
||||
</td>
|
||||
</tr>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { registry } from "@web/core/registry";
|
||||
import { X2ManyField, x2ManyField } from "@web/views/fields/x2many/x2many_field";
|
||||
|
||||
import { DependOnIdsListRenderer } from "./depend_on_ids_list_renderer";
|
||||
|
||||
export class DependOnIdsOne2ManyField extends X2ManyField {
|
||||
static components = {
|
||||
...X2ManyField.components,
|
||||
ListRenderer: DependOnIdsListRenderer,
|
||||
};
|
||||
}
|
||||
|
||||
export const dependOnIdsOne2ManyField = {
|
||||
...x2ManyField,
|
||||
component: DependOnIdsOne2ManyField,
|
||||
};
|
||||
|
||||
registry.category("fields").add("depend_on_ids_one2many", dependOnIdsOne2ManyField);
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
/** @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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
/** @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,
|
||||
};
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
<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>
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
/** @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,45 @@
|
|||
import { ImageCropPlugin } from "@html_editor/main/media/image_crop_plugin";
|
||||
import { ImageSavePlugin } from "@html_editor/main/media/image_save_plugin";
|
||||
import { MediaPlugin } from "@html_editor/main/media/media_plugin";
|
||||
import { MAIN_PLUGINS } from "@html_editor/plugin_sets";
|
||||
|
||||
export class ProjectSharingMediaPlugin extends MediaPlugin {
|
||||
resources = {
|
||||
...this.resources,
|
||||
toolbar_items: this.resources.toolbar_items.filter(
|
||||
item => item.id !== "replace_image"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
export class ProjectSharingImageSavePlugin extends ImageSavePlugin {
|
||||
async createAttachment({ el, imageData, resId }) {
|
||||
const response = JSON.parse(
|
||||
await this.services.http.post(
|
||||
"/project_sharing/attachment/add_image",
|
||||
{
|
||||
name: el.dataset.fileName || "",
|
||||
data: imageData,
|
||||
res_id: resId,
|
||||
access_token: "",
|
||||
csrf_token: odoo.csrf_token,
|
||||
},
|
||||
"text"
|
||||
)
|
||||
);
|
||||
if (response.error) {
|
||||
this.services.notification.add(response.error, { type: "danger" });
|
||||
el.remove();
|
||||
}
|
||||
const attachment = response;
|
||||
attachment.image_src = "/web/image/" + attachment.id + "-" + attachment.name;
|
||||
return attachment;
|
||||
}
|
||||
}
|
||||
|
||||
MAIN_PLUGINS.splice(MAIN_PLUGINS.indexOf(MediaPlugin), 1);
|
||||
MAIN_PLUGINS.push(ProjectSharingMediaPlugin);
|
||||
MAIN_PLUGINS.splice(MAIN_PLUGINS.indexOf(ImageSavePlugin), 1);
|
||||
MAIN_PLUGINS.push(ProjectSharingImageSavePlugin);
|
||||
|
||||
MAIN_PLUGINS.splice(MAIN_PLUGINS.indexOf(ImageCropPlugin), 1);
|
||||
|
|
@ -1,7 +1,4 @@
|
|||
/** @odoo-module **/
|
||||
import { startWebClient } from '@web/start';
|
||||
import { ProjectSharingWebClient } from './project_sharing';
|
||||
import { prepareFavoriteMenuRegister } from './components/favorite_menu_registry';
|
||||
|
||||
prepareFavoriteMenuRegister();
|
||||
startWebClient(ProjectSharingWebClient);
|
||||
|
|
|
|||
|
|
@ -1,19 +1,17 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { useBus, useService } from '@web/core/utils/hooks';
|
||||
import { ActionContainer } from '@web/webclient/actions/action_container';
|
||||
import { browser } from "@web/core/browser/browser";
|
||||
import { useBus, useService } from "@web/core/utils/hooks";
|
||||
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;
|
||||
import { ActionContainer } from "@web/webclient/actions/action_container";
|
||||
import { Component, onMounted, useExternalListener, useState } from "@odoo/owl";
|
||||
|
||||
export class ProjectSharingWebClient extends Component {
|
||||
static props = {};
|
||||
static components = { ActionContainer, MainComponentsContainer };
|
||||
static template = "project.ProjectSharingWebClient";
|
||||
|
||||
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");
|
||||
this.actionService = useService("action");
|
||||
useOwnDebugContext({ categories: ["default"] });
|
||||
this.state = useState({
|
||||
fullscreen: false,
|
||||
|
|
@ -23,28 +21,31 @@ export class ProjectSharingWebClient extends Component {
|
|||
this.state.fullscreen = mode === "fullscreen";
|
||||
}
|
||||
});
|
||||
useEffect(
|
||||
() => {
|
||||
this._showView();
|
||||
},
|
||||
() => []
|
||||
);
|
||||
onMounted(() => {
|
||||
this.loadRouterState();
|
||||
// the chat window and dialog services listen to 'web_client_ready' event in
|
||||
// order to initialize themselves:
|
||||
this.env.bus.trigger("WEB_CLIENT_READY");
|
||||
});
|
||||
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,
|
||||
async loadRouterState() {
|
||||
// ** url-retrocompatibility **
|
||||
const stateLoaded = await this.actionService.loadState();
|
||||
|
||||
// Scroll to anchor after the state is loaded
|
||||
if (stateLoaded) {
|
||||
if (browser.location.hash !== "") {
|
||||
try {
|
||||
const el = document.querySelector(browser.location.hash);
|
||||
if (el !== null) {
|
||||
el.scrollIntoView(true);
|
||||
}
|
||||
} catch {
|
||||
// do nothing if the hash is not a correct selector.
|
||||
}
|
||||
}
|
||||
);
|
||||
if (open_task_action) {
|
||||
await this.actionService.doAction(open_task_action);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -56,7 +57,8 @@ export class ProjectSharingWebClient extends Component {
|
|||
// we let the browser do the default behavior and
|
||||
// we do not want any other listener to execute.
|
||||
if (
|
||||
ev.ctrlKey &&
|
||||
(ev.ctrlKey || ev.metaKey) &&
|
||||
!ev.target.isContentEditable &&
|
||||
((ev.target instanceof HTMLAnchorElement && ev.target.href) ||
|
||||
(ev.target instanceof HTMLElement && ev.target.closest("a[href]:not([href=''])")))
|
||||
) {
|
||||
|
|
@ -65,6 +67,3 @@ export class ProjectSharingWebClient extends Component {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
ProjectSharingWebClient.components = { ActionContainer, MainComponentsContainer };
|
||||
ProjectSharingWebClient.template = 'project.ProjectSharingWebClient';
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
.o_project_sharing .o_control_panel {
|
||||
// to be able to vertically center the content of the control panel since there is no menu header displayed in project sharing
|
||||
padding-top: 16px !important;
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="project.ProjectSharingWebClient" owl="1">
|
||||
<t t-name="project.ProjectSharingWebClient">
|
||||
<ActionContainer />
|
||||
<MainComponentsContainer/>
|
||||
</t>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
import { browser } from "@web/core/browser/browser";
|
||||
import { startUrl, router } from "@web/core/browser/router";
|
||||
import { patch } from "@web/core/utils/patch";
|
||||
|
||||
patch(router, {
|
||||
/**
|
||||
* @param {{ [key: string]: any }} state
|
||||
* @returns {string}
|
||||
*/
|
||||
stateToUrl(state) {
|
||||
const url = super.stateToUrl(state);
|
||||
return url.replace(startUrl(), "my/projects");
|
||||
},
|
||||
urlToState(urlObj) {
|
||||
const { pathname } = urlObj;
|
||||
urlObj.pathname = pathname.replace(
|
||||
/\/my\/projects\/([1234567890]+)\/project_sharing/,
|
||||
"/odoo/project.project/$1/project_sharing"
|
||||
);
|
||||
const state = super.urlToState(urlObj);
|
||||
if (state.actionStack?.length) {
|
||||
state.actionStack.shift();
|
||||
}
|
||||
return state;
|
||||
},
|
||||
});
|
||||
|
||||
// Since the patch for `stateToUrl` and `urlToState` is executed
|
||||
// after the router state was already initialized, it has to be replaced.
|
||||
router.replaceState(router.urlToState(new URL(browser.location)));
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-inherit="project.ProjectTaskControlPanel" t-inherit-mode="extension">
|
||||
<xpath expr="//div[hasclass('o_control_panel_main_buttons')]" position="before">
|
||||
<t t-call="project.ProjectSharingControlPanelNavigationButton" />
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-inherit="web.ControlPanel" t-inherit-mode="extension">
|
||||
<xpath expr="//div[hasclass('o_control_panel_main_buttons')]" position="before">
|
||||
<t t-call="project.ProjectSharingControlPanelNavigationButton" />
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
<t t-name="project.ProjectSharingControlPanelNavigationButton">
|
||||
<a class="btn btn-link" href="/my/projects" title="Go back to 'My Projects'"><i class="fa fa-arrow-left"/></a>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
<?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>
|
||||
|
|
@ -1,9 +1,6 @@
|
|||
/** @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";
|
||||
|
||||
|
|
@ -15,91 +12,58 @@ import { FormCompiler } from "@web/views/form/form_compiler";
|
|||
* @returns
|
||||
*/
|
||||
function compileChatter(node, params) {
|
||||
const chatterContainerXml = createElement('ChatterContainer');
|
||||
const chatterContainerXml = createElement("Chatter");
|
||||
const parentURLQuery = new URLSearchParams(window.parent.location.search);
|
||||
setAttributes(chatterContainerXml, {
|
||||
token: `'${parentURLQuery.get('access_token')}'` || '',
|
||||
resModel: params.resModel,
|
||||
resId: params.resId,
|
||||
token: `'${parentURLQuery.get("access_token")}'` || "",
|
||||
threadModel: params.resModel,
|
||||
threadId: params.resId,
|
||||
projectSharingId: params.projectSharingId,
|
||||
isFollower: params.isFollower,
|
||||
displayFollowButton: params.displayFollowButton,
|
||||
});
|
||||
const chatterContainerHookXml = createElement('div');
|
||||
chatterContainerHookXml.classList.add('o_FormRenderer_chatterContainer');
|
||||
const chatterContainerHookXml = createElement("div");
|
||||
chatterContainerHookXml.classList.add("o-mail-ChatterContainer", "o-mail-Form-chatter", "pt-2");
|
||||
setAttributes(chatterContainerHookXml, { "t-if": "!__comp__.env.inDialog" });
|
||||
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",
|
||||
selector: "chatter",
|
||||
fn: (node) =>
|
||||
compileChatter(node, {
|
||||
resId: "props.record.resId or undefined",
|
||||
resModel: "props.record.resModel",
|
||||
projectSharingId: "props.record.context.active_id_chatter",
|
||||
resId: "__comp__.props.record.resId or undefined",
|
||||
resModel: "__comp__.props.record.resModel",
|
||||
projectSharingId: "__comp__.props.record.context.active_id_chatter",
|
||||
isFollower: "__comp__.props.record.data.message_is_follower",
|
||||
displayFollowButton: "__comp__.props.record.data.display_follow_button",
|
||||
}),
|
||||
});
|
||||
|
||||
patch(FormCompiler.prototype, 'project_sharing_chatter', {
|
||||
patch(FormCompiler.prototype, {
|
||||
compile(node, params) {
|
||||
const res = this._super(node, params);
|
||||
const chatterContainerHookXml = res.querySelector('.o_FormRenderer_chatterContainer');
|
||||
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
|
||||
}
|
||||
if (chatterContainerHookXml.parentNode.classList.contains('o_form_sheet')) {
|
||||
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 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}`,
|
||||
"t-att-class": `{
|
||||
"overflow-x-hidden overflow-y-auto o-aside h-100": __comp__.uiService.size >= ${SIZES.XXL},
|
||||
"px-3 py-0": __comp__.uiService.size < ${SIZES.XXL},
|
||||
}`,
|
||||
});
|
||||
append(parentXml, chatterContainerHookXml);
|
||||
return res;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,36 +1,55 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { useService } from '@web/core/utils/hooks';
|
||||
import { createElement } from "@web/core/utils/xml";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
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';
|
||||
import { useService } from '@web/core/utils/hooks';
|
||||
import { useExternalListener } from "@odoo/owl";
|
||||
|
||||
export class ProjectSharingFormController extends FormController {
|
||||
static components = {
|
||||
...FormController.components,
|
||||
};
|
||||
|
||||
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;
|
||||
this.notification = useService('notification');
|
||||
useExternalListener(window, "paste", this.onGlobalPaste, { capture: true });
|
||||
useExternalListener(window, "drop", this.onGlobalDrop, { capture: true });
|
||||
}
|
||||
|
||||
getActionMenuItems() {
|
||||
get actionMenuItems() {
|
||||
return {};
|
||||
}
|
||||
|
||||
get translateAlert() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
ProjectSharingFormController.components = {
|
||||
...FormController.components,
|
||||
ChatterContainer,
|
||||
onGlobalPaste(ev) {
|
||||
if (ev.target.closest('.o_field_widget[name="description"]')) {
|
||||
ev.preventDefault();
|
||||
const items = ev.clipboardData.items;
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].type.indexOf('image') !== -1 && !this.model.root.resId) {
|
||||
this.notification.add(
|
||||
_t("Save the task to be able to paste images in description"),
|
||||
{ type: 'warning' },
|
||||
)
|
||||
ev.stopImmediatePropagation();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onGlobalDrop(ev) {
|
||||
if (ev.target.closest('.o_field_widget[name="description"]')) {
|
||||
ev.preventDefault();
|
||||
if(ev.dataTransfer.files.length > 0 && !this.model.root.resId){
|
||||
this.notification.add(
|
||||
_t("Save the task to be able to drag images in description"),
|
||||
{ type: 'warning' },
|
||||
)
|
||||
ev.stopImmediatePropagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
<?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>
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
/** @odoo-module */
|
||||
import { FormRenderer } from "@web/views/form/form_renderer";
|
||||
|
||||
import { ChatterContainer } from '../../components/chatter/chatter_container';
|
||||
import { FormRenderer } from '@web/views/form/form_renderer';
|
||||
import { Chatter } from "@mail/chatter/web_portal/chatter";
|
||||
|
||||
export class ProjectSharingFormRenderer extends FormRenderer { }
|
||||
ProjectSharingFormRenderer.components = {
|
||||
...FormRenderer.components,
|
||||
ChatterContainer,
|
||||
};
|
||||
export class ProjectSharingFormRenderer extends FormRenderer {
|
||||
static components = {
|
||||
...FormRenderer.components,
|
||||
Chatter,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { formView } from '@web/views/form/form_view';
|
||||
import { ProjectSharingFormController } from './project_sharing_form_controller';
|
||||
import { ProjectSharingFormRenderer } from './project_sharing_form_renderer';
|
||||
|
|
|
|||
|
|
@ -1,19 +1,16 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { kanbanView } from "@web/views/kanban/kanban_view";
|
||||
import { KanbanDynamicGroupList, KanbanModel } from "@web/views/kanban/kanban_model";
|
||||
import { ProjectTaskRelationalModel } from "@project/views/project_task_relational_model";
|
||||
import { ProjectTaskControlPanel } from "@project/views/project_task_control_panel/project_task_control_panel";
|
||||
|
||||
export class ProjectSharingTaskKanbanDynamicGroupList extends KanbanDynamicGroupList {
|
||||
get context() {
|
||||
return {
|
||||
...super.context,
|
||||
export class ProjectSharingTaskKanbanModel extends ProjectTaskRelationalModel {
|
||||
async _webReadGroup(config) {
|
||||
config.context = {
|
||||
...config.context,
|
||||
project_kanban: true,
|
||||
};
|
||||
return super._webReadGroup(...arguments);
|
||||
}
|
||||
}
|
||||
|
||||
export class ProjectSharingTaskKanbanModel extends KanbanModel {}
|
||||
|
||||
ProjectSharingTaskKanbanModel.DynamicGroupList = ProjectSharingTaskKanbanDynamicGroupList;
|
||||
|
||||
kanbanView.ControlPanel = ProjectTaskControlPanel;
|
||||
kanbanView.Model = ProjectSharingTaskKanbanModel;
|
||||
|
|
|
|||
|
|
@ -1,41 +0,0 @@
|
|||
/** @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]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,6 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import { listView } from "@web/views/list/list_view";
|
||||
|
||||
import { ProjectSharingListRenderer } from "./list_renderer";
|
||||
import { ProjectTaskControlPanel } from "@project/views/project_task_control_panel/project_task_control_panel";
|
||||
import { ProjectTaskRelationalModel } from "@project/views/project_task_relational_model";
|
||||
|
||||
const props = listView.props;
|
||||
listView.props = function (genericProps, view) {
|
||||
|
|
@ -12,4 +10,6 @@ listView.props = function (genericProps, view) {
|
|||
allowSelectors: false,
|
||||
};
|
||||
};
|
||||
listView.Renderer = ProjectSharingListRenderer;
|
||||
|
||||
listView.Model = ProjectTaskRelationalModel;
|
||||
listView.ControlPanel = ProjectTaskControlPanel;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
import { patch } from "@web/core/utils/patch";
|
||||
import { router } from "@web/core/browser/router";
|
||||
import { session } from "@web/session";
|
||||
import { View } from "@web/views/view";
|
||||
|
||||
/** Hack to display the project name when we load project sharing */
|
||||
patch(View.prototype, {
|
||||
setup() {
|
||||
super.setup();
|
||||
if (
|
||||
router.current.action === "project_sharing" &&
|
||||
!router.current.resId &&
|
||||
router.current.active_id === session.project_id
|
||||
) {
|
||||
this.env.config.setDisplayName(session.project_name);
|
||||
}
|
||||
},
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue