In odoo 15, how can I add new action menu in user menu? In other odoo versions (13 and 14), it was possible by inheriting from UserMenu.Actions.
In odoo 15, I tried the following code but it is not working.
Thanks for any suggestion
/** #odoo-module **/
import { registry } from "#web/core/registry";
import { preferencesItem } from "#web/webclient/user_menu/user_menu_items";
export function UserLog(env) {
return Object.assign(
{},
preferencesItem(env),
{
type: "item",
id: "log",
description: env._t("UserRecent Log"),
callback: async function () {
const actionDescription = await env.services.orm.call("user.recent.log", "action_get");
actionDescription.res_id = env.services.user.userId;
env.services.action.doAction(actionDescription);
},
sequence: 70,
}
);
}
registry.category("user_menuitems").add('profile', UserLog, { force: true })
This is my model code.
class UserRecentLog(models.Model):
_name = 'user.recent.log'
_order = "last_visited_on desc"
#api.model
def action_get(self):
return self.env['ir.actions.act_window']._for_xml_id('user_recent_log.action_user_activity')
This is my xml view.
<!-- actions opening views on models -->
<record model="ir.actions.act_window" id="action_user_activity">
<field name="name">User Recent Log(s)</field>
<field name="res_model">user.recent.log</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="user_activity_view_tree"/>
</record>
You don't need to change anything, your code should work. You can check the user preferences menu item in web module (similar to your menu item).
export function preferencesItem(env) {
return {
type: "item",
id: "settings",
description: env._t("Preferences"),
callback: async function () {
const actionDescription = await env.services.orm.call("res.users", "action_get");
actionDescription.res_id = env.services.user.userId;
env.services.action.doAction(actionDescription);
},
sequence: 50,
};
}
registry
.category("user_menuitems")
.add("profile", preferencesItem)
There is another implementation in hr module:
import { registry } from "#web/core/registry";
import { preferencesItem } from "#web/webclient/user_menu/user_menu_items";
export function hrPreferencesItem(env) {
return Object.assign(
{},
preferencesItem(env),
{
description: env._t('My Profile'),
}
);
}
registry.category("user_menuitems").add('profile', hrPreferencesItem, { force: true })
So you can rewrite your code above as following:
import { registry } from "#web/core/registry";
import { preferencesItem } from "#web/webclient/user_menu/user_menu_items";
export function UserLog(env) {
return Object.assign(
{},
preferencesItem(env),
{
type: "item",
id: "log",
description: env._t("Log"),
callback: async function () {
const actionDescription = await env.services.orm.call("res.users.log", "action_user_activity");
env.services.action.doAction(actionDescription);
},
sequence: 70,
}
);
}
registry.category("user_menuitems").add('profile', UserLog, { force: true })
Edit:
The tree view mode is ignored when executing the window action.
The _executeActWindowAction will check for the tree view type in the views registry to construct the views object and unfortunately, the tree view mode was not added to that registry.
To show the tree view, you can add [false, 'list'] to the views list and specify the view type (list) in the doAction options:
actionDescription.views.push([actionDescription.view_id[0], 'list'])
env.services.action.doAction(actionDescription, {viewType: 'list'});
Or update the views list and change tree to list:
actionDescription.views[0][1] = 'list';
Of course , you can do the same in the action_get method:
action = self.env['ir.actions.act_window']._for_xml_id('user_recent_log.action_user_activity')
action['views'][0] = action['view_id'][0], 'list'
return action
Related
I have a custom SAPUI5 application that used SAPUI5 version 1.40.11.
Since this version has been removed from CDN recently, I had to use a long-term maintenance available compatible version for the app. It is - version - 1.38.55.
I'm having an issue with MessagePopoverItem as the subtitle property is not available for this version.
So I'm trying to customize the template of the MessagePopoverItem to display the subtitle field.
While I'm trying to do this via a custom control I received the following error as it looks for a renderer.js from CDN without looking at it from the local location.
Cannot load from
https://sapui5.hana.ondemand.com/1.38.55/resources/sap/m/MessagePopoverItemRenderer.js
This is my custom controller and it's renderer.js
And I followed the below link
TooltipBaseRenderer.js: 404 - NOT FOUND
sap.ui.define([
"sap/m/MessagePopoverItem",
"./CustomMessagePopupItemRender",
], function (MessagePopoverItem,CustomMessagePopupItemRender) {
"use strict";
return MessagePopoverItem.extend("demoapp.customControls.CustomMessagePopoverItem", {
renderer:CustomMessagePopupItemRender,
metadata : {
properties: {
subtitle: {
type: "string",
defaultValue: ""
}
}
},
onAfterRendering: function() {
if(MessagePopoverItem.prototype.onAfterRendering) {
MessagePopoverItem.prototype.onAfterRendering.apply(this, arguments);
}
//...check if there is a highlight and tooltip
if(this.getTitle() !== "") {
sapMLIBContent
var oHl = this.$().find(".sapMLIBContent");
var ss = oHl;
}
}
});
});
Renderer.js
sap.ui.define([
"sap/ui/core/Renderer",
], function(Renderer) {
"use strict";
return Renderer.extend("demoapp.customControls.CustomMessagePopupItemRender", {
apiVersion: 2,
render: function(renderManager, control) {
// Issue: render function is called twice.
// See: https://github.com/SAP/openui5/issues/3169
// Update: fixed since 1.88
const child = control.getAggregation("content");
if (child && child.isA("sap.ui.core.Control")) {
renderManager.openStart("div", control)
.accessibilityState(control, { role: "tooltip" })
.style("max-width", "95vw")
.style("width", control.getWidth())
.openEnd()
.renderControl(child)
.close("div");
} else {
renderManager.openStart("span", control)
.accessibilityState(control, { role: "tooltip" })
.style("max-width", "90vw")
.style("width", control.getWidth())
.style("padding", "0.5rem")
.class("sapThemeBaseBG-asBackgroundColor")
.openEnd()
.text(control.getText())
.close("span");
}
},
});
});
Is this possible to customize the item template of the MessagePopover?
TooltipBaseRenderer.js: 404 - NOT FOUND is a completely different issue than the issue in this question. The element sap.m.MessagePopoverItem which extends sap.ui.core.Item, is not a sap.ui.core.Control but an sap.ui.core.Element. It is not supposed to have a renderer by definition.
sap.ui.core.TooltipBase, on the other hand, is a sap.ui.core.Control. It's a different issue.
Instead, you'll have to:
Extend sap.m.MessagePopoverItem by the subTitle property but without a renderer since it's not a control.
Extend the control that actually renders items using the information from your extended MessagePopoverItem element.
I have managed to extend the sap.m.MessagePopoverItem as mentioned by #Boghyon Hoffmann.
Here's the answer
CustomMessagePopoverItem.js
sap.ui.define([
"sap/m/MessagePopoverItem",
], function (MessagePopoverItem) {
"use strict";
return MessagePopoverItem.extend("demoapp.customControls.CustomMessagePopoverItem", {
metadata : {
properties: {
subtitle: {
type: "string",
defaultValue: ""
}
}
}
});
});
CustomMessagePopover.js
sap.ui.define([
"sap/m/MessagePopover",
"sap/m/StandardListItem",
"sap/ui/core/IconPool",
"./CustomMessagePopoverItem"
], function (MessagePopover,StandardListItem,IconPool,CustomMessagePopoverItem) {
"use strict";
var MessagePopover = MessagePopover.extend("demoapp.customControls.CustomMessagePopover", {
metadata : {
aggregations: {
/**
* A list with message items
*/
items: {type: "demoapp.customControls.CustomMessagePopoverItem", multiple: true, singularName: "item"},
},
},
//Override following method to add the subtitle as the description of the Standard List Item
_mapItemToListItem :function (oMessagePopoverItem) {
if (!oMessagePopoverItem) {
return null;
}
var sType = oMessagePopoverItem.getType(),
oListItem = new StandardListItem({
title: oMessagePopoverItem.getTitle(),
icon: this._mapIcon(sType),
description :oMessagePopoverItem.getSubtitle(),
type: sap.m.ListType.Navigation
}).addStyleClass(CSS_CLASS + "Item").addStyleClass(CSS_CLASS + "Item" + sType);
oListItem._oMessagePopoverItem = oMessagePopoverItem;
return oListItem;
}
});
var CSS_CLASS = "sapMMsgPopover",
ICONS = {
back: IconPool.getIconURI("nav-back"),
close: IconPool.getIconURI("decline"),
information: IconPool.getIconURI("message-information"),
warning: IconPool.getIconURI("message-warning"),
error: IconPool.getIconURI("message-error"),
success: IconPool.getIconURI("message-success")
},
LIST_TYPES = ["all", "error", "warning", "success", "information"],
// Property names array
ASYNC_HANDLER_NAMES = ["asyncDescriptionHandler", "asyncURLHandler"],
// Private class variable used for static method below that sets default async handlers
DEFAULT_ASYNC_HANDLERS = {
asyncDescriptionHandler: function (config) {
var sLongTextUrl = config.item.getLongtextUrl();
if (sLongTextUrl) {
jQuery.ajax({
type: "GET",
url: sLongTextUrl,
success: function (data) {
config.item.setDescription(data);
config.promise.resolve();
},
error: function() {
var sError = "A request has failed for long text data. URL: " + sLongTextUrl;
jQuery.sap.log.error(sError);
config.promise.reject(sError);
}
});
}
}
};
});
Here I invoke the custom control from the controller
var oMessageTemplate = new demoapp.customControls.CustomMessagePopoverItem({
type: '{TYPE}',
title: 'My Title',
description: '{DESC}',
subtitle: '{DESC}'
});
var oMessagePopover2 = new demoapp.customControls.CustomMessagePopover({
items: {
path: '/Messages',
template: oMessageTemplate
}
});
The awesome tiptap wrapper for prosemirror comes with nice documentation but it lacks some clarification how to approach some (i think) basic scenarios when developing custom extensions.
My question is how to invoke toggleWrap on the node when in vue component's context.
I found example that uses transactions and allows for delete - but what i want is to clear the node leaving the text of node intact.
get view() {
return {
directives: {
"click-outside": clickOutside
},
props: ['node', 'updateAttrs', 'view', 'selected', 'getPos'],
data() {
return {
showMenu: false
}
},
computed: {
href: {
get() {
return this.node.attrs.href
},
set(href) {
this.updateAttrs({
href,
})
},
},
},
methods: {
// deleteNode() {
// let transaction = this.view.state.tr // tr - transaction
// let pos = this.getPos()
// transaction.delete(pos, pos + this.node.nodeSize)
// this.view.dispatch(transaction)
// },
stopLinkPropagation(){
return null;
},
hideMenu(){
this.showMenu = false
}
},
template: `<div #click="showMenu = true" v-click-outside="hideMenu">
<a class="email-button" #click.prevent="stopLinkPropagation" :href="href" v-text="node.textContent"></a>
<input class="iframe__input" type="text" v-model="href" v-if="showMenu" />
<button #click="clearNode">clear button wrap</button>
</div>`,
}
}
Any help would be awesome. Thanks.
I'm studying javascript and mithril.js 1.1.6. I'm writing down a simple web app in which users land on a page where he can login. Users who already did login land on a different page. I'm trying this using conditional routing, here is the main component:
const m = require("mithril");
...
import Eventbus from './whafodi/eventbus.js';
import WelcomePage from './ui/welcome.js';
import User from './model/user.js';
var eventbus = new Eventbus();
function MyApp() {
return {
usrAuth: function() {
m.route(document.body, "/", {
"/": { view: () => m("p", "hello")}
})
},
usrNotAuth: function() {
m.route(document.body, "/", {
"/": { render: v => m(WelcomePage, eventbus) }
})
},
oninit: function(vnode) {
vnode.state.user = new User();
eventbus.subscribe({
type: "login",
handle: function(action) {
vnode.state.user.token = action.token;
console.log(JSON.stringify(vnode.state.user));
}
});
},
view: function(vnode) {
if(vnode.state.user.token) {
this.usrAuth();
} else {
this.usrNotAuth();
}
}
}
};
m.mount(document.body, MyApp);
MyApp is the main component. It check if user has a token, then return the proper route. This is the component that is in charge to let users login:
const m = require("mithril");
const hellojs = require("hellojs");
function TopBar(node) {
var bus = node.attrs.eventbus;
function _login() {
hellojs('facebook').login({scope:'email'});
}
return {
oninit: function(vnode) {
hellojs.init({
facebook: XXXXXXX,
}, {
redirect_uri: 'http://localhost'
});
hellojs.on('auth.login', auth => {
var fbtoken = auth.authResponse.access_token;
m.request({
method:"POST",
url:"./myapp/login/fb/token",
data:auth.authResponse,
background: true
}).then(function(result){
console.log(result);
bus.publish({ type: "login", token: result.jwttoken });
m.route.set("/");
}, function(error){
console.log(error);
bus.publish({ type: "login", token: "" });
});
});
},
view: function(vnode) {
return m("div", [
m("button", { onclick: _login }, "Login")
]);
}
}
}
export default TopBar;
TopBar component occurs in the WelcomePage component mentioned in the main one. TopBar renders a button and use hello.js to login. It uses the EventBus bus parameter to tell main component user logged in (there is an handler in main component to update the user model). Once user logins, event is fired and main component updates the user model. Good. Now, how can trigger the main component to load the right route?
I read mithril'docs again and I found that RouteResolvers perfectly suit my needs. Here is an example:
var App = (function() {
var login;
function isLoggedIn(component) {
if(login) {
return component;
} else {
m.route.set("/hey");
}
}
return {
oninit: function(vnode) {
EventBus.subscribe({
type: "login",
handle: function(action) {
console.log("incoming action: " + JSON.stringify(action));
login = action.value;
}
});
},
oncreate: function(vnode) {
Foo.eventbus = EventBus;
Bar.eventbus = EventBus;
Hey.eventbus = EventBus;
m.route(document.body, "/hey", {
"/foo": {
onmatch: function(args, requestedPath, route) { return isLoggedIn(Foo); }
},
"/bar": {
onmatch: function(args, requestedPath, route) { return isLoggedIn(Bar); }
},
"/hey": Hey
});
},
view: function(vnode) {
return m("div", "home..");
}
};
})();
Eventbus is used to let components communicate with App. They fire events (login type events) that App can handle. I found convenient to pass Eventbus the way oncreate method shows, I can use Eventbus in each component's oncreate to let components fire events.
Managed to add a custom dropdown button to the toolbar:
But I don't know how to add a label or an icon to it.
Here's my code:
import Plugin from '#ckeditor/ckeditor5-core/src/plugin';
import Model from '#ckeditor/ckeditor5-ui/src/model';
import { createDropdown, addListToDropdown } from '#ckeditor/ckeditor5-ui/src/dropdown/utils';
import Collection from '#ckeditor/ckeditor5-utils/src/collection';
import imageIcon from '#ckeditor/ckeditor5-core/theme/icons/image.svg';
export default class ImageDropdown extends Plugin {
static get pluginName() {
return 'ImageDropdown';
}
init() {
const editor = this.editor;
const t = editor.t;
const defaultTitle = t('Add image');
const dropdownTooltip = t('Image');
// Register UI component
editor.ui.componentFactory.add('imageDropdown', locale => {
const dropdownView = createDropdown( locale );
dropdownView.set({
label: 'Image',
tooltip: true
});
dropdownView.buttonView.set( {
isOn: false,
withText: true,
tooltip: dropdownTooltip
});
dropdownView.extendTemplate( {
attributes: {
class: [
'ck-image-dropdown'
]
}
});
// The collection of the list items.
const items = new Collection();
items.add( {
type: 'button',
model: new Model( {
label: 'Uppload image',
icon: imageIcon
})
});
items.add( {
type: 'button',
model: new Model( {
label: 'Image URL',
icon: imageIcon
})
});
// Create a dropdown with a list inside the panel.
addListToDropdown( dropdownView, items );
return dropdownView;
});
}
}
Setting labels, icons etc. for a dropdown button should take place on the dropdown's view instance:
dropdownView.buttonView.set({
label: 'some-label',
icon: 'path/to/some/icon'
tooltip: true
});
Note that these properties are observable and can be dynamically evaluated based on some state using the ObservableMixin#bind function.
See an example here: https://github.com/ckeditor/ckeditor5-alignment/blob/894745ecb1e8bd94286b4089eb16079034eb8a0b/src/alignmentui.js#L107-L124
So I'm retrieving my data from my api using vue-resource which is happening correctly, the state is updated and from the console I am able to see the values I'm requesting. My problem is that when the application loads the data from the store doesn't seem to be impacting the application on load, but if for example I change between pages the information is displayed correctly. This is leading me to believe somewhere along the way I have gotten the life cycle hooks incorrect, or I have handled the state incorrectly inside vuex.
Vuex store
import Vue from 'vue'
import Vuex from 'vuex'
import VueResource from 'vue-resource'
Vue.use(VueResource)
Vue.use(Vuex)
const state = {
twitter: 0,
instagram: 0,
youtube: 0,
twitch: 0
}
const actions = {
LOAD_METRICS: ({commit}) => {
Vue.http.get('http://109.74.195.166:2000/metrics').then(response => {
let out = [{
twitter: Number(response.body[0].twitter),
instagram: Number(response.body[0].instagram),
youtube: Number(response.body[0].youtube),
twitch: Number(response.body[0].twitch)
}]
commit('SET_METRICS', out)
}).catch((e) => {
console.log(e)
})
}
}
const mutations = {
SET_METRICS (state, obj) {
state.twitter = obj[0].twitter
state.instagram = obj[0].instagram
state.youtube = obj[0].youtube
state.twitch = obj[0].twitch
}
}
const getters = {}
export default new Vuex.Store({
state,
getters,
actions,
mutations
})
Here I am trying to dispatch an event to gather the needed information using a mutation.
<template>
<div id="app">
<NavigationTop></NavigationTop>
<router-view></router-view>
<SocialBar></SocialBar>
<CopyrightBar></CopyrightBar>
</div>
</template>
<script>
export default {
name: 'app',
ready: function () {
this.$store.dispatch('LOAD_METRICS')
}
}
</script>
<style>
#import url('https://fonts.googleapis.com/css?family=Roboto:400,700,900');
#app {
font-family: 'Roboto', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: white;
background: url('./assets/Images/bodyBackground.jpg');
}
</style>
Then finally I am requesting the information inside of the component to be used by countup.js and also giving it to the method inside data.
<template>
<div class="hero">
<div class="container hero-content">
<div class="row hero-back align-items-end">
<div class="col-lg-6">
<div class="row">
<div class="col-lg-6" v-for="icons in socialIcons">
<Hero-Tile
:name="icons.name"
:icon="icons.iconName"
:count="icons.count"
:numeric="icons.numeric"
></Hero-Tile>
<h1>{{origin}}</h1>
</div>
</div>
</div>
</div>
</div>
<div class="diagonal-left-lines"></div>
<div class="home-hero-img"><img class="img-fluid" src="../../assets/Images/home-hero.jpg"></div>
</div>
</template>
<script>
import HeroTile from './Hero-Tile'
import CountUp from 'countup.js'
export default {
components: {HeroTile},
name: 'hero',
data () {
return {
origin: '',
socialIcons: [
{
name: 'twitter',
iconName: 'twitter',
count: this.$store.state.twitter,
numeric: 26000
},
{
name: 'instagram',
iconName: 'instagram',
count: this.$store.state.instagram,
numeric: 35000
},
{
name: 'youtube',
iconName: 'youtube-play',
count: this.$store.state.youtube,
numeric: 15000
},
{
name: 'twitch',
iconName: 'twitch',
count: this.$store.state.twitch,
numeric: 127000
}
]
}
},
methods: {
updateNumbers: function () {
let options = {
useEasing: true,
useGrouping: true,
separator: ',',
decimal: '.',
prefix: '',
suffix: 'K'
}
function kFormatter (num) {
return num > 999 ? (num / 1000).toFixed(1) : num
}
let twitter = new CountUp('twitter', 0, kFormatter(this.$store.state.twitter), 0, 3, options)
let instagram = new CountUp('instagram', 0, kFormatter(this.$store.state.instagram), 0, 3, options)
let youtube = new CountUp('youtube', 0, kFormatter(this.$store.state.youtube), 0, 3, options)
let twitch = new CountUp('twitch', 0, kFormatter(this.$store.state.twitch), 0, 3, options)
twitter.start()
instagram.start()
youtube.start()
twitch.start()
}
},
mounted: function () {
this.updateNumbers()
}
}
</script>
To be clear at the moment it seems to just load '0k' so it's as if there is some form of race condition occurring causing it not to actually load the information on load-up. Though I'm not sure what the correct approach is here.
This was eventually solved by what I'm going to describe as hacking as I don't actually know the exact correct answer at this time. Though what I have does work.
Points of Interest below:
Store
LOAD_METRICS: ({commit}, context) => {
console.log(context)
if (context === true) {
return new Promise((resolve) => {
resolve('loaded')
})
}
return new Promise((resolve) => {
Vue.http.get('real ip is normally here').then(response => {
let out = {
twitter: Number(response.body[0].twitter),
instagram: Number(response.body[0].instagram),
youtube: Number(response.body[0].youtube),
twitch: Number(response.body[0].twitch),
loaded: false
}
commit('SET_METRICS', out)
resolve(out)
}).catch((e) => {
console.log(e)
})
})
}
In the above I am now sending an instance of the current store.state.metrics.loaded when the dispatch event is sent. Which is then checked to see the truthness of the current value, Because the first load should always return false we then return a promise utilizing an API call while also mutating the store so we have the values from later. Thus onwards because we mutated the loaded event to be true the next further instances shall return a value of true and a new promise will be resolved so we can make sure the .then() handler is present.
Component
created: function () {
this.$store.dispatch('LOAD_METRICS', this.$store.state.metrics.loaded).then((res) => {
if (res !== 'loaded') {
this.updateNumbers(res)
} else {
this.socialIcons[0].count = this.kFormatter(this.$store.state.metrics.twitter) + 'K'
this.socialIcons[1].count = this.kFormatter(this.$store.state.metrics.instagram) + 'K'
this.socialIcons[2].count = this.kFormatter(this.$store.state.metrics.youtube) + 'K'
this.socialIcons[3].count = this.kFormatter(this.$store.state.metrics.twitch) + 'K'
}
})
}
Within our component created life cycle hook we then use the resulting values to identify the path to be taken when the components are created within the DOM again, this time just loading the values and allow normal data binding to update the DOM.
I believe there is a better method of approach then deliberating the logic of the state within the action setter and returning a promise that is essentially redundant other than for ensuring the .then() handle is present.