Browse Source

Merge pull request #3481 from nocodb/fix/3459-theme-palette

fix: theme palette
pull/3495/head
Raju Udava 2 years ago committed by GitHub
parent
commit
ca3e53d4f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 30
      packages/nc-gui-v2/components/general/ChromeWrapper.vue
  2. 22
      packages/nc-gui-v2/components/general/ColorPicker.vue
  3. 16
      packages/nc-gui-v2/composables/useProject.ts
  4. 71
      packages/nc-gui-v2/pages/[projectType]/[projectId]/index.vue
  5. 80
      packages/nc-gui-v2/pages/index/index/index.vue

30
packages/nc-gui-v2/components/general/ChromeWrapper.vue

@ -0,0 +1,30 @@
<script lang="ts" setup>
import { Chrome } from '@ckpack/vue-color'
import { computed } from '#imports'
interface Props {
modelValue?: any
mode?: 'hex' | 'hex8' | 'hsl' | 'hsv' | 'rgba'
}
const props = withDefaults(defineProps<Props>(), {
modelValue: () => '#00FFFFFF',
mode: () => 'hex8',
})
const emit = defineEmits(['update:modelValue', 'input'])
const picked = computed({
get: () => props.modelValue || '#00FFFFFF',
set: (val) => {
emit('update:modelValue', val[props.mode] || null)
emit('input', val[props.mode] || null)
},
})
</script>
<template>
<Chrome v-model="picked" />
</template>
<style scoped></style>

22
packages/nc-gui-v2/components/general/ColorPicker.vue

@ -1,5 +1,4 @@
<script lang="ts" setup> <script lang="ts" setup>
import { Chrome } from '@ckpack/vue-color'
import { computed, enumColor, ref, watch } from '#imports' import { computed, enumColor, ref, watch } from '#imports'
interface Props { interface Props {
@ -18,27 +17,28 @@ const props = withDefaults(defineProps<Props>(), {
pickButton: false, pickButton: false,
}) })
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue', 'input'])
const vModel = computed({ const vModel = computed({
get: () => props.modelValue, get: () => props.modelValue,
set: (val) => { set: (val) => {
emit('update:modelValue', val.hex8 ? val.hex8 : val || null) emit('update:modelValue', val || null)
emit('input', val || null)
}, },
}) })
const picked = ref<string | Record<string, any>>(props.modelValue || enumColor.light[0]) const picked = ref<string>(props.modelValue || enumColor.light[0])
const selectColor = (color: string | Record<string, any>) => { const selectColor = (color: string) => {
picked.value = typeof color === 'string' ? color : color.hex8 ? color.hex8 : color picked.value = color
vModel.value = typeof color === 'string' ? color : color.hex8 ? color.hex8 : color if (props.pickButton) vModel.value = color
} }
const compare = (colorA: string, colorB: string) => colorA.toLowerCase() === colorB.toLowerCase() const compare = (colorA: string, colorB: string) => colorA.toLowerCase() === colorB.toLowerCase()
watch(picked, (n, _o) => { watch(picked, (n, _o) => {
if (!props.pickButton) { if (!props.pickButton) {
vModel.value = typeof n === 'string' ? n : n.hex8 ? n.hex8 : n vModel.value = n
} }
}) })
</script> </script>
@ -50,11 +50,11 @@ watch(picked, (n, _o) => {
v-for="(color, i) of colors.slice((colId - 1) * rowSize, colId * rowSize)" v-for="(color, i) of colors.slice((colId - 1) * rowSize, colId * rowSize)"
:key="`color-${colId}-${i}`" :key="`color-${colId}-${i}`"
class="color-selector" class="color-selector"
:class="compare(typeof picked === 'string' ? picked : picked.hex8, color) ? 'selected' : ''" :class="compare(picked, color) ? 'selected' : ''"
:style="{ 'background-color': `${color}` }" :style="{ 'background-color': `${color}` }"
@click="selectColor(color)" @click="selectColor(color)"
> >
{{ compare(typeof picked === 'string' ? picked : picked.hex8, color) ? '&#10003;' : '' }} {{ compare(picked, color) ? '&#10003;' : '' }}
</button> </button>
</div> </div>
<a-card v-if="props.advanced" class="w-full mt-2" :body-style="{ padding: '0px' }" :bordered="false"> <a-card v-if="props.advanced" class="w-full mt-2" :body-style="{ padding: '0px' }" :bordered="false">
@ -64,7 +64,7 @@ watch(picked, (n, _o) => {
Pick Color Pick Color
</a-button> </a-button>
<div class="flex justify-center py-4"> <div class="flex justify-center py-4">
<Chrome v-model="picked" class="!w-full !shadow-none" /> <GeneralChromeWrapper v-model="picked" class="!w-full !shadow-none" />
</div> </div>
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>

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

@ -10,7 +10,7 @@ const [setup, use] = useInjectionState((_projectId?: MaybeRef<string>) => {
const { $api } = useNuxtApp() const { $api } = useNuxtApp()
const route = useRoute() const route = useRoute()
const { includeM2M } = useGlobal() const { includeM2M } = useGlobal()
const { setTheme } = useTheme() const { setTheme, theme } = useTheme()
const projectId = computed(() => (_projectId ? unref(_projectId) : (route.params.projectId as string))) const projectId = computed(() => (_projectId ? unref(_projectId) : (route.params.projectId as string)))
const project = ref<ProjectType>({}) const project = ref<ProjectType>({})
@ -99,15 +99,21 @@ const [setup, use] = useInjectionState((_projectId?: MaybeRef<string>) => {
} }
} }
async function saveTheme(theme: Partial<ThemeConfig>) { async function saveTheme(_theme: Partial<ThemeConfig>) {
const fullTheme = {
primaryColor: theme.value.primaryColor,
accentColor: theme.value.accentColor,
..._theme,
}
await updateProject({ await updateProject({
color: theme.primaryColor, color: fullTheme.primaryColor,
meta: { meta: {
...projectMeta.value, ...projectMeta.value,
theme, theme: fullTheme,
}, },
}) })
setTheme(theme) setTheme(fullTheme)
} }
watch( watch(

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

@ -1,6 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { Chrome } from '@ckpack/vue-color'
import tinycolor from 'tinycolor2' import tinycolor from 'tinycolor2'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { import {
@ -18,9 +17,7 @@ import {
useProject, useProject,
useRoute, useRoute,
useTabs, useTabs,
useTheme,
useUIPermission, useUIPermission,
watch,
} from '#imports' } from '#imports'
import { TabType } from '~/composables' import { TabType } from '~/composables'
@ -58,8 +55,6 @@ const sidebar = ref()
const email = computed(() => user.value?.email ?? '---') const email = computed(() => user.value?.email ?? '---')
const { theme } = useTheme()
const logout = () => { const logout = () => {
signOut() signOut()
navigateTo('/signin') navigateTo('/signin')
@ -84,37 +79,41 @@ await loadProject()
await loadTables() await loadTables()
const themePrimaryColor = ref<any>(theme.value.primaryColor)
const themeAccentColor = ref<any>(theme.value.accentColor)
const { t } = useI18n() const { t } = useI18n()
// Chrome provides object so if custom picker used we only edit primary otherwise use complement as accent const handleThemeColor = async (mode: 'swatch' | 'primary' | 'accent', color: string) => {
watch(themePrimaryColor, async (nextColor) => { switch (mode) {
const hexColor = nextColor.hex8 ? nextColor.hex8 : nextColor case 'swatch': {
const tcolor = tinycolor(hexColor) const tcolor = tinycolor(color)
if (tcolor) { if (tcolor.isValid()) {
const complement = tcolor.complement() const complement = tcolor.complement()
await saveTheme({ await saveTheme({
primaryColor: hexColor, primaryColor: color,
accentColor: themeAccentColor.value, accentColor: complement.toHex8String(),
}) })
themeAccentColor.value = nextColor.hex8 ? theme.value.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
}
} }
}) }
watch(themeAccentColor, (nextColor) => {
const hexColor = nextColor.hex8 ? nextColor.hex8 : nextColor
// skip if the color is same as saved
if (hexColor === theme.value.accentColor) return
saveTheme({
primaryColor: theme.value.primaryColor,
accentColor: hexColor,
})
})
if (!route.params.type && isUIAllowed('teamAndAuth')) { if (!route.params.type && isUIAllowed('teamAndAuth')) {
addTab({ type: TabType.AUTH, title: t('title.teamAndAuth') }) addTab({ type: TabType.AUTH, title: t('title.teamAndAuth') })
@ -292,10 +291,10 @@ const copyAuthToken = async () => {
<template #expandIcon></template> <template #expandIcon></template>
<GeneralColorPicker <GeneralColorPicker
v-model="themePrimaryColor"
:colors="projectThemeColors" :colors="projectThemeColors"
:row-size="9" :row-size="9"
:advanced="false" :advanced="false"
@input="handleThemeColor('swatch', $event)"
/> />
<!-- Custom Theme --> <!-- Custom Theme -->
@ -322,7 +321,7 @@ const copyAuthToken = async () => {
</div> </div>
</template> </template>
<template #expandIcon></template> <template #expandIcon></template>
<Chrome v-model="themePrimaryColor" /> <GeneralChromeWrapper @input="handleThemeColor('primary', $event)" />
</a-sub-menu> </a-sub-menu>
<!-- Accent Color --> <!-- Accent Color -->
@ -334,7 +333,7 @@ const copyAuthToken = async () => {
</div> </div>
</template> </template>
<template #expandIcon></template> <template #expandIcon></template>
<Chrome v-model="themeAccentColor" /> <GeneralChromeWrapper @input="handleThemeColor('accent', $event)" />
</a-sub-menu> </a-sub-menu>
</a-sub-menu> </a-sub-menu>
</a-sub-menu> </a-sub-menu>

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

@ -1,7 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { Empty, Modal, message } from 'ant-design-vue' import { Empty, Modal, message } from 'ant-design-vue'
import type { ProjectType } from 'nocodb-sdk' import type { ProjectType } from 'nocodb-sdk'
import { Chrome } from '@ckpack/vue-color'
import tinycolor from 'tinycolor2' import tinycolor from 'tinycolor2'
import { import {
computed, computed,
@ -68,50 +67,41 @@ const deleteProject = (project: ProjectType) => {
await loadProjects() await loadProjects()
const themePrimaryColors = $ref( const handleProjectColor = async (projectId: string, color: string) => {
(() => { const tcolor = tinycolor(color)
const colors: Record<string, any> = {} if (tcolor.isValid()) {
for (const project of projects?.value || []) { const complement = tcolor.complement()
if (project?.id) { const project: ProjectType = await $api.project.read(projectId)
try { const meta = project?.meta && typeof project.meta === 'string' ? JSON.parse(project.meta) : project.meta || {}
const projectMeta = typeof project.meta === 'string' ? JSON.parse(project.meta) : project.meta await $api.project.update(projectId, {
colors[project.id] = tinycolor(projectMeta?.theme?.primaryColor).isValid() color,
? projectMeta?.theme?.primaryColor meta: JSON.stringify({
: themeV2Colors['royal-blue'].DEFAULT ...meta,
} catch (e) { theme: {
colors[project.id] = themeV2Colors['royal-blue'].DEFAULT primaryColor: color,
} accentColor: complement.toHex8String(),
} },
} }),
return colors })
})(), // Update local project
) const localProject = projects.value?.find((p) => p.id === projectId)
if (localProject) {
const oldPrimaryColors = ref({ ...themePrimaryColors }) localProject.color = color
localProject.meta = JSON.stringify({
watch(themePrimaryColors, async (nextColors) => { ...meta,
for (const [projectId, nextColor] of Object.entries(nextColors)) { theme: {
if (oldPrimaryColors.value[projectId] === nextColor) continue primaryColor: color,
const hexColor = nextColor.hex8 ? nextColor.hex8 : nextColor accentColor: complement.toHex8String(),
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 } }
})
const getProjectPrimary = (project: ProjectType) => {
const meta = project?.meta && typeof project.meta === 'string' ? JSON.parse(project.meta) : project.meta || {}
return meta?.theme?.primaryColor || themeV2Colors['royal-blue'].DEFAULT
}
</script> </script>
<template> <template>
@ -219,7 +209,7 @@ watch(themePrimaryColors, async (nextColors) => {
<div <div
class="color-selector" class="color-selector"
:style="{ :style="{
'background-color': themePrimaryColors[record.id].hex8 || themePrimaryColors[record.id], 'background-color': getProjectPrimary(record),
'width': '8px', 'width': '8px',
'height': '100%', 'height': '100%',
}" }"
@ -229,10 +219,10 @@ watch(themePrimaryColors, async (nextColors) => {
<template #expandIcon></template> <template #expandIcon></template>
<GeneralColorPicker <GeneralColorPicker
v-model="themePrimaryColors[record.id]"
:colors="projectThemeColors" :colors="projectThemeColors"
:row-size="9" :row-size="9"
:advanced="false" :advanced="false"
@input="handleProjectColor(record.id, $event)"
/> />
<a-sub-menu key="pick-primary"> <a-sub-menu key="pick-primary">
<template #title> <template #title>
@ -242,7 +232,7 @@ watch(themePrimaryColors, async (nextColors) => {
</div> </div>
</template> </template>
<template #expandIcon></template> <template #expandIcon></template>
<Chrome v-model="themePrimaryColors[record.id]" /> <GeneralChromeWrapper @input="handleProjectColor(record.id, $event)" />
</a-sub-menu> </a-sub-menu>
</a-sub-menu> </a-sub-menu>
</template> </template>

Loading…
Cancel
Save