Skip to content
Open

Dev #92

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
3 changes: 2 additions & 1 deletion assets/vue/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ const redirectToLogin = () => {

const appElement = document.getElementById('vue-app');
const apiToken = appElement?.dataset.apiToken;
const apiBaseUrl = appElement?.dataset.apiBaseUrl;
const apiElement = document.getElementById('api-config');
const apiBaseUrl = apiElement?.dataset.apiBaseUrl;

if (!apiBaseUrl) {
console.error('API Base URL is not configured.');
Expand Down
62 changes: 60 additions & 2 deletions assets/vue/views/TemplateEditView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,13 @@
</div>
</div>

<div v-if="saveError" class="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
<div v-if="saveErrors.length" class="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
<p class="font-medium">Please fix the following fields:</p>
<ul class="mt-1 list-disc pl-5 space-y-1">
<li v-for="errorItem in saveErrors" :key="errorItem">{{ errorItem }}</li>
</ul>
</div>
<div v-else-if="saveError" class="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
{{ saveError }}
</div>
<div v-if="saveSuccess" class="rounded-lg border border-green-200 bg-green-50 px-4 py-3 text-sm text-green-700">
Expand Down Expand Up @@ -164,6 +170,7 @@ const isLoading = ref(false)
const loadError = ref('')
const isSaving = ref(false)
const saveError = ref('')
const saveErrors = ref([])
const saveSuccess = ref('')
const form = ref({
title: '',
Expand Down Expand Up @@ -240,19 +247,63 @@ const handleFileChange = (event) => {
form.value.file = file || null
}

const validationFieldLabels = {
title: 'Title',
content: 'Content',
text: 'Text version',
file: 'Template file',
list_order: 'List order',
check_links: 'Check links',
check_images: 'Check images',
check_external_images: 'Check external images'
}

const normalizeFieldName = (fieldPath = '') => {
if (validationFieldLabels[fieldPath]) return validationFieldLabels[fieldPath]

const fallback = String(fieldPath)
.split('.')
.pop()
?.replace(/\[\d+]/g, '')
?.replace(/_/g, ' ')
?.replace(/([a-z])([A-Z])/g, '$1 $2')
?.trim()

if (!fallback) return 'Field'
return fallback.charAt(0).toUpperCase() + fallback.slice(1)
}

const formatValidationErrors = (error) => {
const responseData = error?.responseData
const messages = []

if (responseData && typeof responseData === 'object' && !Array.isArray(responseData)) {
Object.entries(responseData).forEach(([field, rawMessage]) => {
if (!rawMessage) return
const text = Array.isArray(rawMessage) ? rawMessage.join(' ') : String(rawMessage)
messages.push(`${normalizeFieldName(field)}: ${text}`)
})
}

return [...new Set(messages)]
}

const saveTemplate = async () => {
if (!isCreateMode.value && (!Number.isFinite(templateId.value) || templateId.value <= 0)) {
saveError.value = 'Template ID is invalid.'
saveErrors.value = []
return
}

if (!form.value.title) {
saveError.value = 'Title is required.'
saveErrors.value = []
return
}

isSaving.value = true
saveError.value = ''
saveErrors.value = []
saveSuccess.value = ''

try {
Expand Down Expand Up @@ -292,7 +343,14 @@ const saveTemplate = async () => {
saveSuccess.value = 'Template updated successfully.'
} catch (error) {
console.error('Failed to save template:', error)
saveError.value = error?.message || 'Failed to save template.'
const formattedErrors = formatValidationErrors(error)
if (formattedErrors.length > 0) {
saveErrors.value = formattedErrors
saveError.value = ''
} else {
saveError.value = error?.message || 'Failed to save template.'
saveErrors.value = []
}
} finally {
isSaving.value = false
}
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
},
"require": {
"php": "^8.1",
"phplist/core": "dev-main",
"phplist/core": "dev-dev",
"symfony/twig-bundle": "^6.4",
"symfony/webpack-encore-bundle": "^2.2",
"symfony/security-bundle": "^6.4",
Expand Down Expand Up @@ -96,6 +96,7 @@
"PhpList\\Core\\Composer\\ScriptHandler::createBundleConfiguration",
"PhpList\\Core\\Composer\\ScriptHandler::createRoutesConfiguration",
"PhpList\\Core\\Composer\\ScriptHandler::createParametersConfiguration",
"PhpList\\Core\\Composer\\ScriptHandler::createDotenvConfiguration",
"PhpList\\Core\\Composer\\ScriptHandler::clearAllCaches"
],
"publish-phplist-texts": [
Expand Down
2 changes: 2 additions & 0 deletions config/packages/twig.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ twig:
strict_variables: '%kernel.debug%'
default_path: '%kernel.application_dir%/templates'
auto_reload: true
globals:
api_base_url: '%app.api_base_url%'
6 changes: 1 addition & 5 deletions config/services.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
# config/services.yaml
parameters:
api_base_url: '%env(API_BASE_URL)%'
env(API_BASE_URL): 'http://api.phplist.local/'

services:
_defaults:
autowire: true
Expand All @@ -27,7 +23,7 @@ services:
Symfony\Component\HttpFoundation\Session\SessionInterface: '@session'

PhpList\RestApiClient\Client:
$baseUrl: '%api_base_url%'
$baseUrl: '%app.api_base_url%'

PhpList\WebFrontend\EventListener\ApiSessionListener:
tags:
Expand Down
4 changes: 2 additions & 2 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/6.2/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="vendor/autoload.php"
bootstrap="tests/bootstrap.php"
>
<php>
<env name="API_BASE_URL" value="http://api.phplist.local/"/>
<ini name="error_reporting" value="-1"/>
<server name="KERNEL_CLASS" value="PhpList\Core\Core\ApplicationKernel"/>
<server name="APP_ENV" value="test" force="true"/>
</php>
</phpunit>
2 changes: 0 additions & 2 deletions src/Controller/AnalyticsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ public function index(Request $request): Response
{
return $this->render('@PhpListFrontend/spa.html.twig', [
'page' => 'Analytics',
'api_token' => $request->getSession()->get('auth_token'),
'api_base_url' => $this->getParameter('api_base_url'),
]);
}
}
2 changes: 0 additions & 2 deletions src/Controller/BouncesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ public function index(Request $request): Response

return $this->render('@PhpListFrontend/spa.html.twig', [
'page' => 'Bounces',
'api_token' => $request->getSession()->get('auth_token'),
'api_base_url' => $this->getParameter('api_base_url'),
'bounce_actions' => $bounceActions,
]);
}
Expand Down
6 changes: 0 additions & 6 deletions src/Controller/CampaignsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ 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'),
]);
}

Expand All @@ -27,8 +25,6 @@ public function create(Request $request): Response
{
return $this->render('@PhpListFrontend/spa.html.twig', [
'page' => 'Create Campaign',
'api_token' => $request->getSession()->get('auth_token'),
'api_base_url' => $this->getParameter('api_base_url'),
]);
}

Expand All @@ -37,8 +33,6 @@ public function edit(Request $request, int $campaignId): Response
{
return $this->render('@PhpListFrontend/spa.html.twig', [
'page' => sprintf('Edit Campaign #%d', $campaignId),
'api_token' => $request->getSession()->get('auth_token'),
'api_base_url' => $this->getParameter('api_base_url'),
]);
}
}
2 changes: 0 additions & 2 deletions src/Controller/DashboardController.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ public function index(Request $request): Response

