(core) updates from grist-core

pull/1150/head
Paul Fitzpatrick 1 month ago
commit 0a78cdbaab

5
.gitignore vendored

@ -83,3 +83,8 @@ xunit.xml
# ext directory can be overwritten
ext/**
# Docker compose examples - persistent values and secrets
/docker-compose-examples/*/persist
/docker-compose-examples/*/secrets
/docker-compose-examples/grist-traefik-oidc-auth/.env

@ -95,25 +95,46 @@ export interface IOptions extends ISelectOptions {
placement?: Popper.Placement;
}
export interface ICompatibleTypes {
// true if "New Page" is selected in Page Picker
isNewPage: Boolean | undefined;
// true if can be summarized
summarize: Boolean;
}
const testId = makeTestId('test-wselect-');
// The picker disables some choices that do not make much sense. This function return the list of
// compatible types given the tableId and whether user is creating a new page or not.
function getCompatibleTypes(tableId: TableRef, isNewPage: boolean|undefined): IWidgetType[] {
function getCompatibleTypes(tableId: TableRef,
{isNewPage, summarize}: ICompatibleTypes): IWidgetType[] {
let compatibleTypes: Array<IWidgetType> = [];
if (tableId !== 'New Table') {
return ['record', 'single', 'detail', 'chart', 'custom', 'custom.calendar', 'form'];
compatibleTypes = ['record', 'single', 'detail', 'chart', 'custom', 'custom.calendar', 'form'];
} else if (isNewPage) {
// New view + new table means we'll be switching to the primary view.
return ['record', 'form'];
compatibleTypes = ['record', 'form'];
} else {
// The type 'chart' makes little sense when creating a new table.
return ['record', 'single', 'detail', 'form'];
compatibleTypes = ['record', 'single', 'detail', 'form'];
}
return summarize ? compatibleTypes.filter((el) => isSummaryCompatible(el)) : compatibleTypes;
}
// The Picker disables some choices that do not make much sense.
// This function return a boolean telling if summary can be used with this type.
function isSummaryCompatible(widgetType: IWidgetType): boolean {
const incompatibleTypes: Array<IWidgetType> = ['form'];
return !incompatibleTypes.includes(widgetType);
}
// Whether table and type make for a valid selection whether the user is creating a new page or not.
function isValidSelection(table: TableRef, type: IWidgetType, isNewPage: boolean|undefined) {
return table !== null && getCompatibleTypes(table, isNewPage).includes(type);
function isValidSelection(table: TableRef,
type: IWidgetType,
{isNewPage, summarize}: ICompatibleTypes) {
return table !== null && getCompatibleTypes(table, {isNewPage, summarize}).includes(type);
}
export type ISaveFunc = (val: IPageWidget) => Promise<any>;
@ -213,7 +234,13 @@ export function buildPageWidgetPicker(
// whether the current selection is valid
function isValid() {
return isValidSelection(value.table.get(), value.type.get(), options.isNewPage);
return isValidSelection(
value.table.get(),
value.type.get(),
{
isNewPage: options.isNewPage,
summarize: value.summarize.get()
});
}
// Summarizing a table causes the 'Group By' panel to expand on the right. To prevent it from
@ -299,7 +326,7 @@ export class PageWidgetSelect extends Disposable {
null;
private _isNewTableDisabled = Computed.create(this, this._value.type, (use, type) => !isValidSelection(
'New Table', type, this._options.isNewPage));
'New Table', type, {isNewPage: this._options.isNewPage, summarize: use(this._value.summarize)}));
constructor(
private _value: IWidgetValueObs,
@ -318,7 +345,9 @@ export class PageWidgetSelect extends Disposable {
header(t("Select Widget")),
sectionTypes.map((value) => {
const widgetInfo = getWidgetTypes(value);
const disabled = computed(this._value.table, (use, tid) => this._isTypeDisabled(value, tid));
const disabled = computed(this._value.table,
(use, tid) => this._isTypeDisabled(value, tid, use(this._value.summarize))
);
return cssEntry(
dom.autoDispose(disabled),
cssTypeIcon(widgetInfo.icon),
@ -355,11 +384,14 @@ export class PageWidgetSelect extends Disposable {
cssEntry.cls('-selected', (use) => use(this._value.table) === table.id()),
testId('table-label')
),
cssPivot(
cssBigIcon('Pivot'),
cssEntry.cls('-selected', (use) => use(this._value.summarize) && use(this._value.table) === table.id()),
dom.on('click', (ev, el) => this._selectPivot(table.id(), el as HTMLElement)),
testId('pivot'),
cssPivot(
cssBigIcon('Pivot'),
cssEntry.cls('-selected', (use) => use(this._value.summarize) &&
use(this._value.table) === table.id()
),
cssEntry.cls('-disabled', (use) => !isSummaryCompatible(use(this._value.type))),
dom.on('click', (ev, el) => this._selectPivot(table.id(), el as HTMLElement)),
testId('pivot'),
),
testId('table'),
)
@ -410,7 +442,12 @@ export class PageWidgetSelect extends Disposable {
// there are no changes.
this._options.buttonLabel || t("Add to Page"),
dom.prop('disabled', (use) => !isValidSelection(
use(this._value.table), use(this._value.type), this._options.isNewPage)
use(this._value.table),
use(this._value.type),
{
isNewPage: this._options.isNewPage,
summarize: use(this._value.summarize)
})
),
dom.on('click', () => this._onSave().catch(reportError)),
testId('addBtn'),
@ -464,11 +501,11 @@ export class PageWidgetSelect extends Disposable {
this._value.columns.set(newIds);
}
private _isTypeDisabled(type: IWidgetType, table: TableRef) {
private _isTypeDisabled(type: IWidgetType, table: TableRef, isSummaryOn: boolean) {
if (table === null) {
return false;
}
return !getCompatibleTypes(table, this._options.isNewPage).includes(type);
return !getCompatibleTypes(table, {isNewPage: this._options.isNewPage, summarize: isSummaryOn}).includes(type);
}
}
@ -535,6 +572,7 @@ const cssEntry = styled('div', `
&-disabled {
color: ${theme.widgetPickerItemDisabledBg};
cursor: default;
pointer-events: none;
}
&-disabled&-selected {
background-color: inherit;
@ -578,6 +616,10 @@ const cssBigIcon = styled(icon, `
width: 24px;
height: 24px;
background-color: ${theme.widgetPickerSummaryIcon};
.${cssEntry.className}-disabled > & {
opacity: 0.25;
filter: saturate(0);
}
`);
const cssFooter = styled('div', `

@ -0,0 +1,12 @@
This is the simplest example that runs Grist, suitable for local testing.
It is STRONGLY RECOMMENDED not to use this container in a way that makes it accessible to the internet.
This setup lacks basic security or authentication.
Other examples demonstrate how to set up authentication and HTTPS.
See https://support.getgrist.com/self-managed for more information.
## How to run this example
This example can be run with `docker compose up`.

@ -0,0 +1,8 @@
services:
grist:
image: gristlabs/grist:latest
volumes:
# Where to store persistent data, such as documents.
- ${PERSIST_DIR}/grist:/persist
ports:
- 8484:8484

@ -0,0 +1,24 @@
This is the simplest example of Grist with authentication and HTTPS encryption.
It uses Traefik as:
- A reverse proxy to manage certificates and provide HTTPS support
- A basic authentication provided using Traefik's Basic Auth middleware.
This setup, after configuring HTTPS certificates correctly, should be acceptable on the public internet.
However, it doesn't allow a user to sign-out due to the way browsers handle basic authentication.
You may want to try a more secure authentication setup such Authelia, Authentik or traefik-forward-auth.
The OIDC auth example demonstrates a setup using Authelia.
See https://support.getgrist.com/self-managed for more information.
## How to run this example
This example can be run with `docker compose up`.
The default login is:
- Username: `test@example.org`
- Password: `test`
This can be changed in `./configs/traefik-dynamic-config.yaml`. Instructions on how to do this are available in that file.

@ -0,0 +1,35 @@
providers:
# Enables reading docker label config values
docker: {}
# Read additional config from this file.
file:
directory: "/etc/traefik/dynamic"
entrypoints:
# Defines a secure entrypoint using TLS encryption
websecure:
address: ":443"
http:
tls: true
# Defines an insecure entrypoint that redirects to the secure one.
web:
address: ":80"
http:
# Redirects HTTP to HTTPS
redirections:
entrypoint:
to: "websecure"
scheme: "https"
# Enables automatic certificate renewal
certificatesResolvers:
letsencrypt:
acme:
email: "my_email@example.com"
storage: /acme/acme.json
tlschallenge: true
# Enables the web UI
# This is disabled by default for security, but can be useful to debugging traefik.
api:
# insecure: true

@ -0,0 +1,13 @@
http:
# Declaring the user list
middlewares:
grist-basic-auth:
basicAuth:
# The header that Grist will listen for authenticated usernames on.
headerField: "X-Forwarded-User"
# This is the list of users, in the format username:password.
# Passwords can be created using `htpasswd`
# E.g: `htpasswd -nB test@example.org`
users:
# The default username is "test@example.org". The default password is "test".
- "test@example.org:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/"

@ -0,0 +1,44 @@
services:
grist:
image: gristlabs/grist:latest
environment:
# Sets the header to look at for authentication
GRIST_FORWARD_AUTH_HEADER: X-Forwarded-User
# Forces Grist to only use a single team called 'Example'
GRIST_SINGLE_ORG: my-grist-team # alternatively, GRIST_ORG_IN_PATH: "true" for multi-team operation
# Force users to login (disable anonymous access)
GRIST_FORCE_LOGIN: true
# Base URL Grist redirects to when navigating. Change this to your domain.
APP_HOME_URL: https://grist.localhost
# Default email for the "Admin" account
GRIST_DEFAULT_EMAIL: test@example.org
volumes:
# Where to store persistent data, such as documents.
- ${PERSIST_DIR}/grist:/persist
labels:
- "traefik.http.services.grist.loadbalancer.server.port=8484"
- "traefik.http.routers.grist.rule=Host(`grist.localhost`)"
- "traefik.http.routers.grist.tls.certresolver=letsencrypt"
- "traefik.http.routers.grist-auth.rule=Host(`grist.localhost`) && (PathPrefix(`/auth/login`) || PathPrefix(`/_oauth`))"
- "traefik.http.routers.grist-auth.middlewares=grist-basic-auth@file"
- "traefik.http.routers.grist-auth.tls.certresolver=letsencrypt"
traefik:
image: traefik:latest
ports:
# HTTP Ports
- "80:80"
- "443:443"
# The Web UI (enabled by --api.insecure=true)
# - "8080:8080"
volumes:
# Set the config file for traefik - this is loaded automatically.
- ./configs/traefik-config.yml:/etc/traefik/traefik.yml
# Set the config file for the dynamic config, such as middleware.
- ./configs/traefik-dynamic-config.yml:/etc/traefik/dynamic/dynamic-config.yml
# Certificate location, if automatic certificate setup is enabled.
- ./configs/acme:/acme
# Traefik needs docker access when configured via docker labels.
- /var/run/docker.sock:/var/run/docker.sock
depends_on:
- grist

@ -0,0 +1,32 @@
This is an example of Grist with Authelia for OIDC authentication, and Traefik for HTTP encryption and routing.
OIDC enables authentication using many existing providers, including Google, Microsoft, Amazon and Okta.
This example uses Authelia, which is a locally hosted OIDC provider, so that it can work without further setup.
However, Authelia could be easily replaced by one of the providers listed above, or other self-hosted alternatives,
such as Authentik or Dex.
This example could be hosted on a dedicated server, with the following changes:
- DNS setup
- HTTPS / Certificate setup (e.g Let's encrypt)
See https://support.getgrist.com/install/oidc for more information on using Grist with OIDC.
## How to run this example
To run this example, you'll first need to generate several secrets needed by Authelia.
This is automated for you in `generateSecureSecrets.sh`, which uses Authelia's docker image to populate the `./secrets` directory.
This example can then be run with `docker compose up`. This will make Grist available on `https://grist.localhost` with a self-signed certificate (by default), after all the services have started. Note: it may take up to a minute for all of the services to start correctly.
The self-signed certificate will cause a security warning in the web browser when you try to visit Grist.
This is fine for local testing and can be bypassed, but correct certificates should be set up if Grist is being made
available on the internet.
### Users
The default username is `test`, with password `test`.
You can add or modify users in ./configs/authelia/user-database.yml. Additional instructions are provided in that file.

@ -0,0 +1,14 @@
# Primary users file.
# Passwords are generated using 'authelia crypto hash generate argon2'
# E.g:
# docker run authelia/authelia:4 authelia crypto hash generate argon2 --password "test"
# See https://www.authelia.com/reference/guides/passwords/#yaml-format
users:
test:
disabled: false
displayname: 'Test'
password: '$argon2id$v=19$m=65536,t=3,p=4$j1Jub3z0jWBmXNOjNpRK5w$d5176FINCAuzdT3uehQqMS08FC4fadAGrqyZL+0W+p4'
email: 'test@example.org'
groups: []

@ -0,0 +1,30 @@
providers:
# Enables reading docker label config values
docker: {}
entrypoints:
# Defines a secure entrypoint using TLS encryption
websecure:
address: ":443"
http:
tls: true
# Defines an insecure entrypoint that redirects to the secure one.
web:
address: ":80"
http:
# Redirects HTTP to HTTPS
redirections:
entrypoint:
to: "websecure"
scheme: "https"
# Enables automatic certificate renewal
certificatesResolvers:
letsencrypt:
acme:
email: "my_email@example.com"
storage: /acme/acme.json
tlschallenge: true
api:
insecure: true

@ -0,0 +1,118 @@
secrets:
# These secrets are used by Authelia
JWT_SECRET:
file: ${SECRETS_DIR}/JWT_SECRET
SESSION_SECRET:
file: ${SECRETS_DIR}/SESSION_SECRET
STORAGE_ENCRYPTION_KEY:
file: ${SECRETS_DIR}/STORAGE_ENCRYPTION_KEY
# These secrets are for using Authelia as an OIDC provider
HMAC_SECRET:
file: ${SECRETS_DIR}/HMAC_SECRET
JWT_PRIVATE_KEY:
file: ${SECRETS_DIR}/certs/private.pem
GRIST_CLIENT_SECRET_DIGEST:
file: ${SECRETS_DIR}/GRIST_CLIENT_SECRET_DIGEST
services:
grist:
image: gristlabs/grist:latest
environment:
# The URL of given OIDC provider. Used for redirects, among other things.
GRIST_OIDC_IDP_ISSUER: https://${AUTHELIA_DOMAIN}
# Client ID, as configured with the OIDC provider.
GRIST_OIDC_IDP_CLIENT_ID: grist-local
# Client secret, as provided by the OIDC provider.
GRIST_OIDC_IDP_CLIENT_SECRET: ${GRIST_CLIENT_SECRET}
# The URL to redirect to with the OIDC provider to log out.
# Some OIDC providers will automatically configure this.
GRIST_OIDC_IDP_END_SESSION_ENDPOINT: https://${AUTHELIA_DOMAIN}/logout
# Allow self-signed certificates so this example behaves correctly.
# REMOVE THIS IF HOSTING ON THE INTERNET.
NODE_TLS_REJECT_UNAUTHORIZED: 0
# Forces Grist to only use a single team called 'Example'
GRIST_SINGLE_ORG: my-grist-team # alternatively, GRIST_ORG_IN_PATH: "true" for multi-team operation
# Force users to login (disable anonymous access)
GRIST_FORCE_LOGIN: true
# Base URL Grist redirects to when navigating. Change this to your domain.
APP_HOME_URL: https://${GRIST_DOMAIN}
# Default email for the "Admin" account
GRIST_DEFAULT_EMAIL: ${DEFAULT_EMAIL:-test@example.org}
restart: always
volumes:
# Where to store persistent data, such as documents.
- ${PERSIST_DIR}/grist:/persist
labels:
- "traefik.http.services.grist.loadbalancer.server.port=8484"
- "traefik.http.routers.grist.rule=Host(`${GRIST_DOMAIN}`)"
- "traefik.http.routers.grist.service=grist"
# Uncomment and configure in traefik-config.yml to enable automatic HTTPS certificate setup.
#- "traefik.http.routers.grist.tls.certresolver=letsencrypt"
depends_on:
# Grist attempts to setup OIDC when it starts, making a request to the OIDC service.
# This will fail if Authelia isn't ready and reachable.
# Traefik will only start routing to Authelia when it's registered as healthy.
# Making Grist wait for Authelia to be healthy should avoid this issue.
authelia:
condition: service_healthy
traefik:
condition: service_started
traefik:
image: traefik:latest
ports:
# HTTP Ports
- "80:80"
- "443:443"
# The Web UI (enabled by --api.insecure=true)
- "8080:8080"
- "8082:8082"
volumes:
# Set the config file for traefik - this is loaded automatically.
- ./configs/traefik/config.yml:/etc/traefik/traefik.yml
# Certificate location, if automatic certificate setup is enabled.
- ./secrets/acme_certificates:/acme
# Traefik needs docker access when configured via docker labels.
- /var/run/docker.sock:/var/run/docker.sock
networks:
default:
aliases:
# Enables Grist to resolve this domain to Traefik when doing OIDC setup.
- ${AUTHELIA_DOMAIN}
authelia:
image: authelia/authelia:4
secrets:
- HMAC_SECRET
- JWT_SECRET
- JWT_PRIVATE_KEY
- GRIST_CLIENT_SECRET_DIGEST
- SESSION_SECRET
- STORAGE_ENCRYPTION_KEY
environment:
AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET_FILE: '/run/secrets/JWT_SECRET'
AUTHELIA_SESSION_SECRET_FILE: '/run/secrets/SESSION_SECRET'
AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE: '/run/secrets/STORAGE_ENCRYPTION_KEY'
HMAC_SECRET_FILE: '/run/secrets/HMAC_SECRET'
JWT_PRIVATE_KEY_FILE: '/run/secrets/JWT_PRIVATE_KEY'
# Domain Grist is hosted at. Custom variable that's interpolated into the Authelia config
APP_DOMAIN: ${GRIST_DOMAIN}
# Where Authelia should redirect to after successful authentication.
GRIST_OAUTH_CALLBACK_URL: https://${GRIST_DOMAIN}/oauth2/callback
# Hash of the client secret provided to Grist.
GRIST_CLIENT_SECRET_DIGEST_FILE: "/run/secrets/GRIST_CLIENT_SECRET_DIGEST"
volumes:
- ./configs/authelia:/config
- ${PERSIST_DIR}/authelia:/persist
command:
- 'authelia'
- '--config=/config/configuration.yml'
# Enables templating in the config file
- '--config.experimental.filters=template'
labels:
- "traefik.http.services.authelia.loadbalancer.server.port=9091"
- "traefik.http.routers.authelia.rule=Host(`${AUTHELIA_DOMAIN}`)"
- "traefik.http.routers.authelia.service=authelia"
# Uncomment and configure in traefik-config.yml to enable automatic HTTPS certificate setup.
#- "traefik.http.routers.authelia.tls.certresolver=letsencrypt"

@ -0,0 +1,6 @@
GRIST_DOMAIN=grist.localhost
AUTHELIA_DOMAIN=auth.grist.localhost
DEFAULT_EMAIL=test@example.org
PERSIST_DIR=./persist
SECRETS_DIR=./secrets
GRIST_CLIENT_SECRET=

@ -0,0 +1,32 @@
# Helper script to securely generate random secrets for Authelia.
SCRIPT_DIR=$(dirname $0)
# Copy over template files to final locations
cp -R "$SCRIPT_DIR/secrets_template" "$SCRIPT_DIR/secrets"
cp "$SCRIPT_DIR/env-template" "$SCRIPT_DIR/.env"
# Parses an Aurelia generated secret for the value
function getSecret {
cut -d ":" -f 2 <<< "$1" | tr -d '[:blank:]'
}
function generateSecureString {
getSecret "$(docker run authelia/authelia:4 authelia crypto rand --charset=rfc3986 --length="$1")"
}
generateSecureString 128 > "$SCRIPT_DIR/secrets/HMAC_SECRET"
generateSecureString 128 > "$SCRIPT_DIR/secrets/JWT_SECRET"
generateSecureString 128 > "$SCRIPT_DIR/secrets/SESSION_SECRET"
generateSecureString 128 > "$SCRIPT_DIR/secrets/STORAGE_ENCRYPTION_KEY"
# Generates the OIDC secret key for the Grist client
CLIENT_SECRET_OUTPUT="$(docker run authelia/authelia:4 authelia crypto hash generate pbkdf2 --variant sha512 --random --random.length 72 --random.charset rfc3986)"
CLIENT_SECRET=$(getSecret "$(grep 'Password' <<< $CLIENT_SECRET_OUTPUT)")
sed -i "/GRIST_CLIENT_SECRET=$/d" "$SCRIPT_DIR/.env"
echo "GRIST_CLIENT_SECRET=$CLIENT_SECRET" >> "$SCRIPT_DIR/.env"
getSecret "$(grep 'Digest' <<< $CLIENT_SECRET_OUTPUT)" >> "$SCRIPT_DIR/secrets/GRIST_CLIENT_SECRET_DIGEST"
# Generate JWT certificates Authelia needs for OIDC
docker run -v ./secrets/certs:/certs authelia/authelia:4 authelia crypto certificate rsa generate -d /certs

@ -0,0 +1,3 @@
DATABASE_PASSWORD=CHANGE THIS PASSWORD
MINIO_PASSWORD=CHANGE THIS PASSWORD
PERSIST_DIR=./persist

@ -0,0 +1,20 @@
This examples shows how to start up Grist that:
- Uses Postgres as a home database,
- Redis as a state store.
- MinIO for snapshot storage
It is STRONGLY RECOMMENDED not to use this container in a way that makes it accessible to the internet.
This setup lacks basic security or authentication.
Other examples demonstrate how to set up authentication and HTTPS.
See https://support.getgrist.com/self-managed for more information.
This setup is based on one provided by Akito (https://github.com/theAkito).
## How to run this example
Before running this example, it's very strongly recommended to update the `_PASSWORD` environment variables
in `.env` to be long, randomly generated passwords.
This example can be run with `docker compose up`.

@ -0,0 +1,76 @@
services:
grist:
image: gristlabs/grist:latest
environment:
# Postgres database setup
TYPEORM_DATABASE: grist
TYPEORM_USERNAME: grist
TYPEORM_HOST: grist-db
TYPEORM_LOGGING: false
TYPEORM_PASSWORD: ${DATABASE_PASSWORD}
TYPEORM_PORT: 5432
TYPEORM_TYPE: postgres
# Redis setup
REDIS_URL: redis://grist-redis
# MinIO setup. This requires the bucket set up on the MinIO instance with versioning enabled.
GRIST_DOCS_MINIO_ACCESS_KEY: grist
GRIST_DOCS_MINIO_SECRET_KEY: ${MINIO_PASSWORD}
GRIST_DOCS_MINIO_USE_SSL: 0
GRIST_DOCS_MINIO_BUCKET: grist-docs
GRIST_DOCS_MINIO_ENDPOINT: grist-minio
GRIST_DOCS_MINIO_PORT: 9000
volumes:
# Where to store persistent data, such as documents.
- ${PERSIST_DIR}/grist:/persist
ports:
- 8484:8484
depends_on:
- grist-db
- grist-redis
- grist-minio
- minio-setup
grist-db:
image: postgres:alpine
environment:
POSTGRES_DB: grist
POSTGRES_USER: grist
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
volumes:
- ${PERSIST_DIR}/postgres:/var/lib/postgresql/data
grist-redis:
image: redis:alpine
volumes:
- ${PERSIST_DIR}/redis:/data
grist-minio:
image: minio/minio:latest
environment:
MINIO_ROOT_USER: grist
MINIO_ROOT_PASSWORD: ${MINIO_PASSWORD}
volumes:
- ${PERSIST_DIR}/minio:/data
command:
server /data --console-address=":9001"
# This sets up the buckets required in MinIO. It is only needed to make this example work.
# It isn't necessary for deployment and can be safely removed.
minio-setup:
image: minio/mc
environment:
MINIO_PASSWORD: ${MINIO_PASSWORD}
depends_on:
grist-minio:
condition: service_started
restart: on-failure
entrypoint: >
/bin/sh -c "
/usr/bin/mc alias set myminio http://grist-minio:9000 grist '$MINIO_PASSWORD';
/usr/bin/mc mb myminio/grist-docs;
/usr/bin/mc anonymous set public myminio/grist-docs;
/usr/bin/mc version enable myminio/grist-docs;
"

@ -1,6 +1,6 @@
{
"name": "grist-core",
"version": "1.1.16",
"version": "1.1.17",
"license": "Apache-2.0",
"description": "Grist is the evolution of spreadsheets",
"homepage": "https://github.com/gristlabs/grist-core",

@ -1273,7 +1273,7 @@
},
"FieldEditor": {
"It should be impossible to save a plain data value into a formula column": "Ezinezkoa litzateke datu-balio soil bat formula-zutabe batean gordetzea",
"Unable to finish saving edited cell": "Ezin izan da gelaxka gordetzen amaitu"
"Unable to finish saving edited cell": "Ezin da gelaxka gordetzen amaitu"
},
"HyperLinkEditor": {
"[link label] url": "[link label] URLa"

@ -1237,7 +1237,8 @@
"Sorry, not all fields can be edited.": "Spiacente, non tutti i campi possono essere modificati.",
"Status": "Status",
"URL": "URL",
"Filter for changes in these columns (semicolon-separated ids)": "FIltrare i cambiamenti in queste colonne (id separati da ;)"
"Filter for changes in these columns (semicolon-separated ids)": "FIltrare i cambiamenti in queste colonne (id separati da ;)",
"Header Authorization": "Header autorizzazione"
},
"Clipboard": {
"Unavailable Command": "Comando non disponibile",
@ -1498,7 +1499,12 @@
"Authentication": "Autenticazione",
"Check failed.": "Controllo fallito.",
"Check succeeded.": "Controllo riuscito.",
"Grist allows different types of authentication to be configured, including SAML and OIDC. We recommend enabling one of these if Grist is accessible over the network or being made available to multiple people.": "Grist consente di configurare diversi tipi di autenticazione, compresi SAML e OIDC. Raccomandiamo di attivarne uno, se Grist è dispobile in rete, o raggiungibile da più persone."
"Grist allows different types of authentication to be configured, including SAML and OIDC. We recommend enabling one of these if Grist is accessible over the network or being made available to multiple people.": "Grist consente di configurare diversi tipi di autenticazione, compresi SAML e OIDC. Raccomandiamo di attivarne uno, se Grist è dispobile in rete, o raggiungibile da più persone.",
"Session Secret": "Segreto per la sessione",
"Grist signs user session cookies with a secret key. Please set this key via the environment variable GRIST_SESSION_SECRET. Grist falls back to a hard-coded default when it is not set. We may remove this notice in the future as session IDs generated since v1.1.16 are inherently cryptographically secure.": "Grist firma i cookie della sessione con una chiave segreta. Impostare questa chiave con la variabile d'ambiente GRIST_SESSION_SECRET. Se questa non è definita, Grist usa un default non modificabile. Potremmo rimuovere questo avviso in futuro, perché gli ID di sessione generati a partire dalla versione 1.1.16 sono intrinsecamente sicuri dal punto di vista crittografico.",
"Enable Grist Enterprise": "Attiva Grist Enterprise",
"Enterprise": "Enterprise",
"Key to sign sessions with": "Chiave per marcare le sessioni"
},
"WelcomeCoachingCall": {
"Maybe Later": "Forse più tardi",
@ -1629,5 +1635,50 @@
"Table ID": "ID Tabella",
"Column ID": "ID colonna",
"Formula timer": "Cronometro formule"
},
"ToggleEnterpriseWidget": {
"An activation key is used to run Grist Enterprise after a trial period\nof 30 days has expired. Get an activation key by [signing up for Grist\nEnterprise]({{signupLink}}). You do not need an activation key to run\nGrist Core.\n\nLearn more in our [Help Center]({{helpCenter}}).": "La chiave di attivazione serve a usare Grist Enterprise dopo la fine\ndei 30 giorni di prova. Ottieni la chiave di attivazione [iscrivendoti a Grist\nEnterprise]({{signupLink}}). Non c'è bisogno di una chiave di attivazione per\nGrist Core.\n\nScopri di più nel nostro [Centro Assistenza]({{helpCenter}}).",
"Disable Grist Enterprise": "Disattiva Grist Enterprise",
"Enable Grist Enterprise": "Attiva Grist Enterprise",
"Grist Enterprise is **enabled**.": "Grist Enterprise è **attivato**."
},
"DocTutorial": {
"Finish": "Finisci",
"Previous": "Precedente",
"Next": "Successivo",
"Restart": "Riparti",
"Click to expand": "Clicca per espandere",
"Do you want to restart the tutorial? All progress will be lost.": "Vuoi ricominciare il tutorial? Tutti i progressi fatti saranno azzerati.",
"End tutorial": "Termina il tutorial"
},
"OnboardingCards": {
"3 minute video tour": "Video introduttivo di tre minuti",
"Complete our basics tutorial": "Completa il nostro tutorial di base",
"Complete the tutorial": "Completa il tutorial",
"Learn the basic of reference columns, linked widgets, column types, & cards.": "Impara le basi sulle colonne di riferimenti, i widget collegati, i tipi di colonna e le schede."
},
"OnboardingPage": {
"Welcome": "Benvenuto",
"What brings you to Grist (you can select multiple)?": "Che cosa ti ha portato a Grist (puoi scegliere più opzioni)?",
"Go to the tutorial!": "Vai al tutorial!",
"Next step": "Passo successivo",
"Type here": "Scrivi qui",
"Tell us who you are": "Dicci chi sei",
"What organization are you with?": "Di che organizzazione fai parte?",
"Back": "Indietro",
"Discover Grist in 3 minutes": "Scopri Grist in tre minuti",
"Go hands-on with the Grist Basics tutorial": "Inizia a lavorare con il tutorial introduttivo di Grist",
"Skip step": "Salta questo passaggio",
"Skip tutorial": "Salta il tutorial",
"What is your role?": "Qual è il tuo ruolo?",
"Your organization": "La tua organizzazione",
"Your role": "Il tuo ruolo"
},
"ViewLayout": {
"Delete": "Cancella",
"Delete data and this widget.": "Cancella i dati e questo widget.",
"Keep data and delete widget. Table will remain available in {{rawDataLink}}": "Mantieni i dati e cancella il widget. La tabella resterà disponibile in {{rawDataLink}}",
"Table {{tableName}} will no longer be visible": "La tabella {{tableName}} non sarà più visibile",
"raw data page": "pagina dei dati grezzi"
}
}

@ -0,0 +1,12 @@
{
"ACUserManager": {
"Invite new member": "Yeni Üye Davet gönder",
"Enter email address": "mail adresinizi girin",
"We'll email an invite to {{email}}": "{{email}} .. adresine davet gönderilecek"
},
"AccessRules": {
"Add Column Rule": "Sütuna Kural ekle",
"Add Default Rule": "Kural ekle (genel)",
"Add Table Rules": "Tabloya kural ekle"
}
}

@ -99,7 +99,6 @@ describe("saveViewSection", function() {
await switchTypeAndAssert('Card');
await switchTypeAndAssert('Table');
await switchTypeAndAssert('Chart');
});
it("should work correctly when changing grouped by column", async () => {
@ -160,4 +159,30 @@ describe("saveViewSection", function() {
// Check all columns are visible.
await assertActiveSectionColumns('Test', 'count');
});
it("should disable summary when form type is selected", async () => {
// select form type
await driver.find('.test-dp-add-new').doClick();
await driver.find('.test-dp-add-new-page').doClick();
await driver.findContent('.test-wselect-type', gu.exactMatch("Form")).doClick();
// check that summary is disabled
assert.ok(await driver.find('.test-wselect-pivot[class*=-disabled]'));
// close page widget picker
await driver.sendKeys(Key.ESCAPE);
});
it("should disable form when summary is selected", async () => {
// select table type then select summary for a Table
await driver.find('.test-dp-add-new').doClick();
await driver.find('.test-dp-add-new-page').doClick();
await driver.find('.test-wselect-pivot').doClick();
// check that form is disabled
assert.equal(await driver.find('.test-wselect-type[class*=-disabled]').getText(), "Form");
// close page widget picker
await driver.sendKeys(Key.ESCAPE);
});
});

Loading…
Cancel
Save