mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Adding UI for reverse columns
Summary: - Adding an UI for two-way reference column. - Reusing table name as label for the reverse column Test Plan: Updated Reviewers: georgegevoian Reviewed By: georgegevoian Differential Revision: https://phab.getgrist.com/D4344
This commit is contained in:
@@ -109,7 +109,7 @@ export function buildFormulaConfig(
|
||||
) {
|
||||
|
||||
// If we can't modify anything about the column.
|
||||
const disableModify = Computed.create(owner, use => use(origColumn.disableModify));
|
||||
const disableModify = Computed.create(owner, use => use(origColumn.disableModify) || use(origColumn.hasReverse));
|
||||
|
||||
// Intermediate state - user wants to specify formula, but haven't done yet
|
||||
const maybeFormula = Observable.create(owner, false);
|
||||
@@ -320,8 +320,10 @@ export function buildFormulaConfig(
|
||||
|
||||
// Should we disable all other action buttons and formula editor. For now
|
||||
// we will disable them when multiple columns are selected, or any of the column selected
|
||||
// can't be modified.
|
||||
const disableOtherActions = Computed.create(owner, use => use(disableModify) || use(isMultiSelect));
|
||||
// can't be modified or if the column has a reverse column.
|
||||
const disableOtherActions = Computed.create(owner,
|
||||
use => use(disableModify) || use(isMultiSelect) || use(origColumn.hasReverse)
|
||||
);
|
||||
|
||||
const errorMessage = createFormulaErrorObs(owner, gristDoc, origColumn);
|
||||
// Helper that will create different flavors for formula builder.
|
||||
|
||||
@@ -43,7 +43,9 @@ export type Tooltip =
|
||||
| 'accessRulesTableWide'
|
||||
| 'setChoiceDropdownCondition'
|
||||
| 'setRefDropdownCondition'
|
||||
| 'communityWidgets';
|
||||
| 'communityWidgets'
|
||||
| 'twoWayReferences'
|
||||
| 'reasignTwoWayReference';
|
||||
|
||||
export type TooltipContentFunc = (...domArgs: DomElementArg[]) => DomContents;
|
||||
|
||||
@@ -162,6 +164,21 @@ see or edit which parts of your document.')
|
||||
),
|
||||
...args,
|
||||
),
|
||||
twoWayReferences: (...args: DomElementArg[]) => cssTooltipContent(
|
||||
dom('div',
|
||||
t('Creates a reverse column in target table that can be edited from either end.')
|
||||
),
|
||||
...args,
|
||||
),
|
||||
reasignTwoWayReference: (...args: DomElementArg[]) => cssTooltipContent(
|
||||
dom('div',
|
||||
t('This limitation occurs when one end of a two-way reference is configured as a single Reference.')
|
||||
),
|
||||
dom('div',
|
||||
t('To allow multiple assignments, change the type of the Reference column to Reference List.')
|
||||
),
|
||||
...args,
|
||||
),
|
||||
};
|
||||
|
||||
export interface BehavioralPromptContent {
|
||||
|
||||
@@ -36,7 +36,7 @@ import {GridOptions} from 'app/client/ui/GridOptions';
|
||||
import {textarea} from 'app/client/ui/inputs';
|
||||
import {attachPageWidgetPicker, IPageWidget, toPageWidget} from 'app/client/ui/PageWidgetPicker';
|
||||
import {PredefinedCustomSectionConfig} from "app/client/ui/PredefinedCustomSectionConfig";
|
||||
import {cssLabel} from 'app/client/ui/RightPanelStyles';
|
||||
import {cssLabel, cssSeparator} from 'app/client/ui/RightPanelStyles';
|
||||
import {linkId, NoLink, selectBy} from 'app/client/ui/selectBy';
|
||||
import {VisibleFieldsConfig} from 'app/client/ui/VisibleFieldsConfig';
|
||||
import {getTelemetryWidgetTypeFromVS, getWidgetTypes} from "app/client/ui/widgetTypesMap";
|
||||
@@ -1271,10 +1271,6 @@ const cssTabContents = styled('div', `
|
||||
overflow: auto;
|
||||
`);
|
||||
|
||||
const cssSeparator = styled('div', `
|
||||
border-bottom: 1px solid ${theme.pagePanelsBorder};
|
||||
margin-top: 16px;
|
||||
`);
|
||||
|
||||
const cssConfigContainer = styled('div.test-config-container', `
|
||||
overflow: auto;
|
||||
|
||||
@@ -15,6 +15,13 @@ export const cssLabel = styled('div', `
|
||||
font-size: ${vars.xsmallFontSize};
|
||||
`);
|
||||
|
||||
export const cssLabelText = styled('span', `
|
||||
color: ${theme.text};
|
||||
text-transform: uppercase;
|
||||
font-size: ${vars.xsmallFontSize};
|
||||
`);
|
||||
|
||||
|
||||
export const cssHelp = styled('div', `
|
||||
color: ${theme.lightText};
|
||||
margin: -8px 16px 12px 16px;
|
||||
|
||||
@@ -118,12 +118,11 @@ export function makeViewLayoutMenu(viewSection: ViewSectionRec, isReadonly: bool
|
||||
*/
|
||||
export function makeCollapsedLayoutMenu(viewSection: ViewSectionRec, gristDoc: GristDoc) {
|
||||
const isReadonly = gristDoc.isReadonly.get();
|
||||
const isSinglePage = urlState().state.get().params?.style === 'singlePage';
|
||||
const sectionId = viewSection.table.peek().rawViewSectionRef.peek();
|
||||
const anchorUrlState = { hash: { sectionId, popup: true } };
|
||||
const rawUrl = urlState().makeUrl(anchorUrlState);
|
||||
return [
|
||||
dom.maybe((use) => !use(viewSection.isRaw) && !isSinglePage && !use(gristDoc.maximizedSectionId),
|
||||
dom.maybe((use) => !use(viewSection.isRaw) && use(gristDoc.canShowRawData),
|
||||
() => menuItemLink(
|
||||
{ href: rawUrl}, t("Show raw data"), testId('show-raw-data'),
|
||||
dom.on('click', () => {
|
||||
|
||||
376
app/client/ui/buildReassignModal.ts
Normal file
376
app/client/ui/buildReassignModal.ts
Normal file
@@ -0,0 +1,376 @@
|
||||
import * as commands from 'app/client/components/commands';
|
||||
import {makeT} from 'app/client/lib/localization';
|
||||
import {ColumnRec, DocModel} from 'app/client/models/DocModel';
|
||||
import {withInfoTooltip} from 'app/client/ui/tooltips';
|
||||
import {bigBasicButton, bigPrimaryButton, textButton} from 'app/client/ui2018/buttons';
|
||||
import {labeledSquareCheckbox} from 'app/client/ui2018/checkbox';
|
||||
import {theme} from 'app/client/ui2018/cssVars';
|
||||
import {cssModalBody, cssModalButtons, cssModalTitle, cssModalWidth, modal} from 'app/client/ui2018/modals';
|
||||
import {DocAction} from 'app/common/DocActions';
|
||||
import {cached} from 'app/common/gutil';
|
||||
import {decodeObject, encodeObject} from 'app/plugin/objtypes';
|
||||
import {dom, Observable, styled} from 'grainjs';
|
||||
import mapValues from 'lodash/mapValues';
|
||||
|
||||
const t = makeT('ReassignModal');
|
||||
|
||||
/**
|
||||
* Builds a modal that shows the user that they can't reassign records because of uniqueness
|
||||
* constraints on the Ref/RefList column. It shows the user the conflicts and provides option
|
||||
* to resolve the confilic and retry the change.
|
||||
*
|
||||
* Currently we support uniquness only on 2-way referenced columns. While it is techincally
|
||||
* possible to support it on plain Ref/RefList columns, the implementation assumes that we
|
||||
* have the reverse column somewhere and can use it to find the conflicts without building
|
||||
* a dedicated index.
|
||||
*
|
||||
* Mental model of data structure:
|
||||
* Left table: Owners
|
||||
* Columns: [Name, Pets: RefList(Pets)]
|
||||
*
|
||||
* Right table: Pets
|
||||
* Columns: [Name, Owner: Ref(Owners)]
|
||||
*
|
||||
* Actions that were send to the server were updating the Owners table.
|
||||
*
|
||||
* Note: They could affect multiple columns, not only the Pets column.
|
||||
*/
|
||||
export async function buildReassignModal(options: {
|
||||
docModel: DocModel,
|
||||
actions: DocAction[],
|
||||
}) {
|
||||
const {docModel, actions} = options;
|
||||
|
||||
const tableRec = cached((tableId: string) => {
|
||||
return docModel.getTableModel(tableId).tableMetaRow;
|
||||
});
|
||||
|
||||
const columnRec = cached((tableId: string, colId: string) => {
|
||||
const result = tableRec(tableId).columns().all().find(c => c.colId() === colId);
|
||||
if (!result) {
|
||||
throw new Error(`Column ${colId} not found in table ${tableId}`);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
// Helper that gets records, but caches and copies them, so that we can amend them when needed.
|
||||
const amended = new Map<string, any>();
|
||||
const getRow = (tableId: string, rowId: number) => {
|
||||
const key = `${tableId}:${rowId}`;
|
||||
if (amended.has(key)) {
|
||||
return amended.get(key);
|
||||
}
|
||||
const tableData = docModel.getTableModel(tableId).tableData;
|
||||
const origRow = tableData.getRecord(rowId);
|
||||
if (!origRow) {
|
||||
return null;
|
||||
}
|
||||
const row = structuredClone(origRow);
|
||||
amended.set(key, row);
|
||||
return row;
|
||||
};
|
||||
|
||||
// Helper that returns name of the row (as seen in Ref editor).
|
||||
const rowDisplay = cached((tableId: string, rowId: number, colId: string) => {
|
||||
const col = columnRec(tableId, colId);
|
||||
// Name of the row (for 2-way reference) is the value of visible column in reverse table.
|
||||
const visibleCol = col.reverseColModel().visibleColModel().colId();
|
||||
const record = getRow(tableId, rowId);
|
||||
return record?.[visibleCol] ?? String(rowId);
|
||||
});
|
||||
|
||||
// We will generate set of problems, and then explain it.
|
||||
class Problem {
|
||||
constructor(public data: {
|
||||
tableId: string,
|
||||
colRec: ColumnRec,
|
||||
revRec: ColumnRec,
|
||||
pointer: number,
|
||||
newRowId: number,
|
||||
oldRowId: number,
|
||||
}) {}
|
||||
|
||||
public buildReason() {
|
||||
// Pets record Azor is already assigned to Owners record Bob.
|
||||
const {colRec, revRec, pointer, oldRowId} = this.data;
|
||||
const Pets = revRec.table().tableNameDef();
|
||||
const Owners = colRec.table().tableNameDef();
|
||||
const Azor = rowDisplay(revRec.table().tableId(), pointer, revRec.colId()) as string;
|
||||
const Bob = rowDisplay(colRec.table().tableId(), oldRowId, colRec.colId()) as string;
|
||||
const text = t(
|
||||
`{{targetTable}} record {{targetName}} is already assigned to {{sourceTable}} record \
|
||||
{{oldSourceName}}.`,
|
||||
{
|
||||
targetTable: dom('i', Pets),
|
||||
sourceTable: dom('i', Owners),
|
||||
targetName: dom('b', Azor),
|
||||
oldSourceName: dom('b', Bob),
|
||||
});
|
||||
|
||||
return cssBulletLine(text);
|
||||
}
|
||||
|
||||
public buildHeader() {
|
||||
// Generally we try to show a text like this:
|
||||
// Each Pets record may only be assigned to a single Owners record.
|
||||
const {colRec, revRec} = this.data;
|
||||
// Task is the name of the revRec table
|
||||
const Pets = revRec.table().tableNameDef();
|
||||
const Owners = colRec.table().tableNameDef();
|
||||
return dom('div', [
|
||||
t(`Each {{targetTable}} record may only be assigned to a single {{sourceTable}} record.`,
|
||||
{
|
||||
targetTable: dom('i', Pets),
|
||||
sourceTable: dom('i', Owners),
|
||||
})
|
||||
]);
|
||||
}
|
||||
|
||||
public fixUserAction() {
|
||||
// Fix action is the action that removes Task 17 from Bob.
|
||||
const tableId = this.data.tableId;
|
||||
const colId = this.data.colRec.colId();
|
||||
const oldRowId = this.data.oldRowId;
|
||||
const oldRecord = getRow(tableId, oldRowId);
|
||||
const oldValue = decodeObject(oldRecord[colId]);
|
||||
let newValue: any = Array.isArray(oldValue)
|
||||
? oldValue.filter(v => v !== this.data.pointer)
|
||||
: 0;
|
||||
if (Array.isArray(newValue) && newValue.length === 0) {
|
||||
newValue = null;
|
||||
}
|
||||
oldRecord[colId] = encodeObject(newValue);
|
||||
return ['UpdateRecord', tableId, oldRowId, {[colId]: oldRecord[colId]}];
|
||||
}
|
||||
|
||||
public buildAction(checked: Observable<boolean>, multiple: boolean = false) {
|
||||
// Shows a checkbox and explanation what can be done, checkbox has a text
|
||||
// Reassing to People record Ann
|
||||
// Reasing to new Poeple records.
|
||||
const {colRec, newRowId} = this.data;
|
||||
const Ann = rowDisplay(colRec.table().tableId(), newRowId, colRec.colId()) as string;
|
||||
const singleText = () => t(`Reassign to {{sourceTable}} record {{sourceName}}.`,
|
||||
{
|
||||
sourceTable: dom('i', colRec.table().tableNameDef()),
|
||||
sourceName: dom('b', Ann),
|
||||
});
|
||||
const multiText = () => t(`Reassign to new {{sourceTable}} records.`,
|
||||
{
|
||||
sourceTable: dom('i', colRec.table().tableNameDef()),
|
||||
});
|
||||
return labeledSquareCheckbox(checked, multiple ? multiText() : singleText());
|
||||
}
|
||||
}
|
||||
|
||||
// List of problems we found in actions.
|
||||
const problems: Problem[] = [];
|
||||
const uniqueColumns: ColumnRec[] = [];
|
||||
const newOwners = new Set<number|null>();
|
||||
|
||||
// We will hold changes in references, so that we can clear the action itself.
|
||||
const newValues = new Map<string, Map<number, number>>();
|
||||
const assignPet = (colId: string, petId: number, ownerId: number) => {
|
||||
if (!newValues.has(colId)) {
|
||||
newValues.set(colId, new Map());
|
||||
}
|
||||
newValues.get(colId)!.set(petId, ownerId);
|
||||
};
|
||||
const wasPetJustAssigned = (colId: string, petId: number) => {
|
||||
return newValues.has(colId) && newValues.get(colId)!.get(petId);
|
||||
};
|
||||
|
||||
const properActions = [] as DocAction[];
|
||||
// Helper that unassigns a pet from the owner, by amanding the value stored in Ref/RefList column.
|
||||
function unassign(value: any, pet: number) {
|
||||
const newValue = decodeObject(value);
|
||||
const newValueArray = Array.isArray(newValue) ? newValue : [newValue] as any;
|
||||
const filteredOut = newValueArray.filter((v: any) => v !== pet);
|
||||
const wasArray = Array.isArray(newValue);
|
||||
if (wasArray) {
|
||||
if (newValueArray.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return encodeObject(filteredOut);
|
||||
} else {
|
||||
return filteredOut[0] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
// We will go one by one for each action (either update or add), we will flat bulk actions
|
||||
// and simulate applying them to the data, to test if the following actions won't produce
|
||||
// conflicts.
|
||||
for(const origAction of bulkToSingle(actions)) {
|
||||
const action = structuredClone(origAction);
|
||||
if (action[0] === 'UpdateRecord' || action[0] === 'AddRecord') {
|
||||
const ownersTable = action[1]; // this is same for each action.
|
||||
const newOwnerId = action[2];
|
||||
newOwners.add(newOwnerId);
|
||||
const valuesInAction = action[3];
|
||||
for(const colId of Object.keys(valuesInAction)) {
|
||||
// We are only interested in uqniue ref columns with reverse column.
|
||||
const petsCol = columnRec(ownersTable, colId);
|
||||
const ownerRevCol = petsCol.reverseColModel();
|
||||
if (!ownerRevCol || !ownerRevCol.id()) {
|
||||
continue;
|
||||
}
|
||||
if (petsCol.reverseColModel().pureType() !== 'Ref') {
|
||||
continue;
|
||||
}
|
||||
const petsTable = ownerRevCol.table().tableId();
|
||||
uniqueColumns.push(petsCol); // TODO: what it does
|
||||
|
||||
// Prepare the data for testing, we will treat Ref as RefList to simplify the code.
|
||||
const newValue = decodeObject(valuesInAction[colId]);
|
||||
let petsAfter: number[] = Array.isArray(newValue) ? newValue : [newValue] as any;
|
||||
const prevValue = decodeObject(getRow(ownersTable, newOwnerId)?.[colId]) ?? [];
|
||||
const petsBefore: number[] = Array.isArray(prevValue) ? prevValue : [prevValue] as any;
|
||||
|
||||
// The new owner will have new pets. We are only interested in a situation
|
||||
// where owner is assigned with a new pet, if pet was removed, we don't care as this
|
||||
// won't cause a conflict.
|
||||
petsAfter = petsAfter.filter(p => !petsBefore.includes(p));
|
||||
if (petsAfter.length === 0) {
|
||||
continue;
|
||||
}
|
||||
// Now find current owners of the pets that will be assigned to the new owner.
|
||||
for(const pet of petsAfter) {
|
||||
// We will use data available in that other table (Pets). Notice that we assume, that
|
||||
// the reverse column (Owner in Pets) is Ref column.
|
||||
const oldOwner = getRow(petsTable, pet)?.[ownerRevCol.colId()] as number;
|
||||
// If the pet didn't have an owner previously, we don't care, we are fine reasigning it.
|
||||
if (!oldOwner || (typeof oldOwner !== 'number')) {
|
||||
// We ignore it, but there might be other actions that will try to move this pet
|
||||
// to other owner, so remember that one.
|
||||
|
||||
// But before remembering, check if that hasn't happend already.
|
||||
const assignedTo = wasPetJustAssigned(petsCol.colId(), pet);
|
||||
if (assignedTo) {
|
||||
// We have two actions that will assign the same pet to two different owners.
|
||||
// We can't allow that, so we will remove this update from the action.
|
||||
valuesInAction[colId] = unassign(valuesInAction[colId], pet);
|
||||
} else {
|
||||
assignPet(colId, pet, newOwnerId);
|
||||
}
|
||||
} else {
|
||||
// If we will assign it to someone else in previous action, ignore this update.
|
||||
if (wasPetJustAssigned(petsCol.colId(), pet)) {
|
||||
valuesInAction[colId] = unassign(valuesInAction[colId], pet);
|
||||
continue;
|
||||
} else {
|
||||
assignPet(colId, pet, newOwnerId);
|
||||
problems.push(new Problem({
|
||||
tableId: ownersTable,
|
||||
pointer: pet,
|
||||
colRec: petsCol,
|
||||
revRec: ownerRevCol,
|
||||
newRowId: newOwnerId,
|
||||
oldRowId: oldOwner,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
properActions.push(action);
|
||||
} else {
|
||||
throw new Error(`Unsupported action ${action[0]}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!problems.length) {
|
||||
throw new Error('No problems found');
|
||||
}
|
||||
|
||||
const checked = Observable.create(null, false);
|
||||
|
||||
const multipleOrNew = newOwners.size > 1 || newOwners.has(null);
|
||||
|
||||
modal((ctl) => {
|
||||
const reassign = async () => {
|
||||
await docModel.docData.sendActions([
|
||||
...problems.map(p => p.fixUserAction()).filter(Boolean),
|
||||
...properActions
|
||||
]);
|
||||
ctl.close();
|
||||
};
|
||||
const configureReference = async () => {
|
||||
ctl.close();
|
||||
if (!uniqueColumns.length) { return; }
|
||||
const revCol = uniqueColumns[0].reverseColModel();
|
||||
const rawViewSection = revCol.table().rawViewSection();
|
||||
if (!rawViewSection) { return; }
|
||||
await commands.allCommands.showRawData.run(rawViewSection.id());
|
||||
const reverseColId = revCol.colId.peek();
|
||||
if (!reverseColId) { return; } // might happen if it is censored.
|
||||
const targetField = rawViewSection.viewFields.peek().all()
|
||||
.find(f => f.colId.peek() === reverseColId);
|
||||
if (!targetField) { return; }
|
||||
await commands.allCommands.setCursor.run(null, targetField);
|
||||
await commands.allCommands.rightPanelOpen.run();
|
||||
await commands.allCommands.fieldTabOpen.run();
|
||||
};
|
||||
return [
|
||||
cssModalWidth('normal'),
|
||||
cssModalTitle(t('Record already assigned', {count: problems.length})),
|
||||
cssModalBody(() => {
|
||||
// Show single problem in a simple way.
|
||||
return dom('div',
|
||||
problems[0].buildHeader(),
|
||||
dom('div',
|
||||
dom.style('margin-top', '18px'),
|
||||
dom('div', problems.slice(0, 4).map(p => p.buildReason())),
|
||||
problems.length <= 4 ? null : dom('div', `... and ${problems.length - 4} more`),
|
||||
dom('div',
|
||||
problems[0].buildAction(checked, multipleOrNew),
|
||||
dom.style('margin-top', '18px'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
cssModalButtons(
|
||||
dom.style('display', 'flex'),
|
||||
dom.style('justify-content', 'space-between'),
|
||||
dom.style('align-items', 'baseline'),
|
||||
dom.domComputed(checked, (v) => [
|
||||
v ? bigPrimaryButton(t('Reassign'), dom.on('click', reassign))
|
||||
: bigBasicButton(t('Cancel'), dom.on('click', () => ctl.close())),
|
||||
]),
|
||||
dom('div',
|
||||
withInfoTooltip(
|
||||
textButton('Configure reference', dom.on('click', configureReference)),
|
||||
'reasignTwoWayReference',
|
||||
)
|
||||
)
|
||||
)
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is used to traverse through the actions, and if there are bulk actions, it will
|
||||
* flatten them to equivalent single actions.
|
||||
*/
|
||||
function* bulkToSingle(actions: DocAction[]): Iterable<DocAction> {
|
||||
for(const a of actions) {
|
||||
if (a[0].startsWith('Bulk')) {
|
||||
const name = a[0].replace('Bulk', '') as 'AddRecord' | 'UpdateRecord';
|
||||
const rowIds = a[2] as number[];
|
||||
const tableId = a[1];
|
||||
const colValues = a[3] as any;
|
||||
for (let i = 0; i < rowIds.length; i++) {
|
||||
yield [name, tableId, rowIds[i], mapValues(colValues, (values) => values[i])];
|
||||
}
|
||||
} else {
|
||||
yield a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const cssBulletLine = styled('div', `
|
||||
margin-bottom: 8px;
|
||||
&::before {
|
||||
content: '•';
|
||||
margin-right: 4px;
|
||||
color: ${theme.lightText};
|
||||
}
|
||||
`);
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import {logTelemetryEvent} from 'app/client/lib/telemetry';
|
||||
import {GristTooltips, Tooltip} from 'app/client/ui/GristTooltips';
|
||||
import {GristTooltips, Tooltip, TooltipContentFunc} from 'app/client/ui/GristTooltips';
|
||||
import {prepareForTransition} from 'app/client/ui/transitions';
|
||||
import {testId, theme, vars} from 'app/client/ui2018/cssVars';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
@@ -324,12 +324,12 @@ export type InfoTooltipVariant = 'click' | 'hover';
|
||||
* Renders an info icon that shows a tooltip with the specified `content`.
|
||||
*/
|
||||
export function infoTooltip(
|
||||
tooltip: Tooltip,
|
||||
tooltip: Tooltip|TooltipContentFunc,
|
||||
options: InfoTooltipOptions = {},
|
||||
...domArgs: DomElementArg[]
|
||||
) {
|
||||
const {variant = 'click'} = options;
|
||||
const content = GristTooltips[tooltip]();
|
||||
const content = typeof tooltip === 'function' ? tooltip() : GristTooltips[tooltip]();
|
||||
const onOpen = () => logTelemetryEvent('viewedTip', {full: {tipName: tooltip}});
|
||||
switch (variant) {
|
||||
case 'click': {
|
||||
@@ -437,7 +437,7 @@ export function withInfoTooltip(
|
||||
options: WithInfoTooltipOptions = {},
|
||||
) {
|
||||
const {variant = 'click', domArgs, iconDomArgs, popupOptions} = options;
|
||||
return cssDomWithTooltip(
|
||||
return cssInfoTooltip(
|
||||
domContents,
|
||||
infoTooltip(tooltip, {variant, popupOptions}, iconDomArgs),
|
||||
...(domArgs ?? [])
|
||||
@@ -475,6 +475,12 @@ export function descriptionInfoTooltip(
|
||||
);
|
||||
}
|
||||
|
||||
const cssInfoTooltip = styled('div', `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 8px;
|
||||
`);
|
||||
|
||||
const cssTooltipCorner = styled('div', `
|
||||
position: absolute;
|
||||
width: 0;
|
||||
@@ -606,9 +612,3 @@ const cssInfoTooltipPopupCloseButton = styled('div', `
|
||||
background-color: ${theme.hover};
|
||||
}
|
||||
`);
|
||||
|
||||
const cssDomWithTooltip = styled('div', `
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 8px;
|
||||
`);
|
||||
|
||||
Reference in New Issue
Block a user