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
450472f74c
5
.github/workflows/docker.yml
vendored
5
.github/workflows/docker.yml
vendored
@ -15,7 +15,7 @@ jobs:
|
|||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v3
|
uses: docker/metadata-action@v4
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ github.repository_owner }}/grist
|
${{ github.repository_owner }}/grist
|
||||||
@ -23,6 +23,9 @@ jobs:
|
|||||||
type=ref,event=branch
|
type=ref,event=branch
|
||||||
type=ref,event=pr
|
type=ref,event=pr
|
||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=semver,pattern={{major}}
|
||||||
|
stable
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v1
|
uses: docker/setup-qemu-action@v1
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
|
@ -1178,8 +1178,8 @@ Useful for examples and templates, but not for sensitive data.`),
|
|||||||
},
|
},
|
||||||
SchemaEdit: {
|
SchemaEdit: {
|
||||||
name: t("Permission to edit document structure"),
|
name: t("Permission to edit document structure"),
|
||||||
description: t("Allow editors to edit structure (e.g. modify and delete tables, columns, " +
|
description: t("Allow editors to edit structure (e.g. modify and delete tables, columns, \
|
||||||
"layouts), and to write formulas, which give access to all data regardless of read restrictions."),
|
layouts), and to write formulas, which give access to all data regardless of read restrictions."),
|
||||||
availableBits: ['schemaEdit'],
|
availableBits: ['schemaEdit'],
|
||||||
...schemaEditRules.denyEditors,
|
...schemaEditRules.denyEditors,
|
||||||
},
|
},
|
||||||
@ -1323,7 +1323,7 @@ class SpecialSchemaObsRuleSet extends SpecialObsRuleSet {
|
|||||||
return dom.maybe(
|
return dom.maybe(
|
||||||
(use) => use(this._body).every(rule => rule.isBuiltInOrEmpty(use)),
|
(use) => use(this._body).every(rule => rule.isBuiltInOrEmpty(use)),
|
||||||
() => cssConditionError({style: 'margin-left: 56px; margin-bottom: 8px;'},
|
() => cssConditionError({style: 'margin-left: 56px; margin-bottom: 8px;'},
|
||||||
"This default should be changed if editors' access is to be limited. ",
|
t("This default should be changed if editors' access is to be limited. "),
|
||||||
dom('a', {style: 'color: inherit; text-decoration: underline'},
|
dom('a', {style: 'color: inherit; text-decoration: underline'},
|
||||||
'Dismiss', dom.on('click', () => this._allowEditors('confirm'))),
|
'Dismiss', dom.on('click', () => this._allowEditors('confirm'))),
|
||||||
testId('rule-schema-edit-warning'),
|
testId('rule-schema-edit-warning'),
|
||||||
|
@ -660,8 +660,9 @@ export class ChartConfig extends GrainJSDisposable {
|
|||||||
),
|
),
|
||||||
dom.domComputed(this._optionsObj.prop('errorBars'), (value: ChartOptions["errorBars"]) =>
|
dom.domComputed(this._optionsObj.prop('errorBars'), (value: ChartOptions["errorBars"]) =>
|
||||||
value === 'symmetric' ? cssRowHelp(t("Each Y series is followed by a series for the length of error bars.")) :
|
value === 'symmetric' ? cssRowHelp(t("Each Y series is followed by a series for the length of error bars.")) :
|
||||||
value === 'separate' ? cssRowHelp(t("Each Y series is followed by two series, for " +
|
value === 'separate' ? cssRowHelp(
|
||||||
"top and bottom error bars."))
|
t("Each Y series is followed by two series, for top and bottom error bars.")
|
||||||
|
)
|
||||||
: null
|
: null
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
|
@ -265,10 +265,9 @@ export class DocPageModelImpl extends Disposable implements DocPageModel {
|
|||||||
{
|
{
|
||||||
explanation: (
|
explanation: (
|
||||||
isDocOwner
|
isDocOwner
|
||||||
? t("You can try reloading the document, or using recovery mode. " +
|
? t("You can try reloading the document, or using recovery mode. \
|
||||||
"Recovery mode opens the document to be fully accessible to " +
|
Recovery mode opens the document to be fully accessible to owners, and inaccessible to others. \
|
||||||
"owners, and inaccessible to others. It also disables " +
|
It also disables formulas. [{{error}}]", {error: err.message})
|
||||||
"formulas. [{{error}}]", {error: err.message})
|
|
||||||
: isDenied
|
: isDenied
|
||||||
? t('Sorry, access to this document has been denied. [{{error}}]', {error: err.message})
|
? t('Sorry, access to this document has been denied. [{{error}}]', {error: err.message})
|
||||||
: t("Document owners can attempt to recover the document. [{{error}}]", {error: err.message})
|
: t("Document owners can attempt to recover the document. [{{error}}]", {error: err.message})
|
||||||
|
@ -12,6 +12,9 @@ import {TableData} from 'app/common/TableData';
|
|||||||
import {BaseFormatter} from 'app/common/ValueFormatter';
|
import {BaseFormatter} from 'app/common/ValueFormatter';
|
||||||
import {Computed, Disposable, Observable} from 'grainjs';
|
import {Computed, Disposable, Observable} from 'grainjs';
|
||||||
import debounce = require('lodash/debounce');
|
import debounce = require('lodash/debounce');
|
||||||
|
import { makeT } from 'app/client/lib/localization';
|
||||||
|
|
||||||
|
const t = makeT('SearchModel');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SearchModel used to maintain the state of the search UI.
|
* SearchModel used to maintain the state of the search UI.
|
||||||
@ -201,7 +204,7 @@ class FinderImpl implements IFinder {
|
|||||||
// sort in order that is the same as on the raw data list page,
|
// sort in order that is the same as on the raw data list page,
|
||||||
.sort((a, b) => nativeCompare(a.tableNameDef.peek(), b.tableNameDef.peek()))
|
.sort((a, b) => nativeCompare(a.tableNameDef.peek(), b.tableNameDef.peek()))
|
||||||
// get rawViewSection,
|
// get rawViewSection,
|
||||||
.map(t => t.rawViewSection.peek())
|
.map(table => table.rawViewSection.peek())
|
||||||
// and test if it isn't an empty record.
|
// and test if it isn't an empty record.
|
||||||
.filter(s => Boolean(s.id.peek()));
|
.filter(s => Boolean(s.id.peek()));
|
||||||
// Pretend that those are pages.
|
// Pretend that those are pages.
|
||||||
@ -218,7 +221,7 @@ class FinderImpl implements IFinder {
|
|||||||
// Else read all visible pages.
|
// Else read all visible pages.
|
||||||
const pages = this._gristDoc.docModel.visibleDocPages.peek();
|
const pages = this._gristDoc.docModel.visibleDocPages.peek();
|
||||||
this._pageStepper.array = pages.map(p => new PageRecWrapper(p, this._openDocPageCB));
|
this._pageStepper.array = pages.map(p => new PageRecWrapper(p, this._openDocPageCB));
|
||||||
this._pageStepper.index = pages.findIndex(t => t.viewRef.peek() === this._gristDoc.activeViewId.get());
|
this._pageStepper.index = pages.findIndex(page => page.viewRef.peek() === this._gristDoc.activeViewId.get());
|
||||||
if (this._pageStepper.index < 0) { return false; }
|
if (this._pageStepper.index < 0) { return false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -468,7 +471,7 @@ export class SearchModelImpl extends Disposable implements SearchModel {
|
|||||||
this.autoDispose(this.multiPage.addListener(v => { if (v) { this.noMatch.set(false); } }));
|
this.autoDispose(this.multiPage.addListener(v => { if (v) { this.noMatch.set(false); } }));
|
||||||
|
|
||||||
this.allLabel = Computed.create(this, use => use(this._gristDoc.activeViewId) === 'data' ?
|
this.allLabel = Computed.create(this, use => use(this._gristDoc.activeViewId) === 'data' ?
|
||||||
'Search all tables' : 'Search all pages');
|
t('Search all tables') : t('Search all pages'));
|
||||||
|
|
||||||
// Schedule a search restart when user changes pages (otherwise search would resume from the
|
// Schedule a search restart when user changes pages (otherwise search would resume from the
|
||||||
// previous page that is not shown anymore). Also revert noMatch flag when in single page mode.
|
// previous page that is not shown anymore). Also revert noMatch flag when in single page mode.
|
||||||
|
@ -131,9 +131,8 @@ export class AccountPage extends Disposable {
|
|||||||
),
|
),
|
||||||
css.subHeader(t("Two-factor authentication")),
|
css.subHeader(t("Two-factor authentication")),
|
||||||
css.description(
|
css.description(
|
||||||
t("Two-factor authentication is an extra layer of security for your Grist account " +
|
t("Two-factor authentication is an extra layer of security for your Grist account \
|
||||||
"designed to ensure that you're the only person who can access your account, " +
|
designed to ensure that you're the only person who can access your account, even if someone knows your password.")
|
||||||
"even if someone knows your password.")
|
|
||||||
),
|
),
|
||||||
dom.create(MFAConfig, user),
|
dom.create(MFAConfig, user),
|
||||||
),
|
),
|
||||||
|
@ -78,8 +78,8 @@ export class ApiKey extends Disposable {
|
|||||||
dom.maybe((use) => !(use(this._apiKey) || this._anonymous), () => [
|
dom.maybe((use) => !(use(this._apiKey) || this._anonymous), () => [
|
||||||
basicButton(t("Create"), dom.on('click', () => this._onCreate()), testId('create'),
|
basicButton(t("Create"), dom.on('click', () => this._onCreate()), testId('create'),
|
||||||
dom.boolAttr('disabled', this._loading)),
|
dom.boolAttr('disabled', this._loading)),
|
||||||
description(t("By generating an API key, you will be able to " +
|
description(t("By generating an API key, you will be able to \
|
||||||
"make API calls for your own account."), testId('description')),
|
make API calls for your own account."), testId('description')),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -117,8 +117,8 @@ export class ApiKey extends Disposable {
|
|||||||
() => this._onDelete(),
|
() => this._onDelete(),
|
||||||
{
|
{
|
||||||
explanation: t(
|
explanation: t(
|
||||||
"You're about to delete an API key. This will cause all future requests " +
|
"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?"
|
using this API key to be rejected. Do you still want to delete?"
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -21,8 +21,8 @@ export async function startDocTour(docData: DocData, docComm: DocComm, onFinishC
|
|||||||
|
|
||||||
const invalidDocTour: IOnBoardingMsg[] = [{
|
const invalidDocTour: IOnBoardingMsg[] = [{
|
||||||
title: t("No valid document tour"),
|
title: t("No valid document tour"),
|
||||||
body: t("Cannot construct a document tour from the data in this document. " +
|
body: t("Cannot construct a document tour from the data in this document. \
|
||||||
"Ensure there is a table named GristDocTour with columns Title, Body, Placement, and Location."),
|
Ensure there is a table named GristDocTour with columns Title, Body, Placement, and Location."),
|
||||||
selector: 'document',
|
selector: 'document',
|
||||||
showHasModal: true,
|
showHasModal: true,
|
||||||
}];
|
}];
|
||||||
|
@ -36,8 +36,8 @@ export const buildExamples = (): IExampleInfo[] => [{
|
|||||||
tutorialUrl: 'https://support.getgrist.com/investment-research/',
|
tutorialUrl: 'https://support.getgrist.com/investment-research/',
|
||||||
welcomeCard: {
|
welcomeCard: {
|
||||||
title: t("Welcome to the Investment Research template"),
|
title: t("Welcome to the Investment Research template"),
|
||||||
text: t("Check out our related tutorial to learn how to create " +
|
text: t("Check out our related tutorial to learn how to create \
|
||||||
"summary tables and charts, and to link charts dynamically."),
|
summary tables and charts, and to link charts dynamically."),
|
||||||
tutorialName: t("Tutorial: Analyze & Visualize"),
|
tutorialName: t("Tutorial: Analyze & Visualize"),
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
|
@ -50,8 +50,7 @@ export const GristTooltips: Record<Tooltip, TooltipContentFunc> = {
|
|||||||
t('Formulas that trigger in certain cases, and store the calculated value as data.')
|
t('Formulas that trigger in certain cases, and store the calculated value as data.')
|
||||||
),
|
),
|
||||||
dom('div',
|
dom('div',
|
||||||
t('Useful for storing the timestamp or author of a new record, data cleaning, and '
|
t('Useful for storing the timestamp or author of a new record, data cleaning, and more.')
|
||||||
+ 'more.')
|
|
||||||
),
|
),
|
||||||
dom('div',
|
dom('div',
|
||||||
cssLink({href: commonUrls.helpTriggerFormulas, target: '_blank'}, t('Learn more.')),
|
cssLink({href: commonUrls.helpTriggerFormulas, target: '_blank'}, t('Learn more.')),
|
||||||
@ -76,8 +75,8 @@ export const GristTooltips: Record<Tooltip, TooltipContentFunc> = {
|
|||||||
),
|
),
|
||||||
openAccessRules: (...args: DomElementArg[]) => cssTooltipContent(
|
openAccessRules: (...args: DomElementArg[]) => cssTooltipContent(
|
||||||
dom('div',
|
dom('div',
|
||||||
t('Access rules give you the power to create nuanced rules to determine who can '
|
t('Access rules give you the power to create nuanced rules to determine who can \
|
||||||
+ 'see or edit which parts of your document.')
|
see or edit which parts of your document.')
|
||||||
),
|
),
|
||||||
dom('div',
|
dom('div',
|
||||||
cssLink({href: commonUrls.helpAccessRules, target: '_blank'}, t('Learn more.')),
|
cssLink({href: commonUrls.helpAccessRules, target: '_blank'}, t('Learn more.')),
|
||||||
@ -126,8 +125,8 @@ export const GristBehavioralPrompts: Record<BehavioralPrompt, BehavioralPromptCo
|
|||||||
title: () => t('Reference Columns'),
|
title: () => t('Reference Columns'),
|
||||||
content: (...args: DomElementArg[]) => cssTooltipContent(
|
content: (...args: DomElementArg[]) => cssTooltipContent(
|
||||||
dom('div', t('Select the table to link to.')),
|
dom('div', t('Select the table to link to.')),
|
||||||
dom('div', t('Cells in a reference column always identify an {{entire}} ' +
|
dom('div', t('Cells in a reference column always identify an {{entire}} \
|
||||||
'record in that table, but you may select which column from that record to show.', {
|
record in that table, but you may select which column from that record to show.', {
|
||||||
entire: cssItalicizedText(t('entire'))
|
entire: cssItalicizedText(t('entire'))
|
||||||
})),
|
})),
|
||||||
dom('div',
|
dom('div',
|
||||||
@ -140,8 +139,8 @@ export const GristBehavioralPrompts: Record<BehavioralPrompt, BehavioralPromptCo
|
|||||||
rawDataPage: {
|
rawDataPage: {
|
||||||
title: () => t('Raw Data page'),
|
title: () => t('Raw Data page'),
|
||||||
content: (...args: DomElementArg[]) => cssTooltipContent(
|
content: (...args: DomElementArg[]) => cssTooltipContent(
|
||||||
dom('div', t('The Raw Data page lists all data tables in your document, '
|
dom('div', t('The Raw Data page lists all data tables in your document, \
|
||||||
+ 'including summary tables and tables not included in page layouts.')),
|
including summary tables and tables not included in page layouts.')),
|
||||||
dom('div', cssLink({href: commonUrls.helpRawData, target: '_blank'}, t('Learn more.'))),
|
dom('div', cssLink({href: commonUrls.helpRawData, target: '_blank'}, t('Learn more.'))),
|
||||||
...args,
|
...args,
|
||||||
),
|
),
|
||||||
@ -150,8 +149,8 @@ export const GristBehavioralPrompts: Record<BehavioralPrompt, BehavioralPromptCo
|
|||||||
accessRules: {
|
accessRules: {
|
||||||
title: () => t('Access Rules'),
|
title: () => t('Access Rules'),
|
||||||
content: (...args: DomElementArg[]) => cssTooltipContent(
|
content: (...args: DomElementArg[]) => cssTooltipContent(
|
||||||
dom('div', t('Access rules give you the power to create nuanced rules '
|
dom('div', t('Access rules give you the power to create nuanced rules \
|
||||||
+ 'to determine who can see or edit which parts of your document.')),
|
to determine who can see or edit which parts of your document.')),
|
||||||
dom('div', cssLink({href: commonUrls.helpAccessRules, target: '_blank'}, t('Learn more.'))),
|
dom('div', cssLink({href: commonUrls.helpAccessRules, target: '_blank'}, t('Learn more.'))),
|
||||||
...args,
|
...args,
|
||||||
),
|
),
|
||||||
@ -209,8 +208,7 @@ export const GristBehavioralPrompts: Record<BehavioralPrompt, BehavioralPromptCo
|
|||||||
addNew: {
|
addNew: {
|
||||||
title: () => t('Add New'),
|
title: () => t('Add New'),
|
||||||
content: (...args: DomElementArg[]) => cssTooltipContent(
|
content: (...args: DomElementArg[]) => cssTooltipContent(
|
||||||
dom('div', t('Click the Add New button to create new documents or workspaces, '
|
dom('div', t('Click the Add New button to create new documents or workspaces, or import data.')),
|
||||||
+ 'or import data.')),
|
|
||||||
...args,
|
...args,
|
||||||
),
|
),
|
||||||
deploymentTypes: ['saas'],
|
deploymentTypes: ['saas'],
|
||||||
@ -219,8 +217,7 @@ export const GristBehavioralPrompts: Record<BehavioralPrompt, BehavioralPromptCo
|
|||||||
title: () => t('Anchor Links'),
|
title: () => t('Anchor Links'),
|
||||||
content: (...args: DomElementArg[]) => cssTooltipContent(
|
content: (...args: DomElementArg[]) => cssTooltipContent(
|
||||||
dom('div',
|
dom('div',
|
||||||
t('To make an anchor link that takes the user to a specific cell, click on'
|
t('To make an anchor link that takes the user to a specific cell, click on a row and press {{shortcut}}.',
|
||||||
+ ' a row and press {{shortcut}}.',
|
|
||||||
{
|
{
|
||||||
shortcut: ShortcutKey(ShortcutKeyContent(commands.allCommands.copyLink.humanKeys[0])),
|
shortcut: ShortcutKey(ShortcutKeyContent(commands.allCommands.copyLink.humanKeys[0])),
|
||||||
}
|
}
|
||||||
@ -235,8 +232,7 @@ export const GristBehavioralPrompts: Record<BehavioralPrompt, BehavioralPromptCo
|
|||||||
content: (...args: DomElementArg[]) => cssTooltipContent(
|
content: (...args: DomElementArg[]) => cssTooltipContent(
|
||||||
dom('div',
|
dom('div',
|
||||||
t(
|
t(
|
||||||
'You can choose one of our pre-made widgets or embed your own ' +
|
'You can choose one of our pre-made widgets or embed your own by providing its full URL.'
|
||||||
'by providing its full URL.'
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
dom('div', cssLink({href: commonUrls.helpCustomWidgets, target: '_blank'}, t('Learn more.'))),
|
dom('div', cssLink({href: commonUrls.helpCustomWidgets, target: '_blank'}, t('Learn more.'))),
|
||||||
|
@ -41,8 +41,8 @@ export async function replaceTrunkWithFork(user: FullUser|null, doc: Document, a
|
|||||||
if (cmp.summary === 'left' || cmp.summary === 'both') {
|
if (cmp.summary === 'left' || cmp.summary === 'both') {
|
||||||
titleText = t("Original Has Modifications");
|
titleText = t("Original Has Modifications");
|
||||||
buttonText = t("Overwrite");
|
buttonText = t("Overwrite");
|
||||||
warningText = `${warningText} ${t("Be careful, the original has changes " +
|
warningText = `${warningText} ${t("Be careful, the original has changes \
|
||||||
"not in this document. Those changes will be overwritten.")}`;
|
not in this document. Those changes will be overwritten.")}`;
|
||||||
} else if (cmp.summary === 'unrelated') {
|
} else if (cmp.summary === 'unrelated') {
|
||||||
titleText = t("Original Looks Unrelated");
|
titleText = t("Original Looks Unrelated");
|
||||||
buttonText = t("Overwrite");
|
buttonText = t("Overwrite");
|
||||||
|
@ -102,7 +102,7 @@ function shareButton(buttonText: string|null, menuCreateFunc: MenuCreateFunc,
|
|||||||
return cssHoverCircle({ style: `margin: 5px;` },
|
return cssHoverCircle({ style: `margin: 5px;` },
|
||||||
cssTopBarBtn('Share', dom.cls('tour-share-icon')),
|
cssTopBarBtn('Share', dom.cls('tour-share-icon')),
|
||||||
menu(menuCreateFunc, {placement: 'bottom-end'}),
|
menu(menuCreateFunc, {placement: 'bottom-end'}),
|
||||||
hoverTooltip('Share', {key: 'topBarBtnTooltip'}),
|
hoverTooltip(t('Share'), {key: 'topBarBtnTooltip'}),
|
||||||
testId('tb-share'),
|
testId('tb-share'),
|
||||||
);
|
);
|
||||||
} else if (options.buttonAction) {
|
} else if (options.buttonAction) {
|
||||||
@ -115,7 +115,7 @@ function shareButton(buttonText: string|null, menuCreateFunc: MenuCreateFunc,
|
|||||||
cssShareCircle(
|
cssShareCircle(
|
||||||
cssShareIcon('Share'),
|
cssShareIcon('Share'),
|
||||||
menu(menuCreateFunc, {placement: 'bottom-end'}),
|
menu(menuCreateFunc, {placement: 'bottom-end'}),
|
||||||
hoverTooltip('Share', {key: 'topBarBtnTooltip'}),
|
hoverTooltip(t('Share'), {key: 'topBarBtnTooltip'}),
|
||||||
testId('tb-share'),
|
testId('tb-share'),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -128,7 +128,7 @@ function shareButton(buttonText: string|null, menuCreateFunc: MenuCreateFunc,
|
|||||||
cssShareIcon('Share')
|
cssShareIcon('Share')
|
||||||
),
|
),
|
||||||
menu(menuCreateFunc, {placement: 'bottom-end'}),
|
menu(menuCreateFunc, {placement: 'bottom-end'}),
|
||||||
hoverTooltip('Share', {key: 'topBarBtnTooltip'}),
|
hoverTooltip(t('Share'), {key: 'topBarBtnTooltip'}),
|
||||||
testId('tb-share'),
|
testId('tb-share'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
*
|
*
|
||||||
* It can be instantiated by calling showUserManagerModal with the UserAPI and IUserManagerOptions.
|
* It can be instantiated by calling showUserManagerModal with the UserAPI and IUserManagerOptions.
|
||||||
*/
|
*/
|
||||||
|
import { makeT } from 'app/client/lib/localization';
|
||||||
import {commonUrls} from 'app/common/gristUrls';
|
import {commonUrls} from 'app/common/gristUrls';
|
||||||
import {capitalizeFirstWord, isLongerThan} from 'app/common/gutil';
|
import {capitalizeFirstWord, isLongerThan} from 'app/common/gutil';
|
||||||
import {FullUser} from 'app/common/LoginSessionAPI';
|
import {FullUser} from 'app/common/LoginSessionAPI';
|
||||||
@ -42,6 +43,8 @@ import {menu, menuItem, menuText} from 'app/client/ui2018/menus';
|
|||||||
import {confirmModal, cssAnimatedModal, cssModalBody, cssModalButtons, cssModalTitle,
|
import {confirmModal, cssAnimatedModal, cssModalBody, cssModalButtons, cssModalTitle,
|
||||||
IModalControl, modal} from 'app/client/ui2018/modals';
|
IModalControl, modal} from 'app/client/ui2018/modals';
|
||||||
|
|
||||||
|
const t = makeT('UserManager');
|
||||||
|
|
||||||
export interface IUserManagerOptions {
|
export interface IUserManagerOptions {
|
||||||
permissionData: Promise<PermissionData>;
|
permissionData: Promise<PermissionData>;
|
||||||
activeUser: FullUser|null;
|
activeUser: FullUser|null;
|
||||||
@ -101,15 +104,15 @@ export function showUserManagerModal(userApi: UserAPI, options: IUserManagerOpti
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (model.isSelfRemoved.get()) {
|
if (model.isSelfRemoved.get()) {
|
||||||
const name = resourceName(model.resourceType);
|
const resourceType = resourceName(model.resourceType);
|
||||||
confirmModal(
|
confirmModal(
|
||||||
`You are about to remove your own access to this ${name}`,
|
t(`You are about to remove your own access to this {{resourceType}}`, { resourceType }),
|
||||||
'Remove my access', tryToSaveChanges,
|
t('Remove my access'), tryToSaveChanges,
|
||||||
{
|
{
|
||||||
explanation: (
|
explanation: (
|
||||||
'Once you have removed your own access, ' +
|
t(`Once you have removed your own access, \
|
||||||
'you will not be able to get it back without assistance ' +
|
you will not be able to get it back without assistance \
|
||||||
`from someone else with sufficient access to the ${name}.`
|
from someone else with sufficient access to the {{resourceType}}.`, { resourceType })
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -162,22 +165,22 @@ function buildUserManagerModal(
|
|||||||
cssModalButtons(
|
cssModalButtons(
|
||||||
{ style: 'margin: 32px 64px; display: flex;' },
|
{ style: 'margin: 32px 64px; display: flex;' },
|
||||||
(model.isPublicMember ? null :
|
(model.isPublicMember ? null :
|
||||||
bigPrimaryButton('Confirm',
|
bigPrimaryButton(t('Confirm'),
|
||||||
dom.boolAttr('disabled', (use) => !use(model.isAnythingChanged)),
|
dom.boolAttr('disabled', (use) => !use(model.isAnythingChanged)),
|
||||||
dom.on('click', () => onConfirm(ctl)),
|
dom.on('click', () => onConfirm(ctl)),
|
||||||
testId('um-confirm')
|
testId('um-confirm')
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
bigBasicButton(
|
bigBasicButton(
|
||||||
model.isPublicMember ? 'Close' : 'Cancel',
|
model.isPublicMember ? t('Close') : t('Cancel'),
|
||||||
dom.on('click', () => ctl.close()),
|
dom.on('click', () => ctl.close()),
|
||||||
testId('um-cancel')
|
testId('um-cancel')
|
||||||
),
|
),
|
||||||
(model.resourceType === 'document' && model.gristDoc && !model.isPersonal
|
(model.resourceType === 'document' && model.gristDoc && !model.isPersonal
|
||||||
? withInfoTooltip(
|
? withInfoTooltip(
|
||||||
cssLink({href: urlState().makeUrl({docPage: 'acl'})},
|
cssLink({href: urlState().makeUrl({docPage: 'acl'})},
|
||||||
dom.text(use => use(model.isAnythingChanged) ? 'Save & ' : ''),
|
dom.text(use => use(model.isAnythingChanged) ? t('Save & ') : ''),
|
||||||
'Open Access Rules',
|
t('Open Access Rules'),
|
||||||
dom.on('click', (ev) => {
|
dom.on('click', (ev) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
return onConfirm(ctl).then(() => urlState().pushUrl({docPage: 'acl'}));
|
return onConfirm(ctl).then(() => urlState().pushUrl({docPage: 'acl'}));
|
||||||
@ -268,7 +271,7 @@ export class UserManager extends Disposable {
|
|||||||
return dom('div',
|
return dom('div',
|
||||||
cssOptionRowMultiple(
|
cssOptionRowMultiple(
|
||||||
icon('AddUser'),
|
icon('AddUser'),
|
||||||
cssLabel('Invite multiple'),
|
cssLabel(t('Invite multiple')),
|
||||||
dom.on('click', (_ev) => buildMultiUserManagerModal(
|
dom.on('click', (_ev) => buildMultiUserManagerModal(
|
||||||
this,
|
this,
|
||||||
this._model,
|
this._model,
|
||||||
@ -286,30 +289,31 @@ export class UserManager extends Disposable {
|
|||||||
),
|
),
|
||||||
publicMember ? dom('span', { style: `float: right;` },
|
publicMember ? dom('span', { style: `float: right;` },
|
||||||
cssSmallPublicMemberIcon('PublicFilled'),
|
cssSmallPublicMemberIcon('PublicFilled'),
|
||||||
dom('span', 'Public access: '),
|
dom('span', t('Public access: ')),
|
||||||
cssOptionBtn(
|
cssOptionBtn(
|
||||||
menu(() => {
|
menu(() => {
|
||||||
tooltipControl?.close();
|
tooltipControl?.close();
|
||||||
return [
|
return [
|
||||||
menuItem(() => publicMember.access.set(roles.VIEWER), 'On', testId(`um-public-option`)),
|
menuItem(() => publicMember.access.set(roles.VIEWER), t('On'), testId(`um-public-option`)),
|
||||||
menuItem(() => publicMember.access.set(null), 'Off',
|
menuItem(() => publicMember.access.set(null), t('Off'),
|
||||||
// Disable null access if anonymous access is inherited.
|
// Disable null access if anonymous access is inherited.
|
||||||
dom.cls('disabled', (use) => use(publicMember.inheritedAccess) !== null),
|
dom.cls('disabled', (use) => use(publicMember.inheritedAccess) !== null),
|
||||||
testId(`um-public-option`)
|
testId(`um-public-option`)
|
||||||
),
|
),
|
||||||
// If the 'Off' setting is disabled, show an explanation.
|
// If the 'Off' setting is disabled, show an explanation.
|
||||||
dom.maybe((use) => use(publicMember.inheritedAccess) !== null, () => menuText(
|
dom.maybe((use) => use(publicMember.inheritedAccess) !== null, () => menuText(
|
||||||
`Public access inherited from ${getResourceParent(this._model.resourceType)}. ` +
|
t(`Public access inherited from {{parent}}. To remove, set 'Inherit access' option to 'None'.`,
|
||||||
`To remove, set 'Inherit access' option to 'None'.`))
|
{ parent: getResourceParent(this._model.resourceType) }
|
||||||
|
)))
|
||||||
];
|
];
|
||||||
}),
|
}),
|
||||||
dom.text((use) => use(publicMember.effectiveAccess) ? 'On' : 'Off'),
|
dom.text((use) => use(publicMember.effectiveAccess) ? t('On') : t('Off')),
|
||||||
cssCollapseIcon('Collapse'),
|
cssCollapseIcon('Collapse'),
|
||||||
testId('um-public-access')
|
testId('um-public-access')
|
||||||
),
|
),
|
||||||
hoverTooltip((ctl) => {
|
hoverTooltip((ctl) => {
|
||||||
tooltipControl = ctl;
|
tooltipControl = ctl;
|
||||||
return 'Allow anyone with the link to open.';
|
return t('Allow anyone with the link to open.');
|
||||||
}),
|
}),
|
||||||
) : null,
|
) : null,
|
||||||
),
|
),
|
||||||
@ -373,19 +377,23 @@ export class UserManager extends Disposable {
|
|||||||
const annotation = annotations.users.get(member.email);
|
const annotation = annotations.users.get(member.email);
|
||||||
if (!annotation) { return null; }
|
if (!annotation) { return null; }
|
||||||
if (annotation.isSupport) {
|
if (annotation.isSupport) {
|
||||||
return cssMemberType('Grist support');
|
return cssMemberType(t('Grist support'));
|
||||||
}
|
}
|
||||||
if (annotation.isMember && annotations.hasTeam) {
|
if (annotation.isMember && annotations.hasTeam) {
|
||||||
return cssMemberType('Team member');
|
return cssMemberType(t('Team member'));
|
||||||
}
|
}
|
||||||
const collaborator = annotations.hasTeam ? 'guest' : 'free collaborator';
|
const collaborator = annotations.hasTeam ? t('guest') : t('free collaborator');
|
||||||
const limit = annotation.collaboratorLimit;
|
const limit = annotation.collaboratorLimit;
|
||||||
if (!limit || !limit.top) { return null; }
|
if (!limit || !limit.top) { return null; }
|
||||||
const elements: HTMLSpanElement[] = [];
|
const elements: HTMLSpanElement[] = [];
|
||||||
if (limit.at <= limit.top) {
|
if (limit.at <= limit.top) {
|
||||||
elements.push(cssMemberType(`${limit.at} of ${limit.top} ${collaborator}s`));
|
elements.push(cssMemberType(
|
||||||
|
t(`{{limitAt}} of {{limitTop}} {{collaborator}}s`, { limitAt: limit.at, limitTop: limit.top, collaborator }))
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
elements.push(cssMemberTypeProblem(`${capitalizeFirstWord(collaborator)} limit exceeded`));
|
elements.push(cssMemberTypeProblem(
|
||||||
|
t(`{{collaborator}} limit exceeded`, { collaborator: capitalizeFirstWord(collaborator) }))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (annotations.hasTeam) {
|
if (annotations.hasTeam) {
|
||||||
// Add a link for adding a member. For a doc, streamline this so user can make
|
// Add a link for adding a member. For a doc, streamline this so user can make
|
||||||
@ -401,10 +409,10 @@ export class UserManager extends Disposable {
|
|||||||
{ email: member.email }).catch(reportError);
|
{ email: member.email }).catch(reportError);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
`Add ${member.name || 'member'} to your team`));
|
t(`Add {{member}} to your team`, { member: member.name || t('member') })));
|
||||||
} else if (limit.at >= limit.top) {
|
} else if (limit.at >= limit.top) {
|
||||||
elements.push(cssLink({href: commonUrls.plans, target: '_blank'},
|
elements.push(cssLink({href: commonUrls.plans, target: '_blank'},
|
||||||
'Create a team to share with more people'));
|
t('Create a team to share with more people')));
|
||||||
}
|
}
|
||||||
return elements;
|
return elements;
|
||||||
});
|
});
|
||||||
@ -418,13 +426,13 @@ export class UserManager extends Disposable {
|
|||||||
|
|
||||||
let memberType: string;
|
let memberType: string;
|
||||||
if (annotation.isSupport) {
|
if (annotation.isSupport) {
|
||||||
memberType = 'Grist support';
|
memberType = t('Grist support');
|
||||||
} else if (annotation.isMember && annotations.hasTeam) {
|
} else if (annotation.isMember && annotations.hasTeam) {
|
||||||
memberType = 'Team member';
|
memberType = t('Team member');
|
||||||
} else if (annotations.hasTeam) {
|
} else if (annotations.hasTeam) {
|
||||||
memberType = 'Outside collaborator';
|
memberType = t('Outside collaborator');
|
||||||
} else {
|
} else {
|
||||||
memberType = 'Collaborator';
|
memberType = t('Collaborator');
|
||||||
}
|
}
|
||||||
|
|
||||||
return cssMemberType(memberType, testId('um-member-annotation'));
|
return cssMemberType(memberType, testId('um-member-annotation'));
|
||||||
@ -439,8 +447,8 @@ export class UserManager extends Disposable {
|
|||||||
cssMemberListItem(
|
cssMemberListItem(
|
||||||
cssPublicMemberIcon('PublicFilled'),
|
cssPublicMemberIcon('PublicFilled'),
|
||||||
cssMemberText(
|
cssMemberText(
|
||||||
cssMemberPrimary('Public Access'),
|
cssMemberPrimary(t('Public Access')),
|
||||||
cssMemberSecondary('Anyone with link ', makeCopyBtn(this._options.linkToCopy)),
|
cssMemberSecondary(t('Anyone with link '), makeCopyBtn(this._options.linkToCopy)),
|
||||||
),
|
),
|
||||||
this._memberRoleSelector(publicMember.effectiveAccess, publicMember.inheritedAccess, false,
|
this._memberRoleSelector(publicMember.effectiveAccess, publicMember.inheritedAccess, false,
|
||||||
this._model.publicUserSelectOptions
|
this._model.publicUserSelectOptions
|
||||||
@ -472,12 +480,12 @@ export class UserManager extends Disposable {
|
|||||||
cssMemberPrimary(name, testId('um-member-name')),
|
cssMemberPrimary(name, testId('um-member-name')),
|
||||||
activeUser?.email ? cssMemberSecondary(activeUser.email) : null,
|
activeUser?.email ? cssMemberSecondary(activeUser.email) : null,
|
||||||
cssMemberPublicAccess(
|
cssMemberPublicAccess(
|
||||||
dom('span', 'Public access', testId('um-member-annotation')),
|
dom('span', t('Public access'), testId('um-member-annotation')),
|
||||||
cssPublicAccessIcon('PublicFilled'),
|
cssPublicAccessIcon('PublicFilled'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
cssRoleBtn(
|
cssRoleBtn(
|
||||||
accessLabel ?? 'Guest',
|
accessLabel ?? t('Guest'),
|
||||||
cssCollapseIcon('Collapse'),
|
cssCollapseIcon('Collapse'),
|
||||||
dom.cls('disabled'),
|
dom.cls('disabled'),
|
||||||
testId('um-member-role'),
|
testId('um-member-role'),
|
||||||
@ -522,23 +530,24 @@ export class UserManager extends Disposable {
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
// If the user's access is inherited, give an explanation on how to change it.
|
// If the user's access is inherited, give an explanation on how to change it.
|
||||||
isActiveUser ? menuText(`User may not modify their own access.`) : null,
|
isActiveUser ? menuText(t(`User may not modify their own access.`)) : null,
|
||||||
// If the user's access is inherited, give an explanation on how to change it.
|
// If the user's access is inherited, give an explanation on how to change it.
|
||||||
dom.maybe((use) => use(inherited) && !isActiveUser, () => menuText(
|
dom.maybe((use) => use(inherited) && !isActiveUser, () => menuText(
|
||||||
`User inherits permissions from ${getResourceParent(this._model.resourceType)}. To remove, ` +
|
t(`User inherits permissions from {{parent}}. To remove, \
|
||||||
`set 'Inherit access' option to 'None'.`)),
|
set 'Inherit access' option to 'None'.`, { parent: getResourceParent(this._model.resourceType) }))),
|
||||||
// If the user is a guest, give a description of the guest permission.
|
// If the user is a guest, give a description of the guest permission.
|
||||||
dom.maybe((use) => !this._model.isOrg && use(role) === roles.GUEST, () => menuText(
|
dom.maybe((use) => !this._model.isOrg && use(role) === roles.GUEST, () => menuText(
|
||||||
`User has view access to ${this._model.resourceType} resulting from manually-set access ` +
|
t(`User has view access to {{resource}} resulting from manually-set access \
|
||||||
`to resources inside. If removed here, this user will lose access to resources inside.`)),
|
to resources inside. If removed here, this user will lose access to resources inside.`,
|
||||||
this._model.isOrg ? menuText(`No default access allows access to be ` +
|
{ resource: this._model.resourceType }))),
|
||||||
`granted to individual documents or workspaces, rather than the full team site.`) : null
|
this._model.isOrg ? menuText(t(`No default access allows access to be \
|
||||||
|
granted to individual documents or workspaces, rather than the full team site.`)) : null
|
||||||
]),
|
]),
|
||||||
dom.text((use) => {
|
dom.text((use) => {
|
||||||
// Get the label of the active role. Note that the 'Guest' role is assigned when the role
|
// Get the label of the active role. Note that the 'Guest' role is assigned when the role
|
||||||
// is not found because it is not included as a selection.
|
// is not found because it is not included as a selection.
|
||||||
const activeRole = allRoles.find((_role: IOrgMemberSelectOption) => use(role) === _role.value);
|
const activeRole = allRoles.find((_role: IOrgMemberSelectOption) => use(role) === _role.value);
|
||||||
return activeRole ? activeRole.label : "Guest";
|
return activeRole ? activeRole.label : t("Guest");
|
||||||
}),
|
}),
|
||||||
cssCollapseIcon('Collapse'),
|
cssCollapseIcon('Collapse'),
|
||||||
this._model.isPersonal ? dom.cls('disabled') : null,
|
this._model.isPersonal ? dom.cls('disabled') : null,
|
||||||
@ -634,7 +643,7 @@ function getFullUser(member: IEditableMember): FullUser {
|
|||||||
|
|
||||||
// Create a "Copy Link" button.
|
// Create a "Copy Link" button.
|
||||||
function makeCopyBtn(linkToCopy: string|undefined, ...domArgs: DomElementArg[]) {
|
function makeCopyBtn(linkToCopy: string|undefined, ...domArgs: DomElementArg[]) {
|
||||||
return linkToCopy && cssCopyBtn(cssCopyIcon('Copy'), 'Copy Link',
|
return linkToCopy && cssCopyBtn(cssCopyIcon('Copy'), t('Copy Link'),
|
||||||
dom.on('click', (ev, elem) => copyLink(elem, linkToCopy)),
|
dom.on('click', (ev, elem) => copyLink(elem, linkToCopy)),
|
||||||
testId('um-copy-link'),
|
testId('um-copy-link'),
|
||||||
...domArgs,
|
...domArgs,
|
||||||
@ -646,7 +655,7 @@ function makeCopyBtn(linkToCopy: string|undefined, ...domArgs: DomElementArg[])
|
|||||||
async function copyLink(elem: HTMLElement, link: string) {
|
async function copyLink(elem: HTMLElement, link: string) {
|
||||||
await copyToClipboard(link);
|
await copyToClipboard(link);
|
||||||
setTestState({clipboard: link});
|
setTestState({clipboard: link});
|
||||||
showTransientTooltip(elem, 'Link copied to clipboard', {key: 'copy-doc-link'});
|
showTransientTooltip(elem, t('Link copied to clipboard'), { key: 'copy-doc-link' });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function manageTeam(appModel: AppModel,
|
async function manageTeam(appModel: AppModel,
|
||||||
@ -808,9 +817,9 @@ const cssMemberPublicAccess = styled(cssMemberSecondary, `
|
|||||||
function renderTitle(resourceType: ResourceType, resource?: Resource, personal?: boolean) {
|
function renderTitle(resourceType: ResourceType, resource?: Resource, personal?: boolean) {
|
||||||
switch (resourceType) {
|
switch (resourceType) {
|
||||||
case 'organization': {
|
case 'organization': {
|
||||||
if (personal) { return 'Your role for this team site'; }
|
if (personal) { return t('Your role for this team site'); }
|
||||||
return [
|
return [
|
||||||
'Manage members of team site',
|
t('Manage members of team site'),
|
||||||
!resource ? null : cssOrgName(
|
!resource ? null : cssOrgName(
|
||||||
`${(resource as Organization).name} (`,
|
`${(resource as Organization).name} (`,
|
||||||
cssOrgDomain(`${(resource as Organization).domain}.getgrist.com`),
|
cssOrgDomain(`${(resource as Organization).domain}.getgrist.com`),
|
||||||
@ -819,12 +828,14 @@ function renderTitle(resourceType: ResourceType, resource?: Resource, personal?:
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
return personal ? `Your role for this ${resourceType}` : `Invite people to ${resourceType}`;
|
return personal ?
|
||||||
|
t(`Your role for this {{resourceType}}`, { resourceType }) :
|
||||||
|
t(`Invite people to {{resourceType}}`, { resourceType });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rename organization to team site.
|
// Rename organization to team site.
|
||||||
function resourceName(resourceType: ResourceType): string {
|
function resourceName(resourceType: ResourceType): string {
|
||||||
return resourceType === 'organization' ? 'team site' : resourceType;
|
return resourceType === 'organization' ? t('team site') : resourceType;
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ import { dom, styled } from "grainjs";
|
|||||||
|
|
||||||
const t = makeT('WelcomeTour');
|
const t = makeT('WelcomeTour');
|
||||||
|
|
||||||
export const welcomeTour: IOnBoardingMsg[] = [
|
export const WelcomeTour: IOnBoardingMsg[] = [
|
||||||
{
|
{
|
||||||
title: t('Editing Data'),
|
title: t('Editing Data'),
|
||||||
body: () => [
|
body: () => [
|
||||||
@ -97,7 +97,7 @@ export const welcomeTour: IOnBoardingMsg[] = [
|
|||||||
|
|
||||||
export function startWelcomeTour(onFinishCB: () => void) {
|
export function startWelcomeTour(onFinishCB: () => void) {
|
||||||
commands.allCommands.fieldTabOpen.run();
|
commands.allCommands.fieldTabOpen.run();
|
||||||
startOnBoarding(welcomeTour, onFinishCB);
|
startOnBoarding(WelcomeTour, onFinishCB);
|
||||||
}
|
}
|
||||||
|
|
||||||
const TopBarButtonIcon = styled(icon, `
|
const TopBarButtonIcon = styled(icon, `
|
||||||
|
@ -35,8 +35,8 @@ export function createForbiddenPage(appModel: AppModel, message?: string) {
|
|||||||
return pagePanelsError(appModel, t("Access denied{{suffix}}", {suffix: ''}), [
|
return pagePanelsError(appModel, t("Access denied{{suffix}}", {suffix: ''}), [
|
||||||
dom.domComputed(appModel.currentValidUser, user => user ? [
|
dom.domComputed(appModel.currentValidUser, user => user ? [
|
||||||
cssErrorText(message || t("You do not have access to this organization's documents.")),
|
cssErrorText(message || t("You do not have access to this organization's documents.")),
|
||||||
cssErrorText(t("You are signed in as {{email}}. You can sign in with a different " +
|
cssErrorText(t("You are signed in as {{email}}. You can sign in with a different \
|
||||||
"account, or ask an administrator for access.", {email: dom('b', user.email)})),
|
account, or ask an administrator for access.", {email: dom('b', user.email)})),
|
||||||
] : [
|
] : [
|
||||||
// This page is not normally shown because a logged out user with no access will get
|
// This page is not normally shown because a logged out user with no access will get
|
||||||
// redirected to log in. But it may be seen if a user logs out and returns to a cached
|
// redirected to log in. But it may be seen if a user logs out and returns to a cached
|
||||||
|
@ -10,6 +10,9 @@ import { icon } from "app/client/ui2018/icons";
|
|||||||
import { cssMenuItem, defaultMenuOptions, IOpenController, IPopupOptions, setPopupToFunc } from "popweasel";
|
import { cssMenuItem, defaultMenuOptions, IOpenController, IPopupOptions, setPopupToFunc } from "popweasel";
|
||||||
import { mergeWith } from "lodash";
|
import { mergeWith } from "lodash";
|
||||||
import { getOptionFull, SimpleList } from "../lib/simpleList";
|
import { getOptionFull, SimpleList } from "../lib/simpleList";
|
||||||
|
import { makeT } from 'app/client/lib/localization';
|
||||||
|
|
||||||
|
const t = makeT('searchDropdown');
|
||||||
|
|
||||||
const testId = makeTestId('test-sd-');
|
const testId = makeTestId('test-sd-');
|
||||||
|
|
||||||
@ -92,7 +95,7 @@ class DropdownWithSearch<T> extends Disposable {
|
|||||||
cssMenuHeader(
|
cssMenuHeader(
|
||||||
cssSearchIcon('Search'),
|
cssSearchIcon('Search'),
|
||||||
this._inputElem = cssSearch(
|
this._inputElem = cssSearch(
|
||||||
{placeholder: this._options.placeholder || 'Search'},
|
{placeholder: this._options.placeholder || t('Search')},
|
||||||
dom.on('input', () => { this._update(); }),
|
dom.on('input', () => { this._update(); }),
|
||||||
dom.on('blur', () => setTimeout(() => this._inputElem.focus(), 0)),
|
dom.on('blur', () => setTimeout(() => this._inputElem.focus(), 0)),
|
||||||
),
|
),
|
||||||
|
@ -177,7 +177,7 @@ export function searchBar(model: SearchModel, testId: TestId = noTestId) {
|
|||||||
cssTopBarBtn('Search',
|
cssTopBarBtn('Search',
|
||||||
testId('icon'),
|
testId('icon'),
|
||||||
dom.on('click', focusAndSelect),
|
dom.on('click', focusAndSelect),
|
||||||
hoverTooltip('Search', {key: 'topBarBtnTooltip'}),
|
hoverTooltip(t('Search'), {key: 'topBarBtnTooltip'}),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
expandedSearch(
|
expandedSearch(
|
||||||
|
@ -7,23 +7,23 @@ export class Limit extends BaseEntity {
|
|||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
public id: number;
|
public id: number;
|
||||||
|
|
||||||
@Column()
|
@Column({type: Number})
|
||||||
public limit: number;
|
public limit: number;
|
||||||
|
|
||||||
@Column()
|
@Column({type: Number})
|
||||||
public usage: number;
|
public usage: number;
|
||||||
|
|
||||||
@Column()
|
@Column({type: String})
|
||||||
public type: string;
|
public type: string;
|
||||||
|
|
||||||
@Column({name: 'billing_account_id'})
|
@Column({name: 'billing_account_id', type: Number})
|
||||||
public billingAccountId: number;
|
public billingAccountId: number;
|
||||||
|
|
||||||
@ManyToOne(type => BillingAccount)
|
@ManyToOne(type => BillingAccount)
|
||||||
@JoinColumn({name: 'billing_account_id'})
|
@JoinColumn({name: 'billing_account_id'})
|
||||||
public billingAccount: BillingAccount;
|
public billingAccount: BillingAccount;
|
||||||
|
|
||||||
@Column({name: 'created_at', default: () => "CURRENT_TIMESTAMP"})
|
@Column({name: 'created_at', type: nativeValues.dateTimeType, default: () => "CURRENT_TIMESTAMP"})
|
||||||
public createdAt: Date;
|
public createdAt: Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -80,7 +80,7 @@ import {guessColInfo} from 'app/common/ValueGuesser';
|
|||||||
import {parseUserAction} from 'app/common/ValueParser';
|
import {parseUserAction} from 'app/common/ValueParser';
|
||||||
import {TEMPLATES_ORG_DOMAIN} from 'app/gen-server/ApiServer';
|
import {TEMPLATES_ORG_DOMAIN} from 'app/gen-server/ApiServer';
|
||||||
import {Document} from 'app/gen-server/entity/Document';
|
import {Document} from 'app/gen-server/entity/Document';
|
||||||
import {ParseOptions} from 'app/plugin/FileParserAPI';
|
import {ParseFileResult, ParseOptions} from 'app/plugin/FileParserAPI';
|
||||||
import {AccessTokenOptions, AccessTokenResult, GristDocAPI} from 'app/plugin/GristAPI';
|
import {AccessTokenOptions, AccessTokenResult, GristDocAPI} from 'app/plugin/GristAPI';
|
||||||
import {compileAclFormula} from 'app/server/lib/ACLFormula';
|
import {compileAclFormula} from 'app/server/lib/ACLFormula';
|
||||||
import {AssistanceSchemaPromptV1Context} from 'app/server/lib/Assistance';
|
import {AssistanceSchemaPromptV1Context} from 'app/server/lib/Assistance';
|
||||||
@ -113,7 +113,7 @@ import tmp from 'tmp';
|
|||||||
|
|
||||||
import {ActionHistory} from './ActionHistory';
|
import {ActionHistory} from './ActionHistory';
|
||||||
import {ActionHistoryImpl} from './ActionHistoryImpl';
|
import {ActionHistoryImpl} from './ActionHistoryImpl';
|
||||||
import {ActiveDocImport} from './ActiveDocImport';
|
import {ActiveDocImport, FileImportOptions} from './ActiveDocImport';
|
||||||
import {DocClients} from './DocClients';
|
import {DocClients} from './DocClients';
|
||||||
import {DocPluginManager} from './DocPluginManager';
|
import {DocPluginManager} from './DocPluginManager';
|
||||||
import {
|
import {
|
||||||
@ -773,6 +773,17 @@ export class ActiveDoc extends EventEmitter {
|
|||||||
await this._activeDocImport.oneStepImport(docSession, uploadInfo);
|
await this._activeDocImport.oneStepImport(docSession, uploadInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Import data resulting from parsing a file into a new table.
|
||||||
|
* In normal circumstances this is only used internally.
|
||||||
|
* It's exposed publicly for use by grist-static which doesn't use the plugin system.
|
||||||
|
*/
|
||||||
|
public async importParsedFileAsNewTable(
|
||||||
|
docSession: OptDocSession, optionsAndData: ParseFileResult, importOptions: FileImportOptions
|
||||||
|
): Promise<ImportResult> {
|
||||||
|
return this._activeDocImport.importParsedFileAsNewTable(docSession, optionsAndData, importOptions);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function saves attachments from a given upload and creates an entry for them in the database.
|
* This function saves attachments from a given upload and creates an entry for them in the database.
|
||||||
* It returns the list of rowIds for the rows created in the _grist_Attachments table.
|
* It returns the list of rowIds for the rows created in the _grist_Attachments table.
|
||||||
|
@ -44,7 +44,7 @@ interface ReferenceDescription {
|
|||||||
refTableId: string;
|
refTableId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FileImportOptions {
|
export interface FileImportOptions {
|
||||||
// Suggested name of the import file. It is sometimes used as a suggested table name, e.g. for csv imports.
|
// Suggested name of the import file. It is sometimes used as a suggested table name, e.g. for csv imports.
|
||||||
originalFilename: string;
|
originalFilename: string;
|
||||||
// Containing parseOptions as serialized JSON to pass to the import plugin.
|
// Containing parseOptions as serialized JSON to pass to the import plugin.
|
||||||
@ -227,71 +227,14 @@ export class ActiveDocImport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Imports all files as new tables, using the given transform rules and import options.
|
* Import data resulting from parsing a file into a new table.
|
||||||
* The isHidden flag indicates whether to create temporary hidden tables, or final ones.
|
* In normal circumstances this is only used internally.
|
||||||
|
* It's exposed publicly for use by grist-static which doesn't use the plugin system.
|
||||||
*/
|
*/
|
||||||
private async _importFiles(docSession: OptDocSession, upload: UploadInfo, transforms: TransformRuleMap[],
|
public async importParsedFileAsNewTable(
|
||||||
{parseOptions = {}, mergeOptionMaps = []}: ImportOptions,
|
docSession: OptDocSession, optionsAndData: ParseFileResult, importOptions: FileImportOptions
|
||||||
isHidden: boolean): Promise<ImportResult> {
|
): Promise<ImportResult> {
|
||||||
|
const {originalFilename, mergeOptionsMap, isHidden, uploadFileIndex, transformRuleMap} = importOptions;
|
||||||
// Check that upload size is within the configured limits.
|
|
||||||
const limit = (Number(process.env.GRIST_MAX_UPLOAD_IMPORT_MB) * 1024 * 1024) || Infinity;
|
|
||||||
const totalSize = upload.files.reduce((acc, f) => acc + f.size, 0);
|
|
||||||
if (totalSize > limit) {
|
|
||||||
throw new ApiError(`Imported files must not exceed ${gutil.byteString(limit)}`, 413);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The upload must be within the plugin-accessible directory. Once moved, subsequent calls to
|
|
||||||
// moveUpload() will return without having to do anything.
|
|
||||||
if (!this._activeDoc.docPluginManager) { throw new Error('no plugin manager available'); }
|
|
||||||
await moveUpload(upload, this._activeDoc.docPluginManager.tmpDir());
|
|
||||||
|
|
||||||
const importResult: ImportResult = {options: parseOptions, tables: []};
|
|
||||||
for (const [index, file] of upload.files.entries()) {
|
|
||||||
// If we have a better guess for the file's extension, replace it in origName, to ensure
|
|
||||||
// that DocPluginManager has access to it to guess the best parser type.
|
|
||||||
let origName: string = file.origName;
|
|
||||||
if (file.ext) {
|
|
||||||
origName = path.basename(origName, path.extname(origName)) + file.ext;
|
|
||||||
}
|
|
||||||
const res = await this._importFileAsNewTable(docSession, file.absPath, {
|
|
||||||
parseOptions,
|
|
||||||
mergeOptionsMap: mergeOptionMaps[index] || {},
|
|
||||||
isHidden,
|
|
||||||
originalFilename: origName,
|
|
||||||
uploadFileIndex: index,
|
|
||||||
transformRuleMap: transforms[index] || {}
|
|
||||||
});
|
|
||||||
if (index === 0) {
|
|
||||||
// Returned parse options from the first file should be used for all files in one upload.
|
|
||||||
importResult.options = parseOptions = res.options;
|
|
||||||
}
|
|
||||||
importResult.tables.push(...res.tables);
|
|
||||||
}
|
|
||||||
return importResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Imports the data stored at tmpPath.
|
|
||||||
*
|
|
||||||
* Currently it starts a python parser as a child process
|
|
||||||
* outside the sandbox, and supports xlsx, csv, and perhaps some other formats. It may
|
|
||||||
* result in the import of multiple tables, in case of e.g. Excel formats.
|
|
||||||
* @param {OptDocSession} docSession: Session instance to use for importing.
|
|
||||||
* @param {String} tmpPath: The path from of the original file.
|
|
||||||
* @param {FileImportOptions} importOptions: File import options.
|
|
||||||
* @returns {Promise<ImportResult>} with `options` property containing parseOptions as serialized JSON as adjusted
|
|
||||||
* or guessed by the plugin, and `tables`, which is which is a list of objects with information about
|
|
||||||
* tables, such as `hiddenTableId`, `uploadFileIndex`, `origTableName`, `transformSectionRef`, `destTableId`.
|
|
||||||
*/
|
|
||||||
private async _importFileAsNewTable(docSession: OptDocSession, tmpPath: string,
|
|
||||||
importOptions: FileImportOptions): Promise<ImportResult> {
|
|
||||||
const {originalFilename, parseOptions, mergeOptionsMap, isHidden, uploadFileIndex,
|
|
||||||
transformRuleMap} = importOptions;
|
|
||||||
log.info("ActiveDoc._importFileAsNewTable(%s, %s)", tmpPath, originalFilename);
|
|
||||||
if (!this._activeDoc.docPluginManager) { throw new Error('no plugin manager available'); }
|
|
||||||
const optionsAndData: ParseFileResult =
|
|
||||||
await this._activeDoc.docPluginManager.parseFile(tmpPath, originalFilename, parseOptions);
|
|
||||||
const options = optionsAndData.parseOptions;
|
const options = optionsAndData.parseOptions;
|
||||||
|
|
||||||
const parsedTables = optionsAndData.tables;
|
const parsedTables = optionsAndData.tables;
|
||||||
@ -374,6 +317,76 @@ export class ActiveDocImport {
|
|||||||
return ({options, tables});
|
return ({options, tables});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports all files as new tables, using the given transform rules and import options.
|
||||||
|
* The isHidden flag indicates whether to create temporary hidden tables, or final ones.
|
||||||
|
*/
|
||||||
|
private async _importFiles(docSession: OptDocSession, upload: UploadInfo, transforms: TransformRuleMap[],
|
||||||
|
{parseOptions = {}, mergeOptionMaps = []}: ImportOptions,
|
||||||
|
isHidden: boolean): Promise<ImportResult> {
|
||||||
|
|
||||||
|
// Check that upload size is within the configured limits.
|
||||||
|
const limit = (Number(process.env.GRIST_MAX_UPLOAD_IMPORT_MB) * 1024 * 1024) || Infinity;
|
||||||
|
const totalSize = upload.files.reduce((acc, f) => acc + f.size, 0);
|
||||||
|
if (totalSize > limit) {
|
||||||
|
throw new ApiError(`Imported files must not exceed ${gutil.byteString(limit)}`, 413);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The upload must be within the plugin-accessible directory. Once moved, subsequent calls to
|
||||||
|
// moveUpload() will return without having to do anything.
|
||||||
|
if (!this._activeDoc.docPluginManager) { throw new Error('no plugin manager available'); }
|
||||||
|
await moveUpload(upload, this._activeDoc.docPluginManager.tmpDir());
|
||||||
|
|
||||||
|
const importResult: ImportResult = {options: parseOptions, tables: []};
|
||||||
|
for (const [index, file] of upload.files.entries()) {
|
||||||
|
// If we have a better guess for the file's extension, replace it in origName, to ensure
|
||||||
|
// that DocPluginManager has access to it to guess the best parser type.
|
||||||
|
let origName: string = file.origName;
|
||||||
|
if (file.ext) {
|
||||||
|
origName = path.basename(origName, path.extname(origName)) + file.ext;
|
||||||
|
}
|
||||||
|
const res = await this._importFileAsNewTable(docSession, file.absPath, {
|
||||||
|
parseOptions,
|
||||||
|
mergeOptionsMap: mergeOptionMaps[index] || {},
|
||||||
|
isHidden,
|
||||||
|
originalFilename: origName,
|
||||||
|
uploadFileIndex: index,
|
||||||
|
transformRuleMap: transforms[index] || {}
|
||||||
|
});
|
||||||
|
if (index === 0) {
|
||||||
|
// Returned parse options from the first file should be used for all files in one upload.
|
||||||
|
importResult.options = parseOptions = res.options;
|
||||||
|
}
|
||||||
|
importResult.tables.push(...res.tables);
|
||||||
|
}
|
||||||
|
return importResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports the data stored at tmpPath.
|
||||||
|
*
|
||||||
|
* Currently it starts a python parser as a child process
|
||||||
|
* outside the sandbox, and supports xlsx, csv, and perhaps some other formats. It may
|
||||||
|
* result in the import of multiple tables, in case of e.g. Excel formats.
|
||||||
|
* @param {OptDocSession} docSession: Session instance to use for importing.
|
||||||
|
* @param {String} tmpPath: The path from of the original file.
|
||||||
|
* @param {FileImportOptions} importOptions: File import options.
|
||||||
|
* @returns {Promise<ImportResult>} with `options` property containing parseOptions as serialized JSON as adjusted
|
||||||
|
* or guessed by the plugin, and `tables`, which is which is a list of objects with information about
|
||||||
|
* tables, such as `hiddenTableId`, `uploadFileIndex`, `origTableName`, `transformSectionRef`, `destTableId`.
|
||||||
|
*/
|
||||||
|
private async _importFileAsNewTable(docSession: OptDocSession, tmpPath: string,
|
||||||
|
importOptions: FileImportOptions): Promise<ImportResult> {
|
||||||
|
const {originalFilename, parseOptions} = importOptions;
|
||||||
|
log.info("ActiveDoc._importFileAsNewTable(%s, %s)", tmpPath, originalFilename);
|
||||||
|
if (!this._activeDoc.docPluginManager) {
|
||||||
|
throw new Error('no plugin manager available');
|
||||||
|
}
|
||||||
|
const optionsAndData: ParseFileResult =
|
||||||
|
await this._activeDoc.docPluginManager.parseFile(tmpPath, originalFilename, parseOptions);
|
||||||
|
return this.importParsedFileAsNewTable(docSession, optionsAndData, importOptions);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Imports records from `hiddenTableId` into `destTableId`, transforming the column
|
* Imports records from `hiddenTableId` into `destTableId`, transforming the column
|
||||||
* values from `hiddenTableId` according to the `transformRule`. Finalizes import when done.
|
* values from `hiddenTableId` according to the `transformRule`. Finalizes import when done.
|
||||||
|
@ -378,7 +378,7 @@ export class DocStorage implements ISQLiteDB, OnDemandStorage {
|
|||||||
const colListSql = newCols.map(c => `${quoteIdent(c.colId)}=?`).join(', ');
|
const colListSql = newCols.map(c => `${quoteIdent(c.colId)}=?`).join(', ');
|
||||||
const types = newCols.map(c => c.type);
|
const types = newCols.map(c => c.type);
|
||||||
const sqlParams = DocStorage._encodeColumnsToRows(types, newCols.map(c => [PENDING_VALUE]));
|
const sqlParams = DocStorage._encodeColumnsToRows(types, newCols.map(c => [PENDING_VALUE]));
|
||||||
await db.run(`UPDATE ${quoteIdent(tableId)} SET ${colListSql}`, sqlParams[0]);
|
await db.run(`UPDATE ${quoteIdent(tableId)} SET ${colListSql}`, ...sqlParams[0]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -1093,7 +1093,7 @@ export class DocStorage implements ISQLiteDB, OnDemandStorage {
|
|||||||
public _process_RemoveRecord(tableId: string, rowId: string): Promise<RunResult> {
|
public _process_RemoveRecord(tableId: string, rowId: string): Promise<RunResult> {
|
||||||
const sql = "DELETE FROM " + quoteIdent(tableId) + " WHERE id=?";
|
const sql = "DELETE FROM " + quoteIdent(tableId) + " WHERE id=?";
|
||||||
debuglog("RemoveRecord SQL: " + sql, [rowId]);
|
debuglog("RemoveRecord SQL: " + sql, [rowId]);
|
||||||
return this.run(sql, [rowId]);
|
return this.run(sql, rowId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -1130,7 +1130,7 @@ export class DocStorage implements ISQLiteDB, OnDemandStorage {
|
|||||||
const stmt = await this.prepare(preSql + chunkParams + postSql);
|
const stmt = await this.prepare(preSql + chunkParams + postSql);
|
||||||
for (const index of _.range(0, numChunks * chunkSize, chunkSize)) {
|
for (const index of _.range(0, numChunks * chunkSize, chunkSize)) {
|
||||||
debuglog("DocStorage.BulkRemoveRecord: chunk delete " + index + "-" + (index + chunkSize - 1));
|
debuglog("DocStorage.BulkRemoveRecord: chunk delete " + index + "-" + (index + chunkSize - 1));
|
||||||
await stmt.run(rowIds.slice(index, index + chunkSize));
|
await stmt.run(...rowIds.slice(index, index + chunkSize));
|
||||||
}
|
}
|
||||||
await stmt.finalize();
|
await stmt.finalize();
|
||||||
}
|
}
|
||||||
@ -1139,7 +1139,7 @@ export class DocStorage implements ISQLiteDB, OnDemandStorage {
|
|||||||
debuglog("DocStorage.BulkRemoveRecord: leftover delete " + (numChunks * chunkSize) + "-" + (rowIds.length - 1));
|
debuglog("DocStorage.BulkRemoveRecord: leftover delete " + (numChunks * chunkSize) + "-" + (rowIds.length - 1));
|
||||||
const leftoverParams = _.range(numLeftovers).map(q).join(',');
|
const leftoverParams = _.range(numLeftovers).map(q).join(',');
|
||||||
await this.run(preSql + leftoverParams + postSql,
|
await this.run(preSql + leftoverParams + postSql,
|
||||||
rowIds.slice(numChunks * chunkSize, rowIds.length));
|
...rowIds.slice(numChunks * chunkSize, rowIds.length));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1132,9 +1132,6 @@ export class FlexServer implements GristServer {
|
|||||||
await this.loadConfig();
|
await this.loadConfig();
|
||||||
this.addComm();
|
this.addComm();
|
||||||
|
|
||||||
// Temporary duplication of external storage configuration.
|
|
||||||
// This may break https://github.com/gristlabs/grist-core/pull/546,
|
|
||||||
// but will revive other uses of external storage. TODO: reconcile.
|
|
||||||
await this.create.configure?.();
|
await this.create.configure?.();
|
||||||
|
|
||||||
if (!isSingleUserMode()) {
|
if (!isSingleUserMode()) {
|
||||||
@ -1147,7 +1144,7 @@ export class FlexServer implements GristServer {
|
|||||||
this._disableExternalStorage = true;
|
this._disableExternalStorage = true;
|
||||||
externalStorage.flag('active').set(false);
|
externalStorage.flag('active').set(false);
|
||||||
}
|
}
|
||||||
await this.create.configure?.();
|
await this.create.checkBackend?.();
|
||||||
const workers = this._docWorkerMap;
|
const workers = this._docWorkerMap;
|
||||||
const docWorkerId = await this._addSelfAsWorker(workers);
|
const docWorkerId = await this._addSelfAsWorker(workers);
|
||||||
|
|
||||||
|
@ -32,6 +32,8 @@ export interface ICreate {
|
|||||||
sessionSecret(): string;
|
sessionSecret(): string;
|
||||||
// Check configuration of the app early enough to show on startup.
|
// Check configuration of the app early enough to show on startup.
|
||||||
configure?(): Promise<void>;
|
configure?(): Promise<void>;
|
||||||
|
// Optionally perform sanity checks on the configured storage, throwing a fatal error if it is not functional
|
||||||
|
checkBackend?(): Promise<void>;
|
||||||
// Return a string containing 1 or more HTML tags to insert into the head element of every
|
// Return a string containing 1 or more HTML tags to insert into the head element of every
|
||||||
// static page.
|
// static page.
|
||||||
getExtraHeadHtml?(): string;
|
getExtraHeadHtml?(): string;
|
||||||
@ -119,6 +121,13 @@ export function makeSimpleCreator(opts: {
|
|||||||
return secret;
|
return secret;
|
||||||
},
|
},
|
||||||
async configure() {
|
async configure() {
|
||||||
|
for (const s of storage || []) {
|
||||||
|
if (s.check()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async checkBackend() {
|
||||||
for (const s of storage || []) {
|
for (const s of storage || []) {
|
||||||
if (s.check()) {
|
if (s.check()) {
|
||||||
await s.checkBackend?.();
|
await s.checkBackend?.();
|
||||||
|
@ -71,7 +71,11 @@
|
|||||||
"Sign Out": "Abmelden",
|
"Sign Out": "Abmelden",
|
||||||
"Sign in": "Anmelden",
|
"Sign in": "Anmelden",
|
||||||
"Switch Accounts": "Konten wechseln",
|
"Switch Accounts": "Konten wechseln",
|
||||||
"Toggle Mobile Mode": "Mobilmodus umschalten"
|
"Toggle Mobile Mode": "Mobilmodus umschalten",
|
||||||
|
"Activation": "Aktivierung",
|
||||||
|
"Billing Account": "Abrechnungskonto",
|
||||||
|
"Support Grist": "Grist Support",
|
||||||
|
"Upgrade Plan": "Upgrade-Plan"
|
||||||
},
|
},
|
||||||
"ActionLog": {
|
"ActionLog": {
|
||||||
"Action Log failed to load": "Aktionsprotokoll konnte nicht geladen werden",
|
"Action Log failed to load": "Aktionsprotokoll konnte nicht geladen werden",
|
||||||
@ -511,7 +515,8 @@
|
|||||||
"Notifications": "Benachrichtigungen",
|
"Notifications": "Benachrichtigungen",
|
||||||
"Renew": "Erneuern",
|
"Renew": "Erneuern",
|
||||||
"Report a problem": "Ein Problem melden",
|
"Report a problem": "Ein Problem melden",
|
||||||
"Upgrade Plan": "Upgrade-Plan"
|
"Upgrade Plan": "Upgrade-Plan",
|
||||||
|
"Manage billing": "Abrechnung verwalten"
|
||||||
},
|
},
|
||||||
"OnBoardingPopups": {
|
"OnBoardingPopups": {
|
||||||
"Finish": "Beenden",
|
"Finish": "Beenden",
|
||||||
@ -1099,5 +1104,37 @@
|
|||||||
"Welcome back": "Willkommen zurück",
|
"Welcome back": "Willkommen zurück",
|
||||||
"You can always switch sites using the account menu.": "Sie können jederzeit über das Kontomenü zwischen den Websites wechseln.",
|
"You can always switch sites using the account menu.": "Sie können jederzeit über das Kontomenü zwischen den Websites wechseln.",
|
||||||
"You have access to the following Grist sites.": "Sie haben Zugriff auf die folgenden Grist-Seiten."
|
"You have access to the following Grist sites.": "Sie haben Zugriff auf die folgenden Grist-Seiten."
|
||||||
|
},
|
||||||
|
"SupportGristNudge": {
|
||||||
|
"Support Grist": "Grist Support",
|
||||||
|
"Close": "Schließen",
|
||||||
|
"Contribute": "Beitragen",
|
||||||
|
"Help Center": "Hilfe-Center",
|
||||||
|
"Opt in to Telemetry": "Melden Sie sich für Telemetrie an",
|
||||||
|
"Opted In": "Angemeldet",
|
||||||
|
"Support Grist page": "Support Grist-Seite"
|
||||||
|
},
|
||||||
|
"SupportGristPage": {
|
||||||
|
"GitHub Sponsors page": "GitHub-Sponsorenseite",
|
||||||
|
"Help Center": "Hilfe-Center",
|
||||||
|
"Manage Sponsorship": "Sponsoring verwalten",
|
||||||
|
"Opt in to Telemetry": "Melden Sie sich für Telemetrie an",
|
||||||
|
"This instance is opted in to telemetry. Only the site administrator has permission to change this.": "Diese Instanz ist für Telemetrie aktiviert. Nur der Site-Administrator hat die Berechtigung, dies zu ändern.",
|
||||||
|
"This instance is opted out of telemetry. Only the site administrator has permission to change this.": "Diese Instanz ist von der Telemetrie deaktiviert. Nur der Site-Administrator hat die Berechtigung, dies zu ändern.",
|
||||||
|
"We only collect usage statistics, as detailed in our {{link}}, never document contents.": "Wir erfassen nur Nutzungsstatistiken, wie in unserem {{link}} beschrieben, jedoch niemals Inhalte der Dokumenten.",
|
||||||
|
"You can opt out of telemetry at any time from this page.": "Sie können die Telemetrie jederzeit auf dieser Seite deaktivieren.",
|
||||||
|
"GitHub": "GitHub",
|
||||||
|
"Home": "Home",
|
||||||
|
"Opt out of Telemetry": "Deaktivieren Sie die Telemetrie",
|
||||||
|
"Sponsor Grist Labs on GitHub": "Sponsern Sie Grist Labs auf GitHub",
|
||||||
|
"Support Grist": "Grist Support",
|
||||||
|
"Telemetry": "Telemetrie",
|
||||||
|
"You have opted in to telemetry. Thank you!": "Sie haben sich für die Telemetrie entschieden. Vielen Dank!",
|
||||||
|
"You have opted out of telemetry.": "Sie haben sich von der Telemetrie abgemeldet."
|
||||||
|
},
|
||||||
|
"buildViewSectionDom": {
|
||||||
|
"No row selected in {{title}}": "Keine Zeile in {{title}} ausgewählt",
|
||||||
|
"Not all data is shown": "Es werden nicht alle Daten angezeigt",
|
||||||
|
"No data": "Keine Daten"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,9 @@
|
|||||||
"View As": "View As",
|
"View As": "View As",
|
||||||
"Seed rules": "Seed rules",
|
"Seed rules": "Seed rules",
|
||||||
"When adding table rules, automatically add a rule to grant OWNER full access.": "When adding table rules, automatically add a rule to grant OWNER full access.",
|
"When adding table rules, automatically add a rule to grant OWNER full access.": "When adding table rules, automatically add a rule to grant OWNER full access.",
|
||||||
"Permission to edit document structure": "Permission to edit document structure"
|
"Permission to edit document structure": "Permission to edit document structure",
|
||||||
|
"This default should be changed if editors' access is to be limited. ": "This default should be changed if editors' access is to be limited. ",
|
||||||
|
"Allow editors to edit structure (e.g. modify and delete tables, columns, layouts), and to write formulas, which give access to all data regardless of read restrictions.": "Allow editors to edit structure (e.g. modify and delete tables, columns, layouts), and to write formulas, which give access to all data regardless of read restrictions."
|
||||||
},
|
},
|
||||||
"AccountPage": {
|
"AccountPage": {
|
||||||
"API": "API",
|
"API": "API",
|
||||||
@ -589,7 +591,8 @@
|
|||||||
"Send to Google Drive": "Send to Google Drive",
|
"Send to Google Drive": "Send to Google Drive",
|
||||||
"Show in folder": "Show in folder",
|
"Show in folder": "Show in folder",
|
||||||
"Unsaved": "Unsaved",
|
"Unsaved": "Unsaved",
|
||||||
"Work on a Copy": "Work on a Copy"
|
"Work on a Copy": "Work on a Copy",
|
||||||
|
"Share": "Share"
|
||||||
},
|
},
|
||||||
"SiteSwitcher": {
|
"SiteSwitcher": {
|
||||||
"Create new team site": "Create new team site",
|
"Create new team site": "Create new team site",
|
||||||
@ -801,7 +804,8 @@
|
|||||||
"Find Next ": "Find Next ",
|
"Find Next ": "Find Next ",
|
||||||
"Find Previous ": "Find Previous ",
|
"Find Previous ": "Find Previous ",
|
||||||
"No results": "No results",
|
"No results": "No results",
|
||||||
"Search in document": "Search in document"
|
"Search in document": "Search in document",
|
||||||
|
"Search": "Search"
|
||||||
},
|
},
|
||||||
"sendToDrive": {
|
"sendToDrive": {
|
||||||
"Sending file to Google Drive": "Sending file to Google Drive"
|
"Sending file to Google Drive": "Sending file to Google Drive"
|
||||||
@ -979,7 +983,9 @@
|
|||||||
"Add New": "Add New",
|
"Add New": "Add New",
|
||||||
"Use the 𝚺 icon to create summary (or pivot) tables, for totals or subtotals.": "Use the 𝚺 icon to create summary (or pivot) tables, for totals or subtotals.",
|
"Use the 𝚺 icon to create summary (or pivot) tables, for totals or subtotals.": "Use the 𝚺 icon to create summary (or pivot) tables, for totals or subtotals.",
|
||||||
"Anchor Links": "Anchor Links",
|
"Anchor Links": "Anchor Links",
|
||||||
"Custom Widgets": "Custom Widgets"
|
"Custom Widgets": "Custom Widgets",
|
||||||
|
"To make an anchor link that takes the user to a specific cell, click on a row and press {{shortcut}}.": "To make an anchor link that takes the user to a specific cell, click on a row and press {{shortcut}}.",
|
||||||
|
"You can choose one of our pre-made widgets or embed your own by providing its full URL.": "You can choose one of our pre-made widgets or embed your own by providing its full URL."
|
||||||
},
|
},
|
||||||
"DescriptionConfig": {
|
"DescriptionConfig": {
|
||||||
"DESCRIPTION": "DESCRIPTION"
|
"DESCRIPTION": "DESCRIPTION"
|
||||||
@ -1041,6 +1047,61 @@
|
|||||||
"You can always switch sites using the account menu.": "You can always switch sites using the account menu.",
|
"You can always switch sites using the account menu.": "You can always switch sites using the account menu.",
|
||||||
"You have access to the following Grist sites.": "You have access to the following Grist sites."
|
"You have access to the following Grist sites.": "You have access to the following Grist sites."
|
||||||
},
|
},
|
||||||
|
"DescriptionTextArea": {
|
||||||
|
"DESCRIPTION": "DESCRIPTION"
|
||||||
|
},
|
||||||
|
"UserManager": {
|
||||||
|
"Add {{member}} to your team": "Add {{member}} to your team",
|
||||||
|
"Allow anyone with the link to open.": "Allow anyone with the link to open.",
|
||||||
|
"Anyone with link ": "Anyone with link ",
|
||||||
|
"Cancel": "Cancel",
|
||||||
|
"Close": "Close",
|
||||||
|
"Collaborator": "Collaborator",
|
||||||
|
"Confirm": "Confirm",
|
||||||
|
"Copy Link": "Copy Link",
|
||||||
|
"Create a team to share with more people": "Create a team to share with more people",
|
||||||
|
"Grist support": "Grist support",
|
||||||
|
"Guest": "Guest",
|
||||||
|
"Invite multiple": "Invite multiple",
|
||||||
|
"Invite people to {{resourceType}}": "Invite people to {{resourceType}}",
|
||||||
|
"Link copied to clipboard": "Link copied to clipboard",
|
||||||
|
"Manage members of team site": "Manage members of team site",
|
||||||
|
"No default access allows access to be granted to individual documents or workspaces, rather than the full team site.": "No default access allows access to be granted to individual documents or workspaces, rather than the full team site.",
|
||||||
|
"Off": "Off",
|
||||||
|
"On": "On",
|
||||||
|
"Once you have removed your own access, you will not be able to get it back without assistance from someone else with sufficient access to the {{name}}.": "Once you have removed your own access, you will not be able to get it back without assistance from someone else with sufficient access to the {{name}}.",
|
||||||
|
"Open Access Rules": "Open Access Rules",
|
||||||
|
"Outside collaborator": "Outside collaborator",
|
||||||
|
"Public Access": "Public Access",
|
||||||
|
"Public access": "Public access",
|
||||||
|
"Public access inherited from {{parent}}. To remove, set 'Inherit access' option to 'None'.": "Public access inherited from {{parent}}. To remove, set 'Inherit access' option to 'None'.",
|
||||||
|
"Public access: ": "Public access: ",
|
||||||
|
"Remove my access": "Remove my access",
|
||||||
|
"Save & ": "Save & ",
|
||||||
|
"Team member": "Team member",
|
||||||
|
"User inherits permissions from {{parent})}. To remove, set 'Inherit access' option to 'None'.": "User inherits permissions from {{parent})}. To remove, set 'Inherit access' option to 'None'.",
|
||||||
|
"User may not modify their own access.": "User may not modify their own access.",
|
||||||
|
"Your role for this team site": "Your role for this team site",
|
||||||
|
"Your role for this {{resourceType}}": "Your role for this {{resourceType}}",
|
||||||
|
"free collaborator": "free collaborator",
|
||||||
|
"guest": "guest",
|
||||||
|
"member": "member",
|
||||||
|
"team site": "team site",
|
||||||
|
"{{collaborator}} limit exceeded": "{{collaborator}} limit exceeded",
|
||||||
|
"{{limitAt}} of {{limitTop}} {{collaborator}}s": "{{limitAt}} of {{limitTop}} {{collaborator}}s",
|
||||||
|
"No default access allows access to be granted to individual documents or workspaces, rather than the full team site.": "No default access allows access to be granted to individual documents or workspaces, rather than the full team site.",
|
||||||
|
"Once you have removed your own access, you will not be able to get it back without assistance from someone else with sufficient access to the {{resourceType}}.": "Once you have removed your own access, you will not be able to get it back without assistance from someone else with sufficient access to the {{resourceType}}.",
|
||||||
|
"User has view access to {{resource}} resulting from manually-set access to resources inside. If removed here, this user will lose access to resources inside.": "User has view access to {{resource}} resulting from manually-set access to resources inside. If removed here, this user will lose access to resources inside.",
|
||||||
|
"User inherits permissions from {{parent}}. To remove, set 'Inherit access' option to 'None'.": "User inherits permissions from {{parent}}. To remove, set 'Inherit access' option to 'None'.",
|
||||||
|
"You are about to remove your own access to this {{resourceType}}": "You are about to remove your own access to this {{resourceType}}"
|
||||||
|
},
|
||||||
|
"SearchModel": {
|
||||||
|
"Search all pages": "Search all pages",
|
||||||
|
"Search all tables": "Search all tables"
|
||||||
|
},
|
||||||
|
"searchDropdown": {
|
||||||
|
"Search": "Search"
|
||||||
|
},
|
||||||
"SupportGristNudge": {
|
"SupportGristNudge": {
|
||||||
"Close": "Close",
|
"Close": "Close",
|
||||||
"Contribute": "Contribute",
|
"Contribute": "Contribute",
|
||||||
|
@ -66,7 +66,11 @@
|
|||||||
"Sign Out": "Cerrar Sesión",
|
"Sign Out": "Cerrar Sesión",
|
||||||
"Sign in": "Iniciar Sesión",
|
"Sign in": "Iniciar Sesión",
|
||||||
"Switch Accounts": "Cambiar de Cuenta",
|
"Switch Accounts": "Cambiar de Cuenta",
|
||||||
"Toggle Mobile Mode": "Alternar Modo Móvil"
|
"Toggle Mobile Mode": "Alternar Modo Móvil",
|
||||||
|
"Activation": "Activación",
|
||||||
|
"Billing Account": "Cuenta de facturación",
|
||||||
|
"Support Grist": "Soporte Grist",
|
||||||
|
"Upgrade Plan": "Actualizar el Plan"
|
||||||
},
|
},
|
||||||
"AddNewButton": {
|
"AddNewButton": {
|
||||||
"Add New": "Agregar Nuevo"
|
"Add New": "Agregar Nuevo"
|
||||||
@ -430,7 +434,8 @@
|
|||||||
"Notifications": "Notificaciones",
|
"Notifications": "Notificaciones",
|
||||||
"Renew": "Renovar",
|
"Renew": "Renovar",
|
||||||
"Report a problem": "Reportar un problema",
|
"Report a problem": "Reportar un problema",
|
||||||
"Upgrade Plan": "Actualizar el Plan"
|
"Upgrade Plan": "Actualizar el Plan",
|
||||||
|
"Manage billing": "Administrar la facturación"
|
||||||
},
|
},
|
||||||
"OnBoardingPopups": {
|
"OnBoardingPopups": {
|
||||||
"Finish": "Finalizar",
|
"Finish": "Finalizar",
|
||||||
@ -1089,5 +1094,37 @@
|
|||||||
"Welcome back": "Bienvenido de nuevo",
|
"Welcome back": "Bienvenido de nuevo",
|
||||||
"You can always switch sites using the account menu.": "Siempre puedes cambiar de sitio utilizando el menú de la cuenta.",
|
"You can always switch sites using the account menu.": "Siempre puedes cambiar de sitio utilizando el menú de la cuenta.",
|
||||||
"You have access to the following Grist sites.": "Usted tiene acceso a los siguientes sitios de Grist."
|
"You have access to the following Grist sites.": "Usted tiene acceso a los siguientes sitios de Grist."
|
||||||
|
},
|
||||||
|
"SupportGristNudge": {
|
||||||
|
"Help Center": "Centro de ayuda",
|
||||||
|
"Opted In": "Optado por participar",
|
||||||
|
"Support Grist": "Soporte Grist",
|
||||||
|
"Opt in to Telemetry": "Participar en Telemetría",
|
||||||
|
"Support Grist page": "Página de soporte de Grist",
|
||||||
|
"Close": "Cerrar",
|
||||||
|
"Contribute": "Contribuir"
|
||||||
|
},
|
||||||
|
"SupportGristPage": {
|
||||||
|
"GitHub": "GitHub",
|
||||||
|
"GitHub Sponsors page": "Página de patrocinadores de GitHub",
|
||||||
|
"Help Center": "Centro de ayuda",
|
||||||
|
"Manage Sponsorship": "Gestionar el patrocinio",
|
||||||
|
"Opt in to Telemetry": "Optar por la telemetría",
|
||||||
|
"Opt out of Telemetry": "Darse de baja de la telemetría",
|
||||||
|
"Telemetry": "Telemetría",
|
||||||
|
"This instance is opted in to telemetry. Only the site administrator has permission to change this.": "Esta instancia está habilitada para la telemetría. Solo el administrador del sitio tiene permiso para cambiar esto.",
|
||||||
|
"Sponsor Grist Labs on GitHub": "Patrocinar Grist Labs en GitHub",
|
||||||
|
"Support Grist": "Soporte Grist",
|
||||||
|
"This instance is opted out of telemetry. Only the site administrator has permission to change this.": "Esta instancia está inhabilitada para la telemetría. Solo el administrador del sitio tiene permiso para cambiar esto.",
|
||||||
|
"We only collect usage statistics, as detailed in our {{link}}, never document contents.": "Solo recopilamos estadísticas de uso, como se detalla en nuestro {{link}}, nunca el contenido de los documentos.",
|
||||||
|
"You can opt out of telemetry at any time from this page.": "Puede cancelar la telemetría en cualquier momento desde esta página.",
|
||||||
|
"You have opted in to telemetry. Thank you!": "Ha optado por la telemetría. ¡Gracias!",
|
||||||
|
"You have opted out of telemetry.": "Ha optado por no participar en la telemetría.",
|
||||||
|
"Home": "Inicio"
|
||||||
|
},
|
||||||
|
"buildViewSectionDom": {
|
||||||
|
"No data": "Sin datos",
|
||||||
|
"No row selected in {{title}}": "Ninguna fila seleccionada en {{title}}",
|
||||||
|
"Not all data is shown": "No se muestran todos los datos"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -78,7 +78,8 @@
|
|||||||
"No notifications": "Nessuna notifica",
|
"No notifications": "Nessuna notifica",
|
||||||
"Renew": "Rinnova",
|
"Renew": "Rinnova",
|
||||||
"Report a problem": "Segnala un problema",
|
"Report a problem": "Segnala un problema",
|
||||||
"Upgrade Plan": "Aggiorna il tuo piano"
|
"Upgrade Plan": "Aggiorna il tuo piano",
|
||||||
|
"Manage billing": "Gestisci modalità di addebito"
|
||||||
},
|
},
|
||||||
"Pages": {
|
"Pages": {
|
||||||
"Delete data and this page.": "Elimina i dati e questa pagina.",
|
"Delete data and this page.": "Elimina i dati e questa pagina.",
|
||||||
@ -345,7 +346,11 @@
|
|||||||
"Pricing": "Prezzi",
|
"Pricing": "Prezzi",
|
||||||
"Profile Settings": "Impostazioni utente",
|
"Profile Settings": "Impostazioni utente",
|
||||||
"Sign in": "Accedi",
|
"Sign in": "Accedi",
|
||||||
"Switch Accounts": "Cambia account"
|
"Switch Accounts": "Cambia account",
|
||||||
|
"Activation": "Attivazione",
|
||||||
|
"Upgrade Plan": "Cambia il tuo piano",
|
||||||
|
"Billing Account": "Conto di addebito",
|
||||||
|
"Support Grist": "Sostieni Grist"
|
||||||
},
|
},
|
||||||
"ActionLog": {
|
"ActionLog": {
|
||||||
"Column {{colId}} was subsequently removed in action #{{action.actionNum}}": "La colonna {{colId}} è stata successivamente rimossa nell'azione #{{action.actionNum}}",
|
"Column {{colId}} was subsequently removed in action #{{action.actionNum}}": "La colonna {{colId}} è stata successivamente rimossa nell'azione #{{action.actionNum}}",
|
||||||
@ -1035,5 +1040,37 @@
|
|||||||
"Welcome back": "Bentornato",
|
"Welcome back": "Bentornato",
|
||||||
"You can always switch sites using the account menu.": "Puoi sempre cambiare sito usando il menu del tuo profilo.",
|
"You can always switch sites using the account menu.": "Puoi sempre cambiare sito usando il menu del tuo profilo.",
|
||||||
"You have access to the following Grist sites.": "Hai accesso a questi siti di Grist."
|
"You have access to the following Grist sites.": "Hai accesso a questi siti di Grist."
|
||||||
|
},
|
||||||
|
"SupportGristNudge": {
|
||||||
|
"Support Grist page": "Pagina Sostieni Grist",
|
||||||
|
"Close": "Chiudi",
|
||||||
|
"Contribute": "Contribuisci",
|
||||||
|
"Help Center": "Centro Aiuto",
|
||||||
|
"Opt in to Telemetry": "Accetta la telemetria",
|
||||||
|
"Opted In": "Accettato",
|
||||||
|
"Support Grist": "Sostieni Grist"
|
||||||
|
},
|
||||||
|
"SupportGristPage": {
|
||||||
|
"GitHub": "GitHub",
|
||||||
|
"Help Center": "Centro Aiuto",
|
||||||
|
"Home": "Pagina iniziale",
|
||||||
|
"Manage Sponsorship": "Gestisci sponsorizzazione",
|
||||||
|
"Opt out of Telemetry": "Disattiva la telemetria",
|
||||||
|
"Sponsor Grist Labs on GitHub": "Sostieni Grist Labs su GitHub",
|
||||||
|
"Support Grist": "Sostieni Grist",
|
||||||
|
"Telemetry": "Telemetria",
|
||||||
|
"This instance is opted in to telemetry. Only the site administrator has permission to change this.": "Questa istanza accetta la telemetria. Solo un amministratore può cambiare questa impostazione.",
|
||||||
|
"This instance is opted out of telemetry. Only the site administrator has permission to change this.": "Questa istanza ha disattivato la telemetria. Solo un amministratore può cambiare questa opzione.",
|
||||||
|
"You can opt out of telemetry at any time from this page.": "Puoi disattivare la telemetria in qualsiasi momento da questa pagina.",
|
||||||
|
"You have opted in to telemetry. Thank you!": "Hai accettato la telemetria. Grazie!",
|
||||||
|
"You have opted out of telemetry.": "Hai disattivato la telemetria.",
|
||||||
|
"GitHub Sponsors page": "Pagina Sponsor GitHub",
|
||||||
|
"Opt in to Telemetry": "Accetta la telemetria",
|
||||||
|
"We only collect usage statistics, as detailed in our {{link}}, never document contents.": "Raccogliamo solo statistiche di utilizzo, mai contenuti dei documenti, come spiegato in {{link}}."
|
||||||
|
},
|
||||||
|
"buildViewSectionDom": {
|
||||||
|
"No row selected in {{title}}": "Nessuna riga selezionata in {{title}}",
|
||||||
|
"Not all data is shown": "Non tutti i dati sono mostrati",
|
||||||
|
"No data": "Nessun dato"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -129,7 +129,8 @@
|
|||||||
},
|
},
|
||||||
"WelcomeSitePicker": {
|
"WelcomeSitePicker": {
|
||||||
"You can always switch sites using the account menu.": "Sempre pode alternar entre sites através do menu da conta.",
|
"You can always switch sites using the account menu.": "Sempre pode alternar entre sites através do menu da conta.",
|
||||||
"Welcome back": "Bem-vindo de volta"
|
"Welcome back": "Bem-vindo de volta",
|
||||||
|
"You have access to the following Grist sites.": "Tens acesso aos seguintes sítios Grist."
|
||||||
},
|
},
|
||||||
"MakeCopyMenu": {
|
"MakeCopyMenu": {
|
||||||
"Cancel": "Cancelar",
|
"Cancel": "Cancelar",
|
||||||
|
@ -71,7 +71,11 @@
|
|||||||
"Sign Out": "Sair",
|
"Sign Out": "Sair",
|
||||||
"Sign in": "Entrar",
|
"Sign in": "Entrar",
|
||||||
"Switch Accounts": "Alternar Contas",
|
"Switch Accounts": "Alternar Contas",
|
||||||
"Toggle Mobile Mode": "Alternar Modo Móvel"
|
"Toggle Mobile Mode": "Alternar Modo Móvel",
|
||||||
|
"Activation": "Ativação",
|
||||||
|
"Billing Account": "Conta de faturamento",
|
||||||
|
"Support Grist": "Suporte Grist",
|
||||||
|
"Upgrade Plan": "Atualizar o Plano"
|
||||||
},
|
},
|
||||||
"ActionLog": {
|
"ActionLog": {
|
||||||
"Action Log failed to load": "Falha ao carregar o Log de Ações",
|
"Action Log failed to load": "Falha ao carregar o Log de Ações",
|
||||||
@ -511,7 +515,8 @@
|
|||||||
"Notifications": "Notificações",
|
"Notifications": "Notificações",
|
||||||
"Renew": "Renovar",
|
"Renew": "Renovar",
|
||||||
"Report a problem": "Reportar um problema",
|
"Report a problem": "Reportar um problema",
|
||||||
"Upgrade Plan": "Atualizar o Plano"
|
"Upgrade Plan": "Atualizar o Plano",
|
||||||
|
"Manage billing": "Gerenciar faturamento"
|
||||||
},
|
},
|
||||||
"OnBoardingPopups": {
|
"OnBoardingPopups": {
|
||||||
"Finish": "Terminar",
|
"Finish": "Terminar",
|
||||||
@ -1099,5 +1104,37 @@
|
|||||||
"You have access to the following Grist sites.": "Você tem acesso aos seguintes sites do Grist.",
|
"You have access to the following Grist sites.": "Você tem acesso aos seguintes sites do Grist.",
|
||||||
"Welcome back": "Bem-vindo de volta",
|
"Welcome back": "Bem-vindo de volta",
|
||||||
"You can always switch sites using the account menu.": "Você sempre pode alternar entre sites usando o menu da conta."
|
"You can always switch sites using the account menu.": "Você sempre pode alternar entre sites usando o menu da conta."
|
||||||
|
},
|
||||||
|
"SupportGristNudge": {
|
||||||
|
"Close": "Fechar",
|
||||||
|
"Opt in to Telemetry": "Aceitar a Telemetria",
|
||||||
|
"Help Center": "Centro de Ajuda",
|
||||||
|
"Support Grist": "Suporte Grist",
|
||||||
|
"Contribute": "Contribuir",
|
||||||
|
"Opted In": "Optou por participar",
|
||||||
|
"Support Grist page": "Página de Suporte Grist"
|
||||||
|
},
|
||||||
|
"SupportGristPage": {
|
||||||
|
"GitHub": "GitHub",
|
||||||
|
"GitHub Sponsors page": "Página de patrocinadores do GitHub",
|
||||||
|
"Help Center": "Centro de Ajuda",
|
||||||
|
"Home": "Início",
|
||||||
|
"Manage Sponsorship": "Gerenciar patrocínio",
|
||||||
|
"Opt in to Telemetry": "Aceitar a Telemetria",
|
||||||
|
"Opt out of Telemetry": "Desativar a Telemetria",
|
||||||
|
"Sponsor Grist Labs on GitHub": "Patrocine Grist Labs no GitHub",
|
||||||
|
"Support Grist": "Suporte Grist",
|
||||||
|
"Telemetry": "Telemetria",
|
||||||
|
"This instance is opted in to telemetry. Only the site administrator has permission to change this.": "Esta instância está incluída na telemetria. Somente o administrador do site tem permissão para alterar isso.",
|
||||||
|
"You can opt out of telemetry at any time from this page.": "Você pode desativar a telemetria a qualquer momento nesta página.",
|
||||||
|
"You have opted out of telemetry.": "Você decidiu em não participar da telemetria.",
|
||||||
|
"We only collect usage statistics, as detailed in our {{link}}, never document contents.": "Coletamos apenas estatísticas de uso, conforme detalhado em nosso {{link}}, nunca o conteúdo dos documentos.",
|
||||||
|
"This instance is opted out of telemetry. Only the site administrator has permission to change this.": "Esta instância foi desativada da telemetria. Somente o administrador do site tem permissão para alterar isso.",
|
||||||
|
"You have opted in to telemetry. Thank you!": "Você optou pela telemetria. Obrigado!"
|
||||||
|
},
|
||||||
|
"buildViewSectionDom": {
|
||||||
|
"No data": "Sem dados",
|
||||||
|
"No row selected in {{title}}": "Nenhuma linha selecionada em {{title}}",
|
||||||
|
"Not all data is shown": "Nem todos os dados são mostrados"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user