@odoo/o-spreadsheet
Version:
A spreadsheet component
1,363 lines (1,311 loc) • 281 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.0.11
@date 2025-11-24T07:47:37.683Z
@hash f5bdbcc
-->
<odoo>
<t t-name="o-spreadsheet-ValidationMessages">
<t t-foreach="alertBoxes" t-as="box" t-key="'box' + box_index">
<div t-att-class="divClasses" class="d-flex flex-column p-3 m-1 o-validation">
<div class="d-flex align-items-center">
<t t-if="props.msgType === 'info'" t-call="o-spreadsheet-Icon.CIRCLE_INFO"/>
<t t-else="" t-call="o-spreadsheet-Icon.TRIANGLE_EXCLAMATION"/>
<div class="d-flex flex-column overflow-hidden">
<span
t-foreach="box"
t-as="msg"
t-key="msg_index"
class="ps-2"
t-att-class="{'text-truncate': props.singleBox }"
t-esc="msg"
/>
</div>
</div>
</div>
</t>
</t>
<t t-name="o-spreadsheet-TopBar">
<div
class="o-spreadsheet-topbar d-flex flex-column user-select-none"
t-on-click="props.onClick">
<div t-if="!env.isSmall" class="o-topbar-top d-flex justify-content-between">
<!-- Menus -->
<div class="o-topbar-topleft d-flex">
<t t-foreach="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': state.menuState.parentMenu and 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-esc="getMenuName(menu)"/>
</div>
</t>
</div>
<div class="o-topbar-topright d-flex justify-content-end align-items-center">
<div t-foreach="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': !env.model.getters.isReadonly()}"
t-ref="toolBarContainer">
<div
class="o-topbar-toolbar d-flex"
t-att-class="{'flex-shrink-0': env.model.getters.isReadonly()}">
<!-- Toolbar -->
<div
t-if="env.model.getters.isReadonly()"
class="o-readonly-toolbar d-flex align-items-center text-muted">
<span>
<i class="fa fa-eye"/>
Readonly Access
</span>
</div>
<div t-else="" class="o-toolbar-tools d-flex ms-4 flex-grow-1" t-ref="toolBar">
<div
class="d-flex tool-container"
t-foreach="toolsCategories"
t-as="category"
t-key="category"
t-att-id="category">
<t
t-foreach="toolbarMenuRegistry.getEntries(category)"
t-as="toolbarAction"
t-key="toolbarAction_index">
<t t-component="toolbarAction.component" t-props="toolbarAction.props"/>
</t>
<div t-if="showDivider(category_index)" class="o-topbar-divider"/>
</div>
<div
t-ref="moreToolsContainer"
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="moreToolsButton"
t-on-click.stop="toggleMoreTools">
<t t-call="o-spreadsheet-Icon.SHORT_THIN_DRAG_HANDLE"/>
</span>
</div>
</div>
</div>
<TopBarComposer t-if="!env.isSmall"/>
</div>
<div
t-if="this.fingerprints.isEnabled"
class="irregularity-map d-flex align-items-center justify-content-between">
<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 btn-link flex-shrink-0"
t-on-click="() => this.fingerprints.disable()">
Turn off
</div>
</div>
</div>
</div>
<MenuPopover
t-if="state.menuState.isOpen"
anchorRect="state.menuState.anchorRect"
menuItems="state.menuState.menuItems"
onClose="() => this.closeMenus()"
onMenuClicked="() => this.props.onClick()"
popoverPositioning="'bottom-left'"
/>
<Popover t-if="state.toolsPopoverState.isOpen" t-props="toolsPopoverProps">
<div class="d-flex px-2 py-1 flex-wrap align-items-center" style="background-color:white;">
<t
t-foreach="state.invisibleToolsCategories"
t-as="category"
t-key="category"
t-att-id="category">
<t
t-foreach="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 < state.invisibleToolsCategories.length-1"
class="o-topbar-divider"
/>
</t>
</div>
</Popover>
</t>
<div t-name="o-spreadsheet-NumberFormatsTool" t-ref="buttonRef" t-on-click.stop="">
<ActionButton
action="formatNumberMenuItemSpec"
hasTriangleDownIcon="true"
onClick.bind="toggleMenu"
class="props.class"
/>
<MenuPopover
t-if="isActive"
anchorRect="state.anchorRect"
menuItems="state.menuItems"
onClose="() => {}"
popoverPositioning="'bottom-left'"
/>
</div>
<t t-name="o-spreadsheet-TopBarFontSizeEditor">
<FontSizeEditor
currentFontSize="currentFontSize"
onFontSizeChanged.bind="this.setFontSize"
class="props.class"
onToggle.bind="this.onToggle"
onFocusInput.bind="this.onFocusInput"
/>
</t>
<div
t-name="o-spreadsheet-DropdownAction"
class="o-dropdown"
t-ref="actionRef"
t-on-click.stop="">
<ActionButton
action="props.parentAction"
hasTriangleDownIcon="true"
onClick.bind="toggleDropdown"
class="props.class"
/>
<Popover t-if="isActive" t-props="popoverProps">
<div class="o-dropdown-content p-1" t-if="isActive" t-on-click.stop="">
<div class="o-dropdown-line">
<t t-foreach="props.childActions" t-as="action" t-key="action_index">
<ActionButton action="action" class="props.childClass"/>
</t>
</div>
</div>
</Popover>
</div>
<t t-name="o-spreadsheet-ColorEditor">
<div class="d-flex align-items-center">
<ColorPickerWidget
currentColor="currentColor"
toggleColorPicker.bind="onClick"
showColorPicker="isMenuOpen"
onColorPicked="(color) => this.setColor(color)"
title="props.title"
icon="props.icon"
class="props.class"
/>
</div>
</t>
<div t-name="o-spreadsheet-TextInput" class="w-100">
<input
t-ref="input"
class="os-input w-100"
type="text"
t-att-class="inputClass"
t-att-id="props.id"
t-att-placeholder="props.placeholder"
t-att-value="props.value"
t-on-change="save"
t-on-blur="save"
t-on-pointerdown="onMouseDown"
t-on-pointerup="onMouseUp"
t-on-keydown="onKeyDown"
/>
</div>
<t t-name="o-spreadsheet-TableStylesPopover">
<Popover t-if="props.popoverProps" t-props="props.popoverProps">
<div
class="o-table-style-popover d-flex flex-column py-3"
t-ref="tableStyleList"
t-on-contextmenu.prevent="">
<div class="d-flex o-notebook ps-4 mb-3">
<div
t-foreach="Object.keys(categories)"
t-as="category"
t-key="category"
class="o-notebook-tab d-flex align-items-center"
t-att-class="{ 'selected': state.selectedCategory === category }"
t-on-click="() => state.selectedCategory = category"
t-att-data-id="category"
t-esc="categories[category_value]"
/>
</div>
<div class="d-flex flex-wrap px-4">
<t t-foreach="displayedStyles" t-as="styleId" t-key="styleId">
<TableStylePreview
class="'o-table-style-popover-preview'"
styleId="styleId"
selected="styleId === props.selectedStyleId"
tableConfig="props.tableConfig"
tableStyle="env.model.getters.getTableStyle(styleId)"
onClick="() => this.props.onStylePicked(styleId)"
/>
</t>
<div
t-if="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="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': props.selected }"
t-att-data-id="props.styleId"
t-att-title="styleName"
t-on-click="props.onClick"
t-on-contextmenu.prevent="(ev) => this.onContextMenu(ev)">
<div t-att-class="props.class">
<canvas t-ref="canvas" class="w-100 h-100"/>
</div>
<div
class="o-table-style-edit-button position-absolute d-none"
t-if="isStyleEditable"
t-on-click="this.editTableStyle"
title="Edit custom table style">
<t t-call="o-spreadsheet-Icon.EDIT"/>
</div>
</div>
<MenuPopover
t-if="menu.isOpen"
menuItems="menu.menuItems"
anchorRect="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">
<div class="d-flex flex-row overflow-hidden ps-2">
<t t-foreach="getDisplayedTableStyles()" t-as="styleId" t-key="styleId">
<TableStylePreview
class="'o-table-style-picker-preview'"
selected="styleId === props.table.config.styleId"
tableConfig="props.table.config"
tableStyle="env.model.getters.getTableStyle(styleId)"
styleId="styleId"
onClick="() => this.onStylePicked(styleId)"
/>
</t>
</div>
<div
class="o-table-style-picker-arrow d-flex align-items-center px-1"
t-on-click.stop="onArrowButtonClick">
<t t-call="o-spreadsheet-Icon.CARET_DOWN"/>
</div>
</div>
<TableStylesPopover
tableConfig="props.table.config"
selectedStyleId="props.table.config.styleId"
onStylePicked.bind="onStylePicked"
popoverProps="state.popoverProps"
closePopover.bind="closePopover"
/>
</t>
<t t-name="o-spreadsheet-TableResizer">
<div
class="o-table-resizer position-absolute"
t-att-style="containerStyle"
t-on-pointerdown="onMouseDown"
/>
</t>
<t t-name="o-spreadsheet-TableDropdownButton">
<div class="o-table-widget d-flex align-item-center" t-att-class="props.class">
<ActionButton
action="action"
hasTriangleDownIcon="true"
t-on-click="onClick"
class="'o-hoverable-button'"
/>
</div>
<TableStylesPopover
tableConfig="tableConfig"
onStylePicked.bind="onStylePicked"
popoverProps="state.popoverProps"
closePopover.bind="closePopover"
/>
</t>
<t t-name="o-spreadsheet-Spreadsheet">
<div
class="o-spreadsheet h-100 w-100"
t-att-class="{'o-spreadsheet-mobile': env.isSmall}"
t-ref="spreadsheet"
t-att-style="getStyle()">
<t t-if="env.isDashboard()">
<SpreadsheetDashboard getGridSize.bind="getGridSize"/>
<FullScreenFigure/>
</t>
<t t-else="">
<div class="o-spreadsheet-topbar-wrapper o-two-columns">
<TopBar onClick="() => this.focusGrid()" dropdownMaxHeight="gridHeight"/>
</div>
<div
class="o-grid-container"
t-att-class="{'o-two-columns': !sidePanel.isMainPanelOpen}"
t-att-style="gridContainerStyle"
t-on-click="this.focusGrid">
<div class="o-top-left"/>
<div class="o-column-groups">
<HeaderGroupContainer layers="colLayers" dimension="'COL'"/>
</div>
<div class="o-row-groups">
<HeaderGroupContainer layers="rowLayers" dimension="'ROW'"/>
</div>
<div class="o-group-grid overflow-hidden">
<Grid exposeFocus="(focus) => this._focusGrid = focus" getGridSize.bind="getGridSize"/>
</div>
</div>
<SidePanels/>
<div class="o-spreadsheet-bottombar-wrapper o-two-columns overflow-hidden">
<SmallBottomBar t-if="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="menuState.isOpen">
<RibbonMenu onClose="() => this.menuState.isOpen=false"/>
</t>
<t t-else="">
<div class="o-small-composer px-2 py-2 position-relative">
<div class="w-100" t-ref="bottombarComposer">
<Composer t-props="composerProps"/>
<span
t-if="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="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-esc="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">
<Ripple>
<div class="py-1 px-1 mx-2 ribbon-toggler" t-on-click="toggleRibbon">
<i class="o-icon fa fa-cog"/>
</div>
</Ripple>
<BottomBar onClick="props.onClick"/>
</div>
</t>
</div>
</t>
<div t-name="o-spreadsheet-RibbonMenu">
<div class="o-ribbon-menu d-flex flex-column" t-ref="menu">
<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="onClickBack"
t-att-title="backTitle">
<i class="fa fa-angle-left"/>
</div>
<span class="d-flex align-items-center" t-esc="state.title"/>
</div>
<div
class="o-ribbon-menu-wrapper overflow-auto"
t-ref="container"
t-on-scroll="updateShadows">
<Menu t-props="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-model="state.styleName"/>
</Section>
<Section class="'pt-1'" title.translate="Style color">
<RoundColorPicker
currentColor="state.primaryColor"
onColorPicked.bind="onColorPicked"
disableNoColor="true"
/>
</Section>
<Section class="'pt-1'" title.translate="Style template">
<div class="d-flex flex-wrap">
<t t-foreach="tableTemplates" t-as="templateName" t-key="templateName">
<TableStylePreview
class="'o-table-style-edit-template-preview'"
selected="templateName === state.selectedTemplateName"
tableConfig="previewTableConfig"
tableStyle="computeTableStyle(templateName)"
onClick="() => this.onTemplatePicked(templateName)"
/>
</t>
</div>
</Section>
<Section>
<div class="o-sidePanelButtons">
<button
t-if="props.styleId"
t-on-click="onDelete"
class="o-delete o-button-danger o-button">
Delete
</button>
<button t-on-click="onCancel" class="o-cancel o-button">Cancel</button>
<button t-on-click="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="getCheckboxLabel('headerRow')"
name="'headerRow'"
value="tableConfig.numberOfHeaders > 0"
onChange.bind="this.updateHasHeaders"
/>
<input
t-if="tableConfig.numberOfHeaders > 0"
t-att-value="tableConfig.numberOfHeaders"
type="number"
class="o-table-n-of-headers ms-2 o-input"
t-on-change="onChangeNumberOfHeaders"
/>
</div>
<Checkbox
label="getCheckboxLabel('totalRow')"
name="'totalRow'"
value="tableConfig.totalRow"
onChange="(val) => this.updateTableConfig('totalRow', val)"
/>
<Checkbox
label="getCheckboxLabel('bandedRows')"
name="'bandedRows'"
value="tableConfig.bandedRows"
onChange="(val) => this.updateTableConfig('bandedRows', val)"
/>
<Checkbox
label="getCheckboxLabel('hasFilters')"
name="'hasFilters'"
value="tableConfig.hasFilters"
title="hasFilterCheckboxTooltip"
disabled="!this.canHaveFilters"
onChange.bind="this.updateHasFilters"
/>
</div>
<div>
<Checkbox
label="getCheckboxLabel('firstColumn')"
name="'firstColumn'"
value="tableConfig.firstColumn"
onChange="(val) => this.updateTableConfig('firstColumn', val)"
/>
<Checkbox
label="getCheckboxLabel('lastColumn')"
name="'lastColumn'"
value="tableConfig.lastColumn"
onChange="(val) => this.updateTableConfig('lastColumn', val)"
/>
<Checkbox
label="getCheckboxLabel('bandedColumns')"
name="'bandedColumns'"
value="tableConfig.bandedColumns"
onChange="(val) => this.updateTableConfig('bandedColumns', val)"
/>
</div>
</div>
</Section>
<Section>
<TableStylePicker table="props.table"/>
</Section>
<Section title.translate="Data range">
<SelectionInput
t-key="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="getCheckboxLabel('automaticAutofill')"
name="'automaticAutofill'"
value="tableConfig.automaticAutofill"
onChange="(val) => this.updateTableConfig('automaticAutofill', val)"
className="'mb-1'"
/>
<div class="d-flex flex-row align-items-center">
<Checkbox
label="getCheckboxLabel('isDynamic')"
name="'isDynamic'"
value="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="dynamicTableTooltip">
<t t-call="o-spreadsheet-Icon.CIRCLE_INFO"/>
</div>
</div>
</Section>
<Section>
<div class="o-sidePanelButtons">
<button t-on-click="deleteTable" class="o-table-delete o-button o-button-danger">
Delete table
</button>
</div>
</Section>
<Section t-if="errorMessages.length">
<ValidationMessages messages="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="o-input mb-3" t-on-change="(ev) => this.onSeparatorChange(ev.target.value)">
<option
t-foreach="separators"
t-as="separator"
t-key="separator.value"
t-att-value="separator.value"
t-esc="separator.name"
t-att-selected="state.separatorValue === separator.value"
/>
</select>
<input
class="o-input mb-3"
type="text"
t-if="state.separatorValue === 'custom'"
t-att-value="state.customSeparator"
t-on-input="updateCustomSeparator"
placeholder="Add any characters or symbol"
/>
<t t-set="addColumnsLabel">Add new columns to avoid overwriting cells</t>
<Checkbox
value="state.addNewColumns"
label="addColumnsLabel"
onChange.bind="updateAddNewColumnsCheckbox"
/>
</Section>
<Section>
<div class="o-sidePanelButtons">
<button
class="o-button primary"
t-att-class="{'o-disabled': isConfirmDisabled}"
t-on-click="confirm">
Confirm
</button>
</div>
</Section>
<Section t-if="errorMessages.length || warningMessages.length" class="'pb-0 pt-2'">
<ValidationMessages messages="errorMessages" msgType="'error'"/>
<ValidationMessages messages="warningMessages" msgType="'warning'"/>
</Section>
</div>
</t>
<t t-name="o-spreadsheet-SidePanels" t-if="sidePanelStore.isMainPanelOpen">
<div class="o-sidePanels d-flex overflow-hidden">
<t t-foreach="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="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">
<div class="o-sidePanelHeader d-flex align-items-center justify-content-between">
<div
t-if="props.onToggleCollapsePanel"
class="o-collapse-panel o-sidePanelAction rounded"
t-on-click="props.onToggleCollapsePanel">
<i class="fa fa-angle-double-right"/>
</div>
<div class="o-sidePanelTitle o-fw-bold ms-2" t-esc="getTitle()"/>
<div
t-if="props.onTogglePinPanel"
class="o-pin-panel o-sidePanelAction ms-auto rounded"
t-att-class="{'active': props.isPinned}"
t-on-click="props.onTogglePinPanel"
t-att-title="pinInfoMessage">
<i class="fa fa-thumb-tack"/>
</div>
<div class="o-sidePanelClose o-sidePanelAction rounded" t-on-click="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="props.onStartHandleDrag"
t-on-dblclick="props.onResetPanelSize">
<t t-call="o-spreadsheet-Icon.THIN_DRAG_HANDLE"/>
</div>
</div>
<div class="o-sidePanelBody">
<t
t-component="props.panelContent.Body"
t-props="props.panelProps"
onCloseSidePanel="props.onCloseSidePanel"
/>
</div>
<div class="o-sidePanelFooter" t-if="props.panelContent?.Footer">
<t t-component="props.panelContent.Footer" t-props="props.panelProps"/>
</div>
</div>
</div>
</t>
<t t-name="o-spreadsheet-SidePanelCollapsed">
<div class="o-sidePanel collapsed w-100 h-100" t-on-click="props.onToggleCollapsePanel">
<div class="d-flex flex-column align-items-center">
<div
t-if="props.onToggleCollapsePanel"
class="o-collapse-panel o-sidePanelAction rounded mb-1">
<i class="fa fa-angle-double-left"/>
</div>
<div
t-if="props.onTogglePinPanel"
class="o-pin-panel o-sidePanelAction rounded mb-1"
t-att-class="{'active': props.isPinned}"
t-on-click.stop="props.onTogglePinPanel"
t-att-title="pinInfoMessage">
<i class="fa fa-thumb-tack"/>
</div>
<div
class="o-sidePanelClose o-sidePanelAction rounded mb-1"
t-on-click.stop="props.onCloseSidePanel">
✕
</div>
<div class="o-sidePanelTitle o-fw-bold" t-esc="getTitle()"/>
</div>
</div>
</t>
<t t-name="o-spreadsheet-SettingsPanel">
<div class="o-settings-panel">
<Section title.translate="Locale">
<select class="o-input" t-on-change="(ev) => this.onLocaleChange(ev.target.value)">
<option
t-foreach="supportedLocales"
t-as="locale"
t-key="locale.code"
t-att-value="locale.code"
t-esc="locale.name"
t-att-selected="currentLocale.code === locale.code"
/>
</select>
<div class="o-locale-preview mt-4 p-3 rounded">
<div>
<span class="o-fw-bold me-1">Number:</span>
<span t-esc="numberFormatPreview"/>
</div>
<div>
<span class="o-fw-bold me-1">Date:</span>
<span t-esc="dateFormatPreview"/>
</div>
<div>
<span class="o-fw-bold me-1">Date time:</span>
<span t-esc="dateTimeFormatPreview"/>
</div>
<div>
<span class="o-fw-bold me-1">First day of week:</span>
<span t-esc="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-SelectMenu">
<select
t-att-class="props.class"
t-ref="select"
t-on-pointerdown.stop.prevent=""
t-on-click="onClick">
<option selected="true" t-esc="props.selectedValue"/>
</select>
<MenuPopover
t-if="state.isMenuOpen"
menuItems="props.menuItems"
anchorRect="menuAnchorRect"
onClose.bind="onMenuClosed"
menuId="menuId"
popoverPositioning="'bottom-left'"
/>
</t>
<t t-name="o-spreadsheet-RemoveDuplicatesPanel">
<div class="o-remove-duplicates">
<Section>
<ValidationMessages messages="[selectionStatisticalInformation]" msgType="'info'"/>
</Section>
<Section class="'pt-0'">
<t t-set="dataHasHeaderLabel">Data has header row</t>
<Checkbox
name="'dataHasHeader'"
value="state.hasHeader"
label="dataHasHeaderLabel"
onChange.bind="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="isEveryColumnSelected"
label="selectAllLabel"
onChange.bind="toggleAllColumns"
/>
<t t-foreach="Object.keys(state.columns)" t-as="colIndex" t-key="colIndex">
<Checkbox
value="state.columns[colIndex]"
label="getColLabel(colIndex)"
onChange="() => this.toggleColumn(colIndex)"
/>
</t>
</div>
</Section>
<Section>
<div class="o-sidePanelButtons">
<button
class="o-button primary"
t-att-class="{'o-disabled': !canConfirm}"
t-on-click="onRemoveDuplicates">
Remove duplicates
</button>
</div>
</Section>
<Section t-if="errorMessages.length">
<ValidationMessages messages="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="cogWheelMenuItems"/>
</div>
</t>
<TextInput class="'os-pivot-title'" value="name" onChange.bind="onNameChanged"/>
</Section>
</t>
<t t-name="o-spreadsheet-PivotSidePanel">
<t t-component="sidePanelEditor" t-props="props"/>
</t>
<t t-name="o-spreadsheet-PivotSpreadsheetSidePanel">
<t t-set="isReadonly" t-value="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="pivotSidePanel">
<div
t-att="isReadonly ? ['inert', 1] : []"
t-att-class="{ 'pe-none opacity-50': isReadonly }">
<PivotTitleSection pivotId="props.pivotId" flipAxis.bind="flipAxis"/>
<Section title.translate="Range">
<SelectionInput
ranges="ranges"
required="true"
isInvalid="shouldDisplayInvalidRangeError"
hasSingleRange="true"
onSelectionChanged="(ranges) => this.onSelectionChanged(ranges)"
onSelectionConfirmed="() => this.onSelectionConfirmed()"
/>
<span
class="text-danger sp_range_error_message"
t-if="shouldDisplayInvalidRangeError"
t-esc="pivot.invalidRangeMessage"
/>
</Section>
<PivotLayoutConfigurator
t-if="!pivot.isInvalidRange"
unusedGroupableFields="store.unusedGroupableFields"
measureFields="store.measureFields"
unusedGranularities="store.unusedGranularities"
dateGranularities="store.dateGranularities"
datetimeGranularities="store.datetimeGranularities"
definition="definition"
onDimensionsUpdated.bind="onDimensionsUpdated"
getScrollableContainerEl.bind="getScrollableContainerEl"
pivotId="props.pivotId"
/>
</div>
</div>
<PivotDeferUpdate
t-if="!isReadonly"
deferUpdate="store.updatesAreDeferred"
toggleDeferUpdate="(value) => store.deferUpdates(value)"
isDirty="store.isDirty"
discard="store.discardPendingUpdate"
apply="store.applyUpdate"
/>
</div>
</t>
<t t-name="o-spreadsheet-PivotMeasureDisplayPanel">
<Section title.translate="Show measure as:">
<select
class="o-pivot-measure-display-type o-input"
t-on-change="(ev) => this.store.updateMeasureDisplayType(ev.target.value)">
<t t-foreach="measureDisplayTypeLabels" t-as="measureType" t-key="measureType">
<option
t-att-value="measureType"
t-att-selected="measureType === store.measureDisplay.type"
t-esc="measureType_value"
/>
</t>
</select>
<div
class="o-pivot-measure-display-description mt-3 ps-3"
t-esc="measureDisplayDescription[store.measureDisplay.type]"
/>
</Section>
<Section t-if="store.doesDisplayNeedsField" title.translate="Base field:">
<div class="o-pivot-measure-display-field w-100 py-1 px-3">
<t t-if="store.fields.length">
<RadioSelection
choices="fieldChoices"
selectedValue="store.measureDisplay.fieldNameWithGranularity"
name="'baseField'"
onChange.bind="(val) => 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="store.values"/>
<Section t-if="store.doesDisplayNeedsValue and values.length" title.translate="Base item:">
<div class="o-pivot-measure-display-value w-100 py-1 px-3">
<RadioSelection
choices="values"
selectedValue="store.measureDisplay.value"
name="'baseValue'"
onChange.bind="(val) => store.updateMeasureDisplayValue(val)"
direction="'vertical'"
/>
</div>
</Section>
<Section>
<div class="o-sidePanelButtons">
<button t-on-click="onCancel" class="o-pivot-measure-cancel o-button">Cancel</button>
<button t-on-click="onSave" class="o-pivot-measure-save o-button primary">Save</button>
</div>
</Section>
</t>
<t t-name="o-spreadsheet-PivotLayoutConfigurator">
<div class="pivot-dimensions o-section" t-ref="pivot-dimensions">
<div
class="o-fw-bold py-1 d-flex flex-row justify-content-between align-items-center o-section-title">
Columns
<AddDimensionButton
onFieldPicked.bind="addColumnDimension"
fields="props.unusedGroupableFields"
/>
</div>
<t t-foreach="props.definition.columns" t-as="col" t-key="col_index">
<div
t-on-pointerdown="(ev) => this.startDragAndDrop(col, ev)"
t-att-style="dragAndDrop.itemsStyle[col.nameWithGranularity]"
class="pt-1">
<PivotDimension dimension="col" onRemoved.bind="removeDimension">
<t t-set-slot="upper-right-icons">
<t t-set="errorMessage" t-value="getHugeDimensionErrorMessage(col)"/>
<i
t-if="errorMessage"
class="text-warning fa fa-exclamation-triangle"
t-att-title="errorMessage"
/>
</t>
<PivotDimensionGranularity
t-if="isDateOrDatetimeField(col)"
dimension="col"
onUpdated.bind="this.updateGranularity"
availableGranularities="props.unusedGranularities[col.fieldName]"
allGranularities="getGranularitiesFor(col)"
/>
<PivotDimensionOrder dimension="col" onUpdated.bind="this.updateOrder"/>
<PivotCustomGroupsCollapsible
t-if="col.isCustomField"
pivotId="props.pivotId"
customField="getCustomField(col)"
onCustomFieldUpdated="props.onDimensionsUpdated"
/>
</PivotDimension>
</div>
</t>
<div
class="o-fw-bold pt-4 pb-1 d-flex flex-row justify-content-between align-items-center o-section-title"
t-att-style="dragAndDrop.itemsStyle['__rows_title__']">
Rows
<AddDimensionButton
onFieldPicked.bind="addRowDimension"
fields="props.unusedGroupableFields"
/>
</div>
<t t-foreach="props.definition.rows" t-as="row" t-key="row_index">
<div
t-on-pointerdown="(ev) => this.startDragAndDrop(row, ev)"
t-att-style="dragAndDrop.itemsStyle[row.nameWithGranularity]"
class="pt-1">
<PivotDimension dimension="row" onRemoved.bind="removeDimension">
<t t-set-slot="upper-right-icons">
<t t-set="errorMessage" t-value="getHugeDimensionErrorMessage(row)"/>
<i
t-if="errorMessage"
class="text-warning fa fa-exclamation-triangle"
t-att-title="errorMessage"
/>
</t>
<PivotDimensionGranularity
t-if="isDateOrDatetimeField(row)"
dimension="row"
onUpdated.bind="this.updateGranularity"
availableGranularities="props.unusedGranularities[row.fieldName]"
allGranularities="getGranularitiesFor(row)"
/>
<PivotDimensionOrder dimension="row" onUpdated.bind="this.updateOrder"/>
<PivotCustomGroupsCollapsible
t-if="row.isCustomField"
pivotId="props.pivotId"
customField="getCustomField(row)"
onCustomFieldUpdated="props.onDimensionsUpdated"
/>
</PivotDimension>
</div>
</t>
<div
class="o-fw-bold pt-4 pb-1 d-flex flex-row justify-content-between align-items-center o-section-title o-pivot-measure">
Measures
<AddDimensionButton onFieldPicked.bind="addMeasureDimension" fields="props.measureFields">
<div
t-on-click="addCalculatedMeasure"
class="p-2 bg-white border-top d-flex align-items-center sticky-bottom add-calculated-measure">
<i class="pe-1">
<t t-call="o-spreadsheet-Icon.FORMULA"/>
</i>
Add calculated measure
</div>
</AddDimensionButton>
</div>
<t t-foreach="props.definition.measures" t-as="measure" t-key="measure.id">
<div
t-on-pointerdown="(ev) => this.startDragAndDropMeasures(measure, ev)"
t-att-style="dragAndDrop.itemsStyle[measure.id]"
t-att-class="measure.isHidden ? 'opacity-50' : ''"
class="pt-1 pivot-measure">
<PivotMeasureEditor
pivotId="props.pivotId"
definition="props.definition"
measure="measure"
aggregators="AGGREGATORS"
onRemoved="() => this.removeMeasureDimension(measure)"
onMeasureUpdated="(newMeasure) => this.updateMeasure(measure, newMeasure)"
generateMeasureId.bind="getMeasureId"
/>
</div>
</t>
</div>
<PivotSortSection definition="props.definition" pivotId="props.pivotId"/>
</t>
<t t-name="o-spreadsheet-PivotSortSection">
<Section t-if="hasValidSort" class="'o-pivot-sort'">
<t t-set-slot="title">Sorting</t>
<div t-esc="sortDescription" class="pb-2"/>
<div class="d-flex flex-column gap-2">
<t t-foreach="sortValuesAndFields" t-as="valueAndField" t-key="valueAndField_index">
<div class="o-sort-card d-flex gap-1 px-2">
<t t-if="valueAndField.field">
<span class="fw-bolder" t-esc="valueAndField.field"/>
=
</t>
<span class="fw-bolder o-sort-value" t-esc="valueAndField.value"/>
</div>
</t>
</div>
</Section>
</t>
<t t-name="o-spreadsheet-PivotMeasureEditor">
<t t-set="measure" t-value="props.measure"/>
<PivotDimension dimension="measure" onRemoved="props.onRemoved" onNameUpdated.bind="updateName">
<t t-set-slot="upper-right-icons">
<t t-if="measure.isHidden" t-set="hideTitle">Show</t>
<t t-else="" t-set="hideTitle">Hide</t>
<i
t-att-class="measure.isHidden ? 'fa fa-eye-slash': 'fa fa-eye'"
t-att-title="hideTitle"
class="o-button-icon pe-1 ps-2"
t-on-click="toggleMeasureVisibility"
/>
<i
class="o-button-icon pe-1 ps-2 fa fa-cog"
title="Show values as"
t-on-click="openShowValuesAs"
/>
</t>
<div t-if="measure.computedBy" class="d-flex flex-row small">
<div class="d-flex flex-column py-2 px-2 w-100" t-on-pointerdown.stop="">
<StandaloneComposer
onConfirm.bind="updateMeasureFormula"
composerContent="measure.computedBy.formula"
defaultRangeSheetId="measure.computedBy.sheetId"
contextualAutocomplete="getMeasureAutocomplete()"
getContextualColoredSymbolToken.bind="getColoredSymbolToken"
invalid="isCalculatedMeasureInvalid"
/>
</div>
</div>
<div class="d-flex flex-row">
<div class="d-flex py-1 px-2 w-100 small">
<div class="pivot-dim-operator-label">Aggregated by</div>
<select
class="o-input flex-grow-1"
t-on-change="(ev) => this.updateAggregator(ev.target.value)">
<option
t-foreach="Object.keys(props.aggregators[measure.type])"
t-as="agg"
t-key="agg"
t-att-value="agg"
t-att-selected="agg === measure.aggregator"
t-esc="props.aggregators[measure.type][agg]"
/>
<option
t-if="measure.computedBy"
t-att-value="''"
t-att-selected="'' === measure.aggregator">
Compute from totals
</option>
</select>
</div>
</div>
</PivotDimension>
</t>
<t t-name="o-spreadsheet-PivotDimensionOrder">
<div class="d-flex">
<div class="d-flex py-1 px-2 w-100 small">
<div class="pivot-dim-operator-label">Order by</div>
<select
class="o-input flex-grow-1"
t-on-change="(ev) => props.onUpdated(props.dimension, ev.target.value)">
<option value="asc" t-att-selected="props.dimension.order === 'asc'">Ascending</option>
<option value="desc" t-att-selected="props.dimension.order === 'desc'">Descending</option>
<option
t-if="props.dimension.type !== 'date'"
value=""
t-att-selected="props.dimension.order === undefined">
Unsorted
</option>
</select>
</div>
</div>
</t>
<t t-name="o-spreadsheet-PivotDimensionGranularity">
<div class="d-flex flex-row">
<div class="d-flex flex-row py-1 px-2 w-100 small">
<t t-set="granularityProps" t-value="props.dimension.granularity || 'month'"/>
<div class="pivot-dim-operator-label">Granularity</div>
<select
class="o-input flex-grow-1"
t-on-change="(ev) => props.onUpdated(props.dimension, ev.target.value)">
<option
t-foreach="props.allGranularities"
t-as="granularity"
t-key="granularity"
t-if="props.availableGranularities.has(granularity) || granularity === granularityProps"
t-att-value="granularity"
t-esc="periods[granularity]"
t-att-selected="granularity === granularityProps or (granularity === 'month' and !granularityProps)"
/>
</select>
</div>
</div>
</t>
<t t-name="o-spreadsheet-PivotDimension">
<div
class="py-1 px-2 d-flex flex-column shadow-sm pivot-dimension"
t-att-class="{'pivot-dimension-invalid': !props.dimension.isValid}">
<div class="d-flex flex-row justify-content-between align-items-center">
<div class="d-flex align-items-center overflow-hidden text-nowrap">
<span class="text-danger me-1" t-if="!props.dimension.isValid">
<t t-call="o-spreadsheet-Icon.TRIANGLE_EXCLAMATION"/>
</span>
<TextInput
t-if="props.onNameUpdated"
value="props.dimension.displayName"
onChange.bind="updateName"
class="'o-fw-bold'"
/>
<span t-else="1" class="o-fw-bold" t-esc="props.dimension.displayName"/>
</div>
<div class="d-flex flex-rows" t-on-pointerdown.stop="">
<t t-slot="upper-right-icons"/>
<i
class="o-button-icon fa fa-trash pe-1 ps-2"
t-if="props.onRemoved"
t-on-click="() => props.onRemoved(props.dimension)"
/>
</div>
</div>
<t t-slot="default"/>
</div>
</t>
<t t-name="o-spreadsheet-AddDimensionButton">
<button class="add-dimension o-button" t-on-click="togglePopover" t-ref="button">Add</button>
<Popover t-if="popover.isOpen" t-props="popoverProps">
<div
class="p-2 bg-white border-bottom d-flex sticky-top align-items-baseline pivot-dimension-search">
<i class="pe-1 pivot-dimension-search-field-icon text-muted">
<t t-call="o-spreadsheet-Icon.SEARCH"/>
</i>
<input
t-on-input="(ev) => this.updateSearch(ev.target.value)"
t-on-keydown="onKeyDown"
class="border-0 w-100 pivot-dimension-search-field"
t-ref="autofocus"
/>
</div>
<TextValueProvider
proposals="autoComplete.provider.proposals"
selectedIndex="autoComplete.selectedIndex"
onValueSelected="autoComplete.provider.selectProposal"
onValueHovered="() => {}"
/>
<t t-slot="default" t-on-click="togglePopover"/>
</Popover>
</t>
<t t-name="o-spreadsheet-PivotDeferUpdate">
<Section
class="'align-items-center border-top d-flex flex-row justify-content-between py-1 pivot-defer-update'">
<Checkbox
label="deferUpdatesLabel"
title="deferUpdatesTooltip"
value="props.deferUpdate"
onChange="(value) => props.toggleDeferUpdate(value)"
/>
<div t-if="props.isDirty" class="d-flex align-items-center">
<i
class="o-button-icon pe-0 fa fa-undo"
title="Discard all changes"
t-on-click="() => props.discard()"
/>
<span
class="o-button-link sp_apply_update small ps-2"
title="Apply all changes"
t-on-click="() => props.apply()">
Update
</span>
</div>
</Section>
</t>
<t t-name="o-spreadsheet-PivotCustomGroupsCollapsible">
<SidePanelCollapsible
class="'o-pivot-custom-groups'"
isInitiallyCollapsed="true"
title.translate="Groups">
<t t-set-slot="content">
<div class="ps-4">
<div
t-foreach="groups"
t-as="group"
t-key="group_index"
class="o-pivot-custom-group pb-1">
<div
class="d-flex align-items-center justify-content-between small"
t-on-pointerdown.stop="">
<TextInput
value="group.name"
onChange="(newName) => this.onRenameGroup(group_index, newName)"
/>
<i
class="o-button-icon ps-3 fa fa-trash"
t-on-click="() => this.onDeleteGroup(group_index)"
/>
</div>
</div>
<div
t-if="!hasOthersGroup"
class="o-button-link o-add-others-group small pb-1"
t-on-click="() => this.addOthersGroup()">
<span>+ "Others" group</span>
</div>
</div>
</t>
</SidePanelCollapsible>
</t>
<t t-name="o-spreadsheet-MoreFormatsPanel">
<div class="o-more-formats-panel">
<div
t-foreach="dateFormatsActions"
t-as="action"
t-key="action.name(env)"
t-att-data-name="action.name(env)"
t-on-click="() => action.execute(env)"
class="w-100 d-flex align-items-center border-bottom format-preview">
<span class="ms-3 check-icon">
<t t-if="action.isActive(env)" t-call="o-spreadsheet-Icon.CHECK"/>
</span>
<span t-out="action.description(env)"/>
</div>
</div>
</t>
<t t-name="o-spreadsheet-FindAndReplacePanel">
<div class="o-find-and-replace">
<Section title.translate="Search">
<div class="o-input-search-container">
<input
type="text"
t-ref="searchInput"
class="o-input o-input-with-count o-search"
t-on-input="onSearchInput"
t-on-focus="onFocusSearch"
t-on-keydown="onKeydownSearch"
placeholder="e.g. 'search me'"
/>
<div class="o-input-count" t-if="hasSearchResult">
<t t-esc="store.selectedMatchIndex+1"/>
/