diff --git a/frontend/src/metabase/lib/formatting.js b/frontend/src/metabase/lib/formatting.js index d026a665fbc790fcf15f27e556513a45d5d27b64..77684b6f81cc6a0f2f6df2f6042168a822fee9e2 100644 --- a/frontend/src/metabase/lib/formatting.js +++ b/frontend/src/metabase/lib/formatting.js @@ -7,6 +7,9 @@ import Humanize from "humanize-plus"; import React from "react"; import { ngettext, msgid } from "c-3po"; +import Mustache from "mustache"; +import ReactMarkdown from "react-markdown"; + import ExternalLink from "metabase/components/ExternalLink.jsx"; import { @@ -437,21 +440,29 @@ function formatStringFallback(value: Value, options: FormattingOptions = {}) { return value; } +const MARKDOWN_RENDERERS = { + // eslint-disable-next-line react/display-name + link: ({ href, children }) => ( + <ExternalLink href={href}>{children}</ExternalLink> + ), +}; + export function formatValue(value: Value, options: FormattingOptions = {}) { - const { prefix = "", suffix = "", jsx } = options; const formatted = formatValueRaw(value, options); - if (prefix || suffix) { - if (jsx && typeof formatted !== "string") { - return ( - <span> - {prefix} - {formatted} - {suffix} - </span> - ); + if (options.markdown_template) { + if (options.jsx) { + // inject the formatted value as "value" and the unformatted value as "raw" + const markdown = Mustache.render(options.markdown_template, { + value: formatted, + raw: value, + }); + return <ReactMarkdown source={markdown} renderers={MARKDOWN_RENDERERS} />; } else { - // $FlowFixMe: formatted will always be a string if jsx = false but flow doesn't know that - return `${prefix}${formatted}${suffix}`; + // FIXME: render and get the innerText? + console.warn( + "formatValue: options.markdown_template not supported when options.jsx = false", + ); + return formatted; } } else { return formatted; diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingInput.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingInput.jsx index c5bdbe249f26fae3a0c7afecdfe260369715918e..f7705f1931a3e60143ba55ed0e22a2150dbbf9d1 100644 --- a/frontend/src/metabase/visualizations/components/settings/ChartSettingInput.jsx +++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingInput.jsx @@ -1,7 +1,8 @@ import React from "react"; -const ChartSettingInput = ({ value, onChange }) => ( +const ChartSettingInput = ({ value, onChange, ...props }) => ( <input + {...props} className="input block full" value={value} onChange={e => onChange(e.target.value)} diff --git a/frontend/src/metabase/visualizations/lib/settings/column.js b/frontend/src/metabase/visualizations/lib/settings/column.js index 5561f08c65a6f6305aeba981e9bf60355071255d..6127c0161baaa637559392a363e9a69c05546b03 100644 --- a/frontend/src/metabase/visualizations/lib/settings/column.js +++ b/frontend/src/metabase/visualizations/lib/settings/column.js @@ -1,4 +1,5 @@ import { t } from "c-3po"; +import _ from "underscore"; import ChartSettingColumnSettings from "metabase/visualizations/components/settings/ChartSettingColumnSettings"; @@ -168,13 +169,12 @@ export const NUMBER_COLUMN_SETTINGS = { }; const COMMON_COLUMN_SETTINGS = { - prefix: { - title: t`Add a prefix`, - widget: "input", - }, - suffix: { - title: t`Add a suffix`, + markdown_template: { + title: t`Markdown template`, widget: "input", + props: { + placeholder: "{{value}}", + }, }, }; @@ -190,7 +190,13 @@ export function getSettingDefintionsForColumn(column) { export function getComputedSettingsForColumn(column, storedSettings) { const settingsDefs = getSettingDefintionsForColumn(column); - return getComputedSettings(settingsDefs, column, storedSettings); + const computedSettings = getComputedSettings( + settingsDefs, + column, + storedSettings, + ); + // remove undefined settings since they override other settings when merging object + return _.pick(computedSettings, value => value !== undefined); } export function getSettingsWidgetsForColumm( diff --git a/frontend/src/metabase/visualizations/visualizations/Scalar.jsx b/frontend/src/metabase/visualizations/visualizations/Scalar.jsx index bbc3b0d14fdb40847a91340b2a2e9952aab283e0..cf1a5bbf4fcdd6bb4518730e7360812356faa256 100644 --- a/frontend/src/metabase/visualizations/visualizations/Scalar.jsx +++ b/frontend/src/metabase/visualizations/visualizations/Scalar.jsx @@ -17,13 +17,20 @@ import _ from "underscore"; import type { VisualizationProps } from "metabase/meta/types/Visualization"; -function getLegacyScalarSettings(settings) { - return _.chain(settings) +// convert legacy `scalar.*` visualization settings to format options +function legacyScalarSettingsToFormatOptions(settings) { + const o = _.chain(settings) .pairs() .filter(([key, value]) => key.startsWith("scalar.") && value !== undefined) .map(([key, value]) => [key.replace(/^scalar\./, ""), value]) .object() .value(); + // `prefix`/`suffix` replaced with `markdown_template` + if (o.prefix || o.suffix) { + o.markdown_template = `${o.prefix || ""}{{value}}${o.suffix || ""}`; + delete o.prefix, o.suffix; + } + return o; } export default class Scalar extends Component { @@ -133,7 +140,7 @@ export default class Scalar extends Component { const column = cols[0]; const formatOptions = { - ...getLegacyScalarSettings(settings), + ...legacyScalarSettingsToFormatOptions(settings), ...settings.column(column), jsx: true, }; diff --git a/package.json b/package.json index f39b8d74ce6ee6e07d43ebd955e9db18dd577218..c88f81d2da57671ddc512dfe665e86c8b5a0bb5b 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "leaflet.heat": "^0.2.0", "lodash.memoize": "^4.1.2", "moment": "2.19.3", + "mustache": "^2.3.2", "node-libs-browser": "^2.0.0", "normalizr": "^3.0.2", "npm": "^5.8.0", diff --git a/yarn.lock b/yarn.lock index 1a7e08ab4608cc626f858b2a9893cdde5da7323c..e3777a20bb1867f39961976ef5e77aec9010beaa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7185,6 +7185,10 @@ multicast-dns@^6.0.1: dns-packet "^1.0.1" thunky "^0.1.0" +mustache@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/mustache/-/mustache-2.3.2.tgz#a6d4d9c3f91d13359ab889a812954f9230a3d0c5" + mute-stream@0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0"