mirror of https://github.com/nocodb/nocodb
126 changed files with 2108 additions and 1674 deletions
@ -1,11 +1,17 @@
|
||||
<script setup lang="ts"> |
||||
import { computed, provideTheme, useRoute } from '#imports' |
||||
|
||||
const route = useRoute() |
||||
|
||||
const disableBaseLayout = $computed(() => route.path.startsWith('/nc/view') || route.path.startsWith('/nc/form')) |
||||
const disableBaseLayout = computed(() => route.path.startsWith('/nc/view') || route.path.startsWith('/nc/form')) |
||||
|
||||
provideTheme() |
||||
</script> |
||||
|
||||
<template> |
||||
<NuxtLayout :name="disableBaseLayout ? false : 'base'"> |
||||
<NuxtPage /> |
||||
</NuxtLayout> |
||||
<a-config-provider> |
||||
<NuxtLayout :name="disableBaseLayout ? false : 'base'"> |
||||
<NuxtPage /> |
||||
</NuxtLayout> |
||||
</a-config-provider> |
||||
</template> |
||||
|
@ -1,4 +0,0 @@
|
||||
:root { |
||||
--primary: #00b786; |
||||
--secondary: #8ceaf6; |
||||
} |
@ -1,19 +0,0 @@
|
||||
::-webkit-scrollbar { |
||||
width: .7em; |
||||
height: .7em |
||||
} |
||||
|
||||
::-webkit-scrollbar-button { |
||||
background: #77777722 |
||||
} |
||||
|
||||
::-webkit-scrollbar-track-piece { |
||||
background: #66666622 |
||||
} |
||||
|
||||
::-webkit-scrollbar-thumb { |
||||
background: #888; |
||||
border-radius: .7em; |
||||
border: .15em solid #00000000; |
||||
background-clip: padding-box; |
||||
} |
@ -0,0 +1,83 @@
|
||||
<script lang="ts" setup> |
||||
import { useGlobal, useProject } from '#imports' |
||||
|
||||
const showDrawer = ref(false) |
||||
|
||||
const { appInfo } = useGlobal() |
||||
|
||||
const { project } = useProject() |
||||
|
||||
const route = useRoute() |
||||
|
||||
const openSwaggerLink = () => { |
||||
openLink(`/api/v1/db/meta/projects/${route.params.projectId}/swagger`, appInfo.value.ncSiteUrl) |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<div |
||||
class="flex items-center space-x-1 w-full cursor-pointer pl-3 py-1.5 hover:(text-primary bg-primary bg-opacity-5)" |
||||
@click="showDrawer = true" |
||||
> |
||||
<MdiCommentTextOutline class="mr-1 nc-share-base" /> |
||||
<!-- todo: i18n --> |
||||
<div>APIs & Support</div> |
||||
</div> |
||||
|
||||
<a-drawer |
||||
v-model:visible="showDrawer" |
||||
class="h-full relative" |
||||
placement="right" |
||||
size="small" |
||||
:closable="false" |
||||
:body-style="{ padding: '12px 24px 0 24px', background: '#fafafa' }" |
||||
> |
||||
<div class="flex flex-col w-full h-full p-4 pb-0"> |
||||
<!-- todo: i18n --> |
||||
<a-typography-title :level="4" class="!mb-6 !text-gray-500">Help center</a-typography-title> |
||||
|
||||
<GeneralSocialCard show-swagger-link class="!w-full nc-social-card"> |
||||
<template #before> |
||||
<a-list-item v-if="project"> |
||||
<nuxt-link |
||||
v-t="['e:docs']" |
||||
class="text-primary !no-underline !text-current py-4 font-weight-medium" |
||||
target="_blank" |
||||
@click="openSwaggerLink" |
||||
> |
||||
<div class="ml-3 flex items-center text-sm"> |
||||
<LogosSwagger /> |
||||
<!-- todo: i18n --> |
||||
<span class="ml-3">{{ project.title }} : Swagger Documentation</span> |
||||
</div> |
||||
</nuxt-link> |
||||
</a-list-item> |
||||
</template> |
||||
</GeneralSocialCard> |
||||
|
||||
<div class="flex-1 my-2"></div> |
||||
|
||||
<GeneralSponsors class="!w-full" /> |
||||
|
||||
<div class="min-h-10 w-full" /> |
||||
</div> |
||||
</a-drawer> |
||||
</template> |
||||
|
||||
<style scoped lang="scss"> |
||||
/* Social card style */ |
||||
.nc-social-card { |
||||
@apply !shadow-none !border-0 bg-transparent; |
||||
|
||||
:deep(.ant-spin-container) { |
||||
@apply !gap-3; |
||||
|
||||
.ant-list-item { |
||||
@apply mb-2 border-1 bg-white border-gray-200; |
||||
&:last-child { |
||||
@apply !border-solid; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,23 @@
|
||||
<script setup lang="ts"> |
||||
import { OpenNewRecordFormHookInj, inject } from '#imports' |
||||
|
||||
const isLocked = inject(IsLockedInj) |
||||
|
||||
const openNewRecordFormHook = inject(OpenNewRecordFormHookInj)! |
||||
|
||||
const onClick = () => { |
||||
if (!isLocked?.value) openNewRecordFormHook.trigger() |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<a-tooltip placement="bottom"> |
||||
<template #title> {{ $t('activity.addRow') }} </template> |
||||
<div :class="{ 'group': !isLocked, 'disabled-ring': isLocked }" class="nc-add-row flex align-center"> |
||||
<MdiPlusOutline |
||||
:class="{ 'cursor-pointer text-gray-500 group-hover:(text-primary)': !isLocked, 'disabled': isLocked }" |
||||
@click="onClick" |
||||
/> |
||||
</div> |
||||
</a-tooltip> |
||||
</template> |
@ -0,0 +1,63 @@
|
||||
<script setup lang="ts"> |
||||
import MdiLockOutlineIcon from '~icons/mdi/lock-outline' |
||||
import MdiAccountIcon from '~icons/mdi/account' |
||||
import MdiAccountGroupIcon from '~icons/mdi/account-group' |
||||
|
||||
import { LockType } from '~/lib' |
||||
|
||||
const { type, hideTick } = defineProps<{ hideTick?: boolean; type: LockType }>() |
||||
|
||||
const emit = defineEmits(['select']) |
||||
|
||||
const types = { |
||||
[LockType.Personal]: { |
||||
title: 'title.personalView', |
||||
icon: MdiAccountIcon, |
||||
subtitle: 'Only you can edit the view configuration. Other collaborators’ personal views are hidden by default.', |
||||
}, |
||||
[LockType.Collaborative]: { |
||||
title: 'title.collabView', |
||||
icon: MdiAccountGroupIcon, |
||||
subtitle: 'Collaborators with edit permissions or higher can change the view configuration.', |
||||
}, |
||||
[LockType.Locked]: { |
||||
title: 'title.lockedView', |
||||
icon: MdiLockOutlineIcon, |
||||
subtitle: 'No one can edit the view configuration until it is unlocked.', |
||||
}, |
||||
} |
||||
|
||||
const selectedView = inject(ActiveViewInj) |
||||
</script> |
||||
|
||||
<template> |
||||
<div class="nc-locked-menu-item" @click="emit('select', type)"> |
||||
<div :class="{ 'show-tick': !hideTick }"> |
||||
<template v-if="!hideTick"> |
||||
<MdiCheck v-if="selectedView?.lock_type === type" /> |
||||
<span v-else /> |
||||
</template> |
||||
<div> |
||||
<component :is="types[type].icon" class="text-gray-500" /> |
||||
{{ $t(types[type].title) }} |
||||
<div class="nc-subtitle whitespace-normal"> |
||||
{{ types[type].subtitle }} |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
|
||||
<style scoped lang="scss"> |
||||
.nc-locked-menu-item > div { |
||||
@apply p-2 items-center min-w-[350px] max-w-[350px]; |
||||
|
||||
&.show-tick { |
||||
@apply grid gap-2 grid-cols-[30px,auto]; |
||||
} |
||||
|
||||
.nc-subtitle { |
||||
@apply text-xs text-gray-500 font-weight-light; |
||||
} |
||||
} |
||||
</style> |
@ -0,0 +1,280 @@
|
||||
<script lang="ts" setup> |
||||
import * as XLSX from 'xlsx' |
||||
import { ExportTypes } from 'nocodb-sdk' |
||||
import FileSaver from 'file-saver' |
||||
import { message } from 'ant-design-vue' |
||||
import { LockType } from '~/lib' |
||||
import { viewIcons } from '~/utils' |
||||
import { |
||||
ActiveViewInj, |
||||
FieldsInj, |
||||
IsLockedInj, |
||||
IsPublicInj, |
||||
MetaInj, |
||||
extractSdkResponseErrorMsg, |
||||
inject, |
||||
ref, |
||||
useNuxtApp, |
||||
useProject, |
||||
useUIPermission, |
||||
} from '#imports' |
||||
import MdiLockOutlineIcon from '~icons/mdi/lock-outline' |
||||
import MdiAccountIcon from '~icons/mdi/account' |
||||
import MdiAccountGroupIcon from '~icons/mdi/account-group' |
||||
|
||||
const sharedViewListDlg = ref(false) |
||||
|
||||
const isPublicView = inject(IsPublicInj, ref(false)) |
||||
|
||||
const isView = false |
||||
|
||||
const { project } = useProject() |
||||
|
||||
const { $api, $e } = useNuxtApp() |
||||
|
||||
const meta = inject(MetaInj) |
||||
|
||||
const fields = inject(FieldsInj, ref([])) |
||||
|
||||
const selectedView = inject(ActiveViewInj) |
||||
|
||||
const isLocked = inject(IsLockedInj) |
||||
|
||||
const showWebhookDrawer = ref(false) |
||||
|
||||
const quickImportDialog = ref(false) |
||||
|
||||
const { isUIAllowed } = useUIPermission() |
||||
|
||||
const exportFile = async (exportType: ExportTypes) => { |
||||
let offset = 0 |
||||
let c = 1 |
||||
const responseType = exportType === ExportTypes.EXCEL ? 'base64' : 'blob' |
||||
|
||||
try { |
||||
while (!isNaN(offset) && offset > -1) { |
||||
let res |
||||
if (isPublicView.value) { |
||||
const { exportFile: sharedViewExportFile } = useSharedView() |
||||
res = await sharedViewExportFile(fields.value, offset, exportType, responseType) |
||||
} else { |
||||
res = await $api.dbViewRow.export( |
||||
'noco', |
||||
project?.value.title as string, |
||||
meta?.value.title as string, |
||||
selectedView?.value.title as string, |
||||
exportType, |
||||
{ |
||||
responseType, |
||||
query: { |
||||
offset, |
||||
}, |
||||
} as any, |
||||
) |
||||
} |
||||
const { data, headers } = res |
||||
if (exportType === ExportTypes.EXCEL) { |
||||
const workbook = XLSX.read(data, { type: 'base64' }) |
||||
XLSX.writeFile(workbook, `${meta?.value.title}_exported_${c++}.xlsx`) |
||||
} else if (exportType === ExportTypes.CSV) { |
||||
const blob = new Blob([data], { type: 'text/plain;charset=utf-8' }) |
||||
FileSaver.saveAs(blob, `${meta?.value.title}_exported_${c++}.csv`) |
||||
} |
||||
offset = +headers['nc-export-offset'] |
||||
if (offset > -1) { |
||||
message.info('Downloading more files') |
||||
} else { |
||||
message.success('Successfully exported all table data') |
||||
} |
||||
} |
||||
} catch (e: any) { |
||||
message.error(await extractSdkResponseErrorMsg(e)) |
||||
} |
||||
} |
||||
|
||||
const Icon = computed(() => { |
||||
switch ((selectedView?.value as any)?.lock_type) { |
||||
case LockType.Personal: |
||||
return MdiAccountIcon |
||||
case LockType.Locked: |
||||
return MdiLockOutlineIcon |
||||
case LockType.Collaborative: |
||||
default: |
||||
return MdiAccountGroupIcon |
||||
} |
||||
}) |
||||
|
||||
async function changeLockType(type: LockType) { |
||||
$e('a:grid:lockmenu', { lockType: type }) |
||||
|
||||
if (!selectedView?.value) return |
||||
|
||||
if (type === 'personal') { |
||||
return message.info('Coming soon') |
||||
} |
||||
try { |
||||
;(selectedView.value as any).lock_type = type |
||||
$api.dbView.update(selectedView.value.id as string, { |
||||
lock_type: type, |
||||
}) |
||||
|
||||
message.success(`Successfully Switched to ${type} view`) |
||||
} catch (e: any) { |
||||
message.error(await extractSdkResponseErrorMsg(e)) |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<div> |
||||
<a-dropdown :trigger="['click']"> |
||||
<a-button v-t="['c:actions']" class="nc-actions-menu-btn nc-toolbar-btn"> |
||||
<div class="flex gap-2 items-center"> |
||||
<component |
||||
:is="viewIcons[selectedView?.type].icon" |
||||
class="nc-view-icon group-hover:hidden" |
||||
:style="{ color: viewIcons[selectedView?.type].color }" |
||||
/> |
||||
<span class="!text-sm font-weight-normal">{{ selectedView?.title }}</span> |
||||
<component :is="Icon" class="text-gray-500" /> |
||||
<MdiMenuDown class="text-grey" /> |
||||
</div> |
||||
</a-button> |
||||
|
||||
<template #overlay> |
||||
<a-menu class="ml-6 !text-sm !px-0 !py-2 !rounded"> |
||||
<a-menu-item-group> |
||||
<a-sub-menu |
||||
v-if="isUIAllowed('view-type')" |
||||
key="lock-type" |
||||
class="scrollbar-thin-dull min-w-50 max-h-90vh overflow-auto !py-0" |
||||
> |
||||
<template #title> |
||||
<div v-t="['c:navdraw:preview-as']" class="nc-project-menu-item group px-0 !py-0"> |
||||
<SmartsheetToolbarLockType hide-tick :type="selectedView?.lock_type || LockType.Collaborative" /> |
||||
|
||||
<MaterialSymbolsChevronRightRounded |
||||
class="transform group-hover:(scale-115 text-accent) text-xl text-gray-400" |
||||
/> |
||||
</div> |
||||
</template> |
||||
|
||||
<template #expandIcon></template> |
||||
<a-menu-item @click="changeLockType(LockType.Collaborative)"> |
||||
<SmartsheetToolbarLockType :type="LockType.Collaborative" /> |
||||
</a-menu-item> |
||||
<a-menu-item @click="changeLockType(LockType.Locked)"> |
||||
<SmartsheetToolbarLockType :type="LockType.Locked" /> |
||||
</a-menu-item> |
||||
<a-menu-item @click="changeLockType(LockType.Personal)"> |
||||
<SmartsheetToolbarLockType :type="LockType.Personal" /> |
||||
</a-menu-item> |
||||
</a-sub-menu> |
||||
<a-menu-divider /> |
||||
<a-sub-menu key="download"> |
||||
<template #title> |
||||
<div v-t="['c:navdraw:preview-as']" class="nc-project-menu-item group"> |
||||
<MdiDownload class="group-hover:text-accent text-gray-500" /> |
||||
Download |
||||
<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> |
||||
<div v-t="['a:actions:download-csv']" class="nc-project-menu-item" @click="exportFile(ExportTypes.CSV)"> |
||||
<MdiDownloadOutline class="text-gray-500" /> |
||||
<!-- Download as CSV --> |
||||
{{ $t('activity.downloadCSV') }} |
||||
</div> |
||||
</a-menu-item> |
||||
<a-menu-item> |
||||
<div v-t="['a:actions:download-excel']" class="nc-project-menu-item" @click="exportFile(ExportTypes.EXCEL)"> |
||||
<MdiDownloadOutline class="text-gray-500" /> |
||||
<!-- Download as XLSX --> |
||||
{{ $t('activity.downloadExcel') }} |
||||
</div> |
||||
</a-menu-item> |
||||
</a-sub-menu> |
||||
<template v-if="isUIAllowed('csvImport') && !isView && !isPublicView"> |
||||
<a-sub-menu key="upload"> |
||||
<template #title> |
||||
<div v-t="['c:navdraw:preview-as']" class="nc-project-menu-item group"> |
||||
<MdiUpload class="group-hover:text-accent text-gray-500" /> |
||||
Upload |
||||
<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> |
||||
<div |
||||
v-if="isUIAllowed('csvImport') && !isView && !isPublicView" |
||||
v-t="['a:actions:upload-csv']" |
||||
class="nc-project-menu-item" |
||||
:class="{ disabled: isLocked }" |
||||
@click="!isLocked ? (quickImportDialog = true) : {}" |
||||
> |
||||
<MdiUploadOutline class="text-gray-500" /> |
||||
<!-- Upload CSV --> |
||||
{{ $t('activity.uploadCSV') }} |
||||
</div> |
||||
</a-menu-item> |
||||
</a-sub-menu> |
||||
</template> |
||||
<a-menu-divider /> |
||||
<a-menu-item> |
||||
<div |
||||
v-if="isUIAllowed('SharedViewList') && !isView && !isPublicView" |
||||
v-t="['a:actions:shared-view-list']" |
||||
class="py-2 flex gap-2 items-center" |
||||
@click="sharedViewListDlg = true" |
||||
> |
||||
<MdiViewListOutline class="text-gray-500" /> |
||||
<!-- Shared View List --> |
||||
{{ $t('activity.listSharedView') }} |
||||
</div> |
||||
</a-menu-item> |
||||
<a-menu-item> |
||||
<div |
||||
v-if="isUIAllowed('webhook') && !isView && !isPublicView" |
||||
v-t="['c:actions:webhook']" |
||||
class="py-2 flex gap-2 items-center" |
||||
@click="showWebhookDrawer = true" |
||||
> |
||||
<MdiHook class="text-gray-500" /> |
||||
{{ $t('objects.webhooks') }} |
||||
</div> |
||||
</a-menu-item> |
||||
</a-menu-item-group> |
||||
</a-menu> |
||||
</template> |
||||
</a-dropdown> |
||||
|
||||
<DlgQuickImport v-if="quickImportDialog" v-model="quickImportDialog" import-type="csv" :import-only="true" /> |
||||
|
||||
<WebhookDrawer v-if="showWebhookDrawer" v-model="showWebhookDrawer" /> |
||||
|
||||
<a-modal v-model:visible="sharedViewListDlg" title="Shared view list" width="max(900px,60vw)" :footer="null"> |
||||
<SmartsheetToolbarSharedViewList v-if="sharedViewListDlg" /> |
||||
</a-modal> |
||||
</div> |
||||
</template> |
||||
|
||||
<style scoped> |
||||
:deep(.ant-dropdown-menu-submenu-title) { |
||||
@apply py-0; |
||||
} |
||||
|
||||
:deep(.ant-dropdown-menu-item-group-title) { |
||||
@apply hidden; |
||||
} |
||||
</style> |
@ -1,24 +0,0 @@
|
||||
<script setup lang="ts"> |
||||
import { OpenNewRecordFormHookInj, inject } from '#imports' |
||||
|
||||
const { isOpen } = useSidebar({ storageKey: 'nc-right-sidebar' }) |
||||
const isLocked = inject(IsLockedInj) |
||||
|
||||
const openNewRecordFormHook = inject(OpenNewRecordFormHookInj)! |
||||
|
||||
const onClick = () => { |
||||
if (!isLocked?.value) openNewRecordFormHook.trigger() |
||||
} |
||||
</script> |
||||
|
||||
<template> |
||||
<a-tooltip :placement="isOpen ? 'bottomRight' : 'left'"> |
||||
<template #title> {{ $t('activity.addRow') }} </template> |
||||
<div |
||||
:class="{ 'hover:after:bg-primary/75 group': !isLocked, 'disabled-ring': isLocked }" |
||||
class="nc-sidebar-right-item nc-sidebar-add-row" |
||||
> |
||||
<MdiPlusOutline :class="{ 'cursor-pointer group-hover:(!text-white)': !isLocked, 'disabled': isLocked }" @click="onClick" /> |
||||
</div> |
||||
</a-tooltip> |
||||
</template> |
@ -1,116 +0,0 @@
|
||||
<script lang="ts" setup> |
||||
import { message } from 'ant-design-vue' |
||||
import { computed, useSmartsheetStoreOrThrow } from '#imports' |
||||
import { extractSdkResponseErrorMsg } from '~/utils' |
||||
import MdiLockOutlineIcon from '~icons/mdi/lock-outline' |
||||
import MdiAccountIcon from '~icons/mdi/account' |
||||
import MdiAccountGroupIcon from '~icons/mdi/account-group' |
||||
|
||||
enum LockType { |
||||
Personal = 'personal', |
||||
Locked = 'locked', |
||||
Collaborative = 'collaborative', |
||||
} |
||||
|
||||
const { view, $api } = useSmartsheetStoreOrThrow() |
||||
|
||||
const { $e } = useNuxtApp() |
||||
|
||||
async function changeLockType(type: LockType) { |
||||
$e('a:grid:lockmenu', { lockType: type }) |
||||
|
||||
if (type === 'personal') { |
||||
return message.info('Coming soon') |
||||
} |
||||
try { |
||||
;(view.value as any).lock_type = type |
||||
$api.dbView.update(view.value.id as string, { |
||||
lock_type: type, |
||||
}) |
||||
|
||||
message.success(`Successfully Switched to ${type} view`) |
||||
} catch (e: any) { |
||||
message.error(await extractSdkResponseErrorMsg(e)) |
||||
} |
||||
} |
||||
|
||||
const Icon = computed(() => { |
||||
switch ((view.value as any)?.lock_type) { |
||||
case LockType.Personal: |
||||
return MdiAccountIcon |
||||
case LockType.Locked: |
||||
return MdiLockOutlineIcon |
||||
case LockType.Collaborative: |
||||
default: |
||||
return MdiAccountGroupIcon |
||||
} |
||||
}) |
||||
</script> |
||||
|
||||
<template> |
||||
<a-dropdown max-width="350" :trigger="['click']"> |
||||
<div class="nc-sidebar-right-item hover:after:bg-indigo-500 group nc-sidebar-lock-menu"> |
||||
<Icon class="cursor-pointer group-hover:(!text-white)" /> |
||||
</div> |
||||
|
||||
<template #overlay> |
||||
<div class="min-w-[350px] max-w-[500px] shadow bg-white"> |
||||
<div> |
||||
<div class="nc-menu-item" @click="changeLockType(LockType.Collaborative)"> |
||||
<div> |
||||
<MdiCheck v-if="!view?.lock_type || view?.lock_type === LockType.Collaborative" /> |
||||
<span v-else /> |
||||
<div> |
||||
<MdiAccountGroupIcon /> |
||||
Collaborative view |
||||
<div class="nc-subtitle">Collaborators with edit permissions or higher can change the view configuration.</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div class="nc-menu-item" @click="changeLockType(LockType.Locked)"> |
||||
<div> |
||||
<MdiCheck v-if="view.lock_type === LockType.Locked" /> |
||||
<span v-else /> |
||||
<div> |
||||
<MdiLockOutlineIcon /> |
||||
Locked View |
||||
<div class="nc-subtitle">No one can edit the view configuration until it is unlocked.</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<div class="nc-menu-item" @click="changeLockType(LockType.Personal)"> |
||||
<div> |
||||
<MdiCheck v-if="view.lock_type === LockType.Personal" /> |
||||
<span v-else /> |
||||
<div> |
||||
<MdiAccountIcon /> |
||||
Personal view |
||||
<div class="nc-subtitle"> |
||||
Only you can edit the view configuration. Other collaborators’ personal views are hidden by default. |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
</a-dropdown> |
||||
</template> |
||||
|
||||
<style scoped> |
||||
.nc-menu-item > div { |
||||
@apply grid grid-cols-[30px,auto] gap-2 p-2 align-center; |
||||
} |
||||
|
||||
.nc-menu-item > div > svg { |
||||
align-self: center; |
||||
} |
||||
|
||||
.nc-menu-option > :first-child { |
||||
@apply align-self-center; |
||||
} |
||||
|
||||
.nc-subtitle { |
||||
@apply font-size-sm font-weight-light; |
||||
} |
||||
</style> |
@ -0,0 +1,60 @@
|
||||
import { ConfigProvider } from 'ant-design-vue' |
||||
import type { Theme as AntTheme } from 'ant-design-vue/es/config-provider' |
||||
import { useStorage } from '@vueuse/core' |
||||
import { NOCO, hexToRGB, themeV2Colors, useCssVar, useInjectionState } from '#imports' |
||||
|
||||
interface ThemeConfig extends AntTheme { |
||||
primaryColor: string |
||||
accentColor: string |
||||
} |
||||
|
||||
const [setup, use] = useInjectionState((config?: Partial<ThemeConfig>) => { |
||||
const primaryColor = useCssVar('--color-primary', typeof document !== 'undefined' ? document.documentElement : null) |
||||
const accentColor = useCssVar('--color-accent', typeof document !== 'undefined' ? document.documentElement : null) |
||||
|
||||
/** current theme config */ |
||||
const currentTheme = useStorage<ThemeConfig>( |
||||
`${NOCO}db-theme`, |
||||
{ |
||||
primaryColor: themeV2Colors['royal-blue'].DEFAULT, |
||||
accentColor: themeV2Colors.pink['500'], |
||||
}, |
||||
localStorage, |
||||
{ mergeDefaults: true }, |
||||
) |
||||
|
||||
/** set initial config */ |
||||
setTheme(config ?? currentTheme.value) |
||||
|
||||
/** set theme (persists in localstorage) */ |
||||
function setTheme(theme: Partial<ThemeConfig>) { |
||||
// convert hex colors to rgb values
|
||||
if (theme.primaryColor) primaryColor.value = hexToRGB(theme.primaryColor) |
||||
if (theme.accentColor) accentColor.value = hexToRGB(theme.accentColor) |
||||
|
||||
currentTheme.value = theme as ThemeConfig |
||||
|
||||
ConfigProvider.config({ |
||||
theme, |
||||
}) |
||||
} |
||||
|
||||
return { |
||||
theme: currentTheme, |
||||
setTheme, |
||||
} |
||||
}, 'theme') |
||||
|
||||
export const provideTheme = setup |
||||
|
||||
export function useTheme(config?: Partial<ThemeConfig>) { |
||||
const theme = use() |
||||
|
||||
if (!theme) { |
||||
return setup(config) |
||||
} else { |
||||
if (config) theme.setTheme(config) |
||||
} |
||||
|
||||
return theme |
||||
} |
@ -1,2 +1,3 @@
|
||||
export const NOCO = 'noco' |
||||
export const USER_PROJECT_ROLES = 'user_project_roles' |
||||
export const SYSTEM_COLUMNS = ['id', 'title', 'created_at', 'updated_at'] |
||||
|
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue