Browse Source

Merge pull request #7807 from nocodb/nc-feat/color-option-for-base-logo

Nc feat: Color option for Base logo
pull/7849/head
Raju Udava 4 months ago committed by GitHub
parent
commit
c75e8f0a8c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 35
      packages/nc-gui/components/cmd-k/index.vue
  2. 35
      packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue
  3. 2
      packages/nc-gui/components/dlg/ProjectDelete.vue
  4. 1
      packages/nc-gui/components/dlg/ProjectDuplicate.vue
  5. 2
      packages/nc-gui/components/dlg/share-and-collaborate/View.vue
  6. 104
      packages/nc-gui/components/general/BaseIconColorPicker.vue
  7. 104
      packages/nc-gui/components/general/ColorPicker.vue
  8. 61
      packages/nc-gui/components/general/ColorSliderWrapper.vue
  9. 58
      packages/nc-gui/components/general/ProjectIcon.vue
  10. 2
      packages/nc-gui/components/project/View.vue
  11. 16
      packages/nc-gui/components/smartsheet/Form.vue
  12. 2
      packages/nc-gui/components/smartsheet/toolbar/ViewInfo.vue
  13. 20
      packages/nc-gui/components/workspace/CreateProjectDlg.vue
  14. 24
      packages/nc-gui/components/workspace/ProjectList.vue
  15. 4
      packages/nc-gui/lang/en.json
  16. 7
      packages/nc-gui/store/bases.ts
  17. 2
      packages/nc-gui/utils/colorsUtils.ts
  18. 5
      packages/nocodb/src/models/Base.ts
  19. 4
      packages/nocodb/src/schema/swagger-v2.json
  20. 4
      packages/nocodb/src/schema/swagger.json
  21. 3
      packages/nocodb/src/services/command-palette.service.ts

35
packages/nc-gui/components/cmd-k/index.vue

