mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
Merge pull request #406 from incubateur-territoires/column-description
feat: Add a description to a grist table column
This commit is contained in:
@@ -30,6 +30,17 @@
|
||||
padding: .5rem;
|
||||
}
|
||||
|
||||
.g_record_detail_label_container {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.g_record_detail_label_container .info_toggle_icon {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.g_record_detail_label {
|
||||
min-height: 1rem;
|
||||
color: #666;
|
||||
@@ -150,7 +161,7 @@
|
||||
-webkit-flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.detail_theme_field_under > .g_record_detail_label {
|
||||
.detail_theme_field_under .g_record_detail_label {
|
||||
border-top: 1px solid #333;
|
||||
}
|
||||
|
||||
@@ -208,7 +219,7 @@
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.detail_theme_field_compact > .g_record_detail_label {
|
||||
.detail_theme_field_compact .g_record_detail_label {
|
||||
font-weight: normal;
|
||||
font-size: var(--grist-small-font-size);
|
||||
color: var(--grist-theme-card-compact-label, var(--grist-color-slate));
|
||||
@@ -230,7 +241,7 @@
|
||||
padding: 1px 1px 1px 5px;
|
||||
}
|
||||
|
||||
.detail_theme_field_form > .g_record_detail_label {
|
||||
.detail_theme_field_form .g_record_detail_label {
|
||||
font-size: var(--grist-small-font-size);
|
||||
color: var(--grist-theme-card-form-label, var(--grist-color-slate));
|
||||
font-weight: bold;
|
||||
@@ -241,6 +252,10 @@
|
||||
margin-right: -8px;
|
||||
}
|
||||
|
||||
.detail_theme_field_form .g_record_detail_label_container {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* TODO want to style better the values themselves (e.g. more padding, rounded corners, move label
|
||||
* inside value box for compact view for better cursor looks, etc), but first the cell editor
|
||||
* needs to learn to match the value box's style. Right now, the cell editor style is hard-coded.
|
||||
@@ -288,7 +303,7 @@
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.detail_theme_field_blocks > .g_record_detail_label {
|
||||
.detail_theme_field_blocks .g_record_detail_label {
|
||||
font-size: var(--grist-small-font-size);
|
||||
color: var(--grist-theme-card-blocks-label, var(--grist-color-slate));
|
||||
font-weight: normal;
|
||||
|
||||
@@ -15,6 +15,7 @@ const RecordLayout = require('./RecordLayout');
|
||||
const commands = require('./commands');
|
||||
const {RowContextMenu} = require('../ui/RowContextMenu');
|
||||
const {parsePasteForView} = require("./BaseView2");
|
||||
const {columnInfoTooltip} = require("../ui/tooltips");
|
||||
|
||||
/**
|
||||
* DetailView component implements a list of record layouts.
|
||||
@@ -227,8 +228,10 @@ DetailView.prototype.buildFieldDom = function(field, row) {
|
||||
if (field.isNewField) {
|
||||
return dom('div.g_record_detail_el.flexitem',
|
||||
kd.cssClass(function() { return 'detail_theme_field_' + self.viewSection.themeDef(); }),
|
||||
dom('div.g_record_detail_label', field.label),
|
||||
dom('div.g_record_detail_value', field.value)
|
||||
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)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -257,7 +260,10 @@ DetailView.prototype.buildFieldDom = function(field, row) {
|
||||
dom.autoDispose(isCellSelected),
|
||||
dom.autoDispose(isCellActive),
|
||||
kd.cssClass(function() { return 'detail_theme_field_' + self.viewSection.themeDef(); }),
|
||||
dom('div.g_record_detail_label', kd.text(field.displayLabel)),
|
||||
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)
|
||||
),
|
||||
dom('div.g_record_detail_value',
|
||||
kd.toggleClass('scissors', isCopyActive),
|
||||
kd.toggleClass('record-add', row._isAddRow),
|
||||
|
||||
@@ -20,6 +20,7 @@ export interface ViewFieldRec extends IRowModel<"_grist_Views_section_field">, R
|
||||
origCol: ko.Computed<ColumnRec>;
|
||||
colId: ko.Computed<string>;
|
||||
label: ko.Computed<string>;
|
||||
description: ko.Computed<string>;
|
||||
|
||||
// displayLabel displays label by default but switches to the more helpful colId whenever a
|
||||
// formula field in the view is being edited.
|
||||
@@ -108,6 +109,7 @@ export function createViewFieldRec(this: ViewFieldRec, docModel: DocModel): void
|
||||
this.origCol = ko.pureComputed(() => this.column().origCol());
|
||||
this.colId = ko.pureComputed(() => this.column().colId());
|
||||
this.label = ko.pureComputed(() => this.column().label());
|
||||
this.description = ko.pureComputed(() => this.column().description());
|
||||
|
||||
// displayLabel displays label by default but switches to the more helpful colId whenever a
|
||||
// formula field in the view is being edited.
|
||||
|
||||
@@ -5,10 +5,10 @@ import {BEHAVIOR, ColumnRec} from 'app/client/models/entities/ColumnRec';
|
||||
import {buildHighlightedCode, cssCodeBlock} from 'app/client/ui/CodeHighlight';
|
||||
import {GristTooltips} from 'app/client/ui/GristTooltips';
|
||||
import {cssBlockedCursor, cssLabel, cssRow} from 'app/client/ui/RightPanelStyles';
|
||||
import {withInfoTooltip} from 'app/client/ui/tooltips';
|
||||
import { withInfoTooltip } from 'app/client/ui/tooltips';
|
||||
import {buildFormulaTriggers} from 'app/client/ui/TriggerFormulas';
|
||||
import {textButton} from 'app/client/ui2018/buttons';
|
||||
import {testId, theme} from 'app/client/ui2018/cssVars';
|
||||
import { testId, theme } from 'app/client/ui2018/cssVars';
|
||||
import {textInput} from 'app/client/ui2018/editableLabel';
|
||||
import {cssIconButton, icon} from 'app/client/ui2018/icons';
|
||||
import {IconName} from 'app/client/ui2018/IconList';
|
||||
@@ -18,6 +18,7 @@ import {sanitizeIdent} from 'app/common/gutil';
|
||||
import {bundleChanges, Computed, dom, DomContents, DomElementArg, fromKo, MultiHolder,
|
||||
Observable, styled} from 'grainjs';
|
||||
import * as ko from 'knockout';
|
||||
import { textarea } from './inputs';
|
||||
|
||||
const t = makeT('FieldConfig');
|
||||
|
||||
@@ -88,6 +89,40 @@ export function buildNameConfig(
|
||||
];
|
||||
}
|
||||
|
||||
export function buildDescriptionConfig(
|
||||
owner: MultiHolder,
|
||||
origColumn: ColumnRec,
|
||||
cursor: ko.Computed<CursorPos>,
|
||||
) {
|
||||
|
||||
// We will listen to cursor position and force a blur event on
|
||||
// the text input, which will trigger save before the column observable
|
||||
// will change its value.
|
||||
// Otherwise, blur will be invoked after column change and save handler will
|
||||
// update a different column.
|
||||
let editor: HTMLTextAreaElement | undefined;
|
||||
owner.autoDispose(
|
||||
cursor.subscribe(() => {
|
||||
editor?.blur();
|
||||
})
|
||||
);
|
||||
|
||||
return [
|
||||
cssLabel(t("DESCRIPTION")),
|
||||
cssRow(
|
||||
editor = cssTextArea(fromKo(origColumn.description),
|
||||
{ onInput: false },
|
||||
{ rows: '3' },
|
||||
dom.on('blur', async (e, elem) => {
|
||||
await origColumn.description.saveOnly(elem.value);
|
||||
}),
|
||||
testId('column-description'),
|
||||
)
|
||||
),
|
||||
|
||||
];
|
||||
}
|
||||
|
||||
type SaveHandler = (column: ColumnRec, formula: string) => Promise<void>;
|
||||
type BuildEditor = (
|
||||
cellElem: Element,
|
||||
@@ -494,3 +529,22 @@ const cssInput = styled(textInput, `
|
||||
color: ${theme.inputDisabledFg};
|
||||
}
|
||||
`);
|
||||
|
||||
const cssTextArea = styled(textarea, `
|
||||
color: ${theme.inputFg};
|
||||
background-color: ${theme.mainPanelBg};
|
||||
border: 1px solid ${theme.inputBorder};
|
||||
width: 100%;
|
||||
outline: none;
|
||||
border-radius: 3px;
|
||||
padding: 3px 7px;
|
||||
|
||||
&::placeholder {
|
||||
color: ${theme.inputPlaceholderFg};
|
||||
}
|
||||
|
||||
&[readonly] {
|
||||
background-color: ${theme.inputDisabledBg};
|
||||
color: ${theme.inputDisabledFg};
|
||||
}
|
||||
`);
|
||||
|
||||
@@ -43,6 +43,7 @@ import {bundleChanges, Computed, Disposable, dom, domComputed, DomContents,
|
||||
DomElementArg, DomElementMethod, IDomComponent} from 'grainjs';
|
||||
import {MultiHolder, Observable, styled, subscribe} from 'grainjs';
|
||||
import * as ko from 'knockout';
|
||||
import { buildDescriptionConfig } from './FieldConfig';
|
||||
|
||||
const t = makeT('RightPanel');
|
||||
|
||||
@@ -235,6 +236,9 @@ export class RightPanel extends Disposable {
|
||||
cssSection(
|
||||
dom.create(buildNameConfig, origColumn, cursor, isMultiSelect),
|
||||
),
|
||||
cssSection(
|
||||
dom.create(buildDescriptionConfig, origColumn, cursor),
|
||||
),
|
||||
cssSeparator(),
|
||||
cssSection(
|
||||
dom.create(buildFormulaConfig,
|
||||
|
||||
@@ -242,7 +242,7 @@ export function tooltipCloseButton(ctl: ITooltipControl): HTMLElement {
|
||||
/**
|
||||
* Renders an info icon that shows a tooltip with the specified `content` on click.
|
||||
*/
|
||||
function infoTooltip(content: DomContents, menuOptions?: IMenuOptions, ...domArgs: DomElementArg[]) {
|
||||
export function infoTooltip(content: DomContents, menuOptions?: IMenuOptions, ...domArgs: DomElementArg[]) {
|
||||
return cssInfoTooltipButton('?',
|
||||
(elem) => {
|
||||
setPopupToCreateDom(
|
||||
@@ -314,6 +314,62 @@ export function withInfoTooltip(
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an column info icon that shows a tooltip with the specified `content` on click.
|
||||
*/
|
||||
export function columnInfoTooltip(content: DomContents, menuOptions?: IMenuOptions, ...domArgs: DomElementArg[]) {
|
||||
return cssColumnInfoTooltipButton(
|
||||
icon('Info', dom.cls("info_toggle_icon")),
|
||||
(elem) => {
|
||||
setPopupToCreateDom(
|
||||
elem,
|
||||
(ctl) => {
|
||||
return cssInfoTooltipPopup(
|
||||
cssInfoTooltipPopupCloseButton(
|
||||
icon('CrossSmall'),
|
||||
dom.on('click', () => ctl.close()),
|
||||
testId('column-info-tooltip-close'),
|
||||
),
|
||||
cssInfoTooltipPopupBody(
|
||||
content,
|
||||
{ style: 'white-space: pre-wrap;' },
|
||||
testId('column-info-tooltip-popup-body'),
|
||||
),
|
||||
dom.cls(menuCssClass),
|
||||
dom.cls(cssMenu.className),
|
||||
dom.onKeyDown({
|
||||
Enter: () => ctl.close(),
|
||||
Escape: () => ctl.close(),
|
||||
}),
|
||||
(popup) => { setTimeout(() => popup.focus(), 0); },
|
||||
testId('column-info-tooltip-popup'),
|
||||
);
|
||||
},
|
||||
{ ...defaultMenuOptions, ...{ placement: 'bottom-end' }, ...menuOptions },
|
||||
);
|
||||
},
|
||||
testId('column-info-tooltip'),
|
||||
...domArgs,
|
||||
);
|
||||
}
|
||||
|
||||
const cssColumnInfoTooltipButton = styled('div', `
|
||||
cursor: pointer;
|
||||
--icon-color: ${theme.infoButtonFg};
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-left: 5px;
|
||||
line-height: 0px;
|
||||
|
||||
&:hover {
|
||||
--icon-color: ${theme.infoButtonHoverFg};
|
||||
}
|
||||
&:active {
|
||||
--icon-color: ${theme.infoButtonActiveFg};
|
||||
}
|
||||
`);
|
||||
|
||||
|
||||
const cssTooltip = styled('div', `
|
||||
position: absolute;
|
||||
z-index: 5000; /* should be higher than a modal */
|
||||
|
||||
@@ -605,6 +605,11 @@ export const theme = {
|
||||
menuToggleBg: new CustomProp('theme-menu-toggle-bg', undefined, 'white'),
|
||||
menuToggleBorder: new CustomProp('theme-menu-toggle-border', undefined, colors.slate),
|
||||
|
||||
/* Info Button */
|
||||
infoButtonFg: new CustomProp('theme-info-button-fg', undefined, "#8F8F8F"),
|
||||
infoButtonHoverFg: new CustomProp('theme-info-button-hover-fg', undefined, "#707070"),
|
||||
infoButtonActiveFg: new CustomProp('theme-info-button-active-fg', undefined, "#5C5C5C"),
|
||||
|
||||
/* Button Groups */
|
||||
buttonGroupFg: new CustomProp('theme-button-group-fg', undefined, colors.dark),
|
||||
buttonGroupLightFg: new CustomProp('theme-button-group-light-fg', undefined, colors.slate),
|
||||
|
||||
Reference in New Issue
Block a user