Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
258c6e9c17 | ||
| feffe63019 | |||
| 34ba554fba | |||
| b2cc6e96e1 | |||
| 2a68d2f9c6 | |||
| 2898b22440 | |||
|
|
f1fd80d9ac | ||
|
|
24e3e8e989 |
@@ -1,2 +1,2 @@
|
||||
parameters:
|
||||
app.version: '0.3.29'
|
||||
app.version: '0.3.31'
|
||||
|
||||
@@ -41,7 +41,7 @@ services:
|
||||
- "8082:80"
|
||||
volumes:
|
||||
- ./:/var/www/html:ro
|
||||
- ./infra/dev/nginx.conf:/etc/nginx/conf.d/lesstime.conf:ro
|
||||
- ./infra/dev/nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
restart: unless-stopped
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
|
||||
@@ -66,14 +66,10 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<label class="mb-1 block text-sm font-medium text-neutral-700">
|
||||
{{ $t('clientTicket.description') }}
|
||||
</label>
|
||||
<textarea
|
||||
<MalioInputRichText
|
||||
v-model="editForm.description"
|
||||
rows="5"
|
||||
class="w-full rounded-lg border border-neutral-300 px-3 py-2 text-sm focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||
style="resize: vertical; min-height: 140px; max-height: 500px"
|
||||
:label="$t('clientTicket.description')"
|
||||
min-height="180px"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -129,7 +125,13 @@
|
||||
<!-- Description -->
|
||||
<div class="mt-4">
|
||||
<p class="text-sm font-medium text-neutral-700">{{ $t('clientTicket.description') }}</p>
|
||||
<p class="mt-1 whitespace-pre-wrap text-sm text-neutral-600">{{ ticket.description }}</p>
|
||||
<MalioInputRichText
|
||||
v-if="ticket.description"
|
||||
:model-value="ticket.description"
|
||||
:editable="false"
|
||||
group-class="mt-1"
|
||||
/>
|
||||
<p v-else class="mt-1 text-sm italic text-neutral-400">—</p>
|
||||
</div>
|
||||
|
||||
<!-- URL (if bug) -->
|
||||
|
||||
@@ -116,7 +116,12 @@
|
||||
|
||||
<!-- Expanded details -->
|
||||
<div v-if="expandedId === ticket.id" class="border-t border-neutral-100 px-3 py-3">
|
||||
<p class="text-sm text-neutral-600 whitespace-pre-wrap">{{ ticket.description }}</p>
|
||||
<MalioInputRichText
|
||||
v-if="ticket.description"
|
||||
:model-value="ticket.description"
|
||||
:editable="false"
|
||||
/>
|
||||
<p v-else class="text-sm italic text-neutral-400">—</p>
|
||||
<div v-if="ticket.url" class="mt-2">
|
||||
<a
|
||||
:href="ticket.url"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
:error="touched.title && !form.title.trim() ? 'Le titre est requis' : ''"
|
||||
@blur="touched.title = true"
|
||||
/>
|
||||
<MalioInputTextArea
|
||||
<MalioInputRichText
|
||||
v-model="form.description"
|
||||
label="Description"
|
||||
:size="3"
|
||||
min-height="120px"
|
||||
/>
|
||||
<div class="mt-4">
|
||||
<ColorPicker v-model="form.color" />
|
||||
|
||||
@@ -196,33 +196,13 @@
|
||||
|
||||
<!-- Description -->
|
||||
<div class="mt-5">
|
||||
<div class="mb-1 flex items-center justify-between">
|
||||
<label class="text-sm font-medium text-slate-700">Description</label>
|
||||
<button
|
||||
v-if="form.description"
|
||||
type="button"
|
||||
class="flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium text-slate-500 transition-colors hover:bg-slate-100 hover:text-slate-700"
|
||||
@click="showMarkdownPreview = true"
|
||||
>
|
||||
<Icon name="heroicons:eye" class="size-3.5" />
|
||||
Aperçu MD
|
||||
</button>
|
||||
</div>
|
||||
<MalioInputTextArea
|
||||
<MalioInputRichText
|
||||
v-model="form.description"
|
||||
:size="5"
|
||||
resize="vertical"
|
||||
:min-resize-height="140"
|
||||
:max-resize-height="500"
|
||||
label="Description"
|
||||
min-height="180px"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<MarkdownPreviewModal
|
||||
v-model="showMarkdownPreview"
|
||||
:content="form.description"
|
||||
title="Aperçu de la description"
|
||||
/>
|
||||
|
||||
<!-- Documents -->
|
||||
<TaskDocumentUpload
|
||||
v-if="isEditing && task && isAdmin"
|
||||
@@ -558,7 +538,7 @@ const isOpen = computed({
|
||||
})
|
||||
|
||||
function close() {
|
||||
if (confirmDeleteDocOpen.value || confirmDeleteOpen.value || showMarkdownPreview.value) return
|
||||
if (confirmDeleteDocOpen.value || confirmDeleteOpen.value) return
|
||||
isOpen.value = false
|
||||
}
|
||||
|
||||
@@ -566,7 +546,6 @@ const isEditing = computed(() => !!props.task)
|
||||
const isSubmitting = ref(false)
|
||||
const confirmDeleteOpen = ref(false)
|
||||
const activeTab = ref<'details' | 'planning'>('details')
|
||||
const showMarkdownPreview = ref(false)
|
||||
|
||||
const giteaUrl = ref('')
|
||||
const { getSettings: getGiteaSettings } = useGiteaService()
|
||||
|
||||
@@ -11,14 +11,11 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-semibold text-neutral-700">Description</label>
|
||||
<textarea
|
||||
v-model="form.description"
|
||||
rows="3"
|
||||
class="w-full rounded-md border border-neutral-300 px-3 py-2 text-sm focus:border-primary-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<MalioInputRichText
|
||||
v-model="form.description"
|
||||
label="Description"
|
||||
min-height="120px"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<label class="mb-1 block text-sm font-semibold text-neutral-700">Date</label>
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<div class="overflow-y-auto px-6 py-4">
|
||||
<div
|
||||
v-if="content"
|
||||
class="prose prose-slate max-w-none prose-headings:font-semibold prose-a:text-blue-600 prose-code:rounded prose-code:bg-slate-100 prose-code:px-1.5 prose-code:py-0.5 prose-code:text-sm prose-code:before:content-none prose-code:after:content-none prose-pre:bg-slate-900 prose-pre:text-slate-100"
|
||||
class="prose prose-slate max-w-none prose-headings:font-semibold prose-a:text-blue-600 prose-code:rounded prose-code:bg-slate-100 prose-code:px-1.5 prose-code:py-0.5 prose-code:text-sm prose-code:before:content-none prose-code:after:content-none prose-pre:bg-slate-900 prose-pre:text-slate-100 prose-pre:overflow-x-auto [&_pre_code]:bg-transparent [&_pre_code]:p-0 [&_pre_code]:text-inherit [&_pre_code]:text-[0.875rem] [&_pre_code]:leading-relaxed"
|
||||
v-html="renderedHtml"
|
||||
/>
|
||||
<p v-else class="text-sm italic text-slate-400">
|
||||
|
||||
@@ -37,6 +37,9 @@ export default defineNuxtConfig({
|
||||
},
|
||||
},
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: ['markdown-it-task-lists'],
|
||||
},
|
||||
},
|
||||
toast: {
|
||||
settings: {
|
||||
|
||||
882
frontend/package-lock.json
generated
882
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@
|
||||
"build:dist": "nuxt generate && rm -rf dist && cp -R .output/public dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@malio/layer-ui": "^1.2.3",
|
||||
"@malio/layer-ui": "^1.4.8",
|
||||
"@nuxt/icon": "^2.2.1",
|
||||
"@nuxtjs/i18n": "^10.2.3",
|
||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||
|
||||
@@ -37,15 +37,10 @@
|
||||
|
||||
<!-- Description -->
|
||||
<div class="mt-4">
|
||||
<MalioInputTextArea
|
||||
<MalioInputRichText
|
||||
v-model="form.description"
|
||||
:label="$t('clientTicket.description')"
|
||||
:size="5"
|
||||
resize="vertical"
|
||||
:min-resize-height="140"
|
||||
:max-resize-height="500"
|
||||
min-resize-width="100%"
|
||||
max-resize-width="100%"
|
||||
min-height="180px"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -84,7 +84,12 @@
|
||||
|
||||
<!-- Expanded details -->
|
||||
<div v-if="expandedId === ticket.id" class="border-t border-neutral-100 px-4 py-3">
|
||||
<p class="text-sm text-neutral-600 whitespace-pre-wrap">{{ ticket.description }}</p>
|
||||
<MalioInputRichText
|
||||
v-if="ticket.description"
|
||||
:model-value="ticket.description"
|
||||
:editable="false"
|
||||
/>
|
||||
<p v-else class="text-sm italic text-neutral-400">—</p>
|
||||
<div v-if="ticket.url" class="mt-2">
|
||||
<a
|
||||
:href="ticket.url"
|
||||
|
||||
@@ -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(/ /gi, ' ')
|
||||
.replace(/&/gi, '&')
|
||||
.replace(/</gi, '<')
|
||||
.replace(/>/gi, '>')
|
||||
.replace(/"/gi, '"')
|
||||
.replace(/'|'/gi, '\'')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user