WIP
This commit is contained in:
parent
5c4dc2a3d6
commit
a3a9254665
|
@ -33,7 +33,7 @@ export function useFormInput (props, context, formPrefixKey = null) {
|
||||||
})
|
})
|
||||||
|
|
||||||
const hasError = computed(() => {
|
const hasError = computed(() => {
|
||||||
return hasValidation && props.form?.errors?.has(name)
|
return hasValidation && props.form?.errors?.has(props.name)
|
||||||
})
|
})
|
||||||
|
|
||||||
const compVal = computed({
|
const compVal = computed({
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<portal to="modals" :order="portalOrder">
|
<Teleport to="body">
|
||||||
<transition @leave="(el,done) => motions.backdrop.leave(done)">
|
<transition @leave="(el,done) => motions.backdrop.leave(done)">
|
||||||
<div v-if="show" v-motion="'backdrop'" :variants="motionFadeIn"
|
<div v-if="show" v-motion="'backdrop'" :variants="motionFadeIn"
|
||||||
class="fixed z-30 top-0 inset-0 px-4 sm:px-0 flex items-top justify-center bg-gray-700/75 w-full h-screen overflow-y-scroll"
|
class="fixed z-30 top-0 inset-0 px-4 sm:px-0 flex items-top justify-center bg-gray-700/75 w-full h-screen overflow-y-scroll"
|
||||||
|
@ -46,7 +46,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</portal>
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -71,9 +71,6 @@ export default {
|
||||||
},
|
},
|
||||||
closeable: {
|
closeable: {
|
||||||
default: true
|
default: true
|
||||||
},
|
|
||||||
portalOrder: {
|
|
||||||
default: 1
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -279,7 +279,7 @@ export default {
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
init () {
|
init () {
|
||||||
if (this.$route.name === 'forms.create' || this.$route.name === 'forms-create-guest') { // Set Default fields
|
if (this.$route.name === 'forms-create' || this.$route.name === 'forms-create-guest') { // Set Default fields
|
||||||
this.formFields = (this.form.properties.length > 0) ? clonedeep(this.form.properties) : this.getDefaultFields()
|
this.formFields = (this.form.properties.length > 0) ? clonedeep(this.form.properties) : this.getDefaultFields()
|
||||||
} else {
|
} else {
|
||||||
this.formFields = clonedeep(this.form.properties).map((field) => {
|
this.formFields = clonedeep(this.form.properties).map((field) => {
|
||||||
|
|
|
@ -41,19 +41,12 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { computed } from 'vue'
|
|
||||||
import Form from 'vform'
|
|
||||||
import Cookies from 'js-cookie'
|
import Cookies from 'js-cookie'
|
||||||
import { useAuthStore } from '../../../../stores/auth.js'
|
|
||||||
import OpenFormFooter from '../../OpenFormFooter.vue'
|
|
||||||
import Testimonials from '../../welcome/Testimonials.vue'
|
|
||||||
import ForgotPasswordModal from '../ForgotPasswordModal.vue'
|
import ForgotPasswordModal from '../ForgotPasswordModal.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'LoginForm',
|
name: 'LoginForm',
|
||||||
components: {
|
components: {
|
||||||
OpenFormFooter,
|
|
||||||
Testimonials,
|
|
||||||
ForgotPasswordModal
|
ForgotPasswordModal
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
@ -72,7 +65,7 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
data: () => ({
|
data: () => ({
|
||||||
form: new Form({
|
form: useForm({
|
||||||
email: '',
|
email: '',
|
||||||
password: ''
|
password: ''
|
||||||
}),
|
}),
|
||||||
|
@ -83,10 +76,10 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
async login () {
|
async login () {
|
||||||
// Submit the form.
|
// Submit the form.
|
||||||
const { data } = await this.form.post('/api/login')
|
const data = await this.form.post('login')
|
||||||
|
|
||||||
// Save the token.
|
// Save the token.
|
||||||
this.authStore.saveToken(data.token, this.remember)
|
this.authStore.setToken(data.token, this.remember)
|
||||||
|
|
||||||
// Fetch the user.
|
// Fetch the user.
|
||||||
await this.authStore.fetchUser()
|
await this.authStore.fetchUser()
|
||||||
|
|
|
@ -142,7 +142,7 @@ export default {
|
||||||
if (this.isQuick) {
|
if (this.isQuick) {
|
||||||
this.$emit('afterQuickLogin')
|
this.$emit('afterQuickLogin')
|
||||||
} else {
|
} else {
|
||||||
this.$router.push({ name: 'forms.create' })
|
this.$router.push({ name: 'forms-create' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,7 +67,7 @@
|
||||||
<!-- <v-button v-if="!authenticated" class="mr-2 block" :to="{ name: 'forms-create-guest' }" :arrow="true">-->
|
<!-- <v-button v-if="!authenticated" class="mr-2 block" :to="{ name: 'forms-create-guest' }" :arrow="true">-->
|
||||||
<!-- Get started for free-->
|
<!-- Get started for free-->
|
||||||
<!-- </v-button>-->
|
<!-- </v-button>-->
|
||||||
<!-- <v-button v-else class="mr-2 block" :to="{ name: 'forms.create' }" :arrow="true">-->
|
<!-- <v-button v-else class="mr-2 block" :to="{ name: 'forms-create' }" :arrow="true">-->
|
||||||
<!-- Get started for free-->
|
<!-- Get started for free-->
|
||||||
<!-- </v-button>-->
|
<!-- </v-button>-->
|
||||||
<!-- <v-button color="light-gray" class="mr-1 block" :to="{ name: 'aiformbuilder' }">-->
|
<!-- <v-button color="light-gray" class="mr-1 block" :to="{ name: 'aiformbuilder' }">-->
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
|
||||||
|
function arrayWrap(value) {
|
||||||
|
return Array.isArray(value) ? value : [value];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Errors {
|
||||||
|
constructor() {
|
||||||
|
this.errors = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
set(field, messages = undefined) {
|
||||||
|
if (typeof field === 'object') {
|
||||||
|
this.errors = field;
|
||||||
|
} else {
|
||||||
|
this.set({ ...this.errors, [field]: arrayWrap(messages) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
all() {
|
||||||
|
return this.errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
has(field) {
|
||||||
|
return Object.prototype.hasOwnProperty.call(this.errors, field);
|
||||||
|
}
|
||||||
|
|
||||||
|
hasAny(...fields) {
|
||||||
|
return fields.some(field => this.has(field));
|
||||||
|
}
|
||||||
|
|
||||||
|
any() {
|
||||||
|
return Object.keys(this.errors).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get(field) {
|
||||||
|
if (this.has(field)) {
|
||||||
|
return this.getAll(field)[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getAll(field) {
|
||||||
|
return arrayWrap(this.errors[field] || []);
|
||||||
|
}
|
||||||
|
|
||||||
|
only(...fields) {
|
||||||
|
const messages = [];
|
||||||
|
|
||||||
|
fields.forEach((field) => {
|
||||||
|
const message = this.get(field);
|
||||||
|
if (message) {
|
||||||
|
messages.push(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
flatten() {
|
||||||
|
return Object.values(this.errors).reduce((a, b) => a.concat(b), []);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(field = undefined) {
|
||||||
|
const errors = {};
|
||||||
|
|
||||||
|
if (field) {
|
||||||
|
Object.keys(this.errors).forEach((key) => {
|
||||||
|
if (key !== field) {
|
||||||
|
errors[key] = this.errors[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.set(errors);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,175 @@
|
||||||
|
import {serialize} from 'object-to-formdata';
|
||||||
|
import Errors from './Errors';
|
||||||
|
import cloneDeep from 'clone-deep';
|
||||||
|
import {useOpnFetch} from "~/composables/useOpnFetch.js";
|
||||||
|
|
||||||
|
function hasFiles(data) {
|
||||||
|
return data instanceof File ||
|
||||||
|
data instanceof Blob ||
|
||||||
|
data instanceof FileList ||
|
||||||
|
(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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(data) {
|
||||||
|
this.originalData = Object.assign({}, this.originalData, cloneDeep(data));
|
||||||
|
Object.assign(this, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
fill(data = {}) {
|
||||||
|
this.keys().forEach((key) => {
|
||||||
|
this[key] = data[key];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return this.keys().reduce((data, key) => (
|
||||||
|
{...data, [key]: this[key]}
|
||||||
|
), {});
|
||||||
|
}
|
||||||
|
|
||||||
|
keys() {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
finishProcessing() {
|
||||||
|
this.busy = false;
|
||||||
|
this.successful = true;
|
||||||
|
this.recentlySuccessful = true;
|
||||||
|
this.recentlySuccessfulTimeoutId = setTimeout(() => {
|
||||||
|
this.recentlySuccessful = false;
|
||||||
|
}, Form.recentlySuccessfulTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.errors.clear();
|
||||||
|
this.successful = false;
|
||||||
|
this.recentlySuccessful = false;
|
||||||
|
clearTimeout(this.recentlySuccessfulTimeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
Object.keys(this)
|
||||||
|
.filter(key => !Form.ignore.includes(key))
|
||||||
|
.forEach((key) => {
|
||||||
|
this[key] = deepCopy(this.originalData[key]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get(url, config = {}) {
|
||||||
|
return this.submit('get', url, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
post(url, config = {}) {
|
||||||
|
return this.submit('post', url, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
patch(url, config = {}) {
|
||||||
|
return this.submit('patch', url, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
put(url, config = {}) {
|
||||||
|
return this.submit('put', url, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(url, config = {}) {
|
||||||
|
return this.submit('delete', url, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
submit(method, url, config = {}) {
|
||||||
|
this.startProcessing();
|
||||||
|
|
||||||
|
config = {
|
||||||
|
body: {},
|
||||||
|
params: {},
|
||||||
|
url: url,
|
||||||
|
method: method,
|
||||||
|
...config
|
||||||
|
};
|
||||||
|
|
||||||
|
if (method.toLowerCase() === 'get') {
|
||||||
|
config.params = {...this.data(), ...config.params};
|
||||||
|
} else {
|
||||||
|
config.body = {...this.data(), ...config.data};
|
||||||
|
|
||||||
|
if (hasFiles(config.data) && !config.transformRequest) {
|
||||||
|
config.transformRequest = [data => serialize(data)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
useOpnFetch(config.url, config)
|
||||||
|
.then(({data, error}) => {
|
||||||
|
if (error.value) {
|
||||||
|
this.handleErrors(error);
|
||||||
|
reject(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.finishProcessing();
|
||||||
|
resolve(data.value);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleErrors(error) {
|
||||||
|
this.busy = false;
|
||||||
|
|
||||||
|
if (error.value) {
|
||||||
|
this.errors.set(this.extractErrors(error.value.data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extractErrors(data) {
|
||||||
|
if (!data || typeof data !== 'object') {
|
||||||
|
return {error: Form.errorMessage};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.errors) {
|
||||||
|
return {...data.errors};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.message) {
|
||||||
|
return {error: data.message};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {...data};
|
||||||
|
}
|
||||||
|
|
||||||
|
onKeydown(event) {
|
||||||
|
const target = event.target;
|
||||||
|
|
||||||
|
if (target.name) {
|
||||||
|
this.errors.clear(target.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Form;
|
|
@ -0,0 +1,5 @@
|
||||||
|
import Form from "~/composables/lib/vForm/Form.js"
|
||||||
|
|
||||||
|
export const useForm = (formData) => {
|
||||||
|
return new Form(formData)
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import config from "~/opnform.config.js";
|
||||||
|
|
||||||
|
export const useOpnFetch = (request, opts) => {
|
||||||
|
return useFetch(request, { baseURL: config.api_url, ...opts })
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
import {useAuthStore} from "../../resources/js/stores/auth.js";
|
||||||
|
|
||||||
|
export default defineNuxtRouteMiddleware((to, from) => {
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
if (!authStore.user?.admin) {
|
||||||
|
navigateTo({ name: 'home' })
|
||||||
|
}
|
||||||
|
})
|
|
@ -0,0 +1,11 @@
|
||||||
|
import {useAuthStore} from "../../resources/js/stores/auth.js";
|
||||||
|
|
||||||
|
export default defineNuxtRouteMiddleware((to, from) => {
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
if (!authStore.check) {
|
||||||
|
useCookie('intended_url').value = to.path
|
||||||
|
|
||||||
|
navigateTo({ name: 'login' })
|
||||||
|
}
|
||||||
|
})
|
|
@ -0,0 +1,5 @@
|
||||||
|
export default defineNuxtRouteMiddleware((to, from) => {
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
authStore.loadTokenFromCookie()
|
||||||
|
useAuthStore().fetchUserIfNotFetched()
|
||||||
|
})
|
|
@ -1,6 +1,6 @@
|
||||||
export default {
|
export default {
|
||||||
"appName": "OpnForm",
|
"app_ame": "OpnForm",
|
||||||
"app_url": "https://opnform.test",
|
"api_url": "https://opnform.test/api",
|
||||||
"locale": "en",
|
"locale": "en",
|
||||||
"locales": {"en": "EN"},
|
"locales": {"en": "EN"},
|
||||||
"githubAuth": null,
|
"githubAuth": null,
|
||||||
|
|
|
@ -24,14 +24,13 @@
|
||||||
"date-fns": "^2.28.0",
|
"date-fns": "^2.28.0",
|
||||||
"debounce": "^1.2.1",
|
"debounce": "^1.2.1",
|
||||||
"fuse.js": "^6.4.6",
|
"fuse.js": "^6.4.6",
|
||||||
"js-cookie": "^2.2.1",
|
|
||||||
"js-sha256": "^0.9.0",
|
"js-sha256": "^0.9.0",
|
||||||
"libphonenumber-js": "^1.10.44",
|
"libphonenumber-js": "^1.10.44",
|
||||||
|
"object-to-formdata": "^4.5.1",
|
||||||
"prismjs": "^1.24.1",
|
"prismjs": "^1.24.1",
|
||||||
"qrcode": "^1.5.1",
|
"qrcode": "^1.5.1",
|
||||||
"query-builder-vue-3": "^1.0.1",
|
"query-builder-vue-3": "^1.0.1",
|
||||||
"tinymotion": "^0.2.0",
|
"tinymotion": "^0.2.0",
|
||||||
"vform": "^2.1.1",
|
|
||||||
"vue": "^3.2.13",
|
"vue": "^3.2.13",
|
||||||
"vue-chartjs": "^5.2.0",
|
"vue-chartjs": "^5.2.0",
|
||||||
"vue-codemirror": "^4.0.6",
|
"vue-codemirror": "^4.0.6",
|
||||||
|
@ -6751,11 +6750,6 @@
|
||||||
"jiti": "bin/jiti.js"
|
"jiti": "bin/jiti.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/js-cookie": {
|
|
||||||
"version": "2.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz",
|
|
||||||
"integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ=="
|
|
||||||
},
|
|
||||||
"node_modules/js-sha256": {
|
"node_modules/js-sha256": {
|
||||||
"version": "0.9.0",
|
"version": "0.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz",
|
||||||
|
@ -8094,6 +8088,11 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/object-to-formdata": {
|
||||||
|
"version": "4.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-to-formdata/-/object-to-formdata-4.5.1.tgz",
|
||||||
|
"integrity": "sha512-QiM9D0NiU5jV6J6tjE1g7b4Z2tcUnKs1OPUi4iMb2zH+7jwlcUrASghgkFk9GtzqNNq8rTQJtT8AzjBAvLoNMw=="
|
||||||
|
},
|
||||||
"node_modules/ofetch": {
|
"node_modules/ofetch": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.3.3.tgz",
|
||||||
|
@ -10950,15 +10949,6 @@
|
||||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vform": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/vform/-/vform-2.1.2.tgz",
|
|
||||||
"integrity": "sha512-Nobg/0ckWHYQAjJJqucOyRv/a3enO40f087KBpkSUCl0eRQTE8qmUqk/l5gmbxhD8UO3A8dDiSRWKmKF8+l4YQ==",
|
|
||||||
"peerDependencies": {
|
|
||||||
"axios": "*",
|
|
||||||
"vue": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "5.0.7",
|
"version": "5.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.0.7.tgz",
|
||||||
|
|
|
@ -36,14 +36,13 @@
|
||||||
"date-fns": "^2.28.0",
|
"date-fns": "^2.28.0",
|
||||||
"debounce": "^1.2.1",
|
"debounce": "^1.2.1",
|
||||||
"fuse.js": "^6.4.6",
|
"fuse.js": "^6.4.6",
|
||||||
"js-cookie": "^2.2.1",
|
|
||||||
"js-sha256": "^0.9.0",
|
"js-sha256": "^0.9.0",
|
||||||
"libphonenumber-js": "^1.10.44",
|
"libphonenumber-js": "^1.10.44",
|
||||||
|
"object-to-formdata": "^4.5.1",
|
||||||
"prismjs": "^1.24.1",
|
"prismjs": "^1.24.1",
|
||||||
"qrcode": "^1.5.1",
|
"qrcode": "^1.5.1",
|
||||||
"query-builder-vue-3": "^1.0.1",
|
"query-builder-vue-3": "^1.0.1",
|
||||||
"tinymotion": "^0.2.0",
|
"tinymotion": "^0.2.0",
|
||||||
"vform": "^2.1.1",
|
|
||||||
"vue": "^3.2.13",
|
"vue": "^3.2.13",
|
||||||
"vue-chartjs": "^5.2.0",
|
"vue-chartjs": "^5.2.0",
|
||||||
"vue-codemirror": "^4.0.6",
|
"vue-codemirror": "^4.0.6",
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
<v-button v-if="!authenticated" class="mr-1" :to="{ name: 'forms-create-guest' }" :arrow="true">
|
<v-button v-if="!authenticated" class="mr-1" :to="{ name: 'forms-create-guest' }" :arrow="true">
|
||||||
Get started for free
|
Get started for free
|
||||||
</v-button>
|
</v-button>
|
||||||
<v-button v-else class="mr-1" :to="{ name: 'forms.create' }" :arrow="true">
|
<v-button v-else class="mr-1" :to="{ name: 'forms-create' }" :arrow="true">
|
||||||
Get started for free
|
Get started for free
|
||||||
</v-button>
|
</v-button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -273,7 +273,7 @@
|
||||||
<!--<!– <v-button v-if="!authenticated" class="mr-1" :to="{ name: 'forms-create-guest' }" :arrow="true">–>-->
|
<!--<!– <v-button v-if="!authenticated" class="mr-1" :to="{ name: 'forms-create-guest' }" :arrow="true">–>-->
|
||||||
<!--<!– Get started for free–>-->
|
<!--<!– Get started for free–>-->
|
||||||
<!--<!– </v-button>–>-->
|
<!--<!– </v-button>–>-->
|
||||||
<!--<!– <v-button v-else class="mr-1" :to="{ name: 'forms.create' }" :arrow="true">–>-->
|
<!--<!– <v-button v-else class="mr-1" :to="{ name: 'forms-create' }" :arrow="true">–>-->
|
||||||
<!--<!– Get started for free–>-->
|
<!--<!– Get started for free–>-->
|
||||||
<!--<!– </v-button>–>-->
|
<!--<!– </v-button>–>-->
|
||||||
<!--<!– </div>–>-->
|
<!--<!– </div>–>-->
|
||||||
|
@ -446,7 +446,7 @@
|
||||||
<v-button v-if="!authenticated" class="mr-1" :to="{ name: 'forms-create-guest' }" :arrow="true">
|
<v-button v-if="!authenticated" class="mr-1" :to="{ name: 'forms-create-guest' }" :arrow="true">
|
||||||
Get started for free
|
Get started for free
|
||||||
</v-button>
|
</v-button>
|
||||||
<v-button v-else class="mr-1" :to="{ name: 'forms.create' }" :arrow="true">
|
<v-button v-else class="mr-1" :to="{ name: 'forms-create' }" :arrow="true">
|
||||||
Get started for free
|
Get started for free
|
||||||
</v-button>
|
</v-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<h2 class="flex-grow text-gray-900">
|
<h2 class="flex-grow text-gray-900">
|
||||||
Your Forms
|
Your Forms
|
||||||
</h2>
|
</h2>
|
||||||
<v-button v-track.create_form_click :to="{name:'forms.create'}">
|
<v-button v-track.create_form_click :to="{name:'forms-create'}">
|
||||||
<svg class="w-4 h-4 text-white inline mr-1 -mt-1" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg class="w-4 h-4 text-white inline mr-1 -mt-1" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M6.99996 1.1665V12.8332M1.16663 6.99984H12.8333" stroke="currentColor" stroke-width="1.67" stroke-linecap="round" stroke-linejoin="round" />
|
<path d="M6.99996 1.1665V12.8332M1.16663 6.99984H12.8333" stroke="currentColor" stroke-width="1.67" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
</svg>
|
</svg>
|
||||||
|
@ -48,7 +48,7 @@
|
||||||
<div v-if="isFilteringForms && enrichedForms.length === 0 && searchForm.search" class="mt-2 w-full text-center">
|
<div v-if="isFilteringForms && enrichedForms.length === 0 && searchForm.search" class="mt-2 w-full text-center">
|
||||||
Your search "{{ searchForm.search }}" did not match any forms. Please try again.
|
Your search "{{ searchForm.search }}" did not match any forms. Please try again.
|
||||||
</div>
|
</div>
|
||||||
<v-button v-if="forms.length === 0" v-track.create_form_click class="mt-4" :to="{name:'forms.create'}">
|
<v-button v-if="forms.length === 0" v-track.create_form_click class="mt-4" :to="{name:'forms-create'}">
|
||||||
<svg class="w-4 h-4 text-white inline mr-1 -mt-1" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg class="w-4 h-4 text-white inline mr-1 -mt-1" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M6.99996 1.1665V12.8332M1.16663 6.99984H12.8333" stroke="currentColor" stroke-width="1.67" stroke-linecap="round" stroke-linejoin="round" />
|
<path d="M6.99996 1.1665V12.8332M1.16663 6.99984H12.8333" stroke="currentColor" stroke-width="1.67" stroke-linecap="round" stroke-linejoin="round" />
|
||||||
</svg>
|
</svg>
|
||||||
|
@ -119,7 +119,7 @@ const loadForms = function () {
|
||||||
const formsStore = useFormsStore()
|
const formsStore = useFormsStore()
|
||||||
const workspacesStore = useWorkspacesStore()
|
const workspacesStore = useWorkspacesStore()
|
||||||
formsStore.startLoading()
|
formsStore.startLoading()
|
||||||
workspacesStore.loadIfEmpty().then(() => {
|
return workspacesStore.loadIfEmpty().then(() => {
|
||||||
formsStore.loadIfEmpty(workspacesStore.currentId)
|
formsStore.loadIfEmpty(workspacesStore.currentId)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -127,10 +127,6 @@ const loadForms = function () {
|
||||||
export default {
|
export default {
|
||||||
components: { OpenFormFooter, TextInput, ExtraMenu },
|
components: { OpenFormFooter, TextInput, ExtraMenu },
|
||||||
|
|
||||||
beforeRouteEnter (to, from, next) {
|
|
||||||
loadForms()
|
|
||||||
next()
|
|
||||||
},
|
|
||||||
middleware: 'auth',
|
middleware: 'auth',
|
||||||
|
|
||||||
props: {
|
props: {
|
||||||
|
@ -138,10 +134,11 @@ export default {
|
||||||
metaDescription: { type: String, default: 'All of your OpnForm are here. Create new forms, or update your existing one!' }
|
metaDescription: { type: String, default: 'All of your OpnForm are here. Create new forms, or update your existing one!' }
|
||||||
},
|
},
|
||||||
|
|
||||||
setup () {
|
async setup () {
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const formsStore = useFormsStore()
|
const formsStore = useFormsStore()
|
||||||
const workspacesStore = useWorkspacesStore()
|
const workspacesStore = useWorkspacesStore()
|
||||||
|
loadForms()
|
||||||
return {
|
return {
|
||||||
formsStore,
|
formsStore,
|
||||||
workspacesStore,
|
workspacesStore,
|
||||||
|
|
|
@ -47,7 +47,7 @@
|
||||||
<v-button v-if="!authenticated" class="mr-1" :to="{ name: 'forms-create-guest' }" :arrow="true">
|
<v-button v-if="!authenticated" class="mr-1" :to="{ name: 'forms-create-guest' }" :arrow="true">
|
||||||
Create a form for FREE
|
Create a form for FREE
|
||||||
</v-button>
|
</v-button>
|
||||||
<v-button v-else class="mr-1" :to="{ name: 'forms.create' }" :arrow="true">
|
<v-button v-else class="mr-1" :to="{ name: 'forms-create' }" :arrow="true">
|
||||||
Create a form for FREE
|
Create a form for FREE
|
||||||
</v-button>
|
</v-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { ofetch } from 'ofetch'
|
||||||
|
import {useAuthStore} from "~/stores/auth.js";
|
||||||
|
|
||||||
|
function addAuthHeader(request, options) {
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
if (authStore.check) {
|
||||||
|
options.headers = { Authorization: `Bearer ${authStore.token}` }
|
||||||
|
console.log('addidng auth',options)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addPasswordToFormRequest (request) {
|
||||||
|
const url = request.url
|
||||||
|
if (!url || !url.startsWith('/api/forms/')) return
|
||||||
|
|
||||||
|
const slug = url.split('/')[3]
|
||||||
|
const passwordCookie = useCookie('password-' + slug, { maxAge: 60 * 60 * 24 * 30 }) // 30 days
|
||||||
|
if (slug !== undefined && slug !== '' && passwordCookie.value !== undefined) {
|
||||||
|
request.headers['form-password'] = passwordCookie.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineNuxtPlugin((_nuxtApp) => {
|
||||||
|
globalThis.$fetch = ofetch.create({
|
||||||
|
onRequest ({ request, options }) {
|
||||||
|
// TODO: check that it's our own domain called
|
||||||
|
addAuthHeader(request, options)
|
||||||
|
addPasswordToFormRequest(request)
|
||||||
|
},
|
||||||
|
onResponseError ({ response }) {
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
const { status } = response
|
||||||
|
|
||||||
|
if (status === 401 && authStore.check) {
|
||||||
|
// TODO: check that it's our own domain called
|
||||||
|
authStore.logout()
|
||||||
|
useRouter().push({ name: 'login' })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status >= 500) {
|
||||||
|
console.error('Request error', status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
|
@ -1,15 +1,14 @@
|
||||||
import {defineStore} from 'pinia'
|
import {defineStore} from 'pinia'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import Cookies from 'js-cookie'
|
|
||||||
|
|
||||||
export const useAuthStore = defineStore('auth', {
|
export const useAuthStore = defineStore('auth', {
|
||||||
state: () => ({
|
state: () => {
|
||||||
|
return {
|
||||||
|
token: null,
|
||||||
|
admin_token: null,
|
||||||
user: null,
|
user: null,
|
||||||
token: Cookies.get('token'),
|
}
|
||||||
|
},
|
||||||
// For admin impersonation
|
|
||||||
admin_token: Cookies.get('admin_token') ?? null
|
|
||||||
}),
|
|
||||||
getters: {
|
getters: {
|
||||||
check: (state) => (state.user !== null && state.user !== undefined),
|
check: (state) => (state.user !== null && state.user !== undefined),
|
||||||
isImpersonating: (state) => (state.admin_token !== null && state.admin_token !== undefined)
|
isImpersonating: (state) => (state.admin_token !== null && state.admin_token !== undefined)
|
||||||
|
@ -17,21 +16,28 @@ export const useAuthStore = defineStore('auth', {
|
||||||
actions: {
|
actions: {
|
||||||
// Stores admin token temporarily for impersonation
|
// Stores admin token temporarily for impersonation
|
||||||
startImpersonating() {
|
startImpersonating() {
|
||||||
this.admin_token = this.token
|
this.setAdminToken(this.token)
|
||||||
Cookies.set('admin_token', this.token, { expires: 365 })
|
|
||||||
},
|
},
|
||||||
// Stop admin impersonation
|
// Stop admin impersonation
|
||||||
stopImpersonating() {
|
stopImpersonating() {
|
||||||
this.token = this.admin_token
|
this.token = this.admin_token
|
||||||
this.admin_token = null
|
this.admin_token = null
|
||||||
Cookies.set('token', this.token, { expires: 365 })
|
|
||||||
Cookies.remove('admin_token')
|
|
||||||
this.fetchUser()
|
this.fetchUser()
|
||||||
},
|
},
|
||||||
|
|
||||||
saveToken (token, remember) {
|
setToken(token) {
|
||||||
|
useCookie('token', {maxAge: 60 * 60 * 24 * 30}).value = token
|
||||||
this.token = token
|
this.token = token
|
||||||
Cookies.set('token', token, { expires: remember ? 365 : null })
|
},
|
||||||
|
|
||||||
|
setAdminToken(token) {
|
||||||
|
useCookie('admin_token', {maxAge: 60 * 60 * 24 * 30}).value = token
|
||||||
|
this.admin_token = token
|
||||||
|
},
|
||||||
|
|
||||||
|
loadTokenFromCookie() {
|
||||||
|
this.token = useCookie('token').value
|
||||||
|
this.admin_token = useCookie('admin_token').value
|
||||||
},
|
},
|
||||||
|
|
||||||
async fetchUser() {
|
async fetchUser() {
|
||||||
|
@ -42,8 +48,13 @@ export const useAuthStore = defineStore('auth', {
|
||||||
|
|
||||||
return data
|
return data
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.token = null
|
this.setToken(null)
|
||||||
Cookies.remove('token')
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchUserIfNotFetched() {
|
||||||
|
if (this.user === null && this.token) {
|
||||||
|
await this.fetchUser()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -56,16 +67,25 @@ export const useAuthStore = defineStore('auth', {
|
||||||
if (!this.user) return
|
if (!this.user) return
|
||||||
useAmplitude().setUser(this.user)
|
useAmplitude().setUser(this.user)
|
||||||
useCrisp().setUser(this.user)
|
useCrisp().setUser(this.user)
|
||||||
|
|
||||||
|
// Init sentry
|
||||||
|
Sentry.configureScope((scope) => {
|
||||||
|
scope.setUser({
|
||||||
|
id: this.user.id,
|
||||||
|
email: this.user.email,
|
||||||
|
subscription: this.user?.is_subscribed
|
||||||
|
})
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
async logout() {
|
async logout() {
|
||||||
try {
|
try {
|
||||||
await axios.post('/api/logout')
|
await axios.post('/api/logout')
|
||||||
} catch (e) { }
|
} catch (e) {
|
||||||
|
}
|
||||||
|
|
||||||
this.user = null
|
this.user = null
|
||||||
this.token = null
|
this.setToken(null)
|
||||||
Cookies.remove('token')
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async fetchOauthUrl(provider) {
|
async fetchOauthUrl(provider) {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import {defineStore} from 'pinia'
|
import {defineStore} from 'pinia'
|
||||||
import axios from 'axios'
|
import {useOpnFetch} from "~/composables/useOpnFetch.js";
|
||||||
|
|
||||||
export const formsEndpoint = '/api/open/workspaces/{workspaceId}/forms'
|
export const formsEndpoint = '/open/workspaces/{workspaceId}/forms'
|
||||||
export let currentPage = 1
|
export let currentPage = 1
|
||||||
|
|
||||||
export const useFormsStore = defineStore('forms', {
|
export const useFormsStore = defineStore('forms', {
|
||||||
|
@ -56,8 +56,8 @@ export const useFormsStore = defineStore('forms', {
|
||||||
},
|
},
|
||||||
load(workspaceId) {
|
load(workspaceId) {
|
||||||
this.startLoading()
|
this.startLoading()
|
||||||
return axios.get(formsEndpoint.replace('{workspaceId}', workspaceId)+'?page='+currentPage).then((response) => {
|
return useOpnFetch(formsEndpoint.replace('{workspaceId}', workspaceId) + '?page=' + currentPage).get().then((response) => {
|
||||||
if (currentPage == 1) {
|
if (currentPage === 1) {
|
||||||
this.set(response.data.data)
|
this.set(response.data.data)
|
||||||
} else {
|
} else {
|
||||||
this.append(response.data.data)
|
this.append(response.data.data)
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import axios from 'axios'
|
|
||||||
import {defineStore} from 'pinia'
|
import {defineStore} from 'pinia'
|
||||||
export const workspaceEndpoint = '/api/open/workspaces/'
|
import {useOpnFetch} from "~/composables/useOpnFetch.js"
|
||||||
|
import {useStorage} from "@vueuse/core"
|
||||||
|
|
||||||
const localStorageCurrentWorkspaceKey = 'currentWorkspace'
|
export const workspaceEndpoint = 'open/workspaces/'
|
||||||
|
|
||||||
|
const storedWorkspaceId = useStorage('currentWorkspace', 0)
|
||||||
|
|
||||||
export const useWorkspacesStore = defineStore('workspaces', {
|
export const useWorkspacesStore = defineStore('workspaces', {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
|
@ -26,38 +28,34 @@ export const useWorkspacesStore = defineStore('workspaces', {
|
||||||
if (this.currentId == null && this.content.length > 0) {
|
if (this.currentId == null && this.content.length > 0) {
|
||||||
// If one only, set it
|
// If one only, set it
|
||||||
if (this.content.length === 1) {
|
if (this.content.length === 1) {
|
||||||
this.currentId = items[0].id
|
this.setCurrentId(items[0].id)
|
||||||
localStorage.setItem(localStorageCurrentWorkspaceKey, this.currentId)
|
} else if (storedWorkspaceId && this.content.find(item => item.id === parseInt(storedWorkspaceId.value))) {
|
||||||
} else if (localStorage.getItem(localStorageCurrentWorkspaceKey) && this.content.find(item => item.id === parseInt(localStorage.getItem(localStorageCurrentWorkspaceKey)))) {
|
|
||||||
// Check local storage for current workspace, or take first
|
// Check local storage for current workspace, or take first
|
||||||
this.currentId = parseInt(localStorage.getItem(localStorageCurrentWorkspaceKey))
|
this.setCurrentId(parseInt(storedWorkspaceId.value))
|
||||||
localStorage.setItem(localStorageCurrentWorkspaceKey, this.currentId)
|
|
||||||
} else {
|
} else {
|
||||||
// Else, take first
|
// Else, take first
|
||||||
this.currentId = items[0].id
|
this.setCurrentId(items[0].id)
|
||||||
localStorage.setItem(localStorageCurrentWorkspaceKey, this.currentId)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
localStorage.removeItem(localStorageCurrentWorkspaceKey)
|
this.setCurrentId(null)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setCurrentId(id) {
|
setCurrentId(id) {
|
||||||
this.currentId = id
|
this.currentId = id
|
||||||
localStorage.setItem(localStorageCurrentWorkspaceKey, id)
|
storedWorkspaceId.value = id
|
||||||
},
|
},
|
||||||
addOrUpdate(item) {
|
addOrUpdate(item) {
|
||||||
this.content = this.content.filter((val) => val.id !== item.id)
|
this.content = this.content.filter((val) => val.id !== item.id)
|
||||||
this.content.push(item)
|
this.content.push(item)
|
||||||
if (this.currentId == null) {
|
if (this.currentId == null) {
|
||||||
this.currentId = item.id
|
this.currentId = item.id
|
||||||
localStorage.setItem(localStorageCurrentWorkspaceKey, this.currentId)
|
storedWorkspaceId.value = this.currentId
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
remove(itemId) {
|
remove(itemId) {
|
||||||
this.content = this.content.filter((val) => val.id !== itemId)
|
this.content = this.content.filter((val) => val.id !== itemId)
|
||||||
if (this.currentId === itemId) {
|
if (this.currentId === itemId) {
|
||||||
this.currentId = this.content.length > 0 ? this.content[0].id : null
|
this.setCurrentId(this.content.length > 0 ? this.content[0].id : null)
|
||||||
localStorage.setItem(localStorageCurrentWorkspaceKey, this.currentId)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
startLoading() {
|
startLoading() {
|
||||||
|
@ -73,7 +71,7 @@ export const useWorkspacesStore = defineStore('workspaces', {
|
||||||
load() {
|
load() {
|
||||||
this.set([])
|
this.set([])
|
||||||
this.startLoading()
|
this.startLoading()
|
||||||
return axios.get(workspaceEndpoint).then((response) => {
|
return useOpnFetch(workspaceEndpoint).then((response) => {
|
||||||
this.set(response.data)
|
this.set(response.data)
|
||||||
this.stopLoading()
|
this.stopLoading()
|
||||||
})
|
})
|
||||||
|
@ -86,7 +84,7 @@ export const useWorkspacesStore = defineStore('workspaces', {
|
||||||
},
|
},
|
||||||
delete(id) {
|
delete(id) {
|
||||||
this.startLoading()
|
this.startLoading()
|
||||||
return axios.delete(workspaceEndpoint + id).then((response) => {
|
return useOpnFetch(workspaceEndpoint + id, {method: 'DELETE'}).then((response) => {
|
||||||
this.remove(response.data.workspace_id)
|
this.remove(response.data.workspace_id)
|
||||||
this.stopLoading()
|
this.stopLoading()
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue