(core) updates from grist-core

This commit is contained in:
Paul Fitzpatrick 2023-05-15 12:01:19 -04:00
commit 84854b7cfa
18 changed files with 470 additions and 225 deletions

View File

@ -291,6 +291,8 @@ COOKIE_MAX_AGE | session cookie max age, defaults to 90 days; can be set to
HOME_PORT | port number to listen on for REST API server; if set to "share", add API endpoints to regular grist port. HOME_PORT | port number to listen on for REST API server; if set to "share", add API endpoints to regular grist port.
PORT | port number to listen on for Grist server PORT | port number to listen on for Grist server
REDIS_URL | optional redis server for browser sessions and db query caching REDIS_URL | optional redis server for browser sessions and db query caching
GRIST_SNAPSHOT_TIME_CAP | optional. Define the caps for tracking buckets. Usage: {"hour": 25, "day": 32, "isoWeek": 12, "month": 96, "year": 1000}
GRIST_SNAPSHOT_KEEP | optional. Number of recent snapshots to retain unconditionally for a document, regardless of when they were made
Sandbox related variables: Sandbox related variables:

View File

@ -19,7 +19,7 @@ const tableUtil = require('../lib/tableUtil');
const {FieldContextMenu} = require('../ui/FieldContextMenu'); const {FieldContextMenu} = require('../ui/FieldContextMenu');
const {RowContextMenu} = require('../ui/RowContextMenu'); const {RowContextMenu} = require('../ui/RowContextMenu');
const {parsePasteForView} = require("./BaseView2"); const {parsePasteForView} = require("./BaseView2");
const {columnInfoTooltip} = require("../ui/tooltips"); const {descriptionInfoTooltip} = require("../ui/tooltips");
/** /**
@ -266,7 +266,7 @@ DetailView.prototype.buildFieldDom = function(field, row) {
kd.cssClass(function() { return 'detail_theme_field_' + self.viewSection.themeDef(); }), kd.cssClass(function() { return 'detail_theme_field_' + self.viewSection.themeDef(); }),
dom('div.g_record_detail_label_container', dom('div.g_record_detail_label_container',
dom('div.g_record_detail_label', kd.text(field.label)), dom('div.g_record_detail_label', kd.text(field.label)),
kd.scope(field.description, desc => desc ? columnInfoTooltip(kd.text(field.description)) : null) kd.scope(field.description, desc => desc ? descriptionInfoTooltip(kd.text(field.description), "colmun") : null)
), ),
dom('div.g_record_detail_value'), dom('div.g_record_detail_value'),
); );
@ -299,7 +299,7 @@ DetailView.prototype.buildFieldDom = function(field, row) {
kd.cssClass(function() { return 'detail_theme_field_' + self.viewSection.themeDef(); }), kd.cssClass(function() { return 'detail_theme_field_' + self.viewSection.themeDef(); }),
dom('div.g_record_detail_label_container', dom('div.g_record_detail_label_container',
dom('div.g_record_detail_label', kd.text(field.displayLabel)), dom('div.g_record_detail_label', kd.text(field.displayLabel)),
kd.scope(field.description, desc => desc ? columnInfoTooltip(kd.text(field.description)) : null) kd.scope(field.description, desc => desc ? descriptionInfoTooltip(kd.text(field.description), "column") : null)
), ),
dom('div.g_record_detail_value', dom('div.g_record_detail_value',
kd.toggleClass('scissors', isCopyActive), kd.toggleClass('scissors', isCopyActive),

View File

@ -44,7 +44,7 @@ const {testId, isNarrowScreen} = require('app/client/ui2018/cssVars');
const {contextMenu} = require('app/client/ui/contextMenu'); const {contextMenu} = require('app/client/ui/contextMenu');
const {mouseDragMatchElem} = require('app/client/ui/mouseDrag'); const {mouseDragMatchElem} = require('app/client/ui/mouseDrag');
const {menuToggle} = require('app/client/ui/MenuToggle'); const {menuToggle} = require('app/client/ui/MenuToggle');
const {columnInfoTooltip, showTooltip} = require('app/client/ui/tooltips'); const {descriptionInfoTooltip, showTooltip} = require('app/client/ui/tooltips');
const {parsePasteForView} = require("./BaseView2"); const {parsePasteForView} = require("./BaseView2");
const {NEW_FILTER_JSON} = require('app/client/models/ColumnFilter'); const {NEW_FILTER_JSON} = require('app/client/models/ColumnFilter');
const {CombinedStyle} = require("app/client/models/Styles"); const {CombinedStyle} = require("app/client/models/Styles");
@ -1088,7 +1088,7 @@ GridView.prototype.buildDom = function() {
if (btn) { btn.click(); } if (btn) { btn.click(); }
}), }),
dom('div.g-column-label', dom('div.g-column-label',
kd.scope(field.description, desc => desc ? columnInfoTooltip(kd.text(field.description)) : null), kd.scope(field.description, desc => desc ? descriptionInfoTooltip(kd.text(field.description), "column") : null),
dom.on('mousedown', ev => isEditingLabel() ? ev.stopPropagation() : true), dom.on('mousedown', ev => isEditingLabel() ? ev.stopPropagation() : true),
// We are using editableLabel here, but we don't use it for editing. // We are using editableLabel here, but we don't use it for editing.
kf.editableLabel(self.isPreview ? field.label : field.displayLabel, ko.observable(false)), kf.editableLabel(self.isPreview ? field.label : field.displayLabel, ko.observable(false)),

View File

@ -53,6 +53,8 @@ export interface ViewSectionRec extends IRowModel<"_grist_Views_section">, RuleO
// Default widget title (the one that is used in titleDef). // Default widget title (the one that is used in titleDef).
defaultWidgetTitle: ko.PureComputed<string>; defaultWidgetTitle: ko.PureComputed<string>;
description: modelUtil.KoSaveableObservable<string>;
// true if this record is its table's rawViewSection, i.e. a 'raw data view' // true if this record is its table's rawViewSection, i.e. a 'raw data view'
// in which case the UI prevents various things like hiding columns or changing the widget type. // in which case the UI prevents various things like hiding columns or changing the widget type.
isRaw: ko.Computed<boolean>; isRaw: ko.Computed<boolean>;
@ -364,6 +366,9 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel):
// Widget title. // Widget title.
this.titleDef = modelUtil.fieldWithDefault(this.title, this.defaultWidgetTitle); this.titleDef = modelUtil.fieldWithDefault(this.title, this.defaultWidgetTitle);
// Widget description
this.description = modelUtil.fieldWithDefault(this.description, this.description());
// true if this record is its table's rawViewSection, i.e. a 'raw data view' // true if this record is its table's rawViewSection, i.e. a 'raw data view'
// in which case the UI prevents various things like hiding columns or changing the widget type. // in which case the UI prevents various things like hiding columns or changing the widget type.
this.isRaw = this.autoDispose(ko.pureComputed(() => this.table().rawViewSectionRef() === this.getRowId())); this.isRaw = this.autoDispose(ko.pureComputed(() => this.table().rawViewSectionRef() === this.getRowId()));

View File

@ -6,17 +6,16 @@ import {makeT} from 'app/client/lib/localization';
import {setTestState} from 'app/client/lib/testState'; import {setTestState} from 'app/client/lib/testState';
import {ViewFieldRec} from 'app/client/models/DocModel'; import {ViewFieldRec} from 'app/client/models/DocModel';
import {autoGrow} from 'app/client/ui/forms'; import {autoGrow} from 'app/client/ui/forms';
import {textarea} from 'app/client/ui/inputs';
import {showTransientTooltip} from 'app/client/ui/tooltips'; import {showTransientTooltip} from 'app/client/ui/tooltips';
import {basicButton, primaryButton, textButton} from 'app/client/ui2018/buttons'; import {basicButton, primaryButton, textButton} from 'app/client/ui2018/buttons';
import {theme, vars} from 'app/client/ui2018/cssVars'; import {theme, vars} from 'app/client/ui2018/cssVars';
import {cssTextInput} from 'app/client/ui2018/editableLabel';
import {icon} from 'app/client/ui2018/icons'; import {icon} from 'app/client/ui2018/icons';
import {menuCssClass} from 'app/client/ui2018/menus'; import {menuCssClass} from 'app/client/ui2018/menus';
import {Computed, dom, IInputOptions, input, makeTestId, Observable, styled} from 'grainjs'; import {Computed, dom, makeTestId, Observable, styled} from 'grainjs';
import * as ko from 'knockout'; import * as ko from 'knockout';
import {IOpenController, PopupControl, setPopupToCreateDom} from 'popweasel'; import {IOpenController, PopupControl, setPopupToCreateDom} from 'popweasel';
import { cssInput, cssLabel, cssRenamePopup, cssTextArea } from 'app/client/ui/RenamePopupStyles';
const testId = makeTestId('test-column-title-'); const testId = makeTestId('test-column-title-');
@ -281,16 +280,6 @@ const cssAddDescription = styled('div', `
} }
`); `);
const cssRenamePopup = styled('div', `
display: flex;
flex-direction: column;
min-width: 280px;
padding: 16px;
background-color: ${theme.popupBg};
border-radius: 2px;
outline: none;
`);
const cssColLabelBlock = styled('div', ` const cssColLabelBlock = styled('div', `
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -298,17 +287,6 @@ const cssColLabelBlock = styled('div', `
min-width: 80px; min-width: 80px;
`); `);
const cssLabel = styled('label', `
color: ${theme.text};
font-size: ${vars.xsmallFontSize};
font-weight: ${vars.bigControlTextWeight};
text-transform: uppercase;
margin: 0 0 8px 0;
&:not(:first-child) {
margin-top: 16px;
}
`);
const cssColId = styled('div', ` const cssColId = styled('div', `
font-size: ${vars.xsmallFontSize}; font-size: ${vars.xsmallFontSize};
font-weight: ${vars.bigControlTextWeight}; font-weight: ${vars.bigControlTextWeight};
@ -321,29 +299,6 @@ const cssColId = styled('div', `
align-self: start; align-self: start;
`); `);
const cssTextArea = styled(textarea, `
color: ${theme.inputFg};
background-color: ${theme.mainPanelBg};
border: 1px solid ${theme.inputBorder};
width: 100%;
padding: 3px 7px;
outline: none;
max-width: 100%;
min-width: calc(280px - 16px*2);
max-height: 500px;
min-height: calc(3em * 1.5);
resize: none;
border-radius: 3px;
&::placeholder {
color: ${theme.inputPlaceholderFg};
}
&[readonly] {
background-color: ${theme.inputDisabledBg};
color: ${theme.inputDisabledFg};
}
`);
const cssButtons = styled('div', ` const cssButtons = styled('div', `
display: flex; display: flex;
margin-top: 16px; margin-top: 16px;
@ -352,29 +307,3 @@ const cssButtons = styled('div', `
min-width: calc(50 / 13 * 1em); /* Min 50px for 13px font size, to make Save and Close buttons equal width */ min-width: calc(50 / 13 * 1em); /* Min 50px for 13px font size, to make Save and Close buttons equal width */
} }
`); `);
const cssInputWithIcon = styled('div', `
position: relative;
display: flex;
flex-direction: column;
`);
const cssInput = styled((
obs: Observable<string>,
opts: IInputOptions,
...args) => input(obs, opts, cssTextInput.cls(''), ...args), `
text-overflow: ellipsis;
color: ${theme.inputFg};
background-color: transparent;
&:disabled {
color: ${theme.inputDisabledFg};
background-color: ${theme.inputDisabledBg};
pointer-events: none;
}
&::placeholder {
color: ${theme.inputPlaceholderFg};
}
.${cssInputWithIcon.className} > &:disabled {
padding-right: 28px;
}
`);

View File

@ -1,18 +1,21 @@
import {CursorPos} from 'app/client/components/Cursor'; import {CursorPos} from 'app/client/components/Cursor';
import {makeT} from 'app/client/lib/localization'; import {makeT} from 'app/client/lib/localization';
import {ColumnRec} from 'app/client/models/DocModel'; import { KoSaveableObservable } from 'app/client/models/modelUtil';
import {autoGrow} from 'app/client/ui/forms'; import {autoGrow} from 'app/client/ui/forms';
import {textarea} from 'app/client/ui/inputs'; import {textarea} from 'app/client/ui/inputs';
import {cssLabel, cssRow} from 'app/client/ui/RightPanelStyles'; import {cssLabel, cssRow} from 'app/client/ui/RightPanelStyles';
import {testId, theme} from 'app/client/ui2018/cssVars'; import {testId, theme} from 'app/client/ui2018/cssVars';
import {dom, fromKo, MultiHolder, styled} from 'grainjs'; import {dom, fromKo, MultiHolder, styled} from 'grainjs';
const t = makeT('FieldConfig'); const t = makeT('DescriptionConfig');
export function buildDescriptionConfig( export function buildDescriptionConfig(
owner: MultiHolder, owner: MultiHolder,
origColumn: ColumnRec, description: KoSaveableObservable<string>,
cursor: ko.Computed<CursorPos>, options: {
cursor: ko.Computed<CursorPos>,
testPrefix: string,
},
) { ) {
// We will listen to cursor position and force a blur event on // We will listen to cursor position and force a blur event on
@ -22,7 +25,7 @@ export function buildDescriptionConfig(
// update a different column. // update a different column.
let editor: HTMLTextAreaElement | undefined; let editor: HTMLTextAreaElement | undefined;
owner.autoDispose( owner.autoDispose(
cursor.subscribe(() => { options.cursor.subscribe(() => {
editor?.blur(); editor?.blur();
}) })
); );
@ -30,14 +33,14 @@ export function buildDescriptionConfig(
return [ return [
cssLabel(t("DESCRIPTION")), cssLabel(t("DESCRIPTION")),
cssRow( cssRow(
editor = cssTextArea(fromKo(origColumn.description), editor = cssTextArea(fromKo(description),
{ onInput: false }, { onInput: false },
{ rows: '3' }, { rows: '3' },
dom.on('blur', async (e, elem) => { dom.on('blur', async (e, elem) => {
await origColumn.description.setAndSave(elem.value.trim()); await description.saveOnly(elem.value);
}), }),
testId('column-description'), testId(`${options.testPrefix}-description`),
autoGrow(fromKo(origColumn.description)) autoGrow(fromKo(description))
) )
), ),
]; ];

View File

@ -0,0 +1,75 @@
import { theme, vars } from 'app/client/ui2018/cssVars';
import {textarea} from 'app/client/ui/inputs';
import {cssTextInput} from 'app/client/ui2018/editableLabel';
import {IInputOptions, input, Observable, styled} from 'grainjs';
export const cssRenamePopup = styled('div', `
display: flex;
flex-direction: column;
min-width: 280px;
padding: 16px;
background-color: ${theme.popupBg};
border-radius: 2px;
outline: none;
`);
export const cssLabel = styled('label', `
color: ${theme.text};
font-size: ${vars.xsmallFontSize};
font-weight: ${vars.bigControlTextWeight};
text-transform: uppercase;
margin: 0 0 8px 0;
&:not(:first-child) {
margin-top: 16px;
}
`);
const cssInputWithIcon = styled('div', `
position: relative;
display: flex;
flex-direction: column;
`);
export const cssInput = styled((
obs: Observable<string>,
opts: IInputOptions,
...args) => input(obs, opts, cssTextInput.cls(''), ...args), `
text-overflow: ellipsis;
color: ${theme.inputFg};
background-color: transparent;
&:disabled {
color: ${theme.inputDisabledFg};
background-color: ${theme.inputDisabledBg};
pointer-events: none;
}
&::placeholder {
color: ${theme.inputPlaceholderFg};
}
.${cssInputWithIcon.className} > &:disabled {
padding-right: 28px;
}
`);
export const cssTextArea = styled(textarea, `
color: ${theme.inputFg};
background-color: ${theme.mainPanelBg};
border: 1px solid ${theme.inputBorder};
width: 100%;
padding: 3px 6px;
outline: none;
max-width: 100%;
min-width: calc(280px - 16px*2);
max-height: 500px;
min-height: calc(3em * 1.5);
resize: none;
border-radius: 3px;
&::placeholder {
color: ${theme.inputPlaceholderFg};
}
&[readonly] {
background-color: ${theme.inputDisabledBg};
color: ${theme.inputDisabledFg};
}
`);

View File

@ -251,7 +251,7 @@ export class RightPanel extends Disposable {
dom.create(buildNameConfig, origColumn, cursor, isMultiSelect), dom.create(buildNameConfig, origColumn, cursor, isMultiSelect),
), ),
cssSection( cssSection(
dom.create(buildDescriptionConfig, origColumn, cursor), dom.create(buildDescriptionConfig, origColumn.description, { cursor, "testPrefix": "column" }),
), ),
cssSeparator(), cssSeparator(),
cssSection( cssSection(
@ -361,6 +361,13 @@ export class RightPanel extends Disposable {
const hasColumnMapping = use(activeSection.columnsToMap); const hasColumnMapping = use(activeSection.columnsToMap);
return Boolean(isCustom && hasColumnMapping); return Boolean(isCustom && hasColumnMapping);
}); });
// build cursor position observable
const cursor = owner.autoDispose(ko.computed(() => {
const vsi = this._gristDoc.viewModel.activeSection?.().viewInstance();
return vsi?.cursor.currentPosition() ?? {};
}));
return dom.maybe(viewConfigTab, (vct) => [ return dom.maybe(viewConfigTab, (vct) => [
this._disableIfReadonly(), this._disableIfReadonly(),
cssLabel(dom.text(use => use(activeSection.isRaw) ? t("DATA TABLE NAME") : t("WIDGET TITLE")), cssLabel(dom.text(use => use(activeSection.isRaw) ? t("DATA TABLE NAME") : t("WIDGET TITLE")),
@ -377,6 +384,10 @@ export class RightPanel extends Disposable {
testId('right-widget-title') testId('right-widget-title')
)), )),
cssSection(
dom.create(buildDescriptionConfig, activeSection.description, { cursor, "testPrefix": "right-widget" }),
),
dom.maybe( dom.maybe(
(use) => !use(activeSection.isRaw), (use) => !use(activeSection.isRaw),
() => cssRow( () => cssRow(

View File

@ -1,13 +1,16 @@
import * as commands from 'app/client/components/commands';
import {makeT} from 'app/client/lib/localization'; import {makeT} from 'app/client/lib/localization';
import {FocusLayer} from 'app/client/lib/FocusLayer'; import { FocusLayer } from 'app/client/lib/FocusLayer';
import {ViewSectionRec} from 'app/client/models/entities/ViewSectionRec'; import {ViewSectionRec} from 'app/client/models/entities/ViewSectionRec';
import {basicButton, cssButton, primaryButton} from 'app/client/ui2018/buttons'; import {basicButton, cssButton, primaryButton} from 'app/client/ui2018/buttons';
import {theme, vars} from 'app/client/ui2018/cssVars'; import { theme } from 'app/client/ui2018/cssVars';
import {cssTextInput} from 'app/client/ui2018/editableLabel';
import {menuCssClass} from 'app/client/ui2018/menus'; import {menuCssClass} from 'app/client/ui2018/menus';
import {ModalControl} from 'app/client/ui2018/modals'; import {ModalControl} from 'app/client/ui2018/modals';
import {Computed, dom, DomElementArg, IInputOptions, input, makeTestId, Observable, styled} from 'grainjs'; import { Computed, dom, DomElementArg, makeTestId, Observable, styled } from 'grainjs';
import {IOpenController, setPopupToCreateDom} from 'popweasel'; import {IOpenController, setPopupToCreateDom} from 'popweasel';
import { descriptionInfoTooltip } from './tooltips';
import { autoGrow } from './forms';
import { cssInput, cssLabel, cssRenamePopup, cssTextArea } from 'app/client/ui/RenamePopupStyles';
const testId = makeTestId('test-widget-title-'); const testId = makeTestId('test-widget-title-');
const t = makeT('WidgetTitle'); const t = makeT('WidgetTitle');
@ -19,17 +22,20 @@ interface WidgetTitleOptions {
export function buildWidgetTitle(vs: ViewSectionRec, options: WidgetTitleOptions, ...args: DomElementArg[]) { export function buildWidgetTitle(vs: ViewSectionRec, options: WidgetTitleOptions, ...args: DomElementArg[]) {
const title = Computed.create(null, use => use(vs.titleDef)); const title = Computed.create(null, use => use(vs.titleDef));
return buildRenameWidget(vs, title, options, dom.autoDispose(title), ...args); const description = Computed.create(null, use => use(vs.description));
return buildRenameWidget(vs, title, description, options, dom.autoDispose(title), ...args);
} }
export function buildTableName(vs: ViewSectionRec, ...args: DomElementArg[]) { export function buildTableName(vs: ViewSectionRec, ...args: DomElementArg[]) {
const title = Computed.create(null, use => use(use(vs.table).tableNameDef)); const title = Computed.create(null, use => use(use(vs.table).tableNameDef));
return buildRenameWidget(vs, title, { widgetNameHidden: true }, dom.autoDispose(title), ...args); const description = Computed.create(null, use => use(vs.description));
return buildRenameWidget(vs, title, description, { widgetNameHidden: true }, dom.autoDispose(title), ...args);
} }
export function buildRenameWidget( export function buildRenameWidget(
vs: ViewSectionRec, vs: ViewSectionRec,
title: Observable<string>, title: Observable<string>,
description: Observable<string>,
options: WidgetTitleOptions, options: WidgetTitleOptions,
...args: DomElementArg[]) { ...args: DomElementArg[]) {
return cssTitleContainer( return cssTitleContainer(
@ -48,6 +54,9 @@ export function buildRenameWidget(
}, },
dom.on('click', (ev) => { ev.stopPropagation(); ev.preventDefault(); }), dom.on('click', (ev) => { ev.stopPropagation(); ev.preventDefault(); }),
), ),
dom.maybe(description, () => [
descriptionInfoTooltip(description.get(), "widget")
]),
...args ...args
); );
} }
@ -69,11 +78,19 @@ function buildWidgetRenamePopup(ctrl: IOpenController, vs: ViewSectionRec, optio
// - when widget title is set, shows just a text to override it. // - when widget title is set, shows just a text to override it.
const inputWidgetPlaceholder = !vs.title.peek() ? t("Override widget title") : vs.defaultWidgetTitle.peek(); const inputWidgetPlaceholder = !vs.title.peek() ? t("Override widget title") : vs.defaultWidgetTitle.peek();
// User input for widget description
const inputWidgetDesc = Observable.create(ctrl, vs.description.peek() ?? '');
const disableSave = Computed.create(ctrl, (use) => { const disableSave = Computed.create(ctrl, (use) => {
const newTableName = use(inputTableName)?.trim() ?? ''; const newTableName = use(inputTableName)?.trim() ?? '';
const newWidgetTitle = use(inputWidgetTitle)?.trim() ?? ''; const newWidgetTitle = use(inputWidgetTitle)?.trim() ?? '';
const newWidgetDesc = use(inputWidgetDesc)?.trim() ?? '';
// Can't save when table name is empty or there wasn't any change. // Can't save when table name is empty or there wasn't any change.
return !newTableName || (newTableName === tableName && newWidgetTitle === use(vs.title)); return !newTableName || (
newTableName === tableName
&& newWidgetTitle === use(vs.title)
&& newWidgetDesc === use(vs.description)
);
}); });
const modalCtl = ModalControl.create(ctrl, () => ctrl.close()); const modalCtl = ModalControl.create(ctrl, () => ctrl.close());
@ -99,10 +116,20 @@ function buildWidgetRenamePopup(ctrl: IOpenController, vs: ViewSectionRec, optio
await vs.title.saveOnly(newTitle); await vs.title.saveOnly(newTitle);
} }
}; };
const doSave = modalCtl.doWork(() => Promise.all([
const saveWidgetDesc = async () => {
const newWidgetDesc = inputWidgetDesc.get().trim() ?? '';
// If value was changed.
if (newWidgetDesc !== vs.description.peek()) {
await vs.description.saveOnly(newWidgetDesc);
}
};
const save = () => Promise.all([
saveTableName(), saveTableName(),
saveWidgetTitle() saveWidgetTitle(),
]), {close: true}); saveWidgetDesc()
]);
function initialFocus() { function initialFocus() {
const isRawView = !widgetInput; const isRawView = !widgetInput;
@ -122,18 +149,72 @@ function buildWidgetRenamePopup(ctrl: IOpenController, vs: ViewSectionRec, optio
} }
} }
// Build actual dom that looks like: // When the popup is closing we will save everything, unless the user has pressed the cancel button.
// DATA TABLE NAME let cancelled = false;
// [input]
// WIDGET TITLE // Function to close the popup with saving.
// [input] const close = () => ctrl.close();
// [Save] [Cancel]
// Function to close the popup without saving.
const cancel = () => { cancelled = true; close(); };
// Function that is called when popup is closed.
const onClose = () => {
if (!cancelled) {
save().catch(reportError);
}
};
// User interface for the popup.
const myCommands = {
// Escape key: just close the popup.
cancel,
// Enter key: save and close the popup, unless the description input is focused.
// There is also a variant for Ctrl+Enter which will always save.
accept: () => {
// Enters are ignored in the description input (unless ctrl is pressed)
if (document.activeElement === descInput) { return true; }
close();
},
// ArrowUp
cursorUp: () => {
// moves focus to the widget title input if it is already at the top of widget description
if (document.activeElement === descInput && descInput?.selectionStart === 0) {
widgetInput?.focus();
widgetInput?.select();
} else if (document.activeElement === widgetInput) {
tableInput?.focus();
tableInput?.select();
} else {
return true;
}
},
// ArrowDown
cursorDown: () => {
if (document.activeElement === tableInput) {
widgetInput?.focus();
widgetInput?.select();
} else if (document.activeElement === widgetInput) {
descInput?.focus();
descInput?.select();
} else {
return true;
}
}
};
// Create this group and attach it to the popup and all inputs.
const commandGroup = commands.createGroup({ ...myCommands }, ctrl, true);
let tableInput: HTMLInputElement|undefined; let tableInput: HTMLInputElement|undefined;
let widgetInput: HTMLInputElement|undefined; let widgetInput: HTMLInputElement|undefined;
let descInput: HTMLTextAreaElement | undefined;
return cssRenamePopup( return cssRenamePopup(
// Create a FocusLayer to keep focus in this popup while it's active, and prevent keyboard // Create a FocusLayer to keep focus in this popup while it's active, and prevent keyboard
// shortcuts from being seen by the view underneath. // shortcuts from being seen by the view underneath.
elem => { FocusLayer.create(ctrl, {defaultFocusElem: elem, pauseMousetrap: true}); }, elem => { FocusLayer.create(ctrl, { defaultFocusElem: elem, pauseMousetrap: false }); },
dom.onDispose(onClose),
dom.autoDispose(commandGroup),
testId('popup'), testId('popup'),
dom.cls(menuCssClass), dom.cls(menuCssClass),
dom.maybe(!options.tableNameHidden, () => [ dom.maybe(!options.tableNameHidden, () => [
@ -144,30 +225,41 @@ function buildWidgetRenamePopup(ctrl: IOpenController, vs: ViewSectionRec, optio
inputTableName, inputTableName,
updateOnKey, updateOnKey,
{disabled: isSummary, placeholder: t("Provide a table name")}, {disabled: isSummary, placeholder: t("Provide a table name")},
testId('table-name-input') testId('table-name-input'),
commandGroup.attach(),
), ),
]), ]),
dom.maybe(!options.widgetNameHidden, () => [ dom.maybe(!options.widgetNameHidden, () => [
cssLabel(t("WIDGET TITLE")), cssLabel(t("WIDGET TITLE")),
widgetInput = cssInput(inputWidgetTitle, updateOnKey, {placeholder: inputWidgetPlaceholder}, widgetInput = cssInput(inputWidgetTitle, updateOnKey, {placeholder: inputWidgetPlaceholder},
testId('section-name-input') testId('section-name-input'),
commandGroup.attach(),
), ),
]), ]),
cssLabel(t("WIDGET DESCRIPTION")),
descInput = cssTextArea(inputWidgetDesc, updateOnKey,
testId('section-description-input'),
commandGroup.attach(),
autoGrow(inputWidgetDesc),
),
cssButtons( cssButtons(
primaryButton(t("Save"), primaryButton(t("Save"),
dom.on('click', doSave), dom.on('click', close),
dom.boolAttr('disabled', use => use(disableSave) || use(modalCtl.workInProgress)), dom.boolAttr('disabled', use => use(disableSave) || use(modalCtl.workInProgress)),
testId('save'), testId('save'),
), ),
basicButton(t("Cancel"), basicButton(t("Cancel"),
testId('cancel'), testId('cancel'),
dom.on('click', () => modalCtl.close()) dom.on('click', cancel)
), ),
), ),
dom.onKeyDown({ dom.onKeyDown({
Escape: () => modalCtl.close(), Enter$: e => {
// On enter save or cancel - depending on the change. if (e.ctrlKey || e.metaKey) {
Enter: () => disableSave.get() ? modalCtl.close() : doSave(), close();
return false;
}
}
}), }),
elem => { setTimeout(initialFocus, 0); }, elem => { setTimeout(initialFocus, 0); },
); );
@ -180,6 +272,10 @@ const cssTitleContainer = styled('div', `
flex: 1 1 0px; flex: 1 1 0px;
min-width: 0px; min-width: 0px;
display: flex; display: flex;
.info_toggle_icon {
width: 13px;
height: 13px;
}
`); `);
const cssTitle = styled('div', ` const cssTitle = styled('div', `
@ -199,26 +295,6 @@ const cssTitle = styled('div', `
} }
`); `);
const cssRenamePopup = styled('div', `
display: flex;
flex-direction: column;
min-width: 280px;
padding: 16px;
background-color: ${theme.popupBg};
border-radius: 2px;
outline: none;
`);
const cssLabel = styled('label', `
color: ${theme.text};
font-size: ${vars.xsmallFontSize};
font-weight: ${vars.bigControlTextWeight};
margin: 0 0 8px 0;
&:not(:first-child) {
margin-top: 16px;
}
`);
const cssButtons = styled('div', ` const cssButtons = styled('div', `
display: flex; display: flex;
margin-top: 16px; margin-top: 16px;
@ -226,29 +302,3 @@ const cssButtons = styled('div', `
margin-left: 8px; margin-left: 8px;
} }
`); `);
const cssInputWithIcon = styled('div', `
position: relative;
display: flex;
flex-direction: column;
`);
const cssInput = styled((
obs: Observable<string>,
opts: IInputOptions,
...args) => input(obs, opts, cssTextInput.cls(''), ...args), `
text-overflow: ellipsis;
color: ${theme.inputFg};
background-color: transparent;
&:disabled {
color: ${theme.inputDisabledFg};
background-color: ${theme.inputDisabledBg};
pointer-events: none;
}
&::placeholder {
color: ${theme.inputPlaceholderFg};
}
.${cssInputWithIcon.className} > &:disabled {
padding-right: 28px;
}
`);

View File

@ -347,15 +347,18 @@ export function withInfoTooltip(
} }
/** /**
* Renders an column info icon that shows a tooltip with the specified `content` on click. * Renders an description info icon that shows a tooltip with the specified `content` on click.
*/ */
export function columnInfoTooltip(content: DomContents, menuOptions?: IMenuOptions, ...domArgs: DomElementArg[]) { export function descriptionInfoTooltip(
return cssColumnInfoTooltipButton( content: DomContents,
testPrefix: string,
...domArgs: DomElementArg[]) {
return cssDescriptionInfoTooltipButton(
icon('Info', dom.cls("info_toggle_icon")), icon('Info', dom.cls("info_toggle_icon")),
testId('column-info-tooltip'), testId(`${testPrefix}-info-tooltip`),
dom.on('mousedown', (e) => e.stopPropagation()), dom.on('mousedown', (e) => e.stopPropagation()),
dom.on('click', (e) => e.stopPropagation()), dom.on('click', (e) => e.stopPropagation()),
hoverTooltip(() => cssColumnInfoTooltip(content, testId('column-info-tooltip-popup')), { hoverTooltip(() => cssDescriptionInfoTooltip(content, testId(`${testPrefix}-info-tooltip-popup`)), {
closeDelay: 200, closeDelay: 200,
key: 'columnDescription', key: 'columnDescription',
openOnClick: true, openOnClick: true,
@ -365,7 +368,8 @@ export function withInfoTooltip(
); );
} }
const cssColumnInfoTooltip = styled('div', `
const cssDescriptionInfoTooltip = styled('div', `
white-space: pre-wrap; white-space: pre-wrap;
text-align: left; text-align: left;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -373,7 +377,7 @@ const cssColumnInfoTooltip = styled('div', `
max-width: min(500px, calc(100vw - 80px)); /* can't use 100%, 500px and 80px are picked by hand */ max-width: min(500px, calc(100vw - 80px)); /* can't use 100%, 500px and 80px are picked by hand */
`); `);
const cssColumnInfoTooltipButton = styled('div', ` const cssDescriptionInfoTooltipButton = styled('div', `
cursor: pointer; cursor: pointer;
--icon-color: ${theme.infoButtonFg}; --icon-color: ${theme.infoButtonFg};
border-radius: 50%; border-radius: 50%;

View File

@ -1,3 +1,4 @@
import {integerParam} from 'app/server/lib/requestUtils';
import {ObjSnapshotWithMetadata} from 'app/common/DocSnapshot'; import {ObjSnapshotWithMetadata} from 'app/common/DocSnapshot';
import {SnapshotWindow} from 'app/common/Features'; import {SnapshotWindow} from 'app/common/Features';
import {KeyedMutex} from 'app/common/KeyedMutex'; import {KeyedMutex} from 'app/common/KeyedMutex';
@ -350,16 +351,28 @@ export function shouldKeepSnapshots(snapshots: ObjSnapshotWithMetadata[], snapsh
// Get time of current version // Get time of current version
const start = moment.tz(current.lastModified, tz); const start = moment.tz(current.lastModified, tz);
const capObjectString = process.env.GRIST_SNAPSHOT_TIME_CAP
|| '{"hour": 25, "day": 32, "isoWeek": 12, "month": 96, "year": 1000}';
// Parse the stringified JSON object into an actual object
const caps = JSON.parse(capObjectString);
// Extract the cap values for each bucket range and convert them to integers
const capHour = integerParam(caps.hour, "GRIST_SNAPSHOT_TIMEBUCKET_CAP.hour");
const capDay = integerParam(caps.day, "GRIST_SNAPSHOT_TIMEBUCKET_CAP.day");
const capIsoWeek = integerParam(caps.isoWeek, "GRIST_SNAPSHOT_TIMEBUCKET_CAP.isoWeek");
const capMonth = integerParam(caps.month, "GRIST_SNAPSHOT_TIMEBUCKET_CAP.month");
const capYear = integerParam(caps.year, "GRIST_SNAPSHOT_TIMEBUCKET_CAP.year");
// Track saved version per hour, day, week, month, year, and number of times a version // Track saved version per hour, day, week, month, year, and number of times a version
// has been saved based on a corresponding rule. // has been saved based on a corresponding rule.
const buckets: TimeBucket[] = [ const buckets: TimeBucket[] = [
{range: 'hour', prev: start, usage: 0, cap: 25}, {range: 'hour', prev: start, usage: 0, cap: capHour},
{range: 'day', prev: start, usage: 0, cap: 32}, {range: 'day', prev: start, usage: 0, cap: capDay},
{range: 'isoWeek', prev: start, usage: 0, cap: 12}, {range: 'isoWeek', prev: start, usage: 0, cap: capIsoWeek},
{range: 'month', prev: start, usage: 0, cap: 96}, {range: 'month', prev: start, usage: 0, cap: capMonth},
{range: 'year', prev: start, usage: 0, cap: 1000} {range: 'year', prev: start, usage: 0, cap: capYear}
]; ];
// For each snapshot starting with newest, check if it is worth saving by comparing // For each snapshot starting with newest, check if it is worth saving by comparing
// it with the last saved snapshot based on hour, day, week, month, year // it with the last saved snapshot based on hour, day, week, month, year
return snapshots.map((snapshot, index) => { return snapshots.map((snapshot, index) => {
@ -375,7 +388,9 @@ export function shouldKeepSnapshots(snapshots: ObjSnapshotWithMetadata[], snapsh
return false; return false;
} }
let keep = index < 5; // Keep 5 most recent versions // Keep 5 most recent versions if NUM_SNAPSHOT_KEEP not exist
let keep = index < integerParam(process.env.GRIST_SNAPSHOT_KEEP || 5, "GRIST_SNAPSHOT_KEEP");
for (const bucket of buckets) { for (const bucket of buckets) {
if (updateAndCheckRange(date, bucket)) { keep = true; } if (updateAndCheckRange(date, bucket)) { keep = true; }
} }

View File

@ -589,7 +589,8 @@
"Columns_one": "Spalte", "Columns_one": "Spalte",
"Columns_other": "Spalten", "Columns_other": "Spalten",
"Fields_one": "Feld", "Fields_one": "Feld",
"Fields_other": "Felder" "Fields_other": "Felder",
"Add referenced columns": "Referenzspalten hinzufügen"
}, },
"RowContextMenu": { "RowContextMenu": {
"Copy anchor link": "Ankerlink kopieren", "Copy anchor link": "Ankerlink kopieren",
@ -779,7 +780,8 @@
"Override widget title": "Widget-Titel überschreiben", "Override widget title": "Widget-Titel überschreiben",
"Provide a table name": "Geben Sie einen Tabellennamen an", "Provide a table name": "Geben Sie einen Tabellennamen an",
"Save": "Speichern", "Save": "Speichern",
"WIDGET TITLE": "WIDGET TITEL" "WIDGET TITLE": "WIDGET TITEL",
"WIDGET DESCRIPTION": "WIDGET-BESCHREIBUNG"
}, },
"breadcrumbs": { "breadcrumbs": {
"You may make edits, but they will create a new copy and will\nnot affect the original document.": "Sie können Änderungen vornehmen, die jedoch eine neue Kopie erstellen und\ndas Originaldokument nicht beeinflussen.", "You may make edits, but they will create a new copy and will\nnot affect the original document.": "Sie können Änderungen vornehmen, die jedoch eine neue Kopie erstellen und\ndas Originaldokument nicht beeinflussen.",
@ -1047,6 +1049,7 @@
"Provide a column label": "Geben Sie eine Spaltenbeschriftung an", "Provide a column label": "Geben Sie eine Spaltenbeschriftung an",
"Save": "Speichern", "Save": "Speichern",
"Column label": "Spaltenbeschriftung", "Column label": "Spaltenbeschriftung",
"Column ID copied to clipboard": "Spalten-ID in die Zwischenablage kopiert" "Column ID copied to clipboard": "Spalten-ID in die Zwischenablage kopiert",
"Close": "Schließen"
} }
} }

View File

@ -724,7 +724,8 @@
"Override widget title": "Override widget title", "Override widget title": "Override widget title",
"Provide a table name": "Provide a table name", "Provide a table name": "Provide a table name",
"Save": "Save", "Save": "Save",
"WIDGET TITLE": "WIDGET TITLE" "WIDGET TITLE": "WIDGET TITLE",
"WIDGET DESCRIPTION": "WIDGET DESCRIPTION"
}, },
"breadcrumbs": { "breadcrumbs": {
"You may make edits, but they will create a new copy and will\nnot affect the original document.": "You may make edits, but they will create a new copy and will\nnot affect the original document.", "You may make edits, but they will create a new copy and will\nnot affect the original document.": "You may make edits, but they will create a new copy and will\nnot affect the original document.",

View File

@ -486,7 +486,8 @@
"Columns_one": "Columna", "Columns_one": "Columna",
"Columns_other": "Columnas", "Columns_other": "Columnas",
"Fields_one": "Campo", "Fields_one": "Campo",
"Fields_other": "Campos" "Fields_other": "Campos",
"Add referenced columns": "Añadir columnas referenciadas"
}, },
"RowContextMenu": { "RowContextMenu": {
"Copy anchor link": "Copiar enlace de anclaje", "Copy anchor link": "Copiar enlace de anclaje",
@ -636,7 +637,8 @@
"Override widget title": "Sobrescribir título del Widget", "Override widget title": "Sobrescribir título del Widget",
"Provide a table name": "Proporcionar un nombre de tabla", "Provide a table name": "Proporcionar un nombre de tabla",
"Save": "Guardar", "Save": "Guardar",
"WIDGET TITLE": "TÍTULO DEL WIDGET" "WIDGET TITLE": "TÍTULO DEL WIDGET",
"WIDGET DESCRIPTION": "DESCRIPCIÓN DEL WIDGET"
}, },
"errorPages": { "errorPages": {
"Access denied{{suffix}}": "Acceso negado{{suffix}}", "Access denied{{suffix}}": "Acceso negado{{suffix}}",
@ -1037,6 +1039,7 @@
"Add description": "Agregar una descripción", "Add description": "Agregar una descripción",
"Column ID copied to clipboard": "ID de la columna copiada al portapapeles", "Column ID copied to clipboard": "ID de la columna copiada al portapapeles",
"Column description": "Descripción de la Columna", "Column description": "Descripción de la Columna",
"Provide a column label": "Proporciona una etiqueta a la columna" "Provide a column label": "Proporciona una etiqueta a la columna",
"Close": "Cerrar"
} }
} }

