Skip to content

Commit

Permalink
Feat: Move settings from individual pages into one settings modal (#2502
Browse files Browse the repository at this point in the history
)
  • Loading branch information
Mikehrn authored Jul 23, 2024
1 parent 66eb539 commit 65c7dbd
Show file tree
Hide file tree
Showing 47 changed files with 1,979 additions and 1,761 deletions.
484 changes: 309 additions & 175 deletions packages/dui3/lib/common/generated/gql/graphql.ts

Large diffs are not rendered by default.

80 changes: 54 additions & 26 deletions packages/frontend-2/components/header/NavUserMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@
<NuxtLink
:class="[
active ? 'bg-foundation-focus' : '',
'flex gap-2.5 items-center px-3 py-2.5 text-sm text-foreground cursor-pointer transition mx-1 rounded'
'flex gap-3.5 items-center px-3 py-2.5 text-sm text-foreground cursor-pointer transition mx-1 rounded'
]"
@click="() => (showProfileEditDialog = true)"
@click="toggleSettingsDialog(settingsQueries.user.profile)"
>
<UserAvatar :user="activeUser" size="sm" class="-ml-0.5 mr-px" />
Edit profile
<UserCircleIcon class="w-5 h-5" />
Settings
</NuxtLink>
</MenuItem>
<MenuItem v-if="isAdmin" v-slot="{ active }">
Expand All @@ -51,10 +51,10 @@
active ? 'bg-foundation-focus' : '',
'flex gap-3.5 items-center px-3 py-2.5 text-sm text-foreground cursor-pointer transition mx-1 rounded'
]"
@click="goToServerManagement()"
@click="toggleSettingsDialog(settingsQueries.server.general)"
>
<Cog6ToothIcon class="w-5 h-5" />
Server management
<ServerStackIcon class="w-5 h-5" />
Server settings
</NuxtLink>
</MenuItem>
<MenuItem v-slot="{ active }">
Expand Down Expand Up @@ -127,11 +127,16 @@
</MenuItems>
</Transition>
</Menu>
<ServerManagementInviteDialog v-model:open="showInviteDialog" />
<UserProfileEditDialog v-model:open="showProfileEditDialog" />
<SettingsServerUserInviteDialog v-model:open="showInviteDialog" />
<SettingsDialog
v-model:open="showSettingsDialog"
v-model:target-menu-item="settingsDialogTarget"
/>
</div>
</template>
<script setup lang="ts">
import { isString } from 'lodash'
import { useBreakpoints } from '@vueuse/core'
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/vue'
import {
XMarkIcon,
Expand All @@ -140,54 +145,77 @@ import {
SunIcon,
MoonIcon,
EnvelopeIcon,
Cog6ToothIcon,
CloudArrowDownIcon,
ChatBubbleLeftRightIcon
ChatBubbleLeftRightIcon,
UserCircleIcon,
ServerStackIcon
} from '@heroicons/vue/24/outline'
import { Roles } from '@speckle/shared'
import { TailwindBreakpoints } from '~~/lib/common/helpers/tailwind'
import { useActiveUser } from '~~/lib/auth/composables/activeUser'
import { useAuthManager } from '~~/lib/auth/composables/auth'
import { useTheme } from '~~/lib/core/composables/theme'
import { useServerInfo } from '~/lib/core/composables/server'
import { homeRoute, profileRoute, connectorsPageUrl } from '~/lib/common/helpers/route'
import { connectorsPageUrl, settingsQueries } from '~/lib/common/helpers/route'
import type { RouteLocationRaw } from 'vue-router'
import { ToastNotificationType, useGlobalToast } from '~~/lib/common/composables/toast'
defineProps<{
loginUrl?: RouteLocationRaw
}>()
const route = useRoute()
const { logout } = useAuthManager()
const { activeUser, isGuest } = useActiveUser()
const { isDarkTheme, toggleTheme } = useTheme()
const { serverInfo } = useServerInfo()
const router = useRouter()
const route = useRoute()
const { triggerNotification } = useGlobalToast()
const showInviteDialog = ref(false)
const showProfileEditDialog = ref(false)
const showSettingsDialog = ref(false)
const settingsDialogTarget = ref<string | null>(null)
const menuButtonId = useId()
const breakpoints = useBreakpoints(TailwindBreakpoints)
const isMobile = breakpoints.smaller('md')
const Icon = computed(() => (isDarkTheme.value ? SunIcon : MoonIcon))
const version = computed(() => serverInfo.value?.version)
const isAdmin = computed(() => activeUser.value?.role === Roles.Server.Admin)
const isProfileRoute = computed(() => route.path === profileRoute)
const toggleInviteDialog = () => {
showInviteDialog.value = true
}
const goToServerManagement = () => {
router.push('/server-management')
const toggleSettingsDialog = (target: string) => {
showSettingsDialog.value = true
// On mobile open the modal but dont set the target
settingsDialogTarget.value = !isMobile.value ? target : null
}
watch(
isProfileRoute,
(newVal, oldVal) => {
if (newVal && !oldVal) {
showProfileEditDialog.value = true
void router.replace({ path: homeRoute, force: true }) // in-place replace
const deleteSettingsQuery = (): void => {
const currentQueryParams = { ...route.query }
delete currentQueryParams.settings
router.push({ query: currentQueryParams })
}
onMounted(() => {
const settingsQuery = route.query?.settings
if (settingsQuery && isString(settingsQuery)) {
if (settingsQuery.includes('server') && !isAdmin.value) {
triggerNotification({
type: ToastNotificationType.Danger,
title: "You don't have access to server settings"
})
return
}
},
{ immediate: true }
)
showSettingsDialog.value = true
settingsDialogTarget.value = settingsQuery
deleteSettingsQuery()
}
})
</script>
2 changes: 1 addition & 1 deletion packages/frontend-2/components/onboarding/checklist/v1.vue
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@
>
<template #header>Your first upload</template>
</OnboardingDialogFirstSend>
<ServerManagementInviteDialog
<SettingsServerUserInviteDialog
v-model:open="showServerInviteDialog"
@update:open="(v) => (!v ? markComplete(3) : '')"
/>
Expand Down
22 changes: 0 additions & 22 deletions packages/frontend-2/components/server-management/Card.vue

This file was deleted.

51 changes: 0 additions & 51 deletions packages/frontend-2/components/server-management/CardRow.vue

This file was deleted.

158 changes: 158 additions & 0 deletions packages/frontend-2/components/settings/Dialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
<template>
<LayoutDialog
v-model:open="isOpen"
v-bind="
isMobile ? { title: selectedMenuItem ? selectedMenuItem.title : 'Settings' } : {}
"
fullscreen
:show-back-button="isMobile && !!selectedMenuItem"
@back="targetMenuItem = null"
>
<div class="w-full h-full flex">
<LayoutSidebar
v-if="!isMobile || !selectedMenuItem"
class="w-full md:w-56 lg:w-60 md:p-4 md:pt-6 md:bg-foundation-page md:border-r md:border-outline-3"
>
<LayoutSidebarMenu>
<LayoutSidebarMenuGroup title="Account settings">
<template #title-icon>
<UserIcon class="h-5 w-5" />
</template>
<LayoutSidebarMenuGroupItem
v-for="(sidebarMenuItem, key) in menuItemConfig.user"
:key="key"
:label="sidebarMenuItem.title"
:class="{
'bg-foundation-focus hover:!bg-foundation-focus':
selectedMenuItem?.title === sidebarMenuItem.title
}"
@click="targetMenuItem = `${key}`"
/>
</LayoutSidebarMenuGroup>
<LayoutSidebarMenuGroup v-if="isAdmin" title="Server settings">
<template #title-icon>
<ServerStackIcon class="h-5 w-5" />
</template>
<LayoutSidebarMenuGroupItem
v-for="(sidebarMenuItem, key) in menuItemConfig.server"
:key="key"
:label="sidebarMenuItem.title"
:class="{
'bg-foundation-focus hover:!bg-foundation-focus':
selectedMenuItem?.title === sidebarMenuItem.title
}"
@click="targetMenuItem = `${key}`"
/>
</LayoutSidebarMenuGroup>
</LayoutSidebarMenu>
</LayoutSidebar>
<component
:is="selectedMenuItem.component"
v-if="selectedMenuItem"
:class="[
'bg-foundation md:px-10 md:py-12 md:bg-foundation w-full',
!isMobile && 'simple-scrollbar overflow-y-auto flex-1'
]"
:user="user"
/>
</div>
</LayoutDialog>
</template>

