mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
feature widget description (#483)
Add description to widget title popup and right panel
This commit is contained in:
@@ -16,7 +16,7 @@ const RecordLayout = require('./RecordLayout');
|
||||
const commands = require('./commands');
|
||||
const {RowContextMenu} = require('../ui/RowContextMenu');
|
||||
const {parsePasteForView} = require("./BaseView2");
|
||||
const {columnInfoTooltip} = require("../ui/tooltips");
|
||||
const {descriptionInfoTooltip} = require("../ui/tooltips");
|
||||
|
||||
|
||||
/**
|
||||
@@ -247,7 +247,7 @@ DetailView.prototype.buildFieldDom = function(field, row) {
|
||||
kd.cssClass(function() { return 'detail_theme_field_' + self.viewSection.themeDef(); }),
|
||||
dom('div.g_record_detail_label_container',
|
||||
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'),
|
||||
);
|
||||
@@ -280,7 +280,7 @@ DetailView.prototype.buildFieldDom = function(field, row) {
|
||||
kd.cssClass(function() { return 'detail_theme_field_' + self.viewSection.themeDef(); }),
|
||||
dom('div.g_record_detail_label_container',
|
||||
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',
|
||||
kd.toggleClass('scissors', isCopyActive),
|
||||
|
||||
@@ -44,7 +44,7 @@ const {testId, isNarrowScreen} = require('app/client/ui2018/cssVars');
|
||||
const {contextMenu} = require('app/client/ui/contextMenu');
|
||||
const {mouseDragMatchElem} = require('app/client/ui/mouseDrag');
|
||||
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 {NEW_FILTER_JSON} = require('app/client/models/ColumnFilter');
|
||||
const {CombinedStyle} = require("app/client/models/Styles");
|
||||
@@ -1087,7 +1087,7 @@ GridView.prototype.buildDom = function() {
|
||||
if (btn) { btn.click(); }
|
||||
}),
|
||||
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),
|
||||
// We are using editableLabel here, but we don't use it for editing.
|
||||
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).
|
||||
defaultWidgetTitle: ko.PureComputed<string>;
|
||||
|
||||
description: modelUtil.KoSaveableObservable<string>;
|
||||
|
||||
// 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.
|
||||
isRaw: ko.Computed<boolean>;
|
||||
@@ -363,6 +365,9 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel):
|
||||
// Widget title.
|
||||
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'
|
||||
// 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()));
|
||||
|
||||
@@ -6,17 +6,16 @@ import {makeT} from 'app/client/lib/localization';
|
||||
import {setTestState} from 'app/client/lib/testState';
|
||||
import {ViewFieldRec} from 'app/client/models/DocModel';
|
||||
import {autoGrow} from 'app/client/ui/forms';
|
||||
import {textarea} from 'app/client/ui/inputs';
|
||||
import {showTransientTooltip} from 'app/client/ui/tooltips';
|
||||
import {basicButton, primaryButton, textButton} from 'app/client/ui2018/buttons';
|
||||
import {theme, vars} from 'app/client/ui2018/cssVars';
|
||||
import {cssTextInput} from 'app/client/ui2018/editableLabel';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
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 {IOpenController, PopupControl, setPopupToCreateDom} from 'popweasel';
|
||||
import { cssInput, cssLabel, cssRenamePopup, cssTextArea } from 'app/client/ui/RenamePopupStyles';
|
||||
|
||||
|
||||
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', `
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -298,17 +287,6 @@ const cssColLabelBlock = styled('div', `
|
||||
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', `
|
||||
font-size: ${vars.xsmallFontSize};
|
||||
font-weight: ${vars.bigControlTextWeight};
|
||||
@@ -321,29 +299,6 @@ const cssColId = styled('div', `
|
||||
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', `
|
||||
display: flex;
|
||||
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 */
|
||||
}
|
||||
`);
|
||||
|
||||
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 {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 {textarea} from 'app/client/ui/inputs';
|
||||
import {cssLabel, cssRow} from 'app/client/ui/RightPanelStyles';
|
||||
import {testId, theme} from 'app/client/ui2018/cssVars';
|
||||
import {dom, fromKo, MultiHolder, styled} from 'grainjs';
|
||||
|
||||
const t = makeT('FieldConfig');
|
||||
const t = makeT('DescriptionConfig');
|
||||
|
||||
export function buildDescriptionConfig(
|
||||
owner: MultiHolder,
|
||||
origColumn: ColumnRec,
|
||||
cursor: ko.Computed<CursorPos>,
|
||||
description: KoSaveableObservable<string>,
|
||||
options: {
|
||||
cursor: ko.Computed<CursorPos>,
|
||||
testPrefix: string,
|
||||
},
|
||||
) {
|
||||
|
||||
// We will listen to cursor position and force a blur event on
|
||||
@@ -22,7 +25,7 @@ export function buildDescriptionConfig(
|
||||
// update a different column.
|
||||
let editor: HTMLTextAreaElement | undefined;
|
||||
owner.autoDispose(
|
||||
cursor.subscribe(() => {
|
||||
options.cursor.subscribe(() => {
|
||||
editor?.blur();
|
||||
})
|
||||
);
|
||||
@@ -30,14 +33,14 @@ export function buildDescriptionConfig(
|
||||
return [
|
||||
cssLabel(t("DESCRIPTION")),
|
||||
cssRow(
|
||||
editor = cssTextArea(fromKo(origColumn.description),
|
||||
editor = cssTextArea(fromKo(description),
|
||||
{ onInput: false },
|
||||
{ rows: '3' },
|
||||
dom.on('blur', async (e, elem) => {
|
||||
await origColumn.description.setAndSave(elem.value.trim());
|
||||
await description.saveOnly(elem.value);
|
||||
}),
|
||||
testId('column-description'),
|
||||
autoGrow(fromKo(origColumn.description))
|
||||
testId(`${options.testPrefix}-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),
|
||||
),
|
||||
cssSection(
|
||||
dom.create(buildDescriptionConfig, origColumn, cursor),
|
||||
dom.create(buildDescriptionConfig, origColumn.description, { cursor, "testPrefix": "column" }),
|
||||
),
|
||||
cssSeparator(),
|
||||
cssSection(
|
||||
@@ -361,6 +361,13 @@ export class RightPanel extends Disposable {
|
||||
const hasColumnMapping = use(activeSection.columnsToMap);
|
||||
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) => [
|
||||
this._disableIfReadonly(),
|
||||
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')
|
||||
)),
|
||||
|
||||
cssSection(
|
||||
dom.create(buildDescriptionConfig, activeSection.description, { cursor, "testPrefix": "right-widget" }),
|
||||
),
|
||||
|
||||
dom.maybe(
|
||||
(use) => !use(activeSection.isRaw),
|
||||
() => cssRow(
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import * as commands from 'app/client/components/commands';
|
||||
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 {basicButton, cssButton, primaryButton} from 'app/client/ui2018/buttons';
|
||||
import {theme, vars} from 'app/client/ui2018/cssVars';
|
||||
import {cssTextInput} from 'app/client/ui2018/editableLabel';
|
||||
import { theme } from 'app/client/ui2018/cssVars';
|
||||
import {menuCssClass} from 'app/client/ui2018/menus';
|
||||
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 { descriptionInfoTooltip } from './tooltips';
|
||||
import { autoGrow } from './forms';
|
||||
import { cssInput, cssLabel, cssRenamePopup, cssTextArea } from 'app/client/ui/RenamePopupStyles';
|
||||
|
||||
const testId = makeTestId('test-widget-title-');
|
||||
const t = makeT('WidgetTitle');
|
||||
@@ -19,17 +22,20 @@ interface WidgetTitleOptions {
|
||||
|
||||
export function buildWidgetTitle(vs: ViewSectionRec, options: WidgetTitleOptions, ...args: DomElementArg[]) {
|
||||
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[]) {
|
||||
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(
|
||||
vs: ViewSectionRec,
|
||||
title: Observable<string>,
|
||||
description: Observable<string>,
|
||||
options: WidgetTitleOptions,
|
||||
...args: DomElementArg[]) {
|
||||
return cssTitleContainer(
|
||||
@@ -48,6 +54,9 @@ export function buildRenameWidget(
|
||||
},
|
||||
dom.on('click', (ev) => { ev.stopPropagation(); ev.preventDefault(); }),
|
||||
),
|
||||
dom.maybe(description, () => [
|
||||
descriptionInfoTooltip(description.get(), "widget")
|
||||
]),
|
||||
...args
|
||||
);
|
||||
}
|
||||
@@ -69,11 +78,19 @@ function buildWidgetRenamePopup(ctrl: IOpenController, vs: ViewSectionRec, optio
|
||||
// - when widget title is set, shows just a text to override it.
|
||||
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 newTableName = use(inputTableName)?.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.
|
||||
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());
|
||||
@@ -99,10 +116,20 @@ function buildWidgetRenamePopup(ctrl: IOpenController, vs: ViewSectionRec, optio
|
||||
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(),
|
||||
saveWidgetTitle()
|
||||
]), {close: true});
|
||||
saveWidgetTitle(),
|
||||
saveWidgetDesc()
|
||||
]);
|
||||
|
||||
function initialFocus() {
|
||||
const isRawView = !widgetInput;
|
||||
@@ -122,18 +149,72 @@ function buildWidgetRenamePopup(ctrl: IOpenController, vs: ViewSectionRec, optio
|
||||
}
|
||||
}
|
||||
|
||||
// Build actual dom that looks like:
|
||||
// DATA TABLE NAME
|
||||
// [input]
|
||||
// WIDGET TITLE
|
||||
// [input]
|
||||
// [Save] [Cancel]
|
||||
// When the popup is closing we will save everything, unless the user has pressed the cancel button.
|
||||
let cancelled = false;
|
||||
|
||||
// Function to close the popup with saving.
|
||||
const close = () => ctrl.close();
|
||||
|
||||
// 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 widgetInput: HTMLInputElement|undefined;
|
||||
let descInput: HTMLTextAreaElement | undefined;
|
||||
return cssRenamePopup(
|
||||
// Create a FocusLayer to keep focus in this popup while it's active, and prevent keyboard
|
||||
// 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'),
|
||||
dom.cls(menuCssClass),
|
||||
dom.maybe(!options.tableNameHidden, () => [
|
||||
@@ -144,30 +225,41 @@ function buildWidgetRenamePopup(ctrl: IOpenController, vs: ViewSectionRec, optio
|
||||
inputTableName,
|
||||
updateOnKey,
|
||||
{disabled: isSummary, placeholder: t("Provide a table name")},
|
||||
testId('table-name-input')
|
||||
testId('table-name-input'),
|
||||
commandGroup.attach(),
|
||||
),
|
||||
]),
|
||||
dom.maybe(!options.widgetNameHidden, () => [
|
||||
cssLabel(t("WIDGET TITLE")),
|
||||
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(
|
||||
primaryButton(t("Save"),
|
||||
dom.on('click', doSave),
|
||||
dom.on('click', close),
|
||||
dom.boolAttr('disabled', use => use(disableSave) || use(modalCtl.workInProgress)),
|
||||
testId('save'),
|
||||
),
|
||||
basicButton(t("Cancel"),
|
||||
testId('cancel'),
|
||||
dom.on('click', () => modalCtl.close())
|
||||
dom.on('click', cancel)
|
||||
),
|
||||
),
|
||||
dom.onKeyDown({
|
||||
Escape: () => modalCtl.close(),
|
||||
// On enter save or cancel - depending on the change.
|
||||
Enter: () => disableSave.get() ? modalCtl.close() : doSave(),
|
||||
Enter$: e => {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}),
|
||||
elem => { setTimeout(initialFocus, 0); },
|
||||
);
|
||||
@@ -180,6 +272,10 @@ const cssTitleContainer = styled('div', `
|
||||
flex: 1 1 0px;
|
||||
min-width: 0px;
|
||||
display: flex;
|
||||
.info_toggle_icon {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
}
|
||||
`);
|
||||
|
||||
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', `
|
||||
display: flex;
|
||||
margin-top: 16px;
|
||||
@@ -226,29 +302,3 @@ const cssButtons = styled('div', `
|
||||
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[]) {
|
||||
return cssColumnInfoTooltipButton(
|
||||
export function descriptionInfoTooltip(
|
||||
content: DomContents,
|
||||
testPrefix: string,
|
||||
...domArgs: DomElementArg[]) {
|
||||
return cssDescriptionInfoTooltipButton(
|
||||
icon('Info', dom.cls("info_toggle_icon")),
|
||||
testId('column-info-tooltip'),
|
||||
testId(`${testPrefix}-info-tooltip`),
|
||||
dom.on('mousedown', (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,
|
||||
key: 'columnDescription',
|
||||
openOnClick: true,
|
||||
@@ -365,7 +368,8 @@ export function withInfoTooltip(
|
||||
);
|
||||
}
|
||||
|
||||
const cssColumnInfoTooltip = styled('div', `
|
||||
|
||||
const cssDescriptionInfoTooltip = styled('div', `
|
||||
white-space: pre-wrap;
|
||||
text-align: left;
|
||||
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 */
|
||||
`);
|
||||
|
||||
const cssColumnInfoTooltipButton = styled('div', `
|
||||
const cssDescriptionInfoTooltipButton = styled('div', `
|
||||
cursor: pointer;
|
||||
--icon-color: ${theme.infoButtonFg};
|
||||
border-radius: 50%;
|
||||
|
||||
Reference in New Issue
Block a user