return $this->render('@PhpListFrontend/spa.html.twig', [
'page' => 'Dashboard',
'api_token' => $request->getSession()->get('auth_token'),
'api_base_url' => $this->getParameter('api_base_url'),
'dashboard_stats' => $dashboardStats,
'dashboard_error' => $dashboardError,
]);
Expand Down
4 changes: 0 additions & 4 deletions src/Controller/ListsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ public function index(Request $request): JsonResponse|Response
if (! $wantsJson) {
return $this->render('@PhpListFrontend/spa.html.twig', [
'page' => 'Lists',
'api_token' => $request->getSession()->get('auth_token'),
'api_base_url' => $this->getParameter('api_base_url'),
]);
}
$initialData = $this->listClient->getLists();
Expand All @@ -40,8 +38,6 @@ public function view(Request $request, int $listId): JsonResponse|Response
{
return $this->render('@PhpListFrontend/spa.html.twig', [
'page' => 'List Subscribers',
'api_token' => $request->getSession()->get('auth_token'),
'api_base_url' => $this->getParameter('api_base_url'),
]);
}
}
6 changes: 0 additions & 6 deletions src/Controller/PublicPagesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ public function index(Request $request): Response
{
return $this->render('@PhpListFrontend/spa.html.twig', [
'page' => 'Subscribe Pages',
'api_token' => $request->getSession()->get('auth_token'),
'api_base_url' => $this->getParameter('api_base_url'),
]);
}

Expand All @@ -27,8 +25,6 @@ public function create(Request $request): Response
{
return $this->render('@PhpListFrontend/spa.html.twig', [
'page' => 'Create Subscribe Page',
'api_token' => $request->getSession()->get('auth_token'),
'api_base_url' => $this->getParameter('api_base_url'),
]);
}

Expand All @@ -37,8 +33,6 @@ public function edit(Request $request, int $pageId): Response
{
return $this->render('@PhpListFrontend/spa.html.twig', [
'page' => sprintf('Edit Subscribe Page #%d', $pageId),
'api_token' => $request->getSession()->get('auth_token'),
'api_base_url' => $this->getParameter('api_base_url'),
]);
}
}
2 changes: 0 additions & 2 deletions src/Controller/SettingsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ public function index(Request $request): Response
{
return $this->render('@PhpListFrontend/spa.html.twig', [
'page' => 'Settings',
'api_token' => $request->getSession()->get('auth_token'),
'api_base_url' => $this->getParameter('api_base_url'),
]);
}
}
2 changes: 0 additions & 2 deletions src/Controller/SubscribersController.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@ public function index(Request $request): JsonResponse|Response
if (! $wantsJson) {
return $this->render('@PhpListFrontend/spa.html.twig', [
'page' => 'Subscribers',
'api_token' => $request->getSession()->get('auth_token'),
'api_base_url' => $this->getParameter('api_base_url'),
]);
}

