mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) updates from grist-core
This commit is contained in:
commit
0a78cdbaab
5
.gitignore
vendored
5
.gitignore
vendored
@ -83,3 +83,8 @@ xunit.xml
|
|||||||
|
|
||||||
# ext directory can be overwritten
|
# ext directory can be overwritten
|
||||||
ext/**
|
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;
|
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-');
|
const testId = makeTestId('test-wselect-');
|
||||||
|
|
||||||
// The picker disables some choices that do not make much sense. This function return the list of
|
// 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.
|
// 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') {
|
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) {
|
} else if (isNewPage) {
|
||||||
// New view + new table means we'll be switching to the primary view.
|
// New view + new table means we'll be switching to the primary view.
|
||||||
return ['record', 'form'];
|
compatibleTypes = ['record', 'form'];
|
||||||
} else {
|
} else {
|
||||||
// The type 'chart' makes little sense when creating a new table.
|
// 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.
|
// 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) {
|
function isValidSelection(table: TableRef,
|
||||||
return table !== null && getCompatibleTypes(table, isNewPage).includes(type);
|
type: IWidgetType,
|
||||||
|
{isNewPage, summarize}: ICompatibleTypes) {
|
||||||
|
return table !== null && getCompatibleTypes(table, {isNewPage, summarize}).includes(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ISaveFunc = (val: IPageWidget) => Promise<any>;
|
export type ISaveFunc = (val: IPageWidget) => Promise<any>;
|
||||||
@ -213,7 +234,13 @@ export function buildPageWidgetPicker(
|
|||||||
|
|
||||||
// whether the current selection is valid
|
// whether the current selection is valid
|
||||||
function isValid() {
|
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
|
// 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;
|
null;
|
||||||
|
|
||||||
private _isNewTableDisabled = Computed.create(this, this._value.type, (use, type) => !isValidSelection(
|
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(
|
constructor(
|
||||||
private _value: IWidgetValueObs,
|
private _value: IWidgetValueObs,
|
||||||
@ -318,7 +345,9 @@ export class PageWidgetSelect extends Disposable {
|
|||||||
header(t("Select Widget")),
|
header(t("Select Widget")),
|
||||||
sectionTypes.map((value) => {
|
sectionTypes.map((value) => {
|
||||||
const widgetInfo = getWidgetTypes(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(
|
return cssEntry(
|
||||||
dom.autoDispose(disabled),
|
dom.autoDispose(disabled),
|
||||||
cssTypeIcon(widgetInfo.icon),
|
cssTypeIcon(widgetInfo.icon),
|
||||||
@ -355,11 +384,14 @@ export class PageWidgetSelect extends Disposable {
|
|||||||
cssEntry.cls('-selected', (use) => use(this._value.table) === table.id()),
|
cssEntry.cls('-selected', (use) => use(this._value.table) === table.id()),
|
||||||
testId('table-label')
|
testId('table-label')
|
||||||
),
|
),
|
||||||
cssPivot(
|
cssPivot(
|
||||||
cssBigIcon('Pivot'),
|
cssBigIcon('Pivot'),
|
||||||
cssEntry.cls('-selected', (use) => use(this._value.summarize) && use(this._value.table) === table.id()),
|
cssEntry.cls('-selected', (use) => use(this._value.summarize) &&
|
||||||
dom.on('click', (ev, el) => this._selectPivot(table.id(), el as HTMLElement)),
|
use(this._value.table) === table.id()
|
||||||
testId('pivot'),
|
),
|
||||||
|
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'),
|
testId('table'),
|
||||||
)
|
)
|
||||||
@ -410,7 +442,12 @@ export class PageWidgetSelect extends Disposable {
|
|||||||
// there are no changes.
|
// there are no changes.
|
||||||
this._options.buttonLabel || t("Add to Page"),
|
this._options.buttonLabel || t("Add to Page"),
|
||||||
dom.prop('disabled', (use) => !isValidSelection(
|
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)),
|
dom.on('click', () => this._onSave().catch(reportError)),
|
||||||
testId('addBtn'),
|
testId('addBtn'),
|
||||||
@ -464,11 +501,11 @@ export class PageWidgetSelect extends Disposable {
|
|||||||
this._value.columns.set(newIds);
|
this._value.columns.set(newIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _isTypeDisabled(type: IWidgetType, table: TableRef) {
|
private _isTypeDisabled(type: IWidgetType, table: TableRef, isSummaryOn: boolean) {
|
||||||
if (table === null) {
|
if (table === null) {
|
||||||
return false;
|
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 {
|
&-disabled {
|
||||||
color: ${theme.widgetPickerItemDisabledBg};
|
color: ${theme.widgetPickerItemDisabledBg};
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
&-disabled&-selected {
|
&-disabled&-selected {
|
||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
@ -578,6 +616,10 @@ const cssBigIcon = styled(icon, `
|
|||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
background-color: ${theme.widgetPickerSummaryIcon};
|
background-color: ${theme.widgetPickerSummaryIcon};
|
||||||
|
.${cssEntry.className}-disabled > & {
|
||||||
|
opacity: 0.25;
|
||||||
|
filter: saturate(0);
|
||||||
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const cssFooter = styled('div', `
|
const cssFooter = styled('div', `
|
||||||
|
@ -1 +1 @@
|
|||||||
0.9.6
|
0.9.7
|
||||||
|
12
docker-compose-examples/grist-local-testing/README.md
Normal file
12
docker-compose-examples/grist-local-testing/README.md
Normal file
@ -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
|
24
docker-compose-examples/grist-traefik-basic-auth/README.md
Normal file
24
docker-compose-examples/grist-traefik-basic-auth/README.md
Normal file
@ -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
|
32
docker-compose-examples/grist-traefik-oidc-auth/README.md
Normal file
32
docker-compose-examples/grist-traefik-oidc-auth/README.md
Normal file
@ -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.
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
@ -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=
|
32
docker-compose-examples/grist-traefik-oidc-auth/generateSecureSecrets.sh
Executable file
32
docker-compose-examples/grist-traefik-oidc-auth/generateSecureSecrets.sh
Executable file
@ -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",
|
"name": "grist-core",
|
||||||
"version": "1.1.16",
|
"version": "1.1.17",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "Grist is the evolution of spreadsheets",
|
"description": "Grist is the evolution of spreadsheets",
|
||||||
"homepage": "https://github.com/gristlabs/grist-core",
|
"homepage": "https://github.com/gristlabs/grist-core",
|
||||||
|
@ -1273,7 +1273,7 @@
|
|||||||
},
|
},
|
||||||
"FieldEditor": {
|
"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",
|
"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": {
|
"HyperLinkEditor": {
|
||||||
"[link label] url": "[link label] URLa"
|
"[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.",
|
"Sorry, not all fields can be edited.": "Spiacente, non tutti i campi possono essere modificati.",
|
||||||
"Status": "Status",
|
"Status": "Status",
|
||||||
"URL": "URL",
|
"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": {
|
"Clipboard": {
|
||||||
"Unavailable Command": "Comando non disponibile",
|
"Unavailable Command": "Comando non disponibile",
|
||||||
@ -1498,7 +1499,12 @@
|
|||||||
"Authentication": "Autenticazione",
|
"Authentication": "Autenticazione",
|
||||||
"Check failed.": "Controllo fallito.",
|
"Check failed.": "Controllo fallito.",
|
||||||
"Check succeeded.": "Controllo riuscito.",
|
"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": {
|
"WelcomeCoachingCall": {
|
||||||
"Maybe Later": "Forse più tardi",
|
"Maybe Later": "Forse più tardi",
|
||||||
@ -1629,5 +1635,50 @@
|
|||||||
"Table ID": "ID Tabella",
|
"Table ID": "ID Tabella",
|
||||||
"Column ID": "ID colonna",
|
"Column ID": "ID colonna",
|
||||||
"Formula timer": "Cronometro formule"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
12
static/locales/tr.client.json
Normal file
12
static/locales/tr.client.json
Normal file
@ -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('Card');
|
||||||
await switchTypeAndAssert('Table');
|
await switchTypeAndAssert('Table');
|
||||||
await switchTypeAndAssert('Chart');
|
await switchTypeAndAssert('Chart');
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should work correctly when changing grouped by column", async () => {
|
it("should work correctly when changing grouped by column", async () => {
|
||||||
@ -160,4 +159,30 @@ describe("saveViewSection", function() {
|
|||||||
// Check all columns are visible.
|
// Check all columns are visible.
|
||||||
await assertActiveSectionColumns('Test', 'count');
|
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…
Reference in New Issue
Block a user