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
28 changes: 14 additions & 14 deletions assets/vue/components/campaigns/CampaignDirectory.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
v-for="option in filterOptions"
:key="option.id"
type="button"
class="flex-1 sm:flex-none px-3 py-1.5 text-xs font-semibold rounded-md transition-colors"
class="cursor-pointer flex-1 sm:flex-none px-3 py-1.5 text-xs font-semibold rounded-md transition-colors"
:class="statusFilter === option.id ? 'bg-white text-slate-900 shadow-sm' : 'text-slate-500 hover:text-slate-700'"
@click="setFilter(option.id)"
>
Expand Down Expand Up @@ -100,7 +100,7 @@
<button
v-else-if="campaign.statusKey === 'active'"
type="button"
class="inline-flex items-center gap-1 px-2.5 py-1.5 text-xs font-medium rounded-md border border-red-200 text-red-600 hover:bg-red-50 transition-colors disabled:opacity-50"
class="cursor-pointer inline-flex items-center gap-1 px-2.5 py-1.5 text-xs font-medium rounded-md border border-red-200 text-red-600 hover:bg-red-50 transition-colors disabled:opacity-50"
:disabled="isActionLoading(campaign.id)"
@click="handleSuspend(campaign.id)"
>
Expand All @@ -110,7 +110,7 @@
<button
v-else
type="button"
class="inline-flex items-center gap-1 px-2.5 py-1.5 text-xs font-medium rounded-md border border-amber-200 text-amber-600 hover:bg-amber-50 transition-colors disabled:opacity-50"
class="cursor-pointer inline-flex items-center gap-1 px-2.5 py-1.5 text-xs font-medium rounded-md border border-amber-200 text-amber-600 hover:bg-amber-50 transition-colors disabled:opacity-50"
:disabled="isActionLoading(campaign.id)"
@click="handleRequeue(campaign.id)"
>
Expand All @@ -120,7 +120,7 @@
<button
v-if="campaign.statusKey === 'sent'"
type="button"
class="inline-flex items-center gap-1 px-2.5 py-1.5 text-xs font-medium rounded-md border border-green-200 text-green-700 hover:bg-green-50 transition-colors"
class="cursor-pointer inline-flex items-center gap-1 px-2.5 py-1.5 text-xs font-medium rounded-md border border-green-200 text-green-700 hover:bg-green-50 transition-colors"
:disabled="isActionLoading(campaign.id)"
@click="handleCopyToDraft(campaign.id)"
>
Expand All @@ -130,7 +130,7 @@
<button
v-if="campaign.statusKey === 'draft'"
type="button"
class="inline-flex items-center gap-1 px-2.5 py-1.5 text-xs font-medium rounded-md border border-slate-300 text-slate-700 hover:bg-slate-50 transition-colors disabled:opacity-50"
class="cursor-pointer inline-flex items-center gap-1 px-2.5 py-1.5 text-xs font-medium rounded-md border border-slate-300 text-slate-700 hover:bg-slate-50 transition-colors disabled:opacity-50"
:disabled="isActionLoading(campaign.id)"
@click="handleEdit(campaign.id)"
>
Expand All @@ -139,7 +139,7 @@
</button>
<button
type="button"
class="inline-flex items-center gap-1 px-2.5 py-1.5 text-xs font-medium rounded-md border border-slate-300 text-slate-700 hover:bg-slate-50 transition-colors"
class="cursor-pointer inline-flex items-center gap-1 px-2.5 py-1.5 text-xs font-medium rounded-md border border-slate-300 text-slate-700 hover:bg-slate-50 transition-colors"
:disabled="isActionLoading(campaign.id)"
@click="handleView(campaign.id)"
>
Expand Down Expand Up @@ -233,7 +233,7 @@
<button
v-if="campaign.statusKey === 'draft'"
type="button"
class="inline-flex items-center gap-1 px-2.5 py-1.5 text-xs font-medium rounded-md border border-red-200 text-red-600 hover:bg-red-50 transition-colors disabled:opacity-50"
class="cursor-pointer inline-flex items-center gap-1 px-2.5 py-1.5 text-xs font-medium rounded-md border border-red-200 text-red-600 hover:bg-red-50 transition-colors disabled:opacity-50"
:disabled="isActionLoading(campaign.id)"
@click="handleDelete(campaign)"
>
Expand All @@ -243,7 +243,7 @@
<button
v-else-if="campaign.statusKey === 'active'"
type="button"
class="inline-flex items-center gap-1 px-2.5 py-1.5 text-xs font-medium rounded-md border border-red-200 text-red-600 hover:bg-red-50 transition-colors disabled:opacity-50"
class="cursor-pointer inline-flex items-center gap-1 px-2.5 py-1.5 text-xs font-medium rounded-md border border-red-200 text-red-600 hover:bg-red-50 transition-colors disabled:opacity-50"
:disabled="isActionLoading(campaign.id)"
@click="handleSuspend(campaign.id)"
>
Expand All @@ -253,7 +253,7 @@
<button
v-else
type="button"
class="inline-flex items-center gap-1 px-2.5 py-1.5 text-xs font-medium rounded-md border border-amber-200 text-amber-600 hover:bg-amber-50 transition-colors disabled:opacity-50"
class="cursor-pointer inline-flex items-center gap-1 px-2.5 py-1.5 text-xs font-medium rounded-md border border-amber-200 text-amber-600 hover:bg-amber-50 transition-colors disabled:opacity-50"
:disabled="isActionLoading(campaign.id)"
@click="handleRequeue(campaign.id)"
>
Expand All @@ -263,7 +263,7 @@
<button
v-if="campaign.statusKey === 'sent'"
type="button"
class="inline-flex items-center gap-1 px-2.5 py-1.5 text-xs font-medium rounded-md border border-green-200 text-green-700 hover:bg-green-50 transition-colors disabled:opacity-50"
class="cursor-pointer inline-flex items-center gap-1 px-2.5 py-1.5 text-xs font-medium rounded-md border border-green-200 text-green-700 hover:bg-green-50 transition-colors disabled:opacity-50"
:disabled="isActionLoading(campaign.id)"
@click="handleCopyToDraft(campaign.id)"
>
Expand All @@ -273,7 +273,7 @@
<button
v-if="campaign.statusKey === 'draft'"
type="button"
class="inline-flex items-center gap-1 px-2.5 py-1.5 text-xs font-medium rounded-md border border-slate-300 text-slate-700 hover:bg-slate-50 transition-colors disabled:opacity-50"
class="cursor-pointer inline-flex items-center gap-1 px-2.5 py-1.5 text-xs font-medium rounded-md border border-slate-300 text-slate-700 hover:bg-slate-50 transition-colors disabled:opacity-50"
:disabled="isActionLoading(campaign.id)"
@click="handleEdit(campaign.id)"
>
Expand All @@ -282,7 +282,7 @@
</button>
<button
type="button"
class="inline-flex items-center gap-1 px-2.5 py-1.5 text-xs font-medium rounded-md border border-slate-300 text-slate-700 hover:bg-slate-50 transition-colors"
class="cursor-pointer inline-flex items-center gap-1 px-2.5 py-1.5 text-xs font-medium rounded-md border border-slate-300 text-slate-700 hover:bg-slate-50 transition-colors"
:disabled="isActionLoading(campaign.id)"
@click="handleView(campaign.id)"
>
Expand Down Expand Up @@ -419,7 +419,7 @@ const statusClasses = {
unknown: 'bg-amber-100 text-amber-700'
}

