@odoo/o-spreadsheet
Version:
A spreadsheet component
1,307 lines (1,259 loc) • 352 kB
text/xml
<!--
This file is generated by o-spreadsheet build tools. Do not edit it.
@see https://github.com/odoo/o-spreadsheet
@version 19.4.1
@date 2026-07-01T05:05:20.289Z
@hash e359501
-->
<odoo>
<t t-name="o-spreadsheet-ValidationMessages">
<t t-if="this.props.messages">
<t t-foreach="this.props.messages" t-as="msg" t-key="msg_index">
<div t-att-class="this.divClasses" class="d-flex flex-column p-3 m-1 o-validation">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<t t-if="this.props.msgType === 'info'" t-call="o-spreadsheet-Icon.CIRCLE_INFO"/>
<t t-else="" t-call="o-spreadsheet-Icon.TRIANGLE_EXCLAMATION"/>
</div>
<div class="d-flex flex-column overflow-hidden">
<span class="ps-2 text-truncate" t-out="msg"/>
</div>
</div>
</div>
</t>
</t>
<t t-if="this.props.slots and this.props.slots['info-text']">
<div t-att-class="this.divClasses" class="d-flex flex-column p-3 m-1 o-validation">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<t t-if="this.props.msgType === 'info'" t-call="o-spreadsheet-Icon.CIRCLE_INFO"/>
<t t-else="" t-call="o-spreadsheet-Icon.TRIANGLE_EXCLAMATION"/>
</div>
<div class="d-flex flex-column overflow-hidden">
<span class="ps-2" t-call-slot="info-text"/>
</div>
</div>
</div>
</t>
</t>
<t t-name="o-spreadsheet-TopBar">
<div
class="o-spreadsheet-topbar d-flex flex-column user-select-none bg-white"
t-on-click="this.props.onClick">
<div
t-if="!this.env.isSmall"
class="o-topbar-top d-flex justify-content-between border-bottom">
<!-- Menus -->
<div class="o-topbar-topleft d-flex" t-ref="this.topBarTopRef">
<t t-foreach="this.menus" t-as="menu" t-key="menu_index">
<div
t-if="menu.children.length !== 0"
class="o-topbar-menu o-hoverable-button text-nowrap rounded"
t-att-class="{'active': this.state.menuState.parentMenu and this.state.menuState.parentMenu.id === menu.id}"
t-on-click="(ev) => this.toggleContextMenu(menu, ev)"
t-on-mouseover="(ev) => this.onMenuMouseOver(menu, ev)"
t-att-data-id="menu.id">
<t t-out="this.getMenuName(menu)"/>
</div>
</t>
</div>
<div class="o-topbar-topright d-flex justify-content-end align-items-center">
<div t-foreach="this.topbarComponents" t-as="comp" t-key="comp.id" class="px-1">
<t t-component="comp.component"/>
</div>
</div>
</div>
<!-- Toolbar and Cell Content -->
<div
class="d-flex o-topbar-responsive"
t-att-class="{'o-topbar-responsive': !this.env.model.getters.isReadonly()}"
t-ref="this.toolBarContainerRef">
<div
class="o-topbar-toolbar d-flex flex-grow-1"
t-att-class="{'flex-shrink-0': this.env.model.getters.isReadonly()}">
<!-- Toolbar -->
<div
t-if="this.env.model.getters.isReadonly()"
class="o-readonly-toolbar d-flex flex-grow-1 align-items-center text-muted">
<span>
<i class="fa fa-eye"/>
Readonly Access
</span>
</div>
<div
t-else=""
class="o-toolbar-tools d-flex flex-grow-1 align-items-center"
t-ref="this.toolbarRef">
<div
class="d-flex h-100 o-named-range-selector-container align-items-center ms-2 me-1"
t-ref="this.namedRangesRef"
t-on-click.stop="">
<NamedRangeSelector/>
<div class="o-topbar-divider border-end"/>
</div>
<div
class="d-flex tool-container h-100"
t-foreach="this.toolsCategories"
t-as="category"
t-key="category"
t-att-id="category">
<t
t-foreach="this.toolbarMenuRegistry.getEntries(category)"
t-as="toolbarAction"
t-key="toolbarAction_index">
<t t-component="toolbarAction.component" t-props="toolbarAction.props"/>
</t>
<div t-if="this.showDivider(category_index)" class="o-topbar-divider border-end"/>
</div>
<div
t-ref="this.moreToolsContainerRef"
class="d-flex align-items-center flex-grow-1 me-auto more-tools-container">
<span
class="o-toolbar-button o-menu-item-button o-hoverable-button me-2 px-1 py-2 more-tools"
t-ref="this.moreToolsButtonRef"
t-on-click.stop="this.toggleMoreTools">
<t t-call="o-spreadsheet-Icon.SHORT_THIN_DRAG_HANDLE"/>
</span>
</div>
</div>
</div>
<TopBarComposer t-if="!this.env.isSmall"/>
</div>
<div
t-if="this.fingerprints.isEnabled"
class="topbar-banner irregularity-map d-flex align-items-center justify-content-between border-top">
<div
t-on-click="() => this.fingerprints.disable()"
role="button"
title="This tool analyzes spreadsheet formulas for patterns and highlights inconsistencies. Irregularities may indicate potential errors in formula structures, references, or arguments. (Click to turn off)"
class="h-100 d-flex align-items-center text-info px-3">
<t t-call="o-spreadsheet-Icon.IRREGULARITY_MAP"/>
Irregularity map
</div>
<div
class="ps-3 h-100 flex-fill d-flex justify-content-between rounded-0 alert alert-info ps-0 py-0 my-0">
<span class="d-flex align-items-center">
This tool analyzes formulas for patterns and highlights inconsistencies. Irregularities
may indicate potential errors in formula structures, references or arguments.
</span>
<div
class="ps-3 btn o-button-link flex-shrink-0"
t-on-click="() => this.fingerprints.disable()">
Turn off
</div>
</div>
</div>
<div
t-if="!this.env.model.getters.isAutomaticEvaluationEnabled()"
class="topbar-banner manual-evaluation d-flex align-items-center justify-content-between border-top">
<div class="h-100 d-flex align-items-center text-warning px-3">
<i class="fa fa-pause-circle me-1"/>
Manual calculation
</div>
<div
class="ps-3 h-100 flex-fill d-flex justify-content-between rounded-0 alert alert-warning ps-0 py-0 my-0">
<span class="d-flex align-items-center">
Automatic evaluation is disabled. Press F9 to recalculate.
</span>
<div
class="ps-3 btn o-button-link flex-shrink-0"
t-on-click="() => this.env.model.dispatch('SET_AUTOMATIC_EVALUATION', { enabled: true })">
Re-enable
</div>
</div>
</div>
</div>
<MenuPopover
t-if="this.state.menuState.isOpen"
t-key="this.state.menuState.parentMenu.id"
anchorRect="this.state.menuState.anchorRect"
menuItems="this.state.menuState.menuItems"
onClose="() => this.closeMenus()"
onMenuClicked="() => this.props.onClick()"
popoverPositioning="'bottom-left'"
menuId="this.state.menuState.parentMenu.id"
autoSelectFirstItem="this.state.menuState.autoSelectFirstItem"
onKeyboardNavigation.bind="this.onKeyboardNavigation"
/>
<Popover t-if="this.state.toolsPopoverState.isOpen" t-props="this.toolsPopoverProps">
<div class="d-flex px-2 py-1 flex-wrap align-items-center">
<t
t-foreach="this.state.invisibleToolsCategories"
t-as="category"
t-key="category"
t-att-id="category">
<t
t-foreach="this.toolbarMenuRegistry.getEntries(category)"
t-as="toolbarAction"
t-key="toolbarAction_index">
<t t-component="toolbarAction.component" t-props="toolbarAction.props"/>
</t>
<div
t-if="category_index < this.state.invisibleToolsCategories.length-1"
class="o-topbar-divider border-end"
/>
</t>
</div>
</Popover>
</t>
<div t-name="o-spreadsheet-TopBarZoom" t-on-click.stop="">
<NumberEditor
currentValue="this.currentFontSize"
onValueChange.bind="this.setZoom"
class="this.props.class"
onToggle.bind="this.toggle"
onFocusInput.bind="this.onFocusInput"
valueIcon="'%'"
min="25"
max="300"
valueList="this.valueList"
title.translate="Zoom"
/>
</div>
<div t-name="o-spreadsheet-NumberFormatsTool" t-ref="this.buttonRef" t-on-click.stop="">
<ActionButton
action="this.formatNumberMenuItemSpec"
hasTriangleDownIcon="true"
onClick.bind="this.toggleMenu"
class="this.props.class"
/>
<MenuPopover
t-if="this.isActive"
anchorRect="this.state.anchorRect"
menuItems="this.state.menuItems"
onClose="() => {}"
popoverPositioning="'bottom-left'"
/>
</div>
<t t-name="o-spreadsheet-TopBarFontSizeEditor">
<FontSizeEditor
currentFontSize="this.currentFontSize"
onFontSizeChanged.bind="this.setFontSize"
class="this.class"
onToggle.bind="this.onToggle"
onFocusInput.bind="this.onFocusInput"
/>
</t>
<div
t-name="o-spreadsheet-DropdownAction"
class="o-dropdown"
t-ref="this.actionRef"
t-on-click.stop="">
<ActionButton
action="this.props.parentAction"
hasTriangleDownIcon="true"
onClick.bind="this.toggleDropdown"
class="this.props.class"
/>
<Popover t-if="this.isActive" t-props="this.popoverProps">
<div class="o-dropdown-content p-1" t-if="this.isActive" t-on-click.stop="">
<div class="o-dropdown-line">
<t t-foreach="this.props.childActions" t-as="action" t-key="action_index">
<ActionButton action="action" class="this.props.childClass"/>
</t>
</div>
</div>
</Popover>
</div>
<t t-name="o-spreadsheet-ColorEditor">
<div
class="d-flex align-items-center"
t-att-class="{'o-disabled': this.env.model.getters.isCurrentSheetLocked() }">
<ColorPickerWidget
currentColor="this.currentColor"
toggleColorPicker.bind="this.onClick"
showColorPicker="this.isMenuOpen"
onColorPicked="(color) => this.setColor(color)"
title="this.props.title"
icon="this.props.icon"
class="this.props.class"
/>
</div>
</t>
<t t-name="o-spreadsheet-TextInput">
<div class="position-relative">
<input
type="text"
t-ref="this.genericInputRef"
t-att-class="this.inputClass"
t-att-id="this.props.id"
t-att-placeholder="this.props.placeholder"
t-on-change="this.save"
t-on-blur="this.onBlur"
t-on-pointerdown="this.onMouseDown"
t-on-pointerup="this.onMouseUp"
t-on-focus="this.onFocus"
t-on-input="this.onInput"
t-on-keydown="this.onKeyDown"
t-att-maxlength="this.props.maxLength"
/>
<span
t-if="this.props.errorMessage"
class="os-input-error-icon text-danger position-absolute d-flex align-items-center"
t-att-title="this.props.errorMessage">
<t t-call="o-spreadsheet-Icon.ERROR"/>
</span>
</div>
</t>
<t t-name="o-spreadsheet-TableStylesPopover">
<Popover t-if="this.props.popoverProps" t-props="this.props.popoverProps">
<div
class="o-table-style-popover d-flex flex-column py-3"
t-ref="this.tableStyleListRef"
t-on-contextmenu.prevent="">
<div class="d-flex o-notebook ps-4 mb-3">
<div
t-foreach="Object.keys(this.categories)"
t-as="category"
t-key="category"
class="o-notebook-tab d-flex align-items-center border"
t-att-class="{ 'selected': this.state.selectedCategory === category }"
t-on-click="() => this.state.selectedCategory = category"
t-att-data-id="category"
t-out="this.categories[category_value]"
/>
</div>
<div
class="d-flex flex-wrap px-4"
t-att-class="this.props.type === 'pivot' ? 'o-pivot-previews' : 'o-table-previews'">
<t t-foreach="this.displayedStyles" t-as="styleId" t-key="styleId">
<TableStylePreview
styleId="styleId"
selected="styleId === this.props.selectedStyleId"
tableConfig="this.props.tableConfig"
tableStyle="this.props.tableStyles[styleId]"
onClick="() => this.props.onStylePicked(styleId)"
type="this.props.type"
/>
</t>
<div
t-if="this.state.selectedCategory === 'custom'"
class="o-new-table-style o-table-style-list-item o-table-style-popover-preview d-flex justify-content-center align-items-center"
t-on-click="this.newTableStyle">
+
</div>
</div>
</div>
</Popover>
</t>
<t t-name="o-spreadsheet-TableStylePreview">
<div
class="o-table-style-list-item position-relative"
t-att-class="{ 'selected': this.props.selected }"
t-att-data-id="this.props.styleId"
t-att-title="this.styleName"
t-on-click="this.props.onClick"
t-on-contextmenu.prevent="(ev) => this.onContextMenu(ev)">
<div class="o-table-preview">
<canvas t-ref="this.canvasRef" class="w-100 h-100 os-theme-dependant"/>
</div>
<div
class="o-table-style-edit-button position-absolute d-none bg-white border"
t-if="this.isStyleEditable"
t-on-click="this.editTableStyle"
title="Edit custom table style">
<t t-call="o-spreadsheet-Icon.EDIT"/>
</div>
</div>
<MenuPopover
t-if="this.menu.isOpen"
menuItems="this.menu.menuItems"
anchorRect="this.menu.anchorRect"
onClose.bind="this.closeMenu"
/>
</t>
<t t-name="o-spreadsheet-TableStylePicker">
<div class="o-table-style-picker d-flex flew-row justify-content-between ps-1 border rounded">
<div
class="d-flex flex-row overflow-hidden"
t-att-class="this.props.type === 'pivot' ? 'o-pivot-previews' : 'o-table-previews ps-2'">
<t t-foreach="this.getDisplayedTableStyles()" t-as="styleId" t-key="styleId">
<TableStylePreview
selected="styleId === this.props.tableConfig.styleId"
tableConfig="this.props.tableConfig"
tableStyle="this.props.tableStyles[styleId]"
styleId="styleId"
onClick="() => this.onStylePicked(styleId)"
type="this.props.type"
/>
</t>
</div>
<div
class="o-table-style-picker-arrow d-flex align-items-center px-1 border-start"
t-on-click.stop="this.onArrowButtonClick">
<t t-call="o-spreadsheet-Icon.CARET_DOWN"/>
</div>
</div>
<TableStylesPopover
tableConfig="this.props.tableConfig"
selectedStyleId="this.props.tableConfig.styleId"
onStylePicked.bind="this.onStylePicked"
popoverProps="this.state.popoverProps"
closePopover.bind="this.closePopover"
tableStyles="this.props.tableStyles"
type="this.props.type"
/>
</t>
<t t-name="o-spreadsheet-TableResizer">
<div
class="o-table-resizer position-absolute"
t-att-style="this.containerStyle"
t-on-pointerdown="this.onMouseDown"
/>
</t>
<t t-name="o-spreadsheet-TableDropdownButton">
<div class="o-table-widget d-flex align-item-center" t-att-class="this.class">
<ActionButton
action="this.action"
hasTriangleDownIcon="true"
t-on-click="this.onClick"
class="'opacity-100'"
/>
</div>
<TableStylesPopover
tableConfig="this.tableConfig"
onStylePicked.bind="this.onStylePicked"
popoverProps="this.state.popoverProps"
closePopover.bind="this.closePopover"
tableStyles="this.tableStyles"
type="'table'"
/>
</t>
<t t-name="o-spreadsheet-StandaloneGridCanvas">
<canvas t-ref="this.canvasRef"/>
</t>
<t t-name="o-spreadsheet-SpreadsheetPrint">
<div class="o-spreadsheet-print d-flex flex-column h-100 w-100">
<t t-set="pages" t-value="this.printStore.printPages"/>
<div class="o-print-header flex-shrink-0 d-flex align-items-center d-print-none">
<h3 class="m-0 p-3 ps-4 text-white">Print Preview</h3>
<span class="text-white">
<t t-out="pages.length"/>
pages
</span>
<button class="o-button ms-auto me-2 flex-grow-0" t-on-click="this.props.onExitPrintMode">
Cancel
</button>
<button class="o-button primary flex-grow-0 me-4" t-on-click="this.onPrint">Next</button>
</div>
<div class="o-print-container d-flex flex-grow-1 overflow-hidden">
<div class="o-print-preview w-100 overflow-auto d-flex flex-column">
<div class="mx-auto">
<div
t-if="pages.length === 0"
class="o-print-page o-empty-print-page m-4 shadow d-flex flex-column align-items-center justify-content-center flex-shrink-0"
t-att-style="this.pageStyle">
<h4 class="fst-italic text-muted">No content to print</h4>
</div>
<div
t-else=""
t-foreach="pages"
t-as="page"
t-key="page_index"
class="o-print-page m-4 shadow d-flex justify-content-center flex-shrink-0"
t-att-style="this.pageStyle">
<StandaloneGridCanvas
sheetId="page.sheetId"
zone="page.zone"
renderingCtx="page.renderingCtx"
/>
</div>
</div>
</div>
<div class="o-sidePanel flex-shrink-0 bg-white border-top border-start d-print-none">
<div class="o-sidePanelBody pt-4">
<Section t-if="!this.env.model.getters.isDashboard()">
<div class="o-section-title">Print content</div>
<Select
selectedValue="this.printStore.printSelection"
values="this.printStore.printSelectionOptions"
class="'o-print-selection'"
onChange.bind="this.onPrintSelectionChange"
/>
</Section>
<Section>
<div class="o-section-title">Scale</div>
<Select
selectedValue="this.printStore.printScale"
values="this.printStore.printScaleOptions"
class="'o-print-scale'"
onChange.bind="this.onPrintScaleChange"
/>
</Section>
<Section>
<div class="o-section-title">Paper</div>
<Select
selectedValue="this.printStore.pageLayout"
values="this.printStore.layoutOptions"
class="'o-print-layout'"
onChange.bind="this.onLayoutChange"
/>
</Section>
<Section>
<div class="o-section-title">Orientation</div>
<BadgeSelection
choices="this.printStore.orientationChoices"
onChange.bind="this.changeOrientation"
selectedValue="this.printStore.orientation"
/>
</Section>
<Section t-if="!this.env.model.getters.isDashboard()">
<div class="o-section-title">Formatting</div>
<Checkbox
name="'showGridLines'"
value="!this.printStore.hideGridLines"
onChange.bind="this.setGridLinesVisibility"
className="'mt-2'"
label.translate="Show gridlines"
/>
</Section>
</div>
</div>
</div>
</div>
</t>
<t t-name="o-spreadsheet-Spreadsheet">
<div
class="o-spreadsheet h-100 w-100"
t-att-class="this.getSpreadSheetClasses()"
t-ref="this.spreadsheetRef"
t-att-style="this.getStyle()">
<t t-if="this.state.printModeEnabled">
<SpreadsheetPrint onExitPrintMode.bind="this.exitPrintMode"/>
</t>
<t t-elif="this.env.model.getters.isDashboard()">
<SpreadsheetDashboard getGridSize.bind="this.getGridSize"/>
<FullScreenFigure/>
</t>
<t t-else="">
<div class="o-spreadsheet-topbar-wrapper o-two-columns">
<TopBar onClick="() => this.focusGrid()" dropdownMaxHeight="this.gridHeight"/>
</div>
<div
class="o-grid-container"
t-att-class="{'o-two-columns': !this.sidePanel.isMainPanelOpen}"
t-att-style="this.gridContainerStyle"
t-on-click="this.focusGrid">
<div class="o-top-left"/>
<div class="o-column-groups o-zoomable">
<HeaderGroupContainer layers="this.colLayers" dimension="'COL'"/>
</div>
<div class="o-row-groups o-zoomable">
<HeaderGroupContainer layers="this.rowLayers" dimension="'ROW'"/>
</div>
<div class="o-group-grid overflow-hidden">
<Grid
exposeFocus="(focus) => this._focusGrid = focus"
getGridSize.bind="this.getGridSize"
/>
</div>
</div>
<SidePanels/>
<div class="o-spreadsheet-bottombar-wrapper o-two-columns overflow-hidden">
<SmallBottomBar t-if="this.env.isSmall" onClick="() => this.focusGrid()"/>
<BottomBar t-else="" onClick="() => this.focusGrid()"/>
</div>
</t>
</div>
</t>
<t t-name="o-spreadsheet-SmallBottomBar">
<div class="o-spreadsheet-small-bottom-bar o-two-columns d-flex flex-column overflow-hidden">
<t t-if="this.menuState.isOpen">
<RibbonMenu onClose="() => this.menuState.isOpen=false"/>
</t>
<t t-else="">
<div class="o-small-composer px-2 py-2 position-relative bg-white border">
<div class="w-100" t-ref="this.composerRef">
<Composer t-props="this.composerProps"/>
<span
t-if="this.showFxIcon"
class="position-absolute top-50 translate-middle-y ps-2 pe-none">
<t t-call="o-spreadsheet-Icon.FX_SVG"/>
</span>
</div>
<span
class="align-items-center d-flex justify-content-center o-selection-button"
title="confirm edition"
t-if="this.focus !== 'inactive'"
t-on-click="() => this.composerStore.stopEdition()">
<span class="d-flex">
<t t-call="o-spreadsheet-Icon.CHECK"/>
</span>
</span>
</div>
<div class="d-flex flex-row mb-1" t-if="this.focus !== 'inactive'">
<div
t-foreach="this.symbols"
t-as="symbol"
t-key="symbol_index"
class="o-spreadsheet-editor-symbol w-100 d-flex justify-content-center align-items-center mx-1"
t-out="symbol"
tabindex="-1"
t-att-title="symbol"
t-on-click="() => this.insertSymbol(symbol)"
composerFocusableElement="true"
/>
</div>
<div class="d-flex flex-fill align-items-center bottom-bar-menu border-top border-bottom">
<Ripple>
<div class="py-1 px-1 mx-2 ribbon-toggler" t-on-click="this.toggleRibbon">
<i class="o-icon fa fa-cog"/>
</div>
</Ripple>
<BottomBar onClick="this.props.onClick"/>
</div>
</t>
</div>
</t>
<div t-name="o-spreadsheet-RibbonMenu">
<div class="o-ribbon-menu d-flex flex-column" t-ref="this.menuRef">
<div class="o-ribbon-title d-flex py-2 fw-bold">
<div
class="o-previous-button px-3 py-1 mx-2 rounded"
t-on-click="this.onClickBack"
t-att-title="this.backTitle">
<i class="fa fa-angle-left"/>
</div>
<span class="d-flex align-items-center" t-out="this.state.title"/>
</div>
<div
class="o-ribbon-menu-wrapper overflow-auto"
t-ref="this.containerRef"
t-on-scroll="this.updateShadows">
<Menu t-props="this.menuProps"/>
</div>
</div>
</div>
<t t-name="o-spreadsheet-TableStyleEditorPanel">
<div class="o-table-style-editor-panel">
<Section title.translate="Style name">
<input type="text" class="o-input" t-custom-model="this.state.styleName"/>
</Section>
<Section class="'pt-1'" title.translate="Style color">
<RoundColorPicker
currentColor="this.state.primaryColor"
onColorPicked.bind="this.onColorPicked"
disableNoColor="true"
/>
</Section>
<Section class="'pt-1'" title.translate="Style template">
<div class="d-flex flex-wrap">
<t t-foreach="this.tableTemplates" t-as="templateName" t-key="templateName">
<TableStylePreview
selected="templateName === this.state.selectedTemplateName"
tableConfig="this.previewTableConfig"
tableStyle="this.computeTableStyle(templateName)"
onClick="() => this.onTemplatePicked(templateName)"
type="'table'"
/>
</t>
</div>
</Section>
<Section>
<div class="o-sidePanelButtons">
<button
t-if="this.props.styleId"
t-on-click="this.onDelete"
class="o-delete o-button-danger o-button">
Delete
</button>
<button t-on-click="this.onCancel" class="o-cancel o-button">Discard</button>
<button t-on-click="this.onConfirm" class="o-confirm o-button primary">Confirm</button>
</div>
</Section>
</div>
</t>
<t t-name="o-spreadsheet-TablePanel">
<div class="o-table-panel">
<Section title.translate="Style options">
<div class="d-flex flex-row">
<div class="w-50">
<div class="d-flex align-items-center">
<Checkbox
label="this.getCheckboxLabel('headerRow')"
name="'headerRow'"
value="this.tableConfig.numberOfHeaders > 0"
onChange.bind="this.updateHasHeaders"
/>
<NumberInput
t-if="this.tableConfig.numberOfHeaders > 0"
value="this.tableConfig.numberOfHeaders"
class="'o-table-n-of-headers ms-2'"
min="0"
onChange.bind="this.onChangeNumberOfHeaders"
/>
</div>
<Checkbox
label="this.getCheckboxLabel('totalRow')"
name="'totalRow'"
value="this.tableConfig.totalRow"
onChange="(val) => this.updateTableConfig('totalRow', val)"
/>
<Checkbox
label="this.getCheckboxLabel('bandedRows')"
name="'bandedRows'"
value="this.tableConfig.bandedRows"
onChange="(val) => this.updateTableConfig('bandedRows', val)"
/>
<Checkbox
label="this.getCheckboxLabel('hasFilters')"
name="'hasFilters'"
value="this.tableConfig.hasFilters"
title="this.hasFilterCheckboxTooltip"
disabled="!this.canHaveFilters"
onChange.bind="this.updateHasFilters"
/>
</div>
<div>
<Checkbox
label="this.getCheckboxLabel('firstColumn')"
name="'firstColumn'"
value="this.tableConfig.firstColumn"
onChange="(val) => this.updateTableConfig('firstColumn', val)"
/>
<Checkbox
label="this.getCheckboxLabel('lastColumn')"
name="'lastColumn'"
value="this.tableConfig.lastColumn"
onChange="(val) => this.updateTableConfig('lastColumn', val)"
/>
<Checkbox
label="this.getCheckboxLabel('bandedColumns')"
name="'bandedColumns'"
value="this.tableConfig.bandedColumns"
onChange="(val) => this.updateTableConfig('bandedColumns', val)"
/>
</div>
</div>
</Section>
<Section>
<TableStylePicker
tableConfig="this.props.table.config"
onStylePicked.bind="this.onStylePicked"
tableStyles="this.env.model.getters.getTableStyles()"
type="'table'"
/>
</Section>
<Section title.translate="Data range">
<SelectionInput
t-key="this.props.table.type"
ranges="[this.state.tableXc]"
hasSingleRange="true"
isInvalid="this.state.tableZoneErrors.length !== 0"
onSelectionChanged="(ranges) => this.onRangeChanged(ranges)"
onSelectionConfirmed.bind="this.onRangeConfirmed"
/>
</Section>
<Section class="'pt-0'">
<Checkbox
label="this.getCheckboxLabel('automaticAutofill')"
name="'automaticAutofill'"
value="this.tableConfig.automaticAutofill and this.props.table.type !== 'dynamic'"
onChange="(val) => this.updateTableConfig('automaticAutofill', val)"
className="'mb-1'"
disabled="this.props.table.type === 'dynamic'"
/>
<div class="d-flex flex-row align-items-center">
<Checkbox
label="this.getCheckboxLabel('isDynamic')"
name="'isDynamic'"
value="this.props.table.type === 'dynamic'"
onChange.bind="this.updateTableIsDynamic"
disabled="!this.canBeDynamic"
/>
<div
class="o-info-icon d-flex flex-row align-items-center text-muted ms-1"
t-att-title="this.dynamicTableTooltip">
<t t-call="o-spreadsheet-Icon.CIRCLE_INFO"/>
</div>
</div>
</Section>
<Section>
<div class="o-sidePanelButtons">
<button t-on-click="this.deleteTable" class="o-table-delete o-button o-button-danger">
Delete table
</button>
</div>
</Section>
<Section t-if="this.errorMessages.length">
<ValidationMessages messages="this.errorMessages" msgType="'error'"/>
</Section>
</div>
</t>
<t t-name="o-spreadsheet-SplitIntoColumnsPanel">
<div class="o-split-to-cols-panel">
<Section title.translate="Separator">
<Select
class="'mb-3'"
values="this.separators"
selectedValue="this.state.separatorValue"
onChange.bind="this.onSeparatorChange"
/>
<input
class="o-input mb-3"
type="text"
t-if="this.state.separatorValue === 'custom'"
t-att-value="this.state.customSeparator"
t-on-input="this.updateCustomSeparator"
placeholder="Add any characters or symbol"
/>
<t t-set="addColumnsLabel">Add new columns to avoid overwriting cells</t>
<Checkbox
value="this.state.addNewColumns"
label="addColumnsLabel"
onChange.bind="this.updateAddNewColumnsCheckbox"
/>
</Section>
<Section>
<div class="o-sidePanelButtons">
<button
class="o-button primary"
t-att-class="{'o-disabled': this.isConfirmDisabled}"
t-on-click="this.confirm">
Confirm
</button>
</div>
</Section>
<Section t-if="this.errorMessages.length || this.warningMessages.length" class="'pb-0 pt-2'">
<ValidationMessages messages="this.errorMessages" msgType="'error'"/>
<ValidationMessages messages="this.warningMessages" msgType="'warning'"/>
</Section>
</div>
</t>
<t t-name="o-spreadsheet-SidePanels" t-if="this.sidePanelStore.isMainPanelOpen">
<div class="o-sidePanels d-flex overflow-hidden">
<t t-foreach="this.panelList" t-as="panel" t-key="panel.key">
<div t-att-style="panel.style">
<SidePanel t-key="panel.key" t-props="panel.props"/>
</div>
</t>
</div>
</t>
<t t-name="o-spreadsheet-SidePanel">
<t t-if="this.props.isCollapsed" t-call="o-spreadsheet-SidePanelCollapsed"/>
<t t-else="" t-call="o-spreadsheet-SidePanelExtended"/>
</t>
<t t-name="o-spreadsheet-SidePanelExtended">
<div class="o-sidePanel h-100 bg-white border-top border-start">
<div
class="o-sidePanelHeader d-flex align-items-center justify-content-between border-bottom">
<div
t-if="this.props.onToggleCollapsePanel"
class="o-collapse-panel o-sidePanelAction rounded"
t-on-click="this.props.onToggleCollapsePanel">
<i class="fa fa-angle-double-right"/>
</div>
<div class="o-sidePanelTitle o-fw-bold ms-2" t-out="this.getTitle()"/>
<div
t-if="this.props.isPinned"
class="o-pin-panel o-sidePanelAction ms-auto rounded active"
t-on-click="this.props.onTogglePinPanel">
<i class="fa fa-thumb-tack"/>
</div>
<div
class="o-sidePanelClose o-sidePanelAction rounded"
t-on-click="this.props.onCloseSidePanel">
✕
</div>
</div>
<div class="o-sidePanelBody-container d-flex flex-grow-1 ">
<div class="o-sidePanel-handle-container">
<div
class="o-sidePanel-handle"
t-on-pointerdown="this.props.onStartHandleDrag"
t-on-dblclick="this.props.onResetPanelSize">
<t t-call="o-spreadsheet-Icon.THIN_DRAG_HANDLE"/>
</div>
</div>
<div class="o-sidePanelBody">
<t
t-component="this.props.panelContent.Body"
t-props="this.props.panelProps"
onCloseSidePanel="this.props.onCloseSidePanel"
/>
</div>
<div class="o-sidePanelFooter" t-if="this.props.panelContent?.Footer">
<t t-component="this.props.panelContent.Footer" t-props="this.props.panelProps"/>
</div>
</div>
</div>
</t>
<t t-name="o-spreadsheet-SidePanelCollapsed">
<div
class="o-sidePanel collapsed w-100 h-100 bg-white"
t-on-click="this.props.onToggleCollapsePanel">
<div class="d-flex flex-column align-items-center">
<div
t-if="this.props.onToggleCollapsePanel"
class="o-collapse-panel o-sidePanelAction rounded mb-1">
<i class="fa fa-angle-double-left"/>
</div>
<div
t-if="this.props.isPinned"
class="o-pin-panel o-sidePanelAction ms-auto rounded active"
t-on-click="this.props.onTogglePinPanel">
<i class="fa fa-thumb-tack"/>
</div>
<div
class="o-sidePanelClose o-sidePanelAction rounded mb-1"
t-on-click.stop="this.props.onCloseSidePanel">
✕
</div>
<div class="o-sidePanelTitle o-fw-bold" t-out="this.getTitle()"/>
</div>
</div>
</t>
<t t-name="o-spreadsheet-SettingsPanel">
<div class="o-settings-panel">
<Section title.translate="Locale">
<Select
selectedValue="this.currentLocale.code"
values="this.selectOptions"
onChange="(value) => this.onLocaleChange(value)"
/>
<div class="o-locale-preview mt-4 p-3 rounded border">
<div>
<span class="o-fw-bold me-1">Number:</span>
<span t-out="this.numberFormatPreview"/>
</div>
<div>
<span class="o-fw-bold me-1">Date:</span>
<span t-out="this.dateFormatPreview"/>
</div>
<div>
<span class="o-fw-bold me-1">Date time:</span>
<span t-out="this.dateTimeFormatPreview"/>
</div>
<div>
<span class="o-fw-bold me-1">First day of week:</span>
<span t-out="this.firstDayOfWeek"/>
</div>
</div>
</Section>
<Section class="'pt-0'">
<t t-set="message">Those settings affect all users.</t>
<ValidationMessages messages="[message]" msgType="'info'"/>
</Section>
</div>
</t>
<t t-name="o-spreadsheet-RemoveDuplicatesPanel">
<div class="o-remove-duplicates">
<Section>
<ValidationMessages messages="[this.selectionStatisticalInformation]" msgType="'info'"/>
</Section>
<Section class="'pt-0'">
<t t-set="dataHasHeaderLabel">Data has header row</t>
<Checkbox
name="'dataHasHeader'"
value="this.state.hasHeader"
label="dataHasHeaderLabel"
onChange.bind="this.toggleHasHeader"
/>
</Section>
<Section class="'pt-0'" title.translate="Columns to analyze">
<div class="o-checkbox-selection overflow-auto">
<t t-set="selectAllLabel">Select all</t>
<Checkbox
value="this.isEveryColumnSelected"
label="selectAllLabel"
onChange.bind="this.toggleAllColumns"
/>
<t t-foreach="Object.keys(this.state.columns)" t-as="colIndex" t-key="colIndex">
<Checkbox
value="this.state.columns[colIndex]"
label="this.getColLabel(colIndex)"
onChange="() => this.toggleColumn(colIndex)"
/>
</t>
</div>
</Section>
<Section>
<div class="o-sidePanelButtons">
<button
class="o-button primary"
t-att-class="{'o-disabled': !this.canConfirm}"
t-on-click="this.onRemoveDuplicates">
Remove duplicates
</button>
</div>
</Section>
<Section t-if="this.errorMessages.length">
<ValidationMessages messages="this.errorMessages" msgType="'error'"/>
</Section>
</div>
</t>
<t t-name="o-spreadsheet-PivotTitleSection">
<Section>
<t t-set-slot="title">
<div class="d-flex flex-row justify-content-between align-items-center">
Name
<CogWheelMenu items="this.cogWheelMenuItems"/>
</div>
</t>
<TextInput
class="'os-pivot-title'"
value="this.name"
onChange.bind="this.onNameChanged"
selectContentOnFocus="true"
/>
</Section>
</t>
<t t-name="o-spreadsheet-PivotSidePanel">
<div class="o-pivot-panel d-flex flex-column h-100">
<div class="o-panel d-flex">
<div
class="o-sidePanel-tab o-panel-configuration d-flew flex-grow-1 text-center"
t-att-class="this.state.panel !== 'configuration' ? 'inactive' : ''"
t-on-click="this.switchPanel.bind(this, 'configuration')">
<i class="fa fa-sliders me-1"/>
Configuration
</div>
<div
class="o-sidePanel-tab o-panel-design d-flew flex-grow-1 text-center"
t-att-class="this.state.panel !== 'design' ? 'inactive' : ''"
t-on-click="this.switchPanel.bind(this, 'design')">
<i class="fa fa-paint-brush me-1"/>
Design
</div>
</div>
<div class="o-panel-content overflow-hidden h-100">
<div class="h-100" t-att-class="this.state.panel !== 'configuration' ? 'd-none' : ''">
<t
t-component="this.sidePanelEditor"
pivotId="this.props.pivotId"
onCloseSidePanel="this.props.onCloseSidePanel"
/>
</div>
<div class="h-100" t-att-class="this.state.panel !== 'design' ? 'd-none' : ''">
<PivotDesignPanel pivotId="this.props.pivotId"/>
</div>
</div>
</div>
</t>
<t t-name="o-spreadsheet-PivotSpreadsheetSidePanel">
<t t-set="isReadonly" t-value="this.env.model.getters.isReadonly()"/>
<div class="d-flex flex-column h-100 justify-content-between overflow-hidden">
<div
class="h-100 position-relative overflow-x-hidden overflow-y-auto"
t-ref="this.pivotSidePanelRef">
<div
t-att="isReadonly ? ['inert', 1] : []"
t-att-class="{ 'pe-none opacity-50': isReadonly }">
<PivotTitleSection pivotId="this.props.pivotId" flipAxis.bind="this.flipAxis"/>
<Section title.translate="Range">
<SelectionInput
ranges="this.ranges"
required="true"
isInvalid="this.shouldDisplayInvalidRangeError"
hasSingleRange="true"
onSelectionChanged="(ranges) => this.onSelectionChanged(ranges)"
onSelectionConfirmed="() => this.onSelectionConfirmed()"
/>
<span
class="text-danger sp_range_error_message"
t-if="this.shouldDisplayInvalidRangeError"
t-out="this.pivot.invalidRangeMessage"
/>
</Section>
<PivotLayoutConfigurator
t-if="!this.pivot.isInvalidRange"
unusedGroupableFields="this.store.unusedGroupableFields"
measureFields="this.store.measureFields"
unusedGranularities="this.store.unusedGranularities"
dateGranularities="this.store.dateGranularities"
datetimeGranularities="this.store.datetimeGranularities"
definition="this.definition"
onDimensionsUpdated.bind="this.onDimensionsUpdated"
onFiltersUpdated.bind="this.onFiltersUpdated"
getScrollableContainerEl.bind="this.getScrollableContainerEl"
pivotId="this.props.pivotId"
/>
<t t-if="!this.pivot.isInvalidRange">
<Section class="'pt-0'">
<div
class="o-fw-bold pt-0 pb-1 d-flex flex-row justify-content-between align-items-center o-section-title">
Domain
<AddDimensionButton
onFieldPicked.bind="this.addFilter"
fields="this.store.unusedFilterFields"
/>
</div>
<t t-foreach="this.definition.filters" t-as="filter" t-key="filter_index">
<PivotFilterEditor
pivotId="this.props.pivotId"
definition="this.definition"
filter="filter"
onFiltersUpdated.bind="this.onFiltersUpdated"
/>
</t>
</Section>
</t>
</div>
</div>
<PivotDeferUpdate
t-if="!isReadonly"
deferUpdate="this.store.updatesAreDeferred"
toggleDeferUpdate="(value) => this.store.deferUpdates(value)"
isDirty="this.store.isDirty"
discard="this.store.discardPendingUpdate"
apply="this.store.applyUpdate"
/>
</div>
</t>
<t t-name="o-spreadsheet-PivotDesignPanel">
<div class="h-100 overflow-y-auto">
<Section class="'o-pivot-design'" title.translate="Display options">
<div>
<div class="container-fluid p-0 pt-2">
<div class="row mb-2 align-items-center">
<div class="col-6">Max rows:</div>
<div class="col-6 d-flex align-items-center">
<NumberInput
value="this.pivotStyle.numberOfRows ?? ''"
class="'o-pivot-n-of-rows'"
placeholder.translate="e.g. 10"
onChange="(value) => this.updatePivotStyleNumberProperty(value, 'numberOfRows')"
/>
</div>
</div>
<div class="row mb-2 align-items-center">
<div class="col-6">Max columns:</div>
<div class="col-6 d-flex align-items-center">
<NumberInput
value="this.pivotStyle.numberOfColumns ?? ''"
class="'o-pivot-n-of-columns'"
placeholder.translate="e.g. 5"
onChange="(value) => this.updatePivotStyleNumberProperty(value, 'numberOfColumns')"
/>
</div>
</div>
<div class="row mb-2 align-items-center">
<div class="col-6">Show totals:</div>
<div class="col-6 d-flex align-items-center">
<Checkbox
name="'displayTotals'"
value="this.pivotStyle.displayTotals ?? this.defaultStyle.displayTotals"
onChange="(val) => this.updatePivotStyleProperty('displayTotals', val)"
/>
</div>
</div>
<div class="row mb-2 align-items-center">
<div class="col-6">Show column titles:</div>
<div class="col-6 d-flex align-items-center">
<Checkbox
name="'displayColumnHeaders'"
value="this.pivotStyle.displayColumnHeaders ?? this.defaultStyle.displayColumnHeaders"
onChange="(val) => this.updatePivotStyleProperty('displayColumnHeaders', val)"
/>
</div>
</div>
<div class="row mb-2 align-items-center">
<div class="col-6">Show measure titles:</div>
<div class="col-6 d-flex align-items-center">
<Checkbox
name="'displayMeasuresRow'"
value="this.pivotStyle.displayMeasuresRow ?? this.defaultStyle.displayMeasuresRow"
onChange="(val) => this.updatePivotStyleProperty('displayMeasuresRow', val)"
/>
</div>
</div>
</div>
<div class="row mb-2 align-items-center">
<div class="col-6">Use tabular form:</div>
<div class="col-6 d-flex align-items-center">
<Checkbox
name="'tabularForm'"
value="this.pivotStyle.tabularForm ?? this.defaultStyle.tabularForm"
onChange="(val) => this.updatePivotStyleProperty('tabularForm', val)"
/>
</div>
</div>
</div>
</Section>
<Section class="'o-pivot-table-style'" title.translate="Pivot table style">
<div class="row mb-2 align-items-center pt-2">
<div class="col-6">Banded rows:</div>
<div class="col-6 d-flex align-items-center">
<Checkbox
name="'bandedRows'"
value="this.pivotStyle.bandedRows ?? this.defaultStyle.bandedRows"
onChange="(val) => this.updatePivotStyleProperty('bandedRows', val)"
/>
</div>
</div>
<div class="row mb-2 align-items-center">
<div class="col-6">Banded columns</div>
<div class="col-6 d-flex align-items-center">
<Checkbox
name="'bandedColumns'"
value="this.pivotStyle.bandedColumns ?? this.defaultStyle.bandedColumns"
onChange="(val) => this.updatePivotStyleProperty('bandedColumns', val)"
/>
</div>
</div>
<div class="row mb-2 align-items-center">
<div class="col-6">Filter button</div>
<div class="col-6 d-flex align-items-center">
<Checkbox
name="'hasFilters'"
value="this.pivotStyle.hasFilters ?? this.defaultStyle.hasFilters"
onChange="(val) => this.updatePivotStyleProperty('hasFilters', val)"
/>
</div>
</div>
<div class="mt-4">
<TableStylePicker
tableConfig="this.tableConfig"
onStylePicked.bind="this.onStylePicked"
tableStyles="this.tableStyles"
type="'pivot'"
/>
</div>
</Section>
</div>
</t>
<t t-name="o-spreadsheet-PivotMeasureDisplayPanel">
<Section title.translate="Show measure as:">
<Select
class="'o-pivot-measure-display-type'"
values="this.measureDisplayTypeOptions"
selectedValue="this.store.measureDisplay.type"
onChange="(value) => this.store.updateMeasureDisplayType(value)"
/>
<div
class="o-pivot-measure-display-description mt-3 ps-3 border-start"
t-out="this.measureDisplayDescription[this.store.measureDisplay.type]"
/>
</Section>
<Section t-if="this.store.doesDisplayNeedsField" title.translate="Base field:">
<div class="o-pivot-measure-display-field w-100 py-1 px-3 border">
<t t-if="this.store.fields.length">
<RadioSelection
choices="this.fieldChoices"
selectedValue="this.store.measureDisplay.fieldNameWithGranularity"
name="'baseField'"
onChange.bind="(val) => this.store.updateMeasureDisplayField(val)"
direction="'vertical'"
/>
</t>
<t t-else="">
<div class="text-muted text-center my-3">No active dimension in the pivot</div>
</t>
</div>
</Section>
<t t-set="values" t-value="this.store.values"/>
<Section t-if="this.store.doesDisplayNeedsValue and values.length" title.translate="Base item:">
<div class="o-pivot-measure-display-value w-100 py-1 px-3 border">
<RadioSelection
choices="values"
selectedValue="this.store.measureDisplay.value"
name="'baseValue'"
onChange.bind="(val) => this.store.updateMeasureDisplayValue(val)"
direction="'vertical'"
/>
</div>
</Section>
<Section>
<div class="o-sidePanelButtons">
<button t-on-click="this.onCancel" class="o-pivot-measure-cancel o-button">Discard</button>
<button t-on-click="this.onSave" class="o-pivot-measure-save o-button pr