Merge pull request #12971 from nocodb/nc-fix/update-managed-app-base-icon

This commit is contained in:
Anbarasu
2026-01-30 11:36:34 +05:30
committed by GitHub
18 changed files with 238 additions and 39 deletions

View File

@@ -16,6 +16,8 @@ interface CmdAction {
keywords?: string[]
section?: string
iconColor?: string
managed_app_master?: boolean
managed_app_id?: string
}
const props = defineProps<{
@@ -90,6 +92,8 @@ const nestedScope = computed(() => {
section: parentEl?.section,
iconType: parentEl?.iconType,
iconColor: parent.startsWith('ws-') ? parentEl?.iconColor : null,
managed_app_master: !!parentEl?.managed_app_master,
managed_app_id: parentEl?.managed_app_id || '',
})
parent = parentEl?.parent || 'root'
}
@@ -431,9 +435,13 @@ defineExpose({
<template v-else-if="el.section === 'Bases' || el.icon === 'project'">
<GeneralBaseIconColorPicker
:key="el.iconColor"
:key="`${el.iconColor}-${el.managed_app_id}-${el.managed_app_master}`"
:model-value="el.iconColor"
type="database"
:managed-app="{
managed_app_master: el.managed_app_master,
managed_app_id: el.managed_app_id,
}"
readonly
class="cmdk-action-icon !w-5"
>
@@ -544,10 +552,14 @@ defineExpose({
/>
<template v-else-if="item.data.section === 'Bases' || item.data.icon === 'project'">
<GeneralBaseIconColorPicker
:key="item.data.iconColor"
:key="`${item.data.iconColor}-${item.data.managed_app_id}-${item.data.managed_app_master}`"
:model-value="item.data.iconColor"
type="database"
readonly
:managed-app="{
managed_app_master: item.data.managed_app_master,
managed_app_id: item.data.managed_app_id,
}"
>
</GeneralBaseIconColorPicker>
</template>

View File

@@ -219,7 +219,14 @@ onMounted(() => {
</div>
<div class="flex w-1/2 justify-end text-nc-content-gray-subtle2">
<div class="flex gap-2 px-2 py-1 rounded-md items-center">
<component :is="iconMap.project" class="w-4 h-4" />
<GeneralProjectIcon
:managed-app="{
managed_app_master: cmdOption?.managed_app_master,
managed_app_id: cmdOption?.managed_app_id,
}"
:color="cmdOption?.iconColor"
class="!h-4 !w-4"
/>
<a-tooltip overlay-class-name="!px-2 !py-1 !rounded-lg" :tooltip-style="{ zIndex: 1100 }">
<template #title>
{{ cmdOption.baseName }}

View File

@@ -52,7 +52,15 @@ const onDelete = async () => {
v-if="base"
class="flex flex-row items-center py-2 px-2.25 bg-nc-bg-gray-extralight rounded-lg text-nc-content-gray-subtle"
>
<GeneralProjectIcon :color="parseProp(base.meta).iconColor" :type="base.type" class="nc-view-icon w-6 h-6 mx-1" />
<GeneralProjectIcon
:color="parseProp(base.meta).iconColor"
:type="base.type"
:managed-app="{
managed_app_master: base.managed_app_master,
managed_app_id: base.managed_app_id,
}"
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' }"

View File

@@ -394,7 +394,14 @@ onMounted(() => {
>
<template v-if="!!targetBase">
<div class="flex-1 capitalize truncate flex gap-1">
<GeneralProjectIcon :color="parseProp(targetBase?.meta ?? {}).iconColor" size="small" />
<GeneralProjectIcon
:color="parseProp(targetBase?.meta ?? {}).iconColor"
:managed-app="{
managed_app_master: targetBase?.managed_app_master,
managed_app_id: targetBase?.managed_app_id,
}"
size="small"
/>
{{ targetBase?.title }}
</div>
</template>
@@ -439,7 +446,14 @@ onMounted(() => {
</template>
<template #listItemExtraLeft="{ option: optionItem }">
<GeneralProjectIcon :color="parseProp(optionItem.meta).iconColor" size="small" />
<GeneralProjectIcon
:color="parseProp(optionItem.meta).iconColor"
:managed-app="{
managed_app_master: optionItem.managed_app_master,
managed_app_id: optionItem.managed_app_id,
}"
size="small"
/>
</template>
<template #listItemExtraRight="{ option: optionItem }">
<div v-if="activeBase?.id === optionItem.id" class="text-nc-content-gray-muted leading-4.5 text-xs">

View File

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

View File

@@ -7,6 +7,10 @@ const props = withDefaults(
size?: 'xsmall' | 'small' | 'medium' | 'large' | 'xlarge'
readonly?: boolean
iconClass?: string
managedApp?: {
managed_app_master?: boolean
managed_app_id?: string
}
}>(),
{
size: 'small',
@@ -16,13 +20,17 @@ const props = withDefaults(
const emit = defineEmits(['update:modelValue'])
const { modelValue, readonly } = toRefs(props)
const { modelValue, readonly, managedApp } = toRefs(props)
const { size } = props
const isOpen = ref(false)
const colorRef = ref(tinycolor(modelValue.value).isValid() ? modelValue.value : baseIconColors[0])
const isMasterManagedApp = computed(() => {
return isEeUI && !!managedApp.value?.managed_app_id && !!managedApp.value?.managed_app_master
})
const updateIconColor = (color: string) => {
const tcolor = tinycolor(color)
if (tcolor.isValid()) {
@@ -31,7 +39,7 @@ const updateIconColor = (color: string) => {
}
const onClick = (e: Event) => {
if (readonly.value) return
if (readonly.value || isMasterManagedApp.value) return
e.stopPropagation()
@@ -56,13 +64,13 @@ watch(
<a-dropdown
v-model:visible="isOpen"
:trigger="['click']"
:disabled="readonly"
:disabled="readonly || isMasterManagedApp"
overlay-class-name="nc-base-icon-color-picker-dropdown overflow-hidden max-w-[342px] relative"
>
<div
class="flex flex-row justify-center items-center select-none rounded nc-base-icon-picker-trigger"
:class="{
'hover:bg-gray-500 dark:hover:bg-nc-bg-gray-dark hover:bg-opacity-15 cursor-pointer': !readonly,
'hover:bg-gray-500 dark:hover:bg-nc-bg-gray-dark hover:bg-opacity-15 cursor-pointer': !readonly && !isMasterManagedApp,
'bg-gray-500 dark:bg-nc-bg-gray-dark bg-opacity-15': isOpen,
'h-5 w-5 text-base': size === 'xsmall',
'h-6 w-6 text-lg': size === 'small',
@@ -73,14 +81,17 @@ watch(
@click="onClick"
>
<NcTooltip placement="topLeft" :disabled="readonly || isOpen">
<template #title> {{ $t('tooltip.changeIconColour') }} </template>
<template #title>
{{
isMasterManagedApp ? $t('tooltip.changeIconColorNotSupportedForManagedMasterApp') : $t('tooltip.changeIconColour')
}}
</template>
<div>
<GeneralProjectIcon :color="colorRef" :class="iconClass" />
<div class="flex items-center">
<GeneralProjectIcon :color="colorRef" :class="iconClass" :managed-app="managedApp" />
</div>
</NcTooltip>
</div>
<template #overlay>
<div class="flex justify-start">
<GeneralColorPicker

View File

@@ -5,28 +5,51 @@ const props = withDefaults(
defineProps<{
hoverable?: boolean
color?: string
managedApp?: {
managed_app_master?: boolean
managed_app_id?: string
}
}>(),
{
color: baseIconColors[0],
managedApp: () => ({}),
},
)
const { color } = toRefs(props)
const { color, managedApp } = toRefs(props)
const managedAppInfo = computed(() => {
return {
isManagedApp: isEeUI && !!managedApp.value?.managed_app_id,
isMaster: !!managedApp.value?.managed_app_master && !!managedApp.value?.managed_app_id,
}
})
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(),
shade: tinycolor(color.value)
.darken(managedAppInfo.value.isManagedApp ? 30 : 40)
.toHexString(),
}
: {
tint: baseIconColors[0],
shade: tinycolor(baseIconColors[0]).darken(40).toHexString(),
shade: tinycolor(baseIconColors[0])
.darken(managedAppInfo.value.isManagedApp ? 30 : 40)
.toHexString(),
}
})
// Unique gradient ID based on app ID and color to avoid SVG gradient conflicts
const gradientId = computed(() => {
const colorHash = color.value?.replace('#', '') || 'default'
return `sphere-${managedApp.value?.managed_app_id || 'default'}-${colorHash}`
})
</script>
<template>
<svg
v-if="!managedAppInfo.isManagedApp"
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
@@ -46,11 +69,43 @@ const iconColor = computed(() => {
:fill="iconColor.tint"
/>
</svg>
<!-- Master managed app - keep original icon -->
<GeneralIcon
v-else-if="managedAppInfo.isMaster"
icon="ncBox"
class="h-4.5 w-4.5 nc-base-icon text-nc-content-gray-subtle2"
:class="{
'nc-base-icon-hoverable': hoverable,
}"
/>
<!-- 3D Sphere icon for installed managed apps -->
<svg
v-else
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
class="nc-base-icon"
:class="{
'nc-base-icon-hoverable': hoverable,
}"
>
<defs>
<!-- Radial gradient for 3D sphere effect - light from top-left -->
<radialGradient :id="gradientId" cx="30%" cy="30%" r="70%" fx="25%" fy="25%">
<stop offset="0%" :stop-color="iconColor.tint" />
<stop offset="100%" :stop-color="iconColor.shade" />
</radialGradient>
</defs>
<!-- Main sphere -->
<circle cx="10" cy="10" r="8" :fill="`url(#${gradientId})`" />
</svg>
</template>
<style scoped>
.nc-base-icon {
@apply text-xl;
@apply flex-none text-xl;
}
.nc-base-icon-hoverable {
@apply cursor-pointer !hover:bg-nc-bg-gray-medium !hover:bg-opacity-50;

View File

@@ -141,7 +141,14 @@ defineExpose({
<NcListDropdown v-model:is-open="isOpenBaseSelectDropdown" :disabled="disabled" :has-error="!!selectedBase?.ncItemDisabled">
<div class="flex-1 flex items-center gap-2 min-w-0">
<div v-if="selectedBase" class="min-w-5 flex items-center justify-center">
<GeneralProjectIcon :color="parseProp(selectedBase.meta).iconColor" size="small" />
<GeneralProjectIcon
:color="parseProp(selectedBase.meta).iconColor"
:managed-app="{
managed_app_master: selectedBase.managed_app_master,
managed_app_id: selectedBase.managed_app_id,
}"
size="small"
/>
</div>
<NcTooltip hide-on-click class="flex-1 truncate" show-on-truncate-only>
<span
@@ -178,7 +185,14 @@ defineExpose({
>
<template #listItemExtraLeft="{ option }">
<div class="min-w-5 flex items-center justify-center">
<GeneralProjectIcon :color="parseProp(option.meta).iconColor" size="small" />
<GeneralProjectIcon
:color="parseProp(option.meta).iconColor"
:managed-app="{
managed_app_master: option.managed_app_master,
managed_app_id: option.managed_app_id,
}"
size="small"
/>
</div>
</template>
</NcList>

View File

@@ -24,6 +24,8 @@ const baseOptions = computed(() => {
label: base.title,
value: base.id,
meta: base.meta,
managed_app_master: base.managed_app_master,
managed_app_id: base.managed_app_id,
}))
})
</script>
@@ -41,7 +43,14 @@ const baseOptions = computed(() => {
<a-select-option v-for="option of baseOptions" :key="option.value" :value="option.value" :data-label="option.label">
<div class="w-full flex gap-2 items-center" :data-testid="option.value">
<div class="min-w-5 flex items-center justify-center">
<GeneralProjectIcon :color="parseProp(option.meta).iconColor" size="small" />
<GeneralProjectIcon
:color="parseProp(option.meta).iconColor"
:managed-app="{
managed_app_master: option.managed_app_master,
managed_app_id: option.managed_app_id,
}"
size="small"
/>
</div>
<NcTooltip class="flex-1 truncate min-w-0" show-on-truncate-only>
<template #title>

View File

@@ -569,7 +569,13 @@ onBeforeUnmount(() => {
<NcPageHeader>
<template #icon>
<div class="nc-page-header-icon flex justify-center items-center h-5 w-5">
<GeneralBaseIconColorPicker readonly />
<GeneralBaseIconColorPicker
:managed-app="{
managed_app_master: currentBase?.managed_app_master,
managed_app_id: currentBase?.managed_app_id,
}"
readonly
/>
</div>
</template>
<template #title>

View File

@@ -208,6 +208,10 @@ onMounted(() => {
<GeneralProjectIcon
:color="parseProp(currentBase?.meta).iconColor"
:type="currentBase?.type"
:managed-app="{
managed_app_master: currentBase?.managed_app_master,
managed_app_id: currentBase?.managed_app_id,
}"
class="h-6 w-6 md:(h-4 w-4) flex-none"
/>
<NcTooltip

View File

@@ -224,10 +224,6 @@ const refViews = computed(() => {
return (views || []).filter((v) => v.type !== ViewTypes.FORM)
})
const filterOption = (value: string, option: { key: string }) => {
return option.key.toLowerCase().includes(value.toLowerCase())
}
const isLinks = computed(() => vModel.value.uidt === UITypes.Links && vModel.value.type !== RelationTypes.ONE_TO_ONE)
watch(
@@ -529,7 +525,7 @@ const handleScrollIntoView = () => {
v-model:value="referenceBaseId"
show-search
:disabled="isEdit"
:filter-option="filterOption"
:filter-option="(input, option) => antSelectFilterOption(input, option, ['data-label'])"
placeholder="Select base"
dropdown-class-name="nc-dropdown-ltar-child-table"
@change="onBaseChange(referenceBaseId)"
@@ -539,7 +535,8 @@ const handleScrollIntoView = () => {
</template>
<a-select-option
v-for="base of basesList"
:key="base.title"
:key="base.id"
:data-label="base.title"
:disabled="!canCreateCrossBaseLink(base)"
:value="base.id"
>
@@ -549,7 +546,15 @@ const handleScrollIntoView = () => {
</template>
<div class="flex w-full items-center gap-2">
<div class="min-w-5 flex items-center justify-center">
<GeneralProjectIcon :color="parseProp(base.meta).iconColor" :type="base.type" class="nc-project-icon" />
<GeneralProjectIcon
:color="parseProp(base.meta).iconColor"
:type="base.type"
:managed-app="{
managed_app_master: base.managed_app_master,
managed_app_id: base.managed_app_id,
}"
class="nc-project-icon"
/>
</div>
<NcTooltip class="flex-1 truncate" show-on-truncate-only>
<template #title>{{ base.title }}</template>
@@ -573,7 +578,7 @@ const handleScrollIntoView = () => {
v-model:value="referenceTableChildId"
show-search
:disabled="isEdit || isLinkedTablePrivate"
:filter-option="filterOption"
:filter-option="(input, option) => antSelectFilterOption(input, option, ['data-label'])"
placeholder="select table to link"
dropdown-class-name="nc-dropdown-ltar-child-table"
@change="handleUpdateRefTable"
@@ -583,7 +588,8 @@ const handleScrollIntoView = () => {
</template>
<a-select-option
v-for="table of refTables"
:key="table.title"
:key="table.id"
:data-label="table.title"
:value="table.id"
:disabled="(table as any).is_private"
>
@@ -661,10 +667,16 @@ const handleScrollIntoView = () => {
:placeholder="$t('labels.selectView')"
show-search
:disabled="isLinkedViewPrivate"
:filter-option="filterOption"
:filter-option="(input, option) => antSelectFilterOption(input, option, ['data-label'])"
dropdown-class-name="nc-dropdown-ltar-child-view"
>
<a-select-option v-for="view of refViews" :key="view.title" :value="view.id" :disabled="(view as any).is_private">
<a-select-option
v-for="view of refViews"
:key="view.id"
:value="view.id"
:data-label="view.title"
:disabled="(view as any).is_private"
>
<div class="flex w-full items-center gap-2">
<div class="min-w-5 flex items-center justify-center">
<GeneralViewIcon

View File

@@ -83,7 +83,15 @@ const viewModeInfo = computed(() => {
</span>
</template>
<GeneralProjectIcon :type="base?.type" :color="parseProp(base.meta).iconColor" class="!grayscale min-w-4" />
<GeneralProjectIcon
:type="base?.type"
:color="parseProp(base.meta).iconColor"
:managed-app="{
managed_app_master: base?.managed_app_master,
managed_app_id: base?.managed_app_id,
}"
class="!grayscale min-w-4"
/>
</NcTooltip>
<template v-if="isSharedBase">
<NcTooltip

View File

@@ -59,7 +59,16 @@ const handleNavigateToProject = async (base: NcProject) => {
@change="handleNavigateToProject"
>
<template #listItem="{ option }">
<GeneralBaseIconColorPicker :type="option?.type" :model-value="parseProp(option.meta).iconColor" size="xsmall" readonly>
<GeneralBaseIconColorPicker
:type="option?.type"
:model-value="parseProp(option.meta).iconColor"
:managed-app="{
managed_app_master: option?.managed_app_master,
managed_app_id: option?.managed_app_id,
}"
size="xsmall"
readonly
>
</GeneralBaseIconColorPicker>
<NcTooltip class="truncate flex-1" show-on-truncate-only>
<template #title>

View File

@@ -69,6 +69,10 @@ onMounted(() => {
<GeneralBaseIconColorPicker
:type="option?.type"
:model-value="parseProp(option.meta).iconColor"
:managed-app="{
managed_app_master: option?.managed_app_master,
managed_app_id: option?.managed_app_id,
}"
size="xsmall"
readonly
>

View File

@@ -2128,6 +2128,7 @@
"clientCert": "Select .cert file",
"clientCA": "Select CA file",
"changeIconColour": "Change icon colour",
"changeIconColorNotSupportedForManagedMasterApp": "Change icon color is not supported for managed master app",
"preFillFormInfo": "To get a prefilled link, make sure youve filled the necessary fields in the form view builder.",
"surveyFormInfo": "Form mode with one field per page",
"useFieldEditMenuToConfigFieldType": "Use field edit menu for type conversions after file is imported",

View File

@@ -1,5 +1,10 @@
<script lang="ts" setup>
const color1 = ref('')
const managedApp = ref({
managed_app_master: false,
managed_app_id: 'prr1pr4xx9vqn5c',
})
</script>
<template>
@@ -8,8 +13,17 @@ const color1 = ref('')
<h4>Simple</h4>
Selected color: {{ color1 }}
<div class="flex items-center gap-2">
<NcSwitch v-model:checked="managedApp.managed_app_master"> Toggle Managed App state </NcSwitch>
</div>
<div class="inline-block min-h-[24px] min-w-[24px] h-[24px] w-[24px] rounded-md" :class="[`bg-${color1}`]"></div>
<GeneralBaseIconColorPicker :model-value="color1" type="database" @update:model-value="color1 = $event">
<GeneralBaseIconColorPicker
:model-value="color1"
type="database"
@update:model-value="color1 = $event"
:managed-app="managedApp"
>
</GeneralBaseIconColorPicker>
</a-card>
</div>

View File

@@ -1264,7 +1264,7 @@ export const useViewsStore = defineStore('viewsStore', () => {
const tableName = tablesStore.baseTables.get(view.base_id)?.find((t) => t.id === view.fk_model_id)?.title
const baseName = bases.basesList.find((p) => p.id === view.base_id)?.title
const base = bases.basesList.find((p) => p.id === view.base_id)
allRecentViews.value = [
{
viewId: view.id,
@@ -1274,7 +1274,10 @@ export const useViewsStore = defineStore('viewsStore', () => {
viewType: view.type,
workspaceId: activeWorkspaceId.value,
tableName: tableName as string,
baseName: baseName as string,
baseName: base?.title as string,
managed_app_master: base?.managed_app_master,
managed_app_id: base?.managed_app_id,
iconColor: parseProp(base?.meta).iconColor,
},
...allRecentViews.value.filter((f) => f.viewId !== view.id || f.tableID !== view.fk_model_id),
]