const activeStatuses = new Set(['active', 'submitted', 'prepared', 'inprocess', 'requeued', 'scheduled'])
const activeStatuses = new Set(['active', 'submitted', 'prepared', 'inprocess', 'scheduled'])

const resolveStatusKey = (statusRaw) => {
if (statusRaw === 'sent') return 'sent'
Expand Down Expand Up @@ -509,7 +509,7 @@ const handleRequeue = async (campaignId) => {
setActionFeedback(campaignId, 'Requeueing campaign...')

try {
await campaignClient.updateCampaignStatus(campaignId, 'requeued')
await campaignClient.updateCampaignStatus(campaignId, 'submitted')
setActionFeedback(campaignId, 'Campaign requeued.', 'success')
await fetchCampaigns()
} catch (error) {
Expand Down
4 changes: 2 additions & 2 deletions assets/vue/components/campaigns/ViewCampaignModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<h3 class="text-lg font-semibold text-slate-900">Campaign details: {{ campaign?.id || '-' }}</h3>
<button
type="button"
class="text-sm text-slate-500 hover:text-slate-700"
class="cursor-pointer text-sm text-slate-500 hover:text-slate-700"
@click="emit('close')"
data-testid="close-btn"
>
Expand Down Expand Up @@ -105,7 +105,7 @@

<button
type="button"
class="inline-flex items-center justify-center rounded-md bg-slate-900 px-4 py-2 text-sm font-medium text-white hover:bg-slate-800 disabled:cursor-not-allowed disabled:opacity-50"
class="cursor-pointer inline-flex items-center justify-center rounded-md bg-slate-900 px-4 py-2 text-sm font-medium text-white hover:bg-slate-800 disabled:cursor-not-allowed disabled:opacity-50"
:disabled="selectedMailingListIds.length === 0 || isResending"
@click="handleResend"
>
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
3 changes: 1 addition & 2 deletions openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -8225,8 +8225,7 @@
"sent",
"prepared",
"submitted",
"suspended",
"requeued"
"suspended"
],
"example": "draft"
}
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'),
]);
}
}
Loading
Loading