From 9b2d4551b89f44a82d352392422a11f606273aa3 Mon Sep 17 00:00:00 2001 From: Chirag Chhatrala <60499540+chiragchhatrala@users.noreply.github.com> Date: Thu, 15 May 2025 19:54:11 +0530 Subject: [PATCH 01/11] Apply refactor changes for edit submission (#757) --- .../open/components/EditSubmissionModal.vue | 44 ++++++++++++++----- .../open/components/RecordOperations.vue | 2 +- .../composables/useFormInitialization.js | 22 +++------- 3 files changed, 41 insertions(+), 27 deletions(-) diff --git a/client/components/open/components/EditSubmissionModal.vue b/client/components/open/components/EditSubmissionModal.vue index 90b6308f..b339fdde 100644 --- a/client/components/open/components/EditSubmissionModal.vue +++ b/client/components/open/components/EditSubmissionModal.vue @@ -5,19 +5,16 @@ @close="emit('close')" > - diff --git a/client/pages/terms-conditions.vue b/client/pages/terms-conditions.vue index acdbe201..e76a4683 100644 --- a/client/pages/terms-conditions.vue +++ b/client/pages/terms-conditions.vue @@ -6,7 +6,7 @@ Terms & Conditions @@ -16,26 +16,19 @@ diff --git a/client/stores/notion_cms.js b/client/stores/notion_cms.js new file mode 100644 index 00000000..6d0b9538 --- /dev/null +++ b/client/stores/notion_cms.js @@ -0,0 +1,127 @@ +import { defineStore } from 'pinia' +import { computed, ref } from 'vue' +import opnformConfig from "~/opnform.config.js" + +function notionApiFetch (entityId, type = 'table') { + const apiUrl = opnformConfig.notion.worker + return useFetch(`${apiUrl}/${type}/${entityId}`) +} + +function fetchNotionDatabasePages (databaseId) { + return notionApiFetch(databaseId) +} + +function fetchNotionPageContent (pageId) { + return notionApiFetch(pageId, 'page') +} + +export const useNotionCmsStore = defineStore('notion_cms', () => { + + // State + const databases = ref({}) + const pages = ref({}) + const pageContents = ref({}) + const slugToIdMap = ref({}) + + const loading = ref(false) + + // Actions + const loadDatabase = (databaseId) => { + return new Promise((resolve, reject) => { + if (databases.value[databaseId]) return resolve() + + loading.value = true + return fetchNotionDatabasePages(databaseId).then((response) => { + databases.value[databaseId] = response.data.value.map(page => formatId(page.id)) + response.data.value.forEach(page => { + pages.value[formatId(page.id)] = { + ...page, + id: formatId(page.id) + } + const slug = page.Slug ?? page.slug ?? null + if (slug) { + setSlugToIdMap(slug, page.id) + } + }) + loading.value = false + resolve() + }).catch((error) => { + loading.value = false + console.error(error) + reject(error) + }) + }) + } + const loadPage = async (pageId) => { + return new Promise((resolve, reject) => { + if (pageContents.value[pageId]) return resolve('in already here') + loading.value = true + return fetchNotionPageContent(pageId).then((response) => { + pageContents.value[formatId(pageId)] = response.data.value + loading.value = false + return resolve('in finishg') + }).catch((error) => { + console.error(error) + loading.value = false + return reject(error) + }) + }) + } + + const loadPageBySlug = (slug) => { + if (!slugToIdMap.value[slug.toLowerCase()]) return + loadPage(slugToIdMap.value[slug.toLowerCase()]) + } + + const formatId = (id) => id.replaceAll('-', '') + + const getPage = (pageId) => { + return { + ...pages.value[pageId], + blocks: getPageBody(pageId) + } + } + + const getPageBody = (pageId) => { + return pageContents.value[pageId] + } + + const setSlugToIdMap = (slug, pageId) => { + if (!slug) return + slugToIdMap.value[slug.toLowerCase()] = formatId(pageId) + } + + const getPageBySlug = (slug) => { + if (!slug) return + const pageId = slugToIdMap.value[slug.toLowerCase()] + return getPage(pageId) + } + +// Getters + const databasePages = (databaseId) => computed(() => databases.value[databaseId]?.map(id => pages.value[id]) || []) + const pageContent = (pageId) => computed(() => pageContents.value[pageId]) + const pageBySlug = (slug) => computed(() => getPageBySlug(slug)) + + return { + // state + loading, + databases, + pages, + pageContents, + slugToIdMap, + + // actions + loadDatabase, + loadPage, + loadPageBySlug, + getPage, + getPageBody, + setSlugToIdMap, + getPageBySlug, + + // getters + databasePages, + pageContent, + pageBySlug + } +}) diff --git a/client/stores/notion_pages.js b/client/stores/notion_pages.js deleted file mode 100644 index 608f78c9..00000000 --- a/client/stores/notion_pages.js +++ /dev/null @@ -1,26 +0,0 @@ -import { defineStore } from "pinia" -import { useContentStore } from "~/composables/stores/useContentStore.js" -import opnformConfig from "~/opnform.config.js" -export const useNotionPagesStore = defineStore("notion_pages", () => { - const contentStore = useContentStore() - - const load = (pageId) => { - contentStore.startLoading() - - const apiUrl = opnformConfig.notion.worker - return useFetch(`${apiUrl}/page/${pageId}`) - .then(({ data }) => { - const val = data.value - val["id"] = pageId - contentStore.save(val) - }) - .finally(() => { - contentStore.stopLoading() - }) - } - - return { - ...contentStore, - load, - } -}) From 96786215aa694e233abfa29a3c739bfde3a7ddf7 Mon Sep 17 00:00:00 2001 From: JhumanJ Date: Mon, 19 May 2025 15:16:33 +0200 Subject: [PATCH 09/11] Enhance Crisp Plugin Configuration Logic - Updated the Crisp plugin configuration in `crisp.client.js` to prevent initialization when on the public form page. This change introduces a new condition that checks if the current route is the 'forms-slug' page, ensuring that the Crisp chat functionality is only enabled on appropriate pages. These changes aim to improve the user experience by preventing unnecessary chat interactions on specific pages, thereby streamlining the application behavior. --- client/plugins/crisp.client.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/plugins/crisp.client.js b/client/plugins/crisp.client.js index a7d8cbf3..327f9e4d 100644 --- a/client/plugins/crisp.client.js +++ b/client/plugins/crisp.client.js @@ -3,7 +3,9 @@ import { Crisp } from "crisp-sdk-web" export default defineNuxtPlugin(() => { const isIframe = useIsIframe() const crispWebsiteId = useRuntimeConfig().public.crispWebsiteId - if (crispWebsiteId && !isIframe) { + const isPublicFormPage = useRoute().name === 'forms-slug' + + if (crispWebsiteId && !isIframe && !isPublicFormPage) { Crisp.configure(crispWebsiteId) window.Crisp = Crisp } From 1c26e282c5a2348be2f811695f75ecbe43fe0561 Mon Sep 17 00:00:00 2001 From: Chirag Chhatrala <60499540+chiragchhatrala@users.noreply.github.com> Date: Mon, 19 May 2025 19:18:56 +0530 Subject: [PATCH 10/11] Fix form initialization (#759) * Fix form initialization * improve condition --- .../composables/useFormInitialization.js | 185 ++++++++++++------ 1 file changed, 126 insertions(+), 59 deletions(-) diff --git a/client/lib/forms/composables/useFormInitialization.js b/client/lib/forms/composables/useFormInitialization.js index ebb989f7..9e224007 100644 --- a/client/lib/forms/composables/useFormInitialization.js +++ b/client/lib/forms/composables/useFormInitialization.js @@ -1,11 +1,133 @@ import { toValue } from 'vue' import { opnFetch } from '~/composables/useOpnApi.js' +import clonedeep from 'clone-deep' /** * @fileoverview Composable for initializing form data, with complete handling of * form state persistence, URL parameters, and default values. */ export function useFormInitialization(formConfig, form, pendingSubmission) { + + /** + * Main method to initialize the form data. + * Follows a clear priority order: + * 1. Load from submission ID (if provided) + * 2. Load from pendingSubmission (localStorage) - client-side only + * 3. Apply URL parameters + * 4. Apply default values for fields + * + * @param {Object} options - Initialization options + * @param {String} [options.submissionId] - ID of submission to load + * @param {URLSearchParams} [options.urlParams] - URL parameters + * @param {Object} [options.defaultData] - Default data to apply + * @param {Array} [options.fields] - Form fields for special handling + */ + const initialize = async (options = {}) => { + const config = toValue(formConfig) + + // 1. Reset form state + form.reset() + form.errors.clear() + + // 2. Try loading from submission ID + if (options.submissionId) { + const loaded = await tryLoadFromSubmissionId(options.submissionId) + if (loaded) return // Exit if loaded successfully + } + + // 3. Try loading from pendingSubmission + if (!(options.skipPendingSubmission ?? false) && tryLoadFromPendingSubmission()) { + updateSpecialFields() + return // Exit if loaded successfully + } + + // 4. Apply URL parameters + if (!(options.skipUrlParams ?? false) && options.urlParams) { + applyUrlParameters(options.urlParams) + } + + // 5. Apply special field handling + updateSpecialFields() + + // 6. Apply default data from config or options + const defaultValuesToApply = options.defaultData || config?.default_data + if (defaultValuesToApply) { + applyDefaultValues(defaultValuesToApply, config?.properties) + } + + // 7. Process any select fields to ensure IDs are converted to names + // This is crucial when receiving data that might contain IDs instead of names + const currentData = form.data() + if (Object.keys(currentData).length > 0) { + resetAndFill(currentData) + } + } + + /** + * Wrapper for form.resetAndFill that converts select option IDs to names + * @param {Object} formData - Form data to clean and fill + */ + const resetAndFill = (formData) => { + if (!formData) { + form.reset() + return + } + + // Clone the data to avoid mutating the original + const cleanData = clonedeep(formData) + + // Process select fields to convert IDs to names + if (!formConfig.value || !formConfig.value.properties || !Array.isArray(formConfig.value.properties)) { + // If properties aren't available, just use the data as is + form.resetAndFill(cleanData) + return + } + + // Iterate through form fields to process select fields + formConfig.value.properties.forEach(field => { + // Basic validation + if (!field || typeof field !== 'object') return + if (!field.id || !field.type) return + // Skip only when value is truly undefined or null + if (cleanData[field.id] === undefined || cleanData[field.id] === null) return + + // Process checkbox fields - convert string and numeric values to boolean + if (field.type === 'checkbox') { + const value = cleanData[field.id] + if (typeof value === 'string' && value.toLowerCase() === 'true' || value === '1' || value === 1) { + cleanData[field.id] = true + } else if (typeof value === 'string' && value.toLowerCase() === 'false' || value === '0' || value === 0) { + cleanData[field.id] = false + } + } + // Only process select, multi_select fields + else if (['select', 'multi_select'].includes(field.type)) { + // Make sure the field has options + if (!field[field.type] || !Array.isArray(field[field.type].options)) return + + const options = field[field.type].options + + // Process array values (multi-select) + if (Array.isArray(cleanData[field.id])) { + cleanData[field.id] = cleanData[field.id].map(optionId => { + const option = options.find(opt => opt.id === optionId) + return option ? option.name : optionId + }) + } + // Process single values (select) + else { + const option = options.find(opt => opt.id === cleanData[field.id]) + if (option) { + cleanData[field.id] = option.name + } + } + } + }) + + // Fill with cleaned data + form.resetAndFill(cleanData) + } + /** * Applies URL parameters to the form data. * @param {URLSearchParams} params - The URL search parameters. @@ -94,7 +216,7 @@ export function useFormInitialization(formConfig, form, pendingSubmission) { return opnFetch(`/forms/${slug}/submissions/${submissionIdValue}`) .then(submissionData => { if (submissionData.data) { - form.resetAndFill(submissionData.data) + resetAndFill(submissionData.data) return true } else { console.warn(`Submission ${submissionIdValue} for form ${slug} loaded but returned no data.`) @@ -131,69 +253,14 @@ export function useFormInitialization(formConfig, form, pendingSubmission) { return false } - form.resetAndFill(pendingData) + resetAndFill(pendingData) return true } - /** - * Main method to initialize the form data. - * Follows a clear priority order: - * 1. Load from submission ID (if provided) - * 2. Load from pendingSubmission (localStorage) - client-side only - * 3. Apply URL parameters - * 4. Apply default values for fields - * - * @param {Object} options - Initialization options - * @param {String} [options.submissionId] - ID of submission to load - * @param {URLSearchParams} [options.urlParams] - URL parameters - * @param {Object} [options.defaultData] - Default data to apply - * @param {Array} [options.fields] - Form fields for special handling - */ - const initialize = async (options = {}) => { - const config = toValue(formConfig) - - // 1. Reset form state - form.reset() - form.errors.clear() - - // 2. Try loading from submission ID - if (options.submissionId) { - const loaded = await tryLoadFromSubmissionId(options.submissionId) - if (loaded) return // Exit if loaded successfully - } - - // 3. Try loading from pendingSubmission - if (!(options.skipPendingSubmission ?? false) && tryLoadFromPendingSubmission()) { - updateSpecialFields() - return // Exit if loaded successfully - } - - // 4. Start with empty form data - const formData = {} - - // 5. Apply URL parameters - if (!(options.skipUrlParams ?? false) && options.urlParams) { - applyUrlParameters(options.urlParams) - } - - // 6. Apply special field handling - updateSpecialFields() - - // 7. Apply default data from config or options - const defaultValuesToApply = options.defaultData || config?.default_data - if (defaultValuesToApply) { - applyDefaultValues(defaultValuesToApply, config?.properties) - } - - // 8. Fill the form with the collected data - if (Object.keys(formData).length > 0) { - form.resetAndFill(formData) - } - } - return { initialize, applyUrlParameters, - applyDefaultValues + applyDefaultValues, + resetAndFill // Export our wrapped function for use elsewhere } } \ No newline at end of file From 1b67cd808b7d6e2ed46f5b839c4781abe14746ba Mon Sep 17 00:00:00 2001 From: JhumanJ Date: Mon, 19 May 2025 15:54:35 +0200 Subject: [PATCH 11/11] Update GTM Configuration and Dependencies - Added a new `enabled` property to the GTM configuration in `gtm.js`, allowing for conditional enabling of the GTM plugin. - Updated the `package-lock.json` to include the latest versions of `@gtm-support/vue-gtm` and `@gtm-support/core`, ensuring compatibility and access to new features. - Modified the `onMounted` lifecycle hook in `FeatureBase.vue` to include a check for the `user` state, preventing script loading when the user is not available. These changes aim to enhance the GTM integration by providing more control over its activation and ensuring that the latest dependencies are utilized for improved functionality. --- client/components/vendor/FeatureBase.vue | 3 +- client/gtm.js | 3 +- client/package-lock.json | 49 ++++++++++++++++++------ client/plugins/gtm.client.js | 31 +++++++++++++++ 4 files changed, 72 insertions(+), 14 deletions(-) create mode 100644 client/plugins/gtm.client.js diff --git a/client/components/vendor/FeatureBase.vue b/client/components/vendor/FeatureBase.vue index 61ec67d1..54a156be 100644 --- a/client/components/vendor/FeatureBase.vue +++ b/client/components/vendor/FeatureBase.vue @@ -54,7 +54,7 @@ const setupForUser = () => { } onMounted(() => { - if (import.meta.server) return + if (import.meta.server || !user.value) return // Setup base if ( @@ -66,7 +66,6 @@ onMounted(() => { } } - if (!user.value) return loadScript() try { setupForUser() diff --git a/client/gtm.js b/client/gtm.js index c838cef6..8dce6517 100644 --- a/client/gtm.js +++ b/client/gtm.js @@ -1,5 +1,6 @@ export default { id: process.env.NUXT_PUBLIC_GTM_CODE, // Your GTM single container ID, array of container ids ['GTM-xxxxxx', 'GTM-yyyyyy'] or array of objects [{id: 'GTM-xxxxxx', queryParams: { gtm_auth: 'abc123', gtm_preview: 'env-4', gtm_cookies_win: 'x'}}, {id: 'GTM-yyyyyy', queryParams: {gtm_auth: 'abc234', gtm_preview: 'env-5', gtm_cookies_win: 'x'}}], // Your GTM single container ID or array of container ids ['GTM-xxxxxx', 'GTM-yyyyyy'] debug: false, // Whether or not display console logs debugs (optional) - devtools: false, // (optional) + devtools: false, // (optional), + enabled: false, // Disabled by default, will be enabled conditionally by our plugin } diff --git a/client/package-lock.json b/client/package-lock.json index d093ec70..b648ab91 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -8,6 +8,7 @@ "hasInstallScript": true, "dependencies": { "@codemirror/lang-html": "^6.4.9", + "@gtm-support/vue-gtm": "^3.1.0", "@iconify-json/material-symbols": "^1.2.4", "@nuxt/ui": "^2.19.2", "@pinia/nuxt": "^0.11.0", @@ -1448,26 +1449,24 @@ "license": "MIT" }, "node_modules/@gtm-support/core": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@gtm-support/core/-/core-2.3.1.tgz", - "integrity": "sha512-eD0hndQjhgKm5f/7IA9fZYujmHiVMY+fnYv4mdZSmz5XJQlS4TiTmpdZx2l7I2A9rI9J6Ysz8LpXYYNo/Xq4LQ==", - "dev": true, + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@gtm-support/core/-/core-3.0.1.tgz", + "integrity": "sha512-SctcoqvvAbGAgZzOb7DZ4wjbZF3ZS7Las3qIEByv6g7mzPf4E9LpRXcQzjmywYLcUx2jys7PWJAa3s5slvj/0w==", "license": "MIT" }, "node_modules/@gtm-support/vue-gtm": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@gtm-support/vue-gtm/-/vue-gtm-2.2.0.tgz", - "integrity": "sha512-7nhBTRkTG0mD+7r7JvNalJz++YwszZk0oP1HIY6fCgz6wNKxT6LuiXCqdPrZmNPe/WbPIKuqxGZN5s+i6NZqow==", - "dev": true, + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@gtm-support/vue-gtm/-/vue-gtm-3.1.0.tgz", + "integrity": "sha512-kGUnCI+Z5lBeCKd7rzgU7UoFU8Q0EkJfh17SgeyAyx8cLdISqeq54BNJKZrME3WXersoigLZVJ1GLs0buYD3lA==", "license": "MIT", "dependencies": { - "@gtm-support/core": "^2.0.0" + "@gtm-support/core": "^3.0.1" }, "optionalDependencies": { - "vue-router": ">= 4.1.0 < 5.0.0" + "vue-router": ">= 4.4.1 < 5.0.0" }, "peerDependencies": { - "vue": ">= 3.2.0 < 4.0.0" + "vue": ">= 3.2.26 < 4.0.0" }, "peerDependenciesMeta": { "vue-router": { @@ -9117,6 +9116,34 @@ "nuxt": "^3.0.0" } }, + "node_modules/@zadigetvoltaire/nuxt-gtm/node_modules/@gtm-support/core": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@gtm-support/core/-/core-2.3.1.tgz", + "integrity": "sha512-eD0hndQjhgKm5f/7IA9fZYujmHiVMY+fnYv4mdZSmz5XJQlS4TiTmpdZx2l7I2A9rI9J6Ysz8LpXYYNo/Xq4LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@zadigetvoltaire/nuxt-gtm/node_modules/@gtm-support/vue-gtm": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@gtm-support/vue-gtm/-/vue-gtm-2.2.0.tgz", + "integrity": "sha512-7nhBTRkTG0mD+7r7JvNalJz++YwszZk0oP1HIY6fCgz6wNKxT6LuiXCqdPrZmNPe/WbPIKuqxGZN5s+i6NZqow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@gtm-support/core": "^2.0.0" + }, + "optionalDependencies": { + "vue-router": ">= 4.1.0 < 5.0.0" + }, + "peerDependencies": { + "vue": ">= 3.2.0 < 4.0.0" + }, + "peerDependenciesMeta": { + "vue-router": { + "optional": true + } + } + }, "node_modules/@zadigetvoltaire/nuxt-gtm/node_modules/sirv": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", diff --git a/client/plugins/gtm.client.js b/client/plugins/gtm.client.js new file mode 100644 index 00000000..d2c8c338 --- /dev/null +++ b/client/plugins/gtm.client.js @@ -0,0 +1,31 @@ +import gtmConfig from '../gtm' + +export default defineNuxtPlugin(() => { + const route = useRoute() + const isIframe = useIsIframe() + const isPublicFormPage = route.name === 'forms-slug' + + // Only enable GTM if not in a form page (for respondents) and not in an iframe + if (!isPublicFormPage && !isIframe && process.env.NUXT_PUBLIC_GTM_CODE) { + // Initialize GTM manually only when needed + const gtm = useGtm() + + // Override the enabled setting to true for this session + gtmConfig.enabled = true + + // Watch for route changes to track page views + watch(() => route.fullPath, () => { + if (!route.name || route.name !== 'forms-slug') { + gtm.trackView(route.name, route.fullPath) + } + }, { immediate: true }) + + return { + provide: { + gtm + } + } + } + + return {} +}) \ No newline at end of file