fix(rich-text) : strip HTML pour les contextes plain-text

Avec MalioInputRichText qui émet désormais du HTML par défaut,
plusieurs points d'affichage rendaient les balises brutes au
lieu du texte. Ajoute un helper stripRichText() (frontend) et
descriptionToPlainText() (backend) pour neutraliser ces cas.

- TimeEntryList : strip avant truncate dans la liste des time
  entries.
- ProjectGroupTab : strip dans la cellule description du
  tableau des groupes.
- CalDavService : strip_tags + html_entity_decode avant injection
  dans le DESCRIPTION VEVENT/VTODO iCal (sinon Outlook/Apple
  Calendar affichaient les <p>...</p> à l'utilisateur).

Co-Authored-By: RuFlo <ruv@ruv.net>
This commit is contained in:
2026-05-04 19:55:23 +02:00
parent 2a68d2f9c6
commit b2cc6e96e1
4 changed files with 36 additions and 5 deletions

View File

@@ -36,7 +36,7 @@
/>
</template>
<template #cell-description="{ item }">
{{ item.description ?? '—' }}
{{ stripRichText(item.description) || '—' }}
</template>
<template #actions="{ item }">
<MalioButton
@@ -71,6 +71,7 @@ import type { TaskGroup } from '~/services/dto/task-group'
import type { Task } from '~/services/dto/task'
import { useTaskGroupService } from '~/services/task-groups'
import { useTaskService } from '~/services/tasks'
import { stripRichText } from '~/utils/format'
const props = defineProps<{
projectId: number

View File

@@ -33,8 +33,8 @@
</div>
<div class="mt-0.5 flex items-center gap-2 text-xs text-neutral-500">
<span v-if="entry.project">{{ entry.project.name }}</span>
<span v-if="entry.project && entry.description" class="text-neutral-300">·</span>
<span v-if="entry.description" class="truncate">{{ entry.description }}</span>
<span v-if="entry.project && stripRichText(entry.description)" class="text-neutral-300">·</span>
<span v-if="stripRichText(entry.description)" class="truncate">{{ stripRichText(entry.description) }}</span>
</div>
</div>
@@ -68,6 +68,7 @@
<script setup lang="ts">
import type { TimeEntry } from '~/services/dto/time-entry'
import { stripRichText } from '~/utils/format'
const props = defineProps<{
entries: TimeEntry[]

View File

@@ -3,3 +3,17 @@ export function formatFileSize(bytes: number): string {
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} Ko`
return `${(bytes / (1024 * 1024)).toFixed(1)} Mo`
}
export function stripRichText(value: string | null | undefined): string {
if (!value) return ''
return value
.replace(/<[^>]+>/g, ' ')
.replace(/&nbsp;/gi, ' ')
.replace(/&amp;/gi, '&')
.replace(/&lt;/gi, '<')
.replace(/&gt;/gi, '>')
.replace(/&quot;/gi, '"')
.replace(/&#39;|&apos;/gi, '\'')
.replace(/\s+/g, ' ')
.trim()
}

View File

@@ -14,6 +14,9 @@ use Sabre\VObject\Component\VCalendar;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Throwable;
use const ENT_HTML5;
use const ENT_QUOTES;
final class CalDavService
{
public function __construct(
@@ -199,7 +202,7 @@ final class CalDavService
$project = $task->getProject();
$projectCode = null !== $project ? $project->getCode() : '';
$summary = sprintf('[%s-%s] %s', $projectCode, $task->getNumber(), $task->getTitle());
$description = ($task->getDescription() ?? '')."\n\nLesstime task";
$description = $this->descriptionToPlainText($task->getDescription())."\n\nLesstime task";
$vcalendar = new VCalendar();
$vcalendar->add('VEVENT', [
@@ -225,7 +228,7 @@ final class CalDavService
$project = $task->getProject();
$projectCode = null !== $project ? $project->getCode() : '';
$summary = sprintf('[%s-%s] %s (deadline)', $projectCode, $task->getNumber(), $task->getTitle());
$description = ($task->getDescription() ?? '')."\n\nLesstime task";
$description = $this->descriptionToPlainText($task->getDescription())."\n\nLesstime task";
$vcalendar = new VCalendar();
$vcalendar->add('VTODO', [
@@ -337,6 +340,18 @@ final class CalDavService
return sprintf('%s@lesstime', bin2hex(random_bytes(16)));
}
private function descriptionToPlainText(?string $value): string
{
if (null === $value || '' === $value) {
return '';
}
$stripped = strip_tags($value);
$decoded = html_entity_decode($stripped, ENT_QUOTES | ENT_HTML5, 'UTF-8');
return trim((string) preg_replace('/[ \t]+/', ' ', $decoded));
}
/** @return array<string, string> */
private function getDayMap(): array
{