Browse Source

feat(gui): add icon change option for view

Signed-off-by: Pranav C <pranavxc@gmail.com>
pull/4630/head
Pranav C 2 years ago
parent
commit
aed58b8bfd
  1. 26
      packages/nc-gui/components/general/ViewIcon.vue
  2. 20
      packages/nc-gui/components/smartsheet/sidebar/MenuTop.vue
  3. 29
      packages/nc-gui/components/smartsheet/sidebar/RenameableMenuItem.vue
  4. 7
      packages/nc-gui/components/smartsheet/toolbar/ViewActions.vue
  5. 2
      packages/nocodb-sdk/src/lib/Api.ts
  6. 17
      packages/nocodb/src/lib/models/Model.ts
  7. 12
      packages/nocodb/src/lib/models/View.ts
  8. 33
      packages/nocodb/src/lib/utils/modelUtils.ts
  9. 4
      scripts/sdk/swagger.json

26
packages/nc-gui/components/general/ViewIcon.vue

@ -0,0 +1,26 @@
<script lang="ts" setup>
import { Icon as IcIcon } from '@iconify/vue'
import type { TableType } from 'nocodb-sdk'
import { viewIcons } from '#imports'
const { meta: viewMeta } = defineProps<{
meta: TableType
}>()
</script>
<template>
<IcIcon
v-if="viewMeta?.meta?.icon"
:data-testid="`nc-icon-${viewMeta?.meta?.icon}`"
class="text-[16px]"
:icon="viewMeta?.meta?.icon"
></IcIcon>
<component
:is="viewIcons[viewMeta.type]?.icon"
v-else
class="nc-view-icon group-hover"
:style="{ color: viewIcons[viewMeta.type]?.color }"
/>
</template>
<style scoped></style>

20
packages/nc-gui/components/smartsheet/sidebar/MenuTop.vue

@ -140,7 +140,7 @@ const initSortable = (el: HTMLElement) => {
if (sortable) sortable.destroy() if (sortable) sortable.destroy()
sortable = new Sortable(el, { sortable = new Sortable(el, {
handle: '.nc-drag-icon', // handle: '.nc-drag-icon',
ghostClass: 'ghost', ghostClass: 'ghost',
onStart: onSortStart, onStart: onSortStart,
onEnd: onSortEnd, onEnd: onSortEnd,
@ -213,6 +213,23 @@ function openDeleteDialog(view: ViewType) {
close(1000) close(1000)
} }
} }
const setIcon = async (icon: string, view: ViewType) => {
try {
view.meta = {
...(view.meta || {}),
icon,
}
api.dbView.update(view.id as string, {
meta: view.meta,
})
$e('a:view:icon:sidebar', { icon })
} catch (e) {
message.error(await extractSdkResponseErrorMsg(e))
}
}
</script> </script>
<template> <template>
@ -234,6 +251,7 @@ function openDeleteDialog(view: ViewType) {
@open-modal="$emit('openModal', $event)" @open-modal="$emit('openModal', $event)"
@delete="openDeleteDialog" @delete="openDeleteDialog"
@rename="onRename" @rename="onRename"
@select-icon="setIcon($event, view)"
/> />
</a-menu> </a-menu>
</template> </template>

29
packages/nc-gui/components/smartsheet/sidebar/RenameableMenuItem.vue

@ -3,7 +3,6 @@ import type { KanbanType, ViewType, ViewTypes } from 'nocodb-sdk'
import type { WritableComputedRef } from '@vue/reactivity' import type { WritableComputedRef } from '@vue/reactivity'
import { import {
IsLockedInj, IsLockedInj,
computed,
inject, inject,
message, message,
onKeyStroke, onKeyStroke,
@ -11,7 +10,6 @@ import {
useNuxtApp, useNuxtApp,
useUIPermission, useUIPermission,
useVModel, useVModel,
viewIcons,
} from '#imports' } from '#imports'
interface Props { interface Props {
@ -21,9 +19,15 @@ interface Props {
interface Emits { interface Emits {
(event: 'update:view', data: Record<string, any>): void (event: 'update:view', data: Record<string, any>): void
(event: 'selectIcon', icon: string): void
(event: 'changeView', view: Record<string, any>): void (event: 'changeView', view: Record<string, any>): void
(event: 'rename', view: ViewType): void (event: 'rename', view: ViewType): void
(event: 'delete', view: ViewType): void (event: 'delete', view: ViewType): void
(event: 'openModal', data: { type: ViewTypes; title?: string; copyViewId?: string; groupingFieldColumnId?: string }): void (event: 'openModal', data: { type: ViewTypes; title?: string; copyViewId?: string; groupingFieldColumnId?: string }): void
} }
@ -48,8 +52,6 @@ let isStopped = $ref(false)
/** Original view title when editing the view name */ /** Original view title when editing the view name */
let originalTitle = $ref<string | undefined>() let originalTitle = $ref<string | undefined>()
const viewType = computed(() => vModel.value.type as number)
/** Debounce click handler, so we can potentially enable editing view name {@see onDblClick} */ /** Debounce click handler, so we can potentially enable editing view name {@see onDblClick} */
const onClick = useDebounceFn(() => { const onClick = useDebounceFn(() => {
if (isEditing || isStopped) return if (isEditing || isStopped) return
@ -172,17 +174,14 @@ function onStopEdit() {
@click.stop="onClick" @click.stop="onClick"
> >
<div v-e="['a:view:open', { view: vModel.type }]" class="text-xs flex items-center w-full gap-2" data-testid="view-item"> <div v-e="['a:view:open', { view: vModel.type }]" class="text-xs flex items-center w-full gap-2" data-testid="view-item">
<div class="flex w-auto" :data-testid="`view-sidebar-drag-handle-${vModel.alias || vModel.title}`"> <div class="flex w-auto min-w-5" :data-testid="`view-sidebar-drag-handle-${vModel.alias || vModel.title}`">
<MdiDrag <a-dropdown :trigger="['click']" @click.stop>
class="nc-drag-icon hidden group-hover:block transition-opacity opacity-0 group-hover:opacity-100 text-gray-500 !cursor-move" <GeneralViewIcon :meta="props.view"></GeneralViewIcon>
@click.stop.prevent
/> <template v-if="isUIAllowed('viewIconCustomisation')" #overlay>
<GeneralEmojiIcons class="shadow bg-white p-2" @select-icon="emits('selectIcon', $event)" />
<component </template>
:is="viewIcons[viewType].icon" </a-dropdown>
class="nc-view-icon group-hover:hidden"
:style="{ color: viewIcons[viewType].color }"
/>
</div> </div>
<a-input v-if="isEditing" :ref="focusInput" v-model:value="vModel.title" @blur="onCancel" @keydown="onKeyDown($event)" /> <a-input v-if="isEditing" :ref="focusInput" v-model:value="vModel.title" @blur="onCancel" @keydown="onKeyDown($event)" />

7
packages/nc-gui/components/smartsheet/toolbar/ViewActions.vue

@ -4,7 +4,6 @@ import {
IsLockedInj, IsLockedInj,
IsPublicInj, IsPublicInj,
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
getViewIcon,
inject, inject,
message, message,
ref, ref,
@ -93,11 +92,7 @@ useMenuCloseOnEsc(open)
<a-dropdown v-model:visible="open" :trigger="['click']" overlay-class-name="nc-dropdown-actions-menu"> <a-dropdown v-model:visible="open" :trigger="['click']" overlay-class-name="nc-dropdown-actions-menu">
<a-button v-e="['c:actions']" class="nc-actions-menu-btn nc-toolbar-btn"> <a-button v-e="['c:actions']" class="nc-actions-menu-btn nc-toolbar-btn">
<div class="flex gap-2 items-center"> <div class="flex gap-2 items-center">
<component <GeneralViewIcon :meta="selectedView"></GeneralViewIcon>
:is="getViewIcon(selectedView?.type)?.icon"
class="nc-view-icon group-hover:hidden"
:style="{ color: getViewIcon(selectedView?.type)?.color }"
/>
<span class="!text-sm font-weight-normal"> <span class="!text-sm font-weight-normal">
<GeneralTruncateText>{{ selectedView?.title }}</GeneralTruncateText> <GeneralTruncateText>{{ selectedView?.title }}</GeneralTruncateText>

2
packages/nocodb-sdk/src/lib/Api.ts

@ -135,6 +135,7 @@ export interface ViewType {
fk_model_id?: string; fk_model_id?: string;
slug?: string; slug?: string;
uuid?: string; uuid?: string;
meta?: any;
show_system_fields?: boolean; show_system_fields?: boolean;
lock_type?: 'collaborative' | 'locked' | 'personal'; lock_type?: 'collaborative' | 'locked' | 'personal';
type?: number; type?: number;
@ -2316,6 +2317,7 @@ export class Api<
viewId: string, viewId: string,
data: { data: {
order?: number; order?: number;
meta?: any;
title?: string; title?: string;
show_system_fields?: boolean; show_system_fields?: boolean;
lock_type?: 'collaborative' | 'locked' | 'personal'; lock_type?: 'collaborative' | 'locked' | 'personal';

17
packages/nocodb/src/lib/models/Model.ts

@ -1,4 +1,5 @@
import Noco from '../Noco'; import Noco from '../Noco';
import { parseMetaProp } from '../utils/modelUtils'
import Column from './Column'; import Column from './Column';
import NocoCache from '../cache/NocoCache'; import NocoCache from '../cache/NocoCache';
import { XKnex } from '../db/sql-data-mapper'; import { XKnex } from '../db/sql-data-mapper';
@ -22,21 +23,7 @@ import { NcError } from '../meta/helpers/catchError';
import Audit from './Audit'; import Audit from './Audit';
import { sanitize } from '../db/sql-data-mapper/lib/sql/helpers/sanitize'; import { sanitize } from '../db/sql-data-mapper/lib/sql/helpers/sanitize';
function parseMetaProp(modelOrModelList: Model[] | Model) {
if (!modelOrModelList) return;
// parse meta property
for (const model of Array.isArray(modelOrModelList)
? modelOrModelList
: [modelOrModelList]) {
try {
model.meta =
typeof model.meta === 'string' ? JSON.parse(model.meta) : model.meta;
} catch {
model.meta = {};
}
}
}
export default class Model implements TableType { export default class Model implements TableType {
copy_enabled: boolean; copy_enabled: boolean;

12
packages/nocodb/src/lib/models/View.ts

@ -5,6 +5,7 @@ import {
CacheScope, CacheScope,
MetaTable, MetaTable,
} from '../utils/globals'; } from '../utils/globals';
import { parseMetaProp, stringifyMetaProp } from '../utils/modelUtils'
import Model from './Model'; import Model from './Model';
import FormView from './FormView'; import FormView from './FormView';
import GridView from './GridView'; import GridView from './GridView';
@ -118,6 +119,7 @@ export default class View implements ViewType {
)); ));
if (!view) { if (!view) {
view = await ncMeta.metaGet2(null, null, MetaTable.VIEWS, viewId); view = await ncMeta.metaGet2(null, null, MetaTable.VIEWS, viewId);
parseMetaProp(view);
await NocoCache.set(`${CacheScope.VIEW}:${view.id}`, view); await NocoCache.set(`${CacheScope.VIEW}:${view.id}`, view);
} }
@ -156,6 +158,7 @@ export default class View implements ViewType {
], ],
} }
); );
parseMetaProp(view);
// todo: cache - titleOrId can be viewId so we need a different scope here // todo: cache - titleOrId can be viewId so we need a different scope here
await NocoCache.set( await NocoCache.set(
`${CacheScope.VIEW}:${fk_model_id}:${titleOrId}`, `${CacheScope.VIEW}:${fk_model_id}:${titleOrId}`,
@ -188,6 +191,7 @@ export default class View implements ViewType {
}, },
null null
); );
parseMetaProp(view);
await NocoCache.set(`${CacheScope.VIEW}:${fk_model_id}:default`, view); await NocoCache.set(`${CacheScope.VIEW}:${fk_model_id}:default`, view);
} }
return view && new View(view); return view && new View(view);
@ -204,6 +208,7 @@ export default class View implements ViewType {
order: 'asc', order: 'asc',
}, },
}); });
parseMetaProp(viewsList);
await NocoCache.setList(CacheScope.VIEW, [modelId], viewsList); await NocoCache.setList(CacheScope.VIEW, [modelId], viewsList);
} }
viewsList.sort( viewsList.sort(
@ -254,8 +259,11 @@ export default class View implements ViewType {
base_id: view.base_id, base_id: view.base_id,
created_at: view.created_at, created_at: view.created_at,
updated_at: view.updated_at, updated_at: view.updated_at,
meta: view.meta ?? {},
}; };
stringifyMetaProp(insertObj);
// get project and base id if missing // get project and base id if missing
if (!(view.project_id && view.base_id)) { if (!(view.project_id && view.base_id)) {
const model = await Model.getByIdOrName({ id: view.fk_model_id }, ncMeta); const model = await Model.getByIdOrName({ id: view.fk_model_id }, ncMeta);
@ -838,7 +846,9 @@ export default class View implements ViewType {
'meta', 'meta',
'uuid', 'uuid',
]); ]);
updateObj.meta = JSON.stringify(updateObj.meta); // if meta data defined then stringify it
stringifyMetaProp(updateObj);
// get existing cache // get existing cache
const key = `${CacheScope.VIEW}:${viewId}`; const key = `${CacheScope.VIEW}:${viewId}`;
let o = await NocoCache.get(key, CacheGetType.TYPE_OBJECT); let o = await NocoCache.get(key, CacheGetType.TYPE_OBJECT);

33
packages/nocodb/src/lib/utils/modelUtils.ts

@ -0,0 +1,33 @@
export function parseMetaProp(modelOrModelList: { meta: any } | { meta: any }[]) {
if (!modelOrModelList) return
// parse meta property
for (const model of Array.isArray(modelOrModelList)
? modelOrModelList
: [modelOrModelList]) {
try {
model.meta =
typeof model.meta === 'string' ? JSON.parse(model.meta) : model.meta
} catch {
model.meta = {}
}
}
}
export function stringifyMetaProp(
modelOrModelList: { meta?: any } | { meta?: any }[],
) {
if (!modelOrModelList) return
// parse meta property
for (const model of Array.isArray(modelOrModelList)
? modelOrModelList
: [modelOrModelList]) {
try {
model.meta =
typeof model.meta !== 'string' ? model.meta : JSON.parse(model.meta)
} catch (e) {
model.meta = '{}'
}
}
}

4
scripts/sdk/swagger.json

@ -2194,6 +2194,8 @@
"order": { "order": {
"type": "number" "type": "number"
}, },
"meta": {
},
"title": { "title": {
"type": "string" "type": "string"
}, },
@ -7586,6 +7588,8 @@
"uuid": { "uuid": {
"type": "string" "type": "string"
}, },
"meta": {
},
"show_system_fields": { "show_system_fields": {
"type": "boolean" "type": "boolean"
}, },

Loading…
Cancel
Save