0351d front end linting (#377)

* feat: disable custom script for  trial users

* after lint fix

* frontend linting

---------

Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
Favour Olayinka
2024-04-15 18:39:03 +01:00
committed by GitHub
parent 8d35fc8b1a
commit bcd45ce8a6
228 changed files with 17036 additions and 8744 deletions

View File

@@ -1,35 +1,38 @@
import {generateUUID} from "~/lib/utils.js";
import { generateUUID } from "~/lib/utils.js"
export const initForm = (defaultValue = {}, withDefaultProperties = false) => {
return useForm({
title: 'My Form',
title: "My Form",
description: null,
visibility: 'public',
visibility: "public",
workspace_id: null,
properties: withDefaultProperties ? getDefaultProperties() :[],
properties: withDefaultProperties ? getDefaultProperties() : [],
// Customization
theme: 'default',
width: 'centered',
dark_mode: 'auto',
color: '#3B82F6',
theme: "default",
width: "centered",
dark_mode: "auto",
color: "#3B82F6",
hide_title: false,
no_branding: false,
uppercase_labels: true,
transparent_background: false,
closes_at: null,
closed_text: 'This form has now been closed by its owner and does not accept submissions anymore.',
closed_text:
"This form has now been closed by its owner and does not accept submissions anymore.",
auto_save: true,
// Submission
submit_button_text: 'Submit',
submit_button_text: "Submit",
re_fillable: false,
re_fill_button_text: 'Fill Again',
submitted_text: 'Amazing, we saved your answers. Thank you for your time and have a great day!',
re_fill_button_text: "Fill Again",
submitted_text:
"Amazing, we saved your answers. Thank you for your time and have a great day!",
use_captcha: false,
max_submissions_count: null,
max_submissions_reached_text: 'This form has now reached the maximum number of allowed submissions and is now closed.',
editable_submissions_button_text: 'Edit submission',
max_submissions_reached_text:
"This form has now reached the maximum number of allowed submissions and is now closed.",
editable_submissions_button_text: "Edit submission",
confetti_on_submission: false,
// Security & Privacy
@@ -38,31 +41,31 @@ export const initForm = (defaultValue = {}, withDefaultProperties = false) => {
// Custom SEO
seo_meta: {},
...defaultValue
...defaultValue,
})
}
function getDefaultProperties () {
function getDefaultProperties() {
return [
{
name: 'Name',
type: 'text',
name: "Name",
type: "text",
hidden: false,
required: true,
id: generateUUID()
id: generateUUID(),
},
{
name: 'Email',
type: 'email',
name: "Email",
type: "email",
hidden: false,
id: generateUUID()
id: generateUUID(),
},
{
name: 'Message',
type: 'text',
name: "Message",
type: "text",
hidden: false,
multi_lines: true,
id: generateUUID()
}
id: generateUUID(),
},
]
}

View File

@@ -1,10 +1,11 @@
import {hash} from "~/lib/utils.js"
import {useStorage} from "@vueuse/core"
import { hash } from "~/lib/utils.js"
import { useStorage } from "@vueuse/core"
export const pendingSubmission = (form) => {
const formPendingSubmissionKey = computed(() => {
return (form) ? form.form_pending_submission_key + '-' + hash(window.location.href) : ''
return form
? form.form_pending_submission_key + "-" + hash(window.location.href)
: ""
})
const enabled = computed(() => {
@@ -13,7 +14,8 @@ export const pendingSubmission = (form) => {
const set = (value) => {
if (import.meta.server || !enabled.value) return
useStorage(formPendingSubmissionKey.value).value = value === null ? value : JSON.stringify(value)
useStorage(formPendingSubmissionKey.value).value =
value === null ? value : JSON.stringify(value)
}
const remove = () => {
@@ -30,6 +32,6 @@ export const pendingSubmission = (form) => {
enabled,
set,
get,
remove
remove,
}
}

View File

@@ -1,12 +1,12 @@
import FormPropertyLogicRule from "~/lib/forms/FormPropertyLogicRule.js";
import FormPropertyLogicRule from "~/lib/forms/FormPropertyLogicRule.js"
export const validatePropertiesLogic = (properties) => {
properties.forEach((field) => {
const isValid = (new FormPropertyLogicRule(field)).isValid()
const isValid = new FormPropertyLogicRule(field).isValid()
if (!isValid) {
field.logic = {
conditions: null,
actions: []
actions: [],
}
}
})

View File

@@ -1,75 +1,74 @@
function arrayWrap(value) {
return Array.isArray(value) ? value : [value];
return Array.isArray(value) ? value : [value]
}
export default class Errors {
constructor() {
this.errors = {};
this.errors = {}
}
set(field, messages = undefined) {
if (typeof field === 'object') {
this.errors = field;
if (typeof field === "object") {
this.errors = field
} else {
this.set({ ...this.errors, [field]: arrayWrap(messages) });
this.set({ ...this.errors, [field]: arrayWrap(messages) })
}
}
all() {
return this.errors;
return this.errors
}
has(field) {
return Object.prototype.hasOwnProperty.call(this.errors, field);
return Object.prototype.hasOwnProperty.call(this.errors, field)
}
hasAny(...fields) {
return fields.some(field => this.has(field));
return fields.some((field) => this.has(field))
}
any() {
return Object.keys(this.errors).length > 0;
return Object.keys(this.errors).length > 0
}
get(field) {
if (this.has(field)) {
return this.getAll(field)[0];
return this.getAll(field)[0]
}
}
getAll(field) {
return arrayWrap(this.errors[field] || []);
return arrayWrap(this.errors[field] || [])
}
only(...fields) {
const messages = [];
const messages = []
fields.forEach((field) => {
const message = this.get(field);
const message = this.get(field)
if (message) {
messages.push(message);
messages.push(message)
}
});
})
return messages;
return messages
}
flatten() {
return Object.values(this.errors).reduce((a, b) => a.concat(b), []);
return Object.values(this.errors).reduce((a, b) => a.concat(b), [])
}
clear(field = undefined) {
const errors = {};
const errors = {}
if (field) {
Object.keys(this.errors).forEach((key) => {
if (key !== field) {
errors[key] = this.errors[key];
errors[key] = this.errors[key]
}
});
})
}
this.set(errors);
this.set(errors)
}
}

View File

@@ -1,170 +1,182 @@
import {serialize} from 'object-to-formdata';
import Errors from './Errors';
import cloneDeep from 'clone-deep';
import {opnFetch} from "~/composables/useOpnApi.js";
import { serialize } from "object-to-formdata"
import Errors from "./Errors"
import cloneDeep from "clone-deep"
import { opnFetch } from "~/composables/useOpnApi.js"
function hasFiles(data) {
return data instanceof File ||
return (
data instanceof File ||
data instanceof Blob ||
data instanceof FileList ||
(typeof data === 'object' && data !== null && Object.values(data).find(value => hasFiles(value)) !== undefined);
(typeof data === "object" &&
data !== null &&
Object.values(data).find((value) => hasFiles(value)) !== undefined)
)
}
class Form {
constructor(data = {}) {
this.originalData = {};
this.busy = false;
this.successful = false;
this.recentlySuccessful = false;
this.recentlySuccessfulTimeoutId = undefined;
this.errors = new Errors();
this.update(data);
this.originalData = {}
this.busy = false
this.successful = false
this.recentlySuccessful = false
this.recentlySuccessfulTimeoutId = undefined
this.errors = new Errors()
this.update(data)
}
static errorMessage = 'Something went wrong. Please try again.';
static recentlySuccessfulTimeout = 2000;
static ignore = ['busy', 'successful', 'errors', 'originalData', 'recentlySuccessful', 'recentlySuccessfulTimeoutId'];
static errorMessage = "Something went wrong. Please try again."
static recentlySuccessfulTimeout = 2000
static ignore = [
"busy",
"successful",
"errors",
"originalData",
"recentlySuccessful",
"recentlySuccessfulTimeoutId",
]
static make(augment) {
return new this(augment);
return new this(augment)
}
update(data) {
this.originalData = Object.assign({}, this.originalData, cloneDeep(data));
Object.assign(this, data);
this.originalData = Object.assign({}, this.originalData, cloneDeep(data))
Object.assign(this, data)
}
fill(data = {}) {
this.keys().forEach((key) => {
this[key] = data[key];
});
this[key] = data[key]
})
}
data() {
return this.keys().reduce((data, key) => (
{...data, [key]: this[key]}
), {});
return this.keys().reduce(
(data, key) => ({ ...data, [key]: this[key] }),
{},
)
}
keys() {
return Object.keys(this).filter(key => !Form.ignore.includes(key));
return Object.keys(this).filter((key) => !Form.ignore.includes(key))
}
startProcessing() {
this.errors.clear();
this.busy = true;
this.successful = false;
this.recentlySuccessful = false;
clearTimeout(this.recentlySuccessfulTimeoutId);
this.errors.clear()
this.busy = true
this.successful = false
this.recentlySuccessful = false
clearTimeout(this.recentlySuccessfulTimeoutId)
}
finishProcessing() {
this.busy = false;
this.successful = true;
this.recentlySuccessful = true;
this.busy = false
this.successful = true
this.recentlySuccessful = true
this.recentlySuccessfulTimeoutId = setTimeout(() => {
this.recentlySuccessful = false;
}, Form.recentlySuccessfulTimeout);
this.recentlySuccessful = false
}, Form.recentlySuccessfulTimeout)
}
clear() {
this.errors.clear();
this.successful = false;
this.recentlySuccessful = false;
clearTimeout(this.recentlySuccessfulTimeoutId);
this.errors.clear()
this.successful = false
this.recentlySuccessful = false
clearTimeout(this.recentlySuccessfulTimeoutId)
}
reset() {
Object.keys(this)
.filter(key => !Form.ignore.includes(key))
.filter((key) => !Form.ignore.includes(key))
.forEach((key) => {
this[key] = JSON.parse(JSON.stringify(this.originalData[key]));
});
this[key] = JSON.parse(JSON.stringify(this.originalData[key]))
})
}
get(url, config = {}) {
return this.submit('get', url, config);
return this.submit("get", url, config)
}
post(url, config = {}) {
return this.submit('post', url, config);
return this.submit("post", url, config)
}
patch(url, config = {}) {
return this.submit('patch', url, config);
return this.submit("patch", url, config)
}
put(url, config = {}) {
return this.submit('put', url, config);
return this.submit("put", url, config)
}
delete(url, config = {}) {
return this.submit('delete', url, config);
return this.submit("delete", url, config)
}
submit(method, url, config = {}) {
this.startProcessing();
this.startProcessing()
config = {
body: {},
params: {},
url: url,
method: method,
...config
};
...config,
}
if (method.toLowerCase() === 'get') {
config.params = {...this.data(), ...config.params};
if (method.toLowerCase() === "get") {
config.params = { ...this.data(), ...config.params }
} else {
config.body = {...this.data(), ...config.data};
config.body = { ...this.data(), ...config.data }
if (hasFiles(config.data) && !config.transformRequest) {
config.transformRequest = [data => serialize(data)];
config.transformRequest = [(data) => serialize(data)]
}
}
return new Promise((resolve, reject) => {
opnFetch(config.url, config)
.then((data) => {
this.finishProcessing();
resolve(data);
}).catch((error) => {
this.handleErrors(error);
this.finishProcessing()
resolve(data)
})
.catch((error) => {
this.handleErrors(error)
reject(error)
})
});
})
}
handleErrors(error) {
this.busy = false;
this.busy = false
if (error) {
this.errors.set(this.extractErrors(error.data));
this.errors.set(this.extractErrors(error.data))
}
}
extractErrors(data) {
if (!data || typeof data !== 'object') {
return {error: Form.errorMessage};
if (!data || typeof data !== "object") {
return { error: Form.errorMessage }
}
if (data.errors) {
return {...data.errors};
return { ...data.errors }
}
if (data.message) {
return {error: data.message};
return { error: data.message }
}
return {...data};
return { ...data }
}
onKeydown(event) {
const target = event.target;
const target = event.target
if (target.name) {
this.errors.clear(target.name);
this.errors.clear(target.name)
}
}
}
export default Form;
export default Form

View File

@@ -1,8 +1,6 @@
// Composable with all the logic to encapsulate a default content store
export const useContentStore = (mapKey = 'id') => {
export const useContentStore = (mapKey = "id") => {
const content = ref(new Map())
const loading = ref(false)
@@ -12,7 +10,9 @@ export const useContentStore = (mapKey = 'id') => {
})
const getByKey = (key) => {
if (Array.isArray(key)) {
return key.map((k) => content.value.get(k)).filter((item) => item !== undefined)
return key
.map((k) => content.value.get(k))
.filter((item) => item !== undefined)
}
return content.value.get(key)
}
@@ -21,7 +21,7 @@ export const useContentStore = (mapKey = 'id') => {
// Actions
function set(items) {
content.value = new Map
content.value = new Map()
save(items)
}
@@ -33,7 +33,7 @@ export const useContentStore = (mapKey = 'id') => {
})
}
function remove(item) {
content.value.delete( typeof item === 'object' ? item[mapKey] : item)
content.value.delete(typeof item === "object" ? item[mapKey] : item)
}
function startLoading() {
@@ -60,6 +60,6 @@ export const useContentStore = (mapKey = 'id') => {
remove,
startLoading,
stopLoading,
resetState
resetState,
}
}

View File

@@ -1,44 +1,43 @@
const { notify } = useNotification()
export const useAlert = () => {
function success(message, autoClose = 10000) {
notify({
title: 'Success',
title: "Success",
text: message,
type: 'success',
duration: autoClose
type: "success",
duration: autoClose,
})
}
function error(message, autoClose = 10000) {
notify({
title: 'Error',
title: "Error",
text: message,
type: 'error',
duration: autoClose
type: "error",
duration: autoClose,
})
}
function warning(message, autoClose = 10000) {
notify({
title: 'Warning',
title: "Warning",
text: message,
type: 'warning',
duration: autoClose
type: "warning",
duration: autoClose,
})
}
function confirm(message, success, failure = ()=>{}, autoClose = 10000) {
function confirm(message, success, failure = () => {}, autoClose = 10000) {
notify({
title: 'Confirm',
title: "Confirm",
text: message,
type: 'confirm',
type: "confirm",
duration: autoClose,
data: {
success,
failure
}
failure,
},
})
}
@@ -46,6 +45,6 @@ export const useAlert = () => {
success,
error,
warning,
confirm
confirm,
}
}

View File

@@ -1,21 +1,21 @@
import amplitude from 'amplitude-js'
import amplitude from "amplitude-js"
export const useAmplitude = () => {
const config = useRuntimeConfig()
const amplitudeCode = config.public.amplitudeCode
const amplitudeClient = amplitudeCode ? amplitude.getInstance() : null;
const amplitudeClient = amplitudeCode ? amplitude.getInstance() : null
if (amplitudeClient) {
amplitudeClient.init(amplitudeCode)
}
const logEvent = function (eventName, eventData) {
if (!config.public.env === 'production' || !amplitudeClient) {
console.log('[DEBUG] Amplitude logged event:', eventName, eventData)
if (!config.public.env === "production" || !amplitudeClient) {
console.log("[DEBUG] Amplitude logged event:", eventName, eventData)
return
}
if (eventData && typeof eventData !== 'object') {
throw new Error('Amplitude event value must be an object.')
if (eventData && typeof eventData !== "object") {
throw new Error("Amplitude event value must be an object.")
}
amplitudeClient.logEvent(eventName, eventData)
@@ -27,14 +27,13 @@ export const useAmplitude = () => {
amplitudeClient.setUserProperties({
email: user.email,
subscribed: user.is_subscribed,
enterprise_subscription: user.has_enterprise_subscription
enterprise_subscription: user.has_enterprise_subscription,
})
}
return {
logEvent,
setUser,
amplitude: amplitudeClient
amplitude: amplitudeClient,
}
}

View File

@@ -1,22 +1,22 @@
import { ref, onUnmounted } from 'vue'
import { ref, onUnmounted } from "vue"
export const useConfetti = () => {
let timeoutId = ref(null)
const nuxtApp = useNuxtApp()
const $confetti = nuxtApp.vueApp.config.globalProperties.$confetti
function play(duration=3000) {
function play(duration = 3000) {
$confetti.start({ defaultSize: 6 })
timeoutId = setTimeout(() => {
timeoutId.value = setTimeout(() => {
$confetti.stop()
}, duration)
}
onUnmounted(() => {
if (timeoutId) clearTimeout(timeoutId)
if (timeoutId.value) clearTimeout(timeoutId)
})
return {
play
play,
}
}

View File

@@ -1,5 +1,4 @@
export const useCrisp = () => {
let crisp = import.meta.client ? window.Crisp : null
function openChat() {
@@ -35,29 +34,32 @@ export const useCrisp = () => {
crisp.chat.setHelpdeskView()
}
function openHelpdeskArticle(articleSlug, locale = 'en') {
function openHelpdeskArticle(articleSlug, locale = "en") {
if (!crisp) return
crisp.chat.openHelpdeskArticle(locale, articleSlug);
crisp.chat.openHelpdeskArticle(locale, articleSlug)
}
function sendTextMessage(message) {
if (!crisp) return
crisp.message.send('text', message)
crisp.message.send("text", message)
}
function setUser(user) {
if (!crisp) return
crisp.user.setEmail(user.email);
crisp.user.setNickname(user.name);
crisp.user.setEmail(user.email)
crisp.user.setNickname(user.name)
crisp.session.setData({
user_id: user.id,
'pro-subscription': user?.is_subscribed ?? false,
'stripe-id': user?.stripe_id ?? '',
'subscription': user?.has_enterprise_subscription ? 'enterprise' : 'pro'
});
"pro-subscription": user?.is_subscribed ?? false,
"stripe-id": user?.stripe_id ?? "",
subscription: user?.has_enterprise_subscription ? "enterprise" : "pro",
})
if (user?.is_subscribed ?? false) {
setSegments(['subscribed', user?.has_enterprise_subscription ? 'enterprise' : 'pro'])
setSegments([
"subscribed",
user?.has_enterprise_subscription ? "enterprise" : "pro",
])
}
}
@@ -82,6 +84,6 @@ export const useCrisp = () => {
openHelpdeskArticle,
sendTextMessage,
pushEvent,
setUser
setUser,
}
}

View File

@@ -1,20 +1,25 @@
import {getDomain, getHost, customDomainUsed} from "~/lib/utils.js";
import { getDomain, getHost, customDomainUsed } from "~/lib/utils.js"
function addAuthHeader(request, options) {
const authStore = useAuthStore()
if (authStore.token) {
options.headers = {Authorization: `Bearer ${authStore.token}`, ...options.headers}
options.headers = {
Authorization: `Bearer ${authStore.token}`,
...options.headers,
}
}
}
function addPasswordToFormRequest(request, options) {
if (!request || !request.startsWith('/forms/')) return
if (!request || !request.startsWith("/forms/")) return
const slug = request.split('/')[2]
const slug = request.split("/")[2]
const passwordCookie = useCookie('password-' + slug, {maxAge: 60 * 60 * 24 * 30}) // 30 days
if (slug !== undefined && slug !== '' && passwordCookie.value !== undefined) {
options.headers['form-password'] = passwordCookie.value
const passwordCookie = useCookie("password-" + slug, {
maxAge: 60 * 60 * 24 * 30,
}) // 30 days
if (slug !== undefined && slug !== "" && passwordCookie.value !== undefined) {
options.headers["form-password"] = passwordCookie.value
}
}
@@ -23,7 +28,7 @@ function addPasswordToFormRequest(request, options) {
*/
function addCustomDomainHeader(request, options) {
if (!customDomainUsed()) return
options.headers['x-custom-domain'] = getDomain(getHost())
options.headers["x-custom-domain"] = getDomain(getHost())
}
export function getOpnRequestsOptions(request, opts) {
@@ -31,16 +36,16 @@ export function getOpnRequestsOptions(request, opts) {
if (opts.body && opts.body instanceof FormData) {
opts.headers = {
'charset': 'utf-8',
charset: "utf-8",
...opts.headers,
}
}
opts.headers = {accept: 'application/json', ...opts.headers}
opts.headers = { accept: "application/json", ...opts.headers }
// Authenticate requests coming from the server
if (import.meta.server && config.apiSecret) {
opts.headers['x-api-secret'] = config.apiSecret
opts.headers["x-api-secret"] = config.apiSecret
}
addAuthHeader(request, opts)
@@ -50,24 +55,25 @@ export function getOpnRequestsOptions(request, opts) {
if (!opts.baseURL) opts.baseURL = config.privateApiBase || config.public.apiBase
return {
async onResponseError({response}) {
async onResponseError({ response }) {
const authStore = useAuthStore()
const {status} = response
const { status } = response
if (status === 401) {
if (authStore.check) {
console.log("Logging out due to 401")
authStore.logout()
useRouter().push({name: 'login'})
useRouter().push({ name: "login" })
}
} else if (status === 420) {
// If invalid domain, redirect to main domain
window.location.href = config.public.appUrl + '?utm_source=failed_custom_domain_redirect'
window.location.href =
config.public.appUrl + "?utm_source=failed_custom_domain_redirect"
} else if (status >= 500) {
console.error('Request error', status)
console.error("Request error", status)
}
},
...opts
...opts,
}
}

View File

@@ -1,16 +1,22 @@
export const useOpnSeoMeta = (meta) => {
return useSeoMeta({
...meta.title ? {
ogTitle: meta.title,
twitterTitle: meta.title,
} : {},
...meta.description ? {
ogDescription: meta.description,
twitterDescription: meta.description,
} : {},
...meta.ogImage ? {
twitterImage: meta.ogImage,
} : {},
...(meta.title
? {
ogTitle: meta.title,
twitterTitle: meta.title,
}
: {}),
...(meta.description
? {
ogDescription: meta.description,
twitterDescription: meta.description,
}
: {}),
...(meta.ogImage
? {
twitterImage: meta.ogImage,
}
: {}),
...meta,
})
}