From ae75c22ae19724286f2d6206db43641f7bb5c4ea Mon Sep 17 00:00:00 2001 From: Janet Vorobyeva Date: Mon, 21 Aug 2023 16:30:42 -0400 Subject: [PATCH] Added in all link bubbles code --- app/client/components/buildViewSectionDom.ts | 115 +++++++++++++++++++ app/client/ui/WidgetTitle.ts | 2 +- test/nbrowser/Views.ntest.js | 2 +- 3 files changed, 117 insertions(+), 2 deletions(-) diff --git a/app/client/components/buildViewSectionDom.ts b/app/client/components/buildViewSectionDom.ts index 0715638f1e2..79738866283 100644 --- a/app/client/components/buildViewSectionDom.ts +++ b/app/client/components/buildViewSectionDom.ts @@ -13,6 +13,7 @@ import {menu} from 'app/client/ui2018/menus'; import {getWidgetTypes} from "app/client/ui/widgetTypesMap"; import {Computed, dom, DomElementArg, Observable, styled} from 'grainjs'; import {defaultMenuOptions} from 'popweasel'; +import {EmptyFilterState} from "./LinkingState"; const t = makeT('ViewSection'); @@ -54,6 +55,117 @@ export function buildCollapsedSectionDom(options: { } +function buildLinkStateIndicatorDom(options: { + gristDoc: GristDoc, + sectionRowId: number, +}, ...domArgs: DomElementArg[]) { + const {gristDoc, sectionRowId} = options; + const tgtSec = gristDoc.docModel.viewSections.getRowModel(sectionRowId); + + return dom.domComputed((use) => { + //makes an observable for the passed-in section + const lstate = use(tgtSec.linkingState); + if(lstate == null) { return null; } + + // Default to empty for ease of coding, will be set in cases where it's relevant + const lfilter = lstate.filterState ? use(lstate.filterState): EmptyFilterState; + + // crunch filterVals into compact string for display in bubble (not tooltip), + // eg "USA", "USA;2022", "(USA +3 others)" + // only for filter linking and summary-filter-linking + + //filters is a map {column: [vals...]} + // if multiple filters, join each with ";" + // each filter can have multiple vals (if reflist), show as "(SomeValue +3 others)", + + const filterValsShortLabel = Object.keys(lfilter.filterLabels).map(colId => { + const vals = lfilter.filterLabels[colId]; + //selector can be an empty reflist (filterLabels[colId] = []) + if(vals.length == 0) + { return '- blank -'; } + + // Even if vals != [], selector might be a null/empty cell value. + // - if a null reference: filter[colId] = [0], but filterLabels would be [''] + // - if an empty string/choice filter = [''], label = [''] + // - if an empty number/date/etc: filter[colId] = [null], but filterLabel will be [''] + //Note: numeric 0 won't become blank, since filterLabel will be "0", which is truthy + const dispVal = vals[0] || '- blank -'; + + //If 2 or more vals, abbreviate it + return vals.length <= 1 ? dispVal: `(${dispVal} +${vals.length - 1} others)`; + //TODO: could show multiple vals if short, and/or let css overflow ellipsis handle it? + }).join("; "); + + let bubbleContent: DomElementArg[]; + switch (use(lstate.linkTypeDescription)) { + case "Filter:Summary-Group": + case "Filter:Col->Col": + case "Filter:Row->Col": + case "Summary": + bubbleContent = [ + dom("div", dom.style('width', '2px'), dom.style('display', 'inline-block')), //spacer for text + filterValsShortLabel, + ]; + break; + case "Show-Referenced-Records": + bubbleContent = []; + break; + case "Cursor:Same-Table": + case "Cursor:Reference": + bubbleContent = []; + break; + case "Error:Invalid": + default: + bubbleContent = ["Error"]; + break; + } + + return linkStateBubble( + customIcon(dom.style("background-color", theme.filterBarButtonSavedFg + "")), + bubbleContent, + ...domArgs, + ); + + }); + +} + +// eslint-disable-next-line +const tempIconSVGString= `url('data:image/svg+xml;utf8, ')`; + +//TODO JV TEMP: Shamelessly copied from icon.ts +const customIcon = styled('div', ` + -webkit-mask-image: ${tempIconSVGString}; + position: relative; + display: inline-block; + vertical-align: middle; + -webkit-mask-repeat: no-repeat; + -webkit-mask-position: center; + -webkit-mask-size: contain; + width: 16px; + height: 16px; + background-color: var(--icon-color, var(--grist-theme-text, black)); + +`); + +const linkStateBubble = styled('div', ` + cursor: pointer; + overflow: hidden; + border-radius: 3px; + padding: 3px; + text-overflow: ellipsis; + align-self: start; + height: 21px; + margin-top: -4px; + margin-left: 4px; + color: ${theme.filterBarButtonSavedFg}; + background-color: ${theme.filterBarButtonSavedBg}; + &:hover { + background-color: ${theme.filterBarButtonSavedHoverBg}; + } +`); + + export function buildViewSectionDom(options: { gristDoc: GristDoc, sectionRowId: number, @@ -93,6 +205,9 @@ export function buildViewSectionDom(options: { dom.maybe((use) => use(use(viewInstance.viewSection.table).summarySourceTable), () => cssSigmaIcon('Pivot', testId('sigma'))), buildWidgetTitle(vs, options, testId('viewsection-title'), cssTestClick(testId("viewsection-blank"))), + dom.maybe((use) => use(vs.linkSrcSectionRef) != 0, () => + buildLinkStateIndicatorDom({gristDoc, sectionRowId}, testId("viewsection-linkstate"))), + dom("div", dom.style("flex", "1 0 0px")), //spacer, 0 size by default, grows to take up remaining space viewInstance.buildTitleControls(), dom('div.viewsection_buttons', dom.create(viewSectionMenu, gristDoc, vs) diff --git a/app/client/ui/WidgetTitle.ts b/app/client/ui/WidgetTitle.ts index fb13bf3f9d0..a36ad6245a4 100644 --- a/app/client/ui/WidgetTitle.ts +++ b/app/client/ui/WidgetTitle.ts @@ -269,7 +269,7 @@ const updateOnKey = {onInput: true}; // Leave class for tests. const cssTitleContainer = styled('div', ` - flex: 1 1 0px; + flex: 0 1 auto; /* won't grow, starts at size of content, will shrink if needed */ min-width: 0px; display: flex; .info_toggle_icon { diff --git a/test/nbrowser/Views.ntest.js b/test/nbrowser/Views.ntest.js index acc1f1c1b2b..b5a58a80680 100644 --- a/test/nbrowser/Views.ntest.js +++ b/test/nbrowser/Views.ntest.js @@ -36,7 +36,7 @@ describe('Views.ntest', function() { // Check that viewsection titles are correct and editable var recordTitle = recordSection.find('.test-viewsection-title'); assert.equal(await recordTitle.text(), 'TABLE1'); - await recordTitle.click(); + await recordSection.find('.test-viewsection-blank').click(); //switch to recordSection without opening title widget await gu.renameActiveSection('foo'); assert.equal(await recordTitle.text(), 'foo');