From dbc3f33c2677a0a4ca38b43639c2f8f5c442ddea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Guti=C3=A9rrez=20Hermoso?= Date: Thu, 26 Sep 2024 16:30:55 -0400 Subject: [PATCH 1/9] build: update yarn.lock Needed to fix the build --- buildtools/.grist-ee-version | 2 +- yarn.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/buildtools/.grist-ee-version b/buildtools/.grist-ee-version index 7e310bae19..56f3151140 100644 --- a/buildtools/.grist-ee-version +++ b/buildtools/.grist-ee-version @@ -1 +1 @@ -0.9.9 +0.9.10 diff --git a/yarn.lock b/yarn.lock index 3465da77d1..4a9ea139e4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2255,7 +2255,7 @@ builtin-status-codes@^3.0.0: resolved "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz" integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= -bullmq@^5.8.7: +bullmq@5.8.7: version "5.8.7" resolved "https://registry.yarnpkg.com/bullmq/-/bullmq-5.8.7.tgz#d5da6215377fe29494d74ad307f195f7408b9e2e" integrity sha512-IdAgB9WvJHRAcZtamRLj6fbjMyuIogEa1cjOTWM1pkVoHUOpO34q6FzNMX1R8VOeUhkvkOkWcxI5ENgFLh+TVA== From d6701debb114fc832a53efa090c634ef8001f052 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 26 Sep 2024 17:00:43 -0400 Subject: [PATCH 2/9] automated update to translation keys (#1228) Co-authored-by: Paul's Grist Bot --- static/locales/en.client.json | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/static/locales/en.client.json b/static/locales/en.client.json index c291d404a0..57e32aab3a 100644 --- a/static/locales/en.client.json +++ b/static/locales/en.client.json @@ -1213,7 +1213,10 @@ "Community widgets are created and maintained by Grist community members.": "Community widgets are created and maintained by Grist community members.", "Creates a reverse column in target table that can be edited from either end.": "Creates a reverse column in target table that can be edited from either end.", "This limitation occurs when one end of a two-way reference is configured as a single Reference.": "This limitation occurs when one end of a two-way reference is configured as a single Reference.", - "To allow multiple assignments, change the type of the Reference column to Reference List.": "To allow multiple assignments, change the type of the Reference column to Reference List." + "To allow multiple assignments, change the type of the Reference column to Reference List.": "To allow multiple assignments, change the type of the Reference column to Reference List.", + "This limitation occurs when one column in a two-way reference has the Reference type.": "This limitation occurs when one column in a two-way reference has the Reference type.", + "To allow multiple assignments, change the referenced column's type to Reference List.": "To allow multiple assignments, change the referenced column's type to Reference List.", + "Two-way references are not currently supported for Formula or Trigger Formula columns": "Two-way references are not currently supported for Formula or Trigger Formula columns" }, "DescriptionConfig": { "DESCRIPTION": "DESCRIPTION" @@ -1761,7 +1764,9 @@ "Delete column {{column}} in table {{table}}?": "Delete column {{column}} in table {{table}}?", "It is the reverse of the reference column {{column}} in table {{table}}.": "It is the reverse of the reference column {{column}} in table {{table}}.", "Table": "Table", - "Two-way Reference": "Two-way Reference" + "Two-way Reference": "Two-way Reference", + "Delete two-way reference?": "Delete two-way reference?", + "Target table": "Target table" }, "SupportGristButton": { "Admin Panel": "Admin Panel", From bac1c49ac7f6be2dd92a764a2c74a3ecf7f010d7 Mon Sep 17 00:00:00 2001 From: George Gevoian <85144792+georgegevoian@users.noreply.github.com> Date: Fri, 27 Sep 2024 11:00:10 -0400 Subject: [PATCH 3/9] Remove assertion from HomeIntro test (#1230) --- test/nbrowser/HomeIntro.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/nbrowser/HomeIntro.ts b/test/nbrowser/HomeIntro.ts index 85b7de4cb8..e08f77fa4d 100644 --- a/test/nbrowser/HomeIntro.ts +++ b/test/nbrowser/HomeIntro.ts @@ -98,7 +98,6 @@ describe('HomeIntro', function() { assert.isTrue(await driver.find('.test-intro-cards').isDisplayed()); assert.isTrue(await driver.find('.test-intro-video-tour').isDisplayed()); - assert.isTrue(await driver.find('.test-intro-tutorial').isDisplayed()); assert.isTrue(await driver.find('.test-intro-create-doc').isDisplayed()); assert.isTrue(await driver.find('.test-intro-import-doc').isDisplayed()); assert.isTrue(await driver.find('.test-intro-templates').isDisplayed()); From 5b79d4b2069c17059a67a403ae0ae0a03b3425ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Guti=C3=A9rrez=20Hermoso?= Date: Fri, 27 Sep 2024 13:26:48 -0400 Subject: [PATCH 4/9] v1.2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9e92bc918e..773f1ed782 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "grist-core", - "version": "1.1.18", + "version": "1.2.0", "license": "Apache-2.0", "description": "Grist is the evolution of spreadsheets", "homepage": "https://github.com/gristlabs/grist-core", From 755a742d6fe1ded588f848ce3187205981a8a76a Mon Sep 17 00:00:00 2001 From: Tim Newsome Date: Mon, 30 Sep 2024 02:20:22 -0700 Subject: [PATCH 5/9] Add Copy With Headers to grid cell popup. (#1208) * Add "Copy with headers" to grid cell popup. This is what you want when you're going to paste into e.g. an email. Tested just by manually trying copy and paste into an editor and an email, and then again using the new variant to confirm the headers show up. https://github.com/gristlabs/grist-core/pull/1208 --- app/client/components/Clipboard.js | 19 ++++++--- app/client/components/commandList.ts | 5 +++ app/client/lib/tableUtil.ts | 14 ++++--- app/client/ui/CellContextMenu.ts | 1 + test/nbrowser/CopyPaste.ts | 4 +- test/nbrowser/CopyWithHeaders.ts | 59 ++++++++++++++++++++++++++++ 6 files changed, 89 insertions(+), 13 deletions(-) create mode 100644 test/nbrowser/CopyWithHeaders.ts diff --git a/app/client/components/Clipboard.js b/app/client/components/Clipboard.js index c196cd8170..decd3f163a 100644 --- a/app/client/components/Clipboard.js +++ b/app/client/components/Clipboard.js @@ -107,6 +107,7 @@ Base.setBaseFor(Clipboard); Clipboard.commands = { contextMenuCopy: function() { this._doContextMenuCopy(); }, + contextMenuCopyWithHeaders: function() { this._doContextMenuCopyWithHeaders(); }, contextMenuCut: function() { this._doContextMenuCut(); }, contextMenuPaste: function() { this._doContextMenuPaste(); }, }; @@ -126,7 +127,13 @@ Clipboard.prototype._onCopy = function(elem, event) { Clipboard.prototype._doContextMenuCopy = function() { let pasteObj = commands.allCommands.copy.run(); - this._copyToClipboard(pasteObj, 'copy'); + this._copyToClipboard(pasteObj, 'copy', false); +}; + +Clipboard.prototype._doContextMenuCopyWithHeaders = function() { + let pasteObj = commands.allCommands.copy.run(); + + this._copyToClipboard(pasteObj, 'copy', true); }; Clipboard.prototype._onCut = function(elem, event) { @@ -146,21 +153,21 @@ Clipboard.prototype._doContextMenuCut = function() { Clipboard.prototype._setCBdata = function(pasteObj, clipboardData) { if (!pasteObj) { return; } - const plainText = tableUtil.makePasteText(pasteObj.data, pasteObj.selection); + const plainText = tableUtil.makePasteText(pasteObj.data, pasteObj.selection, false); clipboardData.setData('text/plain', plainText); - const htmlText = tableUtil.makePasteHtml(pasteObj.data, pasteObj.selection); + const htmlText = tableUtil.makePasteHtml(pasteObj.data, pasteObj.selection, false); clipboardData.setData('text/html', htmlText); this._setCutCallback(pasteObj, plainText); }; -Clipboard.prototype._copyToClipboard = async function(pasteObj, action) { +Clipboard.prototype._copyToClipboard = async function(pasteObj, action, includeColHeaders) { if (!pasteObj) { return; } - const plainText = tableUtil.makePasteText(pasteObj.data, pasteObj.selection); + const plainText = tableUtil.makePasteText(pasteObj.data, pasteObj.selection, includeColHeaders); let data; if (typeof ClipboardItem === 'function') { - const htmlText = tableUtil.makePasteHtml(pasteObj.data, pasteObj.selection); + const htmlText = tableUtil.makePasteHtml(pasteObj.data, pasteObj.selection, includeColHeaders); // eslint-disable-next-line no-undef data = new ClipboardItem({ // eslint-disable-next-line no-undef diff --git a/app/client/components/commandList.ts b/app/client/components/commandList.ts index 532b7cab83..00a9e02745 100644 --- a/app/client/components/commandList.ts +++ b/app/client/components/commandList.ts @@ -63,6 +63,7 @@ export type CommandName = | 'cut' | 'paste' | 'contextMenuCopy' + | 'contextMenuCopyWithHeaders' | 'contextMenuCut' | 'contextMenuPaste' | 'fillSelectionDown' @@ -470,6 +471,10 @@ export const groups: CommendGroupDef[] = [{ keys: ['Mod+C'], desc: 'Copy current selection to clipboard', bindKeys: false, + }, { + name: 'contextMenuCopyWithHeaders', + keys: [], + desc: 'Copy current selection to clipboard including headers', }, { name: 'contextMenuCut', keys: ['Mod+X'], diff --git a/app/client/lib/tableUtil.ts b/app/client/lib/tableUtil.ts index 5db8153a4b..4bec1ce646 100644 --- a/app/client/lib/tableUtil.ts +++ b/app/client/lib/tableUtil.ts @@ -30,12 +30,16 @@ export function fieldInsertPositions(viewFields: KoArray, index: n * @param {CopySelection} selection - a CopySelection instance * @return {String} **/ -export function makePasteText(tableData: TableData, selection: CopySelection) { +export function makePasteText(tableData: TableData, selection: CopySelection, includeColHeaders: boolean) { // tsvEncode expects data as a 2-d array with each a array representing a row // i.e. [["1-1", "1-2", "1-3"],["2-1", "2-2", "2-3"]] - const values = selection.rowIds.map(rowId => - selection.columns.map(col => col.fmtGetter(rowId))); - return tsvEncode(values); + const result = []; + if (includeColHeaders) { + result.push(selection.fields.map(f => f.label())); + } + result.push(...selection.rowIds.map(rowId => + selection.columns.map(col => col.fmtGetter(rowId)))); + return tsvEncode(result); } /** @@ -70,7 +74,7 @@ export function makePasteHtml(tableData: TableData, selection: CopySelection, in )), // Include column headers if requested. (includeColHeaders ? - dom('tr', selection.colIds.map(colId => dom('th', colId))) : + dom('tr', selection.fields.map(field => dom('th', field.label()))) : null ), // Fill with table cells. diff --git a/app/client/ui/CellContextMenu.ts b/app/client/ui/CellContextMenu.ts index 9bb13702fa..8e4efbbb36 100644 --- a/app/client/ui/CellContextMenu.ts +++ b/app/client/ui/CellContextMenu.ts @@ -38,6 +38,7 @@ export function CellContextMenu(cellOptions: ICellContextMenu, colOptions: IMult result.push( menuItemCmd(allCommands.contextMenuCut, t('Cut'), disableForReadonlyColumn), menuItemCmd(allCommands.contextMenuCopy, t('Copy')), + menuItemCmd(allCommands.contextMenuCopyWithHeaders, t('Copy with headers')), menuItemCmd(allCommands.contextMenuPaste, t('Paste'), disableForReadonlyColumn), menuDivider(), colOptions.isFormula ? diff --git a/test/nbrowser/CopyPaste.ts b/test/nbrowser/CopyPaste.ts index 1da7857f3a..16c1c3953c 100644 --- a/test/nbrowser/CopyPaste.ts +++ b/test/nbrowser/CopyPaste.ts @@ -637,7 +637,7 @@ async function copyAndCheck( } } -function createDummyTextArea() { +export function createDummyTextArea() { const textarea = document.createElement('textarea'); textarea.style.position = "absolute"; textarea.style.top = "0"; @@ -647,7 +647,7 @@ function createDummyTextArea() { window.document.body.appendChild(textarea); } -function removeDummyTextArea() { +export function removeDummyTextArea() { const textarea = document.getElementById('dummyText'); if (textarea) { window.document.body.removeChild(textarea); diff --git a/test/nbrowser/CopyWithHeaders.ts b/test/nbrowser/CopyWithHeaders.ts new file mode 100644 index 0000000000..852de7d1d2 --- /dev/null +++ b/test/nbrowser/CopyWithHeaders.ts @@ -0,0 +1,59 @@ +/** + * Test for copying Grist data with headers. + */ + +import {assert, driver, Key} from 'mocha-webdriver'; +import * as gu from 'test/nbrowser/gristUtils'; +import {setupTestSuite} from 'test/nbrowser/testUtils'; +import {createDummyTextArea, removeDummyTextArea} from 'test/nbrowser/CopyPaste'; + +describe("CopyWithHeaders", function() { + this.timeout(90000); + const cleanup = setupTestSuite(); + const clipboard = gu.getLockableClipboard(); + afterEach(() => gu.checkForErrors()); + gu.bigScreen(); + + after(async function() { + await driver.executeScript(removeDummyTextArea); + }); + + it('should copy headers', async function() { + const session = await gu.session().teamSite.login(); + await session.tempDoc(cleanup, 'Hello.grist'); + await driver.executeScript(createDummyTextArea); + + await clipboard.lockAndPerform(async (cb) => { + // Select all + await gu.sendKeys(Key.chord(Key.CONTROL, 'a')); + await gu.rightClick(gu.getCell({rowNum: 1, col: 'A'})); + await driver.findContent('.grist-floating-menu li', 'Copy with headers').click(); + + await pasteAndCheck(cb, ["A", "B", "C", "D", "E"], 5); + }); + + await clipboard.lockAndPerform(async (cb) => { + // Select a single cell. + await gu.getCell({rowNum: 2, col: 'D'}).click(); + await gu.rightClick(gu.getCell({rowNum: 2, col: 'D'})); + await driver.findContent('.grist-floating-menu li', 'Copy with headers').click(); + + await pasteAndCheck(cb, ["D"], 2); + }); + }); +}); + +async function pasteAndCheck(cb: gu.IClipboard, headers: string[], rows: number) { + // Paste into the dummy textarea. + await driver.find('#dummyText').click(); + await gu.waitAppFocus(false); + await cb.paste(); + + const textarea = await driver.find('#dummyText'); + const text = await textarea.getAttribute('value'); + const lines = text.split('\n'); + const regex = new RegExp(`^${headers.join('\\s+')}$`); + assert.match(lines[0], regex); + assert.equal(lines.length, rows); + await textarea.clear(); +} From 172af40f879a7eebe1f9b12d88b4bbf6bc95ad75 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 12:00:52 -0400 Subject: [PATCH 6/9] automated update to translation keys (#1232) Co-authored-by: Paul's Grist Bot --- static/locales/en.client.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/static/locales/en.client.json b/static/locales/en.client.json index 57e32aab3a..8a9c85c75a 100644 --- a/static/locales/en.client.json +++ b/static/locales/en.client.json @@ -148,7 +148,8 @@ "Comment": "Comment", "Copy": "Copy", "Cut": "Cut", - "Paste": "Paste" + "Paste": "Paste", + "Copy with headers": "Copy with headers" }, "ChartView": { "Create separate series for each value of the selected column.": "Create separate series for each value of the selected column.", From 11fe3e90d4abe5e6f7bd5c73d7d2e9b3711c9f01 Mon Sep 17 00:00:00 2001 From: Paul Fitzpatrick Date: Mon, 30 Sep 2024 15:58:38 -0400 Subject: [PATCH 7/9] check sandbox viability lazily (#1226) This checks whether code can successfully run in the sandbox only when the admin panel needs to report that, rather than at start up. This is motivated by two things: - The desktop app became a lot slower to open with this check, since it uses pyodide by default, and there's been no work on optimizing the pyodide sandbox load times (as opposed to gvisor, where a lot of work was done, and it is also fundamentally faster). - The messages logged by a test sandbox starting and stopping have been confusing people. There is a case for doing the check on startup, especially on servers, so that we can fail early. Still, that isn't what we were doing, and we'd also like to move away from the server refusing to start because of a problem and towards an always-reachable admin page that reports the nature of problems in a clearer way. --- app/client/ui/AdminPanel.ts | 5 ++++- app/server/MergedServer.ts | 6 ------ app/server/lib/BootProbes.ts | 2 +- app/server/lib/FlexServer.ts | 12 ++++++------ app/server/lib/GristServer.ts | 4 ++-- 5 files changed, 13 insertions(+), 16 deletions(-) diff --git a/app/client/ui/AdminPanel.ts b/app/client/ui/AdminPanel.ts index e2cc38bf01..0b1e540410 100644 --- a/app/client/ui/AdminPanel.ts +++ b/app/client/ui/AdminPanel.ts @@ -202,7 +202,10 @@ Please log in as an administrator.`)), const success = result?.status === 'success'; const details = result?.details as SandboxingBootProbeDetails|undefined; if (!details) { - return cssValueLabel(t('unknown')); + // Sandbox details get filled out relatively slowly if + // this is first time on admin panel. So show "checking" + // if we don't have a reported status yet. + return cssValueLabel(result?.status ? t('unknown') : t('checking')); } const flavor = details.flavor; const configured = details.configured; diff --git a/app/server/MergedServer.ts b/app/server/MergedServer.ts index ba307e8060..2ac994d169 100644 --- a/app/server/MergedServer.ts +++ b/app/server/MergedServer.ts @@ -192,12 +192,6 @@ export class MergedServer { this.flexServer.checkOptionCombinations(); this.flexServer.summary(); this.flexServer.ready(); - - // Some tests have their timing perturbed by having this earlier - // TODO: update those tests. - if (this.hasComponent("docs")) { - await this.flexServer.checkSandbox(); - } } catch(e) { await this.flexServer.close(); throw e; diff --git a/app/server/lib/BootProbes.ts b/app/server/lib/BootProbes.ts index 36c3786c56..df0f202efd 100644 --- a/app/server/lib/BootProbes.ts +++ b/app/server/lib/BootProbes.ts @@ -265,7 +265,7 @@ const _sandboxingProbe: Probe = { id: 'sandboxing', name: 'Is document sandboxing effective', apply: async (server, req) => { - const details = server.getSandboxInfo(); + const details = await server.getSandboxInfo(); return { status: (details?.configured && details?.functional) ? 'success' : 'fault', details, diff --git a/app/server/lib/FlexServer.ts b/app/server/lib/FlexServer.ts index 4ce03fcba2..2d8803ebb5 100644 --- a/app/server/lib/FlexServer.ts +++ b/app/server/lib/FlexServer.ts @@ -1397,8 +1397,9 @@ export class FlexServer implements GristServer { } } - public async checkSandbox() { - if (this._check('sandbox', 'doc')) { return; } + public async getSandboxInfo(): Promise { + if (this._sandboxInfo) { return this._sandboxInfo; } + const flavor = process.env.GRIST_SANDBOX_FLAVOR || 'unknown'; const info = this._sandboxInfo = { flavor, @@ -1408,6 +1409,8 @@ export class FlexServer implements GristServer { sandboxed: false, lastSuccessfulStep: 'none', } as SandboxInfo; + // Only meaningful on instances that handle documents. + if (!this._docManager) { return info; } try { const sandbox = createSandbox({ server: this, @@ -1432,10 +1435,7 @@ export class FlexServer implements GristServer { } catch (e) { info.error = String(e); } - } - - public getSandboxInfo(): SandboxInfo|undefined { - return this._sandboxInfo; + return info; } public getInfo(key: string): any { diff --git a/app/server/lib/GristServer.ts b/app/server/lib/GristServer.ts index 602e7760f0..555d18ba08 100644 --- a/app/server/lib/GristServer.ts +++ b/app/server/lib/GristServer.ts @@ -70,7 +70,7 @@ export interface GristServer { servesPlugins(): boolean; getBundledWidgets(): ICustomWidget[]; getBootKey(): string|undefined; - getSandboxInfo(): SandboxInfo|undefined; + getSandboxInfo(): Promise; getInfo(key: string): any; getJobs(): GristJobs; } @@ -165,7 +165,7 @@ export function createDummyGristServer(): GristServer { getPlugins() { return []; }, getBundledWidgets() { return []; }, getBootKey() { return undefined; }, - getSandboxInfo() { return undefined; }, + getSandboxInfo() { throw new Error('no sandbox'); }, getInfo(key: string) { return undefined; }, getJobs(): GristJobs { throw new Error('no job system'); }, }; From 77194dcb200531bb5a98cf052444aaca9d01bfe4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 16:05:14 -0400 Subject: [PATCH 8/9] automated update to translation keys (#1234) Co-authored-by: Paul's Grist Bot --- static/locales/en.client.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/static/locales/en.client.json b/static/locales/en.client.json index 8a9c85c75a..f12cec1acf 100644 --- a/static/locales/en.client.json +++ b/static/locales/en.client.json @@ -1598,7 +1598,8 @@ "Key to sign sessions with": "Key to sign sessions with", "Session Secret": "Session Secret", "Enable Grist Enterprise": "Enable Grist Enterprise", - "Enterprise": "Enterprise" + "Enterprise": "Enterprise", + "checking": "checking" }, "Columns": { "Remove Column": "Remove Column" From 437d7e61c0a7cad3c4ae4b9c2bbd225737a9218f Mon Sep 17 00:00:00 2001 From: jarek Date: Tue, 1 Oct 2024 17:33:50 +0200 Subject: [PATCH 9/9] Removing fixSiteProducts method (#1236) Context: Removing an obsolete method that was fixing an issue with default site products. Details can be found here 76d9448 Proposed solution: Removing this method and its test. Test plan: not needed https://github.com/gristlabs/grist-core/pull/1236 --- app/gen-server/lib/Housekeeper.ts | 62 ------------- stubs/app/server/server.ts | 5 - test/server/fixSiteProducts.ts | 146 ------------------------------ 3 files changed, 213 deletions(-) delete mode 100644 test/server/fixSiteProducts.ts diff --git a/app/gen-server/lib/Housekeeper.ts b/app/gen-server/lib/Housekeeper.ts index c3012bec6b..d380dd11c6 100644 --- a/app/gen-server/lib/Housekeeper.ts +++ b/app/gen-server/lib/Housekeeper.ts @@ -2,10 +2,8 @@ import { ApiError } from 'app/common/ApiError'; import { delay } from 'app/common/delay'; import { buildUrlId } from 'app/common/gristUrls'; import { normalizedDateTimeString } from 'app/common/normalizedDateTimeString'; -import { BillingAccount } from 'app/gen-server/entity/BillingAccount'; import { Document } from 'app/gen-server/entity/Document'; import { Organization } from 'app/gen-server/entity/Organization'; -import { Product } from 'app/gen-server/entity/Product'; import { Workspace } from 'app/gen-server/entity/Workspace'; import { HomeDBManager, Scope } from 'app/gen-server/lib/homedb/HomeDBManager'; import { fromNow } from 'app/gen-server/sqlUtils'; @@ -438,63 +436,3 @@ async function forEachWithBreaks(logText: string, items: T[], callback: (item } log.rawInfo(logText, {itemsProcesssed, itemsTotal, timeMs: Date.now() - start}); } - -/** - * For a brief moment file `stubs/app/server/server.ts` was ignoring the GRIST_DEFAULT_PRODUCT - * variable, which is currently set for all deployment types to 'Free' product. As a result orgs - * created after 2024-06-12 (1.1.15) were created with 'teamFree' product instead of 'Free'. - * It only affected deployments that were using: - * - GRIST_DEFAULT_PRODUCT variable set to 'Free' - * - GRIST_SINGLE_ORG set to enforce single org mode. - * - * This method fixes the product for all orgs created with 'teamFree' product, if the default - * product that should be used is 'Free' and the deployment type is not 'saas' ('saas' deployment - * isn't using GRIST_DEFAULT_PRODUCT variable). This method should be removed after 2024.10.01. - * - * There is a corresponding test that will fail if this method (and that test) are not removed. - * - * @returns true if the method was run, false otherwise. - */ -export async function fixSiteProducts(options: { - deploymentType: string, - db: HomeDBManager -}) { - const {deploymentType, db} = options; - - const hasDefaultProduct = () => Boolean(process.env.GRIST_DEFAULT_PRODUCT); - const defaultProductIsFree = () => process.env.GRIST_DEFAULT_PRODUCT === 'Free'; - const notSaasDeployment = () => deploymentType !== 'saas'; - const mustRun = hasDefaultProduct() && defaultProductIsFree() && notSaasDeployment(); - if (!mustRun) { - return false; - } - const removeMeDate = new Date('2024-10-01'); - const warningMessage = `WARNING: This method should be removed after ${removeMeDate.toDateString()}.`; - if (new Date() > removeMeDate) { - console.warn(warningMessage); - } - - // Find all billing accounts on teamFree product and change them to the Free. - return await db.connection.transaction(async (t) => { - const freeProduct = await t.findOne(Product, {where: {name: 'Free'}}); - const freeTeamProduct = await t.findOne(Product, {where: {name: 'teamFree'}}); - - if (!freeTeamProduct) { - console.warn('teamFree product not found.'); - return false; - } - - if (!freeProduct) { - console.warn('Free product not found.'); - return false; - } - - await t.createQueryBuilder() - .update(BillingAccount) - .set({product: freeProduct.id}) - .where({product: freeTeamProduct.id}) - .execute(); - - return true; - }); -} diff --git a/stubs/app/server/server.ts b/stubs/app/server/server.ts index c7e5b9eb5e..6f5f63fae2 100644 --- a/stubs/app/server/server.ts +++ b/stubs/app/server/server.ts @@ -7,7 +7,6 @@ import {commonUrls} from 'app/common/gristUrls'; import {isAffirmative} from 'app/common/gutil'; import {HomeDBManager} from 'app/gen-server/lib/homedb/HomeDBManager'; -import {fixSiteProducts} from 'app/gen-server/lib/Housekeeper'; const debugging = isAffirmative(process.env.DEBUG) || isAffirmative(process.env.VERBOSE); @@ -132,10 +131,6 @@ export async function main() { if (process.env.GRIST_SERVE_PLUGINS_PORT) { await mergedServer.flexServer.startCopy('pluginServer', parseInt(process.env.GRIST_SERVE_PLUGINS_PORT, 10)); } - await fixSiteProducts({ - deploymentType: mergedServer.flexServer.getDeploymentType(), - db: mergedServer.flexServer.getHomeDBManager() - }); return mergedServer.flexServer; } diff --git a/test/server/fixSiteProducts.ts b/test/server/fixSiteProducts.ts deleted file mode 100644 index 353b4ac8e4..0000000000 --- a/test/server/fixSiteProducts.ts +++ /dev/null @@ -1,146 +0,0 @@ -import {Organization} from 'app/gen-server/entity/Organization'; -import {fixSiteProducts} from 'app/gen-server/lib/Housekeeper'; -import {TestServer} from 'test/gen-server/apiUtils'; -import * as testUtils from 'test/server/testUtils'; -import {assert} from 'chai'; -import sinon from "sinon"; -import {getDefaultProductNames} from 'app/gen-server/entity/Product'; - -const email = 'chimpy@getgrist.com'; -const profile = {email, name: email}; -const org = 'single-org'; - -describe('fixSiteProducts', function() { - this.timeout(6000); - - let oldEnv: testUtils.EnvironmentSnapshot; - let server: TestServer; - - before(async function() { - oldEnv = new testUtils.EnvironmentSnapshot(); - // By default we will simulate 'core' deployment that has 'Free' team site as default product. - process.env.GRIST_TEST_SERVER_DEPLOYMENT_TYPE = 'core'; - process.env.GRIST_DEFAULT_PRODUCT = 'Free'; - server = new TestServer(this); - await server.start(); - }); - - after(async function() { - oldEnv.restore(); - await server.stop(); - }); - - it('fix should be deleted after 2024-10-01', async function() { - const now = new Date(); - const remove_date = new Date('2024-10-01'); - assert.isTrue(now < remove_date, 'This test and a fix method should be deleted after 2024-10-01'); - }); - - it('fixes sites that where created with a wrong product', async function() { - const db = server.dbManager; - const user = await db.getUserByLogin(email, {profile}) as any; - const getOrg = (id: number) => db.connection.manager.findOne( - Organization, - {where: {id}, relations: ['billingAccount', 'billingAccount.product']}); - - const productOrg = (id: number) => getOrg(id)?.then(org => org?.billingAccount?.product?.name); - - const freeOrgId = db.unwrapQueryResult(await db.addOrg(user, { - name: org, - domain: org, - }, { - setUserAsOwner: false, - useNewPlan: true, - product: 'teamFree', - })); - - const teamOrgId = db.unwrapQueryResult(await db.addOrg(user, { - name: 'fix-team-org', - domain: 'fix-team-org', - }, { - setUserAsOwner: false, - useNewPlan: true, - product: 'team', - })); - - // Make sure it is created with teamFree product. - assert.equal(await productOrg(freeOrgId), 'teamFree'); - - // Run the fixer. - assert.isTrue(await fixSiteProducts({ - db, - deploymentType: server.server.getDeploymentType(), - })); - - // Make sure we fixed the product is on Free product. - assert.equal(await productOrg(freeOrgId), 'Free'); - - // Make sure the other org is still on team product. - assert.equal(await productOrg(teamOrgId), 'team'); - }); - - it("doesn't run when on saas deployment", async function() { - process.env.GRIST_TEST_SERVER_DEPLOYMENT_TYPE = 'saas'; - - // Stub it in the server. Notice that we assume some knowledge about how the server is implemented - that it won't - // cache this value (nor any other component) and always read it when needed. Otherwise we would need to recreate - // the server each time. - const sandbox = sinon.createSandbox(); - sandbox.stub(server.server, 'getDeploymentType').returns('saas'); - assert.equal(server.server.getDeploymentType(), 'saas'); - - assert.isFalse(await fixSiteProducts({ - db: server.dbManager, - deploymentType: server.server.getDeploymentType(), - })); - - sandbox.restore(); - }); - - it("doesn't run when default product is not set", async function() { - // Make sure we are in 'core'. - assert.equal(server.server.getDeploymentType(), 'core'); - - // But only when Free product is the default one. - process.env.GRIST_DEFAULT_PRODUCT = 'teamFree'; - assert.equal(getDefaultProductNames().teamInitial, 'teamFree'); // sanity check that Grist sees it. - - assert.isFalse(await fixSiteProducts({ - db: server.dbManager, - deploymentType: server.server.getDeploymentType(), - })); - - process.env.GRIST_DEFAULT_PRODUCT = 'team'; - assert.equal(getDefaultProductNames().teamInitial, 'team'); - - assert.isFalse(await fixSiteProducts({ - db: server.dbManager, - deploymentType: server.server.getDeploymentType(), - })); - - delete process.env.GRIST_DEFAULT_PRODUCT; - assert.equal(getDefaultProductNames().teamInitial, 'stub'); - - const db = server.dbManager; - const user = await db.getUserByLogin(email, {profile}); - const orgId = db.unwrapQueryResult(await db.addOrg(user, { - name: 'sanity-check-org', - domain: 'sanity-check-org', - }, { - setUserAsOwner: false, - useNewPlan: true, - product: 'teamFree', - })); - - const getOrg = (id: number) => db.connection.manager.findOne(Organization, - {where: {id}, relations: ['billingAccount', 'billingAccount.product']}); - const productOrg = (id: number) => getOrg(id)?.then(org => org?.billingAccount?.product?.name); - assert.equal(await productOrg(orgId), 'teamFree'); - - assert.isFalse(await fixSiteProducts({ - db: server.dbManager, - deploymentType: server.server.getDeploymentType(), - })); - assert.equal(await productOrg(orgId), 'teamFree'); - }); -});