mirror of https://github.com/nocodb/nocodb
189 changed files with 2745 additions and 3910 deletions
After Width: | Height: | Size: 685 B |
@ -0,0 +1,60 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
const workspaceStore = useWorkspace() |
||||||
|
const projectStore = useProject() |
||||||
|
|
||||||
|
const { isLeftSidebarOpen } = storeToRefs(useSidebarStore()) |
||||||
|
|
||||||
|
const { isSharedBase } = storeToRefs(projectStore) |
||||||
|
const { activeWorkspace, isWorkspaceLoading } = storeToRefs(workspaceStore) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div |
||||||
|
class="flex items-center px-2 nc-sidebar-header py-1.2 w-full border-b-1 border-gray-200 group" |
||||||
|
:data-workspace-title="activeWorkspace?.title" |
||||||
|
style="height: var(--topbar-height)" |
||||||
|
> |
||||||
|
<div v-if="!isWorkspaceLoading" class="flex flex-row items-center w-full"> |
||||||
|
<WorkspaceMenu /> |
||||||
|
|
||||||
|
<div class="flex flex-grow min-w-1"></div> |
||||||
|
|
||||||
|
<NcTooltip |
||||||
|
class="flex opacity-0 group-hover:opacity-100 transition-opacity duration-50" |
||||||
|
:class="{ |
||||||
|
'!opacity-100': !isLeftSidebarOpen, |
||||||
|
}" |
||||||
|
placement="bottom" |
||||||
|
hide-on-click |
||||||
|
> |
||||||
|
<template #title> |
||||||
|
{{ |
||||||
|
isLeftSidebarOpen |
||||||
|
? `${$t('general.hide')} ${$t('objects.sidebar').toLowerCase()}` |
||||||
|
: `${$t('general.show')} ${$t('objects.sidebar').toLowerCase()}` |
||||||
|
}} |
||||||
|
</template> |
||||||
|
<NcButton |
||||||
|
type="text" |
||||||
|
size="small" |
||||||
|
class="nc-sidebar-left-toggle-icon !text-gray-700 !hover:text-gray-800 !hover:bg-gray-200" |
||||||
|
@click="isLeftSidebarOpen = !isLeftSidebarOpen" |
||||||
|
> |
||||||
|
<div class="flex items-center text-inherit"> |
||||||
|
<GeneralIcon |
||||||
|
icon="doubleLeftArrow" |
||||||
|
class="duration-150 transition-all !text-lg -mt-0.5" |
||||||
|
:class="{ |
||||||
|
'transform rotate-180': !isLeftSidebarOpen, |
||||||
|
}" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
</NcButton> |
||||||
|
</NcTooltip> |
||||||
|
</div> |
||||||
|
<div v-else class="flex flex-row items-center w-full mt-0.25 ml-2.5 gap-x-3"> |
||||||
|
<a-skeleton-input :active="true" class="!w-6 !h-6 !rounded overflow-hidden" /> |
||||||
|
<a-skeleton-input :active="true" class="!w-40 !h-6 !rounded overflow-hidden" /> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
@ -0,0 +1,86 @@ |
|||||||
|
<script setup lang="ts"> |
||||||
|
const workspaceStore = useWorkspace() |
||||||
|
const projectStore = useProject() |
||||||
|
|
||||||
|
const { appInfo } = useGlobal() |
||||||
|
|
||||||
|
const { isWorkspaceLoading, isWorkspaceOwnerOrCreator, isWorkspaceSettingsPageOpened } = storeToRefs(workspaceStore) |
||||||
|
|
||||||
|
const { navigateToWorkspaceSettings } = workspaceStore |
||||||
|
|
||||||
|
const { isSharedBase } = storeToRefs(projectStore) |
||||||
|
|
||||||
|
const isCreateProjectOpen = ref(false) |
||||||
|
|
||||||
|
const navigateToSettings = () => { |
||||||
|
// TODO: Handle cloud case properly |
||||||
|
navigateToWorkspaceSettings() |
||||||
|
|
||||||
|
// if (appInfo.value.baseHostName) { |
||||||
|
// window.location.href = `https://app.${appInfo.value.baseHostName}/dashboard` |
||||||
|
// } else { |
||||||
|
// } |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<template v-if="isWorkspaceLoading"> |
||||||
|
<div class="flex flex-col w-full gap-y-3.75 ml-3 mt-3.75"> |
||||||
|
<div v-if="appInfo.ee" class="flex flex-row items-center w-full gap-x-3"> |
||||||
|
<a-skeleton-input :active="true" class="!w-4 !h-4 !rounded overflow-hidden" /> |
||||||
|
<a-skeleton-input :active="true" class="!w-40 !h-4 !rounded overflow-hidden" /> |
||||||
|
</div> |
||||||
|
<div class="flex flex-row items-center w-full gap-x-3"> |
||||||
|
<a-skeleton-input :active="true" class="!w-4 !h-4 !rounded overflow-hidden" /> |
||||||
|
<a-skeleton-input :active="true" class="!w-40 !h-4 !rounded overflow-hidden" /> |
||||||
|
</div> |
||||||
|
<div class="flex flex-row items-center w-full gap-x-3"> |
||||||
|
<a-skeleton-input :active="true" class="!w-4 !h-4 !rounded overflow-hidden" /> |
||||||
|
<a-skeleton-input :active="true" class="!w-40 !h-4 !rounded overflow-hidden" /> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
<template v-else-if="!isSharedBase"> |
||||||
|
<div class="flex flex-col p-1 gap-y-0.5 mt-0.25"> |
||||||
|
<DashboardSidebarTopSectionHeader /> |
||||||
|
|
||||||
|
<NcButton |
||||||
|
v-if="isWorkspaceOwnerOrCreator" |
||||||
|
type="text" |
||||||
|
size="small" |
||||||
|
class="nc-sidebar-top-button" |
||||||
|
data-testid="nc-sidebar-team-settings-btn" |
||||||
|
:centered="false" |
||||||
|
:class="{ |
||||||
|
'!text-brand-500 !bg-brand-50 !hover:bg-brand-50': isWorkspaceSettingsPageOpened, |
||||||
|
'!hover:bg-gray-200': !isWorkspaceSettingsPageOpened, |
||||||
|
}" |
||||||
|
@click="navigateToSettings" |
||||||
|
> |
||||||
|
<div class="flex items-center gap-2"> |
||||||
|
<GeneralIcon icon="settings" class="!h-4" /> |
||||||
|
<div>Team & Settings</div> |
||||||
|
</div> |
||||||
|
</NcButton> |
||||||
|
<WorkspaceCreateProjectBtn |
||||||
|
v-model:is-open="isCreateProjectOpen" |
||||||
|
modal |
||||||
|
type="text" |
||||||
|
class="nc-sidebar-top-button !hover:bg-gray-200" |
||||||
|
data-testid="nc-sidebar-create-project-btn" |
||||||
|
> |
||||||
|
<div class="gap-x-2 flex flex-row w-full items-center !font-normal"> |
||||||
|
<GeneralIcon icon="plus" /> |
||||||
|
|
||||||
|
<div class="flex">{{ $t('title.newProj') }}</div> |
||||||
|
</div> |
||||||
|
</WorkspaceCreateProjectBtn> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.nc-sidebar-top-button { |
||||||
|
@apply w-full !rounded-md !font-normal !px-3; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,188 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import GithubButton from 'vue-github-button' |
||||||
|
|
||||||
|
const { user, signOut, token, appInfo } = useGlobal() |
||||||
|
|
||||||
|
const { clearWorkspaces } = useWorkspace() |
||||||
|
|
||||||
|
const { leftSidebarState } = storeToRefs(useSidebarStore()) |
||||||
|
|
||||||
|
const { copy } = useCopy(true) |
||||||
|
|
||||||
|
const name = computed(() => `${user.value?.firstname ?? ''} ${user.value?.lastname ?? ''}`.trim()) |
||||||
|
|
||||||
|
const isMenuOpen = ref(false) |
||||||
|
|
||||||
|
const isAuthTokenCopied = ref(false) |
||||||
|
|
||||||
|
const isLoggingOut = ref(false) |
||||||
|
|
||||||
|
const logout = async () => { |
||||||
|
isLoggingOut.value = true |
||||||
|
try { |
||||||
|
await signOut(false) |
||||||
|
|
||||||
|
await clearWorkspaces() |
||||||
|
|
||||||
|
await navigateTo('/signin') |
||||||
|
} catch (e) { |
||||||
|
console.error(e) |
||||||
|
} finally { |
||||||
|
isLoggingOut.value = false |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const onCopy = async () => { |
||||||
|
try { |
||||||
|
await copy(token.value!) |
||||||
|
isAuthTokenCopied.value = true |
||||||
|
} catch (e: any) { |
||||||
|
console.error(e) |
||||||
|
message.error(e.message) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
watch(isMenuOpen, () => { |
||||||
|
if (isAuthTokenCopied.value) { |
||||||
|
isAuthTokenCopied.value = false |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
watch(leftSidebarState, () => { |
||||||
|
if (leftSidebarState.value === 'peekCloseEnd') { |
||||||
|
isMenuOpen.value = false |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
// This is a hack to prevent github button error (prevents navigateTo if user is not signed in) |
||||||
|
const isMounted = ref(false) |
||||||
|
|
||||||
|
onMounted(() => { |
||||||
|
isMounted.value = true |
||||||
|
}) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div class="flex w-full flex-col p-1 border-t-1 border-gray-200 gap-y-2"> |
||||||
|
<NcDropdown v-model:visible="isMenuOpen" placement="topLeft" overlay-class-name="!min-w-64"> |
||||||
|
<div |
||||||
|
class="flex flex-row py-2 px-3 gap-x-2 items-center hover:bg-gray-200 rounded-lg cursor-pointer h-10" |
||||||
|
data-testid="nc-sidebar-userinfo" |
||||||
|
> |
||||||
|
<GeneralUserIcon /> |
||||||
|
<div class="flex truncate"> |
||||||
|
{{ name ? name : user?.email }} |
||||||
|
</div> |
||||||
|
<GeneralIcon icon="arrowUp" class="!min-w-5" /> |
||||||
|
</div> |
||||||
|
<template #overlay> |
||||||
|
<NcMenu> |
||||||
|
<NcMenuItem data-testid="nc-sidebar-user-logout" @click="logout"> |
||||||
|
<GeneralLoader v-if="isLoggingOut" class="!ml-0.5 !mr-0.5 !max-h-4.5 !-mt-0.5" /> |
||||||
|
<GeneralIcon v-else icon="signout" class="menu-icon" /> |
||||||
|
Log Out</NcMenuItem |
||||||
|
> |
||||||
|
<NcDivider /> |
||||||
|
<a href="https://docs.nocodb.com" target="_blank" class="!underline-transparent"> |
||||||
|
<NcMenuItem> |
||||||
|
<GeneralIcon icon="help" class="menu-icon" /> |
||||||
|
Help Center</NcMenuItem |
||||||
|
> |
||||||
|
</a> |
||||||
|
<NcDivider /> |
||||||
|
<a href="https://discord.gg/5RgZmkW" target="_blank" class="!underline-transparent"> |
||||||
|
<NcMenuItem class="social-icon-wrapper" |
||||||
|
><GeneralIcon class="social-icon" icon="discord" />Join our Discord</NcMenuItem |
||||||
|
> |
||||||
|
</a> |
||||||
|
<a href="https://www.reddit.com/r/NocoDB" target="_blank" class="!underline-transparent"> |
||||||
|
<NcMenuItem class="social-icon-wrapper"><GeneralIcon class="social-icon" icon="reddit" />/r/NocoDB</NcMenuItem> |
||||||
|
</a> |
||||||
|
<a href="https://twitter.com/nocodb" target="_blank" class="!underline-transparent"> |
||||||
|
<NcMenuItem class="social-icon-wrapper group" |
||||||
|
><GeneralIcon class="text-gray-500 group-hover:text-gray-800" icon="twitter" />Twitter</NcMenuItem |
||||||
|
> |
||||||
|
</a> |
||||||
|
<template v-if="!appInfo.ee"> |
||||||
|
<NcDivider /> |
||||||
|
<a-popover key="language" class="lang-menu !py-0" placement="rightBottom"> |
||||||
|
<NcMenuItem> |
||||||
|
<GeneralIcon icon="translate" class="group-hover:text-black nc-language ml-0.25 menu-icon" /> |
||||||
|
{{ $t('labels.language') }} |
||||||
|
<div class="flex items-center text-gray-400 text-xs">(Community Translated)</div> |
||||||
|
<div class="flex-1" /> |
||||||
|
|
||||||
|
<MaterialSymbolsChevronRightRounded class="transform group-hover:(scale-115 text-accent) text-xl text-gray-400" /> |
||||||
|
</NcMenuItem> |
||||||
|
|
||||||
|
<template #content> |
||||||
|
<div class="bg-white max-h-50vh scrollbar-thin-dull min-w-50 !overflow-auto"> |
||||||
|
<LazyGeneralLanguageMenu /> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
</a-popover> |
||||||
|
</template> |
||||||
|
|
||||||
|
<NcDivider /> |
||||||
|
<NcMenuItem @click="onCopy" |
||||||
|
><GeneralIcon v-if="isAuthTokenCopied" icon="check" class="group-hover:text-black menu-icon" /><GeneralIcon |
||||||
|
v-else |
||||||
|
icon="copy" |
||||||
|
class="menu-icon" |
||||||
|
/> |
||||||
|
<template v-if="isAuthTokenCopied"> Copied Auth Token </template> |
||||||
|
<template v-else> Copy Auth Token </template> |
||||||
|
</NcMenuItem> |
||||||
|
<nuxt-link v-e="['c:navbar:user:email']" class="!no-underline" to="/account/tokens"> |
||||||
|
<NcMenuItem><GeneralIcon icon="settings" class="menu-icon" /> Account Settings</NcMenuItem> |
||||||
|
</nuxt-link> |
||||||
|
</NcMenu> |
||||||
|
</template> |
||||||
|
</NcDropdown> |
||||||
|
|
||||||
|
<div v-if="appInfo.ee" class="text-gray-500 text-xs pl-3">© 2023 NocoDB. Inc</div> |
||||||
|
<div v-else-if="isMounted" class="flex flex-col gap-y-1 pt-1"> |
||||||
|
<div class="flex items-start flex-row justify-center px-2 gap-2"> |
||||||
|
<GithubButton href="https://github.com/nocodb/nocodb" data-icon="octicon-star" data-show-count="true" data-size="large"> |
||||||
|
Star |
||||||
|
</GithubButton> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="flex items-start flex-row justify-center gap-2"> |
||||||
|
<GeneralJoinCloud class="color-transition px-2 text-gray-500 cursor-pointer select-none hover:text-accent" /> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.menu-icon { |
||||||
|
@apply !min-h-4.5; |
||||||
|
line-height: 1rem; |
||||||
|
font-size: 1.125rem; |
||||||
|
} |
||||||
|
|
||||||
|
:deep(.ant-popover-inner-content) { |
||||||
|
@apply !p-0 !rounded-md; |
||||||
|
} |
||||||
|
.social-icon { |
||||||
|
// Make icon black and white |
||||||
|
filter: grayscale(100%); |
||||||
|
|
||||||
|
// Make icon red on hover |
||||||
|
&:hover { |
||||||
|
filter: grayscale(100%) invert(100%); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.social-icon-wrapper { |
||||||
|
.nc-icon { |
||||||
|
@apply mr-0.15; |
||||||
|
} |
||||||
|
&:hover { |
||||||
|
.social-icon { |
||||||
|
filter: none !important; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,53 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
const { isLeftSidebarOpen: _isLeftSidebarOpen } = storeToRefs(useSidebarStore()) |
||||||
|
const isLeftSidebarOpen = ref(_isLeftSidebarOpen.value) |
||||||
|
|
||||||
|
watch(_isLeftSidebarOpen, (val) => { |
||||||
|
if (val) { |
||||||
|
isLeftSidebarOpen.value = true |
||||||
|
} else { |
||||||
|
setTimeout(() => { |
||||||
|
isLeftSidebarOpen.value = false |
||||||
|
}, 300) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
const onClick = () => { |
||||||
|
if (_isLeftSidebarOpen.value) return |
||||||
|
|
||||||
|
_isLeftSidebarOpen.value = !_isLeftSidebarOpen.value |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<NcTooltip |
||||||
|
placement="topLeft" |
||||||
|
hide-on-click |
||||||
|
class="transition-all duration-100" |
||||||
|
:class="{ |
||||||
|
'!w-0 !opacity-0': isLeftSidebarOpen, |
||||||
|
'!w-8 !opacity-100': !isLeftSidebarOpen, |
||||||
|
}" |
||||||
|
> |
||||||
|
<template #title> |
||||||
|
{{ |
||||||
|
isLeftSidebarOpen |
||||||
|
? `${$t('general.hide')} ${$t('objects.sidebar').toLowerCase()}` |
||||||
|
: `${$t('general.show')} ${$t('objects.sidebar').toLowerCase()}` |
||||||
|
}} |
||||||
|
</template> |
||||||
|
<NcButton |
||||||
|
type="text" |
||||||
|
size="small" |
||||||
|
class="nc-sidebar-left-toggle-icon !text-gray-600 !hover:text-gray-800" |
||||||
|
:class="{ |
||||||
|
'invisible !w-0': isLeftSidebarOpen, |
||||||
|
}" |
||||||
|
@click="onClick" |
||||||
|
> |
||||||
|
<div class="flex items-center text-inherit"> |
||||||
|
<GeneralIcon icon="doubleRightArrow" class="duration-150 transition-all !text-lg -mt-0.25" /> |
||||||
|
</div> |
||||||
|
</NcButton> |
||||||
|
</NcTooltip> |
||||||
|
</template> |
@ -0,0 +1,51 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import type { UserType } from 'nocodb-sdk' |
||||||
|
|
||||||
|
const props = defineProps<{ |
||||||
|
hideLabel?: boolean |
||||||
|
size?: 'small' | 'medium' |
||||||
|
}>() |
||||||
|
|
||||||
|
const { user } = useGlobal() |
||||||
|
|
||||||
|
const backgroundColor = computed(() => (user.value?.id ? stringToColour(user.value?.id) : '#FFFFFF')) |
||||||
|
|
||||||
|
const size = computed(() => props.size || 'medium') |
||||||
|
|
||||||
|
const firstName = computed(() => user.value?.firstname ?? '') |
||||||
|
const lastName = computed(() => user.value?.lastname ?? '') |
||||||
|
const email = computed(() => user.value?.email ?? '') |
||||||
|
|
||||||
|
const usernameInitials = computed(() => { |
||||||
|
if (firstName.value && lastName.value) { |
||||||
|
return firstName.value[0] + lastName.value[0] |
||||||
|
} else if (firstName.value) { |
||||||
|
return firstName.value[0] + (firstName.value.length > 1 ? firstName.value[1] : '') |
||||||
|
} else if (lastName.value) { |
||||||
|
return lastName.value[0] + (lastName.value.length > 1 ? lastName.value[1] : '') |
||||||
|
} else { |
||||||
|
return email.value[0] + email.value[1] |
||||||
|
} |
||||||
|
}) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div |
||||||
|
class="flex nc-user-avatar" |
||||||
|
:class="{ |
||||||
|
'min-w-4 min-h-4': size === 'small', |
||||||
|
'min-w-6 min-h-6': size === 'medium', |
||||||
|
}" |
||||||
|
:style="{ backgroundColor }" |
||||||
|
> |
||||||
|
<template v-if="!props.hideLabel"> |
||||||
|
{{ usernameInitials }} |
||||||
|
</template> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.nc-user-avatar { |
||||||
|
@apply rounded-full text-xs flex items-center justify-center text-white uppercase; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,37 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
import type { WorkspaceType } from 'nocodb-sdk' |
||||||
|
|
||||||
|
const props = defineProps<{ |
||||||
|
workspace: WorkspaceType | undefined |
||||||
|
hideLabel?: boolean |
||||||
|
size?: 'small' | 'medium' | 'large' |
||||||
|
}>() |
||||||
|
|
||||||
|
const workspaceColor = computed(() => |
||||||
|
props.workspace ? props.workspace.meta?.color || stringToColour(props.workspace.id!) : undefined, |
||||||
|
) |
||||||
|
|
||||||
|
const size = computed(() => props.size || 'medium') |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div |
||||||
|
class="flex nc-workspace-avatar" |
||||||
|
:class="{ |
||||||
|
'min-w-4 w-4 h-4 rounded': size === 'small', |
||||||
|
'min-w-6 w-6 h-6 rounded-md': size === 'medium', |
||||||
|
'min-w-10 w-10 h-10 rounded-lg !text-base': size === 'large', |
||||||
|
}" |
||||||
|
:style="{ backgroundColor: workspaceColor }" |
||||||
|
> |
||||||
|
<template v-if="!props.hideLabel"> |
||||||
|
{{ props.workspace?.title?.slice(0, 2) }} |
||||||
|
</template> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.nc-workspace-avatar { |
||||||
|
@apply text-xs flex items-center justify-center text-white uppercase; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,9 @@ |
|||||||
|
<template> |
||||||
|
<a-divider class="nc-divider" /> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style lang="scss"> |
||||||
|
.nc-divider.ant-divider { |
||||||
|
@apply !my-1; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,19 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
const props = defineProps<{ |
||||||
|
selectable?: boolean | undefined |
||||||
|
}>() |
||||||
|
|
||||||
|
const selectable = computed(() => props.selectable ?? false) |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<a-menu class="nc-menu" :selectable="selectable"> |
||||||
|
<slot /> |
||||||
|
</a-menu> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style lang="scss"> |
||||||
|
.nc-menu { |
||||||
|
@apply bg-white !rounded-md !py-1; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,17 @@ |
|||||||
|
<template> |
||||||
|
<a-menu-item class="nc-menu-item"> |
||||||
|
<div class="nc-menu-item-inner"> |
||||||
|
<slot /> |
||||||
|
</div> |
||||||
|
</a-menu-item> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style lang="scss"> |
||||||
|
.nc-menu-item { |
||||||
|
@apply !py-2 font-normal text-sm; |
||||||
|
} |
||||||
|
|
||||||
|
.nc-menu-item-inner { |
||||||
|
@apply flex flex-row items-center gap-x-2.25; |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,53 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
const { isRightSidebarOpen: _isRightSidebarOpen } = storeToRefs(useSidebarStore()) |
||||||
|
const isRightSidebarOpen = ref(_isRightSidebarOpen.value) |
||||||
|
|
||||||
|
watch(_isRightSidebarOpen, (val) => { |
||||||
|
if (val) { |
||||||
|
isRightSidebarOpen.value = true |
||||||
|
} else { |
||||||
|
setTimeout(() => { |
||||||
|
isRightSidebarOpen.value = false |
||||||
|
}, 300) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
const onClick = () => { |
||||||
|
if (_isRightSidebarOpen.value) return |
||||||
|
|
||||||
|
_isRightSidebarOpen.value = !_isRightSidebarOpen.value |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<NcTooltip |
||||||
|
placement="bottomLeft" |
||||||
|
hide-on-click |
||||||
|
class="transition-all duration-100" |
||||||
|
:class="{ |
||||||
|
'!w-0 !opacity-0': isRightSidebarOpen, |
||||||
|
'!w-8 !opacity-100 mr-2': !isRightSidebarOpen, |
||||||
|
}" |
||||||
|
> |
||||||
|
<template #title> |
||||||
|
{{ |
||||||
|
isRightSidebarOpen |
||||||
|
? `${$t('general.hide')} ${$t('objects.sidebar').toLowerCase()}` |
||||||
|
: `${$t('general.show')} ${$t('objects.sidebar').toLowerCase()}` |
||||||
|
}} |
||||||
|
</template> |
||||||
|
<NcButton |
||||||
|
type="text" |
||||||
|
size="small" |
||||||
|
class="nc-sidebar-right-toggle-icon !text-gray-600 !hover:text-gray-800" |
||||||
|
:class="{ |
||||||
|
'invisible !w-0': isRightSidebarOpen, |
||||||
|
}" |
||||||
|
@click="onClick" |
||||||
|
> |
||||||
|
<div class="flex items-center text-inherit"> |
||||||
|
<GeneralIcon icon="doubleLeftArrow" class="duration-150 transition-all !text-lg -mt-0.25" /> |
||||||
|
</div> |
||||||
|
</NcButton> |
||||||
|
</NcTooltip> |
||||||
|
</template> |
@ -1,61 +1,42 @@ |
|||||||
<script setup lang="ts"> |
<script setup lang="ts"> |
||||||
import { NcProjectType, useRouter } from '#imports' |
import type { NcButtonSize } from '~/lib' |
||||||
|
|
||||||
const props = defineProps<{ |
const props = defineProps<{ |
||||||
activeWorkspaceId?: string | undefined |
activeWorkspaceId?: string | undefined |
||||||
modal?: boolean |
modal?: boolean |
||||||
type?: string |
type?: string |
||||||
isOpen: boolean |
isOpen: boolean |
||||||
|
size?: NcButtonSize |
||||||
|
centered?: boolean |
||||||
}>() |
}>() |
||||||
|
|
||||||
const router = useRouter() |
const { isUIAllowed } = useUIPermission() |
||||||
|
|
||||||
|
const { orgRoles, workspaceRoles } = useRoles() |
||||||
|
|
||||||
|
const projectStore = useProject() |
||||||
|
const { isSharedBase } = storeToRefs(projectStore) |
||||||
|
|
||||||
|
const workspaceStore = useWorkspace() |
||||||
|
const { activeWorkspaceId: _activeWorkspaceId } = storeToRefs(workspaceStore) |
||||||
|
|
||||||
const projectCreateDlg = ref(false) |
const projectCreateDlg = ref(false) |
||||||
const projectType = ref(NcProjectType.DB) |
|
||||||
|
const size = computed(() => props.size || 'small') |
||||||
const navigateToCreateProject = (type: NcProjectType) => { |
const centered = computed(() => props.centered ?? true) |
||||||
if (props.modal) { |
|
||||||
projectType.value = type |
|
||||||
projectCreateDlg.value = true |
|
||||||
} else { |
|
||||||
router.push({ |
|
||||||
path: '/create', |
|
||||||
query: { |
|
||||||
type, |
|
||||||
workspaceId: props.activeWorkspaceId, |
|
||||||
}, |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
useEventListener(document, 'keydown', async (e: KeyboardEvent) => { |
|
||||||
const cmdOrCtrl = isMac() ? e.metaKey : e.ctrlKey |
|
||||||
if (e.altKey && !e.shiftKey && !cmdOrCtrl) { |
|
||||||
switch (e.keyCode) { |
|
||||||
// ALT + D |
|
||||||
case 68: { |
|
||||||
e.stopPropagation() |
|
||||||
navigateToCreateProject(NcProjectType.DB) |
|
||||||
break |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
}) |
|
||||||
</script> |
</script> |
||||||
|
|
||||||
<template> |
<template> |
||||||
<div> |
<NcButton |
||||||
<a-button |
v-if="isUIAllowed('projectCreate', false, workspaceRoles ?? orgRoles) && !isSharedBase" |
||||||
class="!py-0 !px-0 !border-0 !h-full !rounded-md w-full hover:bg-gray-100 text-sm select-none cursor-pointer" |
type="text" |
||||||
:type="props.type ?? 'primary'" |
:size="size" |
||||||
@click="navigateToCreateProject(NcProjectType.DB)" |
:centered="centered" |
||||||
> |
@click="projectCreateDlg = true" |
||||||
<div class="flex w-full items-center gap-2"> |
> |
||||||
<slot>{{ $t('title.newProj') }} <MdiMenuDown /></slot> |
<slot /> |
||||||
</div> |
<WorkspaceCreateProjectDlg v-model="projectCreateDlg" /> |
||||||
</a-button> |
</NcButton> |
||||||
<WorkspaceCreateProjectDlg v-model="projectCreateDlg" :type="projectType" /> |
|
||||||
</div> |
|
||||||
</template> |
</template> |
||||||
|
|
||||||
<style scoped></style> |
<style scoped></style> |
||||||
|
@ -1,66 +0,0 @@ |
|||||||
<script lang="ts" setup> |
|
||||||
const isConfirmed = ref(false) |
|
||||||
const isDeleting = ref(false) |
|
||||||
const { signOut } = useGlobal() |
|
||||||
|
|
||||||
const { deleteWorkspace, navigateToWorkspace } = useWorkspace() |
|
||||||
const { workspacesList, activeWorkspaceId } = storeToRefs(useWorkspace()) |
|
||||||
|
|
||||||
const onDelete = async () => { |
|
||||||
isDeleting.value = true |
|
||||||
try { |
|
||||||
await deleteWorkspace(activeWorkspaceId.value) |
|
||||||
|
|
||||||
isConfirmed.value = false |
|
||||||
isDeleting.value = false |
|
||||||
|
|
||||||
if (workspacesList.value.length > 0) { |
|
||||||
await navigateToWorkspace(workspacesList.value[0].id) |
|
||||||
} else { |
|
||||||
await signOut() |
|
||||||
setTimeout(() => { |
|
||||||
window.location.href = '/' |
|
||||||
}, 100) |
|
||||||
} |
|
||||||
} finally { |
|
||||||
isDeleting.value = false |
|
||||||
} |
|
||||||
} |
|
||||||
</script> |
|
||||||
|
|
||||||
<template> |
|
||||||
<div class="flex flex-col items-center"> |
|
||||||
<div class="item flex flex-col"> |
|
||||||
<div class="font-medium text-base">Delete Workspace</div> |
|
||||||
<div class="text-gray-500 mt-2">Delete this workspace and all it’s contents.</div> |
|
||||||
<div class="flex flex-row p-4 border-1 rounded-lg gap-x-2 mt-6"> |
|
||||||
<div class="flex"> |
|
||||||
<GeneralIcon icon="warning" class="text-xl text-orange-600" /> |
|
||||||
</div> |
|
||||||
<div class="flex flex-col items-start gap-y-1"> |
|
||||||
<div class="flex font-medium">This action is irreversible.</div> |
|
||||||
<div class="flex flex-row text-gray-500"> |
|
||||||
You have 31 days to undo this action by following the steps provided in recover workspace mail sent to your email. |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
<div class="flex flex-row mt-8 gap-x-2"> |
|
||||||
<a-checkbox v-model:checked="isConfirmed" /> |
|
||||||
<div class="flex">I understand that this action is irreversible</div> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div class="flex flex-row w-full justify-end mt-8"> |
|
||||||
<NcButton type="danger" :disabled="!isConfirmed" :loading="isDeleting" @click="onDelete"> |
|
||||||
<template #loading> Deleting Workspace </template> |
|
||||||
Delete Workspace |
|
||||||
</NcButton> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</template> |
|
||||||
|
|
||||||
<style lang="scss" scoped> |
|
||||||
.item { |
|
||||||
@apply p-6 rounded-2xl border-1 max-w-180 mt-10; |
|
||||||
} |
|
||||||
</style> |
|
@ -1,371 +1,29 @@ |
|||||||
<script lang="ts" setup> |
<script lang="ts" setup></script> |
||||||
import { storeToRefs } from 'pinia' |
|
||||||
import type { WorkspaceType } from 'nocodb-sdk' |
|
||||||
import tinycolor from 'tinycolor2' |
|
||||||
import { onMounted, projectThemeColors, ref, useWorkspace } from '#imports' |
|
||||||
import { navigateTo } from '#app' |
|
||||||
|
|
||||||
const props = defineProps<{ |
|
||||||
isOpen: boolean |
|
||||||
}>() |
|
||||||
|
|
||||||
const workspaceStore = useWorkspace() |
|
||||||
|
|
||||||
const { saveTheme } = workspaceStore |
|
||||||
const { isWorkspaceOwner } = storeToRefs(workspaceStore) |
|
||||||
const { loadWorkspaces, clearWorkspaces } = workspaceStore |
|
||||||
|
|
||||||
const { signOut, signedIn, user, token } = useGlobal() |
|
||||||
|
|
||||||
const { copy } = useCopy(true) |
|
||||||
|
|
||||||
const email = computed(() => user.value?.email ?? '---') |
|
||||||
|
|
||||||
const { isUIAllowed } = useUIPermission() |
|
||||||
|
|
||||||
const { theme, defaultTheme } = useTheme() |
|
||||||
|
|
||||||
onMounted(async () => { |
|
||||||
// await loadWorkspaces() |
|
||||||
}) |
|
||||||
|
|
||||||
const workspaceModalVisible = ref(false) |
|
||||||
const isWorkspaceDropdownOpen = ref(false) |
|
||||||
const isAuthTokenCopied = ref(false) |
|
||||||
|
|
||||||
const handleThemeColor = async (mode: 'swatch' | 'primary' | 'accent', color?: string) => { |
|
||||||
switch (mode) { |
|
||||||
case 'swatch': { |
|
||||||
if (color === defaultTheme.primaryColor) { |
|
||||||
return await saveTheme(defaultTheme) |
|
||||||
} |
|
||||||
|
|
||||||
const tcolor = tinycolor(color) |
|
||||||
if (tcolor.isValid()) { |
|
||||||
const complement = tcolor.complement() |
|
||||||
|
|
||||||
await saveTheme({ |
|
||||||
primaryColor: color, |
|
||||||
accentColor: complement.toHex8String(), |
|
||||||
}) |
|
||||||
} |
|
||||||
break |
|
||||||
} |
|
||||||
case 'primary': { |
|
||||||
const tcolor = tinycolor(color) |
|
||||||
|
|
||||||
if (tcolor.isValid()) { |
|
||||||
await saveTheme({ |
|
||||||
primaryColor: color, |
|
||||||
}) |
|
||||||
} |
|
||||||
break |
|
||||||
} |
|
||||||
case 'accent': { |
|
||||||
const tcolor = tinycolor(color) |
|
||||||
|
|
||||||
if (tcolor.isValid()) { |
|
||||||
await saveTheme({ |
|
||||||
accentColor: color, |
|
||||||
}) |
|
||||||
} |
|
||||||
break |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
const logout = async () => { |
|
||||||
await signOut() |
|
||||||
navigateTo('/signin') |
|
||||||
} |
|
||||||
|
|
||||||
const projectStore = useProject() |
|
||||||
|
|
||||||
const { isSharedBase } = storeToRefs(projectStore) |
|
||||||
|
|
||||||
// todo: temp |
|
||||||
const modalVisible = false |
|
||||||
|
|
||||||
const copyAuthToken = async () => { |
|
||||||
try { |
|
||||||
await copy(token.value!) |
|
||||||
isAuthTokenCopied.value = true |
|
||||||
} catch (e: any) { |
|
||||||
console.error(e) |
|
||||||
message.error(e.message) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
onKeyStroke('Escape', () => { |
|
||||||
if (isWorkspaceDropdownOpen.value) { |
|
||||||
isWorkspaceDropdownOpen.value = false |
|
||||||
} |
|
||||||
}) |
|
||||||
</script> |
|
||||||
|
|
||||||
<template> |
<template> |
||||||
<div class="flex-grow min-w-20"> |
<div class="flex flex-row flex-grow pl-0.5 pr-1 py-0.5 rounded-md w-full" style="max-width: calc(100% - 2.5rem)"> |
||||||
<a-dropdown |
<div class="flex-grow min-w-20"> |
||||||
v-model:visible="isWorkspaceDropdownOpen" |
|
||||||
class="h-full min-w-0 flex-1" |
|
||||||
:trigger="['click']" |
|
||||||
placement="bottom" |
|
||||||
overlay-class-name="nc-dropdown-workspace-menu" |
|
||||||
> |
|
||||||
<div |
<div |
||||||
:style="{ width: props.isOpen ? 'calc(100% - 40px) pr-2' : '100%' }" |
|
||||||
:class="[props.isOpen ? '' : 'justify-center']" |
|
||||||
data-testid="nc-workspace-menu" |
data-testid="nc-workspace-menu" |
||||||
class="group cursor-pointer flex gap-1 items-center nc-workspace-menu overflow-hidden py-1.25 pr-0.25" |
class="flex items-center nc-workspace-menu overflow-hidden py-1.25 pr-0.25 justify-center w-full" |
||||||
> |
> |
||||||
<slot name="brandIcon" /> |
<a |
||||||
<template v-if="props.isOpen"> |
class="w-10 min-w-10 transition-all duration-200 p-1 transform" |
||||||
Nocodb |
href="https://github.com/nocodb/nocodb" |
||||||
<div class="flex flex-grow"></div> |
target="_blank" |
||||||
<MdiCodeTags class="min-w-[17px] text-md transform rotate-90" /> |
> |
||||||
</template> |
<img width="25" alt="NocoDB" src="~/assets/img/icons/256x256.png" /> |
||||||
|
</a> |
||||||
<template v-else> |
|
||||||
<MdiFolder class="text-primary cursor-pointer transform hover:scale-105 text-2xl" /> |
<div class="font-semibold text-base">Nocodb</div> |
||||||
</template> |
<div class="flex flex-grow"></div> |
||||||
</div> |
</div> |
||||||
|
</div> |
||||||
<template #overlay> |
|
||||||
<a-menu class="" @click="isWorkspaceDropdownOpen = false"> |
|
||||||
<a-menu-item-group class="!border-t-0"> |
|
||||||
<a-menu-divider /> |
|
||||||
|
|
||||||
<template v-if="!isSharedBase"> |
|
||||||
<!-- Copy Auth Token --> |
|
||||||
<a-menu-item key="copy"> |
|
||||||
<div |
|
||||||
v-e="['a:navbar:user:copy-auth-token']" |
|
||||||
class="nc-workspace-menu-item group !gap-x-2" |
|
||||||
@click.stop="copyAuthToken" |
|
||||||
> |
|
||||||
<GeneralIcon v-if="isAuthTokenCopied" icon="check" class="group-hover:text-black" /> |
|
||||||
<GeneralIcon v-else icon="copy" class="group-hover:text-black" /> |
|
||||||
<div v-if="isAuthTokenCopied"> |
|
||||||
{{ $t('activity.account.authTokenCopied') }} |
|
||||||
</div> |
|
||||||
<div v-else> |
|
||||||
{{ $t('activity.account.authToken') }} |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</a-menu-item> |
|
||||||
|
|
||||||
<a-menu-divider v-if="false" /> |
|
||||||
|
|
||||||
<!-- Theme --> |
|
||||||
<template v-if="isUIAllowed('projectTheme') && false"> |
|
||||||
<a-sub-menu key="theme"> |
|
||||||
<template #title> |
|
||||||
<div class="nc-workspace-menu-item group"> |
|
||||||
<GeneralIcon icon="image" class="group-hover:text-accent" /> |
|
||||||
{{ $t('activity.account.themes') }} |
|
||||||
|
|
||||||
<div class="flex-1" /> |
|
||||||
|
|
||||||
<MaterialSymbolsChevronRightRounded |
|
||||||
class="transform group-hover:(scale-115 text-accent) text-xl text-gray-400" |
|
||||||
/> |
|
||||||
</div> |
|
||||||
</template> |
|
||||||
|
|
||||||
<template #expandIcon></template> |
|
||||||
|
|
||||||
<LazyGeneralColorPicker |
|
||||||
:model-value="theme.primaryColor" |
|
||||||
:colors="projectThemeColors" |
|
||||||
:row-size="9" |
|
||||||
:advanced="false" |
|
||||||
class="rounded-t" |
|
||||||
@input="handleThemeColor('swatch', $event)" |
|
||||||
/> |
|
||||||
|
|
||||||
<!-- Custom Theme --> |
|
||||||
<a-sub-menu key="theme-2"> |
|
||||||
<template #title> |
|
||||||
<div class="nc-workspace-menu-item group"> |
|
||||||
{{ $t('labels.customTheme') }} |
|
||||||
|
|
||||||
<div class="flex-1" /> |
|
||||||
|
|
||||||
<MaterialSymbolsChevronRightRounded |
|
||||||
class="transform group-hover:(scale-115 text-accent) text-xl text-gray-400" |
|
||||||
/> |
|
||||||
</div> |
|
||||||
</template> |
|
||||||
|
|
||||||
<!-- Primary Color --> |
|
||||||
<template #expandIcon></template> |
|
||||||
|
|
||||||
<a-sub-menu key="pick-primary"> |
|
||||||
<template #title> |
|
||||||
<div class="nc-workspace-menu-item group"> |
|
||||||
<ClarityColorPickerSolid class="group-hover:text-black" /> |
|
||||||
{{ $t('labels.primaryColor') }} |
|
||||||
</div> |
|
||||||
</template> |
|
||||||
|
|
||||||
<template #expandIcon></template> |
|
||||||
|
|
||||||
<LazyGeneralChromeWrapper @input="handleThemeColor('primary', $event)" /> |
|
||||||
</a-sub-menu> |
|
||||||
|
|
||||||
<!-- Accent Color --> |
|
||||||
<a-sub-menu key="pick-accent"> |
|
||||||
<template #title> |
|
||||||
<div class="nc-workspace-menu-item group"> |
|
||||||
<ClarityColorPickerSolid class="group-hover:text-black" /> |
|
||||||
{{ $t('labels.accentColor') }} |
|
||||||
</div> |
|
||||||
</template> |
|
||||||
|
|
||||||
<template #expandIcon></template> |
|
||||||
|
|
||||||
<LazyGeneralChromeWrapper @input="handleThemeColor('accent', $event)" /> |
|
||||||
</a-sub-menu> |
|
||||||
</a-sub-menu> |
|
||||||
</a-sub-menu> |
|
||||||
</template> |
|
||||||
|
|
||||||
<a-menu-divider v-if="false" /> |
|
||||||
|
|
||||||
<!-- Preview As --> |
|
||||||
<a-sub-menu v-if="isUIAllowed('previewAs') && false" key="preview-as"> |
|
||||||
<template #title> |
|
||||||
<div v-e="['c:navdraw:preview-as']" class="nc-workspace-menu-item group"> |
|
||||||
<GeneralIcon icon="preview" class="group-hover:text-black" /> |
|
||||||
{{ $t('activity.previewAs') }} |
|
||||||
|
|
||||||
<div class="flex-1" /> |
|
||||||
|
|
||||||
<MaterialSymbolsChevronRightRounded |
|
||||||
class="transform group-hover:(scale-115 text-accent) text-xl text-gray-400" |
|
||||||
/> |
|
||||||
</div> |
|
||||||
</template> |
|
||||||
|
|
||||||
<template #expandIcon></template> |
|
||||||
|
|
||||||
<LazyGeneralPreviewAs /> |
|
||||||
</a-sub-menu> |
|
||||||
</template> |
|
||||||
<!-- Language --> |
|
||||||
<a-sub-menu |
|
||||||
v-if="!isEeUI" |
|
||||||
key="language" |
|
||||||
class="lang-menu !py-0" |
|
||||||
popup-class-name="scrollbar-thin-dull min-w-50 max-h-90vh !overflow-auto" |
|
||||||
> |
|
||||||
<template #title> |
|
||||||
<div class="nc-workspace-menu-item group"> |
|
||||||
<GeneralIcon icon="translate" class="group-hover:text-black nc-language mr-0.1" /> |
|
||||||
{{ $t('labels.language') }} |
|
||||||
<div class="flex items-center text-gray-400 text-xs">(Community Translated)</div> |
|
||||||
<div class="flex-1" /> |
|
||||||
|
|
||||||
<MaterialSymbolsChevronRightRounded |
|
||||||
class="transform group-hover:(scale-115 text-accent) text-xl text-gray-400" |
|
||||||
/> |
|
||||||
</div> |
|
||||||
</template> |
|
||||||
|
|
||||||
<template #expandIcon></template> |
|
||||||
|
|
||||||
<LazyGeneralLanguageMenu /> |
|
||||||
</a-sub-menu> |
|
||||||
|
|
||||||
<!-- Account --> |
|
||||||
<template v-if="signedIn && !isSharedBase"> |
|
||||||
<a-sub-menu key="account"> |
|
||||||
<template #title> |
|
||||||
<div class="nc-workspace-menu-item group"> |
|
||||||
<GeneralIcon icon="account" class="group-hover:text-accent" /> |
|
||||||
{{ $t('labels.account') }} |
|
||||||
<div class="flex-1" /> |
|
||||||
|
|
||||||
<MaterialSymbolsChevronRightRounded |
|
||||||
class="transform group-hover:(scale-115 text-accent) text-xl text-gray-400" |
|
||||||
/> |
|
||||||
</div> |
|
||||||
</template> |
|
||||||
|
|
||||||
<template #expandIcon></template> |
|
||||||
|
|
||||||
<a-menu-item key="0" class="!rounded-t"> |
|
||||||
<nuxt-link v-e="['c:navbar:user:email']" class="nc-workspace-menu-item group !no-underline" to="/account/users"> |
|
||||||
<GeneralIcon icon="at" class="mt-1 group-hover:text-accent" /> |
|
||||||
<div class="prose-sm group-hover:text-primary"> |
|
||||||
<div>Account</div> |
|
||||||
<div class="text-xs text-gray-500">{{ email }}</div> |
|
||||||
</div> |
|
||||||
</nuxt-link> |
|
||||||
</a-menu-item> |
|
||||||
|
|
||||||
<a-menu-item key="1" class="!rounded-b"> |
|
||||||
<div v-e="['a:navbar:user:sign-out']" class="nc-workspace-menu-item group" @click="logout"> |
|
||||||
<GeneralIcon icon="signout" class="group-hover:(!text-accent)" /> |
|
||||||
|
|
||||||
<span class="prose-sm nc-user-menu-signout"> |
|
||||||
{{ $t('general.signOut') }} |
|
||||||
</span> |
|
||||||
</div> |
|
||||||
</a-menu-item> |
|
||||||
</a-sub-menu> |
|
||||||
</template> |
|
||||||
</a-menu-item-group> |
|
||||||
</a-menu> |
|
||||||
</template> |
|
||||||
</a-dropdown> |
|
||||||
<GeneralModal v-model:visible="workspaceModalVisible" :class="{ active: modalVisible }" width="80%" :footer="null"> |
|
||||||
<div class="relative flex flex-col px-6 py-2"> |
|
||||||
<div class="absolute right-4 top-4 z-20"> |
|
||||||
<a-button type="text" class="!p-1 !h-7 !rounded" @click="workspaceModalVisible = false"> |
|
||||||
<component :is="iconMap.close" /> |
|
||||||
</a-button> |
|
||||||
</div> |
|
||||||
<a-tabs v-model:activeKey="tab"> |
|
||||||
<template v-if="isWorkspaceOwner"> |
|
||||||
<a-tab-pane key="collab" tab="Collaborators" class="w-full"> |
|
||||||
<WorkspaceCollaboratorsList class="h-full" /> |
|
||||||
</a-tab-pane> |
|
||||||
<!-- <a-tab-pane key="settings" tab="Settings" class="w-full"> |
|
||||||
<div class="min-h-50 flex items-center justify-center">Not available</div> |
|
||||||
</a-tab-pane> --> |
|
||||||
</template> |
|
||||||
</a-tabs> |
|
||||||
</div> |
|
||||||
</GeneralModal> |
|
||||||
</div> |
</div> |
||||||
</template> |
</template> |
||||||
|
|
||||||
<style scoped lang="scss"> |
<style scoped lang="scss"> |
||||||
.nc-workspace-title-input { |
|
||||||
@apply flex-grow py-2 px-3 outline-none hover:(bg-gray-50) focus:(bg-gray-50) font-medium rounded text-md text-defaault; |
|
||||||
} |
|
||||||
|
|
||||||
.nc-menu-sub-head { |
|
||||||
@apply pt-2 pb-2 text-gray-500 text-sm px-5; |
|
||||||
} |
|
||||||
|
|
||||||
.nc-workspace-menu-item { |
.nc-workspace-menu-item { |
||||||
@apply flex items-center pl-2 py-2 gap-2 text-sm hover:text-black; |
@apply flex items-center pl-2 py-2 gap-2 text-sm hover:text-black; |
||||||
} |
} |
||||||
|
|
||||||
:deep(.ant-dropdown-menu-item-group-title) { |
|
||||||
@apply hidden; |
|
||||||
} |
|
||||||
|
|
||||||
:deep(.ant-tabs-nav) { |
|
||||||
@apply !mb-0; |
|
||||||
} |
|
||||||
|
|
||||||
:deep(.ant-dropdown-menu-submenu-title) { |
|
||||||
@apply !py-0; |
|
||||||
.nc-icon { |
|
||||||
@apply !text-xs; |
|
||||||
} |
|
||||||
} |
|
||||||
</style> |
</style> |
||||||
|
@ -0,0 +1,164 @@ |
|||||||
|
<script lang="ts" setup> |
||||||
|
const { signOut } = useGlobal() |
||||||
|
|
||||||
|
const { deleteWorkspace, navigateToWorkspace, updateWorkspace } = useWorkspace() |
||||||
|
const { workspacesList, activeWorkspaceId, activeWorkspace, workspaces } = storeToRefs(useWorkspace()) |
||||||
|
|
||||||
|
const formValidator = ref() |
||||||
|
const isConfirmed = ref(false) |
||||||
|
const isDeleting = ref(false) |
||||||
|
const isErrored = ref(false) |
||||||
|
const isTitleUpdating = ref(false) |
||||||
|
const isCancelButtonVisible = ref(false) |
||||||
|
|
||||||
|
const form = ref({ |
||||||
|
title: '', |
||||||
|
}) |
||||||
|
|
||||||
|
const formRules = { |
||||||
|
title: [ |
||||||
|
{ required: true, message: 'Workspace name required' }, |
||||||
|
{ min: 3, message: 'Workspace name must be at least 3 characters long' }, |
||||||
|
{ max: 50, message: 'Workspace name must be at most 50 characters long' }, |
||||||
|
], |
||||||
|
} |
||||||
|
|
||||||
|
const onDelete = async () => { |
||||||
|
isDeleting.value = true |
||||||
|
try { |
||||||
|
await deleteWorkspace(activeWorkspaceId.value, { skipStateUpdate: true }) |
||||||
|
|
||||||
|
isConfirmed.value = false |
||||||
|
isDeleting.value = false |
||||||
|
|
||||||
|
// We only remove the delete workspace from the list after the api call is successful |
||||||
|
workspaces.value.delete(activeWorkspaceId.value) |
||||||
|
|
||||||
|
if (workspacesList.value.length > 1) { |
||||||
|
await navigateToWorkspace(workspacesList.value[0].id) |
||||||
|
} else { |
||||||
|
// As signin page will clear the workspaces, we need to check if there are more than one workspace |
||||||
|
await signOut(false) |
||||||
|
setTimeout(() => { |
||||||
|
window.location.href = '/' |
||||||
|
}, 100) |
||||||
|
} |
||||||
|
} finally { |
||||||
|
isDeleting.value = false |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const titleChange = async () => { |
||||||
|
const valid = await formValidator.value.validate() |
||||||
|
|
||||||
|
if (!valid) return |
||||||
|
|
||||||
|
if (isTitleUpdating.value) return |
||||||
|
|
||||||
|
isTitleUpdating.value = true |
||||||
|
isErrored.value = false |
||||||
|
|
||||||
|
try { |
||||||
|
await updateWorkspace(activeWorkspaceId.value, { |
||||||
|
title: form.value.title, |
||||||
|
}) |
||||||
|
} catch (e: any) { |
||||||
|
console.error(e) |
||||||
|
} finally { |
||||||
|
isTitleUpdating.value = false |
||||||
|
isCancelButtonVisible.value = false |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
watch( |
||||||
|
() => activeWorkspace.value.title, |
||||||
|
() => { |
||||||
|
form.value.title = activeWorkspace.value.title |
||||||
|
}, |
||||||
|
{ |
||||||
|
immediate: true, |
||||||
|
}, |
||||||
|
) |
||||||
|
|
||||||
|
watch( |
||||||
|
() => form.value.title, |
||||||
|
async () => { |
||||||
|
try { |
||||||
|
if (form.value.title !== activeWorkspace.value?.title) { |
||||||
|
isCancelButtonVisible.value = true |
||||||
|
} else { |
||||||
|
isCancelButtonVisible.value = false |
||||||
|
} |
||||||
|
isErrored.value = !(await formValidator.value.validate()) |
||||||
|
} catch (e: any) { |
||||||
|
isErrored.value = true |
||||||
|
} |
||||||
|
}, |
||||||
|
) |
||||||
|
|
||||||
|
const onCancel = () => { |
||||||
|
form.value.title = activeWorkspace.value?.title |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<template> |
||||||
|
<div class="flex flex-col items-center nc-workspace-settings-settings"> |
||||||
|
<div class="item flex flex-col w-full"> |
||||||
|
<div class="font-medium text-base">Change Workspace Name</div> |
||||||
|
<a-form ref="formValidator" layout="vertical" no-style :model="form" class="w-full" @finish="titleChange"> |
||||||
|
<div class="text-gray-500 mt-6 mb-1.5">Workspace name</div> |
||||||
|
<a-form-item name="title" :rules="formRules.title"> |
||||||
|
<a-input |
||||||
|
v-model:value="form.title" |
||||||
|
class="w-full !rounded-md !py-1.5" |
||||||
|
placeholder="Workspace name" |
||||||
|
data-testid="nc-workspace-settings-settings-rename-input" |
||||||
|
/> |
||||||
|
</a-form-item> |
||||||
|
<div class="flex flex-row w-full justify-end mt-8 gap-4"> |
||||||
|
<NcButton |
||||||
|
v-if="isCancelButtonVisible" |
||||||
|
type="secondary" |
||||||
|
html-type="submit" |
||||||
|
data-testid="nc-workspace-settings-settings-rename-cancel" |
||||||
|
@click="onCancel" |
||||||
|
> |
||||||
|
<template #loading> Renaming Workspace </template> |
||||||
|
Cancel |
||||||
|
</NcButton> |
||||||
|
<NcButton |
||||||
|
type="primary" |
||||||
|
html-type="submit" |
||||||
|
:disabled="isErrored || (form.title && form.title === activeWorkspace.title)" |
||||||
|
:loading="isDeleting" |
||||||
|
data-testid="nc-workspace-settings-settings-rename-submit" |
||||||
|
> |
||||||
|
<template #loading> Renaming Workspace </template> |
||||||
|
Rename Workspace |
||||||
|
</NcButton> |
||||||
|
</div> |
||||||
|
</a-form> |
||||||
|
</div> |
||||||
|
<div class="item flex flex-col"> |
||||||
|
<div class="font-medium text-base">Delete Workspace</div> |
||||||
|
<div class="text-gray-500 mt-2">Delete this workspace and all it’s contents.</div> |
||||||
|
<div class="flex flex-row mt-8 gap-x-2"> |
||||||
|
<a-checkbox v-model:checked="isConfirmed" /> |
||||||
|
<div class="flex">I understand that this action is irreversible</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="flex flex-row w-full justify-end mt-8"> |
||||||
|
<NcButton type="danger" :disabled="!isConfirmed" :loading="isDeleting" @click="onDelete"> |
||||||
|
<template #loading> Deleting Workspace </template> |
||||||
|
Delete Workspace |
||||||
|
</NcButton> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.item { |
||||||
|
@apply p-6 rounded-2xl border-1 max-w-180 mt-10 min-w-100 w-full; |
||||||
|
} |
||||||
|
</style> |
@ -1,3 +1,11 @@ |
|||||||
|
<script lang="ts"> |
||||||
|
export default { |
||||||
|
name: 'EmptyLayout', |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
<template> |
<template> |
||||||
<slot></slot> |
<NuxtLayout class="h-screen"> |
||||||
|
<slot></slot> |
||||||
|
</NuxtLayout> |
||||||
</template> |
</template> |
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue