-
Notifications
You must be signed in to change notification settings - Fork 6
Dev #73
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Dev #73
Changes from all commits
cad7bcf
c8ae45e
1a9c129
21d04aa
722c663
9e0378b
fea446d
718c53a
83ba923
ea253fd
9cf6818
1adf9cd
1f9eb8a
b710a31
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| <VirtualHost *:80> | ||
| ServerName frontend.phplist.local | ||
| ServerAdmin webmaster@localhost | ||
|
|
||
| DocumentRoot /{pathToTheProject}/web-frontend/public | ||
|
|
||
| ErrorLog ${APACHE_LOG_DIR}/web-frontend-error.log | ||
| CustomLog ${APACHE_LOG_DIR}/web-frontend-access.log combined | ||
|
|
||
| <Directory /{pathToTheProject}/web-frontend/public> | ||
| Options FollowSymLinks | ||
| AllowOverride All | ||
| Require all granted | ||
| DirectoryIndex app.php | ||
| </Directory> | ||
|
|
||
| <FilesMatch "^\."> | ||
| Require all denied | ||
| </FilesMatch> | ||
|
|
||
| <IfModule proxy_fcgi_module> | ||
| <FilesMatch \.php$> | ||
| SetHandler "proxy:unix:/run/php/php8.1-fpm.sock|fcgi://localhost" | ||
| </FilesMatch> | ||
| </IfModule> | ||
| </VirtualHost> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,13 @@ | ||
| import './styles/app.css'; | ||
| import { createApp } from 'vue'; | ||
| import App from './vue/App.vue'; | ||
| import { router } from './router'; | ||
|
|
||
| // Mount the main app if the element exists | ||
| const appElement = document.getElementById('vue-app'); | ||
|
|
||
| if (appElement) { | ||
| createApp(App).mount('#vue-app'); | ||
| const app = createApp(App); | ||
| app.use(router); | ||
| app.mount(appElement); | ||
| } | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import { createRouter, createWebHistory } from 'vue-router'; | ||
| import DashboardView from '../vue/views/DashboardView.vue' | ||
| import SubscribersView from '../vue/views/SubscribersView.vue' | ||
| 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(), | ||
| routes: [ | ||
| { path: '/', name: 'dashboard', component: DashboardView, meta: { title: 'Dashboard' } }, | ||
| { 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/create', name: 'template-create', component: TemplateEditView, meta: { title: 'Create Template' } }, | ||
| { 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' } }, | ||
| { path: '/:pathMatch(.*)*', redirect: '/' }, | ||
| ], | ||
| }); | ||
|
|
||
| router.afterEach((to) => { | ||
| const defaultTitle = 'phpList'; | ||
| const pageTitle = to.meta.title; | ||
| document.title = pageTitle ? `${defaultTitle} - ${pageTitle}` : defaultTitle; | ||
| }); | ||
|
Comment on lines
+22
to
+32
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Description: Confirm campaign edit title mismatch between backend title and router meta title.
rg -nP "Edit Campaign|to\.meta\.title|document\.title|campaignId" -C3 assets src testsRepository: phpList/web-frontend Length of output: 23299 Make the campaign ID visible in the edit page title. The backend renders The proposed fix—making Proposed fix- { path: '/campaigns/:campaignId/edit', name: 'campaign-edit', component: CampaignEditView, meta: { title: 'Edit Campaign' } },
+ {
+ path: '/campaigns/:campaignId/edit',
+ name: 'campaign-edit',
+ component: CampaignEditView,
+ meta: { title: (route) => `Edit Campaign #${route.params.campaignId}` },
+ },
@@
router.afterEach((to) => {
const defaultTitle = 'phpList';
- const pageTitle = to.meta.title;
+ const pageTitle = typeof to.meta.title === 'function' ? to.meta.title(to) : to.meta.title;
document.title = pageTitle ? `${defaultTitle} - ${pageTitle}` : defaultTitle;
});🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,16 @@ | ||||||||
| @import "tailwindcss"; | ||||||||
| @tailwind utilities; | ||||||||
|
Comment on lines
+1
to
+2
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: To configure Stylelint to ignore Tailwind CSS v4 at-rules like Citations:
🌐 Web query:
💡 Result: Yes, using Citations:
🌐 Web query:
💡 Result: No, using Citations:
Remove In Tailwind CSS v4, 🧹 Proposed fix `@import` "tailwindcss";
-@tailwind utilities;For the Stylelint false positives, the web search confirms you can suppress them by updating your Stylelint config to recognize Tailwind v4's at-rules. Add an 📝 Committable suggestion
Suggested change
🧰 Tools🪛 Stylelint (17.4.0)[error] 2-2: Unexpected unknown at-rule "@tailwind" (scss/at-rule-no-unknown) (scss/at-rule-no-unknown) 🤖 Prompt for AI Agents |
||||||||
|
|
||||||||
| @theme { | ||||||||
| --color-primary: #2563eb; | ||||||||
| --color-secondary: #6b7280; | ||||||||
| --color-success: #16a34a; | ||||||||
| --color-danger: #dc2626; | ||||||||
| --color-info: #0891b2; | ||||||||
| --color-ext-wf1: #543ff6; /** indigo-500 **/ | ||||||||
| --color-ext-wf2: #eef2ff; /** indigo-50 **/ | ||||||||
| --color-ext-wf3: #303F9F; /** indigo-700 **/ | ||||||||
| } | ||||||||
|
|
||||||||
| @source "../../templates/**/*.twig"; | ||||||||
| @source "../**/*.vue"; | ||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,26 +1,13 @@ | ||
| <template> | ||
| <div> | ||
| <h2>Hello from Vue</h2> | ||
| <p>{{ message }}</p> | ||
| <div class="min-h-screen bg-[#F8FAFC] flex text-slate-900 font-sans"> | ||
| <AppSidebar /> | ||
|
|
||
| <div class="flex flex-col flex-1 min-h-screen"> | ||
| <RouterView /> | ||
| </div> | ||
| </div> | ||
| </template> | ||
|
|
||
| <script> | ||
| export default { | ||
| name: 'App', | ||
| data() { | ||
| return { | ||
| message: 'This is a reusable component!' | ||
| } | ||
| }, | ||
| created() { | ||
| console.log('App component created'); | ||
| }, | ||
| mounted() { | ||
| console.log('App component mounted'); | ||
| }, | ||
| updated() { | ||
| console.log('App component updated'); | ||
| } | ||
| } | ||
| <script setup> | ||
| import AppSidebar from './components/sidebar/AppSidebar.vue' | ||
| </script> |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,58 @@ | ||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||
| CampaignClient, | ||||||||||||||||||||||||||||||||||||||||
| Client, | ||||||||||||||||||||||||||||||||||||||||
| ListMessagesClient, | ||||||||||||||||||||||||||||||||||||||||
| ListClient, | ||||||||||||||||||||||||||||||||||||||||
| StatisticsClient, | ||||||||||||||||||||||||||||||||||||||||
| SubscribersClient, | ||||||||||||||||||||||||||||||||||||||||
| SubscriptionClient, | ||||||||||||||||||||||||||||||||||||||||
| SubscriberAttributesClient, | ||||||||||||||||||||||||||||||||||||||||
| TemplatesClient | ||||||||||||||||||||||||||||||||||||||||
| } from '@tatevikgr/rest-api-client'; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const appElement = document.getElementById('vue-app'); | ||||||||||||||||||||||||||||||||||||||||
| const apiToken = appElement?.dataset.apiToken; | ||||||||||||||||||||||||||||||||||||||||
| const apiBaseUrl = appElement?.dataset.apiBaseUrl; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (!apiBaseUrl) { | ||||||||||||||||||||||||||||||||||||||||
| console.error('API Base URL is not configured.'); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const client = new Client(apiBaseUrl || ''); | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+13
to
+21
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Proceeding with empty URL after error log can cause confusing failures. When Consider throwing or returning early so the app doesn't limp along in a broken state: 🛠️ Suggested fix if (!apiBaseUrl) {
console.error('API Base URL is not configured.');
+ throw new Error('API Base URL is required for the application to function.');
}
-const client = new Client(apiBaseUrl || '');
+const client = new Client(apiBaseUrl);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (apiToken) { | ||||||||||||||||||||||||||||||||||||||||
| client.setSessionId(apiToken); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| export const subscribersClient = new SubscribersClient(client); | ||||||||||||||||||||||||||||||||||||||||
| export const listClient = new ListClient(client); | ||||||||||||||||||||||||||||||||||||||||
| export const campaignClient = new CampaignClient(client); | ||||||||||||||||||||||||||||||||||||||||
| export const listMessagesClient = new ListMessagesClient(client); | ||||||||||||||||||||||||||||||||||||||||
| export const statisticsClient = new StatisticsClient(client); | ||||||||||||||||||||||||||||||||||||||||
| export const subscriptionClient = new SubscriptionClient(client); | ||||||||||||||||||||||||||||||||||||||||
| export const subscriberAttributesClient = new SubscriberAttributesClient(client); | ||||||||||||||||||||||||||||||||||||||||
| export const templateClient = new TemplatesClient(client); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| export const fetchAllLists = async ({ limit = 100, maxPages = 100 } = {}) => { | ||||||||||||||||||||||||||||||||||||||||
| const lists = []; | ||||||||||||||||||||||||||||||||||||||||
| let afterId = null; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| for (let pageIndex = 0; pageIndex < maxPages; pageIndex += 1) { | ||||||||||||||||||||||||||||||||||||||||
| const response = await listClient.getLists(afterId, limit); | ||||||||||||||||||||||||||||||||||||||||
| const items = Array.isArray(response?.items) ? response.items : []; | ||||||||||||||||||||||||||||||||||||||||
| lists.push(...items); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const hasMore = response?.pagination?.hasMore === true; | ||||||||||||||||||||||||||||||||||||||||
| const nextCursor = response?.pagination?.nextCursor; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (!hasMore || !Number.isFinite(nextCursor) || nextCursor === afterId) { | ||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| afterId = nextCursor; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+40
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Inspect the installed client's getLists signature and pagination shape.
fd -t f 'package.json' node_modules/@tatevikgr/rest-api-client 2>/dev/null | head -5
fd -e d.ts . node_modules/@tatevikgr/rest-api-client 2>/dev/null | xargs -I{} sh -c 'echo "== {} =="; cat "{}"' 2>/dev/null | rg -n -C3 'getLists|nextCursor|pagination'
# Fallback: search repo for any existing usage that hints at cursor type
rg -nP -C2 'nextCursor|getLists\s*\(' --type=js --type=ts --type=vueRepository: phpList/web-frontend Length of output: 91 🏁 Script executed: #!/bin/bash
# Search for nextCursor and getLists usage without problematic rg flags
rg -n -C3 'nextCursor|getLists' assets/ src/ --max-count=20
# Check if the client package is installed and look at its metadata
if [ -d "node_modules/@tatevikgr/rest-api-client" ]; then
echo "=== Client package found ==="
ls -la node_modules/@tatevikgr/rest-api-client/ 2>/dev/null | head -20
find node_modules/@tatevikgr/rest-api-client -name "*.d.ts" -o -name "index.js" | head -10
fi
# Look at the api.js file more broadly to understand initialization
echo "=== Full fetchAllLists context ==="
cat -n assets/vue/api.js | head -60Repository: phpList/web-frontend Length of output: 12780
The API client returns The rest of the codebase already uses the correct pattern (e.g., ♻️ Fix- if (!hasMore || !Number.isFinite(nextCursor) || nextCursor === afterId) {
+ if (!hasMore || nextCursor == null || nextCursor === afterId) {
break;
}Also consider logging when the loop exits due to 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| return lists; | ||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| export default client; | ||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| import { mount } from '@vue/test-utils' | ||
| import BaseBadge from './BaseBadge.vue' | ||
|
|
||
| describe('BaseBadge', () => { | ||
| it('renders neutral variant by default', () => { | ||
| const wrapper = mount(BaseBadge, { | ||
| slots: { | ||
| default: 'All', | ||
| }, | ||
| }) | ||
|
|
||
| const classes = wrapper.get('span').classes() | ||
| expect(wrapper.text()).toContain('All') | ||
| expect(classes).toContain('bg-gray-100') | ||
| expect(classes).toContain('text-gray-800') | ||
| }) | ||
|
|
||
| it('renders counter variant styles', () => { | ||
| const wrapper = mount(BaseBadge, { | ||
| props: { | ||
| variant: 'counter', | ||
| }, | ||
| slots: { | ||
| default: '10', | ||
| }, | ||
| }) | ||
|
|
||
| const classes = wrapper.get('span').classes() | ||
| expect(classes).toContain('bg-indigo-50') | ||
| expect(classes).toContain('text-ext-wf3') | ||
|
Comment on lines
+28
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Description: Verify whether `text-ext-wf3` is backed by Tailwind config and find all usages.
# Expected: Tailwind config defines an `ext-wf3` color token, or usages should be changed to a configured token.
set -euo pipefail
echo "== Tailwind config files =="
fd -i '^tailwind\.config\.(js|cjs|mjs|ts)$' -x sh -c '
printf "\n--- %s ---\n" "$1"
sed -n "1,240p" "$1"
' sh {}
echo
echo "== text-ext-wf3 / ext-wf3 usages =="
rg -n -C2 '\btext-ext-wf3\b|\bext-wf3\b|\bext_wf3\b' --glob '!node_modules/**' --glob '!vendor/**'Repository: phpList/web-frontend Length of output: 9653 Add The class 🤖 Prompt for AI Agents |
||
| expect(wrapper.text()).toContain('10') | ||
| }) | ||
| }) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make the
socatinstall non-interactive.sudo apt install socatcan prompt for confirmation in CI. Useapt-get install -yto keep the workflow deterministic.🔧 Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents