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:
@@ -15,7 +15,7 @@
|
||||
>
|
||||
<div
|
||||
v-if="closeable"
|
||||
class="absolute top-4 right-4"
|
||||
class="absolute top-4 right-4 z-10"
|
||||
>
|
||||
<button
|
||||
class="text-gray-500 hover:text-gray-900 cursor-pointer"
|
||||
|
||||
@@ -23,19 +23,19 @@
|
||||
</div>
|
||||
<div
|
||||
v-if="showAuth"
|
||||
class="hidden md:block ml-auto relative"
|
||||
class="hidden md:flex gap-x-2 ml-auto"
|
||||
>
|
||||
<NuxtLink
|
||||
v-if="$route.name !== 'templates'"
|
||||
:to="{ name: 'templates' }"
|
||||
class="text-sm text-gray-600 dark:text-white hover:text-gray-800 cursor-pointer mt-1 mr-8"
|
||||
:class="navLinkClasses"
|
||||
>
|
||||
Templates
|
||||
</NuxtLink>
|
||||
<template v-if="featureBaseEnabled">
|
||||
<button
|
||||
v-if="user"
|
||||
class="text-sm text-gray-600 dark:text-white hidden sm:inline hover:text-gray-800 cursor-pointer mt-1 mr-8"
|
||||
:class="navLinkClasses"
|
||||
@click.prevent="openChangelog"
|
||||
>
|
||||
What's new? <span
|
||||
@@ -48,7 +48,7 @@
|
||||
v-else
|
||||
:href="opnformConfig.links.changelog_url"
|
||||
target="_blank"
|
||||
class="text-sm text-gray-600 dark:text-white hidden lg:inline hover:text-gray-800 cursor-pointer mt-1 mr-8"
|
||||
:class="navLinkClasses"
|
||||
>
|
||||
What's new?
|
||||
</a>
|
||||
@@ -56,7 +56,8 @@
|
||||
<NuxtLink
|
||||
v-if="$route.name !== 'ai-form-builder' && user === null"
|
||||
:to="{ name: 'ai-form-builder' }"
|
||||
class="text-sm text-gray-600 dark:text-white hidden lg:inline hover:text-gray-800 cursor-pointer mt-1 mr-8"
|
||||
:class="navLinkClasses"
|
||||
class="hidden lg:inline"
|
||||
>
|
||||
AI Form Builder
|
||||
</NuxtLink>
|
||||
@@ -67,15 +68,18 @@
|
||||
$route.name !== 'pricing'
|
||||
"
|
||||
:to="{ name: 'pricing' }"
|
||||
class="text-sm text-gray-600 dark:text-white hover:text-gray-800 cursor-pointer mt-1 mr-8"
|
||||
:class="navLinkClasses"
|
||||
>
|
||||
<span v-if="user">Upgrade</span>
|
||||
<span
|
||||
v-if="user"
|
||||
class="text-primary"
|
||||
>Upgrade</span>
|
||||
<span v-else>Pricing</span>
|
||||
</NuxtLink>
|
||||
|
||||
<NuxtLink
|
||||
:href="helpUrl"
|
||||
class="text-sm text-gray-600 dark:text-white hover:text-gray-800 cursor-pointer mt-1"
|
||||
:class="navLinkClasses"
|
||||
target="_blank"
|
||||
>
|
||||
Help
|
||||
@@ -90,7 +94,7 @@
|
||||
class="block"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<div class="ml-3 mr-4 relative">
|
||||
<div class="ml-4 relative">
|
||||
<div class="relative inline-block text-left">
|
||||
<dropdown
|
||||
v-if="user"
|
||||
@@ -100,7 +104,8 @@
|
||||
<button
|
||||
id="dropdown-menu-button"
|
||||
type="button"
|
||||
class="flex items-center justify-center w-full rounded-md px-4 py-2 text-sm text-gray-700 dark:text-gray-50 hover:bg-gray-50 dark:hover:bg-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-100 focus:ring-gray-500"
|
||||
:class="navLinkClasses"
|
||||
class="flex items-center"
|
||||
dusk="nav-dropdown-button"
|
||||
@click.stop="toggle()"
|
||||
>
|
||||
@@ -117,7 +122,7 @@
|
||||
<NuxtLink
|
||||
v-if="userOnboarded"
|
||||
:to="{ name: 'home' }"
|
||||
class="block block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center"
|
||||
class="block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:no-underline transition-colors hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -139,7 +144,7 @@
|
||||
<NuxtLink
|
||||
v-if="userOnboarded"
|
||||
:to="{ name: 'templates-my-templates' }"
|
||||
class="block block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center"
|
||||
class="block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:no-underline transition-colors hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -160,7 +165,7 @@
|
||||
|
||||
<NuxtLink
|
||||
:to="{ name: 'settings-profile' }"
|
||||
class="block block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center"
|
||||
class="block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:no-underline transition-colors hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 mr-2"
|
||||
@@ -188,7 +193,7 @@
|
||||
<NuxtLink
|
||||
v-if="user.moderator"
|
||||
:to="{ name: 'settings-admin' }"
|
||||
class="block block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center"
|
||||
class="block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:no-underline transition-colors hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -209,7 +214,7 @@
|
||||
|
||||
<a
|
||||
href="#"
|
||||
class="block block px-4 py-2 text-md text-gray-700 dark:text-white hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center"
|
||||
class="block px-4 py-2 text-md text-gray-700 hover:no-underline transition-colors hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center"
|
||||
@click.prevent="logout"
|
||||
>
|
||||
<svg
|
||||
@@ -236,7 +241,7 @@
|
||||
<NuxtLink
|
||||
v-if="$route.name !== 'login'"
|
||||
:to="{ name: 'login' }"
|
||||
class="text-gray-600 dark:text-white hover:text-gray-800 dark:hover:text-white px-0 sm:px-3 py-2 rounded-md text-sm"
|
||||
:class="navLinkClasses"
|
||||
active-class="text-gray-800 dark:text-white"
|
||||
>
|
||||
Login
|
||||
@@ -245,6 +250,7 @@
|
||||
<v-button
|
||||
v-track.nav_create_form_click
|
||||
size="small"
|
||||
class="shrink-0"
|
||||
:to="{ name: 'forms-create-guest' }"
|
||||
color="outline-blue"
|
||||
:arrow="true"
|
||||
@@ -290,6 +296,10 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
navLinkClasses: 'border border-transparent hover:border-gray-200 text-gray-500 hover:text-gray-800 hover:no-underline dark:hover:text-white py-2 px-3 hover:bg-gray-50 rounded-md text-sm font-medium transition-colors w-full md:w-auto text-center md:text-left'
|
||||
}),
|
||||
|
||||
computed: {
|
||||
helpUrl() {
|
||||
return this.opnformConfig.links.help_url
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<dropdown
|
||||
<Dropdown
|
||||
v-if="user && workspaces && workspaces.length > 1"
|
||||
ref="dropdown"
|
||||
dropdown-class="origin-top-left absolute left-0 mt-2 w-56 rounded-md shadow-lg bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 z-50"
|
||||
@@ -10,72 +10,47 @@
|
||||
#trigger="{ toggle }"
|
||||
>
|
||||
<div
|
||||
class="flex items-center cursor group"
|
||||
class="flex items-center cursor border border-transparent hover:border-gray-200 py-2 px-3 hover:bg-gray-50 rounded-md transition-colors"
|
||||
role="button"
|
||||
@click.stop="toggle()"
|
||||
>
|
||||
<div class="rounded-full h-8 8">
|
||||
<img
|
||||
v-if="isUrl(workspace.icon)"
|
||||
:src="workspace.icon"
|
||||
:alt="workspace.name + ' icon'"
|
||||
class="flex-shrink-0 h-8 w-8 rounded-full shadow"
|
||||
>
|
||||
<div
|
||||
v-else
|
||||
class="rounded-full pt-2 text-xs truncate bg-nt-blue-lighter h-8 w-8 text-center shadow"
|
||||
v-text="workspace.icon"
|
||||
/>
|
||||
</div>
|
||||
<WorkspaceIcon :workspace="workspace" />
|
||||
<p
|
||||
class="hidden group-hover:underline lg:block max-w-10 truncate ml-2 text-gray-800 dark:text-gray-200"
|
||||
class="hidden md:block max-w-10 truncate text-sm ml-2 text-gray-800 dark:text-gray-200"
|
||||
>
|
||||
{{ workspace.name }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template
|
||||
v-for="worksp in workspaces"
|
||||
:key="worksp.id"
|
||||
>
|
||||
<div class="px-1">
|
||||
<a
|
||||
v-for="worksp in workspaces"
|
||||
:key="worksp.id"
|
||||
href="#"
|
||||
class="px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center"
|
||||
class="px-4 py-2 text-md rounded text-gray-700 hover:no-underline hover:bg-neutral-50 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 flex items-center"
|
||||
:class="{
|
||||
'bg-blue-100 dark:bg-blue-900': workspace?.id === worksp?.id,
|
||||
'bg-blue-100 dark:bg-blue-900 hover:bg-blue-200':
|
||||
workspace?.id === worksp?.id,
|
||||
}"
|
||||
@click.prevent="switchWorkspace(worksp)"
|
||||
>
|
||||
<div
|
||||
class="rounded-full h-8 w-8 flex-shrink-0"
|
||||
role="button"
|
||||
>
|
||||
<img
|
||||
v-if="isUrl(worksp.icon)"
|
||||
:src="worksp.icon"
|
||||
:alt="worksp.name + ' icon'"
|
||||
class="flex-shrink-0 h-8 w-8 rounded-full shadow"
|
||||
>
|
||||
<div
|
||||
v-else
|
||||
class="rounded-full flex-shrink-0 pt-1 text-xs truncate bg-nt-blue-lighter h-8 w-8 text-center shadow"
|
||||
v-text="worksp.icon"
|
||||
/>
|
||||
</div>
|
||||
<p class="ml-4 truncate">{{ worksp.name }}</p>
|
||||
<WorkspaceIcon :workspace="worksp" />
|
||||
<p class="ml-4 truncate text-sm">{{ worksp.name }}</p>
|
||||
</a>
|
||||
</template>
|
||||
</dropdown>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed } from "vue"
|
||||
import Dropdown from "~/components/global/Dropdown.vue"
|
||||
import WorkspaceIcon from "~/components/workspaces/WorkspaceIcon.vue"
|
||||
|
||||
export default {
|
||||
name: "WorkspaceDropdown",
|
||||
components: {
|
||||
WorkspaceIcon,
|
||||
Dropdown,
|
||||
},
|
||||
|
||||
@@ -114,14 +89,6 @@ export default {
|
||||
}
|
||||
this.formsStore.loadAll(workspace.id)
|
||||
},
|
||||
isUrl(str) {
|
||||
try {
|
||||
new URL(str)
|
||||
} catch (_) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user