Browse Source

Merge pull request #3390 from nocodb/fix/project-theme

fix: project theme
pull/3395/head
navi 2 years ago committed by GitHub
parent
commit
6b32671cf6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 19
      packages/nc-gui-v2/composables/useInjectionState/index.ts
  2. 101
      packages/nc-gui-v2/composables/useProject.ts
  3. 2
      packages/nc-gui-v2/composables/useTheme/index.ts
  4. 10
      packages/nc-gui-v2/pages/[projectType]/[projectId]/index.vue
  5. 48
      packages/nc-gui-v2/pages/index/index/[projectId].vue
  6. 110
      packages/nc-gui-v2/pages/index/index/index.vue

19
packages/nc-gui-v2/composables/useInjectionState/index.ts

@ -1,4 +1,5 @@
import type { InjectionKey } from 'vue'
import { inject, provide, tryOnScopeDispose } from '#imports'
export function useInjectionState<Arguments extends any[], Return>(
composable: (...args: Arguments) => Return,
@ -6,15 +7,31 @@ export function useInjectionState<Arguments extends any[], Return>(
): readonly [useInjectionState: (...args: Arguments) => Return, useInjectedState: () => Return | undefined] {
const key: string | InjectionKey<Return> = Symbol(keyName)
let providableState: Return | undefined
const useProvidingState = (...args: Arguments) => {
const providedState = composable(...args)
provide(key, providedState)
providableState = providedState
return providedState
}
const useInjectedState = () => inject(key, undefined)
const useInjectedState = () => {
let injection = inject(key, undefined)
if (typeof injection === 'undefined') {
injection = providableState
}
return injection
}
tryOnScopeDispose(() => {
providableState = undefined
})
return [useProvidingState, useInjectedState]
}

101
packages/nc-gui-v2/composables/useProject.ts

@ -1,34 +1,25 @@
import type { MaybeRef } from '@vueuse/core'
import { SqlUiFactory } from 'nocodb-sdk'
import type { OracleUi, ProjectType, TableType } from 'nocodb-sdk'
import type { MaybeRef } from '@vueuse/core'
import { useNuxtApp, useRoute, useState } from '#app'
import { useNuxtApp, useRoute } from '#app'
import type { ProjectMetaInfo } from '~/lib'
import { USER_PROJECT_ROLES } from '~/lib'
import type { ThemeConfig } from '@/composables/useTheme'
import { useInjectionState } from '#imports'
export function useProject(projectId?: MaybeRef<string>) {
const projectRoles = useState<Record<string, boolean>>(USER_PROJECT_ROLES, () => ({}))
const [setup, use] = useInjectionState((_projectId?: MaybeRef<string>) => {
const { $api } = useNuxtApp()
let _projectId = $ref('')
const project = useState<ProjectType>('project')
const tables = useState<TableType[]>('tables', () => [] as TableType[])
const route = useRoute()
const { includeM2M } = useGlobal()
const { setTheme } = useTheme()
const projectMetaInfo = useState<ProjectMetaInfo | undefined>('projectMetaInfo')
const projectId = computed(() => (_projectId ? unref(_projectId) : (route.params.projectId as string)))
const project = ref<ProjectType>({})
const tables = ref<TableType[]>([])
const projectRoles = ref<Record<string, boolean>>({})
const projectMetaInfo = ref<ProjectMetaInfo | undefined>()
// todo: refactor path param name and variable name
const projectType = $computed(() => route.params.projectType as string)
const isLoaded = ref(false)
const projectBaseType = $computed(() => project.value?.bases?.[0]?.type || '')
const isMysql = computed(() => ['mysql', 'mysql2'].includes(projectBaseType))
const isMssql = computed(() => projectBaseType === 'mssql')
const isPg = computed(() => projectBaseType === 'pg')
const sqlUi = computed(
() => SqlUiFactory.create({ client: projectBaseType }) as Exclude<ReturnType<typeof SqlUiFactory['create']>, typeof OracleUi>,
)
const isSharedBase = computed(() => projectType === 'base')
const projectMeta = computed(() => {
try {
@ -38,6 +29,17 @@ export function useProject(projectId?: MaybeRef<string>) {
}
})
const projectBaseType = $computed(() => project.value?.bases?.[0]?.type || '')
const sqlUi = computed(
() => SqlUiFactory.create({ client: projectBaseType }) as Exclude<ReturnType<typeof SqlUiFactory['create']>, typeof OracleUi>,
)
const isMysql = computed(() => ['mysql', 'mysql2'].includes(projectBaseType))
const isMssql = computed(() => projectBaseType === 'mssql')
const isPg = computed(() => projectBaseType === 'pg')
const isSharedBase = computed(() => projectType === 'base')
async function loadProjectMetaInfo(force?: boolean) {
if (!projectMetaInfo.value || force) {
const data = await $api.project.metaGet(project.value.id!, {}, {})
@ -74,51 +76,54 @@ export function useProject(projectId?: MaybeRef<string>) {
}
async function loadProject() {
if (unref(projectId)) {
_projectId = unref(projectId)!
} else if (projectType === 'base') {
if (projectType === 'base') {
const baseData = await $api.public.sharedBaseGet(route.params.projectId as string)
_projectId = baseData.project_id!
project.value = await $api.project.read(baseData.project_id!)
} else {
_projectId = route.params.projectId as string
project.value = await $api.project.read(projectId.value)
}
isLoaded.value = true
project.value = await $api.project.read(_projectId!)
await loadProjectRoles()
await loadTables()
setTheme(projectMeta.value?.theme)
}
async function updateProject(data: Partial<ProjectType>) {
if (unref(projectId)) {
_projectId = unref(projectId)!
} else if (projectType === 'base') {
if (projectType === 'base') {
return
}
if (data.meta && typeof data.meta === 'string') {
await $api.project.update(projectId.value, data)
} else {
_projectId = route.params.projectId as string
await $api.project.update(projectId.value, { ...data, meta: JSON.stringify(data.meta) })
}
await $api.project.update(_projectId, data)
}
async function saveTheme(theme: Partial<ThemeConfig>) {
await updateProject({
meta: JSON.stringify({
color: theme.primaryColor,
meta: {
...projectMeta.value,
theme,
}),
},
})
setTheme(theme)
}
watch(
() => route.params,
(v) => {
if (!v?.projectId) {
setTheme()
}
},
)
// TODO useProject should only called inside a project for now this doesn't work
onScopeDispose(() => {
if (isLoaded.value === true) {
project.value = {}
tables.value = []
projectMetaInfo.value = undefined
projectRoles.value = {}
setTheme({})
}
project.value = {}
tables.value = []
projectMetaInfo.value = undefined
projectRoles.value = {}
})
return {
@ -138,4 +143,16 @@ export function useProject(projectId?: MaybeRef<string>) {
projectMeta,
saveTheme,
}
}, 'useProject')
export const provideProject = setup
export function useProject(projectId?: MaybeRef<string>) {
const state = use()
if (!state) {
return setup(projectId)
}
return state
}

2
packages/nc-gui-v2/composables/useTheme/index.ts

@ -22,7 +22,7 @@ const [setup, use] = useInjectionState((config?: Partial<ThemeConfig>) => {
setTheme(config ?? currentTheme.value)
/** set theme (persists in localstorage) */
function setTheme(theme: Partial<ThemeConfig>) {
function setTheme(theme?: Partial<ThemeConfig>) {
const themePrimary = theme?.primaryColor ? tinycolor(theme.primaryColor) : tinycolor(themeV2Colors['royal-blue'].DEFAULT)
const themeAccent = theme?.accentColor ? tinycolor(theme.accentColor) : tinycolor(themeV2Colors.pink['500'])

10
packages/nc-gui-v2/pages/[projectType]/[projectId]/index.vue

@ -87,21 +87,21 @@ const themePrimaryColor = ref<any>(theme.value.primaryColor)
const themeAccentColor = ref<any>(theme.value.accentColor)
// Chrome provides object so if custom picker used we only edit primary otherwise use analogous as accent
// Chrome provides object so if custom picker used we only edit primary otherwise use complement as accent
watch(themePrimaryColor, (nextColor) => {
const hexColor = nextColor.hex ? nextColor.hex : nextColor
const hexColor = nextColor.hex8 ? nextColor.hex8 : nextColor
const tcolor = tinycolor(hexColor)
if (tcolor) {
const analogous = tcolor.complement()
const complement = tcolor.complement()
saveTheme({
primaryColor: hexColor,
accentColor: nextColor.hex ? theme.value.accentColor : analogous.toHexString(),
accentColor: nextColor.hex8 ? theme.value.accentColor : complement.toHex8String(),
})
}
})
watch(themeAccentColor, (nextColor) => {
const hexColor = nextColor.hex ? nextColor.hex : nextColor
const hexColor = nextColor.hex8 ? nextColor.hex8 : nextColor
saveTheme({
primaryColor: theme.value.primaryColor,
accentColor: hexColor,

48
packages/nc-gui-v2/pages/index/index/[id].vue → packages/nc-gui-v2/pages/index/index/[projectId].vue

@ -2,7 +2,6 @@
import type { Form } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import type { ProjectType } from 'nocodb-sdk'
import tinycolor from 'tinycolor2'
import {
extractSdkResponseErrorMsg,
navigateTo,
@ -22,7 +21,7 @@ useSidebar({ hasSidebar: false })
const route = useRoute()
const { project, loadProject, updateProject } = useProject(route.params.id as string)
const { project, loadProject, updateProject } = useProject(route.params.projectId as string)
await loadProject()
@ -38,15 +37,13 @@ const form = ref<typeof Form>()
const formState = reactive<Partial<ProjectType>>({
title: '',
color: '#FFFFFF00',
})
const renameProject = async () => {
formState.color = formState.color === '#FFFFFF00' ? '' : formState.color
try {
await updateProject(formState)
navigateTo(`/nc/${route.params.id}`)
navigateTo(`/nc/${route.params.projectId}`)
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
@ -55,7 +52,6 @@ const renameProject = async () => {
// select and focus title field on load
onMounted(async () => {
formState.title = project.value.title as string
formState.color = project.value.color && tinycolor(project.value.color).isValid() ? project.value.color : '#FFFFFF00'
await nextTick(() => {
// todo: replace setTimeout and follow better approach
setTimeout(() => {
@ -98,27 +94,6 @@ onMounted(async () => {
<a-input v-model:value="formState.title" name="title" class="nc-metadb-project-name" />
</a-form-item>
<div class="flex items-center">
<span>Project color: </span>
<a-menu class="!border-0 !m-0 !p-0">
<a-sub-menu key="project-color">
<template #title>
<button type="button" class="color-selector" :style="{ 'background-color': formState.color }">
<MdiNull v-if="formState.color === '#FFFFFF00'" />
</button>
</template>
<template #expandIcon></template>
<GeneralColorPicker v-model="formState.color" name="color" class="nc-metadb-project-color" />
</a-sub-menu>
</a-menu>
<MdiClose
v-show="formState.color !== '#FFFFFF00'"
class="cursor-pointer"
:style="{ color: 'red' }"
@click="formState.color = '#FFFFFF00'"
/>
</div>
<div class="text-center">
<button type="submit" class="submit">
<span class="flex items-center gap-2">
@ -156,23 +131,4 @@ onMounted(async () => {
}
}
}
:deep(.ant-menu-submenu-title) {
@apply !p-0 !mx-2;
}
.color-selector {
position: relative;
height: 32px;
width: 32px;
border-radius: 5px;
-webkit-text-stroke-width: 1px;
-webkit-text-stroke-color: white;
@apply flex text-gray-500 border-4 items-center justify-center;
}
.color-selector:hover {
filter: brightness(90%);
-webkit-filter: brightness(90%);
}
</style>

110
packages/nc-gui-v2/pages/index/index/index.vue

@ -1,6 +1,8 @@
<script lang="ts" setup>
import { Modal, message } from 'ant-design-vue'
import type { ProjectType } from 'nocodb-sdk'
import { Chrome } from '@ckpack/vue-color'
import tinycolor from 'tinycolor2'
import {
computed,
definePageMeta,
@ -17,7 +19,7 @@ definePageMeta({
title: 'title.myProject',
})
const { $e } = useNuxtApp()
const { $api, $e } = useNuxtApp()
const { api, isLoading } = useApi()
@ -64,6 +66,51 @@ const deleteProject = (project: ProjectType) => {
}
await loadProjects()
const themePrimaryColors = $ref(
(() => {
const colors: Record<string, any> = {}
for (const project of projects?.value || []) {
if (project?.id) {
try {
const projectMeta = typeof project.meta === 'string' ? JSON.parse(project.meta) : project.meta
colors[project.id] = tinycolor(projectMeta?.theme?.primaryColor).isValid()
? projectMeta?.theme?.primaryColor
: themeV2Colors['royal-blue'].DEFAULT
} catch (e) {
colors[project.id] = themeV2Colors['royal-blue'].DEFAULT
}
}
}
return colors
})(),
)
const oldPrimaryColors = ref({ ...themePrimaryColors })
watch(themePrimaryColors, async (nextColors) => {
for (const [projectId, nextColor] of Object.entries(nextColors)) {
if (oldPrimaryColors.value[projectId] === nextColor) continue
const hexColor = nextColor.hex8 ? nextColor.hex8 : nextColor
const tcolor = tinycolor(hexColor)
if (tcolor) {
const complement = tcolor.complement()
const project: ProjectType = await $api.project.read(projectId)
const meta = project?.meta && typeof project.meta === 'string' ? JSON.parse(project.meta) : project.meta || {}
await $api.project.update(projectId, {
color: hexColor,
meta: JSON.stringify({
...meta,
theme: {
primaryColor: hexColor,
accentColor: complement.toHex8String(),
},
}),
})
}
}
oldPrimaryColors.value = { ...themePrimaryColors }
})
</script>
<template>
@ -159,14 +206,49 @@ await loadProjects()
<!-- Title -->
<a-table-column key="title" :title="$t('general.title')" data-index="title">
<template #default="{ text, record }">
<div
class="capitalize color-transition group-hover:text-primary !w-[400px] h-full overflow-hidden overflow-ellipsis whitespace-nowrap pl-2"
:class="{ 'border-l-4': record.color }"
:style="{
'border-color': record.color,
}"
>
{{ text }}
<div class="flex items-center">
<div @click.stop>
<a-menu class="!border-0 !m-0 !p-0" trigger-sub-menu-action="click">
<template v-if="isUIAllowed('projectTheme')">
<a-sub-menu key="theme" popup-class-name="custom-color">
<template #title>
<div
class="color-selector"
:style="{
'background-color': themePrimaryColors[record.id].hex8 || themePrimaryColors[record.id],
'width': '8px',
'height': '100%',
}"
/>
</template>
<template #expandIcon></template>
<GeneralColorPicker
v-model="themePrimaryColors[record.id]"
:colors="enumColor.dark"
:row-size="5"
:advanced="false"
/>
<a-sub-menu key="pick-primary">
<template #title>
<div class="nc-project-menu-item group !py-0">
<ClarityColorPickerSolid class="group-hover:text-accent" />
Custom Color
</div>
</template>
<template #expandIcon></template>
<Chrome v-model="themePrimaryColors[record.id]" />
</a-sub-menu>
</a-sub-menu>
</template>
</a-menu>
</div>
<div
class="capitalize color-transition group-hover:text-primary !w-[400px] h-full overflow-hidden overflow-ellipsis whitespace-nowrap pl-2"
>
{{ text }}
</div>
</div>
</template>
</a-table-column>
@ -220,4 +302,14 @@ await loadProjects()
:deep(.ant-table) {
@apply min-h-[428px];
}
:deep(.ant-menu-submenu-title) {
@apply !p-0 !mr-1 !my-0 !h-5;
}
</style>
<style>
.custom-color .ant-menu-submenu-title {
height: auto !important;
}
</style>

Loading…
Cancel
Save