(core) updates from grist-core

This commit is contained in:
Paul Fitzpatrick 2023-12-01 08:14:12 -05:00
commit d89e008a75
29 changed files with 163 additions and 89 deletions

View File

@ -260,6 +260,7 @@ GRIST_SERVERS | the types of server to setup. Comma separated values which may c
GRIST_SESSION_COOKIE | if set, overrides the name of Grist's cookie GRIST_SESSION_COOKIE | if set, overrides the name of Grist's cookie
GRIST_SESSION_DOMAIN | if set, associates the cookie with the given domain - otherwise defaults to GRIST_DOMAIN GRIST_SESSION_DOMAIN | if set, associates the cookie with the given domain - otherwise defaults to GRIST_DOMAIN
GRIST_SESSION_SECRET | a key used to encode sessions GRIST_SESSION_SECRET | a key used to encode sessions
GRIST_SKIP_BUNDLED_WIDGETS | if set, Grist will ignore any bundled widgets included via NPM packages.
GRIST_ANON_PLAYGROUND | When set to 'false' deny anonymous users access to the home page GRIST_ANON_PLAYGROUND | When set to 'false' deny anonymous users access to the home page
GRIST_FORCE_LOGIN | Much like GRIST_ANON_PLAYGROUND but don't support anonymous access at all (features like sharing docs publicly requires authentication) GRIST_FORCE_LOGIN | Much like GRIST_ANON_PLAYGROUND but don't support anonymous access at all (features like sharing docs publicly requires authentication)
GRIST_SINGLE_ORG | set to an org "domain" to pin client to that org GRIST_SINGLE_ORG | set to an org "domain" to pin client to that org
@ -396,6 +397,7 @@ TYPEORM_PASSWORD | password to use
TYPEORM_PORT | port number for db if not the default for that db type TYPEORM_PORT | port number for db if not the default for that db type
TYPEORM_TYPE | set to 'sqlite' or 'postgres' TYPEORM_TYPE | set to 'sqlite' or 'postgres'
TYPEORM_USERNAME | username to connect as TYPEORM_USERNAME | username to connect as
TYPEORM_EXTRA | any other properties to pass to TypeORM in JSON format
#### Testing: #### Testing:

View File

@ -41,7 +41,7 @@ export type PageType =
| "billing" | "billing"
| "welcome" | "welcome"
| "account" | "account"
| "support-grist" | "support"
| "activation"; | "activation";
const G = getBrowserGlobals('document', 'window'); const G = getBrowserGlobals('document', 'window');
@ -316,7 +316,7 @@ export class AppModelImpl extends Disposable implements AppModel {
} else if (state.account) { } else if (state.account) {
return 'account'; return 'account';
} else if (state.supportGrist) { } else if (state.supportGrist) {
return 'support-grist'; return 'support';
} else if (state.activation) { } else if (state.activation) {
return 'activation'; return 'activation';
} else { } else {

View File

@ -226,7 +226,7 @@ export class AccountWidget extends Disposable {
return menuItemLink( return menuItemLink(
t('Support Grist'), t('Support Grist'),
cssHeartIcon('💛'), cssHeartIcon('💛'),
urlState().setLinkUrl({supportGrist: 'support-grist'}), urlState().setLinkUrl({supportGrist: 'support'}),
testId('usermenu-support-grist'), testId('usermenu-support-grist'),
); );
} }

View File

