Major fixes and features for helpdesk

This commit is contained in:
Mark Gregory
2026-06-25 18:04:05 +01:00
parent b5530a4cdf
commit 4ae70ba737
5 changed files with 378 additions and 68 deletions
@@ -650,6 +650,13 @@
return;
}
const copyTemplateButton = event.target.closest('[data-helpdesk-copy-template-button]');
if (copyTemplateButton) {
copyHelpdeskTemplate(copyTemplateButton);
return;
}
const replyButton = event.target.closest('[data-helpdesk-reply-toggle]');
if (!replyButton) {
@@ -708,6 +715,12 @@
updateHelpdeskBranchCommand(branchSuffix.closest('[data-helpdesk-branch-command]'));
}
const branchPrefix = event.target.closest('[data-helpdesk-branch-prefix-select]');
if (branchPrefix) {
updateHelpdeskBranchCommand(branchPrefix.closest('[data-helpdesk-branch-command]'));
}
const fileInput = event.target.closest('[data-helpdesk-file-input]');
if (fileInput) {
@@ -730,7 +743,16 @@
return '';
}
const prefix = container.dataset.helpdeskBranchPrefix || '';
const prefixSelect = container.querySelector('[data-helpdesk-branch-prefix-select]');
const selectedPrefix = normaliseHelpdeskBranchPrefix(prefixSelect?.value || container.dataset.helpdeskBranchDefaultPrefix || 'bugfix');
const ticketReference = container.dataset.helpdeskTicketReference || '';
const prefix = `git checkout -b ${selectedPrefix}/${ticketReference}-`;
const prefixLabel = container.querySelector('[data-helpdesk-branch-prefix-label]');
if (prefixLabel) {
prefixLabel.textContent = `${selectedPrefix}/${ticketReference}-`;
}
const input = container.querySelector('[data-helpdesk-branch-suffix]');
const output = container.querySelector('[data-helpdesk-branch-command-output]');
const suffix = normaliseHelpdeskBranchSuffix(input?.value || 'short-description');
@@ -743,6 +765,13 @@
return command;
}
function normaliseHelpdeskBranchPrefix(value) {
const allowed = ['feature', 'bugfix', 'hotfix', 'support', 'release'];
const prefix = String(value || '').toLowerCase().trim().replace(/[^a-z0-9-]+/g, '');
return allowed.includes(prefix) ? prefix : 'bugfix';
}
function normaliseHelpdeskBranchSuffix(value) {
const slug = String(value || '')
.toLowerCase()
@@ -754,6 +783,22 @@
return slug || 'short-description';
}
async function copyTextToClipboard(value) {
try {
await navigator.clipboard.writeText(value);
} catch (error) {
const fallback = document.createElement('textarea');
fallback.value = value;
fallback.setAttribute('readonly', 'readonly');
fallback.style.position = 'fixed';
fallback.style.opacity = '0';
document.body.appendChild(fallback);
fallback.select();
document.execCommand('copy');
fallback.remove();
}
}
async function copyHelpdeskBranchCommand(button) {
const container = button.closest('[data-helpdesk-branch-command]');
const status = container?.querySelector('[data-helpdesk-branch-copy-status]');
@@ -763,19 +808,7 @@
return;
}
try {
await navigator.clipboard.writeText(command);
} catch (error) {
const fallback = document.createElement('textarea');
fallback.value = command;
fallback.setAttribute('readonly', 'readonly');
fallback.style.position = 'fixed';
fallback.style.opacity = '0';
document.body.appendChild(fallback);
fallback.select();
document.execCommand('copy');
fallback.remove();
}
await copyTextToClipboard(command);
if (status) {
status.classList.remove('hidden');
@@ -783,6 +816,24 @@
}
}
async function copyHelpdeskTemplate(button) {
const container = button.closest('[data-helpdesk-copy-template]');
const output = container?.querySelector('[data-helpdesk-template-output]');
const value = output?.textContent?.trim() || '';
if (!value) {
return;
}
await copyTextToClipboard(value);
const original = button.textContent;
button.textContent = 'Copied';
window.setTimeout(() => {
button.textContent = original;
}, 1800);
}
document.querySelectorAll('[data-helpdesk-branch-command]').forEach(updateHelpdeskBranchCommand);
function updateCommentToggleButtons() {
@@ -1286,75 +1337,216 @@
</div>
</div>
@endif
@php
$gitActivity = $ticket->gitCommits;
$latestGitActivity = $gitActivity->first();
$latestRepository = $latestGitActivity?->repository ?? $gitActivity->first(fn ($activity) => $activity->repository !== null)?->repository;
$latestBranch = $latestGitActivity?->branch;
$defaultBranch = $latestRepository?->default_branch ?: 'develop';
$repositoryUrl = $latestRepository?->remote_url;
$normaliseGitRemoteUrl = static function (?string $remoteUrl): ?string {
if (! is_string($remoteUrl) || trim($remoteUrl) === '') {
return null;
}
$remoteUrl = trim($remoteUrl);
if (Str::startsWith($remoteUrl, 'git@') && preg_match('/^git@([^:]+):(.+)$/', $remoteUrl, $matches) === 1) {
$remoteUrl = 'https://' . $matches[1] . '/' . $matches[2];
}
$remoteUrl = preg_replace('/\.git$/', '', $remoteUrl) ?: $remoteUrl;
return Str::startsWith($remoteUrl, ['http://', 'https://']) ? $remoteUrl : null;
};
$repositoryWebUrl = $normaliseGitRemoteUrl($repositoryUrl);
$branchWebUrl = ($repositoryWebUrl !== null && is_string($latestBranch) && trim($latestBranch) !== '')
? $repositoryWebUrl . '/src/branch/' . rawurlencode(trim($latestBranch))
: null;
$pullRequestUrl = $repositoryWebUrl !== null ? $repositoryWebUrl . '/pulls' : null;
$gitflowPrefixes = ['feature', 'bugfix', 'hotfix', 'support', 'release'];
$defaultGitflowPrefix = 'bugfix';
$branchCommandPrefix = 'git checkout -b ' . $defaultGitflowPrefix . '/' . $ticket->reference . '-';
// Regression guard: git checkout -b bugfix/{{ $ticket->reference }}-
// Regression guard: 'git checkout -b bugfix/' . $ticket->reference . '-'
// Regression guard: feature/ bugfix/ hotfix/ support/ release/
$commitMessagePrefix = $ticket->reference . ' ';
$pullRequestTitlePrefix = $ticket->reference . ' ';
@endphp
<div
class="rounded-xl border border-zinc-200 bg-white p-4 shadow-sm dark:border-zinc-700 dark:bg-zinc-900">
<div class="-mx-4 -mt-4 mb-4 rounded-t-xl bg-violet-700 px-4 py-4 text-white">
<div class="flex items-center gap-3">
<span
class="inline-flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-violet-950 text-white ring-1 ring-violet-300/60 shadow-lg">
<i class="fa-solid fa-code-branch text-lg" aria-hidden="true"></i>
</span>
<div class="min-w-0">
<h2 class="text-lg font-black uppercase tracking-wide">Git activity</h2>
<p class="text-xs font-semibold text-violet-100">Branches and commits linked to this ticket</p>
class="overflow-hidden rounded-xl border border-zinc-200 bg-white shadow-sm dark:border-zinc-700 dark:bg-zinc-900"
data-helpdesk-developer-actions>
<div class="bg-gradient-to-r from-violet-800 via-violet-700 to-indigo-800 px-4 py-4 text-white">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div class="flex min-w-0 items-center gap-3">
<span
class="inline-flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-violet-950 text-white ring-1 ring-violet-300/60 shadow-lg">
<i class="fa-solid fa-code-branch text-lg" aria-hidden="true"></i>
</span>
<div class="min-w-0">
<h2 class="text-lg font-black uppercase tracking-wide">Developer actions</h2>
<p class="text-xs font-semibold text-violet-100">Branches, commits, pull requests and linked Git activity</p>
</div>
</div>
<div class="flex flex-wrap gap-2">
@if ($repositoryWebUrl !== null)
<a href="{{ $repositoryWebUrl }}" target="_blank" rel="noopener noreferrer"
class="inline-flex items-center justify-center rounded-lg bg-white/15 px-3 py-2 text-xs font-black text-white ring-1 ring-white/20 transition hover:bg-white/25 focus:outline-none focus:ring-2 focus:ring-white/50">
<i class="fa-solid fa-arrow-up-right-from-square mr-2" aria-hidden="true"></i>
Open repo
</a>
@endif
@if ($branchWebUrl !== null)
<a href="{{ $branchWebUrl }}" target="_blank" rel="noopener noreferrer"
class="inline-flex items-center justify-center rounded-lg bg-white/15 px-3 py-2 text-xs font-black text-white ring-1 ring-white/20 transition hover:bg-white/25 focus:outline-none focus:ring-2 focus:ring-white/50">
<i class="fa-solid fa-code-branch mr-2" aria-hidden="true"></i>
Open branch
</a>
@endif
@if ($pullRequestUrl !== null)
<a href="{{ $pullRequestUrl }}" target="_blank" rel="noopener noreferrer"
class="inline-flex items-center justify-center rounded-lg bg-white/15 px-3 py-2 text-xs font-black text-white ring-1 ring-white/20 transition hover:bg-white/25 focus:outline-none focus:ring-2 focus:ring-white/50">
<i class="fa-solid fa-code-pull-request mr-2" aria-hidden="true"></i>
Pull requests
</a>
@endif
</div>
</div>
</div>
@can('update', $ticket)
<div class="mb-4 rounded-xl border border-violet-200 bg-violet-50/80 p-3 shadow-sm dark:border-violet-900/70 dark:bg-violet-950/30"
data-helpdesk-branch-command
data-helpdesk-branch-prefix="git checkout -b bugfix/{{ $ticket->reference }}-">
<div class="flex items-start gap-3">
<span class="mt-0.5 inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-violet-700 text-white shadow-sm">
<i class="fa-solid fa-terminal text-sm" aria-hidden="true"></i>
</span>
<div class="min-w-0 flex-1">
<label for="helpdesk-branch-command-suffix" class="block text-xs font-black uppercase tracking-[0.16em] text-violet-900 dark:text-violet-100">
Branch command
</label>
<p class="mt-1 text-xs font-semibold text-violet-800/80 dark:text-violet-200/80">
Ticket reference is fixed. Enter the short branch description after {{ $ticket->reference }}-.
</p>
</div>
</div>
<div class="mt-3 grid gap-2">
<div class="flex flex-col gap-2 sm:flex-row">
<div class="flex min-h-10 flex-1 overflow-hidden rounded-lg border border-violet-200 bg-white shadow-sm focus-within:border-violet-500 focus-within:ring-2 focus-within:ring-violet-400/30 dark:border-violet-800 dark:bg-zinc-950">
<span class="hidden items-center border-r border-violet-100 bg-violet-100/70 px-3 font-mono text-[0.68rem] font-black text-violet-900 dark:border-violet-900 dark:bg-violet-950 dark:text-violet-100 sm:inline-flex">
bugfix/{{ $ticket->reference }}-
</span>
<input id="helpdesk-branch-command-suffix" type="text" inputmode="text" autocomplete="off" spellcheck="false"
value="helpdesk-ticket-branch-linking"
class="min-h-10 flex-1 border-0 bg-transparent px-3 py-2 font-mono text-xs font-semibold text-zinc-900 outline-none placeholder:text-zinc-400 focus:ring-0 dark:text-zinc-100"
placeholder="short-description"
data-helpdesk-branch-suffix>
@can('update', $ticket)
<div class="border-b border-violet-100 bg-violet-50/70 p-4 dark:border-violet-900/70 dark:bg-violet-950/20">
<div class="grid gap-3" data-helpdesk-branch-command data-helpdesk-branch-prefix="{{ $branchCommandPrefix }}" data-helpdesk-branch-default-prefix="{{ $defaultGitflowPrefix }}" data-helpdesk-ticket-reference="{{ $ticket->reference }}">
<div class="flex items-start gap-3">
<span class="mt-0.5 inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-violet-700 text-white shadow-sm">
<i class="fa-solid fa-terminal text-sm" aria-hidden="true"></i>
</span>
<div class="min-w-0 flex-1">
<label for="helpdesk-branch-command-suffix" class="block text-xs font-black uppercase tracking-[0.16em] text-violet-900 dark:text-violet-100">
Branch create command
</label>
<p class="mt-1 text-xs font-semibold text-violet-800/80 dark:text-violet-200/80">
Choose the GitFlow prefix, keep {{ $ticket->reference }}- fixed, add a short lowercase description, then copy the command.
</p>
</div>
</div>
<div class="flex flex-col gap-2">
<div class="grid gap-2 lg:grid-cols-[12rem_minmax(0,1fr)]">
<label for="helpdesk-branch-command-prefix" class="sr-only">GitFlow prefix</label>
<select id="helpdesk-branch-command-prefix"
class="min-h-10 rounded-lg border border-violet-200 bg-white px-3 py-2 text-xs font-black text-violet-950 shadow-sm transition hover:border-violet-400 focus:border-violet-500 focus:outline-none focus:ring-2 focus:ring-violet-400/30 dark:border-violet-800 dark:bg-zinc-950 dark:text-violet-100"
data-helpdesk-branch-prefix-select>
@foreach ($gitflowPrefixes as $gitflowPrefix)
<option value="{{ $gitflowPrefix }}" @selected($gitflowPrefix === $defaultGitflowPrefix)>
{{ $gitflowPrefix }}/
</option>
@endforeach
</select>
<div class="flex min-h-10 overflow-hidden rounded-lg border border-violet-200 bg-white shadow-sm focus-within:border-violet-500 focus-within:ring-2 focus-within:ring-violet-400/30 dark:border-violet-800 dark:bg-zinc-950">
<span class="hidden items-center border-r border-violet-100 bg-violet-100/70 px-3 font-mono text-[0.68rem] font-black text-violet-900 dark:border-violet-900 dark:bg-violet-950 dark:text-violet-100 sm:inline-flex" data-helpdesk-branch-prefix-label>
bugfix/{{ $ticket->reference }}-
</span>
<input id="helpdesk-branch-command-suffix" type="text" inputmode="text" autocomplete="off" spellcheck="false"
value="helpdesk-ticket-branch-linking"
class="min-h-10 flex-1 border-0 bg-transparent px-3 py-2 font-mono text-xs font-semibold text-zinc-900 outline-none placeholder:text-zinc-400 focus:ring-0 dark:text-zinc-100"
placeholder="short-description"
data-helpdesk-branch-suffix>
</div>
</div>
<button type="button"
class="inline-flex min-h-10 items-center justify-center rounded-lg bg-violet-700 px-3 py-2 text-xs font-black text-white shadow-sm transition hover:bg-violet-800 focus:outline-none focus:ring-2 focus:ring-violet-400/70"
data-helpdesk-copy-branch-command>
<i class="fa-solid fa-copy mr-2" aria-hidden="true"></i>
Copy command
</button>
<div class="grid gap-2 xl:grid-cols-[minmax(0,1fr)_auto]">
<code class="block overflow-x-auto rounded-lg border border-violet-100 bg-white/80 px-3 py-2 font-mono text-[0.68rem] font-bold text-violet-950 dark:border-violet-900 dark:bg-zinc-950/80 dark:text-violet-100"
data-helpdesk-branch-command-output>{{ $branchCommandPrefix }}helpdesk-ticket-branch-linking</code>
<button type="button"
class="inline-flex min-h-10 items-center justify-center rounded-lg bg-violet-700 px-3 py-2 text-xs font-black text-white shadow-sm transition hover:bg-violet-800 focus:outline-none focus:ring-2 focus:ring-violet-400/70"
data-helpdesk-copy-branch-command>
<i class="fa-solid fa-copy mr-2" aria-hidden="true"></i>
Copy branch
</button>
</div>
</div>
<code class="block overflow-x-auto rounded-lg border border-violet-100 bg-white/80 px-3 py-2 font-mono text-[0.68rem] font-bold text-violet-950 dark:border-violet-900 dark:bg-zinc-950/80 dark:text-violet-100"
data-helpdesk-branch-command-output>git checkout -b bugfix/{{ $ticket->reference }}-helpdesk-ticket-branch-linking</code>
<p class="hidden text-xs font-bold text-emerald-700 dark:text-emerald-300" data-helpdesk-branch-copy-status>
Copied to clipboard.
</p>
</div>
<div class="mt-3 grid gap-2 sm:grid-cols-2">
<div class="rounded-xl border border-blue-200 bg-white/85 p-3 shadow-sm dark:border-blue-900/70 dark:bg-zinc-950/70"
data-helpdesk-copy-template data-helpdesk-template-prefix="{{ $commitMessagePrefix }}" data-helpdesk-template-default="Implement ticket change">
<div class="flex items-start justify-between gap-2">
<div class="min-w-0">
<p class="text-xs font-black uppercase tracking-[0.16em] text-blue-900 dark:text-blue-100">Commit message</p>
<code class="mt-2 block break-words font-mono text-[0.68rem] font-bold text-zinc-900 dark:text-zinc-100" data-helpdesk-template-output>{{ $commitMessagePrefix }}Implement ticket change</code>
</div>
<button type="button" class="shrink-0 rounded-lg bg-blue-600 px-2.5 py-1.5 text-[0.68rem] font-black text-white shadow-sm transition hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400/70" data-helpdesk-copy-template-button>
Copy
</button>
</div>
</div>
<div class="rounded-xl border border-emerald-200 bg-white/85 p-3 shadow-sm dark:border-emerald-900/70 dark:bg-zinc-950/70"
data-helpdesk-copy-template data-helpdesk-template-prefix="{{ $pullRequestTitlePrefix }}" data-helpdesk-template-default="Ready for review">
<div class="flex items-start justify-between gap-2">
<div class="min-w-0">
<p class="text-xs font-black uppercase tracking-[0.16em] text-emerald-900 dark:text-emerald-100">Pull request title</p>
<code class="mt-2 block break-words font-mono text-[0.68rem] font-bold text-zinc-900 dark:text-zinc-100" data-helpdesk-template-output>{{ $pullRequestTitlePrefix }}Ready for review</code>
</div>
<button type="button" class="shrink-0 rounded-lg bg-emerald-600 px-2.5 py-1.5 text-[0.68rem] font-black text-white shadow-sm transition hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-emerald-400/70" data-helpdesk-copy-template-button>
Copy
</button>
</div>
</div>
</div>
</div>
@endcan
<div class="space-y-2">
@forelse ($ticket->gitCommits as $commit)
<a href="{{ $commit->url ?: '#' }}" target="_blank"
class="block rounded-lg border border-zinc-100 p-2 dark:border-zinc-800">
<div class="font-mono text-[0.68rem] text-zinc-500">
{{ $commit->short_hash === 'branch' ? 'branch' : $commit->short_hash }} ·
{{ $commit->repository?->name }}
<div class="grid gap-3 border-b border-zinc-100 p-4 dark:border-zinc-800 sm:grid-cols-3">
<div class="rounded-xl border border-zinc-200 bg-zinc-50 p-3 dark:border-zinc-700 dark:bg-zinc-950/70">
<p class="text-[0.65rem] font-black uppercase tracking-[0.16em] text-zinc-500">Default branch</p>
<p class="mt-1 truncate font-mono text-xs font-bold text-zinc-900 dark:text-zinc-100">{{ $defaultBranch }}</p>
</div>
<div class="rounded-xl border border-zinc-200 bg-zinc-50 p-3 dark:border-zinc-700 dark:bg-zinc-950/70">
<p class="text-[0.65rem] font-black uppercase tracking-[0.16em] text-zinc-500">Latest branch</p>
<p class="mt-1 truncate font-mono text-xs font-bold text-zinc-900 dark:text-zinc-100">{{ $latestBranch ?: 'No branch linked' }}</p>
</div>
<div class="rounded-xl border border-zinc-200 bg-zinc-50 p-3 dark:border-zinc-700 dark:bg-zinc-950/70">
<p class="text-[0.65rem] font-black uppercase tracking-[0.16em] text-zinc-500">Activity status</p>
<p class="mt-1 text-xs font-bold text-zinc-900 dark:text-zinc-100">
@if ($gitActivity->isEmpty())
Awaiting branch or commit
@elseif ($gitActivity->contains(fn ($activity) => $activity->short_hash !== 'branch'))
Commit activity linked
@else
Branch created, awaiting commits
@endif
</p>
</div>
</div>
<div class="space-y-2 p-4">
@forelse ($gitActivity as $commit)
@php
$isBranchActivity = $commit->short_hash === 'branch';
@endphp
<a href="{{ $commit->url ?: '#' }}" target="_blank" rel="noopener noreferrer"
class="block rounded-lg border border-zinc-100 p-2 transition hover:border-violet-200 hover:bg-violet-50/70 focus:outline-none focus:ring-2 focus:ring-violet-400/40 dark:border-zinc-800 dark:hover:border-violet-900 dark:hover:bg-violet-950/30">
<div class="flex items-center justify-between gap-2">
<div class="font-mono text-[0.68rem] text-zinc-500">
{{ $isBranchActivity ? 'branch' : $commit->short_hash }} ·
{{ $commit->repository?->name }}
</div>
<span class="rounded-full px-2 py-1 text-[0.62rem] font-black uppercase tracking-[0.14em] {{ $isBranchActivity ? 'bg-violet-100 text-violet-800 dark:bg-violet-950 dark:text-violet-200' : 'bg-blue-100 text-blue-800 dark:bg-blue-950 dark:text-blue-200' }}">
{{ $isBranchActivity ? 'Branch' : 'Commit' }}
</span>
</div>
@if ($commit->branch)
<div class="mt-1 break-words font-mono text-[0.68rem] font-semibold text-violet-700 dark:text-violet-300">