mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) updates from grist-core
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -30,12 +30,16 @@ export function fieldInsertPositions(viewFields: KoArray<ViewFieldRec>, 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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 ?
|
||||
|
||||
@@ -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<T>(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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1397,8 +1397,9 @@ export class FlexServer implements GristServer {
|
||||
}
|
||||
}
|
||||
|
||||
public async checkSandbox() {
|
||||
if (this._check('sandbox', 'doc')) { return; }
|
||||
public async getSandboxInfo(): Promise<SandboxInfo> {
|
||||
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 {
|
||||
|
||||
@@ -70,7 +70,7 @@ export interface GristServer {
|
||||
servesPlugins(): boolean;
|
||||
getBundledWidgets(): ICustomWidget[];
|
||||
getBootKey(): string|undefined;
|
||||
getSandboxInfo(): SandboxInfo|undefined;
|
||||
getSandboxInfo(): Promise<SandboxInfo>;
|
||||
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'); },
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user