@ -17,6 +17,7 @@ interface CmdAction {
keywords?: string[]
section?: string
is_default?: number | null
iconColor?: string
}
const props = defineProps<{
@ -385,20 +386,26 @@ defineExpose({
@click="fireAction(act)"
>
<div class="cmdk-action-content w-full">
<component
:is="(iconMap as any)[act.icon]"
v-if="act.icon && typeof act.icon === 'string' && (iconMap as any)[act.icon]"
class="cmdk-action-icon"
:class="{
'!text-blue-500': act.icon === 'grid',
'!text-purple-500': act.icon === 'form',
'!text-[#FF9052]': act.icon === 'kanban',
'!text-pink-500': act.icon === 'gallery',
}"
/>
<div v-else-if="act.icon" class="cmdk-action-icon max-w-4 flex items-center justify-center">
<LazyGeneralEmojiPicker class="!text-sm !h-4 !w-4" size="small" :emoji="act.icon" readonly />
</div>
<template v-if="title === 'Bases' || act.icon === 'project'">
<GeneralBaseIconColorPicker :key="act.iconColor" :model-value="act.iconColor" type="database" readonly>
</GeneralBaseIconColorPicker>
</template>
<template v-else>
<component
:is="(iconMap as any)[act.icon]"
v-if="act.icon && typeof act.icon === 'string' && (iconMap as any)[act.icon]"
class="cmdk-action-icon"
:class="{
'!text-blue-500': act.icon === 'grid',
'!text-purple-500': act.icon === 'form',
'!text-[#FF9052]': act.icon === 'kanban',
'!text-pink-500': act.icon === 'gallery',
}"
/>
<div v-else-if="act.icon" class="cmdk-action-icon max-w-4 flex items-center justify-center">
<LazyGeneralEmojiPicker class="!text-sm !h-4 !w-4" size="small" :emoji="act.icon" readonly />
</div>
</template>
<a-tooltip overlay-class-name="!px-2 !py-1 !rounded-lg">
<template #title>
{{ act.title }}

35
packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue

@ -75,6 +75,8 @@ useTabs()
const { meta: metaKey, ctrlKey } = useMagicKeys()
const { refreshCommandPalette } = useCommandPalette()
const editMode = ref(false)
const tempTitle = ref('')
@ -172,18 +174,20 @@ defineExpose({
enableEditMode,
})
const setIcon = async (icon: string, base: BaseType) => {
const setColor = async (color: string, base: BaseType) => {
try {
const meta = {
...((base.meta as object) || {}),
icon,
...parseProp(base.meta),
iconColor: color,
}
basesStore.updateProject(base.id!, { meta: JSON.stringify(meta) })
$e('a:base:icon:navdraw', { icon })
$e('a:base:icon:color:navdraw', { iconColor: color })
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
} finally {
refreshCommandPalette()
}
}
@ -420,19 +424,20 @@ const projectDelete = () => {
</NcButton>
<div class="flex items-center mr-1" @click="onProjectClick(base)">
<div v-e="['c:base:emojiSelect']" class="flex items-center select-none w-6 h-full">
<div class="flex items-center select-none w-6 h-full">
<a-spin v-if="base.isLoading" class="!ml-1.25 !flex !flex-row !items-center !my-0.5 w-8" :indicator="indicator" />
<LazyGeneralEmojiPicker
v-else
:key="base.meta?.icon"
:emoji="base.meta?.icon"
:readonly="true"
size="small"
@emoji-selected="setIcon($event, base)"
>
<GeneralProjectIcon :type="base.type" />
</LazyGeneralEmojiPicker>
<div v-else>
<GeneralBaseIconColorPicker
:key="`${base.id}_${parseProp(base.meta).iconColor}`"
:type="base?.type"
:model-value="parseProp(base.meta).iconColor"
size="small"
:readonly="(base?.type && base?.type !== 'database') || !isUIAllowed('baseRename')"
@update:model-value="setColor($event, base)"
>
</GeneralBaseIconColorPicker>
</div>
</div>
</div>

2
packages/nc-gui/components/dlg/ProjectDelete.vue

@ -52,7 +52,7 @@ const onDelete = async () => {
<GeneralDeleteModal v-model:visible="visible" :entity-name="$t('objects.project')" :on-delete="onDelete">
<template #entity-preview>
<div v-if="base" class="flex flex-row items-center py-2 px-2.25 bg-gray-50 rounded-lg text-gray-700 mb-4">
<GeneralProjectIcon :type="base.type" class="nc-view-icon px-1.5 w-10" />
<GeneralProjectIcon :color="parseProp(base.meta).iconColor" :type="base.type" class="nc-view-icon w-6 h-6 mx-1" />
<div
class="capitalize text-ellipsis overflow-hidden select-none w-full pl-1.75"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"

1
packages/nc-gui/components/dlg/ProjectDuplicate.vue

@ -62,6 +62,7 @@ const _duplicate = async () => {
primaryColor: color,
accentColor: complement.toHex8String(),
},
iconColor: parseProp(props.base.meta).iconColor,
}),
},
})

2
packages/nc-gui/components/dlg/share-and-collaborate/View.vue

