From 0e57640d120ae4ae61b77eac149810b60c264805 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Wed, 29 Jul 2026 21:34:37 +0400 Subject: [PATCH 1/8] Refactor: env (#91) * Refactor: env * fix test * app.api_base_url * Remove api_base_url from controller render methods and add it as a global variable in twig configuration * Remove api_token from controller render methods and update spa.html.twig to use app.session for auth_token * Update API base URL configuration in templates and JavaScript --------- Co-authored-by: Tatevik --- assets/vue/api.js | 3 ++- composer.json | 3 ++- config/packages/twig.yaml | 2 ++ config/services.yml | 6 +----- phpunit.xml.dist | 4 ++-- src/Controller/AnalyticsController.php | 2 -- src/Controller/BouncesController.php | 2 -- src/Controller/CampaignsController.php | 6 ------ src/Controller/DashboardController.php | 2 -- src/Controller/ListsController.php | 4 ---- src/Controller/PublicPagesController.php | 6 ------ src/Controller/SettingsController.php | 2 -- src/Controller/SubscribersController.php | 2 -- src/Controller/TemplatesController.php | 6 ------ templates/base.html.twig | 1 + templates/spa.html.twig | 3 +-- .../Controller/AnalyticsControllerTest.php | 4 +++- .../Integration/Controller/BouncesControllerTest.php | 4 +++- .../Controller/CampaignsControllerTest.php | 2 ++ .../Controller/DashboardControllerTest.php | 4 +++- tests/Integration/Controller/ListsControllerTest.php | 4 +++- tests/bootstrap.php | 11 +++++++++++ 22 files changed, 36 insertions(+), 47 deletions(-) create mode 100644 tests/bootstrap.php diff --git a/assets/vue/api.js b/assets/vue/api.js index be71e85..3ac00be 100644 --- a/assets/vue/api.js +++ b/assets/vue/api.js @@ -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.'); diff --git a/composer.json b/composer.json index f46ae4e..38bab7a 100755 --- a/composer.json +++ b/composer.json @@ -51,7 +51,7 @@ }, "require": { "php": "^8.1", - "phplist/core": "dev-main", + "phplist/core": "dev-env", "symfony/twig-bundle": "^6.4", "symfony/webpack-encore-bundle": "^2.2", "symfony/security-bundle": "^6.4", @@ -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": [ diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml index 0e4fd73..070f0e2 100644 --- a/config/packages/twig.yaml +++ b/config/packages/twig.yaml @@ -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%' diff --git a/config/services.yml b/config/services.yml index 76325ac..9be8e4c 100755 --- a/config/services.yml +++ b/config/services.yml @@ -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 @@ -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: diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 4e0d445..692cf4d 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -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" > - + diff --git a/src/Controller/AnalyticsController.php b/src/Controller/AnalyticsController.php index d8486ff..407f256 100644 --- a/src/Controller/AnalyticsController.php +++ b/src/Controller/AnalyticsController.php @@ -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'), ]); } } diff --git a/src/Controller/BouncesController.php b/src/Controller/BouncesController.php index 14fb3f2..126db67 100644 --- a/src/Controller/BouncesController.php +++ b/src/Controller/BouncesController.php @@ -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, ]); } diff --git a/src/Controller/CampaignsController.php b/src/Controller/CampaignsController.php index dbf9d6a..7d21668 100644 --- a/src/Controller/CampaignsController.php +++ b/src/Controller/CampaignsController.php @@ -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'), ]); } @@ -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'), ]); } @@ -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'), ]); } } diff --git a/src/Controller/DashboardController.php b/src/Controller/DashboardController.php index aea6970..edfb1ef 100755 --- a/src/Controller/DashboardController.php +++ b/src/Controller/DashboardController.php @@ -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, ]); diff --git a/src/Controller/ListsController.php b/src/Controller/ListsController.php index 94e63f7..3dfb1ba 100644 --- a/src/Controller/ListsController.php +++ b/src/Controller/ListsController.php @@ -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(); @@ -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'), ]); } } diff --git a/src/Controller/PublicPagesController.php b/src/Controller/PublicPagesController.php index 1a6d80c..06064d9 100644 --- a/src/Controller/PublicPagesController.php +++ b/src/Controller/PublicPagesController.php @@ -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'), ]); } @@ -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'), ]); } @@ -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'), ]); } } diff --git a/src/Controller/SettingsController.php b/src/Controller/SettingsController.php index 02d8684..a41bfc1 100644 --- a/src/Controller/SettingsController.php +++ b/src/Controller/SettingsController.php @@ -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'), ]); } } diff --git a/src/Controller/SubscribersController.php b/src/Controller/SubscribersController.php index 8d542ed..331da62 100644 --- a/src/Controller/SubscribersController.php +++ b/src/Controller/SubscribersController.php @@ -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'), ]); } diff --git a/src/Controller/TemplatesController.php b/src/Controller/TemplatesController.php index 5d0cb29..c9a1650 100644 --- a/src/Controller/TemplatesController.php +++ b/src/Controller/TemplatesController.php @@ -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'), ]); } @@ -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'), ]); } @@ -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'), ]); } } diff --git a/templates/base.html.twig b/templates/base.html.twig index 7274070..43f3a68 100755 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -4,6 +4,7 @@ {% block title %}phpList{% endblock %} + diff --git a/templates/spa.html.twig b/templates/spa.html.twig index a0fda5a..244a6d1 100644 --- a/templates/spa.html.twig +++ b/templates/spa.html.twig @@ -4,8 +4,7 @@ {% block body %}
diff --git a/tests/Integration/Controller/AnalyticsControllerTest.php b/tests/Integration/Controller/AnalyticsControllerTest.php index 5691010..8cea6ef 100644 --- a/tests/Integration/Controller/AnalyticsControllerTest.php +++ b/tests/Integration/Controller/AnalyticsControllerTest.php @@ -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; @@ -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(); diff --git a/tests/Integration/Controller/BouncesControllerTest.php b/tests/Integration/Controller/BouncesControllerTest.php index d832cc0..05d4eb7 100644 --- a/tests/Integration/Controller/BouncesControllerTest.php +++ b/tests/Integration/Controller/BouncesControllerTest.php @@ -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; @@ -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(); diff --git a/tests/Integration/Controller/CampaignsControllerTest.php b/tests/Integration/Controller/CampaignsControllerTest.php index d05fa00..8a66684 100644 --- a/tests/Integration/Controller/CampaignsControllerTest.php +++ b/tests/Integration/Controller/CampaignsControllerTest.php @@ -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; @@ -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); diff --git a/tests/Integration/Controller/DashboardControllerTest.php b/tests/Integration/Controller/DashboardControllerTest.php index a24a786..872ef5a 100644 --- a/tests/Integration/Controller/DashboardControllerTest.php +++ b/tests/Integration/Controller/DashboardControllerTest.php @@ -11,6 +11,7 @@ use PhpList\WebFrontend\Controller\DashboardController; 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; @@ -29,7 +30,7 @@ public function testDashboardRouteIsRegistered(): void public function testDashboardRendersSpaPayloadWithStats(): void { self::bootKernel(); - $apiBaseUrl = (string) static::getContainer()->getParameter('api_base_url'); + $apiBaseUrl = (string) static::getContainer()->getParameter('app.api_base_url'); $statsClient = $this->createMock(StatisticsClient::class); $statsClient->expects(self::once()) @@ -43,6 +44,7 @@ public function testDashboardRendersSpaPayloadWithStats(): void $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(); diff --git a/tests/Integration/Controller/ListsControllerTest.php b/tests/Integration/Controller/ListsControllerTest.php index 801453c..72d997a 100644 --- a/tests/Integration/Controller/ListsControllerTest.php +++ b/tests/Integration/Controller/ListsControllerTest.php @@ -10,6 +10,7 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\HttpFoundation\JsonResponse; 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; @@ -31,7 +32,7 @@ public function testListRoutesAreRegistered(string $routeName, array $parameters public function testListsIndexRendersSpaForHtmlRequests(): void { self::bootKernel(); - $apiBaseUrl = (string) static::getContainer()->getParameter('api_base_url'); + $apiBaseUrl = (string) static::getContainer()->getParameter('app.api_base_url'); $listClient = $this->createMock(ListClient::class); $listClient->expects(self::never())->method('getLists'); @@ -106,6 +107,7 @@ private function createRequestWithSession(string $path): Request $session = new Session(new MockArraySessionStorage()); $session->set('auth_token', 'integration-token'); $request->setSession($session); + static::getContainer()->get(RequestStack::class)->push($request); return $request; } diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..8a148f7 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,11 @@ +bootEnv(dirname(__DIR__) . '/.env'); +} From 88c00fca125819b679dbcb79ae5006580d1e5dea Mon Sep 17 00:00:00 2001 From: Tatevik Date: Wed, 29 Jul 2026 21:43:08 +0400 Subject: [PATCH 2/8] use dev branch for core --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 38bab7a..21baad0 100755 --- a/composer.json +++ b/composer.json @@ -51,7 +51,7 @@ }, "require": { "php": "^8.1", - "phplist/core": "dev-env", + "phplist/core": "dev-dev", "symfony/twig-bundle": "^6.4", "symfony/webpack-encore-bundle": "^2.2", "symfony/security-bundle": "^6.4", From cbf56ab75e451aa92491f019dd7929626f449bb2 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Wed, 29 Jul 2026 22:24:18 +0400 Subject: [PATCH 3/8] feat: enhance template error handling with field-specific validation messages --- assets/vue/views/TemplateEditView.vue | 62 ++++++++++++++++++- .../vue/views/TemplateEditorView.spec.js | 21 +++++++ 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/assets/vue/views/TemplateEditView.vue b/assets/vue/views/TemplateEditView.vue index 2ccac85..1e11dee 100644 --- a/assets/vue/views/TemplateEditView.vue +++ b/assets/vue/views/TemplateEditView.vue @@ -121,7 +121,13 @@
-
+
+

Please fix the following fields:

+
    +
  • {{ errorItem }}
  • +
+
+
{{ saveError }}
@@ -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: '', @@ -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 { @@ -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 } diff --git a/tests/Unit/assets/vue/views/TemplateEditorView.spec.js b/tests/Unit/assets/vue/views/TemplateEditorView.spec.js index 4df6feb..444033c 100644 --- a/tests/Unit/assets/vue/views/TemplateEditorView.spec.js +++ b/tests/Unit/assets/vue/views/TemplateEditorView.spec.js @@ -97,6 +97,27 @@ describe('TemplateEditView', () => { ) }) + it('shows field-specific errors returned by the API', async () => { + vi.spyOn(api.templateClient, 'updateTemplate').mockRejectedValue({ + name: 'ValidationException', + message: 'Validation failed', + responseData: { + title: ['This value is too long.'], + list_order: ['This value should be a valid number.'], + }, + }) + + const wrapper = await mountComponent() + + await wrapper.find('#template-title').setValue('Updated Template') + await wrapper.find('form').trigger('submit.prevent') + await flushPromises() + + expect(wrapper.text()).toContain('Title: This value is too long.') + expect(wrapper.text()).toContain('List order: This value should be a valid number.') + expect(wrapper.text()).not.toContain('Validation failed') + }) + it('creates a template in create mode', async () => { mockRoute.name = 'template-create' mockRoute.params.templateId = '' From 20b89db06cbae9733f9e796360b7fb5bb2ec124d Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 4 Aug 2026 12:38:04 +0000 Subject: [PATCH 4/8] Update openapi.json from web frontend workflow 2026-08-04T12:38:04Z --- openapi.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openapi.json b/openapi.json index 11b8f56..27538ce 100644 --- a/openapi.json +++ b/openapi.json @@ -8225,8 +8225,7 @@ "sent", "prepared", "submitted", - "suspended", - "requeued" + "suspended" ], "example": "draft" } From 34972eb882716c3b3cc14324e881c33412aa827c Mon Sep 17 00:00:00 2001 From: Tatevik Date: Tue, 4 Aug 2026 16:54:16 +0400 Subject: [PATCH 5/8] feat: add cursor pointer to buttons for better user interaction --- .../campaigns/CampaignDirectory.vue | 26 +++++++++---------- .../campaigns/ViewCampaignModal.vue | 4 +-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/assets/vue/components/campaigns/CampaignDirectory.vue b/assets/vue/components/campaigns/CampaignDirectory.vue index af6c92c..7054764 100644 --- a/assets/vue/components/campaigns/CampaignDirectory.vue +++ b/assets/vue/components/campaigns/CampaignDirectory.vue @@ -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)" > @@ -100,7 +100,7 @@
-
+
-
+
Loading assets...
-
+
{{ error }}
- - - - - - - - + + + + + + + + - @@ -75,33 +75,33 @@ - - -
PreviewNameTypeSizeUpdated
PreviewNameTypeSizeUpdated
+ No assets found.
{{ extensionLabel(item.fileName) }}
-

{{ item.fileName }}

-

{{ item.url }}

+

{{ item.fileName }}

+

{{ item.url }}

+ {{ item.mimeType }} + {{ formatBytes(item.size) }} + {{ formatDate(item.modifiedAt) }} @@ -118,9 +118,9 @@
-
+
{{ filteredItems.length }} asset{{ filteredItems.length === 1 ? '' : 's' }} - Choose a row to insert it immediately. + Choose a row to insert it immediately.
diff --git a/assets/styles/app.css b/assets/styles/app.css index 614481c..ffcaa25 100644 --- a/assets/styles/app.css +++ b/assets/styles/app.css @@ -1,6 +1,8 @@ @import "tailwindcss"; @tailwind utilities; +@custom-variant dark (&:where(.dark, .dark *)); + @theme { --color-primary: #2563eb; --color-secondary: #6b7280; diff --git a/assets/vue/App.vue b/assets/vue/App.vue index dacfe52..8ce3613 100644 --- a/assets/vue/App.vue +++ b/assets/vue/App.vue @@ -1,5 +1,5 @@ diff --git a/assets/vue/components/base/BaseBadge.vue b/assets/vue/components/base/BaseBadge.vue index 217fd1b..53f2fec 100644 --- a/assets/vue/components/base/BaseBadge.vue +++ b/assets/vue/components/base/BaseBadge.vue @@ -19,10 +19,10 @@ const badgeClass = computed(() => { const base = 'inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium'; switch (props.variant) { case 'counter': - return `${base} bg-indigo-50 text-ext-wf3 border border-indigo-100`; + return `${base} bg-indigo-50 dark:bg-indigo-500/10 text-ext-wf3 dark:text-indigo-300 border border-indigo-100 dark:border-indigo-500/20`; case 'neutral': default: - return `${base} bg-gray-100 text-gray-800`; + return `${base} bg-gray-100 dark:bg-slate-700 text-gray-800 dark:text-slate-200`; } }) diff --git a/assets/vue/components/base/BaseButton.vue b/assets/vue/components/base/BaseButton.vue index aaeb2e8..10e5056 100644 --- a/assets/vue/components/base/BaseButton.vue +++ b/assets/vue/components/base/BaseButton.vue @@ -23,9 +23,9 @@ const buttonClass = computed(() => { const base = 'inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2'; switch (props.variant) { case 'secondary': - return `${base} text-gray-700 bg-white border-gray-300 hover:bg-gray-50 focus:ring-blue-500`; + return `${base} text-gray-700 dark:text-slate-200 bg-white dark:bg-slate-800 border-gray-300 dark:border-slate-600 hover:bg-gray-50 dark:hover:bg-slate-700 focus:ring-blue-500`; case 'ghost': - return 'inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-blue-600 bg-transparent hover:bg-blue-50 focus:outline-none'; + return 'inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-blue-600 dark:text-blue-400 bg-transparent hover:bg-blue-50 dark:hover:bg-blue-500/10 focus:outline-none'; case 'primary': default: return `${base} text-white bg-blue-600 hover:bg-blue-700 focus:ring-blue-500`; diff --git a/assets/vue/components/base/BaseCard.vue b/assets/vue/components/base/BaseCard.vue index 5a15a04..f44dc77 100644 --- a/assets/vue/components/base/BaseCard.vue +++ b/assets/vue/components/base/BaseCard.vue @@ -16,8 +16,8 @@ const props = defineProps({ }) const cardVariantMap = { - default: 'rounded-lg shadow-sm border border-gray-100 bg-white', - subtle: 'rounded-lg shadow-sm border-0 bg-gray-50', + default: 'rounded-lg shadow-sm border border-gray-100 dark:border-slate-700 bg-white dark:bg-slate-800', + subtle: 'rounded-lg shadow-sm border-0 bg-gray-50 dark:bg-slate-800', danger: 'rounded-lg shadow-sm border-0 bg-red-600 text-white', success: 'rounded-lg shadow-sm border-0 bg-green-600 text-white', } diff --git a/assets/vue/components/base/BaseIcon.vue b/assets/vue/components/base/BaseIcon.vue index 2955106..573d2f8 100644 --- a/assets/vue/components/base/BaseIcon.vue +++ b/assets/vue/components/base/BaseIcon.vue @@ -94,6 +94,10 @@ const icons = { pause: ``, + sun: ``, + + moon: ``, + start: ``, }; @@ -108,9 +112,9 @@ const wrapperClass = computed(() => { ]; if (props.active) { - classes.push("text-ext-wf3"); + classes.push("text-ext-wf3 dark:text-indigo-400"); } else { - classes.push("text-slate-400 group-hover:text-slate-600"); + classes.push("text-slate-400 group-hover:text-slate-600 dark:group-hover:text-slate-300"); } return classes.join(" "); diff --git a/assets/vue/components/base/BaseProgressBar.vue b/assets/vue/components/base/BaseProgressBar.vue index 91ac7a7..6deb956 100644 --- a/assets/vue/components/base/BaseProgressBar.vue +++ b/assets/vue/components/base/BaseProgressBar.vue @@ -1,6 +1,6 @@ diff --git a/assets/vue/components/bounces/BounceOverview.vue b/assets/vue/components/bounces/BounceOverview.vue index 02cf479..c368bf7 100644 --- a/assets/vue/components/bounces/BounceOverview.vue +++ b/assets/vue/components/bounces/BounceOverview.vue @@ -1,14 +1,14 @@