@odoo/o-spreadsheet
Version:
A spreadsheet component
1,317 lines (1,263 loc) • 240 kB
text/xml
<!--
This file is generated by o-spreadsheet build tools. Do not edit it.
@see https://github.com/odoo/o-spreadsheet
@version 18.2.6
@date 2025-04-04T08:42:06.055Z
@hash faa00e2
-->
<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">
<t t-set="text_color">Text Color</t>
<t t-set="fill_color">Fill Color</t>
<div
class="o-spreadsheet-topbar o-two-columns d-flex flex-column user-select-none"
t-on-click="props.onClick">
<div 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 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">
<div class="o-topbar-toolbar d-flex flex-shrink-0">
<!-- 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 flex-shrink-0 ms-4">
<ActionButton action="EDIT.undo" class="'o-hoverable-button'"/>
<ActionButton action="EDIT.redo" class="'o-hoverable-button'"/>
<PaintFormatButton class="'o-hoverable-button'"/>
<ActionButton action="FORMAT.clearFormat" class="'o-hoverable-button'"/>
<div class="o-divider"/>
<ActionButton action="FORMAT.formatPercent" class="'o-hoverable-button'"/>
<ActionButton action="FORMAT.decraseDecimalPlaces" class="'o-hoverable-button'"/>
<ActionButton action="FORMAT.incraseDecimalPlaces" class="'o-hoverable-button'"/>
<ActionButton
action="formatNumberMenuItemSpec"
onClick="(ev) => this.toggleToolbarContextMenu(formatNumberMenuItemSpec, ev)"
hasTriangleDownIcon="true"
class="'o-hoverable-button'"
/>
<div class="o-divider"/>
<FontSizeEditor
currentFontSize="currentFontSize"
onFontSizeChanged.bind="this.setFontSize"
class="'o-hoverable-button'"
onToggle.bind="this.onClick"
/>
<div class="o-divider"/>
<ActionButton action="FORMAT.formatBold" class="'o-hoverable-button'"/>
<ActionButton action="FORMAT.formatItalic" class="'o-hoverable-button'"/>
<ActionButton action="FORMAT.formatStrikethrough" class="'o-hoverable-button'"/>
<ColorPickerWidget
currentColor="state.textColor"
toggleColorPicker="(ev) => this.toggleDropdownTool('textColorTool', ev)"
showColorPicker="state.activeTool === 'textColorTool'"
onColorPicked="(color) => this.setColor('textColor', color)"
title="text_color"
icon="'o-spreadsheet-Icon.TEXT_COLOR'"
dropdownMaxHeight="this.props.dropdownMaxHeight"
class="'o-hoverable-button o-menu-item-button'"
/>
<div class="o-divider"/>
<ColorPickerWidget
currentColor="state.fillColor"
toggleColorPicker="(ev) => this.toggleDropdownTool('fillColorTool', ev)"
showColorPicker="state.activeTool === 'fillColorTool'"
onColorPicked="(color) => this.setColor('fillColor', color)"
title="fill_color"
icon="'o-spreadsheet-Icon.FILL_COLOR'"
dropdownMaxHeight="this.props.dropdownMaxHeight"
class="'o-hoverable-button o-menu-item-button'"
/>
<BorderEditorWidget
class="'o-hoverable-button o-menu-item-button'"
toggleBorderEditor="(ev) => this.toggleDropdownTool('borderTool', ev)"
showBorderEditor="state.activeTool === 'borderTool'"
dropdownMaxHeight="this.props.dropdownMaxHeight"
/>
<ActionButton action="EDIT.mergeCells" class="'o-hoverable-button'"/>
<div class="o-divider"/>
<div class="o-dropdown">
<ActionButton
action="FORMAT.formatAlignmentHorizontal"
hasTriangleDownIcon="true"
t-on-click="(ev) => this.toggleDropdownTool('horizontalAlignTool', ev)"
class="'o-hoverable-button'"
/>
<div
class="o-dropdown-content"
t-if="state.activeTool === 'horizontalAlignTool'"
t-att-style="dropdownStyle"
t-on-click.stop="">
<div class="o-dropdown-line">
<ActionButton action="FORMAT.formatAlignmentLeft" class="'o-hoverable-button'"/>
<ActionButton action="FORMAT.formatAlignmentCenter" class="'o-hoverable-button'"/>
<ActionButton action="FORMAT.formatAlignmentRight" class="'o-hoverable-button'"/>
</div>
</div>
</div>
<div class="o-dropdown">
<ActionButton
action="FORMAT.formatAlignmentVertical"
hasTriangleDownIcon="true"
t-on-click="(ev) => this.toggleDropdownTool('verticalAlignTool', ev)"
class="'o-hoverable-button'"
/>
<div
class="o-dropdown-content"
t-att-style="dropdownStyle"
t-if="state.activeTool === 'verticalAlignTool'"
t-on-click.stop="">
<div class="o-dropdown-line">
<ActionButton action="FORMAT.formatAlignmentTop" class="'o-hoverable-button'"/>
<ActionButton action="FORMAT.formatAlignmentMiddle" class="'o-hoverable-button'"/>
<ActionButton action="FORMAT.formatAlignmentBottom" class="'o-hoverable-button'"/>
</div>
</div>
</div>
<div class="o-dropdown">
<ActionButton
action="FORMAT.formatWrapping"
hasTriangleDownIcon="true"
t-on-click="(ev) => this.toggleDropdownTool('textWrappingTool', ev)"
class="'o-hoverable-button'"
/>
<div
class="o-dropdown-content"
t-att-style="dropdownStyle"
t-if="state.activeTool === 'textWrappingTool'"
t-on-click.stop="">
<div class="o-dropdown-line">
<ActionButton
action="FORMAT.formatWrappingOverflow"
class="'o-hoverable-button'"
/>
<ActionButton action="FORMAT.formatWrappingWrap" class="'o-hoverable-button'"/>
<ActionButton action="FORMAT.formatWrappingClip" class="'o-hoverable-button'"/>
</div>
</div>
</div>
<div class="o-divider"/>
<TableDropdownButton/>
<ActionButton action="DATA.createRemoveFilterTool" class="'o-hoverable-button'"/>
</div>
</div>
<TopBarComposer/>
</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 align-items-center rounded-0 alert alert-info ps-0 py-0 my-0">
This tool analyzes formulas for patterns and highlights
inconsistencies. Irregularities may indicate potential errors in formula
structures, references or arguments.
<div class="btn btn-link align-self-end" t-on-click="() => this.fingerprints.disable()">
Turn off
</div>
</div>
</div>
</div>
<Menu
t-if="state.menuState.isOpen"
position="state.menuState.position"
menuItems="state.menuState.menuItems"
onClose="() => this.closeMenus()"
onMenuClicked="() => this.props.onClick()"
/>
</t>
<div t-name="o-spreadsheet-TextInput" class="w-100">
<input
t-ref="input"
class="os-input w-100"
type="text"
t-att-class="props.class"
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>
<Menu
t-if="menu.isOpen"
menuItems="menu.menuItems"
position="menu.position"
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">
<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-ref="spreadsheet" t-att-style="getStyle()">
<t t-if="env.isDashboard()">
<SpreadsheetDashboard/>
</t>
<t t-else="">
<TopBar onClick="() => this.focusGrid()" dropdownMaxHeight="gridHeight"/>
<div
class="o-grid-container"
t-att-class="{'o-two-columns': !sidePanel.isOpen}"
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"/>
</div>
</div>
<SidePanel/>
<BottomBar onClick="() => this.focusGrid()"/>
</t>
</div>
</t>
<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-SidePanel">
<div class="o-sidePanel" t-if="sidePanelStore.isOpen">
<div class="o-sidePanelHeader">
<div class="o-sidePanelTitle o-fw-bold" t-esc="getTitle()"/>
<div class="o-sidePanelClose" t-on-click="close">✕</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="startHandleDrag"
t-on-dblclick="sidePanelStore.resetPanelSize">
<t t-call="o-spreadsheet-Icon.THIN_DRAG_HANDLE"/>
</div>
</div>
<div class="o-sidePanelBody">
<t
t-component="panel.Body"
t-props="sidePanelStore.panelProps"
onCloseSidePanel.bind="close"
t-key="'Body_' + sidePanelStore.componentTag + sidePanelStore.panelKey"
/>
</div>
<div class="o-sidePanelFooter" t-if="panel?.Footer">
<t
t-component="panel.Footer"
t-props="sidePanelStore.panelProps"
t-key="'Footer_' + sidePanelStore.componentTag"
/>
</div>
</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">This setting affects 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>
<Menu
t-if="state.isMenuOpen"
menuItems="props.menuItems"
position="menuPosition"
onClose.bind="onMenuClosed"
menuId="menuId"
/>
</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"
t-att="isReadonly ? ['inert', 1] : []"
t-att-class="{ 'pe-none': isReadonly, 'opacity-50': isReadonly }">
<div class="h-100 position-relative overflow-x-hidden overflow-y-auto" t-ref="pivotSidePanel">
<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>
<PivotDeferUpdate
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.nameWithGranularity">
<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">
<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"/>
</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.nameWithGranularity">
<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">
<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"/>
</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"
/>
</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]"
/>
</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="granularity" t-value="props.dimension.granularity"/>
<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 === props.dimension.granularity"
t-att-value="granularity"
t-esc="periods[granularity]"
t-att-selected="granularity === props.dimension.granularity or (granularity === 'month' and !props.dimension.granularity)"
/>
</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-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"/>
/
<t t-esc="store.searchMatches.length"/>
</div>
<div t-elif="!this.pendingSearch and store.toSearch !== ''" class="o-input-count">
0 / 0
</div>
<div class="d-flex flex-row o-result-buttons align-items-center" t-if="hasSearchResult">
<button
t-on-click="() => store.selectPreviousMatch()"
class="o-button ms-2 d-flex justify-content-center align-items-center">
<t t-call="o-spreadsheet-Icon.ARROW_UP"/>
</button>
<button
t-on-click="() => store.selectNextMatch()"
class="o-button ms-1 d-flex justify-content-center align-items-center">
<t t-call="o-spreadsheet-Icon.ARROW_DOWN"/>
</button>
</div>
</div>
<select
class="o-input o-type-range-selector mt-3 mb-3"
t-on-change="changeSearchScope"
t-att-value="searchOptions.searchScope">
<option value="allSheets">All sheets</option>
<option value="activeSheet">Current sheet</option>
<option value="specificRange">Specific range</option>
</select>
<div t-if="searchOptions.searchScope === 'specificRange'">
<SelectionInput
ranges="[this.state.dataRange]"
onSelectionChanged="(ranges) => this.onSearchRangeChanged(ranges)"
onSelectionConfirmed.bind="updateDataRange"
hasSingleRange="true"
required="true"
/>
</div>
<div>
<t t-set="matchCaseLabel">Match case</t>
<Checkbox
value="searchOptions.matchCase"
label="matchCaseLabel"
onChange.bind="searchMatchCase"
className="'mb-1'"
/>
<t t-set="exactMatchLabel">Match entire cell content</t>
<Checkbox
value="searchOptions.exactMatch"
label="exactMatchLabel"
onChange.bind="searchExactMatch"
className="'mb-1'"
/>
<t t-set="searchFormulasLabel">Search in formulas</t>
<Checkbox
value="searchOptions.searchFormulas"
label="searchFormulasLabel"
onChange.bind="searchFormulas"
/>
</div>
<div class="o-matches-count mt-4" t-if="searchInfo.length">
<ValidationMessages msgType="'info'" messages="searchInfo" singleBox="true"/>
</div>
</Section>
<Section class="'pt-0'" t-if="!env.model.getters.isReadonly()" title.translate="Replace">
<div class="o-input-search-container">
<input
type="text"
class="o-input o-input-without-count o-replace"
t-on-keydown="onKeydownReplace"
t-model="store.toReplace"
placeholder="e.g. 'replace me'"
/>
</div>
</Section>
<Section>
<div class="o-sidePanelButtons" t-if="!env.model.getters.isReadonly()">
<button
t-att-disabled="store.selectedMatchIndex === null"
t-on-click="() => store.replace()"
class="o-button o-replace">
Replace
</button>
<button
t-att-disabled="store.selectedMatchIndex === null"
t-on-click="() => store.replaceAll()"
class="o-button o-replace-all">
Replace all
</button>
</div>
</Section>
</div>
</t>
<t t-name="o-spreadsheet-DataValidationPanel">
<div class="o-data-validation">
<t t-if="state.mode === 'list'">
<div class="o-dv-preview-list">
<t t-foreach="validationRules" t-as="rule" t-key="rule.id">
<DataValidationPreview
rule="localizeDVRule(rule)"
onClick="() => this.onPreviewClick(rule.id)"
/>
</t>
</div>
<div class="o-dv-add o-button-link p-4 float-end" t-on-click="addDataValidationRule">
+ Add another rule
</div>
</t>
<t t-else="">
<DataValidationEditor rule="localizeDVRule(state.activeRule)" onExit.bind="onExitEditMode"/>
</t>
</div>
</t>
<t t-name="o-spreadsheet-DataValidationPreview">
<div class="o-dv-preview p-3" t-on-click="props.onClick" t-ref="dvPreview">
<div class="d-flex justify-content-between">
<div class="o-dv-container d-flex flex-column">
<div class="o-dv-preview-description o-fw-bold text-truncate" t-esc="descriptionString"/>
<div class="o-dv-preview-ranges text-truncate" t-esc="rangesString"/>
</div>
<div
class="o-dv-preview-delete d-flex align-items-center o-button-icon px-3"
t-on-click.stop="deleteDataValidation">
<t t-call="o-spreadsheet-Icon.TRASH_FILLED"/>
</div>
</div>
</div>
</t>
<t t-name="o-spreadsheet-DataValidationEditor">
<div class="o-dv-form w-100 h-100">
<Section class="'o-dv-range'" title.translate="Apply to range">
<SelectionInput
ranges="state.rule.ranges"
onSelectionChanged="(ranges) => this.onRangesChanged(ranges)"
required="true"
/>
</Section>
<Section class="'pt-0'">
<div class="o-subsection o-dv-settings">
<div class="o-section-title">Criteria</div>
<SelectMenu
class="'o-dv-type o-input mb-2'"
menuItems="dvCriterionMenuItems"
selectedValue="selectedCriterionName"
/>
<t
t-if="criterionComponent"