mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) updates from grist-core
This commit is contained in:
commit
84854b7cfa
@ -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:
|
||||||
|
|
||||||
|
@ -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),
|
||||||
|
@ -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)),
|
||||||
|
@ -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()));
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
|
@ -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))
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
75
app/client/ui/RenamePopupStyles.ts
Normal file
75
app/client/ui/RenamePopupStyles.ts
Normal 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};
|
||||||
|
}
|
||||||
|
`);
|
@ -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(
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
|
@ -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%;
|
||||||
|
@ -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; }
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.",
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 d’initialisation",
|
"Set trigger formula": "Définir une formule d’initialisation",
|
||||||
"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 n’avez pas accès en écriture à ce dossier"
|
"You do not have write access to the selected workspace": "Vous n’avez 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 à l’améliorer.",
|
"What brings you to Grist? Please help us serve you better.": "Pourquoi utilisez-vous Grist ? Aidez-nous à l’amé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 n’affecteront 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 n’affecteront 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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
96
test/nbrowser/DescriptionWidget.ts
Normal file
96
test/nbrowser/DescriptionWidget.ts
Normal 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);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user