@ -154,7 +154,7 @@ watch(showShareModal, (val) => {
</div>
<div class="share-base">
<div class="flex flex-row items-center gap-x-2 px-4 pt-3 pb-3 select-none">
<GeneralProjectIcon :type="base.type" class="nc-view-icon group-hover" />
<GeneralProjectIcon :color="parseProp(base.meta).iconColor" :type="base.type" class="nc-view-icon group-hover" />
<div>{{ $t('activity.shareBase.label') }}</div>
<div

104
packages/nc-gui/components/general/BaseIconColorPicker.vue

@ -0,0 +1,104 @@
<script lang="ts" setup>
import tinycolor from 'tinycolor2'
import { NcProjectType, baseIconColors } from '#imports'
const props = withDefaults(
defineProps<{
type?: NcProjectType | string
modelValue?: string
size?: 'small' | 'medium' | 'large' | 'xlarge'
readonly?: boolean
iconClass?: string
}>(),
{
type: NcProjectType.DB,
size: 'small',
},
)
const emit = defineEmits(['update:modelValue'])
const { modelValue } = toRefs(props)
const { size, readonly } = props
const isOpen = ref(false)
const colorRef = ref(tinycolor(modelValue.value).isValid() ? modelValue.value : baseIconColors[0])
const updateIconColor = (color: string) => {
const tcolor = tinycolor(color)
if (tcolor.isValid()) {
colorRef.value = color
}
}
const onClick = (e: Event) => {
if (readonly) return
e.stopPropagation()
isOpen.value = !isOpen.value
}
watch(
isOpen,
(value) => {
if (!value && colorRef.value !== modelValue.value) {
emit('update:modelValue', colorRef.value)
}
},
{
immediate: true,
},
)
</script>
<template>
<div>
<a-dropdown v-model:visible="isOpen" :trigger="['click']" :disabled="readonly">
<div
class="flex flex-row justify-center items-center select-none rounded-md nc-base-icon-picker-trigger"
:class="{
'hover:bg-gray-500 hover:bg-opacity-15 cursor-pointer': !readonly,
'bg-gray-500 bg-opacity-15': isOpen,
'h-6 w-6 text-lg': size === 'small',
'h-8 w-8 text-xl': size === 'medium',
'h-10 w-10 text-2xl': size === 'large',
'h-14 w-16 text-5xl': size === 'xlarge',
}"
@click="onClick"
>
<NcTooltip placement="topLeft" :disabled="readonly">
<template #title> {{ $t('tooltip.changeIconColour') }} </template>
<div>
<GeneralProjectIcon :color="colorRef" :type="type" />
</div>
</NcTooltip>
</div>
<template #overlay>
<div
class="nc-base-icon-color-picker-dropdown relative bg-white rounded-lg border-1 border-gray-200 overflow-hidden max-w-[342px]"
>
<div class="flex justify-start">
<GeneralColorPicker
:model-value="colorRef"
:colors="baseIconColors"
:is-new-design="true"
class="nc-base-icon-color-picker"
@input="updateIconColor"
/>
</div>
</div>
</template>
</a-dropdown>
</div>
</template>
<style lang="scss" scoped>
.nc-base-icon-color-picker-dropdown {
box-shadow: 0px 8px 8px -4px #0000000a, 0px 20px 24px -4px #0000001a;
}
</style>

104
packages/nc-gui/components/general/ColorPicker.vue

@ -1,4 +1,5 @@
<script lang="ts" setup>
import tinycolor from 'tinycolor2'
import { computed, enumColor, ref, watch } from '#imports'
interface Props {
@ -7,7 +8,7 @@ interface Props {
rowSize?: number
advanced?: boolean
pickButton?: boolean
borders?: string[]
colorBoxBorder?: boolean
isNewDesign?: boolean
}
@ -17,6 +18,7 @@ const props = withDefaults(defineProps<Props>(), {
rowSize: 10,
advanced: true,
pickButton: false,
colorBoxBorder: false,
isNewDesign: false,
})
@ -41,7 +43,8 @@ const selectColor = (color: string, closeModal = false) => {
const isPickerOn = ref(false)
const compare = (colorA: string, colorB: string) => colorA.toLowerCase() === colorB.toLowerCase()
const compare = (colorA: string, colorB: string) =>
colorA.toLowerCase() === colorB.toLowerCase() || colorA.toLowerCase() === tinycolor(colorB).toHex8String().toLowerCase()
watch(picked, (n, _o) => {
vModel.value = n
@ -50,31 +53,57 @@ watch(picked, (n, _o) => {
<template>
<div class="color-picker">
<div v-for="colId in Math.ceil(props.colors.length / props.rowSize)" :key="colId" class="color-picker-row">
<button
<div
v-for="colId in Math.ceil(props.colors.length / props.rowSize)"
:key="colId"
class="color-picker-row"
:class="{
'mt-2': colId > 1,
}"
>
<div
v-for="(color, i) of colors.slice((colId - 1) * rowSize, colId * rowSize)"
:key="`color-${colId}-${i}`"
class="color-selector"
:class="{ 'selected': compare(picked, color), 'new-design': isNewDesign }"
:style="{
'background-color': `${color}`,
'border': borders?.length && borders[i] ? `1px solid ${borders[i]}` : undefined,
class="p-1 rounded-md flex h-8"
:class="{
'hover:bg-gray-200': isNewDesign,
}"
@click="selectColor(color, true)"
>
{{ compare(picked, color) && !isNewDesign ? '&#10003;' : '' }}
</button>
<button class="h-6 w-6 mt-2.7 ml-1 border-1 border-[grey] rounded-md" @click="isPickerOn = !isPickerOn">
<GeneralTooltip>
<template #title>{{ $t('activity.moreColors') }}</template>
<div class="flex items-center justify-center">
<GeneralIcon :icon="isPickerOn ? 'minus' : 'plus'" class="w-4 h-4" />
</div>
</GeneralTooltip>
</button>
<button
class="color-selector"
:class="{ 'selected': compare(picked, color), 'new-design': isNewDesign }"
:style="{
backgroundColor: `${color}`,
border: colorBoxBorder ? `1px solid ${tinycolor(color).darken(30).toString()}` : undefined,
}"
@click="selectColor(color, true)"
>
{{ compare(picked, color) && !isNewDesign ? '&#10003;' : '' }}
</button>
</div>
<div
class="p-1 rounded-md h-8"
:class="{
'hover:bg-gray-200': isNewDesign,
}"
>
<button class="nc-more-colors-trigger h-6 w-6 border-1 border-gray-400 rounded" @click="isPickerOn = !isPickerOn">
<GeneralTooltip>
<template #title>{{ $t('activity.moreColors') }}</template>
<div class="flex items-center justify-center">
<GeneralIcon :icon="isPickerOn ? 'minus' : 'plus'" class="w-4 h-4" />
</div>
</GeneralTooltip>
</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="{ paddingLeft: '4px !important', paddingRight: '4px !important' }"
:bordered="false"
>
<div v-if="isPickerOn" class="flex justify-center">
<LazyGeneralChromeWrapper v-model="picked" class="!w-full !shadow-none" />
</div>
@ -82,25 +111,15 @@ watch(picked, (n, _o) => {
</div>
</template>
<style scoped>
<style lansg="scss" scoped>
.color-picker {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
background: white;
padding: 10px;
@apply flex flex-col items-center justify-center bg-white p-2.5;
}
.color-picker-row {
display: flex;
flex-direction: row;
@apply flex flex-row space-x-1;
}
.color-selector {
position: relative;
height: 25px;
width: 25px;
margin: 10px 5px;
border-radius: 5px;
@apply h-6 w-6 rounded;
-webkit-text-stroke-width: 1px;
-webkit-text-stroke-color: white;
}
@ -108,19 +127,14 @@ watch(picked, (n, _o) => {
filter: brightness(90%);
-webkit-filter: brightness(90%);
}
.color-selector.selected:not(.new-design) {
filter: brightness(90%);
-webkit-filter: brightness(90%);
}
.color-selector:focus.new-design {
.color-selector:focus,
.color-selector.selected,
.nc-more-colors-trigger:focus {
outline: none;
box-shadow: 0px 0px 0px 2px #fff, 0px 0px 0px 4px #3069fe;
}
.color-selector.selected.new-design {
box-shadow: 0px 0px 0px 2px #fff, 0px 0px 0px 4px #3069fe;
}
:deep(.vc-chrome-toggle-icon) {
@apply ml-3!important;
@apply !ml-3;
}
</style>

61
packages/nc-gui/components/general/ColorSliderWrapper.vue

@ -0,0 +1,61 @@
<script lang="ts" setup>
import { Slider } from '@ckpack/vue-color'
import tinycolor from 'tinycolor2'
interface Props {
modelValue?: any
mode?: 'hsl' | 'hsv'
}
const props = withDefaults(defineProps<Props>(), {
modelValue: '#3069FE',
mode: 'hsv',
})
const emit = defineEmits(['update:modelValue', 'input'])
const picked = computed({
get: () => tinycolor(props.modelValue || '#3069FE').toHsv() as any,
set: (val) => {
if (val) {
emit('update:modelValue', val[props.mode] || null)
emit('input', val[props.mode] || null)
}
},
})
</script>
<template>
<Slider
v-model="picked"
class="nc-color-slider-wrapper min-w-[200px]"
:style="{
'--nc-color-slider-pointer': tinycolor(`hsv(${picked.h ?? 199}, 100%, 100%)`).toHexString(),
}"
/>
</template>
<style lang="scss" scoped>
.nc-color-slider-wrapper {
&.vc-slider {
@apply !w-full;
}
:deep(.vc-slider-swatches) {
@apply hidden;
}
:deep(.vc-slider-hue-warp) {
@apply h-1.5;
.vc-hue {
@apply rounded-lg;
}
.vc-hue-pointer {
top: -3px !important;
}
.vc-hue-picker {
background-color: white;
box-shadow: 0 0 0 3px var(--nc-color-slider-pointer) !important;
}
}
}
</style>

58
packages/nc-gui/components/general/ProjectIcon.vue

@ -1,18 +1,62 @@
<script lang="ts" setup>
const { hoverable } = defineProps<{
type?: string
hoverable?: boolean
}>()
import tinycolor from 'tinycolor2'
import { baseIconColors } from '#imports'
const props = withDefaults(
defineProps<{
type?: string
hoverable?: boolean
color?: string
}>(),
{
color: baseIconColors[0],
},
)
const { color } = toRefs(props)
const iconColor = computed(() => {
return color.value && tinycolor(color.value).isValid()
? {
tint: baseIconColors.includes(color.value) ? color.value : tinycolor(color.value).lighten(10).toHexString(),
shade: tinycolor(color.value).darken(40).toHexString(),
}
: {
tint: baseIconColors[0],
shade: tinycolor(baseIconColors[0]).darken(40).toHexString(),
}
})
</script>
<template>
<GeneralIcon
icon="project"
<svg
width="16"
height="16"
viewBox="0 0 1073 1073"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="text-[#2824FB] base"
:class="{
'nc-base-icon-hoverable': hoverable,
}"
/>
>
<mask id="mask0_1749_80944" style="mask-type: luminance" maskUnits="userSpaceOnUse" x="94" y="40" width="885" height="993">
<path d="M978.723 40H94V1033H978.723V40Z" fill="white" />
</mask>
<g mask="url(#mask0_1749_80944)">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M638.951 291.265L936.342 462.949C966.129 480.145 980.256 502.958 978.723 525.482V774.266C980.256 796.789 966.129 819.602 936.342 836.798L638.951 1008.48C582.292 1041.19 490.431 1041.19 433.773 1008.48L136.381 836.798C106.595 819.602 92.4675 796.789 93.9999 774.266L93.9999 525.482C92.4675 502.957 106.595 480.145 136.381 462.949L433.773 291.265C490.431 258.556 582.292 258.556 638.951 291.265Z"
:fill="iconColor.shade"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M638.951 65.0055L936.342 236.69C966.129 253.886 980.256 276.699 978.723 299.222V548.006C980.256 570.529 966.129 593.343 936.342 610.538L638.951 782.223C582.292 814.931 490.431 814.931 433.773 782.223L136.381 610.538C106.595 593.343 92.4675 570.529 93.9999 548.006L93.9999 299.222C92.4675 276.699 106.595 253.886 136.381 236.69L433.773 65.0055C490.431 32.2968 582.292 32.2968 638.951 65.0055Z"
:fill="iconColor.tint"
/>
</g>
</svg>
</template>
<style scoped>

2
packages/nc-gui/components/project/View.vue

@ -82,7 +82,7 @@ watch(
<div class="flex flex-row items-center gap-x-3">
<GeneralOpenLeftSidebarBtn />
<div class="flex flex-row items-center h-full gap-x-2.5">
<GeneralProjectIcon :type="openedProject?.type" />
<GeneralProjectIcon :type="openedProject?.type" :color="parseProp(openedProject?.meta).iconColor" />
<NcTooltip class="flex font-medium text-sm capitalize truncate max-w-150" show-on-truncate-only>
<template #title> {{ openedProject?.title }}</template>
<span class="truncate">

16
packages/nc-gui/components/smartsheet/Form.vue

@ -1541,19 +1541,9 @@ useEventListener(
'#E5D4F5',
'#FFCFE6',
]"
:borders="[
'#6A7184',
'#FF4A3F',
'#FA8231',
'#FCBE3A',
'#27D665',
'#36BFFF',
'#FC3AC6',
'#7D26CD',
'#B33771',
]"
:is-new-design="true"
class="nc-form-theme-color-picker !p-0 !-ml-1"
color-box-border
is-new-design
class="nc-form-theme-color-picker !pb-0 !pl-0 -ml-1"
@input="handleChangeBackground"
/>
</div>

2
packages/nc-gui/components/smartsheet/toolbar/ViewInfo.vue

@ -50,7 +50,7 @@ const openedBaseUrl = computed(() => {
</template>
<div class="flex flex-row items-center gap-x-1.5">
<GeneralProjectIcon
:meta="{ type: base?.type }"
:type="base?.type"
class="!grayscale min-w-4"
:style="{
filter: 'grayscale(100%) brightness(115%)',

20
packages/nc-gui/components/workspace/CreateProjectDlg.vue

@ -3,7 +3,16 @@ import type { RuleObject } from 'ant-design-vue/es/form'
import type { Form, Input } from 'ant-design-vue'
import type { VNodeRef } from '@vue/runtime-core'
import { computed } from '@vue/reactivity'
import { NcProjectType, baseTitleValidator, extractSdkResponseErrorMsg, ref, useGlobal, useI18n, useVModel } from '#imports'
import {
NcProjectType,
baseIconColors,
baseTitleValidator,
extractSdkResponseErrorMsg,
ref,
useGlobal,
useI18n,
useVModel,
} from '#imports'
const props = defineProps<{
modelValue: boolean
@ -38,6 +47,9 @@ const form = ref<typeof Form>()
const formState = ref({
title: '',
meta: {
iconColor: baseIconColors[Math.floor(Math.random() * 1000) % baseIconColors.length],
},
})
const creating = ref(false)
@ -48,6 +60,7 @@ const createProject = async () => {
const base = await _createProject({
type: baseType.value,
title: formState.value.title,
meta: formState.value.meta,
})
navigateToProject({
@ -77,6 +90,9 @@ watch(dialogShow, async (n, o) => {
formState.value = {
title: 'Base',
meta: {
iconColor: baseIconColors[Math.floor(Math.random() * 1000) % baseIconColors.length],
},
}
await nextTick()
@ -100,7 +116,7 @@ const typeLabel = computed(() => {
<template #header>
<!-- Create A New Table -->
<div class="flex flex-row items-center">
<GeneralProjectIcon :type="baseType" class="mr-2.5 !text-lg !h-4" />
<GeneralProjectIcon :color="formState.meta.iconColor" :type="baseType" class="mr-2.5 !text-lg !h-4" />
{{
$t('general.createEntity', {
entity: typeLabel,

24
packages/nc-gui/components/workspace/ProjectList.vue

@ -10,6 +10,7 @@ import {
isEeUI,
message,
navigateTo,
parseProp,
ref,
storeToRefs,
useBases,
@ -181,16 +182,16 @@ function onProjectTitleClick(index: number) {
}
}
const setIcon = async (icon: string, base: BaseType) => {
const setColor = async (color: string, base: BaseType) => {
try {
const meta = {
...((base.meta as object) || {}),
icon,
...parseProp(base.meta),
iconColor: color,
}
basesStore.updateProject(base.id!, { meta: JSON.stringify(meta) })
$e('a:base:icon:navdraw', { icon })
$e('a:base:icon:color:navdraw', { iconColor: color })
} catch (e: any) {
message.error(await extractSdkResponseErrorMsg(e))
}
@ -250,15 +251,14 @@ const setIcon = async (icon: string, base: BaseType) => {
<template v-if="column.dataIndex === 'title'">
<div class="flex items-center nc-base-title gap-2.5 max-w-full -ml-1.5">
<div class="flex items-center gap-2 text-center">
<LazyGeneralEmojiPicker
:key="record.id"
:emoji="record.meta?.icon"
size="small"
readonly
@emoji-selected="setIcon($event, record)"
<GeneralBaseIconColorPicker
:key="`${record.id}_${parseProp(record.meta).iconColor}`"
:type="record?.type"
:model-value="parseProp(record.meta).iconColor"
:readonly="(record?.type && record?.type !== 'database') || !isUIAllowed('baseRename')"
@update:model-value="setColor($event, record)"
>
<GeneralProjectIcon :type="record.type" />
</LazyGeneralEmojiPicker>
</GeneralBaseIconColorPicker>
<!-- todo: replace with switch -->
</div>

4
packages/nc-gui/lang/en.json

@ -197,7 +197,8 @@
"banner": "Banner",
"logo": "Logo",
"dropdown": "Dropdown",
"list": "List"
"list": "List",
"apply": "Apply"
},
"objects": {
"day": "Day",
@ -965,6 +966,7 @@
"clientKey": "Select .key file",
"clientCert": "Select .cert file",
"clientCA": "Select CA file",
"changeIconColour": "Change icon colour",
"preFillFormInfo": "Generate share form URL with pre-filled field data. To get a pre-filled link, make sure you’ve filled the necessary fields in the form view builder.",
"surveyFormInfo": "Form mode with one field per page"
},

7
packages/nc-gui/store/bases.ts

@ -201,6 +201,7 @@ export const useBases = defineStore('basesStore', () => {
isExpanded: route.value.params.baseId === baseId || existingProject.isExpanded,
// isLoading is managed by Sidebar
isLoading: existingProject.isLoading,
meta: { ...parseProp(existingProject.meta), ...parseProp(_project.meta) },
}
bases.value.set(baseId, base)
@ -229,7 +230,7 @@ export const useBases = defineStore('basesStore', () => {
...baseUpdatePayload,
}
bases.value.set(baseId, base)
bases.value.set(baseId, { ...base, meta: parseProp(base.meta) })
await api.base.update(baseId, baseUpdatePayload)
@ -241,11 +242,15 @@ export const useBases = defineStore('basesStore', () => {
workspaceId?: string
type: string
linkedDbProjectIds?: string[]
meta?: Record<string, unknown>
}) => {
const result = await api.base.create(
{
title: basePayload.title,
linked_db_project_ids: basePayload.linkedDbProjectIds,
meta: JSON.stringify({
...(basePayload.meta || {}),
}),
},
{
baseURL: getBaseUrl('nc'),

2
packages/nc-gui/utils/colorsUtils.ts

@ -111,6 +111,8 @@ export const baseThemeColors = [
'#333333',
]
export const baseIconColors = ['#36BFFF', '#FA8231', '#FCBE3A', '#27D665', '#6A7184', '#FF4A3F', '#FC3AC6', '#7D26CD']
const designSystem = {
light: [
// '#EBF0FF',

5
packages/nocodb/src/models/Base.ts

@ -62,6 +62,11 @@ export default class Base implements BaseType {
insertObj.order = await ncMeta.metaGetNextOrder(MetaTable.PROJECT, {});
}
// stringify meta
if (insertObj.meta) {
insertObj.meta = stringifyMetaProp(insertObj);
}
const { id: baseId } = await ncMeta.metaInsert2(
null,
null,

4
packages/nocodb/src/schema/swagger-v2.json

@ -16352,6 +16352,10 @@
"items": {
"type": "string"
}
},
"meta": {
"$ref": "#/components/schemas/Meta",
"description": "Base Meta"
}
},
"required": [

4
packages/nocodb/src/schema/swagger.json

@ -22522,6 +22522,10 @@
"items": {
"type": "string"
}
},
"meta": {
"$ref": "#/components/schemas/Meta",
"description": "Base Meta"
}
},
"required": [

3
packages/nocodb/src/services/command-palette.service.ts

@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
import { type UserType, ViewTypes } from 'nocodb-sdk';
import { Base } from '~/models';
import { TablesService } from '~/services/tables.service';
import { deserializeJSON } from '~/utils/serialize';
const viewTypeAlias: Record<number, string> = {
[ViewTypes.GRID]: 'grid',
@ -28,6 +29,7 @@ export class CommandPaletteService {
id: `p-${base.id}`,
title: base.title,
icon: 'project',
iconColor: deserializeJSON(base.meta)?.iconColor,
section: 'Bases',
scopePayload: {
scope: `p-${base.id}`,
@ -70,6 +72,7 @@ export class CommandPaletteService {
id: `p-${b.id}`,
title: b.title,
icon: 'project',
iconColor: deserializeJSON(b.meta)?.iconColor,
section: 'Bases',
});
}

Loading…
Cancel
Save