Table: Styling from field (#110991)

* Table: Styling from field

* fix mistake with gdev

* e2e for kitchen sink

* add counter-example in e2e for completeness

* unit tests for utils

* update to store style field per-column, replace util

* optimize branches column-level variables
This commit is contained in:
Paul Marbach
2025-09-15 10:34:12 -05:00
committed by GitHub
parent c61624ad3c
commit bb9b890e8c
9 changed files with 149 additions and 1 deletions
@@ -1009,6 +1009,10 @@ export interface TableFieldOptions extends HideableFieldConfig {
hideHeader?: boolean;
inspect: boolean;
minWidth?: number;
/**
* The name of the field which contains styling overrides for this cell
*/
styleField?: string;
/**
* Selecting or hovering this field will show a tooltip containing the content within the target field
*/
@@ -129,6 +129,8 @@ TableFieldOptions: {
wrapHeaderText?: bool
// Selecting or hovering this field will show a tooltip containing the content within the target field
tooltip?: TableCellTooltipOptions
// The name of the field which contains styling overrides for this cell
styleField?: string
// options for the footer for this field
footer?: TableFooterOptions
} & HideableFieldConfig @cuetsy(kind="interface")
@@ -96,6 +96,7 @@ import {
shouldTextWrap,
withDataLinksActionsTooltip,
getSummaryCellTextAlign,
parseStyleJson,
} from './utils';
const EXPANDED_COLUMN_KEY = 'expanded';
@@ -477,6 +478,10 @@ export function TableNG(props: TableNGProps) {
const linkStyles = getLinkStyles(theme, canBeColorized);
const cellParentStyles = clsx(defaultCellStyles, linkStyles);
const maxHeightClassName = maxRowHeight ? getMaxHeightCellStyles(theme, cellStyleOptions) : undefined;
const styleFieldValue = field.config.custom?.styleField;
const styleField = styleFieldValue ? data.fields.find(predicateByName(styleFieldValue)) : undefined;
const styleFieldName = styleField ? getDisplayName(styleField) : undefined;
const hasValidStyleField = Boolean(styleFieldName);
// TODO: in future extend this to ensure a non-classic color scheme is set with AutoCell
@@ -504,6 +509,9 @@ export function TableNG(props: TableNGProps) {
const cellColorStyles = getCellColorInlineStyles(cellOptions, displayValue, applyToRowBgFn != null);
Object.assign(style, cellColorStyles);
}
if (hasValidStyleField) {
style = { ...style, ...parseStyleJson(props.row[styleFieldName!]) };
}
return (
<Cell
@@ -46,6 +46,7 @@ import {
getDefaultRowHeight,
getDisplayName,
predicateByName,
parseStyleJson,
calculateFooterHeight,
} from './utils';
@@ -1459,4 +1460,49 @@ describe('TableNG utils', () => {
expect(predicate(field)).toBe(false);
});
});
describe('parseStyleJson', () => {
it('parses the contents of the styleField for this row and returns a style object', () => {
expect(parseStyleJson('{"color":"red"}')).toEqual({ color: 'red' });
});
it.each([
{ type: 'number', value: 12345 },
{ type: 'boolean', value: true },
{ type: 'null', value: null },
{ type: 'undefined', value: undefined },
{ type: 'object', value: { color: 'red' } },
{ type: 'array', value: ['not', 'a', 'string'] },
])('returns void if input is a $type', ({ value }) => {
expect(parseStyleJson(value)).toBeUndefined();
});
it.each([
{ type: 'array', value: '["not","an","object"]' },
{ type: 'string', value: '"just a string"' },
{ type: 'number', value: '12345' },
{ type: 'boolean', value: 'true' },
{ type: 'null', value: 'null' },
])('returns void and does not throw if the parsed JSON is a $type', ({ value }) => {
expect(parseStyleJson(value)).toBeUndefined();
});
it('returns void and does not throw if this is invalid JSON (but it does console.error)', () => {
jest.spyOn(console, 'error').mockImplementation();
expect(parseStyleJson('{"mal": "formed}')).toBeUndefined();
expect(console.error).toHaveBeenCalled();
});
it('only calls console.error once for a given malformed style', () => {
jest.spyOn(console, 'error').mockImplementation();
for (let i = 0; i < 100; i++) {
parseStyleJson('{"mal": "formed-in-a-new-way}');
}
expect(console.error).toHaveBeenCalledTimes(1);
});
it('returns an object with invalid style properties, because we do not validate the style properties', () => {
expect(parseStyleJson('{"notARealStyle": "someValue"}')).toEqual({ notARealStyle: 'someValue' });
});
});
});
@@ -1009,3 +1009,23 @@ export function getSummaryCellTextAlign(textAlign: TextAlign, cellType: TableCel
return textAlign;
}
// we keep this set to avoid spamming the heck out of the console, since it's quite likely that if we fail to parse
// a value once, it'll happen again and again for many rows in a table, and spamming the console is slow.
let warnedAboutStyleJsonSet = new Set<string>();
export function parseStyleJson(rawValue: unknown): CSSProperties | void {
// confirms existence of value and serves as a type guard
if (typeof rawValue === 'string') {
try {
const parsedJsonValue = JSON.parse(rawValue);
if (parsedJsonValue != null && typeof parsedJsonValue === 'object' && !Array.isArray(parsedJsonValue)) {
return parsedJsonValue;
}
} catch (e) {
if (!warnedAboutStyleJsonSet.has(rawValue)) {
console.error(`encountered invalid cell style JSON: ${rawValue}`, e);
warnedAboutStyleJsonSet.add(rawValue);
}
}
}
}