(core) updates from grist-core

pull/783/head
Paul Fitzpatrick 10 months ago
commit d89e008a75

@ -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_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_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_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
@ -396,6 +397,7 @@ TYPEORM_PASSWORD | password to use
TYPEORM_PORT | port number for db if not the default for that db type
TYPEORM_TYPE | set to 'sqlite' or 'postgres'
TYPEORM_USERNAME | username to connect as
TYPEORM_EXTRA | any other properties to pass to TypeORM in JSON format
#### Testing:

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

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

@ -77,7 +77,7 @@ function createMainPage(appModel: AppModel, appObj: App) {
return dom.create(WelcomePage, appModel);
} else if (pageType === 'account') {
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)));
} else if (pageType === 'activation') {
return domAsync(loadActivationPage().then(ap => dom.create(ap.ActivationPage, appModel)));

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

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

@ -43,7 +43,7 @@ export type ActivationPage = typeof ActivationPage.type;
export const LoginPage = StringUnion('signup', 'login', 'verified', 'forgot-password');
export type LoginPage = typeof LoginPage.type;
export const SupportGristPage = StringUnion('support-grist');
export const SupportGristPage = StringUnion('support');
export type SupportGristPage = typeof SupportGristPage.type;
// 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';
}
if (map.has('welcome')) { state.welcome = WelcomePage.parse(map.get('welcome')); }
if (map.has('support-grist')) {
state.supportGrist = SupportGristPage.parse(map.get('support-grist')) || 'support-grist';
if (map.has('support')) {
state.supportGrist = SupportGristPage.parse(map.get('support')) || 'support';
}
if (sp.has('planType')) { state.params!.planType = sp.get('planType')!; }
if (sp.has('billingPlan')) { state.params!.billingPlan = sp.get('billingPlan')!; }

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

@ -1998,8 +1998,13 @@ export class FlexServer implements GristServer {
// 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'));
this.info.push(['userRoot', userRoot]);
const pluginManager = new PluginManager(this.appRoot, userRoot);
// Some custom widgets may be included as an npm package called @gristlabs/grist-widget.
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
// finishes, it will act as if there are no plugins.
// ^ I think this comment was here to justify calling initialize without waiting for

@ -46,7 +46,8 @@ function servePluginContent(req: express.Request, res: express.Response,
req.get('X-From-Plugin-WebView') === "true" ||
mimeTypes.lookup(path.extname(pluginPath)) === "application/javascript") {
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
// 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)

@ -14,9 +14,14 @@ export interface PluginDirectories {
*/
readonly builtIn?: string;
/**
* Directory where user installed plugins are localted.
* Directory where user installed plugins are located.
*/
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.
*
*/
public constructor(public appRoot?: string, userRoot?: string) {
public constructor(public appRoot?: string, userRoot?: string,
public bundledRoot?: string) {
this._dirs = {
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"));
}
// Load bundled plugins
if (this._dirs.bundled) {
this._entries.push(...await scanDirectory(this._dirs.bundled, "bundled"));
}
if (!process.env.GRIST_EXPERIMENTAL_PLUGINS ||
process.env.GRIST_EXPERIMENTAL_PLUGINS === '0') {
// 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[] = [];
let listDir;

@ -187,7 +187,7 @@ export class Telemetry implements ITelemetry {
public addPages(app: express.Application, middleware: express.RequestHandler[]) {
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,
{path: 'app.html', status: 200, config: {}});
}));

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

@ -13,8 +13,8 @@
"install:python3": "buildtools/prepare_python3.sh",
"build:prod": "buildtools/build.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: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": "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 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: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'",
@ -114,6 +114,7 @@
"@googleapis/oauth2": "0.2.0",
"@gristlabs/connect-sqlite3": "0.9.11-grist.5",
"@gristlabs/express-session": "1.17.0",
"@gristlabs/grist-widget": "^0.0.4",
"@gristlabs/moment-guess": "1.2.4-grist.1",
"@gristlabs/pidusage": "2.0.17",
"@gristlabs/sqlite3": "5.1.4-grist.8",

@ -31,7 +31,7 @@ function check_gvisor {
return
fi
# 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_AVAILABLE=1
fi
@ -40,9 +40,9 @@ function check_gvisor {
check_gvisor --unprivileged --ignore-cgroups
check_gvisor --unprivileged
# If we can't use --unprivileged, stick with --rootless and no checkpoint
if [[ -z "$GVISOR_FLAGS" ]]; then
check_gvisor --rootless
else
# If we can't use --unprivileged, stick with --rootless. We will not make a checkpoint.
check_gvisor --rootless
if [[ "$GVISOR_FLAGS" =~ "-unprivileged" ]]; then
export GRIST_CHECKPOINT=/tmp/engine_$(echo $PWD | sed "s/[^a-zA-Z0-9]/_/g")
fi

@ -3,8 +3,17 @@
set -e
if [[ "$GRIST_SANDBOX_FLAVOR" = "gvisor" ]]; then
./sandbox/gvisor/update_engine_checkpoint.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
NODE_PATH=_build:_build/stubs:_build/ext node _build/stubs/app/server/server.js

@ -425,7 +425,9 @@
"Duplicate in {{- label}}": "Duplicate in {{- label}}",
"No reference columns.": "No reference columns.",
"Search columns": "Search columns",
"UUID": "UUID"
"UUID": "UUID",
"Add column with type": "Add column with type",
"Add formula column": "Add formula column"
},
"GristDoc": {
"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.",
"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.",
"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": {
"DESCRIPTION": "DESCRIPTION"

@ -205,7 +205,13 @@
"Table ID copied to clipboard": "Identifiant de table copié",
"Duplicate Table": "Dupliquer la table",
"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": {
"Activity": "Activité",
@ -416,7 +422,9 @@
"Timestamp": "Horodatage",
"no reference column": "pas de colonne de référence",
"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": {
"Import from file": "Importer depuis un fichier",
@ -613,7 +621,8 @@
"Duplicate rows_one": "Dupliquer la ligne",
"Duplicate rows_other": "Dupliquer les lignes",
"Delete": "Supprimer",
"Copy anchor link": "Copier l'ancre"
"Copy anchor link": "Copier l'ancre",
"View as card": "Voir en carte"
},
"SelectionSummary": {
"Copied to clipboard": "Copié dans le presse-papier"
@ -907,7 +916,8 @@
"Mixed format": "Format composite",
"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}}",
"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": {
"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."
},
"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": {
"Add description": "Ajouter une description",
@ -1228,5 +1243,13 @@
"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 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"
}
}

@ -564,7 +564,9 @@
"Timestamp": "Метка времени",
"Adding UUID column": "Добавление столбца UUID",
"Adding duplicates column": "Добавление столбца дубликатов",
"Lookups": "Lookups"
"Lookups": "Lookups",
"Add formula column": "Добавить вычисляемый столбец",
"Add column with type": "Добавить столбец с типом"
},
"FilterBar": {
"SearchColumns": "Столбцы поиска",
@ -1063,7 +1065,8 @@
"A UUID is a randomly-generated string that is useful for unique identifiers and link keys.": "UUID - это случайно сгенерированная строка, которая полезна для уникальных идентификаторов и ключевых ссылок.",
"Lookups return data from related tables.": "Lookups возвращают данные из связанных таблиц.",
"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": {
"DESCRIPTION": "ОПИСАНИЕ"

@ -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?",
"Click to show": "Kliknite za prikaz",
"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 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 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.": "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."
},
"App": {
@ -225,7 +225,9 @@
"Duplicate in {{- label}}": "Dvojnik v {{- label}}",
"Search columns": "Preišči stolpce",
"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": {
"Trash": "Koš",
@ -269,7 +271,7 @@
"TOOLS": "ORODJA",
"Settings": "Nastavitve",
"Access Rules": "Pravila dostopa",
"Code View": "Pogled kode",
"Code View": "Koda",
"Raw Data": "Neobdelani podatki",
"Document History": "Zgodovina Dokumentov",
"Validate Data": "Potrdi podatke",
@ -406,7 +408,7 @@
},
"CodeEditorPanel": {
"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": {
"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.",
"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.",
"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": {
"Anyone with link ": "Vsakdo s povezavo ",

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

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

@ -25,7 +25,7 @@ describe('SavePosition.ntest', function() {
await $('.test-config-data').click();
// 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-select-row:contains(CITY)').click();
await gu.waitForServer();

@ -291,12 +291,12 @@ describe('TypeChange.ntest', function() {
it('should trigger a transform when reference table is changed', async function() {
// 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.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.getSection('Table1').click();
await gu.actions.viewSection('Table1').selectSection();
await gu.clickCellRC(0, 3);
await gu.openSidePane('field');
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.
it('should properly convert from integer to reference', async function() {
// 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.waitForServer();
await gu.setType('Integer');

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

@ -101,40 +101,46 @@ describe('Authorizer', function() {
it.skip("viewer gets redirect by title", async function() {
const resp = await axios.get(`${serverUrl}/o/pr/doc/Bananas`, chimpy);
assert.equal(resp.status, 200);
assert.equal(getGristConfig(resp.data).assignmentId, 'sample_6');
assert.match(resp.request.res.responseUrl, /\/doc\/sample_6$/);
assert.equal(getGristConfig(resp.data).assignmentId, 'sampledocid_6');
assert.match(resp.request.res.responseUrl, /\/doc\/sampledocid_6$/);
const resp2 = await axios.get(`${serverUrl}/o/nasa/doc/Pluto`, chimpy);
assert.equal(resp2.status, 200);
assert.equal(getGristConfig(resp2.data).assignmentId, 'sample_2');
assert.match(resp2.request.res.responseUrl, /\/doc\/sample_2$/);
assert.equal(getGristConfig(resp2.data).assignmentId, 'sampledocid_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() {
const resp = await axios.get(`${serverUrl}/o/pr/doc/Bananas`, charon);
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);
assert.equal(resp2.status, 404);
assert.notMatch(resp.data, /sample_6/);
assert.notMatch(resp.data, /sampledocid_6/);
assert.deepEqual(withoutTimestamp(resp.data),
withoutTimestamp(resp2.data));
});
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);
const config = getGristConfig(resp.data);
assert.equal(config.getDoc![config.assignmentId!].name, 'Bananas');
});
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.notMatch(resp.data, /Bananas/);
});
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);
});
@ -142,7 +148,7 @@ describe('Authorizer', function() {
const cli = await openClient(server, 'chimpy@getgrist.com', 'pr');
cli.ignoreTrivialActions();
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.match(JSON.stringify(openDoc.data), /Table1/);
await cli.close();
@ -152,7 +158,7 @@ describe('Authorizer', function() {
const cli = await openClient(server, 'charon@getgrist.com', 'pr');
cli.ignoreTrivialActions();
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.equal(openDoc.data, undefined);
assert.match(openDoc.errorCode!, /AUTH_NO_VIEW/);
@ -163,7 +169,7 @@ describe('Authorizer', function() {
const cli = await openClient(server, 'charon@getgrist.com', 'nasa');
cli.ignoreTrivialActions();
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);
const nonce = uuidv4();
const applyUserActions = await cli.send("applyUserActions",
@ -182,7 +188,7 @@ describe('Authorizer', function() {
const cli = await openClient(server, 'chimpy@getgrist.com', 'nasa');
cli.ignoreTrivialActions();
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);
const nonce = uuidv4();
const applyUserActions = await cli.send("applyUserActions",
@ -209,9 +215,9 @@ describe('Authorizer', function() {
editor.ignoreTrivialActions();
viewer.ignoreTrivialActions();
stranger.ignoreTrivialActions();
assert.equal((await editor.send("openDoc", "sample_2")).error, undefined);
assert.equal((await viewer.send("openDoc", "sample_2")).error, undefined);
assert.match((await stranger.send("openDoc", "sample_2")).error!, /No view access/);
assert.equal((await editor.send("openDoc", "sampledocid_2")).error, undefined);
assert.equal((await viewer.send("openDoc", "sampledocid_2")).error, undefined);
assert.match((await stranger.send("openDoc", "sampledocid_2")).error!, /No view access/);
const action = [0, [["UpdateRecord", "Table1", 1, {A: "foo"}]]];
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');
cli.ignoreTrivialActions();
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);
const nonce = uuidv4();
const applyUserActions = await cli.send("applyUserActions",
@ -243,12 +249,12 @@ describe('Authorizer', function() {
const cli = await openClient(server, 'charon@getgrist.com', 'nasa');
cli.ignoreTrivialActions();
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);
const result = await cli.send("fork", 0);
assert.equal(result.data.docId, result.data.urlId);
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.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');
cli.ignoreTrivialActions();
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/);
// grant anon access to doc and retry
await dbManager.updateDocPermissions({
userId: await dbManager.testGetId('Chimpy') as number,
urlId: 'sample_2',
urlId: 'sampledocid_2',
org: 'nasa'
}, {users: {"anon@getgrist.com": "viewers"}});
dbManager.flushDocAuthCache();
openDoc = await cli.send("openDoc", "sample_2");
openDoc = await cli.send("openDoc", "sampledocid_2");
assert.equal(openDoc.error, undefined);
// make a fork
const result = await cli.send("fork", 0);
assert.equal(result.data.docId, result.data.urlId);
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.equal(parts.forkUserId, undefined);
});
it("can set user via GRIST_PROXY_AUTH_HEADER", async function() {
// 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, {
headers: {'X-email': 'chimpy@getgrist.com'}
});
@ -297,7 +303,7 @@ describe('Authorizer', function() {
let cli = await openClient(server, 'chimpy@getgrist.com', 'pr', 'X-email');
cli.ignoreTrivialActions();
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.match(JSON.stringify(openDoc.data), /Table1/);
await cli.close();
@ -306,7 +312,7 @@ describe('Authorizer', function() {
cli = await openClient(server, 'notchimpy@getgrist.com', 'pr', 'X-email');
cli.ignoreTrivialActions();
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.equal(openDoc.data, undefined);
assert.match(openDoc.errorCode!, /AUTH_NO_VIEW/);

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

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

@ -303,6 +303,11 @@
safe-buffer "5.2.0"
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":
version "1.2.4-grist.1"
resolved "https://registry.npmjs.org/@gristlabs/moment-guess/-/moment-guess-1.2.4-grist.1.tgz"

Loading…
Cancel
Save