Skip to content

Templates#84

Open
TatevikGr wants to merge 1 commit intodevfrom
feat/templates
Open

Templates#84
TatevikGr wants to merge 1 commit intodevfrom
feat/templates

Conversation

@TatevikGr
Copy link
Copy Markdown
Contributor

@TatevikGr TatevikGr commented Apr 21, 2026

Summary by CodeRabbit

  • New Features
    • Template library interface to browse, view, and organize templates in a responsive grid layout
    • Template editor with file import, text editing, and content validation support

Summary

Provide a general description of the code changes in your pull request …
were there any bugs you had fixed? If so, mention them. If these bugs have open
GitHub issues, be sure to tag them here as well, to keep the conversation
linked together.

Unit test

Are your changes covered with unit tests, and do they not break anything?

You can run the existing unit tests using this command:

vendor/bin/phpunit tests/

Code style

Have you checked that you code is well-documented and follows the PSR-2 coding
style?

You can check for this using this command:

vendor/bin/phpcs --standard=PSR2 src/ tests/

Other Information

If there's anything else that's important and relevant to your pull
request, mention that information here. This could include benchmarks,
or other information.

If you are updating any of the CHANGELOG files or are asked to update the
CHANGELOG files by reviewers, please add the CHANGELOG entry at the top of the file.

Thanks for contributing to phpList!

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 21, 2026

📝 Walkthrough

Walkthrough

This change adds a complete template management feature to the application. It introduces a new Vue Router routes configuration with /templates and /templates/:templateId/edit endpoints. A new backend Symfony controller TemplatesController handles these routes and renders the SPA template with required context. On the frontend, three new Vue components (TemplatesView, TemplateLibrary, and TemplateEditView) provide the UI for browsing and editing templates. The feature integrates with an API client to load and persist template data, including support for file uploads and HTML content editing. A minor dependency version bump updates the rest-api-client library.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Router as Vue Router
    participant View as TemplatesView
    participant Library as TemplateLibrary
    participant Client as Template API Client
    participant Backend as Backend API

    User->>Router: Navigate to /templates
    Router->>View: Render TemplatesView
    View->>Library: Mount TemplateLibrary
    Library->>Library: Set isLoading = true
    Library->>Client: getTemplates(0, 1000)
    Client->>Backend: GET /templates
    Backend-->>Client: Template list
    Client-->>Library: Return templates
    Library->>Library: Set templates data
    Library->>Library: Set isLoading = false
    Library->>View: Render template cards
    View-->>User: Display template library UI