@ -77,7 +77,7 @@ function createMainPage(appModel: AppModel, appObj: App) {
return dom.create(WelcomePage, appModel); return dom.create(WelcomePage, appModel);
} else if (pageType === 'account') { } else if (pageType === 'account') {
return domAsync(loadAccountPage().then(ap => dom.create(ap.AccountPage, appModel))); return domAsync(loadAccountPage().then(ap => dom.create(ap.AccountPage, appModel)));
} else if (pageType === 'support-grist') { } else if (pageType === 'support') {
return domAsync(loadSupportGristPage().then(sgp => dom.create(sgp.SupportGristPage, appModel))); return domAsync(loadSupportGristPage().then(sgp => dom.create(sgp.SupportGristPage, appModel)));
} else if (pageType === 'activation') { } else if (pageType === 'activation') {
return domAsync(loadActivationPage().then(ap => dom.create(ap.ActivationPage, appModel))); return domAsync(loadActivationPage().then(ap => dom.create(ap.ActivationPage, appModel)));

View File

@ -22,7 +22,7 @@ type ButtonState =
| 'expanded'; | 'expanded';
type CardPage = type CardPage =
| 'support-grist' | 'support'
| 'opted-in'; | 'opted-in';
/** /**
@ -45,7 +45,7 @@ export class SupportGristNudge extends Disposable {
this._buttonState = localStorageObs( this._buttonState = localStorageObs(
`u=${this._appModel.currentValidUser?.id ?? 0};supportGristNudge`, 'expanded' `u=${this._appModel.currentValidUser?.id ?? 0};supportGristNudge`, 'expanded'
) as Observable<ButtonState>; ) as Observable<ButtonState>;
this._currentPage = Observable.create(null, 'support-grist'); this._currentPage = Observable.create(null, 'support');
this._isClosed = Observable.create(this, false); this._isClosed = Observable.create(this, false);
} }
@ -122,7 +122,7 @@ export class SupportGristNudge extends Disposable {
private _buildCard() { private _buildCard() {
return cssCard( return cssCard(
dom.domComputed(this._currentPage, page => { dom.domComputed(this._currentPage, page => {
if (page === 'support-grist') { if (page === 'support') {
return this._buildSupportGristCardContent(); return this._buildSupportGristCardContent();
} else { } else {
return this._buildOptedInCardContent(); return this._buildOptedInCardContent();
@ -205,7 +205,7 @@ function helpCenterLink() {
function supportGristLink() { function supportGristLink() {
return cssLink( return cssLink(
t('Support Grist page'), t('Support Grist page'),
{href: urlState().makeUrl({supportGrist: 'support-grist'}), target: '_blank'}, {href: urlState().makeUrl({supportGrist: 'support'}), target: '_blank'},
); );
} }

View File

@ -194,7 +194,7 @@ export class SupportGristPage extends Disposable {
const suffix = getPageTitleSuffix(getGristConfig()); const suffix = getPageTitleSuffix(getGristConfig());
switch (page) { switch (page) {
case undefined: case undefined:
case 'support-grist': { case 'support': {
return document.title = `Support Grist${suffix}`; return document.title = `Support Grist${suffix}`;
} }
} }

View File

@ -43,7 +43,7 @@ export type ActivationPage = typeof ActivationPage.type;
export const LoginPage = StringUnion('signup', 'login', 'verified', 'forgot-password'); export const LoginPage = StringUnion('signup', 'login', 'verified', 'forgot-password');
export type LoginPage = typeof LoginPage.type; export type LoginPage = typeof LoginPage.type;
export const SupportGristPage = StringUnion('support-grist'); export const SupportGristPage = StringUnion('support');
export type SupportGristPage = typeof SupportGristPage.type; export type SupportGristPage = typeof SupportGristPage.type;
// Overall UI style. "full" is normal, "singlePage" is a single page focused, panels hidden experience. // Overall UI style. "full" is normal, "singlePage" is a single page focused, panels hidden experience.
@ -408,8 +408,8 @@ export function decodeUrl(gristConfig: Partial<GristLoadConfig>, location: Locat
state.activation = ActivationPage.parse(map.get('activation')) || 'activation'; state.activation = ActivationPage.parse(map.get('activation')) || 'activation';
} }
if (map.has('welcome')) { state.welcome = WelcomePage.parse(map.get('welcome')); } if (map.has('welcome')) { state.welcome = WelcomePage.parse(map.get('welcome')); }
if (map.has('support-grist')) { if (map.has('support')) {
state.supportGrist = SupportGristPage.parse(map.get('support-grist')) || 'support-grist'; state.supportGrist = SupportGristPage.parse(map.get('support')) || 'support';
} }
if (sp.has('planType')) { state.params!.planType = sp.get('planType')!; } if (sp.has('planType')) { state.params!.planType = sp.get('planType')!; }
if (sp.has('billingPlan')) { state.params!.billingPlan = sp.get('billingPlan')!; } if (sp.has('billingPlan')) { state.params!.billingPlan = sp.get('billingPlan')!; }

View File

@ -212,6 +212,6 @@ export function attachAppEndpoint(options: AttachOptions): void {
// The * is a wildcard in express 4, rather than a regex symbol. // The * is a wildcard in express 4, rather than a regex symbol.
// See https://expressjs.com/en/guide/routing.html // See https://expressjs.com/en/guide/routing.html
app.get('/doc/:urlId([^/]+):remainder(*)', ...docMiddleware, docHandler); app.get('/doc/:urlId([^/]+):remainder(*)', ...docMiddleware, docHandler);
app.get('/:urlId([^/]{12,})/:slug([^/]+):remainder(*)', app.get('/:urlId([^/]{12,})(/:slug([^/]+):remainder(*))?',
...docMiddleware, docHandler); ...docMiddleware, docHandler);
} }

View File

@ -1998,8 +1998,13 @@ export class FlexServer implements GristServer {
// Only used as {userRoot}/plugins as a place for plugins in addition to {appRoot}/plugins // Only used as {userRoot}/plugins as a place for plugins in addition to {appRoot}/plugins
const userRoot = path.resolve(process.env.GRIST_USER_ROOT || getAppPathTo(this.appRoot, '.grist')); const userRoot = path.resolve(process.env.GRIST_USER_ROOT || getAppPathTo(this.appRoot, '.grist'));
this.info.push(['userRoot', userRoot]); this.info.push(['userRoot', userRoot]);
// Some custom widgets may be included as an npm package called @gristlabs/grist-widget.
const pluginManager = new PluginManager(this.appRoot, userRoot); const bundledRoot = isAffirmative(process.env.GRIST_SKIP_BUNDLED_WIDGETS) ? undefined : path.join(
getAppPathTo(this.appRoot, 'node_modules'),
'@gristlabs', 'grist-widget', 'dist'
);
this.info.push(['bundledRoot', bundledRoot]);
const pluginManager = new PluginManager(this.appRoot, userRoot, bundledRoot);
// `initialize()` is asynchronous and reads plugins manifests; if PluginManager is used before it // `initialize()` is asynchronous and reads plugins manifests; if PluginManager is used before it
// finishes, it will act as if there are no plugins. // finishes, it will act as if there are no plugins.
// ^ I think this comment was here to justify calling initialize without waiting for // ^ I think this comment was here to justify calling initialize without waiting for

View File

@ -46,7 +46,8 @@ function servePluginContent(req: express.Request, res: express.Response,
req.get('X-From-Plugin-WebView') === "true" || req.get('X-From-Plugin-WebView') === "true" ||
mimeTypes.lookup(path.extname(pluginPath)) === "application/javascript") { mimeTypes.lookup(path.extname(pluginPath)) === "application/javascript") {
const dirs = pluginManager.dirs(); const dirs = pluginManager.dirs();
const contentRoot = pluginKind === "installed" ? dirs.installed : dirs.builtIn; const contentRoot = pluginKind === "installed" ? dirs.installed :
(pluginKind === "builtIn" ? dirs.builtIn : dirs.bundled);
// Note that pluginPath may not be safe, but `sendFile` with the "root" option restricts // Note that pluginPath may not be safe, but `sendFile` with the "root" option restricts
// relative paths to be within the root folder (see the 3rd party library unit-test: // relative paths to be within the root folder (see the 3rd party library unit-test:
// https://github.com/pillarjs/send/blob/3daa901cf731b86187e4449fa2c52f971e0b3dbc/test/send.js#L1363) // https://github.com/pillarjs/send/blob/3daa901cf731b86187e4449fa2c52f971e0b3dbc/test/send.js#L1363)

View File

@ -14,9 +14,14 @@ export interface PluginDirectories {
*/ */
readonly builtIn?: string; readonly builtIn?: string;
/** /**
* Directory where user installed plugins are localted. * Directory where user installed plugins are located.
*/ */
readonly installed?: string; readonly installed?: string;
/**
* Yet another option, for plugins that are included
* during a build but not part of the codebase itself.
*/
readonly bundled?: string;
} }
/** /**
@ -44,10 +49,12 @@ export class PluginManager {
* @param {string} userRoot: path to user's grist directory; `null` is allowed, to only uses built in plugins. * @param {string} userRoot: path to user's grist directory; `null` is allowed, to only uses built in plugins.
* *
*/ */
public constructor(public appRoot?: string, userRoot?: string) { public constructor(public appRoot?: string, userRoot?: string,
public bundledRoot?: string) {
this._dirs = { this._dirs = {
installed: userRoot ? path.join(userRoot, 'plugins') : undefined, installed: userRoot ? path.join(userRoot, 'plugins') : undefined,
builtIn: appRoot ? getAppPathTo(appRoot, 'plugins') : undefined builtIn: appRoot ? getAppPathTo(appRoot, 'plugins') : undefined,
bundled: bundledRoot ? getAppPathTo(bundledRoot, 'plugins') : undefined,
}; };
} }
@ -91,6 +98,11 @@ export class PluginManager {
this._entries.push(...await scanDirectory(this._dirs.builtIn, "builtIn")); this._entries.push(...await scanDirectory(this._dirs.builtIn, "builtIn"));
} }
// Load bundled plugins
if (this._dirs.bundled) {
this._entries.push(...await scanDirectory(this._dirs.bundled, "bundled"));
}
if (!process.env.GRIST_EXPERIMENTAL_PLUGINS || if (!process.env.GRIST_EXPERIMENTAL_PLUGINS ||
process.env.GRIST_EXPERIMENTAL_PLUGINS === '0') { process.env.GRIST_EXPERIMENTAL_PLUGINS === '0') {
// Remove experimental plugins // Remove experimental plugins
@ -130,7 +142,7 @@ export class PluginManager {
} }
async function scanDirectory(dir: string, kind: "installed"|"builtIn"): Promise<DirectoryScanEntry[]> { async function scanDirectory(dir: string, kind: "installed"|"builtIn"|"bundled"): Promise<DirectoryScanEntry[]> {
const plugins: DirectoryScanEntry[] = []; const plugins: DirectoryScanEntry[] = [];
let listDir; let listDir;

View File

@ -187,7 +187,7 @@ export class Telemetry implements ITelemetry {
public addPages(app: express.Application, middleware: express.RequestHandler[]) { public addPages(app: express.Application, middleware: express.RequestHandler[]) {
if (this._deploymentType === 'core') { if (this._deploymentType === 'core') {
app.get('/support-grist', ...middleware, expressWrap(async (req, resp) => { app.get('/support', ...middleware, expressWrap(async (req, resp) => {
return this._gristServer.sendAppPage(req, resp, return this._gristServer.sendAppPage(req, resp,
{path: 'app.html', status: 200, config: {}}); {path: 'app.html', status: 200, config: {}});
})); }));

View File

@ -142,6 +142,7 @@ export function getTypeORMSettings(): DataSourceOptions {
"subscribers": [ "subscribers": [
`${codeRoot}/app/gen-server/subscriber/*.js` `${codeRoot}/app/gen-server/subscriber/*.js`
], ],
...JSON.parse(process.env.TYPEORM_EXTRA || "{}"),
...cache, ...cache,
}; };
} }

View File

@ -13,8 +13,8 @@
"install:python3": "buildtools/prepare_python3.sh", "install:python3": "buildtools/prepare_python3.sh",
"build:prod": "buildtools/build.sh", "build:prod": "buildtools/build.sh",
"start:prod": "sandbox/run.sh", "start:prod": "sandbox/run.sh",
"test": "GRIST_SESSION_COOKIE=grist_test_cookie GRIST_TEST_LOGIN=1 TEST_SUPPORT_API_KEY=api_key_for_support TEST_CLEAN_DATABASE=true mocha ${DEBUG:+-b --no-exit} --slow 8000 $([ -z $DEBUG ] && echo --forbid-only) -g \"${GREP_TESTS}\" '_build/test/common/*.js' '_build/test/client/*.js' '_build/test/nbrowser/*.js' '_build/test/server/**/*.js' '_build/test/gen-server/**/*.js'", "test": "GRIST_SESSION_COOKIE=grist_test_cookie GRIST_TEST_LOGIN=1 TEST_SUPPORT_API_KEY=api_key_for_support TEST_CLEAN_DATABASE=true LANGUAGE=en_US mocha ${DEBUG:+-b --no-exit} --slow 8000 $([ -z $DEBUG ] && echo --forbid-only) -g \"${GREP_TESTS}\" '_build/test/common/*.js' '_build/test/client/*.js' '_build/test/nbrowser/*.js' '_build/test/server/**/*.js' '_build/test/gen-server/**/*.js'",
"test:nbrowser": "TEST_SUITE=nbrowser TEST_SUITE_FOR_TIMINGS=nbrowser TIMINGS_FILE=test/timings/nbrowser.txt GRIST_SESSION_COOKIE=grist_test_cookie GRIST_TEST_LOGIN=1 TEST_SUPPORT_API_KEY=api_key_for_support TEST_CLEAN_DATABASE=true mocha ${DEBUG:+-b --no-exit} $([ -z $DEBUG ] && echo --forbid-only) -g \"${GREP_TESTS}\" --slow 8000 -R test/xunit-file '_build/test/nbrowser/**/*.js'", "test:nbrowser": "TEST_SUITE=nbrowser TEST_SUITE_FOR_TIMINGS=nbrowser TIMINGS_FILE=test/timings/nbrowser.txt GRIST_SESSION_COOKIE=grist_test_cookie GRIST_TEST_LOGIN=1 TEST_SUPPORT_API_KEY=api_key_for_support TEST_CLEAN_DATABASE=true LANGUAGE=en_US mocha ${DEBUG:+-b --no-exit} $([ -z $DEBUG ] && echo --forbid-only) -g \"${GREP_TESTS}\" --slow 8000 -R test/xunit-file '_build/test/nbrowser/**/*.js'",
"test:client": "GRIST_SESSION_COOKIE=grist_test_cookie mocha ${DEBUG:+'-b'} '_build/test/client/**/*.js'", "test:client": "GRIST_SESSION_COOKIE=grist_test_cookie mocha ${DEBUG:+'-b'} '_build/test/client/**/*.js'",
"test:common": "GRIST_SESSION_COOKIE=grist_test_cookie mocha ${DEBUG:+'-b'} '_build/test/common/**/*.js'", "test:common": "GRIST_SESSION_COOKIE=grist_test_cookie mocha ${DEBUG:+'-b'} '_build/test/common/**/*.js'",
"test:server": "TEST_SUITE=server TEST_SUITE_FOR_TIMINGS=server TIMINGS_FILE=test/timings/server.txt GRIST_SESSION_COOKIE=grist_test_cookie mocha ${DEBUG:+'-b'} -R test/xunit-file '_build/test/server/**/*.js' '_build/test/gen-server/**/*.js'", "test:server": "TEST_SUITE=server TEST_SUITE_FOR_TIMINGS=server TIMINGS_FILE=test/timings/server.txt GRIST_SESSION_COOKIE=grist_test_cookie mocha ${DEBUG:+'-b'} -R test/xunit-file '_build/test/server/**/*.js' '_build/test/gen-server/**/*.js'",
@ -114,6 +114,7 @@
"@googleapis/oauth2": "0.2.0", "@googleapis/oauth2": "0.2.0",
"@gristlabs/connect-sqlite3": "0.9.11-grist.5", "@gristlabs/connect-sqlite3": "0.9.11-grist.5",
"@gristlabs/express-session": "1.17.0", "@gristlabs/express-session": "1.17.0",
"@gristlabs/grist-widget": "^0.0.4",
"@gristlabs/moment-guess": "1.2.4-grist.1", "@gristlabs/moment-guess": "1.2.4-grist.1",
"@gristlabs/pidusage": "2.0.17", "@gristlabs/pidusage": "2.0.17",
"@gristlabs/sqlite3": "5.1.4-grist.8", "@gristlabs/sqlite3": "5.1.4-grist.8",

View File

@ -31,7 +31,7 @@ function check_gvisor {
return return
fi fi
# Check if a trivial command works under gvisor with the proposed flags. # Check if a trivial command works under gvisor with the proposed flags.
if runsc --network none "$@" do true 2> /dev/null; then if runsc --network none "$@" "do" true 2> /dev/null; then
export GVISOR_FLAGS="$@" export GVISOR_FLAGS="$@"
export GVISOR_AVAILABLE=1 export GVISOR_AVAILABLE=1
fi fi
@ -40,9 +40,9 @@ function check_gvisor {
check_gvisor --unprivileged --ignore-cgroups check_gvisor --unprivileged --ignore-cgroups
check_gvisor --unprivileged check_gvisor --unprivileged
# If we can't use --unprivileged, stick with --rootless and no checkpoint # If we can't use --unprivileged, stick with --rootless. We will not make a checkpoint.
if [[ -z "$GVISOR_FLAGS" ]]; then
check_gvisor --rootless check_gvisor --rootless
else
if [[ "$GVISOR_FLAGS" =~ "-unprivileged" ]]; then
export GRIST_CHECKPOINT=/tmp/engine_$(echo $PWD | sed "s/[^a-zA-Z0-9]/_/g") export GRIST_CHECKPOINT=/tmp/engine_$(echo $PWD | sed "s/[^a-zA-Z0-9]/_/g")
fi fi

View File

@ -3,8 +3,17 @@
set -e set -e
if [[ "$GRIST_SANDBOX_FLAVOR" = "gvisor" ]]; then if [[ "$GRIST_SANDBOX_FLAVOR" = "gvisor" ]]; then
./sandbox/gvisor/update_engine_checkpoint.sh
source ./sandbox/gvisor/get_checkpoint_path.sh source ./sandbox/gvisor/get_checkpoint_path.sh
# Check GVISOR_FLAGS we ended up with. Don't ignore the output, it may be helpful in troubleshooting.
if runsc --network none $GVISOR_FLAGS "do" true; then
echo "gvisor check ok (flags: ${GVISOR_FLAGS})"
else
echo "gvisor check failed (flags: ${GVISOR_FLAGS}); consider different GVISOR_FLAGS or GRIST_SANDBOX_FLAVOR"
exit 1
fi
./sandbox/gvisor/update_engine_checkpoint.sh
fi fi
NODE_PATH=_build:_build/stubs:_build/ext node _build/stubs/app/server/server.js NODE_PATH=_build:_build/stubs:_build/ext node _build/stubs/app/server/server.js

View File

@ -425,7 +425,9 @@
"Duplicate in {{- label}}": "Duplicate in {{- label}}", "Duplicate in {{- label}}": "Duplicate in {{- label}}",
"No reference columns.": "No reference columns.", "No reference columns.": "No reference columns.",
"Search columns": "Search columns", "Search columns": "Search columns",
"UUID": "UUID" "UUID": "UUID",
"Add column with type": "Add column with type",
"Add formula column": "Add formula column"
}, },
"GristDoc": { "GristDoc": {
"Added new linked section to view {{viewName}}": "Added new linked section to view {{viewName}}", "Added new linked section to view {{viewName}}": "Added new linked section to view {{viewName}}",
@ -1063,7 +1065,8 @@
"A UUID is a randomly-generated string that is useful for unique identifiers and link keys.": "A UUID is a randomly-generated string that is useful for unique identifiers and link keys.", "A UUID is a randomly-generated string that is useful for unique identifiers and link keys.": "A UUID is a randomly-generated string that is useful for unique identifiers and link keys.",
"Lookups return data from related tables.": "Lookups return data from related tables.", "Lookups return data from related tables.": "Lookups return data from related tables.",
"Use reference columns to relate data in different tables.": "Use reference columns to relate data in different tables.", "Use reference columns to relate data in different tables.": "Use reference columns to relate data in different tables.",
"You can choose from widgets available to you in the dropdown, or embed your own by providing its full URL.": "You can choose from widgets available to you in the dropdown, or embed your own by providing its full URL." "You can choose from widgets available to you in the dropdown, or embed your own by providing its full URL.": "You can choose from widgets available to you in the dropdown, or embed your own by providing its full URL.",
"Formulas support many Excel functions, full Python syntax, and include a helpful AI Assistant.": "Formulas support many Excel functions, full Python syntax, and include a helpful AI Assistant."
}, },
"DescriptionConfig": { "DescriptionConfig": {
"DESCRIPTION": "DESCRIPTION" "DESCRIPTION": "DESCRIPTION"

View File

@ -205,7 +205,13 @@
"Table ID copied to clipboard": "Identifiant de table copié", "Table ID copied to clipboard": "Identifiant de table copié",
"Duplicate Table": "Dupliquer la table", "Duplicate Table": "Dupliquer la table",
"You do not have edit access to this document": "Vous navez pas accès en écriture à ce document", "You do not have edit access to this document": "Vous navez pas accès en écriture à ce document",
"Delete {{formattedTableName}} data, and remove it from all pages?": "Supprimer les données de {{formattedTableName}} et les supprimer de toutes les pages ?" "Delete {{formattedTableName}} data, and remove it from all pages?": "Supprimer les données de {{formattedTableName}} et les supprimer de toutes les pages ?",
"Edit Record Card": "Modifier la vue carte",
"Rename Table": "Renommer la table",
"{{action}} Record Card": "{{action}} la vue carte",
"Record Card": "Vue carte",
"Remove Table": "Supprimer la table",
"Record Card Disabled": "Vue carte désactivée"
}, },
"DocHistory": { "DocHistory": {
"Activity": "Activité", "Activity": "Activité",
@ -416,7 +422,9 @@
"Timestamp": "Horodatage", "Timestamp": "Horodatage",
"no reference column": "pas de colonne de référence", "no reference column": "pas de colonne de référence",
"Adding UUID column": "Ajout d'une colonne UUID", "Adding UUID column": "Ajout d'une colonne UUID",
"Adding duplicates column": "Ajouter des colonnes dupliquées" "Adding duplicates column": "Ajouter des colonnes dupliquées",
"Add formula column": "Ajouter une colonne formule",
"Add column with type": "Ajouter une colonne de type"
}, },
"GristDoc": { "GristDoc": {
"Import from file": "Importer depuis un fichier", "Import from file": "Importer depuis un fichier",
@ -613,7 +621,8 @@
"Duplicate rows_one": "Dupliquer la ligne", "Duplicate rows_one": "Dupliquer la ligne",
"Duplicate rows_other": "Dupliquer les lignes", "Duplicate rows_other": "Dupliquer les lignes",
"Delete": "Supprimer", "Delete": "Supprimer",
"Copy anchor link": "Copier l'ancre" "Copy anchor link": "Copier l'ancre",
"View as card": "Voir en carte"
}, },
"SelectionSummary": { "SelectionSummary": {
"Copied to clipboard": "Copié dans le presse-papier" "Copied to clipboard": "Copié dans le presse-papier"
@ -907,7 +916,8 @@
"Mixed format": "Format composite", "Mixed format": "Format composite",
"Revert field settings for {{colId}} to common": "Réinitialiser les paramètres par défaut pour {{colId}}", "Revert field settings for {{colId}} to common": "Réinitialiser les paramètres par défaut pour {{colId}}",
"Save field settings for {{colId}} as common": "Sauvegarder les paramètres pour {{colId}}", "Save field settings for {{colId}} as common": "Sauvegarder les paramètres pour {{colId}}",
"Use separate field settings for {{colId}}": "Utiliser des paramètres spécifiques pour {{colId}}" "Use separate field settings for {{colId}}": "Utiliser des paramètres spécifiques pour {{colId}}",
"Changing column type": "Changement du type de colonne"
}, },
"WelcomeTour": { "WelcomeTour": {
"Customizing columns": "Personnaliser les colonnes", "Customizing columns": "Personnaliser les colonnes",
@ -1053,7 +1063,12 @@
"end dates and event titles. Note each column's type.": "Pour configurer votre calendrier, sélectionnez les colonnes pour les dates de début/fin et le nom de l'évènement. Notez le type de chaque colonne." "end dates and event titles. Note each column's type.": "Pour configurer votre calendrier, sélectionnez les colonnes pour les dates de début/fin et le nom de l'évènement. Notez le type de chaque colonne."
}, },
"Calendar": "Calendrier", "Calendar": "Calendrier",
"Can't find the right columns? Click 'Change Widget' to select the table with events data.": "Impossible de trouver les bonnes colonnes ? Cliquez sur \"Changer de vue\" pour sélectionner la table contenant les évènements." "Can't find the right columns? Click 'Change Widget' to select the table with events data.": "Impossible de trouver les bonnes colonnes ? Cliquez sur \"Changer de vue\" pour sélectionner la table contenant les évènements.",
"A UUID is a randomly-generated string that is useful for unique identifiers and link keys.": "Un UUID est un texte généré aléatoirement qui est utile pour les identifiants uniques et les clés de jointures.",
"Formulas support many Excel functions, full Python syntax, and include a helpful AI Assistant.": "Les formules supportent beaucoup de fonctions Excel et la syntaxe Python complète. Un assistant IA est disponible sur certaines instances.",
"Lookups return data from related tables.": "Récupère les données d'une table liée.",
"You can choose from widgets available to you in the dropdown, or embed your own by providing its full URL.": "Vous pouvez choisir parmi les widgets disponibles dans le menu déroulant, ou utilisez le votre en fournissant son URL complète.",
"Use reference columns to relate data in different tables.": "Utilisez les colonnes de type Référence pour lier différentes tables entre elles."
}, },
"ColumnTitle": { "ColumnTitle": {
"Add description": "Ajouter une description", "Add description": "Ajouter une description",
@ -1228,5 +1243,13 @@
"Welcome back": "Bon retour parmi nous", "Welcome back": "Bon retour parmi nous",
"You can always switch sites using the account menu.": "Vous pouvez toujours changer d'espace en utilisant le menu du compte.", "You can always switch sites using the account menu.": "Vous pouvez toujours changer d'espace en utilisant le menu du compte.",
"You have access to the following Grist sites.": "Vous avez accès aux espaces Grist suivants." "You have access to the following Grist sites.": "Vous avez accès aux espaces Grist suivants."
},
"CardContextMenu": {
"Insert card above": "Insérer une carte au dessus",
"Duplicate card": "Dupliquer la carte",
"Insert card below": "Insérer une carte en dessous",
"Delete card": "Supprimer la carte",
"Copy anchor link": "Copier le lien d'ancrage",
"Insert card": "Insérer une carte"
} }
} }

View File

@ -564,7 +564,9 @@
"Timestamp": "Метка времени", "Timestamp": "Метка времени",
"Adding UUID column": "Добавление столбца UUID", "Adding UUID column": "Добавление столбца UUID",
"Adding duplicates column": "Добавление столбца дубликатов", "Adding duplicates column": "Добавление столбца дубликатов",
"Lookups": "Lookups" "Lookups": "Lookups",
"Add formula column": "Добавить вычисляемый столбец",
"Add column with type": "Добавить столбец с типом"
}, },
"FilterBar": { "FilterBar": {
"SearchColumns": "Столбцы поиска", "SearchColumns": "Столбцы поиска",
@ -1063,7 +1065,8 @@
"A UUID is a randomly-generated string that is useful for unique identifiers and link keys.": "UUID - это случайно сгенерированная строка, которая полезна для уникальных идентификаторов и ключевых ссылок.", "A UUID is a randomly-generated string that is useful for unique identifiers and link keys.": "UUID - это случайно сгенерированная строка, которая полезна для уникальных идентификаторов и ключевых ссылок.",
"Lookups return data from related tables.": "Lookups возвращают данные из связанных таблиц.", "Lookups return data from related tables.": "Lookups возвращают данные из связанных таблиц.",
"Use reference columns to relate data in different tables.": "Используйте ссылочные столбцы для сопоставления данных в разных таблицах.", "Use reference columns to relate data in different tables.": "Используйте ссылочные столбцы для сопоставления данных в разных таблицах.",
"You can choose from widgets available to you in the dropdown, or embed your own by providing its full URL.": "Вы можете выбрать виджеты, доступные вам в раскрывающемся списке, или встроить свой собственный, указав его полный URL-адрес." "You can choose from widgets available to you in the dropdown, or embed your own by providing its full URL.": "Вы можете выбрать виджеты, доступные вам в раскрывающемся списке, или встроить свой собственный, указав его полный URL-адрес.",
"Formulas support many Excel functions, full Python syntax, and include a helpful AI Assistant.": "Формулы поддерживают множество функций Excel, полный синтаксис Python и включает полезного помощника AI."
}, },
"DescriptionConfig": { "DescriptionConfig": {
"DESCRIPTION": "ОПИСАНИЕ" "DESCRIPTION": "ОПИСАНИЕ"

View File

@ -99,8 +99,8 @@
"You're about to delete an API key. This will cause all future requests using this API key to be rejected. Do you still want to delete?": "Želite izbrisati ključ API. To bo povzročilo zavrnitev vseh prihodnjih zahtevkov, ki bodo uporabljali ta ključ API. Ali še vedno želite izbrisati?", "You're about to delete an API key. This will cause all future requests using this API key to be rejected. Do you still want to delete?": "Želite izbrisati ključ API. To bo povzročilo zavrnitev vseh prihodnjih zahtevkov, ki bodo uporabljali ta ključ API. Ali še vedno želite izbrisati?",
"Click to show": "Kliknite za prikaz", "Click to show": "Kliknite za prikaz",
"Remove API Key": "Odstranite API ključ", "Remove API Key": "Odstranite API ključ",
"This API key can be used to access this account anonymously via the API.": "Ta API ključ lahko uporabite za anonimen dostop do tega računa prek vmesnika API.", "This API key can be used to access this account anonymously via the API.": "Ta ključ lahko uporabite za anonimen dostop do tega računa prek vmesnika API.",
"This API key can be used to access your account via the API. Dont share your API key with anyone.": "Ta ključ API lahko uporabite za dostop do svojega računa prek vmesnika API. Svojega ključa API ne delite z nikomer.", "This API key can be used to access your account via the API. Dont share your API key with anyone.": "API ključ lahko uporabite za dostop do svojega računa prek API vmesnika. Svojega API ključa ne delite z nikomer.",
"By generating an API key, you will be able to make API calls for your own account.": "Z ustvarjanjem API ključa boste lahko uporabljali klice API funkcij za svoj račun." "By generating an API key, you will be able to make API calls for your own account.": "Z ustvarjanjem API ključa boste lahko uporabljali klice API funkcij za svoj račun."
}, },
"App": { "App": {
@ -225,7 +225,9 @@
"Duplicate in {{- label}}": "Dvojnik v {{- label}}", "Duplicate in {{- label}}": "Dvojnik v {{- label}}",
"Search columns": "Preišči stolpce", "Search columns": "Preišči stolpce",
"Adding UUID column": "Dodajanje UUID stolpca", "Adding UUID column": "Dodajanje UUID stolpca",
"Adding duplicates column": "Dodajanje podvojenega stolpca" "Adding duplicates column": "Dodajanje podvojenega stolpca",
"Add formula column": "Dodaj stolpec z formulo",
"Add column with type": "Dodaj stolpec z tipom"
}, },
"HomeLeftPane": { "HomeLeftPane": {
"Trash": "Koš", "Trash": "Koš",
@ -269,7 +271,7 @@
"TOOLS": "ORODJA", "TOOLS": "ORODJA",
"Settings": "Nastavitve", "Settings": "Nastavitve",
"Access Rules": "Pravila dostopa", "Access Rules": "Pravila dostopa",
"Code View": "Pogled kode", "Code View": "Koda",
"Raw Data": "Neobdelani podatki", "Raw Data": "Neobdelani podatki",
"Document History": "Zgodovina Dokumentov", "Document History": "Zgodovina Dokumentov",
"Validate Data": "Potrdi podatke", "Validate Data": "Potrdi podatke",
@ -406,7 +408,7 @@
}, },
"CodeEditorPanel": { "CodeEditorPanel": {
"Access denied": "Dostop zavrnjen", "Access denied": "Dostop zavrnjen",
"Code View is available only when you have full document access.": "Pogled kode je na voljo le, če imate popoln dostop do dokumenta." "Code View is available only when you have full document access.": "Koda je na voljo le, če imate popoln dostop do dokumenta."
}, },
"ColorSelect": { "ColorSelect": {
"Apply": "Uporabi", "Apply": "Uporabi",
@ -685,7 +687,8 @@
"A UUID is a randomly-generated string that is useful for unique identifiers and link keys.": "UUID je naključno ustvarjen niz, ki je uporaben za edinstvene identifikatorje in ključe povezav.", "A UUID is a randomly-generated string that is useful for unique identifiers and link keys.": "UUID je naključno ustvarjen niz, ki je uporaben za edinstvene identifikatorje in ključe povezav.",
"Lookups return data from related tables.": "Iskanje vrne podatke iz povezanih tabel.", "Lookups return data from related tables.": "Iskanje vrne podatke iz povezanih tabel.",
"Use reference columns to relate data in different tables.": "Uporabite referenčne stolpce za povezavo podatkov v različnih tabelah.", "Use reference columns to relate data in different tables.": "Uporabite referenčne stolpce za povezavo podatkov v različnih tabelah.",
"You can choose from widgets available to you in the dropdown, or embed your own by providing its full URL.": "Izbirate lahko med pripomočki, ki so vam na voljo v spustnem meniju, ali vdelate svojega tako, da navedete njegov polni URL." "You can choose from widgets available to you in the dropdown, or embed your own by providing its full URL.": "Izbirate lahko med pripomočki, ki so vam na voljo v spustnem meniju, ali vdelate svojega tako, da navedete njegov polni URL.",
"Formulas support many Excel functions, full Python syntax, and include a helpful AI Assistant.": "Formule podpirajo številne Excelove funkcije, polno Pythonovo sintakso in vključujejo koristnega AI pomočnika."
}, },
"UserManager": { "UserManager": {
"Anyone with link ": "Vsakdo s povezavo ", "Anyone with link ": "Vsakdo s povezavo ",

View File

@ -449,7 +449,7 @@ class Seed {
const d = new Document(); const d = new Document();
d.name = doc; d.name = doc;
d.workspace = w; d.workspace = w;
d.id = `sample_${docId}`; d.id = `sampledocid_${docId}`;
docId++; docId++;
await d.save(); await d.save();
const dgrps = await this.createGroups(w); const dgrps = await this.createGroups(w);

View File

@ -28,19 +28,19 @@ describe('FillLinkedRecords.ntest', function() {
// Link the sections first since the sample document start with no links. // Link the sections first since the sample document start with no links.
// Connect Friends -> Films // Connect Friends -> Films
await gu.getSection('Films record').click(); await gu.actions.viewSection('Films record').selectSection();
await $('.test-right-select-by').click(); await $('.test-right-select-by').click();
await $('.test-select-row:contains(Friends record)').click(); await $('.test-select-row:contains(Friends record)').click();
await gu.waitForServer(); await gu.waitForServer();
// Connect Films -> Performances grid // Connect Films -> Performances grid
await gu.getSection('Performances record').click(); await gu.actions.viewSection('Performances record').selectSection();
await $('.test-right-select-by').click(); await $('.test-right-select-by').click();
await $('.test-select-row:contains(Films record)').click(); await $('.test-select-row:contains(Films record)').click();
await gu.waitForServer(); await gu.waitForServer();
// Connect Films -> Performances detail // Connect Films -> Performances detail
await gu.getSection('Performances detail').click(); await gu.actions.viewSection('Performances detail').selectSection();
await $('.test-right-select-by').click(); await $('.test-right-select-by').click();
await $('.test-select-row:contains(Films record)').click(); await $('.test-select-row:contains(Films record)').click();
await gu.waitForServer(); await gu.waitForServer();

View File

@ -25,7 +25,7 @@ describe('SavePosition.ntest', function() {
await $('.test-config-data').click(); await $('.test-config-data').click();
// Connect CITY -> CITY Card List // Connect CITY -> CITY Card List
await gu.getSection('CITY Card List').click(); await gu.actions.viewSection('CITY Card List').selectSection();
await $('.test-right-select-by').click(); await $('.test-right-select-by').click();
await $('.test-select-row:contains(CITY)').click(); await $('.test-select-row:contains(CITY)').click();
await gu.waitForServer(); await gu.waitForServer();

View File

@ -291,12 +291,12 @@ describe('TypeChange.ntest', function() {
it('should trigger a transform when reference table is changed', async function() { it('should trigger a transform when reference table is changed', async function() {
// Set up conditions for the test // Set up conditions for the test
await gu.getSection('Table1').click(); await gu.actions.viewSection('Table1').selectSection();
await gu.enterGridValues(2, 3, [['red', 'yellow']]); await gu.enterGridValues(2, 3, [['red', 'yellow']]);
await gu.actions.addNewSection('New', 'Table'); await gu.actions.addNewSection('New', 'Table');
await gu.getSection('TABLE3').click(); await gu.actions.viewSection('TABLE3').selectSection();
await gu.enterGridValues(0, 1, [['yellow', 'red', 'green', 'blue']]); await gu.enterGridValues(0, 1, [['yellow', 'red', 'green', 'blue']]);
await gu.getSection('Table1').click(); await gu.actions.viewSection('Table1').selectSection();
await gu.clickCellRC(0, 3); await gu.clickCellRC(0, 3);
await gu.openSidePane('field'); await gu.openSidePane('field');
await gu.setType('Reference'); await gu.setType('Reference');
@ -365,7 +365,7 @@ describe('TypeChange.ntest', function() {
// column were mistaken for row ids and converted to row values instead of AltText values. // column were mistaken for row ids and converted to row values instead of AltText values.
it('should properly convert from integer to reference', async function() { it('should properly convert from integer to reference', async function() {
// Set up conditions for the test // Set up conditions for the test
await gu.getSection('TABLE3').click(); await gu.actions.viewSection('TABLE3').selectSection();
await gu.enterGridValues(0, 2, [['3', '3', '4', '1']]); await gu.enterGridValues(0, 2, [['3', '3', '4', '1']]);
await gu.waitForServer(); await gu.waitForServer();
await gu.setType('Integer'); await gu.setType('Integer');

View File

@ -111,7 +111,7 @@ describe('Views.ntest', function() {
// Reference: https://phab.getgrist.com/T327 // Reference: https://phab.getgrist.com/T327
await gu.actions.addNewSection('New', 'Table'); await gu.actions.addNewSection('New', 'Table');
await gu.waitForServer(); await gu.waitForServer();
await gu.getSection('TABLE4').click(); await gu.actions.viewSection('TABLE4').selectSection();
// Delete the section // Delete the section
await gu.actions.viewSection('TABLE4').selectMenuOption('viewLayout', 'Delete widget'); await gu.actions.viewSection('TABLE4').selectMenuOption('viewLayout', 'Delete widget');
await gu.waitForServer(); await gu.waitForServer();

View File

@ -101,40 +101,46 @@ describe('Authorizer', function() {
it.skip("viewer gets redirect by title", async function() { it.skip("viewer gets redirect by title", async function() {
const resp = await axios.get(`${serverUrl}/o/pr/doc/Bananas`, chimpy); const resp = await axios.get(`${serverUrl}/o/pr/doc/Bananas`, chimpy);
assert.equal(resp.status, 200); assert.equal(resp.status, 200);
assert.equal(getGristConfig(resp.data).assignmentId, 'sample_6'); assert.equal(getGristConfig(resp.data).assignmentId, 'sampledocid_6');
assert.match(resp.request.res.responseUrl, /\/doc\/sample_6$/); assert.match(resp.request.res.responseUrl, /\/doc\/sampledocid_6$/);
const resp2 = await axios.get(`${serverUrl}/o/nasa/doc/Pluto`, chimpy); const resp2 = await axios.get(`${serverUrl}/o/nasa/doc/Pluto`, chimpy);
assert.equal(resp2.status, 200); assert.equal(resp2.status, 200);
assert.equal(getGristConfig(resp2.data).assignmentId, 'sample_2'); assert.equal(getGristConfig(resp2.data).assignmentId, 'sampledocid_2');
assert.match(resp2.request.res.responseUrl, /\/doc\/sample_2$/); assert.match(resp2.request.res.responseUrl, /\/doc\/sampledocid_2$/);
});
it('viewer loads document without slug in the URL', async function () {
const docId = docs.Bananas.id;
const resp = await axios.get(`${serverUrl}/o/pr/${docId}`, chimpy);
assert.equal(resp.status, 200);
}); });
it("stranger gets consistent refusal regardless of title", async function() { it("stranger gets consistent refusal regardless of title", async function() {
const resp = await axios.get(`${serverUrl}/o/pr/doc/Bananas`, charon); const resp = await axios.get(`${serverUrl}/o/pr/doc/Bananas`, charon);
assert.equal(resp.status, 404); assert.equal(resp.status, 404);
assert.notMatch(resp.data, /sample_6/); assert.notMatch(resp.data, /sampledocid_6/);
const resp2 = await axios.get(`${serverUrl}/o/pr/doc/Bananas2`, charon); const resp2 = await axios.get(`${serverUrl}/o/pr/doc/Bananas2`, charon);
assert.equal(resp2.status, 404); assert.equal(resp2.status, 404);
assert.notMatch(resp.data, /sample_6/); assert.notMatch(resp.data, /sampledocid_6/);
assert.deepEqual(withoutTimestamp(resp.data), assert.deepEqual(withoutTimestamp(resp.data),
withoutTimestamp(resp2.data)); withoutTimestamp(resp2.data));
}); });
it("viewer can access title", async function() { it("viewer can access title", async function() {
const resp = await axios.get(`${serverUrl}/o/pr/doc/sample_6`, chimpy); const resp = await axios.get(`${serverUrl}/o/pr/doc/sampledocid_6`, chimpy);
assert.equal(resp.status, 200); assert.equal(resp.status, 200);
const config = getGristConfig(resp.data); const config = getGristConfig(resp.data);
assert.equal(config.getDoc![config.assignmentId!].name, 'Bananas'); assert.equal(config.getDoc![config.assignmentId!].name, 'Bananas');
}); });
it("stranger cannot access title", async function() { it("stranger cannot access title", async function() {
const resp = await axios.get(`${serverUrl}/o/pr/doc/sample_6`, charon); const resp = await axios.get(`${serverUrl}/o/pr/doc/sampledocid_6`, charon);
assert.equal(resp.status, 403); assert.equal(resp.status, 403);
assert.notMatch(resp.data, /Bananas/); assert.notMatch(resp.data, /Bananas/);
}); });
it("viewer cannot access document from wrong org", async function() { it("viewer cannot access document from wrong org", async function() {
const resp = await axios.get(`${serverUrl}/o/nasa/doc/sample_6`, chimpy); const resp = await axios.get(`${serverUrl}/o/nasa/doc/sampledocid_6`, chimpy);
assert.equal(resp.status, 404); assert.equal(resp.status, 404);
}); });
@ -142,7 +148,7 @@ describe('Authorizer', function() {
const cli = await openClient(server, 'chimpy@getgrist.com', 'pr'); const cli = await openClient(server, 'chimpy@getgrist.com', 'pr');
cli.ignoreTrivialActions(); cli.ignoreTrivialActions();
assert.equal((await cli.readMessage()).type, 'clientConnect'); assert.equal((await cli.readMessage()).type, 'clientConnect');
const openDoc = await cli.send("openDoc", "sample_6"); const openDoc = await cli.send("openDoc", "sampledocid_6");
assert.equal(openDoc.error, undefined); assert.equal(openDoc.error, undefined);
assert.match(JSON.stringify(openDoc.data), /Table1/); assert.match(JSON.stringify(openDoc.data), /Table1/);
await cli.close(); await cli.close();
@ -152,7 +158,7 @@ describe('Authorizer', function() {
const cli = await openClient(server, 'charon@getgrist.com', 'pr'); const cli = await openClient(server, 'charon@getgrist.com', 'pr');
cli.ignoreTrivialActions(); cli.ignoreTrivialActions();
assert.equal((await cli.readMessage()).type, 'clientConnect'); assert.equal((await cli.readMessage()).type, 'clientConnect');
const openDoc = await cli.send("openDoc", "sample_6"); const openDoc = await cli.send("openDoc", "sampledocid_6");
assert.match(openDoc.error!, /No view access/); assert.match(openDoc.error!, /No view access/);
assert.equal(openDoc.data, undefined); assert.equal(openDoc.data, undefined);
assert.match(openDoc.errorCode!, /AUTH_NO_VIEW/); assert.match(openDoc.errorCode!, /AUTH_NO_VIEW/);
@ -163,7 +169,7 @@ describe('Authorizer', function() {
const cli = await openClient(server, 'charon@getgrist.com', 'nasa'); const cli = await openClient(server, 'charon@getgrist.com', 'nasa');
cli.ignoreTrivialActions(); cli.ignoreTrivialActions();
assert.equal((await cli.readMessage()).type, 'clientConnect'); assert.equal((await cli.readMessage()).type, 'clientConnect');
const openDoc = await cli.openDocOnConnect("sample_2"); const openDoc = await cli.openDocOnConnect("sampledocid_2");
assert.equal(openDoc.error, undefined); assert.equal(openDoc.error, undefined);
const nonce = uuidv4(); const nonce = uuidv4();
const applyUserActions = await cli.send("applyUserActions", const applyUserActions = await cli.send("applyUserActions",
@ -182,7 +188,7 @@ describe('Authorizer', function() {
const cli = await openClient(server, 'chimpy@getgrist.com', 'nasa'); const cli = await openClient(server, 'chimpy@getgrist.com', 'nasa');
cli.ignoreTrivialActions(); cli.ignoreTrivialActions();
assert.equal((await cli.readMessage()).type, 'clientConnect'); assert.equal((await cli.readMessage()).type, 'clientConnect');
const openDoc = await cli.openDocOnConnect("sample_2"); const openDoc = await cli.openDocOnConnect("sampledocid_2");
assert.equal(openDoc.error, undefined); assert.equal(openDoc.error, undefined);
const nonce = uuidv4(); const nonce = uuidv4();
const applyUserActions = await cli.send("applyUserActions", const applyUserActions = await cli.send("applyUserActions",
@ -209,9 +215,9 @@ describe('Authorizer', function() {
editor.ignoreTrivialActions(); editor.ignoreTrivialActions();
viewer.ignoreTrivialActions(); viewer.ignoreTrivialActions();
stranger.ignoreTrivialActions(); stranger.ignoreTrivialActions();
assert.equal((await editor.send("openDoc", "sample_2")).error, undefined); assert.equal((await editor.send("openDoc", "sampledocid_2")).error, undefined);
assert.equal((await viewer.send("openDoc", "sample_2")).error, undefined); assert.equal((await viewer.send("openDoc", "sampledocid_2")).error, undefined);
assert.match((await stranger.send("openDoc", "sample_2")).error!, /No view access/); assert.match((await stranger.send("openDoc", "sampledocid_2")).error!, /No view access/);
const action = [0, [["UpdateRecord", "Table1", 1, {A: "foo"}]]]; const action = [0, [["UpdateRecord", "Table1", 1, {A: "foo"}]]];
assert.equal((await editor.send("applyUserActions", ...action)).error, undefined); assert.equal((await editor.send("applyUserActions", ...action)).error, undefined);
@ -224,7 +230,7 @@ describe('Authorizer', function() {
const cli = await openClient(server, 'thumbnail@getgrist.com', 'nasa'); const cli = await openClient(server, 'thumbnail@getgrist.com', 'nasa');
cli.ignoreTrivialActions(); cli.ignoreTrivialActions();
assert.equal((await cli.readMessage()).type, 'clientConnect'); assert.equal((await cli.readMessage()).type, 'clientConnect');
const openDoc = await cli.send("openDoc", "sample_2"); const openDoc = await cli.send("openDoc", "sampledocid_2");
assert.equal(openDoc.error, undefined); assert.equal(openDoc.error, undefined);
const nonce = uuidv4(); const nonce = uuidv4();
const applyUserActions = await cli.send("applyUserActions", const applyUserActions = await cli.send("applyUserActions",
@ -243,12 +249,12 @@ describe('Authorizer', function() {
const cli = await openClient(server, 'charon@getgrist.com', 'nasa'); const cli = await openClient(server, 'charon@getgrist.com', 'nasa');
cli.ignoreTrivialActions(); cli.ignoreTrivialActions();
assert.equal((await cli.readMessage()).type, 'clientConnect'); assert.equal((await cli.readMessage()).type, 'clientConnect');
const openDoc = await cli.send("openDoc", "sample_2"); const openDoc = await cli.send("openDoc", "sampledocid_2");
assert.equal(openDoc.error, undefined); assert.equal(openDoc.error, undefined);
const result = await cli.send("fork", 0); const result = await cli.send("fork", 0);
assert.equal(result.data.docId, result.data.urlId); assert.equal(result.data.docId, result.data.urlId);
const parts = parseUrlId(result.data.docId); const parts = parseUrlId(result.data.docId);
assert.equal(parts.trunkId, "sample_2"); assert.equal(parts.trunkId, "sampledocid_2");
assert.isAbove(parts.forkId!.length, 4); assert.isAbove(parts.forkId!.length, 4);
assert.equal(parts.forkUserId, await dbManager.testGetId('Charon') as number); assert.equal(parts.forkUserId, await dbManager.testGetId('Charon') as number);
}); });
@ -258,31 +264,31 @@ describe('Authorizer', function() {
const cli = await openClient(server, 'anon@getgrist.com', 'nasa'); const cli = await openClient(server, 'anon@getgrist.com', 'nasa');
cli.ignoreTrivialActions(); cli.ignoreTrivialActions();
assert.equal((await cli.readMessage()).type, 'clientConnect'); assert.equal((await cli.readMessage()).type, 'clientConnect');
let openDoc = await cli.send("openDoc", "sample_2"); let openDoc = await cli.send("openDoc", "sampledocid_2");
assert.match(openDoc.error!, /No view access/); assert.match(openDoc.error!, /No view access/);
// grant anon access to doc and retry // grant anon access to doc and retry
await dbManager.updateDocPermissions({ await dbManager.updateDocPermissions({
userId: await dbManager.testGetId('Chimpy') as number, userId: await dbManager.testGetId('Chimpy') as number,
urlId: 'sample_2', urlId: 'sampledocid_2',
org: 'nasa' org: 'nasa'
}, {users: {"anon@getgrist.com": "viewers"}}); }, {users: {"anon@getgrist.com": "viewers"}});
dbManager.flushDocAuthCache(); dbManager.flushDocAuthCache();
openDoc = await cli.send("openDoc", "sample_2"); openDoc = await cli.send("openDoc", "sampledocid_2");
assert.equal(openDoc.error, undefined); assert.equal(openDoc.error, undefined);
// make a fork // make a fork
const result = await cli.send("fork", 0); const result = await cli.send("fork", 0);
assert.equal(result.data.docId, result.data.urlId); assert.equal(result.data.docId, result.data.urlId);
const parts = parseUrlId(result.data.docId); const parts = parseUrlId(result.data.docId);
assert.equal(parts.trunkId, "sample_2"); assert.equal(parts.trunkId, "sampledocid_2");
assert.isAbove(parts.forkId!.length, 4); assert.isAbove(parts.forkId!.length, 4);
assert.equal(parts.forkUserId, undefined); assert.equal(parts.forkUserId, undefined);
}); });
it("can set user via GRIST_PROXY_AUTH_HEADER", async function() { it("can set user via GRIST_PROXY_AUTH_HEADER", async function() {
// User can access a doc by setting header. // User can access a doc by setting header.
const docUrl = `${serverUrl}/o/pr/api/docs/sample_6`; const docUrl = `${serverUrl}/o/pr/api/docs/sampledocid_6`;
const resp = await axios.get(docUrl, { const resp = await axios.get(docUrl, {
headers: {'X-email': 'chimpy@getgrist.com'} headers: {'X-email': 'chimpy@getgrist.com'}
}); });
@ -297,7 +303,7 @@ describe('Authorizer', function() {
let cli = await openClient(server, 'chimpy@getgrist.com', 'pr', 'X-email'); let cli = await openClient(server, 'chimpy@getgrist.com', 'pr', 'X-email');
cli.ignoreTrivialActions(); cli.ignoreTrivialActions();
assert.equal((await cli.readMessage()).type, 'clientConnect'); assert.equal((await cli.readMessage()).type, 'clientConnect');
let openDoc = await cli.send("openDoc", "sample_6"); let openDoc = await cli.send("openDoc", "sampledocid_6");
assert.equal(openDoc.error, undefined); assert.equal(openDoc.error, undefined);
assert.match(JSON.stringify(openDoc.data), /Table1/); assert.match(JSON.stringify(openDoc.data), /Table1/);
await cli.close(); await cli.close();
@ -306,7 +312,7 @@ describe('Authorizer', function() {
cli = await openClient(server, 'notchimpy@getgrist.com', 'pr', 'X-email'); cli = await openClient(server, 'notchimpy@getgrist.com', 'pr', 'X-email');
cli.ignoreTrivialActions(); cli.ignoreTrivialActions();
assert.equal((await cli.readMessage()).type, 'clientConnect'); assert.equal((await cli.readMessage()).type, 'clientConnect');
openDoc = await cli.send("openDoc", "sample_6"); openDoc = await cli.send("openDoc", "sampledocid_6");
assert.match(openDoc.error!, /No view access/); assert.match(openDoc.error!, /No view access/);
assert.equal(openDoc.data, undefined); assert.equal(openDoc.data, undefined);
assert.match(openDoc.errorCode!, /AUTH_NO_VIEW/); assert.match(openDoc.errorCode!, /AUTH_NO_VIEW/);

View File

@ -49,10 +49,10 @@ const support = configForUser('support');
// some doc ids // some doc ids
const docIds: { [name: string]: string } = { const docIds: { [name: string]: string } = {
ApiDataRecordsTest: 'sample_7', ApiDataRecordsTest: 'sampledocid_7',
Timesheets: 'sample_13', Timesheets: 'sampledocid_13',
Bananas: 'sample_6', Bananas: 'sampledocid_6',
Antartic: 'sample_11' Antartic: 'sampledocid_11'
}; };
// A testDir of the form grist_test_{USER}_{SERVER_NAME} // A testDir of the form grist_test_{USER}_{SERVER_NAME}

View File

@ -21,10 +21,10 @@ const chimpy = configForUser('Chimpy');
// some doc ids // some doc ids
const docIds: { [name: string]: string } = { const docIds: { [name: string]: string } = {
ApiDataRecordsTest: 'sample_7', ApiDataRecordsTest: 'sampledocid_7',
Timesheets: 'sample_13', Timesheets: 'sampledocid_13',
Bananas: 'sample_6', Bananas: 'sampledocid_6',
Antartic: 'sample_11' Antartic: 'sampledocid_11',
}; };
let dataDir: string; let dataDir: string;

View File

@ -303,6 +303,11 @@
safe-buffer "5.2.0" safe-buffer "5.2.0"
uid-safe "~2.1.5" uid-safe "~2.1.5"
"@gristlabs/grist-widget@^0.0.4":
version "0.0.4"
resolved "https://registry.yarnpkg.com/@gristlabs/grist-widget/-/grist-widget-0.0.4.tgz#df50d988bcdf8fc26a876cf23b82e258bbdb0ccc"
integrity sha512-Q0k+GuudU2+0JkuvVkB9UZzqeUKJH8PsaO9ZfxKuqL9/ssIXUd080msB+PJLXB0TU9BkpzPSl7+kLqXTBSnA5g==
"@gristlabs/moment-guess@1.2.4-grist.1": "@gristlabs/moment-guess@1.2.4-grist.1":
version "1.2.4-grist.1" version "1.2.4-grist.1"
resolved "https://registry.npmjs.org/@gristlabs/moment-guess/-/moment-guess-1.2.4-grist.1.tgz" resolved "https://registry.npmjs.org/@gristlabs/moment-guess/-/moment-guess-1.2.4-grist.1.tgz"