Expand Down
6 changes: 0 additions & 6 deletions src/Controller/TemplatesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ public function index(Request $request): Response
{
return $this->render('@PhpListFrontend/spa.html.twig', [
'page' => 'Templates',
'api_token' => $request->getSession()->get('auth_token'),
'api_base_url' => $this->getParameter('api_base_url'),
]);
}

Expand All @@ -27,8 +25,6 @@ public function create(Request $request): Response
{
return $this->render('@PhpListFrontend/spa.html.twig', [
'page' => 'Create Template',
'api_token' => $request->getSession()->get('auth_token'),
'api_base_url' => $this->getParameter('api_base_url'),
]);
}

Expand All @@ -37,8 +33,6 @@ public function edit(Request $request, int $templateId): Response
{
return $this->render('@PhpListFrontend/spa.html.twig', [
'page' => sprintf('Edit Template #%d', $templateId),
'api_token' => $request->getSession()->get('auth_token'),
'api_base_url' => $this->getParameter('api_base_url'),
]);
}
}
1 change: 1 addition & 0 deletions templates/base.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<meta charset="UTF-8">
<title>{% block title %}phpList{% endblock %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta id="api-config" data-api-base-url="{{ api_base_url }}">
<link rel="stylesheet" href="{{ asset('build/app.css', 'phplist_web_frontend') }}">
<link rel="stylesheet" href="{{ asset('build/styles.css', 'phplist_web_frontend') }}">

Expand Down
3 changes: 1 addition & 2 deletions templates/spa.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@

{% block body %}
<div id="vue-app"
data-api-token="{{ api_token|default('') }}"
data-api-base-url="{{ api_base_url|default('') }}"
data-api-token="{{ app.session.get('auth_token')|default('') }}"
data-dashboard-stats="{{ dashboard_stats|default({})|json_encode|e('html_attr') }}"
data-dashboard-error="{{ dashboard_error|default('')|e('html_attr') }}"
data-bounce-actions="{{ bounce_actions|default({})|json_encode|e('html_attr') }}">
Expand Down
4 changes: 3 additions & 1 deletion tests/Integration/Controller/AnalyticsControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use PhpList\WebFrontend\Controller\AnalyticsController;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
use Symfony\Component\Routing\RouterInterface;
Expand All @@ -27,12 +28,13 @@ public function testAnalyticsPageRendersExpectedSpaPayload(): void
self::bootKernel();
/** @var AnalyticsController $controller */
$controller = static::getContainer()->get(AnalyticsController::class);
$apiBaseUrl = (string) static::getContainer()->getParameter('api_base_url');
$apiBaseUrl = (string) static::getContainer()->getParameter('app.api_base_url');

$request = Request::create('/analytics/');
$session = new Session(new MockArraySessionStorage());
$session->set('auth_token', 'integration-token');
$request->setSession($session);
static::getContainer()->get(RequestStack::class)->push($request);

$response = $controller->index($request);
$content = (string) $response->getContent();
Expand Down
4 changes: 3 additions & 1 deletion tests/Integration/Controller/BouncesControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use PhpList\WebFrontend\Controller\BouncesController;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
use Symfony\Component\Routing\RouterInterface;
Expand All @@ -27,12 +28,13 @@ public function testBouncesPageRendersExpectedSpaPayload(): void
self::bootKernel();
/** @var BouncesController $controller */
$controller = static::getContainer()->get(BouncesController::class);
$apiBaseUrl = (string) static::getContainer()->getParameter('api_base_url');
$apiBaseUrl = (string) static::getContainer()->getParameter('app.api_base_url');

$request = Request::create('/bounces/');
$session = new Session(new MockArraySessionStorage());
$session->set('auth_token', 'integration-token');
$request->setSession($session);
static::getContainer()->get(RequestStack::class)->push($request);

$response = $controller->index($request);
$content = (string) $response->getContent();
Expand Down
2 changes: 2 additions & 0 deletions tests/Integration/Controller/CampaignsControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use PhpList\WebFrontend\Controller\CampaignsController;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
use Symfony\Component\Routing\RouterInterface;
Expand Down Expand Up @@ -41,6 +42,7 @@ public function testCampaignPagesRenderExpectedSpaPayload(
$session = new Session(new MockArraySessionStorage());
$session->set('auth_token', 'integration-token');
$request->setSession($session);
static::getContainer()->get(RequestStack::class)->push($request);

$response = $controller->{$action}($request, ...$actionArguments);

Expand Down
Loading
Loading