[IMP] mail: Make the channel name / description editable
Before: Only the channel name was updatable and needed refactoring. After: The description can be updated as well and both edition have the same code flow. task id 2620687 closes odoo/odoo#74962 Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
This commit is contained in:
committed by
Sébastien Theys
parent
51ff8b09db
commit
8ff923fec6
@@ -768,7 +768,24 @@ class Channel(models.Model):
|
||||
def channel_rename(self, name):
|
||||
self.ensure_one()
|
||||
self.write({'name': name})
|
||||
self._broadcast(self.channel_partner_ids.ids)
|
||||
self.env['bus.bus'].sendone((self._cr.dbname, 'mail.channel', self.id), {
|
||||
'type': 'mail.channel_rename',
|
||||
'payload': {
|
||||
'id': self.id,
|
||||
'name': name
|
||||
},
|
||||
})
|
||||
|
||||
def channel_change_description(self, description):
|
||||
self.ensure_one()
|
||||
self.write({'description': description})
|
||||
self.env['bus.bus'].sendone((self._cr.dbname, 'mail.channel', self.id), {
|
||||
'type': 'mail.channel_description_change',
|
||||
'payload': {
|
||||
'id': self.id,
|
||||
'description': description
|
||||
},
|
||||
})
|
||||
|
||||
def notify_typing(self, is_typing):
|
||||
""" Broadcast the typing notification to channel members
|
||||
|
||||
@@ -14,6 +14,7 @@ export class ThreadViewTopbar extends Component {
|
||||
setup() {
|
||||
super.setup();
|
||||
useRefToModel({ fieldName: 'threadNameInputRef', modelName: 'mail.thread_view_topbar', propNameAsRecordLocalId: 'localId', refName: 'threadNameInput' });
|
||||
useRefToModel({ fieldName: 'threadDescriptionInputRef', modelName: 'mail.thread_view_topbar', propNameAsRecordLocalId: 'localId', refName: 'threadDescriptionInput' });
|
||||
useUpdateToModel({ methodName: 'onComponentUpdate', modelName: 'mail.thread_view_topbar', propNameAsRecordLocalId: 'localId' });
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
height: $font-size-sm;
|
||||
}
|
||||
|
||||
.o_ThreadViewTopbar_threadName {
|
||||
.o_ThreadViewTopbar_threadName, .o_ThreadViewTopbar_threadDescription {
|
||||
border: $border-width solid;
|
||||
}
|
||||
|
||||
@@ -65,3 +65,24 @@
|
||||
border-color: $border-color;
|
||||
}
|
||||
}
|
||||
|
||||
.o_ThreadViewTopbar_threadDescription {
|
||||
|
||||
&:not(.o-isMouseOverThreadDescription) {
|
||||
border-color: transparent; // presence of border even if invisible to prevent flicker
|
||||
}
|
||||
|
||||
&.o-isMouseOverThreadDescription {
|
||||
background-color: $white;
|
||||
border-color: $border-color;
|
||||
}
|
||||
}
|
||||
|
||||
.o_ThreadViewTopbar_threadAddDescriptionEmptyLabel {
|
||||
color: gray('400');
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: gray('900');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<t t-if="threadViewTopBar.thread">
|
||||
<ThreadIcon class="o_ThreadViewTopbar_threadIcon mr-2 align-self-center" threadLocalId="threadViewTopBar.thread.localId"/>
|
||||
</t>
|
||||
<div class="o_ThreadViewTopbar_title d-flex flex-grow-1 align-self-center align-items-baseline">
|
||||
<div class="o_ThreadViewTopbar_title d-flex flex-grow-1 align-self-center align-items-center">
|
||||
<t t-if="threadViewTopBar.thread">
|
||||
<t t-if="!threadViewTopBar.isEditingThreadName">
|
||||
<div class="o_ThreadViewTopbar_threadName flex-shrink-0 mw-75 px-1 text-truncate lead font-weight-bold" t-att-title="threadViewTopBar.thread.displayName" t-on-click="threadViewTopBar.onClickTopbarThreadName" t-on-mouseenter="threadViewTopBar.onMouseEnterTopBarThreadName" t-on-mouseleave="threadViewTopBar.onMouseLeaveTopBarThreadName" t-att-class="{ 'o-isMouseOverThreadName': threadViewTopBar.isMouseOverThreadName }">
|
||||
@@ -23,11 +23,23 @@
|
||||
Discuss
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="threadViewTopBar.thread and threadViewTopBar.thread.description">
|
||||
<t t-if="threadViewTopBar.thread and threadViewTopBar.thread.isChannelDescriptionChangeable">
|
||||
<div class="o_ThreadViewTopbar_threadDescriptionSeparator flex-shrink-0 mx-2"/>
|
||||
<div class="o_ThreadViewTopbar_threadDescription text-truncate" t-att-title="threadViewTopBar.thread.description">
|
||||
<t t-esc="threadViewTopBar.thread.description"/>
|
||||
</div>
|
||||
<t t-if="!threadViewTopBar.isEditingThreadDescription">
|
||||
<t t-if="threadViewTopBar.thread.description">
|
||||
<div class="o_ThreadViewTopbar_threadDescription text-truncate px-1" t-att-title="threadViewTopBar.thread.description" t-on-click="threadViewTopBar.onClickTopbarThreadDescription" t-on-mouseenter="threadViewTopBar.onMouseEnterTopBarThreadDescription" t-on-mouseleave="threadViewTopBar.onMouseLeaveTopBarThreadDescription" t-att-class="{ 'o-isMouseOverThreadDescription': threadViewTopBar.isMouseOverThreadDescription }">
|
||||
<t t-esc="threadViewTopBar.thread.description"/>
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="!threadViewTopBar.thread.description and threadViewTopBar.thread.isChannelDescriptionChangeable">
|
||||
<div class="o_ThreadViewTopbar_threadAddDescriptionEmptyLabel text-truncate" t-on-click="threadViewTopBar.onClickTopbarThreadDescription">
|
||||
Add a description
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
<t t-if="threadViewTopBar.isEditingThreadDescription">
|
||||
<input class="o_ThreadViewTopbar_threadDescriptionInput" type="text" t-att-value="threadViewTopBar.pendingThreadDescription" t-on-input="threadViewTopBar.onInputThreadDescriptionInput" t-on-keydown="threadViewTopBar.onKeyDownThreadDescriptionInput" t-ref="threadDescriptionInput"/>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
<div class="o_ThreadViewTopbar_actions d-flex align-items-center">
|
||||
|
||||
+32
@@ -77,8 +77,12 @@ function factory(dependencies) {
|
||||
}
|
||||
if (typeof message === 'object') {
|
||||
switch (message.type) {
|
||||
case 'mail.channel_description_change':
|
||||
return this._handleNotificationChannelDescriptionChanged(message.payload);
|
||||
case 'mail.channel_joined':
|
||||
return this._handleNotificationChannelJoined(message.payload);
|
||||
case 'mail.channel_rename':
|
||||
return this._handleNotificationChannelRenamed(message.payload);
|
||||
}
|
||||
}
|
||||
const [, model, id] = channel;
|
||||
@@ -137,6 +141,20 @@ function factory(dependencies) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Object} payload
|
||||
* @param {integer} payload.id
|
||||
* @param {String} payload.description
|
||||
*/
|
||||
_handleNotificationChannelDescriptionChanged({ id, description }) {
|
||||
const channel = this.messaging.models['mail.thread'].insert({
|
||||
id,
|
||||
model: 'mail.channel',
|
||||
});
|
||||
channel.update({ description });
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {integer} channelId
|
||||
@@ -194,6 +212,20 @@ function factory(dependencies) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Object} payload
|
||||
* @param {integer} payload.id
|
||||
* @param {String} payload.name
|
||||
*/
|
||||
_handleNotificationChannelRenamed({ id, name }) {
|
||||
const channel = this.messaging.models['mail.thread'].insert({
|
||||
id,
|
||||
model: 'mail.channel',
|
||||
});
|
||||
channel.update({ name });
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {integer} channelId
|
||||
|
||||
@@ -121,6 +121,22 @@ function factory(dependencies) {
|
||||
// Public
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Changes description of the thread to the given new description.
|
||||
* Only makes sense for channels.
|
||||
*
|
||||
* @param {string} description
|
||||
*/
|
||||
async changeDescription(description) {
|
||||
this.update({ description });
|
||||
return this.env.services.rpc({
|
||||
model: 'mail.channel',
|
||||
method: 'channel_change_description',
|
||||
args: [[this.id]],
|
||||
kwargs: { description },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @static
|
||||
* @param {mail.thread} [thread] the concerned thread
|
||||
@@ -988,14 +1004,15 @@ function factory(dependencies) {
|
||||
* Renames this thread to the given new name.
|
||||
* Only makes sense for channels.
|
||||
*
|
||||
* @param {string} newName
|
||||
* @param {string} name
|
||||
*/
|
||||
async rename(newName) {
|
||||
async rename(name) {
|
||||
this.update({ name });
|
||||
return this.env.services.rpc({
|
||||
model: 'mail.channel',
|
||||
method: 'channel_rename',
|
||||
args: [this.id],
|
||||
kwargs: { name: newName },
|
||||
args: [[this.id]],
|
||||
kwargs: { name },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1189,6 +1206,14 @@ function factory(dependencies) {
|
||||
return ['chat', 'livechat'].includes(this.channel_type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
*/
|
||||
_computeIsChannelDescriptionChangeable() {
|
||||
return this.model === 'mail.channel' && this.channel_type === 'channel';
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
@@ -1750,6 +1775,13 @@ function factory(dependencies) {
|
||||
id: attr({
|
||||
required: true,
|
||||
}),
|
||||
/**
|
||||
* Determines whether this description can be changed.
|
||||
* Only makes sense for channels.
|
||||
*/
|
||||
isChannelDescriptionChangeable: attr({
|
||||
compute: '_computeIsChannelDescriptionChangeable',
|
||||
}),
|
||||
/**
|
||||
* Determines whether this thread can be renamed.
|
||||
* Only makes sense for channels.
|
||||
|
||||
@@ -22,6 +22,11 @@ function factory(dependencies) {
|
||||
this.onMouseEnterTopBarThreadName = this.onMouseEnterTopBarThreadName.bind(this);
|
||||
this.onMouseLeaveTopBarThreadName = this.onMouseLeaveTopBarThreadName.bind(this);
|
||||
this._onClickCaptureGlobal = this._onClickCaptureGlobal.bind(this);
|
||||
this.onClickTopbarThreadDescription = this.onClickTopbarThreadDescription.bind(this);
|
||||
this.onInputThreadDescriptionInput = this.onInputThreadDescriptionInput.bind(this);
|
||||
this.onKeyDownThreadDescriptionInput = this.onKeyDownThreadDescriptionInput.bind(this);
|
||||
this.onMouseEnterTopBarThreadDescription = this.onMouseEnterTopBarThreadDescription.bind(this);
|
||||
this.onMouseLeaveTopBarThreadDescription = this.onMouseLeaveTopBarThreadDescription.bind(this);
|
||||
document.addEventListener('click', this._onClickCaptureGlobal, true);
|
||||
return super._created();
|
||||
}
|
||||
@@ -81,6 +86,27 @@ function factory(dependencies) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles click on the "thread description" of this top bar.
|
||||
*
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
onClickTopbarThreadDescription(ev) {
|
||||
if (!this.thread || !this.thread.isChannelDescriptionChangeable) {
|
||||
return;
|
||||
}
|
||||
const selection = window.getSelection();
|
||||
this.update({
|
||||
doFocusOnThreadDescriptionInput: true,
|
||||
doSetSelectionDirectionOnThreadDescriptionInput: selection.anchorOffset < selection.focusOffset ? 'forward' : 'backward',
|
||||
doSetSelectionEndOnThreadDescriptionInput: Math.max(selection.focusOffset, selection.anchorOffset),
|
||||
doSetSelectionStartOnThreadDescriptionInput: Math.min(selection.focusOffset, selection.anchorOffset),
|
||||
isEditingThreadDescription: true,
|
||||
isMouseOverThreadDescription: false,
|
||||
pendingThreadDescription: this.thread.description || "",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles click on the "unstar all" button of Starred box.
|
||||
*
|
||||
@@ -98,6 +124,10 @@ function factory(dependencies) {
|
||||
this.threadNameInputRef.el.focus();
|
||||
this.update({ doFocusOnThreadNameInput: clear() });
|
||||
}
|
||||
if (this.doFocusOnThreadDescriptionInput) {
|
||||
this.threadDescriptionInputRef.el.focus();
|
||||
this.update({ doFocusOnThreadDescriptionInput: clear() });
|
||||
}
|
||||
if (
|
||||
this.doSetSelectionStartOnThreadNameInput !== undefined &&
|
||||
this.doSetSelectionEndOnThreadNameInput !== undefined &&
|
||||
@@ -114,6 +144,22 @@ function factory(dependencies) {
|
||||
doSetSelectionStartOnThreadNameInput: clear(),
|
||||
});
|
||||
}
|
||||
if (
|
||||
this.doSetSelectionStartOnThreadDescriptionInput !== undefined &&
|
||||
this.doSetSelectionEndOnThreadDescriptionInput !== undefined &&
|
||||
this.doSetSelectionDirectionOnThreadDescriptionInput !== undefined
|
||||
) {
|
||||
this.threadDescriptionInputRef.el.setSelectionRange(
|
||||
this.doSetSelectionStartOnThreadDescriptionInput,
|
||||
this.doSetSelectionEndOnThreadDescriptionInput,
|
||||
this.doSetSelectionDirectionOnThreadDescriptionInput
|
||||
);
|
||||
this.update({
|
||||
doSetSelectionDirectionOnThreadDescriptionInput: clear(),
|
||||
doSetSelectionEndOnThreadDescriptionInput: clear(),
|
||||
doSetSelectionStartOnThreadDescriptionInput: clear(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -125,6 +171,15 @@ function factory(dependencies) {
|
||||
this.update({ pendingThreadName: ev.target.value });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles input on the "thread description" input of this top bar.
|
||||
*
|
||||
* @param {InputEvent} ev
|
||||
*/
|
||||
onInputThreadDescriptionInput(ev) {
|
||||
this.update({ pendingThreadDescription: ev.target.value });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles keydown on the "thread name" input of this top bar.
|
||||
*
|
||||
@@ -141,6 +196,22 @@ function factory(dependencies) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles keydown on the "thread description" input of this top bar.
|
||||
*
|
||||
* @param {KeyboardEvent} ev
|
||||
*/
|
||||
onKeyDownThreadDescriptionInput(ev) {
|
||||
switch (ev.key) {
|
||||
case 'Enter':
|
||||
this._applyThreadChangeDescription();
|
||||
break;
|
||||
case 'Escape':
|
||||
this._discardThreadChangeDescription();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles mouseenter on the "thread name" of this top bar.
|
||||
*
|
||||
@@ -153,6 +224,18 @@ function factory(dependencies) {
|
||||
this.update({ isMouseOverThreadName: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles mouseenter on the "thread description" of this top bar.
|
||||
*
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
onMouseEnterTopBarThreadDescription(ev) {
|
||||
if (!this.thread || !this.thread.isChannelDescriptionChangeable) {
|
||||
return;
|
||||
}
|
||||
this.update({ isMouseOverThreadDescription: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles mouseleave on the "thread name" of this top bar.
|
||||
*
|
||||
@@ -162,6 +245,15 @@ function factory(dependencies) {
|
||||
this.update({ isMouseOverThreadName: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles mouseleave on the "thread description" of this top bar.
|
||||
*
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
onMouseLeaveTopBarThreadDescription(ev) {
|
||||
this.update({ isMouseOverThreadDescription: false });
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Private
|
||||
//----------------------------------------------------------------------
|
||||
@@ -183,6 +275,20 @@ function factory(dependencies) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_applyThreadChangeDescription() {
|
||||
const newDescription = this.pendingThreadDescription || "";
|
||||
this.update({
|
||||
isEditingThreadDescription: false,
|
||||
pendingThreadDescription: clear(),
|
||||
});
|
||||
if (this.thread.channel_type === 'channel' && newDescription !== this.thread.description) {
|
||||
this.thread.changeDescription(newDescription);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
@@ -193,20 +299,27 @@ function factory(dependencies) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_discardThreadChangeDescription() {
|
||||
this.update({
|
||||
isEditingThreadDescription: false,
|
||||
pendingThreadDescription: clear(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {MouseEvent} ev
|
||||
*/
|
||||
_onClickCaptureGlobal(ev) {
|
||||
if (!this.threadNameInputRef) {
|
||||
return;
|
||||
}
|
||||
if (this.threadNameInputRef.el && this.threadNameInputRef.el.contains(ev.target)) {
|
||||
return;
|
||||
}
|
||||
if (this.isEditingThreadName) {
|
||||
if (this.isEditingThreadName && this.threadNameInputRef.el && !this.threadNameInputRef.el.contains(ev.target)) {
|
||||
this._applyThreadRename();
|
||||
}
|
||||
if (this.isEditingThreadDescription && this.threadDescriptionInputRef.el && !this.threadDescriptionInputRef.el.contains(ev.target)) {
|
||||
this._applyThreadChangeDescription();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -217,6 +330,10 @@ function factory(dependencies) {
|
||||
*/
|
||||
doFocusOnThreadNameInput: attr(),
|
||||
/**
|
||||
* Determines whether this thread description input needs to be focused.
|
||||
*/
|
||||
doFocusOnThreadDescriptionInput: attr(),
|
||||
/**
|
||||
* Determines the direction to set on the selection of this thread name
|
||||
* input. This value is not a representation of current selection, but
|
||||
* an instruction to set a new selection. Must be set together with
|
||||
@@ -224,6 +341,14 @@ function factory(dependencies) {
|
||||
* to have an effect.
|
||||
*/
|
||||
doSetSelectionDirectionOnThreadNameInput: attr(),
|
||||
/**
|
||||
* Determines the direction to set on the selection of this thread description
|
||||
* input. This value is not a representation of current selection, but
|
||||
* an instruction to set a new selection. Must be set together with
|
||||
* `doSetSelectionEndOnThreadDescriptionInput` and `doSetSelectionStartOnThreadDescriptionInput`
|
||||
* to have an effect.
|
||||
*/
|
||||
doSetSelectionDirectionOnThreadDescriptionInput: attr(),
|
||||
/**
|
||||
* Determines the ending position where to place the selection on this
|
||||
* thread name input (zero-based index). This value is not a
|
||||
@@ -232,6 +357,14 @@ function factory(dependencies) {
|
||||
* and `doSetSelectionStartOnThreadNameInput` to have an effect.
|
||||
*/
|
||||
doSetSelectionEndOnThreadNameInput: attr(),
|
||||
/**
|
||||
* Determines the ending position where to place the selection on this
|
||||
* thread description input (zero-based index). This value is not a
|
||||
* representation of current selection, but an instruction to set a new
|
||||
* selection. Must be set together with `doSetSelectionDirectionOnThreadDescriptionInput`
|
||||
* and `doSetSelectionStartOnThreadDescriptionInput` to have an effect.
|
||||
*/
|
||||
doSetSelectionEndOnThreadDescriptionInput: attr(),
|
||||
/**
|
||||
* Determines the starting position where to place the selection on this
|
||||
* thread name input (zero-based index). This value is not a
|
||||
@@ -240,12 +373,26 @@ function factory(dependencies) {
|
||||
* `doSetSelectionEndOnThreadNameInput` to have an effect.
|
||||
*/
|
||||
doSetSelectionStartOnThreadNameInput: attr(),
|
||||
/**
|
||||
* Determines the starting position where to place the selection on this
|
||||
* thread description input (zero-based index). This value is not a
|
||||
* representation of current selection, but an instruction to set a new
|
||||
* selection. Must be set together with `doSetSelectionDirectionOnThreadDescriptionInput` and
|
||||
* `doSetSelectionEndOnThreadDescriptionInput` to have an effect.
|
||||
*/
|
||||
doSetSelectionStartOnThreadDescriptionInput: attr(),
|
||||
/**
|
||||
* Determines whether this thread is currently being renamed.
|
||||
*/
|
||||
isEditingThreadName: attr({
|
||||
default: false,
|
||||
}),
|
||||
/**
|
||||
* Determines whether this thread description is currently being changed.
|
||||
*/
|
||||
isEditingThreadDescription: attr({
|
||||
default: false,
|
||||
}),
|
||||
/**
|
||||
* States whether the cursor is currently over this thread name in
|
||||
* the top bar.
|
||||
@@ -253,6 +400,13 @@ function factory(dependencies) {
|
||||
isMouseOverThreadName: attr({
|
||||
default: false,
|
||||
}),
|
||||
/**
|
||||
* States whether the cursor is currently over this thread description in
|
||||
* the top bar.
|
||||
*/
|
||||
isMouseOverThreadDescription: attr({
|
||||
default: false,
|
||||
}),
|
||||
/**
|
||||
* Determines the pending name of this thread, which is the new name of
|
||||
* the thread as the current user is currently typing it, with the goal
|
||||
@@ -262,6 +416,15 @@ function factory(dependencies) {
|
||||
pendingThreadName: attr({
|
||||
default: "",
|
||||
}),
|
||||
/**
|
||||
* Determines the pending description of this thread, which is the new description of
|
||||
* the thread as the current user is currently typing it, with the goal
|
||||
* of changing the description the thread.
|
||||
* This value can either be applied or discarded.
|
||||
*/
|
||||
pendingThreadDescription: attr({
|
||||
default: "",
|
||||
}),
|
||||
/**
|
||||
* States the thread that is displayed by this top bar.
|
||||
*/
|
||||
@@ -273,6 +436,11 @@ function factory(dependencies) {
|
||||
* Useful to focus it, or to know when a click is done outside of it.
|
||||
*/
|
||||
threadNameInputRef: attr(),
|
||||
/**
|
||||
* States the OWL ref of the thread description input of this top bar.
|
||||
* Useful to focus it, or to know when a click is done outside of it.
|
||||
*/
|
||||
threadDescriptionInputRef: attr(),
|
||||
/**
|
||||
* States the thread view managing this top bar.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user