mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
Make a good part of the app localizable and add French translations (#325)
Co-authored-by: Yohan Boniface <yohanboniface@free.fr>
This commit is contained in:
@@ -51,8 +51,11 @@ import {
|
||||
Observable,
|
||||
styled
|
||||
} from 'grainjs';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import isEqual = require('lodash/isEqual');
|
||||
|
||||
const t = makeT('aclui.AccessRules');
|
||||
|
||||
// tslint:disable:max-classes-per-file no-console
|
||||
|
||||
// Types for the rows in the ACL tables we use.
|
||||
@@ -317,21 +320,21 @@ export class AccessRules extends Disposable {
|
||||
bigBasicButton({disabled: true}, dom.hide(this._savingEnabled),
|
||||
dom.text((use) => {
|
||||
const s = use(this._ruleStatus);
|
||||
return s === RuleStatus.CheckPending ? 'Checking...' :
|
||||
s === RuleStatus.Unchanged ? 'Saved' : 'Invalid';
|
||||
return s === RuleStatus.CheckPending ? t('Checking') :
|
||||
s === RuleStatus.Unchanged ? t('Saved') : t('Invalid');
|
||||
}),
|
||||
testId('rules-non-save')
|
||||
),
|
||||
bigPrimaryButton('Save', dom.show(this._savingEnabled),
|
||||
bigPrimaryButton(t('Save'), dom.show(this._savingEnabled),
|
||||
dom.on('click', () => this.save()),
|
||||
testId('rules-save'),
|
||||
),
|
||||
bigBasicButton('Reset', dom.show(use => use(this._ruleStatus) !== RuleStatus.Unchanged),
|
||||
bigBasicButton(t('Reset'), dom.show(use => use(this._ruleStatus) !== RuleStatus.Unchanged),
|
||||
dom.on('click', () => this.update()),
|
||||
testId('rules-revert'),
|
||||
),
|
||||
|
||||
bigBasicButton('Add Table Rules', cssDropdownIcon('Dropdown'), {style: 'margin-left: auto'},
|
||||
bigBasicButton(t('AddTableRules'), cssDropdownIcon('Dropdown'), {style: 'margin-left: auto'},
|
||||
menu(() =>
|
||||
this.allTableIds.map((tableId) =>
|
||||
// Add the table on a timeout, to avoid disabling the clicked menu item
|
||||
@@ -343,8 +346,8 @@ export class AccessRules extends Disposable {
|
||||
),
|
||||
),
|
||||
),
|
||||
bigBasicButton('Add User Attributes', dom.on('click', () => this._addUserAttributes())),
|
||||
bigBasicButton('Users', cssDropdownIcon('Dropdown'), elem => this._aclUsersPopup.attachPopup(elem),
|
||||
bigBasicButton(t('AddUserAttributes'), dom.on('click', () => this._addUserAttributes())),
|
||||
bigBasicButton(t('Users'), cssDropdownIcon('Dropdown'), elem => this._aclUsersPopup.attachPopup(elem),
|
||||
dom.style('visibility', use => use(this._aclUsersPopup.isInitialized) ? '' : 'hidden')),
|
||||
),
|
||||
cssConditionError({style: 'margin-left: 16px'},
|
||||
@@ -354,15 +357,15 @@ export class AccessRules extends Disposable {
|
||||
shadowScroll(
|
||||
dom.maybe(use => use(this._userAttrRules).length, () =>
|
||||
cssSection(
|
||||
cssSectionHeading('User Attributes'),
|
||||
cssSectionHeading(t('UserAttributes')),
|
||||
cssTableRounded(
|
||||
cssTableHeaderRow(
|
||||
cssCell1(cssCell.cls('-rborder'), cssCell.cls('-center'), cssColHeaderCell('Name')),
|
||||
cssCell4(
|
||||
cssColumnGroup(
|
||||
cssCell1(cssColHeaderCell('Attribute to Look Up')),
|
||||
cssCell1(cssColHeaderCell('Lookup Table')),
|
||||
cssCell1(cssColHeaderCell('Lookup Column')),
|
||||
cssCell1(cssColHeaderCell(t('AttributeToLookUp'))),
|
||||
cssCell1(cssColHeaderCell(t('LookupTable'))),
|
||||
cssCell1(cssColHeaderCell(t('LookupColumn'))),
|
||||
cssCellIcon(),
|
||||
),
|
||||
),
|
||||
@@ -373,15 +376,15 @@ export class AccessRules extends Disposable {
|
||||
),
|
||||
dom.forEach(this._tableRules, (tableRules) => tableRules.buildDom()),
|
||||
cssSection(
|
||||
cssSectionHeading('Default Rules', testId('rule-table-header')),
|
||||
cssSectionHeading(t("DefaultRules"), testId('rule-table-header')),
|
||||
cssTableRounded(
|
||||
cssTableHeaderRow(
|
||||
cssCell1(cssCell.cls('-rborder'), cssCell.cls('-center'), cssColHeaderCell('Columns')),
|
||||
cssCell4(
|
||||
cssColumnGroup(
|
||||
cssCellIcon(),
|
||||
cssCell2(cssColHeaderCell('Condition')),
|
||||
cssCell1(cssColHeaderCell('Permissions')),
|
||||
cssCell2(cssColHeaderCell(t('Condition'))),
|
||||
cssCell1(cssColHeaderCell(t('Permissions'))),
|
||||
cssCellIcon(),
|
||||
)
|
||||
)
|
||||
@@ -521,13 +524,13 @@ class TableRules extends Disposable {
|
||||
public buildDom() {
|
||||
return cssSection(
|
||||
cssSectionHeading(
|
||||
dom('span', 'Rules for table ', cssTableName(this._accessRules.getTableTitle(this.tableId))),
|
||||
dom('span', t('RulesForTable'), cssTableName(this._accessRules.getTableTitle(this.tableId))),
|
||||
cssIconButton(icon('Dots'), {style: 'margin-left: auto'},
|
||||
menu(() => [
|
||||
menuItemAsync(() => this._addColumnRuleSet(), 'Add Column Rule'),
|
||||
menuItemAsync(() => this._addDefaultRuleSet(), 'Add Default Rule',
|
||||
menuItemAsync(() => this._addColumnRuleSet(), t('AddColumnRule')),
|
||||
menuItemAsync(() => this._addDefaultRuleSet(), t('AddDefaultRule'),
|
||||
dom.cls('disabled', use => Boolean(use(this._defaultRuleSet)))),
|
||||
menuItemAsync(() => this._accessRules.removeTableRules(this), 'Delete Table Rules'),
|
||||
menuItemAsync(() => this._accessRules.removeTableRules(this), t('DeleteTableRules')),
|
||||
]),
|
||||
testId('rule-table-menu-btn'),
|
||||
),
|
||||
@@ -539,8 +542,8 @@ class TableRules extends Disposable {
|
||||
cssCell4(
|
||||
cssColumnGroup(
|
||||
cssCellIcon(),
|
||||
cssCell2(cssColHeaderCell('Condition')),
|
||||
cssCell1(cssColHeaderCell('Permissions')),
|
||||
cssCell2(cssColHeaderCell(t('Condition'))),
|
||||
cssCell1(cssColHeaderCell(t('Permissions'))),
|
||||
cssCellIcon(),
|
||||
)
|
||||
),
|
||||
@@ -654,7 +657,7 @@ class TableRules extends Disposable {
|
||||
class SpecialRules extends TableRules {
|
||||
public buildDom() {
|
||||
return cssSection(
|
||||
cssSectionHeading('Special Rules', testId('rule-table-header')),
|
||||
cssSectionHeading(t('SpecialRules'), testId('rule-table-header')),
|
||||
this.buildColumnRuleSets(),
|
||||
this.buildErrors(),
|
||||
testId('rule-table'),
|
||||
@@ -893,18 +896,17 @@ class DefaultObsRuleSet extends ObsRuleSet {
|
||||
function getSpecialRuleDescription(type: string): string {
|
||||
switch (type) {
|
||||
case 'AccessRules':
|
||||
return 'Allow everyone to view Access Rules.';
|
||||
return t('AccessRulesDescription');
|
||||
case 'FullCopies':
|
||||
return 'Allow everyone to copy the entire document, or view it in full in fiddle mode.\n' +
|
||||
'Useful for examples and templates, but not for sensitive data.';
|
||||
return t('FullCopiesDescription');
|
||||
default: return type;
|
||||
}
|
||||
}
|
||||
|
||||
function getSpecialRuleName(type: string): string {
|
||||
switch (type) {
|
||||
case 'AccessRules': return 'Permission to view Access Rules';
|
||||
case 'FullCopies': return 'Permission to access the document in full when needed';
|
||||
case 'AccessRules': return t('AccessRulesName');
|
||||
case 'FullCopies': return t('FullCopies');
|
||||
default: return type;
|
||||
}
|
||||
}
|
||||
@@ -1037,7 +1039,7 @@ class ObsUserAttributeRule extends Disposable {
|
||||
cssCell1(cssCell.cls('-rborder'),
|
||||
cssCellContent(
|
||||
cssInput(this._name, async (val) => this._name.set(val),
|
||||
{placeholder: 'Attribute name'},
|
||||
{placeholder: t('AttributeNamePlaceholder')},
|
||||
(this._options.focus ? (elem) => { setTimeout(() => elem.focus(), 0); } : null),
|
||||
testId('rule-userattr-name'),
|
||||
),
|
||||
@@ -1253,9 +1255,9 @@ class ObsRulePart extends Disposable {
|
||||
setValue: (value) => this._setAclFormula(value),
|
||||
placeholder: dom.text((use) => {
|
||||
return (
|
||||
this._ruleSet.isSoleCondition(use, this) ? 'Everyone' :
|
||||
this._ruleSet.isLastCondition(use, this) ? 'Everyone Else' :
|
||||
'Enter Condition'
|
||||
this._ruleSet.isSoleCondition(use, this) ? t('Everyone') :
|
||||
this._ruleSet.isLastCondition(use, this) ? t('EveryoneElse') :
|
||||
t('EnterCondition')
|
||||
);
|
||||
}),
|
||||
getSuggestions: (prefix) => this._completions.get(),
|
||||
|
||||
@@ -10,10 +10,13 @@ import {ALL_PERMISSION_PROPS, emptyPermissionSet} from 'app/common/ACLPermission
|
||||
import {capitalize} from 'app/common/gutil';
|
||||
import {dom, DomElementArg, Observable, styled} from 'grainjs';
|
||||
import isEqual = require('lodash/isEqual');
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
|
||||
// One of the strings 'read', 'update', etc.
|
||||
export type PermissionKey = keyof PartialPermissionSet;
|
||||
|
||||
const t = makeT('aclui.PermissionsWidget');
|
||||
|
||||
/**
|
||||
* Renders a box for each of availableBits, and a dropdown with a description and some shortcuts.
|
||||
*/
|
||||
@@ -61,13 +64,13 @@ export function permissionsWidget(
|
||||
null
|
||||
),
|
||||
// If the set matches any recognized pattern, mark that item with a tick (checkmark).
|
||||
cssMenuItem(() => setPermissions(allowAll), tick(isEqual(pset.get(), allowAll)), 'Allow All',
|
||||
cssMenuItem(() => setPermissions(allowAll), tick(isEqual(pset.get(), allowAll)), t('AllowAll'),
|
||||
dom.cls('disabled', options.disabled)
|
||||
),
|
||||
cssMenuItem(() => setPermissions(denyAll), tick(isEqual(pset.get(), denyAll)), 'Deny All',
|
||||
cssMenuItem(() => setPermissions(denyAll), tick(isEqual(pset.get(), denyAll)), t('DenyAll'),
|
||||
dom.cls('disabled', options.disabled)
|
||||
),
|
||||
cssMenuItem(() => setPermissions(readOnly), tick(isEqual(pset.get(), readOnly)), 'Read Only',
|
||||
cssMenuItem(() => setPermissions(readOnly), tick(isEqual(pset.get(), readOnly)), t('ReadOnly'),
|
||||
dom.cls('disabled', options.disabled)
|
||||
),
|
||||
cssMenuItem(() => setPermissions(empty),
|
||||
|
||||
@@ -19,6 +19,7 @@ import {ActionSummary, asTabularDiffs, defunctTableName, getAffectedTables,
|
||||
LabelDelta} from 'app/common/ActionSummary';
|
||||
import {CellDelta} from 'app/common/TabularDiff';
|
||||
import {IDomComponent} from 'grainjs';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
|
||||
/**
|
||||
*
|
||||
@@ -46,6 +47,8 @@ const state = {
|
||||
DEFAULT: 'default'
|
||||
};
|
||||
|
||||
const t = makeT('components.ActionLog');
|
||||
|
||||
export class ActionLog extends dispose.Disposable implements IDomComponent {
|
||||
|
||||
private _displayStack: KoArray<ActionGroupWithState>;
|
||||
@@ -224,7 +227,7 @@ export class ActionLog extends dispose.Disposable implements IDomComponent {
|
||||
}
|
||||
|
||||
private _buildLogDom() {
|
||||
this._loadActionSummaries().catch((error) => gristNotify(`Action Log failed to load`));
|
||||
this._loadActionSummaries().catch((error) => gristNotify(t("ActionLogFailed")));
|
||||
return dom('div.action_log',
|
||||
dom('div.preference_item',
|
||||
koForm.checkbox(this._showAllTables,
|
||||
@@ -392,7 +395,7 @@ export class ActionLog extends dispose.Disposable implements IDomComponent {
|
||||
const newName = tableRename[1];
|
||||
if (!newName) {
|
||||
// TODO - find a better way to send informative notifications.
|
||||
gristNotify(`Table ${tableId} was subsequently removed in action #${action.actionNum}`);
|
||||
gristNotify(t('TableRemovedInAction', {tableId:tableId, actionNum: action.actionNum}));
|
||||
return;
|
||||
}
|
||||
tableId = newName;
|
||||
@@ -403,7 +406,7 @@ export class ActionLog extends dispose.Disposable implements IDomComponent {
|
||||
// Check is this row was removed - if so there's no reason to go on.
|
||||
if (td.removeRows.indexOf(rowId) >= 0) {
|
||||
// TODO - find a better way to send informative notifications.
|
||||
gristNotify(`This row was subsequently removed in action #${action.actionNum}`);
|
||||
gristNotify(t("RowRemovedInAction", {actionNum}));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -413,7 +416,7 @@ export class ActionLog extends dispose.Disposable implements IDomComponent {
|
||||
const newName = columnRename[1];
|
||||
if (!newName) {
|
||||
// TODO - find a better way to send informative notifications.
|
||||
gristNotify(`Column ${colId} was subsequently removed in action #${action.actionNum}`);
|
||||
gristNotify(t("ColumnRemovedInAction", {colId, actionNum: action.actionNum}));
|
||||
return;
|
||||
}
|
||||
colId = newName;
|
||||
|
||||
@@ -38,6 +38,7 @@ import sum = require('lodash/sum');
|
||||
import union = require('lodash/union');
|
||||
import type {Annotations, Config, Datum, ErrorBar, Layout, LayoutAxis, Margin,
|
||||
PlotData as PlotlyPlotData} from 'plotly.js';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
|
||||
|
||||
let Plotly: PlotlyType;
|
||||
@@ -49,6 +50,8 @@ const DONUT_DEFAULT_TEXT_SIZE = 24;
|
||||
|
||||
const testId = makeTestId('test-chart-');
|
||||
|
||||
const t = makeT('components.ChartView');
|
||||
|
||||
function isPieLike(chartType: string) {
|
||||
return ['pie', 'donut'].includes(chartType);
|
||||
}
|
||||
@@ -652,8 +655,8 @@ export class ChartConfig extends GrainJSDisposable {
|
||||
testId('error-bars'),
|
||||
),
|
||||
dom.domComputed(this._optionsObj.prop('errorBars'), (value: ChartOptions["errorBars"]) =>
|
||||
value === 'symmetric' ? cssRowHelp('Each Y series is followed by a series for the length of error bars.') :
|
||||
value === 'separate' ? cssRowHelp('Each Y series is followed by two series, for top and bottom error bars.') :
|
||||
value === 'symmetric' ? cssRowHelp(t('EachYFollowedByOne')) :
|
||||
value === 'separate' ? cssRowHelp(t('EachYFollowedByTwo')) :
|
||||
null
|
||||
),
|
||||
]),
|
||||
@@ -666,7 +669,7 @@ export class ChartConfig extends GrainJSDisposable {
|
||||
select(this._groupDataColId, this._groupDataOptions),
|
||||
testId('group-by-column'),
|
||||
),
|
||||
cssHintRow('Create separate series for each value of the selected column.'),
|
||||
cssHintRow(t('CreateSeparateSeries')),
|
||||
]),
|
||||
|
||||
// TODO: user should select x axis before widget reach page
|
||||
@@ -674,7 +677,7 @@ export class ChartConfig extends GrainJSDisposable {
|
||||
cssRow(
|
||||
select(
|
||||
this._xAxis, this._columnsOptions,
|
||||
{ defaultLabel: 'Pick a column' }
|
||||
{ defaultLabel: t('PickColumn') }
|
||||
),
|
||||
testId('x-axis'),
|
||||
),
|
||||
@@ -770,7 +773,7 @@ export class ChartConfig extends GrainJSDisposable {
|
||||
private async _setGroupDataColumn(colId: string) {
|
||||
const viewFields = this._section.viewFields.peek().peek();
|
||||
|
||||
await this._gristDoc.docData.bundleActions('selected new group data columnd', async () => {
|
||||
await this._gristDoc.docData.bundleActions(t('SelectedNewGroupDataColumns'), async () => {
|
||||
this._freezeXAxis.set(true);
|
||||
this._freezeYAxis.set(true);
|
||||
try {
|
||||
@@ -869,7 +872,7 @@ export class ChartConfig extends GrainJSDisposable {
|
||||
private async _setAggregation(val: boolean) {
|
||||
try {
|
||||
this._freezeXAxis.set(true);
|
||||
await this._gristDoc.docData.bundleActions(`Toggle chart aggregation`, async () => {
|
||||
await this._gristDoc.docData.bundleActions(t("ToggleChartAggregation"), async () => {
|
||||
if (val) {
|
||||
await this._doAggregation();
|
||||
} else {
|
||||
|
||||
@@ -2,12 +2,15 @@ import {GristDoc} from 'app/client/components/GristDoc';
|
||||
import {reportError} from 'app/client/models/errors';
|
||||
import {DisposableWithEvents} from 'app/common/DisposableWithEvents';
|
||||
import {dom, Observable} from 'grainjs';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
|
||||
// Rather than require the whole of highlight.js, require just the core with the one language we
|
||||
// need, to keep our bundle smaller and the build faster.
|
||||
const hljs = require('highlight.js/lib/core');
|
||||
hljs.registerLanguage('python', require('highlight.js/lib/languages/python'));
|
||||
|
||||
const t = makeT('components.CodeEditorPanel');
|
||||
|
||||
export class CodeEditorPanel extends DisposableWithEvents {
|
||||
private _schema = Observable.create(this, '');
|
||||
private _denied = Observable.create(this, false);
|
||||
@@ -25,8 +28,8 @@ export class CodeEditorPanel extends DisposableWithEvents {
|
||||
return dom('div.g-code-panel.clipboard',
|
||||
{tabIndex: "-1"},
|
||||
dom.maybe(this._denied, () => dom('div.g-code-panel-denied',
|
||||
dom('h2', dom.text('Access denied')),
|
||||
dom('div', dom.text('Code View is available only when you have full document access.')),
|
||||
dom('h2', dom.text(t('AccessDenied'))),
|
||||
dom('div', dom.text(t('CodeViewOnlyFullAccess'))),
|
||||
)),
|
||||
dom.maybe(this._schema, (schema) => {
|
||||
// The reason to scope and rebuild instead of using `kd.text(schema)` is because
|
||||
|
||||
@@ -12,9 +12,12 @@ import {loadingDots} from 'app/client/ui2018/loaders';
|
||||
import {menu, menuItem, menuText} from 'app/client/ui2018/menus';
|
||||
import {confirmModal} from 'app/client/ui2018/modals';
|
||||
import {Computed, Disposable, dom, fromKo, makeTestId, Observable, styled} from 'grainjs';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
|
||||
const testId = makeTestId('test-raw-data-');
|
||||
|
||||
const t = makeT('components.DataTables');
|
||||
|
||||
export class DataTables extends Disposable {
|
||||
private _tables: Observable<TableRec[]>;
|
||||
|
||||
@@ -33,7 +36,7 @@ export class DataTables extends Disposable {
|
||||
const dataTables = use(_gristDoc.docModel.rawDataTables.getObservable());
|
||||
const summaryTables = use(_gristDoc.docModel.rawSummaryTables.getObservable());
|
||||
// Remove tables that we don't have access to. ACL will remove tableId from those tables.
|
||||
return [...dataTables, ...summaryTables].filter(t => Boolean(use(t.tableId)));
|
||||
return [...dataTables, ...summaryTables].filter(table => Boolean(use(table.tableId)));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -42,7 +45,7 @@ export class DataTables extends Disposable {
|
||||
cssTableList(
|
||||
/*************** List section **********/
|
||||
testId('list'),
|
||||
docListHeader('Raw Data Tables'),
|
||||
docListHeader(t('RawDataTables')),
|
||||
cssList(
|
||||
dom.forEach(this._tables, tableRec =>
|
||||
cssItem(
|
||||
@@ -62,11 +65,11 @@ export class DataTables extends Disposable {
|
||||
testId('table-id'),
|
||||
dom.text(tableRec.tableId),
|
||||
),
|
||||
{ title : 'Click to copy' },
|
||||
dom.on('click', async (e, t) => {
|
||||
{ title : t('ClickToCopy') },
|
||||
dom.on('click', async (e, d) => {
|
||||
e.stopImmediatePropagation();
|
||||
e.preventDefault();
|
||||
showTransientTooltip(t, 'Table ID copied to clipboard', {
|
||||
showTransientTooltip(d, t('TableIDCopied'), {
|
||||
key: 'copy-table-id'
|
||||
});
|
||||
await copyToClipboard(tableRec.tableId.peek());
|
||||
@@ -124,7 +127,7 @@ export class DataTables extends Disposable {
|
||||
return [
|
||||
menuItem(
|
||||
() => this._duplicateTable(table),
|
||||
'Duplicate Table',
|
||||
t('DuplicateTable'),
|
||||
testId('menu-duplicate-table'),
|
||||
dom.cls('disabled', use =>
|
||||
use(isReadonly) ||
|
||||
@@ -141,23 +144,23 @@ export class DataTables extends Disposable {
|
||||
use(docModel.visibleTables.getObservable()).length <= 1 && !use(table.isHidden)
|
||||
))
|
||||
),
|
||||
dom.maybe(isReadonly, () => menuText('You do not have edit access to this document')),
|
||||
dom.maybe(isReadonly, () => menuText(t("NoEditAccess"))),
|
||||
];
|
||||
}
|
||||
|
||||
private _duplicateTable(t: TableRec) {
|
||||
duplicateTable(this._gristDoc, t.tableId(), {
|
||||
private _duplicateTable(r: TableRec) {
|
||||
duplicateTable(this._gristDoc, r.tableId(), {
|
||||
onSuccess: ({raw_section_id}: DuplicateTableResponse) =>
|
||||
this._gristDoc.viewModel.activeSectionId(raw_section_id),
|
||||
});
|
||||
}
|
||||
|
||||
private _removeTable(t: TableRec) {
|
||||
private _removeTable(r: TableRec) {
|
||||
const {docModel} = this._gristDoc;
|
||||
function doRemove() {
|
||||
return docModel.docData.sendAction(['RemoveTable', t.tableId()]);
|
||||
return docModel.docData.sendAction(['RemoveTable', r.tableId()]);
|
||||
}
|
||||
confirmModal(`Delete ${t.formattedTableName()} data, and remove it from all pages?`, 'Delete', doRemove);
|
||||
confirmModal(t("DeleteData", {formattedTableName : r.formattedTableName()}), 'Delete', doRemove);
|
||||
}
|
||||
|
||||
private _tableRows(table: TableRec) {
|
||||
|
||||
@@ -11,6 +11,9 @@ import {Features, isFreePlan} from 'app/common/Features';
|
||||
import {capitalizeFirstWord} from 'app/common/gutil';
|
||||
import {canUpgradeOrg} from 'app/common/roles';
|
||||
import {Computed, Disposable, dom, DomContents, DomElementArg, makeTestId, styled} from 'grainjs';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
|
||||
const t = makeT('components.DocumentUsage');
|
||||
|
||||
const testId = makeTestId('test-doc-usage-');
|
||||
|
||||
@@ -23,9 +26,6 @@ const DEFAULT_MAX_DATA_SIZE = DEFAULT_MAX_ROWS * 2 * 1024; // 40MB (2KiB per row
|
||||
// Default used by the progress bar to visually indicate attachments size usage.
|
||||
const DEFAULT_MAX_ATTACHMENTS_SIZE = 1 * 1024 * 1024 * 1024; // 1GiB
|
||||
|
||||
const ACCESS_DENIED_MESSAGE = 'Usage statistics are only available to users with '
|
||||
+ 'full access to the document data.';
|
||||
|
||||
/**
|
||||
* Displays statistics about document usage, such as number of rows used.
|
||||
*/
|
||||
@@ -60,7 +60,7 @@ export class DocumentUsage extends Disposable {
|
||||
// Invalid row limits are currently treated as if they are undefined.
|
||||
const maxValue = maxRows && maxRows > 0 ? maxRows : undefined;
|
||||
return {
|
||||
name: 'Rows',
|
||||
name: t('Rows'),
|
||||
currentValue: typeof rowCount !== 'object' ? undefined : rowCount.total,
|
||||
maximumValue: maxValue ?? DEFAULT_MAX_ROWS,
|
||||
unit: 'rows',
|
||||
@@ -75,7 +75,7 @@ export class DocumentUsage extends Disposable {
|
||||
// Invalid data size limits are currently treated as if they are undefined.
|
||||
const maxValue = maxSize && maxSize > 0 ? maxSize : undefined;
|
||||
return {
|
||||
name: 'Data Size',
|
||||
name: t('DataSize'),
|
||||
currentValue: typeof dataSize !== 'number' ? undefined : dataSize,
|
||||
maximumValue: maxValue ?? DEFAULT_MAX_DATA_SIZE,
|
||||
unit: 'MB',
|
||||
@@ -97,7 +97,7 @@ export class DocumentUsage extends Disposable {
|
||||
// Invalid attachments size limits are currently treated as if they are undefined.
|
||||
const maxValue = maxSize && maxSize > 0 ? maxSize : undefined;
|
||||
return {
|
||||
name: 'Attachments Size',
|
||||
name: t('AttachmentsSize'),
|
||||
currentValue: typeof attachmentsSize !== 'number' ? undefined : attachmentsSize,
|
||||
maximumValue: maxValue ?? DEFAULT_MAX_ATTACHMENTS_SIZE,
|
||||
unit: 'GB',
|
||||
@@ -135,7 +135,7 @@ export class DocumentUsage extends Disposable {
|
||||
|
||||
public buildDom() {
|
||||
return dom('div',
|
||||
cssHeader('Usage', testId('heading')),
|
||||
cssHeader(t('Usage'), testId('heading')),
|
||||
dom.domComputed(this._areAllMetricsPending, (isLoading) => {
|
||||
if (isLoading) { return cssSpinner(loadingSpinner(), testId('loading')); }
|
||||
|
||||
@@ -149,7 +149,7 @@ export class DocumentUsage extends Disposable {
|
||||
return dom.domComputed((use) => {
|
||||
const isAccessDenied = use(this._isAccessDenied);
|
||||
if (isAccessDenied === null) { return null; }
|
||||
if (isAccessDenied) { return buildMessage(ACCESS_DENIED_MESSAGE); }
|
||||
if (isAccessDenied) { return buildMessage(t('UsageStatisticsOnlyFullAccess')); }
|
||||
|
||||
const org = use(this._currentOrg);
|
||||
const product = use(this._currentProduct);
|
||||
@@ -237,11 +237,12 @@ export function buildUpgradeMessage(
|
||||
variant: 'short' | 'long',
|
||||
onUpgrade: () => void,
|
||||
) {
|
||||
if (!canUpgrade) { return 'Contact the site owner to upgrade the plan to raise limits.'; }
|
||||
if (!canUpgrade) { return t('LimitContactSiteOwner'); }
|
||||
|
||||
const upgradeLinkText = 'start your 30-day free trial of the Pro plan.';
|
||||
const upgradeLinkText = t('UpgradeLinkText')
|
||||
// TODO i18next
|
||||
return [
|
||||
variant === 'short' ? null : 'For higher limits, ',
|
||||
variant === 'short' ? null : t('ForHigherLimits'),
|
||||
buildUpgradeLink(
|
||||
variant === 'short' ? capitalizeFirstWord(upgradeLinkText) : upgradeLinkText,
|
||||
() => onUpgrade(),
|
||||
|
||||
@@ -4,11 +4,14 @@ import {
|
||||
IDomArgs, MultiHolder, styled, TagElem
|
||||
} from "grainjs";
|
||||
import { GristDoc } from "app/client/components/GristDoc";
|
||||
import { makeT } from 'app/client/lib/localization';
|
||||
import { ITooltipControl, showTooltip, tooltipCloseButton } from "app/client/ui/tooltips";
|
||||
import { FieldEditorStateEvent } from "app/client/widgets/FieldEditor";
|
||||
import { testId, theme } from "app/client/ui2018/cssVars";
|
||||
import { cssLink } from "app/client/ui2018/links";
|
||||
|
||||
const t = makeT('components.Drafts');
|
||||
|
||||
/**
|
||||
* Component that keeps track of editor's state (draft value). If user hits an escape button
|
||||
* by accident, this component will provide a way to continue the work.
|
||||
@@ -270,7 +273,7 @@ class NotificationAdapter extends Disposable implements Notification {
|
||||
}
|
||||
public showUndoDiscard() {
|
||||
const notifier = this._doc.app.topAppModel.notifier;
|
||||
const notification = notifier.createUserMessage("Undo discard", {
|
||||
const notification = notifier.createUserMessage(t("UndoDiscard"), {
|
||||
message: () =>
|
||||
discardNotification(
|
||||
dom.on("click", () => {
|
||||
@@ -418,7 +421,7 @@ const styledTooltip = styled('div', `
|
||||
function cellTooltip(clb: () => any) {
|
||||
return function (ctl: ITooltipControl) {
|
||||
return styledTooltip(
|
||||
cssLink('Restore last edit',
|
||||
cssLink(t('RestoreLastEdit'),
|
||||
dom.on('mousedown', (ev) => { ev.preventDefault(); ctl.close(); clb(); }),
|
||||
testId('draft-tooltip'),
|
||||
),
|
||||
@@ -437,7 +440,7 @@ const styledNotification = styled('div', `
|
||||
`);
|
||||
function discardNotification(...args: IDomArgs<TagElem<"div">>) {
|
||||
return styledNotification(
|
||||
"Undo Discard",
|
||||
t("UndoDiscard"),
|
||||
testId("draft-notification"),
|
||||
...args
|
||||
);
|
||||
|
||||
@@ -24,6 +24,7 @@ import {ViewLayout} from 'app/client/components/ViewLayout';
|
||||
import {get as getBrowserGlobals} from 'app/client/lib/browserGlobals';
|
||||
import {DocPluginManager} from 'app/client/lib/DocPluginManager';
|
||||
import {ImportSourceElement} from 'app/client/lib/ImportSourceElement';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {createSessionObs} from 'app/client/lib/sessionObs';
|
||||
import {setTestState} from 'app/client/lib/testState';
|
||||
import {selectFiles} from 'app/client/lib/uploads';
|
||||
@@ -84,6 +85,8 @@ import * as ko from 'knockout';
|
||||
import cloneDeepWith = require('lodash/cloneDeepWith');
|
||||
import isEqual = require('lodash/isEqual');
|
||||
|
||||
const t = makeT('components.GristDoc');
|
||||
|
||||
const G = getBrowserGlobals('document', 'window');
|
||||
|
||||
// Re-export some tools to move them from main webpack bundle to the one with GristDoc.
|
||||
@@ -307,7 +310,7 @@ export class GristDoc extends DisposableWithEvents {
|
||||
const importSourceElems = ImportSourceElement.fromArray(this.docPluginManager.pluginsList);
|
||||
const importMenuItems = [
|
||||
{
|
||||
label: 'Import from file',
|
||||
label: t('ImportFromFile'),
|
||||
action: () => Importer.selectAndImport(this, importSourceElems, null, createPreview),
|
||||
},
|
||||
...importSourceElems.map(importSourceElem => ({
|
||||
@@ -592,7 +595,7 @@ export class GristDoc extends DisposableWithEvents {
|
||||
}
|
||||
}
|
||||
const res = await docData.bundleActions(
|
||||
`Added new linked section to view ${viewName}`,
|
||||
t("AddedNewLinkedSection", {viewName}),
|
||||
() => this.addWidgetToPageImpl(val, tableId ?? null)
|
||||
);
|
||||
|
||||
@@ -669,7 +672,7 @@ export class GristDoc extends DisposableWithEvents {
|
||||
}
|
||||
|
||||
return await this._viewLayout!.freezeUntil(docData.bundleActions(
|
||||
`Saved linked section ${section.title()} in view ${viewModel.name()}`,
|
||||
t("SavedLinkedSectionIn", {title:section.title(), name: viewModel.name()}),
|
||||
async () => {
|
||||
|
||||
// if table changes or a table is made a summary table, let's replace the view section by a
|
||||
|
||||
@@ -9,6 +9,7 @@ import {buildParseOptionsForm, ParseOptionValues} from 'app/client/components/Pa
|
||||
import {PluginScreen} from 'app/client/components/PluginScreen';
|
||||
import {FocusLayer} from 'app/client/lib/FocusLayer';
|
||||
import {ImportSourceElement} from 'app/client/lib/ImportSourceElement';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {fetchURL, isDriveUrl, selectFiles, uploadFiles} from 'app/client/lib/uploads';
|
||||
import {reportError} from 'app/client/models/AppModel';
|
||||
import {ColumnRec, ViewFieldRec, ViewSectionRec} from 'app/client/models/DocModel';
|
||||
@@ -35,6 +36,8 @@ import {labeledSquareCheckbox} from 'app/client/ui2018/checkbox';
|
||||
import {ACCESS_DENIED, AUTH_INTERRUPTED, canReadPrivateFiles, getGoogleCodeForReading} from 'app/client/ui/googleAuth';
|
||||
import debounce = require('lodash/debounce');
|
||||
|
||||
const t = makeT('components.Importer');
|
||||
|
||||
|
||||
// We expect a function for creating the preview GridView, to avoid the need to require the
|
||||
// GridView module here. That brings many dependencies, making a simple test fixture difficult.
|
||||
@@ -628,7 +631,7 @@ export class Importer extends DisposableWithEvents {
|
||||
cssMergeOptions(
|
||||
cssMergeOptionsToggle(labeledSquareCheckbox(
|
||||
updateExistingRecords,
|
||||
'Update existing records',
|
||||
t('UpdateExistingRecords'),
|
||||
dom.autoDispose(updateRecordsListener),
|
||||
testId('importer-update-existing-records')
|
||||
)),
|
||||
@@ -643,14 +646,14 @@ export class Importer extends DisposableWithEvents {
|
||||
|
||||
return [
|
||||
cssMergeOptionsMessage(
|
||||
'Merge rows that match these fields:',
|
||||
t('MergeRowsThatMatch'),
|
||||
testId('importer-merge-fields-message')
|
||||
),
|
||||
multiSelect(
|
||||
mergeCols,
|
||||
section.viewFields().peek().map(f => ({label: f.label(), value: f.colId()})) ?? [],
|
||||
{
|
||||
placeholder: 'Select fields to match on',
|
||||
placeholder: t("SelectFieldsToMatch"),
|
||||
error: hasInvalidMergeCols
|
||||
},
|
||||
dom.autoDispose(mergeColsListener),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { makeT } from 'app/client/lib/localization';
|
||||
import { bigBasicButton } from 'app/client/ui2018/buttons';
|
||||
import { testId } from 'app/client/ui2018/cssVars';
|
||||
import { loadingSpinner } from 'app/client/ui2018/loaders';
|
||||
@@ -6,6 +7,8 @@ import { PluginInstance } from 'app/common/PluginInstance';
|
||||
import { RenderTarget } from 'app/plugin/RenderOptions';
|
||||
import { Disposable, dom, DomContents, Observable, styled } from 'grainjs';
|
||||
|
||||
const t = makeT('components.PluginScreen');
|
||||
|
||||
/**
|
||||
* Rendering options for the PluginScreen modal.
|
||||
*/
|
||||
@@ -52,7 +55,7 @@ export class PluginScreen extends Disposable {
|
||||
public renderError(message: string) {
|
||||
this.render([
|
||||
this._buildModalTitle(),
|
||||
cssModalBody('Import failed: ', message, testId('importer-error')),
|
||||
cssModalBody(t('ImportFailed'), message, testId('importer-error')),
|
||||
cssModalButtons(
|
||||
bigBasicButton('Close',
|
||||
dom.on('click', () => this.close()),
|
||||
|
||||
@@ -32,6 +32,7 @@ var dispose = require('../lib/dispose');
|
||||
var dom = require('../lib/dom');
|
||||
var {Delay} = require('../lib/Delay');
|
||||
var kd = require('../lib/koDom');
|
||||
var {makeT} = require('../lib/localization');
|
||||
var Layout = require('./Layout');
|
||||
var RecordLayoutEditor = require('./RecordLayoutEditor');
|
||||
var commands = require('./commands');
|
||||
@@ -40,6 +41,8 @@ var {menu} = require('../ui2018/menus');
|
||||
var {testId} = require('app/client/ui2018/cssVars');
|
||||
var {contextMenu} = require('app/client/ui/contextMenu');
|
||||
|
||||
const t = makeT('components.RecordLayout');
|
||||
|
||||
/**
|
||||
* Construct a RecordLayout.
|
||||
* @param {MetaRowModel} options.viewSection: The model for the viewSection represented.
|
||||
@@ -260,7 +263,7 @@ RecordLayout.prototype.saveLayoutSpec = async function(layoutSpec) {
|
||||
// Use separate copies of addColAction, since sendTableActions modified each in-place.
|
||||
let addActions = gutil.arrayRepeat(addColNum, 0).map(() => addColAction.slice());
|
||||
|
||||
await docData.bundleActions('Updating record layout.', () => {
|
||||
await docData.bundleActions(t('UpdatingRecordLayout'), () => {
|
||||
return Promise.try(() => {
|
||||
return addColNum > 0 ? docModel.dataTables[tableId].sendTableActions(addActions) : [];
|
||||
})
|
||||
|
||||
@@ -2,8 +2,11 @@ var _ = require('underscore');
|
||||
var BackboneEvents = require('backbone').Events;
|
||||
|
||||
var dispose = require('app/client/lib/dispose');
|
||||
var {makeT} = require('app/client/lib/localization');
|
||||
var commands = require('./commands');
|
||||
var LayoutEditor = require('./LayoutEditor');
|
||||
|
||||
const t = makeT('components.RecordLayoutEditor');
|
||||
const {basicButton, cssButton, primaryButton} = require('app/client/ui2018/buttons');
|
||||
const {icon} = require('app/client/ui2018/icons');
|
||||
const {menu, menuDivider, menuItem} = require('app/client/ui2018/menus');
|
||||
@@ -90,13 +93,13 @@ RecordLayoutEditor.prototype.buildEditorDom = function() {
|
||||
};
|
||||
|
||||
return cssControls(
|
||||
basicButton('Add Field', cssCollapseIcon('Collapse'),
|
||||
basicButton(t('AddField'), cssCollapseIcon('Collapse'),
|
||||
menu((ctl) => [
|
||||
menuItem(() => addNewField(), 'Create New Field'),
|
||||
menuItem(() => addNewField(), t('CreateNewField')),
|
||||
dom.maybe((use) => use(this._hiddenColumns).length > 0,
|
||||
() => menuDivider()),
|
||||
dom.forEach(this._hiddenColumns, (col) =>
|
||||
menuItem(() => showField(col), `Show field ${col.label()}`)
|
||||
menuItem(() => showField(col), t("ShowField", {label:col.label()}))
|
||||
),
|
||||
testId('edit-layout-add-menu'),
|
||||
]),
|
||||
@@ -110,10 +113,10 @@ RecordLayoutEditor.prototype.buildEditorDom = function() {
|
||||
|
||||
RecordLayoutEditor.prototype.buildFinishButtons = function() {
|
||||
return [
|
||||
primaryButton('Save Layout',
|
||||
primaryButton(t('SaveLayout'),
|
||||
dom.on('click', () => commands.allCommands.accept.run()),
|
||||
),
|
||||
basicButton('Cancel',
|
||||
basicButton(t('Cancel'),
|
||||
dom.on('click', () => commands.allCommands.cancel.run()),
|
||||
{style: 'margin-left: 8px'},
|
||||
),
|
||||
|
||||
@@ -12,6 +12,9 @@ import * as gutil from 'app/common/gutil';
|
||||
import {Disposable, dom, fromKo, styled} from 'grainjs';
|
||||
import ko from 'knockout';
|
||||
import {menu, menuItem} from 'popweasel';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
|
||||
const t = makeT('components.RefSelect');
|
||||
|
||||
interface Item {
|
||||
label: string;
|
||||
@@ -44,8 +47,8 @@ export class RefSelect extends Disposable {
|
||||
// Indicates whether this is a ref col that references a different table.
|
||||
// (That's the only time when RefSelect is offered.)
|
||||
this.isForeignRefCol = this.autoDispose(ko.computed(() => {
|
||||
const t = this._origColumn.refTable();
|
||||
return Boolean(t && t.getRowId() !== this._origColumn.parentId());
|
||||
const table = this._origColumn.refTable();
|
||||
return Boolean(table && table.getRowId() !== this._origColumn.parentId());
|
||||
}));
|
||||
|
||||
// Computed for the current fieldBuilder's field, if it exists.
|
||||
@@ -94,7 +97,7 @@ export class RefSelect extends Disposable {
|
||||
testId('ref-select-item'),
|
||||
)
|
||||
),
|
||||
cssAddLink(cssAddIcon('Plus'), 'Add Column',
|
||||
cssAddLink(cssAddIcon('Plus'), t('AddColumn'),
|
||||
menu(() => [
|
||||
...this._validCols.peek()
|
||||
.filter((col) => !this._addedSet.peek().has(col.colId.peek()))
|
||||
@@ -102,7 +105,7 @@ export class RefSelect extends Disposable {
|
||||
menuItem(() => this._addFormulaField({ label: col.label(), value: col.colId() }),
|
||||
col.label.peek())
|
||||
),
|
||||
cssEmptyMenuText("No columns to add"),
|
||||
cssEmptyMenuText(t("NoColumnsAdd")),
|
||||
testId('ref-select-menu'),
|
||||
]),
|
||||
testId('ref-select-add'),
|
||||
|
||||
@@ -14,6 +14,9 @@ import {TableData} from "app/common/TableData";
|
||||
import {BaseFormatter} from 'app/common/ValueFormatter';
|
||||
import ko from 'knockout';
|
||||
import {Computed, Disposable, dom, makeTestId, Observable, styled, subscribe} from 'grainjs';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
|
||||
const t = makeT('components.SelectionSummary');
|
||||
|
||||
/**
|
||||
* A beginning and end index for a range of columns or rows.
|
||||
@@ -263,7 +266,7 @@ export class SelectionSummary extends Disposable {
|
||||
|
||||
async function doCopy(value: string, elem: Element) {
|
||||
await copyToClipboard(value);
|
||||
showTransientTooltip(elem, 'Copied to clipboard', {key: 'copy-selection-summary'});
|
||||
showTransientTooltip(elem, t('CopiedClipboard'), {key: 'copy-selection-summary'});
|
||||
}
|
||||
|
||||
const cssSummary = styled('div', `
|
||||
|
||||
@@ -17,6 +17,9 @@ import {FieldBuilder} from 'app/client/widgets/FieldBuilder';
|
||||
import {NewAbstractWidget} from 'app/client/widgets/NewAbstractWidget';
|
||||
import {UserAction} from 'app/common/DocActions';
|
||||
import {Computed, dom, fromKo, Observable} from 'grainjs';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
|
||||
const t = makeT('components.TypeTransformation');
|
||||
|
||||
// To simplify diff (avoid rearranging methods to satisfy private/public order).
|
||||
/* eslint-disable @typescript-eslint/member-ordering */
|
||||
@@ -61,25 +64,25 @@ export class TypeTransform extends ColumnTransform {
|
||||
),
|
||||
cssButtonRow(
|
||||
basicButton(dom.on('click', () => { this.cancel().catch(reportError); disableButtons.set(true); }),
|
||||
'Cancel', testId("type-transform-cancel"),
|
||||
t('Cancel'), testId("type-transform-cancel"),
|
||||
dom.cls('disabled', disableButtons)
|
||||
),
|
||||
dom.domComputed(this._reviseTypeChange, revising => {
|
||||
if (revising) {
|
||||
return basicButton(dom.on('click', () => this.editor.writeObservable()),
|
||||
'Preview', testId("type-transform-update"),
|
||||
t('Preview'), testId("type-transform-update"),
|
||||
dom.cls('disabled', (use) => use(disableButtons) || use(this.formulaUpToDate)),
|
||||
{ title: 'Update formula (Shift+Enter)' }
|
||||
{ title: t('UpdateFormula') }
|
||||
);
|
||||
} else {
|
||||
return basicButton(dom.on('click', () => { this._reviseTypeChange.set(true); }),
|
||||
'Revise', testId("type-transform-revise"),
|
||||
t('Revise'), testId("type-transform-revise"),
|
||||
dom.cls('disabled', disableButtons)
|
||||
);
|
||||
}
|
||||
}),
|
||||
primaryButton(dom.on('click', () => { this.execute().catch(reportError); disableButtons.set(true); }),
|
||||
'Apply', testId("type-transform-apply"),
|
||||
t('Apply'), testId("type-transform-apply"),
|
||||
dom.cls('disabled', disableButtons)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -5,6 +5,9 @@ var dom = require('../lib/dom');
|
||||
var kd = require('../lib/koDom');
|
||||
var kf = require('../lib/koForm');
|
||||
var AceEditor = require('./AceEditor');
|
||||
var {makeT} = require('app/client/lib/localization');
|
||||
|
||||
const t = makeT('components.ValidationPanel');
|
||||
|
||||
/**
|
||||
* Document level configuration settings.
|
||||
@@ -30,7 +33,7 @@ dispose.makeDisposable(ValidationPanel);
|
||||
ValidationPanel.prototype.onAddRule = function() {
|
||||
this.validationsTable.sendTableAction(["AddRecord", null, {
|
||||
tableRef: this.docTables.at(0).id(),
|
||||
name: "Rule " + (this.validations.peekLength + 1),
|
||||
name: t("RuleLength", {length: this.validations.peekLength + 1}),
|
||||
formula: ""
|
||||
}])
|
||||
.then(function() {
|
||||
@@ -83,7 +86,7 @@ ValidationPanel.prototype.buildDom = function() {
|
||||
2, '',
|
||||
1, kf.buttonGroup(
|
||||
kf.button(() => editor.writeObservable(),
|
||||
'Apply', { title: 'Update formula (Shift+Enter)' },
|
||||
'Apply', { title: t('UpdateFormula')},
|
||||
kd.toggleClass('disabled', editorUpToDate)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -24,9 +24,12 @@ const {confirmModal} = require('app/client/ui2018/modals');
|
||||
const {Sort} = require('app/common/SortSpec');
|
||||
const isEqual = require('lodash/isEqual');
|
||||
const {cssMenuItem} = require('popweasel');
|
||||
const {makeT} = require('app/client/lib/localization');
|
||||
|
||||
const testId = makeTestId('test-vconfigtab-');
|
||||
|
||||
const t = makeT('components.ViewConfigTab');
|
||||
|
||||
/**
|
||||
* Helper class that combines one ViewSection's data for building dom.
|
||||
*/
|
||||
@@ -132,21 +135,21 @@ ViewConfigTab.prototype.buildSortDom = function() {
|
||||
cssRow(
|
||||
cssExtraMarginTop.cls(''),
|
||||
grainjsDom.maybe(hasChanged, () => [
|
||||
primaryButton('Save', {style: 'margin-right: 8px;'},
|
||||
primaryButton(t('Save'), {style: 'margin-right: 8px;'},
|
||||
grainjsDom.on('click', () => { section.activeSortJson.save(); }),
|
||||
testId('sort-save'),
|
||||
grainjsDom.boolAttr('disabled', this.gristDoc.isReadonly),
|
||||
),
|
||||
// Let's use same label (revert) as the similar button which appear in the view section.
|
||||
// menu.
|
||||
basicButton('Revert',
|
||||
basicButton(t('Revert'),
|
||||
grainjsDom.on('click', () => { section.activeSortJson.revert(); }),
|
||||
testId('sort-reset')
|
||||
)
|
||||
]),
|
||||
cssFlex(),
|
||||
grainjsDom.maybe(section.isSorted, () =>
|
||||
basicButton('Update Data', {style: 'margin-left: 8px; white-space: nowrap;'},
|
||||
basicButton(t('UpdateData'), {style: 'margin-left: 8px; white-space: nowrap;'},
|
||||
grainjsDom.on('click', () => { updatePositions(this.gristDoc, section); }),
|
||||
testId('sort-update'),
|
||||
grainjsDom.show((use) => use(use(section.table).supportsManualSort)),
|
||||
@@ -200,9 +203,9 @@ ViewConfigTab.prototype._buildSortRow = function(colRef, sortSpec, columns) {
|
||||
});
|
||||
return {computed, allowedTypes, flag, label};
|
||||
}
|
||||
const orderByChoice = computedFlag('orderByChoice', ['Choice'], 'Use choice position');
|
||||
const naturalSort = computedFlag('naturalSort', ['Text'], 'Natural sort');
|
||||
const emptyLast = computedFlag('emptyLast', null, 'Empty values last');
|
||||
const orderByChoice = computedFlag('orderByChoice', ['Choice'], t('UseChoicePosition'));
|
||||
const naturalSort = computedFlag('naturalSort', ['Text'], t('NaturalSort'));
|
||||
const emptyLast = computedFlag('emptyLast', null, t('EmptyValuesLast'));
|
||||
const flags = [orderByChoice, emptyLast, naturalSort];
|
||||
|
||||
const column = columns.get().find(col => col.value === Sort.getColRef(colRef));
|
||||
@@ -278,7 +281,7 @@ ViewConfigTab.prototype._buildAddToSortBtn = function(columns) {
|
||||
grainjsDom.autoDispose(showAddNew),
|
||||
grainjsDom.autoDispose(available),
|
||||
cssTextBtn(
|
||||
cssPlusIcon('Plus'), 'Add Column',
|
||||
cssPlusIcon('Plus'), t('AddColumn'),
|
||||
testId('sort-add')
|
||||
),
|
||||
grainjsDom.hide((use) => use(showAddNew) || !use(available).length),
|
||||
@@ -304,7 +307,7 @@ ViewConfigTab.prototype._buildAddToSortBtn = function(columns) {
|
||||
return cssRow(cssSortRow(
|
||||
dom.autoDispose(col),
|
||||
cssSortSelect(
|
||||
select(col, [], {defaultLabel: 'Add Column'}),
|
||||
select(col, [], {defaultLabel: t('AddColumn')}),
|
||||
menu(() => [
|
||||
menuCols,
|
||||
grainjsDom.onDispose(() => { showAddNew.set(false); })
|
||||
@@ -372,9 +375,9 @@ ViewConfigTab.prototype._buildAdvancedSettingsDom = function() {
|
||||
const table = sectionData.section.table();
|
||||
const isCollapsed = ko.observable(true);
|
||||
return [
|
||||
kf.collapserLabel(isCollapsed, 'Advanced settings', dom.testId('ViewConfig_advanced')),
|
||||
kf.collapserLabel(isCollapsed, t('AdvancedSettings'), dom.testId('ViewConfig_advanced')),
|
||||
kf.helpRow(kd.hide(isCollapsed),
|
||||
'Big tables may be marked as "on-demand" to avoid loading them into the data engine.',
|
||||
t('BigTablesMayBeMarked'),
|
||||
kd.style('text-align', 'left'),
|
||||
kd.style('margin-top', '1.5rem')
|
||||
),
|
||||
@@ -383,7 +386,7 @@ ViewConfigTab.prototype._buildAdvancedSettingsDom = function() {
|
||||
),
|
||||
kf.row(kd.hide(isCollapsed),
|
||||
kf.buttonGroup(kf.button(() => this._makeOnDemand(table),
|
||||
kd.text(() => table.onDemand() ? 'Unmark On-Demand' : 'Make On-Demand'),
|
||||
kd.text(() => table.onDemand() ? t('UnmarkOnDemandButton') : t('MakeOnDemandButton')),
|
||||
dom.testId('ViewConfig_onDemandBtn')
|
||||
))
|
||||
),
|
||||
@@ -401,7 +404,7 @@ ViewConfigTab.prototype._buildFilterDom = function() {
|
||||
const hasChangedObs = Computed.create(null, (use) => use(section.filterSpecChanged) || !use(section.activeFilterBar.isSaved))
|
||||
|
||||
async function save() {
|
||||
await docModel.docData.bundleActions("Update Filter settings", () => Promise.all([
|
||||
await docModel.docData.bundleActions(t("UpdateFilterSettings"), () => Promise.all([
|
||||
section.saveFilters(), // Save filter
|
||||
section.activeFilterBar.save(), // Save bar
|
||||
]));
|
||||
@@ -438,7 +441,7 @@ ViewConfigTab.prototype._buildFilterDom = function() {
|
||||
grainjsDom.domComputed((use) => {
|
||||
const filters = use(section.filters);
|
||||
return cssTextBtn(
|
||||
cssPlusIcon('Plus'), 'Add Filter',
|
||||
cssPlusIcon('Plus'), t('AddFilter'),
|
||||
addFilterMenu(filters, section, popupControls, {placement: 'bottom-end'}),
|
||||
testId('add-filter-btn'),
|
||||
);
|
||||
@@ -462,12 +465,12 @@ ViewConfigTab.prototype._buildFilterDom = function() {
|
||||
cssExtraMarginTop.cls(''),
|
||||
testId('save-filter-btns'),
|
||||
primaryButton(
|
||||
'Save', {style: 'margin-right: 8px'},
|
||||
t('Save'), {style: 'margin-right: 8px'},
|
||||
grainjsDom.on('click', save),
|
||||
grainjsDom.boolAttr('disabled', this.gristDoc.isReadonly),
|
||||
),
|
||||
basicButton(
|
||||
'Revert',
|
||||
t('Revert'),
|
||||
grainjsDom.on('click', revert),
|
||||
)
|
||||
))
|
||||
@@ -484,9 +487,9 @@ ViewConfigTab.prototype._buildThemeDom = function() {
|
||||
return cssRow(
|
||||
dom.autoDispose(theme),
|
||||
select(theme, [
|
||||
{label: 'Form', value: 'form' },
|
||||
{label: 'Compact', value: 'compact'},
|
||||
{label: 'Blocks', value: 'blocks' },
|
||||
{label: t('Form'), value: 'form' },
|
||||
{label: t('Compact'), value: 'compact'},
|
||||
{label: t('Blocks'), value: 'blocks' },
|
||||
]),
|
||||
testId('detail-theme')
|
||||
);
|
||||
@@ -505,7 +508,7 @@ ViewConfigTab.prototype._buildLayoutDom = function() {
|
||||
const layoutEditorObs = ko.computed(() => view && view.recordLayout && view.recordLayout.layoutEditor());
|
||||
return cssRow({style: 'margin-top: 16px;'},
|
||||
kd.maybe(layoutEditorObs, (editor) => editor.buildFinishButtons()),
|
||||
primaryButton('Edit Card Layout',
|
||||
primaryButton(t('EditCardLayout'),
|
||||
dom.autoDispose(layoutEditorObs),
|
||||
dom.on('click', () => commands.allCommands.editLayout.run()),
|
||||
grainjsDom.hide(layoutEditorObs),
|
||||
@@ -553,8 +556,8 @@ ViewConfigTab.prototype._buildCustomTypeItems = function() {
|
||||
// 3)
|
||||
showObs: () => activeSection().customDef.mode() === "plugin",
|
||||
buildDom: () => kd.scope(activeSection, ({customDef}) => dom('div',
|
||||
kf.row(5, "Plugin: ", 13, kf.text(customDef.pluginId, {}, {list: "list_plugin"}, dom.testId('ViewConfigTab_customView_pluginId'))),
|
||||
kf.row(5, "Section: ", 13, kf.text(customDef.sectionId, {}, {list: "list_section"}, dom.testId('ViewConfigTab_customView_sectionId'))),
|
||||
kf.row(5, t("PluginColon"), 13, kf.text(customDef.pluginId, {}, {list: "list_plugin"}, dom.testId('ViewConfigTab_customView_pluginId'))),
|
||||
kf.row(5, t("SectionColon"), 13, kf.text(customDef.sectionId, {}, {list: "list_section"}, dom.testId('ViewConfigTab_customView_sectionId'))),
|
||||
// For both `customPlugin` and `selectedSection` it is possible for the value not to be in the
|
||||
// list of options. Combining <datalist> and <input> allows both to freely edit the value with
|
||||
// keyboard and to select it from a list. Although the content of the list seems to be
|
||||
|
||||
@@ -13,6 +13,9 @@ import flatten = require('lodash/flatten');
|
||||
import forEach = require('lodash/forEach');
|
||||
import zip = require('lodash/zip');
|
||||
import zipObject = require('lodash/zipObject');
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
|
||||
const t = makeT('components.duplicatePage');
|
||||
|
||||
// Duplicate page with pageId. Starts by prompting user for a new name.
|
||||
export async function duplicatePage(gristDoc: GristDoc, pageId: number) {
|
||||
@@ -27,8 +30,7 @@ export async function duplicatePage(gristDoc: GristDoc, pageId: number) {
|
||||
cssLabel("Name"),
|
||||
inputEl = cssInput({value: pageName + ' (copy)'}),
|
||||
),
|
||||
"Note that this does not copy data, ",
|
||||
"but creates another view of the same data.",
|
||||
t("DoesNotCopyData"),
|
||||
])
|
||||
));
|
||||
}
|
||||
@@ -39,7 +41,7 @@ async function makeDuplicate(gristDoc: GristDoc, pageId: number, pageName: strin
|
||||
const viewSections = sourceView.viewSections.peek().peek();
|
||||
let viewRef = 0;
|
||||
await gristDoc.docData.bundleActions(
|
||||
`Duplicate page ${pageName}`,
|
||||
t("DuplicatePageName", {pageName}),
|
||||
async () => {
|
||||
// create new view and new sections
|
||||
const results = await createNewViewSections(gristDoc.docData, viewSections);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {ACIndex, ACItem, ACResults, buildHighlightedDom, normalizeText} from "app/client/lib/ACIndex";
|
||||
import {cssSelectItem} from "app/client/lib/ACSelect";
|
||||
import {Autocomplete, IAutocompleteOptions} from "app/client/lib/autocomplete";
|
||||
@@ -17,6 +18,8 @@ import {createUserImage, cssUserImage} from "app/client/ui/UserImage";
|
||||
import {Computed, computed, dom, DomElementArg, Holder, IDisposableOwner, Observable, styled} from "grainjs";
|
||||
import {cssMenuItem} from "popweasel";
|
||||
|
||||
const t = makeT('lib.ACUserManager');
|
||||
|
||||
export interface ACUserItem extends ACItem {
|
||||
value: string;
|
||||
label: string;
|
||||
@@ -106,9 +109,10 @@ export function buildACMemberEmail(
|
||||
cssUserImagePlus.cls('-invalid', (use) => !use(enableAdd),
|
||||
)),
|
||||
cssMemberText(
|
||||
cssMemberPrimaryPlus("Invite new member"),
|
||||
cssMemberPrimaryPlus(t("InviteNewMember")),
|
||||
cssMemberSecondaryPlus(
|
||||
dom.text(use => `We'll email an invite to ${use(emailObs)}`)
|
||||
// dom.text(use => `We'll email an invite to ${use(emailObs)}`)
|
||||
dom.text(use => t('InviteEmail', {email: use(emailObs)})) // TODO i18next
|
||||
)
|
||||
),
|
||||
testId("um-add-email")
|
||||
@@ -139,7 +143,7 @@ export function buildACMemberEmail(
|
||||
(emailInput = cssEmailInput(
|
||||
emailObs,
|
||||
{onInput: true, isValid},
|
||||
{type: "email", placeholder: "Enter email address"},
|
||||
{type: "email", placeholder: t("EmailInputPlaceholder")},
|
||||
dom.on("input", acOpen),
|
||||
dom.on("focus", acOpen),
|
||||
dom.on("click", acOpen),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {get as getBrowserGlobals} from 'app/client/lib/browserGlobals';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {error} from 'app/client/lib/log';
|
||||
import {reportError, setErrorNotifier} from 'app/client/models/errors';
|
||||
import {urlState} from 'app/client/models/gristUrlState';
|
||||
@@ -22,6 +23,8 @@ import {getOrgName, Organization, OrgError, UserAPI, UserAPIImpl} from 'app/comm
|
||||
import {getUserPrefObs, getUserPrefsObs} from 'app/client/models/UserPrefs';
|
||||
import {bundleChanges, Computed, Disposable, Observable, subscribe} from 'grainjs';
|
||||
|
||||
const t = makeT('models.AppModel')
|
||||
|
||||
// Reexported for convenience.
|
||||
export {reportError} from 'app/client/models/errors';
|
||||
|
||||
@@ -192,7 +195,7 @@ export class TopAppModelImpl extends Disposable implements TopAppModel {
|
||||
if (org.billingAccount && org.billingAccount.product &&
|
||||
org.billingAccount.product.name === 'suspended') {
|
||||
this.notifier.createUserMessage(
|
||||
'This team site is suspended. Documents can be read, but not modified.',
|
||||
t('TeamSiteSuspended'),
|
||||
{actions: ['renew', 'personal']}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,9 +25,12 @@ import {canEdit, isOwner} from 'app/common/roles';
|
||||
import {Document, NEW_DOCUMENT_CODE, Organization, UserAPI, Workspace} from 'app/common/UserAPI';
|
||||
import {Holder, Observable, subscribe} from 'grainjs';
|
||||
import {Computed, Disposable, dom, DomArg, DomElementArg} from 'grainjs';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
|
||||
// tslint:disable:no-console
|
||||
|
||||
const t = makeT('models.DocPageModel');
|
||||
|
||||
export interface DocInfo extends Document {
|
||||
isReadonly: boolean;
|
||||
isPreFork: boolean;
|
||||
@@ -233,17 +236,13 @@ export class DocPageModelImpl extends Disposable implements DocPageModel {
|
||||
const isDenied = (err as any).code === 'ACL_DENY';
|
||||
const isDocOwner = isOwner(this.currentDoc.get());
|
||||
confirmModal(
|
||||
"Error accessing document",
|
||||
"Reload",
|
||||
t("ErrorAccessingDocument"),
|
||||
t("Reload"),
|
||||
async () => window.location.reload(true),
|
||||
isDocOwner ? `You can try reloading the document, or using recovery mode. ` +
|
||||
`Recovery mode opens the document to be fully accessible to owners, and ` +
|
||||
`inaccessible to others. It also disables formulas. ` +
|
||||
`[${err.message}]` :
|
||||
isDenied ? `Sorry, access to this document has been denied. [${err.message}]` :
|
||||
`Document owners can attempt to recover the document. [${err.message}]`,
|
||||
isDocOwner ? t('ReloadingOrRecoveryMode', {error: err.message}) :
|
||||
t('AccessError', {context: isDenied ? 'denied' : 'recover', error: err.message}),
|
||||
{ hideCancel: true,
|
||||
extraButtons: (isDocOwner && !isDenied) ? bigBasicButton('Enter recovery mode', dom.on('click', async () => {
|
||||
extraButtons: (isDocOwner && !isDenied) ? bigBasicButton(t('EnterRecoveryMode'), dom.on('click', async () => {
|
||||
await this._api.getDocAPI(this.currentDocId.get()!).recover(true);
|
||||
window.location.reload(true);
|
||||
}), testId('modal-recovery-mode')) : null,
|
||||
@@ -339,18 +338,18 @@ function addMenu(importSources: ImportSource[], gristDoc: GristDoc, isReadonly:
|
||||
menuItem(
|
||||
(elem) => openPageWidgetPicker(elem, gristDoc.docModel, (val) => gristDoc.addNewPage(val).catch(reportError),
|
||||
{isNewPage: true, buttonLabel: 'Add Page'}),
|
||||
menuIcon("Page"), "Add Page", testId('dp-add-new-page'),
|
||||
menuIcon("Page"), t("AddPage"), testId('dp-add-new-page'),
|
||||
dom.cls('disabled', isReadonly)
|
||||
),
|
||||
menuItem(
|
||||
(elem) => openPageWidgetPicker(elem, gristDoc.docModel, (val) => gristDoc.addWidgetToPage(val).catch(reportError),
|
||||
{isNewPage: false, selectBy}),
|
||||
menuIcon("Widget"), "Add Widget to Page", testId('dp-add-widget-to-page'),
|
||||
menuIcon("Widget"), t("AddWidgetToPage"), testId('dp-add-widget-to-page'),
|
||||
// disable for readonly doc and all special views
|
||||
dom.cls('disabled', (use) => typeof use(gristDoc.activeViewId) !== 'number' || isReadonly),
|
||||
),
|
||||
menuItem(() => gristDoc.addEmptyTable().catch(reportError),
|
||||
menuIcon("TypeTable"), "Add Empty Table", testId('dp-empty-table'),
|
||||
menuIcon("TypeTable"), t("AddEmptyTable"), testId('dp-empty-table'),
|
||||
dom.cls('disabled', isReadonly)
|
||||
),
|
||||
menuDivider(),
|
||||
@@ -362,7 +361,7 @@ function addMenu(importSources: ImportSource[], gristDoc: GristDoc, isReadonly:
|
||||
dom.cls('disabled', isReadonly)
|
||||
)
|
||||
),
|
||||
isReadonly ? menuText('You do not have edit access to this document') : null,
|
||||
isReadonly ? menuText(t('NoEditAccess')) : null,
|
||||
testId('dp-add-new-menu')
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {GristDoc} from 'app/client/components/GristDoc';
|
||||
import {AppModel} from 'app/client/models/AppModel';
|
||||
import {DocPageModel} from 'app/client/models/DocPageModel';
|
||||
@@ -10,6 +11,8 @@ import {ANONYMOUS_USER_EMAIL, Document, EVERYONE_EMAIL, FullUser, getRealAccess,
|
||||
import {computed, Computed, Disposable, obsArray, ObsArray, observable, Observable} from 'grainjs';
|
||||
import some = require('lodash/some');
|
||||
|
||||
const t = makeT('models.UserManagerModel');
|
||||
|
||||
export interface UserManagerModel {
|
||||
initData: PermissionData; // PermissionData used to initialize the UserManager
|
||||
resource: Resource|null; // The access resource.
|
||||
@@ -97,28 +100,28 @@ interface IBuildMemberOptions {
|
||||
export class UserManagerModelImpl extends Disposable implements UserManagerModel {
|
||||
// Select options for each individual user's role dropdown.
|
||||
public readonly userSelectOptions: IMemberSelectOption[] = [
|
||||
{ value: roles.OWNER, label: 'Owner' },
|
||||
{ value: roles.EDITOR, label: 'Editor' },
|
||||
{ value: roles.VIEWER, label: 'Viewer' }
|
||||
{ value: roles.OWNER, label: t('Owner') },
|
||||
{ value: roles.EDITOR, label: t('Editor') },
|
||||
{ value: roles.VIEWER, label: t('Viewer') }
|
||||
];
|
||||
// Select options for each individual user's role dropdown in the org.
|
||||
public readonly orgUserSelectOptions: IOrgMemberSelectOption[] = [
|
||||
{ value: roles.OWNER, label: 'Owner' },
|
||||
{ value: roles.EDITOR, label: 'Editor' },
|
||||
{ value: roles.VIEWER, label: 'Viewer' },
|
||||
{ value: roles.MEMBER, label: 'No Default Access' },
|
||||
{ value: roles.OWNER, label: t('Owner') },
|
||||
{ value: roles.EDITOR, label: t('Editor') },
|
||||
{ value: roles.VIEWER, label: t('Viewer') },
|
||||
{ value: roles.MEMBER, label: t('NoDefaultAccess') },
|
||||
];
|
||||
// Select options for the resource's maxInheritedRole dropdown.
|
||||
public readonly inheritSelectOptions: IMemberSelectOption[] = [
|
||||
{ value: roles.OWNER, label: 'In Full' },
|
||||
{ value: roles.EDITOR, label: 'View & Edit' },
|
||||
{ value: roles.VIEWER, label: 'View Only' },
|
||||
{ value: null, label: 'None' }
|
||||
{ value: roles.OWNER, label: t('InFull') },
|
||||
{ value: roles.EDITOR, label: t('ViewAndEdit') },
|
||||
{ value: roles.VIEWER, label: t('ViewOnly') },
|
||||
{ value: null, label: t('None') }
|
||||
];
|
||||
// Select options for the public member's role dropdown.
|
||||
public readonly publicUserSelectOptions: IMemberSelectOption[] = [
|
||||
{ value: roles.EDITOR, label: 'Editor' },
|
||||
{ value: roles.VIEWER, label: 'Viewer' },
|
||||
{ value: roles.EDITOR, label: t('Editor') },
|
||||
{ value: roles.VIEWER, label: t('Viewer') },
|
||||
];
|
||||
|
||||
public activeUser: FullUser|null = this._options.activeUser ?? null;
|
||||
|
||||
@@ -16,8 +16,10 @@ import {cssLink} from 'app/client/ui2018/links';
|
||||
import {getGristConfig} from 'app/common/urlUtils';
|
||||
import {FullUser} from 'app/common/UserAPI';
|
||||
import {Computed, Disposable, dom, domComputed, makeTestId, Observable, styled} from 'grainjs';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
|
||||
const testId = makeTestId('test-account-page-');
|
||||
const t = makeT('AccountPage');
|
||||
|
||||
/**
|
||||
* Creates the account page where a user can manage their profile settings.
|
||||
@@ -56,13 +58,13 @@ export class AccountPage extends Disposable {
|
||||
const {enableCustomCss} = getGristConfig();
|
||||
return domComputed(this._userObs, (user) => user && (
|
||||
css.container(css.accountPage(
|
||||
css.header('Account settings'),
|
||||
css.header(t('AccountSettings')),
|
||||
css.dataRow(
|
||||
css.inlineSubHeader('Email'),
|
||||
css.inlineSubHeader(t('Email')),
|
||||
css.email(user.email),
|
||||
),
|
||||
css.dataRow(
|
||||
css.inlineSubHeader('Name'),
|
||||
css.inlineSubHeader(t('Name')),
|
||||
domComputed(this._isEditingName, (isEditing) => (
|
||||
isEditing ? [
|
||||
transientInput(
|
||||
@@ -76,13 +78,13 @@ export class AccountPage extends Disposable {
|
||||
css.flexGrow.cls(''),
|
||||
),
|
||||
css.textBtn(
|
||||
css.icon('Settings'), 'Save',
|
||||
css.icon('Settings'), t('Save'),
|
||||
// No need to save on 'click'. The transient input already does it on close.
|
||||
),
|
||||
] : [
|
||||
css.name(user.name),
|
||||
css.textBtn(
|
||||
css.icon('Settings'), 'Edit',
|
||||
css.icon('Settings'), t('Edit'),
|
||||
dom.on('click', () => this._isEditingName.set(true)),
|
||||
),
|
||||
]
|
||||
@@ -91,11 +93,11 @@ export class AccountPage extends Disposable {
|
||||
),
|
||||
// show warning for invalid name but not for the empty string
|
||||
dom.maybe(use => use(this._nameEdit) && !use(this._isNameValid), cssWarnings),
|
||||
css.header('Password & Security'),
|
||||
css.header(t('PasswordSecurity')),
|
||||
css.dataRow(
|
||||
css.inlineSubHeader('Login Method'),
|
||||
css.inlineSubHeader(t("LoginMethod")),
|
||||
css.loginMethod(user.loginMethod),
|
||||
user.loginMethod === 'Email + Password' ? css.textBtn('Change Password',
|
||||
user.loginMethod === 'Email + Password' ? css.textBtn(t("ChangePassword"),
|
||||
dom.on('click', () => this._showChangePasswordDialog()),
|
||||
) : null,
|
||||
testId('login-method'),
|
||||
@@ -104,26 +106,24 @@ export class AccountPage extends Disposable {
|
||||
css.dataRow(
|
||||
labeledSquareCheckbox(
|
||||
this._allowGoogleLogin,
|
||||
'Allow signing in to this account with Google',
|
||||
t('AllowGoogleSigning'),
|
||||
testId('allow-google-login-checkbox'),
|
||||
),
|
||||
testId('allow-google-login'),
|
||||
),
|
||||
css.subHeader('Two-factor authentication'),
|
||||
css.subHeader(t('TwoFactorAuth')),
|
||||
css.description(
|
||||
"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, even if someone " +
|
||||
"knows your password."
|
||||
t("TwoFactorAuthDescription")
|
||||
),
|
||||
dom.create(MFAConfig, user),
|
||||
),
|
||||
// Custom CSS is incompatible with custom themes.
|
||||
enableCustomCss ? null : [
|
||||
css.header('Theme'),
|
||||
css.header(t('Theme')),
|
||||
dom.create(ThemeConfig, this._appModel),
|
||||
],
|
||||
css.header('API'),
|
||||
css.dataRow(css.inlineSubHeader('API Key'), css.content(
|
||||
css.header(t('API')),
|
||||
css.dataRow(css.inlineSubHeader(t('APIKey')), css.content(
|
||||
dom.create(ApiKey, {
|
||||
apiKey: this._apiKey,
|
||||
onCreate: () => this._createApiKey(),
|
||||
@@ -214,7 +214,7 @@ export function checkName(name: string): boolean {
|
||||
*/
|
||||
function buildNameWarningsDom() {
|
||||
return css.warning(
|
||||
"Names only allow letters, numbers and certain special characters",
|
||||
t("WarningUsername"),
|
||||
testId('username-warning'),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ import * as roles from 'app/common/roles';
|
||||
import {Disposable, dom, DomElementArg, styled} from 'grainjs';
|
||||
import {cssMenuItem} from 'popweasel';
|
||||
import {maybeAddSiteSwitcherSection} from 'app/client/ui/SiteSwitcher';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
|
||||
const t = makeT('AccountWidget');
|
||||
|
||||
/**
|
||||
* Render the user-icon that opens the account menu. When no user is logged in, render a Sign-in
|
||||
@@ -33,7 +36,7 @@ export class AccountWidget extends Disposable {
|
||||
cssUserIcon(createUserImage(user, 'medium', testId('user-icon')),
|
||||
menu(() => this._makeAccountMenu(user), {placement: 'bottom-end'}),
|
||||
) :
|
||||
cssSignInButton('Sign in', icon('Collapse'), testId('user-signin'),
|
||||
cssSignInButton(t('SignIn'), icon('Collapse'), testId('user-signin'),
|
||||
menu(() => this._makeAccountMenu(user), {placement: 'bottom-end'}),
|
||||
)
|
||||
)
|
||||
@@ -54,24 +57,24 @@ export class AccountWidget extends Disposable {
|
||||
// The 'Document Settings' item, when there is an open document.
|
||||
const documentSettingsItem = (gristDoc ?
|
||||
menuItem(async () => (await loadGristDoc()).showDocSettingsModal(gristDoc.docInfo, this._docPageModel!),
|
||||
'Document Settings',
|
||||
t('DocumentSettings'),
|
||||
testId('dm-doc-settings')) :
|
||||
null);
|
||||
|
||||
// The item to toggle mobile mode (presence of viewport meta tag).
|
||||
const mobileModeToggle = menuItem(viewport.toggleViewport,
|
||||
cssSmallDeviceOnly.cls(''), // Only show this toggle on small devices.
|
||||
'Toggle Mobile Mode',
|
||||
t('ToggleMobileMode'),
|
||||
cssCheckmark('Tick', dom.show(viewport.viewportEnabled)),
|
||||
testId('usermenu-toggle-mobile'),
|
||||
);
|
||||
|
||||
if (!user) {
|
||||
return [
|
||||
menuItemLink({href: getLoginOrSignupUrl()}, 'Sign in'),
|
||||
menuItemLink({href: getLoginOrSignupUrl()}, t('SignIn')),
|
||||
menuDivider(),
|
||||
documentSettingsItem,
|
||||
menuItemLink({href: commonUrls.plans}, 'Pricing'),
|
||||
menuItemLink({href: commonUrls.plans}, t('Pricing')),
|
||||
mobileModeToggle,
|
||||
];
|
||||
}
|
||||
@@ -85,14 +88,14 @@ export class AccountWidget extends Disposable {
|
||||
cssEmail(user.email, testId('usermenu-email'))
|
||||
)
|
||||
),
|
||||
menuItemLink(urlState().setLinkUrl({account: 'account'}), 'Profile Settings'),
|
||||
menuItemLink(urlState().setLinkUrl({account: 'account'}), t('ProfileSettings')),
|
||||
|
||||
documentSettingsItem,
|
||||
|
||||
// Show 'Organization Settings' when on a home page of a valid org.
|
||||
(!this._docPageModel && currentOrg && this._appModel.isTeamSite ?
|
||||
menuItem(() => manageTeamUsers(currentOrg, user, this._appModel.api),
|
||||
roles.canEditAccess(currentOrg.access) ? 'Manage Team' : 'Access Details',
|
||||
roles.canEditAccess(currentOrg.access) ? t('ManageTeam') : t('AccessDetails'),
|
||||
testId('dm-org-access')) :
|
||||
// Don't show on doc pages, or for personal orgs.
|
||||
null),
|
||||
@@ -108,7 +111,7 @@ export class AccountWidget extends Disposable {
|
||||
// org-listing UI below.
|
||||
this._appModel.topAppModel.isSingleOrg || shouldHideUiElement("multiAccounts") ? [] : [
|
||||
menuDivider(),
|
||||
menuSubHeader(dom.text((use) => use(users).length > 1 ? 'Switch Accounts' : 'Accounts')),
|
||||
menuSubHeader(dom.text((use) => use(users).length > 1 ? t('SwitchAccounts') : t('Accounts'))),
|
||||
dom.forEach(users, (_user) => {
|
||||
if (_user.id === user.id) { return null; }
|
||||
return menuItem(() => this._switchAccount(_user),
|
||||
@@ -116,10 +119,10 @@ export class AccountWidget extends Disposable {
|
||||
cssOtherEmail(_user.email, testId('usermenu-other-email')),
|
||||
);
|
||||
}),
|
||||
isExternal ? null : menuItemLink({href: getLoginUrl()}, "Add Account", testId('dm-add-account')),
|
||||
isExternal ? null : menuItemLink({href: getLoginUrl()}, t("AddAccount"), testId('dm-add-account')),
|
||||
],
|
||||
|
||||
menuItemLink({href: getLogoutUrl()}, "Sign Out", testId('dm-log-out')),
|
||||
menuItemLink({href: getLogoutUrl()}, t("SignOut"), testId('dm-log-out')),
|
||||
|
||||
maybeAddSiteSwitcherSection(this._appModel),
|
||||
];
|
||||
|
||||
@@ -3,14 +3,14 @@ import {makeT} from 'app/client/lib/localization';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
import {dom, DomElementArg, Observable, styled} from "grainjs";
|
||||
|
||||
const translate = makeT(`AddNewButton`);
|
||||
const t = makeT(`AddNewButton`);
|
||||
|
||||
export function addNewButton(isOpen: Observable<boolean> | boolean = true, ...args: DomElementArg[]) {
|
||||
return cssAddNewButton(
|
||||
cssAddNewButton.cls('-open', isOpen),
|
||||
// Setting spacing as flex items allows them to shrink faster when there isn't enough space.
|
||||
cssLeftMargin(),
|
||||
cssAddText(translate('AddNew')),
|
||||
cssAddText(t('AddNew')),
|
||||
dom('div', {style: 'flex: 1 1 16px'}),
|
||||
cssPlusButton(cssPlusIcon('Plus')),
|
||||
dom('div', {style: 'flex: 0 1 16px'}),
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { makeT } from 'app/client/lib/localization';
|
||||
import { basicButton, textButton } from 'app/client/ui2018/buttons';
|
||||
import { theme, vars } from 'app/client/ui2018/cssVars';
|
||||
import { icon } from 'app/client/ui2018/icons';
|
||||
import { confirmModal } from 'app/client/ui2018/modals';
|
||||
import { Disposable, dom, IDomArgs, makeTestId, Observable, observable, styled } from 'grainjs';
|
||||
|
||||
const t = makeT('ApiKey');
|
||||
|
||||
interface IWidgetOptions {
|
||||
apiKey: Observable<string>;
|
||||
onDelete: () => Promise<void>;
|
||||
@@ -52,7 +55,7 @@ export class ApiKey extends Disposable {
|
||||
},
|
||||
dom.attr('type', (use) => use(this._isHidden) ? 'password' : 'text'),
|
||||
testId('key'),
|
||||
{title: 'Click to show'},
|
||||
{title: t('ClickToShow')},
|
||||
dom.on('click', (_ev, el) => {
|
||||
this._isHidden.set(false);
|
||||
setTimeout(() => el.select(), 0);
|
||||
@@ -64,7 +67,7 @@ export class ApiKey extends Disposable {
|
||||
this._inputArgs
|
||||
),
|
||||
cssTextBtn(
|
||||
cssTextBtnIcon('Remove'), 'Remove',
|
||||
cssTextBtnIcon('Remove'), t('Remove'),
|
||||
dom.on('click', () => this._showRemoveKeyModal()),
|
||||
testId('delete'),
|
||||
dom.boolAttr('disabled', (use) => use(this._loading) || this._anonymous)
|
||||
@@ -73,10 +76,9 @@ export class ApiKey extends Disposable {
|
||||
description(this._getDescription(), testId('description')),
|
||||
)),
|
||||
dom.maybe((use) => !(use(this._apiKey) || this._anonymous), () => [
|
||||
basicButton('Create', dom.on('click', () => this._onCreate()), testId('create'),
|
||||
basicButton(t('Create'), dom.on('click', () => this._onCreate()), testId('create'),
|
||||
dom.boolAttr('disabled', this._loading)),
|
||||
description('By generating an API key, you will be able to make API calls '
|
||||
+ 'for your own account.', testId('description')),
|
||||
description(t('ByGenerating'), testId('description')),
|
||||
]),
|
||||
);
|
||||
}
|
||||
@@ -101,20 +103,16 @@ export class ApiKey extends Disposable {
|
||||
}
|
||||
|
||||
private _getDescription(): string {
|
||||
if (!this._anonymous) {
|
||||
return 'This API key can be used to access your account via the API. '
|
||||
+ 'Don’t share your API key with anyone.';
|
||||
} else {
|
||||
return 'This API key can be used to access this account anonymously via the API.';
|
||||
}
|
||||
return t(
|
||||
!this._anonymous ? 'OwnAPIKey' : 'AnonymousAPIkey'
|
||||
);
|
||||
}
|
||||
|
||||
private _showRemoveKeyModal(): void {
|
||||
confirmModal(
|
||||
`Remove API Key`, 'Remove',
|
||||
t('RemoveAPIKey'), t('Remove'),
|
||||
() => this._onDelete(),
|
||||
`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?`
|
||||
t("AboutToDeleteAPIKey")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,9 @@ import {fetchFromHome} from 'app/common/urlUtils';
|
||||
import {ISupportedFeatures} from 'app/common/UserConfig';
|
||||
import {dom} from 'grainjs';
|
||||
import * as ko from 'knockout';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
|
||||
const t = makeT('App');
|
||||
|
||||
// tslint:disable:no-console
|
||||
|
||||
@@ -90,8 +93,8 @@ export class App extends DisposableWithEvents {
|
||||
dom('table.g-help-table',
|
||||
dom('thead',
|
||||
dom('tr',
|
||||
dom('th', 'Key'),
|
||||
dom('th', 'Description')
|
||||
dom('th', t('Key')),
|
||||
dom('th', t('Description'))
|
||||
)
|
||||
),
|
||||
dom.forEach(commandList.groups, (group: any) => {
|
||||
@@ -231,7 +234,7 @@ export class App extends DisposableWithEvents {
|
||||
if (message.match(/MemoryError|unmarshallable object/)) {
|
||||
if (err.message.length > 30) {
|
||||
// TLDR
|
||||
err.message = 'Memory Error';
|
||||
err.message = t('MemoryError');
|
||||
}
|
||||
this._mostRecentDocPageModel?.offerRecovery(err);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ import * as roles from 'app/common/roles';
|
||||
import {manageTeamUsersApp} from 'app/client/ui/OpenUserManager';
|
||||
import {maybeAddSiteSwitcherSection} from 'app/client/ui/SiteSwitcher';
|
||||
import {BindableValue, Disposable, dom, DomContents, styled} from 'grainjs';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
|
||||
const t = makeT('AppHeader');
|
||||
|
||||
// Maps a name of a Product (from app/gen-server/entity/Product.ts) to a tag (pill) to show next
|
||||
// to the org name.
|
||||
@@ -54,11 +57,11 @@ export class AppHeader extends Disposable {
|
||||
this._orgName && cssDropdownIcon('Dropdown'),
|
||||
menu(() => [
|
||||
menuSubHeader(
|
||||
`${this._appModel.isTeamSite ? 'Team' : 'Personal'} Site`
|
||||
+ (this._appModel.isLegacySite ? ' (Legacy)' : ''),
|
||||
this._appModel.isTeamSite ? t('TeamSite') : t('PersonalSite')
|
||||
+ (this._appModel.isLegacySite ? ` (${t('Legacy')})` : ''),
|
||||
testId('orgmenu-title'),
|
||||
),
|
||||
menuItemLink(urlState().setLinkUrl({}), 'Home Page', testId('orgmenu-home-page')),
|
||||
menuItemLink(urlState().setLinkUrl({}), t('HomePage'), testId('orgmenu-home-page')),
|
||||
|
||||
// Show 'Organization Settings' when on a home page of a valid org.
|
||||
(!this._docPageModel && currentOrg && !currentOrg.owner ?
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { allCommands } from 'app/client/components/commands';
|
||||
import { makeT } from 'app/client/lib/localization';
|
||||
import { menuDivider, menuItemCmd } from 'app/client/ui2018/menus';
|
||||
import { IMultiColumnContextMenu } from 'app/client/ui/GridViewMenus';
|
||||
import { IRowContextMenu } from 'app/client/ui/RowContextMenu';
|
||||
import { COMMENTS } from 'app/client/models/features';
|
||||
import { dom } from 'grainjs';
|
||||
|
||||
const t = makeT('CellContextMenu');
|
||||
|
||||
export function CellContextMenu(rowOptions: IRowContextMenu, colOptions: IMultiColumnContextMenu) {
|
||||
|
||||
const { disableInsert, disableDelete, isViewSorted } = rowOptions;
|
||||
@@ -16,15 +19,13 @@ export function CellContextMenu(rowOptions: IRowContextMenu, colOptions: IMultiC
|
||||
const disableForReadonlyView = dom.cls('disabled', isReadonly);
|
||||
|
||||
const numCols: number = colOptions.numColumns;
|
||||
const nameClearColumns = colOptions.isFiltered ?
|
||||
(numCols > 1 ? `Clear ${numCols} entire columns` : 'Clear entire column') :
|
||||
(numCols > 1 ? `Clear ${numCols} columns` : 'Clear column');
|
||||
const nameDeleteColumns = numCols > 1 ? `Delete ${numCols} columns` : 'Delete column';
|
||||
const nameClearColumns = colOptions.isFiltered ? t("ClearEntireColumns", {count: numCols}) : t("ClearColumns", {count: numCols})
|
||||
const nameDeleteColumns = t("DeleteColumns", {count: numCols})
|
||||
|
||||
const numRows: number = rowOptions.numRows;
|
||||
const nameDeleteRows = numRows > 1 ? `Delete ${numRows} rows` : 'Delete row';
|
||||
const nameDeleteRows = t("DeleteRows", {count: numRows})
|
||||
|
||||
const nameClearCells = (numRows > 1 || numCols > 1) ? 'Clear values' : 'Clear cell';
|
||||
const nameClearCells = (numRows > 1 || numCols > 1) ? t('ClearValues') : t('ClearCell');
|
||||
|
||||
const result: Array<Element|null> = [];
|
||||
|
||||
@@ -40,12 +41,12 @@ export function CellContextMenu(rowOptions: IRowContextMenu, colOptions: IMultiC
|
||||
...(
|
||||
(numCols > 1 || numRows > 1) ? [] : [
|
||||
menuDivider(),
|
||||
menuItemCmd(allCommands.copyLink, 'Copy anchor link'),
|
||||
menuItemCmd(allCommands.copyLink, t('CopyAnchorLink')),
|
||||
menuDivider(),
|
||||
menuItemCmd(allCommands.filterByThisCellValue, `Filter by this value`),
|
||||
menuItemCmd(allCommands.filterByThisCellValue, t("FilterByValue")),
|
||||
menuItemCmd(allCommands.openDiscussion, 'Comment', dom.cls('disabled', (
|
||||
isReadonly || numRows === 0 || numCols === 0
|
||||
)), dom.hide(use => !use(COMMENTS())))
|
||||
)), dom.hide(use => !use(COMMENTS()))) //TODO: i18next
|
||||
]
|
||||
),
|
||||
|
||||
@@ -57,19 +58,19 @@ export function CellContextMenu(rowOptions: IRowContextMenu, colOptions: IMultiC
|
||||
// When the view is sorted, any newly added records get shifts instantly at the top or
|
||||
// bottom. It could be very confusing for users who might expect the record to stay above or
|
||||
// below the active row. Thus in this case we show a single `insert row` command.
|
||||
[menuItemCmd(allCommands.insertRecordAfter, 'Insert row',
|
||||
[menuItemCmd(allCommands.insertRecordAfter, t("InsertRow"),
|
||||
dom.cls('disabled', disableInsert))] :
|
||||
|
||||
[menuItemCmd(allCommands.insertRecordBefore, 'Insert row above',
|
||||
[menuItemCmd(allCommands.insertRecordBefore, t("InsertRowAbove"),
|
||||
dom.cls('disabled', disableInsert)),
|
||||
menuItemCmd(allCommands.insertRecordAfter, 'Insert row below',
|
||||
menuItemCmd(allCommands.insertRecordAfter, t("InsertRowBelow"),
|
||||
dom.cls('disabled', disableInsert))]
|
||||
),
|
||||
menuItemCmd(allCommands.duplicateRows, `Duplicate ${numRows === 1 ? 'row' : 'rows'}`,
|
||||
menuItemCmd(allCommands.duplicateRows, t("DuplicateRows", {count: numRows}),
|
||||
dom.cls('disabled', disableInsert || numRows === 0)),
|
||||
menuItemCmd(allCommands.insertFieldBefore, 'Insert column to the left',
|
||||
menuItemCmd(allCommands.insertFieldBefore, t("InsertColumnLeft"),
|
||||
disableForReadonlyView),
|
||||
menuItemCmd(allCommands.insertFieldAfter, 'Insert column to the right',
|
||||
menuItemCmd(allCommands.insertFieldAfter, t("InsertColumnRight"),
|
||||
disableForReadonlyView),
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* callback that's triggered on Apply or on Cancel. Changes to the UI result in changes to the underlying model,
|
||||
* but on Cancel the model is reset to its initial state prior to menu closing.
|
||||
*/
|
||||
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {allInclusive, ColumnFilter} from 'app/client/models/ColumnFilter';
|
||||
import {ColumnFilterMenuModel, IFilterCount} from 'app/client/models/ColumnFilterMenuModel';
|
||||
import {ColumnRec, ViewFieldRec, ViewSectionRec} from 'app/client/models/DocModel';
|
||||
@@ -35,6 +35,8 @@ import {isDateLikeType, isList, isNumberType, isRefListType} from 'app/common/gr
|
||||
import {choiceToken} from 'app/client/widgets/ChoiceToken';
|
||||
import {ChoiceOptions} from 'app/client/widgets/ChoiceTextBox';
|
||||
|
||||
const t = makeT('ColumnFilterMenu');
|
||||
|
||||
export interface IFilterMenuOptions {
|
||||
model: ColumnFilterMenuModel;
|
||||
valueCounts: Map<CellValue, IFilterCount>;
|
||||
@@ -90,7 +92,7 @@ export function columnFilterMenu(owner: IDisposableOwner, opts: IFilterMenuOptio
|
||||
|
||||
// Filter by range
|
||||
dom.maybe(showRangeFilter, () => [
|
||||
cssRangeHeader('Filter by Range'),
|
||||
cssRangeHeader(t('FilterByRange')),
|
||||
cssRangeContainer(
|
||||
minRangeInput = rangeInput('Min ', columnFilter.min, rangeInputOptions, testId('min')),
|
||||
cssRangeInputSeparator('→'),
|
||||
@@ -104,7 +106,7 @@ export function columnFilterMenu(owner: IDisposableOwner, opts: IFilterMenuOptio
|
||||
searchInput = cssSearch(
|
||||
searchValueObs, { onInput: true },
|
||||
testId('search-input'),
|
||||
{ type: 'search', placeholder: 'Search values' },
|
||||
{ type: 'search', placeholder: t('SearchValues') },
|
||||
dom.onKeyDown({
|
||||
Enter: () => {
|
||||
if (searchValueObs.get()) {
|
||||
@@ -140,14 +142,14 @@ export function columnFilterMenu(owner: IDisposableOwner, opts: IFilterMenuOptio
|
||||
const state = use(columnFilter.state);
|
||||
return [
|
||||
cssSelectAll(
|
||||
dom.text(searchValue ? 'All Shown' : 'All'),
|
||||
dom.text(searchValue ? t('AllShown') : t('All')),
|
||||
cssSelectAll.cls('-disabled', isEquivalentFilter(state, allSpec)),
|
||||
dom.on('click', () => columnFilter.setState(allSpec)),
|
||||
testId('bulk-action'),
|
||||
),
|
||||
cssDotSeparator('•'),
|
||||
cssSelectAll(
|
||||
searchValue ? 'All Except' : 'None',
|
||||
searchValue ? t('AllExcept') : t('None'),
|
||||
cssSelectAll.cls('-disabled', isEquivalentFilter(state, noneSpec)),
|
||||
dom.on('click', () => columnFilter.setState(noneSpec)),
|
||||
testId('bulk-action'),
|
||||
@@ -162,7 +164,7 @@ export function columnFilterMenu(owner: IDisposableOwner, opts: IFilterMenuOptio
|
||||
),
|
||||
cssItemList(
|
||||
testId('list'),
|
||||
dom.maybe(use => use(filteredValues).length === 0, () => cssNoResults('No matching values')),
|
||||
dom.maybe(use => use(filteredValues).length === 0, () => cssNoResults(t('NoMatchingValues'))),
|
||||
dom.domComputed(filteredValues, (values) => values.slice(0, model.limitShown).map(([key, value]) => (
|
||||
cssMenuItem(
|
||||
cssLabel(
|
||||
@@ -189,17 +191,17 @@ export function columnFilterMenu(owner: IDisposableOwner, opts: IFilterMenuOptio
|
||||
const valuesBeyondLimit = use(model.valuesBeyondLimit);
|
||||
if (isAboveLimit) {
|
||||
return searchValue ? [
|
||||
buildSummary('Other Matching', valuesBeyondLimit, false, model),
|
||||
buildSummary('Other Non-Matching', otherValues, true, model),
|
||||
buildSummary(t('OtherMatching'), valuesBeyondLimit, false, model),
|
||||
buildSummary(t('OtherNonMatching'), otherValues, true, model),
|
||||
] : [
|
||||
buildSummary('Other Values', concat(otherValues, valuesBeyondLimit), false, model),
|
||||
buildSummary('Future Values', [], true, model),
|
||||
buildSummary(t('OtherValues'), concat(otherValues, valuesBeyondLimit), false, model),
|
||||
buildSummary(t('FutureValues'), [], true, model),
|
||||
];
|
||||
} else {
|
||||
return anyOtherValues ? [
|
||||
buildSummary('Others', otherValues, true, model)
|
||||
buildSummary(t('Others'), otherValues, true, model)
|
||||
] : [
|
||||
buildSummary('Future Values', [], true, model)
|
||||
buildSummary(t('FutureValues'), [], true, model)
|
||||
];
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -19,6 +19,9 @@ import {GristLoadConfig} from 'app/common/gristUrls';
|
||||
import {nativeCompare, unwrap} from 'app/common/gutil';
|
||||
import {bundleChanges, Computed, Disposable, dom, fromKo, makeTestId,
|
||||
MultiHolder, Observable, styled, UseCBOwner} from 'grainjs';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
|
||||
const t = makeT('CustomSectionConfig');
|
||||
|
||||
// Custom URL widget id - used as mock id for selectbox.
|
||||
const CUSTOM_ID = 'custom';
|
||||
@@ -58,7 +61,7 @@ class ColumnPicker extends Disposable {
|
||||
return [
|
||||
cssLabel(
|
||||
this._column.title,
|
||||
this._column.optional ? cssSubLabel(" (optional)") : null,
|
||||
this._column.optional ? cssSubLabel(t('Optional')) : null,
|
||||
testId('label-for-' + this._column.name),
|
||||
),
|
||||
this._column.description ? cssHelp(
|
||||
@@ -70,7 +73,7 @@ class ColumnPicker extends Disposable {
|
||||
properValue,
|
||||
options,
|
||||
{
|
||||
defaultLabel: this._column.typeDesc != "any" ? `Pick a ${this._column.typeDesc} column` : 'Pick a column'
|
||||
defaultLabel: this._column.typeDesc != "any" ? t('PickAColumnWithType', {"columnType": this._column.typeDesc}) : t('PickAColumn')
|
||||
}
|
||||
),
|
||||
testId('mapping-for-' + this._column.name),
|
||||
@@ -102,7 +105,7 @@ class ColumnListPicker extends Disposable {
|
||||
return [
|
||||
cssRow(
|
||||
cssAddMapping(
|
||||
cssAddIcon('Plus'), 'Add ' + this._column.title,
|
||||
cssAddIcon('Plus'), t('Add') + ' ' + this._column.title,
|
||||
menu(() => {
|
||||
const otherColumns = this._getNotMappedColumns();
|
||||
const typedColumns = otherColumns.filter(this._typeFilter());
|
||||
@@ -114,7 +117,7 @@ class ColumnListPicker extends Disposable {
|
||||
col.label.peek(),
|
||||
)),
|
||||
wrongTypeCount > 0 ? menuText(
|
||||
`${wrongTypeCount} non-${this._column.type.toLowerCase()} column${wrongTypeCount > 1 ? 's are' : ' is'} not shown`,
|
||||
t("WrongTypesMenuText", {wrongTypeCount, columnType: this._column.type.toLowerCase(), count: wrongTypeCount}),
|
||||
testId('map-message-' + this._column.name)
|
||||
) : null
|
||||
];
|
||||
@@ -367,17 +370,17 @@ export class CustomSectionConfig extends Disposable {
|
||||
return null;
|
||||
}
|
||||
switch(level) {
|
||||
case AccessLevel.none: return cssConfirmLine("Widget does not require any permissions.");
|
||||
case AccessLevel.read_table: return cssConfirmLine("Widget needs to ", dom("b", "read"), " the current table.");
|
||||
case AccessLevel.full: return cssConfirmLine("Widget needs ", dom("b", "full access"), " to this document.");
|
||||
case AccessLevel.none: return cssConfirmLine(t("WidgetNoPermissison"));
|
||||
case AccessLevel.read_table: return cssConfirmLine(t("WidgetNeedRead", {read: dom("b", "read")})); // TODO i18next
|
||||
case AccessLevel.full: return cssConfirmLine(t("WidgetNeedFullAccess", {fullAccess: dom("b", "full access")})); // TODO i18next
|
||||
default: throw new Error(`Unsupported ${level} access level`);
|
||||
}
|
||||
}
|
||||
// Options for access level.
|
||||
const levels: IOptionFull<string>[] = [
|
||||
{label: 'No document access', value: AccessLevel.none},
|
||||
{label: 'Read selected table', value: AccessLevel.read_table},
|
||||
{label: 'Full document access', value: AccessLevel.full},
|
||||
{label: t('NoDocumentAccess'), value: AccessLevel.none},
|
||||
{label: t('ReadSelectedTable'), value: AccessLevel.read_table},
|
||||
{label: t('FullDocumentAccess'), value: AccessLevel.full},
|
||||
];
|
||||
return dom(
|
||||
'div',
|
||||
@@ -385,7 +388,7 @@ export class CustomSectionConfig extends Disposable {
|
||||
this._canSelect
|
||||
? cssRow(
|
||||
select(this._selectedId, options, {
|
||||
defaultLabel: 'Select Custom Widget',
|
||||
defaultLabel: t('SelectCustomWidget'),
|
||||
menuCssClass: cssMenu.className,
|
||||
}),
|
||||
testId('select')
|
||||
@@ -396,7 +399,7 @@ export class CustomSectionConfig extends Disposable {
|
||||
cssTextInput(
|
||||
this._url,
|
||||
async value => this._url.set(value),
|
||||
dom.attr('placeholder', 'Enter Custom URL'),
|
||||
dom.attr('placeholder', t('EnterCustomURL')),
|
||||
testId('url')
|
||||
)
|
||||
),
|
||||
@@ -437,7 +440,7 @@ export class CustomSectionConfig extends Disposable {
|
||||
dom.maybe(this._hasConfiguration, () =>
|
||||
cssSection(
|
||||
textButton(
|
||||
'Open configuration',
|
||||
t('OpenConfiguration'),
|
||||
dom.on('click', () => this._openConfiguration()),
|
||||
testId('open-configuration')
|
||||
)
|
||||
@@ -447,7 +450,7 @@ export class CustomSectionConfig extends Disposable {
|
||||
cssLink(
|
||||
dom.attr('href', 'https://support.getgrist.com/widget-custom'),
|
||||
dom.attr('target', '_blank'),
|
||||
'Learn more about custom widgets'
|
||||
t('LearnMore')
|
||||
)
|
||||
),
|
||||
dom.maybeOwned(use => use(this._section.columnsToMap), (owner, columns) => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {createSessionObs} from 'app/client/lib/sessionObs';
|
||||
import {DocPageModel} from 'app/client/models/DocPageModel';
|
||||
import {reportError} from 'app/client/models/errors';
|
||||
@@ -14,6 +15,8 @@ import {DocSnapshot} from 'app/common/UserAPI';
|
||||
import {Disposable, dom, IDomComponent, MultiHolder, Observable, styled} from 'grainjs';
|
||||
import moment from 'moment';
|
||||
|
||||
const t = makeT('DocHistory');
|
||||
|
||||
const DocHistorySubTab = StringUnion("activity", "snapshots");
|
||||
|
||||
export class DocHistory extends Disposable implements IDomComponent {
|
||||
@@ -25,8 +28,8 @@ export class DocHistory extends Disposable implements IDomComponent {
|
||||
|
||||
public buildDom() {
|
||||
const tabs = [
|
||||
{value: 'activity', label: 'Activity'},
|
||||
{value: 'snapshots', label: 'Snapshots'},
|
||||
{value: 'activity', label: t('Activity')},
|
||||
{value: 'snapshots', label: t('Snapshots')},
|
||||
];
|
||||
return [
|
||||
cssSubTabs(
|
||||
@@ -87,11 +90,11 @@ export class DocHistory extends Disposable implements IDomComponent {
|
||||
),
|
||||
cssMenuDots(icon('Dots'),
|
||||
menu(() => [
|
||||
menuItemLink(setLink(snapshot), 'Open Snapshot'),
|
||||
menuItemLink(setLink(snapshot, origUrlId), 'Compare to Current',
|
||||
menuAnnotate('Beta')),
|
||||
prevSnapshot && menuItemLink(setLink(prevSnapshot, snapshot.docId), 'Compare to Previous',
|
||||
menuAnnotate('Beta')),
|
||||
menuItemLink(setLink(snapshot), t('OpenSnapshot')),
|
||||
menuItemLink(setLink(snapshot, origUrlId), t('CompareToCurrent'),
|
||||
menuAnnotate(t('Beta'))),
|
||||
prevSnapshot && menuItemLink(setLink(prevSnapshot, snapshot.docId), t('CompareToPrevious'),
|
||||
menuAnnotate(t('Beta'))),
|
||||
],
|
||||
{placement: 'bottom-end', parentSelectorToMark: '.' + cssSnapshotCard.className}
|
||||
),
|
||||
|
||||
@@ -34,7 +34,7 @@ import {localStorageBoolObs} from 'app/client/lib/localStorageObs';
|
||||
import {bigBasicButton} from 'app/client/ui2018/buttons';
|
||||
import sortBy = require('lodash/sortBy');
|
||||
|
||||
const translate = makeT(`DocMenu`);
|
||||
const t = makeT(`DocMenu`);
|
||||
|
||||
const testId = makeTestId('test-dm-');
|
||||
|
||||
@@ -60,8 +60,8 @@ function createLoadedDocMenu(owner: IDisposableOwner, home: HomeModel) {
|
||||
showWelcomeQuestions(home.app.userPrefsObs),
|
||||
css.docMenu(
|
||||
dom.maybe(!home.app.currentFeatures.workspaces, () => [
|
||||
css.docListHeader('This service is not available right now'),
|
||||
dom('span', '(The organization needs a paid plan)')
|
||||
css.docListHeader(t('ServiceNotAvailable')),
|
||||
dom('span', t('NeedPaidPlan')),
|
||||
]),
|
||||
|
||||
// currentWS and showIntro observables change together. We capture both in one domComputed call.
|
||||
@@ -87,7 +87,7 @@ function createLoadedDocMenu(owner: IDisposableOwner, home: HomeModel) {
|
||||
// TODO: this is shown on all pages, but there is a hack in currentWSPinnedDocs that
|
||||
// removes all pinned docs when on trash page.
|
||||
dom.maybe((use) => use(home.currentWSPinnedDocs).length > 0, () => [
|
||||
css.docListHeader(css.pinnedDocsIcon('PinBig'), 'Pinned Documents'),
|
||||
css.docListHeader(css.pinnedDocsIcon('PinBig'), t('PinnedDocuments')),
|
||||
createPinnedDocs(home, home.currentWSPinnedDocs),
|
||||
]),
|
||||
|
||||
@@ -95,7 +95,7 @@ function createLoadedDocMenu(owner: IDisposableOwner, home: HomeModel) {
|
||||
dom.maybe((use) => page === 'templates' && use(home.featuredTemplates).length > 0, () => [
|
||||
css.featuredTemplatesHeader(
|
||||
css.featuredTemplatesIcon('Idea'),
|
||||
'Featured',
|
||||
t('Featured'),
|
||||
testId('featured-templates-header')
|
||||
),
|
||||
createPinnedDocs(home, home.featuredTemplates, true),
|
||||
@@ -107,12 +107,12 @@ function createLoadedDocMenu(owner: IDisposableOwner, home: HomeModel) {
|
||||
null :
|
||||
css.docListHeader(
|
||||
(
|
||||
page === 'all' ? translate('AllDocuments') :
|
||||
page === 'all' ? t('AllDocuments') :
|
||||
page === 'templates' ?
|
||||
dom.domComputed(use => use(home.featuredTemplates).length > 0, (hasFeaturedTemplates) =>
|
||||
hasFeaturedTemplates ? translate('MoreExamplesAndTemplates') : translate('ExamplesAndTemplates')
|
||||
hasFeaturedTemplates ? t('MoreExamplesAndTemplates') : t('ExamplesAndTemplates')
|
||||
) :
|
||||
page === 'trash' ? 'Trash' :
|
||||
page === 'trash' ? t('Trash') :
|
||||
workspace && [css.docHeaderIcon('Folder'), workspaceName(home.app, workspace)]
|
||||
),
|
||||
testId('doc-header'),
|
||||
@@ -127,9 +127,9 @@ function createLoadedDocMenu(owner: IDisposableOwner, home: HomeModel) {
|
||||
) :
|
||||
(page === 'trash') ?
|
||||
dom('div',
|
||||
css.docBlock('Documents stay in Trash for 30 days, after which they get deleted permanently.'),
|
||||
css.docBlock(t('DocStayInTrash')),
|
||||
dom.maybe((use) => use(home.trashWorkspaces).length === 0, () =>
|
||||
css.docBlock('Trash is empty.')
|
||||
css.docBlock(t("EmptyTrash"))
|
||||
),
|
||||
buildAllDocsBlock(home, home.trashWorkspaces, false, flashDocId, viewSettings),
|
||||
) :
|
||||
@@ -144,7 +144,7 @@ function createLoadedDocMenu(owner: IDisposableOwner, home: HomeModel) {
|
||||
) :
|
||||
workspace && !workspace.isSupportWorkspace && workspace.docs?.length === 0 ?
|
||||
buildWorkspaceIntro(home) :
|
||||
css.docBlock('Workspace not found')
|
||||
css.docBlock(t('WorkspaceNotFound'))
|
||||
)
|
||||
]),
|
||||
];
|
||||
@@ -176,7 +176,7 @@ function buildAllDocsBlock(
|
||||
|
||||
(ws.removedAt ?
|
||||
[
|
||||
css.docRowUpdatedAt(`Deleted ${getTimeFromNow(ws.removedAt)}`),
|
||||
css.docRowUpdatedAt(t('Deleted', {at:getTimeFromNow(ws.removedAt)})),
|
||||
css.docMenuTrigger(icon('Dots')),
|
||||
menu(() => makeRemovedWsOptionsMenu(home, ws),
|
||||
{placement: 'bottom-end', parentSelectorToMark: '.' + css.docRowWrapper.className}),
|
||||
@@ -210,7 +210,7 @@ function buildAllDocsTemplates(home: HomeModel, viewSettings: ViewSettings) {
|
||||
dom.autoDispose(hideTemplatesObs),
|
||||
css.templatesHeaderWrap(
|
||||
css.templatesHeader(
|
||||
'Examples & Templates',
|
||||
t('Examples&Templates'),
|
||||
dom.domComputed(hideTemplatesObs, (collapsed) =>
|
||||
collapsed ? css.templatesHeaderIcon('Expand') : css.templatesHeaderIcon('Collapse')
|
||||
),
|
||||
@@ -222,7 +222,7 @@ function buildAllDocsTemplates(home: HomeModel, viewSettings: ViewSettings) {
|
||||
dom.maybe((use) => !use(hideTemplatesObs), () => [
|
||||
buildTemplateDocs(home, templates, viewSettings),
|
||||
bigBasicButton(
|
||||
'Discover More Templates',
|
||||
t('DiscoverMoreTemplates'),
|
||||
urlState().setLinkUrl({homePage: 'templates'}),
|
||||
testId('all-docs-templates-discover-more'),
|
||||
)
|
||||
@@ -270,7 +270,7 @@ function buildOtherSites(home: HomeModel) {
|
||||
return css.otherSitesBlock(
|
||||
dom.autoDispose(hideOtherSitesObs),
|
||||
css.otherSitesHeader(
|
||||
translate('OtherSites'),
|
||||
t('OtherSites'),
|
||||
dom.domComputed(hideOtherSitesObs, (collapsed) =>
|
||||
collapsed ? css.otherSitesHeaderIcon('Expand') : css.otherSitesHeaderIcon('Collapse')
|
||||
),
|
||||
@@ -282,7 +282,7 @@ function buildOtherSites(home: HomeModel) {
|
||||
const siteName = home.app.currentOrgName;
|
||||
return [
|
||||
dom('div',
|
||||
translate('OtherSitesWelcome', { siteName, context: personal ? 'personal' : '' }),
|
||||
t('OtherSitesWelcome', { siteName, context: personal ? 'personal' : '' }),
|
||||
testId('other-sites-message')
|
||||
),
|
||||
css.otherSitesButtons(
|
||||
@@ -318,8 +318,8 @@ function buildPrefs(
|
||||
// The Sort selector.
|
||||
options.hideSort ? null : dom.update(
|
||||
select<SortPref>(viewSettings.currentSort, [
|
||||
{value: 'name', label: 'By Name'},
|
||||
{value: 'date', label: 'By Date Modified'},
|
||||
{value: 'name', label: t('ByName')},
|
||||
{value: 'date', label: t('ByDateModified')},
|
||||
],
|
||||
{ buttonCssClass: css.sortSelector.className },
|
||||
),
|
||||
@@ -375,8 +375,8 @@ function buildWorkspaceDocBlock(home: HomeModel, workspace: Workspace, flashDocI
|
||||
),
|
||||
css.docRowUpdatedAt(
|
||||
(doc.removedAt ?
|
||||
`Deleted ${getTimeFromNow(doc.removedAt)}` :
|
||||
`Edited ${getTimeFromNow(doc.updatedAt)}`),
|
||||
t('Deleted', {at: getTimeFromNow(doc.removedAt)}) :
|
||||
t('Edited', {at: getTimeFromNow(doc.updatedAt)})),
|
||||
testId('doc-time')
|
||||
),
|
||||
(doc.removedAt ?
|
||||
@@ -410,7 +410,7 @@ function buildWorkspaceDocBlock(home: HomeModel, workspace: Workspace, flashDocI
|
||||
save: (val) => doRename(home, doc, val, flashDocId),
|
||||
close: () => renaming.set(null),
|
||||
}, testId('doc-name-editor')),
|
||||
css.docRowUpdatedAt(`Edited ${getTimeFromNow(doc.updatedAt)}`, testId('doc-time')),
|
||||
css.docRowUpdatedAt(t('Edited', {at: getTimeFromNow(doc.updatedAt)}), testId('doc-time')),
|
||||
),
|
||||
),
|
||||
testId('doc')
|
||||
@@ -451,9 +451,9 @@ export function makeDocOptionsMenu(home: HomeModel, doc: Document, renaming: Obs
|
||||
const orgAccess: roles.Role|null = org ? org.access : null;
|
||||
|
||||
function deleteDoc() {
|
||||
confirmModal(`Delete "${doc.name}"?`, 'Delete',
|
||||
confirmModal(t('DeleteDoc', {name: doc.name}), t('Delete'),
|
||||
() => home.deleteDoc(doc.id, false).catch(reportError),
|
||||
'Document will be moved to Trash.');
|
||||
t('DocumentMoveToTrash'));
|
||||
}
|
||||
|
||||
async function manageUsers() {
|
||||
@@ -472,11 +472,11 @@ export function makeDocOptionsMenu(home: HomeModel, doc: Document, renaming: Obs
|
||||
}
|
||||
|
||||
return [
|
||||
menuItem(() => renaming.set(doc), "Rename",
|
||||
menuItem(() => renaming.set(doc), t("Rename"),
|
||||
dom.cls('disabled', !roles.canEdit(doc.access)),
|
||||
testId('rename-doc')
|
||||
),
|
||||
menuItem(() => showMoveDocModal(home, doc), 'Move',
|
||||
menuItem(() => showMoveDocModal(home, doc), t('Move'),
|
||||
// Note that moving the doc requires ACL access on the doc. Moving a doc to a workspace
|
||||
// that confers descendant ACL access could otherwise increase the user's access to the doc.
|
||||
// By requiring the user to have ACL edit access on the doc to move it prevents using this
|
||||
@@ -487,16 +487,16 @@ export function makeDocOptionsMenu(home: HomeModel, doc: Document, renaming: Obs
|
||||
dom.cls('disabled', !roles.canEditAccess(doc.access)),
|
||||
testId('move-doc')
|
||||
),
|
||||
menuItem(deleteDoc, 'Remove',
|
||||
menuItem(deleteDoc, t('Remove'),
|
||||
dom.cls('disabled', !roles.canDelete(doc.access)),
|
||||
testId('delete-doc')
|
||||
),
|
||||
menuItem(() => home.pinUnpinDoc(doc.id, !doc.isPinned).catch(reportError),
|
||||
doc.isPinned ? "Unpin Document" : "Pin Document",
|
||||
doc.isPinned ? t("UnpinDocument"): t("PinDocument"),
|
||||
dom.cls('disabled', !roles.canEdit(orgAccess)),
|
||||
testId('pin-doc')
|
||||
),
|
||||
menuItem(manageUsers, roles.canEditAccess(doc.access) ? "Manage Users" : "Access Details",
|
||||
menuItem(manageUsers, roles.canEditAccess(doc.access) ? t("ManageUsers"): t("AccessDetails"),
|
||||
testId('doc-access')
|
||||
)
|
||||
];
|
||||
@@ -504,22 +504,22 @@ export function makeDocOptionsMenu(home: HomeModel, doc: Document, renaming: Obs
|
||||
|
||||
export function makeRemovedDocOptionsMenu(home: HomeModel, doc: Document, workspace: Workspace) {
|
||||
function hardDeleteDoc() {
|
||||
confirmModal(`Permanently Delete "${doc.name}"?`, 'Delete Forever',
|
||||
confirmModal(t("DeleteForeverDoc", {name: doc.name}), t("DeleteForever"),
|
||||
() => home.deleteDoc(doc.id, true).catch(reportError),
|
||||
'Document will be permanently deleted.');
|
||||
t('DeleteDocPerma'));
|
||||
}
|
||||
|
||||
return [
|
||||
menuItem(() => home.restoreDoc(doc), 'Restore',
|
||||
menuItem(() => home.restoreDoc(doc), t('Restore'),
|
||||
dom.cls('disabled', !roles.canDelete(doc.access) || !!workspace.removedAt),
|
||||
testId('doc-restore')
|
||||
),
|
||||
menuItem(hardDeleteDoc, 'Delete Forever',
|
||||
menuItem(hardDeleteDoc, t('DeleteForever'),
|
||||
dom.cls('disabled', !roles.canDelete(doc.access)),
|
||||
testId('doc-delete-forever')
|
||||
),
|
||||
(workspace.removedAt ?
|
||||
menuText('To restore this document, restore the workspace first.') :
|
||||
menuText(t('RestoreThisDocument')) :
|
||||
null
|
||||
)
|
||||
];
|
||||
@@ -527,16 +527,16 @@ export function makeRemovedDocOptionsMenu(home: HomeModel, doc: Document, worksp
|
||||
|
||||
function makeRemovedWsOptionsMenu(home: HomeModel, ws: Workspace) {
|
||||
return [
|
||||
menuItem(() => home.restoreWorkspace(ws), 'Restore',
|
||||
menuItem(() => home.restoreWorkspace(ws), t('Restore'),
|
||||
dom.cls('disabled', !roles.canDelete(ws.access)),
|
||||
testId('ws-restore')
|
||||
),
|
||||
menuItem(() => home.deleteWorkspace(ws.id, true), 'Delete Forever',
|
||||
menuItem(() => home.deleteWorkspace(ws.id, true), t('DeleteForever'),
|
||||
dom.cls('disabled', !roles.canDelete(ws.access) || ws.docs.length > 0),
|
||||
testId('ws-delete-forever')
|
||||
),
|
||||
(ws.docs.length > 0 ?
|
||||
menuText('You may delete a workspace forever once it has no documents in it.') :
|
||||
menuText(t('DeleteWorkspaceForever')) :
|
||||
null
|
||||
)
|
||||
];
|
||||
@@ -554,8 +554,8 @@ function showMoveDocModal(home: HomeModel, doc: Document) {
|
||||
const disabled = isCurrent || !isEditable;
|
||||
return css.moveDocListItem(
|
||||
css.moveDocListText(workspaceName(home.app, ws)),
|
||||
isCurrent ? css.moveDocListHintText('Current workspace') : null,
|
||||
!isEditable ? css.moveDocListHintText('Requires edit permissions') : null,
|
||||
isCurrent ? css.moveDocListHintText(t('CurrentWorkspace')) : null,
|
||||
!isEditable ? css.moveDocListHintText(t('RequiresEditPermissions')) : null,
|
||||
css.moveDocListItem.cls('-disabled', disabled),
|
||||
css.moveDocListItem.cls('-selected', (use) => use(selected) === ws.id),
|
||||
dom.on('click', () => disabled || selected.set(ws.id)),
|
||||
@@ -565,11 +565,11 @@ function showMoveDocModal(home: HomeModel, doc: Document) {
|
||||
)
|
||||
);
|
||||
return {
|
||||
title: `Move ${doc.name} to workspace`,
|
||||
title: t('MoveDocToWorkspace', {name: doc.name}),
|
||||
body,
|
||||
saveDisabled: Computed.create(owner, (use) => !use(selected)),
|
||||
saveFunc: async () => !selected.get() || home.moveDoc(doc.id, selected.get()!).catch(reportError),
|
||||
saveLabel: 'Move'
|
||||
saveLabel: t('Move'),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {Placement} from '@popperjs/core';
|
||||
import {placements} from '@popperjs/core/lib/enums';
|
||||
import {DocComm} from 'app/client/components/DocComm';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {sameDocumentUrlState} from 'app/client/models/gristUrlState';
|
||||
import {cssButtons, cssLinkBtn, cssLinkIcon} from 'app/client/ui/ExampleCard';
|
||||
import {IOnBoardingMsg, startOnBoarding} from 'app/client/ui/OnBoardingPopups';
|
||||
@@ -10,6 +11,7 @@ import {DocData} from 'app/common/DocData';
|
||||
import {dom} from 'grainjs';
|
||||
import sortBy = require('lodash/sortBy');
|
||||
|
||||
const t = makeT('DocTour');
|
||||
|
||||
export async function startDocTour(docData: DocData, docComm: DocComm, onFinishCB: () => void) {
|
||||
const docTour: IOnBoardingMsg[] = await makeDocTour(docData, docComm) || invalidDocTour;
|
||||
@@ -18,9 +20,8 @@ export async function startDocTour(docData: DocData, docComm: DocComm, onFinishC
|
||||
}
|
||||
|
||||
const invalidDocTour: IOnBoardingMsg[] = [{
|
||||
title: 'No valid document tour',
|
||||
body: '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.',
|
||||
title: t('InvalidDocTourTitle'),
|
||||
body: t('InvalidDocTourBody'),
|
||||
selector: 'document',
|
||||
showHasModal: true,
|
||||
}];
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* This module export a component for editing some document settings consisting of the timezone,
|
||||
* (new settings to be added here ...).
|
||||
*/
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {dom, IDisposableOwner, styled} from 'grainjs';
|
||||
import {Computed, Observable} from 'grainjs';
|
||||
|
||||
@@ -20,6 +21,9 @@ import {EngineCode} from 'app/common/DocumentSettings';
|
||||
import {GristLoadConfig} from 'app/common/gristUrls';
|
||||
import {propertyCompare} from "app/common/gutil";
|
||||
import {getCurrency, locales} from "app/common/Locales";
|
||||
|
||||
const t = makeT('DocumentSettings');
|
||||
|
||||
/**
|
||||
* Builds a simple saveModal for saving settings.
|
||||
*/
|
||||
@@ -38,37 +42,36 @@ export async function showDocSettingsModal(docInfo: DocInfoRec, docPageModel: Do
|
||||
const canChangeEngine = getSupportedEngineChoices().length > 0;
|
||||
|
||||
return {
|
||||
title: 'Document Settings',
|
||||
title: t('DocumentSettings'),
|
||||
body: [
|
||||
cssDataRow("This document's ID (for API use):"),
|
||||
cssDataRow(t('ThisDocumentID')),
|
||||
cssDataRow(dom('tt', docPageModel.currentDocId.get())),
|
||||
cssDataRow('Time Zone:'),
|
||||
cssDataRow(t('TimeZone')),
|
||||
cssDataRow(dom.create(buildTZAutocomplete, moment, timezoneObs, (val) => timezoneObs.set(val))),
|
||||
cssDataRow('Locale:'),
|
||||
cssDataRow(t('Locale')),
|
||||
cssDataRow(dom.create(buildLocaleSelect, localeObs)),
|
||||
cssDataRow('Currency:'),
|
||||
cssDataRow(t('Currency')),
|
||||
cssDataRow(dom.domComputed(localeObs, (l) =>
|
||||
dom.create(buildCurrencyPicker, currencyObs, (val) => currencyObs.set(val),
|
||||
{defaultCurrencyLabel: `Local currency (${getCurrency(l)})`})
|
||||
{defaultCurrencyLabel: t('LocalCurrency', {currency: getCurrency(l)})})
|
||||
)),
|
||||
canChangeEngine ? [
|
||||
// Small easter egg: you can click on the skull-and-crossbones to
|
||||
// force a reload of the document.
|
||||
cssDataRow('Engine (experimental ',
|
||||
dom('span',
|
||||
'☠',
|
||||
dom.style('cursor', 'pointer'),
|
||||
dom.on('click', async () => {
|
||||
await docPageModel.appModel.api.getDocAPI(docPageModel.currentDocId.get()!).forceReload();
|
||||
document.location.reload();
|
||||
})),
|
||||
' change at own risk):'),
|
||||
cssDataRow(t('EngineRisk', {span:
|
||||
dom('span', '☠',
|
||||
dom.style('cursor', 'pointer'),
|
||||
dom.on('click', async () => {
|
||||
await docPageModel.appModel.api.getDocAPI(docPageModel.currentDocId.get()!).forceReload();
|
||||
document.location.reload();
|
||||
}))
|
||||
})),
|
||||
select(engineObs, getSupportedEngineChoices()),
|
||||
] : null,
|
||||
],
|
||||
// Modal label is "Save", unless engine is changed. If engine is changed, the document will
|
||||
// need a reload to switch engines, so we replace the label with "Save and Reload".
|
||||
saveLabel: dom.text((use) => (use(engineObs) === docSettings.engine) ? 'Save' : 'Save and Reload'),
|
||||
saveLabel: dom.text((use) => (use(engineObs) === docSettings.engine) ? t('Save') : t('SaveAndReload')),
|
||||
saveFunc: async () => {
|
||||
await docInfo.updateColValues({
|
||||
timezone: timezoneObs.get(),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {GristDoc} from 'app/client/components/GristDoc';
|
||||
import {cssInput} from 'app/client/ui/cssInput';
|
||||
import {cssField} from 'app/client/ui/MakeCopyMenu';
|
||||
@@ -9,6 +10,8 @@ import {saveModal} from 'app/client/ui2018/modals';
|
||||
import {commonUrls} from 'app/common/gristUrls';
|
||||
import {Computed, Disposable, dom, input, makeTestId, Observable, styled} from 'grainjs';
|
||||
|
||||
const t = makeT('DuplicateTable');
|
||||
|
||||
const testId = makeTestId('test-duplicate-table-');
|
||||
|
||||
/**
|
||||
@@ -71,7 +74,7 @@ class DuplicateTableModal extends Disposable {
|
||||
input(
|
||||
this._newTableName,
|
||||
{onInput: true},
|
||||
{placeholder: 'Name for new table'},
|
||||
{placeholder: t('NewName')},
|
||||
(elem) => { setTimeout(() => { elem.focus(); }, 20); },
|
||||
dom.on('focus', (_ev, elem) => { elem.select(); }),
|
||||
dom.cls(cssInput.className),
|
||||
@@ -80,21 +83,21 @@ class DuplicateTableModal extends Disposable {
|
||||
),
|
||||
cssWarning(
|
||||
cssWarningIcon('Warning'),
|
||||
|
||||
dom('div',
|
||||
"Instead of duplicating tables, it's usually better to segment data using linked views. ",
|
||||
cssLink({href: commonUrls.helpLinkingWidgets, target: '_blank'}, 'Read More.')
|
||||
),
|
||||
t("AdviceWithLink", {link: cssLink({href: commonUrls.helpLinkingWidgets, target: '_blank'}, 'Read More.')})
|
||||
), //TODO: i18next
|
||||
),
|
||||
cssField(
|
||||
cssCheckbox(
|
||||
this._includeData,
|
||||
'Copy all data in addition to the table structure.',
|
||||
t('CopyAllData'),
|
||||
testId('copy-all-data'),
|
||||
),
|
||||
),
|
||||
dom.maybe(this._includeData, () => cssWarning(
|
||||
cssWarningIcon('Warning'),
|
||||
dom('div', 'Only the document default access rules will apply to the copy.'),
|
||||
dom('div', t('WarningACL')),
|
||||
testId('acl-warning'),
|
||||
)),
|
||||
];
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {makeT} from 'app/client/lib/localization'
|
||||
|
||||
const t = makeT('ExampleInfo');
|
||||
|
||||
export interface IExampleInfo {
|
||||
id: number;
|
||||
urlId: string;
|
||||
@@ -13,40 +17,37 @@ interface WelcomeCard {
|
||||
tutorialName: string;
|
||||
}
|
||||
|
||||
export const examples: IExampleInfo[] = [{
|
||||
export const buildExamples = (): IExampleInfo[] => [{
|
||||
id: 1, // Identifies the example in UserPrefs.seenExamples
|
||||
urlId: 'lightweight-crm',
|
||||
title: 'Lightweight CRM',
|
||||
title: t('Title', {context: "CRM"}),
|
||||
imgUrl: 'https://www.getgrist.com/themes/grist/assets/images/use-cases/lightweight-crm.png',
|
||||
tutorialUrl: 'https://support.getgrist.com/lightweight-crm/',
|
||||
welcomeCard: {
|
||||
title: 'Welcome to the Lightweight CRM template',
|
||||
text: 'Check out our related tutorial for how to link data, and create ' +
|
||||
'high-productivity layouts.',
|
||||
tutorialName: 'Tutorial: Create a CRM',
|
||||
title: t('WelcomeTitle', {context: "CRM"}),
|
||||
text: t('WelcomeText', {context: "CRM"}),
|
||||
tutorialName: t('WelcomeTutorialName', {context: "CRM"}),
|
||||
},
|
||||
}, {
|
||||
id: 2, // Identifies the example in UserPrefs.seenExamples
|
||||
urlId: 'investment-research',
|
||||
title: 'Investment Research',
|
||||
title: t('Title', {context: "investmentResearch"}),
|
||||
imgUrl: 'https://www.getgrist.com/themes/grist/assets/images/use-cases/data-visualization.png',
|
||||
tutorialUrl: 'https://support.getgrist.com/investment-research/',
|
||||
welcomeCard: {
|
||||
title: 'Welcome to the Investment Research template',
|
||||
text: 'Check out our related tutorial to learn how to create summary tables and charts, ' +
|
||||
'and to link charts dynamically.',
|
||||
tutorialName: 'Tutorial: Analyze & Visualize',
|
||||
title: t('WelcomeTitle', {context: "investmentResearch"}),
|
||||
text: t('WelcomeText', {context: "investmentResearch"}),
|
||||
tutorialName: t('WelcomeTutorialName', {context: "investmentResearch"}),
|
||||
},
|
||||
}, {
|
||||
id: 3, // Identifies the example in UserPrefs.seenExamples
|
||||
urlId: 'afterschool-program',
|
||||
title: 'Afterschool Program',
|
||||
title: t('Title', {context: "afterschool"}),
|
||||
imgUrl: 'https://www.getgrist.com/themes/grist/assets/images/use-cases/business-management.png',
|
||||
tutorialUrl: 'https://support.getgrist.com/afterschool-program/',
|
||||
welcomeCard: {
|
||||
title: 'Welcome to the Afterschool Program template',
|
||||
text: 'Check out our related tutorial for how to model business data, use formulas, ' +
|
||||
'and manage complexity.',
|
||||
tutorialName: 'Tutorial: Manage Business Data',
|
||||
title: t('WelcomeTitle', {context: "afterschool"}),
|
||||
text: t('WelcomeText', {context: "afterschool"}),
|
||||
tutorialName: t('WelcomeTutorialName', {context: "afterschool"}),
|
||||
},
|
||||
}];
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {CursorPos} from 'app/client/components/Cursor';
|
||||
import {GristDoc} from 'app/client/components/GristDoc';
|
||||
import {BEHAVIOR, ColumnRec} from 'app/client/models/entities/ColumnRec';
|
||||
@@ -18,6 +19,8 @@ import {bundleChanges, Computed, dom, DomContents, DomElementArg, fromKo, MultiH
|
||||
Observable, styled} from 'grainjs';
|
||||
import * as ko from 'knockout';
|
||||
|
||||
const t = makeT('FieldConfig');
|
||||
|
||||
export function buildNameConfig(
|
||||
owner: MultiHolder,
|
||||
origColumn: ColumnRec,
|
||||
@@ -51,7 +54,7 @@ export function buildNameConfig(
|
||||
};
|
||||
|
||||
return [
|
||||
cssLabel('COLUMN LABEL AND ID'),
|
||||
cssLabel(t('ColumnLabel')),
|
||||
cssRow(
|
||||
dom.cls(cssBlockedCursor.className, origColumn.disableModify),
|
||||
cssColLabelBlock(
|
||||
@@ -81,7 +84,7 @@ export function buildNameConfig(
|
||||
)
|
||||
),
|
||||
dom.maybe(isSummaryTable,
|
||||
() => cssRow('Column options are limited in summary tables.'))
|
||||
() => cssRow(t('ColumnOptionsLimited')))
|
||||
];
|
||||
}
|
||||
|
||||
@@ -207,18 +210,19 @@ export function buildFormulaConfig(
|
||||
const behaviorName = Computed.create(owner, behavior, (use, type) => {
|
||||
if (use(isMultiSelect)) {
|
||||
const commonType = use(multiType);
|
||||
if (commonType === 'formula') { return "Formula Columns"; }
|
||||
if (commonType === 'data') { return "Data Columns"; }
|
||||
if (commonType === 'mixed') { return "Mixed Behavior"; }
|
||||
return "Empty Columns";
|
||||
if (commonType === 'formula') { return t('ColumnType', {context: 'formula', count: 2}); }
|
||||
if (commonType === 'data') { return t('ColumnType', {context: 'data', count: 2}); }
|
||||
if (commonType === 'mixed') { return t('ColumnType', {context: 'mixed', count: 2}); }
|
||||
return t('ColumnType', {context: 'empty', count: 2});
|
||||
} else {
|
||||
if (type === 'formula') { return "Formula Column"; }
|
||||
if (type === 'data') { return "Data Column"; }
|
||||
return "Empty Column";
|
||||
if (type === 'formula') { return t('ColumnType', {context: 'formula', count: 1}); }
|
||||
if (type === 'data') { return t('ColumnType', {context: 'data', count: 1}); }
|
||||
return t('ColumnType', {context: 'empty', count: 1});
|
||||
}
|
||||
});
|
||||
const behaviorIcon = Computed.create<IconName>(owner, (use) => {
|
||||
return use(behaviorName).startsWith("Data Column") ? "Database" : "Script";
|
||||
return use(behaviorName) === t('ColumnType', {context: 'data', count: 2}) ||
|
||||
use(behaviorName) === t('ColumnType', {context: 'data', count: 1}) ? "Database" : "Script";
|
||||
});
|
||||
const behaviorLabel = () => selectTitle(behaviorName, behaviorIcon);
|
||||
|
||||
@@ -227,26 +231,26 @@ export function buildFormulaConfig(
|
||||
// Converts data column to formula column.
|
||||
const convertDataColumnToFormulaOption = () => selectOption(
|
||||
() => (maybeFormula.set(true), formulaField?.focus()),
|
||||
'Clear and make into formula', 'Script');
|
||||
t('ConvertColumn', {context: 'formula'}), 'Script');
|
||||
|
||||
// Converts to empty column and opens up the editor. (label is the same, but this is used when we have no formula)
|
||||
const convertTriggerToFormulaOption = () => selectOption(
|
||||
() => gristDoc.convertIsFormula([origColumn.id.peek()], {toFormula: true, noRecalc: true}),
|
||||
'Clear and make into formula', 'Script');
|
||||
t('ConvertColumn', {context: 'formula'}), 'Script');
|
||||
|
||||
// Convert column to data.
|
||||
// This method is also available through a text button.
|
||||
const convertToData = () => gristDoc.convertIsFormula([origColumn.id.peek()], {toFormula: false, noRecalc: true});
|
||||
const convertToDataOption = () => selectOption(
|
||||
convertToData,
|
||||
'Convert column to data', 'Database',
|
||||
t('ConvertColumn', {context: 'data'}), 'Database',
|
||||
dom.cls('disabled', isSummaryTable)
|
||||
);
|
||||
|
||||
// Clears the column
|
||||
const clearAndResetOption = () => selectOption(
|
||||
() => gristDoc.clearColumns([origColumn.id.peek()]),
|
||||
'Clear and reset', 'CrossSmall');
|
||||
t('ClearAndReset'), 'CrossSmall');
|
||||
|
||||
// Actions on text buttons:
|
||||
|
||||
@@ -310,7 +314,7 @@ export function buildFormulaConfig(
|
||||
cssRow(formulaField = buildFormula(
|
||||
origColumn,
|
||||
buildEditor,
|
||||
"Enter formula",
|
||||
t('EnterFormula'),
|
||||
disableOtherActions,
|
||||
onSave,
|
||||
clearState)),
|
||||
@@ -318,21 +322,21 @@ export function buildFormulaConfig(
|
||||
];
|
||||
|
||||
return dom.maybe(behavior, (type: BEHAVIOR) => [
|
||||
cssLabel('COLUMN BEHAVIOR'),
|
||||
cssLabel(t('ColumnBehavior')),
|
||||
...(type === "empty" ? [
|
||||
menu(behaviorLabel(), [
|
||||
convertToDataOption(),
|
||||
]),
|
||||
cssEmptySeparator(),
|
||||
cssRow(textButton(
|
||||
"Set formula",
|
||||
t('SetFormula'),
|
||||
dom.on("click", setFormula),
|
||||
dom.prop("disabled", disableOtherActions),
|
||||
testId("field-set-formula")
|
||||
)),
|
||||
cssRow(withInfoTooltip(
|
||||
textButton(
|
||||
"Set trigger formula",
|
||||
t('SetTriggerFormula'),
|
||||
dom.on("click", setTrigger),
|
||||
dom.prop("disabled", use => use(isSummaryTable) || use(disableOtherActions)),
|
||||
testId("field-set-trigger")
|
||||
@@ -340,7 +344,7 @@ export function buildFormulaConfig(
|
||||
GristTooltips.setTriggerFormula(),
|
||||
)),
|
||||
cssRow(textButton(
|
||||
"Make into data column",
|
||||
t('MakeIntoDataColumn'),
|
||||
dom.on("click", convertToData),
|
||||
dom.prop("disabled", use => use(isSummaryTable) || use(disableOtherActions)),
|
||||
testId("field-set-data")
|
||||
@@ -353,7 +357,7 @@ export function buildFormulaConfig(
|
||||
formulaBuilder(onSaveConvertToFormula),
|
||||
cssEmptySeparator(),
|
||||
cssRow(textButton(
|
||||
"Convert to trigger formula",
|
||||
t('ConvertColumn', {context: 'triggerformula'}),
|
||||
dom.on("click", convertFormulaToTrigger),
|
||||
dom.hide(maybeFormula),
|
||||
dom.prop("disabled", use => use(isSummaryTable) || use(disableOtherActions)),
|
||||
@@ -373,7 +377,7 @@ export function buildFormulaConfig(
|
||||
),
|
||||
// If data column is or wants to be a trigger formula:
|
||||
dom.maybe((use) => use(maybeTrigger) || use(origColumn.hasTriggerFormula), () => [
|
||||
cssLabel('TRIGGER FORMULA'),
|
||||
cssLabel(t('TriggerFormula')),
|
||||
formulaBuilder(onSaveConvertToTrigger),
|
||||
dom.create(buildFormulaTriggers, origColumn, {
|
||||
disabled: disableOtherActions,
|
||||
@@ -385,7 +389,7 @@ export function buildFormulaConfig(
|
||||
cssEmptySeparator(),
|
||||
cssRow(withInfoTooltip(
|
||||
textButton(
|
||||
"Set trigger formula",
|
||||
t("SetTriggerFormula"),
|
||||
dom.on("click", convertDataColumnToTriggerColumn),
|
||||
dom.prop("disabled", disableOtherActions),
|
||||
testId("field-set-trigger")
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {menuItem, menuSubHeader} from 'app/client/ui2018/menus';
|
||||
import {dom} from 'grainjs';
|
||||
|
||||
@@ -7,13 +8,15 @@ interface IFieldOptions {
|
||||
revertToCommon: () => void;
|
||||
}
|
||||
|
||||
const t = makeT('FieldMenus');
|
||||
|
||||
export function FieldSettingsMenu(useColOptions: boolean, disableSeparate: boolean, actions: IFieldOptions) {
|
||||
useColOptions = useColOptions || disableSeparate;
|
||||
return [
|
||||
menuSubHeader(`Using ${useColOptions ? 'common' : 'separate'} settings`),
|
||||
useColOptions ? menuItem(actions.useSeparate, 'Use separate settings', dom.cls('disabled', disableSeparate)) : [
|
||||
menuItem(actions.saveAsCommon, 'Save as common settings'),
|
||||
menuItem(actions.revertToCommon, 'Revert to common settings'),
|
||||
menuSubHeader(t('UsingSettings', {context: useColOptions ? 'common' : 'separate'})),
|
||||
useColOptions ? menuItem(actions.useSeparate, t('Settings', {context: 'useseparate'}), dom.cls('disabled', disableSeparate)) : [
|
||||
menuItem(actions.saveAsCommon, t('Settings', {context: 'savecommon'})),
|
||||
menuItem(actions.revertToCommon, t('Settings', {context: 'revertcommon'})),
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { makeT } from "app/client/lib/localization";
|
||||
import { allInclusive } from "app/client/models/ColumnFilter";
|
||||
import { ColumnRec, ViewFieldRec, ViewSectionRec } from "app/client/models/DocModel";
|
||||
import { FilterInfo } from "app/client/models/entities/ViewSectionRec";
|
||||
@@ -9,6 +10,8 @@ import { menu, menuItemAsync } from "app/client/ui2018/menus";
|
||||
import { dom, IDisposableOwner, IDomArgs, styled } from "grainjs";
|
||||
import { IMenuOptions, PopupControl } from "popweasel";
|
||||
|
||||
const t = makeT('FilterBar');
|
||||
|
||||
export function filterBar(_owner: IDisposableOwner, viewSection: ViewSectionRec) {
|
||||
const popupControls = new WeakMap<ColumnRec, PopupControl>();
|
||||
return cssFilterBar(
|
||||
@@ -77,7 +80,7 @@ function makePlusButton(viewSectionRec: ViewSectionRec, popupControls: WeakMap<C
|
||||
cssBtn.cls('-grayed'),
|
||||
cssIcon('Plus'),
|
||||
addFilterMenu(filters, viewSectionRec, popupControls),
|
||||
anyFilter ? null : cssPlusLabel('Add Filter'),
|
||||
anyFilter ? null : cssPlusLabel(t('AddFilter')),
|
||||
testId('add-filter-btn')
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { makeT } from 'app/client/lib/localization';
|
||||
import { ViewSectionRec } from "app/client/models/DocModel";
|
||||
import { KoSaveableObservable, setSaveValue } from "app/client/models/modelUtil";
|
||||
import { cssLabel, cssRow } from "app/client/ui/RightPanelStyles";
|
||||
@@ -5,6 +6,8 @@ import { squareCheckbox } from "app/client/ui2018/checkbox";
|
||||
import { testId } from "app/client/ui2018/cssVars";
|
||||
import { Computed, Disposable, dom, IDisposableOwner, styled } from "grainjs";
|
||||
|
||||
const t = makeT('GridOptions');
|
||||
|
||||
/**
|
||||
* Builds the grid options.
|
||||
*/
|
||||
@@ -17,23 +20,23 @@ export class GridOptions extends Disposable {
|
||||
public buildDom() {
|
||||
const section = this._section;
|
||||
return [
|
||||
cssLabel('Grid Options'),
|
||||
cssLabel(t('GridOptions')),
|
||||
dom('div', [
|
||||
cssRow(
|
||||
checkbox(setSaveValueFromKo(this, section.optionsObj.prop('verticalGridlines'))),
|
||||
'Vertical Gridlines',
|
||||
t('VerticalGridlines'),
|
||||
testId('v-grid-button')
|
||||
),
|
||||
|
||||
cssRow(
|
||||
checkbox(setSaveValueFromKo(this, section.optionsObj.prop('horizontalGridlines'))),
|
||||
'Horizontal Gridlines',
|
||||
t('HorizontalGridlines'),
|
||||
testId('h-grid-button')
|
||||
),
|
||||
|
||||
cssRow(
|
||||
checkbox(setSaveValueFromKo(this, section.optionsObj.prop('zebraStripes'))),
|
||||
'Zebra Stripes',
|
||||
t('ZebraStripes'),
|
||||
testId('zebra-stripe-button')
|
||||
),
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import { allCommands } from 'app/client/components/commands';
|
||||
import { ViewFieldRec } from 'app/client/models/entities/ViewFieldRec';
|
||||
import { testId, theme } from 'app/client/ui2018/cssVars';
|
||||
@@ -7,6 +8,8 @@ import { Sort } from 'app/common/SortSpec';
|
||||
import { dom, DomElementArg, styled } from 'grainjs';
|
||||
import isEqual = require('lodash/isEqual');
|
||||
|
||||
const t = makeT('GridViewMenus');
|
||||
|
||||
interface IView {
|
||||
addNewColumn: () => void;
|
||||
showColumn: (colId: number, atIndex: number) => void;
|
||||
@@ -23,13 +26,13 @@ interface IViewSection {
|
||||
*/
|
||||
export function ColumnAddMenu(gridView: IView, viewSection: IViewSection) {
|
||||
return [
|
||||
menuItem(() => gridView.addNewColumn(), 'Add Column'),
|
||||
menuItem(() => gridView.addNewColumn(), t('AddColumn')),
|
||||
menuDivider(),
|
||||
...viewSection.hiddenColumns().map((col: any) => menuItem(
|
||||
() => {
|
||||
gridView.showColumn(col.id(), viewSection.viewFields().peekLength);
|
||||
// .then(() => gridView.scrollPaneRight());
|
||||
}, `Show column ${col.label()}`))
|
||||
}, t('ShowColumn', {label: col.label()})))
|
||||
];
|
||||
}
|
||||
export interface IMultiColumnContextMenu {
|
||||
@@ -65,13 +68,13 @@ export function ColumnContextMenu(options: IColumnContextMenu) {
|
||||
const addToSortLabel = getAddToSortLabel(sortSpec, colId);
|
||||
|
||||
return [
|
||||
menuItemCmd(allCommands.fieldTabOpen, 'Column Options'),
|
||||
menuItem(filterOpenFunc, 'Filter Data'),
|
||||
menuItemCmd(allCommands.fieldTabOpen, t('ColumnOptions')),
|
||||
menuItem(filterOpenFunc, t('FilterData')),
|
||||
menuDivider({style: 'margin-bottom: 0;'}),
|
||||
cssRowMenuItem(
|
||||
customMenuItem(
|
||||
allCommands.sortAsc.run,
|
||||
dom('span', 'Sort', {style: 'flex: 1 0 auto; margin-right: 8px;'},
|
||||
dom('span', t('Sort'), {style: 'flex: 1 0 auto; margin-right: 8px;'},
|
||||
testId('sort-label')),
|
||||
icon('Sort', dom.style('transform', 'scaley(-1)')),
|
||||
'A-Z',
|
||||
@@ -109,9 +112,9 @@ export function ColumnContextMenu(options: IColumnContextMenu) {
|
||||
),
|
||||
] : null,
|
||||
menuDivider({style: 'margin-bottom: 0; margin-top: 0;'}),
|
||||
menuItem(allCommands.sortFilterTabOpen.run, 'More sort options ...', testId('more-sort-options')),
|
||||
menuItem(allCommands.sortFilterTabOpen.run, t('MoreSortOptions'), testId('more-sort-options')),
|
||||
menuDivider({style: 'margin-top: 0;'}),
|
||||
menuItemCmd(allCommands.renameField, 'Rename column', disableForReadonlyColumn),
|
||||
menuItemCmd(allCommands.renameField, t('RenameColumn'), disableForReadonlyColumn),
|
||||
freezeMenuItemCmd(options),
|
||||
menuDivider(),
|
||||
MultiColumnMenu((options.disableFrozenMenu = true, options)),
|
||||
@@ -132,29 +135,29 @@ export function MultiColumnMenu(options: IMultiColumnContextMenu) {
|
||||
const disableForReadonlyView = dom.cls('disabled', options.isReadonly);
|
||||
const num: number = options.numColumns;
|
||||
const nameClearColumns = options.isFiltered ?
|
||||
(num > 1 ? `Clear ${num} entire columns` : 'Clear entire column') :
|
||||
(num > 1 ? `Clear ${num} columns` : 'Clear column');
|
||||
const nameDeleteColumns = num > 1 ? `Delete ${num} columns` : 'Delete column';
|
||||
const nameHideColumns = num > 1 ? `Hide ${num} columns` : 'Hide column';
|
||||
t('ClearEntireColumns', {count: num}) :
|
||||
t('ClearColumns', {count: num});
|
||||
const nameDeleteColumns = t('DeleteColumns', {count: num});
|
||||
const nameHideColumns = t('HideColumns', {count: num});
|
||||
const frozenMenu = options.disableFrozenMenu ? null : freezeMenuItemCmd(options);
|
||||
return [
|
||||
frozenMenu ? [frozenMenu, menuDivider()]: null,
|
||||
// Offered only when selection includes formula columns, and converts only those.
|
||||
(options.isFormula ?
|
||||
menuItemCmd(allCommands.convertFormulasToData, 'Convert formula to data',
|
||||
menuItemCmd(allCommands.convertFormulasToData, t('ConvertFormulaToData'),
|
||||
disableForReadonlyColumn) : null),
|
||||
|
||||
// With data columns selected, offer an additional option to clear out selected cells.
|
||||
(options.isFormula !== true ?
|
||||
menuItemCmd(allCommands.clearValues, 'Clear values', disableForReadonlyColumn) : null),
|
||||
menuItemCmd(allCommands.clearValues, t('ClearValues'), disableForReadonlyColumn) : null),
|
||||
|
||||
(!options.isRaw ? menuItemCmd(allCommands.hideFields, nameHideColumns, disableForReadonlyView) : null),
|
||||
menuItemCmd(allCommands.clearColumns, nameClearColumns, disableForReadonlyColumn),
|
||||
menuItemCmd(allCommands.deleteFields, nameDeleteColumns, disableForReadonlyColumn),
|
||||
|
||||
menuDivider(),
|
||||
menuItemCmd(allCommands.insertFieldBefore, 'Insert column to the left', disableForReadonlyView),
|
||||
menuItemCmd(allCommands.insertFieldAfter, 'Insert column to the right', disableForReadonlyView)
|
||||
menuItemCmd(allCommands.insertFieldBefore, t('InsertColumn', {to: 'left'}), disableForReadonlyView),
|
||||
menuItemCmd(allCommands.insertFieldAfter, t('InsertColumn', {to: 'right'}), disableForReadonlyView)
|
||||
];
|
||||
}
|
||||
|
||||
@@ -203,12 +206,12 @@ export function freezeAction(options: IMultiColumnContextMenu): { text: string;
|
||||
|
||||
// if user clicked the first column or a column just after frozen set
|
||||
if (firstColumnIndex === 0 || firstColumnIndex === numFrozen) {
|
||||
text = 'Freeze this column';
|
||||
text = t('FreezeColumn', {count: 1});
|
||||
} else {
|
||||
// else user clicked any other column that is farther, offer to freeze
|
||||
// proper number of column
|
||||
const properNumber = firstColumnIndex - numFrozen + 1;
|
||||
text = `Freeze ${properNumber} ${numFrozen ? 'more ' : ''}columns`;
|
||||
text = t('FreezeColumn', {count: properNumber, context: numFrozen ? 'more' : '' });
|
||||
}
|
||||
return {
|
||||
text,
|
||||
@@ -217,12 +220,12 @@ export function freezeAction(options: IMultiColumnContextMenu): { text: string;
|
||||
} else if (isFrozenColumn) {
|
||||
// when user clicked last column in frozen set - offer to unfreeze this column
|
||||
if (firstColumnIndex + 1 === numFrozen) {
|
||||
text = `Unfreeze this column`;
|
||||
text = t('UnfreezeColumn', {count: 1});
|
||||
} else {
|
||||
// else user clicked column that is not the last in a frozen set
|
||||
// offer to unfreeze proper number of columns
|
||||
const properNumber = numFrozen - firstColumnIndex;
|
||||
text = `Unfreeze ${properNumber === numFrozen ? 'all' : properNumber} columns`;
|
||||
text = t('UnfreezeColumn', {count: properNumber, context: properNumber === numFrozen ? 'all' : '' });
|
||||
}
|
||||
return {
|
||||
text,
|
||||
@@ -233,20 +236,20 @@ export function freezeAction(options: IMultiColumnContextMenu): { text: string;
|
||||
}
|
||||
} else {
|
||||
if (isLastFrozenSet) {
|
||||
text = `Unfreeze ${length} columns`;
|
||||
text = t('UnfreezeColumn', {count: length});
|
||||
return {
|
||||
text,
|
||||
numFrozen : numFrozen - length
|
||||
};
|
||||
} else if (isFirstNormalSet) {
|
||||
text = `Freeze ${length} columns`;
|
||||
text = t('FreezeColumn', {count: length});
|
||||
return {
|
||||
text,
|
||||
numFrozen : numFrozen + length
|
||||
};
|
||||
} else if (isSpanSet) {
|
||||
const toFreeze = lastColumnIndex + 1 - numFrozen;
|
||||
text = `Freeze ${toFreeze == 1 ? 'one more column' : (`${toFreeze} more columns`)}`;
|
||||
text = t('FreezeColumn', {count: toFreeze, context: 'more'});
|
||||
return {
|
||||
text,
|
||||
numFrozen : numFrozen + toFreeze
|
||||
@@ -275,9 +278,9 @@ function getAddToSortLabel(sortSpec: Sort.SortSpec, colId: number): string|undef
|
||||
if (sortSpec.length !== 0 && !isEqual(columnsInSpec, [colId])) {
|
||||
const index = columnsInSpec.indexOf(colId);
|
||||
if (index > -1) {
|
||||
return `Sorted (#${index + 1})`;
|
||||
return t('AddToSort', {count: index + 1, context: 'added'});
|
||||
} else {
|
||||
return 'Add to sort';
|
||||
return t('AddToSort');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import {FullUser} from 'app/common/LoginSessionAPI';
|
||||
import * as roles from 'app/common/roles';
|
||||
import {Computed, dom, DomContents, styled} from 'grainjs';
|
||||
|
||||
const translate = makeT('HomeIntro');
|
||||
const t = makeT('HomeIntro');
|
||||
|
||||
export function buildHomeIntro(homeModel: HomeModel): DomContents {
|
||||
const isViewer = homeModel.app.currentOrg?.access === roles.VIEWER;
|
||||
@@ -37,7 +37,7 @@ export function buildHomeIntro(homeModel: HomeModel): DomContents {
|
||||
export function buildWorkspaceIntro(homeModel: HomeModel): DomContents {
|
||||
const isViewer = homeModel.currentWS.get()?.access === roles.VIEWER;
|
||||
const isAnonym = !homeModel.app.currentValidUser;
|
||||
const emptyLine = cssIntroLine(testId('empty-workspace-info'), "This workspace is empty.");
|
||||
const emptyLine = cssIntroLine(testId('empty-workspace-info'), t('EmptyWorkspace'));
|
||||
if (isAnonym || isViewer) {
|
||||
return emptyLine;
|
||||
} else {
|
||||
@@ -58,39 +58,41 @@ function makeViewerTeamSiteIntro(homeModel: HomeModel) {
|
||||
const docLink = (dom.maybe(personalOrg, org => {
|
||||
return cssLink(
|
||||
urlState().setLinkUrl({org: org.domain ?? undefined}),
|
||||
'personal site',
|
||||
t('PersonalSite'),
|
||||
testId('welcome-personal-url'));
|
||||
}));
|
||||
return [
|
||||
css.docListHeader(
|
||||
dom.autoDispose(personalOrg),
|
||||
`Welcome to ${homeModel.app.currentOrgName}`,
|
||||
t('WelcomeTo', {orgName: homeModel.app.currentOrgName}),
|
||||
productPill(homeModel.app.currentOrg, {large: true}),
|
||||
testId('welcome-title')
|
||||
),
|
||||
cssIntroLine(
|
||||
testId('welcome-info'),
|
||||
"You have read-only access to this site. Currently there are no documents.", dom('br'),
|
||||
"Any documents created in this site will appear here."),
|
||||
t('WelcomeInfoNoDocuments'),
|
||||
dom('br'),
|
||||
t('WelcomeInfoAppearHere'),
|
||||
),
|
||||
cssIntroLine(
|
||||
'Interested in using Grist outside of your team? Visit your free ', docLink, '.',
|
||||
t('WelcomeTextVistGrist'), docLink, '.',
|
||||
testId('welcome-text')
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
function makeTeamSiteIntro(homeModel: HomeModel) {
|
||||
const sproutsProgram = cssLink({href: commonUrls.sproutsProgram, target: '_blank'}, 'Sprouts Program');
|
||||
const sproutsProgram = cssLink({href: commonUrls.sproutsProgram, target: '_blank'}, t('SproutsProgram'));
|
||||
return [
|
||||
css.docListHeader(
|
||||
`Welcome to ${homeModel.app.currentOrgName}`,
|
||||
t('WelcomeTo', {orgName: homeModel.app.currentOrgName}),
|
||||
productPill(homeModel.app.currentOrg, {large: true}),
|
||||
testId('welcome-title')
|
||||
),
|
||||
cssIntroLine('Get started by inviting your team and creating your first Grist document.'),
|
||||
cssIntroLine(t('TeamSiteIntroGetStarted')),
|
||||
(shouldHideUiElement('helpCenter') ? null :
|
||||
cssIntroLine(
|
||||
'Learn more in our ', helpCenterLink(), ', or find an expert via our ', sproutsProgram, '.',
|
||||
'Learn more in our ', helpCenterLink(), ', or find an expert via our ', sproutsProgram, '.', // TODO i18n
|
||||
testId('welcome-text')
|
||||
)
|
||||
),
|
||||
@@ -100,10 +102,10 @@ function makeTeamSiteIntro(homeModel: HomeModel) {
|
||||
|
||||
function makePersonalIntro(homeModel: HomeModel, user: FullUser) {
|
||||
return [
|
||||
css.docListHeader(`Welcome to Grist, ${user.name}!`, testId('welcome-title')),
|
||||
cssIntroLine('Get started by creating your first Grist document.'),
|
||||
css.docListHeader(t('WelcomeUser', {name: user.name}), testId('welcome-title')),
|
||||
cssIntroLine(t('PersonalIntroGetStarted')),
|
||||
(shouldHideUiElement('helpCenter') ? null :
|
||||
cssIntroLine(translate('VisitHelpCenter', { link: helpCenterLink() }),
|
||||
cssIntroLine(t('VisitHelpCenter', { link: helpCenterLink() }),
|
||||
testId('welcome-text'))
|
||||
),
|
||||
makeCreateButtons(homeModel),
|
||||
@@ -111,19 +113,19 @@ function makePersonalIntro(homeModel: HomeModel, user: FullUser) {
|
||||
}
|
||||
|
||||
function makeAnonIntro(homeModel: HomeModel) {
|
||||
const signUp = cssLink({href: getLoginOrSignupUrl()}, translate('SignUp'));
|
||||
const signUp = cssLink({href: getLoginOrSignupUrl()}, t('SignUp'));
|
||||
return [
|
||||
css.docListHeader(translate('Welcome'), testId('welcome-title')),
|
||||
cssIntroLine('Get started by exploring templates, or creating your first Grist document.'),
|
||||
cssIntroLine(signUp, ' to save your work. ',
|
||||
(shouldHideUiElement('helpCenter') ? null : translate('VisitHelpCenter', { link: helpCenterLink() })),
|
||||
css.docListHeader(t('Welcome'), testId('welcome-title')),
|
||||
cssIntroLine(t('AnonIntroGetStarted')),
|
||||
cssIntroLine(signUp, ' to save your work. ', // TODO i18n
|
||||
(shouldHideUiElement('helpCenter') ? null : t('VisitHelpCenter', { link: helpCenterLink() })),
|
||||
testId('welcome-text')),
|
||||
makeCreateButtons(homeModel),
|
||||
];
|
||||
}
|
||||
|
||||
function helpCenterLink() {
|
||||
return cssLink({href: commonUrls.help, target: '_blank'}, cssInlineIcon('Help'), 'Help Center');
|
||||
return cssLink({href: commonUrls.help, target: '_blank'}, cssInlineIcon('Help'), t('HelpCenter'));
|
||||
}
|
||||
|
||||
function buildButtons(homeModel: HomeModel, options: {
|
||||
@@ -134,22 +136,22 @@ function buildButtons(homeModel: HomeModel, options: {
|
||||
}) {
|
||||
return cssBtnGroup(
|
||||
!options.invite ? null :
|
||||
cssBtn(cssBtnIcon('Help'), 'Invite Team Members', testId('intro-invite'),
|
||||
cssBtn(cssBtnIcon('Help'), t('InviteTeamMembers'), testId('intro-invite'),
|
||||
cssButton.cls('-primary'),
|
||||
dom.on('click', () => manageTeamUsersApp(homeModel.app)),
|
||||
),
|
||||
!options.templates ? null :
|
||||
cssBtn(cssBtnIcon('FieldTable'), 'Browse Templates', testId('intro-templates'),
|
||||
cssBtn(cssBtnIcon('FieldTable'), t('BrowseTemplates'), testId('intro-templates'),
|
||||
cssButton.cls('-primary'),
|
||||
dom.hide(shouldHideUiElement("templates")),
|
||||
urlState().setLinkUrl({homePage: 'templates'}),
|
||||
),
|
||||
!options.import ? null :
|
||||
cssBtn(cssBtnIcon('Import'), 'Import Document', testId('intro-import-doc'),
|
||||
cssBtn(cssBtnIcon('Import'), t('ImportDocument'), testId('intro-import-doc'),
|
||||
dom.on('click', () => importDocAndOpen(homeModel)),
|
||||
),
|
||||
!options.empty ? null :
|
||||
cssBtn(cssBtnIcon('Page'), 'Create Empty Document', testId('intro-create-doc'),
|
||||
cssBtn(cssBtnIcon('Page'), t('CreateEmptyDocument'), testId('intro-create-doc'),
|
||||
dom.on('click', () => createDocAndOpen(homeModel)),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {loadUserManager} from 'app/client/lib/imports';
|
||||
import {ImportSourceElement} from 'app/client/lib/ImportSourceElement';
|
||||
import {reportError} from 'app/client/models/AppModel';
|
||||
@@ -20,6 +21,8 @@ import {computed, dom, domComputed, DomElementArg, observable, Observable, style
|
||||
import {createHelpTools, cssLeftPanel, cssScrollPane,
|
||||
cssSectionHeader, cssTools} from 'app/client/ui/LeftPanelCommon';
|
||||
|
||||
const t = makeT('HomeLeftPane');
|
||||
|
||||
export function createHomeLeftPane(leftPanelOpen: Observable<boolean>, home: HomeModel) {
|
||||
const creating = observable<boolean>(false);
|
||||
const renaming = observable<Workspace|null>(null);
|
||||
@@ -39,13 +42,14 @@ export function createHomeLeftPane(leftPanelOpen: Observable<boolean>, home: Hom
|
||||
cssPageEntry(
|
||||
cssPageEntry.cls('-selected', (use) => use(home.currentPage) === "all"),
|
||||
cssPageLink(cssPageIcon('Home'),
|
||||
cssLinkText('All Documents'),
|
||||
cssLinkText(t('AllDocuments')),
|
||||
urlState().setLinkUrl({ws: undefined, homePage: undefined}),
|
||||
testId('dm-all-docs'),
|
||||
),
|
||||
),
|
||||
dom.maybe(use => !use(home.singleWorkspace), () =>
|
||||
cssSectionHeader('Workspaces',
|
||||
cssSectionHeader(
|
||||
t('Workspaces'),
|
||||
// Give it a testId, because it's a good element to simulate "click-away" in tests.
|
||||
testId('dm-ws-label')
|
||||
),
|
||||
@@ -104,14 +108,14 @@ export function createHomeLeftPane(leftPanelOpen: Observable<boolean>, home: Hom
|
||||
cssPageEntry(
|
||||
dom.hide(shouldHideUiElement("templates")),
|
||||
cssPageEntry.cls('-selected', (use) => use(home.currentPage) === "templates"),
|
||||
cssPageLink(cssPageIcon('FieldTable'), cssLinkText("Examples & Templates"),
|
||||
cssPageLink(cssPageIcon('FieldTable'), cssLinkText(t("ExamplesAndTemplates")),
|
||||
urlState().setLinkUrl({homePage: "templates"}),
|
||||
testId('dm-templates-page'),
|
||||
),
|
||||
),
|
||||
cssPageEntry(
|
||||
cssPageEntry.cls('-selected', (use) => use(home.currentPage) === "trash"),
|
||||
cssPageLink(cssPageIcon('Remove'), cssLinkText("Trash"),
|
||||
cssPageLink(cssPageIcon('Remove'), cssLinkText(t("Trash")),
|
||||
urlState().setLinkUrl({homePage: "trash"}),
|
||||
testId('dm-trash'),
|
||||
),
|
||||
@@ -172,11 +176,11 @@ function addMenu(home: HomeModel, creating: Observable<boolean>): DomElementArg[
|
||||
const needUpgrade = home.app.currentFeatures.maxWorkspacesPerOrg === 1;
|
||||
|
||||
return [
|
||||
menuItem(() => createDocAndOpen(home), menuIcon('Page'), "Create Empty Document",
|
||||
menuItem(() => createDocAndOpen(home), menuIcon('Page'), t("CreateEmptyDocument"),
|
||||
dom.cls('disabled', !home.newDocWorkspace.get()),
|
||||
testId("dm-new-doc")
|
||||
),
|
||||
menuItem(() => importDocAndOpen(home), menuIcon('Import'), "Import Document",
|
||||
menuItem(() => importDocAndOpen(home), menuIcon('Import'), t("ImportDocument"),
|
||||
dom.cls('disabled', !home.newDocWorkspace.get()),
|
||||
testId("dm-import")
|
||||
),
|
||||
@@ -191,7 +195,7 @@ function addMenu(home: HomeModel, creating: Observable<boolean>): DomElementArg[
|
||||
])),
|
||||
// For workspaces: if ACL says we can create them, but product says we can't,
|
||||
// then offer an upgrade link.
|
||||
upgradableMenuItem(needUpgrade, () => creating.set(true), menuIcon('Folder'), "Create Workspace",
|
||||
upgradableMenuItem(needUpgrade, () => creating.set(true), menuIcon('Folder'), t("CreateWorkspace"),
|
||||
dom.cls('disabled', (use) => !roles.canEdit(orgAccess) || !use(home.available)),
|
||||
testId("dm-new-workspace")
|
||||
),
|
||||
@@ -201,9 +205,9 @@ function addMenu(home: HomeModel, creating: Observable<boolean>): DomElementArg[
|
||||
|
||||
function workspaceMenu(home: HomeModel, ws: Workspace, renaming: Observable<Workspace|null>) {
|
||||
function deleteWorkspace() {
|
||||
confirmModal(`Delete ${ws.name} and all included documents?`, 'Delete',
|
||||
confirmModal(t('WorkspaceDeleteTitle', {workspace: ws.name}), t('Delete'),
|
||||
() => home.deleteWorkspace(ws.id, false),
|
||||
'Workspace will be moved to Trash.');
|
||||
t('WorkspaceDeleteText'));
|
||||
}
|
||||
|
||||
async function manageWorkspaceUsers() {
|
||||
@@ -221,17 +225,17 @@ function workspaceMenu(home: HomeModel, ws: Workspace, renaming: Observable<Work
|
||||
const needUpgrade = home.app.currentFeatures.maxWorkspacesPerOrg === 1;
|
||||
|
||||
return [
|
||||
upgradableMenuItem(needUpgrade, () => renaming.set(ws), "Rename",
|
||||
upgradableMenuItem(needUpgrade, () => renaming.set(ws), t("Rename"),
|
||||
dom.cls('disabled', !roles.canEdit(ws.access)),
|
||||
testId('dm-rename-workspace')),
|
||||
upgradableMenuItem(needUpgrade, deleteWorkspace, "Delete",
|
||||
upgradableMenuItem(needUpgrade, deleteWorkspace, t("Delete"),
|
||||
dom.cls('disabled', user => !roles.canEdit(ws.access)),
|
||||
testId('dm-delete-workspace')),
|
||||
// TODO: Personal plans can't currently share workspaces, but that restriction
|
||||
// should formally be documented and defined in `Features`, with this check updated
|
||||
// to look there instead.
|
||||
home.app.isPersonal ? null : upgradableMenuItem(needUpgrade, manageWorkspaceUsers,
|
||||
roles.canEditAccess(ws.access) ? "Manage Users" : "Access Details",
|
||||
roles.canEditAccess(ws.access) ? t("ManageUsers") : t("AccessDetails"),
|
||||
testId('dm-workspace-access')),
|
||||
upgradeText(needUpgrade, () => home.app.showUpgradeModal()),
|
||||
];
|
||||
|
||||
@@ -14,12 +14,15 @@
|
||||
* )
|
||||
*/
|
||||
import {beaconOpenMessage} from 'app/client/lib/helpScout';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {AppModel} from 'app/client/models/AppModel';
|
||||
import {testId, theme, vars} from 'app/client/ui2018/cssVars';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
import {commonUrls, shouldHideUiElement} from 'app/common/gristUrls';
|
||||
import {dom, DomContents, Observable, styled} from 'grainjs';
|
||||
|
||||
const t = makeT('LeftPanelCommon');
|
||||
|
||||
/**
|
||||
* Creates the "help tools", a button/link to open HelpScout beacon, and one to open the
|
||||
* HelpCenter in a new tab.
|
||||
@@ -31,7 +34,7 @@ export function createHelpTools(appModel: AppModel): DomContents {
|
||||
return cssSplitPageEntry(
|
||||
cssPageEntryMain(
|
||||
cssPageLink(cssPageIcon('Help'),
|
||||
cssLinkText('Help Center'),
|
||||
cssLinkText(t('HelpCenter')),
|
||||
dom.cls('tour-help-center'),
|
||||
dom.on('click', (ev) => beaconOpenMessage({appModel})),
|
||||
testId('left-feedback'),
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* the sample documents (those in the Support user's Examples & Templates workspace).
|
||||
*/
|
||||
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {AppModel, reportError} from 'app/client/models/AppModel';
|
||||
import {getLoginOrSignupUrl, urlState} from 'app/client/models/gristUrlState';
|
||||
import {getWorkspaceInfo, ownerName, workspaceName} from 'app/client/models/WorkspaceInfo';
|
||||
@@ -19,35 +20,35 @@ import {Document, isTemplatesOrg, Organization, Workspace} from 'app/common/User
|
||||
import {Computed, Disposable, dom, input, Observable, styled, subscribe} from 'grainjs';
|
||||
import sortBy = require('lodash/sortBy');
|
||||
|
||||
const t = makeT('MakeCopyMenu');
|
||||
|
||||
export async function replaceTrunkWithFork(user: FullUser|null, doc: Document, app: AppModel, origUrlId: string) {
|
||||
const trunkAccess = (await app.api.getDoc(origUrlId)).access;
|
||||
if (!roles.canEdit(trunkAccess)) {
|
||||
modal((ctl) => [
|
||||
cssModalBody(`Replacing the original requires editing rights on the original document.`),
|
||||
cssModalBody(t('CannotEditOriginal')),
|
||||
cssModalButtons(
|
||||
bigBasicButton('Cancel', dom.on('click', () => ctl.close())),
|
||||
bigBasicButton(t('Cancel'), dom.on('click', () => ctl.close())),
|
||||
)
|
||||
]);
|
||||
return;
|
||||
}
|
||||
const docApi = app.api.getDocAPI(origUrlId);
|
||||
const cmp = await docApi.compareDoc(doc.id);
|
||||
let titleText = 'Update Original';
|
||||
let buttonText = 'Update';
|
||||
let warningText = 'The original version of this document will be updated.';
|
||||
let titleText = t('UpdateOriginal');
|
||||
let buttonText = t('Update');
|
||||
let warningText = t('WarningOriginalWillBeUpdated');
|
||||
if (cmp.summary === 'left' || cmp.summary === 'both') {
|
||||
titleText = 'Original Has Modifications';
|
||||
buttonText = 'Overwrite';
|
||||
warningText = `${warningText} Be careful, the original has changes not in this document. ` +
|
||||
`Those changes will be overwritten.`;
|
||||
titleText = t('OriginalHasModifications');
|
||||
buttonText = t('Overwrite');
|
||||
warningText = `${warningText} ${t('WarningOverwriteOriginalChanges')}`;
|
||||
} else if (cmp.summary === 'unrelated') {
|
||||
titleText = 'Original Looks Unrelated';
|
||||
buttonText = 'Overwrite';
|
||||
warningText = `${warningText} It will be overwritten, losing any content not in this document.`;
|
||||
titleText = t('OriginalLooksUnrelated');
|
||||
buttonText = t('Overwrite');
|
||||
warningText = `${warningText} ${t('WarningWillBeOverwritten')}`;
|
||||
} else if (cmp.summary === 'same') {
|
||||
titleText = 'Original Looks Identical';
|
||||
warningText = `${warningText} However, it appears to be already identical.`;
|
||||
warningText = `${warningText} ${t('WarningAlreadyIdentical')}`;
|
||||
}
|
||||
confirmModal(titleText, buttonText,
|
||||
async () => {
|
||||
@@ -65,8 +66,8 @@ function signupModal(message: string) {
|
||||
return modal((ctl) => [
|
||||
cssModalBody(message),
|
||||
cssModalButtons(
|
||||
bigPrimaryButtonLink('Sign up', {href: getLoginOrSignupUrl(), target: '_blank'}, testId('modal-signup')),
|
||||
bigBasicButton('Cancel', dom.on('click', () => ctl.close())),
|
||||
bigPrimaryButtonLink(t('SignUp'), {href: getLoginOrSignupUrl(), target: '_blank'}, testId('modal-signup')),
|
||||
bigBasicButton(t('Cancel'), dom.on('click', () => ctl.close())),
|
||||
),
|
||||
cssModalWidth('normal'),
|
||||
]);
|
||||
@@ -95,7 +96,7 @@ function allowOtherOrgs(doc: Document, app: AppModel): boolean {
|
||||
*/
|
||||
export async function makeCopy(doc: Document, app: AppModel, modalTitle: string): Promise<void> {
|
||||
if (!app.currentValidUser) {
|
||||
signupModal('To save your changes, please sign up, then reload this page.');
|
||||
signupModal(t('ToSaveSignUpAndReload'));
|
||||
return;
|
||||
}
|
||||
let orgs = allowOtherOrgs(doc, app) ? await app.api.getOrgs(true) : null;
|
||||
@@ -149,7 +150,7 @@ class SaveCopyModal extends Disposable {
|
||||
|
||||
public async save() {
|
||||
const ws = this._destWS.get();
|
||||
if (!ws) { throw new Error('No destination workspace'); }
|
||||
if (!ws) { throw new Error(t('NoDestinationWorkspace')); }
|
||||
const api = this._app.api;
|
||||
const org = this._destOrg.get();
|
||||
const docWorker = await api.getWorkerAPI('import');
|
||||
@@ -171,8 +172,8 @@ class SaveCopyModal extends Disposable {
|
||||
public buildDom() {
|
||||
return [
|
||||
cssField(
|
||||
cssLabel("Name"),
|
||||
input(this._destName, {onInput: true}, {placeholder: 'Enter document name'}, dom.cls(cssInput.className),
|
||||
cssLabel(t("Name")),
|
||||
input(this._destName, {onInput: true}, {placeholder: t('EnterDocumentName')}, dom.cls(cssInput.className),
|
||||
// modal dialog grabs focus after 10ms delay; so to focus this input, wait a bit longer
|
||||
// (see the TODO in app/client/ui2018/modals.ts about weasel.js and focus).
|
||||
(elem) => { setTimeout(() => { elem.focus(); }, 20); },
|
||||
@@ -180,15 +181,15 @@ class SaveCopyModal extends Disposable {
|
||||
testId('copy-dest-name'))
|
||||
),
|
||||
cssField(
|
||||
cssLabel("As Template"),
|
||||
cssCheckbox(this._asTemplate, 'Include the structure without any of the data.',
|
||||
cssLabel(t("AsTemplate")),
|
||||
cssCheckbox(this._asTemplate, t('IncludeStructureWithoutData'),
|
||||
testId('save-as-template'))
|
||||
),
|
||||
// Show the team picker only when saving to other teams is allowed and there are other teams
|
||||
// accessible.
|
||||
(this._orgs ?
|
||||
cssField(
|
||||
cssLabel("Organization"),
|
||||
cssLabel(t("Organization")),
|
||||
select(this._destOrg, this._orgs.map(value => ({value, label: value.name}))),
|
||||
testId('copy-dest-org'),
|
||||
) : null
|
||||
@@ -198,11 +199,11 @@ class SaveCopyModal extends Disposable {
|
||||
// Show the workspace picker only when destOrg is a team site, because personal orgs do not have workspaces.
|
||||
dom.domComputed((use) => use(this._showWorkspaces) && use(this._workspaces), (wss) =>
|
||||
wss === false ? null :
|
||||
wss && wss.length === 0 ? cssWarningText("You do not have write access to this site",
|
||||
wss && wss.length === 0 ? cssWarningText(t("NoWriteAccessToSite"),
|
||||
testId('copy-warning')) :
|
||||
[
|
||||
cssField(
|
||||
cssLabel("Workspace"),
|
||||
cssLabel(t("Workspace")),
|
||||
(wss === null ?
|
||||
cssSpinner(loadingSpinner()) :
|
||||
select(this._destWS, wss.map(value => ({
|
||||
@@ -215,7 +216,7 @@ class SaveCopyModal extends Disposable {
|
||||
),
|
||||
wss ? dom.domComputed(this._destWS, (destWs) =>
|
||||
destWs && !roles.canEdit(destWs.access) ?
|
||||
cssWarningText("You do not have write access to the selected workspace",
|
||||
cssWarningText(t("NoWriteAccessToWorkspace"),
|
||||
testId('copy-warning')
|
||||
) : null
|
||||
) : null
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {beaconOpenMessage, IBeaconOpenOptions} from 'app/client/lib/helpScout';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {AppModel} from 'app/client/models/AppModel';
|
||||
import {ConnectState} from 'app/client/models/ConnectState';
|
||||
import {urlState} from 'app/client/models/gristUrlState';
|
||||
@@ -13,6 +14,8 @@ import {commonUrls, shouldHideUiElement} from 'app/common/gristUrls';
|
||||
import {dom, makeTestId, styled} from 'grainjs';
|
||||
import {cssMenu, defaultMenuOptions, IOpenController, setPopupToCreateDom} from 'popweasel';
|
||||
|
||||
const t = makeT('NotifyUI');
|
||||
|
||||
const testId = makeTestId('test-notifier-');
|
||||
|
||||
|
||||
@@ -21,10 +24,10 @@ function buildAction(action: NotifyAction, item: Notification, options: IBeaconO
|
||||
switch (action) {
|
||||
case 'upgrade':
|
||||
if (appModel) {
|
||||
return cssToastAction('Upgrade Plan', dom.on('click', () =>
|
||||
return cssToastAction(t('UpgradePlan'), dom.on('click', () =>
|
||||
appModel.showUpgradeModal()));
|
||||
} else {
|
||||
return dom('a', cssToastAction.cls(''), 'Upgrade Plan', {target: '_blank'},
|
||||
return dom('a', cssToastAction.cls(''), t('UpgradePlan'), {target: '_blank'},
|
||||
{href: commonUrls.plans});
|
||||
}
|
||||
case 'renew':
|
||||
@@ -34,22 +37,22 @@ function buildAction(action: NotifyAction, item: Notification, options: IBeaconO
|
||||
if (appModel && appModel.currentOrg && appModel.currentOrg.billingAccount &&
|
||||
!appModel.currentOrg.billingAccount.isManager) { return null; }
|
||||
// Otherwise return a link to the billing page.
|
||||
return dom('a', cssToastAction.cls(''), 'Renew', {target: '_blank'},
|
||||
return dom('a', cssToastAction.cls(''), t('Renew'), {target: '_blank'},
|
||||
{href: urlState().makeUrl({billing: 'billing'})});
|
||||
|
||||
case 'personal':
|
||||
if (!appModel) { return null; }
|
||||
return cssToastAction('Go to your free personal site', dom.on('click', async () => {
|
||||
return cssToastAction(t('GoToPersonalSite'), dom.on('click', async () => {
|
||||
const info = await appModel.api.getSessionAll();
|
||||
const orgs = info.orgs.filter(org => org.owner && org.owner.id === appModel.currentUser?.id);
|
||||
if (orgs.length !== 1) {
|
||||
throw new Error('Cannot find personal site, sorry!');
|
||||
throw new Error(t('ErrorCannotFindPersonalSite'));
|
||||
}
|
||||
window.location.assign(urlState().makeUrl({org: orgs[0].domain || undefined}));
|
||||
}));
|
||||
|
||||
case 'report-problem':
|
||||
return cssToastAction('Report a problem', testId('toast-report-problem'),
|
||||
return cssToastAction(t('ReportProblem'), testId('toast-report-problem'),
|
||||
dom.on('click', () => beaconOpenMessage({...options, includeAppErrors: true})));
|
||||
|
||||
case 'ask-for-help': {
|
||||
@@ -57,7 +60,7 @@ function buildAction(action: NotifyAction, item: Notification, options: IBeaconO
|
||||
error: new Error(item.options.message as string),
|
||||
timestamp: item.options.timestamp,
|
||||
}];
|
||||
return cssToastAction('Ask for help',
|
||||
return cssToastAction(t('AskForHelp'),
|
||||
dom.on('click', () => beaconOpenMessage({...options, includeAppErrors: true, errors})));
|
||||
}
|
||||
|
||||
@@ -151,11 +154,11 @@ function buildNotifyDropdown(ctl: IOpenController, notifier: Notifier, appModel:
|
||||
|
||||
cssDropdownContent(
|
||||
cssDropdownHeader(
|
||||
cssDropdownHeaderTitle('Notifications'),
|
||||
cssDropdownHeaderTitle(t('Notifications')),
|
||||
shouldHideUiElement("helpCenter") ? null :
|
||||
cssDropdownFeedbackLink(
|
||||
cssDropdownFeedbackIcon('Feedback'),
|
||||
'Give feedback',
|
||||
t('GiveFeedback'),
|
||||
dom.on('click', () => beaconOpenMessage({appModel, onOpen: () => ctl.close(), route: '/ask/message/'})),
|
||||
testId('feedback'),
|
||||
)
|
||||
@@ -168,7 +171,7 @@ function buildNotifyDropdown(ctl: IOpenController, notifier: Notifier, appModel:
|
||||
),
|
||||
dom.maybe((use) => use(dropdownItems).length === 0 && !use(disconnectMsg), () =>
|
||||
cssDropdownStatus(
|
||||
dom('div', cssDropdownStatusText('No notifications')),
|
||||
dom('div', cssDropdownStatusText(t('NoNotifications'))),
|
||||
)
|
||||
),
|
||||
dom.forEach(dropdownItems, item =>
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
import { Disposable, dom, DomElementArg, Holder, makeTestId, styled, svg } from "grainjs";
|
||||
import { createPopper, Placement } from '@popperjs/core';
|
||||
import { FocusLayer } from 'app/client/lib/FocusLayer';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import * as Mousetrap from 'app/client/lib/Mousetrap';
|
||||
import { bigBasicButton, bigPrimaryButton } from "app/client/ui2018/buttons";
|
||||
import { theme, vars } from "app/client/ui2018/cssVars";
|
||||
@@ -35,6 +36,8 @@ import {delay} from "app/common/delay";
|
||||
import {reportError} from "app/client/models/errors";
|
||||
import {cssBigIcon, cssCloseButton} from "./ExampleCard";
|
||||
|
||||
const t = makeT('OnBoardingPopups');
|
||||
|
||||
const testId = makeTestId('test-onboarding-');
|
||||
|
||||
// Describes an onboarding popup. Each popup is uniquely identified by its id.
|
||||
@@ -296,7 +299,7 @@ class OnBoardingPopupsCtl extends Disposable {
|
||||
{style: `margin-right: 8px; visibility: ${isFirstStep ? 'hidden' : 'visible'}`},
|
||||
),
|
||||
bigPrimaryButton(
|
||||
isLastStep ? 'Finish' : 'Next', testId('next'),
|
||||
isLastStep ? t('Finish') : t('Next'), testId('next'),
|
||||
dom.on('click', () => this._move(+1, true)),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as commands from 'app/client/components/commands';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {cssLinkText, cssPageEntryMain, cssPageIcon, cssPageLink} from 'app/client/ui/LeftPanelCommon';
|
||||
import {theme} from 'app/client/ui2018/cssVars';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
@@ -6,6 +7,8 @@ import {modal} from 'app/client/ui2018/modals';
|
||||
import {commonUrls, shouldHideUiElement} from 'app/common/gristUrls';
|
||||
import {dom, makeTestId, styled} from 'grainjs';
|
||||
|
||||
const t = makeT('OpenVideoTour');
|
||||
|
||||
const testId = makeTestId('test-video-tour-');
|
||||
|
||||
/**
|
||||
@@ -25,7 +28,7 @@ const testId = makeTestId('test-video-tour-');
|
||||
cssVideo(
|
||||
{
|
||||
src: commonUrls.videoTour,
|
||||
title: 'YouTube video player',
|
||||
title: t('YouTubeVideoPlayer'),
|
||||
frameborder: '0',
|
||||
allow: 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture',
|
||||
allowfullscreen: '',
|
||||
@@ -48,7 +51,7 @@ const testId = makeTestId('test-video-tour-');
|
||||
export function createVideoTourTextButton(): HTMLDivElement {
|
||||
const elem: HTMLDivElement = cssVideoTourTextButton(
|
||||
cssVideoIcon('Video'),
|
||||
'Grist Video Tour',
|
||||
t('GristVideoTour'),
|
||||
dom.on('click', () => openVideoTour(elem)),
|
||||
testId('text-button'),
|
||||
);
|
||||
@@ -74,7 +77,7 @@ export function createVideoTourToolsButton(): HTMLDivElement | null {
|
||||
dom.autoDispose(commandsGroup),
|
||||
cssPageLink(
|
||||
iconElement = cssPageIcon('Video'),
|
||||
cssLinkText('Video Tour'),
|
||||
cssLinkText(t('VideoTour')),
|
||||
dom.cls('tour-help-center'),
|
||||
dom.on('click', () => openVideoTour(iconElement)),
|
||||
testId('tools-button'),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import { reportError } from 'app/client/models/AppModel';
|
||||
import { ColumnRec, DocModel, TableRec, ViewSectionRec } from 'app/client/models/DocModel';
|
||||
import { GristTooltips } from 'app/client/ui/GristTooltips';
|
||||
@@ -15,6 +16,8 @@ import without = require('lodash/without');
|
||||
import Popper from 'popper.js';
|
||||
import { IOpenController, popupOpen, setPopupToCreateDom } from 'popweasel';
|
||||
|
||||
const t = makeT('PageWidgetPicker');
|
||||
|
||||
type TableId = number|'New Table'|null;
|
||||
|
||||
// Describes a widget selection.
|
||||
@@ -177,7 +180,7 @@ export function buildPageWidgetPicker(
|
||||
// should be handle by the caller.
|
||||
if (await isLongerThan(savePromise, DELAY_BEFORE_SPINNER_MS)) {
|
||||
const label = getWidgetTypes(type).label;
|
||||
await spinnerModal(`Building ${label} widget`, savePromise);
|
||||
await spinnerModal(t('BuildingWidget', { label }), savePromise);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -279,7 +282,7 @@ export class PageWidgetSelect extends Disposable {
|
||||
testId('container'),
|
||||
cssBody(
|
||||
cssPanel(
|
||||
header('Select Widget'),
|
||||
header(t('SelectWidget')),
|
||||
sectionTypes.map((value) => {
|
||||
const {label, icon: iconName} = getWidgetTypes(value);
|
||||
const disabled = computed(this._value.table, (use, tid) => this._isTypeDisabled(value, tid));
|
||||
@@ -296,7 +299,7 @@ export class PageWidgetSelect extends Disposable {
|
||||
),
|
||||
cssPanel(
|
||||
testId('data'),
|
||||
header('Select Data'),
|
||||
header(t('SelectData')),
|
||||
cssEntry(
|
||||
cssIcon('TypeTable'), 'New Table',
|
||||
// prevent the selection of 'New Table' if it is disabled
|
||||
@@ -324,7 +327,7 @@ export class PageWidgetSelect extends Disposable {
|
||||
)),
|
||||
),
|
||||
cssPanel(
|
||||
header('Group by'),
|
||||
header(t('GroupBy')),
|
||||
dom.hide((use) => !use(this._value.summarize)),
|
||||
domComputed(
|
||||
(use) => use(this._columns)
|
||||
@@ -359,7 +362,7 @@ export class PageWidgetSelect extends Disposable {
|
||||
bigPrimaryButton(
|
||||
// TODO: The button's label of the page widget picker should read 'Close' instead when
|
||||
// there are no changes.
|
||||
this._options.buttonLabel || 'Add to Page',
|
||||
this._options.buttonLabel || t('AddToPage'),
|
||||
dom.prop('disabled', (use) => !isValidSelection(
|
||||
use(this._value.table), use(this._value.type), this._options.isNewPage)
|
||||
),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {createGroup} from 'app/client/components/commands';
|
||||
import {duplicatePage} from 'app/client/components/duplicatePage';
|
||||
import {GristDoc} from 'app/client/components/GristDoc';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {PageRec} from 'app/client/models/DocModel';
|
||||
import {urlState} from 'app/client/models/gristUrlState';
|
||||
import MetaTableModel from 'app/client/models/MetaTableModel';
|
||||
@@ -15,6 +16,8 @@ import {buildPageDom, PageActions} from 'app/client/ui2018/pages';
|
||||
import {mod} from 'app/common/gutil';
|
||||
import {Computed, Disposable, dom, DomContents, fromKo, makeTestId, observable, Observable, styled} from 'grainjs';
|
||||
|
||||
const t = makeT('Pages');
|
||||
|
||||
// build dom for the tree view of pages
|
||||
export function buildPagesDom(owner: Disposable, activeDoc: GristDoc, isOpen: Observable<boolean>) {
|
||||
const pagesTable = activeDoc.docModel.pages;
|
||||
@@ -128,14 +131,14 @@ function buildPrompt(tableNames: string[], onSave: (option: RemoveOption) => Pro
|
||||
const saveDisabled = Computed.create(owner, use => use(selected) === '');
|
||||
const saveFunc = () => onSave(selected.get());
|
||||
return {
|
||||
title: `The following table${tableNames.length > 1 ? 's' : ''} will no longer be visible`,
|
||||
title: t('TableWillNoLongerBeVisible', { count: tableNames.length }),
|
||||
body: dom('div',
|
||||
testId('popup'),
|
||||
buildWarning(tableNames),
|
||||
cssOptions(
|
||||
buildOption(selected, 'data', `Delete data and this page.`),
|
||||
buildOption(selected, 'data', t('DeleteDataAndPage')),
|
||||
buildOption(selected, 'page',
|
||||
[
|
||||
[ // TODO i18n
|
||||
`Keep data and delete page. `,
|
||||
`Table will remain available in `,
|
||||
cssLink(urlState().setHref({docPage: 'data'}), 'raw data page', { target: '_blank'}),
|
||||
@@ -144,7 +147,7 @@ function buildPrompt(tableNames: string[], onSave: (option: RemoveOption) => Pro
|
||||
)
|
||||
),
|
||||
saveDisabled,
|
||||
saveLabel: 'Delete',
|
||||
saveLabel: t('Delete'),
|
||||
saveFunc,
|
||||
width: 'fixed-wide',
|
||||
extraButtons: [],
|
||||
|
||||
@@ -20,6 +20,7 @@ import {RefSelect} from 'app/client/components/RefSelect';
|
||||
import ViewConfigTab from 'app/client/components/ViewConfigTab';
|
||||
import {domAsync} from 'app/client/lib/domAsync';
|
||||
import * as imports from 'app/client/lib/imports';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {createSessionObs} from 'app/client/lib/sessionObs';
|
||||
import {reportError} from 'app/client/models/AppModel';
|
||||
import {ColumnRec, ViewSectionRec} from 'app/client/models/DocModel';
|
||||
@@ -42,23 +43,25 @@ import {bundleChanges, Computed, Disposable, dom, domComputed, DomContents,
|
||||
import {MultiHolder, Observable, styled, subscribe} from 'grainjs';
|
||||
import * as ko from 'knockout';
|
||||
|
||||
const t = makeT('RightPanel');
|
||||
|
||||
// Represents a top tab of the right side-pane.
|
||||
const TopTab = StringUnion("pageWidget", "field");
|
||||
|
||||
// Represents a subtab of pageWidget in the right side-pane.
|
||||
const PageSubTab = StringUnion("widget", "sortAndFilter", "data");
|
||||
|
||||
// A map of widget type to the icon and label to use for a field of that widget.
|
||||
const fieldTypes = new Map<IWidgetType, {label: string, icon: IconName, pluralLabel: string}>([
|
||||
['record', {label: 'Column', icon: 'TypeCell', pluralLabel: 'Columns'}],
|
||||
['detail', {label: 'Field', icon: 'TypeCell', pluralLabel: 'Fields'}],
|
||||
['single', {label: 'Field', icon: 'TypeCell', pluralLabel: 'Fields'}],
|
||||
['chart', {label: 'Series', icon: 'ChartLine', pluralLabel: 'Series'}],
|
||||
['custom', {label: 'Column', icon: 'TypeCell', pluralLabel: 'Columns'}],
|
||||
]);
|
||||
|
||||
// Returns the icon and label of a type, default to those associate to 'record' type.
|
||||
export function getFieldType(widgetType: IWidgetType|null) {
|
||||
// A map of widget type to the icon and label to use for a field of that widget.
|
||||
const fieldTypes = new Map<IWidgetType, {label: string, icon: IconName, pluralLabel: string}>([
|
||||
['record', {label: t('Column', { count: 1 }), icon: 'TypeCell', pluralLabel: t('Column', { count: 2 })}],
|
||||
['detail', {label: t('Field', { count: 1 }), icon: 'TypeCell', pluralLabel: t('Field', { count: 2 })}],
|
||||
['single', {label: t('Field', { count: 1 }), icon: 'TypeCell', pluralLabel: t('Field', { count: 2 })}],
|
||||
['chart', {label: t('Series', { count: 1 }), icon: 'ChartLine', pluralLabel: t('Series', { count: 2 })}],
|
||||
['custom', {label: t('Column', { count: 1 }), icon: 'TypeCell', pluralLabel: t('Column', { count: 2 })}],
|
||||
]);
|
||||
|
||||
return fieldTypes.get(widgetType || 'record') || fieldTypes.get('record')!;
|
||||
}
|
||||
|
||||
@@ -234,7 +237,7 @@ export class RightPanel extends Disposable {
|
||||
),
|
||||
cssSeparator(),
|
||||
dom.maybe<FieldBuilder|null>(fieldBuilder, builder => [
|
||||
cssLabel('COLUMN TYPE'),
|
||||
cssLabel(t('ColumnType')),
|
||||
cssSection(
|
||||
builder.buildSelectTypeDom(),
|
||||
),
|
||||
@@ -257,7 +260,7 @@ export class RightPanel extends Disposable {
|
||||
cssRow(refSelect.buildDom()),
|
||||
cssSeparator()
|
||||
]),
|
||||
cssLabel('TRANSFORM'),
|
||||
cssLabel(t('Transform')),
|
||||
dom.maybe<FieldBuilder|null>(fieldBuilder, builder => builder.buildTransformDom()),
|
||||
dom.maybe(isMultiSelect, () => disabledSection()),
|
||||
testId('panel-transform'),
|
||||
@@ -287,15 +290,15 @@ export class RightPanel extends Disposable {
|
||||
private _buildPageWidgetContent(_owner: MultiHolder) {
|
||||
return [
|
||||
cssSubTabContainer(
|
||||
cssSubTab('Widget',
|
||||
cssSubTab(t('Widget'),
|
||||
cssSubTab.cls('-selected', (use) => use(this._subTab) === 'widget'),
|
||||
dom.on('click', () => this._subTab.set("widget")),
|
||||
testId('config-widget')),
|
||||
cssSubTab('Sort & Filter',
|
||||
cssSubTab(t('SortAndFilter'),
|
||||
cssSubTab.cls('-selected', (use) => use(this._subTab) === 'sortAndFilter'),
|
||||
dom.on('click', () => this._subTab.set("sortAndFilter")),
|
||||
testId('config-sortAndFilter')),
|
||||
cssSubTab('Data',
|
||||
cssSubTab(t('Data'),
|
||||
cssSubTab.cls('-selected', (use) => use(this._subTab) === 'data'),
|
||||
dom.on('click', () => this._subTab.set("data")),
|
||||
testId('config-data')),
|
||||
@@ -337,7 +340,7 @@ export class RightPanel extends Disposable {
|
||||
});
|
||||
return dom.maybe(viewConfigTab, (vct) => [
|
||||
this._disableIfReadonly(),
|
||||
cssLabel(dom.text(use => use(activeSection.isRaw) ? 'DATA TABLE NAME' : 'WIDGET TITLE'),
|
||||
cssLabel(dom.text(use => use(activeSection.isRaw) ? t('DataTableName') : t('WidgetTitle')),
|
||||
dom.style('margin-bottom', '14px'),
|
||||
),
|
||||
cssRow(cssTextInput(
|
||||
@@ -354,7 +357,7 @@ export class RightPanel extends Disposable {
|
||||
dom.maybe(
|
||||
(use) => !use(activeSection.isRaw),
|
||||
() => cssRow(
|
||||
primaryButton('Change Widget', this._createPageWidgetPicker()),
|
||||
primaryButton(t('ChangeWidget'), this._createPageWidgetPicker()),
|
||||
cssRow.cls('-top-space')
|
||||
),
|
||||
),
|
||||
@@ -362,7 +365,7 @@ export class RightPanel extends Disposable {
|
||||
cssSeparator(),
|
||||
|
||||
dom.maybe((use) => ['detail', 'single'].includes(use(this._pageWidgetType)!), () => [
|
||||
cssLabel('Theme'),
|
||||
cssLabel(t('Theme')),
|
||||
dom('div',
|
||||
vct._buildThemeDom(),
|
||||
vct._buildLayoutDom())
|
||||
@@ -377,22 +380,22 @@ export class RightPanel extends Disposable {
|
||||
if (use(this._pageWidgetType) !== 'record') { return null; }
|
||||
return [
|
||||
cssSeparator(),
|
||||
cssLabel('ROW STYLE'),
|
||||
cssLabel(t('RowStyleUpper')),
|
||||
domAsync(imports.loadViewPane().then(ViewPane =>
|
||||
dom.create(ViewPane.ConditionalStyle, "Row Style", activeSection, this._gristDoc)
|
||||
dom.create(ViewPane.ConditionalStyle, t("RowStyle"), activeSection, this._gristDoc)
|
||||
))
|
||||
];
|
||||
}),
|
||||
|
||||
dom.maybe((use) => use(this._pageWidgetType) === 'chart', () => [
|
||||
cssLabel('CHART TYPE'),
|
||||
cssLabel(t('ChartType')),
|
||||
vct._buildChartConfigDom(),
|
||||
]),
|
||||
|
||||
dom.maybe((use) => use(this._pageWidgetType) === 'custom', () => {
|
||||
const parts = vct._buildCustomTypeItems() as any[];
|
||||
return [
|
||||
cssLabel('CUSTOM'),
|
||||
cssLabel(t('Custom')),
|
||||
// If 'customViewPlugin' feature is on, show the toggle that allows switching to
|
||||
// plugin mode. Note that the default mode for a new 'custom' view is 'url', so that's
|
||||
// the only one that will be shown without the feature flag.
|
||||
@@ -423,11 +426,11 @@ export class RightPanel extends Disposable {
|
||||
private _buildPageSortFilterConfig(owner: MultiHolder) {
|
||||
const viewConfigTab = this._createViewConfigTab(owner);
|
||||
return [
|
||||
cssLabel('SORT'),
|
||||
cssLabel(t('Sort')),
|
||||
dom.maybe(viewConfigTab, (vct) => vct.buildSortDom()),
|
||||
cssSeparator(),
|
||||
|
||||
cssLabel('FILTER'),
|
||||
cssLabel(t('Filter')),
|
||||
dom.maybe(viewConfigTab, (vct) => dom('div', vct._buildFilterDom())),
|
||||
];
|
||||
}
|
||||
@@ -464,15 +467,15 @@ export class RightPanel extends Disposable {
|
||||
link.onWrite((val) => this._gristDoc.saveLink(val));
|
||||
return [
|
||||
this._disableIfReadonly(),
|
||||
cssLabel('DATA TABLE'),
|
||||
cssLabel(t('DataTable')),
|
||||
cssRow(
|
||||
cssIcon('TypeTable'), cssDataLabel('SOURCE DATA'),
|
||||
cssIcon('TypeTable'), cssDataLabel(t('SourceData')),
|
||||
cssContent(dom.text((use) => use(use(table).primaryTableId)),
|
||||
testId('pwc-table'))
|
||||
),
|
||||
dom(
|
||||
'div',
|
||||
cssRow(cssIcon('Pivot'), cssDataLabel('GROUPED BY')),
|
||||
cssRow(cssIcon('Pivot'), cssDataLabel(t('GroupedBy'))),
|
||||
cssRow(domComputed(groupedBy, (cols) => cssList(cols.map((c) => (
|
||||
cssListItem(dom.text(c.label),
|
||||
testId('pwc-groupedBy-col'))
|
||||
@@ -484,12 +487,12 @@ export class RightPanel extends Disposable {
|
||||
),
|
||||
|
||||
dom.maybe((use) => !use(activeSection.isRaw), () =>
|
||||
cssButtonRow(primaryButton('Edit Data Selection', this._createPageWidgetPicker(),
|
||||
cssButtonRow(primaryButton(t('EditDataSelection'), this._createPageWidgetPicker(),
|
||||
testId('pwc-editDataSelection')),
|
||||
dom.maybe(
|
||||
use => Boolean(use(use(activeSection.table).summarySourceTable)),
|
||||
() => basicButton(
|
||||
'Detach',
|
||||
t('Detach'),
|
||||
dom.on('click', () => this._gristDoc.docData.sendAction(
|
||||
["DetachSummaryViewSection", activeSection.getRowId()])),
|
||||
testId('detach-button'),
|
||||
@@ -506,10 +509,10 @@ export class RightPanel extends Disposable {
|
||||
cssSeparator(),
|
||||
|
||||
dom.maybe((use) => !use(activeSection.isRaw), () => [
|
||||
cssLabel('SELECT BY'),
|
||||
cssLabel(t('SelectBy')),
|
||||
cssRow(
|
||||
dom.update(
|
||||
select(link, linkOptions, {defaultLabel: 'Select Widget'}),
|
||||
select(link, linkOptions, {defaultLabel: t('SelectWidget')}),
|
||||
dom.on('click', () => {
|
||||
refreshTrigger.set(!refreshTrigger.get());
|
||||
})
|
||||
@@ -525,7 +528,7 @@ export class RightPanel extends Disposable {
|
||||
// TODO: sections should be listed following the order of appearance in the view layout (ie:
|
||||
// left/right - top/bottom);
|
||||
return selectorFor.length ? [
|
||||
cssLabel('SELECTOR FOR', testId('selector-for')),
|
||||
cssLabel(t('SelectorFor'), testId('selector-for')),
|
||||
cssRow(cssList(selectorFor.map((sec) => this._buildSectionItem(sec))))
|
||||
] : null;
|
||||
}),
|
||||
@@ -537,7 +540,7 @@ export class RightPanel extends Disposable {
|
||||
const section = gristDoc.viewModel.activeSection;
|
||||
const onSave = (val: IPageWidget) => gristDoc.saveViewSection(section.peek(), val);
|
||||
return (elem) => { attachPageWidgetPicker(elem, gristDoc.docModel, onSave, {
|
||||
buttonLabel: 'Save',
|
||||
buttonLabel: t('Save'),
|
||||
value: () => toPageWidget(section.peek()),
|
||||
selectBy: (val) => gristDoc.selectBy(val),
|
||||
}); };
|
||||
@@ -558,7 +561,7 @@ export class RightPanel extends Disposable {
|
||||
return dom.maybe(this._gristDoc.docPageModel.isReadonly, () => (
|
||||
cssOverlay(
|
||||
testId('disable-overlay'),
|
||||
cssBottomText('You do not have edit access to this document'),
|
||||
cssBottomText(t('NoEditAccess')),
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { allCommands } from 'app/client/components/commands';
|
||||
import { makeT } from 'app/client/lib/localization';
|
||||
import { menuDivider, menuItemCmd } from 'app/client/ui2018/menus';
|
||||
import { dom } from 'grainjs';
|
||||
|
||||
const t = makeT('RowContextMenu');
|
||||
|
||||
export interface IRowContextMenu {
|
||||
disableInsert: boolean;
|
||||
disableDelete: boolean;
|
||||
@@ -16,29 +19,29 @@ export function RowContextMenu({ disableInsert, disableDelete, isViewSorted, num
|
||||
// bottom. It could be very confusing for users who might expect the record to stay above or
|
||||
// below the active row. Thus in this case we show a single `insert row` command.
|
||||
result.push(
|
||||
menuItemCmd(allCommands.insertRecordAfter, 'Insert row',
|
||||
menuItemCmd(allCommands.insertRecordAfter, t('InsertRow'),
|
||||
dom.cls('disabled', disableInsert)),
|
||||
);
|
||||
} else {
|
||||
result.push(
|
||||
menuItemCmd(allCommands.insertRecordBefore, 'Insert row above',
|
||||
menuItemCmd(allCommands.insertRecordBefore, t('InsertRowAbove'),
|
||||
dom.cls('disabled', disableInsert)),
|
||||
menuItemCmd(allCommands.insertRecordAfter, 'Insert row below',
|
||||
menuItemCmd(allCommands.insertRecordAfter, t('InsertRowBelow'),
|
||||
dom.cls('disabled', disableInsert)),
|
||||
);
|
||||
}
|
||||
result.push(
|
||||
menuItemCmd(allCommands.duplicateRows, `Duplicate ${numRows === 1 ? 'row' : 'rows'}`,
|
||||
menuItemCmd(allCommands.duplicateRows, t('DuplicateRows', { count: numRows }),
|
||||
dom.cls('disabled', disableInsert || numRows === 0)),
|
||||
);
|
||||
result.push(
|
||||
menuDivider(),
|
||||
// TODO: should show `Delete ${num} rows` when multiple are selected
|
||||
menuItemCmd(allCommands.deleteRecords, 'Delete',
|
||||
menuItemCmd(allCommands.deleteRecords, t('Delete'),
|
||||
dom.cls('disabled', disableDelete)),
|
||||
);
|
||||
result.push(
|
||||
menuDivider(),
|
||||
menuItemCmd(allCommands.copyLink, 'Copy anchor link'));
|
||||
menuItemCmd(allCommands.copyLink, t('CopyAnchorLink')));
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ import * as roles from 'app/common/roles';
|
||||
import {Document} from 'app/common/UserAPI';
|
||||
import {dom, DomContents, styled} from 'grainjs';
|
||||
import {MenuCreateFunc} from 'popweasel';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
|
||||
const t = makeT('ShareMenu');
|
||||
|
||||
function buildOriginalUrlId(urlId: string, isSnapshot: boolean): string {
|
||||
const parts = parseUrlId(urlId);
|
||||
@@ -32,18 +35,18 @@ export function buildShareMenuButton(pageModel: DocPageModel): DomContents {
|
||||
// available (a user quick enough to open the menu in this state would have to re-open it).
|
||||
return dom.maybe(pageModel.currentDoc, (doc) => {
|
||||
const appModel = pageModel.appModel;
|
||||
const saveCopy = () => makeCopy(doc, appModel, 'Save Document').catch(reportError);
|
||||
const saveCopy = () => makeCopy(doc, appModel, t('SaveDocument')).catch(reportError);
|
||||
if (doc.idParts.snapshotId) {
|
||||
const backToCurrent = () => urlState().pushUrl({doc: buildOriginalUrlId(doc.id, true)});
|
||||
return shareButton('Back to Current', () => [
|
||||
return shareButton(t('BackToCurrent'), () => [
|
||||
menuManageUsers(doc, pageModel),
|
||||
menuSaveCopy('Save Copy', doc, appModel),
|
||||
menuSaveCopy(t('SaveCopy'), doc, appModel),
|
||||
menuOriginal(doc, appModel, true),
|
||||
menuExports(doc, pageModel),
|
||||
], {buttonAction: backToCurrent});
|
||||
} else if (doc.isPreFork || doc.isBareFork) {
|
||||
// A new unsaved document, or a fiddle, or a public example.
|
||||
const saveActionTitle = doc.isBareFork ? 'Save Document' : 'Save Copy';
|
||||
const saveActionTitle = doc.isBareFork ? t('SaveDocument') : t('SaveCopy');
|
||||
return shareButton(saveActionTitle, () => [
|
||||
menuManageUsers(doc, pageModel),
|
||||
menuSaveCopy(saveActionTitle, doc, appModel),
|
||||
@@ -55,16 +58,16 @@ export function buildShareMenuButton(pageModel: DocPageModel): DomContents {
|
||||
// Copy" primary and keep it as an action button on top. Otherwise, show a tag without a
|
||||
// default action; click opens the menu where the user can choose.
|
||||
if (!roles.canEdit(doc.trunkAccess || null)) {
|
||||
return shareButton('Save Copy', () => [
|
||||
return shareButton(t('SaveCopy'), () => [
|
||||
menuManageUsers(doc, pageModel),
|
||||
menuSaveCopy('Save Copy', doc, appModel),
|
||||
menuSaveCopy(t('SaveCopy'), doc, appModel),
|
||||
menuOriginal(doc, appModel, false),
|
||||
menuExports(doc, pageModel),
|
||||
], {buttonAction: saveCopy});
|
||||
} else {
|
||||
return shareButton('Unsaved', () => [
|
||||
return shareButton(t('Unsaved'), () => [
|
||||
menuManageUsers(doc, pageModel),
|
||||
menuSaveCopy('Save Copy', doc, appModel),
|
||||
menuSaveCopy(t('SaveCopy'), doc, appModel),
|
||||
menuOriginal(doc, appModel, false),
|
||||
menuExports(doc, pageModel),
|
||||
]);
|
||||
@@ -72,7 +75,7 @@ export function buildShareMenuButton(pageModel: DocPageModel): DomContents {
|
||||
} else {
|
||||
return shareButton(null, () => [
|
||||
menuManageUsers(doc, pageModel),
|
||||
menuSaveCopy('Duplicate Document', doc, appModel),
|
||||
menuSaveCopy(t('DuplicateDocument'), doc, appModel),
|
||||
menuWorkOnCopy(pageModel),
|
||||
menuExports(doc, pageModel),
|
||||
]);
|
||||
@@ -129,7 +132,7 @@ function shareButton(buttonText: string|null, menuCreateFunc: MenuCreateFunc,
|
||||
function menuManageUsers(doc: DocInfo, pageModel: DocPageModel) {
|
||||
return [
|
||||
menuItem(() => manageUsers(doc, pageModel),
|
||||
roles.canEditAccess(doc.access) ? 'Manage Users' : 'Access Details',
|
||||
roles.canEditAccess(doc.access) ? t('ManageUsers') : t('AccessDetails'),
|
||||
dom.cls('disabled', doc.isFork),
|
||||
testId('tb-share-option')
|
||||
),
|
||||
@@ -140,7 +143,7 @@ function menuManageUsers(doc: DocInfo, pageModel: DocPageModel) {
|
||||
// Renders "Return to Original" and "Replace Original" menu items. When used with snapshots, we
|
||||
// say "Current Version" in place of the word "Original".
|
||||
function menuOriginal(doc: Document, appModel: AppModel, isSnapshot: boolean) {
|
||||
const termToUse = isSnapshot ? "Current Version" : "Original";
|
||||
const termToUse = isSnapshot ? t("CurrentVersion") : t("Original");
|
||||
const origUrlId = buildOriginalUrlId(doc.id, isSnapshot);
|
||||
const originalUrl = urlState().makeUrl({doc: origUrlId});
|
||||
|
||||
@@ -163,18 +166,18 @@ function menuOriginal(doc: Document, appModel: AppModel, isSnapshot: boolean) {
|
||||
}
|
||||
return [
|
||||
cssMenuSplitLink({href: originalUrl},
|
||||
cssMenuSplitLinkText(`Return to ${termToUse}`), testId('return-to-original'),
|
||||
cssMenuSplitLinkText(t('ReturnToTermToUse', {termToUse})), testId('return-to-original'),
|
||||
cssMenuIconLink({href: originalUrl, target: '_blank'}, testId('open-original'),
|
||||
cssMenuIcon('FieldLink'),
|
||||
)
|
||||
),
|
||||
menuItem(replaceOriginal, `Replace ${termToUse}...`,
|
||||
menuItem(replaceOriginal, t('ReplaceTermToUse', {termToUse}),
|
||||
// Disable if original is not writable, and also when comparing snapshots (since it's
|
||||
// unclear which of the versions to use).
|
||||
dom.cls('disabled', !roles.canEdit(doc.trunkAccess || null) || comparingSnapshots),
|
||||
testId('replace-original'),
|
||||
),
|
||||
menuItemLink(compareHref, {target: '_blank'}, `Compare to ${termToUse}`,
|
||||
menuItemLink(compareHref, {target: '_blank'}, t('CompareTermToUse', {termToUse}),
|
||||
menuAnnotate('Beta'),
|
||||
testId('compare-original'),
|
||||
),
|
||||
@@ -202,10 +205,10 @@ function menuWorkOnCopy(pageModel: DocPageModel) {
|
||||
};
|
||||
|
||||
return [
|
||||
menuItem(makeUnsavedCopy, 'Work on a Copy', testId('work-on-copy')),
|
||||
menuItem(makeUnsavedCopy, t('WorkOnCopy'), testId('work-on-copy')),
|
||||
menuText(
|
||||
withInfoTooltip(
|
||||
'Edit without affecting the original',
|
||||
t('EditWithoutAffecting'),
|
||||
GristTooltips.workOnACopy(),
|
||||
{tooltipMenuOptions: {attach: null}}
|
||||
)
|
||||
@@ -226,21 +229,21 @@ function menuExports(doc: Document, pageModel: DocPageModel) {
|
||||
menuDivider(),
|
||||
(isElectron ?
|
||||
menuItem(() => gristDoc.app.comm.showItemInFolder(doc.name),
|
||||
'Show in folder', testId('tb-share-option')) :
|
||||
t('ShowInFolder'), testId('tb-share-option')) :
|
||||
menuItemLink({
|
||||
href: pageModel.appModel.api.getDocAPI(doc.id).getDownloadUrl(),
|
||||
target: '_blank', download: ''
|
||||
},
|
||||
menuIcon('Download'), 'Download', testId('tb-share-option'))
|
||||
menuIcon('Download'), t('Download'), testId('tb-share-option'))
|
||||
),
|
||||
menuItemLink({ href: gristDoc.getCsvLink(), target: '_blank', download: ''},
|
||||
menuIcon('Download'), 'Export CSV', testId('tb-share-option')),
|
||||
menuIcon('Download'), t('ExportCSV'), testId('tb-share-option')),
|
||||
menuItemLink({
|
||||
href: pageModel.appModel.api.getDocAPI(doc.id).getDownloadXlsxUrl(),
|
||||
target: '_blank', download: ''
|
||||
}, menuIcon('Download'), 'Export XLSX', testId('tb-share-option')),
|
||||
}, menuIcon('Download'), t('ExportXLSX'), testId('tb-share-option')),
|
||||
menuItem(() => sendToDrive(doc, pageModel),
|
||||
menuIcon('Download'), 'Send to Google Drive', testId('tb-share-option')),
|
||||
menuIcon('Download'), t('SendToGoogleDrive'), testId('tb-share-option')),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import {dom, makeTestId, styled} from 'grainjs';
|
||||
import {getSingleOrg, shouldHideUiElement} from 'app/common/gristUrls';
|
||||
import {getOrgName} from 'app/common/UserAPI';
|
||||
import {dom, makeTestId, styled} from 'grainjs';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {AppModel} from 'app/client/models/AppModel';
|
||||
import {urlState} from 'app/client/models/gristUrlState';
|
||||
import {theme} from 'app/client/ui2018/cssVars';
|
||||
import {menuDivider, menuIcon, menuItem, menuItemLink, menuSubHeader} from 'app/client/ui2018/menus';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
|
||||
const t = makeT('SiteSwitcher');
|
||||
|
||||
const testId = makeTestId('test-site-switcher-');
|
||||
|
||||
/**
|
||||
@@ -30,7 +33,7 @@ export function buildSiteSwitcher(appModel: AppModel) {
|
||||
const orgs = appModel.topAppModel.orgs;
|
||||
|
||||
return [
|
||||
menuSubHeader('Switch Sites'),
|
||||
menuSubHeader(t('SwitchSites')),
|
||||
dom.forEach(orgs, (org) =>
|
||||
menuItemLink(urlState().setLinkUrl({ org: org.domain || undefined }),
|
||||
cssOrgSelected.cls('', appModel.currentOrg ? org.id === appModel.currentOrg.id : false),
|
||||
@@ -42,7 +45,7 @@ export function buildSiteSwitcher(appModel: AppModel) {
|
||||
menuItem(
|
||||
() => appModel.showNewSiteModal(),
|
||||
menuIcon('Plus'),
|
||||
'Create new team site',
|
||||
t('CreateNewTeamSite'),
|
||||
testId('create-new-site'),
|
||||
),
|
||||
];
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {AppModel} from 'app/client/models/AppModel';
|
||||
import * as css from 'app/client/ui/AccountPageCss';
|
||||
import {labeledSquareCheckbox} from 'app/client/ui2018/checkbox';
|
||||
@@ -6,6 +7,7 @@ import {ThemeAppearance} from 'app/common/ThemePrefs';
|
||||
import {Computed, Disposable, dom, makeTestId, styled} from 'grainjs';
|
||||
|
||||
const testId = makeTestId('test-theme-config-');
|
||||
const t = makeT('ThemeConfig');
|
||||
|
||||
export class ThemeConfig extends Disposable {
|
||||
private _themePrefs = this._appModel.themePrefs;
|
||||
@@ -24,7 +26,7 @@ export class ThemeConfig extends Disposable {
|
||||
|
||||
public buildDom() {
|
||||
return dom('div',
|
||||
css.subHeader('Appearance ', css.betaTag('Beta')),
|
||||
css.subHeader(t('Appearance'), css.betaTag('Beta')),
|
||||
css.dataRow(
|
||||
cssAppearanceSelect(
|
||||
select(
|
||||
@@ -40,7 +42,7 @@ export class ThemeConfig extends Disposable {
|
||||
css.dataRow(
|
||||
labeledSquareCheckbox(
|
||||
this._syncWithOS,
|
||||
'Switch appearance automatically to match system',
|
||||
t('SyncWithOS'),
|
||||
testId('sync-with-os'),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {GristDoc} from 'app/client/components/GristDoc';
|
||||
import {urlState} from 'app/client/models/gristUrlState';
|
||||
import {getUserOrgPrefObs, markAsSeen} from 'app/client/models/UserPrefs';
|
||||
import {showExampleCard} from 'app/client/ui/ExampleCard';
|
||||
import {examples} from 'app/client/ui/ExampleInfo';
|
||||
import {buildExamples} from 'app/client/ui/ExampleInfo';
|
||||
import {createHelpTools, cssLinkText, cssPageEntry, cssPageEntryMain, cssPageEntrySmall,
|
||||
cssPageIcon, cssPageLink, cssSectionHeader, cssSpacer, cssSplitPageEntry,
|
||||
cssTools} from 'app/client/ui/LeftPanelCommon';
|
||||
@@ -17,6 +18,7 @@ import {isOwner} from 'app/common/roles';
|
||||
import {Disposable, dom, makeTestId, Observable, observable, styled} from 'grainjs';
|
||||
|
||||
const testId = makeTestId('test-tools-');
|
||||
const t = makeT('Tools');
|
||||
|
||||
export function tools(owner: Disposable, gristDoc: GristDoc, leftPanelOpen: Observable<boolean>): Element {
|
||||
const docPageModel = gristDoc.docPageModel;
|
||||
@@ -31,14 +33,14 @@ export function tools(owner: Disposable, gristDoc: GristDoc, leftPanelOpen: Obse
|
||||
updateCanViewAccessRules();
|
||||
return cssTools(
|
||||
cssTools.cls('-collapsed', (use) => !use(leftPanelOpen)),
|
||||
cssSectionHeader("TOOLS"),
|
||||
cssSectionHeader(t("Tools")),
|
||||
cssPageEntry(
|
||||
cssPageEntry.cls('-selected', (use) => use(gristDoc.activeViewId) === 'acl'),
|
||||
cssPageEntry.cls('-disabled', (use) => !use(canViewAccessRules)),
|
||||
dom.domComputed(canViewAccessRules, (_canViewAccessRules) => {
|
||||
return cssPageLink(
|
||||
cssPageIcon('EyeShow'),
|
||||
cssLinkText('Access Rules',
|
||||
cssLinkText(t('AccessRules'),
|
||||
menuAnnotate('Beta', cssBetaTag.cls(''))
|
||||
),
|
||||
_canViewAccessRules ? urlState().setLinkUrl({docPage: 'acl'}) : null,
|
||||
@@ -51,35 +53,35 @@ export function tools(owner: Disposable, gristDoc: GristDoc, leftPanelOpen: Obse
|
||||
cssPageEntry.cls('-selected', (use) => use(gristDoc.activeViewId) === 'data'),
|
||||
cssPageLink(
|
||||
cssPageIcon('Database'),
|
||||
cssLinkText('Raw Data'),
|
||||
cssLinkText(t('RawData')),
|
||||
testId('raw'),
|
||||
urlState().setLinkUrl({docPage: 'data'})
|
||||
)
|
||||
),
|
||||
cssPageEntry(
|
||||
cssPageLink(cssPageIcon('Log'), cssLinkText('Document History'), testId('log'),
|
||||
cssPageLink(cssPageIcon('Log'), cssLinkText(t('DocumentHistory')), testId('log'),
|
||||
dom.on('click', () => gristDoc.showTool('docHistory')))
|
||||
),
|
||||
// TODO: polish validation and add it back
|
||||
dom.maybe((use) => use(gristDoc.app.features).validationsTool, () =>
|
||||
cssPageEntry(
|
||||
cssPageLink(cssPageIcon('Validation'), cssLinkText('Validate Data'), testId('validate'),
|
||||
cssPageLink(cssPageIcon('Validation'), cssLinkText(t('ValidateData')), testId('validate'),
|
||||
dom.on('click', () => gristDoc.showTool('validations'))))
|
||||
),
|
||||
cssPageEntry(
|
||||
cssPageEntry.cls('-selected', (use) => use(gristDoc.activeViewId) === 'code'),
|
||||
cssPageLink(cssPageIcon('Code'),
|
||||
cssLinkText('Code View'),
|
||||
cssLinkText(t('CodeView')),
|
||||
urlState().setLinkUrl({docPage: 'code'})
|
||||
),
|
||||
testId('code'),
|
||||
),
|
||||
cssSpacer(),
|
||||
dom.maybe(docPageModel.currentDoc, (doc) => {
|
||||
const ex = examples.find(e => e.urlId === doc.urlId);
|
||||
const ex = buildExamples().find(e => e.urlId === doc.urlId);
|
||||
if (!ex || !ex.tutorialUrl) { return null; }
|
||||
return cssPageEntry(
|
||||
cssPageLink(cssPageIcon('Page'), cssLinkText('How-to Tutorial'), testId('tutorial'),
|
||||
cssPageLink(cssPageIcon('Page'), cssLinkText(t('HowToTutorial')), testId('tutorial'),
|
||||
{href: ex.tutorialUrl, target: '_blank'},
|
||||
cssExampleCardOpener(
|
||||
icon('TypeDetails'),
|
||||
@@ -99,14 +101,14 @@ export function tools(owner: Disposable, gristDoc: GristDoc, leftPanelOpen: Obse
|
||||
cssSplitPageEntry(
|
||||
cssPageEntryMain(
|
||||
cssPageLink(cssPageIcon('Page'),
|
||||
cssLinkText('Tour of this Document'),
|
||||
cssLinkText(t('DocumentTour')),
|
||||
urlState().setLinkUrl({docTour: true}),
|
||||
testId('doctour'),
|
||||
),
|
||||
),
|
||||
!isDocOwner ? null : cssPageEntrySmall(
|
||||
cssPageLink(cssPageIcon('Remove'),
|
||||
dom.on('click', () => confirmModal('Delete document tour?', 'Delete', () =>
|
||||
dom.on('click', () => confirmModal(t('DeleteDocumentTour'), t('Delete'), () =>
|
||||
gristDoc.docData.sendAction(['RemoveTable', 'GristDocTour']))
|
||||
),
|
||||
testId('remove-doctour')
|
||||
@@ -193,7 +195,7 @@ function addRevertViewAsUI() {
|
||||
// A tooltip that allows reverting back to yourself.
|
||||
hoverTooltip((ctl) =>
|
||||
cssConvertTooltip(icon('Convert'),
|
||||
cssLink('Return to viewing as yourself',
|
||||
cssLink(t('ViewingAsYourself'),
|
||||
urlState().setHref(userOverrideParams(null, {docPage: 'acl'})),
|
||||
),
|
||||
tooltipCloseButton(ctl),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {GristDoc} from 'app/client/components/GristDoc';
|
||||
import {loadSearch} from 'app/client/lib/imports';
|
||||
import {AppModel, reportError} from 'app/client/models/AppModel';
|
||||
@@ -19,6 +20,8 @@ import {waitGrainObs} from 'app/common/gutil';
|
||||
import * as roles from 'app/common/roles';
|
||||
import {Computed, dom, DomElementArg, makeTestId, MultiHolder, Observable, styled} from 'grainjs';
|
||||
|
||||
const t = makeT('TopBar');
|
||||
|
||||
export function createTopBarHome(appModel: AppModel) {
|
||||
return [
|
||||
cssFlexSpace(),
|
||||
@@ -26,7 +29,7 @@ export function createTopBarHome(appModel: AppModel) {
|
||||
(appModel.isTeamSite && roles.canEditAccess(appModel.currentOrg?.access || null) ?
|
||||
[
|
||||
basicButton(
|
||||
'Manage Team',
|
||||
t('ManageTeam'),
|
||||
dom.on('click', () => manageTeamUsersApp(appModel)),
|
||||
testId('topbar-manage-team')
|
||||
),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import type {ColumnRec} from 'app/client/models/entities/ColumnRec';
|
||||
import type {TableRec} from 'app/client/models/entities/TableRec';
|
||||
import {reportError} from 'app/client/models/errors';
|
||||
@@ -17,6 +18,8 @@ import {Computed, dom, IDisposableOwner, MultiHolder, Observable, styled} from '
|
||||
import {cssMenu, cssMenuItem, defaultMenuOptions, IOpenController, setPopupToCreateDom} from "popweasel";
|
||||
import isEqual = require('lodash/isEqual');
|
||||
|
||||
const t = makeT('TriggerFormulas');
|
||||
|
||||
/**
|
||||
* Build UI to select triggers for formulas in data columns (such for default values).
|
||||
*/
|
||||
@@ -70,7 +73,7 @@ export function buildFormulaTriggers(owner: MultiHolder, column: ColumnRec, opti
|
||||
const docModel = column._table.docModel;
|
||||
const summaryText = Computed.create(owner, use => {
|
||||
if (use(column.recalcWhen) === RecalcWhen.MANUAL_UPDATES) {
|
||||
return 'Any field';
|
||||
return t('AnyField');
|
||||
}
|
||||
const deps = decodeObject(use(column.recalcDeps)) as number[]|null;
|
||||
if (!deps || deps.length === 0) { return ''; }
|
||||
@@ -95,7 +98,7 @@ export function buildFormulaTriggers(owner: MultiHolder, column: ColumnRec, opti
|
||||
cssRow(
|
||||
labeledSquareCheckbox(
|
||||
applyToNew,
|
||||
'Apply to new records',
|
||||
t('NewRecords'),
|
||||
dom.boolAttr('disabled', newRowsDisabled),
|
||||
testId('field-formula-apply-to-new'),
|
||||
),
|
||||
@@ -104,8 +107,8 @@ export function buildFormulaTriggers(owner: MultiHolder, column: ColumnRec, opti
|
||||
labeledSquareCheckbox(
|
||||
applyOnChanges,
|
||||
dom.text(use => use(applyOnChanges) ?
|
||||
'Apply on changes to:' :
|
||||
'Apply on record changes'
|
||||
t('ChangesTo') :
|
||||
t('RecordChanges')
|
||||
),
|
||||
dom.boolAttr('disabled', changesDisabled),
|
||||
testId('field-formula-apply-on-changes'),
|
||||
@@ -197,14 +200,14 @@ function buildTriggerSelectors(ctl: IOpenController, tableRec: TableRec, column:
|
||||
cssItemsFixed(
|
||||
cssSelectorItem(
|
||||
labeledSquareCheckbox(current,
|
||||
['Current field ', cssSelectorNote('(data cleaning)')],
|
||||
[t('CurrentField'), cssSelectorNote('(data cleaning)')],
|
||||
dom.boolAttr('disabled', allUpdates),
|
||||
),
|
||||
),
|
||||
menuDivider(),
|
||||
cssSelectorItem(
|
||||
labeledSquareCheckbox(allUpdates,
|
||||
['Any field ', cssSelectorNote('(except formulas)')]
|
||||
[`${t('AnyField')} `, cssSelectorNote('(except formulas)')]
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -221,12 +224,12 @@ function buildTriggerSelectors(ctl: IOpenController, tableRec: TableRec, column:
|
||||
cssItemsFixed(
|
||||
cssSelectorFooter(
|
||||
dom.maybe(isChanged, () =>
|
||||
primaryButton('OK',
|
||||
primaryButton(t('OK'),
|
||||
dom.on('click', () => close(true)),
|
||||
testId('trigger-deps-apply')
|
||||
),
|
||||
),
|
||||
basicButton(dom.text(use => use(isChanged) ? 'Cancel' : 'Close'),
|
||||
basicButton(dom.text(use => use(isChanged) ? t('Cancel') : t('Close')),
|
||||
dom.on('click', () => close(false)),
|
||||
testId('trigger-deps-cancel')
|
||||
),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {allCommands} from 'app/client/components/commands';
|
||||
import {ViewSectionRec} from 'app/client/models/DocModel';
|
||||
import {urlState} from 'app/client/models/gristUrlState';
|
||||
@@ -5,6 +6,8 @@ import {testId} from 'app/client/ui2018/cssVars';
|
||||
import {menuDivider, menuItemCmd, menuItemLink} from 'app/client/ui2018/menus';
|
||||
import {dom} from 'grainjs';
|
||||
|
||||
const t = makeT('ViewLayoutMenu');
|
||||
|
||||
/**
|
||||
* Returns a list of menu items for a view section.
|
||||
*/
|
||||
@@ -21,11 +24,11 @@ export function makeViewLayoutMenu(viewSection: ViewSectionRec, isReadonly: bool
|
||||
|
||||
const contextMenu = [
|
||||
menuItemCmd(allCommands.deleteRecords,
|
||||
'Delete record',
|
||||
t('DeleteRecord'),
|
||||
testId('section-delete-card'),
|
||||
dom.cls('disabled', isReadonly || isAddRow)),
|
||||
menuItemCmd(allCommands.copyLink,
|
||||
'Copy anchor link',
|
||||
t('CopyAnchorLink'),
|
||||
testId('section-card-link'),
|
||||
),
|
||||
menuDivider(),
|
||||
@@ -36,30 +39,30 @@ export function makeViewLayoutMenu(viewSection: ViewSectionRec, isReadonly: bool
|
||||
return [
|
||||
dom.maybe((use) => ['single'].includes(use(viewSection.parentKey)), () => contextMenu),
|
||||
dom.maybe((use) => !use(viewSection.isRaw) && !isLight,
|
||||
() => menuItemCmd(allCommands.showRawData, 'Show raw data', testId('show-raw-data')),
|
||||
() => menuItemCmd(allCommands.showRawData, t('ShowRawData'), testId('show-raw-data')),
|
||||
),
|
||||
menuItemCmd(allCommands.printSection, 'Print widget', testId('print-section')),
|
||||
menuItemCmd(allCommands.printSection, t('PrintWidget'), testId('print-section')),
|
||||
menuItemLink({ href: gristDoc.getCsvLink(), target: '_blank', download: ''},
|
||||
'Download as CSV', testId('download-section')),
|
||||
t('DownloadCSV'), testId('download-section')),
|
||||
menuItemLink({ href: gristDoc.getXlsxActiveViewLink(), target: '_blank', download: ''},
|
||||
'Download as XLSX', testId('download-section')),
|
||||
t('DownloadXLSX'), testId('download-section')),
|
||||
dom.maybe((use) => ['detail', 'single'].includes(use(viewSection.parentKey)), () =>
|
||||
menuItemCmd(allCommands.editLayout, 'Edit Card Layout',
|
||||
menuItemCmd(allCommands.editLayout, t('EditCardLayout'),
|
||||
dom.cls('disabled', isReadonly))),
|
||||
|
||||
dom.maybe(!isLight, () => [
|
||||
menuDivider(),
|
||||
menuItemCmd(allCommands.viewTabOpen, 'Widget options', testId('widget-options')),
|
||||
menuItemCmd(allCommands.sortFilterTabOpen, 'Advanced Sort & Filter'),
|
||||
menuItemCmd(allCommands.dataSelectionTabOpen, 'Data selection'),
|
||||
menuItemCmd(allCommands.viewTabOpen, t('WidgetOptions'), testId('widget-options')),
|
||||
menuItemCmd(allCommands.sortFilterTabOpen, t('AdvancedSortFilter')),
|
||||
menuItemCmd(allCommands.dataSelectionTabOpen, t('DataSelection')),
|
||||
]),
|
||||
|
||||
menuDivider(),
|
||||
dom.maybe((use) => use(viewSection.parentKey) === 'custom' && use(viewSection.hasCustomOptions), () =>
|
||||
menuItemCmd(allCommands.openWidgetConfiguration, 'Open configuration',
|
||||
menuItemCmd(allCommands.openWidgetConfiguration, t('OpenConfiguration'),
|
||||
testId('section-open-configuration')),
|
||||
),
|
||||
menuItemCmd(allCommands.deleteSection, 'Delete widget',
|
||||
menuItemCmd(allCommands.deleteSection, t('DeleteWidget'),
|
||||
dom.cls('disabled', !viewRec.getRowId() || viewRec.viewSections().peekLength <= 1 || isReadonly),
|
||||
testId('section-delete')),
|
||||
];
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {reportError} from 'app/client/models/AppModel';
|
||||
import {ColumnRec, DocModel, ViewSectionRec} from 'app/client/models/DocModel';
|
||||
import {FilterInfo} from 'app/client/models/entities/ViewSectionRec';
|
||||
@@ -16,10 +17,11 @@ import {PopupControl} from 'popweasel';
|
||||
import difference = require('lodash/difference');
|
||||
|
||||
const testId = makeTestId('test-section-menu-');
|
||||
const t = makeT('ViewSectionMenu');
|
||||
|
||||
// Handler for [Save] button.
|
||||
async function doSave(docModel: DocModel, viewSection: ViewSectionRec): Promise<void> {
|
||||
await docModel.docData.bundleActions("Update Sort&Filter settings", () => Promise.all([
|
||||
await docModel.docData.bundleActions(t("UpdateSortFilterSettings"), () => Promise.all([
|
||||
viewSection.activeSortJson.save(), // Save sort
|
||||
viewSection.saveFilters(), // Save filter
|
||||
viewSection.activeFilterBar.save(), // Save bar
|
||||
@@ -92,10 +94,10 @@ export function viewSectionMenu(owner: IDisposableOwner, docModel: DocModel, vie
|
||||
// [Save] [Revert] buttons
|
||||
dom.domComputed(displaySaveObs, displaySave => [
|
||||
displaySave ? cssMenuInfoHeader(
|
||||
cssSaveButton('Save', testId('btn-save'),
|
||||
cssSaveButton(t('Save'), testId('btn-save'),
|
||||
dom.on('click', () => { save(); ctl.close(); }),
|
||||
dom.boolAttr('disabled', isReadonly)),
|
||||
basicButton('Revert', testId('btn-revert'),
|
||||
basicButton(t('Revert'), testId('btn-revert'),
|
||||
dom.on('click', () => { revert(); ctl.close(); }))
|
||||
) : null,
|
||||
]),
|
||||
@@ -160,7 +162,7 @@ function makeSortPanel(section: ViewSectionRec, sortSpec: Sort.SortSpec, getColu
|
||||
});
|
||||
|
||||
return [
|
||||
cssMenuInfoHeader('Sorted by', testId('heading-sorted')),
|
||||
cssMenuInfoHeader(t('SortedBy'), testId('heading-sorted')),
|
||||
sortColumns.length > 0 ? sortColumns : cssGrayedMenuText('(Default)')
|
||||
];
|
||||
}
|
||||
@@ -181,7 +183,7 @@ export function makeAddFilterButton(viewSectionRec: ViewSectionRec, popupControl
|
||||
testId('plus-button'),
|
||||
dom.on('click', (ev) => ev.stopPropagation()),
|
||||
),
|
||||
cssMenuTextLabel('Add Filter'),
|
||||
cssMenuTextLabel(t('AddFilter')),
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -201,7 +203,7 @@ export function makeFilterBarToggle(activeFilterBar: CustomComputed<boolean>) {
|
||||
}),
|
||||
),
|
||||
dom.on('click', () => activeFilterBar(!activeFilterBar.peek())),
|
||||
cssMenuTextLabel("Toggle Filter Bar"),
|
||||
cssMenuTextLabel(t("ToggleFilterBar")),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -235,7 +237,7 @@ function makeFilterPanel(section: ViewSectionRec, activeFilters: FilterInfo[],
|
||||
});
|
||||
|
||||
return [
|
||||
cssMenuInfoHeader('Filtered by', {style: 'margin-top: 4px'}, testId('heading-filtered')),
|
||||
cssMenuInfoHeader(t('FilteredBy'), {style: 'margin-top: 4px'}, testId('heading-filtered')),
|
||||
activeFilters.length > 0 ? filters : cssGrayedMenuText('(Not filtered)')
|
||||
];
|
||||
}
|
||||
@@ -247,13 +249,13 @@ function makeCustomOptions(section: ViewSectionRec) {
|
||||
const color = Computed.create(null, use => use(section.activeCustomOptions.isSaved) ? "-gray" : "-green");
|
||||
const text = Computed.create(null, use => {
|
||||
if (use(section.activeCustomOptions)) {
|
||||
return use(section.activeCustomOptions.isSaved) ? "(customized)" : "(modified)";
|
||||
return use(section.activeCustomOptions.isSaved) ? t("Customized") : t("Modified");
|
||||
} else {
|
||||
return "(empty)";
|
||||
return t("Empty");
|
||||
}
|
||||
});
|
||||
return [
|
||||
cssMenuInfoHeader('Custom options', testId('heading-widget-options')),
|
||||
cssMenuInfoHeader(t('CustomOptions'), testId('heading-widget-options')),
|
||||
cssMenuText(
|
||||
dom.autoDispose(text),
|
||||
dom.autoDispose(color),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { GristDoc } from "app/client/components/GristDoc";
|
||||
import { KoArray, syncedKoArray } from "app/client/lib/koArray";
|
||||
import * as kf from 'app/client/lib/koForm';
|
||||
import { makeT } from 'app/client/lib/localization';
|
||||
import * as tableUtil from 'app/client/lib/tableUtil';
|
||||
import { ColumnRec, ViewFieldRec, ViewSectionRec } from "app/client/models/DocModel";
|
||||
import { getFieldType } from "app/client/ui/RightPanel";
|
||||
@@ -16,6 +17,7 @@ import difference = require("lodash/difference");
|
||||
import isEqual = require("lodash/isEqual");
|
||||
|
||||
const testId = makeTestId('test-vfc-');
|
||||
const t = makeT('VisibleFieldsConfig');
|
||||
|
||||
export type IField = ViewFieldRec|ColumnRec;
|
||||
|
||||
@@ -161,8 +163,8 @@ export class VisibleFieldsConfig extends Disposable {
|
||||
options.hiddenFields.itemCreateFunc,
|
||||
{
|
||||
itemClass: cssDragRow.className,
|
||||
reorder() { throw new Error('Hidden Fields cannot be reordered'); },
|
||||
receive() { throw new Error('Cannot drop items into Hidden Fields'); },
|
||||
reorder() { throw new Error(t('NoReorderHiddenField')); },
|
||||
receive() { throw new Error(t('NoDropInHiddenField')); },
|
||||
remove(item: ColumnRec) {
|
||||
// Return the column object. This value is passed to the viewFields
|
||||
// receive function as its respective item parameter
|
||||
@@ -202,7 +204,7 @@ export class VisibleFieldsConfig extends Disposable {
|
||||
() => (
|
||||
cssControlLabel(
|
||||
icon('Tick'),
|
||||
'Select All',
|
||||
t('SelectAll'),
|
||||
dom.on('click', () => this._setVisibleCheckboxes(fieldsDraggable, true)),
|
||||
testId('visible-fields-select-all'),
|
||||
)
|
||||
@@ -217,7 +219,7 @@ export class VisibleFieldsConfig extends Disposable {
|
||||
dom.on('click', () => this._removeSelectedFields()),
|
||||
),
|
||||
basicButton(
|
||||
'Clear',
|
||||
t('Clear'),
|
||||
dom.on('click', () => this._setVisibleCheckboxes(fieldsDraggable, false)),
|
||||
),
|
||||
testId('visible-batch-buttons')
|
||||
@@ -238,7 +240,7 @@ export class VisibleFieldsConfig extends Disposable {
|
||||
() => (
|
||||
cssControlLabel(
|
||||
icon('Tick'),
|
||||
'Select All',
|
||||
t('SelectAll'),
|
||||
dom.on('click', () => this._setHiddenCheckboxes(hiddenFieldsDraggable, true)),
|
||||
testId('hidden-fields-select-all'),
|
||||
)
|
||||
@@ -259,7 +261,7 @@ export class VisibleFieldsConfig extends Disposable {
|
||||
dom.on('click', () => this._addSelectedFields()),
|
||||
),
|
||||
basicButton(
|
||||
'Clear',
|
||||
t('Clear'),
|
||||
dom.on('click', () => this._setHiddenCheckboxes(hiddenFieldsDraggable, false)),
|
||||
),
|
||||
testId('hidden-batch-buttons')
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import * as commands from 'app/client/components/commands';
|
||||
import {getUserPrefObs} from 'app/client/models/UserPrefs';
|
||||
import {colors, testId} from 'app/client/ui2018/cssVars';
|
||||
@@ -9,6 +10,8 @@ import {UserPrefs} from 'app/common/Prefs';
|
||||
import {getGristConfig} from 'app/common/urlUtils';
|
||||
import {dom, input, Observable, styled, subscribeElem} from 'grainjs';
|
||||
|
||||
const t = makeT('WelcomeQuestions');
|
||||
|
||||
export function showWelcomeQuestions(userPrefsObs: Observable<UserPrefs>) {
|
||||
if (!(getGristConfig().survey && userPrefsObs.get()?.showNewUserQuestions)) {
|
||||
return null;
|
||||
@@ -20,9 +23,9 @@ export function showWelcomeQuestions(userPrefsObs: Observable<UserPrefs>) {
|
||||
const showQuestions = getUserPrefObs(userPrefsObs, 'showNewUserQuestions');
|
||||
|
||||
async function onConfirm() {
|
||||
const selected = choices.filter((c, i) => selection[i].get()).map(c => c.text);
|
||||
const selected = choices.filter((c, i) => selection[i].get()).map(c => t(c.textKey));
|
||||
const use_cases = ['L', ...selected]; // Format to populate a ChoiceList column
|
||||
const use_other = selected.includes('Other') ? otherText.get() : '';
|
||||
const use_other = selected.includes(t('Other')) ? otherText.get() : '';
|
||||
|
||||
const submitUrl = new URL(window.location.href);
|
||||
submitUrl.pathname = '/welcome/info';
|
||||
@@ -42,7 +45,7 @@ export function showWelcomeQuestions(userPrefsObs: Observable<UserPrefs>) {
|
||||
});
|
||||
|
||||
return {
|
||||
title: [cssLogo(), dom('div', 'Welcome to Grist!')],
|
||||
title: [cssLogo(), dom('div', t('WelcomeToGrist'))],
|
||||
body: buildInfoForm(selection, otherText),
|
||||
saveLabel: 'Start using Grist',
|
||||
saveFunc: onConfirm,
|
||||
@@ -53,32 +56,32 @@ export function showWelcomeQuestions(userPrefsObs: Observable<UserPrefs>) {
|
||||
});
|
||||
}
|
||||
|
||||
const choices: Array<{icon: IconName, color: string, text: string}> = [
|
||||
{icon: 'UseProduct', color: `${colors.lightGreen}`, text: 'Product Development' },
|
||||
{icon: 'UseFinance', color: '#0075A2', text: 'Finance & Accounting'},
|
||||
{icon: 'UseMedia', color: '#F7B32B', text: 'Media Production' },
|
||||
{icon: 'UseMonitor', color: '#F2545B', text: 'IT & Technology' },
|
||||
{icon: 'UseChart', color: '#7141F9', text: 'Marketing' },
|
||||
{icon: 'UseScience', color: '#231942', text: 'Research' },
|
||||
{icon: 'UseSales', color: '#885A5A', text: 'Sales' },
|
||||
{icon: 'UseEducate', color: '#4A5899', text: 'Education' },
|
||||
{icon: 'UseHr', color: '#688047', text: 'HR & Management' },
|
||||
{icon: 'UseOther', color: '#929299', text: 'Other' },
|
||||
const choices: Array<{icon: IconName, color: string, textKey: string}> = [
|
||||
{icon: 'UseProduct', color: `${colors.lightGreen}`, textKey: 'ProductDevelopment' },
|
||||
{icon: 'UseFinance', color: '#0075A2', textKey: 'FinanceAccounting' },
|
||||
{icon: 'UseMedia', color: '#F7B32B', textKey: 'MediaProduction' },
|
||||
{icon: 'UseMonitor', color: '#F2545B', textKey: 'ITTechnology' },
|
||||
{icon: 'UseChart', color: '#7141F9', textKey: 'Marketing' },
|
||||
{icon: 'UseScience', color: '#231942', textKey: 'Research' },
|
||||
{icon: 'UseSales', color: '#885A5A', textKey: 'Sales' },
|
||||
{icon: 'UseEducate', color: '#4A5899', textKey: 'Education' },
|
||||
{icon: 'UseHr', color: '#688047', textKey: 'HRManagement' },
|
||||
{icon: 'UseOther', color: '#929299', textKey: 'Other' },
|
||||
];
|
||||
|
||||
function buildInfoForm(selection: Observable<boolean>[], otherText: Observable<string>) {
|
||||
return [
|
||||
dom('span', 'What brings you to Grist? Please help us serve you better.'),
|
||||
dom('span', t('WhatBringsYouToGrist')),
|
||||
cssChoices(
|
||||
choices.map((item, i) => cssChoice(
|
||||
cssIcon(icon(item.icon), {style: `--icon-color: ${item.color}`}),
|
||||
cssChoice.cls('-selected', selection[i]),
|
||||
dom.on('click', () => selection[i].set(!selection[i].get())),
|
||||
(item.icon !== 'UseOther' ?
|
||||
item.text :
|
||||
t(item.textKey) :
|
||||
[
|
||||
cssOtherLabel(item.text),
|
||||
cssOtherInput(otherText, {}, {type: 'text', placeholder: 'Type here'},
|
||||
cssOtherLabel(t(item.textKey)),
|
||||
cssOtherInput(otherText, {}, {type: 'text', placeholder: t('TypeHere')},
|
||||
// The following subscribes to changes to selection observable, and focuses the input when
|
||||
// this item is selected.
|
||||
(elem) => subscribeElem(elem, selection[i], val => val && setTimeout(() => elem.focus(), 0)),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {FocusLayer} from 'app/client/lib/FocusLayer';
|
||||
import {ViewSectionRec} from 'app/client/models/entities/ViewSectionRec';
|
||||
import {basicButton, cssButton, primaryButton} from 'app/client/ui2018/buttons';
|
||||
@@ -9,6 +10,7 @@ import {Computed, dom, DomElementArg, IInputOptions, input, makeTestId, Observab
|
||||
import {IOpenController, setPopupToCreateDom} from 'popweasel';
|
||||
|
||||
const testId = makeTestId('test-widget-title-');
|
||||
const t = makeT('WidgetTitle');
|
||||
|
||||
interface WidgetTitleOptions {
|
||||
tableNameHidden?: boolean,
|
||||
@@ -65,7 +67,7 @@ function buildWidgetRenamePopup(ctrl: IOpenController, vs: ViewSectionRec, optio
|
||||
// Placeholder for widget title:
|
||||
// - when widget title is empty shows a default widget title (what would be shown when title is empty)
|
||||
// - when widget title is set, shows just a text to override it.
|
||||
const inputWidgetPlaceholder = !vs.title.peek() ? 'Override widget title' : vs.defaultWidgetTitle.peek();
|
||||
const inputWidgetPlaceholder = !vs.title.peek() ? t('OverrideTitle') : vs.defaultWidgetTitle.peek();
|
||||
|
||||
const disableSave = Computed.create(ctrl, (use) => {
|
||||
const newTableName = use(inputTableName)?.trim() ?? '';
|
||||
@@ -135,29 +137,29 @@ function buildWidgetRenamePopup(ctrl: IOpenController, vs: ViewSectionRec, optio
|
||||
testId('popup'),
|
||||
dom.cls(menuCssClass),
|
||||
dom.maybe(!options.tableNameHidden, () => [
|
||||
cssLabel('DATA TABLE NAME'),
|
||||
cssLabel(t('DataTableName')),
|
||||
// Update tableName on key stroke - this will show the default widget name as we type.
|
||||
// above this modal.
|
||||
tableInput = cssInput(
|
||||
inputTableName,
|
||||
updateOnKey,
|
||||
{disabled: isSummary, placeholder: 'Provide a table name'},
|
||||
{disabled: isSummary, placeholder: t('NewTableName')},
|
||||
testId('table-name-input')
|
||||
),
|
||||
]),
|
||||
dom.maybe(!options.widgetNameHidden, () => [
|
||||
cssLabel('WIDGET TITLE'),
|
||||
cssLabel(t('WidgetTitle')),
|
||||
widgetInput = cssInput(inputWidgetTitle, updateOnKey, {placeholder: inputWidgetPlaceholder},
|
||||
testId('section-name-input')
|
||||
),
|
||||
]),
|
||||
cssButtons(
|
||||
primaryButton('Save',
|
||||
primaryButton(t('Save'),
|
||||
dom.on('click', doSave),
|
||||
dom.boolAttr('disabled', use => use(disableSave) || use(modalCtl.workInProgress)),
|
||||
testId('save'),
|
||||
),
|
||||
basicButton('Cancel',
|
||||
basicButton(t('Cancel'),
|
||||
testId('cancel'),
|
||||
dom.on('click', () => modalCtl.close())
|
||||
),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {AppModel} from 'app/client/models/AppModel';
|
||||
import {getLoginUrl, getMainOrgUrl, urlState} from 'app/client/models/gristUrlState';
|
||||
import {AppHeader} from 'app/client/ui/AppHeader';
|
||||
@@ -12,6 +13,8 @@ import {dom, DomElementArg, makeTestId, observable, styled} from 'grainjs';
|
||||
|
||||
const testId = makeTestId('test-');
|
||||
|
||||
const t = makeT('errorPages');
|
||||
|
||||
export function createErrPage(appModel: AppModel) {
|
||||
const gristConfig: GristLoadConfig = (window as any).gristConfig || {};
|
||||
const message = gristConfig.errMessage;
|
||||
@@ -25,25 +28,24 @@ export function createErrPage(appModel: AppModel) {
|
||||
* Creates a page to show that the user has no access to this org.
|
||||
*/
|
||||
export function createForbiddenPage(appModel: AppModel, message?: string) {
|
||||
document.title = `Access denied${getPageTitleSuffix(getGristConfig())}`;
|
||||
document.title = t('AccessDenied', {suffix: getPageTitleSuffix(getGristConfig())});
|
||||
|
||||
const isAnonym = () => !appModel.currentValidUser;
|
||||
const isExternal = () => appModel.currentValidUser?.loginMethod === 'External';
|
||||
return pagePanelsError(appModel, 'Access denied', [
|
||||
return pagePanelsError(appModel, t('AccessDenied', {suffix: ''}), [
|
||||
dom.domComputed(appModel.currentValidUser, user => user ? [
|
||||
cssErrorText(message || "You do not have access to this organization's documents."),
|
||||
cssErrorText("You are signed in as ", dom('b', user.email),
|
||||
". You can sign in with a different account, or ask an administrator for access."),
|
||||
cssErrorText(message || t("DeniedOrganizationDocuments")),
|
||||
cssErrorText(t("SignInWithDifferentAccount", {email: dom('b', user.email)})), // TODO: i18next
|
||||
] : [
|
||||
// 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
|
||||
// version of this page or is an external user (connected through GristConnect).
|
||||
cssErrorText("Sign in to access this organization's documents."),
|
||||
cssErrorText(t("SignInToAccess")),
|
||||
]),
|
||||
cssButtonWrap(bigPrimaryButtonLink(
|
||||
isExternal() ? 'Go to main page' :
|
||||
isAnonym() ? 'Sign in' :
|
||||
'Add account',
|
||||
isExternal() ? t("GoToMainPage") :
|
||||
isAnonym() ? t("SignIn") :
|
||||
t("AddAcount"),
|
||||
{href: isExternal() ? getMainOrgUrl() : getLoginUrl()},
|
||||
testId('error-signin'),
|
||||
))
|
||||
@@ -54,12 +56,12 @@ export function createForbiddenPage(appModel: AppModel, message?: string) {
|
||||
* Creates a page that shows the user is logged out.
|
||||
*/
|
||||
export function createSignedOutPage(appModel: AppModel) {
|
||||
document.title = `Signed out${getPageTitleSuffix(getGristConfig())}`;
|
||||
document.title = t('SignedOut', {suffix: getPageTitleSuffix(getGristConfig())});
|
||||
|
||||
return pagePanelsError(appModel, 'Signed out', [
|
||||
cssErrorText("You are now signed out."),
|
||||
return pagePanelsError(appModel, t('SignedOut', {suffix: ''}), [
|
||||
cssErrorText(t('SignedOutNow')),
|
||||
cssButtonWrap(bigPrimaryButtonLink(
|
||||
'Sign in again', {href: getLoginUrl()}, testId('error-signin')
|
||||
t('SignedInAgain'), {href: getLoginUrl()}, testId('error-signin')
|
||||
))
|
||||
]);
|
||||
}
|
||||
@@ -68,14 +70,13 @@ export function createSignedOutPage(appModel: AppModel) {
|
||||
* Creates a "Page not found" page.
|
||||
*/
|
||||
export function createNotFoundPage(appModel: AppModel, message?: string) {
|
||||
document.title = `Page not found${getPageTitleSuffix(getGristConfig())}`;
|
||||
document.title = t('PageNotFound', {suffix: getPageTitleSuffix(getGristConfig())});
|
||||
|
||||
return pagePanelsError(appModel, 'Page not found', [
|
||||
cssErrorText(message || "The requested page could not be found.", dom('br'),
|
||||
"Please check the URL and try again."),
|
||||
cssButtonWrap(bigPrimaryButtonLink('Go to main page', testId('error-primary-btn'),
|
||||
return pagePanelsError(appModel, t('PageNotFound', {suffix: ''}), [
|
||||
cssErrorText(message || t('NotFoundMainText', {separator: dom('br')})), // TODO: i18next
|
||||
cssButtonWrap(bigPrimaryButtonLink(t('GoToMainPage'), testId('error-primary-btn'),
|
||||
urlState().setLinkUrl({}))),
|
||||
cssButtonWrap(bigBasicButtonLink('Contact support', {href: 'https://getgrist.com/contact'})),
|
||||
cssButtonWrap(bigBasicButtonLink(t('ContactSupport'), {href: 'https://getgrist.com/contact'})),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -83,14 +84,14 @@ export function createNotFoundPage(appModel: AppModel, message?: string) {
|
||||
* Creates a generic error page with the given message.
|
||||
*/
|
||||
export function createOtherErrorPage(appModel: AppModel, message?: string) {
|
||||
document.title = `Error${getPageTitleSuffix(getGristConfig())}`;
|
||||
document.title = t('GenericError', {suffix: getPageTitleSuffix(getGristConfig())});
|
||||
|
||||
return pagePanelsError(appModel, 'Something went wrong', [
|
||||
cssErrorText(message ? `There was an error: ${addPeriod(message)}` :
|
||||
"There was an unknown error."),
|
||||
cssButtonWrap(bigPrimaryButtonLink('Go to main page', testId('error-primary-btn'),
|
||||
return pagePanelsError(appModel, t('SomethingWentWrong'), [
|
||||
cssErrorText(message ? t('ErrorHappened', {context: 'message', message: addPeriod(message)}) :
|
||||
t('ErrorHappened', {context: 'unknown'})),
|
||||
cssButtonWrap(bigPrimaryButtonLink(t('GoToMainPage'), testId('error-primary-btn'),
|
||||
urlState().setLinkUrl({}))),
|
||||
cssButtonWrap(bigBasicButtonLink('Contact support', {href: 'https://getgrist.com/contact'})),
|
||||
cssButtonWrap(bigBasicButtonLink(t('ContactSupport'), {href: 'https://getgrist.com/contact'})),
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@ import type {DocPageModel} from 'app/client/models/DocPageModel';
|
||||
import type {Document} from 'app/common/UserAPI';
|
||||
import { getGoogleCodeForSending } from "app/client/ui/googleAuth";
|
||||
const G = getBrowserGlobals('window');
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
|
||||
const t = makeT('sendToDrive');
|
||||
|
||||
/**
|
||||
* Sends xlsx file to Google Drive. It first authenticates with Google to get encrypted access
|
||||
@@ -21,7 +24,7 @@ export async function sendToDrive(doc: Document, pageModel: DocPageModel) {
|
||||
// Create send to google drive handler (it will return a spreadsheet url).
|
||||
const send = (code: string) =>
|
||||
// Decorate it with a spinner
|
||||
spinnerModal('Sending file to Google Drive',
|
||||
spinnerModal(t('SendingToGoogleDrive'),
|
||||
pageModel.appModel.api.getDocAPI(doc.id)
|
||||
.sendToDrive(code, pageModel.currentDocTitle.get())
|
||||
);
|
||||
|
||||
@@ -8,6 +8,9 @@ import {cssSelectBtn} from 'app/client/ui2018/select';
|
||||
import {isValidHex} from 'app/common/gutil';
|
||||
import {BindableValue, Computed, Disposable, dom, Observable, onKeyDown, styled} from 'grainjs';
|
||||
import {defaultMenuOptions, IOpenController, setPopupToCreateDom} from 'popweasel';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
|
||||
const t = makeT('ui2018.ColorSelect');
|
||||
|
||||
export interface StyleOptions {
|
||||
textColor: ColorOption,
|
||||
@@ -61,7 +64,7 @@ export function colorSelect(
|
||||
onSave,
|
||||
onOpen,
|
||||
onRevert,
|
||||
placeholder = 'Default cell style',
|
||||
placeholder = t('DefaultCellStyle'),
|
||||
} = options;
|
||||
const selectBtn = cssSelectBtn(
|
||||
cssContent(
|
||||
@@ -185,12 +188,12 @@ function buildColorPicker(ctl: IOpenController,
|
||||
}),
|
||||
|
||||
cssButtonRow(
|
||||
primaryButton('Apply',
|
||||
primaryButton(t('Apply'),
|
||||
dom.on('click', () => ctl.close()),
|
||||
dom.boolAttr("disabled", notChanged),
|
||||
testId('colors-save')
|
||||
),
|
||||
basicButton('Cancel',
|
||||
basicButton(t('Cancel'),
|
||||
dom.on('click', () => revert()),
|
||||
testId('colors-cancel')
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*
|
||||
* Workspace is a clickable link and document and page names are editable labels.
|
||||
*/
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import { urlState } from 'app/client/models/gristUrlState';
|
||||
import { cssHideForNarrowScreen, mediaNotSmall, testId, theme } from 'app/client/ui2018/cssVars';
|
||||
import { editableLabel } from 'app/client/ui2018/editableLabel';
|
||||
@@ -15,6 +16,8 @@ import { userOverrideParams } from 'app/common/gristUrls';
|
||||
import { BindableValue, dom, Observable, styled } from 'grainjs';
|
||||
import { tooltip } from 'popweasel';
|
||||
|
||||
const t = makeT('ui2018.breadcrumbs');
|
||||
|
||||
export const cssBreadcrumbs = styled('div', `
|
||||
color: ${theme.lightText};
|
||||
white-space: nowrap;
|
||||
@@ -82,11 +85,6 @@ interface PartialWorkspace {
|
||||
name: string;
|
||||
}
|
||||
|
||||
const fiddleExplanation = (
|
||||
'You may make edits, but they will create a new copy and will\n' +
|
||||
'not affect the original document.'
|
||||
);
|
||||
|
||||
export function docBreadcrumbs(
|
||||
workspace: Observable<PartialWorkspace|null>,
|
||||
docName: Observable<string>,
|
||||
@@ -143,20 +141,20 @@ export function docBreadcrumbs(
|
||||
dom.maybe(options.isPublic, () => cssPublicIcon('PublicFilled', testId('bc-is-public'))),
|
||||
dom.domComputed((use) => {
|
||||
if (options.isSnapshot && use(options.isSnapshot)) {
|
||||
return cssTag('snapshot', testId('snapshot-tag'));
|
||||
return cssTag(t('Snapshot'), testId('snapshot-tag'));
|
||||
}
|
||||
if (use(options.isFork)) {
|
||||
return cssTag('unsaved', testId('unsaved-tag'));
|
||||
return cssTag(t('Unsaved'), testId('unsaved-tag'));
|
||||
}
|
||||
if (use(options.isRecoveryMode)) {
|
||||
return cssAlertTag('recovery mode',
|
||||
return cssAlertTag(t('RecoveryMode'),
|
||||
dom('a', dom.on('click', () => options.cancelRecoveryMode()),
|
||||
icon('CrossSmall')),
|
||||
testId('recovery-mode-tag'));
|
||||
}
|
||||
const userOverride = use(options.userOverride);
|
||||
if (userOverride) {
|
||||
return cssAlertTag(userOverride.user?.email || 'override',
|
||||
return cssAlertTag(userOverride.user?.email || t('Override'),
|
||||
dom('a',
|
||||
urlState().setHref(userOverrideParams(null)),
|
||||
icon('CrossSmall')
|
||||
@@ -165,7 +163,7 @@ export function docBreadcrumbs(
|
||||
);
|
||||
}
|
||||
if (use(options.isFiddle)) {
|
||||
return cssTag('fiddle', tooltip({title: fiddleExplanation}), testId('fiddle-tag'));
|
||||
return cssTag(t('Fiddle'), tooltip({title: t('FiddleExplanation')}), testId('fiddle-tag'));
|
||||
}
|
||||
}),
|
||||
separator(' / ',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Command } from 'app/client/components/commands';
|
||||
import { makeT } from 'app/client/lib/localization';
|
||||
import { NeedUpgradeError, reportError } from 'app/client/models/errors';
|
||||
import { textButton } from 'app/client/ui2018/buttons';
|
||||
import { cssCheckboxSquare, cssLabel, cssLabelText } from 'app/client/ui2018/checkbox';
|
||||
@@ -10,6 +11,8 @@ import { BindableValue, Computed, dom, DomElementArg, DomElementMethod, IDomArgs
|
||||
MaybeObsArray, MutableObsArray, Observable, styled } from 'grainjs';
|
||||
import * as weasel from 'popweasel';
|
||||
|
||||
const t = makeT('ui2018.menus');
|
||||
|
||||
export interface IOptionFull<T> {
|
||||
value: T;
|
||||
label: string;
|
||||
@@ -175,7 +178,7 @@ export function multiSelect<T>(selectedOptions: MutableObsArray<T>,
|
||||
|
||||
const selectedOptionsText = Computed.create(null, selectedOptionsSet, (use, selectedOpts) => {
|
||||
if (selectedOpts.size === 0) {
|
||||
return options.placeholder ?? 'Select fields';
|
||||
return options.placeholder ?? t('SelectFields');
|
||||
}
|
||||
|
||||
const optionArray = Array.isArray(availableOptions) ? availableOptions : use(availableOptions);
|
||||
@@ -309,8 +312,8 @@ export function upgradableMenuItem(needUpgrade: boolean, action: () => void, ...
|
||||
|
||||
export function upgradeText(needUpgrade: boolean, onClick: () => void) {
|
||||
if (!needUpgrade) { return null; }
|
||||
return menuText(dom('span', '* Workspaces are available on team plans. ',
|
||||
cssUpgradeTextButton('Upgrade now', dom.on('click', () => onClick()))));
|
||||
return menuText(dom('span', t('WorkspacesAvailableOnTeamPlans'),
|
||||
cssUpgradeTextButton(t('UpgradeNow'), dom.on('click', () => onClick()))));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {FocusLayer} from 'app/client/lib/FocusLayer';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {reportError} from 'app/client/models/errors';
|
||||
import {cssInput} from 'app/client/ui/cssInput';
|
||||
import {prepareForTransition, TransitionWatcher} from 'app/client/ui/transitions';
|
||||
@@ -11,6 +12,8 @@ import {Computed, Disposable, dom, DomContents, DomElementArg, input, keyframes,
|
||||
MultiHolder, Observable, styled} from 'grainjs';
|
||||
import {cssMenuElem} from 'app/client/ui2018/menus';
|
||||
|
||||
const t = makeT('ui2018.modals');
|
||||
|
||||
// IModalControl is passed into the function creating the body of the modal.
|
||||
export interface IModalControl {
|
||||
// Observable for whether there is work in progress that's delaying the closing of the modal. It
|
||||
@@ -303,13 +306,13 @@ export function saveModal(createFunc: (ctl: IModalControl, owner: MultiHolder) =
|
||||
cssModalTitle(options.title, testId('modal-title')),
|
||||
cssModalBody(options.body),
|
||||
cssModalButtons(
|
||||
bigPrimaryButton(options.saveLabel || 'Save',
|
||||
bigPrimaryButton(options.saveLabel || t('Save'),
|
||||
dom.boolAttr('disabled', isSaveDisabled),
|
||||
dom.on('click', save),
|
||||
testId('modal-confirm'),
|
||||
),
|
||||
options.extraButtons,
|
||||
options.hideCancel ? null : bigBasicButton('Cancel',
|
||||
options.hideCancel ? null : bigBasicButton(t('Cancel'),
|
||||
dom.on('click', () => ctl.close()),
|
||||
testId('modal-cancel'),
|
||||
),
|
||||
@@ -423,7 +426,7 @@ export function invokePrompt(
|
||||
const prom = new Promise<string|undefined>((resolve) => {
|
||||
onResolve = resolve;
|
||||
});
|
||||
promptModal(title, onResolve!, btnText ?? 'Ok', initial, placeholder, () => {
|
||||
promptModal(title, onResolve!, btnText ?? t('Ok'), initial, placeholder, () => {
|
||||
if (onResolve) {
|
||||
onResolve(undefined);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { isDesktop } from 'app/client/lib/browserInfo';
|
||||
import { makeT } from 'app/client/lib/localization';
|
||||
import { cssEditorInput } from "app/client/ui/HomeLeftPane";
|
||||
import { itemHeader, itemHeaderWrapper, treeViewContainer } from "app/client/ui/TreeViewComponentCss";
|
||||
import { theme } from "app/client/ui2018/cssVars";
|
||||
@@ -6,6 +7,8 @@ import { icon } from "app/client/ui2018/icons";
|
||||
import { menu, menuItem, menuText } from "app/client/ui2018/menus";
|
||||
import { dom, domComputed, DomElementArg, makeTestId, observable, Observable, styled } from "grainjs";
|
||||
|
||||
const t = makeT('ui2018.pages');
|
||||
|
||||
const testId = makeTestId('test-docpage-');
|
||||
|
||||
// the actions a page can do
|
||||
@@ -31,13 +34,13 @@ export function buildPageDom(name: Observable<string>, actions: PageActions, ...
|
||||
|
||||
const isRenaming = observable(false);
|
||||
const pageMenu = () => [
|
||||
menuItem(() => isRenaming.set(true), "Rename", testId('rename'),
|
||||
menuItem(() => isRenaming.set(true), t("Rename"), testId('rename'),
|
||||
dom.cls('disabled', actions.isReadonly)),
|
||||
menuItem(actions.onRemove, 'Remove', testId('remove'),
|
||||
menuItem(actions.onRemove, t('Remove'), testId('remove'),
|
||||
dom.cls('disabled', (use) => use(actions.isReadonly) || actions.isRemoveDisabled())),
|
||||
menuItem(actions.onDuplicate, 'Duplicate Page', testId('duplicate'),
|
||||
menuItem(actions.onDuplicate, t('DuplicatePage'), testId('duplicate'),
|
||||
dom.cls('disabled', actions.isReadonly)),
|
||||
dom.maybe(actions.isReadonly, () => menuText('You do not have edit access to this document')),
|
||||
dom.maybe(actions.isReadonly, () => menuText(t('NoEditAccess'))),
|
||||
];
|
||||
let pageElem: HTMLElement;
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* Takes a `SearchModel` that controls the search behavior.
|
||||
*/
|
||||
import { allCommands, createGroup } from 'app/client/components/commands';
|
||||
import { makeT } from 'app/client/lib/localization';
|
||||
import { reportError } from 'app/client/models/AppModel';
|
||||
import { SearchModel } from 'app/client/models/SearchModel';
|
||||
import { hoverTooltip } from 'app/client/ui/tooltips';
|
||||
@@ -16,6 +17,8 @@ import debounce = require('lodash/debounce');
|
||||
|
||||
export * from 'app/client/models/SearchModel';
|
||||
|
||||
const t = makeT('ui2018.search');
|
||||
|
||||
const EXPAND_TIME = .5;
|
||||
|
||||
const searchWrapper = styled('div', `
|
||||
@@ -143,7 +146,7 @@ export function searchBar(model: SearchModel, testId: TestId = noTestId) {
|
||||
model.isOpen.set(_value === undefined ? !model.isOpen.get() : _value);
|
||||
}, 100);
|
||||
const inputElem: HTMLInputElement = searchInput(model.value, {onInput: true},
|
||||
{type: 'text', placeholder: 'Search in document'},
|
||||
{type: 'text', placeholder: t('SearchInDocument')},
|
||||
dom.on('blur', () => (
|
||||
keepExpanded ?
|
||||
setTimeout(() => inputElem.focus(), 0) :
|
||||
@@ -182,7 +185,7 @@ export function searchBar(model: SearchModel, testId: TestId = noTestId) {
|
||||
const noMatch = use(model.noMatch);
|
||||
const isEmpty = use(model.isEmpty);
|
||||
if (isEmpty) { return null; }
|
||||
if (noMatch) { return cssLabel("No results"); }
|
||||
if (noMatch) { return cssLabel(t("NoResults")); }
|
||||
return [
|
||||
cssArrowBtn(
|
||||
icon('Dropdown'),
|
||||
@@ -192,7 +195,7 @@ export function searchBar(model: SearchModel, testId: TestId = noTestId) {
|
||||
dom.on('click', () => model.findNext()),
|
||||
hoverTooltip(
|
||||
[
|
||||
'Find Next ',
|
||||
t('FindNext'),
|
||||
cssShortcut(`(${['Enter', allCommands.findNext.humanKeys].join(', ')})`),
|
||||
],
|
||||
{key: 'searchArrowBtnTooltip'}
|
||||
@@ -206,7 +209,7 @@ export function searchBar(model: SearchModel, testId: TestId = noTestId) {
|
||||
dom.on('click', () => model.findPrev()),
|
||||
hoverTooltip(
|
||||
[
|
||||
'Find Previous ',
|
||||
t('FindPrevious'),
|
||||
cssShortcut(allCommands.findPrev.getKeysDesc()),
|
||||
],
|
||||
{key: 'searchArrowBtnTooltip'}
|
||||
|
||||
Reference in New Issue
Block a user