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>
This commit is contained in:
parent
9a2d7b9d8d
commit
2366f9515d
|
|
@ -108,10 +108,9 @@ class FormController extends Controller
|
||||||
|
|
||||||
public function store(StoreFormRequest $request)
|
public function store(StoreFormRequest $request)
|
||||||
{
|
{
|
||||||
$this->authorize('create', Form::class);
|
|
||||||
|
|
||||||
$workspace = Workspace::findOrFail($request->get('workspace_id'));
|
$workspace = Workspace::findOrFail($request->get('workspace_id'));
|
||||||
$this->authorize('view', $workspace);
|
$this->authorize('view', $workspace);
|
||||||
|
$this->authorize('create', [Form::class, $workspace]);
|
||||||
|
|
||||||
$formData = $this->formCleaner
|
$formData = $this->formCleaner
|
||||||
->processRequest($request)
|
->processRequest($request)
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ class WorkspaceUserController extends Controller
|
||||||
|
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'email' => 'required|email',
|
'email' => 'required|email',
|
||||||
'role' => 'required|in:admin,user',
|
'role' => 'required|in:' . implode(',', User::ROLES),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$user = User::where('email', $request->email)->first();
|
$user = User::where('email', $request->email)->first();
|
||||||
|
|
@ -62,10 +62,11 @@ class WorkspaceUserController extends Controller
|
||||||
{
|
{
|
||||||
if (
|
if (
|
||||||
UserInvite::where('email', $email)
|
UserInvite::where('email', $email)
|
||||||
->where('workspace_id', $workspace->id)
|
->where('workspace_id', $workspace->id)
|
||||||
->notExpired()
|
->notExpired()
|
||||||
->pending()
|
->pending()
|
||||||
->exists()) {
|
->exists()
|
||||||
|
) {
|
||||||
return $this->success([
|
return $this->success([
|
||||||
'message' => 'User has already been invited.'
|
'message' => 'User has already been invited.'
|
||||||
]);
|
]);
|
||||||
|
|
@ -86,7 +87,7 @@ class WorkspaceUserController extends Controller
|
||||||
$this->authorize('adminAction', $workspace);
|
$this->authorize('adminAction', $workspace);
|
||||||
|
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'role' => 'required|in:admin,user',
|
'role' => 'required|in:' . implode(',', User::ROLES),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$workspace->users()->sync([
|
$workspace->users()->sync([
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ class WorkspaceResource extends JsonResource
|
||||||
{
|
{
|
||||||
return array_merge(parent::toArray($request), [
|
return array_merge(parent::toArray($request), [
|
||||||
'max_file_size' => $this->max_file_size / 1000000,
|
'max_file_size' => $this->max_file_size / 1000000,
|
||||||
|
'is_readonly' => $this->isReadonlyUser($request->user()),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,13 @@ class User extends Authenticatable implements JWTSubject
|
||||||
|
|
||||||
public const ROLE_ADMIN = 'admin';
|
public const ROLE_ADMIN = 'admin';
|
||||||
public const ROLE_USER = 'user';
|
public const ROLE_USER = 'user';
|
||||||
|
public const ROLE_READONLY = 'readonly';
|
||||||
|
|
||||||
|
public const ROLES = [
|
||||||
|
self::ROLE_ADMIN,
|
||||||
|
self::ROLE_USER,
|
||||||
|
self::ROLE_READONLY,
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes that are mass assignable.
|
* The attributes that are mass assignable.
|
||||||
|
|
|
||||||
|
|
@ -203,4 +203,12 @@ class Workspace extends Model implements CachableAttributes
|
||||||
{
|
{
|
||||||
return $this->hasMany(Form::class);
|
return $this->hasMany(Form::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isReadonlyUser(?User $user)
|
||||||
|
{
|
||||||
|
return $user ? $this->users()
|
||||||
|
->wherePivot('user_id', $user->id)
|
||||||
|
->wherePivot('role', User::ROLE_READONLY)
|
||||||
|
->exists() : false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ namespace App\Policies;
|
||||||
|
|
||||||
use App\Models\Forms\Form;
|
use App\Models\Forms\Form;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Models\Workspace;
|
||||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||||
|
|
||||||
class FormPolicy
|
class FormPolicy
|
||||||
|
|
@ -35,9 +36,17 @@ class FormPolicy
|
||||||
*
|
*
|
||||||
* @return mixed
|
* @return mixed
|
||||||
*/
|
*/
|
||||||
public function create(User $user)
|
public function create(User $user, Workspace $workspace)
|
||||||
{
|
{
|
||||||
return true;
|
return !$workspace->isReadonlyUser($user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can perform write operations on the model.
|
||||||
|
*/
|
||||||
|
private function canPerformWriteOperation(User $user, Form $form): bool
|
||||||
|
{
|
||||||
|
return $user->ownsForm($form) && !$form->workspace->isReadonlyUser($user);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -47,7 +56,7 @@ class FormPolicy
|
||||||
*/
|
*/
|
||||||
public function update(User $user, Form $form)
|
public function update(User $user, Form $form)
|
||||||
{
|
{
|
||||||
return $user->ownsForm($form);
|
return $this->canPerformWriteOperation($user, $form);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -57,7 +66,7 @@ class FormPolicy
|
||||||
*/
|
*/
|
||||||
public function delete(User $user, Form $form)
|
public function delete(User $user, Form $form)
|
||||||
{
|
{
|
||||||
return $user->ownsForm($form);
|
return $this->canPerformWriteOperation($user, $form);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -67,7 +76,7 @@ class FormPolicy
|
||||||
*/
|
*/
|
||||||
public function restore(User $user, Form $form)
|
public function restore(User $user, Form $form)
|
||||||
{
|
{
|
||||||
return $user->ownsForm($form);
|
return $this->canPerformWriteOperation($user, $form);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -77,6 +86,6 @@ class FormPolicy
|
||||||
*/
|
*/
|
||||||
public function forceDelete(User $user, Form $form)
|
public function forceDelete(User $user, Form $form)
|
||||||
{
|
{
|
||||||
return $user->ownsForm($form);
|
return $this->canPerformWriteOperation($user, $form);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,6 @@ class TemplatePolicy
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine whether the user can create models.
|
* Determine whether the user can create models.
|
||||||
*
|
|
||||||
* @return \Illuminate\Auth\Access\Response|bool
|
|
||||||
*/
|
*/
|
||||||
public function create(User $user)
|
public function create(User $user)
|
||||||
{
|
{
|
||||||
|
|
@ -21,22 +19,20 @@ class TemplatePolicy
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine whether the user can update the model.
|
* Determine whether the user can perform write operations on the model.
|
||||||
*
|
|
||||||
* @return mixed
|
|
||||||
*/
|
*/
|
||||||
public function update(User $user, Template $template)
|
private function canPerformWriteOperation(User $user, Template $template): bool
|
||||||
{
|
{
|
||||||
return $user->admin || $user->template_editor || $template->creator_id === $user->id;
|
return $user->admin || $user->template_editor || $template->creator_id === $user->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function update(User $user, Template $template)
|
||||||
* Determine whether the user can delete the model.
|
{
|
||||||
*
|
return $this->canPerformWriteOperation($user, $template);
|
||||||
* @return mixed
|
}
|
||||||
*/
|
|
||||||
public function delete(User $user, Template $template)
|
public function delete(User $user, Template $template)
|
||||||
{
|
{
|
||||||
return $user->admin || $user->template_editor || $template->creator_id === $user->id;
|
return $this->canPerformWriteOperation($user, $template);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@
|
||||||
</p>
|
</p>
|
||||||
</resizable-th>
|
</resizable-th>
|
||||||
<th
|
<th
|
||||||
|
v-if="hasActions"
|
||||||
class="n-table-cell p-0 relative"
|
class="n-table-cell p-0 relative"
|
||||||
style="width: 100px"
|
style="width: 100px"
|
||||||
>
|
>
|
||||||
|
|
@ -181,6 +182,8 @@ export default {
|
||||||
return {
|
return {
|
||||||
workingFormStore,
|
workingFormStore,
|
||||||
form: storeToRefs(workingFormStore).content,
|
form: storeToRefs(workingFormStore).content,
|
||||||
|
user: useAuthStore().user,
|
||||||
|
workspace: useWorkspacesStore().getCurrent,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -188,7 +191,6 @@ export default {
|
||||||
return {
|
return {
|
||||||
tableHash: null,
|
tableHash: null,
|
||||||
skip: false,
|
skip: false,
|
||||||
hasActions: true,
|
|
||||||
internalColumns: [],
|
internalColumns: [],
|
||||||
rafId: null,
|
rafId: null,
|
||||||
fieldComponents: {
|
fieldComponents: {
|
||||||
|
|
@ -213,6 +215,9 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
hasActions() {
|
||||||
|
return !this.workspace.is_readonly
|
||||||
|
},
|
||||||
formData() {
|
formData() {
|
||||||
return [...this.data].sort((a, b) => new Date(b.created_at) - new Date(a.created_at))
|
return [...this.data].sort((a, b) => new Date(b.created_at) - new Date(a.created_at))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,8 @@ const workspacesStore = useWorkspacesStore()
|
||||||
|
|
||||||
const roleOptions = [
|
const roleOptions = [
|
||||||
{name: "User", value: "user"},
|
{name: "User", value: "user"},
|
||||||
{name: "Admin", value: "admin"}
|
{name: "Admin", value: "admin"},
|
||||||
|
{name: "Read Only", value: "readonly"}
|
||||||
]
|
]
|
||||||
|
|
||||||
const newUser = ref("")
|
const newUser = ref("")
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,8 @@
|
||||||
:label="'New Role for '+props.user.name"
|
:label="'New Role for '+props.user.name"
|
||||||
:options="[
|
:options="[
|
||||||
{ name: 'User', value: 'user' },
|
{ name: 'User', value: 'user' },
|
||||||
{ name: 'Admin', value: 'admin' }
|
{ name: 'Admin', value: 'admin' },
|
||||||
|
{ name: 'Read Only', value: 'readonly' },
|
||||||
]"
|
]"
|
||||||
option-key="value"
|
option-key="value"
|
||||||
display-key="name"
|
display-key="name"
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<select-input
|
<select-input
|
||||||
|
v-if="!disableEmail"
|
||||||
name="hear_about_us"
|
name="hear_about_us"
|
||||||
:options="hearAboutUsOptions"
|
:options="hearAboutUsOptions"
|
||||||
:form="form"
|
:form="form"
|
||||||
|
|
@ -169,6 +170,7 @@ export default {
|
||||||
form: useForm({
|
form: useForm({
|
||||||
name: "",
|
name: "",
|
||||||
email: "",
|
email: "",
|
||||||
|
hear_about_us: "",
|
||||||
password: "",
|
password: "",
|
||||||
password_confirmation: "",
|
password_confirmation: "",
|
||||||
agree_terms: false,
|
agree_terms: false,
|
||||||
|
|
@ -216,6 +218,7 @@ export default {
|
||||||
if (this.$route.query?.invite_token) {
|
if (this.$route.query?.invite_token) {
|
||||||
if (this.$route.query?.email) {
|
if (this.$route.query?.email) {
|
||||||
this.form.email = this.$route.query?.email
|
this.form.email = this.$route.query?.email
|
||||||
|
this.form.hear_about_us = 'invite'
|
||||||
this.disableEmail = true
|
this.disableEmail = true
|
||||||
}
|
}
|
||||||
this.form.invite_token = this.$route.query?.invite_token
|
this.form.invite_token = this.$route.query?.invite_token
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="form">
|
<div v-if="form">
|
||||||
<div
|
<div
|
||||||
v-if="loadingDuplicate || loadingDelete"
|
v-if="loadingDuplicate || loadingDelete"
|
||||||
class="pr-4 pt-2"
|
class="pr-4 pt-2"
|
||||||
>
|
>
|
||||||
<Loader class="h-6 w-6 mx-auto" />
|
<Loader class="h-6 w-6 mx-auto" />
|
||||||
</div>
|
</div>
|
||||||
<UDropdown v-else :items="items">
|
<UDropdown
|
||||||
|
v-else
|
||||||
|
:items="items"
|
||||||
|
>
|
||||||
<v-button
|
<v-button
|
||||||
color="white"
|
color="white"
|
||||||
>
|
>
|
||||||
|
|
@ -99,6 +102,7 @@ const authStore = useAuthStore()
|
||||||
const formsStore = useFormsStore()
|
const formsStore = useFormsStore()
|
||||||
const formEndpoint = "/open/forms/{id}"
|
const formEndpoint = "/open/forms/{id}"
|
||||||
const user = computed(() => authStore.user)
|
const user = computed(() => authStore.user)
|
||||||
|
const workspace = computed(() => useWorkspacesStore().getCurrent)
|
||||||
|
|
||||||
const loadingDuplicate = ref(false)
|
const loadingDuplicate = ref(false)
|
||||||
const loadingDelete = ref(false)
|
const loadingDelete = ref(false)
|
||||||
|
|
@ -128,8 +132,9 @@ const items = computed(() => {
|
||||||
}
|
}
|
||||||
}] : []
|
}] : []
|
||||||
],
|
],
|
||||||
[
|
...workspace.value.is_readonly ? [] : [
|
||||||
...props.isMainPage ? [{
|
[
|
||||||
|
...props.isMainPage ? [{
|
||||||
label: 'Edit',
|
label: 'Edit',
|
||||||
icon: 'i-heroicons-pencil-square-20-solid',
|
icon: 'i-heroicons-pencil-square-20-solid',
|
||||||
to: { name: 'forms-slug-edit', params: { slug: props.form.slug } }
|
to: { name: 'forms-slug-edit', params: { slug: props.form.slug } }
|
||||||
|
|
@ -166,6 +171,7 @@ const items = computed(() => {
|
||||||
class: 'text-red-800 hover:bg-red-50 hover:text-red-600 group',
|
class: 'text-red-800 hover:bg-red-50 hover:text-red-600 group',
|
||||||
iconClass: 'text-red-900 group-hover:text-red-800'
|
iconClass: 'text-red-900 group-hover:text-red-800'
|
||||||
}
|
}
|
||||||
|
]
|
||||||
]
|
]
|
||||||
].filter((group) => group.length > 0)
|
].filter((group) => group.length > 0)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
Workspace Members
|
Workspace Members
|
||||||
</h4>
|
</h4>
|
||||||
<UButton
|
<UButton
|
||||||
|
v-if="!workspace.is_readonly"
|
||||||
label="Invite User"
|
label="Invite User"
|
||||||
icon="i-heroicons-user-plus-20-solid"
|
icon="i-heroicons-user-plus-20-solid"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@
|
||||||
</h2>
|
</h2>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<extra-menu
|
<extra-menu
|
||||||
|
v-if="!workspace.is_readonly"
|
||||||
class="mr-2"
|
class="mr-2"
|
||||||
:form="form"
|
:form="form"
|
||||||
/>
|
/>
|
||||||
|
|
@ -98,6 +99,7 @@
|
||||||
</svg>
|
</svg>
|
||||||
</v-button>
|
</v-button>
|
||||||
<v-button
|
<v-button
|
||||||
|
v-if="!workspace.is_readonly"
|
||||||
class="text-white"
|
class="text-white"
|
||||||
:to="{ name: 'forms-slug-edit', params: { slug: slug } }"
|
:to="{ name: 'forms-slug-edit', params: { slug: slug } }"
|
||||||
>
|
>
|
||||||
|
|
@ -253,6 +255,7 @@ const slug = useRoute().params.slug
|
||||||
|
|
||||||
formsStore.startLoading()
|
formsStore.startLoading()
|
||||||
const form = computed(() => formsStore.getByKey(slug))
|
const form = computed(() => formsStore.getByKey(slug))
|
||||||
|
const workspace = computed(() => workspacesStore.getCurrent)
|
||||||
|
|
||||||
const loading = computed(() => formsStore.loading || workspacesStore.loading)
|
const loading = computed(() => formsStore.loading || workspacesStore.loading)
|
||||||
const displayClosesDate = computed(() => {
|
const displayClosesDate = computed(() => {
|
||||||
|
|
@ -279,11 +282,13 @@ const tabsList = [
|
||||||
route: "forms-slug-show-submissions",
|
route: "forms-slug-show-submissions",
|
||||||
params: { 'slug': slug }
|
params: { 'slug': slug }
|
||||||
},
|
},
|
||||||
{
|
...workspace.value.is_readonly ? [] : [
|
||||||
name: "Integrations",
|
{
|
||||||
route: "forms-slug-show-integrations",
|
name: "Integrations",
|
||||||
params: { 'slug': slug }
|
route: "forms-slug-show-integrations",
|
||||||
},
|
params: { 'slug': slug }
|
||||||
|
},
|
||||||
|
],
|
||||||
{
|
{
|
||||||
name: "Analytics",
|
name: "Analytics",
|
||||||
route: "forms-slug-show-stats",
|
route: "forms-slug-show-stats",
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
<div class="mb-20">
|
<div class="mb-20">
|
||||||
<div class="mb-6 pb-6 border-b w-full flex flex-col sm:flex-row gap-2">
|
<div class="mb-6 pb-6 border-b w-full flex flex-col sm:flex-row gap-2">
|
||||||
<regenerate-form-link
|
<regenerate-form-link
|
||||||
|
v-if="!workspace.is_readonly"
|
||||||
class="sm:w-1/2 flex"
|
class="sm:w-1/2 flex"
|
||||||
:form="props.form"
|
:form="props.form"
|
||||||
/>
|
/>
|
||||||
|
|
@ -54,6 +55,8 @@ import RegenerateFormLink from "~/components/pages/forms/show/RegenerateFormLink
|
||||||
import AdvancedFormUrlSettings from "~/components/open/forms/components/AdvancedFormUrlSettings.vue"
|
import AdvancedFormUrlSettings from "~/components/open/forms/components/AdvancedFormUrlSettings.vue"
|
||||||
import EmbedFormAsPopupModal from "~/components/pages/forms/show/EmbedFormAsPopupModal.vue"
|
import EmbedFormAsPopupModal from "~/components/pages/forms/show/EmbedFormAsPopupModal.vue"
|
||||||
|
|
||||||
|
const workspace = computed(() => useWorkspacesStore().getCurrent)
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
form: { type: Object, required: true },
|
form: { type: Object, required: true },
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
Your Forms
|
Your Forms
|
||||||
</h2>
|
</h2>
|
||||||
<v-button
|
<v-button
|
||||||
|
v-if="!workspace.is_readonly"
|
||||||
v-track.create_form_click
|
v-track.create_form_click
|
||||||
:to="{ name: 'forms-create' }"
|
:to="{ name: 'forms-create' }"
|
||||||
>
|
>
|
||||||
|
|
@ -86,7 +87,7 @@
|
||||||
again.
|
again.
|
||||||
</div>
|
</div>
|
||||||
<v-button
|
<v-button
|
||||||
v-if="forms.length === 0"
|
v-if="!workspace.is_readonly && forms.length === 0"
|
||||||
v-track.create_form_click
|
v-track.create_form_click
|
||||||
class="mt-4"
|
class="mt-4"
|
||||||
:to="{ name: 'forms-create' }"
|
:to="{ name: 'forms-create' }"
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@ useOpnSeoMeta({
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const user = computed(() => authStore.user)
|
const user = computed(() => authStore.user)
|
||||||
|
const workspace = computed(() => useWorkspacesStore().getCurrent)
|
||||||
const tabsList = computed(() => {
|
const tabsList = computed(() => {
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
|
|
@ -66,14 +67,16 @@ const tabsList = computed(() => {
|
||||||
name: "Workspace Settings",
|
name: "Workspace Settings",
|
||||||
route: "settings-workspace",
|
route: "settings-workspace",
|
||||||
},
|
},
|
||||||
{
|
...workspace.value.is_readonly ? [] : [
|
||||||
name: "Access Tokens",
|
{
|
||||||
route: "settings-access-tokens",
|
name: "Access Tokens",
|
||||||
},
|
route: "settings-access-tokens",
|
||||||
{
|
},
|
||||||
name: "Connections",
|
{
|
||||||
route: "settings-connections",
|
name: "Connections",
|
||||||
},
|
route: "settings-connections",
|
||||||
|
},
|
||||||
|
],
|
||||||
{
|
{
|
||||||
name: "Password",
|
name: "Password",
|
||||||
route: "settings-password",
|
route: "settings-password",
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,10 @@
|
||||||
You can switch to another workspace in top left corner of the page.</small>
|
You can switch to another workspace in top left corner of the page.</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full flex flex-wrap gap-2">
|
<div class="w-full flex flex-wrap gap-2">
|
||||||
<WorkSpaceCustomDomains v-if="useFeatureFlag('custom_domains') && !loading" />
|
<template v-if="!workspace.is_readonly">
|
||||||
<WorkSpaceEmailSettings v-if="!loading" />
|
<WorkSpaceCustomDomains v-if="useFeatureFlag('custom_domains') && !loading" />
|
||||||
|
<WorkSpaceEmailSettings v-if="!loading" />
|
||||||
|
</template>
|
||||||
<UButton
|
<UButton
|
||||||
label="New Workspace"
|
label="New Workspace"
|
||||||
icon="i-heroicons-plus"
|
icon="i-heroicons-plus"
|
||||||
|
|
@ -95,12 +97,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {watch, ref} from "vue"
|
|
||||||
import {fetchAllWorkspaces} from "~/stores/workspaces.js"
|
import {fetchAllWorkspaces} from "~/stores/workspaces.js"
|
||||||
|
|
||||||
const crisp = useCrisp()
|
|
||||||
const workspacesStore = useWorkspacesStore()
|
const workspacesStore = useWorkspacesStore()
|
||||||
const workspaces = computed(() => workspacesStore.getAll)
|
const workspace = computed(() => workspacesStore.getCurrent)
|
||||||
const loading = computed(() => workspacesStore.loading)
|
const loading = computed(() => workspacesStore.loading)
|
||||||
|
|
||||||
useOpnSeoMeta({
|
useOpnSeoMeta({
|
||||||
|
|
@ -116,8 +116,6 @@ const form = useForm({
|
||||||
})
|
})
|
||||||
const workspaceModal = ref(false)
|
const workspaceModal = ref(false)
|
||||||
|
|
||||||
const workspace = computed(() => workspacesStore.getCurrent)
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchAllWorkspaces()
|
fetchAllWorkspaces()
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue