Browse Source

Merge pull request #6604 from nocodb/nc-fix/kanban

feat(nc-gui): update kanban card design
pull/6621/head
Raju Udava 11 months ago committed by GitHub
parent
commit
67274fbd44
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      packages/nc-gui/components/cell/attachment/index.vue
  2. 180
      packages/nc-gui/components/smartsheet/Kanban.vue
  3. 17
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  4. 7
      packages/nc-gui/components/smartsheet/toolbar/ViewInfo.vue
  5. 17
      packages/nc-gui/composables/useExpandedFormStore.ts
  6. 2
      packages/nocodb/src/db/BaseModelSqlv2.ts
  7. 13
      packages/nocodb/src/helpers/getAst.ts

5
packages/nc-gui/components/cell/attachment/index.vue

@ -8,6 +8,7 @@ import {
DropZoneRef, DropZoneRef,
IsExpandedFormOpenInj, IsExpandedFormOpenInj,
IsGalleryInj, IsGalleryInj,
IsKanbanInj,
RowHeightInj, RowHeightInj,
iconMap, iconMap,
inject, inject,
@ -46,6 +47,8 @@ const isLockedMode = inject(IsLockedInj, ref(false))
const isGallery = inject(IsGalleryInj, ref(false)) const isGallery = inject(IsGalleryInj, ref(false))
const isKanban = inject(IsKanbanInj, ref(false))
const isExpandedForm = inject(IsExpandedFormOpenInj, ref(false)) const isExpandedForm = inject(IsExpandedFormOpenInj, ref(false))
const { isSharedForm } = useSmartsheetStoreOrThrow()! const { isSharedForm } = useSmartsheetStoreOrThrow()!
@ -223,7 +226,7 @@ const onExpand = () => {
<template v-if="visibleItems.length"> <template v-if="visibleItems.length">
<div <div
ref="sortableRef" ref="sortableRef"
:class="{ 'justify-center': !isExpandedForm && !isGallery }" :class="{ 'justify-center': !isExpandedForm && !isGallery && !isKanban }"
class="flex cursor-pointer w-full items-center flex-wrap gap-2 py-1.5 scrollbar-thin-dull overflow-hidden mt-0 items-start" class="flex cursor-pointer w-full items-center flex-wrap gap-2 py-1.5 scrollbar-thin-dull overflow-hidden mt-0 items-start"
:style="{ :style="{
maxHeight: isForm || isExpandedForm ? undefined : `max(${(rowHeight || 1) * 1.8}rem, 41px)`, maxHeight: isForm || isExpandedForm ? undefined : `max(${(rowHeight || 1) * 1.8}rem, 41px)`,

180
packages/nc-gui/components/smartsheet/Kanban.vue

@ -56,6 +56,8 @@ const expandedFormRow = ref<RowType>()
const expandedFormRowState = ref<Record<string, any>>() const expandedFormRowState = ref<Record<string, any>>()
provide(RowHeightInj, ref(1 as const))
const deleteStackVModel = ref(false) const deleteStackVModel = ref(false)
const stackToBeDeleted = ref('') const stackToBeDeleted = ref('')
@ -107,7 +109,9 @@ const hasEditPermission = computed(() => isUIAllowed('dataEdit'))
const fields = inject(FieldsInj, ref([])) const fields = inject(FieldsInj, ref([]))
const fieldsWithoutCover = computed(() => fields.value.filter((f) => f.id !== kanbanMetaData.value?.fk_cover_image_col_id)) const fieldsWithoutDisplay = computed(() => fields.value.filter((f) => !isPrimary(f)))
const displayField = computed(() => meta.value?.columns?.find((c) => c.pv && fields.value.includes(c)) ?? null)
const coverImageColumn: any = computed(() => const coverImageColumn: any = computed(() =>
meta.value?.columnsById meta.value?.columnsById
@ -209,6 +213,8 @@ const expandedFormOnRowIdDlg = computed({
}) })
const expandFormClick = async (e: MouseEvent, row: RowType) => { const expandFormClick = async (e: MouseEvent, row: RowType) => {
const target = e.target as HTMLElement
if (target.closest('.arrow') || target.closest('.slick-dots')) return
if (e.target as HTMLElement) { if (e.target as HTMLElement) {
expandForm(row) expandForm(row)
} }
@ -380,6 +386,11 @@ watch(
immediate: true, immediate: true,
}, },
) )
const getRowId = (row: RowType) => {
const pk = extractPkFromRow(row.row, meta.value!.columns!)
return pk ? `row-${pk}` : ''
}
</script> </script>
<template> <template>
@ -425,7 +436,7 @@ watch(
<!-- Non Collapsed Stacks --> <!-- Non Collapsed Stacks -->
<a-card <a-card
v-if="!stack.collapsed" v-if="!stack.collapsed"
:key="stack.id" :key="`${stack.id}-${stackIdx}`"
class="mx-4 !bg-gray-100 flex flex-col w-80 h-full !rounded-xl overflow-y-hidden" class="mx-4 !bg-gray-100 flex flex-col w-80 h-full !rounded-xl overflow-y-hidden"
:class="{ :class="{
'not-draggable': stack.title === null || isLocked || isPublic || !hasEditPermission, 'not-draggable': stack.title === null || isLocked || isPublic || !hasEditPermission,
@ -516,95 +527,123 @@ watch(
@end="(e) => e.target.classList.remove('grabbing')" @end="(e) => e.target.classList.remove('grabbing')"
@change="onMove($event, stack.title)" @change="onMove($event, stack.title)"
> >
<template #item="{ element: record }"> <template #item="{ element: record, index }">
<div class="nc-kanban-item py-2 pl-3 pr-2"> <div class="nc-kanban-item py-2 pl-3 pr-2">
<LazySmartsheetRow :row="record"> <LazySmartsheetRow :row="record">
<a-card <a-card
hoverable :key="`${getRowId(record)}-${index}`"
class="!rounded-lg h-full border-gray-200 border-1 group overflow-hidden break-all max-w-[450px] shadow-sm hover:shadow-md cursor-pointer"
:body-style="{ padding: '0px' }"
:data-stack="stack.title" :data-stack="stack.title"
class="!rounded-xl h-full overflow-hidden break-all max-w-[450px]" :data-testid="`nc-gallery-card-${record.row.id}`"
:class="{ :class="{
'not-draggable': isLocked || !hasEditPermission || isPublic, 'not-draggable': isLocked || !hasEditPermission || isPublic,
'!cursor-default': isLocked || !hasEditPermission || isPublic, '!cursor-default': isLocked || !hasEditPermission || isPublic,
}" }"
:body-style="{ padding: '10px' }"
@click="expandFormClick($event, record)" @click="expandFormClick($event, record)"
@contextmenu="showContextMenu($event, record)" @contextmenu="showContextMenu($event, record)"
> >
<template v-if="kanbanMetaData?.fk_cover_image_col_id" #cover> <template v-if="kanbanMetaData?.fk_cover_image_col_id" #cover>
<a-carousel <template v-if="!reloadAttachments && attachments(record).length">
v-if="!reloadAttachments && attachments(record).length" <a-carousel
autoplay :key="attachments(record).reduce((acc, curr) => acc + curr?.path, '')"
class="gallery-carousel" class="gallery-carousel !border-b-1 !border-gray-200"
arrows >
> <template #customPaging>
<template #customPaging> <a>
<a> <div class="pt-[12px]">
<div class="pt-[12px]"> <div></div>
<div></div> </div>
</a>
</template>
<template #prevArrow>
<div class="z-10 arrow">
<MdiChevronLeft
class="text-gray-700 w-6 h-6 absolute left-1.5 bottom-[-90px] !opacity-0 !group-hover:opacity-100 !bg-white border-1 border-gray-200 rounded-md transition"
/>
</div> </div>
</a> </template>
</template>
<template #prevArrow>
<div style="z-index: 1"></div>
</template>
<template #nextArrow>
<div style="z-index: 1"></div>
</template>
<template v-for="(attachment, index) in attachments(record)">
<LazyCellAttachmentImage
v-if="isImage(attachment.title, attachment.mimetype ?? attachment.type)"
:key="`carousel-${record.row.id}-${index}`"
class="h-52 object-cover"
:srcs="getPossibleAttachmentSrc(attachment)"
/>
</template>
</a-carousel>
<component :is="iconMap.imagePlaceholder" v-else class="w-full h-48 my-4 text-cool-gray-200" /> <template #nextArrow>
<div class="z-10 arrow">
<MdiChevronRight
class="text-gray-700 w-6 h-6 absolute right-1.5 bottom-[-90px] !opacity-0 !group-hover:opacity-100 !bg-white border-1 border-gray-200 rounded-md transition"
/>
</div>
</template>
<template v-for="(attachment, index) in attachments(record)">
<LazyCellAttachmentImage
v-if="isImage(attachment.title, attachment.mimetype ?? attachment.type)"
:key="attachment.path"
class="h-52 object-cover"
:srcs="getPossibleAttachmentSrc(attachment)"
/>
</template>
</a-carousel>
</template>
<div
v-else
class="h-52 w-full !flex flex-row !border-b-1 !border-gray-200 items-center justify-center"
>
<img class="object-contain w-[48px] h-[48px]" src="~assets/icons/FileIconImageBox.png" />
</div>
</template> </template>
<div <h2 v-if="displayField" class="text-base mt-3 mx-3 font-bold">
v-for="col in fieldsWithoutCover" <LazySmartsheetVirtualCell
:key="`record-${record.row.id}-${col.id}`" v-if="isVirtualCol(displayField)"
class="flex flex-col rounded-lg w-full" v-model="record.row[displayField.title]"
> class="!text-gray-600"
<div v-if="!isRowEmpty(record, col) || isLTAR(col.uidt, col.colOptions)"> :column="displayField"
<!-- Smartsheet Header (Virtual) Cell --> :row="record"
<div class="flex flex-row w-full justify-start pt-2"> />
<div class="w-full text-gray-400">
<LazySmartsheetCell
v-else
v-model="record.row[displayField.title]"
class="!text-gray-600"
:column="displayField"
:edit-enabled="false"
:read-only="true"
/>
</h2>
<div v-for="col in fieldsWithoutDisplay" :key="`record-${record.row.id}-${col.id}`">
<div class="flex flex-col first:mt-3 ml-2 !pr-3.5 !mb-[0.75rem] rounded-lg w-full">
<div class="flex flex-row w-full justify-start scale-75">
<div class="w-full pb-1 text-gray-300">
<LazySmartsheetHeaderVirtualCell <LazySmartsheetHeaderVirtualCell
v-if="isVirtualCol(col)" v-if="isVirtualCol(col)"
:column="col" :column="col"
:hide-menu="true" :hide-menu="true"
:hide-icon="true"
/> />
<LazySmartsheetHeaderCell v-else :column="col" :hide-menu="true" />
<LazySmartsheetHeaderCell v-else :column="col" :hide-menu="true" :hide-icon="true" />
</div> </div>
</div> </div>
<!-- Smartsheet (Virtual) Cell -->
<div <div
class="flex flex-row w-full items-center justify-start" v-if="!isRowEmpty(record, col)"
:class="{ '!ml-[-12px] pl-3': col.uidt === UITypes.SingleSelect }" class="flex flex-row w-full text-gray-700 px-1 mt-[-0.25rem] items-center justify-start"
> >
<LazySmartsheetVirtualCell <LazySmartsheetVirtualCell
v-if="col.title && isVirtualCol(col)" v-if="isVirtualCol(col)"
v-model="record.row[col.title]" v-model="record.row[col.title]"
class="text-sm pt-1 pl-5"
:column="col" :column="col"
:row="record" :row="record"
/> />
<LazySmartsheetCell <LazySmartsheetCell
v-else-if="col.title" v-else
v-model="record.row[col.title]" v-model="record.row[col.title]"
class="text-sm pt-1 pl-7.25"
:column="col" :column="col"
:edit-enabled="false" :edit-enabled="false"
:read-only="true" :read-only="true"
/> />
</div> </div>
<div v-else class="flex flex-row w-full h-[1.375rem] pl-1 items-center justify-start">-</div>
</div> </div>
</div> </div>
</a-card> </a-card>
@ -755,4 +794,37 @@ watch(
transform-origin: left top 0px; transform-origin: left top 0px;
transition: left 0.2s ease-in-out 0s; transition: left 0.2s ease-in-out 0s;
} }
:deep(.slick-dots li button) {
@apply !bg-black;
}
.ant-carousel.gallery-carousel :deep(.slick-dots) {
@apply !w-auto absolute h-auto bottom-[-15px] absolute h-auto;
height: auto;
}
.ant-carousel.gallery-carousel :deep(.slick-dots li div > div) {
@apply rounded-full border-0 cursor-pointer block opacity-100 p-0 outline-none transition-all duration-500 text-transparent h-2 w-2 bg-[#d9d9d9];
font-size: 0;
}
.ant-carousel.gallery-carousel :deep(.slick-dots li.slick-active div > div) {
@apply bg-brand-500 opacity-100;
}
.ant-carousel.gallery-carousel :deep(.slick-dots li) {
@apply !w-auto;
}
.ant-carousel.gallery-carousel :deep(.slick-prev) {
@apply left-0;
}
.ant-carousel.gallery-carousel :deep(.slick-next) {
@apply right-0;
}
:deep(.slick-slide) {
@apply !pointer-events-none;
}
</style> </style>

17
packages/nc-gui/components/smartsheet/expanded-form/index.vue

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { TableType, ViewType } from 'nocodb-sdk' import type { TableType, ViewType } from 'nocodb-sdk'
import { isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk' import { ViewTypes, isLinksOrLTAR, isSystemColumn, isVirtualCol } from 'nocodb-sdk'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import MdiChevronDown from '~icons/mdi/chevron-down' import MdiChevronDown from '~icons/mdi/chevron-down'
@ -47,6 +47,8 @@ const props = defineProps<Props>()
const emits = defineEmits(['update:modelValue', 'cancel', 'next', 'prev']) const emits = defineEmits(['update:modelValue', 'cancel', 'next', 'prev'])
const { activeView } = storeToRefs(useViewsStore())
const key = ref(0) const key = ref(0)
const wrapper = ref() const wrapper = ref()
@ -86,6 +88,8 @@ const { isUIAllowed } = useRoles()
const reloadTrigger = inject(ReloadRowDataHookInj, createEventHook()) const reloadTrigger = inject(ReloadRowDataHookInj, createEventHook())
const { addOrEditStackRow } = useKanbanViewStoreOrThrow()
// override cell click hook to avoid unexpected behavior at form fields // override cell click hook to avoid unexpected behavior at form fields
provide(CellClickHookInj, undefined) provide(CellClickHookInj, undefined)
@ -184,7 +188,16 @@ const save = async () => {
await syncLTARRefs(data) await syncLTARRefs(data)
reloadTrigger?.trigger() reloadTrigger?.trigger()
} else { } else {
await _save() let kanbanClbk
if (activeView.value?.type === ViewTypes.KANBAN) {
kanbanClbk = (row: any, isNewRow: boolean) => {
addOrEditStackRow(row, isNewRow)
}
}
await _save(undefined, undefined, {
kanbanClbk,
})
reloadTrigger?.trigger() reloadTrigger?.trigger()
} }
isUnsavedFormExist.value = false isUnsavedFormExist.value = false

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

@ -1,6 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import { ViewTypes } from 'nocodb-sdk'
const { isMobileMode } = useGlobal() const { isMobileMode } = useGlobal()
const { openedViewsTab, activeView } = storeToRefs(useViewsStore()) const { openedViewsTab, activeView } = storeToRefs(useViewsStore())
@ -16,9 +14,8 @@ const { isLeftSidebarOpen } = storeToRefs(useSidebarStore())
<div <div
class="ml-0.25 flex flex-row font-medium items-center border-gray-50 transition-all duration-100" class="ml-0.25 flex flex-row font-medium items-center border-gray-50 transition-all duration-100"
:class="{ :class="{
'min-w-36/100 max-w-36/100': !isMobileMode && activeView?.type !== ViewTypes.KANBAN && isLeftSidebarOpen, 'min-w-36/100 max-w-36/100': !isMobileMode && isLeftSidebarOpen,
'min-w-39/100 max-w-39/100': !isMobileMode && activeView?.type !== ViewTypes.KANBAN && !isLeftSidebarOpen, 'min-w-39/100 max-w-39/100': !isMobileMode && !isLeftSidebarOpen,
'min-w-1/4 max-w-1/4': !isMobileMode && activeView?.type === ViewTypes.KANBAN,
'w-2/3 text-base ml-1.5': isMobileMode, 'w-2/3 text-base ml-1.5': isMobileMode,
'!max-w-3/4': isSharedBase && !isMobileMode, '!max-w-3/4': isSharedBase && !isMobileMode,
}" }"

17
packages/nc-gui/composables/useExpandedFormStore.ts

@ -153,7 +153,16 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
$e('a:row-expand:comment') $e('a:row-expand:comment')
} }
const save = async (ltarState: Record<string, any> = {}, undo = false) => { const save = async (
ltarState: Record<string, any> = {},
undo = false,
// TODO: Hack. Remove this when kanban injection store issue is resolved
{
kanbanClbk,
}: {
kanbanClbk?: (row: Row, isNewRow: boolean) => void
} = {},
) => {
let data let data
try { try {
const isNewRow = row.value.rowMeta?.new ?? false const isNewRow = row.value.rowMeta?.new ?? false
@ -266,13 +275,13 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
} }
} }
if (activeView.value?.type === ViewTypes.KANBAN) { if (activeView.value?.type === ViewTypes.KANBAN && kanbanClbk) {
const { addOrEditStackRow } = useKanbanViewStoreOrThrow() kanbanClbk(row.value, isNewRow)
addOrEditStackRow(row.value, isNewRow)
} }
changedColumns.value = new Set() changedColumns.value = new Set()
} catch (e: any) { } catch (e: any) {
console.error(e)
message.error(`${t('msg.error.rowUpdateFailed')}: ${await extractSdkResponseErrorMsg(e)}`) message.error(`${t('msg.error.rowUpdateFailed')}: ${await extractSdkResponseErrorMsg(e)}`)
} }
$e('a:row-expand:add') $e('a:row-expand:add')

2
packages/nocodb/src/db/BaseModelSqlv2.ts

@ -3922,7 +3922,7 @@ class BaseModelSqlv2 {
const proto = await this.getProto(); const proto = await this.getProto();
const data = await groupedQb; const data: any[] = await this.execAndParse(groupedQb);
const result = data?.map((d) => { const result = data?.map((d) => {
d.__proto__ = proto; d.__proto__ = proto;
return d; return d;

13
packages/nocodb/src/helpers/getAst.ts

@ -1,11 +1,11 @@
import { isSystemColumn, RelationTypes, UITypes } from 'nocodb-sdk'; import { isSystemColumn, RelationTypes, UITypes, ViewTypes } from 'nocodb-sdk';
import type { import type {
Column, Column,
LinkToAnotherRecordColumn, LinkToAnotherRecordColumn,
LookupColumn, LookupColumn,
Model, Model,
} from '~/models'; } from '~/models';
import { GalleryView, View } from '~/models'; import { GalleryView, KanbanView, View } from '~/models';
const getAst = async ({ const getAst = async ({
query, query,
@ -33,11 +33,12 @@ const getAst = async ({
dependencyFields.fieldsSet = dependencyFields.fieldsSet || new Set(); dependencyFields.fieldsSet = dependencyFields.fieldsSet || new Set();
let coverImageId; let coverImageId;
if (view) { if (view && view.type === ViewTypes.GALLERY) {
const gallery = await GalleryView.get(view.id); const gallery = await GalleryView.get(view.id);
if (gallery) { coverImageId = gallery.fk_cover_image_col_id;
coverImageId = gallery.fk_cover_image_col_id; } else if (view && view.type === ViewTypes.KANBAN) {
} const kanban = await KanbanView.get(view.id);
coverImageId = kanban.fk_cover_image_col_id;
} }
if (!model.columns?.length) await model.getColumns(); if (!model.columns?.length) await model.getColumns();

Loading…
Cancel
Save