From dfb88e4fa2e8c73fd032a5ad0a51bd543917bad8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Rahir?= Date: Tue, 10 Jan 2023 08:40:25 +0000 Subject: [PATCH] [IMP] spreadsheet: update o_spreadsheet to latest version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://github.com/odoo/o-spreadsheet/commit/fc313d38 [IMP] images: delete images from the file store https://github.com/odoo/o-spreadsheet/commit/d4e45186 [REF] config: move data external dependencies to config https://github.com/odoo/o-spreadsheet/commit/3f6cd0f5 [FIX] headers: fix rounding error in header size https://github.com/odoo/o-spreadsheet/commit/ac07e847 [FIX] functions: fix error message https://github.com/odoo/o-spreadsheet/commit/e068c651 [IMP] clipboard: copy cell as HTML in OS clipboard closes odoo/odoo#109497 Related: odoo/enterprise#35607 Signed-off-by: Rémi Rahir (rar) --- .../actions/spreadsheet_download_action.js | 2 +- .../chart/plugins/odoo_chart_core_plugin.js | 2 +- .../static/src/currency/plugins/currency.js | 2 +- .../plugins/global_filters_ui_plugin.js | 2 +- .../src/ir_ui_menu/ir_ui_menu_plugin.js | 2 +- .../src/list/plugins/list_core_plugin.js | 2 +- .../static/src/list/plugins/list_ui_plugin.js | 2 +- .../static/src/o_spreadsheet/o_spreadsheet.js | 304 +++++++++++++----- .../src/pivot/plugins/pivot_core_plugin.js | 2 +- .../charts/model/link_chart_plugin_test.js | 8 +- .../global_filters_model_test.js | 4 +- .../tests/links/model/link_menu_cells_test.js | 12 +- .../static/tests/links/open_links_tests.js | 6 +- .../spreadsheet/static/tests/utils/chart.js | 2 +- addons/spreadsheet/static/tests/utils/list.js | 2 +- .../spreadsheet/static/tests/utils/model.js | 6 +- .../spreadsheet/static/tests/utils/pivot.js | 4 +- addons/spreadsheet/static/tests/utils/ui.js | 2 +- .../static/src/plugins/accounting_plugin.js | 2 +- .../tests/ui/accounting_drilldown_tests.js | 2 +- .../dashboard_action/dashboard_loader.js | 2 +- 21 files changed, 266 insertions(+), 106 deletions(-) diff --git a/addons/spreadsheet/static/src/actions/spreadsheet_download_action.js b/addons/spreadsheet/static/src/actions/spreadsheet_download_action.js index 7eb1fe08c39..e2199b7fa72 100644 --- a/addons/spreadsheet/static/src/actions/spreadsheet_download_action.js +++ b/addons/spreadsheet/static/src/actions/spreadsheet_download_action.js @@ -13,7 +13,7 @@ const { Model } = spreadsheet; async function downloadSpreadsheet(env, action) { const { orm, name, data, stateUpdateMessages } = action.params; const dataSources = new DataSources(orm); - const model = new Model(migrate(data), { external: { dataSources } }, stateUpdateMessages); + const model = new Model(migrate(data), { custom: { dataSources } }, stateUpdateMessages); await dataSources.waitForAllLoaded(); await waitForDataLoaded(model); const { files } = model.exportXLSX(); diff --git a/addons/spreadsheet/static/src/chart/plugins/odoo_chart_core_plugin.js b/addons/spreadsheet/static/src/chart/plugins/odoo_chart_core_plugin.js index 51a78e0cb7c..e5181b46244 100644 --- a/addons/spreadsheet/static/src/chart/plugins/odoo_chart_core_plugin.js +++ b/addons/spreadsheet/static/src/chart/plugins/odoo_chart_core_plugin.js @@ -20,7 +20,7 @@ const { CorePlugin } = spreadsheet; export default class OdooChartCorePlugin extends CorePlugin { constructor(config) { super(config); - this.dataSources = config.external.dataSources; + this.dataSources = config.custom.dataSources; /** @type {Object.} */ this.charts = {}; diff --git a/addons/spreadsheet/static/src/currency/plugins/currency.js b/addons/spreadsheet/static/src/currency/plugins/currency.js index 084547730fb..7b152350942 100644 --- a/addons/spreadsheet/static/src/currency/plugins/currency.js +++ b/addons/spreadsheet/static/src/currency/plugins/currency.js @@ -13,7 +13,7 @@ const DATA_SOURCE_ID = "CURRENCIES"; class CurrencyPlugin extends spreadsheet.UIPlugin { constructor(config) { super(config); - this.dataSources = config.external.dataSources; + this.dataSources = config.custom.dataSources; if (this.dataSources) { this.dataSources.add(DATA_SOURCE_ID, CurrencyDataSource); } diff --git a/addons/spreadsheet/static/src/global_filters/plugins/global_filters_ui_plugin.js b/addons/spreadsheet/static/src/global_filters/plugins/global_filters_ui_plugin.js index 32979207deb..71c0e178f39 100644 --- a/addons/spreadsheet/static/src/global_filters/plugins/global_filters_ui_plugin.js +++ b/addons/spreadsheet/static/src/global_filters/plugins/global_filters_ui_plugin.js @@ -45,7 +45,7 @@ const uuidGenerator = new UuidGenerator(); export default class GlobalFiltersUIPlugin extends spreadsheet.UIPlugin { constructor(config) { super(config); - this.orm = config.external.env ? config.external.env.services.orm : undefined; + this.orm = config.custom.env ? config.custom.env.services.orm : undefined; /** * Cache record display names for relation filters. * For each filter, contains a promise resolving to diff --git a/addons/spreadsheet/static/src/ir_ui_menu/ir_ui_menu_plugin.js b/addons/spreadsheet/static/src/ir_ui_menu/ir_ui_menu_plugin.js index 7b8d85f999b..324893a6970 100644 --- a/addons/spreadsheet/static/src/ir_ui_menu/ir_ui_menu_plugin.js +++ b/addons/spreadsheet/static/src/ir_ui_menu/ir_ui_menu_plugin.js @@ -5,7 +5,7 @@ const { CorePlugin } = spreadsheet; export default class IrMenuPlugin extends CorePlugin { constructor(config) { super(config); - this.env = config.external.env; + this.env = config.custom.env; } /** diff --git a/addons/spreadsheet/static/src/list/plugins/list_core_plugin.js b/addons/spreadsheet/static/src/list/plugins/list_core_plugin.js index 9ff429d6a44..7dad87dbc44 100644 --- a/addons/spreadsheet/static/src/list/plugins/list_core_plugin.js +++ b/addons/spreadsheet/static/src/list/plugins/list_core_plugin.js @@ -34,7 +34,7 @@ const { CorePlugin } = spreadsheet; export default class ListCorePlugin extends CorePlugin { constructor(config) { super(config); - this.dataSources = config.external.dataSources; + this.dataSources = config.custom.dataSources; this.nextId = 1; /** @type {Object.} */ diff --git a/addons/spreadsheet/static/src/list/plugins/list_ui_plugin.js b/addons/spreadsheet/static/src/list/plugins/list_ui_plugin.js index 7be05a16971..516b6157e63 100644 --- a/addons/spreadsheet/static/src/list/plugins/list_ui_plugin.js +++ b/addons/spreadsheet/static/src/list/plugins/list_ui_plugin.js @@ -15,7 +15,7 @@ export default class ListUIPlugin extends spreadsheet.UIPlugin { super(config); /** @type {string} */ this.selectedListId = undefined; - this.env = config.external.env; + this.env = config.custom.env; } beforeHandle(cmd) { diff --git a/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js b/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js index 55921df7eca..d967c95d155 100644 --- a/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js +++ b/addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js @@ -723,6 +723,14 @@ function computeTextLinesHeight(textLineHeight, numberOfLines = 1) { return numberOfLines * (textLineHeight + MIN_CELL_TEXT_MARGIN) - MIN_CELL_TEXT_MARGIN; } + /** + * Get the default height of the cell given its style. + */ + function getDefaultCellHeight(style) { + // TO DO: take multi text line into account to compute the real cell height in case of wrapping cell + const fontSize = computeTextFontSizeInPixels(style); + return computeTextLinesHeight(fontSize) + 2 * PADDING_AUTORESIZE_VERTICAL; + } function computeTextWidth(context, text, style) { context.save(); context.font = computeTextFont(style); @@ -3568,6 +3576,12 @@ CellValueType["error"] = "error"; })(CellValueType || (CellValueType = {})); + var ClipboardMIMEType; + (function (ClipboardMIMEType) { + ClipboardMIMEType["PlainText"] = "text/plain"; + ClipboardMIMEType["Html"] = "text/html"; + })(ClipboardMIMEType || (ClipboardMIMEType = {})); + function isSheetDependent(cmd) { return "sheetId" in cmd; } @@ -4216,6 +4230,18 @@ } return `${strikethrough ? "line-through" : ""} ${underline ? "underline" : ""}`; } + /** + * Convert the cell style to CSS properties. + */ + function cellStyleToCss(style) { + const attributes = cellTextStyleToCss(style); + if (!style) + return attributes; + if (style.fillColor) { + attributes["background"] = style.fillColor; + } + return attributes; + } /** * Convert the cell text style to CSS properties. */ @@ -4239,11 +4265,12 @@ } return attributes; } - function cssPropertiesToCss(attributes) { + function cssPropertiesToCss(attributes, newLine = true) { + const separator = newLine ? "\n" : ""; const str = Object.entries(attributes) .map(([attName, attValue]) => `${attName}: ${attValue};`) - .join("\n"); - return "\n" + str + "\n"; + .join(separator); + return str ? "\n" + str + "\n" : ""; } const ERROR_TOOLTIP_MAX_HEIGHT = 80; @@ -7837,17 +7864,6 @@ style, }); } - async function readOsClipboard(env) { - try { - return await env.clipboard.readText(); - } - catch (e) { - // Permission is required to read the clipboard. - console.warn("The OS clipboard could not be read."); - console.error(e); - return undefined; - } - } //------------------------------------------------------------------------------ // Simple actions //------------------------------------------------------------------------------ @@ -7855,40 +7871,35 @@ const REDO_ACTION = (env) => env.model.dispatch("REQUEST_REDO"); const COPY_ACTION = async (env) => { env.model.dispatch("COPY"); - await env.clipboard.writeText(env.model.getters.getClipboardContent()); + await env.clipboard.write(env.model.getters.getClipboardContent()); }; const CUT_ACTION = async (env) => { interactiveCut(env); - await env.clipboard.writeText(env.model.getters.getClipboardContent()); + await env.clipboard.write(env.model.getters.getClipboardContent()); }; - const PASTE_ACTION = async (env) => { - const spreadsheetClipboard = env.model.getters.getClipboardContent(); - const osClipboard = await readOsClipboard(env); - const target = env.model.getters.getSelectedZones(); - if (osClipboard && osClipboard !== spreadsheetClipboard) { - interactivePasteFromOS(env, target, osClipboard); + const PASTE_ACTION = async (env) => paste(env); + const PASTE_VALUE_ACTION = async (env) => paste(env, "onlyValue"); + async function paste(env, pasteOption) { + const spreadsheetClipboard = env.model.getters.getClipboardTextContent(); + const osClipboard = await env.clipboard.readText(); + switch (osClipboard.status) { + case "ok": + const target = env.model.getters.getSelectedZones(); + if (osClipboard && osClipboard.content !== spreadsheetClipboard) { + interactivePasteFromOS(env, target, osClipboard.content); + } + else { + interactivePaste(env, target, pasteOption); + } + break; + case "notImplemented": + env.raiseError(_lt("Pasting from the context menu is not supported in this browser. Use keyboard shortcuts ctrl+c / ctrl+v instead.")); + break; + case "permissionDenied": + env.raiseError(_lt("Access to the clipboard denied by the browser. Please enable clipboard permission for this page in your browser settings.")); + break; } - else { - interactivePaste(env, target); - } - }; - const PASTE_VALUE_ACTION = async (env) => { - const spreadsheetClipboard = env.model.getters.getClipboardContent(); - const osClipboard = await readOsClipboard(env); - const target = env.model.getters.getSelectedZones(); - if (osClipboard && osClipboard !== spreadsheetClipboard) { - env.model.dispatch("PASTE_FROM_OS_CLIPBOARD", { - target, - text: osClipboard, - }); - } - else { - env.model.dispatch("PASTE", { - target: env.model.getters.getSelectedZones(), - pasteOption: "onlyValue", - }); - } - }; + } const PASTE_FORMAT_ACTION = (env) => interactivePaste(env, env.model.getters.getSelectedZones(), "onlyFormat"); const DELETE_CONTENT_ACTION = (env) => env.model.dispatch("DELETE_CONTENT", { sheetId: env.model.getters.getActiveSheetId(), @@ -9126,6 +9137,7 @@ name: NumberFormatTerms.CustomCurrency, sequence: 39, separator: true, + isVisible: (env) => env.loadCurrencies !== undefined, action: OPEN_CUSTOM_CURRENCY_SIDEPANEL_ACTION, }) .addChild("format_number_date", ["format", "format_number"], { @@ -11639,7 +11651,7 @@ action: async () => { this.env.model.dispatch("SELECT_FIGURE", { id: this.props.figure.id }); this.env.model.dispatch("COPY"); - await this.env.clipboard.writeText(this.env.model.getters.getClipboardContent()); + await this.env.clipboard.clear(); }, }); registry.add("cut", { @@ -11648,7 +11660,7 @@ action: async () => { this.env.model.dispatch("SELECT_FIGURE", { id: this.props.figure.id }); this.env.model.dispatch("CUT"); - await this.env.clipboard.writeText(this.env.model.getters.getClipboardContent()); + await this.env.clipboard.clear(); }, }); registry.add("delete", { @@ -15341,7 +15353,7 @@ assert(() => pv > 0, _lt("The present value (%s) must be strictly positive.", pv.toString())); } function assertPeriodSmallerOrEqualToLife(period, life) { - assert(() => period <= life, _lt("The period (%s) must be less than or equal life (%.", period.toString(), life.toString())); + assert(() => period <= life, _lt("The period (%s) must be less than or equal life (%s).", period.toString(), life.toString())); } function assertInvestmentStrictlyPositive(investment) { assert(() => investment > 0, _lt("The investment (%s) must be strictly positive.", investment.toString())); @@ -23510,6 +23522,11 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A if (!this.gridEl.contains(document.activeElement)) { return; } + const clipboardData = ev.clipboardData; + if (!clipboardData) { + this.displayWarningCopyPasteNotSupported(); + return; + } /* If we are currently editing a cell, let the default behavior */ if (this.env.model.getters.getEditionMode() !== "inactive") { return; @@ -23521,7 +23538,9 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A this.env.model.dispatch("COPY"); } const content = this.env.model.getters.getClipboardContent(); - ev.clipboardData.setData("text/plain", content); + for (const type in content) { + clipboardData.setData(type, content[type]); + } ev.preventDefault(); } paste(ev) { @@ -23529,11 +23548,15 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A return; } const clipboardData = ev.clipboardData; - if (clipboardData.types.indexOf("text/plain") > -1) { - const content = clipboardData.getData("text/plain"); + if (!clipboardData) { + this.displayWarningCopyPasteNotSupported(); + return; + } + if (clipboardData.types.indexOf(ClipboardMIMEType.PlainText) > -1) { + const content = clipboardData.getData(ClipboardMIMEType.PlainText); const target = this.env.model.getters.getSelectedZones(); - const clipBoardString = this.env.model.getters.getClipboardContent(); - if (clipBoardString === content) { + const clipboardString = this.env.model.getters.getClipboardTextContent(); + if (clipboardString === content) { // the paste actually comes from o-spreadsheet itself interactivePaste(this.env, target); } @@ -23542,6 +23565,9 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A } } } + displayWarningCopyPasteNotSupported() { + this.env.raiseError(_lt("Copy/Paste is not supported in this browser.")); + } closeMenu() { this.menuState.isOpen = false; this.focus(); @@ -25467,8 +25493,8 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A function formatAttributes(attrs) { return new XMLString(attrs.map(([key, val]) => `${key}="${xmlEscape(val)}"`).join(" ")); } - function parseXML(xmlString) { - const document = new DOMParser().parseFromString(xmlString.toString(), "text/xml"); + function parseXML(xmlString, mimeType = "text/xml") { + const document = new DOMParser().parseFromString(xmlString.toString(), mimeType); const parserError = document.querySelector("parsererror"); if (parserError) { const errorString = parserError.innerHTML; @@ -27213,6 +27239,11 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A * @param sheetId an optional sheetId to adapt either range of that sheet specifically, or ranges pointing to that sheet */ adaptRanges(applyChange, sheetId) { } + /** + * Implement this method to clean unused external resources, such as images + * stored on a server which have been deleted. + */ + garbageCollectExternalResources() { } } /** @@ -29233,7 +29264,7 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A } getHeaderSize(sheetId, dimension, index) { var _a, _b, _c, _d; - return (((_b = (_a = this.sizes[sheetId]) === null || _a === void 0 ? void 0 : _a[dimension][index]) === null || _b === void 0 ? void 0 : _b.manualSize) || + return Math.round(((_b = (_a = this.sizes[sheetId]) === null || _a === void 0 ? void 0 : _a[dimension][index]) === null || _b === void 0 ? void 0 : _b.manualSize) || ((_d = (_c = this.sizes[sheetId]) === null || _c === void 0 ? void 0 : _c[dimension][index]) === null || _d === void 0 ? void 0 : _d.computedSize()) || this.getDefaultHeaderSize(dimension)); } @@ -29266,9 +29297,7 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A return DEFAULT_CELL_HEIGHT; } const cell = this.getters.getCell(position); - // TO DO: take multi text line into account to compute the real cell height in case of wrapping cell - const fontSize = computeTextFontSizeInPixels(cell === null || cell === void 0 ? void 0 : cell.style); - return computeTextLinesHeight(fontSize) + 2 * PADDING_AUTORESIZE_VERTICAL; + return getDefaultCellHeight(cell === null || cell === void 0 ? void 0 : cell.style); } /** * Get the tallest cell of a row and its size. @@ -29540,7 +29569,7 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A action: async () => { this.env.model.dispatch("SELECT_FIGURE", { id: this.figureId }); this.env.model.dispatch("COPY"); - await this.env.clipboard.writeText(this.env.model.getters.getClipboardContent()); + await this.env.clipboard.clear(); }, }); registry.add("cut", { @@ -29550,7 +29579,7 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A action: async () => { this.env.model.dispatch("SELECT_FIGURE", { id: this.figureId }); this.env.model.dispatch("CUT"); - await this.env.clipboard.writeText(this.env.model.getters.getClipboardContent()); + await this.env.clipboard.clear(); }, }); registry.add("reset_size", { @@ -29618,10 +29647,15 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A }; class ImagePlugin extends CorePlugin { - constructor() { - super(...arguments); + constructor(config) { + super(config); this.images = {}; + /** + * paths of images synced with the file store server. + */ + this.syncedImages = new Set(); this.nextId = 1; + this.fileStore = config.external.fileStore; } // --------------------------------------------------------------------------- // Command Handling @@ -29642,6 +29676,7 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A case "CREATE_IMAGE": this.addFigure(cmd.figureId, cmd.sheetId, cmd.position, cmd.size); this.history.update("images", cmd.sheetId, cmd.figureId, cmd.definition); + this.syncedImages.add(cmd.definition.path); break; case "DUPLICATE_SHEET": { const sheetFiguresFrom = this.getters.getFigures(cmd.sheetId); @@ -29672,6 +29707,18 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A break; } } + /** + * Delete unused images from the file store + */ + garbageCollectExternalResources() { + var _a; + const images = new Set(this.getAllImages().map((image) => image.path)); + for (const path of this.syncedImages) { + if (!images.has(path)) { + (_a = this.fileStore) === null || _a === void 0 ? void 0 : _a.delete(path); + } + } + } // --------------------------------------------------------------------------- // Getters // --------------------------------------------------------------------------- @@ -29709,6 +29756,7 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A for (const image of images) { this.history.update("nextId", this.nextId + 1); this.history.update("images", sheet.id, image.id, image.data); + this.syncedImages.add(image.data.path); } } } @@ -29721,6 +29769,13 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A } } } + getAllImages() { + const images = []; + for (const sheetId in this.images) { + images.push(...Object.values(this.images[sheetId] || {}).filter(isDefined$1)); + } + return images; + } } ImagePlugin.getters = ["getImage", "getImagePath", "getImageSize"]; figureRegistry.add("image", { @@ -31532,7 +31587,7 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A super(config); this.isUpToDate = false; this.evaluatedCells = {}; - this.evalContext = config.external; + this.evalContext = config.custom; this.lazyEvaluation = config.lazyEvaluation; } // --------------------------------------------------------------------------- @@ -36712,6 +36767,7 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A const position = { col, row, sheetId }; cellsInRow.push({ cell: getters.getCell(position), + style: getters.getCellComputedStyle(position), evaluatedCell: getters.getEvaluatedCell(position), border: getters.getCellBorder(position) || undefined, position, @@ -37062,6 +37118,12 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A } } getClipboardContent() { + return { + [ClipboardMIMEType.PlainText]: this.getPlainTextContent(), + [ClipboardMIMEType.Html]: this.getHTMLContent(), + }; + } + getPlainTextContent() { return (this.cells .map((cells) => { return cells @@ -37070,6 +37132,23 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A }) .join("\n") || "\t"); } + getHTMLContent() { + if (this.cells.length == 1 && this.cells[0].length == 1) { + return this.getters.getCellText(this.cells[0][0].position); + } + let htmlTable = ''; + for (const row of this.cells) { + htmlTable += ""; + for (const cell of row) { + const cssStyle = cssPropertiesToCss(cellStyleToCss(cell.style), false); + const cellText = this.getters.getCellText(cell.position); + htmlTable += `"; + } + htmlTable += ""; + } + htmlTable += "
` + xmlEscape(cellText) + "
"; + return htmlTable; + } isColRowDirtyingClipboard(position, dimension) { if (!this.zones) return false; @@ -37160,7 +37239,7 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A this.dispatch("SELECT_FIGURE", { id: newId }); } getClipboardContent() { - return "\t"; + return { [ClipboardMIMEType.PlainText]: "\t" }; } isColRowDirtyingClipboard(position, dimension) { return false; @@ -37250,7 +37329,9 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A this.selection.selectZone({ cell: { col: activeCol, row: activeRow }, zone }); } getClipboardContent() { - return this.values.map((values) => values.join("\t")).join("\n"); + return { + [ClipboardMIMEType.PlainText]: this.values.map((values) => values.join("\t")).join("\n"), + }; } getPasteZone(target) { const height = this.values.length; @@ -37407,7 +37488,11 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A */ getClipboardContent() { var _a; - return ((_a = this.state) === null || _a === void 0 ? void 0 : _a.getClipboardContent()) || "\t"; + return ((_a = this.state) === null || _a === void 0 ? void 0 : _a.getClipboardContent()) || { [ClipboardMIMEType.PlainText]: "\t" }; + } + getClipboardTextContent() { + var _a; + return ((_a = this.state) === null || _a === void 0 ? void 0 : _a.getClipboardContent()[ClipboardMIMEType.PlainText]) || "\t"; } isCutOperation() { return this.state ? this.state.operation === "CUT" : false; @@ -37485,7 +37570,12 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A } } ClipboardPlugin.layers = [2 /* LAYERS.Clipboard */]; - ClipboardPlugin.getters = ["getClipboardContent", "isCutOperation", "isPaintingFormat"]; + ClipboardPlugin.getters = [ + "getClipboardContent", + "getClipboardTextContent", + "isCutOperation", + "isPaintingFormat", + ]; const selectionStatisticFunctions = [ { @@ -39296,6 +39386,66 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A onComposerContentFocused: Function, }; + function instantiateClipboard() { + return new WebClipboardWrapper(navigator.clipboard); + } + class WebClipboardWrapper { + // Can be undefined because navigator.clipboard doesn't exist in old browsers + constructor(clipboard) { + this.clipboard = clipboard; + } + async write(clipboardContent) { + var _a; + try { + (_a = this.clipboard) === null || _a === void 0 ? void 0 : _a.write(this.getClipboardItems(clipboardContent)); + } + catch (e) { } + } + async writeText(text) { + var _a; + try { + (_a = this.clipboard) === null || _a === void 0 ? void 0 : _a.writeText(text); + } + catch (e) { } + } + async readText() { + let permissionResult = undefined; + try { + //@ts-ignore - clipboard-read is not implemented in all browsers + permissionResult = await navigator.permissions.query({ name: "clipboard-read" }); + } + catch (e) { } + try { + const clipboardContent = await this.clipboard.readText(); + return { status: "ok", content: clipboardContent }; + } + catch (e) { + const status = (permissionResult === null || permissionResult === void 0 ? void 0 : permissionResult.state) === "denied" ? "permissionDenied" : "notImplemented"; + return { status }; + } + } + async clear() { + var _a; + try { + (_a = this.clipboard) === null || _a === void 0 ? void 0 : _a.write([]); + } + catch (e) { } + } + getClipboardItems(content) { + return [ + new ClipboardItem({ + [ClipboardMIMEType.PlainText]: this.getBlob(content, ClipboardMIMEType.PlainText), + [ClipboardMIMEType.Html]: this.getBlob(content, ClipboardMIMEType.Html), + }), + ]; + } + getBlob(clipboardContent, type) { + return new Blob([clipboardContent[type] || ""], { + type, + }); + } + } + css /* scss */ ` .o-spreadsheet { position: relative; @@ -39392,14 +39542,16 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A "CTRL+H": () => this.toggleSidePanel("FindAndReplace", {}), "CTRL+F": () => this.toggleSidePanel("FindAndReplace", {}), }; + const fileStore = this.model.config.external.fileStore; owl.useSubEnv({ model: this.model, - imageProvider: this.props.fileStore ? new ImageProvider(this.props.fileStore) : undefined, + imageProvider: fileStore ? new ImageProvider(fileStore) : undefined, + loadCurrencies: this.model.config.external.loadCurrencies, isDashboard: () => this.model.getters.isDashboard(), openSidePanel: this.openSidePanel.bind(this), toggleSidePanel: this.toggleSidePanel.bind(this), _t: Spreadsheet._t, - clipboard: navigator.clipboard, + clipboard: this.env.clipboard || instantiateClipboard(), }); owl.useExternalListener(window, "resize", () => this.render(true)); owl.useExternalListener(window, "beforeunload", this.unbindModelEvents.bind(this)); @@ -39535,7 +39687,6 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A Spreadsheet._t = t; Spreadsheet.props = { model: Object, - fileStore: { type: Object, optional: true }, }; class LocalTransportService { @@ -43046,6 +43197,7 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A this.joinSession(); if (config.snapshotRequested) { this.session.snapshot(this.exportData()); + this.garbageCollectExternalResources(); } // mark all models as "raw", so they will not be turned into reactive objects // by owl, since we do not rely on reactivity @@ -43136,6 +43288,7 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A return { ...config, mode: config.mode || "normal", + custom: config.custom || {}, external: config.external || {}, transportService, client, @@ -43152,6 +43305,7 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A range: this.range, dispatch: this.dispatchFromCorePlugin, uuidGenerator: this.uuidGenerator, + custom: this.config.custom, external: this.config.external, }; } @@ -43162,7 +43316,7 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A dispatch: this.dispatch, selection: this.selection, moveClient: this.session.move.bind(this.session), - external: this.config.external, + custom: this.config.custom, uiActions: this.config, lazyEvaluation: this.config.lazyEvaluation, }; @@ -43241,6 +43395,7 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A if (mode !== "normal") { this.dispatch("STOP_EDITION", { cancel: true }); } + // @ts-ignore For testing purposes only this.config.mode = mode; this.trigger("update"); } @@ -43264,6 +43419,11 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A data = JSON.parse(JSON.stringify(data)); return getXLSX(data); } + garbageCollectExternalResources() { + for (const plugin of this.corePlugins) { + plugin.garbageCollectExternalResources(); + } + } } /** @@ -43399,8 +43559,8 @@ day_count_convention (number, default=${DEFAULT_DAY_COUNT_CONVENTION} ) ${_lt("A Object.defineProperty(exports, '__esModule', { value: true }); exports.__info__.version = '2.0.0'; - exports.__info__.date = '2023-01-04T17:31:52.940Z'; - exports.__info__.hash = 'f88b16b'; + exports.__info__.date = '2023-01-10T08:40:22.199Z'; + exports.__info__.hash = 'fc313d3'; })(this.o_spreadsheet = this.o_spreadsheet || {}, owl); //# sourceMappingURL=o_spreadsheet.js.map diff --git a/addons/spreadsheet/static/src/pivot/plugins/pivot_core_plugin.js b/addons/spreadsheet/static/src/pivot/plugins/pivot_core_plugin.js index 0bce164c34a..d22b2184d54 100644 --- a/addons/spreadsheet/static/src/pivot/plugins/pivot_core_plugin.js +++ b/addons/spreadsheet/static/src/pivot/plugins/pivot_core_plugin.js @@ -39,7 +39,7 @@ const { CorePlugin } = spreadsheet; export default class PivotCorePlugin extends CorePlugin { constructor(config) { super(config); - this.dataSources = config.external.dataSources; + this.dataSources = config.custom.dataSources; this.nextId = 1; /** @type {Object.} */ diff --git a/addons/spreadsheet/static/tests/charts/model/link_chart_plugin_test.js b/addons/spreadsheet/static/tests/charts/model/link_chart_plugin_test.js index 0fd6ba4d60a..fac5a488c44 100644 --- a/addons/spreadsheet/static/tests/charts/model/link_chart_plugin_test.js +++ b/addons/spreadsheet/static/tests/charts/model/link_chart_plugin_test.js @@ -124,7 +124,7 @@ QUnit.module( "Links between charts and ir.menus are correctly imported/exported", async function (assert) { const env = await makeTestEnv({ serverData: this.serverData }); - const model = new Model({}, { external: { env } }); + const model = new Model({}, { custom: { env } }); createBasicChart(model, chartId); model.dispatch("LINK_ODOO_MENU_TO_CHART", { chartId, @@ -136,7 +136,7 @@ QUnit.module( 1, "Link to odoo menu is exported" ); - const importedModel = new Model(exportedData, { external: { env } }); + const importedModel = new Model(exportedData, { custom: { env } }); const chartMenu = importedModel.getters.getChartOdooMenu(chartId); assert.equal(chartMenu.id, 1, "Link to odoo menu is imported"); } @@ -144,7 +144,7 @@ QUnit.module( QUnit.test("Can undo-redo a LINK_ODOO_MENU_TO_CHART", async function (assert) { const env = await makeTestEnv({ serverData: this.serverData }); - const model = new Model({}, { external: { env } }); + const model = new Model({}, { custom: { env } }); createBasicChart(model, chartId); model.dispatch("LINK_ODOO_MENU_TO_CHART", { chartId, @@ -159,7 +159,7 @@ QUnit.module( QUnit.test("link is removed when figure is deleted", async function (assert) { const env = await makeTestEnv({ serverData: this.serverData }); - const model = new Model({}, { external: { env } }); + const model = new Model({}, { custom: { env } }); createBasicChart(model, chartId); model.dispatch("LINK_ODOO_MENU_TO_CHART", { chartId, diff --git a/addons/spreadsheet/static/tests/global_filters/global_filters_model_test.js b/addons/spreadsheet/static/tests/global_filters/global_filters_model_test.js index 9a9e89eb415..74fa6e15dab 100644 --- a/addons/spreadsheet/static/tests/global_filters/global_filters_model_test.js +++ b/addons/spreadsheet/static/tests/global_filters/global_filters_model_test.js @@ -378,7 +378,7 @@ QUnit.module("spreadsheet > Global filters model", {}, () => { assert.equal(listDomain.length, 7, "it should have updated the list domain"); const newModel = new Model(model.exportData(), { - external: model.config.external, + custom: model.config.custom, }); assert.equal(newModel.getters.getGlobalFilters().length, 2); @@ -638,7 +638,7 @@ QUnit.module("spreadsheet > Global filters model", {}, () => { const model = new Model( {}, { - external: { + custom: { dataSources: new DataSources({ ...orm, silent: orm }), env: { services: { orm } }, }, diff --git a/addons/spreadsheet/static/tests/links/model/link_menu_cells_test.js b/addons/spreadsheet/static/tests/links/model/link_menu_cells_test.js index d12901e1620..de7ff0a1418 100644 --- a/addons/spreadsheet/static/tests/links/model/link_menu_cells_test.js +++ b/addons/spreadsheet/static/tests/links/model/link_menu_cells_test.js @@ -26,7 +26,7 @@ function beforeEach() { QUnit.module("spreadsheet > menu link cells", { beforeEach }, () => { QUnit.test("ir.menu linked based on xml id", async function (assert) { const env = await makeTestEnv({ serverData: getMenuServerData() }); - const model = new Model({}, { external: { env } }); + const model = new Model({}, { custom: { env } }); setCellContent(model, "A1", "[label](odoo://ir_menu_xml_id/test_menu)"); const cell = getCell(model, "A1"); const evaluatedCell = getEvaluatedCell(model, "A1"); @@ -46,7 +46,7 @@ QUnit.module("spreadsheet > menu link cells", { beforeEach }, () => { QUnit.test("ir.menu linked based on record id", async function (assert) { const env = await makeTestEnv({ serverData: getMenuServerData() }); - const model = new Model({}, { external: { env } }); + const model = new Model({}, { custom: { env } }); setCellContent(model, "A1", "[label](odoo://ir_menu_id/2)"); const cell = getCell(model, "A1"); const evaluatedCell = getEvaluatedCell(model, "A1"); @@ -66,7 +66,7 @@ QUnit.module("spreadsheet > menu link cells", { beforeEach }, () => { QUnit.test("ir.menu linked based on xml id which does not exists", async function (assert) { const env = await makeTestEnv({ serverData: getMenuServerData() }); - const model = new Model({}, { external: { env } }); + const model = new Model({}, { custom: { env } }); setCellContent(model, "A1", "[label](odoo://ir_menu_xml_id/does_not_exists)"); assert.equal( getCell(model, "A1").content, @@ -81,7 +81,7 @@ QUnit.module("spreadsheet > menu link cells", { beforeEach }, () => { QUnit.test("ir.menu linked based on record id which does not exists", async function (assert) { const env = await makeTestEnv({ serverData: getMenuServerData() }); - const model = new Model({}, { external: { env } }); + const model = new Model({}, { custom: { env } }); setCellContent(model, "A1", "[label](odoo://ir_menu_id/9999)"); assert.equal(getCell(model, "A1").content, "[label](odoo://ir_menu_id/9999)"); assert.equal(getEvaluatedCell(model, "A1").value, "#LINK"); @@ -93,7 +93,7 @@ QUnit.module("spreadsheet > menu link cells", { beforeEach }, () => { QUnit.test("Odoo link cells can be imported/exported", async function (assert) { const env = await makeTestEnv({ serverData: getMenuServerData() }); - const model = new Model({}, { external: { env } }); + const model = new Model({}, { custom: { env } }); setCellContent(model, "A1", "[label](odoo://ir_menu_id/2)"); let cell = getCell(model, "A1"); let evaluatedCell = getEvaluatedCell(model, "A1"); @@ -109,7 +109,7 @@ QUnit.module("spreadsheet > menu link cells", { beforeEach }, () => { "odoo://ir_menu_id/2", "The link url should reference the correct menu" ); - const model2 = new Model(model.exportData(), { external: { env } }); + const model2 = new Model(model.exportData(), { custom: { env } }); cell = getCell(model2, "A1"); evaluatedCell = getEvaluatedCell(model, "A1"); assert.equal(evaluatedCell.value, "label", "The value should be the menu name"); diff --git a/addons/spreadsheet/static/tests/links/open_links_tests.js b/addons/spreadsheet/static/tests/links/open_links_tests.js index 381bd210263..d24761e1f1d 100644 --- a/addons/spreadsheet/static/tests/links/open_links_tests.js +++ b/addons/spreadsheet/static/tests/links/open_links_tests.js @@ -41,7 +41,7 @@ QUnit.test("click a web link", async (assert) => { }, ], }; - const model = new Model(data, { external: { env } }); + const model = new Model(data, { custom: { env } }); const cell = getEvaluatedCell(model, "A1"); assert.strictEqual(urlRepresentation(cell.link, model.getters), "https://odoo.com"); openLink(cell.link, env); @@ -68,7 +68,7 @@ QUnit.test("click a menu link", async (assert) => { }, ], }; - const model = new Model(data, { external: { env } }); + const model = new Model(data, { custom: { env } }); const cell = getEvaluatedCell(model, "A1"); assert.strictEqual(urlRepresentation(cell.link, model.getters), "menu with xmlid"); openLink(cell.link, env); @@ -112,7 +112,7 @@ QUnit.test("click a menu link", async (assert) => { }, ], }; - const model = new Model(data, { external: { env } }); + const model = new Model(data, { custom: { env } }); const cell = getEvaluatedCell(model, "A1"); assert.strictEqual(urlRepresentation(cell.link, model.getters), "an odoo view"); openLink(cell.link, env); diff --git a/addons/spreadsheet/static/tests/utils/chart.js b/addons/spreadsheet/static/tests/utils/chart.js index 113e58be098..2ef7f441444 100644 --- a/addons/spreadsheet/static/tests/utils/chart.js +++ b/addons/spreadsheet/static/tests/utils/chart.js @@ -38,7 +38,7 @@ export async function createSpreadsheetWithChart(params = {}) { insertChartInSpreadsheet(model, params.type); - const env = model.config.external.env; + const env = model.config.custom.env; env.model = model; await nextTick(); return { model, env }; diff --git a/addons/spreadsheet/static/tests/utils/list.js b/addons/spreadsheet/static/tests/utils/list.js index faf52dd925c..7b13aeaba15 100644 --- a/addons/spreadsheet/static/tests/utils/list.js +++ b/addons/spreadsheet/static/tests/utils/list.js @@ -62,7 +62,7 @@ export async function createSpreadsheetWithList(params = {}) { sheetId: params.sheetId, }); - const env = model.config.external.env; + const env = model.config.custom.env; env.model = model; await waitForDataSourcesLoaded(model); return { model, env }; diff --git a/addons/spreadsheet/static/tests/utils/model.js b/addons/spreadsheet/static/tests/utils/model.js index fe75b039fe5..eba98b62b74 100644 --- a/addons/spreadsheet/static/tests/utils/model.js +++ b/addons/spreadsheet/static/tests/utils/model.js @@ -16,7 +16,7 @@ const { Model } = spreadsheet; */ export function setupDataSourceEvaluation(model) { - model.config.external.dataSources.addEventListener("data-source-updated", () => { + model.config.custom.dataSources.addEventListener("data-source-updated", () => { const sheetId = model.getters.getActiveSheetId(); model.dispatch("EVALUATE_CELLS", { sheetId }); }); @@ -37,7 +37,7 @@ export async function createModelWithDataSource(params = {}) { mockRPC: params.mockRPC, }); const model = new Model(params.spreadsheetData, { - external: { + custom: { env, dataSources: new DataSources(env.services.orm.silent), }, @@ -62,7 +62,7 @@ export async function waitForDataSourcesLoaded(model) { // Read a first time in order to trigger the RPC readAllCellsValue(); //@ts-ignore - await model.config.external.dataSources.waitForAllLoaded(); + await model.config.custom.dataSources.waitForAllLoaded(); await nextTick(); // Read a second time to trigger the compute format (which could trigger a RPC for currency, in list) readAllCellsValue(); diff --git a/addons/spreadsheet/static/tests/utils/pivot.js b/addons/spreadsheet/static/tests/utils/pivot.js index 39af528544c..29160d5ceff 100644 --- a/addons/spreadsheet/static/tests/utils/pivot.js +++ b/addons/spreadsheet/static/tests/utils/pivot.js @@ -32,7 +32,7 @@ export async function insertPivotInSpreadsheet(model, params) { }, name: "Partner Pivot", }; - const dataSource = model.config.external.dataSources.create(PivotDataSource, definition); + const dataSource = model.config.custom.dataSources.create(PivotDataSource, definition); await dataSource.load(); const { cols, rows, measures } = dataSource.getTableStructure().export(); const table = { @@ -68,7 +68,7 @@ export async function createSpreadsheetWithPivot(params = {}) { }); const arch = params.arch || serverData.views["partner,false,pivot"]; await insertPivotInSpreadsheet(model, { arch }); - const env = model.config.external.env; + const env = model.config.custom.env; env.model = model; await waitForDataSourcesLoaded(model); return { model, env }; diff --git a/addons/spreadsheet/static/tests/utils/ui.js b/addons/spreadsheet/static/tests/utils/ui.js index 182bfc8c221..08770df7e7e 100644 --- a/addons/spreadsheet/static/tests/utils/ui.js +++ b/addons/spreadsheet/static/tests/utils/ui.js @@ -21,7 +21,7 @@ export async function mountSpreadsheet(model) { const app = new App(Spreadsheet, { props: { model }, templates: templates, - env: model.config.external.env, + env: model.config.custom.env, test: true, }); registerCleanup(() => app.destroy()); diff --git a/addons/spreadsheet_account/static/src/plugins/accounting_plugin.js b/addons/spreadsheet_account/static/src/plugins/accounting_plugin.js index cd1e780a544..05f6fe26a3b 100644 --- a/addons/spreadsheet_account/static/src/plugins/accounting_plugin.js +++ b/addons/spreadsheet_account/static/src/plugins/accounting_plugin.js @@ -11,7 +11,7 @@ const DATA_SOURCE_ID = "ACCOUNTING_AGGREGATES"; export default class AccountingPlugin extends spreadsheet.UIPlugin { constructor(config) { super(config); - this.dataSources = config.external.dataSources; + this.dataSources = config.custom.dataSources; if (this.dataSources) { this.dataSources.add(DATA_SOURCE_ID, AccountingDataSource); } diff --git a/addons/spreadsheet_account/static/tests/ui/accounting_drilldown_tests.js b/addons/spreadsheet_account/static/tests/ui/accounting_drilldown_tests.js index 6fe9062ebc1..1b56622d5ca 100644 --- a/addons/spreadsheet_account/static/tests/ui/accounting_drilldown_tests.js +++ b/addons/spreadsheet_account/static/tests/ui/accounting_drilldown_tests.js @@ -62,7 +62,7 @@ QUnit.module("spreadsheet_account > Accounting Drill down", { beforeEach }, () = } }, }); - const env = model.config.external.env; + const env = model.config.custom.env; env.model = model; setCellContent(model, "A1", `=ODOO.BALANCE("100", 2020)`); setCellContent(model, "A2", `=ODOO.BALANCE("100", 0)`); diff --git a/addons/spreadsheet_dashboard/static/src/bundle/dashboard_action/dashboard_loader.js b/addons/spreadsheet_dashboard/static/src/bundle/dashboard_action/dashboard_loader.js index 108adfb1ff7..e9c2f930aa5 100644 --- a/addons/spreadsheet_dashboard/static/src/bundle/dashboard_action/dashboard_loader.js +++ b/addons/spreadsheet_dashboard/static/src/bundle/dashboard_action/dashboard_loader.js @@ -211,7 +211,7 @@ export class DashboardLoader { const model = new Model( migrate(data), { - external: { env: this.env, orm: this.orm, dataSources }, + custom: { env: this.env, orm: this.orm, dataSources }, mode: "dashboard", }, revisions