mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) updates from grist-core
This commit is contained in:
commit
05d8976c5b
@ -107,6 +107,7 @@ Base.setBaseFor(Clipboard);
|
|||||||
|
|
||||||
Clipboard.commands = {
|
Clipboard.commands = {
|
||||||
contextMenuCopy: function() { this._doContextMenuCopy(); },
|
contextMenuCopy: function() { this._doContextMenuCopy(); },
|
||||||
|
contextMenuCopyWithHeaders: function() { this._doContextMenuCopyWithHeaders(); },
|
||||||
contextMenuCut: function() { this._doContextMenuCut(); },
|
contextMenuCut: function() { this._doContextMenuCut(); },
|
||||||
contextMenuPaste: function() { this._doContextMenuPaste(); },
|
contextMenuPaste: function() { this._doContextMenuPaste(); },
|
||||||
};
|
};
|
||||||
@ -126,7 +127,13 @@ Clipboard.prototype._onCopy = function(elem, event) {
|
|||||||
Clipboard.prototype._doContextMenuCopy = function() {
|
Clipboard.prototype._doContextMenuCopy = function() {
|
||||||
let pasteObj = commands.allCommands.copy.run();
|
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) {
|
Clipboard.prototype._onCut = function(elem, event) {
|
||||||
@ -146,21 +153,21 @@ Clipboard.prototype._doContextMenuCut = function() {
|
|||||||
Clipboard.prototype._setCBdata = function(pasteObj, clipboardData) {
|
Clipboard.prototype._setCBdata = function(pasteObj, clipboardData) {
|
||||||
if (!pasteObj) { return; }
|
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);
|
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);
|
clipboardData.setData('text/html', htmlText);
|
||||||
|
|
||||||
this._setCutCallback(pasteObj, plainText);
|
this._setCutCallback(pasteObj, plainText);
|
||||||
};
|
};
|
||||||
|
|
||||||
Clipboard.prototype._copyToClipboard = async function(pasteObj, action) {
|
Clipboard.prototype._copyToClipboard = async function(pasteObj, action, includeColHeaders) {
|
||||||
if (!pasteObj) { return; }
|
if (!pasteObj) { return; }
|
||||||
|
|
||||||
const plainText = tableUtil.makePasteText(pasteObj.data, pasteObj.selection);
|
const plainText = tableUtil.makePasteText(pasteObj.data, pasteObj.selection, includeColHeaders);
|
||||||
let data;
|
let data;
|
||||||
if (typeof ClipboardItem === 'function') {
|
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
|
// eslint-disable-next-line no-undef
|
||||||
data = new ClipboardItem({
|
data = new ClipboardItem({
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
|
@ -63,6 +63,7 @@ export type CommandName =
|
|||||||
| 'cut'
|
| 'cut'
|
||||||
| 'paste'
|
| 'paste'
|
||||||
| 'contextMenuCopy'
|
| 'contextMenuCopy'
|
||||||
|
| 'contextMenuCopyWithHeaders'
|
||||||
| 'contextMenuCut'
|
| 'contextMenuCut'
|
||||||
| 'contextMenuPaste'
|
| 'contextMenuPaste'
|
||||||
| 'fillSelectionDown'
|
| 'fillSelectionDown'
|
||||||
@ -470,6 +471,10 @@ export const groups: CommendGroupDef[] = [{
|
|||||||
keys: ['Mod+C'],
|
keys: ['Mod+C'],
|
||||||
desc: 'Copy current selection to clipboard',
|
desc: 'Copy current selection to clipboard',
|
||||||
bindKeys: false,
|
bindKeys: false,
|
||||||
|
}, {
|
||||||
|
name: 'contextMenuCopyWithHeaders',
|
||||||
|
keys: [],
|
||||||
|
desc: 'Copy current selection to clipboard including headers',
|
||||||
}, {
|
}, {
|
||||||
name: 'contextMenuCut',
|
name: 'contextMenuCut',
|
||||||
keys: ['Mod+X'],
|
keys: ['Mod+X'],
|
||||||
|
@ -30,12 +30,16 @@ export function fieldInsertPositions(viewFields: KoArray<ViewFieldRec>, index: n
|
|||||||
* @param {CopySelection} selection - a CopySelection instance
|
* @param {CopySelection} selection - a CopySelection instance
|
||||||
* @return {String}
|
* @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
|
// 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"]]
|
// i.e. [["1-1", "1-2", "1-3"],["2-1", "2-2", "2-3"]]
|
||||||
const values = selection.rowIds.map(rowId =>
|
const result = [];
|
||||||
selection.columns.map(col => col.fmtGetter(rowId)));
|
if (includeColHeaders) {
|
||||||
return tsvEncode(values);
|
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.
|
// Include column headers if requested.
|
||||||
(includeColHeaders ?
|
(includeColHeaders ?
|
||||||
dom('tr', selection.colIds.map(colId => dom('th', colId))) :
|
dom('tr', selection.fields.map(field => dom('th', field.label()))) :
|
||||||
null
|
null
|
||||||
),
|
),
|
||||||
// Fill with table cells.
|
// Fill with table cells.
|
||||||
|
@ -202,7 +202,10 @@ Please log in as an administrator.`)),
|
|||||||
const success = result?.status === 'success';
|
const success = result?.status === 'success';
|
||||||
const details = result?.details as SandboxingBootProbeDetails|undefined;
|
const details = result?.details as SandboxingBootProbeDetails|undefined;
|
||||||
if (!details) {
|
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 flavor = details.flavor;
|
||||||
const configured = details.configured;
|
const configured = details.configured;
|
||||||
|
@ -38,6 +38,7 @@ export function CellContextMenu(cellOptions: ICellContextMenu, colOptions: IMult
|
|||||||
result.push(
|
result.push(
|
||||||
menuItemCmd(allCommands.contextMenuCut, t('Cut'), disableForReadonlyColumn),
|
menuItemCmd(allCommands.contextMenuCut, t('Cut'), disableForReadonlyColumn),
|
||||||
menuItemCmd(allCommands.contextMenuCopy, t('Copy')),
|
menuItemCmd(allCommands.contextMenuCopy, t('Copy')),
|
||||||
|
menuItemCmd(allCommands.contextMenuCopyWithHeaders, t('Copy with headers')),
|
||||||
menuItemCmd(allCommands.contextMenuPaste, t('Paste'), disableForReadonlyColumn),
|
menuItemCmd(allCommands.contextMenuPaste, t('Paste'), disableForReadonlyColumn),
|
||||||
menuDivider(),
|
menuDivider(),
|
||||||
colOptions.isFormula ?
|
colOptions.isFormula ?
|
||||||
|
@ -2,10 +2,8 @@ import { ApiError } from 'app/common/ApiError';
|
|||||||
import { delay } from 'app/common/delay';
|
import { delay } from 'app/common/delay';
|
||||||
import { buildUrlId } from 'app/common/gristUrls';
|
import { buildUrlId } from 'app/common/gristUrls';
|
||||||
import { normalizedDateTimeString } from 'app/common/normalizedDateTimeString';
|
import { normalizedDateTimeString } from 'app/common/normalizedDateTimeString';
|
||||||
import { BillingAccount } from 'app/gen-server/entity/BillingAccount';
|
|
||||||
import { Document } from 'app/gen-server/entity/Document';
|
import { Document } from 'app/gen-server/entity/Document';
|
||||||
import { Organization } from 'app/gen-server/entity/Organization';
|
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 { Workspace } from 'app/gen-server/entity/Workspace';
|
||||||
import { HomeDBManager, Scope } from 'app/gen-server/lib/homedb/HomeDBManager';
|
import { HomeDBManager, Scope } from 'app/gen-server/lib/homedb/HomeDBManager';
|
||||||
import { fromNow } from 'app/gen-server/sqlUtils';
|
import { fromNow } from 'app/gen-server/sqlUtils';
|
||||||
@ -438,63 +436,3 @@ async function forEachWithBreaks<T>(logText: string, items: T[], callback: (item
|
|||||||
}
|
}
|
||||||
log.rawInfo(logText, {itemsProcesssed, itemsTotal, timeMs: Date.now() - start});
|
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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
@ -192,12 +192,6 @@ export class MergedServer {
|
|||||||
this.flexServer.checkOptionCombinations();
|
this.flexServer.checkOptionCombinations();
|
||||||
this.flexServer.summary();
|
this.flexServer.summary();
|
||||||
this.flexServer.ready();
|
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) {
|
} catch(e) {
|
||||||
await this.flexServer.close();
|
await this.flexServer.close();
|
||||||
throw e;
|
throw e;
|
||||||
|
@ -265,7 +265,7 @@ const _sandboxingProbe: Probe = {
|
|||||||
id: 'sandboxing',
|
id: 'sandboxing',
|
||||||
name: 'Is document sandboxing effective',
|
name: 'Is document sandboxing effective',
|
||||||
apply: async (server, req) => {
|
apply: async (server, req) => {
|
||||||
const details = server.getSandboxInfo();
|
const details = await server.getSandboxInfo();
|
||||||
return {
|
return {
|
||||||
status: (details?.configured && details?.functional) ? 'success' : 'fault',
|
status: (details?.configured && details?.functional) ? 'success' : 'fault',
|
||||||
details,
|
details,
|
||||||
|
@ -1397,8 +1397,9 @@ export class FlexServer implements GristServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async checkSandbox() {
|
public async getSandboxInfo(): Promise<SandboxInfo> {
|
||||||
if (this._check('sandbox', 'doc')) { return; }
|
if (this._sandboxInfo) { return this._sandboxInfo; }
|
||||||
|
|
||||||
const flavor = process.env.GRIST_SANDBOX_FLAVOR || 'unknown';
|
const flavor = process.env.GRIST_SANDBOX_FLAVOR || 'unknown';
|
||||||
const info = this._sandboxInfo = {
|
const info = this._sandboxInfo = {
|
||||||
flavor,
|
flavor,
|
||||||
@ -1408,6 +1409,8 @@ export class FlexServer implements GristServer {
|
|||||||
sandboxed: false,
|
sandboxed: false,
|
||||||
lastSuccessfulStep: 'none',
|
lastSuccessfulStep: 'none',
|
||||||
} as SandboxInfo;
|
} as SandboxInfo;
|
||||||
|
// Only meaningful on instances that handle documents.
|
||||||
|
if (!this._docManager) { return info; }
|
||||||
try {
|
try {
|
||||||
const sandbox = createSandbox({
|
const sandbox = createSandbox({
|
||||||
server: this,
|
server: this,
|
||||||
@ -1432,10 +1435,7 @@ export class FlexServer implements GristServer {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
info.error = String(e);
|
info.error = String(e);
|
||||||
}
|
}
|
||||||
}
|
return info;
|
||||||
|
|
||||||
public getSandboxInfo(): SandboxInfo|undefined {
|
|
||||||
return this._sandboxInfo;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getInfo(key: string): any {
|
public getInfo(key: string): any {
|
||||||
|
@ -70,7 +70,7 @@ export interface GristServer {
|
|||||||
servesPlugins(): boolean;
|
servesPlugins(): boolean;
|
||||||
getBundledWidgets(): ICustomWidget[];
|
getBundledWidgets(): ICustomWidget[];
|
||||||
getBootKey(): string|undefined;
|
getBootKey(): string|undefined;
|
||||||
getSandboxInfo(): SandboxInfo|undefined;
|
getSandboxInfo(): Promise<SandboxInfo>;
|
||||||
getInfo(key: string): any;
|
getInfo(key: string): any;
|
||||||
getJobs(): GristJobs;
|
getJobs(): GristJobs;
|
||||||
}
|
}
|
||||||
@ -165,7 +165,7 @@ export function createDummyGristServer(): GristServer {
|
|||||||
getPlugins() { return []; },
|
getPlugins() { return []; },
|
||||||
getBundledWidgets() { return []; },
|
getBundledWidgets() { return []; },
|
||||||
getBootKey() { return undefined; },
|
getBootKey() { return undefined; },
|
||||||
getSandboxInfo() { return undefined; },
|
getSandboxInfo() { throw new Error('no sandbox'); },
|
||||||
getInfo(key: string) { return undefined; },
|
getInfo(key: string) { return undefined; },
|
||||||
getJobs(): GristJobs { throw new Error('no job system'); },
|
getJobs(): GristJobs { throw new Error('no job system'); },
|
||||||
};
|
};
|
||||||
|
@ -1 +1 @@
|
|||||||
0.9.9
|
0.9.10
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "grist-core",
|
"name": "grist-core",
|
||||||
"version": "1.1.18",
|
"version": "1.2.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "Grist is the evolution of spreadsheets",
|
"description": "Grist is the evolution of spreadsheets",
|
||||||
"homepage": "https://github.com/gristlabs/grist-core",
|
"homepage": "https://github.com/gristlabs/grist-core",
|
||||||
|
@ -148,7 +148,8 @@
|
|||||||
"Comment": "Comment",
|
"Comment": "Comment",
|
||||||
"Copy": "Copy",
|
"Copy": "Copy",
|
||||||
"Cut": "Cut",
|
"Cut": "Cut",
|
||||||
"Paste": "Paste"
|
"Paste": "Paste",
|
||||||
|
"Copy with headers": "Copy with headers"
|
||||||
},
|
},
|
||||||
"ChartView": {
|
"ChartView": {
|
||||||
"Create separate series for each value of the selected column.": "Create separate series for each value of the selected column.",
|
"Create separate series for each value of the selected column.": "Create separate series for each value of the selected column.",
|
||||||
@ -1213,7 +1214,10 @@
|
|||||||
"Community widgets are created and maintained by Grist community members.": "Community widgets are created and maintained by Grist community members.",
|
"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.",
|
"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.",
|
"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": {
|
"DescriptionConfig": {
|
||||||
"DESCRIPTION": "DESCRIPTION"
|
"DESCRIPTION": "DESCRIPTION"
|
||||||
@ -1594,7 +1598,8 @@
|
|||||||
"Key to sign sessions with": "Key to sign sessions with",
|
"Key to sign sessions with": "Key to sign sessions with",
|
||||||
"Session Secret": "Session Secret",
|
"Session Secret": "Session Secret",
|
||||||
"Enable Grist Enterprise": "Enable Grist Enterprise",
|
"Enable Grist Enterprise": "Enable Grist Enterprise",
|
||||||
"Enterprise": "Enterprise"
|
"Enterprise": "Enterprise",
|
||||||
|
"checking": "checking"
|
||||||
},
|
},
|
||||||
"Columns": {
|
"Columns": {
|
||||||
"Remove Column": "Remove Column"
|
"Remove Column": "Remove Column"
|
||||||
@ -1761,7 +1766,9 @@
|
|||||||
"Delete column {{column}} in table {{table}}?": "Delete column {{column}} in table {{table}}?",
|
"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}}.",
|
"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",
|
"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": {
|
"SupportGristButton": {
|
||||||
"Admin Panel": "Admin Panel",
|
"Admin Panel": "Admin Panel",
|
||||||
|
@ -7,7 +7,6 @@
|
|||||||
import {commonUrls} from 'app/common/gristUrls';
|
import {commonUrls} from 'app/common/gristUrls';
|
||||||
import {isAffirmative} from 'app/common/gutil';
|
import {isAffirmative} from 'app/common/gutil';
|
||||||
import {HomeDBManager} from 'app/gen-server/lib/homedb/HomeDBManager';
|
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);
|
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) {
|
if (process.env.GRIST_SERVE_PLUGINS_PORT) {
|
||||||
await mergedServer.flexServer.startCopy('pluginServer', parseInt(process.env.GRIST_SERVE_PLUGINS_PORT, 10));
|
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;
|
return mergedServer.flexServer;
|
||||||
}
|
}
|
||||||
|
@ -637,7 +637,7 @@ async function copyAndCheck(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createDummyTextArea() {
|
export function createDummyTextArea() {
|
||||||
const textarea = document.createElement('textarea');
|
const textarea = document.createElement('textarea');
|
||||||
textarea.style.position = "absolute";
|
textarea.style.position = "absolute";
|
||||||
textarea.style.top = "0";
|
textarea.style.top = "0";
|
||||||
@ -647,7 +647,7 @@ function createDummyTextArea() {
|
|||||||
window.document.body.appendChild(textarea);
|
window.document.body.appendChild(textarea);
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeDummyTextArea() {
|
export function removeDummyTextArea() {
|
||||||
const textarea = document.getElementById('dummyText');
|
const textarea = document.getElementById('dummyText');
|
||||||
if (textarea) {
|
if (textarea) {
|
||||||
window.document.body.removeChild(textarea);
|
window.document.body.removeChild(textarea);
|
||||||
|
59
test/nbrowser/CopyWithHeaders.ts
Normal file
59
test/nbrowser/CopyWithHeaders.ts
Normal file
@ -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();
|
||||||
|
}
|
@ -98,7 +98,6 @@ describe('HomeIntro', function() {
|
|||||||
|
|
||||||
assert.isTrue(await driver.find('.test-intro-cards').isDisplayed());
|
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-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-create-doc').isDisplayed());
|
||||||
assert.isTrue(await driver.find('.test-intro-import-doc').isDisplayed());
|
assert.isTrue(await driver.find('.test-intro-import-doc').isDisplayed());
|
||||||
assert.isTrue(await driver.find('.test-intro-templates').isDisplayed());
|
assert.isTrue(await driver.find('.test-intro-templates').isDisplayed());
|
||||||
|
@ -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 freeOrg = db.unwrapQueryResult(await db.addOrg(user, {
|
|
||||||
name: org,
|
|
||||||
domain: org,
|
|
||||||
}, {
|
|
||||||
setUserAsOwner: false,
|
|
||||||
useNewPlan: true,
|
|
||||||
product: 'teamFree',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const teamOrg = 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(freeOrg.id), '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(freeOrg.id), 'Free');
|
|
||||||
|
|
||||||
// Make sure the other org is still on team product.
|
|
||||||
assert.equal(await productOrg(teamOrg.id), '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 org = 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(org.id), 'teamFree');
|
|
||||||
|
|
||||||
assert.isFalse(await fixSiteProducts({
|
|
||||||
db: server.dbManager,
|
|
||||||
deploymentType: server.server.getDeploymentType(),
|
|
||||||
}));
|
|
||||||
assert.equal(await productOrg(org.id), 'teamFree');
|
|
||||||
});
|
|
||||||
});
|
|
@ -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"
|
resolved "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz"
|
||||||
integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=
|
integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=
|
||||||
|
|
||||||
bullmq@^5.8.7:
|
bullmq@5.8.7:
|
||||||
version "5.8.7"
|
version "5.8.7"
|
||||||
resolved "https://registry.yarnpkg.com/bullmq/-/bullmq-5.8.7.tgz#d5da6215377fe29494d74ad307f195f7408b9e2e"
|
resolved "https://registry.yarnpkg.com/bullmq/-/bullmq-5.8.7.tgz#d5da6215377fe29494d74ad307f195f7408b9e2e"
|
||||||
integrity sha512-IdAgB9WvJHRAcZtamRLj6fbjMyuIogEa1cjOTWM1pkVoHUOpO34q6FzNMX1R8VOeUhkvkOkWcxI5ENgFLh+TVA==
|
integrity sha512-IdAgB9WvJHRAcZtamRLj6fbjMyuIogEa1cjOTWM1pkVoHUOpO34q6FzNMX1R8VOeUhkvkOkWcxI5ENgFLh+TVA==
|
||||||
|
Loading…
Reference in New Issue
Block a user