Migrate to nuxt settings page AND remove axios (#266)

* Settings pages migration

* remove axios and use opnFetch

* Make created form reactive (#267)

* Remove verify pages and axios lib

---------

Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
formsdev 2024-01-02 17:39:41 +05:30 committed by GitHub
parent 6fd2985ff5
commit 178424a184
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 622 additions and 888 deletions

View File

@ -76,7 +76,6 @@
</template> </template>
<script> <script>
import axios from 'axios'
import { inputProps, useFormInput } from './useFormInput.js' import { inputProps, useFormInput } from './useFormInput.js'
import InputWrapper from './components/InputWrapper.vue' import InputWrapper from './components/InputWrapper.vue'
import UploadedFile from './components/UploadedFile.vue' import UploadedFile from './components/UploadedFile.vue'
@ -193,13 +192,14 @@ export default {
} }
if (this.moveToFormAssets) { if (this.moveToFormAssets) {
// Move file to permanent storage for form assets // Move file to permanent storage for form assets
axios.post('/api/open/forms/assets/upload', { opnFetch('/open/forms/assets/upload', {
method: 'POST',
type: 'files', type: 'files',
url: file.name.split('.').slice(0, -1).join('.') + '_' + response.uuid + '.' + response.extension url: file.name.split('.').slice(0, -1).join('.') + '_' + response.uuid + '.' + response.extension
}).then(moveFileResponse => { }).then(moveFileResponseData => {
this.files.push({ this.files.push({
file: file, file: file,
url: moveFileResponse.data.url, url: moveFileResponseData.url,
src: this.getFileSrc(file) src: this.getFileSrc(file)
}) })
this.loading = false this.loading = false

View File

@ -107,7 +107,6 @@
</template> </template>
<script> <script>
import axios from 'axios'
import { inputProps, useFormInput } from './useFormInput.js' import { inputProps, useFormInput } from './useFormInput.js'
import InputWrapper from './components/InputWrapper.vue' import InputWrapper from './components/InputWrapper.vue'
import Modal from '../global/Modal.vue' import Modal from '../global/Modal.vue'
@ -190,13 +189,14 @@ export default {
// Store file in s3 // Store file in s3
this.storeFile(this.file).then(response => { this.storeFile(this.file).then(response => {
// Move file to permanent storage for form assets // Move file to permanent storage for form assets
axios.post('/api/open/forms/assets/upload', { opnFetch('/open/forms/assets/upload', {
method: 'POST',
url: this.file.name.split('.').slice(0, -1).join('.') + '_' + response.uuid + '.' + response.extension url: this.file.name.split('.').slice(0, -1).join('.') + '_' + response.uuid + '.' + response.extension
}).then(moveFileResponse => { }).then(moveFileResponseData => {
if (!this.multiple) { if (!this.multiple) {
this.files = [] this.files = []
} }
this.compVal = moveFileResponse.data.url this.compVal = moveFileResponseData.url
this.showUploadModal = false this.showUploadModal = false
this.loading = false this.loading = false
}).catch((error) => { }).catch((error) => {

View File

@ -16,7 +16,6 @@
</template> </template>
<script> <script>
import axios from 'axios'
export default { export default {
components: { }, components: { },
@ -52,10 +51,10 @@ export default {
this.useAlert.confirm('Do you really want to delete this record?', this.deleteRecord) this.useAlert.confirm('Do you really want to delete this record?', this.deleteRecord)
}, },
async deleteRecord () { async deleteRecord () {
axios.delete('/api/open/forms/' + this.form.id + '/records/' + this.rowid + '/delete').then(async (response) => { opnFetch('/open/forms/' + this.form.id + '/records/' + this.rowid + '/delete', {method:'DELETE'}).then(async (data) => {
if (response.data.type === 'success') { if (data.type === 'success') {
this.$emit('deleted') this.$emit('deleted')
this.useAlert.success(response.data.message) this.useAlert.success(data.message)
} else { } else {
this.useAlert.error('Something went wrong!') this.useAlert.error('Something went wrong!')
} }

View File

@ -60,7 +60,6 @@
</template> </template>
<script> <script>
import axios from 'axios'
import clonedeep from 'clone-deep' import clonedeep from 'clone-deep'
import draggable from 'vuedraggable' import draggable from 'vuedraggable'
import OpenFormButton from './OpenFormButton.vue' import OpenFormButton from './OpenFormButton.vue'
@ -293,8 +292,8 @@ export default {
return null return null
} }
await this.recordsStore.loadRecord( await this.recordsStore.loadRecord(
axios.get('/api/forms/' + this.form.slug + '/submissions/' + this.form.submission_id).then((response) => { opnFetch('/forms/' + this.form.slug + '/submissions/' + this.form.submission_id).then((data) => {
return { submission_id: this.form.submission_id, ...response.data.data } return { submission_id: this.form.submission_id, ...data.data }
}) })
) )
return this.recordsStore.getById(this.form.submission_id) return this.recordsStore.getById(this.form.submission_id)

View File

@ -28,7 +28,6 @@
</template> </template>
<script> <script>
import axios from 'axios'
import { Line as LineChart } from 'vue-chartjs' import { Line as LineChart } from 'vue-chartjs'
import { import {
Chart as ChartJS, Chart as ChartJS,

View File

@ -72,7 +72,6 @@
</template> </template>
<script> <script>
import axios from 'axios'
import { computed } from 'vue' import { computed } from 'vue'
import QuestionsEditor from './QuestionsEditor.vue' import QuestionsEditor from './QuestionsEditor.vue'
@ -175,9 +174,9 @@ export default {
}, },
async deleteFormTemplate () { async deleteFormTemplate () {
if (!this.template) return if (!this.template) return
axios.delete('/api/templates/' + this.template.id).then((response) => { opnFetch('/templates/' + this.template.id, {method:'DELETE'}).then((data) => {
if (response.data.message) { if (data.message) {
this.useAlert.success(response.data.message) this.useAlert.success(data.message)
} }
this.$router.push({ name: 'templates' }) this.$router.push({ name: 'templates' })
this.templatesStore.remove(this.template) this.templatesStore.remove(this.template)

View File

@ -63,7 +63,6 @@
<script> <script>
import { computed } from 'vue' import { computed } from 'vue'
import axios from 'axios'
import { useAuthStore } from '../../../stores/auth'; import { useAuthStore } from '../../../stores/auth';
import VTransition from '~/components/global/transitions/VTransition.vue' import VTransition from '~/components/global/transitions/VTransition.vue'
@ -98,8 +97,8 @@ export default {
methods: { methods: {
loadChangelogEntries () { loadChangelogEntries () {
axios.get('/api/content/changelog/entries').then(response => { opnFetch('/content/changelog/entries').then(data => {
this.changelogEntries = response.data.splice(0, 3) this.changelogEntries = data.splice(0, 3)
}) })
} }
} }

View File

@ -91,7 +91,6 @@
</template> </template>
<script> <script>
import axios from 'axios'
export default { export default {
props: { props: {
@ -133,12 +132,12 @@ export default {
fetchGeneratedForm (generationId) { fetchGeneratedForm (generationId) {
// check every 4 seconds if form is generated // check every 4 seconds if form is generated
setTimeout(() => { setTimeout(() => {
axios.get('/api/forms/ai/' + generationId).then(response => { opnFetch('/forms/ai/' + generationId).then(data => {
if (response.data.ai_form_completion.status === 'completed') { if (data.ai_form_completion.status === 'completed') {
this.useAlert.success(response.data.message) this.useAlert.success(data.message)
this.$emit('form-generated', JSON.parse(response.data.ai_form_completion.result)) this.$emit('form-generated', JSON.parse(data.ai_form_completion.result))
this.$emit('close') this.$emit('close')
} else if (response.data.ai_form_completion.status === 'failed') { } else if (data.ai_form_completion.status === 'failed') {
this.useAlert.error('Something went wrong, please try again.') this.useAlert.error('Something went wrong, please try again.')
this.state = 'default' this.state = 'default'
this.loading = false this.loading = false

View File

@ -140,7 +140,6 @@
<script> <script>
import { computed } from 'vue' import { computed } from 'vue'
import axios from 'axios'
import Dropdown from '~/components/global/Dropdown.vue' import Dropdown from '~/components/global/Dropdown.vue'
import FormTemplateModal from '../../../open/forms/components/templates/FormTemplateModal.vue' import FormTemplateModal from '../../../open/forms/components/templates/FormTemplateModal.vue'

View File

@ -73,7 +73,6 @@
<script> <script>
import { computed } from 'vue' import { computed } from 'vue'
import axios from 'axios'
import { useFormsStore } from '../../../../stores/forms' import { useFormsStore } from '../../../../stores/forms'
export default { export default {
@ -103,10 +102,10 @@ export default {
regenerateLink(option) { regenerateLink(option) {
if (this.loadingNewLink) return if (this.loadingNewLink) return
this.loadingNewLink = true this.loadingNewLink = true
axios.put(this.formEndpoint.replace('{id}', this.form.id) + '/regenerate-link/' + option).then((response) => { opnFetch(this.formEndpoint.replace('{id}', this.form.id) + '/regenerate-link/' + option, {method:'PUT'}).then((data) => {
this.formsStore.addOrUpdate(response.data.form) this.formsStore.addOrUpdate(data.form)
this.$router.push({name: 'forms-slug-show-share', params: {slug: response.data.form.slug}}) this.$router.push({name: 'forms-slug-show-share', params: {slug: data.form.slug}})
useAlert().success(response.data.message) useAlert().success(data.message)
this.loadingNewLink = false this.loadingNewLink = false
}).finally(() => { }).finally(() => {
this.showGenerateFormLinkModal = false this.showGenerateFormLinkModal = false

View File

@ -12,7 +12,6 @@
<script> <script>
import { computed } from 'vue' import { computed } from 'vue'
import axios from 'axios'
import TextInput from '../../forms/TextInput.vue' import TextInput from '../../forms/TextInput.vue'
import VButton from '~/components/global/VButton.vue' import VButton from '~/components/global/VButton.vue'
@ -81,8 +80,8 @@ export default {
if (this.form.busy) return if (this.form.busy) return
this.form.put('api/subscription/update-customer-details').then(() => { this.form.put('api/subscription/update-customer-details').then(() => {
this.loading = true this.loading = true
axios.get('/api/subscription/new/' + this.plan + '/' + (!this.yearly ? 'monthly' : 'yearly') + '/checkout/with-trial').then((response) => { opnFetch('/subscription/new/' + this.plan + '/' + (!this.yearly ? 'monthly' : 'yearly') + '/checkout/with-trial').then((data) => {
window.location = response.data.checkout_url window.location = data.checkout_url
}).catch((error) => { }).catch((error) => {
useAlert().error(error.response.data.message) useAlert().error(error.response.data.message)
}).finally(() => { }).finally(() => {

View File

@ -109,7 +109,6 @@
<script> <script>
import { computed } from 'vue' import { computed } from 'vue'
import { useAuthStore } from '../../../stores/auth' import { useAuthStore } from '../../../stores/auth'
import axios from 'axios'
import MonthlyYearlySelector from './MonthlyYearlySelector.vue' import MonthlyYearlySelector from './MonthlyYearlySelector.vue'
import CheckoutDetailsModal from './CheckoutDetailsModal.vue' import CheckoutDetailsModal from './CheckoutDetailsModal.vue'
import CustomPlan from './CustomPlan.vue' import CustomPlan from './CustomPlan.vue'
@ -160,9 +159,9 @@ export default {
}, },
openBilling () { openBilling () {
this.billingLoading = true this.billingLoading = true
axios.get('/api/subscription/billing-portal').then((response) => { opnFetch('/subscription/billing-portal').then((data) => {
this.billingLoading = false this.billingLoading = false
const url = response.data.portal_url const url = data.portal_url
window.location = url window.location = url
}) })
} }

View File

@ -77,7 +77,7 @@ class Form {
Object.keys(this) Object.keys(this)
.filter(key => !Form.ignore.includes(key)) .filter(key => !Form.ignore.includes(key))
.forEach((key) => { .forEach((key) => {
this[key] = deepCopy(this.originalData[key]); this[key] = JSON.parse(JSON.stringify(this.originalData[key]));
}); });
} }
@ -128,7 +128,7 @@ class Form {
resolve(data); resolve(data);
}).catch((error) => { }).catch((error) => {
this.handleErrors(error); this.handleErrors(error);
resolve(error) reject(error)
}) })
}); });
} }

View File

@ -1,5 +1,5 @@
import Form from "~/composables/lib/vForm/Form.js" import Form from "~/composables/lib/vForm/Form.js"
export const useForm = (formData = {}) => { export const useForm = (formData = {}) => {
return new Form(formData) return reactive(new Form(formData))
} }

677
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -27,7 +27,6 @@
"@vueuse/motion": "^2.0.0", "@vueuse/motion": "^2.0.0",
"@vueuse/nuxt": "^10.7.0", "@vueuse/nuxt": "^10.7.0",
"amplitude-js": "^8.21.9", "amplitude-js": "^8.21.9",
"axios": "^0.21.1",
"chart.js": "^4.4.0", "chart.js": "^4.4.0",
"clone-deep": "^4.0.1", "clone-deep": "^4.0.1",
"crisp-sdk-web": "^1.0.21", "crisp-sdk-web": "^1.0.21",

View File

@ -1,55 +0,0 @@
<template>
<div class="row">
<div class="col-lg-8 m-auto px-4">
<h1 class="my-6">
Verify Email
</h1>
<form @submit.prevent="send" @keydown="form.onKeydown($event)">
<alert-success :form="form" :message="status" />
<!-- Email -->
<text-input name="email" :form="form" label="Email" :required="true" />
<!-- Submit Button -->
<div class="form-group row">
<div class="col-md-9 ml-md-auto">
<v-button :loading="form.busy">
Send Verification Link
</v-button>
</div>
</div>
</form>
</div>
</div>
</template>
<script>
export default {
middleware: 'guest',
data: () => ({
metaTitle: 'Verify Email',
status: '',
form: useForm({
email: ''
})
}),
created () {
if (this.$route.query.email) {
this.form.email = this.$route.query.email
}
},
methods: {
async send () {
const { data } = await this.form.post('/api/email/resend')
this.status = data.status
this.form.reset()
}
}
}
</script>

View File

@ -1,59 +0,0 @@
<template>
<div class="row">
<div class="col-lg-8 m-auto px-4">
<h1 class="my-6">
Verify Email
</h1>
<template v-if="success">
<div class="alert alert-success" role="alert">
{{ success }}
</div>
<NuxtLink :to="{ name: 'login' }" class="btn btn-primary">
Login
</NuxtLink>
</template>
<template v-else>
<div class="alert alert-danger" role="alert">
{{ error || 'Failed to verify email.' }}
</div>
<NuxtLink :to="{ name: 'auth-verification-resend' }" class="small float-right">
Resend Verification Link?
</NuxtLink>
</template>
</div>
</div>
</template>
<script>
import axios from 'axios'
import SeoMeta from '../../../mixins/seo-meta.js'
const qs = (params) => Object.keys(params).map(key => `${key}=${params[key]}`).join('&')
export default {
mixins: [SeoMeta],
async beforeRouteEnter (to, from, next) {
try {
const { data } = await axios.post(`/api/email/verify/${to.params.id}?${qs(to.query)}`)
next(vm => {
vm.success = data.status
})
} catch (e) {
next(vm => {
vm.error = e.response.data.status
})
}
},
middleware: 'guest',
data: () => ({
metaTitle: 'Verify Email',
error: '',
success: ''
})
}
</script>

87
client/pages/settings.vue Normal file
View File

@ -0,0 +1,87 @@
<template>
<div class="bg-white">
<div class="flex bg-gray-50">
<div class="w-full md:w-4/5 lg:w-3/5 md:mx-auto md:max-w-4xl px-4">
<div class="pt-4 pb-0">
<div class="flex">
<h2 class="flex-grow text-gray-900">
My Account
</h2>
</div>
<ul class="flex text-gray-500">
<li>{{ user.email }}</li>
</ul>
<div class="mt-4 border-b border-gray-200 dark:border-gray-700">
<ul class="flex flex-wrap -mb-px text-sm font-medium text-center">
<li v-for="(tab, i) in tabsList" :key="i+1" class="mr-6">
<nuxt-link :to="{ name: tab.route }"
class="hover:no-underline inline-block py-4 rounded-t-lg border-b-2 text-gray-500 hover:text-gray-600"
active-class="text-blue-600 hover:text-blue-900 dark:text-blue-500 dark:hover:text-blue-500 border-blue-600 dark:border-blue-500"
>
{{ tab.name }}
</nuxt-link>
</li>
</ul>
</div>
</div>
</div>
</div>
<div class="flex bg-white">
<div class="w-full md:w-4/5 lg:w-3/5 md:mx-auto md:max-w-4xl px-4">
<div class="mt-8 pb-0">
<NuxtPage />
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useAuthStore } from '../stores/auth'
definePageMeta({
middleware: "auth"
})
const authStore = useAuthStore()
const user = computed(() => authStore.user)
const tabsList = computed(() => {
const tabs = [
{
name: 'Profile',
route: 'settings-profile'
},
{
name: 'Workspace Settings',
route: 'settings-workspace'
},
{
name: 'Password',
route: 'settings-password'
},
{
name: 'Delete Account',
route: 'settings-account'
}
]
if (user.value.is_subscribed) {
tabs.splice(1, 0, {
name: 'Billing',
route: 'settings-billing'
})
}
if (user.value.admin) {
tabs.push({
name: 'Admin',
route: 'settings-admin'
})
}
return tabs
})
</script>

View File

@ -9,49 +9,34 @@
</p> </p>
<!-- Submit Button --> <!-- Submit Button -->
<v-button :loading="loading" class="mt-4" color="red" @click="alertConfirm('Do you really want to delete your account?',deleteAccount)"> <v-button :loading="loading" class="mt-4" color="red" @click="useAlert().confirm('Do you really want to delete your account?',deleteAccount)">
Delete account Delete account
</v-button> </v-button>
</div> </div>
</template> </template>
<script> <script setup>
import axios from 'axios' import { useRouter } from 'vue-router';
export default { const router = useRouter()
scrollToTop: false, const authStore = useAuthStore()
const metaTitle = 'Account'
let loading = false
setup () { const deleteAccount = () => {
const authStore = useAuthStore() loading = true
return { opnFetch('/user', {method:'DELETE'}).then(async (data) => {
authStore loading = false
} useAlert().success(data.message)
},
// Log out the user.
await authStore.logout()
data: () => ({ // Redirect to login.
metaTitle: 'Account', router.push({ name: 'login' })
form: useForm({ }).catch((error) => {
identifier: '' useAlert().error(error.response.data.message)
}), loading = false
loading: false })
}),
methods: {
async deleteAccount () {
this.loading = true
axios.delete('/api/user').then(async (response) => {
this.loading = false
useAlert().success(response.data.message)
// Log out the user.
await this.authStore.logout()
// Redirect to login.
this.$router.push({ name: 'login' })
}).catch((error) => {
useAlert().error(error.response.data.message)
this.loading = false
})
}
}
} }
</script> </script>

View File

@ -34,54 +34,40 @@
</div> </div>
</template> </template>
<script> <script setup>
import axios from 'axios' import { useRouter } from 'vue-router';
export default { definePageMeta({
components: { }, middleware: "admin"
middleware: 'admin', })
scrollToTop: false,
setup () { const metaTitle = 'Admin'
const authStore = useAuthStore() const authStore = useAuthStore()
const workspacesStore = useWorkspacesStore() const workspacesStore = useWorkspacesStore()
return { const router = useRouter()
authStore, let form = useForm({
workspacesStore identifier: ''
} })
}, let loading = false
data: () => ({ const impersonate = () => {
metaTitle: 'Admin', loading = true
form: useForm({ authStore.startImpersonating()
identifier: '' opnFetch('/admin/impersonate/' + encodeURI(form.identifier)).then(async (data) => {
}), loading = false
loading: false
}),
methods: { // Save the token.
async impersonate () { authStore.saveToken(data.token, false)
this.loading = true
this.authStore.startImpersonating()
axios.get('/api/admin/impersonate/' + encodeURI(this.form.identifier)).then(async (response) => {
this.loading = false
// Save the token. // Fetch the user.
this.authStore.saveToken(response.data.token, false) await authStore.fetchUser()
// Fetch the user. // Redirect to the dashboard.
await this.authStore.fetchUser() workspacesStore.set([])
router.push({ name: 'home' })
// Redirect to the dashboard. }).catch((error) => {
this.workspacesStore.set([]) useAlert().error(error.response.data.message)
this.$router.push({ name: 'home' }) loading = false
}).catch((error) => { })
useAlert().error(error.response.data.message)
this.loading = false
})
// this.form.reset()
}
}
} }
</script> </script>

View File

@ -19,45 +19,25 @@
</div> </div>
</template> </template>
<script> <script setup>
import axios from 'axios'
import { computed } from 'vue' import { computed } from 'vue'
import { useAuthStore } from '../../stores/auth' import { useAuthStore } from '../../stores/auth'
import VButton from '~/components/global/VButton.vue'
import SeoMeta from '../../mixins/seo-meta.js'
import AppSumoBilling from '../../components/vendor/appsumo/AppSumoBilling.vue' import AppSumoBilling from '../../components/vendor/appsumo/AppSumoBilling.vue'
export default { const metaTitle = 'Billing'
components: { AppSumoBilling, VButton }, const authStore = useAuthStore()
mixins: [SeoMeta], let user = computed(() => authStore.user)
scrollToTop: false, let billingLoading = false
setup () { const openBillingDashboard = () => {
const authStore = useAuthStore() billingLoading = true
return { opnFetch('/subscription/billing-portal').then((data) => {
user : computed(() => authStore.user) const url = data.portal_url
} window.location = url
}, }).catch((error) => {
useAlert().error(error.response.data.message)
data: () => ({ }).finally(() => {
metaTitle: 'Billing', billingLoading = false
billingLoading: false })
}),
methods: {
openBillingDashboard () {
this.billingLoading = true
axios.get('/api/subscription/billing-portal').then((response) => {
const url = response.data.portal_url
window.location = url
}).catch((error) => {
useAlert().error(error.response.data.message)
}).finally(() => {
this.billingLoading = false
})
}
},
computed: {}
} }
</script> </script>

View File

@ -1,105 +1,5 @@
<template> <script setup>
<div class="bg-white"> useRouter().push({
<div class="flex bg-gray-50"> name: 'settings-profile'
<div class="w-full md:w-4/5 lg:w-3/5 md:mx-auto md:max-w-4xl px-4"> })
<div class="pt-4 pb-0">
<div class="flex">
<h2 class="flex-grow text-gray-900">
My Account
</h2>
</div>
<ul class="flex text-gray-500">
<li>{{ user.email }}</li>
</ul>
<div class="mt-4 border-b border-gray-200 dark:border-gray-700">
<ul class="flex flex-wrap -mb-px text-sm font-medium text-center">
<li v-for="(tab, i) in tabsList" :key="i+1" class="mr-6">
<NuxtLink :to="{ name: tab.route }"
class="hover:no-underline inline-block py-4 rounded-t-lg border-b-2 text-gray-500 hover:text-gray-600"
active-class="text-blue-600 hover:text-blue-900 dark:text-blue-500 dark:hover:text-blue-500 border-blue-600 dark:border-blue-500"
>
{{ tab.name }}
</NuxtLink>
</li>
</ul>
</div>
</div>
</div>
</div>
<div class="flex bg-white">
<div class="w-full md:w-4/5 lg:w-3/5 md:mx-auto md:max-w-4xl px-4">
<div class="mt-8 pb-0">
<router-view v-slot="{ Component }">
<transition name="page" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</div>
</div>
</div>
</div>
</template>
<script>
import { computed } from 'vue'
import { useAuthStore } from '../../stores/auth'
export default {
middleware: 'auth',
setup () {
const authStore = useAuthStore()
return {
user: computed(() => authStore.user)
}
},
data () {
return {
}
},
computed: {
tabsList () {
const tabs = [
{
name: 'Profile',
route: 'settings-profile'
},
{
name: 'Workspace Settings',
route: 'settings-workspace'
},
{
name: 'Password',
route: 'settings-password'
},
{
name: 'Delete Account',
route: 'settings-account'
}
]
if (this.user.is_subscribed) {
tabs.splice(1, 0, {
name: 'Billing',
route: 'settings-billing'
})
}
if (this.user.admin) {
tabs.push({
name: 'Admin',
route: 'settings-admin'
})
}
return tabs
}
},
methods: {
}
}
</script> </script>

View File

@ -6,8 +6,6 @@
<small class="text-gray-600">Manage your password.</small> <small class="text-gray-600">Manage your password.</small>
<form class="mt-3" @submit.prevent="update" @keydown="form.onKeydown($event)"> <form class="mt-3" @submit.prevent="update" @keydown="form.onKeydown($event)">
<alert-success class="mb-5" :form="form" message="Password updated." />
<!-- Password --> <!-- Password -->
<text-input native-type="password" <text-input native-type="password"
name="password" :form="form" label="Password" :required="true" name="password" :form="form" label="Password" :required="true"
@ -26,27 +24,17 @@
</div> </div>
</template> </template>
<script> <script setup>
import SeoMeta from '../../mixins/seo-meta.js' const metaTitle = 'Password'
let form = useForm({
password: '',
password_confirmation: ''
})
export default { const update = () => {
mixins: [SeoMeta], form.patch('/settings/password').then((response) => {
scrollToTop: false, form.reset()
useAlert().success('Password updated.')
data: () => ({ })
metaTitle: 'Password',
form: useForm({
password: '',
password_confirmation: ''
})
}),
methods: {
async update () {
await this.form.patch('/api/settings/password')
this.form.reset()
}
}
} }
</script> </script>

View File

@ -6,8 +6,6 @@
<small class="text-gray-600">Update your username and manage your account details.</small> <small class="text-gray-600">Update your username and manage your account details.</small>
<form class="mt-3" @submit.prevent="update" @keydown="form.onKeydown($event)"> <form class="mt-3" @submit.prevent="update" @keydown="form.onKeydown($event)">
<alert-success class="mb-5" :form="form" message="Your info has been updated!" />
<!-- Name --> <!-- Name -->
<text-input name="name" :form="form" label="Name" :required="true" /> <text-input name="name" :form="form" label="Name" :required="true" />
@ -22,39 +20,26 @@
</div> </div>
</template> </template>
<script> <script setup>
export default { const authStore = useAuthStore()
scrollToTop: false, const user = computed(() => authStore.user)
const metaTitle = 'Profile'
let form = useForm({
name: '',
email: ''
})
setup () { const update = () => {
const authStore = useAuthStore() form.patch('/settings/profile').then((response) => {
return { authStore.updateUser(response)
authStore, useAlert().success('Your info has been updated!')
user : computed(() => authStore.user) })
}
},
data: () => ({
metaTitle: 'Profile',
form: useForm({
name: '',
email: ''
})
}),
created () {
// Fill the form with user data.
this.form.keys().forEach(key => {
this.form[key] = this.user[key]
})
},
methods: {
async update () {
const { data } = await this.form.patch('/api/settings/profile')
this.authStore.updateUser(data)
}
}
} }
onBeforeMount(() => {
// Fill the form with user data.
form.keys().forEach(key => {
form[key] = user.value[key]
})
})
</script> </script>

View File

@ -46,7 +46,7 @@
/> />
<p class="text-gray-500 text-sm"> <p class="text-gray-500 text-sm">
Read our <a href="#" Read our <a href="#"
@click.prevent="$crisp.push(['do', 'helpdesk:article:open', ['en', 'how-to-use-my-own-domain-9m77g7']])" @click.prevent="crisp.openHelpdeskArticle('how-to-use-my-own-domain-9m77g7')"
>custom >custom
domain instructions</a> to learn how to use your own domain. domain instructions</a> to learn how to use your own domain.
</p> </p>
@ -65,7 +65,7 @@
Save Domains Save Domains
</v-button> </v-button>
<v-button v-if="workspaces.length > 1" color="white" class="group w-full sm:w-auto" :loading="loading" <v-button v-if="workspaces.length > 1" color="white" class="group w-full sm:w-auto" :loading="loading"
@click="deleteWorkspace(workspace)" @click="deleteWorkspace(workspace.id)"
> >
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 -mt-1 inline group-hover:text-red-700" fill="none" <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 -mt-1 inline group-hover:text-red-700" fill="none"
viewBox="0 0 24 24" stroke="currentColor" viewBox="0 0 24 24" stroke="currentColor"
@ -114,103 +114,88 @@
</div> </div>
</template> </template>
<script> <script setup>
import TextAreaInput from '../../components/forms/TextAreaInput.vue' import {watch} from "vue";
import axios from 'axios' import opnformConfig from "~/opnform.config.js";
import {fetchAllWorkspaces} from "~/stores/workspaces.js";
export default { const crisp = useCrisp()
components: { TextAreaInput }, const workspacesStore = useWorkspacesStore()
scrollToTop: false, const workspaces = computed(() => workspacesStore.getAll)
let loading = computed(() => workspacesStore.loading)
const metaTitle = 'Workspaces'
let form = useForm({
name: '',
emoji: ''
})
let workspaceModal = ref(false)
let customDomains = ''
let customDomainsLoading = ref(false)
setup () { let workspace = computed(() => workspacesStore.getCurrent)
const formsStore = useFormsStore() let customDomainsEnabled = computed(() => opnformConfig.custom_domains_enabled)
const workspacesStore = useWorkspacesStore()
return {
formsStore,
workspacesStore,
workspaces: computed(() => workspacesStore.content),
loading: computed(() => workspacesStore.loading)
}
},
data: () => ({ watch(() => workspace, () => {
metaTitle: 'Workspaces', initCustomDomains()
form: useForm({ })
name: '',
emoji: ''
}),
workspaceModal: false,
customDomains: '',
customDomainsLoading: false
}),
mounted () { onMounted(() => {
this.workspacesStore.loadIfEmpty() fetchAllWorkspaces()
this.initCustomDomains() initCustomDomains()
}, })
computed: { const saveChanges = () => {
workspace () { if (customDomainsLoading.value) return
return this.workspacesStore.getCurrent() customDomainsLoading.value = true
}, // Update the workspace custom domain
customDomainsEnabled () { opnFetch('/open/workspaces/' + workspace.value.id + '/custom-domains', {
return this.$config.custom_domains_enabled method:'PUT',
} custom_domains: customDomains.split('\n')
}, .map(domain => domain.trim())
.filter(domain => domain && domain.length > 0)
methods: { }).then((data) => {
saveChanges () { workspacesStore.addOrUpdate(data)
if (this.customDomainsLoading) return useAlert().success('Custom domains saved.')
this.customDomainsLoading = true }).catch((error) => {
// Update the workspace custom domain useAlert().error('Failed to update custom domains: ' + error.response.data.message)
axios.put('/api/open/workspaces/' + this.workspace.id + '/custom-domains', { }).finally(() => {
custom_domains: this.customDomains.split('\n') customDomainsLoading.value = false
.map(domain => domain.trim()) })
.filter(domain => domain && domain.length > 0)
}).then((response) => {
this.workspacesStore.addOrUpdate(response.data)
useAlert().success('Custom domains saved.')
}).catch((error) => {
useAlert().error('Failed to update custom domains: ' + error.response.data.message)
}).finally(() => {
this.customDomainsLoading = false
})
},
initCustomDomains () {
if (!this.workspace) return
this.customDomains = this.workspace.custom_domains.join('\n')
},
deleteWorkspace (workspace) {
if (this.workspaces.length <= 1) {
useAlert().error('You cannot delete your only workspace.')
return
}
useAlert().confirm('Do you really want to delete this workspace? All forms created in this workspace will be removed.', () => {
this.workspacesStore.delete(workspace.id).then(() => {
useAlert().success('Workspace successfully removed.')
})
})
},
isUrl (str) {
const pattern = new RegExp('^(https?:\\/\\/)?' + // protocol
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name
'((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path
'(\\?[;&a-z\\d%_.~+=-]*)?' + // query string
'(\\#[-a-z\\d_]*)?$', 'i') // fragment locator
return !!pattern.test(str)
},
async createWorkspace() {
const {data} = await this.form.post('/api/open/workspaces/create')
this.workspacesStore.load()
this.workspaceModal = false
}
},
watch: {
workspace () {
this.initCustomDomains()
}
}
} }
const initCustomDomains = () => {
if (!workspace || !workspace.value.custom_domains) return
customDomains = workspace.value.custom_domains.join('\n')
}
const deleteWorkspace = (workspaceId) => {
if (workspaces.length <= 1) {
useAlert().error('You cannot delete your only workspace.')
return
}
useAlert().confirm('Do you really want to delete this workspace? All forms created in this workspace will be removed.', () => {
opnFetch('/open/workspaces/' + workspaceId, {method:'DELETE'}).then((data) => {
useAlert().success('Workspace successfully removed.')
workspacesStore.remove(workspaceId)
})
})
}
const isUrl = (str) => {
const pattern = new RegExp('^(https?:\\/\\/)?' + // protocol
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name
'((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path
'(\\?[;&a-z\\d%_.~+=-]*)?' + // query string
'(\\#[-a-z\\d_]*)?$', 'i') // fragment locator
return !!pattern.test(str)
}
const createWorkspace = () => {
form.post('/open/workspaces/create').then((response) => {
fetchAllWorkspaces()
workspaceModal.value = false
useAlert().success('Workspace successfully created.')
})
}
</script> </script>

View File

@ -35,7 +35,7 @@ export const useWorkspacesStore = defineStore('workspaces', () => {
const remove = (itemId) => { const remove = (itemId) => {
contentStore.remove(itemId) contentStore.remove(itemId)
if (currentId.value === itemId) { if (currentId.value === itemId) {
setCurrentId(contentStore.length.value > 0 ? contentStore.getAll[0].id : null) setCurrentId(contentStore.length.value > 0 ? contentStore.getAll.value[0].id : null)
} }
} }