Add survey question drag-sort and backend assets

- Frontend editor: drag-to-sort questions with dedicated sort assets/JS
- Backend list/search refinements for tl_survey and tl_survey_content
- Repository helpers for survey/question/editor queries
- Translations and AI handover notes update
This commit is contained in:
Jürgen Mummert
2026-06-14 09:13:07 +02:00
parent 79c4402aee
commit 1b16675ae9
23 changed files with 928 additions and 40 deletions
+1 -1
View File
@@ -156,7 +156,7 @@ final class SurveyEditorRepository
foreach ($rows as $row) {
$name = trim(sprintf('%s %s', (string) $row['firstname'], (string) $row['lastname']));
$choices[(int) $row['id']] = '' !== $name ? sprintf('%s <%s>', $name, (string) $row['email']) : (string) $row['email'];
$choices[(int) $row['id']] = '' !== $name ? $name : (string) $row['email'];
}
return $choices;
@@ -165,6 +165,46 @@ final class SurveyQuestionRepository
);
}
/**
* @param list<int> $orderedQuestionIds
*/
public function reorderQuestions(int $surveyId, array $orderedQuestionIds): void
{
$orderedQuestionIds = array_values(array_unique(array_filter(
array_map('intval', $orderedQuestionIds),
static fn (int $questionId): bool => $questionId > 0,
)));
$currentQuestionIds = array_map(static fn (SurveyContentModel $question): int => (int) $question->id, $this->findAllBySurvey($surveyId));
if ($orderedQuestionIds === $currentQuestionIds || count($orderedQuestionIds) < 2) {
return;
}
$sortedCurrentIds = $currentQuestionIds;
$sortedOrderedIds = $orderedQuestionIds;
sort($sortedCurrentIds);
sort($sortedOrderedIds);
if ($sortedCurrentIds !== $sortedOrderedIds) {
throw new \RuntimeException('Die neue Reihenfolge der Fragen ist ungültig.');
}
$this->connection->transactional(function () use ($surveyId, $orderedQuestionIds): void {
$sorting = 128;
foreach ($orderedQuestionIds as $questionId) {
$this->connection->update('tl_survey_content', [
'sorting' => $sorting,
], [
'id' => $questionId,
'pid' => $surveyId,
]);
$sorting += 128;
}
});
}
private function hydrate(array $ids): array
{
$models = [];
+45 -1
View File
@@ -53,6 +53,28 @@ final class SurveyRepository
return $this->findById((int) $id);
}
public function titleExists(string $title, int $excludeId = 0): bool
{
$qb = $this->connection->createQueryBuilder()
->select('id')
->from('tl_survey')
->where('title = :title')
->setParameter('title', trim($title))
->setMaxResults(1)
;
if ($excludeId > 0) {
$qb
->andWhere('id != :excludeId')
->setParameter('excludeId', $excludeId)
;
}
$id = $qb->executeQuery()->fetchOne();
return false !== $id;
}
/**
* @return list<array<string, mixed>>
*/
@@ -95,6 +117,7 @@ final class SurveyRepository
'alias' => $this->ensurePublicAlias(null),
'category' => serialize($this->normalizeCategoryIds($data['category'] ?? [])),
'description' => (string) ($data['description'] ?? ''),
'internalNote' => trim((string) ($data['internalNote'] ?? '')),
'published' => '',
'isActive' => '1',
'isLocked' => '',
@@ -121,6 +144,7 @@ final class SurveyRepository
'alias' => $this->ensurePublicAlias((string) $survey->alias, (int) $survey->id),
'category' => serialize($this->normalizeCategoryIds($data['category'] ?? StringUtil::deserialize($survey->category, true))),
'description' => (string) ($data['description'] ?? $survey->description),
'internalNote' => trim((string) ($data['internalNote'] ?? $survey->internalNote)),
'published' => array_key_exists('published', $data) ? (!empty($data['published']) ? '1' : '') : (string) $survey->published,
'isActive' => array_key_exists('isActive', $data) ? (!empty($data['isActive']) ? '1' : '') : (string) ($survey->isActive ?? ''),
'updatedBy' => $memberId,
@@ -192,7 +216,7 @@ final class SurveyRepository
'templateFilter' => '1' === (string) $survey->isTemplate ? '1' : '0',
'templateSummary' => '1' === (string) $survey->isTemplate ? 'Ja' : 'Nein',
'assignedMembersFilter' => implode(',', $memberIds),
'assignedMembersSummary' => implode(', ', $memberNames),
'assignedMembersSummary' => $this->buildAssignedMembersSummary($memberNames),
], [
'id' => $surveyId,
]);
@@ -391,6 +415,26 @@ final class SurveyRepository
);
}
/**
* @param list<string> $memberNames
*/
private function buildAssignedMembersSummary(array $memberNames): string
{
if ([] === $memberNames) {
return '';
}
$visibleNames = array_slice($memberNames, 0, 3);
$remainingCount = count($memberNames) - count($visibleNames);
$summary = implode(', ', $visibleNames);
if ($remainingCount <= 0) {
return $summary;
}
return sprintf('%s, ... und %d weitere', $summary, $remainingCount);
}
private function isValidPublicAlias(string $alias): bool
{
return 1 === preg_match('/^[a-z0-9]{20}$/', $alias);