Browse Source

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

feat(nc-gui): update kanban card design
pull/6621/head
Raju Udava 1 year 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. 132
      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. 11
      packages/nocodb/src/helpers/getAst.ts

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

@ -8,6 +8,7 @@ import {
DropZoneRef,
IsExpandedFormOpenInj,
IsGalleryInj,
IsKanbanInj,
RowHeightInj,
iconMap,
inject,
@ -46,6 +47,8 @@ const isLockedMode = inject(IsLockedInj, ref(false))
const isGallery = inject(IsGalleryInj, ref(false))
const isKanban = inject(IsKanbanInj, ref(false))
const isExpandedForm = inject(IsExpandedFormOpenInj, ref(false))
const { isSharedForm } = useSmartsheetStoreOrThrow()!
@ -223,7 +226,7 @@ const onExpand = () => {
<template v-if="visibleItems.length">
<div
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"
:style="{
maxHeight: isForm || isExpandedForm ? undefined : `max(${(rowHeight || 1) * 1.8}rem, 41px)`,

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

@ -56,6 +56,8 @@ const expandedFormRow = ref<RowType>()
const expandedFormRowState = ref<Record<string, any>>()
provide(RowHeightInj, ref(1 as const))
const deleteStackVModel = ref(false)
const stackToBeDeleted = ref('')
@ -107,7 +109,9 @@ const hasEditPermission = computed(() => isUIAllowed('dataEdit'))
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(() =>
meta.value?.columnsById
@ -209,6 +213,8 @@ const expandedFormOnRowIdDlg = computed({
})
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) {
expandForm(row)
}
@ -380,6 +386,11 @@ watch(
immediate: true,
},
)
const getRowId = (row: RowType) => {
const pk = extractPkFromRow(row.row, meta.value!.columns!)
return pk ? `row-${pk}` : ''
}
</script>
<template>
@ -425,7 +436,7 @@ watch(
<!-- Non Collapsed Stacks -->
<a-card
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="{
'not-draggable': stack.title === null || isLocked || isPublic || !hasEditPermission,
@ -516,27 +527,27 @@ watch(
@end="(e) => e.target.classList.remove('grabbing')"
@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">
<LazySmartsheetRow :row="record">
<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"
class="!rounded-xl h-full overflow-hidden break-all max-w-[450px]"
:data-testid="`nc-gallery-card-${record.row.id}`"
:class="{
'not-draggable': isLocked || !hasEditPermission || isPublic,
'!cursor-default': isLocked || !hasEditPermission || isPublic,
}"
:body-style="{ padding: '10px' }"
@click="expandFormClick($event, record)"
@contextmenu="showContextMenu($event, record)"
>
<template v-if="kanbanMetaData?.fk_cover_image_col_id" #cover>
<template v-if="!reloadAttachments && attachments(record).length">
<a-carousel
v-if="!reloadAttachments && attachments(record).length"
autoplay
class="gallery-carousel"
arrows
:key="attachments(record).reduce((acc, curr) => acc + curr?.path, '')"
class="gallery-carousel !border-b-1 !border-gray-200"
>
<template #customPaging>
<a>
@ -547,64 +558,92 @@ watch(
</template>
<template #prevArrow>
<div style="z-index: 1"></div>
<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>
</template>
<template #nextArrow>
<div style="z-index: 1"></div>
<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="`carousel-${record.row.id}-${index}`"
:key="attachment.path"
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>
<div
v-for="col in fieldsWithoutCover"
:key="`record-${record.row.id}-${col.id}`"
class="flex flex-col rounded-lg w-full"
v-else
class="h-52 w-full !flex flex-row !border-b-1 !border-gray-200 items-center justify-center"
>
<div v-if="!isRowEmpty(record, col) || isLTAR(col.uidt, col.colOptions)">
<!-- Smartsheet Header (Virtual) Cell -->
<div class="flex flex-row w-full justify-start pt-2">
<div class="w-full text-gray-400">
<img class="object-contain w-[48px] h-[48px]" src="~assets/icons/FileIconImageBox.png" />
</div>
</template>
<h2 v-if="displayField" class="text-base mt-3 mx-3 font-bold">
<LazySmartsheetVirtualCell
v-if="isVirtualCol(displayField)"
v-model="record.row[displayField.title]"
class="!text-gray-600"
:column="displayField"
:row="record"
/>
<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
v-if="isVirtualCol(col)"
:column="col"
: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>
<!-- Smartsheet (Virtual) Cell -->
<div
class="flex flex-row w-full items-center justify-start"
:class="{ '!ml-[-12px] pl-3': col.uidt === UITypes.SingleSelect }"
v-if="!isRowEmpty(record, col)"
class="flex flex-row w-full text-gray-700 px-1 mt-[-0.25rem] items-center justify-start"
>
<LazySmartsheetVirtualCell
v-if="col.title && isVirtualCol(col)"
v-if="isVirtualCol(col)"
v-model="record.row[col.title]"
class="text-sm pt-1 pl-5"
:column="col"
:row="record"
/>
<LazySmartsheetCell
v-else-if="col.title"
v-else
v-model="record.row[col.title]"
class="text-sm pt-1 pl-7.25"
:column="col"
:edit-enabled="false"
:read-only="true"
/>
</div>
<div v-else class="flex flex-row w-full h-[1.375rem] pl-1 items-center justify-start">-</div>
</div>
</div>
</a-card>
@ -755,4 +794,37 @@ watch(
transform-origin: left top 0px;
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>

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

@ -1,6 +1,6 @@
<script setup lang="ts">
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 MdiChevronDown from '~icons/mdi/chevron-down'
@ -47,6 +47,8 @@ const props = defineProps<Props>()
const emits = defineEmits(['update:modelValue', 'cancel', 'next', 'prev'])
const { activeView } = storeToRefs(useViewsStore())
const key = ref(0)
const wrapper = ref()
@ -86,6 +88,8 @@ const { isUIAllowed } = useRoles()
const reloadTrigger = inject(ReloadRowDataHookInj, createEventHook())
const { addOrEditStackRow } = useKanbanViewStoreOrThrow()
// override cell click hook to avoid unexpected behavior at form fields
provide(CellClickHookInj, undefined)
@ -184,7 +188,16 @@ const save = async () => {
await syncLTARRefs(data)
reloadTrigger?.trigger()
} 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()
}
isUnsavedFormExist.value = false

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

@ -1,6 +1,4 @@
<script setup lang="ts">
import { ViewTypes } from 'nocodb-sdk'
const { isMobileMode } = useGlobal()
const { openedViewsTab, activeView } = storeToRefs(useViewsStore())
@ -16,9 +14,8 @@ const { isLeftSidebarOpen } = storeToRefs(useSidebarStore())
<div
class="ml-0.25 flex flex-row font-medium items-center border-gray-50 transition-all duration-100"
:class="{
'min-w-36/100 max-w-36/100': !isMobileMode && activeView?.type !== ViewTypes.KANBAN && isLeftSidebarOpen,
'min-w-39/100 max-w-39/100': !isMobileMode && activeView?.type !== ViewTypes.KANBAN && !isLeftSidebarOpen,
'min-w-1/4 max-w-1/4': !isMobileMode && activeView?.type === ViewTypes.KANBAN,
'min-w-36/100 max-w-36/100': !isMobileMode && isLeftSidebarOpen,
'min-w-39/100 max-w-39/100': !isMobileMode && !isLeftSidebarOpen,
'w-2/3 text-base ml-1.5': 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')
}
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
try {
const isNewRow = row.value.rowMeta?.new ?? false
@ -266,13 +275,13 @@ const [useProvideExpandedFormStore, useExpandedFormStore] = useInjectionState((m
}
}
if (activeView.value?.type === ViewTypes.KANBAN) {
const { addOrEditStackRow } = useKanbanViewStoreOrThrow()
addOrEditStackRow(row.value, isNewRow)
if (activeView.value?.type === ViewTypes.KANBAN && kanbanClbk) {
kanbanClbk(row.value, isNewRow)
}
changedColumns.value = new Set()
} catch (e: any) {
console.error(e)
message.error(`${t('msg.error.rowUpdateFailed')}: ${await extractSdkResponseErrorMsg(e)}`)
}
$e('a:row-expand:add')

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

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

11
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 {
Column,
LinkToAnotherRecordColumn,
LookupColumn,
Model,
} from '~/models';
import { GalleryView, View } from '~/models';
import { GalleryView, KanbanView, View } from '~/models';
const getAst = async ({
query,
@ -33,11 +33,12 @@ const getAst = async ({
dependencyFields.fieldsSet = dependencyFields.fieldsSet || new Set();
let coverImageId;
if (view) {
if (view && view.type === ViewTypes.GALLERY) {
const gallery = await GalleryView.get(view.id);
if (gallery) {
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();

Loading…
Cancel
Save