Team functionality (#459)
* add api enpoints for adding, removing, updating user to workspace and leaving workspace * feat: updates client site workspace settings * refactor and add domain setting ui in modal * move workspace user functionality to its own component * adds tests * fix linting * updates select input to FlatSelectInput * moves workspace user role edit to seperated component * move user adding to its own component * adds check to usure users exist before checking is admin * fix loading users * feat: invite user to team functionality * fix token coulmn * fix self host mode changes * tests for user invite * Refactor back-end * Rename variables * Improve some styling elements + refactor workspace settings * More styling * More UI polishing * More UI fixes * PHP linting * Implemented most of the logic for team-functionnality * Fix user avatar URL * WIP remove users on cancellation * Finished pricing for team functionality * Fix tests * Fix linting * Added pricing_enabled helper * Fix pricing_enabled shortcut * Debug CI * Disable pricing when testing --------- Co-authored-by: LL-Etiane <lukongleinyuyetiane@gmail.com> Co-authored-by: Lukong Etiane <83535251+LL-Etiane@users.noreply.github.com> Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
87
client/components/pages/admin/AddUserToWorkspace.vue
Normal file
87
client/components/pages/admin/AddUserToWorkspace.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<form
|
||||
v-if="isWorkspaceAdmin"
|
||||
class="my-2"
|
||||
@submit.prevent="addUser"
|
||||
>
|
||||
<text-input
|
||||
v-model="newUser"
|
||||
name="email"
|
||||
label="Email"
|
||||
:required="true"
|
||||
:disabled="disabled"
|
||||
placeholder="Add a new user by email"
|
||||
/>
|
||||
<select-input
|
||||
v-model="newUserRole"
|
||||
name="newUserRole"
|
||||
:options="roleOptions"
|
||||
:disabled="disabled"
|
||||
placeholder="Select User Role"
|
||||
label="Role"
|
||||
:required="true"
|
||||
/>
|
||||
<div class="flex justify-center mt-2">
|
||||
<UButton
|
||||
type="submit"
|
||||
:disabled="disabled"
|
||||
:loading="addingUsersState"
|
||||
icon="i-heroicons-envelope"
|
||||
>
|
||||
Invite User
|
||||
</UButton>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { watch, ref } from "vue"
|
||||
|
||||
const props = defineProps({
|
||||
isWorkspaceAdmin: {},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
const emit = defineEmits(['fetchUsers'])
|
||||
|
||||
const workspacesStore = useWorkspacesStore()
|
||||
|
||||
const roleOptions = [
|
||||
{ name: "User", value: "user" },
|
||||
{ name: "Admin", value: "admin" }
|
||||
]
|
||||
|
||||
const newUser = ref("")
|
||||
const newUserRole = ref("user")
|
||||
const addingUsersState = ref(false)
|
||||
|
||||
|
||||
const addUser = () => {
|
||||
if (!newUser.value) return
|
||||
addingUsersState.value = true
|
||||
opnFetch(
|
||||
"/open/workspaces/" + workspacesStore.currentId + "/users/add",
|
||||
{
|
||||
method: "POST",
|
||||
body: {
|
||||
email: newUser.value,
|
||||
role: newUserRole.value,
|
||||
},
|
||||
}
|
||||
).then((data) => {
|
||||
newUser.value = ""
|
||||
newUserRole.value = "user"
|
||||
|
||||
useAlert().success(data.message)
|
||||
|
||||
emit("fetchUsers")
|
||||
}).catch((error) => {
|
||||
useAlert().error("There was an error adding user")
|
||||
}).finally(() => {
|
||||
addingUsersState.value = false
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
82
client/components/pages/admin/EditWorkSpaceUser.vue
Normal file
82
client/components/pages/admin/EditWorkSpaceUser.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<modal
|
||||
:show="showEditUserModal"
|
||||
max-width="lg"
|
||||
@close="$emit('close')"
|
||||
>
|
||||
<template #title>
|
||||
Edit User Role
|
||||
</template>
|
||||
<div class="px-4">
|
||||
<form
|
||||
@submit.prevent="updateUserRole"
|
||||
>
|
||||
<div>
|
||||
<FlatSelectInput
|
||||
v-model="userNewRole"
|
||||
name="newUserRole"
|
||||
:label="'New Role for '+props.user.name"
|
||||
:options="[
|
||||
{ name: 'User', value: 'user' },
|
||||
{ name: 'Admin', value: 'admin' }
|
||||
]"
|
||||
option-key="value"
|
||||
display-key="name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="w-full mt-6">
|
||||
<v-button
|
||||
:loading="updatingUserRoleState"
|
||||
class="w-full my-3"
|
||||
>
|
||||
Update
|
||||
</v-button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {watch, ref} from "vue"
|
||||
|
||||
const props = defineProps(['user', 'showEditUserModal'])
|
||||
const emit = defineEmits(['close', 'fetchUsers'])
|
||||
|
||||
const workspacesStore = useWorkspacesStore()
|
||||
const userNewRole = ref("")
|
||||
|
||||
const updatingUserRoleState = ref(false)
|
||||
|
||||
watch(() => props.user, () => {
|
||||
userNewRole.value = props.user.pivot.role
|
||||
})
|
||||
|
||||
const updateUserRole = () => {
|
||||
updatingUserRoleState.value = true
|
||||
opnFetch(
|
||||
"/open/workspaces/" +
|
||||
workspacesStore.currentId +
|
||||
"/users/" +
|
||||
props.user.id +
|
||||
"/update-role",
|
||||
{
|
||||
method: "PUT",
|
||||
body: {
|
||||
role: userNewRole.value,
|
||||
},
|
||||
},
|
||||
{showSuccess: false},
|
||||
).then(() => {
|
||||
useAlert().success("User role updated.")
|
||||
emit('fetchUsers')
|
||||
emit('close')
|
||||
}).catch((error) => {
|
||||
useAlert().error("There was an error updating user role")
|
||||
}).finally(() => {
|
||||
updatingUserRoleState.value = false
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -20,6 +20,7 @@
|
||||
:form="form"
|
||||
label="Email"
|
||||
:required="true"
|
||||
:disabled="disableEmail"
|
||||
placeholder="Your email address"
|
||||
/>
|
||||
|
||||
@@ -135,6 +136,7 @@ export default {
|
||||
agree_terms: false,
|
||||
appsumo_license: null,
|
||||
}),
|
||||
disableEmail:false
|
||||
}),
|
||||
|
||||
computed: {
|
||||
@@ -167,12 +169,26 @@ export default {
|
||||
) {
|
||||
this.form.appsumo_license = this.$route.query.appsumo_license
|
||||
}
|
||||
|
||||
if (this.$route.query?.invite_token) {
|
||||
if (this.$route.query?.email) {
|
||||
this.form.email = this.$route.query?.email
|
||||
this.disableEmail = true
|
||||
}
|
||||
this.form.invite_token = this.$route.query?.invite_token
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async register() {
|
||||
// Register the user.
|
||||
const data = await this.form.post("/register")
|
||||
let data
|
||||
try {
|
||||
// Register the user.
|
||||
data = await this.form.post("/register")
|
||||
} catch (err) {
|
||||
useAlert().error(err.response?._data?.message)
|
||||
return false
|
||||
}
|
||||
|
||||
// Log in the user.
|
||||
const tokenData = await this.form.post("/login")
|
||||
@@ -216,7 +232,13 @@ export default {
|
||||
if (this.isQuick) {
|
||||
this.$emit("afterQuickLogin")
|
||||
} else {
|
||||
this.$router.push({name: "forms-create"})
|
||||
// If is invite just redirect to home
|
||||
if (this.form.invite_token) {
|
||||
useAlert().success("You have successfully accepted the invite and joined this workspace.")
|
||||
this.$router.push({name: "home"})
|
||||
} else {
|
||||
this.$router.push({name: "forms-create"})
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
<template>
|
||||
<div class="border relative max-w-5xl mx-auto mt-4 lg:mt-10">
|
||||
<div class="w-full">
|
||||
<div
|
||||
class="rounded-lg bg-gray-50 dark:bg-gray-800 px-6 py-8 sm:p-10 lg:flex lg:items-center"
|
||||
>
|
||||
<div class="flex-1">
|
||||
<h3
|
||||
class="inline-flex px-4 py-1 rounded-full text-md font-bold tracking-wide uppercase bg-white text-gray-800"
|
||||
>
|
||||
Custom plan
|
||||
</h3>
|
||||
<div class="mt-4 text-md text-gray-600 dark:text-gray-400">
|
||||
Get a custom file upload limit, enterprise-level support, custom
|
||||
contract, dedicated application instance in a specific region,
|
||||
payment via invoice/PO etc.
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 rounded-md lg:mt-0 lg:ml-10 lg:flex-shrink-0">
|
||||
<v-button
|
||||
color="white"
|
||||
class="w-full mt-4"
|
||||
@click.prevent="customPlanClick"
|
||||
>
|
||||
Contact us
|
||||
</v-button>
|
||||
<div class="border lg:rounded-xl bg-gray-50 dark:bg-gray-800 relative max-w-5xl mx-auto mt-10">
|
||||
<div
|
||||
class=" p-6 lg:flex lg:items-center"
|
||||
>
|
||||
<div class="flex-1">
|
||||
<h3
|
||||
class="inline-flex px-4 py-1 rounded-full text-md font-semibold tracking-wide bg-blue-500 text-white"
|
||||
>
|
||||
Custom Plan
|
||||
</h3>
|
||||
<div class="mt-4 text-gray-600 dark:text-gray-400 max-w-2xl">
|
||||
Get a custom file upload limit, enterprise-level support, custom
|
||||
contract, dedicated application instance in a specific region,
|
||||
payment via invoice/PO etc.
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 lg:mt-0 lg:ml-10 lg:flex-shrink-0">
|
||||
<UButton
|
||||
size="xl"
|
||||
color="white"
|
||||
class="w-auto"
|
||||
icon="i-heroicons-chat-bubble-left"
|
||||
@click.prevent="customPlanClick"
|
||||
>
|
||||
Contact us
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -97,7 +97,10 @@
|
||||
</svg>
|
||||
{{ title }}
|
||||
</li>
|
||||
<slot name="pricing-table" />
|
||||
<slot
|
||||
name="pricing-table"
|
||||
:is-yearly="isYearly"
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
132
client/components/pages/settings/WorkSpaceCustomDomains.vue
Normal file
132
client/components/pages/settings/WorkSpaceCustomDomains.vue
Normal file
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="customDomainsEnabled"
|
||||
id="custom-domains"
|
||||
>
|
||||
<UButton
|
||||
color="gray"
|
||||
label="Manage Custom Domains"
|
||||
icon="i-heroicons-globe-alt"
|
||||
@click="showCustomDomainModal = !showCustomDomainModal"
|
||||
/>
|
||||
|
||||
<modal
|
||||
:show="showCustomDomainModal"
|
||||
max-width="lg"
|
||||
@close="showCustomDomainModal = false"
|
||||
>
|
||||
<h4 class="mb-4 font-medium">
|
||||
Manage your custom domains
|
||||
</h4>
|
||||
<UAlert
|
||||
v-if="!workspace.is_pro"
|
||||
icon="i-heroicons-user-group-20-solid"
|
||||
class="mb-4"
|
||||
color="orange"
|
||||
variant="subtle"
|
||||
title="Pro plan required"
|
||||
>
|
||||
<template #description>
|
||||
Please <NuxtLink
|
||||
:to="{name:'pricing'}"
|
||||
class="underline"
|
||||
>
|
||||
upgrade your account
|
||||
</NuxtLink> to setup a custom domain.
|
||||
</template>
|
||||
</UAlert>
|
||||
<p class="text-gray-500 text-sm mb-4">
|
||||
Read
|
||||
<a
|
||||
href="#"
|
||||
class="underline"
|
||||
@click.prevent="
|
||||
crisp.openHelpdeskArticle('how-to-use-my-own-domain-9m77g7')
|
||||
"
|
||||
>our instructions</a>
|
||||
to learn how to setup your own domain.
|
||||
</p>
|
||||
<text-area-input
|
||||
:form="customDomainsForm"
|
||||
name="custom_domains"
|
||||
:required="false"
|
||||
:disabled="!workspace.is_pro"
|
||||
label="Workspace Custom Domains"
|
||||
wrapper-class=""
|
||||
placeholder="yourdomain.com - 1 per line"
|
||||
/>
|
||||
<UButton
|
||||
class="mt-3"
|
||||
:loading="customDomainsLoading"
|
||||
:disabled="!workspace.is_pro"
|
||||
icon="i-heroicons-check"
|
||||
@click="saveChanges"
|
||||
>
|
||||
Save Domain(s)
|
||||
</UButton>
|
||||
</modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {watch} from "vue"
|
||||
|
||||
const crisp = useCrisp()
|
||||
const workspacesStore = useWorkspacesStore()
|
||||
const workspace = computed(() => workspacesStore.getCurrent)
|
||||
const loading = computed(() => workspacesStore.loading)
|
||||
|
||||
const customDomainsForm = useForm({
|
||||
custom_domain: "",
|
||||
})
|
||||
const customDomainsLoading = ref(false)
|
||||
const showCustomDomainModal = ref(false)
|
||||
|
||||
const customDomainsEnabled = computed(
|
||||
() => useRuntimeConfig().public.customDomainsEnabled,
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
initCustomDomains()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => workspace,
|
||||
() => {
|
||||
initCustomDomains()
|
||||
},
|
||||
)
|
||||
|
||||
const saveChanges = () => {
|
||||
if (customDomainsLoading.value) return
|
||||
customDomainsLoading.value = true
|
||||
|
||||
// Update the workspace custom domain
|
||||
customDomainsForm
|
||||
.put("/open/workspaces/" + workspace.value.id + "/custom-domains", {
|
||||
data: {
|
||||
custom_domains: customDomainsForm?.custom_domains
|
||||
?.split("\n")
|
||||
.map((domain) => (domain ? domain.trim() : null))
|
||||
.filter((domain) => domain && domain.length > 0),
|
||||
},
|
||||
})
|
||||
.then((data) => {
|
||||
workspacesStore.save(data)
|
||||
useAlert().success("Custom domains saved.")
|
||||
})
|
||||
.catch((error) => {
|
||||
useAlert().error(
|
||||
"Failed to update custom domains: " + error.response.data.message,
|
||||
)
|
||||
})
|
||||
.finally(() => {
|
||||
customDomainsLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const initCustomDomains = () => {
|
||||
if (!workspace || !workspace.value.custom_domains) return
|
||||
customDomainsForm.custom_domains = workspace.value?.custom_domains.join("\n")
|
||||
}
|
||||
</script>
|
||||
362
client/components/pages/settings/WorkSpaceUser.vue
Normal file
362
client/components/pages/settings/WorkSpaceUser.vue
Normal file
@@ -0,0 +1,362 @@
|
||||
<template>
|
||||
<div class="border rounded-md p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="font-semibold">
|
||||
Workspace Members
|
||||
</h4>
|
||||
<UButton
|
||||
label="Invite User"
|
||||
icon="i-heroicons-user-plus-20-solid"
|
||||
:loading="loading"
|
||||
@click="userInviteModal = true"
|
||||
/>
|
||||
</div>
|
||||
<!-- User invite modal -->
|
||||
<modal
|
||||
:show="userInviteModal"
|
||||
max-width="lg"
|
||||
@close="userInviteModal = false"
|
||||
>
|
||||
<h4 class="mb-4 font-medium">
|
||||
Invite a new user and collaborate on building forms
|
||||
</h4>
|
||||
|
||||
<template v-if="paidPlansEnabled">
|
||||
<UAlert
|
||||
v-if="workspace.is_pro"
|
||||
icon="i-heroicons-credit-card"
|
||||
color="primary"
|
||||
variant="subtle"
|
||||
title="This is a billable event."
|
||||
>
|
||||
<template #description>
|
||||
You will be charged $6/month for each user you invite to this workspace. More details on the
|
||||
<NuxtLink
|
||||
target="_blank"
|
||||
class="underline"
|
||||
:to="{name:'settings-billing'}"
|
||||
>
|
||||
billing
|
||||
</NuxtLink>
|
||||
and
|
||||
<NuxtLink
|
||||
target="_blank"
|
||||
class="underline"
|
||||
:to="{name:'pricing'}"
|
||||
>
|
||||
pricing
|
||||
</NuxtLink>
|
||||
page.
|
||||
</template>
|
||||
</UAlert>
|
||||
<UAlert
|
||||
v-else
|
||||
icon="i-heroicons-user-group-20-solid"
|
||||
color="orange"
|
||||
variant="subtle"
|
||||
title="Pro plan required"
|
||||
>
|
||||
<template #description>
|
||||
You need a Pro plan to invite new users on OpnForm. Please upgrade on our
|
||||
<NuxtLink
|
||||
target="_blank"
|
||||
class="underline"
|
||||
:to="{name:'pricing'}"
|
||||
>
|
||||
pricing
|
||||
</NuxtLink>
|
||||
page.
|
||||
</template>
|
||||
</UAlert>
|
||||
</template>
|
||||
|
||||
<AddUserToWorkspace
|
||||
:disabled="!canInviteUser"
|
||||
:is-workspace-admin="isWorkspaceAdmin"
|
||||
@fetch-users="getWorkspaceUsers"
|
||||
/>
|
||||
</modal>
|
||||
<UTable
|
||||
class="-mx-4 border-y mt-4"
|
||||
:loading="loadingUsers"
|
||||
:rows="users"
|
||||
:columns="columns"
|
||||
>
|
||||
<template
|
||||
v-if="isWorkspaceAdmin"
|
||||
#actions-data="{ row, index }"
|
||||
>
|
||||
<div class="space-x-2 flex justify-center">
|
||||
<template v-if="row.type == 'user'">
|
||||
<p
|
||||
v-if="row.is_current_user"
|
||||
class="text-gray-500 text-center text-sm"
|
||||
>
|
||||
-
|
||||
</p>
|
||||
<UButtonGroup
|
||||
v-else
|
||||
size="2xs"
|
||||
>
|
||||
<UTooltip
|
||||
text="Edit user"
|
||||
>
|
||||
<UButton
|
||||
icon="i-heroicons-pencil"
|
||||
color="gray"
|
||||
class="hover:text-blue-500"
|
||||
square
|
||||
@click="editUser(index)"
|
||||
/>
|
||||
</UTooltip>
|
||||
<UTooltip
|
||||
text="Remove user"
|
||||
>
|
||||
<UButton
|
||||
v-if="row.type == 'user'"
|
||||
icon="i-heroicons-trash"
|
||||
color="gray"
|
||||
class="hover:text-red-500"
|
||||
square
|
||||
@click="removeUser(index)"
|
||||
/>
|
||||
</UTooltip>
|
||||
</UButtonGroup>
|
||||
</template>
|
||||
<UButtonGroup
|
||||
v-else-if="row.type == 'invitee'"
|
||||
size="2xs"
|
||||
>
|
||||
<UTooltip
|
||||
text="Resend Invite"
|
||||
>
|
||||
<UButton
|
||||
icon="i-heroicons-envelope"
|
||||
color="gray"
|
||||
class="hover:text-blue-500"
|
||||
square
|
||||
@click="resendInvite(index)"
|
||||
/>
|
||||
</UTooltip>
|
||||
<UTooltip
|
||||
text="Cancel Invite"
|
||||
>
|
||||
<UButton
|
||||
icon="i-heroicons-trash"
|
||||
color="gray"
|
||||
class="hover:text-red-500"
|
||||
square
|
||||
@click="cancelInvite(index)"
|
||||
/>
|
||||
</UTooltip>
|
||||
</UButtonGroup>
|
||||
</div>
|
||||
</template>
|
||||
</UTable>
|
||||
|
||||
<EditWorkSpaceUser
|
||||
:user="selectedUser"
|
||||
:show-edit-user-modal="showEditUserModal"
|
||||
@close="showEditUserModal = false"
|
||||
@fetch-users="getWorkspaceUsers"
|
||||
/>
|
||||
|
||||
<div class="flex gap-2 mt-4">
|
||||
<UButton
|
||||
v-if="users.length > 1"
|
||||
color="gray"
|
||||
icon="i-heroicons-arrow-left-start-on-rectangle-20-solid"
|
||||
:loading="leaveWorkspaceLoadingState"
|
||||
@click="leaveWorkSpace(workspace.id)"
|
||||
>
|
||||
Leave Workspace
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
v-if="isWorkspaceAdmin && users.length == 1"
|
||||
icon="i-heroicons-trash"
|
||||
color="gray"
|
||||
:loading="loading"
|
||||
@click="deleteWorkspace(workspace.id)"
|
||||
>
|
||||
Remove workspace
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const workspacesStore = useWorkspacesStore()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const workspace = computed(() => workspacesStore.getCurrent)
|
||||
const loading = computed(() => workspacesStore.loading)
|
||||
const workspaces = computed(() => workspacesStore.getAll)
|
||||
|
||||
const users = ref([])
|
||||
const loadingUsers = ref(true)
|
||||
const leaveWorkspaceLoadingState = ref(false)
|
||||
|
||||
const userInviteModal = ref(false)
|
||||
const showEditUserModal = ref(false)
|
||||
const selectedUser = ref(null)
|
||||
const userNewRole = ref("")
|
||||
|
||||
const paidPlansEnabled = computed(() => useRuntimeConfig().public.paidPlansEnabled)
|
||||
const canInviteUser = computed(() => {
|
||||
return paidPlansEnabled.value ? workspace.value.is_pro : true
|
||||
})
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
getWorkspaceUsers()
|
||||
})
|
||||
|
||||
const getWorkspaceUsers = async () => {
|
||||
userInviteModal.value = false
|
||||
loadingUsers.value = true
|
||||
let data = await workspacesStore.getWorkspaceUsers()
|
||||
data = data.map(d => {
|
||||
return {
|
||||
...d,
|
||||
id: d.id,
|
||||
is_current_user: d.id === authStore.user.id,
|
||||
name: d.name,
|
||||
email: d.email,
|
||||
status: 'accepted',
|
||||
role: d.pivot.role,
|
||||
type: 'user'
|
||||
}
|
||||
})
|
||||
let invites = await workspacesStore.getWorkspaceInvites()
|
||||
invites = invites.filter(i => i.status !== 'accepted').map(i => {
|
||||
return {
|
||||
...i,
|
||||
name: 'Invitee',
|
||||
email: i.email,
|
||||
status: i.status,
|
||||
type: 'invitee'
|
||||
}
|
||||
})
|
||||
users.value = [...data, ...invites]
|
||||
loadingUsers.value = false
|
||||
}
|
||||
|
||||
const isWorkspaceAdmin = computed(() => {
|
||||
if (!users.value) return false
|
||||
const user = users.value.find((user) => user.id === authStore.user.id)
|
||||
return user && user.pivot.role === "admin"
|
||||
})
|
||||
|
||||
const columns = computed(() => {
|
||||
return [
|
||||
{key: 'name', label: 'Name'},
|
||||
{key: 'email', label: 'Email'},
|
||||
{key: 'role', label: 'Role'},
|
||||
...(isWorkspaceAdmin.value ? [{key: 'actions', label: 'Action', class: 'text-center'}] : [])
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
const editUser = (row) => {
|
||||
selectedUser.value = users.value[row]
|
||||
userNewRole.value = selectedUser.value.pivot.role
|
||||
showEditUserModal.value = true
|
||||
}
|
||||
|
||||
|
||||
const removeUser = (index) => {
|
||||
const user = users.value[index]
|
||||
useAlert().confirm(
|
||||
"Do you really want to remove " + user.name + " from this workspace?",
|
||||
() => {
|
||||
loadingUsers.value = true
|
||||
opnFetch(
|
||||
"/open/workspaces/" + workspacesStore.currentId + "/users/" + user.id + "/remove",
|
||||
{
|
||||
method: "DELETE",
|
||||
},
|
||||
{showSuccess: false},
|
||||
).then(() => {
|
||||
useAlert().success("User successfully removed.")
|
||||
getWorkspaceUsers()
|
||||
}).catch((error) => {
|
||||
useAlert().error("There was an error removing user")
|
||||
}).finally(() => {
|
||||
loadingUsers.value = false
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
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(
|
||||
() => {
|
||||
useAlert().success("Workspace successfully removed.")
|
||||
workspacesStore.remove(workspaceId)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const leaveWorkSpace = (workspaceId) => {
|
||||
useAlert().confirm(
|
||||
"Do you really want to leave this workspace? You will lose access to all forms in this workspace.",
|
||||
() => {
|
||||
leaveWorkspaceLoadingState.value = true
|
||||
opnFetch("/open/workspaces/" + workspaceId + "/leave", {
|
||||
method: "POST",
|
||||
}).then(() => {
|
||||
useAlert().success("You have left the workspace.")
|
||||
workspacesStore.remove(workspaceId)
|
||||
getWorkspaceUsers()
|
||||
}).catch((error) => {
|
||||
useAlert().error("There was an error leaving the workspace.")
|
||||
}).finally(() => {
|
||||
leaveWorkspaceLoadingState.value = false
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const resendInvite = (id) => {
|
||||
const inviteId = users.value[id].id
|
||||
useAlert().confirm(
|
||||
"Do you really want to resend invite email to this user?",
|
||||
() => {
|
||||
opnFetch("/open/workspaces/" + workspace.value.id + "/invites/" + inviteId + "/resend", {method: "POST"}).then(
|
||||
() => {
|
||||
useAlert().success("Invitation resent successfully.")
|
||||
getWorkspaceUsers()
|
||||
},
|
||||
).catch(err => {
|
||||
useAlert().error(err.response._data?.message)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const cancelInvite = (id) => {
|
||||
const inviteId = users.value[id].id
|
||||
useAlert().confirm(
|
||||
"Do you really want to cancel this user's invitation to this workspace?",
|
||||
() => {
|
||||
opnFetch("/open/workspaces/" + workspace.value.id + "/invites/" + inviteId + "/cancel", {method: "DELETE"}).then(
|
||||
() => {
|
||||
useAlert().success("Invitation cancelled successfully.")
|
||||
getWorkspaceUsers()
|
||||
},
|
||||
).catch(err => {
|
||||
useAlert().error(err.response._data?.message)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
Reference in New Issue
Block a user