Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions assets/router/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import ListsView from '../vue/views/ListsView.vue'
import ListSubscribersView from '../vue/views/ListSubscribersView.vue'
import CampaignsView from '../vue/views/CampaignsView.vue'
import CampaignEditView from '../vue/views/CampaignEditView.vue'
import TemplatesView from '../vue/views/TemplatesView.vue'
import TemplateEditView from '../vue/views/TemplateEditView.vue'

export const router = createRouter({
history: createWebHistory(),
Expand All @@ -13,6 +15,8 @@ export const router = createRouter({
{ path: '/subscribers', name: 'subscribers', component: SubscribersView, meta: { title: 'Subscribers' } },
{ path: '/lists', name: 'lists', component: ListsView, meta: { title: 'Lists' } },
{ path: '/campaigns', name: 'campaigns', component: CampaignsView, meta: { title: 'Campaigns' } },
{ path: '/templates', name: 'templates', component: TemplatesView, meta: { title: 'Templates' } },
{ path: '/templates/:templateId/edit', name: 'template-edit', component: TemplateEditView, meta: { title: 'Edit Template' } },
{ path: '/campaigns/create', name: 'campaign-create', component: CampaignEditView, meta: { title: 'Create Campaign' } },
{ path: '/campaigns/:campaignId/edit', name: 'campaign-edit', component: CampaignEditView, meta: { title: 'Edit Campaign' } },
{ path: '/lists/:listId/subscribers', name: 'list-subscribers', component: ListSubscribersView, meta: { title: 'List Subscribers' } },
Expand Down
148 changes: 148 additions & 0 deletions assets/vue/components/templates/TemplateLibrary.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<template>
<section class="bg-white rounded-xl border border-slate-200 shadow-sm">
<header class="p-6 border-b border-slate-200 flex flex-col sm:flex-row justify-between items-center gap-4">
<h2 class="text-xl font-bold text-slate-900">Email Template Library</h2>
<div class="flex gap-2">
<button
class="px-4 py-2 bg-ext-wf1 hover:bg-ext-wf3 text-white text-sm font-medium rounded-lg flex items-center gap-2 transition-colors"
type="button"
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M5 12h14"></path>
<path d="M12 5v14"></path>
</svg>
New Template
</button>
Comment on lines +5 to +15

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Wire up or hide the inactive action buttons.

New Template, copy, and delete are rendered as enabled buttons, but none has a click handler. Please implement the actions or disable/hide them until supported.

Also applies to: 63-77

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@assets/vue/components/templates/TemplateLibrary.vue` around lines 5 - 15, The
New Template, Copy, and Delete buttons in TemplateLibrary.vue are rendered as
active but have no click handlers; either wire them to actual methods (e.g.,
createTemplate, copyTemplate, deleteTemplate) on the component and implement
those methods or explicitly disable/hide them until implemented. Locate the
buttons (labels "New Template", "Copy", "Delete") and add `@click` bindings to the
corresponding component methods and ensure those methods live on the component's
methods/options (or Vue setup) with appropriate behavior, error handling and UI
feedback; if you prefer to block UI, add the disabled attribute (or v-if/v-show)
and aria-disabled/tooltip to indicate the feature is not yet available. Ensure
template actions and method names match to avoid silent no-ops and keep
accessibility attributes updated.

</div>
</header>

<div class="p-6">
<div v-if="isLoading" class="py-12 text-center text-slate-500">
Loading templates...
</div>

<div v-else-if="errorMessage" class="py-12 text-center text-red-600">
{{ errorMessage }}
</div>

<div v-else-if="templates.length === 0" class="py-12 text-center text-slate-500">
No templates found.
</div>

<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<article
v-for="templateItem in templates"
:key="templateItem.id"
class="border border-slate-200 rounded-lg overflow-hidden hover:shadow-lg transition-all"
>
<div class="aspect-video bg-gradient-to-br from-slate-100 to-slate-200 flex items-center justify-center text-6xl">
{{ getTemplateEmoji(templateItem.id) }}
</div>

<div class="p-4">
<h3 class="font-semibold text-slate-900 mb-1">{{ templateItem.title || `Template #${templateItem.id}` }}</h3>
<p class="text-xs text-slate-500 mb-3">
{{ getTemplateType(templateItem) }}
</p>

<div class="flex justify-between items-center text-xs text-slate-400">
<span>ID {{ templateItem.id }}</span>
<span>Order {{ templateItem.order ?? '-' }}</span>
</div>

<div class="mt-4 flex gap-2">
<button
class="flex-1 px-3 py-2 bg-ext-wf1 hover:bg-ext-wf3 text-white text-sm rounded-lg transition-colors flex items-center justify-center gap-1"
type="button"
@click="goToEditTemplate(templateItem.id)"
>
<BaseIcon name="edit" />
Edit
</button>

<button
class="px-3 py-2 border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors"
type="button"
aria-label="Copy template"
>
<BaseIcon name="copy" />
</button>

<button
class="px-3 py-2 border border-slate-200 rounded-lg hover:bg-red-50 transition-colors"
type="button"
aria-label="Delete template"
>
<BaseIcon name="delete" />
</button>
</div>
</div>
</article>
</div>
</div>
</section>
</template>

<script setup>
import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import BaseIcon from '../base/BaseIcon.vue'
import { templateClient } from '../../api'

const router = useRouter()
const templates = ref([])
const isLoading = ref(false)
const errorMessage = ref('')

const emojiPool = ['📧', '🚀', '👋', '🛒', '🎉', '📝', '✨', '📢', '💼', '🎯']

const getTemplateEmoji = (id) => {
if (!Number.isFinite(id)) {
return '📄'
}

return emojiPool[id % emojiPool.length]
}

const getTemplateType = (templateItem) => {
const hasHtml = typeof templateItem.content === 'string' && templateItem.content.trim().length > 0
const hasText = typeof templateItem.text === 'string' && templateItem.text.trim().length > 0

if (hasHtml && hasText) {
return 'html + text'
}

if (hasHtml) {
return 'html'
}

if (hasText) {
return 'text'
}

return 'empty'
}

const loadTemplates = async () => {
isLoading.value = true
errorMessage.value = ''

try {
const response = await templateClient.getTemplates(0, 1000)
templates.value = Array.isArray(response?.items) ? response.items : []
Comment on lines +126 to +132

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect template pagination usage/contracts without modifying the repo.

rg -n -C3 'getTemplates\s*\('
rg -n -C5 'pagination|hasMore|nextCursor'
rg -n -C5 'class TemplatesClient|function getTemplates|getTemplates\s*='

Repository: phpList/web-frontend

Length of output: 40146


🏁 Script executed:

rg -n 'templateClient|TemplatesClient' -A5 -B2 assets/vue

Repository: phpList/web-frontend

Length of output: 4791


🏁 Script executed:

rg -n 'export.*templateClient|new.*TemplatesClient'

Repository: phpList/web-frontend

Length of output: 143


🏁 Script executed:

cat -n assets/vue/api.js | head -100

Repository: phpList/web-frontend

Length of output: 2236


Update loadTemplates to paginate through all templates instead of capping at 1000.

The getTemplates(0, 1000) call hard-caps the template library. Since the API supports cursor-based pagination, loop through all pages using pagination.hasMore and pagination.nextCursor, similar to how CampaignDirectory.vue and fetchAllLists handle it in this codebase.

Also affects CampaignEditView.vue line 947.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@assets/vue/components/templates/TemplateLibrary.vue` around lines 126 - 132,
loadTemplates currently calls templateClient.getTemplates(0, 1000) which caps
results; update loadTemplates to iterate over cursor-based pages until
pagination.hasMore is false by calling templateClient.getTemplates with the
current cursor (using pagination.nextCursor) and appending each page's items
into templates.value (instead of replacing it), mirroring the pagination pattern
used in CampaignDirectory.vue and the fetchAllLists helper; also apply the same
cursor-loop fix where CampaignEditView.vue performs its template fetch (around
the getTemplates usage referenced).

} catch (error) {
console.error('Failed to load templates:', error)
errorMessage.value = 'Failed to load templates.'
} finally {
isLoading.value = false
}
}

const goToEditTemplate = (templateId) => {
router.push(`/templates/${templateId}/edit`)
}

onMounted(() => {
loadTemplates()
})
</script>
Loading
Loading