Skip to content
Open
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
4 changes: 4 additions & 0 deletions assets/router/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ 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'

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: '/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: '/' },
],
Expand Down
16 changes: 15 additions & 1 deletion assets/vue/api.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import {Client, ListClient, SubscribersClient, SubscriptionClient, SubscriberAttributesClient} from '@tatevikgr/rest-api-client';
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;
Expand All @@ -16,7 +26,11 @@ if (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 default client;
113 changes: 113 additions & 0 deletions assets/vue/components/base/CkEditorField.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<template>
<div class="editor-field" :style="{ '--editor-min-height': `300px` }">
<label
v-if="label"
:for="fieldId"
class="mb-1 block text-sm font-medium text-slate-700"
>
{{ label }}
</label>

<ckeditor
:id="fieldId"
v-model="localValue"
:editor="ClassicEditor"
:config="editorConfig"
/>
</div>
</template>

<script setup>
import { computed } from 'vue'
import { Ckeditor } from '@ckeditor/ckeditor5-vue'

import {
ClassicEditor,
Essentials,
Paragraph,
Bold,
Italic,
Heading,
Link,
List,
BlockQuote,
Table,
TableToolbar,
HorizontalLine
} from 'ckeditor5'

import 'ckeditor5/ckeditor5.css'

defineOptions({
components: {
ckeditor: Ckeditor
}
})

const props = defineProps({
modelValue: {
type: String,
default: ''
},
label: {
type: String,
default: ''
},
id: {
type: String,
default: ''
}
})

const emit = defineEmits(['update:modelValue'])

const localValue = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})

const fieldId = props.id || `ckeditor-${crypto.randomUUID()}`
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

🧩 Analysis chain

🌐 Web query:

For Vue 3.5.x, is useId()the recommended SSR-safe way to generate stable form control IDs, and what are the browser/security-context limitations ofcrypto.randomUUID()?

💡 Result:

Yes, for Vue 3.5.x, useId is the recommended SSR-safe way to generate stable form control IDs. It produces unique-per-app IDs (e.g., "v-1-2-3") that remain consistent between server and client renders, preventing hydration mismatches. This is explicitly stated in the official Vue documentation and Vue 3.5 announcement. crypto.randomUUID has these browser/security-context limitations: - Requires a secure context (HTTPS or localhost); unavailable over plain HTTP (non-localhost), causing errors like "crypto.randomUUID is not a function". - Broad browser support since 2022 (Chrome 92+, Firefox 95+, Safari 15.4+, etc.), but the secure context requirement limits it in development/non-HTTPS environments. - Not SSR-safe, as it generates different random values on server vs. client.

Citations:


🏁 Script executed:

cd /tmp && find . -type f -name "CkEditorField.vue" 2>/dev/null | head -20

Repository: phpList/web-frontend

Length of output: 46


🏁 Script executed:

# Search for the file in common Vue project locations
find . -path "*/assets/vue/components/base/CkEditorField.vue" -o -path "*/src/components/CkEditorField.vue" 2>/dev/null | head -10

Repository: phpList/web-frontend

Length of output: 110


🏁 Script executed:

# Broader search
fd -t f "CkEditorField" 2>/dev/null

Repository: phpList/web-frontend

Length of output: 108


🏁 Script executed:

cat -n assets/vue/components/base/CkEditorField.vue | head -100

Repository: phpList/web-frontend

Length of output: 2354


🏁 Script executed:

cat package.json | grep -A 2 '"vue"'

Repository: phpList/web-frontend

Length of output: 141


Use useId() instead of crypto.randomUUID() for SSR-safe ID generation.

Line 69 uses crypto.randomUUID(), which fails in non-HTTPS environments and produces different values on server vs. client (hydration mismatch). Additionally, fieldId is a constant, so it won't update if props.id changes. Vue 3.5.x provides useId() specifically for this—it generates stable, consistent IDs across renders.

Suggested fix
- import { computed } from 'vue'
+ import { computed, useId } from 'vue'
...
- const fieldId = props.id || `ckeditor-${crypto.randomUUID()}`
+ const generatedId = useId()
+ const fieldId = computed(() => props.id || `ckeditor-${generatedId}`)
📝 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 fieldId = props.id || `ckeditor-${crypto.randomUUID()}`
import { computed, useId } from 'vue'
// ... other code ...
const generatedId = useId()
const fieldId = computed(() => props.id || `ckeditor-${generatedId}`)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@assets/vue/components/base/CkEditorField.vue` at line 69, Replace the
non-SSR-safe crypto.randomUUID() usage: import and call Vue's useId() to
generate a stable id and make fieldId reactive so it respects changes to
props.id; specifically, create a local id via useId() (e.g., const generatedId =
useId()) and expose fieldId as a computed value like computed(() => props.id ||
generatedId) instead of the current const fieldId = props.id ||
`ckeditor-${crypto.randomUUID()}` so IDs are stable across server/client and
update when props.id changes.


const editorConfig = {
licenseKey: 'GPL',
plugins: [
Essentials,
Paragraph,
Bold,
Italic,
Heading,
Link,
List,
BlockQuote,
Table,
TableToolbar,
HorizontalLine
],
toolbar: [
'undo',
'redo',
'|',
'heading',
'|',
'bold',
'italic',
'link',
'|',
'bulletedList',
'numberedList',
'|',
'blockQuote',
'insertTable',
'|',
'horizontalLine'
]
}

</script>

<style scoped>
:deep(.ck-editor__editable_inline) {
min-height: var(--editor-min-height) !important;
overflow-y: auto;
}
</style>
Loading
Loading