opnform-host-nginx/client/pages/home.vue

318 lines
11 KiB
Vue
Raw Normal View History

2023-12-09 15:47:03 +01:00
<template>
<div class="bg-white">
<div class="flex bg-gray-50 pb-5 border-b">
<div class="w-full md:w-4/5 lg:w-3/5 md:mx-auto md:max-w-4xl p-4">
<div class="pt-4 pb-0">
<div class="flex">
<h2 class="flex-grow text-gray-900">
Your Forms
</h2>
<v-button
Readonly User (#637) * Readonly User * Refactor FormPolicy and TemplatePolicy to centralize write operation logic - Introduced a private method `canPerformWriteOperation` in both FormPolicy and TemplatePolicy to encapsulate the logic for determining if a user can perform write operations on the respective models. - Updated the `update`, `delete`, `restore`, and `forceDelete` methods in FormPolicy to use the new method for improved readability and maintainability. - Simplified the `update` and `delete` methods in TemplatePolicy to leverage the centralized write operation logic. This refactoring enhances code clarity and reduces duplication across policy classes. * Refactor user and workspace permissions handling - Updated FormController to authorize form creation based on workspace context. - Removed the `is_readonly` attribute from UserResource and integrated it into WorkspaceResource for better encapsulation. - Refactored User model to eliminate the `getIsReadonlyAttribute` method, shifting readonly logic to the Workspace model. - Adjusted FormPolicy and TemplatePolicy to utilize workspace readonly checks for user permissions. - Updated various frontend components to reference workspace readonly status instead of user readonly status, enhancing clarity and consistency in permission handling. These changes improve the management of user permissions in relation to workspaces, ensuring a more robust and maintainable authorization system. * Fix isReadonlyUser * fix pint --------- Co-authored-by: Julien Nahum <julien@nahum.net>
2024-12-30 14:35:23 +01:00
v-if="!workspace.is_readonly"
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"
>
<path
d="M6.99996 1.1665V12.8332M1.16663 6.99984H12.8333"
stroke="currentColor"
stroke-width="1.67"
stroke-linecap="round"
stroke-linejoin="round"
/>
2023-12-09 15:47:03 +01:00
</svg>
Create a new form
</v-button>
</div>
<small class="flex text-gray-500">Manage your forms and submissions.</small>
</div>
</div>
</div>
<div class="flex bg-white">
2024-01-17 14:26:31 +01:00
<div class="w-full md:w-4/5 lg:w-3/5 md:mx-auto md:max-w-4xl">
<div class="mt-4 pb-0">
<text-input
v-if="forms.length > 0"
v-model="search"
class="mb-6 px-4"
name="search"
label="Search a form"
placeholder="Name of form to search"
2023-12-09 15:47:03 +01:00
/>
<div
v-if="allTags.length > 0"
class="mb-4 px-6"
>
<div
v-for="tag in allTags"
:key="tag"
:class="[
'inline-flex items-center rounded-full px-2 py-1 text-xs font-medium ring-1 ring-inset cursor-pointer mr-2',
{
'bg-blue-50 text-blue-600 ring-blue-500/10 dark:bg-blue-400':
selectedTags.has(tag),
'bg-gray-50 text-gray-600 ring-gray-500/10 dark:bg-gray-700 hover:bg-blue-50 hover:text-blue-600 hover:ring-blue-500/10 hover:dark:bg-blue-400':
!selectedTags.has(tag),
},
]"
title="Click for filter by tag(s)"
@click="onTagClick(tag)"
2023-12-09 15:47:03 +01:00
>
{{ tag }}
</div>
</div>
<div
v-if="!formsLoading && enrichedForms.length === 0"
class="flex flex-wrap justify-center max-w-4xl"
>
<img
class="w-56"
src="/img/pages/forms/search_notfound.png"
alt="search-not-found"
>
2023-12-09 15:47:03 +01:00
<h3 class="w-full mt-4 text-center text-gray-900 font-semibold">
No forms found
</h3>
<div
v-if="isFilteringForms && enrichedForms.length === 0 && search"
class="mt-2 w-full text-center"
>
Your search "{{ search }}" did not match any forms. Please try
again.
2023-12-09 15:47:03 +01:00
</div>
<v-button
Readonly User (#637) * Readonly User * Refactor FormPolicy and TemplatePolicy to centralize write operation logic - Introduced a private method `canPerformWriteOperation` in both FormPolicy and TemplatePolicy to encapsulate the logic for determining if a user can perform write operations on the respective models. - Updated the `update`, `delete`, `restore`, and `forceDelete` methods in FormPolicy to use the new method for improved readability and maintainability. - Simplified the `update` and `delete` methods in TemplatePolicy to leverage the centralized write operation logic. This refactoring enhances code clarity and reduces duplication across policy classes. * Refactor user and workspace permissions handling - Updated FormController to authorize form creation based on workspace context. - Removed the `is_readonly` attribute from UserResource and integrated it into WorkspaceResource for better encapsulation. - Refactored User model to eliminate the `getIsReadonlyAttribute` method, shifting readonly logic to the Workspace model. - Adjusted FormPolicy and TemplatePolicy to utilize workspace readonly checks for user permissions. - Updated various frontend components to reference workspace readonly status instead of user readonly status, enhancing clarity and consistency in permission handling. These changes improve the management of user permissions in relation to workspaces, ensuring a more robust and maintainable authorization system. * Fix isReadonlyUser * fix pint --------- Co-authored-by: Julien Nahum <julien@nahum.net>
2024-12-30 14:35:23 +01:00
v-if="!workspace.is_readonly && 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"
>
<path
d="M6.99996 1.1665V12.8332M1.16663 6.99984H12.8333"
stroke="currentColor"
stroke-width="1.67"
stroke-linecap="round"
stroke-linejoin="round"
/>
2023-12-09 15:47:03 +01:00
</svg>
Create a new form
</v-button>
</div>
<div
v-else-if="forms.length > 0"
class="mb-10"
>
2023-12-09 15:47:03 +01:00
<div v-if="enrichedForms && enrichedForms.length">
<div
v-for="(form) in enrichedForms"
:key="form.id"
class="mt-4 p-4 flex group bg-white hover:bg-gray-50 dark:bg-notion-dark items-center relative"
2023-12-09 15:47:03 +01:00
>
<div
class="flex-grow items-center truncate cursor-pointer relative"
>
<NuxtLink
:to="{name:'forms-slug-show-submissions', params: {slug:form.slug}}"
class="absolute inset-0"
/>
<span class="font-semibold text-gray-900 dark:text-white">{{
form.title
}}</span>
<ul class="flex text-gray-500 text-sm gap-4">
<li class="pr-1 mr-3">
{{ form.views_count }} view{{
form.views_count > 0 ? "s" : ""
}}
2023-12-09 15:47:03 +01:00
</li>
<li class="list-disc mr-3">
2023-12-09 15:47:03 +01:00
{{ form.submissions_count }}
submission{{ form.submissions_count > 0 ? "s" : "" }}
2023-12-09 15:47:03 +01:00
</li>
<li class="list-disc mr-3">
2023-12-09 15:47:03 +01:00
Edited {{ form.last_edited_human }}
</li>
<li
v-if="form.creator"
class="list-disc hidden lg:list-item"
>
By
2024-03-12 13:08:55 +01:00
{{ form?.creator?.name }}
</li>
2023-12-09 15:47:03 +01:00
</ul>
<div
v-if="['draft','closed'].includes(form.visibility) || (form.tags && form.tags.length > 0)"
class="mt-1 flex items-center flex-wrap gap-3"
>
<span
v-if="form.visibility=='draft'"
class="inline-flex items-center rounded-full bg-yellow-100 px-2 py-1 text-xs font-medium text-yellow-600 ring-1 ring-inset ring-gray-500/10 dark:text-white dark:bg-gray-700"
>
2023-12-09 15:47:03 +01:00
Draft
</span>
<span
v-else-if="form.visibility=='closed'"
class="inline-flex items-center rounded-full bg-yellow-100 px-2 py-1 text-xs font-medium text-yellow-600 ring-1 ring-inset ring-gray-500/10 dark:text-white dark:bg-gray-700"
>
2023-12-09 15:47:03 +01:00
Closed
</span>
<span
v-for="(tag) in form.tags"
:key="tag"
class="inline-flex items-center rounded-full bg-gray-50 px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10 dark:text-white dark:bg-gray-700"
2023-12-09 15:47:03 +01:00
>
{{ tag }}
</span>
</div>
</div>
<extra-menu
:form="form"
:is-main-page="true"
/>
2023-12-09 15:47:03 +01:00
</div>
</div>
<div
v-if="!workspace.is_pro"
class="px-4"
>
<UAlert
class="mt-8 p-4"
icon="i-heroicons-sparkles"
color="primary"
variant="subtle"
description="You can add components to your app using the cli."
>
<template #title>
<h3 class="font-semibold text-md">
Discover our Pro plan
</h3>
</template>
<template #description>
<div class="flex flex-wrap sm:flex-nowrap gap-4 items-start">
<p class="flex-grow">
Remove OpnForm branding, customize forms further, use your custom domain, integrate with your
favorite tools, invite users, and more!
</p>
<UButton
v-track.upgrade_banner_home_click
color="white"
class="block"
@click.prevent="subscriptionModalStore.openModal()"
>
Upgrade Now
</UButton>
</div>
</template>
</UAlert>
</div>
2023-12-09 15:47:03 +01:00
</div>
<div
v-if="formsLoading"
class="text-center"
>
<Loader class="h-6 w-6 text-nt-blue mx-auto" />
2023-12-09 15:47:03 +01:00
</div>
</div>
</div>
</div>
<open-form-footer class="mt-8 border-t" />
2023-12-09 15:47:03 +01:00
</div>
</template>
2023-12-19 18:57:31 +01:00
<script setup>
import {useFormsStore} from "../stores/forms"
import {useWorkspacesStore} from "../stores/workspaces"
import Fuse from "fuse.js"
import TextInput from "../components/forms/TextInput.vue"
import ExtraMenu from "../components/pages/forms/show/ExtraMenu.vue"
import {refDebounced} from "@vueuse/core"
2023-12-09 15:47:03 +01:00
2023-12-19 19:42:02 +01:00
definePageMeta({
middleware: ["auth", "self-hosted-credentials"],
2023-12-19 19:42:02 +01:00
})
2023-12-09 15:47:03 +01:00
useOpnSeoMeta({
title: "Your Forms",
description:
"All of your OpnForm are here. Create new forms, or update your existing forms.",
})
2023-12-09 15:47:03 +01:00
const subscriptionModalStore = useSubscriptionModalStore()
2023-12-19 18:57:31 +01:00
const formsStore = useFormsStore()
const workspacesStore = useWorkspacesStore()
2023-12-20 18:38:43 +01:00
formsStore.startLoading()
2023-12-09 15:47:03 +01:00
const workspace = computed(() => workspacesStore.getCurrent)
2023-12-19 19:42:02 +01:00
onMounted(() => {
2023-12-20 16:10:32 +01:00
if (!formsStore.allLoaded) {
2023-12-20 18:38:43 +01:00
formsStore.loadAll(workspacesStore.currentId)
} else {
formsStore.stopLoading()
2023-12-20 16:10:32 +01:00
}
2023-12-19 18:57:31 +01:00
})
2023-12-16 19:21:03 +01:00
2023-12-19 18:57:31 +01:00
// State
const {
getAll: forms,
loading: formsLoading,
allTags,
} = storeToRefs(formsStore)
const search = ref("")
2023-12-19 19:42:02 +01:00
const debouncedSearch = refDebounced(search, 500)
2023-12-19 18:57:31 +01:00
const selectedTags = ref(new Set())
2023-12-16 19:21:03 +01:00
2023-12-19 18:57:31 +01:00
// Methods
2023-12-19 18:57:31 +01:00
const onTagClick = (tag) => {
2024-02-10 12:46:17 +01:00
if (selectedTags?.value?.has(tag)) {
2023-12-19 18:57:31 +01:00
selectedTags.value.remove(tag)
} else {
selectedTags.value.add(tag)
}
}
2023-12-09 15:47:03 +01:00
2023-12-19 18:57:31 +01:00
// Computed
const isFilteringForms = computed(() => {
return (
(search.value !== "" && search.value !== null) ||
selectedTags.value.size > 0
)
2023-12-19 18:57:31 +01:00
})
2023-12-20 16:10:32 +01:00
2023-12-19 18:57:31 +01:00
const enrichedForms = computed(() => {
const enrichedForms = forms.value.map((form) => {
2023-12-19 18:57:31 +01:00
form.workspace = workspacesStore.getByKey(form.workspace_id)
return form
}).filter((form) => {
2023-12-19 19:42:02 +01:00
if (selectedTags.value.size === 0) {
return true
}
2024-01-16 12:20:05 +01:00
return form.tags && form.tags.length ? [...selectedTags.value].every(r => form.tags.includes(r)) : false
2023-12-19 18:57:31 +01:00
})
2023-12-09 15:47:03 +01:00
if (!isFilteringForms || search.value === "" || search.value === null) {
2023-12-19 18:57:31 +01:00
return enrichedForms
}
2023-12-09 15:47:03 +01:00
2023-12-19 18:57:31 +01:00
// Fuze search
const fuzeOptions = {
keys: ["title", "slug", "tags"],
2023-12-09 15:47:03 +01:00
}
2023-12-19 18:57:31 +01:00
const fuse = new Fuse(enrichedForms, fuzeOptions)
2023-12-19 19:42:02 +01:00
return fuse.search(debouncedSearch.value).map((res) => {
2023-12-19 18:57:31 +01:00
return res.item
})
})
2023-12-09 15:47:03 +01:00
</script>