Loading
sequenceDiagram
    actor User
    participant Router as Vue Router
    participant View as TemplateEditView
    participant Client as Template API Client
    participant Backend as Backend API

    User->>Router: Navigate to /templates/:id/edit
    Router->>View: Render TemplateEditView
    View->>View: Extract templateId from route
    View->>View: Set isLoading = true
    View->>Client: getTemplate(templateId)
    Client->>Backend: GET /templates/:id
    Backend-->>Client: Template data
    Client-->>View: Return template
    View->>View: Populate form with title/content/text
    View->>View: Set isLoading = false
    User->>View: Edit form fields & click Save
    View->>View: Validate form.title exists
    View->>View: Create CreateTemplateRequest
    View->>View: Set isSaving = true
    View->>Client: updateTemplate(request, templateId)
    Client->>Backend: PUT/POST /templates/:id
    Backend-->>Client: Success response
    Client-->>View: Return result
    View->>View: Set saveSuccess = true
    View->>View: Set isSaving = false
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Templates' is too vague and generic; it doesn't convey the specific nature of the changes (adding template management routes, views, and controller endpoints). Consider a more descriptive title like 'Add template management routes and views' or 'Implement templates feature with editor and library' to better communicate the scope of changes.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/templates

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@assets/vue/components/templates/TemplateLibrary.vue`:
- Around line 38-43: The template rendering accesses
templateItem.images[0].mimetype directly which throws when images is an empty
array; update getTemplateImage(templateItem) (and any other places around the
same component block referenced at lines 101-105) to guard against missing/empty
images by first checking templateItem.images && templateItem.images.length > 0
(or using optional chaining) before accessing images[0], and return a safe
fallback (e.g., a placeholder image URL or null) when no preview image exists so
the library render won't break.
- Around line 6-15: The "New Template" button (and the similar inactive buttons
around the component) are interactive but have no click handlers—either wire
them to real handlers or disable them until implemented; to quickly fix, add the
disabled attribute and aria-disabled plus a visual disabled style to the "New
Template" <button> and the other inactive buttons (apply to the buttons at the
other ranges noted) or alternatively bind them to placeholder methods like
createTemplate/copyTemplate/deleteTemplate that emit a TODO event or open a "Not
implemented" toast; update class names to include a disabled state (e.g., remove
hover:bg-ext-wf3 and add opacity-50 cursor-not-allowed) so users get correct
affordance.

In `@assets/vue/views/TemplateEditView.vue`:
- Around line 177-189: The plain-text generator in populateTextFromContent
currently strips tags but leaves HTML entities (e.g., &amp;, &nbsp;) intact;
after the existing tag/style/script stripping and trimming (in
populateTextFromContent operating on form.value.content and setting
form.value.text) decode HTML entities by using the browser DOM decode approach
(e.g., create a temporary element, set its innerHTML to the intermediate string
and read .textContent) or an equivalent HTML-entity decoder, then assign the
decoded result to form.value.text so entities are converted to their correct
characters.

In `@src/Controller/TemplatesController.php`:
- Around line 15-22: In the index(Request $request) action (the controller
method named index for the #[Route('/', name: 'templates', ...)]), change the
hard-coded page label passed to the template: replace the 'page' => 'Campaigns'
entry with the correct templates page label (e.g. 'Templates' or the appropriate
i18n key) so the /templates route renders the correct SPA title.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e71d3c1e-c314-4329-a42d-3cc1d562866d

📥 Commits

Reviewing files that changed from the base of the PR and between 1f9eb8a and 30e3413.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (6)
  • assets/router/index.js
  • assets/vue/components/templates/TemplateLibrary.vue
  • assets/vue/views/TemplateEditView.vue
  • assets/vue/views/TemplatesView.vue
  • package.json
  • src/Controller/TemplatesController.php

Comment on lines +6 to +15
<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>
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 | 🟡 Minor

Wire or disable the inactive template actions.

These buttons are enabled but have no handlers, so clicks silently do nothing. Either implement them now or ship them disabled/hidden until supported.

Proposed temporary disablement
         <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"
+          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 disabled:opacity-50 disabled:cursor-not-allowed"
           type="button"
+          disabled
+          title="Template creation is not implemented yet"
         >
               <button
-                class="px-3 py-2 border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors"
+                class="px-3 py-2 border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
                 type="button"
                 aria-label="Copy template"
+                disabled
+                title="Copy is not implemented yet"
               >
               <button
-                class="px-3 py-2 border border-slate-200 rounded-lg hover:bg-red-50 transition-colors"
+                class="px-3 py-2 border border-slate-200 rounded-lg hover:bg-red-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
                 type="button"
                 aria-label="Delete template"
+                disabled
+                title="Delete is not implemented yet"
               >

Happy to help sketch the create/copy/delete handlers if you want to track that separately.

Also applies to: 67-81

🤖 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 6 - 15, The
"New Template" button (and the similar inactive buttons around the component)
are interactive but have no click handlers—either wire them to real handlers or
disable them until implemented; to quickly fix, add the disabled attribute and
aria-disabled plus a visual disabled style to the "New Template" <button> and
the other inactive buttons (apply to the buttons at the other ranges noted) or
alternatively bind them to placeholder methods like
createTemplate/copyTemplate/deleteTemplate that emit a TODO event or open a "Not
implemented" toast; update class names to include a disabled state (e.g., remove
hover:bg-ext-wf3 and add opacity-50 cursor-not-allowed) so users get correct
affordance.

Comment on lines +38 to +43
<div class="aspect-video bg-gradient-to-br from-slate-100 to-slate-200 flex items-center justify-center text-6xl">
<img
:src="getTemplateImage(templateItem)"
:alt="`Template #${templateItem.id}`"
class="w-full h-full object-cover"
>
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

Guard templates that do not have preview images.

[] is truthy, so images[0].mimetype throws when a template has an empty images array and can break the whole library render.

Proposed fix
           <div class="aspect-video bg-gradient-to-br from-slate-100 to-slate-200 flex items-center justify-center text-6xl">
             <img
+              v-if="getTemplateImage(templateItem)"
               :src="getTemplateImage(templateItem)"
               :alt="`Template #${templateItem.id}`"
               class="w-full h-full object-cover"
             >
+            <span v-else class="text-sm text-slate-500">No preview</span>
           </div>
 const getTemplateImage = (templateItem) => {
-  const images = templateItem?.images
-  if (!images) return '';
+  const image = Array.isArray(templateItem?.images) ? templateItem.images[0] : null
+  if (!image?.mimetype || !image?.data) return ''
 
-  return `data:${images[0].mimetype};base64,${images[0].data}`;
+  return `data:${image.mimetype};base64,${image.data}`
 }

Also applies to: 101-105

🤖 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 38 - 43,
The template rendering accesses templateItem.images[0].mimetype directly which
throws when images is an empty array; update getTemplateImage(templateItem) (and
any other places around the same component block referenced at lines 101-105) to
guard against missing/empty images by first checking templateItem.images &&
templateItem.images.length > 0 (or using optional chaining) before accessing
images[0], and return a safe fallback (e.g., a placeholder image URL or null)
when no preview image exists so the library render won't break.

Comment on lines +177 to +189
const populateTextFromContent = () => {
if (!form.value.content) {
form.value.text = ''
return
}

form.value.text = form.value.content
.replace(/<style[\s\S]*?<\/style>/gi, ' ')
.replace(/<script[\s\S]*?<\/script>/gi, ' ')
.replace(/<[^>]+>/g, ' ')
.replace(/\s+/g, ' ')
.trim()
}
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 | 🟡 Minor

Decode HTML entities when generating the text version.

The current regex removes tags but leaves entities like &amp; and &nbsp; in the plain-text email.

Proposed fix
 const populateTextFromContent = () => {
   if (!form.value.content) {
     form.value.text = ''
     return
   }
 
-  form.value.text = form.value.content
-    .replace(/<style[\s\S]*?<\/style>/gi, ' ')
-    .replace(/<script[\s\S]*?<\/script>/gi, ' ')
-    .replace(/<[^>]+>/g, ' ')
+  const parser = new DOMParser()
+  const doc = parser.parseFromString(form.value.content, 'text/html')
+  doc.querySelectorAll('script, style').forEach((element) => element.remove())
+
+  form.value.text = (doc.body.textContent || '')
     .replace(/\s+/g, ' ')
     .trim()
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const populateTextFromContent = () => {
if (!form.value.content) {
form.value.text = ''
return
}
form.value.text = form.value.content
.replace(/<style[\s\S]*?<\/style>/gi, ' ')
.replace(/<script[\s\S]*?<\/script>/gi, ' ')
.replace(/<[^>]+>/g, ' ')
.replace(/\s+/g, ' ')
.trim()
}
const populateTextFromContent = () => {
if (!form.value.content) {
form.value.text = ''
return
}
const parser = new DOMParser()
const doc = parser.parseFromString(form.value.content, 'text/html')
doc.querySelectorAll('script, style').forEach((element) => element.remove())
form.value.text = (doc.body.textContent || '')
.replace(/\s+/g, ' ')
.trim()
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@assets/vue/views/TemplateEditView.vue` around lines 177 - 189, The plain-text
generator in populateTextFromContent currently strips tags but leaves HTML
entities (e.g., &amp;, &nbsp;) intact; after the existing tag/style/script
stripping and trimming (in populateTextFromContent operating on
form.value.content and setting form.value.text) decode HTML entities by using
the browser DOM decode approach (e.g., create a temporary element, set its
innerHTML to the intermediate string and read .textContent) or an equivalent
HTML-entity decoder, then assign the decoded result to form.value.text so
entities are converted to their correct characters.

Comment on lines +15 to +22
#[Route('/', name: 'templates', methods: ['GET'])]
public function index(Request $request): Response
{
return $this->render('@PhpListFrontend/spa.html.twig', [
'page' => 'Campaigns',
'api_token' => $request->getSession()->get('auth_token'),
'api_base_url' => $this->getParameter('api_base_url'),
]);
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 | 🟡 Minor

Use the templates page label here.

Line 19 still passes Campaigns, so /templates renders the wrong SPA title.

Proposed fix
-            'page' => 'Campaigns',
+            'page' => 'Templates',
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Controller/TemplatesController.php` around lines 15 - 22, In the
index(Request $request) action (the controller method named index for the
#[Route('/', name: 'templates', ...)]), change the hard-coded page label passed
to the template: replace the 'page' => 'Campaigns' entry with the correct
templates page label (e.g. 'Templates' or the appropriate i18n key) so the
/templates route renders the correct SPA title.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants