Browse Source

Merge branch 'develop' into feat/6430-group-by

pull/6489/head
Pranav C 12 months ago
parent
commit
cc58a2f956
  1. 4
      .github/workflows/playwright-test-workflow.yml
  2. 3
      packages/nc-gui/assets/nc-icons/comment_here.svg
  3. 11
      packages/nc-gui/assets/style.scss
  4. 24
      packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue
  5. 4
      packages/nc-gui/components/dashboard/TreeView/TableNode.vue
  6. 7
      packages/nc-gui/components/dashboard/TreeView/ViewsNode.vue
  7. 3
      packages/nc-gui/components/dashboard/View.vue
  8. 28
      packages/nc-gui/components/general/OpenLeftSidebarBtn.vue
  9. 2
      packages/nc-gui/components/nc/Dropdown.vue
  10. 2
      packages/nc-gui/components/nc/MenuItem.vue
  11. 10
      packages/nc-gui/components/nc/Modal.vue
  12. 82
      packages/nc-gui/components/nc/Pagination.vue
  13. 34
      packages/nc-gui/components/smartsheet/Pagination.vue
  14. 11
      packages/nc-gui/components/smartsheet/Topbar.vue
  15. 77
      packages/nc-gui/components/smartsheet/expanded-form/Comments.vue
  16. 365
      packages/nc-gui/components/smartsheet/expanded-form/index.vue
  17. 51
      packages/nc-gui/components/smartsheet/grid/Table.vue
  18. 5
      packages/nc-gui/components/smartsheet/grid/index.vue
  19. 3
      packages/nc-gui/composables/useViewData.ts
  20. 3
      packages/nc-gui/lang/en.json
  21. 30
      packages/nc-gui/plugins/font.ts
  22. 6
      packages/nc-gui/store/views.ts
  23. 4
      packages/nc-gui/utils/iconUtils.ts
  24. 2
      packages/nc-gui/windi.config.ts
  25. 43
      tests/playwright/pages/Dashboard/Grid/index.ts
  26. 4
      tests/playwright/pages/Dashboard/common/Footbar/index.ts
  27. 2
      tests/playwright/playwright.config.ts
  28. 10
      tests/playwright/quickTests/commonTest.ts
  29. 8
      tests/playwright/tests/db/features/pagination.spec.ts

4
.github/workflows/playwright-test-workflow.yml

@ -233,6 +233,6 @@ jobs:
cp -r ./tests/playwright/playwright-report ${target_dir}/ || echo "playwright reports directory does not exists" >> ${target_dir}/playwright-report/index.html
cp ./packages/nocodb/*_test_backend.log ${target_dir}/ || echo "backend logs file does not exists" >> ${target_dir}/index.html
# end: artifacts copy
SUMMARY='[Artifacts]('${REPORTS_HOST}/${path}')
[playwright-report]('${REPORTS_HOST}/${path}'/playwright-report)'
SUMMARY='[Artifacts]('${REPORTS_HOST:-http://135.181.48.96}/${path}')
[playwright-report]('${REPORTS_HOST:-http://135.181.48.96}/${path}'/playwright-report)'
echo "$SUMMARY" >> $GITHUB_STEP_SUMMARY

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;
}
.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 {
overflow-y: scroll;
overflow-x: hidden;
@ -434,9 +442,6 @@ a {
@apply !shadow-none rounded ring-1 ring-red-600;
}
.ant-modal {
@apply !top-[30px];
}
.ant-modal-content {
@apply !p-6;
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
>
<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
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 }"
/>
</div>
</template>
<a-collapse-panel :key="`collapse-${base.id}`">
<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
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)"
>
<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
</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"
@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
: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' }"
>
{{ base.alias || '' }}
</div>
<a-tooltip>
<a-tooltip class="xs:(hidden)">
<template #title>External DB</template>
<div>
<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>
: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) {

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="{
'hover:bg-gray-200': openedTableId !== table.id,
'pl-12': baseIndex !== 0,
'pl-12 xs:(pl-14)': baseIndex !== 0,
'pl-6.5': baseIndex === 0,
'!bg-primary-selected': isTableOpened,
}"
@ -181,7 +181,7 @@ const isTableOpened = computed(() => {
:key="table.meta?.icon"
:emoji="table.meta?.icon"
size="small"
:readonly="!canUserEditEmote"
:readonly="!canUserEditEmote || isMobileMode"
@emoji-selected="setIcon($event, table)"
>
<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 { isMobileMode } = useGlobal()
const { isUIAllowed } = useRoles()
const { activeViewTitleOrId } = storeToRefs(useViewsStore())
@ -212,8 +214,8 @@ function onRef(el: HTMLElement) {
<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="{
'!pl-18': isDefaultBase,
'!pl-23.5': !isDefaultBase,
'!pl-18 !xs:(pl-19.75)': isDefaultBase,
'!pl-23.5 !xs:(pl-27)': !isDefaultBase,
}"
:data-testid="`view-sidebar-view-${vModel.alias || vModel.title}`"
@dblclick.stop="onDblClick"
@ -231,6 +233,7 @@ function onRef(el: HTMLElement) {
:emoji="props.view?.meta?.icon"
size="small"
:clearable="true"
:readonly="isMobileMode"
@emoji-selected="emits('selectIcon', $event)"
>
<template #default>

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

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

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

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

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

@ -8,7 +8,7 @@
<style lang="scss">
.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 {

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

@ -18,7 +18,13 @@ const emits = defineEmits(['update:visible'])
const { width: propWidth, destroyOnClose, maskClosable } = props
const { isMobileMode } = useGlobal()
const width = computed(() => {
if (isMobileMode.value) {
return '95vw'
}
if (propWidth) {
return propWidth
}
@ -39,6 +45,10 @@ const width = computed(() => {
})
const height = computed(() => {
if (isMobileMode.value) {
return '95vh'
}
if (props.size === 'small') {
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 { isPaginationLoading } = storeToRefs(useViewsStore())
const { isViewDataLoading, isPaginationLoading } = storeToRefs(useViewsStore())
const { isLeftSidebarOpen } = storeToRefs(useSidebarStore())
const count = computed(() => vPaginationData.value?.totalRows ?? Infinity)
@ -42,13 +44,13 @@ const size = computed(() => vPaginationData.value?.pageSize ?? 25)
const page = computed({
get: () => vPaginationData?.value?.page ?? 1,
set: async (p) => {
isPaginationLoading.value = true
isViewDataLoading.value = true
try {
await changePage?.(p)
} catch (e) {
console.error(e)
} finally {
isPaginationLoading.value = false
isViewDataLoading.value = false
}
},
})
@ -58,7 +60,7 @@ const isRTLLanguage = computed(() => isRtlLang(locale.value as keyof typeof Lang
<template>
<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 }"
:style="`${fixedSize ? `width: ${fixedSize}px;` : ''}${
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>
</div>
<template v-if="!hidePagination">
<a-pagination
v-if="count !== Infinity"
<div
v-if="!hidePagination"
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:page-size="size"
size="small"
class="!text-xs !m-1 nc-pagination"
class="xs:(mr-2)"
:class="{ 'rtl-pagination': isRTLLanguage }"
:total="+count"
show-less-items
:show-size-changer="false"
/>
<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>
@ -95,7 +103,7 @@ const isRTLLanguage = computed(() => isRtlLang(locale.value as keyof typeof Lang
</template>
</a-input>
</div>
</template>
</div>
<div v-if="!isMobileMode" class="flex-1 flex justify-end items-center">
<GeneralApiTiming v-if="isEeUI && props.showApiTiming" class="m-1" />
<div class="text-right">
@ -112,7 +120,7 @@ const isRTLLanguage = computed(() => isRtlLang(locale.value as keyof typeof Lang
</template>
<style lang="scss">
.nc-pagination-wrapper {
.nc-grid-pagination-wrapper {
.ant-pagination-item-active {
a {
@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 { isLeftSidebarOpen } = storeToRefs(useSidebarStore())
const { isMobileMode } = storeToRefs(useConfigStore())
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 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 />
</div>

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

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

365
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 wrapper = ref()
const { isMobileMode } = useGlobal()
const { t } = useI18n()
const row = ref(props.row)
@ -329,6 +333,23 @@ const onConfirmDeleteRowClick = async () => {
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 lang="ts">
@ -338,143 +359,117 @@ export default {
</script>
<template>
<a-modal
<NcModal
:key="key"
v-model:visible="isExpanded"
: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 }"
:closable="false"
class="nc-drawer-expanded-form max-h-[856px]"
size="large"
class="nc-drawer-expanded-form"
:class="{ active: isExpanded }"
>
<div class="flex flex-shrink-0 w-full items-center nc-expanded-form-header relative pb-2 justify-between">
<div class="flex gap-3">
<div class="flex gap-1">
<NcButton v-if="props.showNextPrevIcons" type="secondary" size="small" class="nc-prev-arrow" @click="$emit('prev')">
<MdiChevronUp class="text-md text-gray-700" />
</NcButton>
<NcButton v-if="!props.lastRow" type="secondary" size="small" class="nc-next-arrow" @click="onNext">
<MdiChevronDown class="text-md text-gray-700" />
</NcButton>
</div>
<div v-if="displayValue" class="flex items-center truncate max-w-32 font-bold text-gray-800 text-xl">
{{ displayValue }}
</div>
<div class="bg-gray-100 px-2 gap-1 flex items-center rounded-md text-gray-800">
<TableIcon class="w-6 h-6 text-sm" />
All {{ meta.title }}
</div>
</div>
<div class="flex gap-1">
<NcDropdown v-if="!isNew">
<NcButton type="secondary" size="small" class="nc-expand-form-more-actions">
<MdiMoreVert 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>
<NcMenuItem
v-if="isUIAllowed('dataEdit') && !isNew"
class="text-gray-700"
@click="!isNew ? onDuplicateRow() : () => {}"
>
<div v-e="['c:row-expand:duplicate']" class="flex gap-2 items-center">
<component :is="iconMap.copy" class="cursor-pointer nc-duplicate-row" />
Duplicate record
</div>
</NcMenuItem>
<a-menu-divider class="my-1" />
<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>
<NcButton type="secondary" size="small" class="nc-expand-form-close-btn" @click="onClose">
<MdiClose class="text-md text-gray-700" />
</NcButton>
</div>
</div>
<div class="flex flex-row w-full gap-4">
<div class="flex w-full flex-col h-[85vh] max-h-[770px] border-1 rounded-xl border-gray-200">
<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"
>
<div
v-for="(col, i) of fields"
v-show="isFormula(col) || !isVirtualCol(col) || !isNew || isLinksOrLTAR(col)"
:key="col.title"
class="mt-2 py-2"
:class="`nc-expand-col-${col.title}`"
:data-testid="`nc-expand-col-${col.title}`"
>
<div class="flex items-start flex-row">
<div class="w-[12rem] mt-2.5 scale-110">
<LazySmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" />
<LazySmartsheetHeaderCell v-else :column="col" />
</div>
<LazySmartsheetDivDataCell
v-if="col.title"
: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"
>
<LazySmartsheetVirtualCell v-if="isVirtualCol(col)" v-model="row.row[col.title]" :row="row" :column="col" />
<LazySmartsheetCell
v-else
v-model="row.row[col.title]"
:column="col"
:edit-enabled="true"
:active="true"
:read-only="isPublic"
@update:model-value="changedColumns.add(col.title)"
/>
</LazySmartsheetDivDataCell>
<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-2">
<NcButton v-if="props.showNextPrevIcons" type="secondary" class="nc-prev-arrow !w-10" @click="$emit('prev')">
<MdiChevronUp class="text-md text-gray-700" />
</NcButton>
<NcButton v-if="!props.lastRow" type="secondary" class="nc-next-arrow !w-10" @click="onNext">
<MdiChevronDown class="text-md text-gray-700" />
</NcButton>
</div>
<div v-if="displayValue" class="flex items-center truncate max-w-32 font-bold text-gray-800 text-xl">
{{ displayValue }}
</div>
<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" />
All {{ meta.title }}
</div>
</div>
<div class="flex gap-2">
<NcDropdown v-if="!isNew">
<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>
<NcMenuItem
v-if="isUIAllowed('dataEdit') && !isNew"
class="text-gray-700"
@click="!isNew ? onDuplicateRow() : () => {}"
>
<div v-e="['c:row-expand:duplicate']" class="flex gap-2 items-center">
<component :is="iconMap.copy" class="cursor-pointer nc-duplicate-row" />
Duplicate record
</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>
<NcButton type="secondary" class="nc-expand-form-close-btn w-10" @click="onClose">
<GeneralIcon icon="close" class="text-md text-gray-700" />
</NcButton>
</div>
<div v-if="hiddenFields.length > 0" class="flex w-full px-12 items-center py-3">
<div class="flex-grow h-px mr-1 bg-gray-100"></div>
<NcButton type="secondary" size="small" class="flex-shrink-1 !text-sm" @click="toggleHiddenFields">
{{ showHiddenFields ? `Hide ${hiddenFields.length} hidden` : `Show ${hiddenFields.length} hidden` }}
{{ hiddenFields.length > 1 ? `fields` : `field` }}
<MdiChevronDown class="ml-1" :class="showHiddenFields ? 'transform rotate-180' : ''" />
</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>
<NcButton v-if="!props.lastRow" type="secondary" class="nc-next-arrow !w-10" @click="onNext">
<GeneralIcon icon="arrowRight" class="text-lg text-gray-700" />
</NcButton>
<div class="flex-grow h-px ml-1 bg-gray-100"></div>
</div>
<div v-if="hiddenFields.length > 0 && showHiddenFields" class="mb-3">
</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
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
v-for="(col, i) of hiddenFields"
v-for="(col, i) of fields"
v-show="isFormula(col) || !isVirtualCol(col) || !isNew || isLinksOrLTAR(col)"
: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}`"
:col-id="col.id"
:data-testid="`nc-expand-col-${col.title}`"
>
<div class="flex flex-row items-start">
<div class="w-[12rem] scale-110 mt-2.5">
<LazySmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" />
<div class="flex items-start flex-row xs:(flex-col w-full) nc-expanded-cell">
<div class="w-[12rem] xs:(w-full) mt-1.5">
<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>
<LazySmartsheetDivDataCell
v-if="col.title"
: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" />
@ -490,35 +485,145 @@ export default {
</LazySmartsheetDivDataCell>
</div>
</div>
<div v-if="hiddenFields.length > 0" class="flex w-full px-12 items-center py-3">
<div class="flex-grow h-px mr-1 bg-gray-100"></div>
<NcButton type="secondary" size="small" class="flex-shrink-1 !text-sm" @click="toggleHiddenFields">
{{ showHiddenFields ? `Hide ${hiddenFields.length} hidden` : `Show ${hiddenFields.length} hidden` }}
{{ hiddenFields.length > 1 ? `fields` : `field` }}
<MdiChevronDown class="ml-1" :class="showHiddenFields ? 'transform rotate-180' : ''" />
</NcButton>
<div class="flex-grow h-px ml-1 bg-gray-100"></div>
</div>
<div v-if="hiddenFields.length > 0 && showHiddenFields" class="mb-3">
<div
v-for="(col, i) of hiddenFields"
v-show="isFormula(col) || !isVirtualCol(col) || !isNew || isLinksOrLTAR(col)"
:key="col.title"
class="mt-2 py-2"
:class="`nc-expand-col-${col.title}`"
:data-testid="`nc-expand-col-${col.title}`"
>
<div class="flex flex-row items-start">
<div class="w-[12rem] scale-110 mt-2.5">
<LazySmartsheetHeaderVirtualCell v-if="isVirtualCol(col)" :column="col" />
<LazySmartsheetHeaderCell v-else :column="col" />
</div>
<LazySmartsheetDivDataCell
v-if="col.title"
: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"
>
<LazySmartsheetVirtualCell v-if="isVirtualCol(col)" v-model="row.row[col.title]" :row="row" :column="col" />
<LazySmartsheetCell
v-else
v-model="row.row[col.title]"
:column="col"
:edit-enabled="true"
:active="true"
:read-only="isPublic"
@update:model-value="changedColumns.add(col.title)"
/>
</LazySmartsheetDivDataCell>
</div>
</div>
</div>
</div>
<div
v-if="isUIAllowed('dataEdit')"
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)"
>
<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
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"
v-if="!isNew && commentsDrawer && isUIAllowed('commentList')"
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') }"
>
<NcButton type="primary" size="medium" class="nc-expand-form-save-btn" @click="save"> Save </NcButton>
<LazySmartsheetExpandedFormComments />
</div>
</div>
<div
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="{ active: commentsDrawer && isUIAllowed('commentList') }"
>
<LazySmartsheetExpandedFormComments />
</div>
</div>
</a-modal>
</NcModal>
<GeneralModal v-model:visible="showDeleteRowModal" class="!w-[25rem]">
<div class="p-4">
<NcModal v-model:visible="showDeleteRowModal" class="!w-[25rem] !xs-">
<div class="">
<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>
<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>
</div>
</GeneralModal>
</NcModal>
</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>

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

@ -497,7 +497,7 @@ const {
activeCell,
handleMouseDown,
handleMouseOver,
handleCellClick,
handleCellClick: _handleCellClick,
clearSelectedRange,
copyValue,
isCellActive,
@ -1135,6 +1135,16 @@ const expandAndLooseFocus = (row: Row, col: Record<string, any>) => {
activeCell.col = null
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>
<template>
@ -1149,6 +1159,10 @@ const expandAndLooseFocus = (row: Row, col: Record<string, any>) => {
<table
ref="smartTable"
class="xc-row-table nc-grid backgroundColorDefault !h-auto bg-white"
:class="{
mobile: isMobileMode,
desktop: !isMobileMode,
}"
@contextmenu="showContextMenu"
>
<thead v-show="hideHeader !== true" ref="tableHeadEl">
@ -1586,11 +1600,8 @@ const expandAndLooseFocus = (row: Row, col: Record<string, any>) => {
</NcDropdown>
</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
v-else-if="headerOnly !== true"
v-if="headerOnly !== true"
:key="isMobileMode"
v-model:pagination-data="paginationDataRef"
show-api-timing
@ -1611,7 +1622,7 @@ const expandAndLooseFocus = (row: Row, col: Record<string, any>) => {
placement="top"
@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>
<template v-if="isAddNewRecordGridMode"> {{ $t('activity.newRecord') }} </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>
<style lang="scss">
.nc-pagination-wrapper .ant-dropdown-button {
.nc-grid-pagination-wrapper .ant-dropdown-button {
> .ant-btn {
@apply !p-0 !rounded-l-lg hover:border-gray-400;
}
@ -1793,19 +1804,21 @@ const expandAndLooseFocus = (row: Row, col: Record<string, any>) => {
background: white;
}
thead th:nth-child(2) {
position: sticky !important;
left: 85px;
z-index: 5;
@apply border-r-1 border-r-gray-200;
}
.desktop {
thead th:nth-child(2) {
position: sticky !important;
z-index: 5;
left: 85px;
@apply border-r-1 border-r-gray-200;
}
tbody td:nth-child(2) {
position: sticky !important;
left: 85px;
z-index: 4;
background: white;
@apply border-r-1 border-r-gray-100;
tbody td:nth-child(2) {
position: sticky !important;
z-index: 4;
left: 85px;
background: white;
@apply border-r-1 border-r-gray-100;
}
}
.nc-grid-skelton-loader {

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[])
if (rowId) {
expandedFormRowState.value = state
router.push({
query: {
...routeQuery.value,
@ -242,6 +244,7 @@ onMounted(() => {
v-model="expandedFormOnRowIdDlg"
:row="{ row: {}, oldRow: {}, rowMeta: {} }"
:meta="meta"
:state="expandedFormRowState"
:row-id="routeQuery.rowId"
:view="view"
show-next-prev-icons
@ -269,7 +272,7 @@ onMounted(() => {
</template>
<style lang="scss">
.nc-pagination-wrapper .ant-dropdown-button {
.nc-grid-pagination-wrapper .ant-dropdown-button {
> .ant-btn {
@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 { isPaginationLoading } = storeToRefs(useViewsStore())
const paginationData = computed({
get: () => (isPublic.value ? sharedPaginationData.value : _paginationData.value),
set: (value) => {
@ -178,6 +180,7 @@ export function useViewData(
formattedData.value = formatData(response.list)
paginationData.value = response.pageInfo
isPaginationLoading.value = false
// 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

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

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

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

@ -10,12 +10,12 @@ export default defineNuxtPlugin(() => {
const fontFaces = [...document.fonts.values()]
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')
return
}
materialFont?.loaded
materialFont.loaded
.then(function () {
document.documentElement?.classList.remove('nc-fonts-not-loaded')
})
@ -23,6 +23,32 @@ export default defineNuxtPlugin(() => {
document.documentElement?.classList.remove('nc-fonts-not-loaded')
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) {
document.documentElement?.classList.remove('nc-fonts-not-loaded')
console.error(error)

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

@ -79,7 +79,7 @@ export const useViewsStore = defineStore('viewsStore', () => {
})
// Used for Grid View Pagination
const isPaginationLoading = ref(false)
const isPaginationLoading = ref(true)
const loadViews = async ({
tableId,
@ -183,6 +183,10 @@ export const useViewsStore = defineStore('viewsStore', () => {
}
}
watch(activeViewTitleOrId, () => {
isPaginationLoading.value = true
})
return {
isLockedView,
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 Down from '~icons/material-symbols/keyboard-arrow-down-rounded'
import PhTriangleFill from '~icons/ph/triangle-fill'
import LcSend from '~icons/lucide/send'
import NcCommentHere from '~icons/nc-icons/comment-here'
// Roles
import MaterialSymbolsManageAccountsOutline from '~icons/material-symbols/manage-accounts-outline'
@ -343,6 +345,7 @@ export const iconMap = {
number: h('span', { class: 'material-symbols' }, 'looks_one'),
email: h('span', { class: 'material-symbols' }, 'email'),
sendEmail: h('span', { class: 'material-symbols' }, 'email'),
send: LcSend,
currency: h('span', { class: 'material-symbols' }, 'attach_money'),
percent: h('span', { class: 'material-symbols' }, 'percent'),
decimal: h('span', { class: 'material-symbols' }, 'decimal_increase'),
@ -417,6 +420,7 @@ export const iconMap = {
role_commenter: MdiCommentAccountOutline,
role_viewer: MaterialSymbolsPersonSearchOutline,
role_no_access: MaterialSymbolsBlock,
commentHere: NcCommentHere,
}
export const getMdiIcon = (type: string): any => {

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

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

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

@ -299,40 +299,23 @@ export class GridPage extends BasePage {
expect(parseInt(recordCnt)).toEqual(count);
}
async verifyPaginationCount({ count }: { count: number }) {
let i = 0;
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);
async verifyPaginationCount({ count }: { count: string }) {
await expect(this.get().locator(`.nc-pagination .total`)).toHaveText(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({ page, skipWait = false }: { page: string; skipWait?: boolean }) {
async clickPagination({
type,
skipWait = false,
}: {
type: 'first-page' | 'last-page' | 'next-page' | 'prev-page';
skipWait?: boolean;
}) {
if (!skipWait) {
await (await this.pagination({ page })).click();
await this.get().locator(`.nc-pagination .${type}`).click();
await this.waitLoading();
} else {
await this.waitForResponse({
uiAction: async () => (await this.pagination({ page })).click(),
uiAction: async () => (await this.get().locator(`.nc-pagination .${type}`)).click(),
httpMethodsToMatch: ['GET'],
requestUrlPathToMatch: '/views/',
responseJsonMatcher: resJson => resJson?.pageInfo,
@ -342,8 +325,8 @@ export class GridPage extends BasePage {
}
}
async verifyActivePage({ page }: { page: string }) {
await expect(await this.pagination({ page })).toHaveClass(/ant-pagination-item-active/);
async verifyActivePage({ pageNumber }: { pageNumber: string }) {
await expect(this.get().locator(`.nc-pagination .active`)).toHaveText(pageNumber);
}
async waitLoading() {

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

@ -17,11 +17,11 @@ export class FootbarPage extends BasePage {
this.parent = parent;
this.leftSidebarToggle = this.get().locator(`div.nc-sidebar-left-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() {
return this.rootPage.locator(`div.nc-pagination-wrapper`);
return this.rootPage.locator(`div.nc-grid-pagination-wrapper`);
}
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. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 1 : 0,
retries: process.env.CI ? 3 : 0,
/* Opt out of parallel tests on CI. */
// 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
await dashboard.grid.verifyActivePage({ page: '1' });
await dashboard.grid.clickPagination({ page: '>', skipWait: true });
await dashboard.grid.verifyActivePage({ page: '2' });
await dashboard.grid.clickPagination({ page: '<', skipWait: true });
await dashboard.grid.verifyActivePage({ page: '1' });
await dashboard.grid.verifyActivePage({ pageNumber: '1' });
await dashboard.grid.clickPagination({ type: 'next-page', skipWait: true });
await dashboard.grid.verifyActivePage({ pageNumber: '2' });
await dashboard.grid.clickPagination({ type: 'prev-page', skipWait: true });
await dashboard.grid.verifyActivePage({ pageNumber: '1' });
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' });
// click ">" to go to next page
await dashboard.grid.clickPagination({ page: '>' });
await dashboard.grid.verifyActivePage({ page: '2' });
await dashboard.grid.clickPagination({ type: 'next-page' });
await dashboard.grid.verifyActivePage({ pageNumber: '2' });
// click "<" to go to prev page
await dashboard.grid.clickPagination({ page: '<' });
await dashboard.grid.verifyActivePage({ page: '1' });
await dashboard.grid.clickPagination({ type: 'prev-page' });
await dashboard.grid.verifyActivePage({ pageNumber: '1' });
});
});

Loading…
Cancel
Save