Browse Source

Merge pull request #6490 from nocodb/nc-fix/mobile-response-followup

Mobile responsive followup
pull/6507/head
Muhammed Mustafa 1 year ago committed by GitHub
parent
commit
770418f4e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      packages/nc-gui/assets/nc-icons/comment_here.svg
  2. 11
      packages/nc-gui/assets/style.scss
  3. 24
      packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue
  4. 4
      packages/nc-gui/components/dashboard/TreeView/TableNode.vue
  5. 7
      packages/nc-gui/components/dashboard/TreeView/ViewsNode.vue
  6. 3
      packages/nc-gui/components/dashboard/View.vue
  7. 28
      packages/nc-gui/components/general/OpenLeftSidebarBtn.vue
  8. 2
      packages/nc-gui/components/nc/Dropdown.vue
  9. 2
      packages/nc-gui/components/nc/MenuItem.vue
  10. 10
      packages/nc-gui/components/nc/Modal.vue
  11. 82
      packages/nc-gui/components/nc/Pagination.vue
  12. 34
      packages/nc-gui/components/smartsheet/Pagination.vue
  13. 11
      packages/nc-gui/components/smartsheet/Topbar.vue
  14. 63
      packages/nc-gui/components/smartsheet/expanded-form/Comments.vue
  15. 169
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  16. 31
      packages/nc-gui/components/smartsheet/grid/Table.vue
  17. 5
      packages/nc-gui/components/smartsheet/grid/index.vue
  18. 3
      packages/nc-gui/composables/useViewData.ts
  19. 3
      packages/nc-gui/lang/en.json
  20. 30
      packages/nc-gui/plugins/font.ts
  21. 6
      packages/nc-gui/store/views.ts
  22. 4
      packages/nc-gui/utils/iconUtils.ts
  23. 2
      packages/nc-gui/windi.config.ts
  24. 43
      tests/playwright/pages/Dashboard/Grid/index.ts
  25. 4
      tests/playwright/pages/Dashboard/common/Footbar/index.ts
  26. 2
      tests/playwright/playwright.config.ts
  27. 10
      tests/playwright/quickTests/commonTest.ts
  28. 8
      tests/playwright/tests/db/features/pagination.spec.ts

3
packages/nc-gui/assets/nc-icons/comment_here.svg

@ -0,0 +1,3 @@
<svg width="40" height="41" viewBox="0 0 40 41" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Vector" d="M38 19.5001C38.0069 22.1398 37.3901 24.7438 36.2 27.1001C34.7889 29.9235 32.6195 32.2984 29.9349 33.9586C27.2503 35.6188 24.1565 36.4988 21 36.5001C18.3603 36.5069 15.7562 35.8902 13.4 34.7001L2 38.5001L5.8 27.1001C4.60986 24.7438 3.99312 22.1398 4 19.5001C4.00122 16.3436 4.88122 13.2498 6.54144 10.5652C8.20165 7.88055 10.5765 5.71119 13.4 4.30006C15.7562 3.10992 18.3603 2.49317 21 2.50006H22C26.1687 2.73004 30.1061 4.48958 33.0583 7.44177C36.0105 10.394 37.77 14.3314 38 18.5001V19.5001Z" stroke="#6A7184" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 701 B

11
packages/nc-gui/assets/style.scss

@ -53,6 +53,14 @@ main {
@apply !rounded-lg !py-2 !px-3 mb-1; @apply !rounded-lg !py-2 !px-3 mb-1;
} }
.mobile {
.nc-scrollbar-md, nc-scrollbar-dark-md, nc-scrollbar-dark-md, nc-scrollbar-sm-dark, nc-scrollbar-x-md {
&::-webkit-scrollbar {
width: 0px;
}
}
}
.nc-scrollbar-md { .nc-scrollbar-md {
overflow-y: scroll; overflow-y: scroll;
overflow-x: hidden; overflow-x: hidden;
@ -434,9 +442,6 @@ a {
@apply !shadow-none rounded ring-1 ring-red-600; @apply !shadow-none rounded ring-1 ring-red-600;
} }
.ant-modal {
@apply !top-[30px];
}
.ant-modal-content { .ant-modal-content {
@apply !p-6; @apply !p-6;
border-radius: 1rem; border-radius: 1rem;

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

@ -591,23 +591,25 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
ghost ghost
> >
<template #expandIcon="{ isActive }"> <template #expandIcon="{ isActive }">
<div class="flex flex-row items-center -mt-2"> <div
class="nc-sidebar-expand nc-sidebar-node-btn flex flex-row items-center -mt-2 xs:(mt-3 border-1 border-gray-200 px-2.25 py-0.5 rounded-md !mr-0.25)"
>
<GeneralIcon <GeneralIcon
icon="triangleFill" icon="triangleFill"
class="nc-sidebar-base-node-btns -mt-0.75 invisible cursor-pointer transform transition-transform duration-500 h-1.5 w-1.5 text-gray-500 rotate-90" class="nc-sidebar-base-node-btns -mt-0.75 invisible xs:visible cursor-pointer transform transition-transform duration-500 h-1.5 w-1.5 text-gray-500 rotate-90"
:class="{ '!rotate-180': isActive }" :class="{ '!rotate-180': isActive }"
/> />
</div> </div>
</template> </template>
<a-collapse-panel :key="`collapse-${base.id}`"> <a-collapse-panel :key="`collapse-${base.id}`">
<template #header> <template #header>
<div class="min-w-20 w-full flex flex-row group"> <div class="nc-sidebar-node min-w-20 w-full flex flex-row group py-0.25">
<div <div
v-if="baseIndex === 0" v-if="baseIndex === 0"
class="base-context flex items-center gap-2 text-gray-800" class="base-context flex items-center gap-2 text-gray-800 nc-sidebar-node-title"
@contextmenu="setMenuContext('base', base)" @contextmenu="setMenuContext('base', base)"
> >
<GeneralBaseLogo :base-type="base.type" /> <GeneralBaseLogo :base-type="base.type" class="min-w-4 !xs:(min-w-4.25 w-4.25 text-sm)" />
Default Default
</div> </div>
<div <div
@ -615,15 +617,15 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
class="base-context flex flex-grow items-center gap-1.75 text-gray-800 min-w-1/20 max-w-full" class="base-context flex flex-grow items-center gap-1.75 text-gray-800 min-w-1/20 max-w-full"
@contextmenu="setMenuContext('base', base)" @contextmenu="setMenuContext('base', base)"
> >
<GeneralBaseLogo :base-type="base.type" class="min-w-4" /> <GeneralBaseLogo :base-type="base.type" class="min-w-4 !xs:(min-w-4.25 w-4.25 text-sm)" />
<div <div
:data-testid="`nc-sidebar-project-${base.alias}`" :data-testid="`nc-sidebar-project-${base.alias}`"
class="flex capitalize text-ellipsis overflow-hidden select-none" class="nc-sidebar-node-title flex capitalize text-ellipsis overflow-hidden select-none"
:style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }" :style="{ wordBreak: 'keep-all', whiteSpace: 'nowrap', display: 'inline' }"
> >
{{ base.alias || '' }} {{ base.alias || '' }}
</div> </div>
<a-tooltip> <a-tooltip class="xs:(hidden)">
<template #title>External DB</template> <template #title>External DB</template>
<div> <div>
<GeneralIcon icon="info" class="text-gray-400 -mt-0.5 hover:text-gray-700 mr-1" /> <GeneralIcon icon="info" class="text-gray-400 -mt-0.5 hover:text-gray-700 mr-1" />
@ -757,7 +759,11 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
<style lang="scss" scoped> <style lang="scss" scoped>
:deep(.ant-collapse-header) { :deep(.ant-collapse-header) {
@apply !mx-0 !pl-8.75 !pr-0.5 !py-0.75 hover:bg-gray-200 !rounded-md; @apply !mx-0 !pl-8.75 !xs:(pl-8) !pr-0.5 !py-0.5 hover:bg-gray-200 xs:(hover:bg-gray-50 ) !rounded-md;
}
:deep(.ant-collapse-item) {
@apply h-full;
} }
:deep(.ant-collapse-content-box) { :deep(.ant-collapse-content-box) {

4
packages/nc-gui/components/dashboard/TreeView/TableNode.vue

@ -148,7 +148,7 @@ const isTableOpened = computed(() => {
class="nc-tree-item-inner nc-sidebar-node pl-11 pr-0.75 mb-0.25 rounded-md h-7.1 w-full group cursor-pointer hover:bg-gray-200" class="nc-tree-item-inner nc-sidebar-node pl-11 pr-0.75 mb-0.25 rounded-md h-7.1 w-full group cursor-pointer hover:bg-gray-200"
:class="{ :class="{
'hover:bg-gray-200': openedTableId !== table.id, 'hover:bg-gray-200': openedTableId !== table.id,
'pl-12': baseIndex !== 0, 'pl-12 xs:(pl-14)': baseIndex !== 0,
'pl-6.5': baseIndex === 0, 'pl-6.5': baseIndex === 0,
'!bg-primary-selected': isTableOpened, '!bg-primary-selected': isTableOpened,
}" }"
@ -181,7 +181,7 @@ const isTableOpened = computed(() => {
:key="table.meta?.icon" :key="table.meta?.icon"
:emoji="table.meta?.icon" :emoji="table.meta?.icon"
size="small" size="small"
:readonly="!canUserEditEmote" :readonly="!canUserEditEmote || isMobileMode"
@emoji-selected="setIcon($event, table)" @emoji-selected="setIcon($event, table)"
> >
<template #default> <template #default>

7
packages/nc-gui/components/dashboard/TreeView/ViewsNode.vue

@ -41,6 +41,8 @@ const vModel = useVModel(props, 'view', emits) as WritableComputedRef<ViewType &
const { $e } = useNuxtApp() const { $e } = useNuxtApp()
const { isMobileMode } = useGlobal()
const { isUIAllowed } = useRoles() const { isUIAllowed } = useRoles()
const { activeViewTitleOrId } = storeToRefs(useViewsStore()) const { activeViewTitleOrId } = storeToRefs(useViewsStore())
@ -212,8 +214,8 @@ function onRef(el: HTMLElement) {
<a-menu-item <a-menu-item
class="nc-sidebar-node !min-h-7 !max-h-7 !mb-0.25 select-none group text-gray-700 !flex !items-center !mt-0 hover:(!bg-gray-200 !text-gray-900) cursor-pointer" class="nc-sidebar-node !min-h-7 !max-h-7 !mb-0.25 select-none group text-gray-700 !flex !items-center !mt-0 hover:(!bg-gray-200 !text-gray-900) cursor-pointer"
:class="{ :class="{
'!pl-18': isDefaultBase, '!pl-18 !xs:(pl-19.75)': isDefaultBase,
'!pl-23.5': !isDefaultBase, '!pl-23.5 !xs:(pl-27)': !isDefaultBase,
}" }"
:data-testid="`view-sidebar-view-${vModel.alias || vModel.title}`" :data-testid="`view-sidebar-view-${vModel.alias || vModel.title}`"
@dblclick.stop="onDblClick" @dblclick.stop="onDblClick"
@ -231,6 +233,7 @@ function onRef(el: HTMLElement) {
:emoji="props.view?.meta?.icon" :emoji="props.view?.meta?.icon"
size="small" size="small"
:clearable="true" :clearable="true"
:readonly="isMobileMode"
@emoji-selected="emits('selectIcon', $event)" @emoji-selected="emits('selectIcon', $event)"
> >
<template #default> <template #default>

3
packages/nc-gui/components/dashboard/View.vue

@ -64,12 +64,11 @@ watch(isLeftSidebarOpen, () => {
setTimeout(() => (sidebarState.value = 'openEnd'), animationDuration) setTimeout(() => (sidebarState.value = 'openEnd'), animationDuration)
} else { } else {
sideBarSize.value.old = sideBarSize.value.current sideBarSize.value.old = sideBarSize.value.current
sideBarSize.value.current = 0
sidebarState.value = 'hiddenStart' sidebarState.value = 'hiddenStart'
setTimeout(() => { setTimeout(() => {
sideBarSize.value.current = 0
sidebarState.value = 'hiddenEnd' sidebarState.value = 'hiddenEnd'
}, animationDuration) }, animationDuration)
} }

28
packages/nc-gui/components/general/OpenLeftSidebarBtn.vue

@ -1,23 +1,12 @@
<script lang="ts" setup> <script lang="ts" setup>
const { isLeftSidebarOpen: _isLeftSidebarOpen } = storeToRefs(useSidebarStore()) const { isLeftSidebarOpen } = storeToRefs(useSidebarStore())
const isLeftSidebarOpen = ref(_isLeftSidebarOpen.value)
const { isMobileMode } = useGlobal() const { isMobileMode } = useGlobal()
watch(_isLeftSidebarOpen, (val) => {
if (val) {
isLeftSidebarOpen.value = true
} else {
setTimeout(() => {
isLeftSidebarOpen.value = false
}, 300)
}
})
const onClick = () => { const onClick = () => {
if (_isLeftSidebarOpen.value) return if (isLeftSidebarOpen.value) return
_isLeftSidebarOpen.value = !_isLeftSidebarOpen.value isLeftSidebarOpen.value = !isLeftSidebarOpen.value
} }
</script> </script>
@ -25,10 +14,10 @@ const onClick = () => {
<NcTooltip <NcTooltip
placement="topLeft" placement="topLeft"
hide-on-click hide-on-click
class="transition-all duration-100" class="transition-all duration-150"
:class="{ :class="{
'opacity-0 max-w-0': !isMobileMode && isLeftSidebarOpen, 'opacity-0 w-0': !isMobileMode && isLeftSidebarOpen,
'opacity-100': isMobileMode || !isLeftSidebarOpen, 'opacity-100 max-w-10': isMobileMode || !isLeftSidebarOpen,
}" }"
> >
<template #title> <template #title>
@ -41,10 +30,7 @@ const onClick = () => {
<NcButton <NcButton
:type="isMobileMode ? 'secondary' : 'text'" :type="isMobileMode ? 'secondary' : 'text'"
:size="isMobileMode ? 'medium' : 'small'" :size="isMobileMode ? 'medium' : 'small'"
class="nc-sidebar-left-toggle-icon !text-gray-600 !hover:text-gray-800" class="nc-sidebar-left-toggle-icon !text-gray-600 !hover:text-gray-800 w-8"
:class="{
'invisible !w-0': !isMobileMode && isLeftSidebarOpen,
}"
@click="onClick" @click="onClick"
> >
<div class="flex items-center text-inherit"> <div class="flex items-center text-inherit">

2
packages/nc-gui/components/nc/Dropdown.vue

@ -25,7 +25,7 @@ const overlayClassName = toRef(props, 'overlayClassName')
const autoClose = computed(() => props.autoClose) const autoClose = computed(() => props.autoClose)
const overlayClassNameComputed = computed(() => { const overlayClassNameComputed = computed(() => {
let className = 'nc-dropdown bg-white rounded-lg border-1 border-gray-100 shadow-lg shadow-gray-200' let className = 'nc-dropdown bg-white rounded-lg border-1 border-gray-100 shadow-lg'
if (overlayClassName.value) { if (overlayClassName.value) {
className += ` ${overlayClassName.value}` className += ` ${overlayClassName.value}`
} }

2
packages/nc-gui/components/nc/MenuItem.vue

@ -8,7 +8,7 @@
<style lang="scss"> <style lang="scss">
.ant-dropdown-menu-item.nc-menu-item { .ant-dropdown-menu-item.nc-menu-item {
@apply py-2 px-2 mx-1.5 font-normal text-dropdown rounded-md overflow-hidden hover:bg-gray-100; @apply p-2 mx-1.5 font-normal text-sm xs:(text-base py-3 px-3.5 mx-0) rounded-md overflow-hidden hover:bg-gray-100;
} }
.nc-menu-item-inner { .nc-menu-item-inner {

10
packages/nc-gui/components/nc/Modal.vue

@ -18,7 +18,13 @@ const emits = defineEmits(['update:visible'])
const { width: propWidth, destroyOnClose, maskClosable } = props const { width: propWidth, destroyOnClose, maskClosable } = props
const { isMobileMode } = useGlobal()
const width = computed(() => { const width = computed(() => {
if (isMobileMode.value) {
return '95vw'
}
if (propWidth) { if (propWidth) {
return propWidth return propWidth
} }
@ -39,6 +45,10 @@ const width = computed(() => {
}) })
const height = computed(() => { const height = computed(() => {
if (isMobileMode.value) {
return '95vh'
}
if (props.size === 'small') { if (props.size === 'small') {
return 'auto' return 'auto'
} }

82
packages/nc-gui/components/nc/Pagination.vue

@ -0,0 +1,82 @@
<script setup lang="ts">
const props = defineProps<{
current: number
total: number
pageSize: number
}>()
const emits = defineEmits(['update:current', 'update:pageSize'])
const { total } = toRefs(props)
const current = useVModel(props, 'current', emits)
const pageSize = useVModel(props, 'pageSize', emits)
const totalPages = computed(() => Math.ceil(total.value / pageSize.value))
const { isMobileMode } = useGlobal()
const changePage = ({ increase }: { increase: boolean }) => {
if (increase && current.value < totalPages.value) {
current.value = current.value + 1
} else if (current.value > 0) {
current.value = current.value - 1
}
}
const goToLastPage = () => {
current.value = totalPages.value
}
const goToFirstPage = () => {
current.value = 1
}
</script>
<template>
<div class="nc-pagination flex flex-row items-center gap-x-2">
<NcButton
v-if="!isMobileMode"
class="first-page"
type="secondary"
size="small"
:disabled="current === 1"
@click="goToFirstPage"
>
<GeneralIcon icon="doubleLeftArrow" />
</NcButton>
<NcButton class="prev-page" type="secondary" size="small" :disabled="current === 1" @click="changePage({ increase: false })">
<GeneralIcon icon="arrowLeft" />
</NcButton>
<div class="text-gray-600">
<span class="active"> {{ current }} </span>
<span class="mx-1"> {{ isMobileMode ? '/' : 'of' }} </span>
<span class="total">
{{ totalPages }}
</span>
</div>
<NcButton
class="next-page"
type="secondary"
size="small"
:disabled="current === totalPages"
@click="changePage({ increase: true })"
>
<GeneralIcon icon="arrowRight" />
</NcButton>
<NcButton
v-if="!isMobileMode"
class="last-page"
type="secondary"
size="small"
:disabled="current === totalPages"
@click="goToLastPage"
>
<GeneralIcon icon="doubleRightArrow" />
</NcButton>
</div>
</template>

34
packages/nc-gui/components/smartsheet/Pagination.vue

@ -33,7 +33,9 @@ const extraStyle = toRef(props, 'extraStyle')
const isGroupBy = inject(IsGroupByInj, ref(false)) const isGroupBy = inject(IsGroupByInj, ref(false))
const { isPaginationLoading } = storeToRefs(useViewsStore()) const { isViewDataLoading, isPaginationLoading } = storeToRefs(useViewsStore())
const { isLeftSidebarOpen } = storeToRefs(useSidebarStore())
const count = computed(() => vPaginationData.value?.totalRows ?? Infinity) const count = computed(() => vPaginationData.value?.totalRows ?? Infinity)
@ -42,13 +44,13 @@ const size = computed(() => vPaginationData.value?.pageSize ?? 25)
const page = computed({ const page = computed({
get: () => vPaginationData?.value?.page ?? 1, get: () => vPaginationData?.value?.page ?? 1,
set: async (p) => { set: async (p) => {
isPaginationLoading.value = true isViewDataLoading.value = true
try { try {
await changePage?.(p) await changePage?.(p)
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} finally { } finally {
isPaginationLoading.value = false isViewDataLoading.value = false
} }
}, },
}) })
@ -58,7 +60,7 @@ const isRTLLanguage = computed(() => isRtlLang(locale.value as keyof typeof Lang
<template> <template>
<div <div
class="flex items-center bg-white border-gray-200 nc-pagination-wrapper" class="flex items-center bg-white border-gray-200 nc-grid-pagination-wrapper"
:class="{ 'border-t-1': !isGroupBy, 'h-13': isMobileMode, 'h-10': !isMobileMode }" :class="{ 'border-t-1': !isGroupBy, 'h-13': isMobileMode, 'h-10': !isMobileMode }"
:style="`${fixedSize ? `width: ${fixedSize}px;` : ''}${ :style="`${fixedSize ? `width: ${fixedSize}px;` : ''}${
isGroupBy ? 'margin-top:1px; border-radius: 0 0 12px 12px !important;' : '' isGroupBy ? 'margin-top:1px; border-radius: 0 0 12px 12px !important;' : ''
@ -75,17 +77,23 @@ const isRTLLanguage = computed(() => isRtlLang(locale.value as keyof typeof Lang
</span> </span>
</div> </div>
<template v-if="!hidePagination"> <div
<a-pagination v-if="!hidePagination"
v-if="count !== Infinity" class="transition-all duration-350"
:class="{
'-ml-17': isLeftSidebarOpen,
}"
>
<div v-if="isPaginationLoading" class="flex flex-row justify-center item-center min-h-10 min-w-42">
<a-skeleton :active="true" :title="true" :paragraph="false" class="-mt-1 max-w-60" />
</div>
<NcPagination
v-else-if="count !== Infinity"
v-model:current="page" v-model:current="page"
v-model:page-size="size" v-model:page-size="size"
size="small" class="xs:(mr-2)"
class="!text-xs !m-1 nc-pagination"
:class="{ 'rtl-pagination': isRTLLanguage }" :class="{ 'rtl-pagination': isRTLLanguage }"
:total="+count" :total="+count"
show-less-items
:show-size-changer="false"
/> />
<div v-else class="mx-auto flex items-center mt-n1" style="max-width: 250px"> <div v-else class="mx-auto flex items-center mt-n1" style="max-width: 250px">
<span class="text-xs" style="white-space: nowrap"> Change page:</span> <span class="text-xs" style="white-space: nowrap"> Change page:</span>
@ -95,7 +103,7 @@ const isRTLLanguage = computed(() => isRtlLang(locale.value as keyof typeof Lang
</template> </template>
</a-input> </a-input>
</div> </div>
</template> </div>
<div v-if="!isMobileMode" class="flex-1 flex justify-end items-center"> <div v-if="!isMobileMode" class="flex-1 flex justify-end items-center">
<GeneralApiTiming v-if="isEeUI && props.showApiTiming" class="m-1" /> <GeneralApiTiming v-if="isEeUI && props.showApiTiming" class="m-1" />
<div class="text-right"> <div class="text-right">
@ -112,7 +120,7 @@ const isRTLLanguage = computed(() => isRtlLang(locale.value as keyof typeof Lang
</template> </template>
<style lang="scss"> <style lang="scss">
.nc-pagination-wrapper { .nc-grid-pagination-wrapper {
.ant-pagination-item-active { .ant-pagination-item-active {
a { a {
@apply text-sm !text-gray-700 !hover:text-gray-800; @apply text-sm !text-gray-700 !hover:text-gray-800;

11
packages/nc-gui/components/smartsheet/Topbar.vue

@ -10,6 +10,8 @@ const isPublic = inject(IsPublicInj, ref(false))
const { isViewsLoading } = storeToRefs(useViewsStore()) const { isViewsLoading } = storeToRefs(useViewsStore())
const { isLeftSidebarOpen } = storeToRefs(useSidebarStore())
const { isMobileMode } = storeToRefs(useConfigStore()) const { isMobileMode } = storeToRefs(useConfigStore())
const isSharedBase = computed(() => route.value.params.typeOrId === 'base') const isSharedBase = computed(() => route.value.params.typeOrId === 'base')
@ -29,7 +31,14 @@ const isSharedBase = computed(() => route.value.params.typeOrId === 'base')
<div class="flex-1" /> <div class="flex-1" />
<div v-if="!isSharedBase && !isMobileMode" class="absolute mx-auto -left-1/8 right-0 w-47.5"> <div
v-if="!isSharedBase && !isMobileMode"
class="absolute mx-auto transition-all duration-150 right-0 w-47.5"
:class="{
'-left-1/10': isLeftSidebarOpen,
'-left-0': !isLeftSidebarOpen,
}"
>
<SmartsheetTopbarSelectMode /> <SmartsheetTopbarSelectMode />
</div> </div>

63
packages/nc-gui/components/smartsheet/expanded-form/Comments.vue

@ -16,6 +16,8 @@ const tab = ref<'comments' | 'audits'>('comments')
const { isUIAllowed } = useRoles() const { isUIAllowed } = useRoles()
const { appInfo } = useGlobal()
const hasEditPermission = computed(() => isUIAllowed('commentEdit')) const hasEditPermission = computed(() => isUIAllowed('commentEdit'))
const editLog = ref<AuditType>() const editLog = ref<AuditType>()
@ -73,6 +75,8 @@ onKeyStroke('Enter', (event) => {
const comments = computed(() => commentsAndLogs.value.filter((log) => log.op_type === 'COMMENT')) const comments = computed(() => commentsAndLogs.value.filter((log) => log.op_type === 'COMMENT'))
const audits = computed(() => commentsAndLogs.value.filter((log) => log.op_type !== 'COMMENT')) const audits = computed(() => commentsAndLogs.value.filter((log) => log.op_type !== 'COMMENT'))
const isSearchBoxFocused = ref(false)
function editComment(log: AuditType) { function editComment(log: AuditType) {
editLog.value = log editLog.value = log
isEditing.value = true isEditing.value = true
@ -80,7 +84,7 @@ function editComment(log: AuditType) {
const value = computed({ const value = computed({
get() { get() {
return editLog.value.description.substring(editLog.value.description.indexOf(':') + 1) ?? '' return editLog.value?.description?.substring(editLog.value?.description?.indexOf(':') + 1) ?? ''
}, },
set(val) { set(val) {
if (!editLog.value) return if (!editLog.value) return
@ -97,11 +101,20 @@ watch(
}, },
{ immediate: true }, { immediate: true },
) )
// Ignore first line if its the only one
const processedAudit = (log: string) => {
const dotSplit = log.split('.')
if (dotSplit.length === 1) return log
return log.substring(log.indexOf('.') + 1)
}
</script> </script>
<template> <template>
<div class="flex flex-col h-full w-full bg-gray-50 rounded-lg"> <div class="flex flex-col h-full w-full">
<div class="bg-white rounded-t-lg border-gray-200 border-b-1"> <div class="h-16 bg-white rounded-t-lg border-gray-200 border-b-1">
<div class="flex flex-row gap-2 m-2 p-1 bg-gray-100 rounded-lg"> <div class="flex flex-row gap-2 m-2 p-1 bg-gray-100 rounded-lg">
<div <div
class="tab flex-1 px-4 py-2 transition-all text-gray-600 cursor-pointer rounded-lg" class="tab flex-1 px-4 py-2 transition-all text-gray-600 cursor-pointer rounded-lg"
@ -129,16 +142,21 @@ watch(
</div> </div>
</div> </div>
</div> </div>
<div> <div
<div v-if="tab === 'comments'" ref="commentsWrapperEl" class="flex flex-col h-[74vh] max-h-[680px]"> class="h-[calc(100%-4rem)]"
:class="{
'pb-2': tab !== 'comments',
}"
>
<div v-if="tab === 'comments'" ref="commentsWrapperEl" class="flex flex-col h-full">
<div v-if="comments.length === 0" class="flex flex-col my-1 text-center justify-center h-full"> <div v-if="comments.length === 0" class="flex flex-col my-1 text-center justify-center h-full">
<div class="text-center text-3xl text-gray-700"> <div class="text-center text-3xl text-gray-700">
<MdiChatProcessingOutline /> <GeneralIcon icon="commentHere" />
</div> </div>
<div class="font-bold text-center my-1 text-gray-700">Start a conversation</div> <div class="font-medium text-center my-6 text-gray-500">{{ $t('activity.startCommenting') }}</div>
</div> </div>
<div v-else class="flex-grow-1 my-2 px-2 space-y-2 overflow-y-scroll nc-scrollbar-md"> <div v-else class="flex flex-col h-full p-2 space-y-2 nc-scrollbar-md">
<div v-for="log of comments" :key="log.id" class="!last:mb-11"> <div v-for="log of comments" :key="log.id">
<div class="bg-white rounded-xl group border-1 gap-2 border-gray-200"> <div class="bg-white rounded-xl group border-1 gap-2 border-gray-200">
<div class="flex flex-col p-4 gap-3"> <div class="flex flex-col p-4 gap-3">
<div class="flex justify-between"> <div class="flex justify-between">
@ -154,7 +172,7 @@ watch(
</div> </div>
</div> </div>
<NcButton <NcButton
v-if="log.user === user.email && !editLog" v-if="log.user === user!.email && !editLog && !appInfo.ee"
type="secondary" type="secondary"
class="!px-2 opacity-0 group-hover:opacity-100 transition-all" class="!px-2 opacity-0 group-hover:opacity-100 transition-all"
size="sm" size="sm"
@ -182,27 +200,26 @@ watch(
</div> </div>
</div> </div>
</div> </div>
<div <div v-if="hasEditPermission" class="h-16.5 p-2 rounded-b-xl bg-gray-50 gap-2 flex">
v-if="hasEditPermission" <div class="flex flex-row items-end">
class="mt-1 absolute bottom-0 left-0 right-0 w-[285px] p-2 rounded-b-xl border-t-1 bg-white gap-2 flex" <GeneralUserIcon size="base" />
> </div>
<div class="flex flex-row bg-white py-2.75 px-1.5 items-center rounded-lg border-1 border-gray-200">
<a-input <a-input
v-model:value="comment" v-model:value="comment"
class="!rounded-lg border-1 bg-white !px-4 !py-2 !border-gray-200 nc-comment-box" class="!rounded-lg border-1 bg-white !px-2 !py-2 !border-gray-200 nc-comment-box !outline-none"
placeholder="Start typing..." placeholder="Start typing..."
:bordered="false"
@keyup.enter.prevent="saveComment" @keyup.enter.prevent="saveComment"
> >
</a-input> </a-input>
<NcButton type="secondary" size="medium" @click="saveComment"> <NcButton size="medium" class="!w-8" :disabled="!comment.length" @click="saveComment">
<Icon class="iconify text-gray-800" icon="lucide:send" /> <GeneralIcon icon="send" />
</NcButton> </NcButton>
</div> </div>
</div> </div>
<div </div>
v-else <div v-else ref="commentsWrapperEl" class="flex flex-col h-full pl-1.5 pr-1 pt-1 nc-scrollbar-md space-y-2">
ref="commentsWrapperEl"
class="flex flex-col m-1 p-1 h-[74vh] max-h-[680px] overflow-y-scroll nc-scrollbar-md space-y-2"
>
<template v-if="audits.length === 0"> <template v-if="audits.length === 0">
<div class="flex flex-col text-center justify-center h-full"> <div class="flex flex-col text-center justify-center h-full">
<div class="text-center text-3xl text-gray-600"> <div class="text-center text-3xl text-gray-600">
@ -229,7 +246,7 @@ watch(
</div> </div>
</div> </div>
<div class="text-sm font-medium text-gray-700"> <div class="text-sm font-medium text-gray-700">
{{ log.description.split('.')[1] }} {{ processedAudit(log.description) }}
</div> </div>
</div> </div>
</div> </div>

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

@ -50,6 +50,10 @@ const emits = defineEmits(['update:modelValue', 'cancel', 'next', 'prev'])
const key = ref(0) const key = ref(0)
const wrapper = ref()
const { isMobileMode } = useGlobal()
const { t } = useI18n() const { t } = useI18n()
const row = ref(props.row) const row = ref(props.row)
@ -329,6 +333,23 @@ const onConfirmDeleteRowClick = async () => {
onClose() onClose()
} }
} }
watch(
state,
() => {
if (!state.value?.id) return
setTimeout(() => {
const rowDom = wrapper.value?.querySelector(`.nc-expanded-form-row[col-id="${state.value?.id}"]`)
if (rowDom) {
rowDom.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
}, 650)
},
{
immediate: true,
},
)
</script> </script>
<script lang="ts"> <script lang="ts">
@ -338,38 +359,41 @@ export default {
</script> </script>
<template> <template>
<a-modal <NcModal
:key="key" :key="key"
v-model:visible="isExpanded" v-model:visible="isExpanded"
:footer="null" :footer="null"
:width="commentsDrawer && isUIAllowed('commentList') ? 'min(90vw,1280px)' : 'min(90vw,1280px)'" :width="commentsDrawer && isUIAllowed('commentList') ? 'min(80vw,1280px)' : 'min(80vw,1280px)'"
:body-style="{ padding: 0 }" :body-style="{ padding: 0 }"
:closable="false" :closable="false"
class="nc-drawer-expanded-form max-h-[856px]" size="large"
class="nc-drawer-expanded-form"
:class="{ active: isExpanded }" :class="{ active: isExpanded }"
> >
<div class="flex flex-shrink-0 w-full items-center nc-expanded-form-header relative pb-2 justify-between"> <div class="h-[75vh] xs:(h-[95vh] max-h-full) max-h-215 flex flex-col p-6">
<div class="flex h-8 flex-shrink-0 w-full items-center nc-expanded-form-header relative mb-4 justify-between">
<template v-if="!isMobileMode">
<div class="flex gap-3"> <div class="flex gap-3">
<div class="flex gap-1"> <div class="flex gap-2">
<NcButton v-if="props.showNextPrevIcons" type="secondary" size="small" class="nc-prev-arrow" @click="$emit('prev')"> <NcButton v-if="props.showNextPrevIcons" type="secondary" class="nc-prev-arrow !w-10" @click="$emit('prev')">
<MdiChevronUp class="text-md text-gray-700" /> <MdiChevronUp class="text-md text-gray-700" />
</NcButton> </NcButton>
<NcButton v-if="!props.lastRow" type="secondary" size="small" class="nc-next-arrow" @click="onNext"> <NcButton v-if="!props.lastRow" type="secondary" class="nc-next-arrow !w-10" @click="onNext">
<MdiChevronDown class="text-md text-gray-700" /> <MdiChevronDown class="text-md text-gray-700" />
</NcButton> </NcButton>
</div> </div>
<div v-if="displayValue" class="flex items-center truncate max-w-32 font-bold text-gray-800 text-xl"> <div v-if="displayValue" class="flex items-center truncate max-w-32 font-bold text-gray-800 text-xl">
{{ displayValue }} {{ displayValue }}
</div> </div>
<div class="bg-gray-100 px-2 gap-1 flex items-center rounded-md text-gray-800"> <div class="bg-gray-100 px-2 gap-1 flex my-1 items-center rounded-lg text-gray-800 font-medium">
<TableIcon class="w-6 h-6 text-sm" /> <TableIcon class="w-6 h-6 text-sm" />
All {{ meta.title }} All {{ meta.title }}
</div> </div>
</div> </div>
<div class="flex gap-1"> <div class="flex gap-2">
<NcDropdown v-if="!isNew"> <NcDropdown v-if="!isNew">
<NcButton type="secondary" size="small" class="nc-expand-form-more-actions"> <NcButton type="secondary" class="nc-expand-form-more-actions w-10">
<MdiMoreVert class="text-md text-gray-700" /> <GeneralIcon icon="threeDotVertical" class="text-md text-gray-700" />
</NcButton> </NcButton>
<template #overlay> <template #overlay>
<NcMenu> <NcMenu>
@ -389,7 +413,7 @@ export default {
Duplicate record Duplicate record
</div> </div>
</NcMenuItem> </NcMenuItem>
<a-menu-divider class="my-1" /> <NcDivider />
<NcMenuItem <NcMenuItem
v-if="isUIAllowed('dataEdit') && !isNew" v-if="isUIAllowed('dataEdit') && !isNew"
v-e="['c:row-expand:delete']" v-e="['c:row-expand:delete']"
@ -402,35 +426,50 @@ export default {
</NcMenu> </NcMenu>
</template> </template>
</NcDropdown> </NcDropdown>
<NcButton type="secondary" size="small" class="nc-expand-form-close-btn" @click="onClose"> <NcButton type="secondary" class="nc-expand-form-close-btn w-10" @click="onClose">
<MdiClose class="text-md text-gray-700" /> <GeneralIcon icon="close" class="text-md text-gray-700" />
</NcButton> </NcButton>
</div> </div>
</template>
<template v-else>
<div class="flex flex-row w-full">
<NcButton v-if="props.showNextPrevIcons" type="secondary" class="nc-prev-arrow !w-10" @click="$emit('prev')">
<GeneralIcon icon="arrowLeft" class="text-lg text-gray-700" />
</NcButton>
<div class="flex flex-grow justify-center items-center font-semibold text-lg">
<div>{{ meta.title }}</div>
</div> </div>
<div class="flex flex-row w-full gap-4"> <NcButton v-if="!props.lastRow" type="secondary" class="nc-next-arrow !w-10" @click="onNext">
<div class="flex w-full flex-col h-[85vh] max-h-[770px] border-1 rounded-xl border-gray-200"> <GeneralIcon icon="arrowRight" class="text-lg text-gray-700" />
</NcButton>
</div>
</template>
</div>
<div ref="wrapper" class="flex flex-grow flex-row h-[calc(100%-3rem)] w-full gap-4">
<div class="flex w-full flex-col border-1 rounded-xl overflow-hidden border-gray-200 xs:(border-0 rounded-none)">
<div <div
class="flex flex-grow-1 h-full flex-col !pb-12 nc-scrollbar-md overflow-y-scroll items-center w-full rounded-xl bg-white p-4" class="flex flex-col flex-grow mt-2 h-full max-h-full nc-scrollbar-md !pb-2 items-center w-full bg-white p-4 xs:p-0"
> >
<div <div
v-for="(col, i) of fields" v-for="(col, i) of fields"
v-show="isFormula(col) || !isVirtualCol(col) || !isNew || isLinksOrLTAR(col)" v-show="isFormula(col) || !isVirtualCol(col) || !isNew || isLinksOrLTAR(col)"
:key="col.title" :key="col.title"
class="mt-2 py-2" class="nc-expanded-form-row mt-2 py-2 xs:w-full"
:class="`nc-expand-col-${col.title}`" :class="`nc-expand-col-${col.title}`"
:col-id="col.id"
:data-testid="`nc-expand-col-${col.title}`" :data-testid="`nc-expand-col-${col.title}`"
> >
<div class="flex items-start flex-row"> <div class="flex items-start flex-row xs:(flex-col w-full) nc-expanded-cell">
<div class="w-[12rem] mt-2.5 scale-110"> <div class="w-[12rem] xs:(w-full) mt-1.5">
<LazySmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" /> <LazySmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" class="nc-expanded-cell-header" :column="col" />
<LazySmartsheetHeaderCell v-else :column="col" /> <LazySmartsheetHeaderCell v-else class="nc-expanded-cell-header" :column="col" />
</div> </div>
<LazySmartsheetDivDataCell <LazySmartsheetDivDataCell
v-if="col.title" v-if="col.title"
:ref="i ? null : (el: any) => (cellWrapperEl = el)" :ref="i ? null : (el: any) => (cellWrapperEl = el)"
class="!bg-white rounded-lg !w-[20rem] border-1 border-gray-200 px-1 min-h-[35px] flex items-center relative" class="!bg-white rounded-lg !w-[20rem] !xs:w-full border-1 border-gray-200 px-1 min-h-[35px] flex items-center relative"
> >
<LazySmartsheetVirtualCell v-if="isVirtualCol(col)" v-model="row.row[col.title]" :row="row" :column="col" /> <LazySmartsheetVirtualCell v-if="isVirtualCol(col)" v-model="row.row[col.title]" :row="row" :column="col" />
@ -494,31 +533,97 @@ export default {
</div> </div>
<div <div
v-if="isUIAllowed('dataEdit')" v-if="isUIAllowed('dataEdit')"
class="w-full flex-shrink-1 rounded-xl border-t-1 border-gray-200 bottom-0 z-10 bg-white flex justify-end p-2" class="w-full h-14 border-t-1 border-gray-200 bg-white flex items-center justify-end p-2 xs:(p-0 mt-4 border-t-0 gap-x-4 justify-between)"
> >
<NcButton type="primary" size="medium" class="nc-expand-form-save-btn" @click="save"> Save </NcButton> <NcDropdown v-if="!isNew && isMobileMode">
<NcButton type="secondary" class="nc-expand-form-more-actions w-10">
<GeneralIcon icon="threeDotVertical" class="text-md text-gray-700" />
</NcButton>
<template #overlay>
<NcMenu>
<NcMenuItem v-if="!isNew" class="text-gray-700" @click="_loadRow()">
<div v-e="['c:row-expand:reload']" class="flex gap-2 items-center">
<component :is="iconMap.reload" class="cursor-pointer" />
{{ $t('general.reload') }}
</div>
</NcMenuItem>
<NcDivider />
<NcMenuItem
v-if="isUIAllowed('dataEdit') && !isNew"
v-e="['c:row-expand:delete']"
class="!text-red-500"
@click="!isNew && onDeleteRowClick()"
>
<component :is="iconMap.delete" class="cursor-pointer nc-delete-row" />
Delete record
</NcMenuItem>
</NcMenu>
</template>
</NcDropdown>
<div class="flex flex-row gap-x-3">
<NcButton
v-if="isMobileMode"
type="secondary"
size="medium"
class="nc-expand-form-save-btn !xs:(text-base)"
@click="onClose"
>
<div class="px-1">Close</div>
</NcButton>
<NcButton type="primary" size="medium" class="nc-expand-form-save-btn !xs:(text-base)" @click="save">
<div class="xs:px-1">Save</div>
</NcButton>
</div>
</div> </div>
</div> </div>
<div <div
v-if="!isNew && commentsDrawer && isUIAllowed('commentList')" v-if="!isNew && commentsDrawer && isUIAllowed('commentList')"
class="nc-comments-drawer border-1 relative border-gray-200 w-[380px] bg-gray-50 rounded-lg min-w-0" class="nc-comments-drawer border-1 relative border-gray-200 w-[380px] bg-gray-50 rounded-xl min-w-0 overflow-hidden h-full xs:hidden"
:class="{ active: commentsDrawer && isUIAllowed('commentList') }" :class="{ active: commentsDrawer && isUIAllowed('commentList') }"
> >
<LazySmartsheetExpandedFormComments /> <LazySmartsheetExpandedFormComments />
</div> </div>
</div> </div>
</a-modal> </div>
</NcModal>
<GeneralModal v-model:visible="showDeleteRowModal" class="!w-[25rem]"> <NcModal v-model:visible="showDeleteRowModal" class="!w-[25rem] !xs-">
<div class="p-4"> <div class="">
<div class="prose-xl font-bold self-center">Delete row ?</div> <div class="prose-xl font-bold self-center">Delete row ?</div>
<div class="mt-4">Are you sure you want to delete this row?</div> <div class="mt-4">Are you sure you want to delete this row?</div>
</div> </div>
<div class="flex flex-row gap-x-2 mt-1 pt-1.5 justify-end p-4"> <div class="flex flex-row gap-x-2 mt-4 pt-1.5 justify-end pt-4 gap-x-3">
<NcButton v-if="isMobileMode" type="secondary" @click="showDeleteRowModal = false">{{ $t('general.cancel') }} </NcButton>
<NcButton @click="onConfirmDeleteRowClick">{{ $t('general.confirm') }} </NcButton> <NcButton @click="onConfirmDeleteRowClick">{{ $t('general.confirm') }} </NcButton>
</div> </div>
</GeneralModal> </NcModal>
</template> </template>
<style scoped lang="scss"></style> <style lang="scss">
.nc-drawer-expanded-form {
@apply xs:my-0;
}
.nc-expanded-cell {
input {
@apply xs:(h-12 text-base);
}
}
.nc-expanded-cell-header {
@apply w-full text-gray-700 xs:mb-2;
}
.nc-expanded-cell-header > :nth-child(2) {
@apply !text-sm !xs:text-base;
}
.nc-expanded-cell-header > :first-child {
@apply !text-xl;
}
.nc-drawer-expanded-form .nc-modal {
@apply !p-0;
}
</style>

31
packages/nc-gui/components/smartsheet/grid/Table.vue

@ -497,7 +497,7 @@ const {
activeCell, activeCell,
handleMouseDown, handleMouseDown,
handleMouseOver, handleMouseOver,
handleCellClick, handleCellClick: _handleCellClick,
clearSelectedRange, clearSelectedRange,
copyValue, copyValue,
isCellActive, isCellActive,
@ -1135,6 +1135,16 @@ const expandAndLooseFocus = (row: Row, col: Record<string, any>) => {
activeCell.col = null activeCell.col = null
selectedRange.clear() selectedRange.clear()
} }
const handleCellClick = (event: MouseEvent, row: number, col: number) => {
const rowData = dataRef.value[row]
if (isMobileMode.value) {
return expandAndLooseFocus(rowData, fields.value[col])
}
_handleCellClick(event, row, col)
}
</script> </script>
<template> <template>
@ -1149,6 +1159,10 @@ const expandAndLooseFocus = (row: Row, col: Record<string, any>) => {
<table <table
ref="smartTable" ref="smartTable"
class="xc-row-table nc-grid backgroundColorDefault !h-auto bg-white" class="xc-row-table nc-grid backgroundColorDefault !h-auto bg-white"
:class="{
mobile: isMobileMode,
desktop: !isMobileMode,
}"
@contextmenu="showContextMenu" @contextmenu="showContextMenu"
> >
<thead v-show="hideHeader !== true" ref="tableHeadEl"> <thead v-show="hideHeader !== true" ref="tableHeadEl">
@ -1586,11 +1600,8 @@ const expandAndLooseFocus = (row: Row, col: Record<string, any>) => {
</NcDropdown> </NcDropdown>
</div> </div>
<div v-if="showSkeleton && headerOnly !== true" class="flex flex-row justify-center item-center min-h-10">
<a-skeleton :active="true" :title="true" :paragraph="false" class="-mt-1 max-w-60" />
</div>
<LazySmartsheetPagination <LazySmartsheetPagination
v-else-if="headerOnly !== true" v-if="headerOnly !== true"
:key="isMobileMode" :key="isMobileMode"
v-model:pagination-data="paginationDataRef" v-model:pagination-data="paginationDataRef"
show-api-timing show-api-timing
@ -1611,7 +1622,7 @@ const expandAndLooseFocus = (row: Row, col: Record<string, any>) => {
placement="top" placement="top"
@click="isAddNewRecordGridMode ? addEmptyRow() : onNewRecordToFormClick()" @click="isAddNewRecordGridMode ? addEmptyRow() : onNewRecordToFormClick()"
> >
<div class="flex items-center px-2 text-gray-600 hover:text-black"> <div data-testid="nc-pagination-add-record" class="flex items-center px-2 text-gray-600 hover:text-black">
<span> <span>
<template v-if="isAddNewRecordGridMode"> {{ $t('activity.newRecord') }} </template> <template v-if="isAddNewRecordGridMode"> {{ $t('activity.newRecord') }} </template>
<template v-else> {{ $t('activity.newRecord') }} - {{ $t('objects.viewType.form') }} </template> <template v-else> {{ $t('activity.newRecord') }} - {{ $t('objects.viewType.form') }} </template>
@ -1676,7 +1687,7 @@ const expandAndLooseFocus = (row: Row, col: Record<string, any>) => {
</template> </template>
<style lang="scss"> <style lang="scss">
.nc-pagination-wrapper .ant-dropdown-button { .nc-grid-pagination-wrapper .ant-dropdown-button {
> .ant-btn { > .ant-btn {
@apply !p-0 !rounded-l-lg hover:border-gray-400; @apply !p-0 !rounded-l-lg hover:border-gray-400;
} }
@ -1793,20 +1804,22 @@ const expandAndLooseFocus = (row: Row, col: Record<string, any>) => {
background: white; background: white;
} }
.desktop {
thead th:nth-child(2) { thead th:nth-child(2) {
position: sticky !important; position: sticky !important;
left: 85px;
z-index: 5; z-index: 5;
left: 85px;
@apply border-r-1 border-r-gray-200; @apply border-r-1 border-r-gray-200;
} }
tbody td:nth-child(2) { tbody td:nth-child(2) {
position: sticky !important; position: sticky !important;
left: 85px;
z-index: 4; z-index: 4;
left: 85px;
background: white; background: white;
@apply border-r-1 border-r-gray-100; @apply border-r-1 border-r-gray-100;
} }
}
.nc-grid-skelton-loader { .nc-grid-skelton-loader {
thead th:nth-child(2) { thead th:nth-child(2) {

5
packages/nc-gui/components/smartsheet/grid/index.vue

@ -102,6 +102,8 @@ function expandForm(row: Row, state?: Record<string, any>, fromToolbar = false)
const rowId = extractPkFromRow(row.row, meta.value?.columns as ColumnType[]) const rowId = extractPkFromRow(row.row, meta.value?.columns as ColumnType[])
if (rowId) { if (rowId) {
expandedFormRowState.value = state
router.push({ router.push({
query: { query: {
...routeQuery.value, ...routeQuery.value,
@ -242,6 +244,7 @@ onMounted(() => {
v-model="expandedFormOnRowIdDlg" v-model="expandedFormOnRowIdDlg"
:row="{ row: {}, oldRow: {}, rowMeta: {} }" :row="{ row: {}, oldRow: {}, rowMeta: {} }"
:meta="meta" :meta="meta"
:state="expandedFormRowState"
:row-id="routeQuery.rowId" :row-id="routeQuery.rowId"
:view="view" :view="view"
show-next-prev-icons show-next-prev-icons
@ -269,7 +272,7 @@ onMounted(() => {
</template> </template>
<style lang="scss"> <style lang="scss">
.nc-pagination-wrapper .ant-dropdown-button { .nc-grid-pagination-wrapper .ant-dropdown-button {
> .ant-btn { > .ant-btn {
@apply !p-0 !rounded-l-lg hover:border-gray-300; @apply !p-0 !rounded-l-lg hover:border-gray-300;
} }

3
packages/nc-gui/composables/useViewData.ts

@ -85,6 +85,8 @@ export function useViewData(
const routeQuery = computed(() => route.value.query as Record<string, string>) const routeQuery = computed(() => route.value.query as Record<string, string>)
const { isPaginationLoading } = storeToRefs(useViewsStore())
const paginationData = computed({ const paginationData = computed({
get: () => (isPublic.value ? sharedPaginationData.value : _paginationData.value), get: () => (isPublic.value ? sharedPaginationData.value : _paginationData.value),
set: (value) => { set: (value) => {
@ -178,6 +180,7 @@ export function useViewData(
formattedData.value = formatData(response.list) formattedData.value = formatData(response.list)
paginationData.value = response.pageInfo paginationData.value = response.pageInfo
isPaginationLoading.value = false
// to cater the case like when querying with a non-zero offset // to cater the case like when querying with a non-zero offset
// the result page may point to the target page where the actual returned data don't display on // the result page may point to the target page where the actual returned data don't display on

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

@ -566,7 +566,8 @@
"openInGoogleMaps": "Google Maps", "openInGoogleMaps": "Google Maps",
"openInOpenStreetMap": "OSM" "openInOpenStreetMap": "OSM"
}, },
"toggleMobileMode": "Toggle Mobile Mode" "toggleMobileMode": "Toggle Mobile Mode",
"startCommenting": "Start commenting!"
}, },
"tooltip": { "tooltip": {
"saveChanges": "Save changes", "saveChanges": "Save changes",

30
packages/nc-gui/plugins/font.ts

@ -10,12 +10,12 @@ export default defineNuxtPlugin(() => {
const fontFaces = [...document.fonts.values()] const fontFaces = [...document.fonts.values()]
const materialFont = fontFaces.find((fontFace) => fontFace.family === 'Material Symbols') const materialFont = fontFaces.find((fontFace) => fontFace.family === 'Material Symbols')
if (!materialFont || !materialFont?.loaded) { if (!materialFont || !materialFont.loaded) {
document.documentElement?.classList.remove('nc-fonts-not-loaded') document.documentElement?.classList.remove('nc-fonts-not-loaded')
return return
} }
materialFont?.loaded materialFont.loaded
.then(function () { .then(function () {
document.documentElement?.classList.remove('nc-fonts-not-loaded') document.documentElement?.classList.remove('nc-fonts-not-loaded')
}) })
@ -23,6 +23,32 @@ export default defineNuxtPlugin(() => {
document.documentElement?.classList.remove('nc-fonts-not-loaded') document.documentElement?.classList.remove('nc-fonts-not-loaded')
console.error(error) console.error(error)
}) })
// Safari issue where loaded promise is always in pending state.
// So we need to poll for font status to be 'unloaded'
let intervalId: any
function poll() {
const fontFaces = [...document.fonts.values()]
const materialFont = fontFaces.find((fontFace) => fontFace.family === 'Material Symbols')
if (materialFont?.status === 'unloaded') {
document.documentElement?.classList.remove('nc-fonts-not-loaded')
stopPolling()
} else if (materialFont?.status === 'loaded') {
stopPolling()
}
}
function startPolling(interval: number) {
intervalId = setInterval(poll, interval)
}
function stopPolling() {
clearInterval(intervalId)
}
startPolling(200)
} catch (error) { } catch (error) {
document.documentElement?.classList.remove('nc-fonts-not-loaded') document.documentElement?.classList.remove('nc-fonts-not-loaded')
console.error(error) console.error(error)

6
packages/nc-gui/store/views.ts

@ -79,7 +79,7 @@ export const useViewsStore = defineStore('viewsStore', () => {
}) })
// Used for Grid View Pagination // Used for Grid View Pagination
const isPaginationLoading = ref(false) const isPaginationLoading = ref(true)
const loadViews = async ({ const loadViews = async ({
tableId, tableId,
@ -183,6 +183,10 @@ export const useViewsStore = defineStore('viewsStore', () => {
} }
} }
watch(activeViewTitleOrId, () => {
isPaginationLoading.value = true
})
return { return {
isLockedView, isLockedView,
isViewsLoading, isViewsLoading,

4
packages/nc-gui/utils/iconUtils.ts

@ -76,6 +76,8 @@ import Left from '~icons/material-symbols/chevron-left-rounded'
import Up from '~icons/material-symbols/keyboard-arrow-up-rounded' import Up from '~icons/material-symbols/keyboard-arrow-up-rounded'
import Down from '~icons/material-symbols/keyboard-arrow-down-rounded' import Down from '~icons/material-symbols/keyboard-arrow-down-rounded'
import PhTriangleFill from '~icons/ph/triangle-fill' import PhTriangleFill from '~icons/ph/triangle-fill'
import LcSend from '~icons/lucide/send'
import NcCommentHere from '~icons/nc-icons/comment-here'
// Roles // Roles
import MaterialSymbolsManageAccountsOutline from '~icons/material-symbols/manage-accounts-outline' import MaterialSymbolsManageAccountsOutline from '~icons/material-symbols/manage-accounts-outline'
@ -343,6 +345,7 @@ export const iconMap = {
number: h('span', { class: 'material-symbols' }, 'looks_one'), number: h('span', { class: 'material-symbols' }, 'looks_one'),
email: h('span', { class: 'material-symbols' }, 'email'), email: h('span', { class: 'material-symbols' }, 'email'),
sendEmail: h('span', { class: 'material-symbols' }, 'email'), sendEmail: h('span', { class: 'material-symbols' }, 'email'),
send: LcSend,
currency: h('span', { class: 'material-symbols' }, 'attach_money'), currency: h('span', { class: 'material-symbols' }, 'attach_money'),
percent: h('span', { class: 'material-symbols' }, 'percent'), percent: h('span', { class: 'material-symbols' }, 'percent'),
decimal: h('span', { class: 'material-symbols' }, 'decimal_increase'), decimal: h('span', { class: 'material-symbols' }, 'decimal_increase'),
@ -417,6 +420,7 @@ export const iconMap = {
role_commenter: MdiCommentAccountOutline, role_commenter: MdiCommentAccountOutline,
role_viewer: MaterialSymbolsPersonSearchOutline, role_viewer: MaterialSymbolsPersonSearchOutline,
role_no_access: MaterialSymbolsBlock, role_no_access: MaterialSymbolsBlock,
commentHere: NcCommentHere,
} }
export const getMdiIcon = (type: string): any => { export const getMdiIcon = (type: string): any => {

2
packages/nc-gui/windi.config.ts

@ -60,7 +60,7 @@ export default defineConfig({
max: '480px', max: '480px',
}, },
sm: { sm: {
max: '820px', min: '480px',
}, },
md: { md: {
min: '820px', min: '820px',

43
tests/playwright/pages/Dashboard/Grid/index.ts

@ -299,40 +299,23 @@ export class GridPage extends BasePage {
expect(parseInt(recordCnt)).toEqual(count); expect(parseInt(recordCnt)).toEqual(count);
} }
async verifyPaginationCount({ count }: { count: number }) { async verifyPaginationCount({ count }: { count: string }) {
let i = 0; await expect(this.get().locator(`.nc-pagination .total`)).toHaveText(count);
await this.get().locator(`.nc-pagination`).first().waitFor();
let records = await this.get().locator(`[data-testid="grid-pagination"]`).allInnerTexts();
let recordCnt = records[0].split(' ')[0];
while (parseInt(recordCnt) !== count && i < 5) {
await this.get().locator(`.nc-pagination`).first().waitFor();
records = await this.get().locator(`[data-testid="grid-pagination"]`).allInnerTexts();
recordCnt = records[0].split(' ')[0];
// to ensure page loading is complete
i++;
await this.rootPage.waitForTimeout(300 * i);
} }
expect(parseInt(recordCnt)).toEqual(count);
}
private async pagination({ page }: { page: string }) {
await this.get().locator(`.nc-pagination`).waitFor();
if (page === '<') return this.get().locator('.nc-pagination > .ant-pagination-prev');
if (page === '>') return this.get().locator('.nc-pagination > .ant-pagination-next');
return this.get().locator(`.nc-pagination > .ant-pagination-item.ant-pagination-item-${page}`); async clickPagination({
} type,
skipWait = false,
async clickPagination({ page, skipWait = false }: { page: string; skipWait?: boolean }) { }: {
type: 'first-page' | 'last-page' | 'next-page' | 'prev-page';
skipWait?: boolean;
}) {
if (!skipWait) { if (!skipWait) {
await (await this.pagination({ page })).click(); await this.get().locator(`.nc-pagination .${type}`).click();
await this.waitLoading(); await this.waitLoading();
} else { } else {
await this.waitForResponse({ await this.waitForResponse({
uiAction: async () => (await this.pagination({ page })).click(), uiAction: async () => (await this.get().locator(`.nc-pagination .${type}`)).click(),
httpMethodsToMatch: ['GET'], httpMethodsToMatch: ['GET'],
requestUrlPathToMatch: '/views/', requestUrlPathToMatch: '/views/',
responseJsonMatcher: resJson => resJson?.pageInfo, responseJsonMatcher: resJson => resJson?.pageInfo,
@ -342,8 +325,8 @@ export class GridPage extends BasePage {
} }
} }
async verifyActivePage({ page }: { page: string }) { async verifyActivePage({ pageNumber }: { pageNumber: string }) {
await expect(await this.pagination({ page })).toHaveClass(/ant-pagination-item-active/); await expect(this.get().locator(`.nc-pagination .active`)).toHaveText(pageNumber);
} }
async waitLoading() { async waitLoading() {

4
tests/playwright/pages/Dashboard/common/Footbar/index.ts

@ -17,11 +17,11 @@ export class FootbarPage extends BasePage {
this.parent = parent; this.parent = parent;
this.leftSidebarToggle = this.get().locator(`div.nc-sidebar-left-toggle-icon`); this.leftSidebarToggle = this.get().locator(`div.nc-sidebar-left-toggle-icon`);
this.rightSidebarToggle = this.get().locator(`div.nc-sidebar-right-toggle-icon`); this.rightSidebarToggle = this.get().locator(`div.nc-sidebar-right-toggle-icon`);
this.btn_addNewRow = this.get().locator('button.ant-btn').nth(0); this.btn_addNewRow = this.get().getByTestId('nc-pagination-add-record');
} }
get() { get() {
return this.rootPage.locator(`div.nc-pagination-wrapper`); return this.rootPage.locator(`div.nc-grid-pagination-wrapper`);
} }
async clickAddRecord() { async clickAddRecord() {

2
tests/playwright/playwright.config.ts

@ -27,7 +27,7 @@ export default defineConfig({
/* Fail the build on CI if you accidentally left test.only in the source code. */ /* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI, forbidOnly: !!process.env.CI,
/* Retry on CI only */ /* Retry on CI only */
retries: process.env.CI ? 1 : 0, retries: process.env.CI ? 3 : 0,
/* Opt out of parallel tests on CI. */ /* Opt out of parallel tests on CI. */
// workers: process.env.CI ? 2 : 4, // workers: process.env.CI ? 2 : 4,
workers: process.env.CI ? 2 : 4, workers: process.env.CI ? 2 : 4,

10
tests/playwright/quickTests/commonTest.ts

@ -190,11 +190,11 @@ const quickVerify = async ({
} }
// Verify pagination // Verify pagination
await dashboard.grid.verifyActivePage({ page: '1' }); await dashboard.grid.verifyActivePage({ pageNumber: '1' });
await dashboard.grid.clickPagination({ page: '>', skipWait: true }); await dashboard.grid.clickPagination({ type: 'next-page', skipWait: true });
await dashboard.grid.verifyActivePage({ page: '2' }); await dashboard.grid.verifyActivePage({ pageNumber: '2' });
await dashboard.grid.clickPagination({ page: '<', skipWait: true }); await dashboard.grid.clickPagination({ type: 'prev-page', skipWait: true });
await dashboard.grid.verifyActivePage({ page: '1' }); await dashboard.grid.verifyActivePage({ pageNumber: '1' });
await dashboard.viewSidebar.openView({ title: 'Filter&Sort' }); await dashboard.viewSidebar.openView({ title: 'Filter&Sort' });

8
tests/playwright/tests/db/features/pagination.spec.ts

@ -22,10 +22,10 @@ test.describe('Grid pagination', () => {
await dashboard.treeView.openTable({ title: 'Country' }); await dashboard.treeView.openTable({ title: 'Country' });
// click ">" to go to next page // click ">" to go to next page
await dashboard.grid.clickPagination({ page: '>' }); await dashboard.grid.clickPagination({ type: 'next-page' });
await dashboard.grid.verifyActivePage({ page: '2' }); await dashboard.grid.verifyActivePage({ pageNumber: '2' });
// click "<" to go to prev page // click "<" to go to prev page
await dashboard.grid.clickPagination({ page: '<' }); await dashboard.grid.clickPagination({ type: 'prev-page' });
await dashboard.grid.verifyActivePage({ page: '1' }); await dashboard.grid.verifyActivePage({ pageNumber: '1' });
}); });
}); });

Loading…
Cancel
Save