From 7d01e8e4e2383275a8e76459187e6dd62775bb72 Mon Sep 17 00:00:00 2001 From: Azri Kahar <42867097+azrikahar@users.noreply.github.com> Date: Wed, 29 Jun 2022 03:17:05 +0800 Subject: [PATCH] Fix relational fields for Download Page as CSV & relevant displays' handler improvements (#14147) * fix save-as-csv for aliased relational fields * strip & decode HTML in formatted-value handler * fix labels handler when value is an array * fallback to value when text is empty * Update app/src/displays/labels/index.ts Co-authored-by: Brainslug * use dompurify to strip html tags Co-authored-by: Brainslug --- .../formatted-value/formatted-value.vue | 3 +- app/src/displays/formatted-value/index.ts | 13 ++++++- app/src/displays/labels/index.ts | 24 ++++++++----- app/src/utils/save-as-csv.ts | 34 +++++++++++++++---- 4 files changed, 58 insertions(+), 16 deletions(-) diff --git a/app/src/displays/formatted-value/formatted-value.vue b/app/src/displays/formatted-value/formatted-value.vue index b572c1e78c..a796a1c1f6 100644 --- a/app/src/displays/formatted-value/formatted-value.vue +++ b/app/src/displays/formatted-value/formatted-value.vue @@ -25,6 +25,7 @@ import formatTitle from '@directus/format-title'; import { decode } from 'html-entities'; import { useI18n } from 'vue-i18n'; import { isNil } from 'lodash'; +import dompurify from 'dompurify'; export default defineComponent({ props: { @@ -152,7 +153,7 @@ export default defineComponent({ let value = String(props.value); // Strip out all HTML tags - value = value.replace(/(<([^>]+)>)/gi, ''); + value = dompurify.sanitize(value, { ALLOWED_TAGS: [] }); // Decode any HTML encoded characters (like ©) value = decode(value); diff --git a/app/src/displays/formatted-value/index.ts b/app/src/displays/formatted-value/index.ts index 834c5fe198..2ea4e2ab54 100644 --- a/app/src/displays/formatted-value/index.ts +++ b/app/src/displays/formatted-value/index.ts @@ -2,6 +2,8 @@ import { defineDisplay } from '@directus/shared/utils'; import { DisplayConfig } from '@directus/shared/types'; import DisplayFormattedValue from './formatted-value.vue'; import formatTitle from '@directus/format-title'; +import { decode } from 'html-entities'; +import dompurify from 'dompurify'; export default defineDisplay({ id: 'formatted-value', @@ -13,7 +15,16 @@ export default defineDisplay({ handler: (value, options) => { const prefix = options.prefix ?? ''; const suffix = options.suffix ?? ''; - const formattedValue = options.format ? formatTitle(value) : value; + + let sanitizedValue = String(value); + + // Strip out all HTML tags + sanitizedValue = dompurify.sanitize(value, { ALLOWED_TAGS: [] }); + + // Decode any HTML encoded characters (like ©) + sanitizedValue = decode(sanitizedValue); + + const formattedValue = options.format ? formatTitle(sanitizedValue) : sanitizedValue; return `${prefix}${formattedValue}${suffix}`; }, diff --git a/app/src/displays/labels/index.ts b/app/src/displays/labels/index.ts index c206725012..feaa9f461e 100644 --- a/app/src/displays/labels/index.ts +++ b/app/src/displays/labels/index.ts @@ -10,16 +10,24 @@ export default defineDisplay({ icon: 'flag', component: DisplayLabels, handler: (value, options, { interfaceOptions }) => { - const configuredChoice = - options?.choices?.find((choice: { value: string }) => choice.value === value) ?? - interfaceOptions?.choices?.find((choice: { value: string }) => choice.value === value); - - if (configuredChoice) { - const { text } = translate(configuredChoice); - return text; + if (Array.isArray(value)) { + return value.map((val) => getConfiguredChoice(val)).join(', '); + } else { + return getConfiguredChoice(value); } - return value; + function getConfiguredChoice(val: string) { + const configuredChoice = + options?.choices?.find((choice: { value: string }) => choice.value === val) ?? + interfaceOptions?.choices?.find((choice: { value: string }) => choice.value === val); + + if (configuredChoice) { + const { text } = translate(configuredChoice); + return text ? text : val; + } + + return val; + } }, options: [ { diff --git a/app/src/utils/save-as-csv.ts b/app/src/utils/save-as-csv.ts index 20063912f5..0ac2f02a50 100644 --- a/app/src/utils/save-as-csv.ts +++ b/app/src/utils/save-as-csv.ts @@ -1,9 +1,11 @@ -import { Item, Field, DisplayConfig } from '@directus/shared/types'; -import { saveAs } from 'file-saver'; -import { parse } from 'json2csv'; +import useAliasFields from '@/composables/use-alias-fields'; import { getDisplay } from '@/displays'; import { useFieldsStore } from '@/stores'; -import { get } from 'lodash'; +import { get } from '@/utils/get-with-arrays'; +import { DisplayConfig, Field, Item } from '@directus/shared/types'; +import { saveAs } from 'file-saver'; +import { parse } from 'json2csv'; +import { ref } from 'vue'; /** * Saves the given collection + items combination as a CSV file @@ -17,14 +19,34 @@ export async function saveAsCSV(collection: string, fields: string[], items: Ite fieldsUsed[key] = fieldsStore.getField(collection, key); } + const { aliasFields } = useAliasFields(ref(fields)); + const parsedItems = []; for (const item of items) { const parsedItem: Record = {}; for (const key of fields) { - const name = fieldsUsed[key]?.name ?? key; - const value = get(item, key); + let name: string; + + const keyParts = key.split('.'); + + if (keyParts.length > 1) { + const names = keyParts.map((fieldKey, index) => { + const pathPrefix = keyParts.slice(0, index); + const field = fieldsStore.getField(collection, [...pathPrefix, fieldKey].join('.')); + return field?.name ?? fieldKey; + }); + + name = names.join(' -> '); + } else { + name = fieldsUsed[key]?.name ?? key; + } + + const value = + !aliasFields.value?.[key] || item[key] !== undefined + ? get(item, key) + : get(item, aliasFields.value[key].fullAlias); let display: DisplayConfig | undefined;