mirror of
https://github.com/directus/directus.git
synced 2026-01-28 15:28:24 -05:00
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 <br41nslug@users.noreply.github.com> * use dompurify to strip html tags Co-authored-by: Brainslug <br41nslug@users.noreply.github.com>
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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}`;
|
||||
},
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -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<string, any> = {};
|
||||
|
||||
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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user