<script setup lang="ts">
import type { defineComponent } from 'vue'
import SettingsUserProfile from '~/components/settings/user/Profile.vue'
import SettingsUserNotifications from '~/components/settings/user/Notifications.vue'
import SettingsUserDeveloper from '~/components/settings/user/Developer.vue'
import SettingsServerGeneral from '~/components/settings/server/General.vue'
import SettingsServerProjects from '~/components/settings/server/Projects.vue'
import SettingsServerActiveUsers from '~/components/settings/server/ActiveUsers.vue'
import SettingsServerPendingInvitations from '~/components/settings/server/PendingInvitations.vue'
import { useBreakpoints } from '@vueuse/core'
import { TailwindBreakpoints } from '~~/lib/common/helpers/tailwind'
import { UserIcon, ServerStackIcon } from '@heroicons/vue/24/outline'
import { settingsQueries } from '~/lib/common/helpers/route'
import { useActiveUser } from '~/lib/auth/composables/activeUser'
import {
LayoutSidebar,
LayoutSidebarMenu,
LayoutSidebarMenuGroup
} from '@speckle/ui-components'
import { Roles } from '@speckle/shared'
type MenuItem = {
title: string
component: ReturnType<typeof defineComponent>
}
const { activeUser: user } = useActiveUser()
const breakpoints = useBreakpoints(TailwindBreakpoints)
const isMobile = breakpoints.smaller('md')
const menuItemConfig = shallowRef<{ [key: string]: { [key: string]: MenuItem } }>({
user: {
[settingsQueries.user.profile]: {
title: 'Profile',
component: SettingsUserProfile
},
[settingsQueries.user.notifications]: {
title: 'Notifications',
component: SettingsUserNotifications
},
[settingsQueries.user.developerSettings]: {
title: 'Developer settings',
component: SettingsUserDeveloper
}
},
server: {
[settingsQueries.server.general]: {
title: 'General',
component: SettingsServerGeneral
},
[settingsQueries.server.projects]: {
title: 'Projects',
component: SettingsServerProjects
},
[settingsQueries.server.activeUsers]: {
title: 'Active users',
component: SettingsServerActiveUsers
},
[settingsQueries.server.pendingInvitations]: {
title: 'Pending invitations',
component: SettingsServerPendingInvitations
}
}
})
const isOpen = defineModel<boolean>('open', { required: true })
const targetMenuItem = defineModel<string | null>('targetMenuItem', { required: true })
const isAdmin = computed(() => user.value?.role === Roles.Server.Admin)
const selectedMenuItem = computed((): MenuItem | null => {
const categories = [menuItemConfig.value.user, menuItemConfig.value.server]
for (const category of categories) {
if (targetMenuItem.value && targetMenuItem.value in category) {
return category[targetMenuItem.value]
}
}
if (!isMobile.value && targetMenuItem.value) {
// Fallback for invalid queries/typos
return targetMenuItem.value.includes('server') && isAdmin.value
? menuItemConfig.value.server.general
: menuItemConfig.value.user.profile
}
return null
})
watch(
() => user.value,
(newVal) => {
if (!newVal) {
isOpen.value = false
}
},
{ immediate: true }
)
</script>
Loading

0 comments on commit 65c7dbd

Please sign in to comment.