View File

@ -133,7 +133,7 @@
"Each Y series is followed by two series, for top and bottom error bars.": "Each Y series is followed by two series, for top and bottom error bars.", "Each Y series is followed by two series, for top and bottom error bars.": "Each Y series is followed by two series, for top and bottom error bars.",
"Create separate series for each value of the selected column.": "Créer une série séparée pour chaque valeur de la colonne sélectionnée.", "Create separate series for each value of the selected column.": "Créer une série séparée pour chaque valeur de la colonne sélectionnée.",
"Pick a column": "Choisir une colonne", "Pick a column": "Choisir une colonne",
"selected new group data columns": "selected new group data columns", "selected new group data columns": "nouveau groupe de colonnes sélectionné",
"Toggle chart aggregation": "Activer/désactiver l'agrégation des graphiques" "Toggle chart aggregation": "Activer/désactiver l'agrégation des graphiques"
}, },
"CodeEditorPanel": { "CodeEditorPanel": {
@ -263,12 +263,12 @@
"Locale:": "Langue :", "Locale:": "Langue :",
"Currency:": "Devise :", "Currency:": "Devise :",
"Local currency ({{currency}})": "Devise locale ({{currency}})", "Local currency ({{currency}})": "Devise locale ({{currency}})",
"Engine (experimental {{span}} change at own risk):": "Moteur (expérimental {{span}} changez à vos risques et périls):", "Engine (experimental {{span}} change at own risk):": "Moteur (expérimental {{span}} changez à vos risques et périls) :",
"Save": "Enregistrer", "Save": "Enregistrer",
"Save and Reload": "Enregistrer et recharger", "Save and Reload": "Enregistrer et recharger",
"Document ID copied to clipboard": "Identifiant de document copié", "Document ID copied to clipboard": "Identifiant de document copié",
"API": "API", "API": "API",
"Ok": "Ok" "Ok": "OK"
}, },
"DocumentUsage": { "DocumentUsage": {
"Usage statistics are only available to users with full access to the document data.": "Les statistiques d'utilisation ne sont disponibles qu'aux utilisateurs ayant un accès complet aux données du document.", "Usage statistics are only available to users with full access to the document data.": "Les statistiques d'utilisation ne sont disponibles qu'aux utilisateurs ayant un accès complet aux données du document.",
@ -316,19 +316,20 @@
"Mixed Behavior": "Comportement mixte", "Mixed Behavior": "Comportement mixte",
"Clear and make into formula": "Effacer et transformer en formule", "Clear and make into formula": "Effacer et transformer en formule",
"Convert column to data": "Convertir la colonne en données", "Convert column to data": "Convertir la colonne en données",
"Convert to trigger formula": "Convert to trigger formula", "Convert to trigger formula": "Convertir en formule",
"Clear and reset": "Effacer et réinitialiser", "Clear and reset": "Effacer et réinitialiser",
"Enter formula": "Saisir la formule", "Enter formula": "Saisir la formule",
"COLUMN BEHAVIOR": "NATURE DE COLONNE", "COLUMN BEHAVIOR": "NATURE DE COLONNE",
"Set formula": "Définir la formule", "Set formula": "Définir la formule",
"Set trigger formula": "Définir une formule dinitialisation", "Set trigger formula": "Définir une formule dinitialisation",
"Make into data column": "Transformer en colonne de données", "Make into data column": "Transformer en colonne de données",
"TRIGGER FORMULA": "TRIGGER FORMULA" "TRIGGER FORMULA": "TRIGGER FORMULA",
"DESCRIPTION": "DESCRIPTION"
}, },
"FieldMenus": { "FieldMenus": {
"Using common settings": "Using common settings", "Using common settings": "Utilisation des paramètres communs",
"Using separate settings": "Using separate settings", "Using separate settings": "Utilisation de paramètres distincts",
"Use separate settings": "Use separate settings", "Use separate settings": "Utiliser des paramètres distincts",
"Save as common settings": "Save common settings", "Save as common settings": "Save common settings",
"Revert to common settings": "Revert common settings" "Revert to common settings": "Revert common settings"
}, },
@ -374,12 +375,14 @@
"Unfreeze all columns": "Libérer toutes les colonnes", "Unfreeze all columns": "Libérer toutes les colonnes",
"Add to sort": "Ajouter au tri", "Add to sort": "Ajouter au tri",
"Sorted (#{{count}})_one": "Trié (#{{count}})", "Sorted (#{{count}})_one": "Trié (#{{count}})",
"Sorted (#{{count}})_other": "Triés (#{{count}})" "Sorted (#{{count}})_other": "Triés (#{{count}})",
"Insert column to the right": "Insérer une colonne à droite",
"Insert column to the left": "Insérer une colonne à gauche"
}, },
"GristDoc": { "GristDoc": {
"Import from file": "Importer depuis un fichier", "Import from file": "Importer depuis un fichier",
"Added new linked section to view {{viewName}}": "Added new linked section to view {{viewName}}", "Added new linked section to view {{viewName}}": "Ajout d'une nouvelle section à la page {{viewName}}",
"Saved linked section {{title}} in view {{name}}": "Saved linked section {{title}} in view {{name}}" "Saved linked section {{title}} in view {{name}}": "Sauvegarder la section {{title}} dans la page {{name}}"
}, },
"HomeIntro": { "HomeIntro": {
"Sign up": "S'inscrire", "Sign up": "S'inscrire",
@ -393,7 +396,7 @@
"Welcome to Grist, {{name}}!": "Bienvenue sur Grist, {{name}} !", "Welcome to Grist, {{name}}!": "Bienvenue sur Grist, {{name}} !",
"Get started by inviting your team and creating your first Grist document.": "Pour commencer, inviter votre équipe et créer votre premier document Grist.", "Get started by inviting your team and creating your first Grist document.": "Pour commencer, inviter votre équipe et créer votre premier document Grist.",
"Get started by creating your first Grist document.": "Commencez en créant votre premier document Grist.", "Get started by creating your first Grist document.": "Commencez en créant votre premier document Grist.",
"Get started by exploring templates, or creating your first Grist document.": "Get started by exploring templates, or creating your first Grist document.", "Get started by exploring templates, or creating your first Grist document.": "Commencez par explorer des modèles ou créez votre premier document Grist.",
"Welcome to Grist!": "Bienvenue sur Grist !", "Welcome to Grist!": "Bienvenue sur Grist !",
"Help Center": "Centre d'aide", "Help Center": "Centre d'aide",
"Invite Team Members": "Inviter un nouveau membre", "Invite Team Members": "Inviter un nouveau membre",
@ -401,11 +404,13 @@
"Create Empty Document": "Créer un document vide", "Create Empty Document": "Créer un document vide",
"Import Document": "Importer un Fichier", "Import Document": "Importer un Fichier",
"Visit our {{link}} to learn more.": "Consulter le {{link}} pour en savoir plus.", "Visit our {{link}} to learn more.": "Consulter le {{link}} pour en savoir plus.",
"{{signUp}} to save your work. ": "{{signUp}} pour enregistrer votre travail. " "{{signUp}} to save your work. ": "{{signUp}} pour enregistrer votre travail. ",
"Welcome to Grist, {{- name}}!": "Bienvenue sur Grist, {{- name}} !",
"Welcome to {{- orgName}}": "Bienvenue sur {{- orgName}}"
}, },
"HomeLeftPane": { "HomeLeftPane": {
"All Documents": "Tous les documents", "All Documents": "Tous les documents",
"Examples & Templates": "Exemples & Templates", "Examples & Templates": "Modèles",
"Create Empty Document": "Créer un document vide", "Create Empty Document": "Créer un document vide",
"Import Document": "Importer un Fichier", "Import Document": "Importer un Fichier",
"Create Workspace": "Créer un nouveau dossier", "Create Workspace": "Créer un nouveau dossier",
@ -416,18 +421,19 @@
"Delete {{workspace}} and all included documents?": "Supprimer le dossier {{workspace}} et tous les documents qu'il contient ?", "Delete {{workspace}} and all included documents?": "Supprimer le dossier {{workspace}} et tous les documents qu'il contient ?",
"Workspace will be moved to Trash.": "Le dossier va être mis à la corbeille.", "Workspace will be moved to Trash.": "Le dossier va être mis à la corbeille.",
"Manage Users": "Gérer les utilisateurs", "Manage Users": "Gérer les utilisateurs",
"Access Details": "Access Details" "Access Details": "Détails d'accès",
"Tutorial": "Tutoriel"
}, },
"Importer": { "Importer": {
"Update existing records": "Update existing records", "Update existing records": "Mettre à jour les enregistrements existants",
"Merge rows that match these fields:": "Fusionner les lignes si ces champs correspondent:", "Merge rows that match these fields:": "Fusionner les lignes si ces champs correspondent :",
"Select fields to match on": "Sélectionner les champs pour l'appairage" "Select fields to match on": "Sélectionner les champs pour l'appairage"
}, },
"LeftPanelCommon": { "LeftPanelCommon": {
"Help Center": "Centre d'aide" "Help Center": "Centre d'aide"
}, },
"MakeCopyMenu": { "MakeCopyMenu": {
"Replacing the original requires editing rights on the original document.": "Replacing the original requires editing rights on the original document.", "Replacing the original requires editing rights on the original document.": "Le remplacement de l'original nécessite des droits d'édition sur le document d'origine.",
"Cancel": "Annuler", "Cancel": "Annuler",
"Update Original": "Mettre à jour l'original", "Update Original": "Mettre à jour l'original",
"Update": "Mettre à jour", "Update": "Mettre à jour",
@ -435,10 +441,10 @@
"Original Has Modifications": "L'original a été modifié", "Original Has Modifications": "L'original a été modifié",
"Overwrite": "Remplacer", "Overwrite": "Remplacer",
"Be careful, the original has changes not in this document. Those changes will be overwritten.": "Attention, l'original a des modifications qui ne sont pas dans ce document. Ces modifications seront écrasées.", "Be careful, the original has changes not in this document. Those changes will be overwritten.": "Attention, l'original a des modifications qui ne sont pas dans ce document. Ces modifications seront écrasées.",
"Original Looks Unrelated": "Original Looks Unrelated", "Original Looks Unrelated": "L'original ne semble pas relié",
"It will be overwritten, losing any content not in this document.": "It will be overwritten, losing any content not in this document.", "It will be overwritten, losing any content not in this document.": "Il sera écrasé, perdant tout contenu ne figurant pas dans ce document.",
"Original Looks Identical": "Original Looks Identical", "Original Looks Identical": "L'original semble identique",
"However, it appears to be already identical.": "However, it appears to be already identical.", "However, it appears to be already identical.": "Cependant, il semble être déjà identique.",
"Sign up": "Inscription", "Sign up": "Inscription",
"To save your changes, please sign up, then reload this page.": "Pour enregistrer vos modifications, veuillez vous inscrire, puis recharger cette page.", "To save your changes, please sign up, then reload this page.": "Pour enregistrer vos modifications, veuillez vous inscrire, puis recharger cette page.",
"No destination workspace": "Aucun dossier destination", "No destination workspace": "Aucun dossier destination",
@ -452,7 +458,7 @@
"You do not have write access to the selected workspace": "Vous navez pas accès en écriture à ce dossier" "You do not have write access to the selected workspace": "Vous navez pas accès en écriture à ce dossier"
}, },
"NotifyUI": { "NotifyUI": {
"Upgrade Plan": "Upgrade Plan", "Upgrade Plan": "Améliorer votre abonnement",
"Renew": "Renouveler", "Renew": "Renouveler",
"Go to your free personal site": "Accéder à votre espace personnel", "Go to your free personal site": "Accéder à votre espace personnel",
"Cannot find personal site, sorry!": "Espace personnel introuvable, désolé !", "Cannot find personal site, sorry!": "Espace personnel introuvable, désolé !",
@ -490,10 +496,10 @@
"Read Only": "Lecture seule" "Read Only": "Lecture seule"
}, },
"PluginScreen": { "PluginScreen": {
"Import failed: ": "Échec de l'importation: " "Import failed: ": "Échec de l'importation : "
}, },
"RecordLayout": { "RecordLayout": {
"Updating record layout.": "Updating record layout." "Updating record layout.": "Mise à jour de la disposition."
}, },
"RecordLayoutEditor": { "RecordLayoutEditor": {
"Add Field": "Ajouter un champ", "Add Field": "Ajouter un champ",
@ -530,7 +536,7 @@
"SOURCE DATA": "DONNÉES SOURCE", "SOURCE DATA": "DONNÉES SOURCE",
"GROUPED BY": "GROUPER PAR", "GROUPED BY": "GROUPER PAR",
"Edit Data Selection": "Données source", "Edit Data Selection": "Données source",
"Detach": "Detach", "Detach": "Détacher",
"SELECT BY": "SÉLECTIONNER PAR", "SELECT BY": "SÉLECTIONNER PAR",
"Select Widget": "Choisir la vue", "Select Widget": "Choisir la vue",
"SELECTOR FOR": "SÉLECTEUR", "SELECTOR FOR": "SÉLECTEUR",
@ -578,7 +584,7 @@
"Add Column": "Ajouter une colonne", "Add Column": "Ajouter une colonne",
"Update Data": "Mettre à jour les données", "Update Data": "Mettre à jour les données",
"Use choice position": "Use choice position", "Use choice position": "Use choice position",
"Natural sort": "Natural sort", "Natural sort": "Tri naturel",
"Empty values last": "Valeurs vides en dernier", "Empty values last": "Valeurs vides en dernier",
"Search Columns": "Rechercher" "Search Columns": "Rechercher"
}, },
@ -650,8 +656,8 @@
"Compact": "Compact", "Compact": "Compact",
"Blocks": "Blocs", "Blocks": "Blocs",
"Edit Card Layout": "Disposition de la carte", "Edit Card Layout": "Disposition de la carte",
"Plugin: ": "Plugin: ", "Plugin: ": "Plugin : ",
"Section: ": "Section: " "Section: ": "Section : "
}, },
"ViewLayoutMenu": { "ViewLayoutMenu": {
"Delete record": "Supprimer la ligne", "Delete record": "Supprimer la ligne",
@ -665,7 +671,9 @@
"Advanced Sort & Filter": "Tri et filtre avancés", "Advanced Sort & Filter": "Tri et filtre avancés",
"Data selection": "Sélection des données", "Data selection": "Sélection des données",
"Open configuration": "Ouvrir la configuration", "Open configuration": "Ouvrir la configuration",
"Delete widget": "Supprimer la vue" "Delete widget": "Supprimer la vue",
"Collapse widget": "Réduire la vue",
"Add to page": "Ajouter à la page"
}, },
"ViewSectionMenu": { "ViewSectionMenu": {
"Update Sort&Filter settings": "Mettre à jour le tri et le filtre", "Update Sort&Filter settings": "Mettre à jour le tri et le filtre",
@ -682,19 +690,23 @@
"Hidden Fields cannot be reordered": "Les champs masqués ne peuvent pas être réordonnés", "Hidden Fields cannot be reordered": "Les champs masqués ne peuvent pas être réordonnés",
"Cannot drop items into Hidden Fields": "Impossible de mettre des éléments dans les champs cachés", "Cannot drop items into Hidden Fields": "Impossible de mettre des éléments dans les champs cachés",
"Select All": "Sélectionner tout", "Select All": "Sélectionner tout",
"Clear": "Effacer" "Clear": "Effacer",
"Visible {{label}}": "{{label}} visible",
"Hide {{label}}": "Cacher {{label}}",
"Show {{label}}": "Montrer {{label}}",
"Hidden {{label}}": "{{label}} caché"
}, },
"WelcomeQuestions": { "WelcomeQuestions": {
"Welcome to Grist!": "Bienvenue sur Grist !", "Welcome to Grist!": "Bienvenue sur Grist !",
"Product Development": "Développement de produit", "Product Development": "Développement de produit",
"Finance & Accounting": "Finance & comptabilité", "Finance & Accounting": "Finance et comptabilité",
"Media Production": "Production de média", "Media Production": "Production de média",
"IT & Technology": "Technologie informatique", "IT & Technology": "Technologie informatique",
"Marketing": "Marketing", "Marketing": "Marketing",
"Research": "Recherche", "Research": "Recherche",
"Sales": "Ventes", "Sales": "Ventes",
"Education": "Éducation", "Education": "Éducation",
"HR & Management": "RH & Gestion", "HR & Management": "RH et Gestion",
"Other": "Autres", "Other": "Autres",
"What brings you to Grist? Please help us serve you better.": "Pourquoi utilisez-vous Grist ? Aidez-nous à laméliorer.", "What brings you to Grist? Please help us serve you better.": "Pourquoi utilisez-vous Grist ? Aidez-nous à laméliorer.",
"Type here": "Écrire ici" "Type here": "Écrire ici"
@ -708,7 +720,7 @@
"Cancel": "Annuler" "Cancel": "Annuler"
}, },
"breadcrumbs": { "breadcrumbs": {
"You may make edits, but they will create a new copy and will\nnot affect the original document.": "Vous pouvez faire des modifications, mais une nouvelle copie sera créée et ces modifications naffecteront pas le document original.", "You may make edits, but they will create a new copy and will\nnot affect the original document.": "Vous pouvez faire des modifications, mais une nouvelle copie\n sera créée et ces modifications naffecteront pas le document original.",
"snapshot": "instantané", "snapshot": "instantané",
"unsaved": "non enregistré", "unsaved": "non enregistré",
"recovery mode": "mode récupération", "recovery mode": "mode récupération",
@ -716,7 +728,7 @@
"fiddle": "bac à sable" "fiddle": "bac à sable"
}, },
"duplicatePage": { "duplicatePage": {
"Note that this does not copy data, but creates another view of the same data.": "Note that this does not copy data, but creates another view of the same data.", "Note that this does not copy data, but creates another view of the same data.": "Notez que cette opération ne duplique pas les données, mais crée une autre page avec les mêmes données.",
"Duplicate page {{pageName}}": "Dupliquer la page {{pageName}}" "Duplicate page {{pageName}}": "Dupliquer la page {{pageName}}"
}, },
"errorPages": { "errorPages": {
@ -741,12 +753,23 @@
"menus": { "menus": {
"Select fields": "Sélectionner les champs", "Select fields": "Sélectionner les champs",
"* Workspaces are available on team plans. ": "* Les dossiers sont disponibles avec une offre équipe. ", "* Workspaces are available on team plans. ": "* Les dossiers sont disponibles avec une offre équipe. ",
"Upgrade now": "Mettre à jour maintenant" "Upgrade now": "Mettre à jour maintenant",
"Numeric": "Numérique",
"Reference List": "Référence multiple",
"Attachment": "Pièce jointe",
"Text": "Texte",
"Date": "Date",
"DateTime": "Date et Heure",
"Choice": "Choix unique",
"Integer": "Entier",
"Choice List": "Choix multiple",
"Toggle": "Booléen",
"Reference": "Référence"
}, },
"modals": { "modals": {
"Save": "Enregistrer", "Save": "Enregistrer",
"Cancel": "Annuler", "Cancel": "Annuler",
"Ok": "Ok" "Ok": "OK"
}, },
"pages": { "pages": {
"Rename": "Renommer", "Rename": "Renommer",
@ -820,7 +843,7 @@
"Configuring your document": "Configuration de votre document", "Configuring your document": "Configuration de votre document",
"Double-click or hit {{enter}} on a cell to edit it. ": "Double-cliquer ou appuyer sur {{enter}} sur une cellule pour la modifier ", "Double-click or hit {{enter}} on a cell to edit it. ": "Double-cliquer ou appuyer sur {{enter}} sur une cellule pour la modifier ",
"Editing Data": "Modification des données", "Editing Data": "Modification des données",
"Welcome to Grist!": "Bienvenue sur Grist!", "Welcome to Grist!": "Bienvenue sur Grist !",
"Start with {{equal}} to enter a formula.": "Commencer par {{equal}} pour ajouter une formule.", "Start with {{equal}} to enter a formula.": "Commencer par {{equal}} pour ajouter une formule.",
"Sharing": "Partager", "Sharing": "Partager",
"Reference": "Référence", "Reference": "Référence",
@ -868,7 +891,7 @@
}, },
"ColumnInfo": { "ColumnInfo": {
"COLUMN DESCRIPTION": "DESCRIPTION", "COLUMN DESCRIPTION": "DESCRIPTION",
"COLUMN ID: ": "ID: ", "COLUMN ID: ": "Identifiant de la colonne : ",
"COLUMN LABEL": "LIBELLÉ", "COLUMN LABEL": "LIBELLÉ",
"Cancel": "Annuler", "Cancel": "Annuler",
"Save": "Enregistrer" "Save": "Enregistrer"
@ -902,7 +925,7 @@
"SHOW COLUMN": "MONTRER LA COLONNE" "SHOW COLUMN": "MONTRER LA COLONNE"
}, },
"HyperLinkEditor": { "HyperLinkEditor": {
"[link label] url": "[label du lien] url" "[link label] url": "[label du lien] URL"
}, },
"GristTooltips": { "GristTooltips": {
"Apply conditional formatting to cells in this column when formula conditions are met.": "Appliquez un formatage conditionnel aux cellules de cette colonne lorsque les conditions de la formule sont remplies.", "Apply conditional formatting to cells in this column when formula conditions are met.": "Appliquez un formatage conditionnel aux cellules de cette colonne lorsque les conditions de la formule sont remplies.",
@ -942,6 +965,25 @@
"Add New": "Nouveau", "Add New": "Nouveau",
"Access rules give you the power to create nuanced rules to determine who can see or edit which parts of your document.": "Les règles d'accès vous donnent le pouvoir de créer des règles nuancées pour déterminer qui peut voir ou modifier quelles parties de votre document.", "Access rules give you the power to create nuanced rules to determine who can see or edit which parts of your document.": "Les règles d'accès vous donnent le pouvoir de créer des règles nuancées pour déterminer qui peut voir ou modifier quelles parties de votre document.",
"Use the 𝚺 icon to create summary (or pivot) tables, for totals or subtotals.": "Utilisez l'icône 𝚺 pour créer des tables récapitulatives (ou tables croisées dynamiques), pour les totaux ou les sous-totaux.", "Use the 𝚺 icon to create summary (or pivot) tables, for totals or subtotals.": "Utilisez l'icône 𝚺 pour créer des tables récapitulatives (ou tables croisées dynamiques), pour les totaux ou les sous-totaux.",
"Unpin to hide the the button while keeping the filter.": "Détachez pour cacher le bouton tout en conservant le filtre." "Unpin to hide the the button while keeping the filter.": "Détachez pour cacher le bouton tout en conservant le filtre.",
"Anchor Links": "Ancres",
"Custom Widgets": "Vues personnalisées"
},
"ColumnTitle": {
"Add description": "Ajouter une description",
"Cancel": "Annuler",
"Column ID copied to clipboard": "Identifiant de la column copié",
"COLUMN ID: ": "Identifiant de la column : ",
"Column description": "Description de la colonne",
"Column label": "Libellé de la colonne",
"Provide a column label": "Fournir un libellé pour la colonne",
"Save": "Sauvegarder"
},
"DescriptionConfig": {
"DESCRIPTION": "DESCRIPTION"
},
"PagePanels": {
"Open Creator Panel": "Ouvrir le menu latéral",
"Close Creator Panel": "Fermer le menu latéral"
} }
} }

View File

@ -122,7 +122,8 @@
"SELECTOR FOR": "SELETTORE PER", "SELECTOR FOR": "SELETTORE PER",
"Series_one": "Serie", "Series_one": "Serie",
"Series_other": "Serie", "Series_other": "Serie",
"Sort & Filter": "Ordina e filtra" "Sort & Filter": "Ordina e filtra",
"Add referenced columns": "Aggiungi colonne referenziate"
}, },
"RowContextMenu": { "RowContextMenu": {
"Copy anchor link": "Copia link", "Copy anchor link": "Copia link",
@ -853,7 +854,8 @@
"Override widget title": "Sovrascrivi titolo widget", "Override widget title": "Sovrascrivi titolo widget",
"Provide a table name": "Inserisci un nome per la tabella", "Provide a table name": "Inserisci un nome per la tabella",
"Save": "Salva", "Save": "Salva",
"WIDGET TITLE": "TITOLO WIDGET" "WIDGET TITLE": "TITOLO WIDGET",
"WIDGET DESCRIPTION": "DESCRIZIONE WIDGET"
}, },
"breadcrumbs": { "breadcrumbs": {
"You may make edits, but they will create a new copy and will\nnot affect the original document.": "Puoi fare delle modifiche, ma queste generano una nuova copia\ne l'originale resta immutato.", "You may make edits, but they will create a new copy and will\nnot affect the original document.": "Puoi fare delle modifiche, ma queste generano una nuova copia\ne l'originale resta immutato.",
@ -983,6 +985,7 @@
"COLUMN ID: ": "ID COLONNA: ", "COLUMN ID: ": "ID COLONNA: ",
"Column label": "Etichetta colonna", "Column label": "Etichetta colonna",
"Provide a column label": "Dare un'etichetta alla colonna", "Provide a column label": "Dare un'etichetta alla colonna",
"Cancel": "Annulla" "Cancel": "Annulla",
"Close": "Chiudi"
} }
} }

View File

@ -589,7 +589,8 @@
"Columns_one": "Coluna", "Columns_one": "Coluna",
"Columns_other": "Colunas", "Columns_other": "Colunas",
"Fields_one": "Campo", "Fields_one": "Campo",
"Fields_other": "Campos" "Fields_other": "Campos",
"Add referenced columns": "Adicionar colunas referenciadas"
}, },
"RowContextMenu": { "RowContextMenu": {
"Copy anchor link": "Copiar o link de ancoragem", "Copy anchor link": "Copiar o link de ancoragem",
@ -779,7 +780,8 @@
"Override widget title": "Substituir o título do Widget", "Override widget title": "Substituir o título do Widget",
"Provide a table name": "Forneça um nome de tabela", "Provide a table name": "Forneça um nome de tabela",
"Save": "Salvar", "Save": "Salvar",
"WIDGET TITLE": "TÍTULO DO WIDGET" "WIDGET TITLE": "TÍTULO DO WIDGET",
"WIDGET DESCRIPTION": "DESCRIÇÃO DO WIDGET"
}, },
"breadcrumbs": { "breadcrumbs": {
"You may make edits, but they will create a new copy and will\nnot affect the original document.": "Você pode fazer edições, mas elas criarão uma nova cópia e\nnão afetarão o documento original.", "You may make edits, but they will create a new copy and will\nnot affect the original document.": "Você pode fazer edições, mas elas criarão uma nova cópia e\nnão afetarão o documento original.",
@ -1047,6 +1049,7 @@
"Add description": "Adicionar descrição", "Add description": "Adicionar descrição",
"Column ID copied to clipboard": "ID da coluna copiada para a área de transferência", "Column ID copied to clipboard": "ID da coluna copiada para a área de transferência",
"COLUMN ID: ": "ID DA COLUNA: ", "COLUMN ID: ": "ID DA COLUNA: ",
"Provide a column label": "Forneça um rótulo de coluna" "Provide a column label": "Forneça um rótulo de coluna",
"Close": "Fechar"
} }
} }

View File

@ -0,0 +1,96 @@
import { assert, driver, Key } from 'mocha-webdriver';
import * as gu from 'test/nbrowser/gristUtils';
import { setupTestSuite } from 'test/nbrowser/testUtils';
describe('DescriptionWidget', function() {
this.timeout(20000);
const cleanup = setupTestSuite();
it('should support basic edition in right panel', async () => {
const mainSession = await gu.session().teamSite.login();
await mainSession.tempDoc(cleanup, "CardView.grist", { load: true });
const newWidgetDesc = "This is the widget description\nIt is in two lines";
await driver.find('.test-right-opener').click();
// Sleep 100ms to let open the right panel and make the description input clickable
await driver.sleep(100);
const rightPanelDescriptionInput = await driver.find('.test-right-panel .test-right-widget-description');
await rightPanelDescriptionInput.click();
await rightPanelDescriptionInput.sendKeys(newWidgetDesc);
// Click on other input to unselect descriptionInput
await driver.find('.test-right-panel .test-right-widget-title').click();
await checkDescValueInWidgetTooltip("Table", newWidgetDesc);
});
it('should support basic edition in widget popup', async () => {
const mainSession = await gu.session().teamSite.login();
await mainSession.tempDoc(cleanup, "CardView.grist", { load: true });
const widgetName = "Table";
const newWidgetDescFirstLine = "First line of the description";
const newWidgetDescSecondLine = "Second line of the description";
await addWidgetDescription(widgetName, newWidgetDescFirstLine, newWidgetDescSecondLine);
await checkDescValueInWidgetTooltip(widgetName, `${newWidgetDescFirstLine}\n${newWidgetDescSecondLine}`);
});
it('should show info tooltip only if there is a description', async () => {
const mainSession = await gu.session().teamSite.login();
await mainSession.tempDoc(cleanup, "CardView.grist", { load: true });
const newWidgetDesc = "New description for widget Table";
await addWidgetDescription("Table", newWidgetDesc);
assert.isFalse(await getWidgetTooltip("Single card").isPresent());
assert.isTrue(await getWidgetTooltip("Table").isPresent());
await checkDescValueInWidgetTooltip("Table", newWidgetDesc);
});
});
async function waitForEditPopup() {
await gu.waitToPass(async () => {
assert.isTrue(await driver.find(".test-widget-title-popup").isDisplayed());
});
}
async function waitForTooltip() {
await gu.waitToPass(async () => {
assert.isTrue(await driver.find(".test-widget-info-tooltip-popup").isDisplayed());
});
}
function getWidgetTitle(widgetName: string) {
return driver.findContent('.test-widget-title-text', `${widgetName}`);
}
function getWidgetTooltip(widgetName: string) {
return getWidgetTitle(widgetName).findClosest(".test-viewsection-title").find(".test-widget-info-tooltip");
}
async function addWidgetDescription(widgetName: string, desc: string, descSecondLine: string = "") {
// Click on the title and open the edition popup
await getWidgetTitle(widgetName).click();
await waitForEditPopup();
const widgetEditPopup = await driver.find('.test-widget-title-popup');
const widgetDescInput = await widgetEditPopup.find('.test-widget-title-section-description-input');
// Edit the description of the widget inside the popup
await widgetDescInput.click();
await widgetDescInput.sendKeys(desc);
if (descSecondLine !== "") {
await widgetDescInput.sendKeys(Key.ENTER, descSecondLine);
}
await widgetDescInput.sendKeys(Key.CONTROL, Key.ENTER);
await gu.waitForServer();
}
async function checkDescValueInWidgetTooltip(widgetName: string, desc: string) {
await getWidgetTooltip(widgetName).click();
await waitForTooltip();
const descriptionTooltip = await driver
.find('.test-widget-info-tooltip-popup');
assert.equal(await descriptionTooltip.getText(), desc);
}