[IMP] web,*: force props validation for components

*: base_automation, lunch, mail, mrp, project, web_editor, website

This commit adds a warning if the props validation is not set for a
component.

The props validation is important to tell how a component should be
used, by looking at its code, it's a good documentation of the component.
It's also critical, to test if the component is correctly used, if all
the obligatory props are passed and that there are of the correct type.

For more information, see: https://github.com/odoo/owl/blob/master/doc/reference/props.md#props-validation

closes odoo/odoo#103723

Related: odoo/enterprise#33044
Signed-off-by: Géry Debongnie <ged@odoo.com>
This commit is contained in:
Jorge Pinna Puissant
2022-10-26 16:07:18 +02:00
parent 36158fe17a
commit 886f3de768
77 changed files with 343 additions and 54 deletions
@@ -49,6 +49,7 @@
value="props.username"
resModel="'res.users'"
getDomain="getDomain"
fieldString="props.username"
activeActions="{}"
update.bind="props.onUpdateUser"
/>
@@ -61,6 +62,7 @@
value="props.location"
resModel="'lunch.location'"
getDomain="getDomain"
fieldString="props.location"
activeActions="{}"
update.bind="props.onUpdateLunchLocation"
/>
@@ -20,6 +20,7 @@ export class ActivityMenuContainer extends Component {
}
}
ActivityMenuContainer.props = {};
Object.assign(ActivityMenuContainer, {
components: { ActivityMenuView: getMessagingComponent('ActivityMenuView') },
@@ -22,6 +22,7 @@ export class CallSystrayMenuContainer extends Component {
}
}
CallSystrayMenuContainer.props = {};
Object.assign(CallSystrayMenuContainer, {
components: { CallSystrayMenu: getMessagingComponent('CallSystrayMenu') },
@@ -21,6 +21,7 @@ export class ChatWindowManagerContainer extends Component {
return this.env.services.messaging.modelManager.messaging;
}
}
ChatWindowManagerContainer.props = {};
Object.assign(ChatWindowManagerContainer, {
components: { ChatWindowManager: getMessagingComponent('ChatWindowManager') },
@@ -21,6 +21,7 @@ export class DialogManagerContainer extends Component {
return this.env.services.messaging.modelManager.messaging;
}
}
DialogManagerContainer.props = {};
Object.assign(DialogManagerContainer, {
components: { DialogManager: getMessagingComponent('DialogManager') },
@@ -21,6 +21,7 @@ export class MessagingMenuContainer extends Component {
return this.env.services.messaging.modelManager.messaging;
}
}
MessagingMenuContainer.props = {};
Object.assign(MessagingMenuContainer, {
components: { MessagingMenu: getMessagingComponent('MessagingMenu') },
@@ -9,6 +9,11 @@ export class NotificationAlertContainer extends Component {}
Object.assign(NotificationAlertContainer, {
components: { NotificationAlert: getMessagingComponent('NotificationAlert') },
template: 'mail.NotificationAlertContainer',
props: {
// Should call standard_widget_props.js
readonly: { type: Boolean, optional: true },
record: { type: Object },
},
});
registry.category("view_widgets").add(
"notification_alert",
@@ -21,6 +21,7 @@ export class PopoverManagerContainer extends Component {
return this.env.services.messaging.modelManager.messaging;
}
}
PopoverManagerContainer.props = {};
Object.assign(PopoverManagerContainer, {
components: { PopoverManager: getMessagingComponent('PopoverManager') },
@@ -15,6 +15,13 @@ patch(FormRenderer.prototype, 'mail', {
},
});
patch(FormRenderer.props, "mail", {
hasAttachmentViewerInArch: { type: Boolean, optional: true },
// Template props : added by the FormCompiler
hasAttachmentViewer: { type: Boolean, optional: true },
chatter: { type: Object, optional: true },
});
Object.assign(FormRenderer.components, {
ChatterContainer,
WebClientViewAttachmentViewContainer,
@@ -28,7 +28,7 @@ addFakeModel('res.fake', {
addFakeModel('m2x.avatar.user', {
user_id: { type: 'many2one', relation: 'res.users' },
user_ids: { type: 'many2many', relation: 'res.users' },
user_ids: { type: 'many2many', relation: 'res.users', string: 'Users'},
});
//--------------------------------------------------------------------------
+7
View File
@@ -3,6 +3,7 @@
import { registry } from "@web/core/registry";
import { parseFloatTime } from "@web/views/fields/parsers";
import { useInputField } from "@web/views/fields/input_field_hook";
import { standardFieldProps } from "@web/views/fields/standard_field_props";
const { Component, useState, onWillUpdateProps, onWillStart, onWillDestroy } = owl;
@@ -73,6 +74,12 @@ export class MrpTimer extends Component {
}
MrpTimer.supportedTypes = ["float"];
MrpTimer.props = {
...standardFieldProps,
duration: { type: Number, optional: true },
ongoing: { type: Boolean, optional: true },
value: { optional: true },
};
MrpTimer.template = "mrp.MrpTimer";
registry.category("fields").add("mrp_timer", MrpTimer);
@@ -66,5 +66,6 @@ export class ProjectSharingWebClient extends Component {
}
}
ProjectSharingWebClient.props = {};
ProjectSharingWebClient.components = { ActionContainer, MainComponentsContainer };
ProjectSharingWebClient.template = 'project.ProjectSharingWebClient';
@@ -15,6 +15,7 @@ const {
Component,
onWillStart,
onWillDestroy,
EventBus,
useRef,
useState,
markRaw,
@@ -87,6 +88,15 @@ export function splitCommandName(name, searchValue) {
export class DefaultCommandItem extends Component {}
DefaultCommandItem.template = "web.DefaultCommandItem";
DefaultCommandItem.props = {
slots: { type: Object, optional: true },
// Props send by the command palette:
hotkey: { type: String, optional: true },
hotkeyOptions: { type: String, optional: true },
name: { type: String, optional: true },
searchValue: { type: String, optional: true },
executeCommand: { type: Function, optional: true },
};
export class CommandPalette extends Component {
setup() {
@@ -366,5 +376,11 @@ export class CommandPalette extends Component {
}
}
CommandPalette.lastSessionId = 0;
CommandPalette.props = {
bus: { type: EventBus, optional: true },
close: Function,
config: Object,
closeMe: { type: Function, optional: true },
};
CommandPalette.template = "web.CommandPalette";
CommandPalette.components = { Dialog };
@@ -59,3 +59,4 @@ export class DebugMenu extends DebugMenuBasic {
}
}
DebugMenu.components = { Dropdown, DropdownItem };
DebugMenu.props = {};
@@ -13,6 +13,9 @@ export class DialogContainer extends Component {
}
}
DialogContainer.components = { ErrorHandler, WithEnv };
DialogContainer.props = {
dialogs: Object,
};
//Legacy : The div wrapping the t-foreach, is placed to avoid owl to delete non-owl dialogs.
//This div can be removed after removing all legacy dialogs.
DialogContainer.template = xml`
@@ -1,6 +1,6 @@
/** @odoo-module **/
const { Component, xml, onWillDestroy } = owl;
const { EventBus, Component, xml, onWillDestroy } = owl;
export class EffectContainer extends Component {
setup() {
@@ -18,6 +18,9 @@ export class EffectContainer extends Component {
this.render();
}
}
EffectContainer.props = {
bus: EventBus,
};
EffectContainer.template = xml`
<div class="o_effects_manager">
@@ -63,4 +63,12 @@ export class RainbowMan extends Component {
}
}
RainbowMan.template = "web.RainbowMan";
RainbowMan.props = {
fadeout: String,
close: Function,
message: String,
imgUrl: String,
Component: { type: Function, optional: true },
props: { type: Object, optional: true },
};
RainbowMan.rainbowFadeouts = { slow: 4500, medium: 3500, fast: 2000, no: false };
@@ -9,6 +9,19 @@ import { capitalize } from "../utils/strings";
const { Component, useState } = owl;
// This props are added by the error handler
export const standardErrorDialogProps = {
traceback: { type: [String, { value: null }], optional: true },
message: { type: String, optional: true },
name: { type: String, optional: true },
exceptionName: { type: [String, { value: null }], optional: true },
data: { type: [Object, { value: null }], optional: true },
subType: { type: [String, { value: null }], optional: true },
code: { type: [Number, String, { value: null }], optional: true },
type: { type: [String, { value: null }], optional: true },
close: Function, // prop added by the Dialog service
};
export const odooExceptionTitleMap = new Map(
Object.entries({
"odoo.addons.base.models.ir_mail_server.MailDeliveryException": _lt(
@@ -41,6 +54,7 @@ export class ErrorDialog extends Component {
ErrorDialog.template = "web.ErrorDialog";
ErrorDialog.components = { Dialog };
ErrorDialog.title = _lt("Odoo Error");
ErrorDialog.props = { ...standardErrorDialogProps };
// -----------------------------------------------------------------------------
// Client Error Dialog
@@ -118,6 +132,10 @@ export class WarningDialog extends Component {
}
WarningDialog.template = "web.WarningDialog";
WarningDialog.components = { Dialog };
WarningDialog.props = {
...standardErrorDialogProps,
title: { type: String, optional: true },
};
// -----------------------------------------------------------------------------
// Redirect Warning Dialog
@@ -144,6 +162,7 @@ export class RedirectWarningDialog extends Component {
}
RedirectWarningDialog.template = "web.RedirectWarningDialog";
RedirectWarningDialog.components = { Dialog };
RedirectWarningDialog.props = { ...standardErrorDialogProps };
// -----------------------------------------------------------------------------
// Error 504 Dialog
@@ -152,6 +171,7 @@ export class Error504Dialog extends Component {}
Error504Dialog.template = "web.Error504Dialog";
Error504Dialog.components = { Dialog };
Error504Dialog.title = _lt("Request timeout");
Error504Dialog.props = { ...standardErrorDialogProps };
// -----------------------------------------------------------------------------
// Expired Session Error Dialog
@@ -164,6 +184,7 @@ export class SessionExpiredDialog extends Component {
SessionExpiredDialog.template = "web.SessionExpiredDialog";
SessionExpiredDialog.components = { Dialog };
SessionExpiredDialog.title = _lt("Odoo Session Expired");
SessionExpiredDialog.props = { ...standardErrorDialogProps };
registry
.category("error_dialogs")
@@ -117,6 +117,7 @@ export function rpcErrorHandler(env, error, originalError) {
return true;
}
}
errorHandlerRegistry.add("rpcErrorHandler", rpcErrorHandler, { sequence: 97 });
// -----------------------------------------------------------------------------
@@ -40,3 +40,4 @@ MainComponentsContainer.template = xml`
</div>
`;
MainComponentsContainer.components = { ErrorHandler };
MainComponentsContainer.props = {};
@@ -10,6 +10,9 @@ export class NotificationContainer extends Component {
this.notifications = useState(this.props.notifications);
}
}
NotificationContainer.props = {
notifications: Object,
};
NotificationContainer.template = xml`
<div class="o_notification_manager">
@@ -2,7 +2,7 @@
import { Popover } from "./popover";
const { Component, onMounted, onWillUnmount, useExternalListener, useState, xml } = owl;
const { EventBus, Component, onMounted, onWillUnmount, useExternalListener, useState, xml } = owl;
class PopoverController extends Component {
setup() {
@@ -44,8 +44,10 @@ class PopoverController extends Component {
}
}
onClickAway(ev) {
if (this.target.contains(ev.target)
|| ev.target.closest(`.o_popover[popover-id="${this.props.id}"]`)) {
if (
this.target.contains(ev.target) ||
ev.target.closest(`.o_popover[popover-id="${this.props.id}"]`)
) {
return;
}
if (this.props.preventClose && this.props.preventClose(ev)) {
@@ -80,6 +82,10 @@ export class PopoverContainer extends Component {
}
}
PopoverContainer.components = { PopoverController };
PopoverContainer.props = {
popovers: Object,
bus: EventBus,
};
PopoverContainer.template = xml`
<div class="o_popover_container">
<t t-foreach="Object.values(props.popovers)" t-as="popover" t-key="popover.id">
+4 -1
View File
@@ -2,7 +2,7 @@
import { browser } from "@web/core/browser/browser";
const { Component, useState, xml } = owl;
const { EventBus, Component, useState, xml } = owl;
export class BlockUI extends Component {
setup() {
@@ -69,6 +69,9 @@ export class BlockUI extends Component {
this.state.line2 = "";
}
}
BlockUI.props = {
bus: EventBus,
};
BlockUI.template = xml`
<div t-att-class="state.blockUI ? 'o_blockUI fixed-top d-flex justify-content-center align-items-center flex-column vh-100 bg-black-50' : ''">
@@ -10,7 +10,7 @@ export class ErrorHandler extends Component {
}
}
ErrorHandler.template = xml`<t t-slot="default" />`;
ErrorEvent.props = ["onError"];
ErrorHandler.props = ["onError", "slots"];
export class WithEnv extends Component {
setup() {
@@ -18,3 +18,4 @@ export class WithEnv extends Component {
}
}
WithEnv.template = xml`<t t-slot="default"/>`;
WithEnv.props = ["env", "slots"];
@@ -90,3 +90,6 @@ export class LegacyComponent extends owl.Component {
}
}
}
LegacyComponent.props = {
"*": true,
};
@@ -208,3 +208,7 @@ ControlPanel.components = {
Dropdown,
};
ControlPanel.template = "web.ControlPanel";
ControlPanel.props = {
display: { type: Object, optional: true },
slots: { type: Object, optional: true },
};
@@ -119,7 +119,7 @@
/>
<t t-if="state.showSearchBar or !display['top-left']">
<t t-slot="control-panel-top-right">
<SearchBar class="o_searchview_quick" />
<SearchBar/>
<button
type="button"
class="o_toggle_searchview_full btn fa fa-filter"
@@ -92,6 +92,7 @@ export class CustomFavoriteItem extends Component {
CustomFavoriteItem.template = "web.CustomFavoriteItem";
CustomFavoriteItem.components = { CheckBox, Dropdown };
CustomFavoriteItem.props = {};
favoriteMenuRegistry.add(
"custom-favorite-item",
{ Component: CustomFavoriteItem, groupNumber: 3 },
@@ -66,3 +66,4 @@ export class FavoriteMenu extends Component {
FavoriteMenu.template = "web.FavoriteMenu";
FavoriteMenu.components = { Dropdown, DropdownItem };
FavoriteMenu.props = {};
@@ -317,5 +317,6 @@ export class CustomFilterItem extends Component {
}
}
CustomFilterItem.props = {};
CustomFilterItem.components = { DatePicker, DateTimePicker, Dropdown };
CustomFilterItem.template = "web.CustomFilterItem";
@@ -40,3 +40,6 @@ export class FilterMenu extends Component {
FilterMenu.components = { CustomFilterItem, Dropdown, DropdownItem };
FilterMenu.template = "web.FilterMenu";
FilterMenu.props = {
class: { type: String, optional: true },
};
@@ -15,3 +15,7 @@ export class CustomGroupByItem extends Component {
CustomGroupByItem.template = "web.CustomGroupByItem";
CustomGroupByItem.components = { Dropdown };
CustomGroupByItem.props = {
fields: Array,
onAddCustomGroup: Function,
};
@@ -76,6 +76,9 @@ export class GroupByMenu extends Component {
GroupByMenu.components = { CustomGroupByItem, Dropdown, DropdownItem };
GroupByMenu.template = "web.GroupByMenu";
GroupByMenu.defaultProps = {
showActiveItems: true,
showCaretDown: false,
};
GroupByMenu.props = {
...Dropdown.props,
showCaretDown: { type: Boolean, optional: true },
};
@@ -35,7 +35,9 @@ export class SearchBar extends Component {
this.keepLast = new KeepLast();
this.inputRef = this.env.config.disableSearchBarAutofocus ? useRef("autofocus") : useAutofocus();
this.inputRef = this.env.config.disableSearchBarAutofocus
? useRef("autofocus")
: useAutofocus();
useBus(this.env.searchModel, "focus-search", () => {
this.inputRef.el.focus();
@@ -457,3 +459,4 @@ export class SearchBar extends Component {
}
SearchBar.template = "web.SearchBar";
SearchBar.props = {};
+1
View File
@@ -40,6 +40,7 @@ export async function startWebClient(Webclient) {
env,
templates,
dev: env.debug,
warnIfNoStaticProps: true,
translatableAttributes: ["data-tooltip"],
translateFn: env._t,
});
@@ -7,7 +7,6 @@
<div class="w-100 d-inline-flex">
<FileUploader
acceptedFileExtensions="props.acceptedFileExtensions"
file="{ data: props.value, name: fileName }"
onUploaded.bind="update"
>
<t t-set-slot="toggler">
@@ -298,4 +298,5 @@ Field.parseFieldNode = function (node, models, modelName, viewType, jsClass) {
Field.forbiddenAttributeNames = {
decorations: `You cannot use the "decorations" attribute name as it is used as generated prop name for the composite decoration-<something> attributes.`,
};
Field.props = ["fieldInfo?", "*"];
Field.defaultProps = { fieldInfo: {} };
@@ -75,3 +75,12 @@ export class FileUploader extends Component {
FileUploader.template = "web.FileUploader";
FileUploader.nextId = 0;
FileUploader.props = {
onUploaded: Function,
onUploadComplete: { type: Function, optional: true },
multiUpload: { type: Boolean, optional: true },
inputName: { type: String, optional: true },
fileUploadClass: { type: String, optional: true },
acceptedFileExtensions: { type: String, optional: true },
slots: { type: Object, optional: true },
};
@@ -8,7 +8,6 @@
<FileUploader
acceptedFileExtensions="props.acceptedFileExtensions"
onUploaded.bind="onFileUploaded"
type="'image'"
>
<t t-set-slot="toggler">
<button
@@ -13,7 +13,7 @@
placeholder="tags.length ? '' : props.placeholder"
resModel="props.relation"
autoSelect="true"
fieldString="props.string"
fieldString="string"
activeActions="activeActions"
update="update"
quickCreate="activeActions.create ? quickCreate : null"
@@ -7,7 +7,6 @@
<t t-if="props.value">
<FileUploader
acceptedFileExtensions="'.pdf'"
file="{ data: props.value, name: fileName }"
onUploaded.bind="onFileUploaded"
>
<t t-set-slot="toggler">
@@ -325,6 +325,26 @@ export class Many2XAutocomplete extends Component {
}
Many2XAutocomplete.template = "web.Many2XAutocomplete";
Many2XAutocomplete.components = { AutoComplete };
Many2XAutocomplete.props = {
value: { type: String, optional: true },
activeActions: Object,
context: { type: Object, optional: true },
nameCreateField: { type: String, optional: true },
setInputFloats: { type: Function, optional: true },
update: Function,
resModel: String,
getDomain: Function,
searchLimit: { type: Number, optional: true },
quickCreate: { type: [Function, { value: null }], optional: true },
noSearchMore: { type: Boolean, optional: true },
searchMoreLimit: { type: Number, optional: true },
fieldString: String,
id: { type: String, optional: true },
placeholder: { type: String, optional: true },
autoSelect: { type: Boolean, optional: true },
isToMany: { type: Boolean, optional: true },
autocomplete_container: { type: Function, optional: true },
};
Many2XAutocomplete.defaultProps = {
searchLimit: 7,
searchMoreLimit: 320,
@@ -332,6 +352,7 @@ Many2XAutocomplete.defaultProps = {
value: "",
setInputFloats: () => {},
quickCreate: null,
context: {},
};
export function useOpenMany2XRecord({
@@ -3,7 +3,7 @@
<t t-name="web.X2ManyFieldDialog" owl="1">
<Dialog title="title" withBodyPadding="false" modalRef="modalRef" contentClass="'o_form_view'">
<FormRenderer record="record" fields="archInfo.fields" archInfo="archInfo"/>
<FormRenderer record="record" archInfo="archInfo"/>
<t t-set-slot="footer">
<t t-if="footerArchInfo">
<FormRenderer record="record" archInfo="footerArchInfo"/>
@@ -428,7 +428,6 @@ export class FormCompiler extends ViewCompiler {
}
let slotId = 0;
const statusBarButtons = createElement("StatusBarButtons");
statusBarButtons.setAttribute("readonly", "!props.record.isInEdition");
for (const button of buttons) {
const slot = createElement("t", {
"t-set-slot": `button_${slotId++}`,
@@ -197,7 +197,7 @@ export class FormController extends Component {
getLocalState: () => {
// TODO: export the whole model?
return {
activeNotebookPages: !this.model.root.isNew && activeNotebookPages,
activeNotebookPages: !this.model.root.isNew ? activeNotebookPages : {},
resId: this.model.root.resId,
fieldsToTranslate: toRaw(this.fieldsToTranslate),
};
@@ -57,3 +57,11 @@ export class FormLabel extends Component {
}
}
FormLabel.template = "web.FormLabel";
FormLabel.props = {
fieldInfo: { type: Object },
record: { type: Object },
fieldName: { type: String },
className: { type: String, optional: true },
string: { type: String },
id: { type: String },
};
@@ -87,7 +87,21 @@ FormRenderer.components = {
InnerGroup,
StatusBarButtons,
};
FormRenderer.props = {
archInfo: Object,
Compiler: { type: Function, optional: true },
record: Object,
// Template props : added by the FormCompiler
class: { type: String, optional: 1 },
translateAlert: { type: [Object, { value: null }], optional: true },
enableViewButtons: { type: Function, optional: true },
disableViewButtons: { type: Function, optional: true },
onNotebookPageChange: { type: Function, optional: true },
activeNotebookPages: { type: Object, optional: true },
};
FormRenderer.defaultProps = {
activeNotebookPages: {},
onNotebookPageChange: () => {},
enableViewButtons: () => {},
disableViewButtons: () => {},
};
@@ -20,3 +20,6 @@ StatusBarButtons.components = {
Dropdown,
DropdownItem,
};
StatusBarButtons.props = {
slots: { type: Object, optional: 1 },
};
@@ -73,4 +73,11 @@ export class KanbanColumnQuickCreate extends Component {
});
}
}
KanbanColumnQuickCreate.props = {
exampleData: [Object, { value: null }],
onFoldChange: Function,
onValidate: Function,
folded: Boolean,
groupByFieldString: String,
};
KanbanColumnQuickCreate.template = "web.KanbanColumnQuickCreate";
@@ -9,3 +9,6 @@ export class KanbanDropdownMenuWrapper extends Component {
}
}
KanbanDropdownMenuWrapper.template = "web.KanbanDropdownMenuWrapper";
KanbanDropdownMenuWrapper.props = {
slots: Object,
};
@@ -79,5 +79,12 @@ export class KanbanRecordQuickCreate extends Component {
this.state.disabled = false;
}
}
KanbanRecordQuickCreate.props = {
onCancel: Function,
onValidate: Function,
listIsGrouped: Boolean,
record: Object,
archInfo: Object,
};
KanbanRecordQuickCreate.template = "web.KanbanRecordQuickCreate";
KanbanRecordQuickCreate.components = { FormRenderer };
@@ -58,3 +58,10 @@ export class PivotGroupByMenu extends GroupByMenu {
});
}
}
PivotGroupByMenu.props = {
...GroupByMenu.props,
cell: Object,
customGroupBys: Object,
onAddCustomGroupBy: Function,
onItemSelected: Function,
};
@@ -78,7 +78,6 @@
position="isXAxis ? 'bottom-start' : 'bottom-end'"
cell="cell"
customGroupBys="model.metaData.customGroupBys"
showActiveItems="false"
onItemSelected="(payload) => this.onGroupBySelected(isXAxis ? 'col' : 'row', payload)"
onAddCustomGroupBy="fieldName => this.onAddCustomGroupBy(isXAxis ? 'col' : 'row', cell.groupId, fieldName)"
/>
+3 -1
View File
@@ -400,5 +400,7 @@ View.defaultProps = {
loadIrFilters: false,
className: "",
};
View.props = {
"*": true,
};
View.searchMenuTypes = ["filter", "groupBy", "favorite"];
@@ -69,21 +69,23 @@ export class SelectCreateDialog extends Component {
}
SelectCreateDialog.components = { Dialog, View };
SelectCreateDialog.template = "web.SelectCreateDialog";
SelectCreateDialog.props = {
context: { type: Object, optional: true },
domain: { type: Array, optional: true },
dynamicFilters: { type: Array, optional: true },
resModel: String,
searchViewId: { type: [Number, { value: false }], optional: true },
multiSelect: { type: Boolean, optional: true },
onSelected: { type: Function, optional: true },
close: { type: Function, optional: true },
onCreateEdit: { type: Function, optional: true },
title: { type: String, optional: true },
noCreate: { type: Boolean, optional: true },
};
SelectCreateDialog.defaultProps = {
dynamicFilters: [],
multiSelect: true,
searchViewId: false,
type: "list",
domain: [],
context: {},
};
/**
* Props: (to complete)
*
* resModel
* domain
* context
* title
* onSelected
* type
*/
@@ -72,3 +72,6 @@ Widget.parseWidgetNode = function (node) {
WidgetComponent,
};
};
Widget.props = {
"*": true,
};
@@ -27,3 +27,4 @@ ActionContainer.template = xml`
<t t-if="info.Component" t-component="info.Component" className="'o_action'" t-props="info.componentProps" t-key="info.id"/>
</div>
</t>`;
ActionContainer.props = {};
@@ -48,6 +48,12 @@ export async function clearUncommittedChanges(env) {
return !res.includes(false);
}
export const standardActionServiceProps = {
action: Object, // prop added by _getActionInfo
actionId: { type: Number, optional: true }, // prop added by _getActionInfo
className: String, // prop added by the ActionContainer
};
function parseActiveIds(ids) {
const activeIds = [];
if (typeof ids === "string") {
@@ -734,6 +740,9 @@ function makeActionManager(env) {
}
ControllerComponent.template = ControllerComponentTemplate;
ControllerComponent.Component = controller.Component;
ControllerComponent.props = {
"*": true,
};
let nextDialog = null;
if (action.target === "new") {
@@ -85,6 +85,7 @@ BurgerMenu.components = {
MobileSwitchCompanyMenu,
Transition,
};
BurgerMenu.props = {};
const systrayItem = {
Component: BurgerMenu,
@@ -70,6 +70,7 @@ export class LoadingIndicator extends Component {
LoadingIndicator.template = "web.LoadingIndicator";
LoadingIndicator.components = { Transition };
LoadingIndicator.props = {};
registry.category("main_components").add("LoadingIndicator", {
Component: LoadingIndicator,
@@ -4,11 +4,17 @@ import { _lt } from "@web/core/l10n/translation";
import { registry } from "@web/core/registry";
import { fuzzyLookup } from "@web/core/utils/search";
import { computeAppsAndMenuItems } from "@web/webclient/menus/menu_helpers";
import { DefaultCommandItem } from "@web/core/commands/command_palette";
const { Component } = owl;
class AppIconCommand extends Component {}
AppIconCommand.template = "web.AppIconCommand";
AppIconCommand.props = {
webIconData: { type: String, optional: true },
webIcon: { type: Object, optional: true },
...DefaultCommandItem.props,
};
const commandCategoryRegistry = registry.category("command_categories");
commandCategoryRegistry.add("apps", { namespace: "/" }, { sequence: 10 });
@@ -202,3 +202,4 @@ export class NavBar extends Component {
}
NavBar.template = "web.NavBar";
NavBar.components = { Dropdown, DropdownItem, MenuDropdown, ErrorHandler };
NavBar.props = {};
@@ -25,3 +25,11 @@ export class FormLabelHighlightText extends FormLabel {
FormLabelHighlightText.template = "web.FormLabelHighlightText";
FormLabelHighlightText.components = { HighlightText };
FormLabelHighlightText.props = {
fieldInfo: { type: Object, optional: true },
className: { type: String, optional: true },
record: { type: Object, optional: true },
fieldName: { type: String, optional: true },
string: { type: String },
id: { type: String },
};
@@ -40,7 +40,7 @@ export class SettingsFormController extends formView.Controller {
}
});
this.initialApp = "module" in this.props.context && this.props.context.module;
this.initialApp = "module" in this.props.context ? this.props.context.module : "";
}
/**
@@ -59,3 +59,8 @@ SettingsFormRenderer.components = {
HighlightText,
FormLabel: FormLabelHighlightText,
};
SettingsFormRenderer.props = {
...FormRenderer.props,
initialApp: String,
slots: Object,
};
@@ -41,6 +41,7 @@ export class SwitchCompanyMenu extends Component {
SwitchCompanyMenu.template = "web.SwitchCompanyMenu";
SwitchCompanyMenu.components = { Dropdown, DropdownItem };
SwitchCompanyMenu.toggleDelay = 1000;
SwitchCompanyMenu.props = {};
export const systrayItem = {
Component: SwitchCompanyMenu,
@@ -33,6 +33,7 @@ export class UserMenu extends Component {
}
UserMenu.template = "web.UserMenu";
UserMenu.components = { Dropdown, DropdownItem, CheckBox };
UserMenu.props = {};
export const systrayItem = {
Component: UserMenu,
@@ -114,3 +114,4 @@ WebClient.components = {
MainComponentsContainer,
};
WebClient.template = "web.WebClient";
WebClient.props = {};
@@ -6,6 +6,7 @@ import {
ClientErrorDialog,
RPCErrorDialog,
NetworkErrorDialog,
standardErrorDialogProps,
} from "@web/core/errors/error_dialogs";
import { errorService } from "@web/core/errors/error_service";
import { ConnectionLostError, RPCError } from "@web/core/network/rpc_service";
@@ -98,6 +99,7 @@ QUnit.test(
class CustomDialog extends Component {}
CustomDialog.template = xml`<RPCErrorDialog title="'Strange Error'"/>`;
CustomDialog.components = { RPCErrorDialog };
CustomDialog.props = { ...standardErrorDialogProps };
const error = new RPCError();
error.code = 701;
error.message = "Some strange error occured";
+47 -11
View File
@@ -60,22 +60,24 @@ export async function makeWithSearch(params) {
class Parent extends Component {
setup() {
this.withSearchProps = props;
this.componentProps = componentProps;
}
getDisplay(display) {
return Object.assign({}, display, componentProps.display);
getProps(search) {
const props = Object.assign({}, componentProps, {
context: search.context,
domain: search.domain,
groupBy: search.groupBy,
orderBy: search.orderBy,
comparison: search.comparison,
display: Object.assign({}, search.display, componentProps.display),
});
return filterPropsForComponent(params.Component, props);
}
}
Parent.template = xml`
<WithSearch t-props="withSearchProps" t-slot-scope="search">
<Component
t-props="componentProps"
context="search.context"
domain="search.domain"
groupBy="search.groupBy"
orderBy="search.orderBy"
comparison="search.comparison"
display="getDisplay(search.display)"/>
<Component t-props="getProps(search)"/>
</WithSearch>`;
Parent.components = { Component: params.Component, WithSearch };
@@ -89,6 +91,40 @@ export async function makeWithSearch(params) {
return component;
}
/** This function is aim to be used only in the tests.
* It will filter the props that are needed by the Component.
* This is to avoid errors of props validation. This occurs for example, on ControlPanel tests.
* In production, View use WithSearch for the Controllers, and the Layout send only the props that
* need to the ControlPanel.
*
* @param {Component} Component
* @param {Object} props
* @returns {Object} filtered props
*/
function filterPropsForComponent(Component, props) {
// This if, can be removed once all the Components have the props defined
if (Component.props) {
let componentKeys = null;
if (Component.props instanceof Array) {
componentKeys = Component.props.map((x) => x.replace("?", ""));
} else {
componentKeys = Object.keys(Component.props);
}
if (componentKeys.includes("*")) {
return props;
} else {
return Object.keys(props)
.filter((k) => componentKeys.includes(k))
.reduce((o, k) => {
o[k] = props[k];
return o;
}, {});
}
} else {
return props;
}
}
function getUniqueChild(node) {
return Object.values(node.children)[0];
}
@@ -184,7 +184,7 @@ QUnit.module("Form Compiler", (hooks) => {
const expected = /*xml*/ `
<t>
<div t-att-class="props.class" t-attf-class="{{props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-block {{ props.record.isDirty ? 'o_form_dirty' : !props.record.isVirtual ? 'o_form_saved' : '' }}" class="o_form_nosheet" t-ref="compiled_view_root">
<div class="o_form_statusbar position-relative d-flex justify-content-between border-bottom"><StatusBarButtons readonly="!props.record.isInEdition"/></div>
<div class="o_form_statusbar position-relative d-flex justify-content-between border-bottom"><StatusBarButtons/></div>
<div>someDiv</div>
</div>
</t>`;
@@ -207,7 +207,7 @@ QUnit.module("Form Compiler", (hooks) => {
<t>
<div t-att-class="props.class" t-attf-class="{{props.record.isInEdition ? 'o_form_editable' : 'o_form_readonly'}} d-flex {{ uiService.size &lt; 6 ? &quot;flex-column&quot; : &quot;flex-nowrap h-100&quot; }} {{ props.record.isDirty ? 'o_form_dirty' : !props.record.isVirtual ? 'o_form_saved' : '' }}" t-ref="compiled_view_root">
<div class="o_form_sheet_bg">
<div class="o_form_statusbar position-relative d-flex justify-content-between border-bottom"><StatusBarButtons readonly="!props.record.isInEdition"/></div>
<div class="o_form_statusbar position-relative d-flex justify-content-between border-bottom"><StatusBarButtons/></div>
<div>someDiv</div>
<div class="o_form_sheet position-relative">
<div>inside sheet</div>
@@ -268,7 +268,7 @@ QUnit.module("Form Compiler", (hooks) => {
const expected = /*xml*/ `
<div class="o_form_statusbar position-relative d-flex justify-content-between border-bottom">
<StatusBarButtons readonly="!props.record.isInEdition">
<StatusBarButtons>
<t t-set-slot="button_0" isVisible="true">
<div>someDiv</div>
</t>
@@ -286,7 +286,7 @@ QUnit.module("Form Compiler", (hooks) => {
const expected = /*xml*/ `
<div class="o_form_statusbar position-relative d-flex justify-content-between border-bottom">
<StatusBarButtons readonly="!props.record.isInEdition"/>
<StatusBarButtons/>
</div>`;
assert.areContentEquivalent(compileTemplate(arch), expected);
+1 -1
View File
@@ -88,7 +88,7 @@ export async function makeView(params) {
const target = getFixture();
const viewEnv = Object.assign(Object.create(env), { config });
const view = await mount(View, target, { env: viewEnv, props });
await mount(MainComponentsContainer, target, { env, props });
await mount(MainComponentsContainer, target, { env });
const viewNode = view.__owl__;
const withSearchNode = Object.values(viewNode.children)[0];
@@ -227,7 +227,6 @@ QUnit.module("ViewDialogs", (hooks) => {
const webClient = await createWebClient({ serverData, mockRPC });
webClient.env.services.dialog.add(SelectCreateDialog, {
noCreate: true,
readonly: true, //Not used
resModel: "partner",
domain: [["id", "=", session.user_context.uid]],
});
@@ -9,6 +9,14 @@ export class ProgressBar extends Component {
}
}
ProgressBar.template = 'web_editor.ProgressBar';
ProgressBar.props = {
progress: Number,
hasError: Boolean,
uploaded: Boolean,
name: String,
size: String,
errorMessage: String,
};
export class UploadProgressToast extends Component {
setup() {
@@ -21,3 +29,6 @@ UploadProgressToast.template = 'web_editor.UploadProgressToast';
UploadProgressToast.components = {
ProgressBar
};
UploadProgressToast.props = {
close: Function,
};
@@ -28,8 +28,7 @@
hasError="file_value.hasError"
name="file_value.name"
uploaded="file_value.uploaded"
size="file_value.size"
id="file_value.id"/>
size="file_value.size"/>
</div>
</div>
</div>
@@ -1,6 +1,6 @@
/** @odoo-module **/
const { Component, xml, useState } = owl;
const { EventBus, Component, xml, useState } = owl;
export class FullscreenIndication extends Component {
setup() {
@@ -21,6 +21,9 @@ export class FullscreenIndication extends Component {
}
}
}
FullscreenIndication.props = {
bus: EventBus,
};
FullscreenIndication.template = xml`
<div class="o_fullscreen_indication" t-att-class="{ o_visible: state.isVisible }">
<p>Press <span>esc</span> to exit full screen</p>
@@ -1,6 +1,6 @@
/** @odoo-module **/
const { Component, useState } = owl;
const { EventBus, Component, useState } = owl;
export class WebsiteLoader extends Component {
setup() {
@@ -25,4 +25,7 @@ export class WebsiteLoader extends Component {
});
}
}
WebsiteLoader.props = {
bus: EventBus,
};
WebsiteLoader.template = 'website.website_loader';